/********************************************************************* * * Software License Agreement (BSD License) * * Copyright (c) 2015-2016, Jiri Horner. * Copyright (c) 2022, Carlos Alvarez. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * Neither the name of the Jiri Horner nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * *********************************************************************/ #include #include // #include #include #include "testing_helpers.h" #define private public #include const std::array hector_maps = { "map00.pgm", "map05.pgm", }; const std::array gmapping_maps = { "2011-08-09-12-22-52.pgm", "2012-01-28-11-12-01.pgm", }; constexpr bool verbose_tests = false; #define EXPECT_VALID_GRID(grid) \ ASSERT_TRUE(static_cast(grid)); \ EXPECT_TRUE(consistentData(*grid)); \ EXPECT_GT(grid->info.resolution, 0); \ EXPECT_TRUE(isIdentity(grid->info.origin.orientation)) TEST(MergingPipeline, canStich0Grid) { std::vector maps; combine_grids::MergingPipeline merger; merger.feed(maps.begin(), maps.end()); EXPECT_TRUE(merger.estimateTransforms()); EXPECT_EQ(merger.composeGrids(), nullptr); EXPECT_EQ(merger.getTransforms().size(), (long unsigned int) 0); } TEST(MergingPipeline, canStich1Grid) { auto map = loadMap(hector_maps[1]); combine_grids::MergingPipeline merger; merger.feed(&map, &map + 1); merger.estimateTransforms(); auto merged_grid = merger.composeGrids(); EXPECT_VALID_GRID(merged_grid); // don't use EXPECT_EQ, since it prints too much info EXPECT_TRUE(maps_equal(*merged_grid, *map)); // check estimated transforms auto transforms = merger.getTransforms(); EXPECT_EQ(transforms.size(), (long unsigned int) 1); EXPECT_TRUE(isIdentity(transforms[0])); } TEST(MergingPipeline, canStich2Grids) { auto maps = loadMaps(hector_maps.begin(), hector_maps.end()); combine_grids::MergingPipeline merger; merger.feed(maps.begin(), maps.end()); merger.estimateTransforms(); auto merged_grid = merger.composeGrids(); EXPECT_VALID_GRID(merged_grid); // grid size should indicate sucessful merge EXPECT_NEAR(2091, merged_grid->info.width, 30); EXPECT_NEAR(2091, merged_grid->info.height, 30); if (verbose_tests) { saveMap("test_canStich2Grids.pgm", merged_grid); } } TEST(MergingPipeline, canStichGridsGmapping) { auto maps = loadMaps(gmapping_maps.begin(), gmapping_maps.end()); combine_grids::MergingPipeline merger; merger.feed(maps.begin(), maps.end()); merger.estimateTransforms(); auto merged_grid = merger.composeGrids(); EXPECT_VALID_GRID(merged_grid); // grid size should indicate sucessful merge EXPECT_NEAR(5427, merged_grid->info.width, 30); EXPECT_NEAR(5427, merged_grid->info.height, 30); if (verbose_tests) { saveMap("canStichGridsGmapping.pgm", merged_grid); } } TEST(MergingPipeline, estimationAccuracy) { // for this test we measure estimation on the same grid artificially // transformed double angle = 0.523599 /* 30 deg in rads*/; // TODO test also translations double tx = 0; double ty = 0; cv::Matx23d transform{std::cos(angle), -std::sin(angle), tx, std::sin(angle), std::cos(angle), ty}; auto map = loadMap(hector_maps[1]); combine_grids::MergingPipeline merger; merger.feed(&map, &map + 1); // warp the map with Affine Transform combine_grids::internal::GridWarper warper; cv::Mat warped; auto roi = warper.warp(merger.images_[0], cv::Mat(transform), warped); // add warped map // this relies on internal implementation of merging pipeline merger.grids_.emplace_back(); merger.images_.push_back(warped); merger.estimateTransforms(); auto merged_grid = merger.composeGrids(); EXPECT_VALID_GRID(merged_grid); // transforms auto transforms = merger.getTransforms(); EXPECT_EQ(transforms.size(), (long unsigned int) 2); EXPECT_TRUE(isIdentity(transforms[0])); tf2::Transform t; tf2::fromMsg(transforms[1], t); EXPECT_NEAR(angle, t.getRotation().getAngle(), 1e-2); EXPECT_NEAR(tx - roi.tl().x, t.getOrigin().x(), 2); EXPECT_NEAR(ty - roi.tl().y, t.getOrigin().y(), 2); } TEST(MergingPipeline, transformsRoundTrip) { auto map = loadMap(hector_maps[0]); combine_grids::MergingPipeline merger; merger.feed(&map, &map + 1); for (size_t i = 0; i < 1000; ++i) { auto in_transform = randomTransform(); merger.setTransforms(&in_transform, &in_transform + 1); auto out_transforms = merger.getTransforms(); ASSERT_EQ(out_transforms.size(), (long unsigned int) 1); auto out_transform = out_transforms[0]; EXPECT_FLOAT_EQ(in_transform.translation.x, out_transform.translation.x); EXPECT_FLOAT_EQ(in_transform.translation.y, out_transform.translation.y); EXPECT_FLOAT_EQ(in_transform.translation.z, out_transform.translation.z); EXPECT_FLOAT_EQ(in_transform.rotation.x, out_transform.rotation.x); EXPECT_FLOAT_EQ(in_transform.rotation.y, out_transform.rotation.y); EXPECT_FLOAT_EQ(in_transform.rotation.z, out_transform.rotation.z); EXPECT_FLOAT_EQ(in_transform.rotation.w, out_transform.rotation.w); } } TEST(MergingPipeline, setTransformsInternal) { auto map = loadMap(hector_maps[0]); combine_grids::MergingPipeline merger; merger.feed(&map, &map + 1); for (size_t i = 0; i < 1000; ++i) { auto transform = randomTransform(); merger.setTransforms(&transform, &transform + 1); ASSERT_EQ(merger.transforms_.size(), (long unsigned int) 1); auto& transform_internal = merger.transforms_[0]; // verify that transforms are the same in 2D tf2::Vector3 a[2] = {{1., 0., 1.}, {0., 1., 1.}}; cv::Point3d b[2] = {{1., 0., 1.}, {0., 1., 1.}}; for (auto j : {0, 1}) { tf2::Transform t; fromMsg(transform, t); auto p1 = t * a[j]; cv::Mat p2 = transform_internal * cv::Mat(b[j]); // some precision is naturally lost during conversion, float precision is // still good for us EXPECT_FLOAT_EQ(p1.x(), p2.at(0)); EXPECT_FLOAT_EQ(p1.y(), p2.at(1)); } } } TEST(MergingPipeline, getTransformsInternal) { auto map = loadMap(hector_maps[0]); combine_grids::MergingPipeline merger; merger.feed(&map, &map + 1); // set internal transform merger.transforms_.resize(1); for (size_t i = 0; i < 1000; ++i) { cv::Mat transform_internal = randomTransformMatrix(); merger.transforms_[0] = transform_internal; auto transforms = merger.getTransforms(); ASSERT_EQ(transforms.size(), (long unsigned int) 1); // output quaternion should be normalized auto& q = transforms[0].rotation; EXPECT_DOUBLE_EQ(1., q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w); // verify that transforms are the same in 2D tf2::Transform transform; fromMsg(transforms[0], transform); tf2::Vector3 a[2] = {{1., 0., 1.}, {0., 1., 1.}}; cv::Point3d b[2] = {{1., 0., 1.}, {0., 1., 1.}}; for (auto j : {0, 1}) { auto p1 = transform * a[j]; cv::Mat p2 = transform_internal * cv::Mat(b[j]); EXPECT_FLOAT_EQ(p1.x(), p2.at(0)); EXPECT_FLOAT_EQ(p1.y(), p2.at(1)); } } } TEST(MergingPipeline, setEmptyTransforms) { constexpr size_t size = 2; std::vector maps(size); std::vector transforms(size); combine_grids::MergingPipeline merger; merger.feed(maps.begin(), maps.end()); merger.setTransforms(transforms.begin(), transforms.end()); EXPECT_EQ(merger.composeGrids(), nullptr); EXPECT_EQ(merger.getTransforms().size(), size); } /* empty image may end with identity transform. */ TEST(MergingPipeline, emptyImageWithTransform) { constexpr size_t size = 1; std::vector maps(size); std::vector transforms(size); transforms[0].rotation.z = 1; // set transform to identity combine_grids::MergingPipeline merger; merger.feed(maps.begin(), maps.end()); merger.setTransforms(transforms.begin(), transforms.end()); EXPECT_EQ(merger.composeGrids(), nullptr); EXPECT_EQ(merger.getTransforms().size(), size); } /* one image may be empty */ TEST(MergingPipeline, oneEmptyImage) { std::vector maps{nullptr, loadMap(gmapping_maps[0])}; combine_grids::MergingPipeline merger; merger.feed(maps.begin(), maps.end()); merger.estimateTransforms(); auto merged_grid = merger.composeGrids(); auto transforms = merger.getTransforms(); EXPECT_VALID_GRID(merged_grid); // don't use EXPECT_EQ, since it prints too much info EXPECT_TRUE(maps_equal(*merged_grid, *maps[1])); // transforms EXPECT_EQ(transforms.size(), (long unsigned int) 2); EXPECT_TRUE(isIdentity(transforms[1])); } // non-identity known positions etc. TEST(MergingPipeline, knownInitPositions) { auto maps = loadMaps(gmapping_maps.begin(), gmapping_maps.end()); combine_grids::MergingPipeline merger; merger.feed(maps.begin(), maps.end()); for (size_t i = 0; i < 5; ++i) { std::vector transforms{randomTransform(), randomTransform()}; merger.setTransforms(transforms.begin(), transforms.end()); auto merged_grid = merger.composeGrids(); EXPECT_VALID_GRID(merged_grid); } } int main(int argc, char** argv) { // ros::Time::init(); // if (verbose_tests && // ros::console::set_logger_level(ROSCONSOLE_DEFAULT_NAME, // ros::console::levels::Debug)) { // ros::console::notifyLoggerLevelsChanged(); // } testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }