Test AprilTag detection.
#include <visp3/core/vpConfig.h>
#if defined(VISP_HAVE_CATCH2) && defined(VISP_HAVE_APRILTAG) && (VISP_HAVE_DATASET_VERSION >= 0x030300)
#include <catch_amalgamated.hpp>
#include <iostream>
#include <visp3/core/vpDisplay.h>
#include <visp3/core/vpIoTools.h>
#include <visp3/core/vpPixelMeterConversion.h>
#include <visp3/core/vpPoint.h>
#include <visp3/detection/vpDetectorAprilTag.h>
#include <visp3/io/vpImageIo.h>
#include <visp3/vision/vpPose.h>
#ifdef ENABLE_VISP_NAMESPACE
#endif
namespace
{
struct TagGroundTruth
{
std::string m_message;
std::vector<vpImagePoint> m_corners;
TagGroundTruth(const std::string &msg, const std::vector<vpImagePoint> &c) : m_message(msg), m_corners(c) { }
bool operator==(const TagGroundTruth &b) const
{
if (m_message != b.m_message || m_corners.size() != b.m_corners.size()) {
return false;
}
for (size_t i = 0; i < m_corners.size(); i++) {
if (!
vpMath::equal(m_corners[i].get_u(), b.m_corners[i].get_u(), 0.5) ||
!
vpMath::equal(m_corners[i].get_v(), b.m_corners[i].get_v(), 0.5)) {
return false;
}
}
return true;
}
bool operator!=(const TagGroundTruth &b) const { return !(*this == b); }
double rmse(const std::vector<vpImagePoint> &c)
{
double error = 0;
if (m_corners.size() == c.size()) {
for (size_t i = 0; i < m_corners.size(); i++) {
}
}
else {
return -1;
}
return sqrt(error / (2 * m_corners.size()));
}
};
#if defined(VISP_HAVE_LAPACK) || defined(VISP_HAVE_OPENCV) || defined(VISP_HAVE_EIGEN3)
std::ostream &operator<<(std::ostream &os, TagGroundTruth &t)
{
os << t.m_message << std::endl;
for (size_t i = 0; i < t.m_corners.size(); i++) {
os << t.m_corners[i] << std::endl;
}
return os;
}
#endif
struct FailedTestCase
{
int m_tagId;
: m_family(family), m_method(method), m_tagId(tagId)
{ }
bool operator==(const FailedTestCase &b) const
{
return m_family == b.m_family && m_method == b.m_method && m_tagId == b.m_tagId;
}
bool operator!=(const FailedTestCase &b) const { return !(*this == b); }
};
}
TEST_CASE("Apriltag pose estimation test", "[apriltag_pose_estimation_test]")
{
std::map<vpDetectorAprilTag::vpAprilTagFamily, std::string> apriltagMap = {
#if defined(VISP_HAVE_APRILTAG_BIG_FAMILY)
,
#endif
};
std::map<vpDetectorAprilTag::vpAprilTagFamily, double> tagSizeScales = {
#if defined(VISP_HAVE_APRILTAG_BIG_FAMILY)
,
#endif
};
std::vector<vpDetectorAprilTag::vpPoseEstimationMethod> poseMethods = {
#if defined(VISP_HAVE_LAPACK) || defined(VISP_HAVE_OPENCV) || defined(VISP_HAVE_EIGEN3)
#endif
};
std::map<vpDetectorAprilTag::vpPoseEstimationMethod, std::string> methodNames = {
#if defined(VISP_HAVE_LAPACK) || defined(VISP_HAVE_OPENCV) || defined(VISP_HAVE_EIGEN3)
#endif
};
const size_t nbTags = 5;
const double tagSize_ = 0.25;
std::map<int, double> tagsSize = {
{0, tagSize_}, {1, tagSize_}, {2, tagSize_}, {3, tagSize_ / 2}, {4, tagSize_ / 2},
};
std::map<int, double> errorTranslationThresh = {
{0, 0.025}, {1, 0.09}, {2, 0.05}, {3, 0.13}, {4, 0.09},
};
std::map<int, double> errorRotationThresh = {
{0, 0.04}, {1, 0.075}, {2, 0.07}, {3, 0.18}, {4, 0.13},
};
std::vector<FailedTestCase> ignoreTests = {
std::map<int, vpHomogeneousMatrix> groundTruthPoses;
for (size_t i = 0; i < nbTags; i++) {
std::string filename =
std::to_string(i) + std::string(".txt"));
std::ifstream file(filename);
groundTruthPoses[static_cast<int>(i)].load(file);
}
for (const auto &kv : apriltagMap) {
auto family = kv.first;
std::cout << "\nApriltag family: " << family << std::endl;
std::string filename =
std::string("AprilTag/benchmark/640x480/") + kv.second + std::string("_640x480.png"));
const double tagSize = tagSize_ * tagSizeScales[family];
for (auto method : poseMethods) {
std::cout << "\tPose estimation method: " << method << std::endl;
apriltag_detector.setAprilTagPoseEstimationMethod(method);
{
std::vector<vpHomogeneousMatrix> cMo_vec;
apriltag_detector.detect(I, tagSize, cam, cMo_vec);
CHECK(cMo_vec.size() == nbTags);
std::vector<std::vector<vpImagePoint> > tagsCorners = apriltag_detector.getPolygon();
CHECK(tagsCorners.size() == nbTags);
std::vector<std::string> messages = apriltag_detector.getMessage();
CHECK(messages.size() == nbTags);
std::vector<int> tagsId = apriltag_detector.getTagsId();
CHECK(tagsId.size() == nbTags);
std::map<int, int> idsCount;
for (size_t i = 0; i < tagsId.size(); i++) {
idsCount[tagsId[i]]++;
CHECK((tagsId[i] >= 0 && tagsId[i] < 5));
}
CHECK(idsCount.size() == nbTags);
for (size_t i = 0; i < cMo_vec.size(); i++) {
int id = tagsId[i];
if (id >= 3) {
continue;
}
std::cout << "\t\tSame size, Tag: " << i << std::endl;
double error_trans = sqrt(error_translation.
sumSquare() / 3);
double error_orientation = sqrt(error_thetau.
sumSquare() / 3);
std::cout << "\t\t\tTranslation error: " << error_trans << " / Rotation error: " << error_orientation
<< std::endl;
CHECK((error_trans < errorTranslationThresh[id] && error_orientation < errorRotationThresh[id]));
}
}
{
apriltag_detector.detect(I);
std::vector<std::vector<vpImagePoint> > tagsCorners = apriltag_detector.getPolygon();
CHECK(tagsCorners.size() == nbTags);
std::vector<std::string> messages = apriltag_detector.getMessage();
CHECK(messages.size() == nbTags);
std::vector<int> tagsId = apriltag_detector.getTagsId();
CHECK(tagsId.size() == nbTags);
std::map<int, int> idsCount;
for (size_t i = 0; i < tagsId.size(); i++) {
idsCount[tagsId[i]]++;
CHECK((tagsId[i] >= 0 && tagsId[i] < 5));
}
CHECK(idsCount.size() == nbTags);
for (size_t idx = 0; idx < tagsId.size(); idx++) {
std::cout << "\t\tCustom size, Tag: " << idx << std::endl;
const int id = tagsId[idx];
apriltag_detector.getPose(idx, tagsSize[id] * tagSizeScales[family], cam, cMo);
double error_trans = sqrt(error_translation.
sumSquare() / 3);
double error_orientation = sqrt(error_thetau.
sumSquare() / 3);
std::cout << "\t\t\tTranslation error: " << error_trans << " / Rotation error: " << error_orientation
<< std::endl;
if (std::find(ignoreTests.begin(), ignoreTests.end(),
FailedTestCase(family, method, static_cast<int>(idx))) == ignoreTests.end()) {
CHECK((error_trans < errorTranslationThresh[id] && error_orientation < errorRotationThresh[id]));
}
}
}
{
apriltag_detector.detect(I);
std::vector<std::vector<vpImagePoint> > tagsCorners = apriltag_detector.getPolygon();
CHECK(tagsCorners.size() == nbTags);
std::vector<std::string> messages = apriltag_detector.getMessage();
CHECK(messages.size() == nbTags);
std::vector<int> tagsId = apriltag_detector.getTagsId();
CHECK(tagsId.size() == nbTags);
std::map<int, int> idsCount;
for (size_t i = 0; i < tagsId.size(); i++) {
idsCount[tagsId[i]]++;
CHECK((tagsId[i] >= 0 && tagsId[i] < 5));
}
CHECK(idsCount.size() == nbTags);
for (size_t idx = 0; idx < tagsId.size(); idx++) {
std::cout << "\t\tCustom size + aligned Z-axis, Tag: " << idx << std::endl;
const int id = tagsId[idx];
apriltag_detector.setZAlignedWithCameraAxis(true);
apriltag_detector.getPose(idx, tagsSize[id] * tagSizeScales[family], cam, cMo);
apriltag_detector.setZAlignedWithCameraAxis(false);
double error_trans = sqrt(error_translation.
sumSquare() / 3);
double error_orientation = sqrt(error_thetau.
sumSquare() / 3);
std::cout << "\t\t\tTranslation error: " << error_trans << " / Rotation error: " << error_orientation
<< std::endl;
if (std::find(ignoreTests.begin(), ignoreTests.end(),
FailedTestCase(family, method, static_cast<int>(idx))) == ignoreTests.end()) {
CHECK((error_trans < errorTranslationThresh[id] && error_orientation < errorRotationThresh[id]));
}
}
}
}
}
}
TEST_CASE("Apriltag corners accuracy test", "[apriltag_corners_accuracy_test]")
{
std::map<vpDetectorAprilTag::vpAprilTagFamily, std::string> apriltagMap = {
#if defined(VISP_HAVE_APRILTAG_BIG_FAMILY)
,
#endif
};
const size_t nbTags = 5;
std::map<vpDetectorAprilTag::vpAprilTagFamily, std::map<int, std::vector<vpImagePoint> > > groundTruthCorners;
for (const auto &kv : apriltagMap) {
std::string filename =
std::string("AprilTag/benchmark/640x480/corners_") + kv.second + std::string(".txt"));
std::ifstream file(filename);
REQUIRE(file.is_open());
int id = 0;
double p0x = 0, p0y = 0;
double p1x = 0, p1y = 0;
double p2x = 0, p2y = 0;
double p3x = 0, p3y = 0;
while (file >> id >> p0x >> p0y >> p1x >> p1y >> p2x >> p2y >> p3x >> p3y) {
groundTruthCorners[kv.first][id].push_back(
vpImagePoint(p0y, p0x));
groundTruthCorners[kv.first][id].push_back(
vpImagePoint(p1y, p1x));
groundTruthCorners[kv.first][id].push_back(
vpImagePoint(p2y, p2x));
groundTruthCorners[kv.first][id].push_back(
vpImagePoint(p3y, p3x));
REQUIRE(groundTruthCorners[kv.first][id].size() == 4);
}
}
for (const auto &kv : apriltagMap) {
auto family = kv.first;
std::cout << "\nApriltag family: " << family << std::endl;
std::string filename =
std::string("AprilTag/benchmark/640x480/") + kv.second + std::string("_640x480.png"));
apriltag_detector.detect(I);
std::vector<int> tagsId = apriltag_detector.getTagsId();
std::vector<std::vector<vpImagePoint> > tagsCorners = apriltag_detector.getTagsCorners();
REQUIRE(tagsCorners.size() == nbTags);
REQUIRE(tagsId.size() == nbTags);
for (size_t i = 0; i < tagsCorners.size(); i++) {
const int tagId = tagsId[i];
REQUIRE(tagsCorners[i].size() == 4);
TagGroundTruth corners_ref("", groundTruthCorners[family][tagId]);
TagGroundTruth corners_cur("", tagsCorners[i]);
CHECK((corners_ref == corners_cur));
std::cout << "\tid: " << tagId << " - RMSE: " << corners_ref.rmse(corners_cur.m_corners) << std::endl;
}
}
}
#if defined(VISP_HAVE_LAPACK) || defined(VISP_HAVE_OPENCV) || defined(VISP_HAVE_EIGEN3)
TEST_CASE("Apriltag regression test", "[apriltag_regression_test]")
{
#if (VISP_HAVE_DATASET_VERSION >= 0x030600)
#else
#endif
const double tagSize = 0.053;
const float quad_decimate = 1.0;
dynamic_cast<vpDetectorAprilTag *
>(detector)->setAprilTagPoseEstimationMethod(poseEstimationMethod);
std::vector<vpHomogeneousMatrix> cMo_vec;
std::map<std::string, TagGroundTruth> mapOfTagsGroundTruth;
{
std::string filename_ground_truth =
std::ifstream file_ground_truth(filename_ground_truth.c_str());
REQUIRE(file_ground_truth.is_open());
std::string message = "";
double v1 = 0.0, v2 = 0.0, v3 = 0.0, v4 = 0.0;
double u1 = 0.0, u2 = 0.0, u3 = 0.0, u4 = 0.0;
while (file_ground_truth >> message >> v1 >> u1 >> v2 >> u2 >> v3 >> u3 >> v4 >> u4) {
std::vector<vpImagePoint> tagCorners(4);
tagCorners[0].set_ij(v1, u1);
tagCorners[1].set_ij(v2, u2);
tagCorners[2].set_ij(v3, u3);
tagCorners[3].set_ij(v4, u4);
mapOfTagsGroundTruth.insert(std::make_pair(message, TagGroundTruth(message, tagCorners)));
}
}
std::map<std::string, vpPoseVector> mapOfPosesGroundTruth;
{
std::string filename_ground_truth =
std::ifstream file_ground_truth(filename_ground_truth.c_str());
REQUIRE(file_ground_truth.is_open());
std::string message = "";
double tx = 0.0, ty = 0.0, tz = 0.0;
double tux = 0.0, tuy = 0.0, tuz = 0.0;
while (file_ground_truth >> message >> tx >> ty >> tz >> tux >> tuy >> tuz) {
mapOfPosesGroundTruth.insert(std::make_pair(message,
vpPoseVector(tx, ty, tz, tux, tuy, tuz)));
}
}
std::vector<vpImagePoint> p = detector->
getPolygon(i);
std::replace(message.begin(), message.end(), ' ', '_');
std::map<std::string, TagGroundTruth>::iterator it = mapOfTagsGroundTruth.find(message);
TagGroundTruth current(message, p);
if (it == mapOfTagsGroundTruth.end()) {
std::cerr << "Problem with tag decoding (tag_family or id): " << message << std::endl;
}
else if (it->second != current) {
std::cerr << "Problem, current detection:\n" << current << "\nReference:\n" << it->second << std::endl;
}
REQUIRE(it != mapOfTagsGroundTruth.end());
CHECK(it->second == current);
}
for (size_t i = 0; i < cMo_vec.size(); i++) {
std::replace(message.begin(), message.end(), ' ', '_');
std::map<std::string, vpPoseVector>::iterator it = mapOfPosesGroundTruth.find(message);
if (it == mapOfPosesGroundTruth.end()) {
std::cerr << "Problem with tag decoding (tag_family or id): " << message << std::endl;
}
REQUIRE(it != mapOfPosesGroundTruth.end());
std::cout << "Tag: " << message << std::endl;
std::cout << "\tEstimated pose: " << pose_vec.t() << std::endl;
std::cout << "\tReference pose: " << it->second.t() << std::endl;
for (unsigned int cpt = 0; cpt < 3; cpt++) {
std::cerr << "Problem, current pose: " << pose_vec.t() << "\nReference pose: " << it->second.t() << std::endl;
}
}
}
delete detector;
}
TEST_CASE("Apriltag copy constructor test", "[apriltag_copy_constructor_test]")
{
const std::string filename =
const double tagSize = 0.25 * 5 / 9;
const float quad_decimate = 1.0;
std::vector<vpHomogeneousMatrix> cMo_vec;
detector->
detect(I, tagSize, cam, cMo_vec);
std::vector<std::vector<vpImagePoint> > tagsCorners = detector->
getTagsCorners();
std::vector<int> tagsId = detector->
getTagsId();
delete detector;
std::vector<std::vector<vpImagePoint> > tagsCorners_copy = detector_copy.
getTagsCorners();
std::vector<int> tagsId_copy = detector_copy.getTagsId();
REQUIRE(tagsCorners_copy.size() == tagsCorners.size());
REQUIRE(tagsId_copy.size() == tagsId.size());
REQUIRE(tagsCorners_copy.size() == tagsId_copy.size());
for (size_t i = 0; i < tagsCorners.size(); i++) {
const std::vector<vpImagePoint> &corners_ref = tagsCorners[i];
const std::vector<vpImagePoint> &corners_copy = tagsCorners_copy[i];
REQUIRE(corners_ref.size() == corners_copy.size());
for (size_t j = 0; j < corners_ref.size(); j++) {
CHECK(corner_ref == corner_copy);
}
int id_ref = tagsId[i];
int id_copy = tagsId_copy[i];
CHECK(id_ref == id_copy);
}
std::vector<vpHomogeneousMatrix> cMo_vec_copy;
detector_copy.detect(I, tagSize, cam, cMo_vec_copy);
REQUIRE(cMo_vec.size() == cMo_vec_copy.size());
for (size_t idx = 0; idx < cMo_vec_copy.size(); idx++) {
for (unsigned int i = 0; i < 3; i++) {
for (unsigned int j = 0; j < 4; j++) {
CHECK(
vpMath::equal(cMo[i][j], cMo_copy[i][j], std::numeric_limits<double>::epsilon()));
}
}
}
}
TEST_CASE("Apriltag assignment operator test", "[apriltag_assignment_operator_test]")
{
const std::string filename =
const double tagSize = 0.25 * 5 / 9;
const float quad_decimate = 1.0;
std::vector<vpHomogeneousMatrix> cMo_vec;
detector->
detect(I, tagSize, cam, cMo_vec);
std::vector<std::vector<vpImagePoint> > tagsCorners = detector->
getTagsCorners();
std::vector<int> tagsId = detector->
getTagsId();
delete detector;
std::vector<std::vector<vpImagePoint> > tagsCorners_copy = detector_copy.
getTagsCorners();
std::vector<int> tagsId_copy = detector_copy.
getTagsId();
REQUIRE(tagsCorners_copy.size() == tagsCorners.size());
REQUIRE(tagsId_copy.size() == tagsId.size());
REQUIRE(tagsCorners_copy.size() == tagsId_copy.size());
for (size_t i = 0; i < tagsCorners.size(); i++) {
const std::vector<vpImagePoint> &corners_ref = tagsCorners[i];
const std::vector<vpImagePoint> &corners_copy = tagsCorners_copy[i];
REQUIRE(corners_ref.size() == corners_copy.size());
for (size_t j = 0; j < corners_ref.size(); j++) {
CHECK(corner_ref == corner_copy);
}
int id_ref = tagsId[i];
int id_copy = tagsId_copy[i];
CHECK(id_ref == id_copy);
}
std::vector<vpHomogeneousMatrix> cMo_vec_copy;
detector_copy.
detect(I, tagSize, cam, cMo_vec_copy);
REQUIRE(cMo_vec.size() == cMo_vec_copy.size());
for (size_t idx = 0; idx < cMo_vec_copy.size(); idx++) {
for (unsigned int i = 0; i < 3; i++) {
for (unsigned int j = 0; j < 4; j++) {
CHECK(
vpMath::equal(cMo[i][j], cMo_copy[i][j], std::numeric_limits<double>::epsilon()));
}
}
}
}
TEST_CASE("Apriltag getTagsPoints3D test", "[apriltag_get_tags_points3D_test]")
{
const std::string filename =
const double familyScale = 5.0 / 9;
const double tagSize = 0.25;
std::map<int, double> tagsSize = {
{-1, tagSize * familyScale}, {3, tagSize / 2 * familyScale}, {4, tagSize / 2 * familyScale} };
std::vector<vpHomogeneousMatrix> cMo_vec;
std::vector<int> tagsId = detector.
getTagsId();
for (size_t i = 0; i < tagsId.size(); i++) {
int id = tagsId[i];
double size = tagsSize[-1];
if (tagsSize.find(id) != tagsSize.end()) {
size = tagsSize[id];
}
detector.
getPose(i, size, cam, cMo);
cMo_vec.push_back(cMo);
}
std::vector<std::vector<vpPoint> > tagsPoints = detector.
getTagsPoints3D(tagsId, tagsSize);
std::vector<std::vector<vpImagePoint> > tagsCorners = detector.
getTagsCorners();
REQUIRE(tagsPoints.size() == tagsCorners.size());
for (size_t i = 0; i < tagsPoints.size(); i++) {
REQUIRE(tagsPoints[i].size() == tagsCorners[i].size());
for (size_t j = 0; j < tagsPoints[i].size(); j++) {
double x = 0, y = 0;
}
double epsilon = 1e-12;
for (
unsigned int row = 0; row < cMo.
getRows(); row++) {
for (
unsigned int col = 0; col < cMo.
getCols(); col++) {
CHECK(
vpMath::equal(cMo[row][col], cMo_manual[row][col], epsilon));
}
}
}
}
#endif
int main(int argc, const char *argv[])
{
Catch::Session session;
session.applyCommandLine(argc, argv);
int numFailed = session.run();
return numFailed;
}
#else
int main() { return EXIT_SUCCESS; }
#endif
unsigned int getCols() const
unsigned int getRows() const
Generic class defining intrinsic camera parameters.
void initPersProjWithoutDistortion(double px, double py, double u0, double v0)
Implementation of column vector and the associated operations.
@ BEST_RESIDUAL_VIRTUAL_VS
@ HOMOGRAPHY_ORTHOGONAL_ITERATION
std::vector< std::vector< vpImagePoint > > getTagsCorners() const
void setAprilTagQuadDecimate(float quadDecimate)
std::vector< std::vector< vpPoint > > getTagsPoints3D(const std::vector< int > &tagsId, const std::map< int, double > &tagsSize) const
bool detect(const vpImage< unsigned char > &I) VP_OVERRIDE
@ TAG_CIRCLE21h7
AprilTag Circle21h7 pattern.
@ TAG_25h9
AprilTag 25h9 pattern.
@ TAG_CUSTOM48h12
AprilTag Custom48h12 pattern.
@ TAG_36h11
AprilTag 36h11 pattern (recommended)
@ TAG_STANDARD52h13
AprilTag Standard52h13 pattern.
@ TAG_16h5
AprilTag 16h5 pattern.
@ TAG_STANDARD41h12
AprilTag Standard41h12 pattern.
@ TAG_CIRCLE49h12
AprilTag Circle49h12 pattern.
std::vector< int > getTagsId() const
bool getPose(size_t tagIndex, double tagSize, const vpCameraParameters &cam, vpHomogeneousMatrix &cMo, vpHomogeneousMatrix *cMo2=nullptr, double *projError=nullptr, double *projError2=nullptr)
std::vector< std::vector< vpImagePoint > > & getPolygon()
size_t getNbObjects() const
std::vector< std::string > & getMessage()
Implementation of an homogeneous matrix and operations on such kind of matrices.
static void read(vpImage< unsigned char > &I, const std::string &filename, int backend=IO_DEFAULT_BACKEND)
Class that defines a 2D point in an image. This class is useful for image processing and stores only ...
unsigned int getSize() const
static bool equal(double x, double y, double threshold=0.001)
static void convertPoint(const vpCameraParameters &cam, const double &u, const double &v, double &x, double &y)
Class that defines a 3D point in the object frame and allows forward projection of a 3D point in the ...
void set_x(double x)
Set the point x coordinate in the image plane.
void set_y(double y)
Set the point y coordinate in the image plane.
Implementation of a pose vector and operations on poses.
Class used for pose computation from N points (pose from point only). Some of the algorithms implemen...
@ DEMENTHON_LAGRANGE_VIRTUAL_VS
Implementation of a rotation vector as axis-angle minimal representation.
Class that consider the case of a translation vector.