// 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 #include #include #include #include #include #include #include #include #include #include #include #include using namespace rtabmap; namespace { static const int ITERATIONS = 15; struct Approach { std::string name; std::function(const cv::Mat &)> encode; std::function &)> 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 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 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(r, c)) * 0.001f : depth.at(r, c); } Result run(const cv::Mat & depth, const Approach & approach) { Result result; std::vector bytes; cv::Mat restored; std::vector encodeTimes, decodeTimes; for(int i=0; i 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 & 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 encode(const cv::Mat & depth, const std::string & format) { return compressImage(depth, format); } cv::Mat decode(const std::vector & bytes) { return uncompressImage(bytes); } std::vector encodeZlib(const cv::Mat & depth) { return compressData(depth); } cv::Mat decodeZlib(const std::vector & bytes) { return uncompressData(bytes); } std::vector 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 & bytes) { return util2d::cvtDepthToFloat(uncompressImage(bytes)); }, false, 65.535f, 0.0f, [](float) { return 0.001f + 1e-6f; }}; // truncated to millimeters } std::vector 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 loadSampleDepths() { std::vector 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 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(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 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 = loadSampleDepths(); if(depths.empty()) { GTEST_SKIP() << "No sample depth images in " << RTABMAP_TEST_DATA_ROOT << "/rgbd/depth"; } for(size_t i=0; i