diff --git a/corelib/include/rtabmap/core/Compression.h b/corelib/include/rtabmap/core/Compression.h index 8d4ff237..80fb0c7d 100644 --- a/corelib/include/rtabmap/core/Compression.h +++ b/corelib/include/rtabmap/core/Compression.h @@ -114,6 +114,15 @@ private: * 0 is invalid and v>0 is the depth depthQuantA/(v-depthQuantB). */ +/** @brief Signature of the ".rvl" layout (8 bytes, not null-terminated). */ +const char kCompressedDepthRvlSignature[8] = {'D', 'E', 'P', 'T', 'H', 'R', 'V', 'L'}; +/** @brief Size of the ".rvl" header: signature, uint32 cols, uint32 rows. */ +const size_t kCompressedDepthRvlHeaderSize = 16; +/** @brief Signature of the inverse depth layout (8 bytes, not null-terminated). */ +const char kCompressedDepthInvSignature[8] = {'D', 'E', 'P', 'T', 'H', 'I', 'N', 'V'}; +/** @brief Size of the inverse depth header: signature, float depthQuantA, float depthQuantB. */ +const size_t kCompressedDepthInvHeaderSize = 16; + /** * @brief Parses an image compression format "[:[:]]". * diff --git a/corelib/src/Compression.cpp b/corelib/src/Compression.cpp index 3d62c0da..de3727ec 100644 --- a/corelib/src/Compression.cpp +++ b/corelib/src/Compression.cpp @@ -70,19 +70,6 @@ int deserializeMatType(int serializedType) ((serializedType >> kSerializedCnShift) & 511) + 1); } -// Signatures at the start of rtabmap's own depth formats. Anything else is -// assumed to be a standard image format (PNG, JPG) decoded by OpenCV. -const char kRvlSignature[8] = {'D', 'E', 'P', 'T', 'H', 'R', 'V', 'L'}; -const size_t kRvlHeaderSize = 16; // signature, uint32 cols, uint32 rows - -// Quantized inverse depth of a 32FC1 depth image (same quantization as -// ROS's compressed_depth_image_transport): signature, float depthQuantA, -// float depthQuantB, followed by the 16UC1 inverse depth image compressed -// in one of the formats above (PNG, or RVL with its own signature). -// See the layouts documented in Compression.h, rtabmap_ros relies on them. -const char kInvDepthSignature[8] = {'D', 'E', 'P', 'T', 'H', 'I', 'N', 'V'}; -const size_t kInvDepthHeaderSize = 16; - // Default quantization when only the maximum depth is set in the format. const float kDefaultDepthQuantization = 100.0f; @@ -269,11 +256,11 @@ std::vector compressImage(const cv::Mat & image, const std::strin std::vector invDepthBytes = compressImage(invDepth, codec); if(!invDepthBytes.empty()) { - bytes.resize(kInvDepthHeaderSize + invDepthBytes.size()); - memcpy(&bytes[0], kInvDepthSignature, 8); + bytes.resize(kCompressedDepthInvHeaderSize + invDepthBytes.size()); + memcpy(&bytes[0], kCompressedDepthInvSignature, 8); memcpy(&bytes[8], &depthQuantA, 4); memcpy(&bytes[12], &depthQuantB, 4); - memcpy(&bytes[kInvDepthHeaderSize], invDepthBytes.data(), invDepthBytes.size()); + memcpy(&bytes[kCompressedDepthInvHeaderSize], invDepthBytes.data(), invDepthBytes.size()); } } else if(image.type() == CV_32FC1) @@ -284,7 +271,7 @@ std::vector compressImage(const cv::Mat & image, const std::strin } else if(codec == ".rvl") { - bytes.assign(kRvlSignature, kRvlSignature+8); + bytes.assign(kCompressedDepthRvlSignature, kCompressedDepthRvlSignature+8); int numPixels = image.rows * image.cols; // In the worst case, RVL compression results in ~1.5x larger data. bytes.resize(3 * numPixels + 20); @@ -293,8 +280,8 @@ std::vector compressImage(const cv::Mat & image, const std::strin memcpy(&bytes[8], &cols, 4); memcpy(&bytes[12], &rows, 4); RvlCodec rvl; - int compressedSize = rvl.CompressRVL(image.ptr(), &bytes[kRvlHeaderSize], numPixels); - bytes.resize(kRvlHeaderSize + compressedSize); + int compressedSize = rvl.CompressRVL(image.ptr(), &bytes[kCompressedDepthRvlHeaderSize], numPixels); + bytes.resize(kCompressedDepthRvlHeaderSize + compressedSize); } else { @@ -317,6 +304,10 @@ cv::Mat compressImage2(const cv::Mat & image, const std::string & format) cv::Mat uncompressImage(const cv::Mat & bytes) { + if(bytes.empty()) + { + return cv::Mat(); + } return uncompressImage(bytes.data, bytes.total()*bytes.elemSize()); } @@ -330,9 +321,9 @@ cv::Mat uncompressImage(const unsigned char * bytes, size_t size) cv::Mat image; if(bytes && size) { - if(hasSignature(bytes, size, kInvDepthSignature)) + if(hasSignature(bytes, size, kCompressedDepthInvSignature)) { - if(size <= kInvDepthHeaderSize) + if(size <= kCompressedDepthInvHeaderSize) { UERROR("Inverse depth image is truncated (%d bytes).", (int)size); return image; @@ -340,7 +331,7 @@ cv::Mat uncompressImage(const unsigned char * bytes, size_t size) float depthQuantA, depthQuantB; memcpy(&depthQuantA, &bytes[8], 4); memcpy(&depthQuantB, &bytes[12], 4); - cv::Mat invDepth = uncompressImage(&bytes[kInvDepthHeaderSize], size - kInvDepthHeaderSize); + cv::Mat invDepth = uncompressImage(&bytes[kCompressedDepthInvHeaderSize], size - kCompressedDepthInvHeaderSize); if(invDepth.type() == CV_16UC1) { image = invDepthToDepth(invDepth, depthQuantA, depthQuantB); @@ -350,9 +341,9 @@ cv::Mat uncompressImage(const unsigned char * bytes, size_t size) UERROR("Inverse depth image should be 16UC1 (type=%d).", invDepth.type()); } } - else if(hasSignature(bytes, size, kRvlSignature)) + else if(hasSignature(bytes, size, kCompressedDepthRvlSignature)) { - if(size < kRvlHeaderSize) + if(size < kCompressedDepthRvlHeaderSize) { UERROR("RVL depth image is truncated (%d bytes).", (int)size); return image; @@ -362,7 +353,7 @@ cv::Mat uncompressImage(const unsigned char * bytes, size_t size) memcpy(&rows, &bytes[12], 4); image = cv::Mat(rows, cols, CV_16UC1); RvlCodec rvl; - rvl.DecompressRVL(&bytes[kRvlHeaderSize], image.ptr(), cols * rows); + rvl.DecompressRVL(&bytes[kCompressedDepthRvlHeaderSize], image.ptr(), cols * rows); } else { @@ -526,17 +517,17 @@ std::string compressedDepthFormat(const unsigned char * bytes, size_t size) std::string format; if(bytes && size) { - if(hasSignature(bytes, size, kInvDepthSignature) && size > kInvDepthHeaderSize) + if(hasSignature(bytes, size, kCompressedDepthInvSignature) && size > kCompressedDepthInvHeaderSize) { float depthQuantA, depthQuantB, maxDepth, quantization; memcpy(&depthQuantA, &bytes[8], 4); memcpy(&depthQuantB, &bytes[12], 4); invDepthParameters(depthQuantA, depthQuantB, maxDepth, quantization); format = uFormat("%s:%g:%g", - compressedDepthFormat(&bytes[kInvDepthHeaderSize], size - kInvDepthHeaderSize).c_str(), + compressedDepthFormat(&bytes[kCompressedDepthInvHeaderSize], size - kCompressedDepthInvHeaderSize).c_str(), maxDepth, quantization); } - else if(hasSignature(bytes, size, kRvlSignature)) + else if(hasSignature(bytes, size, kCompressedDepthRvlSignature)) { format = ".rvl"; } diff --git a/corelib/test/test_compression.cpp b/corelib/test/test_compression.cpp index 475223c6..b2a929f4 100644 --- a/corelib/test/test_compression.cpp +++ b/corelib/test/test_compression.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -380,3 +381,33 @@ TEST(CompressionTest, LegacyFloatDepthIsLossless) EXPECT_EQ(memcmp(restored.data, depth.data, depth.total() * depth.elemSize()), 0); } } + +TEST(CompressionTest, MalformedDepthFormatsDecodeToEmpty) +{ + // Signature and header only, no payload + std::vector invDepth = {'D', 'E', 'P', 'T', 'H', 'I', 'N', 'V'}; + invDepth.resize(16, 0); + EXPECT_TRUE(uncompressImage(invDepth).empty()); + EXPECT_EQ(compressedDepthFormat(invDepth), ".png") << "too short to be inverse depth"; + + // Inverse depth header followed by an 8 bits image instead of a 16 bits one + const std::vector png8 = compressImage(cv::Mat(4, 4, CV_8UC1, cv::Scalar(1)), ".png"); + invDepth.insert(invDepth.end(), png8.begin(), png8.end()); + EXPECT_TRUE(uncompressImage(invDepth).empty()); + + // RVL signature without its size + const std::vector rvl = {'D', 'E', 'P', 'T', 'H', 'R', 'V', 'L', 4, 0}; + EXPECT_TRUE(uncompressImage(rvl).empty()); + EXPECT_EQ(compressedDepthFormat(rvl), ".rvl"); + + EXPECT_TRUE(uncompressImage(nullptr, 0).empty()); +} + +TEST(CompressionTest, CompressionThreadRejectsInvalidFormat) +{ + // std::string: a string literal would select the (bytes, isImage) constructor + const cv::Mat depth(4, 4, CV_32FC1, cv::Scalar(1.0f)); + EXPECT_THROW(CompressionThread(depth, std::string(".jpg:10")), UException); + EXPECT_THROW(CompressionThread(depth, std::string(".bmp")), UException); + EXPECT_NO_THROW(CompressionThread(depth, std::string(".rvl:10:100"))); +} diff --git a/corelib/test/test_memory.cpp b/corelib/test/test_memory.cpp index 7853f925..7a38a3fc 100644 --- a/corelib/test/test_memory.cpp +++ b/corelib/test/test_memory.cpp @@ -4690,10 +4690,19 @@ TEST(MemoryTest, CreateSignatureRecompressesStereoPairAfterRectification) namespace { +enum DepthInput +{ + kRawDepth, + kCompressedDepthWithRaw, // e.g., received from ROS and decoded + kCompressedDepthOnly // raw depth not needed (no features extracted here) +}; + struct InverseDepthCase { const char * targetVersion; - bool precompressed; // depth already compressed as inverse depth, e.g., from ROS + DepthInput input; + const char * depthCompressionFormat; + bool parallelCompression; const char * expectedFormat; }; @@ -4706,7 +4715,8 @@ TEST_P(MemoryInverseDepthTest, StoredDepthFormatFollowsDatabaseVersion) const InverseDepthCase & cs = GetParam(); ParametersMap params = defaultMemoryParams(); params[Parameters::kMemBinDataKept()] = "true"; - params[Parameters::kMemDepthCompressionFormat()] = ".rvl:10:100"; + params[Parameters::kMemDepthCompressionFormat()] = cs.depthCompressionFormat; + params[Parameters::kMemCompressionParallelized()] = cs.parallelCompression ? "true" : "false"; params[Parameters::kDbTargetVersion()] = cs.targetVersion; Memory memory(params); const std::string dbPath = uniqueDbPath(); @@ -4717,15 +4727,18 @@ TEST_P(MemoryInverseDepthTest, StoredDepthFormatFollowsDatabaseVersion) cv::randu(depth, 0.5f, 8.0f); const CameraModel model(10.0, 10.0, 8.0, 8.0, CameraModel::opticalRotation()); SensorData data; - if(cs.precompressed) + if(cs.input == kRawDepth) { - data = SensorData(compressImage2(rgb, ".png"), compressImage2(depth, ".png:10:100"), model); - data.uncompressData(); // raw images are also there, as when received from ROS - ASSERT_FALSE(data.depthRaw().empty()); + data = SensorData(rgb, depth, model); } else { - data = SensorData(rgb, depth, model); + data = SensorData(compressImage2(rgb, ".png"), compressImage2(depth, ".png:10:100"), model); + if(cs.input == kCompressedDepthWithRaw) + { + data.uncompressData(); + ASSERT_FALSE(data.depthRaw().empty()); + } } ASSERT_TRUE(memory.update(data, Transform(0, 0, 0, 0, 0, 0), cv::Mat::eye(6, 6, CV_64FC1) * 0.01)); @@ -4748,7 +4761,12 @@ INSTANTIATE_TEST_SUITE_P( DatabaseVersions, MemoryInverseDepthTest, ::testing::Values( - InverseDepthCase{"", false, ".rvl:10:100"}, - InverseDepthCase{"", true, ".png:10:100"}, // reused as is - InverseDepthCase{"0.23.0", false, ".png"}, // legacy 32FC1 format - InverseDepthCase{"0.23.0", true, ".png"})); // re-compressed + InverseDepthCase{"", kRawDepth, ".rvl:10:100", true, ".rvl:10:100"}, + InverseDepthCase{"", kRawDepth, ".rvl:10:100", false, ".rvl:10:100"}, + InverseDepthCase{"", kCompressedDepthWithRaw, ".rvl:10:100", true, ".png:10:100"}, // reused as is + InverseDepthCase{"", kCompressedDepthOnly, ".rvl:10:100", true, ".png:10:100"}, // reused as is + InverseDepthCase{"0.23.0", kRawDepth, ".rvl:10:100", true, ".png"}, // legacy 32FC1 format + InverseDepthCase{"0.23.0", kCompressedDepthWithRaw, ".rvl:10:100", true, ".png"}, // re-compressed + InverseDepthCase{"0.23.0", kCompressedDepthOnly, ".rvl:10:100", true, ".png"}, // decompressed, re-compressed + InverseDepthCase{"", kRawDepth, ".rvl", true, ".png"}, // RVL is 16UC1 only: legacy + InverseDepthCase{"", kRawDepth, ".jpg", true, ".png"})); // invalid: default ".rvl"