Skip to main content

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    /// 0 = IoU, 1 = 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    /// 0 = keep-best (default): the highest-scoring box of each matched
74    /// group is kept and the boxes it matched are dropped. 1 = union: the
75    /// group becomes its enclosing union carrying the max score.
76    ///
77    /// Keep-best is `0`, but note that a zero-initialised struct is still
78    /// not the library default: `metric` defaults to `1` (IoS), so an
79    /// all-zero struct selects IoU. Fill with `ef_merge_config_default`.
80    /// The union measured about 0.05 AP50 worse on every frame of the Ocean
81    /// Cleanup ADIS 4K validation (TOP2-836), which is why it is opt-in.
82    ///
83    /// Occupies what was this struct's 4-byte tail pad, so adding it left
84    /// every other field's offset and the 32-byte size unchanged.
85    pub mode: u32,
86}
87
88/// One tile's placement within a frame grid.
89///
90/// Produced by `edgefirst_image::ImageProcessor::plan_tiles` and consumed by
91/// `edgefirst_decoder::tiling` to lift per-tile detections back to full-frame
92/// coordinates. It lives here because it is the contract *between* those two
93/// crates: it was previously defined in `decoder` and re-exported by `image`,
94/// so the producer imported its own output type from its consumer.
95#[derive(Debug, Clone, Copy, PartialEq)]
96pub struct TilePlacement {
97    /// Tile index within the frame grid, `0..count`.
98    pub index: usize,
99    /// Total tiles for this frame (the streaming fan-in fence).
100    pub count: usize,
101    /// Native crop origin `(ox, oy)` in full-frame pixels.
102    pub origin: (f32, f32),
103    /// Native crop size `(cw, ch)` in full-frame pixels. Equals the tile size
104    /// for the full-size tiles the EvenDist grid produces.
105    pub crop_size: (f32, f32),
106    /// Normalized letterbox content bounds `[lx0, ly0, lx1, ly1]` on the model
107    /// input, or `None` when the crop was stretched to fill it (the hot path).
108    pub letterbox: Option<[f32; 4]>,
109    /// Full-frame dimensions `(frame_w, frame_h)` in pixels.
110    pub frame_dims: (f32, f32),
111}
112
113/// C layout of `ef_tile_placement` in `detect.h`.
114///
115/// One declaration for image-capi (producer) and decoder-capi (consumer).
116/// cbindgen excludes this type so the header-only `detect.h` definition wins.
117#[repr(C)]
118#[derive(Debug, Clone, Copy)]
119pub struct EfTilePlacement {
120    pub index: usize,
121    pub count: usize,
122    pub origin_x: f32,
123    pub origin_y: f32,
124    pub crop_width: f32,
125    pub crop_height: f32,
126    pub has_letterbox: c_int,
127    pub letterbox: [f32; 4],
128    pub frame_width: f32,
129    pub frame_height: f32,
130}
131
132impl From<&TilePlacement> for EfTilePlacement {
133    fn from(p: &TilePlacement) -> Self {
134        let (has_letterbox, letterbox) = match p.letterbox {
135            Some(lb) => (1, lb),
136            None => (0, [0.0; 4]),
137        };
138        Self {
139            index: p.index,
140            count: p.count,
141            origin_x: p.origin.0,
142            origin_y: p.origin.1,
143            crop_width: p.crop_size.0,
144            crop_height: p.crop_size.1,
145            has_letterbox,
146            letterbox,
147            frame_width: p.frame_dims.0,
148            frame_height: p.frame_dims.1,
149        }
150    }
151}
152
153impl From<&EfTilePlacement> for TilePlacement {
154    fn from(p: &EfTilePlacement) -> Self {
155        Self {
156            index: p.index,
157            count: p.count,
158            origin: (p.origin_x, p.origin_y),
159            crop_size: (p.crop_width, p.crop_height),
160            letterbox: if p.has_letterbox != 0 {
161                Some(p.letterbox)
162            } else {
163                None
164            },
165            frame_dims: (p.frame_width, p.frame_height),
166        }
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    /// Load-bearing: `edgefirst-image` and `edgefirst-tracker` depend on this
175    /// crate to NAME decoder's types without linking decoder's implementation.
176    /// A single dependency here would drag code into both and defeat that.
177    #[test]
178    fn this_crate_has_no_dependencies_and_never_may() {
179        let manifest = include_str!("../Cargo.toml");
180        assert!(
181            !manifest.contains("[dependencies."),
182            "dependency sub-tables are dependencies too"
183        );
184        let deps = manifest
185            .split("[dependencies]")
186            .nth(1)
187            .expect("manifest has a [dependencies] table");
188        let body = deps.split("\n[").next().unwrap_or(deps);
189        let lines: Vec<&str> = body
190            .lines()
191            .map(str::trim)
192            .filter(|l| !l.is_empty() && !l.starts_with('#'))
193            .collect();
194        assert!(
195            lines.is_empty(),
196            "edgefirst-decoder-abi grew dependencies: {lines:?}"
197        );
198    }
199
200    #[test]
201    fn detect_box_is_six_packed_32_bit_fields() {
202        assert_eq!(std::mem::size_of::<EfDetectBox>(), 24);
203        assert_eq!(std::mem::align_of::<EfDetectBox>(), 4);
204    }
205
206    #[test]
207    fn segmentation_layout_is_pinned() {
208        assert_eq!(std::mem::size_of::<EfSegmentation>(), 32);
209        assert_eq!(std::mem::align_of::<EfSegmentation>(), 8);
210    }
211
212    #[test]
213    fn merge_config_layout_is_pinned() {
214        // `max_det: usize` forces 8-byte alignment and 4 bytes of pad after
215        // `class_agnostic`; `mode` fills what was the tail pad at 28, so the
216        // struct is 32 bytes with or without it. Matches `detect.h`.
217        assert_eq!(std::mem::size_of::<EfMergeConfig>(), 32);
218        assert_eq!(std::mem::align_of::<EfMergeConfig>(), 8);
219        assert_eq!(std::mem::offset_of!(EfMergeConfig, metric), 0);
220        assert_eq!(std::mem::offset_of!(EfMergeConfig, threshold), 4);
221        assert_eq!(std::mem::offset_of!(EfMergeConfig, class_agnostic), 8);
222        assert_eq!(std::mem::offset_of!(EfMergeConfig, max_det), 16);
223        assert_eq!(std::mem::offset_of!(EfMergeConfig, score_threshold), 24);
224        assert_eq!(std::mem::offset_of!(EfMergeConfig, mode), 28);
225    }
226
227    #[test]
228    fn tile_placement_layout_is_pinned() {
229        // 60 bytes of fields, align 8 -> 64. Matches `detect.h`.
230        assert_eq!(std::mem::size_of::<EfTilePlacement>(), 64);
231        assert_eq!(std::mem::align_of::<EfTilePlacement>(), 8);
232    }
233}