James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 1 | #ifndef Y2019_CONTROL_LOOPS_DRIVETRAIN_CAMERA_H_ |
| 2 | #define Y2019_CONTROL_LOOPS_DRIVETRAIN_CAMERA_H_ |
| 3 | |
| 4 | #include <vector> |
| 5 | |
| 6 | #include "aos/containers/sized_array.h" |
| 7 | #include "frc971/control_loops/pose.h" |
| 8 | |
| 9 | namespace y2019 { |
| 10 | namespace control_loops { |
| 11 | |
| 12 | // Represents a target on the field. Currently just consists of a pose and a |
| 13 | // indicator for whether it is occluded (occlusion is only used by the simulator |
| 14 | // for testing). |
| 15 | // Orientation convention: |
| 16 | // -The absolute position of the pose is the center of the vision target on the |
| 17 | // field. |
| 18 | // -The yaw of the pose shall be such that the positive X-axis in the Target's |
| 19 | // frame wil be pointed straight through the target--i.e., if you are looking |
| 20 | // at the target head-on, then you will be facing in the same direction as the |
| 21 | // positive X-axis. |
| 22 | // E.g., if the Target has a global position of (1, 1, 0) and yaw of pi / 2, |
| 23 | // then it is at position (1, 1, 0) on the field and is oriented so that if |
| 24 | // someone were to stand at (1, 0, 0) and turn themselves to have a yaw of |
| 25 | // pi / 2, they would see the target 1 meter straight ahead of them. |
| 26 | // |
| 27 | // Generally, the position of a target should not change dynamically; if we do |
| 28 | // start having targets that move, we may want to start optimizing certain |
| 29 | // things (e.g., caching the position of the Target--currently, if the Pose of a |
| 30 | // target is in an absolute frame, then calling abs_pos will be inexpensive; if |
| 31 | // that changes, then we start having to recalculate transformations on every |
| 32 | // frame). |
| 33 | template <typename Scalar = double> |
| 34 | class TypedTarget { |
| 35 | public: |
| 36 | typedef ::frc971::control_loops::TypedPose<Scalar> Pose; |
| 37 | TypedTarget(const Pose &pose) : pose_(pose) {} |
James Kuszmaul | 090563a | 2019-02-09 14:43:20 -0800 | [diff] [blame] | 38 | TypedTarget() {} |
James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 39 | Pose pose() const { return pose_; } |
| 40 | |
| 41 | bool occluded() const { return occluded_; } |
| 42 | void set_occluded(bool occluded) { occluded_ = occluded; } |
| 43 | |
| 44 | // Get a list of points for plotting. These points should be plotted on |
| 45 | // an x/y plane in the global frame with lines connecting the points. |
| 46 | // Essentially, this provides a Polygon that is a reasonable representation |
| 47 | // of a Target. |
| 48 | // This should not be called from real-time code, as it will probably |
| 49 | // dynamically allocate memory. |
| 50 | ::std::vector<Pose> PlotPoints() const { |
| 51 | // For the actual representation, we will use a triangle such that the |
| 52 | // base of the triangle corresponds to the surface the target is on. |
| 53 | // The third point is shown behind the target, so that the user can |
| 54 | // visually identify which side of the target is the front. |
| 55 | Pose base1(&pose_, {0, 0.125, 0}, 0); |
| 56 | Pose base2(&pose_, {0, -0.125, 0}, 0); |
| 57 | Pose behind(&pose_, {0.05, 0, 0}, 0); |
| 58 | // Include behind at the start and end to indicate that we want to draw |
| 59 | // a closed polygon. |
| 60 | return {behind, base1, base2, behind}; |
| 61 | } |
| 62 | |
| 63 | private: |
| 64 | Pose pose_; |
| 65 | bool occluded_ = false; |
| 66 | }; // class TypedTarget |
| 67 | |
| 68 | typedef TypedTarget<double> Target; |
| 69 | |
| 70 | // Represents a camera that can see targets and provide information about their |
| 71 | // relative positions. |
| 72 | // Note on coordinate systems: |
| 73 | // -The camera's Pose shall be such that the camera frame's positive X-axis is |
| 74 | // pointed straight out of the lens (as always, positive Z will be up; we |
| 75 | // assume that all cameras mounted level, or compensated for such that this |
| 76 | // code won't care). |
| 77 | // -The origin of the camera shall be "at" the camera. For this code, I don't |
| 78 | // think we care too much about the details of the camera model, so we can just |
| 79 | // assume that it is an idealized pinhole camera with the pinhole being the |
| 80 | // location of the camera. |
| 81 | // |
| 82 | // Template parameters: |
| 83 | // -num_targets: The number of targets on the field, should be the same for |
| 84 | // all the actual cameras on the robot (although it may change in tests). |
| 85 | // -Scalar: The floating point type to use (double vs. float). |
| 86 | // -num_obstacles: The number of obstacles on the field to account for; like |
| 87 | // the number of targets, it should be consistent across actual cameras, |
| 88 | // although for simulation we may add extra obstacles for testing. |
| 89 | template <int num_targets, int num_obstacles, typename Scalar = double> |
| 90 | class TypedCamera { |
| 91 | public: |
| 92 | typedef ::frc971::control_loops::TypedPose<Scalar> Pose; |
| 93 | typedef ::frc971::control_loops::TypedLineSegment<Scalar> LineSegment; |
| 94 | |
| 95 | // TargetView contains the information associated with a sensor reading |
| 96 | // from the camera--the readings themselves and noise values, *from the |
| 97 | // Camera's persective* for each reading. |
| 98 | // Note that the noise terms are just accounting for the inaccuracy you |
| 99 | // expect to get due to visual noise, pixel-level resolution, etc. These |
| 100 | // do not account for the fact that, e.g., there is noise in the Pose of the |
| 101 | // robot which can translate into noise in the target reading. |
| 102 | // The noise terms are standard deviations, and so have units identical |
| 103 | // to that of the actual readings. |
| 104 | struct TargetView { |
| 105 | struct Reading { |
| 106 | // The heading as reported from the camera; zero = straight ahead, |
| 107 | // positive = target in the left half of the image. |
| 108 | Scalar heading; // radians |
| 109 | // The distance from the camera to the target. |
| 110 | Scalar distance; // meters |
James Kuszmaul | 81df16a | 2019-03-03 17:17:34 -0800 | [diff] [blame] | 111 | // Height of the target from the camera. |
James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 112 | Scalar height; // meters |
James Kuszmaul | 289756f | 2019-03-05 21:52:10 -0800 | [diff] [blame^] | 113 | // The angle of the target relative to line between the camera and |
| 114 | // the center of the target. |
James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 115 | Scalar skew; // radians |
| 116 | }; |
| 117 | Reading reading; |
| 118 | Reading noise; |
| 119 | |
| 120 | // The target that this view corresponds to. |
James Kuszmaul | 09f564a | 2019-02-18 17:37:09 -0800 | [diff] [blame] | 121 | const TypedTarget<Scalar> *target = nullptr; |
James Kuszmaul | 090563a | 2019-02-09 14:43:20 -0800 | [diff] [blame] | 122 | // The Pose the camera was at when viewing the target: |
| 123 | Pose camera_pose; |
James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 124 | }; |
| 125 | |
| 126 | // Important parameters for dealing with camera noise calculations. |
| 127 | // Ultimately, this should end up coming from the constants file. |
| 128 | struct NoiseParameters { |
| 129 | // The maximum distance from which we can see a target head-on (when the |
| 130 | // target is not head-on, we adjust for that). |
| 131 | Scalar max_viewable_distance; // meters |
| 132 | |
| 133 | // All noises are standard deviations of the noise, assuming an ~normal |
| 134 | // distribution. |
| 135 | |
| 136 | // Noise in the heading measurement, which should be constant regardless of |
| 137 | // other factors. |
| 138 | Scalar heading_noise; // radians |
| 139 | // Noise in the distance measurement when the target is 1m away and head-on |
| 140 | // to us. This is adjusted by assuming the noise is proportional to the |
| 141 | // apparent width of the target (because the target also has height, this |
| 142 | // may not be strictly correct). |
| 143 | // TODO(james): Is this a good model? It should be reasonable, but there |
| 144 | // may be more complexity somewhere. |
| 145 | Scalar nominal_distance_noise; // meters |
| 146 | // The noise in the skew measurement when the target is 1m away and head-on |
| 147 | // to us. Calculated in the same manner with the same caveats as the |
| 148 | // distance noise. |
| 149 | Scalar nominal_skew_noise; // radians |
| 150 | // Noise in the height measurement, same rules as for skew and distance. |
| 151 | // TODO(james): Figure out how much noise we will actually get in the |
| 152 | // height, since there will be extremely low resolution on it. |
| 153 | Scalar nominal_height_noise; // meters |
| 154 | }; |
| 155 | |
James Kuszmaul | 09f564a | 2019-02-18 17:37:09 -0800 | [diff] [blame] | 156 | // Provide a default constructor to make life easier. |
| 157 | TypedCamera() {} |
| 158 | |
James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 159 | // Creates a camera: |
| 160 | // pose: The Pose of the camera, relative to the robot at least transitively. |
| 161 | // fov: The field-of-view of the camera, in radians. Note that this is the |
| 162 | // *total* field-of-view in the horizontal plane (left-right), so the angle |
| 163 | // from the left edge of the image to the right edge. |
| 164 | // targets: The list of targets on the field that could be seen by the camera. |
| 165 | // obstacles: The list of known obstacles on the field. |
| 166 | TypedCamera(const Pose &pose, Scalar fov, |
| 167 | const NoiseParameters &noise_parameters, |
| 168 | const ::std::array<TypedTarget<Scalar>, num_targets> &targets, |
| 169 | const ::std::array<LineSegment, num_obstacles> &obstacles) |
| 170 | : pose_(pose), |
| 171 | fov_(fov), |
| 172 | noise_parameters_(noise_parameters), |
| 173 | targets_(targets), |
| 174 | obstacles_(obstacles) {} |
| 175 | |
| 176 | // Returns a list of TargetViews for all the currently visible targets. |
| 177 | // These will contain ground-truth TargetViews, so the readings will be |
| 178 | // perfect; a pseudo-random number generator should be used to add noise |
| 179 | // separately for simulation. |
| 180 | ::aos::SizedArray<TargetView, num_targets> target_views() const { |
| 181 | ::aos::SizedArray<TargetView, num_targets> views; |
James Kuszmaul | 090563a | 2019-02-09 14:43:20 -0800 | [diff] [blame] | 182 | Pose camera_abs_pose = pose_.Rebase(nullptr); |
James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 183 | // Because there are num_targets in targets_ and because AddTargetIfVisible |
| 184 | // adds at most 1 view to views, we should never exceed the size of |
| 185 | // SizedArray. |
| 186 | for (const auto &target : targets_) { |
James Kuszmaul | 090563a | 2019-02-09 14:43:20 -0800 | [diff] [blame] | 187 | AddTargetIfVisible(target, camera_abs_pose, &views); |
James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 188 | } |
| 189 | return views; |
| 190 | } |
| 191 | |
| 192 | // Returns a list of list of points for plotting. Each list of points should |
| 193 | // be plotted as a line; currently, each list is just a pair drawing a line |
| 194 | // from the camera aperture to the target location. |
| 195 | // This should not be called from real-time code, as it will probably |
| 196 | // dynamically allocate memory. |
| 197 | ::std::vector<::std::vector<Pose>> PlotPoints() const { |
| 198 | ::std::vector<::std::vector<Pose>> list_of_lists; |
| 199 | for (const auto &view : target_views()) { |
James Kuszmaul | 1057ce8 | 2019-02-09 17:58:24 -0800 | [diff] [blame] | 200 | list_of_lists.push_back({pose_, view.target->pose().Rebase(&pose_)}); |
James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 201 | } |
| 202 | return list_of_lists; |
| 203 | } |
| 204 | |
James Kuszmaul | 090563a | 2019-02-09 14:43:20 -0800 | [diff] [blame] | 205 | const Pose &pose() const { return pose_; } |
James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 206 | Scalar fov() const { return fov_; } |
| 207 | |
James Kuszmaul | 09f564a | 2019-02-18 17:37:09 -0800 | [diff] [blame] | 208 | // Estimates the noise values of a target based on the raw readings. |
| 209 | // Also estimates whether we would expect the target to be visible, and |
| 210 | // populates is_visible if is_visible is not nullptr. |
| 211 | void PopulateNoise(TargetView *view, bool *is_visible = nullptr) const { |
| 212 | // Calculate the width of the target as it appears in the image. |
| 213 | // This number is unitless and if greater than 1, implies that the target is |
| 214 | // visible to the camera and if less than 1 implies it is too small to be |
| 215 | // registered on the camera. |
| 216 | Scalar apparent_width = |
| 217 | ::std::max<Scalar>(0.0, ::std::cos(view->reading.skew) * |
| 218 | noise_parameters_.max_viewable_distance / |
| 219 | view->reading.distance); |
| 220 | // As both a sanity check and for the sake of numerical stability, manually |
| 221 | // set apparent_width to something "very small" if the distance is too |
| 222 | // close. |
| 223 | if (view->reading.distance < 0.01) { |
| 224 | apparent_width = 0.01; |
| 225 | } |
| 226 | |
| 227 | if (is_visible != nullptr) { |
| 228 | *is_visible = apparent_width >= 1.0; |
| 229 | } |
| 230 | |
| 231 | view->noise.heading = noise_parameters_.heading_noise; |
| 232 | |
| 233 | const Scalar normalized_width = |
| 234 | apparent_width / noise_parameters_.max_viewable_distance; |
| 235 | view->noise.distance = |
| 236 | noise_parameters_.nominal_distance_noise / normalized_width; |
| 237 | view->noise.skew = |
| 238 | noise_parameters_.nominal_skew_noise / normalized_width; |
| 239 | view->noise.height = |
| 240 | noise_parameters_.nominal_height_noise / normalized_width; |
| 241 | } |
| 242 | |
James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 243 | private: |
| 244 | |
| 245 | // If the specified target is visible from the current camera Pose, adds it to |
| 246 | // the views array. |
| 247 | void AddTargetIfVisible( |
James Kuszmaul | 090563a | 2019-02-09 14:43:20 -0800 | [diff] [blame] | 248 | const TypedTarget<Scalar> &target, const Pose &camera_abs_pose, |
James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 249 | ::aos::SizedArray<TargetView, num_targets> *views) const; |
| 250 | |
| 251 | // The Pose of this camera. |
James Kuszmaul | 09f564a | 2019-02-18 17:37:09 -0800 | [diff] [blame] | 252 | Pose pose_; |
James Kuszmaul | 090563a | 2019-02-09 14:43:20 -0800 | [diff] [blame] | 253 | |
James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 254 | // Field of view of the camera, in radians. |
James Kuszmaul | 09f564a | 2019-02-18 17:37:09 -0800 | [diff] [blame] | 255 | Scalar fov_; |
James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 256 | |
| 257 | // Various constants to calclate sensor noise; see definition of |
| 258 | // NoiseParameters for more detail. |
James Kuszmaul | 09f564a | 2019-02-18 17:37:09 -0800 | [diff] [blame] | 259 | NoiseParameters noise_parameters_; |
James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 260 | |
| 261 | // A list of all the targets on the field. |
James Kuszmaul | 090563a | 2019-02-09 14:43:20 -0800 | [diff] [blame] | 262 | // TODO(james): Is it worth creating some sort of cache for the targets and |
| 263 | // obstacles? e.g., passing around pointer to the targets/obstacles. |
James Kuszmaul | 09f564a | 2019-02-18 17:37:09 -0800 | [diff] [blame] | 264 | ::std::array<TypedTarget<Scalar>, num_targets> targets_; |
James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 265 | // Known obstacles on the field which can interfere with our view of the |
| 266 | // targets. An "obstacle" is a line segment which we cannot see through, as |
| 267 | // such a logical obstacle (e.g., the cargo ship) may consist of many |
| 268 | // obstacles in this list to account for all of its sides. |
James Kuszmaul | 09f564a | 2019-02-18 17:37:09 -0800 | [diff] [blame] | 269 | ::std::array<LineSegment, num_obstacles> obstacles_; |
James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 270 | }; // class TypedCamera |
| 271 | |
| 272 | template <int num_targets, int num_obstacles, typename Scalar> |
| 273 | void TypedCamera<num_targets, num_obstacles, Scalar>::AddTargetIfVisible( |
James Kuszmaul | 090563a | 2019-02-09 14:43:20 -0800 | [diff] [blame] | 274 | const TypedTarget<Scalar> &target, const Pose &camera_abs_pose, |
James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 275 | ::aos::SizedArray<TargetView, num_targets> *views) const { |
| 276 | if (target.occluded()) { |
| 277 | return; |
| 278 | } |
| 279 | |
| 280 | // Precompute the current absolute pose of the camera, because we will reuse |
| 281 | // it a bunch. |
James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 282 | const Pose relative_pose = target.pose().Rebase(&camera_abs_pose); |
| 283 | const Scalar heading = relative_pose.heading(); |
| 284 | const Scalar distance = relative_pose.xy_norm(); |
James Kuszmaul | 289756f | 2019-03-05 21:52:10 -0800 | [diff] [blame^] | 285 | const Scalar skew = |
| 286 | ::aos::math::NormalizeAngle(relative_pose.rel_theta() - heading); |
James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 287 | |
| 288 | // Check if the camera is in the angular FOV. |
| 289 | if (::std::abs(heading) > fov_ / 2.0) { |
| 290 | return; |
| 291 | } |
| 292 | |
James Kuszmaul | 09f564a | 2019-02-18 17:37:09 -0800 | [diff] [blame] | 293 | TargetView view; |
| 294 | view.reading.heading = heading; |
| 295 | view.reading.distance = distance; |
| 296 | view.reading.skew = skew; |
James Kuszmaul | 81df16a | 2019-03-03 17:17:34 -0800 | [diff] [blame] | 297 | view.reading.height = relative_pose.rel_pos().z(); |
James Kuszmaul | 09f564a | 2019-02-18 17:37:09 -0800 | [diff] [blame] | 298 | view.target = ⌖ |
| 299 | view.camera_pose = camera_abs_pose; |
James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 300 | |
James Kuszmaul | 09f564a | 2019-02-18 17:37:09 -0800 | [diff] [blame] | 301 | bool is_visible = false; |
| 302 | |
| 303 | PopulateNoise(&view, &is_visible); |
| 304 | |
| 305 | if (!is_visible) { |
James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 306 | return; |
| 307 | } |
| 308 | |
| 309 | // Final visibility check is for whether there are any obstacles blocking or |
| 310 | // line of sight. |
| 311 | for (const auto &obstacle : obstacles_) { |
| 312 | if (obstacle.Intersects({camera_abs_pose, target.pose()})) { |
| 313 | return; |
| 314 | } |
| 315 | } |
| 316 | |
James Kuszmaul | 09f564a | 2019-02-18 17:37:09 -0800 | [diff] [blame] | 317 | // At this point, we've passed all the checks to ensure that the target is |
| 318 | // visible and we can add it to the list of targets. |
James Kuszmaul | 57c7c9b | 2019-01-27 16:16:01 -0800 | [diff] [blame] | 319 | views->push_back(view); |
| 320 | } |
| 321 | |
| 322 | } // namespace control_loops |
| 323 | } // namespace y2019 |
| 324 | |
| 325 | #endif // Y2019_CONTROL_LOOPS_DRIVETRAIN_CAMERA_H_ |