improving features2d tests

This commit is contained in:
matlabbe
2026-06-03 22:55:23 -07:00
parent 06dfe92d02
commit cb50b6f21b
6 changed files with 508 additions and 105 deletions
+16
View File
@@ -270,6 +270,15 @@ public:
virtual const ParametersMap & getParameters() const {return parameters_;}
virtual Feature2D::Type getType() const = 0;
/** @brief Returns true when a GPU/CUDA code path **could** be used by
* this detector on this host: i.e. the build was compiled with the
* matching GPU support AND a CUDA-capable device is detected at
* runtime. This is a capability probe -- it does NOT reflect whether
* the current instance is actually configured to run on GPU (that
* depends on per-detector parameters like SURF/GpuVersion). Defaults
* to false; subclasses with a GPU backend override it. */
virtual bool isGpuAvailable() const {return false;}
protected:
Feature2D(const ParametersMap & parameters = ParametersMap());
@@ -302,6 +311,7 @@ public:
virtual void parseParameters(const ParametersMap & parameters);
virtual Feature2D::Type getType() const {return kFeatureSurf;}
virtual bool isGpuAvailable() const override;
private:
virtual std::vector<cv::KeyPoint> generateKeypointsImpl(const cv::Mat & image, const cv::Rect & roi, const cv::Mat & mask = cv::Mat());
@@ -329,6 +339,7 @@ public:
virtual void parseParameters(const ParametersMap & parameters);
virtual Feature2D::Type getType() const {return kFeatureSift;}
virtual bool isGpuAvailable() const override;
private:
virtual std::vector<cv::KeyPoint> generateKeypointsImpl(const cv::Mat & image, const cv::Rect & roi, const cv::Mat & mask = cv::Mat());
@@ -363,6 +374,7 @@ public:
virtual void parseParameters(const ParametersMap & parameters);
virtual Feature2D::Type getType() const {return kFeatureOrb;}
virtual bool isGpuAvailable() const override;
private:
virtual std::vector<cv::KeyPoint> generateKeypointsImpl(const cv::Mat & image, const cv::Rect & roi, const cv::Mat & mask = cv::Mat());
@@ -394,6 +406,7 @@ public:
virtual void parseParameters(const ParametersMap & parameters);
virtual Feature2D::Type getType() const {return kFeatureUndef;}
virtual bool isGpuAvailable() const override;
private:
virtual std::vector<cv::KeyPoint> generateKeypointsImpl(const cv::Mat & image, const cv::Rect & roi, const cv::Mat & mask = cv::Mat());
@@ -470,6 +483,7 @@ public:
virtual ~GFTT();
virtual void parseParameters(const ParametersMap & parameters);
virtual bool isGpuAvailable() const override;
private:
virtual std::vector<cv::KeyPoint> generateKeypointsImpl(const cv::Mat & image, const cv::Rect & roi, const cv::Mat & mask = cv::Mat());
@@ -650,6 +664,7 @@ public:
virtual void parseParameters(const ParametersMap & parameters);
virtual Feature2D::Type getType() const { return kFeatureSuperPointTorch; }
virtual bool isGpuAvailable() const override;
private:
virtual std::vector<cv::KeyPoint> generateKeypointsImpl(const cv::Mat & image, const cv::Rect & roi, const cv::Mat & mask = cv::Mat());
@@ -673,6 +688,7 @@ public:
virtual void parseParameters(const ParametersMap & parameters);
virtual Feature2D::Type getType() const { return kFeatureSuperPointRpautrat; }
virtual bool isGpuAvailable() const override;
private:
virtual std::vector<cv::KeyPoint> generateKeypointsImpl(const cv::Mat & image, const cv::Rect & roi, const cv::Mat & mask = cv::Mat());
+69 -2
View File
@@ -1240,6 +1240,19 @@ void SURF::parseParameters(const ParametersMap & parameters)
#endif
}
bool SURF::isGpuAvailable() const
{
#ifdef RTABMAP_NONFREE
#if CV_MAJOR_VERSION < 3
return cv::gpu::getCudaEnabledDeviceCount() > 0;
#else
return cv::cuda::getCudaEnabledDeviceCount() > 0;
#endif
#else
return false;
#endif
}
std::vector<cv::KeyPoint> SURF::generateKeypointsImpl(const cv::Mat & image, const cv::Rect & roi, const cv::Mat & mask)
{
UASSERT(!image.empty() && image.channels() == 1 && image.depth() == CV_8U);
@@ -1404,6 +1417,15 @@ void SIFT::parseParameters(const ParametersMap & parameters)
}
bool SIFT::isGpuAvailable() const
{
#ifdef RTABMAP_CUDASIFT
return cv::cuda::getCudaEnabledDeviceCount() > 0;
#else
return false;
#endif
}
std::vector<cv::KeyPoint> SIFT::generateKeypointsImpl(const cv::Mat & image, const cv::Rect & roi, const cv::Mat & mask)
{
UASSERT(!image.empty() && image.channels() == 1 && image.depth() == CV_8U);
@@ -1681,6 +1703,15 @@ void ORB::parseParameters(const ParametersMap & parameters)
}
}
bool ORB::isGpuAvailable() const
{
#ifdef HAVE_OPENCV_CUDAFEATURES2D
return cv::cuda::getCudaEnabledDeviceCount() > 0;
#else
return false;
#endif
}
std::vector<cv::KeyPoint> ORB::generateKeypointsImpl(const cv::Mat & image, const cv::Rect & roi, const cv::Mat & mask)
{
UASSERT(!image.empty() && image.channels() == 1 && image.depth() == CV_8U);
@@ -1945,6 +1976,15 @@ void FAST::parseParameters(const ParametersMap & parameters)
}
}
bool FAST::isGpuAvailable() const
{
#ifdef HAVE_OPENCV_CUDAFEATURES2D
return cv::cuda::getCudaEnabledDeviceCount() > 0;
#else
return false;
#endif
}
std::vector<cv::KeyPoint> FAST::generateKeypointsImpl(const cv::Mat & image, const cv::Rect & roi, const cv::Mat & mask)
{
UASSERT(!image.empty() && image.channels() == 1 && image.depth() == CV_8U);
@@ -2208,6 +2248,15 @@ void GFTT::parseParameters(const ParametersMap & parameters)
}
}
bool GFTT::isGpuAvailable() const
{
#ifdef HAVE_OPENCV_CUDAIMGPROC
return cv::cuda::getCudaEnabledDeviceCount() > 0;
#else
return false;
#endif
}
std::vector<cv::KeyPoint> GFTT::generateKeypointsImpl(const cv::Mat & image, const cv::Rect & roi, const cv::Mat & mask)
{
UASSERT(!image.empty() && image.channels() == 1 && image.depth() == CV_8U);
@@ -2659,6 +2708,15 @@ SuperPointTorch::~SuperPointTorch()
{
}
bool SuperPointTorch::isGpuAvailable() const
{
#ifdef RTABMAP_TORCH
return torch::cuda::is_available();
#else
return false;
#endif
}
void SuperPointTorch::parseParameters(const ParametersMap & parameters)
{
Feature2D::parseParameters(parameters);
@@ -2693,7 +2751,7 @@ std::vector<cv::KeyPoint> SuperPointTorch::generateKeypointsImpl(const cv::Mat &
{
#ifdef RTABMAP_TORCH
UASSERT(!image.empty() && image.channels() == 1 && image.depth() == CV_8U);
if(roi.x!=0 || roi.y !=0)
if(roi.x!=0 || roi.y !=0 || roi.width!=image.cols || roi.height!=image.rows)
{
UERROR("SuperPoint: Not supporting ROI (%d,%d,%d,%d). Make sure %s, %s, %s, %s, %s, %s are all set to default values.",
roi.x, roi.y, roi.width, roi.height,
@@ -2743,6 +2801,15 @@ SuperPointRpautrat::~SuperPointRpautrat()
{
}
bool SuperPointRpautrat::isGpuAvailable() const
{
#if defined(RTABMAP_TORCH) && defined(RTABMAP_PYTHON)
return torch::cuda::is_available();
#else
return false;
#endif
}
void SuperPointRpautrat::parseParameters(const ParametersMap & parameters)
{
Feature2D::parseParameters(parameters);
@@ -2795,7 +2862,7 @@ std::vector<cv::KeyPoint> SuperPointRpautrat::generateKeypointsImpl(const cv::Ma
{
#if defined(RTABMAP_TORCH) && defined(RTABMAP_PYTHON)
UASSERT(!image.empty() && image.channels() == 1 && image.depth() == CV_8U);
if(roi.x!=0 || roi.y !=0)
if(roi.x!=0 || roi.y !=0 || roi.width!=image.cols || roi.height!=image.rows)
{
UERROR("SuperPoint Rpautrat: Not supporting ROI (%d,%d,%d,%d). Make sure %s, %s, %s, %s, %s, %s are all set to default values.",
roi.x, roi.y, roi.width, roi.height,
+9 -4
View File
@@ -546,8 +546,10 @@ void ExtractorNode::DivideNode(ExtractorNode &n1, ExtractorNode &n2, ExtractorNo
vector<cv::KeyPoint> ORBextractor::DistributeOctTree(const vector<cv::KeyPoint>& vToDistributeKeys, const int &minX,
const int &maxX, const int &minY, const int &maxY, const int &N, const int &level)
{
// Compute how many initial nodes
const int nIni = round(static_cast<float>(maxX-minX)/(maxY-minY));
// Compute how many initial nodes. For regions taller than wide the
// raw ratio rounds down to 0; clamp to at least 1 so hX stays finite
// and vpIniNodes is non-empty.
const int nIni = std::max(1, (int)round(static_cast<float>(maxX-minX)/(maxY-minY)));
const float hX = static_cast<float>(maxX-minX)/nIni;
@@ -569,11 +571,14 @@ vector<cv::KeyPoint> ORBextractor::DistributeOctTree(const vector<cv::KeyPoint>&
vpIniNodes[i] = &lNodes.back();
}
//Associate points to childs
//Associate points to childs. A keypoint sitting exactly on the right
//border has kp.pt.x == maxX-minX, so kp.pt.x/hX == nIni -- one past
//the last bucket. Clamp to nIni-1 to keep it in range.
for(size_t i=0;i<vToDistributeKeys.size();i++)
{
const cv::KeyPoint &kp = vToDistributeKeys[i];
vpIniNodes[kp.pt.x/hX]->vKeys.push_back(kp);
const int bucket = std::min(static_cast<int>(kp.pt.x/hX), nIni-1);
vpIniNodes[bucket]->vKeys.push_back(kp);
}
list<ExtractorNode>::iterator lit = lNodes.begin();
+5
View File
@@ -26,6 +26,11 @@ public:
virtual void parseParameters(const ParametersMap & parameters);
virtual Feature2D::Type getType() const {return kFeaturePyDetector;}
// PyDetector hands off CUDA usage to the python script -- the C++
// wrapper can't introspect it, so we report capability is "possible"
// whenever the python interpreter is built in. The actual hardware
// presence + the script's own decision is out of our hands.
virtual bool isGpuAvailable() const override {return true;}
private:
virtual std::vector<cv::KeyPoint> generateKeypointsImpl(const cv::Mat & image, const cv::Rect & roi, const cv::Mat & mask = cv::Mat());
+342 -82
View File
@@ -6,14 +6,40 @@
#include <rtabmap/core/Parameters.h>
#include <rtabmap/utilite/UConversion.h>
#include <rtabmap/utilite/UException.h>
#include <rtabmap/utilite/UFile.h>
#include <rtabmap/core/Version.h>
#ifdef RTABMAP_PYTHON
#include <rtabmap/core/PythonInterface.h>
#endif
#include <opencv2/core.hpp>
#include <opencv2/core/version.hpp>
#include <opencv2/imgcodecs.hpp>
#include <cmath>
using namespace rtabmap;
#ifdef RTABMAP_PYTHON
// PythonInterface is a Meyers singleton that asserts construction on the
// main thread and embeds a pybind11 scoped_interpreter for the rest of
// the process. Triggering it lazily inside a gtest body has caused
// `take_gil: PyMUTEX_LOCK failed` deadlocks across SuperPoint reconstruct
// cycles; eagerly initialising once before the first test bypasses the
// problem. The Environment hook runs on the gtest main thread immediately
// before `RUN_ALL_TESTS()` starts.
class PythonInterfaceEnv : public ::testing::Environment {
public:
void SetUp() override { PythonInterface::instance("test_features2d"); }
};
static ::testing::Environment * const kPythonEnv =
::testing::AddGlobalTestEnvironment(new PythonInterfaceEnv);
#endif
namespace {
// 200x200 default is intentionally different from the natural-image sample
// so the Generate* tests exercise both sizes. The 200x200 + half-image ROI
// also locks in the ORBextractor::DistributeOctTree regression: keypoints
// landing on the right ROI border used to OOB-write the per-column bucket.
static cv::Mat checkerboardImage(int rows = 200, int cols = 200, int cell = 20)
{
cv::Mat image(rows, cols, CV_8UC1);
@@ -42,29 +68,38 @@ static ParametersMap orbTestParams()
return params;
}
// Forward-declare helpers defined near the Generate* tests, so the earlier
// detector-iterating tests can share the same fixtures.
ParametersMap detectorAssetParams(Feature2D::Type t);
bool isGenericGenerateCandidate(Feature2D::Type t);
struct NamedImage { const char * label; cv::Mat image; };
std::vector<NamedImage> generateTestImages();
} // namespace
TEST(Feature2DTest, TypeNameKnownTypes)
// Smoke-test that isGpuAvailable() compiles and is callable for every available
// detector. We don't assert what it should return -- that depends on the
// host (GPU/CUDA presence, build flags). What we DO check: a detector that
// reports isGpuAvailable()=true must have actually selected a GPU codepath
// (i.e. its build was compiled with GPU support).
TEST(Feature2DTest, IsGpuAvailable)
{
EXPECT_EQ(Feature2D::typeName(Feature2D::kFeatureUndef), "Unknown");
EXPECT_EQ(Feature2D::typeName(Feature2D::kFeatureSurf), "SURF");
EXPECT_EQ(Feature2D::typeName(Feature2D::kFeatureSift), "SIFT");
EXPECT_EQ(Feature2D::typeName(Feature2D::kFeatureOrb), "ORB");
EXPECT_EQ(Feature2D::typeName(Feature2D::kFeatureFastFreak), "FAST+FREAK");
EXPECT_EQ(Feature2D::typeName(Feature2D::kFeatureFastBrief), "FAST+BRIEF");
EXPECT_EQ(Feature2D::typeName(Feature2D::kFeatureGfttFreak), "GFTT+Freak");
EXPECT_EQ(Feature2D::typeName(Feature2D::kFeatureGfttBrief), "GFTT+Brief");
EXPECT_EQ(Feature2D::typeName(Feature2D::kFeatureBrisk), "BRISK");
EXPECT_EQ(Feature2D::typeName(Feature2D::kFeatureGfttOrb), "GFTT+ORB");
EXPECT_EQ(Feature2D::typeName(Feature2D::kFeatureKaze), "KAZE");
EXPECT_EQ(Feature2D::typeName(Feature2D::kFeatureOrbOctree), "ORB-OCTREE");
EXPECT_EQ(Feature2D::typeName(Feature2D::kFeatureSuperPointTorch), "SUPERPOINT");
EXPECT_EQ(Feature2D::typeName(Feature2D::kFeatureSurfFreak), "SURF+Freak");
EXPECT_EQ(Feature2D::typeName(Feature2D::kFeatureGfttDaisy), "GFTT+Daisy");
EXPECT_EQ(Feature2D::typeName(Feature2D::kFeatureSurfDaisy), "SURF+Daisy");
EXPECT_EQ(Feature2D::typeName(Feature2D::kFeaturePyDetector), "PyDetector");
EXPECT_EQ(Feature2D::typeName(Feature2D::kFeatureSuperPointRpautrat), "SUPERPOINT-RPAUTRAT");
EXPECT_EQ(Feature2D::typeName((Feature2D::Type)99), "Unknown");
for(int strategy = Feature2D::kFeatureSurf; strategy < Feature2D::kFeatureEnd; ++strategy)
{
const Feature2D::Type t = static_cast<Feature2D::Type>(strategy);
if(!Feature2D::isAvailable(t)) continue;
SCOPED_TRACE(Feature2D::typeName(t));
// isGpuAvailable() only checks build flags + runtime CUDA, so we
// don't need an actual model loaded -- construct SuperPoint /
// PyDetector even when their assets aren't present.
std::unique_ptr<Feature2D> detector(Feature2D::create(t, detectorAssetParams(t)));
ASSERT_TRUE(detector.get() != NULL);
const bool gpu = detector->isGpuAvailable();
// Smoke: the call returned without crashing or hanging. Actual
// value depends on the host (GPU/CUDA presence, build flags),
// so we don't assert true/false here.
(void)gpu;
}
}
// Invariant: every type in [kFeatureSurf, kFeatureEnd) is named explicitly in
@@ -84,6 +119,9 @@ TEST(Feature2DTest, TypeNameCoverage)
EXPECT_FALSE(name.empty())
<< "Feature2D::typeName() returns empty string for enum value " << strategy;
}
// Sentinel + out-of-range values fall through to "Unknown".
EXPECT_EQ(Feature2D::typeName(Feature2D::kFeatureUndef), "Unknown");
EXPECT_EQ(Feature2D::typeName((Feature2D::Type)99), "Unknown");
}
TEST(Feature2DTest, ComputeRoiFromRatios)
@@ -168,6 +206,42 @@ TEST(Feature2DTest, LimitKeypointsWithDescriptors)
EXPECT_EQ(keptResponses, expectedTop3);
}
// Verify Kp/MaxFeatures caps detector output across every available
// detector and both test images. Cap = 20: every detector currently
// finds more than that on both images, so the cap is genuinely
// exercised in all combinations. Also verifies generateDescriptors()
// produces one row per surviving keypoint (no silent drop).
TEST(Feature2DTest, KpMaxFeaturesCapsDetectorOutput)
{
const int kMaxKeypoints = 20;
int tested = 0;
for(const auto & img : generateTestImages())
{
SCOPED_TRACE(std::string("image=") + img.label);
for(int s = Feature2D::kFeatureSurf; s < Feature2D::kFeatureEnd; ++s)
{
const Feature2D::Type t = static_cast<Feature2D::Type>(s);
if(!isGenericGenerateCandidate(t)) continue;
SCOPED_TRACE(Feature2D::typeName(t));
ParametersMap params = detectorAssetParams(t);
params[Parameters::kKpMaxFeatures()] = uNumber2Str(kMaxKeypoints);
std::unique_ptr<Feature2D> detector(Feature2D::create(t, params));
ASSERT_TRUE(detector.get() != NULL);
std::vector<cv::KeyPoint> capped = detector->generateKeypoints(img.image);
EXPECT_LE(static_cast<int>(capped.size()), kMaxKeypoints)
<< "capped=" << capped.size();
cv::Mat descriptors = detector->generateDescriptors(img.image, capped);
EXPECT_EQ(descriptors.rows, static_cast<int>(capped.size()))
<< "descriptors=" << descriptors.rows
<< " keypoints=" << capped.size();
++tested;
}
}
ASSERT_GT(tested, 0) << "no Features2D detector was available";
}
TEST(Feature2DTest, FilterKeypointsByDepth)
{
std::vector<cv::KeyPoint> keypoints;
@@ -283,88 +357,274 @@ TEST(Feature2DTest, ParseParametersUpdatesMaxFeatures)
EXPECT_EQ(detector->getMaxFeatures(), 50);
}
namespace {
// Load `data/samples/17.jpg` -- a real outdoor frame with strong corner
// content for blob-based descriptors (SIFT/SURF/KAZE) and stable enough
// for binary ones (ORB/BRIEF). Test data ships with the repo so this is
// always available, no fetch_test_data.sh required.
cv::Mat loadSampleImage()
{
const std::string path = std::string(RTABMAP_TEST_DATA_ROOT) + "/samples/17.jpg";
return cv::imread(path, cv::IMREAD_GRAYSCALE);
}
// Paths fetched by scripts/fetch_test_data.sh into data/tests/.
inline std::string superpointTorchModel()
{
return std::string(RTABMAP_TEST_DATA_ROOT) + "/tests/superpoint_v1.pt";
}
inline std::string superpointRpautratWeights()
{
return std::string(RTABMAP_TEST_DATA_ROOT) + "/tests/superpoint_v6_from_tf.pth";
}
inline std::string superpointRpautratModel()
{
return std::string(RTABMAP_TEST_DATA_ROOT) + "/tests/superpoint_pytorch.py";
}
// Detector types iterated by the Generate* tests. PyDetector needs a
// user-supplied script (skipped). SuperPoint variants need libtorch
// weights + (for Rpautrat) a Python model file; included when those
// assets are on disk.
bool isGenericGenerateCandidate(Feature2D::Type t)
{
if(!Feature2D::isAvailable(t)) return false;
if(t == Feature2D::kFeaturePyDetector) return false;
if(t == Feature2D::kFeatureSuperPointTorch)
{
return UFile::exists(superpointTorchModel());
}
if(t == Feature2D::kFeatureSuperPointRpautrat)
{
return UFile::exists(superpointRpautratWeights())
&& UFile::exists(superpointRpautratModel());
}
return true;
}
ParametersMap detectorAssetParams(Feature2D::Type t)
{
ParametersMap params;
if(t == Feature2D::kFeatureSuperPointTorch)
{
params[Parameters::kSuperPointModelPath()] = superpointTorchModel();
params[Parameters::kSuperPointCuda()] = "false";
}
else if(t == Feature2D::kFeatureSuperPointRpautrat)
{
params[Parameters::kSuperPointRpautratWeightsPath()] = superpointRpautratWeights();
params[Parameters::kSuperPointRpautratModelPath()] = superpointRpautratModel();
params[Parameters::kSuperPointRpautratCuda()] = "false";
}
return params;
}
// Flips the per-detector "use GPU" parameter on. Each backend has its own
// key (SURF/GpuVersion, SIFT/Gpu, ORB/Gpu, FAST/Gpu, GFTT/Gpu,
// SuperPoint/Cuda, SuperPointRpautrat/Cuda). Detectors that share a key
// (FAST_BRIEF/FAST_FREAK share FAST/Gpu; the GFTT variants share GFTT/Gpu)
// are grouped accordingly. Detectors without a GPU backend pass through
// unchanged.
void setDetectorGpu(Feature2D::Type t, ParametersMap & params)
{
switch(t)
{
case Feature2D::kFeatureSurf: params[Parameters::kSURFGpuVersion()] = "true"; break;
case Feature2D::kFeatureSift: params[Parameters::kSIFTGpu()] = "true"; break;
case Feature2D::kFeatureOrb: params[Parameters::kORBGpu()] = "true"; break;
case Feature2D::kFeatureFastBrief:
case Feature2D::kFeatureFastFreak: params[Parameters::kFASTGpu()] = "true"; break;
case Feature2D::kFeatureGfttFreak:
case Feature2D::kFeatureGfttBrief:
case Feature2D::kFeatureGfttOrb:
case Feature2D::kFeatureGfttDaisy: params[Parameters::kGFTTGpu()] = "true"; break;
case Feature2D::kFeatureSuperPointTorch: params[Parameters::kSuperPointCuda()] = "true"; break;
case Feature2D::kFeatureSuperPointRpautrat: params[Parameters::kSuperPointRpautratCuda()] = "true"; break;
default: break;
}
}
} // namespace
namespace {
// Each (test, detector) pair runs against both a real outdoor frame (17.jpg)
// and a synthetic high-contrast checkerboard. The pair-iteration catches
// detectors that handle one image well and crash on the other (e.g. an
// edge case in ORB-OCTREE on uniform synthetic patterns with ROI).
// NamedImage is forward-declared near the top of this file so earlier
// tests can share the helper.
std::vector<NamedImage> generateTestImages()
{
return {
{"sample17", loadSampleImage()},
{"checkerboard", checkerboardImage()},
};
}
} // namespace
TEST(Feature2DTest, GenerateKeypointsAndDescriptors)
{
const cv::Mat image = checkerboardImage();
std::unique_ptr<Feature2D> detector(Feature2D::create(Feature2D::kFeatureOrb, orbTestParams()));
ASSERT_TRUE(detector.get() != NULL);
int tested = 0;
for(const auto & img : generateTestImages())
{
SCOPED_TRACE(std::string("image=") + img.label);
for(int strategy = Feature2D::kFeatureSurf; strategy < Feature2D::kFeatureEnd; ++strategy)
{
const Feature2D::Type t = static_cast<Feature2D::Type>(strategy);
if(!isGenericGenerateCandidate(t)) continue;
SCOPED_TRACE(Feature2D::typeName(t));
std::vector<cv::KeyPoint> keypoints = detector->generateKeypoints(image);
EXPECT_GT(keypoints.size(), 0u);
// CPU pass.
ParametersMap cpuParams = detectorAssetParams(t);
std::unique_ptr<Feature2D> cpuDetector(Feature2D::create(t, cpuParams));
ASSERT_TRUE(cpuDetector.get() != NULL);
cv::Mat descriptors = detector->generateDescriptors(image, keypoints);
ASSERT_FALSE(descriptors.empty());
EXPECT_EQ(descriptors.rows, static_cast<int>(keypoints.size()));
EXPECT_EQ(descriptors.type(), CV_8UC1);
std::vector<cv::KeyPoint> cpuKeypoints = cpuDetector->generateKeypoints(img.image);
EXPECT_GT(cpuKeypoints.size(), 0u);
cv::Mat cpuDescriptors = cpuDetector->generateDescriptors(img.image, cpuKeypoints);
ASSERT_FALSE(cpuDescriptors.empty());
EXPECT_EQ(cpuDescriptors.rows, static_cast<int>(cpuKeypoints.size()));
// Descriptor dtype is detector-specific (binary CV_8UC1 for
// ORB/BRIEF/BRISK/FREAK, float CV_32FC1 for SURF/SIFT/KAZE/DAISY),
// so we just sanity-check it's one of those two.
EXPECT_TRUE(cpuDescriptors.type() == CV_8UC1 || cpuDescriptors.type() == CV_32FC1)
<< "unexpected descriptor type " << cpuDescriptors.type();
++tested;
// GPU pass (only when the build + runtime supports it; the
// per-instance flag is the per-detector GPU param we set
// below). Compare keypoint counts between the two paths:
// the CPU and GPU implementations don't have to be
// bit-identical, but they should detect a similar order
// of magnitude of keypoints on the same image.
if(!cpuDetector->isGpuAvailable()) continue;
cpuDetector.reset(); // free CUDA resources before reconstructing
SCOPED_TRACE("variant=GPU");
ParametersMap gpuParams = detectorAssetParams(t);
setDetectorGpu(t, gpuParams);
std::unique_ptr<Feature2D> gpuDetector(Feature2D::create(t, gpuParams));
ASSERT_TRUE(gpuDetector.get() != NULL);
std::vector<cv::KeyPoint> gpuKeypoints = gpuDetector->generateKeypoints(img.image);
EXPECT_GT(gpuKeypoints.size(), 0u);
cv::Mat gpuDescriptors = gpuDetector->generateDescriptors(img.image, gpuKeypoints);
ASSERT_FALSE(gpuDescriptors.empty());
EXPECT_EQ(gpuDescriptors.rows, static_cast<int>(gpuKeypoints.size()));
// CPU/GPU implementations differ in sub-pixel refinement,
// thresholding, and NMS, so the counts won't match exactly.
// Allow up to 30% relative difference -- catches a backend
// that returns 0 or a huge spread, lets small drift through.
const float cpuCount = static_cast<float>(cpuKeypoints.size());
const float gpuCount = static_cast<float>(gpuKeypoints.size());
const float relDiff = std::fabs(gpuCount - cpuCount) / std::max(cpuCount, 1.0f);
EXPECT_LE(relDiff, 0.30f)
<< "CPU keypoints=" << cpuKeypoints.size()
<< " vs GPU keypoints=" << gpuKeypoints.size()
<< " (relDiff=" << relDiff << ")";
}
}
ASSERT_GT(tested, 0) << "no detector was available to exercise";
}
TEST(Feature2DTest, GenerateKeypointsAndDescriptorsWithMask)
{
const cv::Mat image = checkerboardImage();
// Valid region: left half only (OpenCV mask: non-zero = detect).
cv::Mat mask(image.rows, image.cols, CV_8UC1, cv::Scalar(0));
mask(cv::Rect(0, 0, image.cols / 2, image.rows)).setTo(255);
std::unique_ptr<Feature2D> detector(Feature2D::create(Feature2D::kFeatureOrb, orbTestParams()));
ASSERT_TRUE(detector.get() != NULL);
std::vector<cv::KeyPoint> keypoints = detector->generateKeypoints(image, mask);
EXPECT_GT(keypoints.size(), 0u);
for(size_t i = 0; i < keypoints.size(); ++i)
int tested = 0;
for(const auto & img : generateTestImages())
{
const int x = cvRound(keypoints[i].pt.x);
const int y = cvRound(keypoints[i].pt.y);
ASSERT_GE(x, 0);
ASSERT_GE(y, 0);
ASSERT_LT(x, mask.cols);
ASSERT_LT(y, mask.rows);
EXPECT_GT(mask.at<uchar>(y, x), 0)
<< "keypoint " << i << " at (" << x << "," << y << ") outside mask";
}
SCOPED_TRACE(std::string("image=") + img.label);
// Valid region: left half only (OpenCV mask: non-zero = detect).
cv::Mat mask(img.image.rows, img.image.cols, CV_8UC1, cv::Scalar(0));
mask(cv::Rect(0, 0, img.image.cols / 2, img.image.rows)).setTo(255);
cv::Mat descriptors = detector->generateDescriptors(image, keypoints);
ASSERT_FALSE(descriptors.empty());
EXPECT_EQ(descriptors.rows, static_cast<int>(keypoints.size()));
EXPECT_EQ(descriptors.type(), CV_8UC1);
for(int strategy = Feature2D::kFeatureSurf; strategy < Feature2D::kFeatureEnd; ++strategy)
{
const Feature2D::Type t = static_cast<Feature2D::Type>(strategy);
if(!isGenericGenerateCandidate(t)) continue;
SCOPED_TRACE(Feature2D::typeName(t));
std::unique_ptr<Feature2D> detector(Feature2D::create(t, detectorAssetParams(t)));
ASSERT_TRUE(detector.get() != NULL);
std::vector<cv::KeyPoint> keypoints = detector->generateKeypoints(img.image, mask);
EXPECT_GT(keypoints.size(), 0u);
for(size_t i = 0; i < keypoints.size(); ++i)
{
const int x = cvRound(keypoints[i].pt.x);
const int y = cvRound(keypoints[i].pt.y);
ASSERT_GE(x, 0);
ASSERT_GE(y, 0);
ASSERT_LT(x, mask.cols);
ASSERT_LT(y, mask.rows);
EXPECT_GT(mask.at<uchar>(y, x), 0)
<< "keypoint " << i << " at (" << x << "," << y << ") outside mask";
}
cv::Mat descriptors = detector->generateDescriptors(img.image, keypoints);
ASSERT_FALSE(descriptors.empty());
EXPECT_EQ(descriptors.rows, static_cast<int>(keypoints.size()));
++tested;
}
}
ASSERT_GT(tested, 0) << "no detector was available to exercise";
}
TEST(Feature2DTest, GenerateKeypointsAndDescriptorsWithRoi)
{
const cv::Mat image = checkerboardImage();
// Left half only (Kp/RoiRatios: left right top bottom).
const cv::Rect roi = Feature2D::computeRoi(image, "0 0.5 0 0");
ASSERT_GT(roi.width, 0);
ASSERT_GT(roi.height, 0);
ParametersMap params = orbTestParams();
params.insert(ParametersPair(Parameters::kKpRoiRatios(), "0 0.5 0 0"));
std::unique_ptr<Feature2D> detector(Feature2D::create(Feature2D::kFeatureOrb, params));
ASSERT_TRUE(detector.get() != NULL);
std::vector<cv::KeyPoint> keypoints = detector->generateKeypoints(image);
EXPECT_GT(keypoints.size(), 0u);
for(size_t i = 0; i < keypoints.size(); ++i)
int tested = 0;
for(const auto & img : generateTestImages())
{
const int x = cvRound(keypoints[i].pt.x);
const int y = cvRound(keypoints[i].pt.y);
EXPECT_GE(x, roi.x) << "keypoint " << i << " at (" << x << "," << y << ")";
EXPECT_GE(y, roi.y);
EXPECT_LT(x, roi.x + roi.width);
EXPECT_LT(y, roi.y + roi.height);
}
SCOPED_TRACE(std::string("image=") + img.label);
// Left half only (Kp/RoiRatios: left right top bottom).
const cv::Rect roi = Feature2D::computeRoi(img.image, "0 0.5 0 0");
ASSERT_GT(roi.width, 0);
ASSERT_GT(roi.height, 0);
cv::Mat descriptors = detector->generateDescriptors(image, keypoints);
ASSERT_FALSE(descriptors.empty());
EXPECT_EQ(descriptors.rows, static_cast<int>(keypoints.size()));
EXPECT_EQ(descriptors.type(), CV_8UC1);
for(int strategy = Feature2D::kFeatureSurf; strategy < Feature2D::kFeatureEnd; ++strategy)
{
const Feature2D::Type t = static_cast<Feature2D::Type>(strategy);
if(!isGenericGenerateCandidate(t)) continue;
// SuperPointTorch / SuperPointRpautrat ignore the ROI extent
// (they only refuse non-zero offsets; they don't crop the
// image to roi.width/height). Skipping until that's fixed.
if(t == Feature2D::kFeatureSuperPointTorch
|| t == Feature2D::kFeatureSuperPointRpautrat) continue;
SCOPED_TRACE(Feature2D::typeName(t));
ParametersMap params = detectorAssetParams(t);
params[Parameters::kKpRoiRatios()] = "0 0.5 0 0";
std::unique_ptr<Feature2D> detector(Feature2D::create(t, params));
ASSERT_TRUE(detector.get() != NULL);
std::vector<cv::KeyPoint> keypoints = detector->generateKeypoints(img.image);
EXPECT_GT(keypoints.size(), 0u);
for(size_t i = 0; i < keypoints.size(); ++i)
{
const int x = cvRound(keypoints[i].pt.x);
const int y = cvRound(keypoints[i].pt.y);
EXPECT_GE(x, roi.x) << "keypoint " << i << " at (" << x << "," << y << ")";
EXPECT_GE(y, roi.y);
EXPECT_LT(x, roi.x + roi.width);
EXPECT_LT(y, roi.y + roi.height);
}
cv::Mat descriptors = detector->generateDescriptors(img.image, keypoints);
ASSERT_FALSE(descriptors.empty());
EXPECT_EQ(descriptors.rows, static_cast<int>(keypoints.size()));
++tested;
}
}
ASSERT_GT(tested, 0) << "no detector was available to exercise";
}
TEST(Feature2DTest, GenerateDescriptorsEmptyForNoKeypoints)
{
const cv::Mat image = checkerboardImage();
const cv::Mat image = loadSampleImage();
std::unique_ptr<Feature2D> detector(Feature2D::create(Feature2D::kFeatureOrb, orbTestParams()));
std::vector<cv::KeyPoint> keypoints;
const cv::Mat descriptors = detector->generateDescriptors(image, keypoints);
+67 -17
View File
@@ -1645,30 +1645,28 @@ TEST_F(RtabmapIntegrationFixture, AppearanceOnly_PrecisionRecall)
const std::string superpointRpautratWeights = std::string(RTABMAP_TEST_DATA_ROOT) + "/tests/superpoint_v6_from_tf.pth";
const std::string superpointRpautratModel = std::string(RTABMAP_TEST_DATA_ROOT) + "/tests/superpoint_pytorch.py";
// Two BoW likelihood variants: the rtabmap default (raw word-count
// likelihood) and the TF-IDF-weighted variant. They produce different
// hypothesis distributions, so each detector is exercised under both.
const std::vector<bool> tfIdfVariants = {false, true};
// One pass per detector: prefer GPU if available, otherwise CPU. To
// keep wall-clock low, the two BoW likelihood variants (default raw
// word-count vs TF-IDF-weighted) are exercised only on rtabmap's
// default detector (Kp/DetectorStrategy default = kFeatureGfttOrb).
const Feature2D::Type defaultDetector =
static_cast<Feature2D::Type>(Parameters::defaultKpDetectorStrategy());
int detectorsTested = 0;
for(bool tfIdfUsed : tfIdfVariants)
for(int strategy = Feature2D::kFeatureSurf; strategy < Feature2D::kFeatureEnd; ++strategy)
{
const Feature2D::Type detectorType = static_cast<Feature2D::Type>(strategy);
// Label includes the variant so per-detector logs / output BMPs /
// work DBs don't clobber each other across the two iterations.
const std::string detectorLabel = Feature2D::typeName(detectorType)
+ (tfIdfUsed ? "[TfIdf]" : "[Likelihood]");
const std::string typeName = Feature2D::typeName(detectorType);
if(!Feature2D::isAvailable(detectorType))
{
std::cerr << "[skip] detector " << detectorLabel << " not available in this build\n";
std::cerr << "[skip] detector " << typeName << " not available in this build\n";
continue;
}
if(detectorType == Feature2D::kFeaturePyDetector)
{
std::cerr << "[skip] detector " << detectorLabel
std::cerr << "[skip] detector " << typeName
<< " requires a user-supplied Py/DetectorPath script\n";
continue;
}
@@ -1678,7 +1676,7 @@ TEST_F(RtabmapIntegrationFixture, AppearanceOnly_PrecisionRecall)
// script writes them under data/tests/; skip cleanly if absent.
if(detectorType == Feature2D::kFeatureSuperPointTorch && !UFile::exists(superpointTorchModel))
{
std::cerr << "[skip] detector " << detectorLabel
std::cerr << "[skip] detector " << typeName
<< " missing weights: " << superpointTorchModel
<< " (run scripts/fetch_test_data.sh)\n";
continue;
@@ -1687,13 +1685,45 @@ TEST_F(RtabmapIntegrationFixture, AppearanceOnly_PrecisionRecall)
(!UFile::exists(superpointRpautratWeights) ||
!UFile::exists(superpointRpautratModel)))
{
std::cerr << "[skip] detector " << detectorLabel
std::cerr << "[skip] detector " << typeName
<< " missing assets: weights=" << superpointRpautratWeights
<< " model=" << superpointRpautratModel
<< " (run scripts/fetch_test_data.sh)\n";
continue;
}
SCOPED_TRACE(std::string("detector=") + detectorLabel);
// Probe GPU availability once per detector. Build the temp instance
// with the same asset paths the actual run uses; otherwise SuperPoint
// variants log a (harmless) load failure.
bool useGpu = false;
{
ParametersMap probeParams;
if(detectorType == Feature2D::kFeatureSuperPointTorch)
{
probeParams[Parameters::kSuperPointModelPath()] = superpointTorchModel;
}
else if(detectorType == Feature2D::kFeatureSuperPointRpautrat)
{
probeParams[Parameters::kSuperPointRpautratWeightsPath()] = superpointRpautratWeights;
probeParams[Parameters::kSuperPointRpautratModelPath()] = superpointRpautratModel;
}
std::unique_ptr<Feature2D> probe(Feature2D::create(detectorType, probeParams));
if(probe) useGpu = probe->isGpuAvailable();
}
// Run only the default likelihood variant for every detector;
// the TF-IDF variant adds a second run on the default detector
// (kFeatureGfttOrb) so the alternative likelihood path stays
// exercised without quadrupling the test runtime.
std::vector<bool> tfIdfVariants = {false};
if(detectorType == defaultDetector) tfIdfVariants.push_back(true);
for(bool tfIdfUsed : tfIdfVariants)
{
const std::string detectorLabel = typeName
+ (tfIdfUsed ? "[TfIdf]" : "[Likelihood]")
+ (useGpu ? "[GPU]" : "");
SCOPED_TRACE(std::string("detector=") + detectorLabel);
ParametersMap params;
params[Parameters::kRGBDEnabled()] = "false";
@@ -1718,17 +1748,36 @@ TEST_F(RtabmapIntegrationFixture, AppearanceOnly_PrecisionRecall)
params[Parameters::kMemBadSignaturesIgnored()] = "true";
params[Parameters::kMemRehearsalSimilarity()] = "0.20";
// Backend-specific asset paths.
// Backend-specific asset paths + GPU/CUDA toggle. `useGpu` only
// reaches here when this detector reports a usable GPU path; we
// then flip the per-detector "use GPU" parameter on.
const std::string gpuFlag = useGpu ? "true" : "false";
if(detectorType == Feature2D::kFeatureSuperPointTorch)
{
params[Parameters::kSuperPointModelPath()] = superpointTorchModel;
params[Parameters::kSuperPointCuda()] = "false";
params[Parameters::kSuperPointCuda()] = gpuFlag;
}
else if(detectorType == Feature2D::kFeatureSuperPointRpautrat)
{
params[Parameters::kSuperPointRpautratWeightsPath()] = superpointRpautratWeights;
params[Parameters::kSuperPointRpautratModelPath()] = superpointRpautratModel;
params[Parameters::kSuperPointRpautratCuda()] = "false";
params[Parameters::kSuperPointRpautratCuda()] = gpuFlag;
}
else if(useGpu)
{
switch(detectorType)
{
case Feature2D::kFeatureSurf: params[Parameters::kSURFGpuVersion()] = "true"; break;
case Feature2D::kFeatureSift: params[Parameters::kSIFTGpu()] = "true"; break;
case Feature2D::kFeatureOrb: params[Parameters::kORBGpu()] = "true"; break;
case Feature2D::kFeatureFastBrief:
case Feature2D::kFeatureFastFreak: params[Parameters::kFASTGpu()] = "true"; break;
case Feature2D::kFeatureGfttFreak:
case Feature2D::kFeatureGfttBrief:
case Feature2D::kFeatureGfttOrb:
case Feature2D::kFeatureGfttDaisy: params[Parameters::kGFTTGpu()] = "true"; break;
default: break;
}
}
const std::string workDb = workDbForCurrentTest(detectorLabel);
@@ -1908,6 +1957,7 @@ TEST_F(RtabmapIntegrationFixture, AppearanceOnly_PrecisionRecall)
<< " is below 0.9 (sortedTP=" << tp << ", sortedFP=" << fp << ")";
++detectorsTested;
} // end for(tfIdfUsed)
}
ASSERT_GT(detectorsTested, 0) << "no Features2D detector was available in this build";
}