James Kuszmaul | 9f9676d | 2019-01-25 21:27:58 -0800 | [diff] [blame] | 1 | #ifndef FRC971_CONTROL_LOOPS_POSE_H_ |
| 2 | #define FRC971_CONTROL_LOOPS_POSE_H_ |
| 3 | |
James Kuszmaul | 090563a | 2019-02-09 14:43:20 -0800 | [diff] [blame] | 4 | #include <vector> |
| 5 | |
James Kuszmaul | 9f9676d | 2019-01-25 21:27:58 -0800 | [diff] [blame] | 6 | #include "Eigen/Dense" |
| 7 | #include "aos/util/math.h" |
| 8 | |
| 9 | namespace frc971 { |
| 10 | namespace control_loops { |
| 11 | |
James Kuszmaul | 3ca2861 | 2020-02-15 17:52:27 -0800 | [diff] [blame] | 12 | // Constructs a homogeneous transformation matrix for rotating about the Z axis. |
| 13 | template <typename Scalar> |
| 14 | Eigen::Matrix<Scalar, 4, 4> TransformationMatrixForYaw(Scalar yaw) { |
| 15 | Eigen::Matrix<Scalar, 4, 4> matrix; |
| 16 | matrix.setIdentity(); |
| 17 | const Scalar stheta = std::sin(yaw); |
| 18 | const Scalar ctheta = std::cos(yaw); |
| 19 | matrix(0, 0) = ctheta; |
| 20 | matrix(1, 1) = ctheta; |
| 21 | matrix(0, 1) = -stheta; |
| 22 | matrix(1, 0) = stheta; |
| 23 | return matrix; |
| 24 | } |
| 25 | |
James Kuszmaul | 9f9676d | 2019-01-25 21:27:58 -0800 | [diff] [blame] | 26 | // Provides a representation of a transformation on the field. |
| 27 | // Currently, this is heavily geared towards things that occur in a 2-D plane. |
| 28 | // The Z-axis is rarely used (but still relevant; e.g., in 2019 some of the |
| 29 | // targets are at a different height). |
| 30 | // For rotations, we currently just represent the yaw axis (the rotation about |
| 31 | // the Z-axis). |
| 32 | // As a convention, we use right-handed coordinate systems; the positive Z |
| 33 | // axis will go up on the field, the positive X axis shall be "forwards" for |
| 34 | // some relevant meaning of forwards, and the origin shall be chosen as |
| 35 | // appropriate. |
| 36 | // For 2019, at least, the global origin will be on the ground at the center |
| 37 | // of the driver's station wall of your current alliance and the positive X-axis |
| 38 | // will point straight into the field from the driver's station. |
| 39 | // In future years this may need to change if the field's symmetry changes and |
| 40 | // we can't interchangeably handle which side of the field we are on. |
| 41 | // This means that if we had a Pose for the center of mass of the robot with a |
| 42 | // position of (10, -5, 0) and a yaw of pi / 2, that suggests the robot is |
| 43 | // facing straight to the left from the driver's perspective and is placed 10m |
| 44 | // from the driver's station wall and 5m to the right of the center of the wall. |
| 45 | // |
| 46 | // Furthermore, Poses can be chained such that a Pose can be placed relative to |
| 47 | // another Pose; the other Pose can dynamically update, thus allowing us to, |
| 48 | // e.g., provide a Pose for a camera that is relative to the Pose of the robot. |
| 49 | // Poses can also be in the global frame with no parent Pose. |
| 50 | template <typename Scalar = double> |
| 51 | class TypedPose { |
| 52 | public: |
| 53 | EIGEN_MAKE_ALIGNED_OPERATOR_NEW; |
| 54 | |
| 55 | // The type that contains the translational (x, y, z) component of the Pose. |
| 56 | typedef Eigen::Matrix<Scalar, 3, 1> Pos; |
| 57 | |
James Kuszmaul | 3ca2861 | 2020-02-15 17:52:27 -0800 | [diff] [blame] | 58 | // Provide a default constructor that creates a pose at the origin. |
James Kuszmaul | 090563a | 2019-02-09 14:43:20 -0800 | [diff] [blame] | 59 | TypedPose() : TypedPose({0.0, 0.0, 0.0}, 0.0) {} |
| 60 | |
James Kuszmaul | 9f9676d | 2019-01-25 21:27:58 -0800 | [diff] [blame] | 61 | // Construct a Pose in the absolute frame with a particular position and yaw. |
| 62 | TypedPose(const Pos &abs_pos, Scalar theta) : pos_(abs_pos), theta_(theta) {} |
| 63 | // Construct a Pose relative to another Pose (base). |
| 64 | // If you provide a base of nullptr, then this will |
| 65 | // construct a Pose in the global frame. |
| 66 | // Note that the lifetime of base should be greater than the lifetime of |
| 67 | // the object being constructed. |
| 68 | TypedPose(const TypedPose<Scalar> *base, const Pos &rel_pos, Scalar rel_theta) |
| 69 | : base_(base), pos_(rel_pos), theta_(rel_theta) {} |
| 70 | |
James Kuszmaul | 3ca2861 | 2020-02-15 17:52:27 -0800 | [diff] [blame] | 71 | // Constructs a Pose from a homogeneous transformation matrix. Ignores the |
| 72 | // pitch/roll components of the rotation. Ignores the bottom row. |
| 73 | TypedPose(const Eigen::Matrix<Scalar, 4, 4> &H) { |
| 74 | pos_ = H.template block<3, 1>(0, 3); |
| 75 | const Eigen::Vector3d rotated_x = |
| 76 | H.template block<3, 3>(0, 0) * Eigen::Vector3d::UnitX(); |
| 77 | theta_ = std::atan2(rotated_x.y(), rotated_x.x()); |
| 78 | } |
| 79 | |
James Kuszmaul | 9f9676d | 2019-01-25 21:27:58 -0800 | [diff] [blame] | 80 | // Calculate the current global position of this Pose. |
| 81 | Pos abs_pos() const { |
| 82 | if (base_ == nullptr) { |
| 83 | return pos_; |
| 84 | } |
| 85 | Pos base_pos = base_->abs_pos(); |
| 86 | Scalar base_theta = base_->abs_theta(); |
| 87 | return base_pos + YawRotation(base_theta) * pos_; |
| 88 | } |
| 89 | // Calculate the absolute yaw of this Pose. Since we only have a single |
| 90 | // rotational axis, we can just sum the angle with that of the base Pose. |
| 91 | Scalar abs_theta() const { |
| 92 | if (base_ == nullptr) { |
| 93 | return theta_; |
| 94 | } |
| 95 | return aos::math::NormalizeAngle(theta_ + base_->abs_theta()); |
| 96 | } |
| 97 | // Provide access to the position and yaw relative to the base Pose. |
| 98 | Pos rel_pos() const { return pos_; } |
| 99 | Scalar rel_theta() const { return theta_; } |
James Kuszmaul | 090563a | 2019-02-09 14:43:20 -0800 | [diff] [blame] | 100 | const TypedPose<Scalar> *base() const { return base_; } |
James Kuszmaul | 9f9676d | 2019-01-25 21:27:58 -0800 | [diff] [blame] | 101 | |
| 102 | Pos *mutable_pos() { return &pos_; } |
| 103 | void set_theta(Scalar theta) { theta_ = theta; } |
James Kuszmaul | 090563a | 2019-02-09 14:43:20 -0800 | [diff] [blame] | 104 | // Swap out the base Pose, keeping the current relative position/angle. |
| 105 | void set_base(const TypedPose<Scalar> *new_base) { base_ = new_base; } |
James Kuszmaul | 9f9676d | 2019-01-25 21:27:58 -0800 | [diff] [blame] | 106 | |
| 107 | // For 2-D calculation, provide the heading, which is distinct from the |
| 108 | // yaw/theta value. heading is the heading relative to the base Pose if you |
| 109 | // were to draw a line from the base to this Pose. i.e., if heading() is zero |
| 110 | // then you are directly in front of the base Pose. |
| 111 | Scalar heading() const { return ::std::atan2(pos_.y(), pos_.x()); } |
| 112 | // The 2-D distance from the base Pose to this Pose. |
| 113 | Scalar xy_norm() const { return pos_.template topRows<2>().norm(); } |
| 114 | // Return the absolute xy position. |
| 115 | Eigen::Matrix<Scalar, 2, 1> abs_xy() const { |
| 116 | return abs_pos().template topRows<2>(); |
| 117 | } |
| 118 | |
James Kuszmaul | 3ca2861 | 2020-02-15 17:52:27 -0800 | [diff] [blame] | 119 | // Returns a transformation matrix representing this pose--note that this |
| 120 | // explicitly does not include the base position, so this is equivalent to a |
| 121 | // translation and rotation by rel_pos and rel_theta. |
| 122 | Eigen::Matrix<Scalar, 4, 4> AsTransformationMatrix() const { |
| 123 | Eigen::Matrix<Scalar, 4, 4> matrix = TransformationMatrixForYaw(theta_); |
| 124 | matrix.template block<3, 1>(0, 3) = pos_; |
| 125 | return matrix; |
| 126 | } |
| 127 | |
James Kuszmaul | 9f9676d | 2019-01-25 21:27:58 -0800 | [diff] [blame] | 128 | // Provide a copy of this that is set to have the same |
| 129 | // current absolute Pose as this, but have a different base. |
| 130 | // This can be used, e.g., to compute a Pose for a vision target that is |
| 131 | // relative to the camera instead of relative to the field. You can then |
| 132 | // access the rel_* variables to get what the position of the target is |
| 133 | // relative to the robot/camera. |
| 134 | // If new_base == nullptr, provides a Pose referenced to the global frame. |
| 135 | // Note that the lifetime of new_base should be greater than the lifetime of |
| 136 | // the returned object (unless new_base == nullptr). |
| 137 | TypedPose Rebase(const TypedPose<Scalar> *new_base) const; |
| 138 | |
James Kuszmaul | 3ca2861 | 2020-02-15 17:52:27 -0800 | [diff] [blame] | 139 | // Convert this pose to the heading/distance/skew numbers that we |
| 140 | // traditionally use for EKF corrections. |
| 141 | Eigen::Matrix<Scalar, 3, 1> ToHeadingDistanceSkew() const { |
| 142 | const Scalar target_heading = heading(); |
| 143 | return {target_heading, xy_norm(), |
| 144 | aos::math::NormalizeAngle(rel_theta() - target_heading)}; |
| 145 | } |
| 146 | |
James Kuszmaul | 9f9676d | 2019-01-25 21:27:58 -0800 | [diff] [blame] | 147 | private: |
| 148 | // A rotation-matrix like representation of the rotation for a given angle. |
| 149 | inline static Eigen::AngleAxis<Scalar> YawRotation(double theta) { |
| 150 | return Eigen::AngleAxis<Scalar>(theta, Pos::UnitZ()); |
| 151 | } |
| 152 | |
| 153 | // A pointer to the base Pose. If uninitialized, then this Pose is in the |
| 154 | // global frame. |
| 155 | const TypedPose<Scalar> *base_ = nullptr; |
| 156 | // Position and yaw relative to base_. |
| 157 | Pos pos_; |
| 158 | Scalar theta_; |
| 159 | }; // class TypedPose |
| 160 | |
| 161 | typedef TypedPose<double> Pose; |
| 162 | |
| 163 | template <typename Scalar> |
| 164 | TypedPose<Scalar> TypedPose<Scalar>::Rebase( |
| 165 | const TypedPose<Scalar> *new_base) const { |
| 166 | if (new_base == nullptr) { |
| 167 | return TypedPose<Scalar>(nullptr, abs_pos(), abs_theta()); |
| 168 | } |
| 169 | // Calculate the absolute position/yaws of this and of the new_base, and then |
| 170 | // calculate where we are relative to new_base, essentially reversing the |
| 171 | // calculation in abs_*. |
| 172 | Pos base_pos = new_base->abs_pos(); |
| 173 | Scalar base_theta = new_base->abs_theta(); |
| 174 | Pos self_pos = abs_pos(); |
| 175 | Scalar self_theta = abs_theta(); |
| 176 | Scalar diff_theta = ::aos::math::DiffAngle(self_theta, base_theta); |
| 177 | Pos diff_pos = YawRotation(-base_theta) * (self_pos - base_pos); |
| 178 | return TypedPose<Scalar>(new_base, diff_pos, diff_theta); |
| 179 | } |
| 180 | |
| 181 | // Represents a 2D line segment constructed from a pair of Poses. |
| 182 | // The line segment goes between the two Poses, but for calculating |
| 183 | // intersections we use the 2D projection of the Poses onto the global X-Y |
| 184 | // plane. |
| 185 | template <typename Scalar = double> |
| 186 | class TypedLineSegment { |
| 187 | public: |
James Kuszmaul | 090563a | 2019-02-09 14:43:20 -0800 | [diff] [blame] | 188 | TypedLineSegment() {} |
James Kuszmaul | 9f9676d | 2019-01-25 21:27:58 -0800 | [diff] [blame] | 189 | TypedLineSegment(const TypedPose<Scalar> &pose1, |
| 190 | const TypedPose<Scalar> &pose2) |
| 191 | : pose1_(pose1), pose2_(pose2) {} |
| 192 | // Detects if two line segments intersect. |
| 193 | // When at least one end of one line segment is collinear with the other, |
| 194 | // the line segments are treated as not intersecting. |
| 195 | bool Intersects(const TypedLineSegment<Scalar> &other) const { |
| 196 | // Source for algorithm: |
| 197 | // https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/ |
| 198 | // Method: |
| 199 | // We will consider the four triangles that can be made out of any 3 points |
| 200 | // from the pair of line segments. |
| 201 | // Basically, if you consider one line segment the base of the triangle, |
| 202 | // then the two points of the other line segment should be on opposite |
| 203 | // sides of the first line segment (we use the PointsAreCCW function for |
| 204 | // this). This must hold when splitting off of both line segments. |
| 205 | Eigen::Matrix<Scalar, 2, 1> p1 = pose1_.abs_xy(); |
| 206 | Eigen::Matrix<Scalar, 2, 1> p2 = pose2_.abs_xy(); |
| 207 | Eigen::Matrix<Scalar, 2, 1> q1 = other.pose1_.abs_xy(); |
| 208 | Eigen::Matrix<Scalar, 2, 1> q2 = other.pose2_.abs_xy(); |
| 209 | return (::aos::math::PointsAreCCW<Scalar>(p1, q1, q2) != |
| 210 | ::aos::math::PointsAreCCW<Scalar>(p2, q1, q2)) && |
| 211 | (::aos::math::PointsAreCCW<Scalar>(p1, p2, q1) != |
| 212 | ::aos::math::PointsAreCCW<Scalar>(p1, p2, q2)); |
| 213 | } |
James Kuszmaul | 090563a | 2019-02-09 14:43:20 -0800 | [diff] [blame] | 214 | |
| 215 | TypedPose<Scalar> pose1() const { return pose1_; } |
| 216 | TypedPose<Scalar> pose2() const { return pose2_; } |
| 217 | TypedPose<Scalar> *mutable_pose1() { return &pose1_; } |
| 218 | TypedPose<Scalar> *mutable_pose2() { return &pose2_; } |
| 219 | |
| 220 | ::std::vector<TypedPose<Scalar>> PlotPoints() const { |
| 221 | return {pose1_, pose2_}; |
| 222 | } |
James Kuszmaul | 9f9676d | 2019-01-25 21:27:58 -0800 | [diff] [blame] | 223 | private: |
James Kuszmaul | 090563a | 2019-02-09 14:43:20 -0800 | [diff] [blame] | 224 | TypedPose<Scalar> pose1_; |
| 225 | TypedPose<Scalar> pose2_; |
James Kuszmaul | 9f9676d | 2019-01-25 21:27:58 -0800 | [diff] [blame] | 226 | }; // class TypedLineSegment |
| 227 | |
| 228 | typedef TypedLineSegment<double> LineSegment; |
| 229 | |
| 230 | } // namespace control_loops |
| 231 | } // namespace frc971 |
| 232 | |
| 233 | #endif // FRC971_CONTROL_LOOPS_POSE_H_ |