41 #include <visp3/core/vpConfig.h> 43 #if defined(VISP_HAVE_CATCH2) && defined(VISP_HAVE_APRILTAG) 44 #define CATCH_CONFIG_RUNNER 48 #include <visp3/core/vpDisplay.h> 49 #include <visp3/core/vpIoTools.h> 50 #include <visp3/core/vpPoint.h> 51 #include <visp3/core/vpPixelMeterConversion.h> 52 #include <visp3/detection/vpDetectorAprilTag.h> 53 #include <visp3/io/vpImageIo.h> 54 #include <visp3/vision/vpPose.h> 57 struct TagGroundTruth {
58 std::string m_message;
59 std::vector<vpImagePoint> m_corners;
61 TagGroundTruth(
const std::string &msg,
const std::vector<vpImagePoint> &c) : m_message(msg), m_corners(c) {}
63 bool operator==(
const TagGroundTruth &b)
const 65 if (m_message != b.m_message || m_corners.size() != b.m_corners.size()) {
69 for (
size_t i = 0; i < m_corners.size(); i++) {
71 if (!
vpMath::equal(m_corners[i].get_u(), b.m_corners[i].get_u(), 0.5) ||
72 !
vpMath::equal(m_corners[i].get_v(), b.m_corners[i].get_v(), 0.5)) {
80 bool operator!=(
const TagGroundTruth &b)
const {
return !(*
this == b); }
82 double rmse(
const std::vector<vpImagePoint> &c)
86 if (m_corners.size() == c.size()) {
87 for (
size_t i = 0; i < m_corners.size(); i++) {
97 return sqrt(error / (2*m_corners.size()));
101 std::ostream &operator<<(std::ostream &os, TagGroundTruth &t)
103 os << t.m_message << std::endl;
104 for (
size_t i = 0; i < t.m_corners.size(); i++) {
105 os << t.m_corners[i] << std::endl;
111 struct FailedTestCase {
119 : m_family(family), m_method(method), m_tagId(tagId) {}
121 bool operator==(
const FailedTestCase &b)
const 123 return m_family == b.m_family && m_method == b.m_method && m_tagId == b.m_tagId;
126 bool operator!=(
const FailedTestCase &b)
const {
return !(*
this == b); }
130 TEST_CASE(
"Apriltag pose estimation test",
"[apriltag_pose_estimation_test]") {
131 std::map<vpDetectorAprilTag::vpAprilTagFamily, std::string> apriltagMap = {
136 #if defined(VISP_HAVE_APRILTAG_BIG_FAMILY) 144 std::map<vpDetectorAprilTag::vpAprilTagFamily, double> tagSizeScales = {
149 #if defined(VISP_HAVE_APRILTAG_BIG_FAMILY) 164 std::map<vpDetectorAprilTag::vpPoseEstimationMethod, std::string> methodNames = {
173 const size_t nbTags = 5;
174 const double tagSize_ = 0.25;
175 std::map<int, double> tagsSize = {
183 std::map<int, double> errorTranslationThresh = {
190 std::map<int, double> errorRotationThresh = {
197 std::vector<FailedTestCase> ignoreTests = {
206 std::map<int, vpHomogeneousMatrix> groundTruthPoses;
207 for (
size_t i = 0; i < nbTags; i++) {
209 std::string(
"AprilTag/benchmark/640x480/cMo_") +
210 std::to_string(i) + std::string(
".txt"));
211 std::ifstream file(filename);
212 groundTruthPoses[
static_cast<int>(i)].load(file);
215 for (
const auto& kv : apriltagMap) {
216 auto family = kv.first;
217 std::cout <<
"\nApriltag family: " << family << std::endl;
219 std::string(
"AprilTag/benchmark/640x480/") +
220 kv.second + std::string(
"_640x480.png"));
221 const double tagSize = tagSize_ * tagSizeScales[family];
226 REQUIRE(I.
getSize() == 640*480);
230 for (
auto method : poseMethods) {
231 std::cout <<
"\tPose estimation method: " << method << std::endl;
232 apriltag_detector.setAprilTagPoseEstimationMethod(method);
236 std::vector<vpHomogeneousMatrix> cMo_vec;
237 apriltag_detector.detect(I, tagSize, cam, cMo_vec);
238 CHECK(cMo_vec.size() == nbTags);
240 std::vector<std::vector<vpImagePoint>> tagsCorners = apriltag_detector.getPolygon();
241 CHECK(tagsCorners.size() == nbTags);
243 std::vector<std::string> messages = apriltag_detector.getMessage();
244 CHECK(messages.size() == nbTags);
246 std::vector<int> tagsId = apriltag_detector.getTagsId();
247 CHECK(tagsId.size() == nbTags);
248 std::map<int, int> idsCount;
249 for (
size_t i = 0; i < tagsId.size(); i++) {
250 idsCount[tagsId[i]]++;
251 CHECK((tagsId[i] >= 0 && tagsId[i] < 5));
253 CHECK(idsCount.size() == nbTags);
255 for (
size_t i = 0; i < cMo_vec.size(); i++) {
262 std::cout <<
"\t\tSame size, Tag: " << i << std::endl;
266 vpColVector error_translation = pose.getTranslationVector() - pose_truth.getTranslationVector();
268 double error_trans = sqrt(error_translation.
sumSquare()/3);
269 double error_orientation = sqrt(error_thetau.
sumSquare() / 3);
270 std::cout <<
"\t\t\tTranslation error: " << error_trans <<
" / Rotation error: " << error_orientation << std::endl;
271 CHECK((error_trans < errorTranslationThresh[
id] &&
272 error_orientation < errorRotationThresh[
id]));
278 apriltag_detector.detect(I);
280 std::vector<std::vector<vpImagePoint>> tagsCorners = apriltag_detector.getPolygon();
281 CHECK(tagsCorners.size() == nbTags);
283 std::vector<std::string> messages = apriltag_detector.getMessage();
284 CHECK(messages.size() == nbTags);
286 std::vector<int> tagsId = apriltag_detector.getTagsId();
287 CHECK(tagsId.size() == nbTags);
288 std::map<int, int> idsCount;
289 for (
size_t i = 0; i < tagsId.size(); i++) {
290 idsCount[tagsId[i]]++;
291 CHECK((tagsId[i] >= 0 && tagsId[i] < 5));
293 CHECK(idsCount.size() == nbTags);
295 for (
size_t idx = 0; idx < tagsId.size(); idx++) {
296 std::cout <<
"\t\tCustom size, Tag: " << idx << std::endl;
297 const int id = tagsId[idx];
299 apriltag_detector.getPose(idx, tagsSize[
id] * tagSizeScales[family], cam, cMo);
305 vpColVector error_translation = pose.getTranslationVector() - pose_truth.getTranslationVector();
307 double error_trans = sqrt(error_translation.
sumSquare()/3);
308 double error_orientation = sqrt(error_thetau.
sumSquare() / 3);
309 std::cout <<
"\t\t\tTranslation error: " << error_trans <<
" / Rotation error: " << error_orientation << std::endl;
310 if (std::find(ignoreTests.begin(), ignoreTests.end(), FailedTestCase(family, method, static_cast<int>(idx))) == ignoreTests.end()) {
311 CHECK((error_trans < errorTranslationThresh[
id] &&
312 error_orientation < errorRotationThresh[
id]));
319 apriltag_detector.detect(I);
321 std::vector<std::vector<vpImagePoint>> tagsCorners = apriltag_detector.getPolygon();
322 CHECK(tagsCorners.size() == nbTags);
324 std::vector<std::string> messages = apriltag_detector.getMessage();
325 CHECK(messages.size() == nbTags);
327 std::vector<int> tagsId = apriltag_detector.getTagsId();
328 CHECK(tagsId.size() == nbTags);
329 std::map<int, int> idsCount;
330 for (
size_t i = 0; i < tagsId.size(); i++) {
331 idsCount[tagsId[i]]++;
332 CHECK((tagsId[i] >= 0 && tagsId[i] < 5));
334 CHECK(idsCount.size() == nbTags);
336 for (
size_t idx = 0; idx < tagsId.size(); idx++) {
337 std::cout <<
"\t\tCustom size + aligned Z-axis, Tag: " << idx << std::endl;
338 const int id = tagsId[idx];
340 apriltag_detector.setZAlignedWithCameraAxis(
true);
341 apriltag_detector.getPose(idx, tagsSize[
id] * tagSizeScales[family], cam, cMo);
342 apriltag_detector.setZAlignedWithCameraAxis(
false);
349 vpColVector error_translation = pose.getTranslationVector() - pose_truth.getTranslationVector();
351 double error_trans = sqrt(error_translation.
sumSquare()/3);
352 double error_orientation = sqrt(error_thetau.
sumSquare() / 3);
353 std::cout <<
"\t\t\tTranslation error: " << error_trans <<
" / Rotation error: " << error_orientation << std::endl;
354 if (std::find(ignoreTests.begin(), ignoreTests.end(), FailedTestCase(family, method, static_cast<int>(idx))) == ignoreTests.end()) {
355 CHECK((error_trans < errorTranslationThresh[
id] &&
356 error_orientation < errorRotationThresh[
id]));
364 TEST_CASE(
"Apriltag corners accuracy test",
"[apriltag_corners_accuracy_test]") {
365 std::map<vpDetectorAprilTag::vpAprilTagFamily, std::string> apriltagMap = {
370 #if defined(VISP_HAVE_APRILTAG_BIG_FAMILY) 378 const size_t nbTags = 5;
379 std::map<vpDetectorAprilTag::vpAprilTagFamily, std::map<int, std::vector<vpImagePoint>>> groundTruthCorners;
380 for (
const auto& kv : apriltagMap) {
382 std::string(
"AprilTag/benchmark/640x480/corners_") +
383 kv.second + std::string(
".txt"));
384 std::ifstream file(filename);
385 REQUIRE(file.is_open());
388 double p0x = 0, p0y = 0;
389 double p1x = 0, p1y = 0;
390 double p2x = 0, p2y = 0;
391 double p3x = 0, p3y = 0;
392 while (file >>
id >> p0x >> p0y >> p1x >> p1y >>
393 p2x >> p2y >> p3x >> p3y) {
394 groundTruthCorners[kv.first][id].push_back(
vpImagePoint(p0y, p0x));
395 groundTruthCorners[kv.first][id].push_back(
vpImagePoint(p1y, p1x));
396 groundTruthCorners[kv.first][id].push_back(
vpImagePoint(p2y, p2x));
397 groundTruthCorners[kv.first][id].push_back(
vpImagePoint(p3y, p3x));
398 REQUIRE(groundTruthCorners[kv.first][
id].size() == 4);
402 for (
const auto& kv : apriltagMap) {
403 auto family = kv.first;
404 std::cout <<
"\nApriltag family: " << family << std::endl;
406 std::string(
"AprilTag/benchmark/640x480/") +
407 kv.second + std::string(
"_640x480.png"));
412 REQUIRE(I.
getSize() == 640*480);
415 apriltag_detector.detect(I);
416 std::vector<int> tagsId = apriltag_detector.getTagsId();
417 std::vector<std::vector<vpImagePoint>> tagsCorners = apriltag_detector.getTagsCorners();
419 REQUIRE(tagsCorners.size() == nbTags);
420 REQUIRE(tagsId.size() == nbTags);
421 for (
size_t i = 0; i < tagsCorners.size(); i++) {
422 const int tagId = tagsId[i];
423 REQUIRE(tagsCorners[i].size() == 4);
425 TagGroundTruth corners_ref(
"", groundTruthCorners[family][tagId]);
426 TagGroundTruth corners_cur(
"", tagsCorners[i]);
427 CHECK((corners_ref == corners_cur));
429 std::cout <<
"\tid: " << tagId <<
" - RMSE: " << corners_ref.rmse(corners_cur.m_corners) << std::endl;
434 TEST_CASE(
"Apriltag regression test",
"[apriltag_regression_test]") {
440 REQUIRE(I.
getSize() == 640*480);
444 const double tagSize = 0.053;
445 const float quad_decimate = 1.0;
448 dynamic_cast<vpDetectorAprilTag *
>(detector)->setAprilTagPoseEstimationMethod(poseEstimationMethod);
453 std::vector<vpHomogeneousMatrix> cMo_vec;
457 std::map<std::string, TagGroundTruth> mapOfTagsGroundTruth;
460 "AprilTag/ground_truth_detection.txt");
461 std::ifstream file_ground_truth(filename_ground_truth.c_str());
462 REQUIRE(file_ground_truth.is_open());
463 std::string message =
"";
464 double v1 = 0.0, v2 = 0.0, v3 = 0.0, v4 = 0.0;
465 double u1 = 0.0, u2 = 0.0, u3 = 0.0, u4 = 0.0;
466 while (file_ground_truth >> message >> v1 >> u1 >> v2 >> u2 >> v3 >> u3 >> v4 >> u4) {
467 std::vector<vpImagePoint> tagCorners(4);
468 tagCorners[0].set_ij(v1, u1);
469 tagCorners[1].set_ij(v2, u2);
470 tagCorners[2].set_ij(v3, u3);
471 tagCorners[3].set_ij(v4, u4);
472 mapOfTagsGroundTruth.insert(std::make_pair(message, TagGroundTruth(message, tagCorners)));
476 std::map<std::string, vpPoseVector> mapOfPosesGroundTruth;
479 "AprilTag/ground_truth_pose.txt");
480 std::ifstream file_ground_truth(filename_ground_truth.c_str());
481 REQUIRE(file_ground_truth.is_open());
482 std::string message =
"";
483 double tx = 0.0, ty = 0.0, tz = 0.0;
484 double tux = 0.0, tuy = 0.0, tuz = 0.0;
485 while (file_ground_truth >> message >> tx >> ty >> tz >> tux >> tuy >> tuz) {
486 mapOfPosesGroundTruth.insert(std::make_pair(message,
vpPoseVector(tx, ty, tz, tux, tuy, tuz)));
491 std::vector<vpImagePoint> p = detector->
getPolygon(i);
493 std::string message = detector->
getMessage(i);
494 std::replace(message.begin(), message.end(),
' ',
'_');
495 std::map<std::string, TagGroundTruth>::iterator it = mapOfTagsGroundTruth.find(message);
496 TagGroundTruth current(message, p);
497 if (it == mapOfTagsGroundTruth.end()) {
498 std::cerr <<
"Problem with tag decoding (tag_family or id): " << message << std::endl;
499 }
else if (it->second != current) {
500 std::cerr <<
"Problem, current detection:\n" << current <<
"\nReference:\n" << it->second << std::endl;
502 REQUIRE(it != mapOfTagsGroundTruth.end());
503 CHECK(it->second == current);
506 for (
size_t i = 0; i < cMo_vec.size(); i++) {
509 std::string message = detector->
getMessage(i);
510 std::replace(message.begin(), message.end(),
' ',
'_');
511 std::map<std::string, vpPoseVector>::iterator it = mapOfPosesGroundTruth.find(message);
512 if (it == mapOfPosesGroundTruth.end()) {
513 std::cerr <<
"Problem with tag decoding (tag_family or id): " << message << std::endl;
515 REQUIRE(it != mapOfPosesGroundTruth.end());
516 std::cout <<
"Tag: " << message << std::endl;
517 std::cout <<
"\tEstimated pose: " << pose_vec.t() << std::endl;
518 std::cout <<
"\tReference pose: " << it->second.t() << std::endl;
519 for (
unsigned int cpt = 0; cpt < 3; cpt++) {
522 std::cerr <<
"Problem, current pose: " << pose_vec.t() <<
"\nReference pose: " << it->second.t()
525 CHECK((
vpMath::equal(it->second[cpt], pose_vec[cpt], 0.005) &&
533 TEST_CASE(
"Apriltag copy constructor test",
"[apriltag_copy_constructor_test]") {
535 "AprilTag/benchmark/640x480/tag21_07_640x480.png");
540 REQUIRE(I.
getSize() == 640*480);
544 const double tagSize = 0.25 * 5/9;
545 const float quad_decimate = 1.0;
552 std::vector<vpHomogeneousMatrix> cMo_vec;
553 detector->
detect(I, tagSize, cam, cMo_vec);
554 std::vector<std::vector<vpImagePoint> > tagsCorners = detector->
getTagsCorners();
555 std::vector<int> tagsId = detector->
getTagsId();
562 std::vector<std::vector<vpImagePoint> > tagsCorners_copy = detector_copy.
getTagsCorners();
563 std::vector<int> tagsId_copy = detector_copy.getTagsId();
564 REQUIRE(tagsCorners_copy.size() == tagsCorners.size());
565 REQUIRE(tagsId_copy.size() == tagsId.size());
566 REQUIRE(tagsCorners_copy.size() == tagsId_copy.size());
568 for (
size_t i = 0; i < tagsCorners.size(); i++) {
569 const std::vector<vpImagePoint>& corners_ref = tagsCorners[i];
570 const std::vector<vpImagePoint>& corners_copy = tagsCorners_copy[i];
571 REQUIRE(corners_ref.size() == corners_copy.size());
573 for (
size_t j = 0; j < corners_ref.size(); j++) {
576 CHECK(corner_ref == corner_copy);
579 int id_ref = tagsId[i];
580 int id_copy = tagsId_copy[i];
581 CHECK(id_ref == id_copy);
584 std::vector<vpHomogeneousMatrix> cMo_vec_copy;
585 detector_copy.detect(I, tagSize, cam, cMo_vec_copy);
586 REQUIRE(cMo_vec.size() == cMo_vec_copy.size());
587 for (
size_t idx = 0; idx < cMo_vec_copy.size(); idx++) {
590 for (
int i = 0; i < 3; i++) {
591 for (
int j = 0; j < 4; j++) {
593 std::numeric_limits<double>::epsilon()));
599 TEST_CASE(
"Apriltag assignment operator test",
"[apriltag_assignment_operator_test]") {
601 "AprilTag/benchmark/640x480/tag21_07_640x480.png");
606 REQUIRE(I.
getSize() == 640*480);
610 const double tagSize = 0.25 * 5/9;
611 const float quad_decimate = 1.0;
618 std::vector<vpHomogeneousMatrix> cMo_vec;
619 detector->
detect(I, tagSize, cam, cMo_vec);
620 std::vector<std::vector<vpImagePoint> > tagsCorners = detector->
getTagsCorners();
621 std::vector<int> tagsId = detector->
getTagsId();
628 std::vector<std::vector<vpImagePoint> > tagsCorners_copy = detector_copy.
getTagsCorners();
629 std::vector<int> tagsId_copy = detector_copy.
getTagsId();
630 REQUIRE(tagsCorners_copy.size() == tagsCorners.size());
631 REQUIRE(tagsId_copy.size() == tagsId.size());
632 REQUIRE(tagsCorners_copy.size() == tagsId_copy.size());
634 for (
size_t i = 0; i < tagsCorners.size(); i++) {
635 const std::vector<vpImagePoint>& corners_ref = tagsCorners[i];
636 const std::vector<vpImagePoint>& corners_copy = tagsCorners_copy[i];
637 REQUIRE(corners_ref.size() == corners_copy.size());
639 for (
size_t j = 0; j < corners_ref.size(); j++) {
642 CHECK(corner_ref == corner_copy);
645 int id_ref = tagsId[i];
646 int id_copy = tagsId_copy[i];
647 CHECK(id_ref == id_copy);
650 std::vector<vpHomogeneousMatrix> cMo_vec_copy;
651 detector_copy.
detect(I, tagSize, cam, cMo_vec_copy);
652 REQUIRE(cMo_vec.size() == cMo_vec_copy.size());
653 for (
size_t idx = 0; idx < cMo_vec_copy.size(); idx++) {
656 for (
int i = 0; i < 3; i++) {
657 for (
int j = 0; j < 4; j++) {
659 std::numeric_limits<double>::epsilon()));
665 TEST_CASE(
"Apriltag getTagsPoints3D test",
"[apriltag_get_tags_points3D_test]") {
667 "AprilTag/benchmark/640x480/tag21_07_640x480.png");
672 REQUIRE(I.
getSize() == 640*480);
676 const double familyScale = 5.0/9;
677 const double tagSize = 0.25;
678 std::map<int, double> tagsSize = {
679 {-1, tagSize * familyScale},
680 {3, tagSize / 2 * familyScale},
681 {4, tagSize / 2 * familyScale}
689 std::vector<vpHomogeneousMatrix> cMo_vec;
690 REQUIRE(detector.
detect(I));
693 std::vector<int> tagsId = detector.
getTagsId();
694 for (
size_t i = 0; i < tagsId.size(); i++) {
696 double size = tagsSize[-1];
697 if (tagsSize.find(
id) != tagsSize.end()) {
702 detector.
getPose(i, size, cam, cMo);
703 cMo_vec.push_back(cMo);
707 std::vector<std::vector<vpPoint>> tagsPoints = detector.
getTagsPoints3D(tagsId, tagsSize);
708 std::vector<std::vector<vpImagePoint>> tagsCorners = detector.
getTagsCorners();
709 REQUIRE(tagsPoints.size() == tagsCorners.size());
711 for (
size_t i = 0; i < tagsPoints.size(); i++) {
712 REQUIRE(tagsPoints[i].size() == tagsCorners[i].size());
714 for (
size_t j = 0; j < tagsPoints[i].size(); j++) {
715 vpPoint& pt = tagsPoints[i][j];
723 vpPose pose(tagsPoints[i]);
728 for (
unsigned int row = 0; row < cMo.
getRows(); row++) {
729 for (
unsigned int col = 0; col < cMo.
getCols(); col++) {
730 CHECK(
vpMath::equal(cMo[row][col], cMo_manual[row][col], std::numeric_limits<double>::epsilon()));
736 int main(
int argc,
const char *argv[])
738 Catch::Session session;
741 session.applyCommandLine(argc, argv);
743 int numFailed = session.run();
AprilTag Standard52h13 pattern.
Implementation of an homogeneous matrix and operations on such kind of matrices.
AprilTag 36h11 pattern (recommended)
static bool equal(double x, double y, double s=0.001)
AprilTag Circle21h7 pattern.
std::vector< std::vector< vpPoint > > getTagsPoints3D(const std::vector< int > &tagsId, const std::map< int, double > &tagsSize) const
unsigned int getRows() const
static void convertPoint(const vpCameraParameters &cam, const double &u, const double &v, double &x, double &y)
size_t getNbObjects() const
bool getPose(size_t tagIndex, double tagSize, const vpCameraParameters &cam, vpHomogeneousMatrix &cMo, vpHomogeneousMatrix *cMo2=NULL, double *projError=NULL, double *projError2=NULL)
Class that defines what is a point.
void set_x(double x)
Set the point x coordinate in the image plane.
AprilTag Circle49h12 pattern.
void set_y(double y)
Set the point y coordinate in the image plane.
unsigned int getCols() const
void setAprilTagQuadDecimate(float quadDecimate)
Class used for pose computation from N points (pose from point only). Some of the algorithms implemen...
Generic class defining intrinsic camera parameters.
std::vector< std::vector< vpImagePoint > > getTagsCorners() const
void initPersProjWithoutDistortion(double px, double py, double u0, double v0)
std::vector< int > getTagsId() const
AprilTag Standard41h12 pattern.
static void read(vpImage< unsigned char > &I, const std::string &filename)
Implementation of column vector and the associated operations.
unsigned int getSize() const
Implementation of a pose vector and operations on poses.
Class that defines a 2D point in an image. This class is useful for image processing and stores only ...
AprilTag Custom48h12 pattern.
std::vector< std::string > & getMessage()
Class that consider the case of a translation vector.
Implementation of a rotation vector as axis-angle minimal representation.
std::vector< std::vector< vpImagePoint > > & getPolygon()
bool detect(const vpImage< unsigned char > &I)