HoloLib
High-performance holonomic (X-Drive) control library for VEX V5
Loading...
Searching...
No Matches
ApriltagLocalization.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "Eigen/Core" // IWYU pragma: export
4#include "Eigen/src/Core/Matrix.h"
6#include "pros/ai_vision.hpp"
7#include "pros/imu.hpp"
8#include <array>
9#include <numbers>
10#include <utility>
11
12namespace hololib {
13
14/** @brief Estimates the robot's field pose from AprilTag detections. */
16public:
17 /** @brief The two pose solutions produced by the IPPE square solver. */
18 struct IPPEOutput {
19 /** Rotation vector for the lower-error solution, in radians. */
20 Eigen::Vector3f rvec1;
21 /** Rotation vector for the alternate solution, in radians. */
22 Eigen::Vector3f rvec2;
23 /** Translation vector for the lower-error solution. */
24 Eigen::Vector3f tvec1;
25 /** Translation vector for the alternate solution. */
26 Eigen::Vector3f tvec2;
27 /** Reprojection error of the first solution, in pixels. */
29 /** Reprojection error of the second solution, in pixels. */
31 /** Whether both solutions contain finite reprojection errors. */
32 bool valid;
33 };
34
35 /**
36 * @brief Constructs an AprilTag localization system.
37 * @param visionSensor AI Vision sensor used for tag detections.
38 * @param imu IMU associated with the robot.
39 * @param odomPoseGetter Callback returning the current odometry pose; its
40 * boolean argument selects radians.
41 * @param cameraMatrix Camera intrinsic matrix.
42 * @param distCoeffs Distortion coefficients ordered as k1, k2, p1, p2, k3.
43 * @param visionSensorOffset Camera offset from the robot tracking center as
44 * +x forward and +y right, in inches.
45 * @param visionSensorYawOffset Camera yaw relative to the robot, in radians.
46 *
47 * visionSensorOffset is measured from the robot tracking center to the
48 * camera optical center in robot coordinates: +x forward and +y right.
49 * visionSensorYawOffset and returned Pose angles use the odometry convention:
50 * zero faces field +Y and positive rotation is clockwise. Angles passed to
51 * this function through Pose are radians.
52 * All translations must use the same units as FIELD_TAGS (inches).
53 */
54 ApriltagLocalization(pros::AIVision& visionSensor,
55 pros::Imu& imu,
56 std::function<hololib::Pose(bool)> odomPoseGetter,
57 const Eigen::Matrix3f& cameraMatrix,
58 const Eigen::Vector<float, 5>& distCoeffs,
59 const Eigen::Vector2f& visionSensorOffset = Eigen::Vector2f::Zero(),
60 float visionSensorYawOffset = 0.0f)
61 : visionSensor(visionSensor), imu(imu), odomPoseGetter(odomPoseGetter), cameraMatrix(cameraMatrix),
62 distCoeffs(distCoeffs), visionSensorOffset(visionSensorOffset),
63 visionSensorYawOffset(visionSensorYawOffset) {};
64
65 /**
66 * @brief Continuously processes tag detections and updates the estimated pose.
67 * @param period_ms Update period in milliseconds.
68 */
69 void update(uint32_t period_ms);
70
71 /**
72 * @brief Starts the localization task if it is not already running.
73 * @param period_ms Update period in milliseconds.
74 *
75 * @b Example:
76 * @code
77 * // Start the AprilTag localization task with a 100 ms update period.
78 * initialize() {
79 * aprilTagLocalization.startTask(100);
80 * }
81 * @endcode
82 */
83 void startTask(uint32_t period_ms = 10);
84
85 /**
86 * @brief Returns the latest tag-based pose estimate.
87 * @param useRadians Return heading in radians when true, otherwise degrees.
88 * @return The latest estimated field pose.
89 *
90 * @b Example:
91 * @code
92 * // Get the latest tag-based pose estimate in radians.
93 * tagOdom.startTask(100);
94 * while (true) {
95 * hololib::Pose p = tagOdom.getPoseFromTag(false);
96 * std::println("Tag pose: x: {}, y: {}, theta: {}", p.x, p.y, p.theta);
97 * pros::delay(100);
98 * }
99 * @endcode
100 */
101 Pose getPoseFromTag(bool useRadians = false);
102
103 /**
104 * @brief Estimates the robot's field pose from a 6-DOF camera pose produced by IPPE.
105 * @param ippeOutput Candidate camera poses produced by IPPE.
106 * @param odometryPose Current odometry pose, with heading in radians.
107 * @param tagId Detected field tag ID.
108 * @return The best matching field pose, or odometryPose if no valid match exists.
109 */
110 Pose estimateRobotPose(const IPPEOutput& ippeOutput, const Pose& odometryPose, int tagId) const;
111
112private:
113 pros::AIVision& visionSensor;
114 pros::Imu& imu;
115 std::function<hololib::Pose(bool)> odomPoseGetter;
116 const float IPPE_SMALL = 1e-6f;
117 /**
118 * Tag edge length, in inches. This sets the scale of the IPPE solution, so
119 * the translation it returns comes back in these units and must match the
120 * units used by FIELD_TAGS. Measure the black square of the printed tag and
121 * update this if your tags are a different size.
122 */
123 const float APRILTAG_SIZE = 2.0f;
124 static constexpr float RAD2DEG = 180.0f / M_PI;
125 const Eigen::Matrix3f cameraMatrix;
126 const Eigen::Vector<float, 5> distCoeffs;
127 const Eigen::Vector2f visionSensorOffset;
128 const float visionSensorYawOffset;
129 Pose currentPose;
130
131 pros::Mutex poseMutex;
132 std::optional<pros::Task> m_task = std::nullopt;
133
134 struct SortedPoses {
135 Eigen::Matrix4f M1;
136 Eigen::Matrix4f M2;
137 float err1;
138 float err2;
139 };
140
141 struct FieldTag {
142 int tagId;
143 float yawRad;
144 std::array<float, 3> position;
145 };
146
147 static constexpr float PI = std::numbers::pi_v<float>;
148
149 static constexpr float YAW_POS_X = 0.0f;
150 static constexpr float YAW_POS_Y = PI / 2.0f;
151 static constexpr float YAW_NEG_X = PI;
152 static constexpr float YAW_NEG_Y = 3.0f * PI / 2.0f;
153
154 static constexpr std::array<FieldTag, 36> FIELD_TAGS{
155 {
156 // goal 1 +x side
157 {1, YAW_POS_Y, {46.640f, -21.455f, 4.355f}},
158 {1, YAW_NEG_X, {44.775f, -23.320f, 4.355f}},
159 {1, YAW_NEG_Y, {46.640f, -25.185f, 4.355f}},
160 {1, YAW_POS_X, {48.505f, -23.320f, 4.355f}},
161
162 // goal 2 +x side
163 {2, YAW_POS_Y, {46.640f, 25.185f, 4.355f}},
164 {2, YAW_NEG_X, {44.775f, 23.320f, 4.355f}},
165 {2, YAW_NEG_Y, {46.640f, 21.455f, 4.355f}},
166 {2, YAW_POS_X, {48.505f, 23.320f, 4.355f}},
167
168 // goal 3 +y side
169 {3, YAW_POS_Y, {23.320f, 48.505f, 4.355f}},
170 {3, YAW_NEG_X, {21.455f, 46.640f, 4.355f}},
171 {3, YAW_NEG_Y, {23.320f, 44.775f, 4.355f}},
172 {3, YAW_POS_X, {25.185f, 46.640f, 4.355f}},
173
174 // goal 4: +y side
175 {4, YAW_POS_Y, {-23.320f, 48.505f, 4.355f}},
176 {4, YAW_NEG_X, {-25.185f, 46.640f, 4.355f}},
177 {4, YAW_NEG_Y, {-23.320f, 44.775f, 4.355f}},
178 {4, YAW_POS_X, {-21.455f, 46.640f, 4.355f}},
179
180 // goal 1: -x side
181 {1, YAW_POS_Y, {-46.640f, 25.185f, 4.355f}},
182 {1, YAW_NEG_X, {-48.505f, 23.320f, 4.355f}},
183 {1, YAW_NEG_Y, {-46.640f, 21.455f, 4.355f}},
184 {1, YAW_POS_X, {-44.775f, 23.320f, 4.355f}},
185
186 // goal 2: -x side
187 {2, YAW_POS_Y, {-46.640f, -21.455f, 4.355f}},
188 {2, YAW_NEG_X, {-48.505f, -23.320f, 4.355f}},
189 {2, YAW_NEG_Y, {-46.640f, -25.185f, 4.355f}},
190 {2, YAW_POS_X, {-44.775f, -23.320f, 4.355f}},
191
192 // goal 3: -y side
193 {3, YAW_POS_Y, {-23.320f, -44.775f, 4.355f}},
194 {3, YAW_NEG_X, {-25.185f, -46.640f, 4.355f}},
195 {3, YAW_NEG_Y, {-23.320f, -48.505f, 4.355f}},
196 {3, YAW_POS_X, {-21.455f, -46.640f, 4.355f}},
197
198 // goal 4: -y side
199 {4, YAW_POS_Y, {23.320f, -44.775f, 4.355f}},
200 {4, YAW_NEG_X, {21.455f, -46.640f, 4.355f}},
201 {4, YAW_NEG_Y, {23.320f, -48.505f, 4.355f}},
202 {4, YAW_POS_X, {25.185f, -46.640f, 4.355f}},
203
204 // center goal
205 {0, YAW_POS_Y, {0.000f, 1.865f, 4.355f}},
206 {0, YAW_NEG_X, {-1.865f, 0.000f, 4.355f}},
207 {0, YAW_NEG_Y, {0.000f, -1.865f, 4.355f}},
208 {0, YAW_POS_X, {1.865f, 0.000f, 4.355f}},
209 }
210 };
211
212 std::array<Eigen::Vector2f, 4> generateSquareObjectCorners2D(float squareLength);
213
214 std::array<Eigen::Vector3f, 4> generateSquareObjectCorners3D(float squareLength);
215
216 Eigen::Matrix3f rotateVec2ZAxis(const Eigen::Vector3f& a);
217
218 Eigen::Vector3f rot2vec(const Eigen::Matrix3f& R);
219
220 Eigen::Vector3f computeTranslation(const std::array<Eigen::Vector2f, 4>& objectPoints,
221 const std::array<Eigen::Vector2f, 4>& normalizedImgPoints,
222 const Eigen::Matrix3f& R);
223
224 std::pair<Eigen::Matrix3f, Eigen::Matrix3f>
225 computeRotations(float j00, float j01, float j10, float j11, float p, float q);
226
227 std::pair<Eigen::Matrix4f, Eigen::Matrix4f>
228 solveCanonicalForm(const std::array<Eigen::Vector2f, 4>& canonicalObjPoints,
229 const std::array<Eigen::Vector2f, 4>& normalizedInputPoints,
230 const Eigen::Matrix3f& H);
231
232 Eigen::Matrix3f homographyFromSquarePoints(const std::array<Eigen::Vector2f, 4>& targetPoints, float halfLength);
233
234 std::array<Eigen::Vector2f, 4> projectPoints(const std::array<Eigen::Vector3f, 4>& objectPoints,
235 const Eigen::Matrix3f& R,
236 const Eigen::Vector3f& t,
237 const Eigen::Matrix3f& cameraMatrix,
238 const Eigen::Vector<float, 5>& distCoeffs);
239
240 float evalReprojError(const std::array<Eigen::Vector3f, 4>& objectPoints,
241 const std::array<Eigen::Vector2f, 4>& imagePoints,
242 const Eigen::Matrix3f& cameraMatrix,
243 const Eigen::Vector<float, 5>& distCoeffs,
244 const Eigen::Matrix4f& M);
245
246 SortedPoses sortPosesByReprojError(const std::array<Eigen::Vector3f, 4>& objectPoints,
247 const std::array<Eigen::Vector2f, 4>& imagePoints,
248 const Eigen::Matrix3f& cameraMatrix,
249 const Eigen::Vector<float, 5>& distCoeffs,
250 const Eigen::Matrix4f& Ma,
251 const Eigen::Matrix4f& Mb);
252
253 std::array<Eigen::Vector2f, 4> undistortPoints5Coeffs(const std::array<Eigen::Vector2f, 4>& src,
254 const Eigen::Matrix3f& cameraMatrix,
255 const Eigen::Vector<float, 5>& distCoeffs,
256 int maxIterations = 5,
257 float epsilon = 0.01f);
258
259 IPPEOutput IPPESquare(const std::array<Eigen::Vector2f, 4>& imagePoints,
260 const Eigen::Matrix3f& cameraMatrix,
261 const Eigen::Vector<float, 5>& distCoeffs,
262 float squareLength);
263
264 std::pair<Eigen::Vector3f, Eigen::Vector3f> selectBestIPPESolution(const IPPEOutput& ippeOutput,
265 const Eigen::Quaternionf& imuQuat,
266 const Eigen::Quaternionf& cameraToImuRotation);
267};
268} // namespace hololib
Estimates the robot's field pose from AprilTag detections.
ApriltagLocalization(pros::AIVision &visionSensor, pros::Imu &imu, std::function< hololib::Pose(bool)> odomPoseGetter, const Eigen::Matrix3f &cameraMatrix, const Eigen::Vector< float, 5 > &distCoeffs, const Eigen::Vector2f &visionSensorOffset=Eigen::Vector2f::Zero(), float visionSensorYawOffset=0.0f)
Constructs an AprilTag localization system.
Pose estimateRobotPose(const IPPEOutput &ippeOutput, const Pose &odometryPose, int tagId) const
Estimates the robot's field pose from a 6-DOF camera pose produced by IPPE.
Pose getPoseFromTag(bool useRadians=false)
Returns the latest tag-based pose estimate.
void update(uint32_t period_ms)
Continuously processes tag detections and updates the estimated pose.
void startTask(uint32_t period_ms=10)
Starts the localization task if it is not already running.
pros::Imu imu
pros::AIVision visionSensor
The two pose solutions produced by the IPPE square solver.
Eigen::Vector3f rvec2
Rotation vector for the alternate solution, in radians.
Eigen::Vector3f tvec2
Translation vector for the alternate solution.
Eigen::Vector3f tvec1
Translation vector for the lower-error solution.
bool valid
Whether both solutions contain finite reprojection errors.
float reprojection_error1
Reprojection error of the first solution, in pixels.
Eigen::Vector3f rvec1
Rotation vector for the lower-error solution, in radians.
float reprojection_error2
Reprojection error of the second solution, in pixels.