bbox3d_estimator/
lib.rs

1mod caliper;
2
3use crate::caliper::Caliper;
4use geo::{prelude::*, Coord, Line};
5use itertools::{izip, Itertools};
6use nalgebra as na;
7use noisy_float::prelude::*;
8use std::f64::consts::{FRAC_PI_2, PI};
9
10const EPSILON: f64 = 1e-6;
11
12#[derive(Debug, Clone)]
13pub struct BBox3D {
14    /// The center coordinates of the box in `[x, y, z]`.
15    pub center: [f64; 3],
16
17    /// The extent of the box. The first element corresponds to the
18    /// extent along the X-axis if the box is not rotated.
19    pub extent: [f64; 3],
20
21    /// The quaternion of 3D box stored in `[i, j, k, w]` order.
22    pub rotation: [f64; 4],
23}
24
25/// Fit a rotated 3D box on a set of points.
26pub fn get_rotated_bbox3d(points: impl IntoIterator<Item = [f64; 3]>) -> Option<BBox3D> {
27    let (points3d, points2d): (Vec<na::Point3<f64>>, Vec<geo::Point>) = points
28        .into_iter()
29        .map(|[x, y, z]| {
30            let point3d = na::Point3::new(x, y, z);
31            let point2d = geo::Point::new(point3d.x, point3d.y);
32            (point3d, point2d)
33        })
34        .unzip();
35
36    let convex_hull: Vec<geo::Point<f64>> = {
37        let convex_hull = geo::MultiPoint::from(points2d.clone()).convex_hull();
38        let exterior = convex_hull.exterior();
39        let num_points = exterior.0.len();
40        exterior.points().take(num_points - 1).collect()
41    };
42
43    let ((bottom, _), (top, _)) = convex_hull
44        .iter()
45        .map(|point| point.y())
46        .map(r64)
47        .enumerate()
48        .minmax_by_key(|&(_, y)| y)
49        .into_option()?;
50    let ((left, _), (right, _)) = convex_hull
51        .iter()
52        .map(|point| point.x())
53        .map(r64)
54        .enumerate()
55        .minmax_by_key(|&(_, x)| x)
56        .into_option()?;
57
58    let mut top_caliper = Caliper {
59        convex_hull: convex_hull.clone(),
60        index: top,
61        angle: PI,
62    };
63    let mut bottom_caliper = Caliper {
64        convex_hull: convex_hull.clone(),
65        index: bottom,
66        angle: 0.0,
67    };
68    let mut right_caliper = Caliper {
69        convex_hull: convex_hull.clone(),
70        index: right,
71        angle: FRAC_PI_2,
72    };
73    let mut left_caliper = Caliper {
74        convex_hull,
75        index: left,
76        angle: PI + FRAC_PI_2,
77    };
78
79    let mut bboxes: Vec<[(f64, f64); 4]> = vec![];
80
81    while bottom_caliper.angle < FRAC_PI_2 {
82        let vertices = [
83            bottom_caliper.intersection(&right_caliper),
84            top_caliper.intersection(&right_caliper),
85            top_caliper.intersection(&left_caliper),
86            bottom_caliper.intersection(&left_caliper),
87        ];
88        bboxes.push(vertices);
89
90        let angle = top_caliper.rotate_to_next();
91        let angle = if bottom_caliper.rotate_to_next() < angle {
92            bottom_caliper.rotate_to_next()
93        } else {
94            angle
95        };
96        let angle = if right_caliper.rotate_to_next() < angle {
97            right_caliper.rotate_to_next()
98        } else {
99            angle
100        };
101        let angle = if left_caliper.rotate_to_next() < angle {
102            left_caliper.rotate_to_next()
103        } else {
104            angle
105        };
106
107        top_caliper.rotate(angle);
108        bottom_caliper.rotate(angle);
109        right_caliper.rotate(angle);
110        left_caliper.rotate(angle);
111    }
112
113    let haus_error: Vec<f64> = {
114        let loss: Vec<f64> = bboxes
115            .iter()
116            .map(|bbox| hausdorff_distance(bbox, &points2d))
117            .collect();
118
119        let (min_loss, max_loss) = loss.iter().copied().minmax().into_option()?;
120        let loss_range = max_loss - min_loss;
121        if loss_range <= EPSILON {
122            return None;
123        }
124        loss.iter().map(|x| (x - min_loss) / loss_range).collect()
125    };
126    let area_error: Vec<f64> = {
127        let loss: Vec<f64> = bboxes.iter().map(bbox_area).collect();
128        let (min_loss, max_loss) = loss.iter().copied().minmax().into_option()?;
129        let loss_range = max_loss - min_loss;
130        if loss_range <= EPSILON {
131            return None;
132        }
133        loss.iter().map(|x| (x - min_loss) / loss_range).collect()
134    };
135    let loss: Vec<f64> = haus_error
136        .iter()
137        .zip(area_error)
138        .map(|(a, b)| a * 0.7 + b * 0.3)
139        .collect();
140
141    let (minimal_bbox, _) = izip!(&bboxes, &loss).min_by_key(|(_, &loss)| r64(loss))?;
142
143    let rotation = {
144        let from = na::Point2::new(minimal_bbox[0].0, minimal_bbox[0].1);
145        let to = na::Point2::new(minimal_bbox[1].0, minimal_bbox[1].1);
146        let vector = to - from;
147        let yaw = vector.y.atan2(vector.x);
148        na::UnitQuaternion::from_euler_angles(0.0, 0.0, yaw)
149    };
150
151    let z_minmax = points3d.iter().map(|p| p.z).map(r64).minmax();
152    let (z_min, z_max) = z_minmax.into_option()?;
153    let z_range = (z_max - z_min).raw();
154    if z_range <= EPSILON {
155        return None;
156    }
157
158    let center_x =
159        (minimal_bbox[0].0 + minimal_bbox[1].0 + minimal_bbox[2].0 + minimal_bbox[3].0) / 4.0;
160    let center_y =
161        (minimal_bbox[0].1 + minimal_bbox[1].1 + minimal_bbox[2].1 + minimal_bbox[3].1) / 4.0;
162    let center_z = z_min.raw() + z_range / 2.0;
163    let size_x = ((minimal_bbox[1].0 - minimal_bbox[0].0).powf(2.0)
164        + (minimal_bbox[1].1 - minimal_bbox[0].1).powf(2.0))
165    .sqrt();
166    let size_y = ((minimal_bbox[0].0 - minimal_bbox[3].0).powf(2.0)
167        + (minimal_bbox[0].1 - minimal_bbox[3].1).powf(2.0))
168    .sqrt();
169    let size_z = z_range;
170
171    Some(BBox3D {
172        center: [center_x, center_y, center_z],
173        extent: [size_x, size_y, size_z],
174        rotation: rotation.coords.into(),
175    })
176}
177
178fn bbox_area(vertex: &[(f64, f64); 4]) -> f64 {
179    let w = distance(vertex[0], vertex[3]);
180    let h = distance(vertex[0], vertex[1]);
181    w * h
182}
183
184fn distance(lhs: (f64, f64), rhs: (f64, f64)) -> f64 {
185    ((lhs.0 - rhs.0).powi(2) + (lhs.1 - rhs.1).powi(2)).sqrt()
186}
187
188fn point_to_bbox(vertex: &[(f64, f64); 4], point: &geo::Point<f64>) -> f64 {
189    let line: Vec<_> = vertex
190        .iter()
191        .map(|&(x, y)| Coord { x, y })
192        .circular_tuple_windows()
193        .map(|(lhs, rhs)| Line::new(lhs, rhs))
194        .collect();
195    line.iter()
196        .map(|line| point.euclidean_distance(line))
197        .map(r64)
198        .min()
199        .unwrap()
200        .raw()
201}
202
203// Update this from using all including points
204fn hausdorff_distance(vertex: &[(f64, f64); 4], covex_hull: &[geo::Point<f64>]) -> f64 {
205    covex_hull
206        .iter()
207        .map(|p| point_to_bbox(vertex, p))
208        .map(r64)
209        .sum::<R64>()
210        .raw()
211}