From 29e43469867d86160849303a9d0dae1262ec98d8 Mon Sep 17 00:00:00 2001 From: Artem Date: Sun, 11 Oct 2026 22:57:02 -0400 Subject: [PATCH] Memory::computeTransform(): keep global descriptors when reloading node data (#1788) When a registration needs data that a node no longer has in RAM, its SensorData is replaced by getNodeData(), which doesn't load global descriptors from the database. The next Signature::compareTo() with that node then fails its assertion on the number of global descriptors. Same issue as #1686 in copyData(). --- corelib/src/Memory.cpp | 5 +++ corelib/test/test_memory.cpp | 59 ++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/corelib/src/Memory.cpp b/corelib/src/Memory.cpp index 82eea94e..e1b4dd55 100644 --- a/corelib/src/Memory.cpp +++ b/corelib/src/Memory.cpp @@ -3453,13 +3453,18 @@ Transform Memory::computeTransform( (_registrationPipeline->isScanRequired() && fromS.sensorData().imageCompressed().empty() && fromS.sensorData().laserScanCompressed().isEmpty()) || (_registrationPipeline->isUserDataRequired() && fromS.sensorData().imageCompressed().empty() && fromS.sensorData().userDataCompressed().empty())) { + // getNodeData() doesn't load global descriptors from the database, keep those in RAM + std::vector globalDescriptors = fromS.sensorData().globalDescriptors(); fromS.sensorData() = getNodeData(fromS.id(), true, true, true, true); + fromS.sensorData().setGlobalDescriptors(globalDescriptors); } if(((_reextractLoopClosureFeatures && (_registrationPipeline->isImageRequired() || guess.isNull())) && toS.sensorData().imageCompressed().empty()) || (_registrationPipeline->isScanRequired() && toS.sensorData().imageCompressed().empty() && toS.sensorData().laserScanCompressed().isEmpty()) || (_registrationPipeline->isUserDataRequired() && toS.sensorData().imageCompressed().empty() && toS.sensorData().userDataCompressed().empty())) { + std::vector globalDescriptors = toS.sensorData().globalDescriptors(); toS.sensorData() = getNodeData(toS.id(), true, true, true, true); + toS.sensorData().setGlobalDescriptors(globalDescriptors); } // uncompress only what we need cv::Mat imgBuf, depthBuf, userBuf; diff --git a/corelib/test/test_memory.cpp b/corelib/test/test_memory.cpp index bda4a611..8bb76014 100644 --- a/corelib/test/test_memory.cpp +++ b/corelib/test/test_memory.cpp @@ -4437,6 +4437,65 @@ TEST_F(MemoryFixture, ComputeIcpTransformMultiRejectsScansTooFarApart) << "unexpected reason: " << info.rejectedMsg; } +// --------------------------------------------------------------------------- +// Memory::computeTransform(): reloading a saved node's data for a registration. +// +// Once a node is saved to the database, its compressed data is dropped from RAM, +// and a registration that needs it (here ICP) reloads it with getNodeData(), +// which doesn't load global descriptors. The node must keep them: a later +// Signature::compareTo() asserts that both nodes have the same number of global +// descriptors (same issue as #1686 in copyData()). +// --------------------------------------------------------------------------- + +TEST(MemoryTest, ComputeTransformKeepsGlobalDescriptorsWhenReloadingData) +{ + const std::string dbPath = uniqueDbPath(); + { + Memory memory(icpMemoryParams()); + ASSERT_TRUE(memory.init(dbPath)); + + const cv::Mat image(8, 8, CV_8UC1, cv::Scalar(128)); + const cv::Mat covariance = cv::Mat::eye(6, 6, CV_64FC1) * 0.01; + const Transform motion(0.10f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f); + const LaserScan corner = memoryCorner2D(); + const cv::Mat descriptor(1, 4, CV_32FC1, cv::Scalar(0.5f)); // normalized + + SensorData first(image); + first.setLaserScan(corner); + first.addGlobalDescriptor(GlobalDescriptor(1, descriptor)); + ASSERT_TRUE(memory.update(first, Transform::getIdentity(), covariance)); + const int oldId = memory.getLastSignatureId(); + + SensorData second(image); + second.setLaserScan(util3d::transformLaserScan(corner, motion.inverse())); + second.addGlobalDescriptor(GlobalDescriptor(1, descriptor)); + ASSERT_TRUE(memory.update(second, motion, covariance)); + const int newId = memory.getLastSignatureId(); + + // As Rtabmap::process() does after adding a node, with a database on disk. + memory.saveLocationData(oldId); + memory.emptyTrash(); + memory.joinTrashThread(); // emptyTrash() is async; wait for the DB write + ASSERT_TRUE(memory.getSignature(oldId)->isSaved()); + ASSERT_TRUE(memory.getSignature(oldId)->sensorData().laserScanCompressed().isEmpty()); + ASSERT_EQ(memory.getSignature(oldId)->sensorData().globalDescriptors().size(), 1u); + + RegistrationInfo info; + memory.computeTransform(oldId, newId, motion, &info); + + const Signature * oldS = memory.getSignature(oldId); + const Signature * newS = memory.getSignature(newId); + ASSERT_NE(oldS, nullptr); + ASSERT_NE(newS, nullptr); + EXPECT_FALSE(oldS->sensorData().laserScanCompressed().isEmpty()); // reloaded + EXPECT_EQ(oldS->sensorData().globalDescriptors().size(), 1u); + EXPECT_NO_THROW(newS->compareTo(*oldS)); + + memory.close(false); + } + UFile::erase(dbPath); +} + // --------------------------------------------------------------------------- // createSignature() reuses the caller's compressed blob instead of // re-compressing, but only while the pixels it would store are provably the