ifc_lite_processing/appearance/
calibration.rs1use super::{Mapping, MappingFrame};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Deserialize, Serialize)]
9#[serde(rename_all = "camelCase", deny_unknown_fields)]
10pub struct PlaneCalibrationRequest {
11 pub raster_to_source: [f64; 6],
14 pub raster_size: [u32; 2],
15 pub source_points: [[f64; 2]; 2],
17 pub distance_metres: f64,
18 pub world_anchor: [f64; 3],
20 pub world_direction: [f64; 3],
22 pub plane_normal: [f64; 3],
24}
25
26#[derive(Debug, Clone, Serialize)]
27#[serde(rename_all = "camelCase")]
28pub struct CalibratedPlane {
29 pub mapping: Mapping,
31 pub raster_corners: [[f64; 3]; 4],
33 pub metres_per_source_unit: f64,
34}
35
36fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
37 a.iter().zip(b).map(|(x, y)| x * y).sum()
38}
39fn length(v: [f64; 3]) -> f64 { v[0].hypot(v[1]).hypot(v[2]) }
40fn preserves_vector(start: [f64; 3], end: [f64; 3], intended: [f64; 3]) -> bool {
44 let error = length(std::array::from_fn(|i| (end[i] - start[i]) - intended[i]));
45 error.is_finite() && error <= length(intended) * 1e-6
46}
47fn unit(v: [f64; 3]) -> Result<[f64; 3], String> {
48 let len = length(v);
49 if !v.iter().all(|v| v.is_finite()) || !len.is_finite() || len <= 0. {
50 return Err("Calibration needs finite, nonzero plane directions".into());
51 }
52 Ok(v.map(|value| value / len))
53}
54
55pub fn calibrate_appearance_plane(request: &PlaneCalibrationRequest) -> Result<CalibratedPlane, String> {
60 let r = request;
61 if !r.raster_to_source.iter().chain(r.source_points.iter().flatten())
62 .chain(r.world_anchor.iter()).all(|v| v.is_finite())
63 || !r.distance_metres.is_finite() || r.distance_metres <= 0.
64 || r.raster_size.iter().any(|&v| v == 0 || v > 8192)
65 || u64::from(r.raster_size[0]) * u64::from(r.raster_size[1]) > 16 * 1024 * 1024 {
66 return Err("Calibration needs finite coordinates, a positive measured distance and a bounded raster".into());
67 }
68 let dx = r.source_points[1][0] - r.source_points[0][0];
69 let dy = r.source_points[1][1] - r.source_points[0][1];
70 let span = dx.hypot(dy);
71 if !span.is_finite() || span <= 0. {
72 return Err("Choose two distinct source points for calibration".into());
73 }
74 let scale = r.distance_metres / span;
75 if !scale.is_finite() || scale <= 0. {
76 return Err("The measured source scale cannot be represented".into());
77 }
78 let direction = unit(r.world_direction)?;
79 let normal = unit(r.plane_normal)?;
80 if dot(direction, normal).abs() > 1e-10 {
81 return Err("The measured direction must lie in the chosen plane".into());
82 }
83 let sideways = unit([
84 normal[1] * direction[2] - normal[2] * direction[1],
85 normal[2] * direction[0] - normal[0] * direction[2],
86 normal[0] * direction[1] - normal[1] * direction[0],
87 ])?;
88 let sx = dx / span;
89 let sy = dy / span;
90 let vector = |x: f64, y: f64| -> [f64; 3] {
91 let along = (x * sx + y * sy) * scale;
92 let across = (-x * sy + y * sx) * scale;
93 std::array::from_fn(|i| direction[i] * along + sideways[i] * across)
94 };
95 let [a, b, c, d, e, f] = r.raster_to_source;
96 let raster_x = vector(a, b);
97 let raster_y = vector(c, d);
98 let axis_u = unit(raster_x)?;
99 let axis_v = unit(raster_y.map(|v| -v))?;
100 if dot(axis_u, axis_v).abs() > 1e-10 {
101 return Err("A sheared raster needs rectification before planar projection".into());
102 }
103 let width = length(raster_x) * f64::from(r.raster_size[0]);
104 let height = length(raster_y) * f64::from(r.raster_size[1]);
105 let top_offset = vector(e - r.source_points[0][0], f - r.source_points[0][1]);
106 let top_left = std::array::from_fn(|i| r.world_anchor[i] + top_offset[i]);
107 let top_right = std::array::from_fn(|i| top_left[i] + axis_u[i] * width);
108 let bottom_left = std::array::from_fn(|i| top_left[i] - axis_v[i] * height);
109 let bottom_right = std::array::from_fn(|i| bottom_left[i] + axis_u[i] * width);
110 let corners = [top_left, top_right, bottom_right, bottom_left];
111 if !corners.iter().flatten().all(|v| v.is_finite())
112 || !width.is_finite() || !height.is_finite() || width <= 0. || height <= 0.
113 || !preserves_vector(r.world_anchor, top_left, top_offset)
114 || !preserves_vector(top_left, top_right, axis_u.map(|v| v * width))
115 || !preserves_vector(bottom_left, bottom_right, axis_u.map(|v| v * width))
116 || !preserves_vector(bottom_left, top_left, axis_v.map(|v| v * height))
117 || !preserves_vector(bottom_right, top_right, axis_v.map(|v| v * height)) {
118 return Err("The calibrated plane exceeds coordinate precision; use a local world frame".into());
119 }
120 Ok(CalibratedPlane {
121 mapping: Mapping::Planar { frame: MappingFrame::World, origin: bottom_left,
122 axis_u, axis_v, metres_per_tile: [width, height] },
123 raster_corners: corners,
124 metres_per_source_unit: scale,
125 })
126}
127
128#[cfg(test)]
129#[path = "calibration_tests.rs"]
130mod tests;