wave/measurements.rs
1//! Sensor Measurements
2
3use core::f32::consts::FRAC_PI_2;
4
5use crate::math::{CoordsCartesian, CoordsSpherical};
6
7/// The recognized hand state.
8#[repr(C)]
9#[derive(Debug, Clone, Copy)]
10pub enum HandState {
11 /// No hand was found
12 HandNotFound,
13 /// Hand was found with this position
14 HandFound {
15 /// The hand position in spherical coordinates
16 hand_pos: CoordsSpherical,
17 },
18}
19
20/// Configurable sensor parameters. Different for every sensor.
21#[repr(C)]
22#[derive(Debug, Clone, Copy)]
23pub struct SensorParams {
24 /// The horizontal FOV of the sensor
25 pub fov_horizontal: f32,
26 /// The verctical FOV of the sensor
27 pub fov_vertical: f32,
28}
29
30impl SensorParams {
31 /// The default parameters for the ST VL53L5CX TOF-Sensor.
32 pub fn default_vl53l5cx() -> Self {
33 Self {
34 // The VL53L5CX has a diagonal FOV of 63deg, so fov_x = fov_y = 63.0 / sqrt(2) = 45.0
35 fov_horizontal: 45.0,
36 fov_vertical: 45.0,
37 }
38 }
39}
40
41/// Represents a sensor measurement coming from the TOF sensor.
42///
43/// Expects that the zones are already rotated and mirrored,
44/// so that the zone with index \[0\]\[0\] is the top left corner when looking at the sensor
45#[repr(C)]
46#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
47pub struct SensorMeasurement<const RES_X: usize, const RES_Y: usize> {
48 /// The measured distances of each zone.
49 ///
50 /// Invalid distance measurements are represented by value -1.0
51 pub zone_dist: [[f32; RES_X]; RES_Y],
52 /// The time of the measurement in milliseconds. Must be monotonically increasing.
53 pub time_ms: u32,
54}
55
56impl<const RES_X: usize, const RES_Y: usize> SensorMeasurement<RES_X, RES_Y> {
57 /// Creates a new measurement
58 pub fn new(zone_dist: [[f32; RES_X]; RES_Y]) -> Self {
59 Self {
60 zone_dist,
61 time_ms: 0,
62 }
63 }
64
65 /// An invalid measurement. The time is set to zero, distances are set to invalid ( value -1.0)
66 pub fn invalid() -> Self {
67 Self {
68 zone_dist: [[-1.0; RES_X]; RES_Y],
69 time_ms: 0,
70 }
71 }
72
73 /// Finds the position in the matrix and distance value of the zone with minimal distance
74 ///
75 /// Returns the tuple: (["x-pos in matrix", "y-pos in matrix"], "distance")
76 pub(crate) fn min_dist(&self) -> ([usize; 2], f32) {
77 self.zone_dist
78 .into_iter()
79 .enumerate()
80 .flat_map(|(y_pos, r)| {
81 r.into_iter().enumerate().filter_map(move |(x_pos, d)| {
82 if d > 0.0 {
83 Some(([x_pos, y_pos], d))
84 } else {
85 None
86 }
87 })
88 })
89 .fold(([0, 0], f32::MAX), |(acc_pos, acc_d), (pos, d)| {
90 if acc_d <= d {
91 (acc_pos, acc_d)
92 } else {
93 (pos, d)
94 }
95 })
96 }
97
98 /// Finds the minimal distance of each column.
99 #[allow(dead_code)]
100 pub(crate) fn min_dist_cols(&self) -> [f32; RES_X] {
101 let mut columns = [f32::MAX; RES_X];
102
103 for (i, c) in self.zone_dist.iter().enumerate() {
104 columns[i] = c
105 .iter()
106 .filter_map(|&d| if d > 0.0 { Some(d) } else { None })
107 .fold(f32::MAX, f32::min)
108 }
109
110 columns
111 }
112
113 /// Attempts to recognize a hand from the measurement and finds its position, distance, etc.
114 pub(crate) fn recognize_hand(&self, params: &SensorParams, threshold_dist: f32) -> HandState {
115 let hand_pos = self.hand_pos(params);
116
117 if hand_pos.r > 0.0 && hand_pos.r <= threshold_dist {
118 HandState::HandFound { hand_pos }
119 } else {
120 HandState::HandNotFound
121 }
122 }
123
124 /// Attempts to find the hand position. Expects that there is at least one valid distance value in one of the zones,
125 /// else returns invalid spherical coordinates.
126 fn hand_pos(&self, params: &SensorParams) -> CoordsSpherical {
127 // First calculate the positions of the zone measurements
128 let mut zones_pos = [[CoordsSpherical::invalid(); RES_X]; RES_Y];
129
130 for (pos_y, row) in self.zone_dist.iter().enumerate() {
131 for (pos_x, zone) in row.iter().enumerate() {
132 zones_pos[pos_y][pos_x] =
133 dist_position_spher::<RES_X, RES_Y>(*zone, pos_x, pos_y, params);
134 }
135 }
136
137 let (min_dist_zone_pos, _min_dist) = self.min_dist();
138 let min_pos_spher = zones_pos[min_dist_zone_pos[1]][min_dist_zone_pos[0]];
139
140 // Then calculate the the hand position as weighted average mean, given the smallest distance as initial position and weighing in the distance of the other measurements to it.
141
142 // calculates the weight of the positions in regards to min_pos for the weighted average mean
143 fn pos_weight(min_pos: &CoordsCartesian, other: &CoordsCartesian) -> f32 {
144 /// This is the factor that determines how much the distance weighs in into the average mean.
145 ///
146 /// E.g. a factor of 10.0 means that a position 1cm away from the measurement with min dist weighs in with 10.0,
147 /// a position 2cm away weighs in with 5.0.
148 const WEIGHT_FACTOR: f32 = 100.0;
149
150 let dist_to = min_pos.dist_to(other);
151
152 WEIGHT_FACTOR / dist_to
153 //1.0 / dist_to
154 }
155
156 // The sum of the positions multiplied by the weight (numerator of the weighted mean)
157 let coords_sum_weighted = (0..RES_Y)
158 .flat_map(|pos_y| {
159 (0..RES_X).filter_map(move |pos_x| {
160 if zones_pos[pos_y][pos_x].is_invalid() {
161 return None;
162 }
163 let c_cart = CoordsCartesian::from(zones_pos[pos_y][pos_x]);
164
165 let weight = if pos_x == min_dist_zone_pos[0] && pos_y == min_dist_zone_pos[1] {
166 // if we are the zone with min dist, skip the weight calc
167 1.0
168 } else {
169 pos_weight(&min_pos_spher.into(), &c_cart)
170 };
171
172 Some(CoordsCartesian {
173 x: c_cart.x * weight,
174 y: c_cart.y * weight,
175 z: c_cart.z * weight,
176 })
177 })
178 })
179 .fold(CoordsCartesian::zero(), |mut acc, x| {
180 acc.x += x.x;
181 acc.y += x.y;
182 acc.z += x.z;
183 acc
184 });
185
186 // The sum of the weights (denominator of the weighted mean)
187 let summed_weight: f32 = (0..RES_Y)
188 .flat_map(|pos_y| {
189 (0..RES_X).filter_map(move |pos_x| {
190 if zones_pos[pos_y][pos_x].is_invalid() {
191 return None;
192 }
193 let c_cart = CoordsCartesian::from(zones_pos[pos_y][pos_x]);
194
195 let weight = if pos_x == min_dist_zone_pos[0] && pos_y == min_dist_zone_pos[1] {
196 // if we are the zone with min dist, skip the weight calc
197 1.0
198 } else {
199 pos_weight(&min_pos_spher.into(), &c_cart)
200 };
201
202 Some(weight)
203 })
204 })
205 .sum();
206
207 if summed_weight == 0.0 {
208 return CoordsSpherical::invalid();
209 }
210
211 let mean = CoordsCartesian {
212 x: coords_sum_weighted.x / summed_weight,
213 y: coords_sum_weighted.y / summed_weight,
214 z: coords_sum_weighted.z / summed_weight,
215 };
216 mean.into()
217
218 /*
219 // Simply use the zone with min dist as pos
220 let (min_dist_zone_pos, min_dist) = self.min_dist();
221
222 dist_position_spher::<RES_X, RES_Y>(
223 min_dist,
224 min_dist_zone_pos[0],
225 min_dist_zone_pos[1],
226 params,
227 )
228 */
229 }
230}
231
232/// Calculates the position in space for the measurement in a sensor zone.
233/// dist: the distance value of the measurement
234/// zone_pos_x: The x-index of the zone in the sensor grid
235/// zone_pos_y: The y-index of the zone in the sensor grid
236///
237/// Returns the position in spherical coordinates
238pub(crate) fn dist_position_spher<const RES_X: usize, const RES_Y: usize>(
239 dist: f32,
240 zone_pos_x: usize,
241 zone_pos_y: usize,
242 params: &SensorParams,
243) -> CoordsSpherical {
244 let angle_per_zone_hor = (params.fov_horizontal / (RES_X as f32)).to_radians();
245 let angle_per_zone_vert = (params.fov_vertical / (RES_Y as f32)).to_radians();
246
247 let r = dist;
248
249 if r < 0.0 {
250 return CoordsSpherical::invalid();
251 }
252
253 let theta = (zone_pos_x as f32 - (RES_X as f32 / 2.0)) * angle_per_zone_hor;
254 // 90.0 deg - .. because the z-axis is pointing up from the center of the sensor grid
255 let phi = FRAC_PI_2 - (zone_pos_y as f32 - (RES_Y as f32 / 2.0)) * angle_per_zone_vert;
256
257 CoordsSpherical { r, theta, phi }
258}
259
260/// Finds the nearest zone for all given measurements.
261///
262/// Returns the tuple: ("index of the measurement containing the nearest zone", ["x-pos in matrix", "y-pos in matrix"], "distance")
263pub(crate) fn find_nearest_zone<
264 const RES_X: usize,
265 const RES_Y: usize,
266 T: IntoIterator<Item = SensorMeasurement<RES_X, RES_Y>>,
267>(
268 measurements: T,
269) -> (usize, [usize; 2], f32) {
270 measurements
271 .into_iter()
272 .enumerate()
273 .fold((0, [0, 0], f32::MAX), |acc, m| {
274 let m_min_dist = m.1.min_dist();
275
276 if acc.2 <= m_min_dist.1 {
277 acc
278 } else {
279 (m.0, m_min_dist.0, m_min_dist.1)
280 }
281 })
282}
283
284/// Finds the nearest hand position for all given hand states.
285/// If the iterator contains no valid hand state, it returns index 0 and invalid spherical coordinates
286///
287/// Returns the tuple: ("index of the hand state containing the nearest pos", "hand pos")
288#[allow(dead_code)]
289pub(crate) fn find_nearest_hand_pos<T: IntoIterator<Item = HandState>>(
290 states: T,
291) -> (usize, CoordsSpherical) {
292 states
293 .into_iter()
294 .enumerate()
295 .filter_map(|(i, hs)| {
296 if let HandState::HandFound { hand_pos } = hs {
297 Some((i, hand_pos))
298 } else {
299 None
300 }
301 })
302 .fold(
303 (0, CoordsSpherical::invalid()),
304 |(acc_i, acc_pos), (i, pos)| {
305 if pos.r < acc_pos.r {
306 (i, pos)
307 } else {
308 (acc_i, acc_pos)
309 }
310 },
311 )
312}