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