edgefirst_decoder_abi/lib.rs
1// SPDX-FileCopyrightText: Copyright 2026 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4//! Layout-only C ABI contract for the EdgeFirst detection vocabulary.
5//!
6//! Decoding model outputs into user-consumable structures — detection boxes,
7//! segmentation masks, tile placements — is what the decoder library *is*, so
8//! these types belong to it. They lived in `edgefirst-tensor` because of the
9//! r2 "plain-values-only boundary rule", whose premise (sibling libraries
10//! never link each other) spec Revision 3 overturned. A plain value that
11//! crosses needs a shared DECLARATION, not a shared LINK.
12//!
13//! Emitted into `edgefirst/decoder.h` by cbindgen's `[parse] include`,
14//! exactly as `edgefirst-tensor-abi`'s types are emitted into `tensor.h`.
15
16use std::ffi::c_int;
17
18/// A detection: a normalized box, a score, and a label index.
19///
20/// Coordinates are normalized to `[0, 1]` against the *model input*, not the
21/// source image — un-letterboxing is the consumer's job and needs the letterbox
22/// parameters, which this type deliberately does not carry.
23#[repr(C)]
24#[derive(Debug, Clone, Copy, PartialEq, Default)]
25pub struct EfDetectBox {
26 pub xmin: f32,
27 pub ymin: f32,
28 pub xmax: f32,
29 pub ymax: f32,
30 /// Model-specific confidence; higher is more confident.
31 pub score: f32,
32 /// Label index into the model's class list.
33 pub label: u32,
34}
35
36/// One segmentation result, as plain values.
37///
38/// Crosses library boundaries by the same rule as [`EfDetectBox`]: plain
39/// values, never a handle. `mask` **borrows** the producing list's buffer and
40/// is valid only until that list is freed — stated here because C gives a
41/// caller no way to discover it.
42///
43/// The bounds describe the **mask region**, which is snapped to the proto grid
44/// and therefore encloses rather than equals the companion detection's box.
45#[repr(C)]
46#[derive(Debug, Clone, Copy)]
47pub struct EfSegmentation {
48 pub xmin: f32,
49 pub ymin: f32,
50 pub xmax: f32,
51 pub ymax: f32,
52 /// Row-major mask bytes, `height * width`, borrowed from the list.
53 pub mask: *const u8,
54 pub width: u32,
55 pub height: u32,
56}
57
58/// How overlapping detections from neighbouring tiles are merged.
59#[repr(C)]
60#[derive(Debug, Clone, Copy)]
61pub struct EfMergeConfig {
62 /// An `EF_MATCH_METRIC_*` code: IoU, or Intersection-over-Smaller.
63 ///
64 /// IoS is the default for a reason: an object split across a tile seam has
65 /// *low* IoU with its own fragment but high IoS, so IoU would keep both
66 /// halves as separate detections.
67 pub metric: u32,
68 pub threshold: f32,
69 /// Non-zero to merge across classes.
70 pub class_agnostic: c_int,
71 pub max_det: usize,
72 pub score_threshold: f32,
73 /// An `EF_MERGE_MODE_*` code. `EF_MERGE_MODE_KEEP_BEST` (default): the
74 /// highest-scoring box of each matched group is kept and the boxes it
75 /// matched are dropped. `EF_MERGE_MODE_UNION`: the group becomes its
76 /// enclosing union carrying the max score.
77 ///
78 /// Keep-best is `0`, but note that a zero-initialised struct is still
79 /// not the library default: `metric` defaults to `EF_MATCH_METRIC_IOS`,
80 /// so an all-zero struct selects IoU. Fill with `ef_merge_config_default`.
81 /// The union measured about 0.05 AP50 worse on every frame of the Ocean
82 /// Cleanup ADIS 4K validation (TOP2-836), which is why it is opt-in.
83 ///
84 /// Occupies what was this struct's 4-byte tail pad, so adding it left
85 /// every other field's offset and the 32-byte size unchanged.
86 pub mode: u32,
87}
88
89/// One tile's placement within a frame grid.
90///
91/// Produced by `edgefirst_image::ImageProcessor::plan_tiles` and consumed by
92/// `edgefirst_decoder::tiling` to lift per-tile detections back to full-frame
93/// coordinates. It lives here because it is the contract *between* those two
94/// crates: it was previously defined in `decoder` and re-exported by `image`,
95/// so the producer imported its own output type from its consumer.
96#[derive(Debug, Clone, Copy, PartialEq)]
97pub struct TilePlacement {
98 /// Tile index within the frame grid, `0..count`.
99 pub index: usize,
100 /// Total tiles for this frame (the streaming fan-in fence).
101 pub count: usize,
102 /// Native crop origin `(ox, oy)` in full-frame pixels.
103 pub origin: (f32, f32),
104 /// Native crop size `(cw, ch)` in full-frame pixels. Equals the tile size
105 /// for the full-size tiles the EvenDist grid produces.
106 pub crop_size: (f32, f32),
107 /// The image content on the model input as normalized
108 /// `[xmin, ymin, xmax, ymax]` (a letterbox's content region), or `None`
109 /// when the crop was stretched to fill it (the hot path).
110 pub content_region: Option<[f32; 4]>,
111 /// Full-frame dimensions `(frame_w, frame_h)` in pixels.
112 pub frame_dims: (f32, f32),
113}
114
115/// C layout of `ef_tile_placement` in `detect.h`.
116///
117/// One declaration for image-capi (producer) and decoder-capi (consumer).
118/// cbindgen excludes this type so the header-only `detect.h` definition wins.
119#[repr(C)]
120#[derive(Debug, Clone, Copy)]
121pub struct EfTilePlacement {
122 pub index: usize,
123 pub count: usize,
124 pub origin_x: f32,
125 pub origin_y: f32,
126 pub crop_width: f32,
127 pub crop_height: f32,
128 pub has_content_region: c_int,
129 pub content_region: [f32; 4],
130 pub frame_width: f32,
131 pub frame_height: f32,
132}
133
134impl From<&TilePlacement> for EfTilePlacement {
135 fn from(p: &TilePlacement) -> Self {
136 let (has_content_region, content_region) = match p.content_region {
137 Some(lb) => (1, lb),
138 None => (0, [0.0; 4]),
139 };
140 Self {
141 index: p.index,
142 count: p.count,
143 origin_x: p.origin.0,
144 origin_y: p.origin.1,
145 crop_width: p.crop_size.0,
146 crop_height: p.crop_size.1,
147 has_content_region,
148 content_region,
149 frame_width: p.frame_dims.0,
150 frame_height: p.frame_dims.1,
151 }
152 }
153}
154
155impl From<&EfTilePlacement> for TilePlacement {
156 fn from(p: &EfTilePlacement) -> Self {
157 Self {
158 index: p.index,
159 count: p.count,
160 origin: (p.origin_x, p.origin_y),
161 crop_size: (p.crop_width, p.crop_height),
162 content_region: if p.has_content_region != 0 {
163 Some(p.content_region)
164 } else {
165 None
166 },
167 frame_dims: (p.frame_width, p.frame_height),
168 }
169 }
170}
171
172/// How a source is fit into the model input. C `ef_fit` in `detect.h`.
173#[repr(u32)]
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub enum EfFit {
176 /// Stretch the source to fill the model input.
177 Stretch = 0,
178 /// Keep the aspect ratio and pad the rest (a letterbox).
179 Letterbox = 1,
180}
181
182/// Where a letterboxed image sits. C `ef_letterbox_placement` in `detect.h`.
183#[repr(u32)]
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub enum EfLetterboxPlacement {
186 /// Centred, padding split between both sides.
187 Center = 0,
188 /// Anchored at the top-left corner, padding on the right and bottom.
189 TopLeft = 1,
190}
191
192/// The space a letterbox pad is expressed in. C `ef_pad_space` in `detect.h`.
193#[repr(u32)]
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub enum EfPadSpace {
196 /// A 0-255 pixel value, normalized like the image.
197 Pixel = 0,
198 /// A value written as-is into the normalized destination.
199 Normalized = 1,
200}
201
202/// A resampling filter. C `ef_interpolation` in `detect.h`.
203#[repr(u32)]
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205pub enum EfInterpolation {
206 /// Bilinear (the default).
207 Bilinear = 0,
208 /// Nearest neighbour.
209 Nearest = 1,
210 /// Bicubic; the image library runs bilinear in its place.
211 Bicubic = 2,
212 /// Pixel-area averaging; the image library runs bilinear in its place.
213 Area = 3,
214}
215
216/// A model's input preprocessing as plain values. C `ef_preprocess_spec` in
217/// `detect.h`, produced by the decoder library and consumed by the image
218/// library.
219#[repr(C)]
220#[derive(Debug, Clone, Copy, PartialEq, Default)]
221pub struct EfPreprocessSpec {
222 /// Model input width in pixels, 0 when unknown.
223 pub width: u32,
224 /// Model input height in pixels, 0 when unknown.
225 pub height: u32,
226 /// An `EF_FIT_*` code.
227 pub fit: u32,
228 /// An `EF_LETTERBOX_PLACEMENT_*` code; ignored by a stretch.
229 pub placement: u32,
230 /// An `EF_PAD_SPACE_*` code for `pad`.
231 pub pad_space: u32,
232 /// Letterbox pad per RGB channel.
233 pub pad: [f32; 3],
234 /// Normalization mean per RGB channel, on 0-255 values.
235 pub mean: [f32; 3],
236 /// Normalization standard deviation per RGB channel, on 0-255 values.
237 pub std: [f32; 3],
238 /// An `EF_INTERPOLATION_*` code.
239 pub interpolation: u32,
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 /// Load-bearing: `edgefirst-image` and `edgefirst-tracker` depend on this
247 /// crate to NAME decoder's types without linking decoder's implementation.
248 /// A single dependency here would drag code into both and defeat that.
249 #[test]
250 fn this_crate_has_no_dependencies_and_never_may() {
251 let manifest = include_str!("../Cargo.toml");
252 assert!(
253 !manifest.contains("[dependencies."),
254 "dependency sub-tables are dependencies too"
255 );
256 let deps = manifest
257 .split("[dependencies]")
258 .nth(1)
259 .expect("manifest has a [dependencies] table");
260 let body = deps.split("\n[").next().unwrap_or(deps);
261 let lines: Vec<&str> = body
262 .lines()
263 .map(str::trim)
264 .filter(|l| !l.is_empty() && !l.starts_with('#'))
265 .collect();
266 assert!(
267 lines.is_empty(),
268 "edgefirst-decoder-abi grew dependencies: {lines:?}"
269 );
270 }
271
272 #[test]
273 fn detect_box_is_six_packed_32_bit_fields() {
274 assert_eq!(std::mem::size_of::<EfDetectBox>(), 24);
275 assert_eq!(std::mem::align_of::<EfDetectBox>(), 4);
276 }
277
278 #[test]
279 fn segmentation_layout_is_pinned() {
280 assert_eq!(std::mem::size_of::<EfSegmentation>(), 32);
281 assert_eq!(std::mem::align_of::<EfSegmentation>(), 8);
282 }
283
284 #[test]
285 fn merge_config_layout_is_pinned() {
286 // `max_det: usize` forces 8-byte alignment and 4 bytes of pad after
287 // `class_agnostic`; `mode` fills what was the tail pad at 28, so the
288 // struct is 32 bytes with or without it. Matches `detect.h`.
289 assert_eq!(std::mem::size_of::<EfMergeConfig>(), 32);
290 assert_eq!(std::mem::align_of::<EfMergeConfig>(), 8);
291 assert_eq!(std::mem::offset_of!(EfMergeConfig, metric), 0);
292 assert_eq!(std::mem::offset_of!(EfMergeConfig, threshold), 4);
293 assert_eq!(std::mem::offset_of!(EfMergeConfig, class_agnostic), 8);
294 assert_eq!(std::mem::offset_of!(EfMergeConfig, max_det), 16);
295 assert_eq!(std::mem::offset_of!(EfMergeConfig, score_threshold), 24);
296 assert_eq!(std::mem::offset_of!(EfMergeConfig, mode), 28);
297 }
298
299 #[test]
300 fn tile_placement_layout_is_pinned() {
301 // 60 bytes of fields, align 8 -> 64. Matches `detect.h`.
302 assert_eq!(std::mem::size_of::<EfTilePlacement>(), 64);
303 assert_eq!(std::mem::align_of::<EfTilePlacement>(), 8);
304 }
305
306 #[test]
307 fn preprocess_spec_layout_is_pinned() {
308 // Five u32, three [f32; 3], one u32: 60 bytes, align 4. Matches
309 // `detect.h`.
310 type S = EfPreprocessSpec;
311 assert_eq!(std::mem::size_of::<S>(), 60);
312 assert_eq!(std::mem::align_of::<S>(), 4);
313 assert_eq!(std::mem::offset_of!(S, width), 0);
314 assert_eq!(std::mem::offset_of!(S, height), 4);
315 assert_eq!(std::mem::offset_of!(S, fit), 8);
316 assert_eq!(std::mem::offset_of!(S, placement), 12);
317 assert_eq!(std::mem::offset_of!(S, pad_space), 16);
318 assert_eq!(std::mem::offset_of!(S, pad), 20);
319 assert_eq!(std::mem::offset_of!(S, mean), 32);
320 assert_eq!(std::mem::offset_of!(S, std), 44);
321 assert_eq!(std::mem::offset_of!(S, interpolation), 56);
322 }
323
324 #[test]
325 fn tile_placement_offsets_are_pinned() {
326 type P = EfTilePlacement;
327 assert_eq!(std::mem::offset_of!(P, has_content_region), 32);
328 assert_eq!(std::mem::offset_of!(P, content_region), 36);
329 assert_eq!(std::mem::offset_of!(P, frame_width), 52);
330 }
331
332 #[test]
333 fn detect_h_declares_the_preprocess_vocabulary() {
334 let h = include_str!("../include/edgefirst/detect.h");
335 let codes = [
336 ("EF_FIT_STRETCH", EfFit::Stretch as u32),
337 ("EF_FIT_LETTERBOX", EfFit::Letterbox as u32),
338 (
339 "EF_LETTERBOX_PLACEMENT_CENTER",
340 EfLetterboxPlacement::Center as u32,
341 ),
342 (
343 "EF_LETTERBOX_PLACEMENT_TOP_LEFT",
344 EfLetterboxPlacement::TopLeft as u32,
345 ),
346 ("EF_PAD_SPACE_PIXEL", EfPadSpace::Pixel as u32),
347 ("EF_PAD_SPACE_NORMALIZED", EfPadSpace::Normalized as u32),
348 (
349 "EF_INTERPOLATION_BILINEAR",
350 EfInterpolation::Bilinear as u32,
351 ),
352 ("EF_INTERPOLATION_NEAREST", EfInterpolation::Nearest as u32),
353 ("EF_INTERPOLATION_BICUBIC", EfInterpolation::Bicubic as u32),
354 ("EF_INTERPOLATION_AREA", EfInterpolation::Area as u32),
355 ];
356 for (name, code) in codes {
357 let line = format!("{name} = {code},");
358 assert!(h.contains(&line), "detect.h does not declare `{line}`");
359 }
360 assert!(h.contains("typedef struct ef_preprocess_spec {"));
361 }
362}