diff --git a/source/armarx/navigation/components/dynamic_scene_provider/CMakeLists.txt b/source/armarx/navigation/components/dynamic_scene_provider/CMakeLists.txt index e061ea23b64b4980fe421fd0af0077b6b4fe727b..52e4efdccb9d044cc6262f8953838d00e53b3043 100644 --- a/source/armarx/navigation/components/dynamic_scene_provider/CMakeLists.txt +++ b/source/armarx/navigation/components/dynamic_scene_provider/CMakeLists.txt @@ -9,11 +9,9 @@ armarx_add_component(dynamic_scene_provider SOURCES Component.cpp ArVizDrawer.cpp - HumanTracker.cpp HEADERS Component.h ArVizDrawer.h - HumanTracker.h DEPENDENCIES # ArmarXCore ArmarXCore diff --git a/source/armarx/navigation/components/dynamic_scene_provider/Component.cpp b/source/armarx/navigation/components/dynamic_scene_provider/Component.cpp index 23a58b26105a5987313dd793449c41e83f2c9ceb..29c23bbf88e03ed5332dcdfb5bab22317080e7a3 100644 --- a/source/armarx/navigation/components/dynamic_scene_provider/Component.cpp +++ b/source/armarx/navigation/components/dynamic_scene_provider/Component.cpp @@ -76,18 +76,23 @@ namespace armarx::navigation::components::dynamic_scene_provider // def->required(properties.boxLayerName, "p.box.LayerName", "Name of the box layer in ArViz."); // Add an optionalproperty. - def->optional(properties.taskPeriodMs, "p.taskPeriodMs", "Update rate of the running task."); - - def->optional(properties.laserScannerFeatures.providerName, "p.laserScannerFeatures.providerName", ""); + def->optional( + properties.taskPeriodMs, "p.taskPeriodMs", "Update rate of the running task."); + + def->optional(properties.laserScannerFeatures.providerName, + "p.laserScannerFeatures.providerName", + ""); def->optional(properties.laserScannerFeatures.name, "p.laserScannerFeatures.name", ""); - + def->optional(properties.robot.name, "p.robot.name", ""); - + def->optional(properties.occupancyGrid.providerName, "p.occupancyGrid.providerName", ""); def->optional(properties.occupancyGrid.name, "p.occupancyGrid.name", ""); - def->optional(properties.occupancyGrid.freespaceThreshold, "p.occupancyGrid.freespaceThreshold", ""); - def->optional(properties.occupancyGrid.occupiedThreshold, "p.occupancyGrid.occupiedThreshold", ""); - + def->optional( + properties.occupancyGrid.freespaceThreshold, "p.occupancyGrid.freespaceThreshold", ""); + def->optional( + properties.occupancyGrid.occupiedThreshold, "p.occupancyGrid.occupiedThreshold", ""); + def->optional(properties.humanPoseProvider, "p.humanPoseProvider", ""); return def; @@ -137,10 +142,10 @@ namespace armarx::navigation::components::dynamic_scene_provider } */ - while(true) + while (true) { robot = virtualRobotReaderPlugin->get().getRobot(properties.robot.name); - if(robot != nullptr) + if (robot != nullptr) { break; } @@ -205,12 +210,15 @@ namespace armarx::navigation::components::dynamic_scene_provider ARMARX_INFO << "Querying humans"; - const armem::human::client::Reader::Query humanPoseQuery{.providerName = properties.humanPoseProvider, - .timestamp = timestamp, .maxAge = Duration::MilliSeconds(500)}; + const armem::human::client::Reader::Query humanPoseQuery{ + .providerName = properties.humanPoseProvider, + .timestamp = timestamp, + .maxAge = Duration::MilliSeconds(500)}; const armem::human::client::Reader::Result humanPoseResult = humanPoseReaderPlugin->get().query(humanPoseQuery); - ARMARX_CHECK_EQUAL(humanPoseResult.status, armem::human::client::Reader::Result::Success) << humanPoseResult.errorMessage; + ARMARX_CHECK_EQUAL(humanPoseResult.status, armem::human::client::Reader::Result::Success) + << humanPoseResult.errorMessage; ARMARX_INFO << humanPoseResult.humanPoses.size() << " humans in the scene."; @@ -265,10 +273,10 @@ namespace armarx::navigation::components::dynamic_scene_provider ARMARX_INFO << "Querying costmap"; - const memory::client::costmap::Reader::Query costmapQuery{.providerName = - "distance_to_obstacle_costmap_provider", // TODO check - .name = "distance_to_obstacles", - .timestamp = timestamp}; + const memory::client::costmap::Reader::Query costmapQuery{ + .providerName = "distance_to_obstacle_costmap_provider", // TODO check + .name = "distance_to_obstacles", + .timestamp = timestamp}; const memory::client::costmap::Reader::Result costmapResult = costmapReaderPlugin->get().query(costmapQuery); @@ -343,11 +351,11 @@ namespace armarx::navigation::components::dynamic_scene_provider ARMARX_INFO << "Running human tracker"; - humanTracker.update(HumanTracker::Measurements{.detectionTime = timestamp, - .humanPoses = humanPoseResult.humanPoses}); + humanTracker.update(human::HumanTracker::Measurements{ + .detectionTime = timestamp, .humanPoses = humanPoseResult.humanPoses}); - humanWriterPlugin->get().store(humanTracker.getTrackedHumans(),getName(), timestamp); + humanWriterPlugin->get().store(humanTracker.getTrackedHumans(), getName(), timestamp); } diff --git a/source/armarx/navigation/components/dynamic_scene_provider/Component.h b/source/armarx/navigation/components/dynamic_scene_provider/Component.h index bcc3ad7d5f8163380eeed9a5deb65429f6c9cd58..7d58e1bd8fc95277021458498e9d993132d8e522 100644 --- a/source/armarx/navigation/components/dynamic_scene_provider/Component.h +++ b/source/armarx/navigation/components/dynamic_scene_provider/Component.h @@ -47,7 +47,7 @@ #include <RobotAPI/libraries/RobotAPIComponentPlugins/ArVizComponentPlugin.h> #include "armarx/navigation/components/dynamic_scene_provider/ArVizDrawer.h" -#include "armarx/navigation/components/dynamic_scene_provider/HumanTracker.h" +#include "armarx/navigation/human/HumanTracker.h" #include "armarx/navigation/memory/client/costmap/Reader.h" #include "armarx/navigation/memory/client/human/Writer.h" #include <armarx/navigation/components/dynamic_scene_provider/ComponentInterface.h> @@ -205,7 +205,7 @@ namespace armarx::navigation::components::dynamic_scene_provider ReaderWriterPlugin<memory::client::human::Writer>* humanWriterPlugin = nullptr; - HumanTracker humanTracker; + human::HumanTracker humanTracker; }; } // namespace armarx::navigation::components::dynamic_scene_provider diff --git a/source/armarx/navigation/components/dynamic_scene_provider/HumanTracker.cpp b/source/armarx/navigation/components/dynamic_scene_provider/HumanTracker.cpp deleted file mode 100644 index 197d76e06377bef4efdf5782e7bac6552a318153..0000000000000000000000000000000000000000 --- a/source/armarx/navigation/components/dynamic_scene_provider/HumanTracker.cpp +++ /dev/null @@ -1,203 +0,0 @@ -#include "HumanTracker.h" - -#include "ArmarXCore/core/exceptions/local/ExpressionException.h" - -#include "armarx/navigation/human/types.h" -#include <armarx/navigation/conversions/eigen.h> -#include <range/v3/range/conversion.hpp> -#include <range/v3/view/transform.hpp> - -namespace armarx::navigation::components::dynamic_scene_provider -{ - - - HumanTracker::DetectedHuman - convertHumanPoseToPosition(const DateTime& time, const armem::human::HumanPose& humanPose) - { - const std::map<std::string, armem::human::PoseKeypoint>& keypoints = humanPose.keypoints; - ARMARX_CHECK_NOT_EMPTY(keypoints); - - Eigen::Vector3f centerPos; - int size = 0; - for (const auto& [_, v] : keypoints) - { - if (v.positionGlobal.has_value()) - { - centerPos += v.positionGlobal.value().toEigen(); - size++; - } - } - centerPos /= size; - - core::Pose2D pose = core::Pose2D::Identity(); - pose.translation() = conv::to2D(centerPos); - //TODO: angle - pose.linear() = Eigen::Rotation2Df(0).toRotationMatrix(); - - return {pose, humanPose.humanTrackingId, time, false}; - } - - void - HumanTracker::update(const Measurements& measurements) - { - for (auto it = trackedHumans.begin(); it != trackedHumans.end();) - { - auto& human = *it; - if ((measurements.detectionTime - human.human.detectionTime) >= - parameters.maxTrackingAge) - { - it = trackedHumans.erase(it); - } - else - { - human.associated = false; - human.human.pose = human.human.estimateAt(measurements.detectionTime); - it++; - } - } - - std::vector<DetectedHuman> newPoses = - measurements.humanPoses | - ranges::views::transform( - [measurements](const armem::human::HumanPose& humanPose) -> DetectedHuman - { return convertHumanPoseToPosition(measurements.detectionTime, humanPose); }) | - ranges::to_vector; - - associateHumans(newPoses); - - // add all not associated humans as new tracked humans - for (const auto& detectedHuman : newPoses) - { - if (!detectedHuman.associated) - { - human::Human human{.pose = detectedHuman.pose, - .linearVelocity = Eigen::Vector2f::Zero(), - .detectionTime = detectedHuman.detectionTime}; - - trackedHumans.push_back( - {.human = human, .trackingId = detectedHuman.trackingId, .associated = true}); - } - } - } - - struct PosDistance - { - HumanTracker::TrackedHuman* oldHuman; - HumanTracker::DetectedHuman* newHuman; - float distance; - }; - - std::vector<PosDistance> - getSortedDistances(std::vector<HumanTracker::TrackedHuman>& oldHumans, - std::vector<HumanTracker::DetectedHuman>& newHumans) - { - std::vector<PosDistance> posDistances; - - for (auto& oldHuman : oldHumans) - { - if (oldHuman.associated) - { - continue; - } - for (auto& newHuman : newHumans) - { - if (newHuman.associated) - { - continue; - } - posDistances.push_back( - {&oldHuman, - &newHuman, - (newHuman.pose.translation() - oldHuman.human.pose.translation()).norm()}); - } - } - - std::sort(posDistances.begin(), - posDistances.end(), - [](const PosDistance& a, const PosDistance& b) -> bool - { return a.distance < b.distance; }); - - return posDistances; - } - - void - HumanTracker::associateHumans(std::vector<DetectedHuman>& detectedHumans) - { - // associate humans by their tracking id - for (auto& oldHuman : trackedHumans) - { - if (oldHuman.associated || !oldHuman.trackingId) - { - continue; - } - for (auto& newHuman : detectedHumans) - { - if (newHuman.associated || !newHuman.trackingId) - { - continue; - } - if (oldHuman.trackingId.value() == newHuman.trackingId.value()) - { - associate(&oldHuman, &newHuman); - } - } - } - - - // associate leftover humans by their distances - const auto sortedDistances = getSortedDistances(trackedHumans, detectedHumans); - - for (auto& posDistance : sortedDistances) - { - if (posDistance.distance > parameters.maxAssociationDistance) - { - break; - } - if (posDistance.oldHuman->associated || posDistance.newHuman->associated) - { - continue; - } - associate(posDistance.oldHuman, posDistance.newHuman); - } - } - - void - HumanTracker::associate(TrackedHuman* trackedHuman, DetectedHuman* detectedHuman) - { - ARMARX_CHECK(!trackedHuman->associated); - ARMARX_CHECK(!detectedHuman->associated); - - trackedHuman->associated = true; - detectedHuman->associated = true; - - float dt = - (detectedHuman->detectionTime - trackedHuman->human.detectionTime).toSecondsDouble(); - Eigen::Vector2f ds = - (detectedHuman->pose.translation() - trackedHuman->human.pose.translation()); - Eigen::Vector2f linVelocity = ds / dt; - - Eigen::Vector2f velocity = - parameters.velocityAlpha * linVelocity + - (1 - parameters.velocityAlpha) * trackedHuman->human.linearVelocity; - - trackedHuman->human = {detectedHuman->pose, velocity, detectedHuman->detectionTime}; - trackedHuman->trackingId = detectedHuman->trackingId; - } - - std::vector<human::Human> - HumanTracker::getTrackedHumans() const - { - return trackedHumans | - ranges::views::transform([](const TrackedHuman& h) -> human::Human - { return h.human; }) | - ranges::to_vector; - } - - - void - HumanTracker::reset() - { - trackedHumans.clear(); - } - -} // namespace armarx::navigation::components::dynamic_scene_provider diff --git a/source/armarx/navigation/components/dynamic_scene_provider/HumanTracker.h b/source/armarx/navigation/components/dynamic_scene_provider/HumanTracker.h deleted file mode 100644 index a6991aa2f0f2cb93573a5221fc07f5929c1a0729..0000000000000000000000000000000000000000 --- a/source/armarx/navigation/components/dynamic_scene_provider/HumanTracker.h +++ /dev/null @@ -1,85 +0,0 @@ -/** - * This file is part of ArmarX. - * - * ArmarX is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2 as - * published by the Free Software Foundation. - * - * ArmarX is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - * @author Tobias Gröger ( tobias dot groeger at student dot kit dot edu ) - * @author Corvin Navarro Ecker ( corvin dot ecker at student dot kit dot edu ) - * @date 2022 - * @copyright http://www.gnu.org/licenses/gpl-2.0.txt - * GNU General Public License - */ - -#pragma once - -#include <ArmarXCore/core/time.h> - -#include "VisionX/libraries/armem_human/types.h" - -#include "armarx/navigation/core/basic_types.h" -#include "armarx/navigation/human/types.h" - -namespace armarx::navigation::components::dynamic_scene_provider -{ - - class HumanTracker - { - public: - HumanTracker() = default; - - struct Measurements - { - DateTime detectionTime; - std::vector<armem::human::HumanPose> humanPoses; - }; - - struct DetectedHuman - { - core::Pose2D pose; - std::optional<std::string> trackingId; - DateTime detectionTime; - bool associated; - }; - - struct TrackedHuman - { - human::Human human; - std::optional<std::string> trackingId = std::nullopt; - bool associated; - }; - - struct Parameters - { - // the duration after which tracked humans will be erased if no new measurement for this human is found - Duration maxTrackingAge = Duration::MilliSeconds(500); - // the maximum distance in millimeters of two human measurements to be associated with each other - float maxAssociationDistance = 600; - // alpha value from interval [0,1] to determine how much the new (and respectively the old) velocity should be weighted - float velocityAlpha = 0.7; - }; - - void update(const Measurements& measurements); - - std::vector<human::Human> getTrackedHumans() const; - - void reset(); - - private: - void associateHumans(std::vector<DetectedHuman>& detectedHumans); - void associate(TrackedHuman* tracked, DetectedHuman* detected); - - private: - std::vector<TrackedHuman> trackedHumans; - Parameters parameters; - }; -} // namespace armarx::navigation::components::dynamic_scene_provider diff --git a/source/armarx/navigation/human/CMakeLists.txt b/source/armarx/navigation/human/CMakeLists.txt index c0a1b74e0860559d872b92efa2db2f7a8e94a0d8..4cf568f3a5404f826bf970fdbdc8e7b393f1c3b1 100644 --- a/source/armarx/navigation/human/CMakeLists.txt +++ b/source/armarx/navigation/human/CMakeLists.txt @@ -11,15 +11,90 @@ armarx_add_library(teb_human ArmarXCore armarx_navigation::core armarx_navigation::conversions + ukfm DEPENDENCIES_PRIVATE range-v3::range-v3 SOURCES types.cpp aron_conversions.cpp ProxemicZoneCreator.cpp + HumanSystemModel.cpp + UnscentedKalmanFilter_impl.cpp + HumanTracker.cpp + HumanFilter.cpp HEADERS types.h aron_conversions.h shapes.h ProxemicZoneCreator.h + HumanSystemModel.h + HumanTracker.h + HumanFilter.h +) + +find_package(sciplot) + +armarx_add_test(so2_kalman_test + TEST_FILES + test/so2kalmanFilterTest.cpp + DEPENDENCIES + PUBLIC + ArmarXCore + armarx_navigation::teb_human + sciplot::sciplot +) + +armarx_add_test(se2_kalman_test + TEST_FILES + test/se2KalmanFilterTest.cpp + DEPENDENCIES + PUBLIC + ArmarXCore + armarx_navigation::teb_human + sciplot::sciplot +) + +armarx_add_test(se2xV_kalman_test + TEST_FILES + test/se2xVkalmanFilterTest.cpp + DEPENDENCIES + PUBLIC + ArmarXCore + armarx_navigation::teb_human + sciplot::sciplot +) + +armarx_add_test(manif_kalman_test + TEST_FILES + test/manifKalmanTest.cpp + DEPENDENCIES + PUBLIC + ArmarXCore + armarx_navigation::teb_human + sciplot::sciplot +) + + +armarx_add_test(se3_kalman_test + TEST_FILES + test/UnscentedKalmanFilterTest.cpp + DEPENDENCIES + PUBLIC + ArmarXCore + ukfm + sciplot::sciplot +) + + +armarx_add_test(kalman_filter_test + TEST_FILES + test/kalman_filter_test.cpp + DEPENDENCIES + PUBLIC + ArmarXCore + armarx_navigation::core + armarx_navigation::teb_human + + PRIVATE + range-v3::range-v3 ) diff --git a/source/armarx/navigation/human/HumanFilter.cpp b/source/armarx/navigation/human/HumanFilter.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ba786d13976e984216d9e7ec0c0b278cc89ba67b --- /dev/null +++ b/source/armarx/navigation/human/HumanFilter.cpp @@ -0,0 +1,124 @@ +#include "HumanFilter.h" + +namespace armarx::navigation::human +{ + + HumanFilter::HumanFilter(const core::Pose2D& pose0, const DateTime& detectionTime) + { + //initialize new human for detected human + human.pose = pose0; + human.linearVelocity = Eigen::Vector2f::Zero(); + human.detectionTime = detectionTime; + + + //initialize new kalman filter for new detected human + UnscentedKalmanFilter<SystemModelT>::PropCovT Q = + UnscentedKalmanFilter<SystemModelT>::PropCovT::Identity(); + Q.block<2, 2>(0, 0) *= params.control_pos_cov * params.control_pos_cov; + Q.block<1, 1>(2, 2) *= params.control_rot_cov * params.control_rot_cov; + + UnscentedKalmanFilter<SystemModelT>::ObsCovT R = + UnscentedKalmanFilter<SystemModelT>::ObsCovT::Identity(); + R.block<2, 2>(0, 0) *= params.obs_pos_cov * params.obs_pos_cov; + R.block<1, 1>(2, 2) *= params.obs_rot_cov * params.obs_rot_cov; + + UnscentedKalmanFilter<SystemModelT>::StateCovT P0 = + UnscentedKalmanFilter<SystemModelT>::StateCovT::Identity(); + P0.block<2, 2>(0, 0) *= params.initial_state_pos_cov * params.initial_state_pos_cov; + P0.block<1, 1>(2, 2) *= params.initial_state_rot_cov * params.initial_state_rot_cov; + + UnscentedKalmanFilter<SystemModelT>::AlphaT alpha = + UnscentedKalmanFilter<SystemModelT>::AlphaT::Ones() * params.alpha; + + //initial position and orientation according to detected human + SystemModelT::StateT state0 = toUkfState(pose0); + + ukf = std::make_unique<UnscentedKalmanFilter<SystemModelT>>(Q, R, alpha, state0, P0); + } + + void + HumanFilter::propagation(const armarx::core::time::DateTime& detectionTime) + { + double dt = (detectionTime - human.detectionTime).toSecondsDouble(); + + SystemModelT::ControlT control = toUkfControl(human.linearVelocity); + ukf->propagation(control, dt); + + human.pose = fromUkfState(ukf->getCurrentState()); + human.detectionTime = detectionTime; + } + + void + HumanFilter::update(const core::Pose2D& pose, const DateTime& detectionTime) + { + double dt = (detectionTime - oldHuman.detectionTime).toSecondsDouble(); + + //update ukf with new observation + SystemModelT::ObsT observation = toUkfObs(pose); + ukf->update(observation); + + core::Pose2D newPose = fromUkfState(ukf->getCurrentState()); + + // calculate velocity + Eigen::Vector2f ds = newPose.translation() - oldHuman.pose.translation(); + Eigen::Vector2f linVelocity = ds / dt; + // apply exponential smoothing to velocity + //TODO try other approaches? + Eigen::Vector2f newVelocity = + params.velocityAlpha * linVelocity + (1 - params.velocityAlpha) * human.linearVelocity; + + //update stored information about the human + human.detectionTime = detectionTime; + human.pose = newPose; + human.linearVelocity = newVelocity; + + oldHuman = human; + } + + const Human& + HumanFilter::get() const + { + return human; + } + + + HumanFilter::SystemModelT::StateT + HumanFilter::toUkfState(const core::Pose2D& pose) + { + // [mm] to [m] + return {{pose.translation().x() / 1000, + pose.translation().y() / 1000, + Eigen::Rotation2Df(pose.linear()).angle()}}; + } + + core::Pose2D + HumanFilter::fromUkfState(const SystemModelT::StateT& pose) + { + core::Pose2D pose2d; + // [m] to [mm] + pose2d.translation().x() = pose.pose.x() * 1000; + pose2d.translation().y() = pose.pose.y() * 1000; + pose2d.linear() = Eigen::Rotation2Df(pose.pose.angle()).toRotationMatrix(); + + return pose2d; + } + + HumanFilter::SystemModelT::ControlT + HumanFilter::toUkfControl(const Eigen::Vector2f& vel) + { + // convert velocity from global frame to human frame (required by kalman filter) + const auto& global_R_human = human.pose.linear(); + const auto& human_V_vel = global_R_human.inverse() * vel; + + // [mm] to [m] + return {{human_V_vel.x() / 1000, human_V_vel.y() / 1000, 0}}; + } + + HumanFilter::SystemModelT::ObsT + HumanFilter::toUkfObs(const core::Pose2D& pose) + { + return SystemModelT::observationFunction(toUkfState(pose)); + } + + +} // namespace armarx::navigation::human diff --git a/source/armarx/navigation/human/HumanFilter.h b/source/armarx/navigation/human/HumanFilter.h new file mode 100644 index 0000000000000000000000000000000000000000..ceb13aa13478c36481812a3248fe740dafc57e8e --- /dev/null +++ b/source/armarx/navigation/human/HumanFilter.h @@ -0,0 +1,108 @@ +/** + * This file is part of ArmarX. + * + * ArmarX is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * ArmarX is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @author Tobias Gröger ( tobias dot groeger at student dot kit dot edu ) + * @author Corvin Navarro Ecker ( corvin dot ecker at student dot kit dot edu ) + * @date 2022 + * @copyright http://www.gnu.org/licenses/gpl-2.0.txt + * GNU General Public License + */ + +#pragma once + +#include <RobotAPI/libraries/ukfm/UnscentedKalmanFilter.h> + +#include <armarx/navigation/human/HumanSystemModel.h> +#include <armarx/navigation/human/types.h> + +namespace armarx::navigation::human +{ + + /** + * @brief The HumanFilter class can be used to track and filter the state of a single human. It + * hides implementation detail on how the filtering is done. New information about the human can + * be fed by using the update method. The human itself can be obtained using the get method. + */ + class HumanFilter + { + + public: + struct Parameters + { + //TODO which values are appropriate? + double control_pos_cov = 0.01; + double control_rot_cov = 0.01; + double obs_pos_cov = 0.01; + double obs_rot_cov = 0.01; + double initial_state_pos_cov = 0.01; + double initial_state_rot_cov = 0.01; + double alpha = 0.5; + + float velocityAlpha = 0.7; + }; + + /** + * @brief HumanFilter::HumanFilter creates a new filter that filters 2D poses (position and + * rotation in a two dimensional plane) + * @param pose0 the first known pose of the human + * @param detectionTime the point of detection + */ + HumanFilter(const core::Pose2D& pose0, const DateTime& detectionTime); + + /** + * @brief HumanFilter::propagation propagate the system model to the given point in time. This + * means that the human pose is updated according to the filters prediction for the given point + * in time. Should be called at most once between every two calls of HumanFilter::update. + * @param detectionTime the point in time for which the pose should be predicted + */ + void propagation(const DateTime& detectionTime); + /** + * @brief HumanFilter::update update the filter a new detected pose of the tracked human and + * the detection time + * @param pose the new detected pose of the human + * @param detectionTime the detection time + */ + void update(const core::Pose2D& pose, const DateTime& detectionTime); + /** + * @brief HumanFilter::get returns the human whose pose is filtered containing the most recent + * state + * @return the human + */ + const Human& get() const; + + private: + using SystemModelT = kalman_filter::SystemModelSE2<double>; + + SystemModelT::StateT toUkfState(const core::Pose2D& pose); + core::Pose2D fromUkfState(const SystemModelT::StateT& pose); + SystemModelT::ControlT toUkfControl(const Eigen::Vector2f& vel); + SystemModelT::ObsT toUkfObs(const core::Pose2D& pose); + + private: + Parameters params; + /** + * @brief oldHuman stores information about the human at the point in time of the last + * HumanFilter::update call + */ + Human oldHuman; + /** + * @brief human stores information about the human at the point in time of the last + * HumanFilter::propagation call + */ + Human human; + std::unique_ptr<UnscentedKalmanFilter<SystemModelT>> ukf; + }; + +} // namespace armarx::navigation::human diff --git a/source/armarx/navigation/human/HumanSystemModel.cpp b/source/armarx/navigation/human/HumanSystemModel.cpp new file mode 100644 index 0000000000000000000000000000000000000000..80f6820535832028682dad6f14dc28c0723c0cb4 --- /dev/null +++ b/source/armarx/navigation/human/HumanSystemModel.cpp @@ -0,0 +1,171 @@ +#include "HumanSystemModel.h" + +namespace armarx::navigation::human::kalman_filter +{ + + // ------------------------------ SO2xR2 ----------------------------------------- + + template <typename floatT> + typename SystemModelSO2xR2<floatT>::ObsT + SystemModelSO2xR2<floatT>::observationFunction(const SystemModelSO2xR2::StateT& state) + { + ObsT observation; + observation.segment(0, 1) = state.orientation.log().coeffs(); + observation.segment(1, 2) = state.position; + return observation; + } + + template <typename floatT> + typename SystemModelSO2xR2<floatT>::StateT + SystemModelSO2xR2<floatT>::propagationFunction(const SystemModelSO2xR2::StateT& state, + const SystemModelSO2xR2::ControlT& control, + const SystemModelSO2xR2::ControlNoiseT& noise, + FloatT dt) + { + StateT new_state; + new_state.orientation = state.orientation.template rplus(control.angular_velocity * dt); + new_state.position = state.position + control.euclidean_velocity * dt; + //new_state.position = + // state.position + state.orientation.rotation() * control.euclidean_velocity * dt; + return new_state; + } + + template <typename floatT> + typename SystemModelSO2xR2<floatT>::StateT + SystemModelSO2xR2<floatT>::retraction(const SystemModelSO2xR2::StateT& state, + const SystemModelSO2xR2::SigmaPointsT& sigmaPoints) + { + StateT new_state; + manif::SO2Tangent<FloatT> tan; + tan.coeffs() << sigmaPoints.segment(0, 1); + //new_state.orientation = state.orientation.lplus(tan); + new_state.orientation = state.orientation.rplus(tan); + + new_state.position = state.position + sigmaPoints.segment(1, 2); + return new_state; + } + + template <typename floatT> + typename SystemModelSO2xR2<floatT>::SigmaPointsT + SystemModelSO2xR2<floatT>::inverseRetraction(const SystemModelSO2xR2::StateT& state1, + const SystemModelSO2xR2::StateT& state2) + { + SigmaPointsT sigma; + // TODO: check if right order of substraction + + //sigma.segment(0, 1) = state2.orientation.lminus(state1.orientation).coeffs(); + sigma.segment(0, 1) = state1.orientation.lminus(state2.orientation).coeffs(); + + sigma.segment(1, 2) = state1.position - state2.position; + //sigma.segment(1, 2) = state2.position - state1.position; + + return sigma; + } + + template struct SystemModelSO2xR2<float>; + template struct SystemModelSO2xR2<double>; + + + // ------------------------------ SE2 ----------------------------------------- + + template <typename floatT> + typename SystemModelSE2<floatT>::ObsT + SystemModelSE2<floatT>::observationFunction(const SystemModelSE2::StateT& state) + { + ObsT observation = state.pose.log().coeffs(); + return observation; + } + + template <typename floatT> + typename SystemModelSE2<floatT>::StateT + SystemModelSE2<floatT>::propagationFunction(const SystemModelSE2::StateT& state, + const SystemModelSE2::ControlT& control, + const SystemModelSE2::ControlNoiseT& noise, + FloatT dt) + { + StateT new_state; + new_state.pose = state.pose.template rplus(control.velocity * dt); + return new_state; + } + + template <typename floatT> + typename SystemModelSE2<floatT>::StateT + SystemModelSE2<floatT>::retraction(const SystemModelSE2::StateT& state, + const SystemModelSE2::SigmaPointsT& sigmaPoints) + { + StateT new_state; + manif::SE2Tangent<FloatT> tan; + tan.coeffs() << sigmaPoints; + new_state.pose = state.pose.lplus(tan); + return new_state; + } + + template <typename floatT> + typename SystemModelSE2<floatT>::SigmaPointsT + SystemModelSE2<floatT>::inverseRetraction(const SystemModelSE2::StateT& state1, + const SystemModelSE2::StateT& state2) + { + SigmaPointsT sigma; + sigma = state2.pose.lminus(state1.pose).coeffs(); + return sigma; + } + + template struct SystemModelSE2<float>; + template struct SystemModelSE2<double>; + + + // ------------------------------ SE2xV ----------------------------------------- + + template <typename floatT> + typename SystemModelSE2xV<floatT>::ObsT + SystemModelSE2xV<floatT>::observationFunction(const SystemModelSE2xV::StateT& state) + { + ObsT observation = state.pose.log().coeffs(); + return observation; + } + + template <typename floatT> + typename SystemModelSE2xV<floatT>::StateT + SystemModelSE2xV<floatT>::propagationFunction(const SystemModelSE2xV::StateT& state, + const SystemModelSE2xV::ControlT& control, + const SystemModelSE2xV::ControlNoiseT& noise, + FloatT dt) + { + StateT new_state; + new_state.pose = state.pose.template rplus(state.velocity * dt); + new_state.velocity = state.velocity; + return new_state; + } + + template <typename floatT> + typename SystemModelSE2xV<floatT>::StateT + SystemModelSE2xV<floatT>::retraction(const SystemModelSE2xV::StateT& state, + const SystemModelSE2xV::SigmaPointsT& sigmaPoints) + { + StateT new_state; + manif::SE2Tangent<FloatT> tan; + tan.coeffs() << sigmaPoints.segment(0, 3); + new_state.pose = state.pose.lplus(tan); + tan.coeffs() << sigmaPoints.segment(3, 3); + // TODO: this is probably not correct, i.e. there needs to be some interaction between velocity and pose + new_state.velocity += tan; + return new_state; + } + + template <typename floatT> + typename SystemModelSE2xV<floatT>::SigmaPointsT + SystemModelSE2xV<floatT>::inverseRetraction(const SystemModelSE2xV::StateT& state1, + const SystemModelSE2xV::StateT& state2) + { + SigmaPointsT sigma; + sigma.segment(0, 3) = state2.pose.lminus(state1.pose).coeffs(); + // TODO: this is probably not correct; probably one cannot subtract two tangent vectors at two different poses + // from each other + sigma.segment(3, 3) = (state2.velocity - state1.velocity).coeffs(); + return sigma; + } + + template struct SystemModelSE2xV<float>; + template struct SystemModelSE2xV<double>; + +} // namespace armarx::navigation::human::kalman_filter diff --git a/source/armarx/navigation/human/HumanSystemModel.h b/source/armarx/navigation/human/HumanSystemModel.h new file mode 100644 index 0000000000000000000000000000000000000000..05de1fdcecb71f274a8b3ce26fa88cbcaee575c6 --- /dev/null +++ b/source/armarx/navigation/human/HumanSystemModel.h @@ -0,0 +1,171 @@ +/** + * This file is part of ArmarX. + * + * ArmarX is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * ArmarX is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @author Tobias Gröger ( tobias dot groeger at student dot kit dot edu ) + * @author Corvin Navarro Ecker ( corvin dot ecker at student dot kit dot edu ) + * @date 2022 + * @copyright http://www.gnu.org/licenses/gpl-2.0.txt + * GNU General Public License + */ +#pragma once + +#include <manif/SE2.h> +#include <manif/SO2.h> + +namespace armarx::navigation::human::kalman_filter +{ + //----------- SO2xR2 ----------------- + + template <typename floatT> + struct StateSO2xR2 + { + manif::SO2<floatT> orientation; + Eigen::Matrix<floatT, 2, 1> position; + }; + + template <typename floatT> + struct ControlSO2xR2 + { + typename manif::SO2<floatT>::Tangent angular_velocity; + Eigen::Matrix<floatT, 2, 1> euclidean_velocity; + }; + + template <typename floatT> + struct SystemModelSO2xR2 + { + static_assert(std::is_floating_point_v<floatT>); + struct dim + { + static constexpr long state = 3, control = 3, obs = 3; + }; + + using FloatT = floatT; + using StateT = StateSO2xR2<FloatT>; + using ControlT = ControlSO2xR2<FloatT>; + using ObsT = Eigen::Matrix<FloatT, dim::obs, 1>; + using ControlNoiseT = Eigen:: + Matrix<FloatT, dim::control, 1>; // todo: correct name? Is it the same as ControlT? + using SigmaPointsT = Eigen::Matrix<FloatT, dim::state, 1>; // todo: rename + + static ObsT observationFunction(const StateT& state); + + static StateT propagationFunction(const StateT& state, + const ControlT& control, + const ControlNoiseT& noise, + FloatT dt); + + static StateT retraction(const StateT& state, const SigmaPointsT& sigmaPoints); + + static SigmaPointsT inverseRetraction(const StateT& state1, const StateT& state2); + }; + + extern template struct SystemModelSO2xR2<float>; + extern template struct SystemModelSO2xR2<double>; + + + //----------- SE2 ----------------- + + template <typename floatT> + struct StateSE2 + { + manif::SE2<floatT> pose; + }; + + template <typename floatT> + struct ControlSE2 + { + typename manif::SE2<floatT>::Tangent velocity; + }; + + template <typename floatT> + struct SystemModelSE2 + { + static_assert(std::is_floating_point_v<floatT>); + struct dim + { + static constexpr long state = 3, control = 3, obs = 3; + }; + + using FloatT = floatT; + using StateT = StateSE2<FloatT>; + using ControlT = ControlSE2<FloatT>; + using ObsT = Eigen::Matrix<FloatT, dim::obs, 1>; + using ControlNoiseT = Eigen:: + Matrix<FloatT, dim::control, 1>; // todo: correct name? Is it the same as ControlT? + using SigmaPointsT = Eigen::Matrix<FloatT, dim::state, 1>; // todo: rename + + static ObsT observationFunction(const StateT& state); + + static StateT propagationFunction(const StateT& state, + const ControlT& control, + const ControlNoiseT& noise, + FloatT dt); + + static StateT retraction(const StateT& state, const SigmaPointsT& sigmaPoints); + + static SigmaPointsT inverseRetraction(const StateT& state1, const StateT& state2); + }; + + extern template struct SystemModelSE2<float>; + extern template struct SystemModelSE2<double>; + + + //----------- SE2xV ----------------- + + template <typename floatT> + struct StateSE2xV + { + manif::SE2<floatT> pose; + typename manif::SE2<floatT>::Tangent velocity; + }; + + template <typename floatT> + struct ControlSE2xV + { + }; + + template <typename floatT> + struct SystemModelSE2xV + { + static_assert(std::is_floating_point_v<floatT>); + struct dim + { + static constexpr long state = 6, control = 1, obs = 3; + }; + + using FloatT = floatT; + using StateT = StateSE2xV<FloatT>; + using ControlT = ControlSE2xV<FloatT>; + using ObsT = Eigen::Matrix<FloatT, dim::obs, 1>; + using ControlNoiseT = Eigen:: + Matrix<FloatT, dim::control, 1>; // todo: correct name? Is it the same as ControlT? + using SigmaPointsT = Eigen::Matrix<FloatT, dim::state, 1>; // todo: rename + + static ObsT observationFunction(const StateT& state); + + static StateT propagationFunction(const StateT& state, + const ControlT& control, + const ControlNoiseT& noise, + FloatT dt); + + static StateT retraction(const StateT& state, const SigmaPointsT& sigmaPoints); + + static SigmaPointsT inverseRetraction(const StateT& state1, const StateT& state2); + }; + + extern template struct SystemModelSE2xV<float>; + extern template struct SystemModelSE2xV<double>; + +} // namespace armarx::navigation::human::kalman_filter diff --git a/source/armarx/navigation/human/HumanTracker.cpp b/source/armarx/navigation/human/HumanTracker.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2332377e9b0583eee5ae543aa4c6386723f7dd9e --- /dev/null +++ b/source/armarx/navigation/human/HumanTracker.cpp @@ -0,0 +1,223 @@ +#include "HumanTracker.h" + +#include "ArmarXCore/core/exceptions/local/ExpressionException.h" + +#include "armarx/navigation/human/types.h" +#include <armarx/navigation/conversions/eigen.h> +#include <range/v3/range/conversion.hpp> +#include <range/v3/view/transform.hpp> + +namespace armarx::navigation::human +{ + void + HumanTracker::update(const Measurements& measurements) + { + // iterate over all existing tracked humans + for (auto it = trackedHumans.begin(); it != trackedHumans.end();) + { + auto& human = *it; + // when the tracked human recieved no new measurement for too long, remove it + if ((measurements.detectionTime - human.humanFilter.get().detectionTime) >= + parameters.maxTrackingAge) + { + it = trackedHumans.erase(it); + } + // otherwise the tracked human is prepared for association at the current point in time + else + { + human.associated = false; + human.humanFilter.propagation(measurements.detectionTime); + it++; + } + } + + // calculate the poses according to the new received measurements + std::vector<DetectedHuman> newPoses = + measurements.humanPoses | + ranges::views::transform( + [measurements, this](const armem::human::HumanPose& humanPose) -> DetectedHuman { + return convertHumanPoseToDetectedHuman(measurements.detectionTime, humanPose); + }) | + ranges::to_vector; + + // associate the new poses from the measurement with the tracked humans + associateHumans(newPoses); + + // add all not associated poses as new tracked humans + for (const auto& detectedHuman : newPoses) + { + if (not detectedHuman.associated) + { + //add new tracked human to list of tracked humans + trackedHumans.push_back(TrackedHuman{ + .humanFilter = HumanFilter{detectedHuman.pose, detectedHuman.detectionTime}, + .trackingId = detectedHuman.trackingId, + .associated = true}); + } + } + } + + std::vector<human::Human> + HumanTracker::getTrackedHumans() const + { + return trackedHumans | + ranges::views::transform([](const TrackedHuman& h) -> human::Human + { return h.humanFilter.get(); }) | + ranges::to_vector; + } + + void + HumanTracker::reset() + { + trackedHumans.clear(); + } + + + HumanTracker::DetectedHuman + HumanTracker::convertHumanPoseToDetectedHuman(const DateTime& time, + const armem::human::HumanPose& humanPose) + { + const std::map<std::string, armem::human::PoseKeypoint>& keypoints = humanPose.keypoints; + ARMARX_CHECK_NOT_EMPTY(keypoints); + + // calculate the arithmetic mean of all keypoint positions + Eigen::Vector3f centerPos; + int size = 0; + for (const auto& [_, v] : keypoints) + { + if (v.positionGlobal.has_value()) + { + centerPos += v.positionGlobal.value().toEigen(); + size++; + } + } + centerPos /= size; + + // calculate the yaw of the specified keypoint representing the orientation if it exists + double yaw = 0; + if (humanPose.keypoints.count(parameters.rotationKeypoint) > 0) + { + const armem::human::PoseKeypoint& refKeypoint = + humanPose.keypoints.at(parameters.rotationKeypoint); + ARMARX_CHECK(refKeypoint.orientationGlobal.has_value()); + Eigen::Quaternionf qhead = refKeypoint.orientationGlobal->toEigenQuaternion(); + Eigen::Vector3f vec(1, 0, 0); + Eigen::Vector3f rot3 = qhead._transformVector(vec); + Eigen::Vector2f rot2(rot3.x(), rot3.y()); + if (rot2.norm() != 0) + { + // calculate angle of rot2 + yaw = atan2(rot2.y(), rot2.x()); + } + //old version using euler angles: + //yaw = humanPose //from all human pose keypoints + // .keypoints + // .at(parameters.rotationKeypoint) //find the keypoint representing the head + // .orientationGlobal //get its global orientation + // ->toEigen() + // .eulerAngles(2, 1, 0)[0]; //and extract the yaw (rotation around z axis in + // global coordinates) + } + + // create the new pose with the calculated position and yaw + core::Pose2D pose = core::Pose2D::Identity(); + pose.translation() = conv::to2D(centerPos); + pose.linear() = Eigen::Rotation2Df(yaw).toRotationMatrix(); + + return {pose, humanPose.humanTrackingId, time, false}; + } + + std::vector<HumanTracker::PosDistance> + HumanTracker::getSortedDistances(std::vector<HumanTracker::TrackedHuman>& oldHumans, + std::vector<HumanTracker::DetectedHuman>& newHumans) + { + std::vector<PosDistance> posDistances; + + for (auto& oldHuman : oldHumans) + { + if (oldHuman.associated) + { + continue; + } + for (auto& newHuman : newHumans) + { + if (newHuman.associated) + { + continue; + } + // calculate distance between every possible combination of tracked and detected + // humans where none of them was associated + posDistances.push_back( + {&oldHuman, + &newHuman, + (newHuman.pose.translation() - oldHuman.humanFilter.get().pose.translation()) + .norm()}); + } + } + + // sort the distances ascending by their numeric value + std::sort(posDistances.begin(), + posDistances.end(), + [](const PosDistance& a, const PosDistance& b) -> bool + { return a.distance < b.distance; }); + + return posDistances; + } + + void + HumanTracker::associateHumans(std::vector<DetectedHuman>& detectedHumans) + { + // first, associate humans by their tracking id + for (auto& oldHuman : trackedHumans) + { + if (oldHuman.associated || not oldHuman.trackingId) + { + continue; + } + for (auto& newHuman : detectedHumans) + { + if (newHuman.associated || not newHuman.trackingId) + { + continue; + } + // check for every possible combination of old and new humans (that were not already + // associated and have a tracking id) if their tracking id is the same + if (oldHuman.trackingId.value() == newHuman.trackingId.value()) + { + associate(&oldHuman, &newHuman); + } + } + } + + // second, associate leftover humans by their distances + const auto sortedDistances = getSortedDistances(trackedHumans, detectedHumans); + + for (auto& posDistance : sortedDistances) + { + if (posDistance.distance > parameters.maxAssociationDistance) + { + break; + } + if (posDistance.oldHuman->associated || posDistance.newHuman->associated) + { + continue; + } + // associate the pair with the currently smallest distance between non-associated humans + associate(posDistance.oldHuman, posDistance.newHuman); + } + } + + void + HumanTracker::associate(TrackedHuman* trackedHuman, DetectedHuman* detectedHuman) + { + ARMARX_CHECK(not trackedHuman->associated); + ARMARX_CHECK(not detectedHuman->associated); + + trackedHuman->associated = true; + detectedHuman->associated = true; + + trackedHuman->humanFilter.update(detectedHuman->pose, detectedHuman->detectionTime); + trackedHuman->trackingId = detectedHuman->trackingId; + } + +} // namespace armarx::navigation::human diff --git a/source/armarx/navigation/human/HumanTracker.h b/source/armarx/navigation/human/HumanTracker.h new file mode 100644 index 0000000000000000000000000000000000000000..1a83fb65b8d981a11709dd5d31efd74b29c3eb0d --- /dev/null +++ b/source/armarx/navigation/human/HumanTracker.h @@ -0,0 +1,157 @@ +/** + * This file is part of ArmarX. + * + * ArmarX is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * ArmarX is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @author Tobias Gröger ( tobias dot groeger at student dot kit dot edu ) + * @author Corvin Navarro Ecker ( corvin dot ecker at student dot kit dot edu ) + * @date 2022 + * @copyright http://www.gnu.org/licenses/gpl-2.0.txt + * GNU General Public License + */ + +#pragma once + +#include <ArmarXCore/core/time.h> + +#include <RobotAPI/libraries/ukfm/UnscentedKalmanFilter.h> + +#include <VisionX/libraries/armem_human/types.h> + +#include <armarx/navigation/core/basic_types.h> +#include <armarx/navigation/human/HumanFilter.h> +#include <armarx/navigation/human/HumanSystemModel.h> +#include <armarx/navigation/human/types.h> + +namespace armarx::navigation::human +{ + using T = double; + using Vector = Eigen::Matrix<T, 2, 1>; + using SystemModelT = kalman_filter::SystemModelSO2xR2<T>; + + /** + * @brief The HumanTracker class can be used to track and filter multiple humans. It hides + * implementation detail on how new detected humans are associated to the old, already tracked + * humans. New detected humans can be fed by using the update method. The tracked humans can + * be obtained using the getTrackedHumans method. + */ + class HumanTracker + { + public: + HumanTracker() = default; + + struct Measurements + { + DateTime detectionTime; + std::vector<armem::human::HumanPose> humanPoses; + }; + + struct DetectedHuman + { + core::Pose2D pose; + std::optional<std::string> trackingId; + DateTime detectionTime; + bool associated; + }; + + struct TrackedHuman + { + HumanFilter humanFilter; + std::optional<std::string> trackingId = std::nullopt; + bool associated; + }; + + struct PosDistance + { + HumanTracker::TrackedHuman* oldHuman; + HumanTracker::DetectedHuman* newHuman; + float distance; + }; + + struct Parameters + { + // the keypoint which should be used for calculating the rotation of the humans + const std::string rotationKeypoint = "HEAD"; + // the duration after which tracked humans will be erased if no new measurement for + // this human is found + Duration maxTrackingAge = Duration::MilliSeconds(500); + // the maximum distance in millimeters of two human measurements where they are still + // associated with each other + float maxAssociationDistance = 600; + // alpha value from interval [0,1] to determine how much the current (and respectively + // the old) velocity should be weighted when calculating the new velocity + float velocityAlpha = 0.7; + }; + /** + * @brief HumanTracker::update Updates the tracked humans with the measurements. When a + * measurement is close enough to an existing tracked human, they are associated, otherwise a + * new tracked human is created. Tracked humans that were not associated with a new measurement + * for a specified amount of time are removed. New associated measurements for a tracked human + * are always filtered to provide a less noisy state. + * @param measurements the new measurements of the environment + */ + void update(const Measurements& measurements); + /** + * @brief HumanTracker::getTrackedHumans Returns all humans that are currently tracked. + * @return the tracked humans + */ + std::vector<human::Human> getTrackedHumans() const; + /** + * @brief HumanTracker::reset Resets this instance to the same state as if it just would have + * been created. + */ + void reset(); + + private: + /** + * @brief convertHumanPoseToDetectedHuman Sets every parameter of a detected human according + * to the human pose and returns the new created detected human. + * @param time the time of detection + * @param humanPose the human pose representing the human + * @return the new created detected human + */ + DetectedHuman convertHumanPoseToDetectedHuman(const DateTime& time, + const armem::human::HumanPose& humanPose); + /** + * @brief getSortedDistances Returns a sorted vector of the distances between every possible + * combination (T, D) where T is an old, tracked human and D is a new, detected human and + * both of them are not already associated. The smallest distance will be the first entry in + * the vector + * @param oldHumans the old, tracked humans + * @param newHumans the new, detected humans + * @return the sorted vector of distances with references to the according humans + */ + std::vector<PosDistance> + getSortedDistances(std::vector<HumanTracker::TrackedHuman>& oldHumans, + std::vector<HumanTracker::DetectedHuman>& newHumans); + /** + * @brief HumanTracker::associateHumans Associates those tracked and detected humans that + * belong together. + * @param detectedHumans The detected humans against which the saved list of tracked humans is + * matched. + */ + void associateHumans(std::vector<DetectedHuman>& detectedHumans); + /** + * @brief HumanTracker::associate Associates the given tracked and detected human. Therefore it + * updates all necessary variables of the TrackedHuman + * @param trackedHuman the tracked human + * @param detectedHuman the detected human + */ + void associate(TrackedHuman* tracked, DetectedHuman* detected); + + + private: + std::vector<TrackedHuman> trackedHumans; + Parameters parameters; + }; +} // namespace armarx::navigation::human diff --git a/source/armarx/navigation/human/UnscentedKalmanFilter_impl.cpp b/source/armarx/navigation/human/UnscentedKalmanFilter_impl.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5818d6edc6e852bc991b6fbcd9d752eeee1013f9 --- /dev/null +++ b/source/armarx/navigation/human/UnscentedKalmanFilter_impl.cpp @@ -0,0 +1,14 @@ +#include <RobotAPI/libraries/ukfm/UnscentedKalmanFilter.cpp> + +#include <armarx/navigation/human/HumanSystemModel.h> + + +template class UnscentedKalmanFilter< + armarx::navigation::human::kalman_filter::SystemModelSO2xR2<float>>; +template class UnscentedKalmanFilter< + armarx::navigation::human::kalman_filter::SystemModelSO2xR2<double>>; + +template class UnscentedKalmanFilter< + armarx::navigation::human::kalman_filter::SystemModelSE2<double>>; +template class UnscentedKalmanFilter< + armarx::navigation::human::kalman_filter::SystemModelSE2xV<double>>; diff --git a/source/armarx/navigation/human/test/UnscentedKalmanFilterTest.cpp b/source/armarx/navigation/human/test/UnscentedKalmanFilterTest.cpp new file mode 100644 index 0000000000000000000000000000000000000000..bc1ec3847e849a6383cdee4f4f9e3bc9ef9a9542 --- /dev/null +++ b/source/armarx/navigation/human/test/UnscentedKalmanFilterTest.cpp @@ -0,0 +1,322 @@ +/* + * This file is part of ArmarX. + * + * ArmarX is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * ArmarX is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @package ROBDEKON + * @author Christoph Pohl ( christoph dot pohl at kit dot edu ) + * @date 18.03.21 + * @copyright http://www.gnu.org/licenses/gpl-2.0.txt + * GNU General Public License + * + * Code adapted to C++ from: https://github.com/CAOR-MINES-ParisTech/ukfm + * See also: + * - https://arxiv.org/pdf/2002.00878.pdf + * - https://ieeexplore.ieee.org/document/8206066/ + */ +#define BOOST_TEST_MODULE Navigation::ArmarXLibraries::human +#define ARMARX_BOOST_TEST + +#include <cstdlib> /* srand, rand */ +#include <ctime> /* time */ + +#include <ArmarXCore/core/exceptions/local/ExpressionException.h> +#include <ArmarXCore/core/logging/Logging.h> +#include <ArmarXCore/util/time.h> + +#include <RobotAPI/libraries/ukfm/UnscentedKalmanFilter.h> + +#include <armarx/navigation/Test.h> +#include <sciplot/sciplot.hpp> + +using T = double; +using Vector = Eigen::Matrix<T, 3, 1>; +using Matrix = Eigen::Matrix<T, 3, 3>; +using SystemModelT = SystemModelSE3<T>; + +constexpr long num_timesteps = 3000; +constexpr T max_time = 1; +constexpr T dt = max_time / num_timesteps; +constexpr T c = (1 / max_time) * 2 * M_PI; + +constexpr T acc_noise_std = 0.01; +constexpr T om_noise_std = 0.01; +constexpr T obs_noise_std = 0.01; +constexpr T initial_offset_angle = 0.0 * 10 * M_PI / 180; +const Vector initial_offet_pos = 0.5 * Vector(1, 0.5, 0.7); + +void +test_retract() +{ + for (int i = 0; i < num_timesteps; i++) + { + SystemModelT::StateT state1, state2; + state1.pose = manif::SE3<T>::Random(); + // state1.velocity.setRandom(); + state2.pose = manif::SE3<T>::Random(); + // state2.velocity.setRandom(); + // sigma = state2 - state1 + SystemModelT::SigmaPointsT sigma = SystemModelT::inverseRetraction(state1, state2); + // state3 = state1 + sigma --> state3 = state2 + SystemModelT::StateT state3 = SystemModelT::retraction(state1, sigma); + // ARMARX_CHECK((state2.velocity - state3.velocity).norm() < 10 * std::numeric_limits<T>::epsilon()); + ARMARX_CHECK((state2.pose - state3.pose).coeffs().norm() < + 10000 * std::numeric_limits<T>::epsilon()) + << (state2.pose - state3.pose).coeffs().norm(); + + SystemModelT::SigmaPointsT sigma2 = SystemModelT::inverseRetraction(state1, state3); + // TODO: fix bad accuracy + ARMARX_CHECK((sigma2 - sigma).norm() < 10000 * std::numeric_limits<T>::epsilon()) + << (sigma2 - sigma).norm(); + } +} + +void +test_se3() +{ + for (int i = 1; i < num_timesteps; i++) + { + const T t = dt * i; + const T angle = t * c; + const Eigen::Matrix<T, 3, 1> pos(std::sin(angle), std::cos(angle), 0); + manif::SO3<T> rot = manif::SO3<T>(0, 0, angle); + ARMARX_CHECK(std::abs(rot.log().coeffs()(2) - angle) < 1e-6); + manif::SE3<T> pose(pos, rot); + + manif::SE3Tangent<T> tangent = pose.log(); + ARMARX_CHECK((tangent.coeffs().segment(0, 3) - pos).norm() < 1e-6); + ARMARX_CHECK(std::abs(tangent.coeffs()(5) - angle) < 1e-6); + } +} + +void +simulate_trajectory(std::vector<SystemModelT::StateT>& states, + std::vector<SystemModelT::ControlT>& omegas) +{ + SystemModelT::StateT state; + // state.velocity = Vector(c, 0, 0); + state.pose = manif::SE3<T>(0, 1, 0, M_PI, M_PI, 0); + states.push_back(state); + for (int i = 1; i < num_timesteps; i++) + { + const T t = dt * i; + const T t_prev = dt * (i - 1); + const T angle = t * c; + // yaw goes from -pi to pi + const T yaw = 1 * M_PI * std::sin(angle); + const T roll = 0.1 * M_PI * std::sin(angle) + M_PI; + const T pitch = 0.1 * M_PI * std::sin(angle) + M_PI; + const Vector pos(std::sin(angle), std::cos(angle), 0); + manif::SE3Tangent<T> tangent(states.at(i - 1).pose.log()); + ARMARX_CHECK((states.at(i - 1).pose.translation() - pos).norm() < 0.1); + // ARMARX_CHECK((tangent.coeffs().segment(0, 3) - pos).norm() < 0.1); + const Vector vel(std::cos(angle) * c, -std::sin(angle) * c, 0); + const Vector acc(-std::sin(angle) * (c * c), -std::cos(angle) * (c * c), 0); + const Vector acc_prev(-std::sin(t_prev * c) * (c * c), -std::cos(t_prev * c) * (c * c), 0); + // const Eigen::Vector3f vel((pos - state.position) / dt); + // Eigen::Vector3f acc = (vel - state.velocity) / dt; + manif::SO3<T> rot = manif::SO3<T>(roll, pitch, yaw); + // Eigen::Matrix<T, 3, 1> rot_tan = rot.log().coeffs(); + // ARMARX_CHECK(std::abs(rot_tan(2) - yaw) < 1e-6); + SystemModelT::ControlT control; + // control.linear_accel = state.pose.rotation().inverse() * acc_prev; + Vector angular_velocity(0.1 * M_PI * std::cos(angle) * c, + 0.1 * M_PI * std::cos(angle) * c, + M_PI * std::cos(angle) * c); + control.velocity.coeffs() << state.pose.asSO3().inverse().act(vel), angular_velocity; + + const SystemModelT::StateT propagated = SystemModelT::propagationFunction( + state, control, SystemModelT::ControlNoiseT::Zero(), dt); + manif::SE3Tangent<T> pro_tangent = propagated.pose.log(); + T pos_diff1 = (propagated.pose.translation() - pos).norm(); + // ARMARX_CHECK(pos_diff1 < pos_diff2); + ARMARX_CHECK(pos_diff1 < 2e-5) + << "Position is not the same: " << pos << " vs " << propagated.pose.translation(); + // ARMARX_CHECK((propagated.velocity - vel).norm() < 1e-4) << "Velocity is not the same: " << vel << " vs " << propagated.velocity; + // ARMARX_CHECK(propagated.pose.asSO3().lminus(rot).coeffs().norm() < 1e-2) << "Rotation is not the same: " << rot.rotation() << " vs " << propagated.pose.rotation(); + // state.velocity = vel; + state.pose = manif::SE3<T>(pos, rot); + states.push_back(propagated); + + tangent = state.pose.log(); + ARMARX_CHECK((state.pose.translation() - pos).norm() < 1e-6); + // ARMARX_CHECK(std::abs(tangent.coeffs()(5) - yaw) < 1e-2); + + // add noise + control.velocity.coeffs().segment(0, 3) += acc_noise_std * Vector::Random(); + control.velocity.coeffs().segment(3, 3) += om_noise_std * Vector::Random(); + omegas.push_back(control); + } +} + +void +simulate_observation(const std::vector<SystemModelT::StateT>& states, + std::vector<SystemModelT::ObsT>& observations) +{ + for (const auto& state : states) + { + SystemModelT::ObsT obs = SystemModelT::observationFunction(state); + SystemModelT::ObsT true_obs; + true_obs = state.pose.log().coeffs(); + // true_obs.segment(0, 3) = state.pose.translation(); + // true_obs.segment(3, 3) = state.pose.asSO3().log().coeffs(); + ARMARX_CHECK((obs - true_obs).norm() < std::numeric_limits<T>::epsilon()); + observations.push_back(obs + obs_noise_std * SystemModelT::ObsT::Random()); + } +} + + +BOOST_AUTO_TEST_CASE(se3KalmanFilterTest) +{ + srand(time(NULL)); + + test_retract(); + // test_se3(); + + std::vector<SystemModelT::StateT> states; + std::vector<SystemModelT::ControlT> omegas; + std::vector<SystemModelT::ObsT> observations; + simulate_trajectory(states, omegas); + simulate_observation(states, observations); + + ARMARX_INFO << "Num States: " << states.size() << " Num Controls: " << omegas.size() + << " Num Obs: " << observations.size(); + UnscentedKalmanFilter<SystemModelT>::PropCovT Q = + UnscentedKalmanFilter<SystemModelT>::PropCovT::Identity(); + Q.block<3, 3>(0, 0) *= acc_noise_std * acc_noise_std; + Q.block<3, 3>(3, 3) *= om_noise_std * om_noise_std; + + UnscentedKalmanFilter<SystemModelT>::ObsCovT R = + UnscentedKalmanFilter<SystemModelT>::ObsCovT::Identity() * obs_noise_std * obs_noise_std; + UnscentedKalmanFilter<SystemModelT>::StateCovT P0 = + UnscentedKalmanFilter<SystemModelT>::StateCovT::Identity(); + P0.block<3, 3>(3, 3) *= initial_offset_angle * initial_offset_angle; + P0.block<3, 3>(0, 0) *= initial_offet_pos.norm() * initial_offet_pos.norm(); + // P0.block<3, 3>(6, 6) *= 0.0; + UnscentedKalmanFilter<SystemModelT>::AlphaT alpha = + UnscentedKalmanFilter<SystemModelT>::AlphaT::Ones() * 1e-3; + + SystemModelT::StateT state0; + manif::SO3<T> rot(0, 0, initial_offset_angle); + state0.pose = states.at(0).pose.lplus(manif::SE3<T>(initial_offet_pos, rot).log()); + // state0.velocity = states.at(0).velocity; + UnscentedKalmanFilter<SystemModelT> ukf(Q, R, alpha, state0, P0); + + std::vector<SystemModelT::StateT> ukf_states; + std::vector<UnscentedKalmanFilter<SystemModelT>::StateCovT> ukf_Ps; + ukf_states.push_back(state0); + ukf_Ps.push_back(P0); + + std::vector<T> x_true, y_true, z_true, x_obs, y_obs, z_obs, x_ukf, y_ukf, z_ukf; + std::vector<T> vx_true, vy_true, vz_true, vx_ukf, vy_ukf, vz_ukf; + std::vector<T> a_true, b_true, c_true, a_obs, b_obs, c_obs, a_ukf, b_ukf, c_ukf; + + for (int i = 1; i < num_timesteps; i++) + { + // propagate + TIMING_START(LOOP); + TIMING_START(PROPAGATION); + ukf.propagation(omegas.at(i - 1), dt); + TIMING_END(PROPAGATION); + if ((i - 1) % 100 == 0) + { + TIMING_START(UPDATE); + ukf.update(observations.at(i)); + TIMING_END(UPDATE); + TIMING_START(REST); + const SystemModelT::StateT& current_state = ukf.getCurrentState(); + // ARMARX_INFO << "Velocity Diff: " << (states.at(i).velocity - current_state.velocity).norm(); + ARMARX_INFO << "Pose Diff: " + << (states.at(i).pose - current_state.pose).coeffs().norm(); + // ARMARX_INFO << "Vel: " << current_state.velocity - states.at(i).velocity; + ARMARX_INFO << "Max Cov " << ukf.getCurrentStateCovariance().cwiseAbs().maxCoeff(); + ARMARX_INFO + << "Diag: " + << (ukf.getCurrentStateCovariance() - Eigen::Matrix<T, 6, 6>::Identity()).norm(); + // ARMARX_CHECK((states.at(i).position - current_state.position).norm() < 1); + + x_true.push_back(states.at(i).pose.log().coeffs()(0)); + y_true.push_back(states.at(i).pose.log().coeffs()(1)); + z_true.push_back(states.at(i).pose.log().coeffs()(2)); + + x_obs.push_back(observations.at(i)(0)); + y_obs.push_back(observations.at(i)(1)); + z_obs.push_back(observations.at(i)(2)); + + x_ukf.push_back(ukf.getCurrentState().pose.log().coeffs()(0)); + y_ukf.push_back(ukf.getCurrentState().pose.log().coeffs()(1)); + z_ukf.push_back(ukf.getCurrentState().pose.log().coeffs()(2)); + + // vx_true.push_back(states.at(i).velocity(0, 0)); + // vy_true.push_back(states.at(i).velocity(1, 0)); + // vz_true.push_back(states.at(i).velocity(2, 0)); + // + // vx_ukf.push_back(ukf.getCurrentState().velocity(0, 0)); + // vy_ukf.push_back(ukf.getCurrentState().velocity(1, 0)); + // vz_ukf.push_back(ukf.getCurrentState().velocity(2, 0)); + + a_true.push_back(states.at(i).pose.log().coeffs()(3)); + b_true.push_back(states.at(i).pose.log().coeffs()(4)); + c_true.push_back(states.at(i).pose.log().coeffs()(5)); + + a_obs.push_back(observations.at(i)(3, 0)); + b_obs.push_back(observations.at(i)(4, 0)); + c_obs.push_back(observations.at(i)(5, 0)); + + a_ukf.push_back(ukf.getCurrentState().pose.log().coeffs()(3)); + b_ukf.push_back(ukf.getCurrentState().pose.log().coeffs()(4)); + c_ukf.push_back(ukf.getCurrentState().pose.log().coeffs()(5)); + TIMING_END(REST); + } + + ukf_states.push_back(ukf.getCurrentState()); + ukf_Ps.push_back(ukf.getCurrentStateCovariance()); + + TIMING_END(LOOP); + } + + sciplot::Plot3D position_plot; + sciplot::Plot2D pos_plot; + + position_plot.drawCurve(x_true, y_true, z_true).label("True").lineWidth(1); + position_plot.drawCurve(x_obs, y_obs, z_obs).label("Obs").lineWidth(1); + position_plot.drawCurve(x_ukf, y_ukf, z_ukf).label("UKF").lineWidth(1); + + pos_plot.drawCurve(x_true, y_true).label("True"); + pos_plot.drawCurve(x_obs, y_obs).label("Obs"); + pos_plot.drawCurve(x_ukf, y_ukf).label("UKF"); + + // sciplot::Plot3D vel_plot; + // + // vel_plot.drawCurve(vx_true, vy_true, vz_true).label("True").lineWidth(1); + // vel_plot.drawCurve(vx_ukf, vy_ukf, vz_ukf).label("UKF").lineWidth(1); + + + sciplot::Plot3D orientation_plot; + + orientation_plot.drawCurve(a_true, b_true, c_true).label("True").lineWidth(1); + orientation_plot.drawCurve(a_obs, b_obs, c_obs).label("Obs").lineWidth(1); + orientation_plot.drawCurve(a_ukf, b_ukf, c_ukf).label("UKF").lineWidth(1); + + // Create figure to hold plot + sciplot::Figure fig = {{pos_plot}, {position_plot}, {orientation_plot}}; + // Create canvas to hold figure + sciplot::Canvas canvas = {{fig}}; + canvas.size(600, 600); + + // Show the plot in a pop-up window + canvas.show(); + + // Save the plot to a PDF file + canvas.save("kalman_output.pdf"); +} diff --git a/source/armarx/navigation/human/test/findings/Notice.md b/source/armarx/navigation/human/test/findings/Notice.md new file mode 100644 index 0000000000000000000000000000000000000000..61048442fd9deed08db635a2f2b1c6c172c1fc98 --- /dev/null +++ b/source/armarx/navigation/human/test/findings/Notice.md @@ -0,0 +1,18 @@ +# Different configurations of the kalman filter test: + +- start_* is the start angle of the simulated trajectory +- mod_* is the modulo value used to define how often the kalman filter is updated +- error is the noise added to the measurements + - low: 0.01 + - high: 0.1 +- offset is the initial position offset: + - blank: 0.1 + - high: 0.5 (1 for the alternative models) +- dirty hack: skip orientations close to pi + +Alternative models use different calculations for retract and inverseRetract + +- normal: lplus(tan), 2.lminus(1), 1-2 +- alternative_model: rplus(tan), 1.lminus(2), 2-1 +- alternative_model2: rplus(tan), 1.lminus(2), 1-2 + diff --git a/source/armarx/navigation/human/test/findings/alternative_model-start_0-mod_100-low_error-dirty_hack.pdf b/source/armarx/navigation/human/test/findings/alternative_model-start_0-mod_100-low_error-dirty_hack.pdf new file mode 100644 index 0000000000000000000000000000000000000000..dcb8673c9ff6ed9fbb63c7d22740bb65ddfac5b2 Binary files /dev/null and b/source/armarx/navigation/human/test/findings/alternative_model-start_0-mod_100-low_error-dirty_hack.pdf differ diff --git a/source/armarx/navigation/human/test/findings/alternative_model-start_0-mod_100-low_error.pdf b/source/armarx/navigation/human/test/findings/alternative_model-start_0-mod_100-low_error.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8727bb0cf9a7c758407d3542771e16dd6ff4ca7c Binary files /dev/null and b/source/armarx/navigation/human/test/findings/alternative_model-start_0-mod_100-low_error.pdf differ diff --git a/source/armarx/navigation/human/test/findings/alternative_model-start_0-mod_101-high_error-high_offset.pdf b/source/armarx/navigation/human/test/findings/alternative_model-start_0-mod_101-high_error-high_offset.pdf new file mode 100644 index 0000000000000000000000000000000000000000..934d6ddc327f7f50022048264af1854d8fdccbdd Binary files /dev/null and b/source/armarx/navigation/human/test/findings/alternative_model-start_0-mod_101-high_error-high_offset.pdf differ diff --git a/source/armarx/navigation/human/test/findings/alternative_model-start_0-mod_101-high_error.pdf b/source/armarx/navigation/human/test/findings/alternative_model-start_0-mod_101-high_error.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e03efc8c2789142d0bc4ae68e80f665922019e52 Binary files /dev/null and b/source/armarx/navigation/human/test/findings/alternative_model-start_0-mod_101-high_error.pdf differ diff --git a/source/armarx/navigation/human/test/findings/alternative_model-start_0-mod_101-low_error.pdf b/source/armarx/navigation/human/test/findings/alternative_model-start_0-mod_101-low_error.pdf new file mode 100644 index 0000000000000000000000000000000000000000..dcf06f312ad2b02bf07cb0c964adc10bd30f2230 Binary files /dev/null and b/source/armarx/navigation/human/test/findings/alternative_model-start_0-mod_101-low_error.pdf differ diff --git a/source/armarx/navigation/human/test/findings/alternative_model2-start_0-mod_100-low_error-dirty_hack.pdf b/source/armarx/navigation/human/test/findings/alternative_model2-start_0-mod_100-low_error-dirty_hack.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a6027ca201421ad9d2782af4304b31b09f22a614 Binary files /dev/null and b/source/armarx/navigation/human/test/findings/alternative_model2-start_0-mod_100-low_error-dirty_hack.pdf differ diff --git a/source/armarx/navigation/human/test/findings/alternative_model2-start_0-mod_101-high_error-high_offset.pdf b/source/armarx/navigation/human/test/findings/alternative_model2-start_0-mod_101-high_error-high_offset.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b629c7e6242145463bb012fdd78a014c93388d88 Binary files /dev/null and b/source/armarx/navigation/human/test/findings/alternative_model2-start_0-mod_101-high_error-high_offset.pdf differ diff --git a/source/armarx/navigation/human/test/findings/alternative_model2-start_0-mod_101-high_error.pdf b/source/armarx/navigation/human/test/findings/alternative_model2-start_0-mod_101-high_error.pdf new file mode 100644 index 0000000000000000000000000000000000000000..643e9787e7f41d39d88fbbd0a53b6714c8021ab4 Binary files /dev/null and b/source/armarx/navigation/human/test/findings/alternative_model2-start_0-mod_101-high_error.pdf differ diff --git a/source/armarx/navigation/human/test/findings/alternative_model2-start_0-mod_101-low_error.pdf b/source/armarx/navigation/human/test/findings/alternative_model2-start_0-mod_101-low_error.pdf new file mode 100644 index 0000000000000000000000000000000000000000..aa24b385b403d604fb3737e340d118a4ded0f878 Binary files /dev/null and b/source/armarx/navigation/human/test/findings/alternative_model2-start_0-mod_101-low_error.pdf differ diff --git a/source/armarx/navigation/human/test/findings/start_0-mod_10-low_error-dirty_hack.pdf b/source/armarx/navigation/human/test/findings/start_0-mod_10-low_error-dirty_hack.pdf new file mode 100644 index 0000000000000000000000000000000000000000..cfa0646aa7a315504cbd4f24d74b949788b8922b Binary files /dev/null and b/source/armarx/navigation/human/test/findings/start_0-mod_10-low_error-dirty_hack.pdf differ diff --git a/source/armarx/navigation/human/test/findings/start_0-mod_10-low_error.pdf b/source/armarx/navigation/human/test/findings/start_0-mod_10-low_error.pdf new file mode 100644 index 0000000000000000000000000000000000000000..3a16edf0b390d7664d6e3f452cbadbd0e8df0ae2 Binary files /dev/null and b/source/armarx/navigation/human/test/findings/start_0-mod_10-low_error.pdf differ diff --git a/source/armarx/navigation/human/test/findings/start_0-mod_100-low_error-dirty_hack.pdf b/source/armarx/navigation/human/test/findings/start_0-mod_100-low_error-dirty_hack.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2b6a6085f88d032e3807c9c697596e17be2cad0d Binary files /dev/null and b/source/armarx/navigation/human/test/findings/start_0-mod_100-low_error-dirty_hack.pdf differ diff --git a/source/armarx/navigation/human/test/findings/start_0-mod_100-low_error.pdf b/source/armarx/navigation/human/test/findings/start_0-mod_100-low_error.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8da253e7f6f68998d8c5d99491f7054a938f288c Binary files /dev/null and b/source/armarx/navigation/human/test/findings/start_0-mod_100-low_error.pdf differ diff --git a/source/armarx/navigation/human/test/findings/start_0-mod_101-high_error-high_offset.pdf b/source/armarx/navigation/human/test/findings/start_0-mod_101-high_error-high_offset.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8cea1d6550d228ecc2560e0b6d161468df77155a Binary files /dev/null and b/source/armarx/navigation/human/test/findings/start_0-mod_101-high_error-high_offset.pdf differ diff --git a/source/armarx/navigation/human/test/findings/start_0-mod_101-high_error.pdf b/source/armarx/navigation/human/test/findings/start_0-mod_101-high_error.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5025819c3db55cb23db1b746584e3f995ca67a77 Binary files /dev/null and b/source/armarx/navigation/human/test/findings/start_0-mod_101-high_error.pdf differ diff --git a/source/armarx/navigation/human/test/findings/start_0-mod_101-low_error.pdf b/source/armarx/navigation/human/test/findings/start_0-mod_101-low_error.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8c3dd640942f6580b83c934beb0b16b39ceba9a8 Binary files /dev/null and b/source/armarx/navigation/human/test/findings/start_0-mod_101-low_error.pdf differ diff --git a/source/armarx/navigation/human/test/findings/start_0-mod_11-low_error.pdf b/source/armarx/navigation/human/test/findings/start_0-mod_11-low_error.pdf new file mode 100644 index 0000000000000000000000000000000000000000..36aeefc8fa67abab03f8c55b611d7aee3fb55842 Binary files /dev/null and b/source/armarx/navigation/human/test/findings/start_0-mod_11-low_error.pdf differ diff --git a/source/armarx/navigation/human/test/findings/start_01-mod_10-low_error.pdf b/source/armarx/navigation/human/test/findings/start_01-mod_10-low_error.pdf new file mode 100644 index 0000000000000000000000000000000000000000..608e403453645f5a6cd7581c3f9da443e2695c57 Binary files /dev/null and b/source/armarx/navigation/human/test/findings/start_01-mod_10-low_error.pdf differ diff --git a/source/armarx/navigation/human/test/findings/start_01-mod_100-low_error.pdf b/source/armarx/navigation/human/test/findings/start_01-mod_100-low_error.pdf new file mode 100644 index 0000000000000000000000000000000000000000..81dc12e0c98617dbbcdc0a51400d4e2ceef72d6c Binary files /dev/null and b/source/armarx/navigation/human/test/findings/start_01-mod_100-low_error.pdf differ diff --git a/source/armarx/navigation/human/test/findings/start_pi_2-mod_10-low_error.pdf b/source/armarx/navigation/human/test/findings/start_pi_2-mod_10-low_error.pdf new file mode 100644 index 0000000000000000000000000000000000000000..662647f3cdbc8cee6e7eec0dbe4998665c6f9e87 Binary files /dev/null and b/source/armarx/navigation/human/test/findings/start_pi_2-mod_10-low_error.pdf differ diff --git a/source/armarx/navigation/human/test/findings/start_pi_2-mod_10-low_error_dirty-hack.pdf b/source/armarx/navigation/human/test/findings/start_pi_2-mod_10-low_error_dirty-hack.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4588d6b1975fd9b8cff13350d14858343a4ce472 Binary files /dev/null and b/source/armarx/navigation/human/test/findings/start_pi_2-mod_10-low_error_dirty-hack.pdf differ diff --git a/source/armarx/navigation/human/test/findings/start_pi_2-mod_100-low_error.pdf b/source/armarx/navigation/human/test/findings/start_pi_2-mod_100-low_error.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c1a797f1ca66b3105a20946a6d6ea0f259472f1b Binary files /dev/null and b/source/armarx/navigation/human/test/findings/start_pi_2-mod_100-low_error.pdf differ diff --git a/source/armarx/navigation/human/test/kalman_filter_test.cpp b/source/armarx/navigation/human/test/kalman_filter_test.cpp new file mode 100644 index 0000000000000000000000000000000000000000..04cf7c69a1d530d61d172871ec1eca58a03a8729 --- /dev/null +++ b/source/armarx/navigation/human/test/kalman_filter_test.cpp @@ -0,0 +1,112 @@ +/** + * This file is part of ArmarX. + * + * ArmarX is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * ArmarX is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @author Corvin Navarro Ecker ( corvin dot ecker at student dot kit dot edu ) + * @date 2022 + * @copyright http://www.gnu.org/licenses/gpl-2.0.txt + * GNU General Public License + */ + +#include <ArmarXCore/core/time/DateTime.h> + +#include <armarx/navigation/core/basic_types.h> +#include <armarx/navigation/human/HumanFilter.h> +#include <armarx/navigation/human/types.h> + +// test includes +#define BOOST_TEST_MODULE Navigation::ArmarXLibraries::human +#define ARMARX_BOOST_TEST + +#include <armarx/navigation/Test.h> + +using armarx::navigation::core::Pose2D; +using Eigen::Vector2f; + +namespace armarx::navigation::human +{ + Pose2D + generatePose(double x, double y, double yaw) + { + Pose2D pose = Pose2D::Identity(); + pose.translation() = Eigen::Vector2f(x, y); + pose.linear() = Eigen::Rotation2Df(yaw).toRotationMatrix(); + + return pose; + } + + DateTime + generateTime(double time) + { + return DateTime(time); + } + + double + extractRotation(const Pose2D& pose) + { + return Eigen::Rotation2Df(pose.linear()).angle(); + } + + + bool + doublesEqual(const double d1, const double d2, const double relErr) + { + if (d1 == 0) + { + return d2 == 0; + } + return std::abs(d1 - d2) / d1 <= relErr; + } + + bool + posesEqual(const Pose2D& p1, const Pose2D& p2, const double relErr) + { + return doublesEqual(p1.translation().x(), p2.translation().x(), relErr) && + doublesEqual(p1.translation().y(), p2.translation().y(), relErr) && + doublesEqual(extractRotation(p1), extractRotation(p2), relErr); + } + + bool + vectorsEqual(const Vector2f& v1, const Vector2f v2, const double relErr) + { + return doublesEqual(v1.x(), v2.x(), relErr) && doublesEqual(v1.y(), v2.y(), relErr); + } + + bool + timesEqual(const DateTime& t1, const DateTime& t2, const double relErr) + { + return doublesEqual(t1.toMilliSecondsSinceEpoch(), t2.toMilliSecondsSinceEpoch(), relErr); + } + + bool + humansEqual(const Human& h1, const Human& h2, const double relErr) + { + return posesEqual(h1.pose, h2.pose, relErr) && + vectorsEqual(h1.linearVelocity, h2.linearVelocity, relErr) && + timesEqual(h1.detectionTime, h2.detectionTime, relErr); + } + + + BOOST_AUTO_TEST_CASE(testInitialization) + { + HumanFilter filter(generatePose(3, 2, 1), generateTime(4)); + + Human human{.pose = generatePose(3, 2, 1), + .linearVelocity = Vector2f::Zero(), + .detectionTime = generateTime(4)}; + + BOOST_TEST(humansEqual(filter.get(), human, 0.001), + "filter should not have changed humans initial pose properties"); + } +} // namespace armarx::navigation::human diff --git a/source/armarx/navigation/human/test/manifKalmanTest.cpp b/source/armarx/navigation/human/test/manifKalmanTest.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4940ab7cd36227965209ac837a676b265816f06a --- /dev/null +++ b/source/armarx/navigation/human/test/manifKalmanTest.cpp @@ -0,0 +1,462 @@ +/** + * \file se2_localization_ukfm.cpp + * + * Created on: Dec 10, 2018 + * \author: artivis + * + * --------------------------------------------------------- + * This file is: + * (c) 2021 Jeremie Deray + * + * This file is part of `manif`, a C++ template-only library + * for Lie theory targeted at estimation for robotics. + * Manif is: + * (c) 2018 Jeremie Deray @ IRI-UPC, Barcelona + * --------------------------------------------------------- + * + * --------------------------------------------------------- + * Demonstration example: + * + * 2D Robot localization based on fixed beacons. + * + * See se3_localization.cpp for the 3D equivalent. + * See se3_sam.cpp for a more advanced example performing smoothing and mapping. + * --------------------------------------------------------- + * + * This demo showcases an application of an Unscented Kalman Filter on Manifold, + * based on the paper + * 'A Code for Unscented Kalman Filtering on Manifolds (UKF-M)' + * [https://arxiv.org/pdf/2002.00878.pdf], M. Brossard, A. Barrau and S. Bonnabel. + * + * The following is an abstract of the example hereafter. + * Please consult the aforemention paper for better UKF-M reference + * and the paper Sola-18, [https://arxiv.org/abs/1812.01537] for general + * Lie group reference. + * + * + * We consider a robot in the plane surrounded by a small + * number of punctual landmarks or _beacons_. + * The robot receives control actions in the form of axial + * and angular velocities, and is able to measure the location + * of the beacons w.r.t its own reference frame. + * + * The robot pose X is in SE(2) and the beacon positions b_k in R^2, + * + * | cos th -sin th x | + * X = | sin th cos th y | // position and orientation + * | 0 0 1 | + * + * b_k = (bx_k, by_k) // lmk coordinates in world frame + * + * The control signal u is a twist in se(2) comprising longitudinal + * velocity v and angular velocity w, with no lateral velocity + * component, integrated over the sampling time dt. + * + * u = (v*dt, 0, w*dt) + * + * The control is corrupted by additive Gaussian noise u_noise, + * with covariance + * + * Q = diagonal(sigma_v^2, sigma_s^2, sigma_w^2). + * + * This noise accounts for possible lateral slippage u_s + * through a non-zero value of sigma_s, + * + * At the arrival of a control u, the robot pose is updated + * with X <-- X * Exp(u) = X + u. + * + * Landmark measurements are of the range and bearing type, + * though they are put in Cartesian form for simplicity. + * Their noise n is zero mean Gaussian, and is specified + * with a covariances matrix R. + * We notice the rigid motion action y = h(X,b) = X^-1 * b + * (see appendix C), + * + * y_k = (brx_k, bry_k) // lmk coordinates in robot frame + * + * We consider the beacons b_k situated at known positions. + * We define the pose to estimate as X in SE(2). + * The estimation error dx and its covariance P are expressed + * in the tangent space at X. + * + * All these variables are summarized again as follows + * + * X : robot pose, SE(2) + * u : robot control, (v*dt ; 0 ; w*dt) in se(2) + * Q : control perturbation covariance + * b_k : k-th landmark position, R^2 + * y : Cartesian landmark measurement in robot frame, R^2 + * R : covariance of the measurement noise + * + * The motion and measurement models are + * + * X_(t+1) = f(X_t, u) = X_t * Exp ( w ) // motion equation + * y_k = h(X, b_k) = X^-1 * b_k // measurement equation + * + * The algorithm below comprises first a simulator to + * produce measurements, then uses these measurements + * to estimate the state, using a Lie-based error-state Kalman filter. + * + * This file has plain code with only one main() function. + * There are no function calls other than those involving `manif`. + * + * Printing simulated state and estimated state together + * with an unfiltered state (i.e. without Kalman corrections) + * allows for evaluating the quality of the estimates. + */ + +#include <iomanip> +#include <iostream> +#include <tuple> +#include <vector> + +#include <Eigen/Cholesky> + +#include "manif/SE2.h" +#include <sciplot/sciplot.hpp> + +using std::cout; +using std::endl; + +using namespace Eigen; + +typedef Array<double, 2, 1> Array2d; +typedef Array<double, 3, 1> Array3d; + +template <typename Scalar> +struct Weights +{ + Weights() = default; + ~Weights() = default; + + Weights(const Scalar l, const Scalar alpha) + { + using std::sqrt; + + const Scalar m = (alpha * alpha - 1) * l; + const Scalar ml = m + l; + + sqrt_d_lambda = sqrt(ml); + wj = Scalar(1) / (Scalar(2) * (ml)); + wm = m / (ml); + w0 = m / (ml) + Scalar(3) - alpha * alpha; + } + + Scalar sqrt_d_lambda; + Scalar wj; + Scalar wm; + Scalar w0; +}; + +using Weightsd = Weights<double>; + +template <typename Scalar> +std::tuple<Weights<Scalar>, Weights<Scalar>, Weights<Scalar>> +compute_sigma_weights(const Scalar state_size, + const Scalar propagation_noise_size, + const Scalar alpha_0, + const Scalar alpha_1, + const Scalar alpha_2) +{ + assert(state_size > 0); + assert(propagation_noise_size > 0); + assert(alpha_0 >= 1e-3 && alpha_0 <= 1); + assert(alpha_1 >= 1e-3 && alpha_1 <= 1); + assert(alpha_2 >= 1e-3 && alpha_2 <= 1); + + return std::make_tuple(Weights<Scalar>(state_size, alpha_0), + Weights<Scalar>(propagation_noise_size, alpha_1), + Weights<Scalar>(state_size, alpha_2)); +} + +int +main() +{ + std::srand((unsigned int)time(0)); + + // START CONFIGURATION + // + // + const int NUMBER_OF_LMKS_TO_MEASURE = 3; + constexpr int DoF = manif::SE2d::DoF; + constexpr int SystemNoiseSize = manif::SE2d::DoF; + // Measurement Dim + constexpr int Rp = 2; + + // Define the robot pose element and its covariance + manif::SE2d X = manif::SE2d::Identity(), X_simulation = manif::SE2d::Identity(), + X_unfiltered = manif::SE2d::Identity(); + Matrix3d P = Matrix3d::Identity() * 1e-6; + + // Define a control vector and its noise and covariance + manif::SE2Tangentd u_simu, u_est, u_unfilt; + Vector3d u_nom, u_noisy, u_noise; + Array3d u_sigmas; + Matrix3d U, Uchol; + + u_nom << 0.1, 0.0, 0.05; + u_sigmas << 0.1, 0.1, 0.1; + U = (u_sigmas * u_sigmas).matrix().asDiagonal(); + Uchol = U.llt().matrixL(); + + // Define three landmarks in R^2 + Eigen::Vector2d b; + const std::vector<Eigen::Vector2d> landmarks{ + Eigen::Vector2d(2.0, 0.0), Eigen::Vector2d(2.0, 1.0), Eigen::Vector2d(2.0, -1.0)}; + + // Define the beacon's measurements + Vector2d y, y_bar, y_noise; + Matrix<double, Rp, 2 * DoF> yj; + Array2d y_sigmas; + Matrix2d R; + std::vector<Vector2d> measurements(landmarks.size()); + + y_sigmas << 0.01, 0.01; + R = (y_sigmas * y_sigmas).matrix().asDiagonal(); + + + // Declare UFK variables + Array3d alpha; + alpha << 1e-3, 1e-3, 1e-3; + + Weightsd w_d, w_q, w_u; + std::tie(w_d, w_q, w_u) = compute_sigma_weights<double>(DoF, Rp, alpha(0), alpha(1), alpha(2)); + + // Declare some temporaries + + manif::SE2d X_new; + Matrix3d P_new; + manif::SE2d s_j_p, s_j_m; + Vector3d xi_mean; + Vector3d w_p, w_m; + + Matrix2d P_yy; + Matrix<double, DoF, 2 * DoF> xij; + Matrix<double, DoF, 2> P_xiy; + + Vector2d e, z; // expectation, innovation + Matrix<double, 3, 2> K; // Kalman gain + manif::SE2Tangentd dx; // optimal update step, or error-state + + Matrix<double, DoF, DoF> xis; + Matrix<double, DoF, DoF * 2> xis_new; + Matrix<double, DoF, SystemNoiseSize * 2> xis_new2; + + // + // + // CONFIGURATION DONE + + + // DEBUG + cout << std::fixed << std::setprecision(4) << std::showpos << endl; + cout << "X STATE : X Y THETA" << endl; + cout << "----------------------------------" << endl; + cout << "X initial : " << X_simulation.log().coeffs().transpose() << endl; + cout << "----------------------------------" << endl; + // END DEBUG + + + std::vector<double> X_simulated_x, X_simulated_y, X_simulated_theta; + std::vector<double> X_estimated_x, X_estimated_y, X_estimated_theta; + std::vector<double> X_unfiltered_x, X_unfiltered_y, X_unfiltered_theta; + + // START TEMPORAL LOOP + // + // + + // Make 10 steps. Measure up to three landmarks each time. + for (int t = 0; t < 10; t++) + { + //// I. Simulation ############################################################################### + + /// simulate noise + u_noise = u_sigmas * Array3d::Random(); // control noise + u_noisy = u_nom + u_noise; // noisy control + + u_simu = u_nom; + u_est = u_noisy; + u_unfilt = u_noisy; + + /// first we move - - - - - - - - - - - - - - - - - - - - - - - - - - - - + X_simulation = X_simulation + u_simu; // overloaded X.rplus(u) = X * exp(u) + + /// then we measure all landmarks - - - - - - - - - - - - - - - - - - - - + for (std::size_t i = 0; i < landmarks.size(); i++) + { + b = landmarks[i]; // lmk coordinates in world frame + + /// simulate noise + y_noise = y_sigmas * Array2d::Random(); // measurement noise + + y = X_simulation.inverse().act(b); // landmark measurement, before adding noise + y = y + y_noise; // landmark measurement, noisy + measurements[i] = y; // store for the estimator just below + } + + + //// II. Estimation ############################################################################### + + /// First we move - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + X_new = X + u_est; // X * exp(u) + + // set sigma points + xis = w_d.sqrt_d_lambda * P.llt().matrixL().toDenseMatrix(); + + // sigma points on manifold + for (int i = 0; i < DoF; ++i) + { + s_j_p = X + manif::SE2Tangentd(xis.col(i)); + s_j_m = X + manif::SE2Tangentd(-xis.col(i)); + + xis_new.col(i) = (X_new.lminus(s_j_p + u_est)).coeffs(); + xis_new.col(i + DoF) = (X_new.lminus(s_j_m + u_est)).coeffs(); + } + + // compute covariance + xi_mean = w_d.wj * xis_new.rowwise().sum(); + xis_new.colwise() -= xi_mean; + + P_new = w_d.wj * xis_new * xis_new.transpose() + w_d.w0 * xi_mean * xi_mean.transpose(); + + // sigma points on manifold + for (int i = 0; i < SystemNoiseSize; ++i) + { + w_p = w_q.sqrt_d_lambda * Uchol.col(i); + w_m = -w_q.sqrt_d_lambda * Uchol.col(i); + + xis_new2.col(i) = (X_new.lminus(X + (u_est + w_p))).coeffs(); + xis_new2.col(i + SystemNoiseSize) = (X_new.lminus(X + (u_est + w_m))).coeffs(); + } + + xi_mean = w_q.wj * xis_new2.rowwise().sum(); + xis_new2.colwise() -= xi_mean; + + U = w_q.wj * xis_new2 * xis_new2.transpose() + w_q.w0 * xi_mean * xi_mean.transpose(); + + P = P_new + U; + + X = X_new; + + /// Then we correct using the measurements of each lmk - - - - - - - - - + for (int i = 0; i < NUMBER_OF_LMKS_TO_MEASURE; i++) + { + // landmark + b = landmarks[i]; // lmk coordinates in world frame + + // measurement + y = measurements[i]; // lmk measurement, noisy + + // expectation + e = X.inverse().act(b); + + // set sigma points + xis = w_u.sqrt_d_lambda * P.llt().matrixL().toDenseMatrix(); + + // compute measurement sigma points + for (int d = 0; d < DoF; ++d) + { + s_j_p = X + manif::SE2Tangentd(xis.col(d)); + s_j_m = X + manif::SE2Tangentd(-xis.col(d)); + + yj.col(d) = s_j_p.inverse().act(b); + yj.col(d + DoF) = s_j_m.inverse().act(b); + } + + // measurement mean + y_bar = w_u.wm * e + w_u.wj * yj.rowwise().sum(); + + yj.colwise() -= y_bar; + e -= y_bar; + + // compute covariance and cross covariance matrices + P_yy = w_u.w0 * e * e.transpose() + w_u.wj * yj * yj.transpose() + R; + + xij << xis, -xis; + P_xiy = w_u.wj * xij * yj.transpose(); + + // Kalman gain + K = P_yy.colPivHouseholderQr().solve(P_xiy.transpose()).transpose(); + + // innovation + z = y - y_bar; + + // Correction step + dx = K * z; // dx is in the tangent space at X + + // Update + X = X + dx; // overloaded X.rplus(dx) = X * exp(dx) + P = P - K * P_yy * K.transpose(); + } + + + //// III. Unfiltered ############################################################################## + + // move also an unfiltered version for comparison purposes + X_unfiltered = X_unfiltered + u_unfilt; + + + //// IV. Results ############################################################################## + + // DEBUG + cout << "X simulated : " << X_simulation.log().coeffs().transpose() << "\n"; + cout << "X estimated : " << X.log().coeffs().transpose() << "\n"; + cout << "X unfilterd : " << X_unfiltered.log().coeffs().transpose() << "\n"; + cout << "----------------------------------" << endl; + + const auto& X_simu = X_simulation.log().coeffs().transpose(); + const auto& X_est = X.log().coeffs().transpose(); + const auto& X_unf = X_unfiltered.log().coeffs().transpose(); + X_simulated_x.push_back(X_simu(0)); + X_simulated_y.push_back(X_simu(1)); + X_simulated_theta.push_back(X_simu(2)); + X_estimated_x.push_back(X_est(0)); + X_estimated_y.push_back(X_est(1)); + X_estimated_theta.push_back(X_est(2)); + X_unfiltered_x.push_back(X_unf(0)); + X_unfiltered_y.push_back(X_unf(1)); + X_unfiltered_theta.push_back(X_unf(2)); + // END DEBUG + } + + sciplot::Plot2D pos_plot; + pos_plot.xlabel("x"); + pos_plot.ylabel("y"); + pos_plot.legend().atOutsideBottom().displayHorizontal().displayExpandWidthBy(2); + pos_plot.xrange("*", "*").yrange("*", "*"); + + pos_plot.drawCurve(X_simulated_x, X_simulated_y).label("Simulated"); + pos_plot.drawCurve(X_estimated_x, X_estimated_y).label("Estimated"); + pos_plot.drawCurve(X_unfiltered_x, X_unfiltered_y).label("Unfiltered"); + + + sciplot::Plot2D orientation_plot; + orientation_plot.xlabel("t"); + orientation_plot.ylabel("rot"); + orientation_plot.legend().atOutsideBottom().displayHorizontal().displayExpandWidthBy(2); + orientation_plot.xrange("*", "*").yrange("*", "*"); + + sciplot::Vec x = sciplot::linspace(0.0, 1.0, X_estimated_theta.size()); + orientation_plot.drawCurve(x, X_simulated_theta).label("Simulated"); + orientation_plot.drawCurve(x, X_estimated_theta).label("Estimated"); + orientation_plot.drawCurve(x, X_unfiltered_theta).label("Unfiltered"); + + // Create figure to hold plot + sciplot::Figure fig = {{pos_plot, orientation_plot}}; + // Create canvas to hold figure + sciplot::Canvas canvas = {{fig}}; + canvas.size(1000, 500); + + // Show the plot in a pop-up window + canvas.show(); + + // Save the plot to a PDF file + canvas.save("manif_kalman_output.pdf"); + + + // + // + // END OF TEMPORAL LOOP. DONE. + + return 0; +} diff --git a/source/armarx/navigation/human/test/se2KalmanFilterTest.cpp b/source/armarx/navigation/human/test/se2KalmanFilterTest.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c83f4db5b4fedc49a6af0077229a075ea9e7cd25 --- /dev/null +++ b/source/armarx/navigation/human/test/se2KalmanFilterTest.cpp @@ -0,0 +1,296 @@ +/** + * This file is part of ArmarX. + * + * ArmarX is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * ArmarX is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @author Tobias Gröger ( tobias dot groeger at student dot kit dot edu ) + * @date 2022 + * @copyright http://www.gnu.org/licenses/gpl-2.0.txt + * GNU General Public License + */ + +#define BOOST_TEST_MODULE Navigation::ArmarXLibraries::human +#define ARMARX_BOOST_TEST + +#include <cstdlib> /* srand, rand */ +#include <ctime> /* time */ + +#include <ArmarXCore/core/exceptions/local/ExpressionException.h> +#include <ArmarXCore/core/logging/Logging.h> +#include <ArmarXCore/util/time.h> + +#include <RobotAPI/libraries/ukfm/UnscentedKalmanFilter.h> + +#include <armarx/navigation/Test.h> +#include <armarx/navigation/human/HumanSystemModel.h> +#include <manif/SO2.h> +#include <sciplot/sciplot.hpp> + +namespace armarx::navigation::human +{ + using T = double; //TODO double or float? + using Vector = Eigen::Matrix<T, 2, 1>; + using SystemModelT = kalman_filter::SystemModelSE2<T>; + + constexpr T RAD = 10; + constexpr long num_timesteps = 3000; + constexpr T max_time = 1; + constexpr T dt = max_time / num_timesteps; + constexpr T c = (1 / max_time) * 2 * M_PI; // rotation per time + + constexpr T rot_noise_std = 0.1; + constexpr T pos_noise_std = 1; + constexpr T obs_rot_noise_std = 0.1; + constexpr T obs_pos_noise_std = 1; + constexpr T initial_offset_angle = 0.1 * 10 * M_PI / 180; + const Vector initial_offet_pos = 0.1 * RAD * Vector(1, 0.5); + + + void + simulate_trajectory(std::vector<SystemModelT::StateT>& states, + std::vector<SystemModelT::ControlT>& omegas) + { + constexpr T startAngle = 0; + + SystemModelT::StateT state; + state.pose = manif::SE2<T>(RAD, 0, startAngle); + states.push_back(state); + + for (int i = 1; i < num_timesteps; i++) + { + const T t = dt * i; + const T angle = t * c; + + const Vector last_pos = states.at(i - 1).pose.translation(); + //const Vector pos(angle, std::cos(angle)); + const Vector pos = RAD * Vector(std::cos(angle), std::sin(angle)); + + BOOST_TEST(((last_pos - pos).norm() < 0.1 * RAD), + "position differs too much from last step: " << (last_pos - pos).norm()); + + Eigen::Vector2d euclidean_velocity = RAD * Eigen::Vector2d{0, c}; + SystemModelT::ControlT control; + //control.velocity.coeffs() << (pos - last_pos) / dt, c; + control.velocity.coeffs() << euclidean_velocity, c; + + const SystemModelT::StateT propagated = SystemModelT::propagationFunction( + state, control, SystemModelT::ControlNoiseT::Zero(), dt); + + state.pose = manif::SE2<T>(pos.x(), pos.y(), angle + startAngle); + + T pos_diff = (propagated.pose.translation() - state.pose.translation()).norm(); + BOOST_TEST(pos_diff < 2e-10, + "propagated position differs too much from real position: " << pos_diff); + + T rot_diff = propagated.pose.angle() - state.pose.angle(); + BOOST_TEST(rot_diff < 2e-10, + "propagated rotation differs too much from real orientation: " << rot_diff); + + states.push_back(propagated); + + + // add noise + control.velocity.coeffs().segment(0, 2) += pos_noise_std * Vector::Random(); + control.velocity.coeffs().segment(2, 1) += + rot_noise_std * Eigen::Matrix<T, 1, 1>::Random(); + omegas.push_back(control); + } + } + + void + simulate_observation(const std::vector<SystemModelT::StateT>& states, + std::vector<SystemModelT::ObsT>& observations) + { + for (const auto& state : states) + { + SystemModelT::ObsT obs = SystemModelT::observationFunction(state); + + SystemModelT::ObsT noise = SystemModelT::ObsT::Random(); + noise.segment(0, 2) *= obs_pos_noise_std; + noise.segment(2, 1) *= obs_rot_noise_std; + + BOOST_TEST_MESSAGE("SALT TEST: noise " << noise); + + observations.push_back(obs + noise); + + BOOST_TEST_MESSAGE("SALT TEST: state:\n" + << state.pose.x() << "\n" + << state.pose.y() << "\n" + << state.pose.angle()); + + BOOST_TEST_MESSAGE("SALT TEST: state.log:\n" << state.pose.log().coeffs()); + + BOOST_TEST_MESSAGE("SALT TEST: noisy obs:\n" << (obs + noise)); + + SystemModelT::ObsT diff = + (obs + noise) - + SystemModelT::ObsT(state.pose.x(), state.pose.y(), state.pose.angle()); + + BOOST_TEST_MESSAGE("\nSALT TEST: diff:\n" << diff); + } + } + + + BOOST_AUTO_TEST_CASE(se2KalmanFilterTest) + { + srand(time(NULL)); + + std::vector<SystemModelT::StateT> states; + std::vector<SystemModelT::ControlT> omegas; + std::vector<SystemModelT::ObsT> observations; + simulate_trajectory(states, omegas); + simulate_observation(states, observations); + + ARMARX_INFO << "Num States: " << states.size() << " Num Controls: " << omegas.size() + << " Num Obs: " << observations.size(); + + UnscentedKalmanFilter<SystemModelT>::PropCovT Q = + UnscentedKalmanFilter<SystemModelT>::PropCovT::Identity(); + Q.block<2, 2>(0, 0) *= pos_noise_std * pos_noise_std; + Q.block<1, 1>(2, 2) *= rot_noise_std * rot_noise_std; + + UnscentedKalmanFilter<SystemModelT>::ObsCovT R = + UnscentedKalmanFilter<SystemModelT>::ObsCovT::Identity(); + R.block<2, 2>(0, 0) *= obs_pos_noise_std * obs_pos_noise_std; + R.block<1, 1>(2, 2) *= obs_rot_noise_std * obs_rot_noise_std; + + UnscentedKalmanFilter<SystemModelT>::StateCovT P0 = + UnscentedKalmanFilter<SystemModelT>::StateCovT::Identity(); + P0.block<2, 2>(0, 0) *= initial_offet_pos.norm() * initial_offet_pos.norm(); + P0.block<1, 1>(2, 2) *= initial_offset_angle * initial_offset_angle; + + UnscentedKalmanFilter<SystemModelT>::AlphaT alpha; + alpha(0) = 0.5; //0.8; // state + alpha(1) = 0.5; //0.1; // control + alpha(2) = 0.5; //0.1; // update + + SystemModelT::StateT state0; + const Vector startPos = states.at(0).pose.translation() + initial_offet_pos; + state0.pose = manif::SE2<T>{ + startPos.x(), startPos.y(), states.at(0).pose.angle() + initial_offset_angle}; + + UnscentedKalmanFilter<SystemModelT> ukf(Q, R, alpha, state0, P0); + + std::vector<SystemModelT::StateT> ukf_states; + std::vector<UnscentedKalmanFilter<SystemModelT>::StateCovT> ukf_Ps; + ukf_states.push_back(state0); + ukf_Ps.push_back(P0); + + std::vector<T> x_true, y_true, x_obs, y_obs, x_ukf, y_ukf; + std::vector<T> a_true, a_obs, a_ukf; + std::vector<T> x_ukf_full, y_ukf_full, a_ukf_full; + std::vector<T> ukf_pos_var, ukf_rot_var, ukf_var; + + for (int i = 1; i < num_timesteps; i++) + { + // propagate + TIMING_START(LOOP); + TIMING_START(PROPAGATION); + ukf.propagation(omegas.at(i - 1), dt); + TIMING_END(PROPAGATION); + if ((i - 1) % 1 == 0) + { + TIMING_START(UPDATE); + ukf.update(observations.at(i)); + TIMING_END(UPDATE); + TIMING_START(REST); + + BOOST_TEST_MESSAGE("Max Cov " + << ukf.getCurrentStateCovariance().cwiseAbs().maxCoeff()); + BOOST_TEST_MESSAGE("Diag: " << (ukf.getCurrentStateCovariance() - + Eigen::Matrix<T, 3, 3>::Identity()) + .norm()); + + manif::SE2Tangent<T> obs{observations.at(i)}; + x_obs.push_back(obs.exp().x()); + y_obs.push_back(obs.exp().y()); + a_obs.push_back(obs.exp().angle()); + + x_ukf.push_back(ukf.getCurrentState().pose.x()); + y_ukf.push_back(ukf.getCurrentState().pose.y()); + a_ukf.push_back(ukf.getCurrentState().pose.angle()); + + TIMING_END(REST); + } + x_true.push_back(states.at(i).pose.x()); + y_true.push_back(states.at(i).pose.y()); + a_true.push_back(states.at(i).pose.angle()); + + x_ukf_full.push_back(ukf.getCurrentState().pose.x()); + y_ukf_full.push_back(ukf.getCurrentState().pose.y()); + a_ukf_full.push_back(ukf.getCurrentState().pose.angle()); + + ukf_states.push_back(ukf.getCurrentState()); + ukf_Ps.push_back(ukf.getCurrentStateCovariance()); + + ukf_pos_var.push_back( + ukf.getCurrentStateCovariance().block<2, 2>(0, 0).cwiseAbs().maxCoeff()); + ukf_rot_var.push_back( + ukf.getCurrentStateCovariance().block<1, 1>(2, 2).cwiseAbs().maxCoeff()); + ukf_var.push_back(ukf.getCurrentStateCovariance().cwiseAbs().maxCoeff()); + + TIMING_END(LOOP); + } + + sciplot::Plot2D pos_plot; + pos_plot.xlabel("x"); + pos_plot.ylabel("y"); + pos_plot.legend().atOutsideBottom().displayHorizontal().displayExpandWidthBy(2); + pos_plot.xrange(-RAD * 1.2, RAD * 1.2).yrange(-RAD * 1.2, RAD * 1.2); + //pos_plot.xrange(-4, 4).yrange(-4, 4); + + pos_plot.drawCurve(x_true, y_true).label("True"); + pos_plot.drawCurve(x_obs, y_obs).label("Obs"); + pos_plot.drawCurveWithPoints(x_ukf, y_ukf).label("UKF"); + pos_plot.drawCurve(x_ukf_full, y_ukf_full).label("Full UKF"); + + + sciplot::Plot2D orientation_plot; + orientation_plot.xlabel("t"); + orientation_plot.ylabel("rot"); + orientation_plot.legend().atOutsideBottom().displayHorizontal().displayExpandWidthBy(2); + orientation_plot.xrange(-0.1, 1.1).yrange(-4, 4); + + sciplot::Vec x = sciplot::linspace(0.0, 1.0, a_obs.size()); + sciplot::Vec x_t = sciplot::linspace(0.0, 1.0, a_true.size()); + orientation_plot.drawCurve(x_t, a_true).label("True").lineWidth(1); + orientation_plot.drawCurve(x, a_obs).label("Obs").lineWidth(1); + orientation_plot.drawCurve(x, a_ukf).label("UKF").lineWidth(1); + orientation_plot.drawCurve(x_t, a_ukf_full).label("Full UKF").lineWidth(1); + + + sciplot::Plot2D error_plot; + error_plot.xlabel("t"); + error_plot.ylabel("err"); + error_plot.legend().atOutsideBottom().displayHorizontal().displayExpandWidthBy(2); + error_plot.xrange(-0.1, 1.1).yrange("*", "*"); + + error_plot.drawCurve(x_t, ukf_pos_var).label("Pos variance").lineWidth(1); + error_plot.drawCurve(x_t, ukf_rot_var).label("Rot variance").lineWidth(1); + error_plot.drawCurve(x_t, ukf_var).label("Total variance").lineWidth(1); + + + // Create figure to hold plot + sciplot::Figure fig = {{pos_plot, orientation_plot}, {error_plot}}; + // Create canvas to hold figure + sciplot::Canvas canvas = {{fig}}; + canvas.size(1000, 1000); + + // Show the plot in a pop-up window + canvas.show(); + + // Save the plot to a PDF file + canvas.save("se2_kalman_output.pdf"); + } + +} // namespace armarx::navigation::human diff --git a/source/armarx/navigation/human/test/se2xVkalmanFilterTest.cpp b/source/armarx/navigation/human/test/se2xVkalmanFilterTest.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9693e0f83b60b5b93d01a549e4d8ca1e2d4c0f14 --- /dev/null +++ b/source/armarx/navigation/human/test/se2xVkalmanFilterTest.cpp @@ -0,0 +1,292 @@ +/** + * This file is part of ArmarX. + * + * ArmarX is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * ArmarX is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @author Tobias Gröger ( tobias dot groeger at student dot kit dot edu ) + * @date 2022 + * @copyright http://www.gnu.org/licenses/gpl-2.0.txt + * GNU General Public License + */ + +#define BOOST_TEST_MODULE Navigation::ArmarXLibraries::human +#define ARMARX_BOOST_TEST + +#include <cstdlib> /* srand, rand */ +#include <ctime> /* time */ + +#include <ArmarXCore/core/exceptions/local/ExpressionException.h> +#include <ArmarXCore/core/logging/Logging.h> +#include <ArmarXCore/util/time.h> + +#include <RobotAPI/libraries/ukfm/UnscentedKalmanFilter.h> + +#include <armarx/navigation/Test.h> +#include <armarx/navigation/human/HumanSystemModel.h> +#include <manif/SO2.h> +#include <sciplot/sciplot.hpp> + +namespace armarx::navigation::human +{ + using T = double; //TODO double or float? + using Vector = Eigen::Matrix<T, 2, 1>; + using SystemModelT = kalman_filter::SystemModelSE2xV<T>; + + constexpr T RAD = 1; + constexpr long num_timesteps = 3000; + constexpr T max_time = 1; + constexpr T dt = max_time / num_timesteps; + constexpr T c = (1 / max_time) * 2 * M_PI; // rotation per time + + constexpr T obs_rot_noise_std = 0.01; + constexpr T obs_pos_noise_std = 0.01; + constexpr T initial_offset_angle = 1 * 10 * M_PI / 180; + const Vector initial_offet_pos = 0.8 * RAD * Vector(1, 0.5); + const Eigen::Matrix<T, 3, 1> initial_offet_vel = + 0.8 * RAD * Eigen::Matrix<T, 3, 1>(1, 0.5, 0.5); + + + void + simulate_trajectory(std::vector<SystemModelT::StateT>& states) + { + constexpr T startAngle = 0; + + SystemModelT::StateT state; + state.pose = manif::SE2<T>(RAD, 0, startAngle); + state.velocity = manif::SE2Tangent<T>(0, c, c); + states.push_back(state); + + for (int i = 1; i < num_timesteps; i++) + { + const T t = dt * i; + const T angle = t * c; + + const Vector last_pos = states.at(i - 1).pose.translation(); + //const Vector pos(angle, std::cos(angle)); + const Vector pos = RAD * Vector(std::cos(angle), std::sin(angle)); + + BOOST_TEST(((last_pos - pos).norm() < 0.1 * RAD), + "position differs too much from last step: " << (last_pos - pos).norm()); + + SystemModelT::ControlT control; + + const SystemModelT::StateT propagated = SystemModelT::propagationFunction( + state, control, SystemModelT::ControlNoiseT::Zero(), dt); + + state.pose = manif::SE2<T>(pos.x(), pos.y(), angle + startAngle); + state.velocity = manif::SE2Tangent<T>(0, c, c); + + T pos_diff = (propagated.pose.translation() - state.pose.translation()).norm(); + BOOST_TEST(pos_diff < 2e-10, + "propagated position differs too much from real position: " << pos_diff); + + T rot_diff = propagated.pose.angle() - state.pose.angle(); + BOOST_TEST(rot_diff < 2e-10, + "propagated rotation differs too much from real orientation: " << rot_diff); + + T vel_diff = (propagated.velocity.coeffs() - state.velocity.coeffs()).norm(); + BOOST_TEST(vel_diff < 2e-10, + "propagated velocity differs too much from real velocity: " << vel_diff); + + states.push_back(state); + } + } + + void + simulate_observation(const std::vector<SystemModelT::StateT>& states, + std::vector<SystemModelT::ObsT>& observations) + { + for (const auto& state : states) + { + SystemModelT::ObsT obs = SystemModelT::observationFunction(state); + + SystemModelT::ObsT noise = SystemModelT::ObsT::Random(); + noise.segment(0, 2) *= obs_pos_noise_std; + noise.segment(2, 1) *= obs_rot_noise_std; + + observations.push_back(obs + noise); + } + } + + + BOOST_AUTO_TEST_CASE(se2xVkalmanFilterTest) + { + srand(time(NULL)); + + std::vector<SystemModelT::StateT> states; + std::vector<SystemModelT::ObsT> observations; + simulate_trajectory(states); + simulate_observation(states, observations); + + ARMARX_INFO << "Num States: " << states.size() << " Num Obs: " << observations.size(); + + UnscentedKalmanFilter<SystemModelT>::PropCovT Q = + UnscentedKalmanFilter<SystemModelT>::PropCovT::Identity(); + + UnscentedKalmanFilter<SystemModelT>::ObsCovT R = + UnscentedKalmanFilter<SystemModelT>::ObsCovT::Identity(); + R.block<2, 2>(0, 0) *= obs_pos_noise_std * obs_pos_noise_std; + R.block<1, 1>(2, 2) *= obs_rot_noise_std * obs_rot_noise_std; + + UnscentedKalmanFilter<SystemModelT>::StateCovT P0 = + UnscentedKalmanFilter<SystemModelT>::StateCovT::Identity(); + P0.block<2, 2>(0, 0) *= initial_offet_pos.norm() * initial_offet_pos.norm(); + P0.block<1, 1>(2, 2) *= initial_offset_angle * initial_offset_angle; + P0.block<3, 3>(3, 3) *= initial_offet_vel.norm() * initial_offet_vel.norm(); + + UnscentedKalmanFilter<SystemModelT>::AlphaT alpha; + alpha(0) = 0.001; // state + alpha(1) = 0.001; // control + alpha(2) = 0.001; // update + + SystemModelT::StateT state0; + const Vector startPos = states.at(0).pose.translation() + initial_offet_pos; + state0.pose = manif::SE2<T>{ + startPos.x(), startPos.y(), states.at(0).pose.angle() + initial_offset_angle}; + state0.velocity = states.at(0).velocity + initial_offet_vel; + + UnscentedKalmanFilter<SystemModelT> ukf(Q, R, alpha, state0, P0); + + std::vector<SystemModelT::StateT> ukf_states; + std::vector<UnscentedKalmanFilter<SystemModelT>::StateCovT> ukf_Ps; + ukf_states.push_back(state0); + ukf_Ps.push_back(P0); + + std::vector<T> x_true, y_true, x_obs, y_obs, x_ukf, y_ukf; + std::vector<T> a_true, a_obs, a_ukf; + std::vector<T> vel_x_true, vel_y_true, vel_a_true; + std::vector<T> vel_x_ukf_full, vel_y_ukf_full, vel_a_ukf_full; + std::vector<T> x_ukf_full, y_ukf_full, a_ukf_full; + std::vector<T> ukf_pos_var, ukf_rot_var, ukf_var; + + for (int i = 1; i < num_timesteps; i++) + { + // propagate + TIMING_START(LOOP); + TIMING_START(PROPAGATION); + ukf.propagation(SystemModelT::ControlT{}, dt); + TIMING_END(PROPAGATION); + if ((i - 1) % 5 == 0) + { + TIMING_START(UPDATE); + ukf.update(observations.at(i)); + TIMING_END(UPDATE); + TIMING_START(REST); + + BOOST_TEST_MESSAGE("Max Cov " + << ukf.getCurrentStateCovariance().cwiseAbs().maxCoeff()); + + x_obs.push_back(observations.at(i)(0)); + y_obs.push_back(observations.at(i)(1)); + a_obs.push_back(observations.at(i)(2)); + + x_ukf.push_back(ukf.getCurrentState().pose.x()); + y_ukf.push_back(ukf.getCurrentState().pose.y()); + a_ukf.push_back(ukf.getCurrentState().pose.angle()); + + TIMING_END(REST); + } + + x_true.push_back(states.at(i).pose.x()); + y_true.push_back(states.at(i).pose.y()); + a_true.push_back(states.at(i).pose.angle()); + + vel_x_true.push_back(states.at(i).velocity.x()); + vel_y_true.push_back(states.at(i).velocity.y()); + vel_a_true.push_back(states.at(i).velocity.angle()); + + x_ukf_full.push_back(ukf.getCurrentState().pose.x()); + y_ukf_full.push_back(ukf.getCurrentState().pose.y()); + a_ukf_full.push_back(ukf.getCurrentState().pose.angle()); + + vel_x_ukf_full.push_back(ukf.getCurrentState().velocity.x()); + vel_y_ukf_full.push_back(ukf.getCurrentState().velocity.y()); + vel_a_ukf_full.push_back(ukf.getCurrentState().velocity.angle()); + + ukf_states.push_back(ukf.getCurrentState()); + ukf_Ps.push_back(ukf.getCurrentStateCovariance()); + + ukf_pos_var.push_back( + ukf.getCurrentStateCovariance().block<2, 2>(0, 0).cwiseAbs().maxCoeff()); + ukf_rot_var.push_back( + ukf.getCurrentStateCovariance().block<1, 1>(2, 2).cwiseAbs().maxCoeff()); + ukf_var.push_back(ukf.getCurrentStateCovariance().cwiseAbs().maxCoeff()); + + TIMING_END(LOOP); + } + + sciplot::Plot2D pos_plot; + pos_plot.xlabel("x"); + pos_plot.ylabel("y"); + pos_plot.legend().atOutsideBottom().displayHorizontal().displayExpandWidthBy(2); + pos_plot.xrange(-RAD * 1.1, RAD * 1.1).yrange(-RAD * 1.1, RAD * 1.1); + + pos_plot.drawCurve(x_true, y_true).label("True"); + pos_plot.drawCurve(x_obs, y_obs).label("Obs"); + pos_plot.drawCurveWithPoints(x_ukf, y_ukf).label("UKF"); + pos_plot.drawCurve(x_ukf_full, y_ukf_full).label("Full UKF"); + + + sciplot::Plot2D orientation_plot; + orientation_plot.xlabel("t"); + orientation_plot.ylabel("rot"); + orientation_plot.legend().atOutsideBottom().displayHorizontal().displayExpandWidthBy(2); + orientation_plot.xrange(-0.1, 1.1).yrange(-4, 4); + + sciplot::Vec x = sciplot::linspace(0.0, 1.0, a_obs.size()); + sciplot::Vec x_t = sciplot::linspace(0.0, 1.0, a_true.size()); + orientation_plot.drawCurve(x_t, a_true).label("True").lineWidth(1); + orientation_plot.drawCurve(x, a_obs).label("Obs").lineWidth(1); + orientation_plot.drawCurve(x, a_ukf).label("UKF").lineWidth(1); + orientation_plot.drawCurve(x_t, a_ukf_full).label("Full UKF").lineWidth(1); + + + sciplot::Plot2D velocity_plot; + velocity_plot.xlabel("t"); + velocity_plot.ylabel("vel"); + velocity_plot.legend().atOutsideBottom().displayHorizontal().displayExpandWidthBy(2); + velocity_plot.xrange(-0.1, 1.1).yrange("*", "*"); + + velocity_plot.drawCurve(x_t, vel_x_true).label("x True").lineWidth(1); + velocity_plot.drawCurve(x_t, vel_y_true).label("y True").lineWidth(1); + velocity_plot.drawCurve(x_t, vel_a_true).label("a True").lineWidth(1); + velocity_plot.drawCurve(x_t, vel_x_ukf_full).label("x Ukf").lineWidth(1); + velocity_plot.drawCurve(x_t, vel_y_ukf_full).label("y Ukf").lineWidth(1); + velocity_plot.drawCurve(x_t, vel_a_ukf_full).label("a Ukf").lineWidth(1); + + + sciplot::Plot2D error_plot; + error_plot.xlabel("t"); + error_plot.ylabel("err"); + error_plot.legend().atOutsideBottom().displayHorizontal().displayExpandWidthBy(2); + error_plot.xrange(-0.1, 1.1).yrange("*", "*"); + + error_plot.drawCurve(x_t, ukf_pos_var).label("Pos variance").lineWidth(1); + error_plot.drawCurve(x_t, ukf_rot_var).label("Rot variance").lineWidth(1); + error_plot.drawCurve(x_t, ukf_var).label("Total variance").lineWidth(1); + + + // Create figure to hold plot + sciplot::Figure fig = {{pos_plot, orientation_plot}, {velocity_plot, error_plot}}; + // Create canvas to hold figure + sciplot::Canvas canvas = {{fig}}; + canvas.size(1000, 1000); + + // Show the plot in a pop-up window + canvas.show(); + + // Save the plot to a PDF file + canvas.save("se2xV_kalman_output.pdf"); + } + +} // namespace armarx::navigation::human diff --git a/source/armarx/navigation/human/test/so2kalmanFilterTest.cpp b/source/armarx/navigation/human/test/so2kalmanFilterTest.cpp new file mode 100644 index 0000000000000000000000000000000000000000..48529d44139b767010cc920790826bae9936312f --- /dev/null +++ b/source/armarx/navigation/human/test/so2kalmanFilterTest.cpp @@ -0,0 +1,294 @@ +/** + * This file is part of ArmarX. + * + * ArmarX is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * ArmarX is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @author Tobias Gröger ( tobias dot groeger at student dot kit dot edu ) + * @date 2022 + * @copyright http://www.gnu.org/licenses/gpl-2.0.txt + * GNU General Public License + */ + +#define BOOST_TEST_MODULE Navigation::ArmarXLibraries::human +#define ARMARX_BOOST_TEST + +#include <cstdlib> /* srand, rand */ +#include <ctime> /* time */ + +#include <ArmarXCore/core/exceptions/local/ExpressionException.h> +#include <ArmarXCore/core/logging/Logging.h> +#include <ArmarXCore/util/time.h> + +#include <RobotAPI/libraries/ukfm/UnscentedKalmanFilter.h> + +#include <armarx/navigation/Test.h> +#include <armarx/navigation/human/HumanSystemModel.h> +#include <manif/SO2.h> +#include <sciplot/sciplot.hpp> + +namespace armarx::navigation::human +{ + using T = double; //TODO double or float? + using Vector = Eigen::Matrix<T, 2, 1>; + using SystemModelT = kalman_filter::SystemModelSO2xR2<T>; + + constexpr T RAD = 40; + constexpr long num_timesteps = 3000; + constexpr T max_time = 1; + constexpr T dt = max_time / num_timesteps; + constexpr T c = (1 / max_time) * 2 * M_PI; // rotation per time + + constexpr T rot_noise_std = 0.2; + constexpr T pos_noise_std = 5; + constexpr T obs_rot_noise_std = 0.2; + constexpr T obs_pos_noise_std = 5; + constexpr T initial_offset_angle = 1 * 10 * M_PI / 180; + const Vector initial_offet_pos = 0.8 * RAD * Vector(1, 0.5); + + + void + simulate_trajectory(std::vector<SystemModelT::StateT>& states, + std::vector<SystemModelT::ControlT>& omegas) + { + constexpr T startAngle = 0; + + SystemModelT::StateT state; + state.position = Vector{RAD, 0}; + state.orientation = manif::SO2<T>(startAngle); + states.push_back(state); + + for (int i = 1; i < num_timesteps; i++) + { + const T t = dt * i; + const T angle = t * c; + const T last_angle = dt * (i - 1) * c; + + const Vector last_pos = states.at(i - 1).position; + //const Vector pos(angle, std::cos(angle)); + const Vector pos = RAD * Vector(std::cos(angle), std::sin(angle)); + + BOOST_TEST(((last_pos - pos).norm() < 0.1 * RAD), + "position differs too much from last step: " << (last_pos - pos).norm()); + + SystemModelT::ControlT control; + //TODO (SALt): check angular velocity (>pi overflow) + control.angular_velocity.coeffs() << c; //(last_angle - angle) / dt; + control.euclidean_velocity << (pos - last_pos) / dt; + //control.euclidean_velocity = Eigen::Vector2d{0, c}; + + const SystemModelT::StateT propagated = SystemModelT::propagationFunction( + state, control, SystemModelT::ControlNoiseT::Zero(), dt); + + state.position = pos; + state.orientation = manif::SO2<T>(angle + startAngle); + + T pos_diff = (propagated.position - state.position).norm(); + BOOST_TEST(pos_diff < 2e-10, + "propagated position differs too much from real position: " << pos_diff); + + T rot_diff = propagated.orientation.angle() - state.orientation.angle(); + BOOST_TEST(rot_diff < 2e-10, + "propagated rotation differs too much from real orientation: " << rot_diff); + + states.push_back(propagated); + + + // add noise + control.angular_velocity.coeffs() += rot_noise_std * Eigen::Matrix<T, 1, 1>::Random(); + control.euclidean_velocity += pos_noise_std * Vector::Random(); + omegas.push_back(control); + } + } + + void + simulate_observation(const std::vector<SystemModelT::StateT>& states, + std::vector<SystemModelT::ObsT>& observations) + { + for (const auto& state : states) + { + SystemModelT::ObsT obs = SystemModelT::observationFunction(state); + SystemModelT::ObsT true_obs; + true_obs.segment(0, 1) = state.orientation.log().coeffs(); + true_obs.segment(1, 2) = state.position; + + BOOST_TEST( + (obs - true_obs).norm() < std::numeric_limits<T>::epsilon(), + "observation differs too much from real observation: " << (obs - true_obs).norm()); + + SystemModelT::ObsT noise = SystemModelT::ObsT::Random(); + noise.segment(0, 1) *= obs_rot_noise_std; + noise.segment(1, 2) *= obs_pos_noise_std; + + observations.push_back(obs + noise); + } + } + + + BOOST_AUTO_TEST_CASE(so2KalmanFilterTest) + { + srand(time(NULL)); + + std::vector<SystemModelT::StateT> states; + std::vector<SystemModelT::ControlT> omegas; + std::vector<SystemModelT::ObsT> observations; + simulate_trajectory(states, omegas); + simulate_observation(states, observations); + + ARMARX_INFO << "Num States: " << states.size() << " Num Controls: " << omegas.size() + << " Num Obs: " << observations.size(); + + UnscentedKalmanFilter<SystemModelT>::PropCovT Q = + UnscentedKalmanFilter<SystemModelT>::PropCovT::Identity(); + Q.block<1, 1>(0, 0) *= rot_noise_std * rot_noise_std; + Q.block<2, 2>(1, 1) *= pos_noise_std * pos_noise_std; + + UnscentedKalmanFilter<SystemModelT>::ObsCovT R = + UnscentedKalmanFilter<SystemModelT>::ObsCovT::Identity(); + R.block<1, 1>(0, 0) *= obs_rot_noise_std * obs_rot_noise_std; + R.block<2, 2>(1, 1) *= obs_pos_noise_std * obs_pos_noise_std; + + UnscentedKalmanFilter<SystemModelT>::StateCovT P0 = + UnscentedKalmanFilter<SystemModelT>::StateCovT::Identity(); + P0.block<1, 1>(0, 0) *= initial_offset_angle * initial_offset_angle; + P0.block<2, 2>(1, 1) *= initial_offet_pos.norm() * initial_offet_pos.norm(); + + UnscentedKalmanFilter<SystemModelT>::AlphaT alpha; + alpha(0) = 0.5; // state + alpha(1) = 0.5; // control + alpha(2) = 0.5; // update + + SystemModelT::StateT state0; + state0.orientation = manif::SO2<T>(states.at(0).orientation.angle() + initial_offset_angle); + state0.position = states.at(0).position + initial_offet_pos; + + UnscentedKalmanFilter<SystemModelT> ukf(Q, R, alpha, state0, P0); + + std::vector<SystemModelT::StateT> ukf_states; + std::vector<UnscentedKalmanFilter<SystemModelT>::StateCovT> ukf_Ps; + ukf_states.push_back(state0); + ukf_Ps.push_back(P0); + + std::vector<T> x_true, y_true, x_obs, y_obs, x_ukf, y_ukf; + std::vector<T> a_true, a_obs, a_ukf; + std::vector<T> x_ukf_full, y_ukf_full, a_ukf_full; + std::vector<T> ukf_pos_var, ukf_rot_var, ukf_var; + + for (int i = 1; i < num_timesteps; i++) + { + // propagate + TIMING_START(LOOP); + TIMING_START(PROPAGATION); + ukf.propagation(omegas.at(i - 1), dt); + TIMING_END(PROPAGATION); + if ((i - 1) % 50 == 0) + { + TIMING_START(UPDATE); + ukf.update(observations.at(i)); + TIMING_END(UPDATE); + TIMING_START(REST); + const SystemModelT::StateT& current_state = ukf.getCurrentState(); + + BOOST_TEST_MESSAGE( + "Position Diff: " << (states.at(i).position - current_state.position).norm()); + BOOST_TEST_MESSAGE( + "Orientation Diff: " + << (states.at(i).orientation - current_state.orientation).coeffs().norm()); + + BOOST_TEST_MESSAGE("Max Cov " + << ukf.getCurrentStateCovariance().cwiseAbs().maxCoeff()); + BOOST_TEST_MESSAGE("Diag: " << (ukf.getCurrentStateCovariance() - + Eigen::Matrix<T, 3, 3>::Identity()) + .norm()); + + x_obs.push_back(observations.at(i)(1)); + y_obs.push_back(observations.at(i)(2)); + a_obs.push_back(observations.at(i)(0)); + + x_ukf.push_back(ukf.getCurrentState().position.x()); + y_ukf.push_back(ukf.getCurrentState().position.y()); + a_ukf.push_back(ukf.getCurrentState().orientation.log().coeffs()(0)); + + TIMING_END(REST); + } + x_true.push_back(states.at(i).position.x()); + y_true.push_back(states.at(i).position.y()); + a_true.push_back(states.at(i).orientation.log().coeffs()(0)); + + x_ukf_full.push_back(ukf.getCurrentState().position.x()); + y_ukf_full.push_back(ukf.getCurrentState().position.y()); + a_ukf_full.push_back(ukf.getCurrentState().orientation.log().coeffs()(0)); + + ukf_states.push_back(ukf.getCurrentState()); + ukf_Ps.push_back(ukf.getCurrentStateCovariance()); + + ukf_pos_var.push_back( + ukf.getCurrentStateCovariance().block<2, 2>(1, 1).cwiseAbs().maxCoeff()); + ukf_rot_var.push_back( + ukf.getCurrentStateCovariance().block<1, 1>(0, 0).cwiseAbs().maxCoeff()); + ukf_var.push_back(ukf.getCurrentStateCovariance().cwiseAbs().maxCoeff()); + + TIMING_END(LOOP); + } + + sciplot::Plot2D pos_plot; + pos_plot.xlabel("x"); + pos_plot.ylabel("y"); + pos_plot.legend().atOutsideBottom().displayHorizontal().displayExpandWidthBy(2); + pos_plot.xrange(-RAD * 1.1, RAD * 1.1).yrange(-RAD * 1.1, RAD * 1.1); + //pos_plot.xrange(-4, 4).yrange(-4, 4); + + pos_plot.drawCurve(x_true, y_true).label("True"); + pos_plot.drawCurve(x_obs, y_obs).label("Obs"); + pos_plot.drawCurveWithPoints(x_ukf, y_ukf).label("UKF"); + pos_plot.drawCurve(x_ukf_full, y_ukf_full).label("Full UKF"); + + + sciplot::Plot2D orientation_plot; + orientation_plot.xlabel("x"); + orientation_plot.ylabel("y"); + orientation_plot.legend().atOutsideBottom().displayHorizontal().displayExpandWidthBy(2); + orientation_plot.xrange(-0.1, 1.1).yrange(-4, 4); + + sciplot::Vec x = sciplot::linspace(0.0, 1.0, a_obs.size()); + sciplot::Vec x_t = sciplot::linspace(0.0, 1.0, a_true.size()); + orientation_plot.drawCurve(x_t, a_true).label("True").lineWidth(1); + orientation_plot.drawCurve(x, a_obs).label("Obs").lineWidth(1); + orientation_plot.drawCurve(x, a_ukf).label("UKF").lineWidth(1); + orientation_plot.drawCurve(x_t, a_ukf_full).label("Full UKF").lineWidth(1); + + + sciplot::Plot2D error_plot; + error_plot.xlabel("x"); + error_plot.ylabel("y"); + error_plot.legend().atOutsideBottom().displayHorizontal().displayExpandWidthBy(2); + error_plot.xrange(-0.1, 1.1).yrange("*", "*"); + + error_plot.drawCurve(x_t, ukf_pos_var).label("Pos variance").lineWidth(1); + error_plot.drawCurve(x_t, ukf_rot_var).label("Rot variance").lineWidth(1); + error_plot.drawCurve(x_t, ukf_var).label("Total variance").lineWidth(1); + + + // Create figure to hold plot + sciplot::Figure fig = {{pos_plot, orientation_plot}, {error_plot}}; + // Create canvas to hold figure + sciplot::Canvas canvas = {{fig}}; + canvas.size(1000, 1000); + + // Show the plot in a pop-up window + canvas.show(); + + // Save the plot to a PDF file + canvas.save("so2_kalman_output.pdf"); + } + +} // namespace armarx::navigation::human