pub struct Decoder {
pub iou_threshold: f32,
pub score_threshold: f32,
pub nms: Option<Nms>,
pub pre_nms_top_k: usize,
pub max_det: usize,
/* private fields */
}Fields§
§iou_threshold: f32§score_threshold: f32§nms: Option<Nms>NMS mode (always a concrete variant after build — Nms::Auto is
resolved during DecoderBuilder::build() and never stored here):
Some(ClassAgnostic)— class-agnostic NMSSome(ClassAware)— class-aware NMSNone— NMS bypassed (end-to-end models)
pre_nms_top_k: usizeMaximum number of candidate boxes fed into NMS after score filtering.
Reduces O(N²) NMS cost when many low-confidence proposals pass the
threshold (common during COCO mAP evaluation with threshold ≈ 0.001).
Candidates are ranked by score; only the top pre_nms_top_k proceed
to NMS. Default: 300. Ignored when nms is None.
§⚠️ Validation vs Deployment
The default of 300 is tuned for deployment (score threshold ≥ 0.25) where few anchors pass the score filter, making top-K a no-op in practice while bounding worst-case NMS cost.
For mAP evaluation (score threshold ≈ 0.001), most of the 8 400
YOLO anchors pass the score filter. At pre_nms_top_k = 300, roughly
74 % of candidates that would survive NMS are discarded before NMS
runs, causing ~9 pp box mAP loss — a measurement artifact, not a
model quality issue.
| Use case | pre_nms_top_k | score_threshold |
|---|---|---|
| Deployment | 300 (default) | ≥ 0.25 |
| COCO mAP evaluation | 8 400 (all anchors) | 0.001 |
| Unbounded | 0 (no limit) | any |
Post-processing latency scales with the number of candidates entering
NMS. At deployment thresholds the candidate count is already small, so
raising pre_nms_top_k has negligible cost. At validation thresholds
the increase is measurable but necessary for correct recall.
max_det: usizeMaximum number of detections returned after NMS. Matches the
Ultralytics max_det parameter. Default: 300.
This bound applies uniformly across all segmentation and detection
decode paths reached via Decoder::decode / Decoder::decode_proto.
The output Vec’s capacity is only an allocation hint; the post-NMS
detection count is bounded solely by max_det (EDGEAI-1302).
Implementations§
Source§impl Decoder
impl Decoder
Sourcepub fn model_type(&self) -> &ModelType
pub fn model_type(&self) -> &ModelType
This function returns the parsed model type of the decoder.
§Examples
let decoder = DecoderBuilder::default()
.with_config_yaml_str(config_yaml)
.build()?;
assert!(matches!(
decoder.model_type(),
ModelType::ModelPackDetSplit { .. }
));Sourcepub fn normalized_boxes(&self) -> Option<bool>
pub fn normalized_boxes(&self) -> Option<bool>
Returns the coordinate format of the boxes the decoder emits to the caller.
Some(true): Boxes are in normalized[0, 1]coordinatesSome(false): Boxes are in pixel coordinates relative to the model inputNone: Unknown, caller must infer (e.g., check if any coordinate1.0)
This describes the post-decode coordinate space, not the raw
schema annotation. The decoder applies EDGEAI-1303 normalization
(dividing bbox channels by (input_w, input_h)) on a per-path
basis, not unconditionally. Four paths are known to invoke the
helper uniformly across all of their entry points (decode,
decode_proto, and — where applicable — decode_tracked and
decode_tracked_proto):
- The per-scale fast path (DFL/LTRB → dist2bbox → sigmoid), which emits pixel-space boxes by design and always normalizes before returning.
ModelType::YoloSegDet, whose quantized and float, tracked and untracked, masks and proto variants each call the helper after NMS.ModelType::YoloSplitSegDet, aligned acrossdecode,decode_proto,decode_tracked, anddecode_tracked_protofor both quantized and float variants.ModelType::YoloSegDet2Way, aligned across the same four entry points and both element type variants.
When any of those paths is active and the schema declares
normalized: false with valid input_dims,
this accessor reports Some(true) to match what the caller
actually receives.
The remaining model types still surface the raw schema flag
because their post-decode contract differs:
ModelType::YoloDet and
ModelType::YoloSplitDet
(detection-only, no protobox crop coupling), the
YoloEndToEnd* family (model embeds its own NMS and emits its
own coordinate space), and the ModelPack* family (separate
model conventions). For those, this accessor returns
self.normalized verbatim and leaves it to the caller to
handle pixel-space output explicitly (e.g. divide by
input_dims() themselves).
§Examples
let decoder = DecoderBuilder::default()
.with_config_yaml_str(config_yaml)
.build()?;
// Config doesn't specify normalized, so it's None
assert!(decoder.normalized_boxes().is_none());Sourcepub fn input_dims(&self) -> Option<(usize, usize)>
pub fn input_dims(&self) -> Option<(usize, usize)>
Model input dimensions (width, height) captured from the
schema’s input.shape / input.dshape, or None when the
schema did not declare an input spec (e.g. flat YAML configs
or DecoderBuilder::add_output(...) programmatic builds).
Drives EDGEAI-1303 normalization on the paths that invoke the
helper uniformly: when the schema declares pixel-space outputs
and input_dims() is Some((w, h)), the per-scale bridge and
the ModelType::YoloSegDet, ModelType::YoloSplitSegDet, and
ModelType::YoloSegDet2Way dispatch paths divide post-NMS
bbox coordinates by (w, h) so they enter the canonical
[0, 1] range before mask cropping / tracker dispatch, and
normalized_boxes reports
Some(true) to match. The remaining legacy ModelType
dispatch paths (detection-only YoloDet/YoloSplitDet, the
YoloEndToEnd* family, and the ModelPack* family) do not
apply this division — see
normalized_boxes for the per-path
contract. The legacy protobox > 2.0 reject acts as a safety
net for paths that emit pixel-space coordinates.
§Examples
let json = r#"{
"schema_version": 2,
"nms": "class_agnostic",
"input": {
"shape": [1, 640, 640, 3],
"dshape": [{"batch": 1}, {"height": 640}, {"width": 640}, {"num_features": 3}]
},
"outputs": [{
"name": "out", "type": "detection",
"shape": [1, 38, 256],
"dshape": [{"batch": 1}, {"num_features": 38}, {"num_boxes": 256}],
"decoder": "ultralytics", "encoding": "direct", "normalized": false
}]
}"#;
let schema: SchemaV2 = serde_json::from_str(json).unwrap();
let decoder = DecoderBuilder::default().with_schema(schema).build()?;
assert_eq!(decoder.input_dims(), Some((640, 640)));Sourcepub fn decode(
&self,
outputs: &[&TensorDyn],
output_boxes: &mut Vec<DetectBox>,
output_masks: &mut Vec<Segmentation>,
) -> Result<(), DecoderError>
pub fn decode( &self, outputs: &[&TensorDyn], output_boxes: &mut Vec<DetectBox>, output_masks: &mut Vec<Segmentation>, ) -> Result<(), DecoderError>
Decode model outputs into detection boxes and segmentation masks.
This is the primary decode API. Accepts TensorDyn outputs directly
from model inference. Automatically dispatches to quantized or float
paths based on the tensor dtype.
§Arguments
outputs- Tensor outputs from model inferenceoutput_boxes- Destination for decoded detection boxes (cleared first)output_masks- Destination for decoded segmentation masks (cleared first)
§output_boxes / output_masks capacity
The capacity of the supplied Vecs is only an allocation hint —
it is not a cap on the number of detections returned. The
post-NMS detection count is bounded by Decoder::max_det (set
via DecoderBuilder::with_max_det, default 300). Passing
Vec::new() (capacity 0) returns up to max_det detections;
pre-allocating with Vec::with_capacity only avoids the
reallocation when the decoder grows the buffer.
§Errors
Returns DecoderError if tensor mapping fails, dtypes are unsupported,
or the outputs don’t match the decoder’s model configuration.
Sourcepub fn decode_proto(
&self,
outputs: &[&TensorDyn],
output_boxes: &mut Vec<DetectBox>,
) -> Result<Option<ProtoData>, DecoderError>
pub fn decode_proto( &self, outputs: &[&TensorDyn], output_boxes: &mut Vec<DetectBox>, ) -> Result<Option<ProtoData>, DecoderError>
Decode model outputs into detection boxes, returning raw proto data for segmentation models instead of materialized masks.
Accepts TensorDyn outputs directly from model inference.
Detections are always decoded into output_boxes regardless of model type.
Returns Ok(None) for detection-only and ModelPack models.
Returns Ok(Some(ProtoData)) for YOLO segmentation models.
§Arguments
outputs- Tensor outputs from model inferenceoutput_boxes- Destination for decoded detection boxes (cleared first)
§output_boxes capacity
The capacity of output_boxes is only an allocation hint — it
is not a cap on the number of detections returned. The
post-NMS detection count is bounded by Decoder::max_det (set
via DecoderBuilder::with_max_det, default 300). Passing
Vec::new() (capacity 0) returns up to max_det detections.
§Errors
Returns DecoderError if tensor mapping fails, dtypes are unsupported,
or the outputs don’t match the decoder’s model configuration.
Trait Implementations§
Source§impl Clone for Decoder
impl Clone for Decoder
Source§fn clone(&self) -> Self
fn clone(&self) -> Self
Cloning a Decoder preserves the legacy decode path
(decode_program) but drops the per-scale fast path:
PerScaleDecoder owns mutable per-frame scratch buffers and is
not Clone. Decoders built from a per-scale schema should be
rebuilt via DecoderBuilder rather than cloned to preserve the
fast path; cloning is intended for tests and rare configs.
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreAuto Trait Implementations§
impl !Freeze for Decoder
impl RefUnwindSafe for Decoder
impl Send for Decoder
impl Sync for Decoder
impl Unpin for Decoder
impl UnsafeUnpin for Decoder
impl UnwindSafe for Decoder
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more