ifc_lite_processing/scan_segmentation/
report.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Measured output of scan segmentation. Coordinates are in the positions'
6//! frame plus `options.origin`, in metres.
7use serde::{Deserialize, Serialize};
8
9/// Classification hint relative to `options.up_axis`.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub enum PlaneOrientation {
13    /// Normal within `classificationAngleDegrees` of up: floor, ceiling, slab.
14    Horizontal,
15    /// Normal within that angle of perpendicular to up: wall, column face.
16    Vertical,
17    Sloped,
18}
19
20/// Which side the reported normal faces.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub enum NormalSource {
24    /// Toward `options.scanner_position`: the scanned (visible) side.
25    Scanner,
26    /// No scanner given: horizontal planes face up; other planes face along
27    /// their largest normal component's positive axis. Says nothing about
28    /// which side was scanned.
29    Canonical,
30}
31
32/// Oriented box of the plane's inlier voxel means within the plane. For
33/// vertical and sloped planes `v_axis` points up (along the fall line) and
34/// `u_axis` is horizontal; horizontal planes take the minimum-area rectangle.
35/// `u_axis x v_axis = normal`.
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct PlaneExtent {
39    pub center: [f64; 3],
40    pub u_axis: [f64; 3],
41    pub v_axis: [f64; 3],
42    pub u_length: f64,
43    pub v_length: f64,
44    /// Counter-clockwise about the normal, starting at (-u, -v).
45    pub corners: [[f64; 3]; 4],
46}
47
48#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
49#[serde(rename_all = "camelCase")]
50pub struct ScanPlane {
51    /// Unit normal; `normal . x + d = 0`.
52    pub normal: [f64; 3],
53    pub d: f64,
54    /// Mean of the inlier voxel means.
55    pub centroid: [f64; 3],
56    /// Input points inside the inlier voxels.
57    pub inlier_points: u64,
58    pub inlier_voxels: u32,
59    /// Occupied voxel-sized cells after projecting the inliers onto the plane.
60    pub area_square_metres: f64,
61    /// RMS distance of the inlier voxel means to the plane.
62    pub rms_metres: f64,
63    pub extent: PlaneExtent,
64    pub orientation: PlaneOrientation,
65    pub normal_source: NormalSource,
66}
67
68/// Axis direction relative to `options.up_axis`, with the plane tolerance.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(rename_all = "camelCase")]
71pub enum AxisOrientation {
72    /// Axis along up: a column.
73    Vertical,
74    /// Axis perpendicular to up: a horizontal pipe or beam.
75    Horizontal,
76    Sloped,
77}
78
79/// The faces of a polygonal (prism) column: a regular polygon of `faces`
80/// sides about the cylinder's axis, `radius` being its circumradius (axis to
81/// corner).
82#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83#[serde(rename_all = "camelCase")]
84pub struct ScanFacets {
85    /// Number of sides of the whole polygon (6 or more), seen or not.
86    pub faces: u32,
87    /// Unit outward normal of one face, perpendicular to the axis; the others
88    /// follow at multiples of 360 / `faces` degrees about the axis. Of the
89    /// equivalent choices, the one nearest the first in-plane basis direction
90    /// (see the guide) is reported, so it is deterministic.
91    pub face_normal: [f64; 3],
92    /// Distance from the axis to each face (inradius).
93    pub apothem: f64,
94}
95
96/// A detected cylinder (column, pipe). The axis runs from `axis_start` to
97/// `axis_end` over the inliers' extent. A polygonal column of six or more
98/// faces is reported here too, with `faceted` set.
99#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
100#[serde(rename_all = "camelCase")]
101pub struct ScanCylinder {
102    pub axis_start: [f64; 3],
103    pub axis_end: [f64; 3],
104    /// Unit; points up for vertical and sloped axes, along the positive axis
105    /// of its largest component for horizontal ones.
106    pub axis_direction: [f64; 3],
107    /// Radius; for a faceted column the circumradius (axis to corner).
108    pub radius: f64,
109    pub length: f64,
110    /// Lowest and highest axis end, measured along `up_axis`.
111    pub height_range: [f64; 2],
112    /// Share of the circumference the inliers cover, in degrees. For pieces
113    /// joined across a density gap, the larger piece's arc (a lower bound).
114    pub arc_degrees: f64,
115    pub inlier_points: u64,
116    pub inlier_voxels: u32,
117    /// RMS radial distance of the inlier voxel means from the surface.
118    pub rms_metres: f64,
119    pub orientation: AxisOrientation,
120    /// Set for a polygonal column (a ring of six or more flat faces); None
121    /// for a round surface.
122    pub faceted: Option<ScanFacets>,
123}
124
125#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
126#[serde(rename_all = "camelCase")]
127pub struct ScanSegmentationStats {
128    pub input_points: u64,
129    pub accepted_points: u64,
130    /// Non-finite, or farther than 1e6 m from the positions' frame origin.
131    pub rejected_points: u64,
132    pub outside_region_points: u64,
133    /// Spacing of adjacent f32 values at the largest accepted coordinate.
134    /// Positions far from their frame origin cannot resolve finer than this.
135    pub coordinate_spacing_metres: f64,
136    /// Effective voxel edge after any coarsening (the quantised lattice step).
137    pub voxel_size_metres: f64,
138    pub coarsenings: u32,
139    pub voxels: u64,
140    pub voxels_with_normals: u64,
141    pub seed_voxels: u64,
142    /// The curvature gate the seeds passed: `maxSeedCurvature`, or, when noise
143    /// lifts even flat voxels above it, the curvature of the flattest 5 %.
144    pub seed_curvature: f64,
145    pub regions_grown: u64,
146    /// Coplanar adjacent regions joined into one.
147    pub regions_merged: u64,
148    /// Regions refused because their normals turn faster than `maxBendPerMetre`.
149    pub curved_regions_rejected: u64,
150    /// Regions refused for an area under `minPlaneAreaSquareMetres`.
151    pub small_regions_rejected: u64,
152    pub planar_voxels: u64,
153    /// Smoothly connected non-planar voxel groups large enough to try.
154    pub cylinder_groups: u64,
155    /// Candidates whose inliers fit a sphere at least as well.
156    pub cylinders_rejected_as_spheres: u64,
157    /// Groups whose best RANSAC candidate fit under `minCylinderInlierFraction`
158    /// of the group (or no pair defined a candidate at all).
159    pub cylinder_candidates_below_share: u64,
160    /// Candidates whose least-squares refit kept too few inliers or degenerated.
161    pub cylinder_refits_failed: u64,
162    /// Refits outside the radius range; every group when the range is empty
163    /// (an unset minimum, two voxels, above `maxCylinderRadiusMetres`).
164    pub cylinders_rejected_for_radius: u64,
165    /// Candidates whose inliers cover under 40 % of the patch their length and
166    /// arc claim: loose fits through scattered voxels, not a surface.
167    pub cylinders_rejected_as_sparse: u64,
168    /// Candidates whose normals do not behave as a round surface's: they
169    /// point more than 11 degrees (RMS over 5 degree bins) away from the
170    /// radial direction, or turn under 0.6 radians per radian of position.
171    /// Flat facets meeting at an angle (a pier, a chamfered corner) and
172    /// clutter in a wall corner.
173    pub cylinders_rejected_as_facets: u64,
174    /// Candidates whose inside is crossed by a scanned plane over more than
175    /// half their length: the walls of an inside corner cutting through the
176    /// circle a rounded crease fits, not a solid column or pipe.
177    pub cylinders_rejected_as_pierced: u64,
178    /// Candidates where fewer than half the axial slices agree with the
179    /// widest slice's arc: fragments at different heights (clutter in a wall
180    /// corner), not one round surface.
181    pub cylinders_rejected_for_uneven_arc: u64,
182    /// Coaxial pieces of one radius joined across a band without points.
183    pub cylinders_joined_across_gaps: u64,
184    /// The same surface found twice (across groups or tries); the better
185    /// supported one is kept.
186    pub cylinders_rejected_as_duplicates: u64,
187    /// Candidates covering less than `minCylinderArcDegrees`.
188    pub cylinders_rejected_for_arc: u64,
189    /// Candidates shorter than `minCylinderLengthMetres`.
190    pub cylinders_rejected_for_length: u64,
191    /// Rings of adjacent vertical planes around a common axis, examined as
192    /// wide round or polygonal columns.
193    pub plane_rings: u64,
194    /// Rings that were neither round nor a regular polygon of six or more
195    /// similar faces turning at least 120 degrees in all: they stay planes.
196    pub plane_rings_rejected_as_irregular: u64,
197    /// Rings with scanned surface inside their footprint (a bay or a niche
198    /// seen from the room, whose floor runs inside): not a solid column.
199    pub plane_rings_rejected_as_hollow: u64,
200    /// Planes removed from `planes` because they lie on a reported cylinder
201    /// or are the faces of a faceted column: a surface is reported once.
202    pub planes_absorbed_into_cylinders: u64,
203}
204
205/// Which bounds acted. A bound that acts is always reported here.
206#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
207#[serde(rename_all = "camelCase")]
208pub struct ScanSegmentationLimits {
209    /// The voxel budget doubled the voxel size at least once.
210    pub voxel_budget_coarsened: bool,
211    /// More planes passed than `maxPlanes`; the smallest were dropped.
212    pub plane_limit_hit: bool,
213    /// The f32 positions are coarser than a tenth of the voxel edge
214    /// (`stats.coordinateSpacingMetres`): the data lies too far from its frame
215    /// origin, so normals and planes degrade (a room 300 km out splits into
216    /// strips). Subtract a local origin before narrowing to f32 and pass it as
217    /// `options.origin`.
218    pub coordinate_precision_degraded: bool,
219    /// More non-planar groups qualified than `maxCylinderGroups`; the
220    /// smallest were not examined.
221    pub cylinder_group_limit_hit: bool,
222}
223
224#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
225#[serde(rename_all = "camelCase")]
226pub struct ScanSegmentationReport {
227    pub algorithm: String,
228    /// Largest area first; ties by centroid.
229    pub planes: Vec<ScanPlane>,
230    /// Longest first; ties by axis start.
231    pub cylinders: Vec<ScanCylinder>,
232    pub stats: ScanSegmentationStats,
233    pub limits: ScanSegmentationLimits,
234}