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}