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}