Support Inverted Depth compression (with png or rvl)

This commit is contained in:
matlabbe
2026-10-03 12:40:06 -07:00
parent a4e7f13522
commit 34f0638261
11 changed files with 958 additions and 68 deletions
+2 -2
View File
@@ -21,8 +21,8 @@ SET(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake_modules")
# VERSION
#######################
SET(RTABMAP_MAJOR_VERSION 0)
SET(RTABMAP_MINOR_VERSION 23)
SET(RTABMAP_PATCH_VERSION 13)
SET(RTABMAP_MINOR_VERSION 24)
SET(RTABMAP_PATCH_VERSION 0)
SET(RTABMAP_VERSION
${RTABMAP_MAJOR_VERSION}.${RTABMAP_MINOR_VERSION}.${RTABMAP_PATCH_VERSION})
+58 -4
View File
@@ -41,7 +41,8 @@ namespace rtabmap {
* @brief Background thread to compress or uncompress images and generic matrices.
*
* In compress mode, pass a source matrix to the constructor with an optional image
* format (".png", ".jpg", ".rvl", or empty for zlib data). In uncompress mode, pass
* format (".png", ".jpg", ".rvl", or empty for zlib data, see @ref compressImage() for
* depth options). In uncompress mode, pass
* compressed bytes and set @c isImage accordingly. Call @ref UThread::start() then
* @ref UThread::join() to obtain the result from @ref getCompressedData() or
* @ref getUncompressedData().
@@ -70,7 +71,7 @@ public:
/**
* @brief Constructs a thread in compress mode.
* @param mat Source image or data matrix to compress.
* @param format Image format: @c ".png", @c ".jpg", @c ".rvl", or empty for zlib (@ref compressData2).
* @param format Image format: @c ".png", @c ".jpg", @c ".rvl" (see @ref compressImage()), or empty for zlib (@ref compressData2).
*/
CompressionThread(const cv::Mat & mat, const std::string & format = "");
/**
@@ -93,7 +94,54 @@ private:
bool compressMode_;
};
/** @brief Encodes @p image to a byte buffer (OpenCV @c imencode or RVL for depth). */
/*
* Compressed depth image layouts (all values little-endian). They are stable: they are
* saved in databases, and rtabmap_ros converts them to and from ROS's
* compressed_depth_image_transport messages without decompressing the images.
*
* - ".png": a standard PNG file. 16UC1 depth images are 16 bits grayscale PNGs,
* 32FC1 depth images (legacy) are 4-channel 8 bits PNGs holding the float bytes.
* - ".rvl" (16UC1):
* [0..7] "DEPTHRVL"
* [8..11] uint32 cols
* [12..15] uint32 rows
* [16..] RVL data (see RvlCodec)
* - ".png:<maxDepth>:<quantization>" or ".rvl:<maxDepth>:<quantization>" (32FC1):
* [0..7] "DEPTHINV"
* [8..11] float depthQuantA = quantization*(quantization+1)
* [12..15] float depthQuantB = 1 - depthQuantA/maxDepth
* [16..] the 16UC1 inverse depth image in ".png" or ".rvl" layout above, where
* 0 is invalid and v>0 is the depth depthQuantA/(v-depthQuantB).
*/
/**
* @brief Parses an image compression format "<codec>[:<maxDepth>[:<quantization>]]".
*
* @param format Format, e.g., @c ".jpg", @c ".png", @c ".rvl", @c ".png:10" or @c ".rvl:20:100".
* The empty format is valid (general zlib compression, see @ref CompressionThread).
* @param codec Output codec (e.g., @c ".png").
* @param maxDepth Output maximum depth (m) of the inverse depth format, 0 if not set.
* @param quantization Output depth quantization of the inverse depth format
* (100 if not set but @p maxDepth is), 0 if @p maxDepth is not set.
* @return false if the format is invalid. The inverse depth parameters are only
* valid with @c ".png" and @c ".rvl", and should be positive.
*/
bool RTABMAP_CORE_EXPORT parseImageCompressionFormat(const std::string & format, std::string & codec, float & maxDepth, float & quantization);
/**
* @brief Encodes @p image to a byte buffer (OpenCV @c imencode or RVL for depth).
*
* @param format @c ".png", @c ".jpg" or @c ".rvl" (16UC1 only), optionally followed by
* @c ":<maxDepth>[:<quantization>]" (see @ref parseImageCompressionFormat()).
* For 32FC1 depth images, if @c maxDepth is set, depth is quantized on 16 bits
* as inverse depth (as ROS's @c compressed_depth_image_transport) and compressed
* with the codec. With A=quantization*(quantization+1) and B=1-A/maxDepth, the
* precision is ~d^2/(2A), and depth values over @c maxDepth or under A/(65535-B)
* are lost (set to 0). For example, ".png:10:100" keeps depth between 0.15 and
* 10 m with errors of 0.05 mm at 1 m and 5 mm at 10 m. Otherwise, 32FC1 depth
* images are compressed losslessly as 4-channel 8 bits PNG (legacy format). Other
* image types ignore the depth parameters.
*/
std::vector<unsigned char> RTABMAP_CORE_EXPORT compressImage(const cv::Mat & image, const std::string & format = ".png");
/** @brief Same as @ref compressImage() but returns a @c CV_8UC1 row matrix. */
cv::Mat RTABMAP_CORE_EXPORT compressImage2(const cv::Mat & image, const std::string & format = ".png");
@@ -102,6 +150,8 @@ cv::Mat RTABMAP_CORE_EXPORT compressImage2(const cv::Mat & image, const std::str
cv::Mat RTABMAP_CORE_EXPORT uncompressImage(const cv::Mat & bytes);
/** @brief Decodes compressed image bytes to a @cv::Mat. */
cv::Mat RTABMAP_CORE_EXPORT uncompressImage(const std::vector<unsigned char> & bytes);
/** @brief Decodes compressed image bytes to a @cv::Mat. */
cv::Mat RTABMAP_CORE_EXPORT uncompressImage(const unsigned char * bytes, size_t size);
/** @brief Compresses a matrix with zlib; appends rows, cols and type at the end. */
std::vector<unsigned char> RTABMAP_CORE_EXPORT compressData(const cv::Mat & data);
@@ -122,7 +172,10 @@ std::string RTABMAP_CORE_EXPORT uncompressString(const cv::Mat & bytes);
/**
* @brief Detects the compression format of depth image bytes.
* @return @c ".rvl" if the buffer has an RVL signature, otherwise @c ".png".
* @return @c ".rvl" if the buffer has an RVL signature, @c ".png:<maxDepth>:<quantization>"
* or @c ".rvl:<maxDepth>:<quantization>" for inverse depth images (see
* @ref compressImage()), otherwise @c ".png". The returned format can be passed
* back to @ref compressImage() to compress in the same format.
*/
std::string RTABMAP_CORE_EXPORT compressedDepthFormat(const cv::Mat & bytes);
/** @overload */
@@ -130,5 +183,6 @@ std::string RTABMAP_CORE_EXPORT compressedDepthFormat(const std::vector<unsigned
/** @overload */
std::string RTABMAP_CORE_EXPORT compressedDepthFormat(const unsigned char * bytes, size_t size);
} /* namespace rtabmap */
#endif /* COMPRESSION_H_ */
+1
View File
@@ -848,6 +848,7 @@ private:
unsigned int _imagePreDecimation;
unsigned int _imagePostDecimation;
bool _legacyDecimatedOctave;
bool _inverseDepthCompressionAllowed; // database version >= 0.24
bool _compressionParallelized;
float _laserScanDownsampleStepSize;
float _laserScanVoxelSize;
+1 -1
View File
@@ -222,7 +222,7 @@ class RTABMAP_CORE_EXPORT Parameters
RTABMAP_PARAM(Mem, NotLinkedNodesKept, bool, true, "Keep not linked nodes in db (rehearsed nodes and deleted nodes).");
RTABMAP_PARAM(Mem, IntermediateNodeDataKept, bool, false, "Keep intermediate node data in db.");
RTABMAP_PARAM_STR(Mem, ImageCompressionFormat, ".jpg", "RGB image compression format. It should be \".jpg\" or \".png\".");
RTABMAP_PARAM_STR(Mem, DepthCompressionFormat, ".rvl", "Depth image compression format for 16UC1 depth type. It should be \".png\" or \".rvl\". If depth type is 32FC1, \".png\" is used.");
RTABMAP_PARAM_STR(Mem, DepthCompressionFormat, ".rvl", "Depth image compression format. It should be \".png\" or \".rvl\", optionally followed by \":maxDepth[:quantization]\" (e.g., \".rvl:10:100\", quantization is 100 by default) to compress 32FC1 depth images as 16 bits inverse depth with that codec (same quantization than ROS's compressed_depth_image_transport). 16UC1 depth images are always compressed losslessly with the codec. Warning: the inverse depth format is lossy, the precision is ~d^2/(2*q*(q+1)) (q=quantization, e.g., 0.05 mm at 1 m and 5 mm at 10 m with q=100) and depth values over maxDepth or under ~q*(q+1)/65535 meters (0.15 m with q=100) are lost. Without depth parameters, 32FC1 depth images are compressed losslessly in \".png\" format (4 channels 8 bits). Databases with depth images compressed in inverse depth format cannot be opened by rtabmap versions under 0.24.");
RTABMAP_PARAM(Mem, STMSize, unsigned int, 10, "Short-term memory size.");
RTABMAP_PARAM(Mem, IncrementalMemory, bool, true, "SLAM mode, otherwise it is Localization mode.");
RTABMAP_PARAM(Mem, LocalizationReadOnly, bool, false, uFormat("In localization mode, open the database in read-only mode (ignored if %s=true). Currrenty incompatible with memory management (%s and %s cannot be used) and if there are disjoint sessions in working memory. Last localization pose won't be saved back in the database at the end of the session, so the robot will always restart to original last localization pose, unless %s is used or an external initial pose is provided on initialization.", kMemIncrementalMemory().c_str(), kRtabmapLoopThr().c_str(), kRtabmapMemoryThr().c_str(), kRGBDStartAtOrigin().c_str()).c_str());
+203 -51
View File
@@ -28,9 +28,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "rtabmap/core/Compression.h"
#include <rtabmap/utilite/ULogger.h>
#include <rtabmap/utilite/UConversion.h>
#include <rtabmap/utilite/UStl.h>
#include <opencv2/opencv.hpp>
#include <zlib.h>
#include <cmath>
#include <cstring>
namespace rtabmap {
@@ -67,16 +70,130 @@ 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;
bool hasSignature(const unsigned char * bytes, size_t size, const void * signature)
{
return bytes && size >= 8 && memcmp(bytes, signature, 8) == 0;
}
// Values over maxDepth, NaN, inf, 0 and negative values are set to 0 (invalid),
// as well as values too close to be represented on 16 bits (under
// depthQuantA / (65535 - depthQuantB) meters).
cv::Mat depthToInvDepth(const cv::Mat & depth, float maxDepth, float quantization, float & depthQuantA, float & depthQuantB)
{
UASSERT(depth.type() == CV_32FC1);
depthQuantA = quantization * (quantization + 1.0f);
depthQuantB = 1.0f - depthQuantA / maxDepth;
cv::Mat invDepth(depth.size(), CV_16UC1);
for(int i=0; i<depth.rows; ++i)
{
const float * in = depth.ptr<float>(i);
uint16_t * out = invDepth.ptr<uint16_t>(i);
for(int j=0; j<depth.cols; ++j)
{
const float d = in[j];
if(d > 0.0f && d < maxDepth) // false for NaN
{
// Rounded (ROS truncates), the decoding is the same.
const float v = depthQuantA / d + depthQuantB + 0.5f;
out[j] = v < 65536.0f ? (uint16_t)v : 0;
}
else
{
out[j] = 0;
}
}
}
return invDepth;
}
cv::Mat invDepthToDepth(const cv::Mat & invDepth, float depthQuantA, float depthQuantB)
{
UASSERT(invDepth.type() == CV_16UC1);
cv::Mat depth(invDepth.size(), CV_32FC1);
for(int i=0; i<invDepth.rows; ++i)
{
const uint16_t * in = invDepth.ptr<uint16_t>(i);
float * out = depth.ptr<float>(i);
for(int j=0; j<invDepth.cols; ++j)
{
out[j] = in[j] ? depthQuantA / (float(in[j]) - depthQuantB) : 0.0f;
}
}
return depth;
}
void invDepthParameters(float depthQuantA, float depthQuantB, float & maxDepth, float & quantization)
{
// inverse of depthQuantA = q*(q+1) and depthQuantB = 1 - depthQuantA/maxDepth
quantization = (std::sqrt(1.0f + 4.0f*depthQuantA) - 1.0f) / 2.0f;
maxDepth = depthQuantA / (1.0f - depthQuantB);
}
} // namespace
// format : ".jpg" ".png" ".rvl" "" (empty is general)
bool parseImageCompressionFormat(const std::string & format, std::string & codec, float & maxDepth, float & quantization)
{
codec.clear();
maxDepth = 0.0f;
quantization = 0.0f;
std::vector<std::string> fields = uListToVector(uSplit(format, ':'));
if(fields.empty())
{
return format.empty(); // empty is general (zlib)
}
if(fields[0].size() < 2 || fields[0][0] != '.')
{
return false;
}
if(fields.size() > 1)
{
// Inverse depth parameters only for formats supporting 16UC1
if((fields[0] != ".png" && fields[0] != ".rvl") || fields.size() > 3)
{
return false;
}
for(size_t i=1; i<fields.size(); ++i)
{
if(!uIsNumber(fields[i]) || uStr2Float(fields[i]) <= 0.0f)
{
return false;
}
}
maxDepth = uStr2Float(fields[1]);
quantization = fields.size() == 3 ? uStr2Float(fields[2]) : kDefaultDepthQuantization;
}
codec = fields[0];
return true;
}
// format : ".jpg" ".png" ".rvl" "" (empty is general), see parseImageCompressionFormat()
CompressionThread::CompressionThread(const cv::Mat & mat, const std::string & format) :
uncompressedData_(mat),
format_(format),
image_(!format.empty()),
compressMode_(true)
{
UASSERT(format.empty() || format.compare(".jpg") == 0 || format.compare(".png") == 0 || format.compare(".rvl") == 0);
std::string codec;
float maxDepth, quantization;
UASSERT_MSG(parseImageCompressionFormat(format, codec, maxDepth, quantization) &&
(codec.empty() || codec == ".jpg" || codec == ".png" || codec == ".rvl"),
uFormat("Invalid compression format \"%s\"", format.c_str()).c_str());
}
// assume image
CompressionThread::CompressionThread(const cv::Mat & bytes, bool isImage) :
@@ -131,21 +248,43 @@ void CompressionThread::mainLoop()
this->kill();
}
// ".jpg" or ".png" or ".rvl"
// ".jpg" or ".png" or ".rvl", with optional ":maxDepth[:quantization]" for 32FC1 depth images
std::vector<unsigned char> compressImage(const cv::Mat & image, const std::string & format)
{
std::vector<unsigned char> bytes;
if(!image.empty())
{
if(image.type() == CV_32FC1)
std::string codec;
float maxDepth, quantization;
if(!parseImageCompressionFormat(format, codec, maxDepth, quantization) || codec.empty())
{
UERROR("Invalid image compression format \"%s\"", format.c_str());
return bytes;
}
if(image.type() == CV_32FC1 && maxDepth > 0.0f)
{
float depthQuantA, depthQuantB;
cv::Mat invDepth = depthToInvDepth(image, maxDepth, quantization, depthQuantA, depthQuantB);
std::vector<unsigned char> invDepthBytes = compressImage(invDepth, codec);
if(!invDepthBytes.empty())
{
bytes.resize(kInvDepthHeaderSize + invDepthBytes.size());
memcpy(&bytes[0], kInvDepthSignature, 8);
memcpy(&bytes[8], &depthQuantA, 4);
memcpy(&bytes[12], &depthQuantB, 4);
memcpy(&bytes[kInvDepthHeaderSize], invDepthBytes.data(), invDepthBytes.size());
}
}
else if(image.type() == CV_32FC1)
{
//save in 8bits-4channel
cv::Mat bgra(image.size(), CV_8UC4, image.data);
cv::imencode(".png", bgra, bytes);
}
else if(format == ".rvl")
else if(codec == ".rvl")
{
bytes = {'D', 'E', 'P', 'T', 'H', 'R', 'V', 'L'};
bytes.assign(kRvlSignature, kRvlSignature+8);
int numPixels = image.rows * image.cols;
// In the worst case, RVL compression results in ~1.5x larger data.
bytes.resize(3 * numPixels + 20);
@@ -154,18 +293,18 @@ std::vector<unsigned char> 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<uint16_t>(), &bytes[16], numPixels);
bytes.resize(16 + compressedSize);
int compressedSize = rvl.CompressRVL(image.ptr<uint16_t>(), &bytes[kRvlHeaderSize], numPixels);
bytes.resize(kRvlHeaderSize + compressedSize);
}
else
{
cv::imencode(format, image, bytes);
cv::imencode(codec, image, bytes);
}
}
return bytes;
}
// ".jpg" or ".png" or ".rvl"
// ".jpg" or ".png" or ".rvl", with optional ":maxDepth[:quantization]" for 32FC1 depth images
cv::Mat compressImage2(const cv::Mat & image, const std::string & format)
{
std::vector<unsigned char> bytes = compressImage(image, format);
@@ -177,25 +316,61 @@ cv::Mat compressImage2(const cv::Mat & image, const std::string & format)
}
cv::Mat uncompressImage(const cv::Mat & bytes)
{
return uncompressImage(bytes.data, bytes.total()*bytes.elemSize());
}
cv::Mat uncompressImage(const std::vector<unsigned char> & bytes)
{
return uncompressImage(bytes.data(), bytes.size());
}
cv::Mat uncompressImage(const unsigned char * bytes, size_t size)
{
cv::Mat image;
if(!bytes.empty())
if(bytes && size)
{
if (compressedDepthFormat(bytes) == ".rvl")
if(hasSignature(bytes, size, kInvDepthSignature))
{
if(size <= kInvDepthHeaderSize)
{
UERROR("Inverse depth image is truncated (%d bytes).", (int)size);
return image;
}
float depthQuantA, depthQuantB;
memcpy(&depthQuantA, &bytes[8], 4);
memcpy(&depthQuantB, &bytes[12], 4);
cv::Mat invDepth = uncompressImage(&bytes[kInvDepthHeaderSize], size - kInvDepthHeaderSize);
if(invDepth.type() == CV_16UC1)
{
image = invDepthToDepth(invDepth, depthQuantA, depthQuantB);
}
else if(!invDepth.empty())
{
UERROR("Inverse depth image should be 16UC1 (type=%d).", invDepth.type());
}
}
else if(hasSignature(bytes, size, kRvlSignature))
{
if(size < kRvlHeaderSize)
{
UERROR("RVL depth image is truncated (%d bytes).", (int)size);
return image;
}
uint32_t cols, rows;
memcpy(&cols, &bytes.data[8], 4);
memcpy(&rows, &bytes.data[12], 4);
memcpy(&cols, &bytes[8], 4);
memcpy(&rows, &bytes[12], 4);
image = cv::Mat(rows, cols, CV_16UC1);
RvlCodec rvl;
rvl.DecompressRVL(&bytes.data[16], image.ptr<uint16_t>(), cols * rows);
rvl.DecompressRVL(&bytes[kRvlHeaderSize], image.ptr<uint16_t>(), cols * rows);
}
else
{
const cv::Mat buf(1, (int)size, CV_8UC1, (void *)bytes);
#if CV_MAJOR_VERSION>2 || (CV_MAJOR_VERSION >=2 && CV_MINOR_VERSION >=4)
image = cv::imdecode(bytes, cv::IMREAD_UNCHANGED);
image = cv::imdecode(buf, cv::IMREAD_UNCHANGED);
#else
image = cv::imdecode(bytes, -1);
image = cv::imdecode(buf, -1);
#endif
if(image.type() == CV_8UC4)
{
@@ -210,36 +385,6 @@ cv::Mat uncompressImage(const cv::Mat & bytes)
return image;
}
cv::Mat uncompressImage(const std::vector<unsigned char> & bytes)
{
cv::Mat image;
if(bytes.size())
{
if (compressedDepthFormat(bytes) == ".rvl")
{
uint32_t cols, rows;
memcpy(&cols, &bytes[8], 4);
memcpy(&rows, &bytes[12], 4);
image = cv::Mat(rows, cols, CV_16UC1);
RvlCodec rvl;
rvl.DecompressRVL(&bytes[16], image.ptr<uint16_t>(), cols * rows);
}
else
{
#if CV_MAJOR_VERSION>2 || (CV_MAJOR_VERSION >=2 && CV_MINOR_VERSION >=4)
image = cv::imdecode(bytes, cv::IMREAD_UNCHANGED);
#else
image = cv::imdecode(bytes, -1);
#endif
if(image.type() == CV_8UC4)
{
image = cv::Mat(image.size(), CV_32FC1, image.data).clone();
}
}
}
return image;
}
std::vector<unsigned char> compressData(const cv::Mat & data)
{
std::vector<unsigned char> bytes;
@@ -381,10 +526,17 @@ std::string compressedDepthFormat(const unsigned char * bytes, size_t size)
std::string format;
if(bytes && size)
{
size_t maxlen = std::min(size, size_t(8));
std::vector<unsigned char> signature(maxlen);
memcpy(&signature[0], bytes, maxlen);
if (std::string(signature.begin(), signature.end()) == "DEPTHRVL")
if(hasSignature(bytes, size, kInvDepthSignature) && size > kInvDepthHeaderSize)
{
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(),
maxDepth, quantization);
}
else if(hasSignature(bytes, size, kRvlSignature))
{
format = ".rvl";
}
+68 -9
View File
@@ -102,6 +102,7 @@ Memory::Memory(const ParametersMap & parameters) :
_imagePreDecimation(Parameters::defaultMemImagePreDecimation()),
_imagePostDecimation(Parameters::defaultMemImagePostDecimation()),
_legacyDecimatedOctave(false),
_inverseDepthCompressionAllowed(true),
_compressionParallelized(Parameters::defaultMemCompressionParallelized()),
_laserScanDownsampleStepSize(Parameters::defaultMemLaserScanDownsampleStepSize()),
_laserScanVoxelSize(Parameters::defaultMemLaserScanVoxelSize()),
@@ -228,6 +229,11 @@ bool Memory::init(const std::string & dbUrl, bool dbOverwritten, const Parameter
// filling it that way; a new one gets the corrected scaling.
_legacyDecimatedOctave =
uStrNumCmp(_dbDriver->getDatabaseVersion(), "0.23.12") < 0;
// Depth images compressed as inverse depth cannot be read before 0.24, which
// would still open databases created with Db/TargetVersion < 0.24 or by an
// older version.
_inverseDepthCompressionAllowed =
uStrNumCmp(_dbDriver->getDatabaseVersion(), "0.24.0") >= 0;
// Only where the descriptors stored in the map end up different: keypoints
// from odometry, scaled into the pre-decimated image before being described.
if(_legacyDecimatedOctave && _useOdometryFeatures && _imagePreDecimation > 1)
@@ -826,6 +832,19 @@ void Memory::parseParameters(const ParametersMap & parameters)
Parameters::parse(params, Parameters::kMemIntermediateNodeDataKept(), _saveIntermediateNodeData);
Parameters::parse(params, Parameters::kMemImageCompressionFormat(), _rgbCompressionFormat);
Parameters::parse(params, Parameters::kMemDepthCompressionFormat(), _depthCompressionFormat);
{
std::string codec;
float maxDepth, quantization;
if(!parseImageCompressionFormat(_depthCompressionFormat, codec, maxDepth, quantization) ||
(codec != ".png" && codec != ".rvl"))
{
UWARN("Invalid %s=\"%s\", using default \"%s\".",
Parameters::kMemDepthCompressionFormat().c_str(),
_depthCompressionFormat.c_str(),
Parameters::defaultMemDepthCompressionFormat().c_str());
_depthCompressionFormat = Parameters::defaultMemDepthCompressionFormat();
}
}
Parameters::parse(params, Parameters::kMemRehearsalIdUpdatedToNewOne(), _idUpdatedToNewOneRehearsal);
Parameters::parse(params, Parameters::kMemGenerateIds(), _generateIds);
Parameters::parse(params, Parameters::kMemBadSignaturesIgnored(), _badSignaturesIgnored);
@@ -6625,6 +6644,44 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor
std::vector<unsigned char> imageBytes;
std::vector<unsigned char> depthBytes;
std::string depthCompressionFormat = _depthCompressionFormat;
bool reuseCompressedDepth =
depthOrRightImage.data == data.depthOrRightRaw().data &&
!data.depthOrRightCompressed().empty();
if(!_inverseDepthCompressionAllowed)
{
std::string codec;
float maxDepth, quantization;
if(parseImageCompressionFormat(depthCompressionFormat, codec, maxDepth, quantization) && maxDepth > 0.0f)
{
static bool warned = false;
if(!warned)
{
UWARN("%s=\"%s\": inverse depth compression format is not compatible with database "
"version %s (requires >= 0.24, see %s), \"%s\" format is used instead. This "
"warning is only printed once.",
Parameters::kMemDepthCompressionFormat().c_str(),
depthCompressionFormat.c_str(),
_dbDriver?_dbDriver->getDatabaseVersion().c_str():"",
Parameters::kDbTargetVersion().c_str(),
codec.c_str());
warned = true;
}
depthCompressionFormat = codec;
}
if(reuseCompressedDepth &&
compressedDepthFormat(data.depthOrRightCompressed()).find(':') != std::string::npos)
{
// Already compressed as inverse depth (e.g., received from ROS's
// compressed_depth_image_transport), re-compress it.
reuseCompressedDepth = false;
if(depthOrRightImage.empty())
{
depthOrRightImage = uncompressImage(data.depthOrRightCompressed());
}
}
}
if(!depthOrRightImage.empty() && depthOrRightImage.type() == CV_32FC1)
{
if(_saveDepth16Format)
@@ -6640,7 +6697,7 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor
}
depthOrRightImage = util2d::cvtDepthFromFloat(depthOrRightImage);
}
else if(_depthCompressionFormat == ".rvl")
else if(depthCompressionFormat == ".rvl")
{
static bool warned = false;
if(!warned)
@@ -6650,13 +6707,16 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor
"images will be compressed in \".png\" format instead. Explicitly "
"set %s to true to keep using \"%s\" format and images will be "
"converted to 16bits for convenience (warning: that would "
"remove all depth values over 65 meters). Explicitly set %s=\".png\" "
"remove all depth values over 65 meters). Set %s=\".rvl:<maxDepth>:<quantization>\" "
"(e.g., \".rvl:10:100\") to compress them in RVL as 16 bits inverse depth "
"(lossy, see parameter's description). Explicitly set %s=\".png\" "
"to suppress this warning. This warning is only printed once.",
Parameters::kMemSaveDepth16Format().c_str(),
Parameters::kMemDepthCompressionFormat().c_str(),
_depthCompressionFormat.c_str(),
depthCompressionFormat.c_str(),
Parameters::kMemSaveDepth16Format().c_str(),
_depthCompressionFormat.c_str(),
depthCompressionFormat.c_str(),
Parameters::kMemDepthCompressionFormat().c_str(),
Parameters::kMemDepthCompressionFormat().c_str());
warned = true;
}
@@ -6666,9 +6726,8 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor
bool reuseCompressedImage =
image.data == data.imageRaw().data &&
!data.imageCompressed().empty();
bool reuseCompressedDepth =
depthOrRightImage.data == data.depthOrRightRaw().data &&
!data.depthOrRightCompressed().empty();
reuseCompressedDepth = reuseCompressedDepth &&
depthOrRightImage.data == data.depthOrRightRaw().data;
bool reuseCompressedDepthConfidence =
depthConfidence.data == data.depthConfidenceRaw().data &&
!data.depthConfidenceCompressed().empty();
@@ -6685,7 +6744,7 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor
if(_compressionParallelized)
{
rtabmap::CompressionThread ctImage(image, _rgbCompressionFormat);
rtabmap::CompressionThread ctDepth(depthOrRightImage, depthOrRightImage.type() == CV_32FC1 || depthOrRightImage.type() == CV_16UC1?_depthCompressionFormat:_rgbCompressionFormat);
rtabmap::CompressionThread ctDepth(depthOrRightImage, depthOrRightImage.type() == CV_32FC1 || depthOrRightImage.type() == CV_16UC1?depthCompressionFormat:_rgbCompressionFormat);
rtabmap::CompressionThread ctDepthConfidence(depthConfidence);
rtabmap::CompressionThread ctLaserScan(laserScan.data());
rtabmap::CompressionThread ctUserData(data.userDataRaw());
@@ -6724,7 +6783,7 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor
else
{
compressedImage = reuseCompressedImage?cv::Mat():compressImage2(image, _rgbCompressionFormat);
compressedDepth = reuseCompressedDepth?cv::Mat():compressImage2(depthOrRightImage, depthOrRightImage.type() == CV_32FC1 || depthOrRightImage.type() == CV_16UC1?_depthCompressionFormat:_rgbCompressionFormat);
compressedDepth = reuseCompressedDepth?cv::Mat():compressImage2(depthOrRightImage, depthOrRightImage.type() == CV_32FC1 || depthOrRightImage.type() == CV_16UC1?depthCompressionFormat:_rgbCompressionFormat);
compressedDepthConfidence = reuseCompressedDepthConfidence?cv::Mat():compressData2(depthConfidence);
compressedScan = reuseCompressedScan?data.laserScanCompressed().data():compressData2(laserScan.data());
compressedUserData = reuseCompressedUserData?data.userDataCompressed():compressData2(data.userDataRaw());
+14
View File
@@ -162,6 +162,20 @@ IF(BUILD_PERF_TESTS)
set_tests_properties(test_graph_perf PROPERTIES
TIMEOUT ${_perf_timeout}
LABELS "performance")
# Comparison of the depth image compression approaches (sizes, times, errors) for
# 16UC1 and 32FC1 depth images: PNG, RVL, zlib, the legacy 4-channel PNG of 32FC1
# images, their conversion to 16UC1 millimeters, and their quantization as 16 bits
# inverse depth (Mem/DepthCompressionFormat=".png:max:q" or ".rvl:max:q"):
# bin/test_compression_perf
# bin/test_compression_perf --gtest_filter=*Synthetic*
add_executable(test_compression_perf perf_compression.cpp)
target_link_libraries(test_compression_perf gtest_main rtabmap_core)
add_test(NAME test_compression_perf COMMAND test_compression_perf)
set_tests_properties(test_compression_perf PROPERTIES
TIMEOUT ${_perf_timeout}
LABELS "performance")
ENDIF(BUILD_PERF_TESTS)
# Rtabmap end-to-end replay of sample DBs (test data fetched by
+342
View File
@@ -0,0 +1,342 @@
// Comparison of the depth image compression approaches of Compression.h, for each
// depth type rtabmap receives:
//
// 16UC1 (millimeters):
// - ".png" lossless, 16 bits grayscale PNG
// - ".rvl" lossless, RVL (Mem/DepthCompressionFormat default)
// - zlib lossless, compressData2(), as a reference
// 32FC1 (meters):
// - ".png" lossless, float bytes as a 4-channel 8 bits PNG (legacy)
// - zlib lossless, compressData2(), as a reference
// - 16UC1 mm + ".png/.rvl" lossy, util2d::cvtDepthFromFloat() then 16 bits codec,
// what Mem/SaveDepth16Format=true does
// - ".png:max:q/.rvl:max:q" lossy, 16 bits quantized inverse depth (same
// quantization than ROS's compressed_depth_image_transport)
//
// over the depth images of data/rgbd/depth (a structured light camera, millimeters),
// the same images converted to meters in 32FC1 (as many drivers publish them), and a
// synthetic 32FC1 image with continuous values, like stereo or lidar projected depth.
//
// Its own executable, run by ctest under the "performance" label, so that its seconds
// of benchmarking stay out of the unit test shards:
// ctest -L performance to run them
// ctest -LE performance to skip them
// bin/test_compression_perf --gtest_filter=*Synthetic*
//
// The times are reported rather than asserted on, as they depend on the machine. What
// is asserted is that the lossless approaches give back the same image, and that the
// lossy ones stay within their error bounds for the depth range they keep.
#include <gtest/gtest.h>
#include <rtabmap/core/Compression.h>
#include <rtabmap/core/util2d.h>
#include <rtabmap/utilite/ULogger.h>
#include <rtabmap/utilite/UConversion.h>
#include <rtabmap/utilite/UTimer.h>
#include <opencv2/imgcodecs.hpp>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <functional>
#include <string>
#include <vector>
using namespace rtabmap;
namespace {
static const int ITERATIONS = 15;
struct Approach
{
std::string name;
std::function<std::vector<unsigned char>(const cv::Mat &)> encode;
std::function<cv::Mat(const std::vector<unsigned char> &)> decode;
bool lossless;
float maxDepth; // meters, lossy approaches only: depth kept under it
float minDepth; // meters, lossy approaches only: depth kept over it
std::function<float(float)> tolerance; // meters, lossy approaches only, for a depth in meters
};
struct Result
{
size_t bytes = 0;
double encodeMs = 0.0;
double decodeMs = 0.0;
double maxError = 0.0; // mm, over the depth range kept
double rmse = 0.0; // mm, over the depth range kept
double lost = 0.0; // % of the valid pixels set to 0
int outOfTolerance = 0; // pixels with an error over the tolerance
};
double median(std::vector<double> v)
{
std::sort(v.begin(), v.end());
return v[v.size()/2];
}
float toMeters(const cv::Mat & depth, int r, int c)
{
return depth.type() == CV_16UC1 ? float(depth.at<uint16_t>(r, c)) * 0.001f : depth.at<float>(r, c);
}
Result run(const cv::Mat & depth, const Approach & approach)
{
Result result;
std::vector<unsigned char> bytes;
cv::Mat restored;
std::vector<double> encodeTimes, decodeTimes;
for(int i=0; i<ITERATIONS; ++i)
{
UTimer timer;
bytes = approach.encode(depth);
encodeTimes.push_back(timer.restart() * 1000.0);
restored = approach.decode(bytes);
decodeTimes.push_back(timer.ticks() * 1000.0);
}
result.bytes = bytes.size();
result.encodeMs = median(encodeTimes);
result.decodeMs = median(decodeTimes);
EXPECT_EQ(restored.size(), depth.size());
EXPECT_EQ(restored.type(), approach.lossless ? depth.type() : restored.type());
if(restored.size() != depth.size())
{
return result;
}
if(approach.lossless)
{
EXPECT_EQ(memcmp(restored.data, depth.data, depth.total()*depth.elemSize()), 0);
return result;
}
int valid = 0, lost = 0, kept = 0;
double sumSq = 0.0;
for(int r=0; r<depth.rows; ++r)
{
for(int c=0; c<depth.cols; ++c)
{
const float d = toMeters(depth, r, c);
if(!(std::isfinite(d) && d > 0.0f))
{
continue;
}
++valid;
const float out = toMeters(restored, r, c);
if(out == 0.0f)
{
++lost;
// Only allowed outside the kept range
if(d >= approach.minDepth && d < approach.maxDepth)
{
++result.outOfTolerance;
}
continue;
}
const double err = std::fabs(out - d);
result.maxError = std::max(result.maxError, err*1000.0);
sumSq += err*err*1e6;
++kept;
if(err > approach.tolerance(d))
{
++result.outOfTolerance;
}
}
}
result.rmse = kept ? std::sqrt(sumSq / kept) : 0.0;
result.lost = valid ? 100.0 * lost / valid : 0.0;
EXPECT_EQ(result.outOfTolerance, 0) << approach.name;
return result;
}
void report(const std::string & title, const cv::Mat & depth, const std::vector<Approach> & approaches)
{
const size_t raw = depth.total() * depth.elemSize();
std::printf("\n%s: %dx%d %s, %zu bytes raw\n", title.c_str(), depth.cols, depth.rows,
depth.type() == CV_16UC1 ? "16UC1" : "32FC1", raw);
std::printf(" %-22s %10s %7s %10s %10s %11s %10s %8s\n",
"approach", "bytes", "ratio", "encode ms", "decode ms", "max err mm", "rmse mm", "lost %");
for(const Approach & approach : approaches)
{
SCOPED_TRACE(title + " " + approach.name);
const Result r = run(depth, approach);
if(approach.lossless)
{
std::printf(" %-22s %10zu %6.1fx %10.2f %10.2f %11s %10s %8s\n",
approach.name.c_str(), r.bytes, double(raw)/double(r.bytes), r.encodeMs, r.decodeMs,
"lossless", "-", "-");
}
else
{
std::printf(" %-22s %10zu %6.1fx %10.2f %10.2f %11.3f %10.3f %8.2f\n",
approach.name.c_str(), r.bytes, double(raw)/double(r.bytes), r.encodeMs, r.decodeMs,
r.maxError, r.rmse, r.lost);
}
}
std::fflush(stdout);
}
std::vector<unsigned char> encode(const cv::Mat & depth, const std::string & format)
{
return compressImage(depth, format);
}
cv::Mat decode(const std::vector<unsigned char> & bytes)
{
return uncompressImage(bytes);
}
std::vector<unsigned char> encodeZlib(const cv::Mat & depth)
{
return compressData(depth);
}
cv::Mat decodeZlib(const std::vector<unsigned char> & bytes)
{
return uncompressData(bytes);
}
std::vector<Approach> approaches16U()
{
using namespace std::placeholders;
return {
{".png", std::bind(encode, _1, ".png"), decode, true, 0, 0, nullptr},
{".rvl", std::bind(encode, _1, ".rvl"), decode, true, 0, 0, nullptr},
{"zlib", encodeZlib, decodeZlib, true, 0, 0, nullptr}};
}
Approach invDepth(const std::string & codec, float maxDepth, float quantization)
{
using namespace std::placeholders;
const float A = quantization * (quantization + 1.0f);
const float B = 1.0f - A / maxDepth;
const std::string format = uFormat("%s:%g:%g", codec.c_str(), maxDepth, quantization);
return {format, std::bind(encode, _1, format), decode, false,
maxDepth,
A / (65535.0f - B) * 1.001f,
[A](float d) { return 0.51f * d * d / A + 1e-6f; }};
}
Approach depth16(const std::string & codec)
{
return {"16UC1 mm + " + codec,
[codec](const cv::Mat & depth) { return compressImage(util2d::cvtDepthFromFloat(depth), codec); },
[](const std::vector<unsigned char> & bytes) { return util2d::cvtDepthToFloat(uncompressImage(bytes)); },
false,
65.535f,
0.0f,
[](float) { return 0.001f + 1e-6f; }}; // truncated to millimeters
}
std::vector<Approach> approaches32F()
{
using namespace std::placeholders;
return {
{".png (legacy RGBA)", std::bind(encode, _1, ".png"), decode, true, 0, 0, nullptr},
{"zlib", encodeZlib, decodeZlib, true, 0, 0, nullptr},
depth16(".png"),
depth16(".rvl"),
invDepth(".png", 10.0f, 100.0f),
invDepth(".rvl", 10.0f, 100.0f),
invDepth(".png", 40.0f, 100.0f),
invDepth(".rvl", 40.0f, 100.0f),
invDepth(".rvl", 40.0f, 200.0f)};
}
std::vector<cv::Mat> loadSampleDepths()
{
std::vector<cv::Mat> depths;
for(const std::string & name : {"17.png", "154.png"})
{
const std::string path = std::string(RTABMAP_TEST_DATA_ROOT) + "/rgbd/depth/" + name;
cv::Mat depth = cv::imread(path, cv::IMREAD_UNCHANGED);
if(depth.type() == CV_16UC1)
{
depths.push_back(depth);
}
else
{
std::printf("Cannot load 16UC1 depth image \"%s\", skipped.\n", path.c_str());
}
}
return depths;
}
// Ground plane, walls and boxes seen by a 640x480 camera, with continuous
// values up to ~35 m, noise growing with depth (as stereo) and holes.
cv::Mat makeSyntheticDepth(int cols = 640, int rows = 480)
{
cv::RNG rng(42);
const float fx = 0.75f * cols, cx = cols / 2.0f, cy = rows / 2.0f;
const float cameraHeight = 1.0f;
cv::Mat depth(rows, cols, CV_32FC1);
for(int v=0; v<rows; ++v)
{
for(int u=0; u<cols; ++u)
{
const float x = (u - cx) / fx; // ray direction, z = 1
const float y = (v - cy) / fx;
float d = 35.0f; // far wall
if(y > 0.0f)
{
d = std::min(d, cameraHeight / y); // ground
}
if(x < 0.0f)
{
d = std::min(d, 3.0f / -x); // left wall, 3 m away
}
// boxes
if(x > 0.05f && x < 0.25f && y > -0.1f && y < cameraHeight / 2.5f)
{
d = std::min(d, 2.5f - 1.5f * x);
}
if(x > -0.35f && x < -0.15f && y > -0.2f && y < cameraHeight / 12.0f)
{
d = std::min(d, 12.0f);
}
d += (float)rng.gaussian(0.002 * d * d); // stereo-like noise
depth.at<float>(v, u) = d;
}
}
// Holes
for(int i=0; i<40; ++i)
{
const int u = rng.uniform(0, cols - 20), v = rng.uniform(0, rows - 20);
depth(cv::Rect(u, v, rng.uniform(2, 20), rng.uniform(2, 20))).setTo(0.0f);
}
return depth;
}
} // namespace
TEST(CompressionPerf, SampleDepth16UC1)
{
const std::vector<cv::Mat> depths = loadSampleDepths();
if(depths.empty())
{
GTEST_SKIP() << "No sample depth images in " << RTABMAP_TEST_DATA_ROOT << "/rgbd/depth";
}
for(size_t i=0; i<depths.size(); ++i)
{
report(uFormat("Sample depth %d", (int)i), depths[i], approaches16U());
}
}
TEST(CompressionPerf, SampleDepth32FC1)
{
const std::vector<cv::Mat> depths = loadSampleDepths();
if(depths.empty())
{
GTEST_SKIP() << "No sample depth images in " << RTABMAP_TEST_DATA_ROOT << "/rgbd/depth";
}
for(size_t i=0; i<depths.size(); ++i)
{
report(uFormat("Sample depth %d in meters", (int)i), util2d::cvtDepthToFloat(depths[i]), approaches32F());
}
}
TEST(CompressionPerf, Synthetic32FC1)
{
report("Synthetic continuous depth", makeSyntheticDepth(), approaches32F());
report("Synthetic continuous depth HD", makeSyntheticDepth(1280, 720), approaches32F());
}
+197
View File
@@ -1,6 +1,8 @@
#include <gtest/gtest.h>
#include <rtabmap/core/Compression.h>
#include <opencv2/core.hpp>
#include <cstring>
#include <limits>
using namespace rtabmap;
@@ -183,3 +185,198 @@ TEST(CompressionTest, CompressionThreadDataRoundTrip)
expectMatEqual(uncompressThread.getUncompressedData(), data);
}
namespace {
// 32FC1 depth image covering [minDepth, maxDepth[ with sub-millimeter values,
// and the invalid values of the inverse depth format on the first row.
cv::Mat makeFloatDepth(int rows, int cols, float minDepth, float maxDepth)
{
cv::Mat depth(rows, cols, CV_32FC1);
for(int r = 0; r < rows; ++r)
{
for(int c = 0; c < cols; ++c)
{
depth.at<float>(r, c) = minDepth + (maxDepth - minDepth) * float(r * cols + c) / float(rows * cols);
}
}
return depth;
}
// Error bound of the inverse depth format: half a quantization step.
float invDepthTolerance(float d, float quantization)
{
// (with some margin for the float rounding of A/d + B, up to ~66000)
return 0.51f * d * d / (quantization * (quantization + 1.0f)) + 1e-6f;
}
} // namespace
TEST(CompressionTest, ParseImageCompressionFormat)
{
std::string codec;
float maxDepth, quantization;
EXPECT_TRUE(parseImageCompressionFormat("", codec, maxDepth, quantization));
EXPECT_TRUE(codec.empty());
EXPECT_EQ(maxDepth, 0.0f);
EXPECT_TRUE(parseImageCompressionFormat(".jpg", codec, maxDepth, quantization));
EXPECT_EQ(codec, ".jpg");
EXPECT_EQ(maxDepth, 0.0f);
EXPECT_EQ(quantization, 0.0f);
EXPECT_TRUE(parseImageCompressionFormat(".rvl", codec, maxDepth, quantization));
EXPECT_EQ(codec, ".rvl");
EXPECT_EQ(maxDepth, 0.0f);
EXPECT_TRUE(parseImageCompressionFormat(".png:20", codec, maxDepth, quantization));
EXPECT_EQ(codec, ".png");
EXPECT_FLOAT_EQ(maxDepth, 20.0f);
EXPECT_FLOAT_EQ(quantization, 100.0f);
EXPECT_TRUE(parseImageCompressionFormat(".rvl:10.5:50", codec, maxDepth, quantization));
EXPECT_EQ(codec, ".rvl");
EXPECT_FLOAT_EQ(maxDepth, 10.5f);
EXPECT_FLOAT_EQ(quantization, 50.0f);
EXPECT_FALSE(parseImageCompressionFormat("png", codec, maxDepth, quantization));
EXPECT_FALSE(parseImageCompressionFormat(".jpg:10:100", codec, maxDepth, quantization));
EXPECT_FALSE(parseImageCompressionFormat(".png:abc", codec, maxDepth, quantization));
EXPECT_FALSE(parseImageCompressionFormat(".png:0:100", codec, maxDepth, quantization));
EXPECT_FALSE(parseImageCompressionFormat(".png:-10:100", codec, maxDepth, quantization));
EXPECT_FALSE(parseImageCompressionFormat(".png:10:0", codec, maxDepth, quantization));
EXPECT_FALSE(parseImageCompressionFormat(".png:10:100:1", codec, maxDepth, quantization));
}
TEST(CompressionTest, InvalidFormatReturnsEmpty)
{
const cv::Mat depth = makeFloatDepth(4, 4, 1.0f, 2.0f);
EXPECT_TRUE(compressImage(depth, ".jpg:10").empty());
EXPECT_TRUE(compressImage(depth, ".png:x").empty());
}
TEST(CompressionTest, InverseDepthRoundTrip)
{
const float maxDepth = 10.0f;
const float quantization = 100.0f;
const float minDepth = quantization * (quantization + 1.0f) / (65535.0f + quantization * (quantization + 1.0f) / maxDepth);
cv::Mat depth = makeFloatDepth(48, 64, minDepth * 1.001f, maxDepth * 0.999f);
const float invalid[] = {
0.0f, -1.0f, maxDepth, maxDepth * 2.0f, minDepth * 0.9f,
std::numeric_limits<float>::quiet_NaN(),
std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity()};
const int nInvalid = sizeof(invalid) / sizeof(float);
for(int i = 0; i < nInvalid; ++i)
{
depth.at<float>(0, i) = invalid[i];
}
for(const std::string codec : {".png", ".rvl"})
{
SCOPED_TRACE(codec);
const std::string format = codec + ":10:100";
const std::vector<unsigned char> bytes = compressImage(depth, format);
ASSERT_FALSE(bytes.empty());
EXPECT_LT(bytes.size(), depth.total() * depth.elemSize() / 2);
EXPECT_EQ(compressedDepthFormat(bytes), format);
const cv::Mat restored = uncompressImage(bytes);
ASSERT_EQ(restored.type(), CV_32FC1);
ASSERT_EQ(restored.size(), depth.size());
for(int r = 0; r < depth.rows; ++r)
{
for(int c = 0; c < depth.cols; ++c)
{
const float d = depth.at<float>(r, c);
if(r == 0 && c < nInvalid)
{
EXPECT_EQ(restored.at<float>(r, c), 0.0f) << "input=" << d;
}
else
{
ASSERT_NEAR(restored.at<float>(r, c), d, invDepthTolerance(d, quantization)) << "r=" << r << " c=" << c;
}
}
}
// Re-compressing with the detected format gives back the same bytes
// (e.g., DatabaseViewer saving an edited depth image).
EXPECT_EQ(compressImage(restored, compressedDepthFormat(bytes)), compressImage(restored, format));
// Same through cv::Mat and thread overloads
CompressionThread compressThread(depth, format);
compressThread.start();
compressThread.join();
const cv::Mat bytesMat = compressThread.getCompressedData();
ASSERT_EQ(bytesMat.total(), bytes.size());
EXPECT_EQ(memcmp(bytesMat.data, bytes.data(), bytes.size()), 0);
CompressionThread uncompressThread(bytesMat, true);
uncompressThread.start();
uncompressThread.join();
expectMatEqual(uncompressThread.getUncompressedData(), restored);
}
}
TEST(CompressionTest, InverseDepthQuantizationParameters)
{
const cv::Mat depth = makeFloatDepth(32, 32, 1.0f, 39.0f);
const std::vector<unsigned char> bytes = compressImage(depth, ".png:40:50");
EXPECT_EQ(compressedDepthFormat(bytes), ".png:40:50");
const cv::Mat restored = uncompressImage(bytes);
ASSERT_EQ(restored.type(), CV_32FC1);
for(int r = 0; r < depth.rows; ++r)
{
for(int c = 0; c < depth.cols; ++c)
{
const float d = depth.at<float>(r, c);
ASSERT_NEAR(restored.at<float>(r, c), d, invDepthTolerance(d, 50.0f));
}
}
}
TEST(CompressionTest, InverseDepthNonContinuousImage)
{
const cv::Mat depth = makeFloatDepth(20, 30, 1.0f, 5.0f);
const cv::Mat roi = depth(cv::Rect(3, 2, 10, 8));
ASSERT_FALSE(roi.isContinuous());
const cv::Mat restored = uncompressImage(compressImage(roi, ".rvl:10:100"));
ASSERT_EQ(restored.size(), roi.size());
for(int r = 0; r < roi.rows; ++r)
{
for(int c = 0; c < roi.cols; ++c)
{
const float d = roi.at<float>(r, c);
ASSERT_NEAR(restored.at<float>(r, c), d, invDepthTolerance(d, 100.0f));
}
}
}
TEST(CompressionTest, DepthParametersIgnoredFor16UC1)
{
cv::Mat depth(24, 32, CV_16UC1);
cv::randu(depth, 0, 20000); // includes values over the max depth below
for(const std::string codec : {".png", ".rvl"})
{
SCOPED_TRACE(codec);
const std::vector<unsigned char> bytes = compressImage(depth, codec + ":10:100");
EXPECT_EQ(bytes, compressImage(depth, codec));
EXPECT_EQ(compressedDepthFormat(bytes), codec);
expectMatEqual(uncompressImage(bytes), depth);
}
}
TEST(CompressionTest, LegacyFloatDepthIsLossless)
{
const cv::Mat depth = makeFloatDepth(16, 16, 0.01f, 100.0f);
for(const std::string format : {".png", ".rvl"})
{
SCOPED_TRACE(format);
const std::vector<unsigned char> bytes = compressImage(depth, format);
EXPECT_EQ(compressedDepthFormat(bytes), ".png");
const cv::Mat restored = uncompressImage(bytes);
ASSERT_EQ(restored.type(), CV_32FC1);
EXPECT_EQ(memcmp(restored.data, depth.data, depth.total() * depth.elemSize()), 0);
}
}
+71
View File
@@ -4681,3 +4681,74 @@ TEST(MemoryTest, CreateSignatureRecompressesStereoPairAfterRectification)
EXPECT_GT(cv::countNonZero(uncompressImage(stored.depthOrRightCompressed()) != right), 0)
<< "stored right image still holds the unrectified pixels";
}
// ---------------------------------------------------------------------------
// Mem/DepthCompressionFormat with inverse depth (".rvl:max:q"), which databases
// older than 0.24 cannot hold: rtabmap 0.23 would still open them (e.g., created
// with Db/TargetVersion=0.23.0) but could not decode their depth images.
// ---------------------------------------------------------------------------
namespace {
struct InverseDepthCase
{
const char * targetVersion;
bool precompressed; // depth already compressed as inverse depth, e.g., from ROS
const char * expectedFormat;
};
class MemoryInverseDepthTest : public ::testing::TestWithParam<InverseDepthCase> {};
} // namespace
TEST_P(MemoryInverseDepthTest, StoredDepthFormatFollowsDatabaseVersion)
{
const InverseDepthCase & cs = GetParam();
ParametersMap params = defaultMemoryParams();
params[Parameters::kMemBinDataKept()] = "true";
params[Parameters::kMemDepthCompressionFormat()] = ".rvl:10:100";
params[Parameters::kDbTargetVersion()] = cs.targetVersion;
Memory memory(params);
const std::string dbPath = uniqueDbPath();
ASSERT_TRUE(memory.init(dbPath, true, params));
const cv::Mat rgb(16, 16, CV_8UC3, cv::Scalar(10, 20, 30));
cv::Mat depth(16, 16, CV_32FC1);
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)
{
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());
}
else
{
data = SensorData(rgb, depth, model);
}
ASSERT_TRUE(memory.update(data, Transform(0, 0, 0, 0, 0, 0), cv::Mat::eye(6, 6, CV_64FC1) * 0.01));
const Signature * s = memory.getSignature(memory.getLastSignatureId());
ASSERT_NE(s, nullptr);
const cv::Mat & stored = s->sensorData().depthOrRightCompressed();
ASSERT_FALSE(stored.empty());
EXPECT_EQ(compressedDepthFormat(stored), cs.expectedFormat);
const cv::Mat restored = uncompressImage(stored);
ASSERT_EQ(restored.type(), CV_32FC1);
ASSERT_EQ(restored.size(), depth.size());
EXPECT_LT(cv::norm(restored, depth, cv::NORM_INF), 0.01);
memory.close(false);
UFile::erase(dbPath);
}
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
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<package format="2">
<name>rtabmap</name>
<version>0.23.13</version>
<version>0.24.0</version>
<description>RTAB-Map's standalone library. RTAB-Map is a RGB-D SLAM approach with real-time constraints.</description>
<maintainer email="[email protected]">Mathieu Labbe</maintainer>
<author>Mathieu Labbe</author>