pub struct Decoder {
pub iou_threshold: f32,
pub score_threshold: f32,
pub nms: Option<Nms>,
pub pre_nms_top_k: Option<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)
Assigning Some(Nms::Auto) to this field directly violates that
invariant: Nms::Auto must be resolved by
DecoderBuilder::build. Debug builds
panic on it at decode (a debug_assert! in NMS dispatch); release
builds treat it as class-aware without consulting the config.
pre_nms_top_k: Option<usize>Maximum 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, equal scores by anchor index; only the
top pre_nms_top_k proceed to NMS. Applies to every NMS decode path;
ignored when nms is None.
None(the default unless the builder was given a cap): resolved per decode byDecoder::pre_nms_top_k_for—MULTI_LABEL_PRE_NMS_TOP_K(30 000, Ultralytics’max_nms) for a multi-label decode,DEFAULT_PRE_NMS_TOP_K(300) for an argmax decode, including the tracking entry points of a multi-label decoder.Some(n): an explicit cap used by every decode as given;Some(0)is unbounded. Assigning it after build is as explicit asDecoderBuilder::with_pre_nms_top_k.
§⚠️ 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 | None (300) | ≥ 0.25 |
| COCO mAP evaluation, argmax | Some(0), or ≥ the anchor count (8 400) | 0.001 |
| COCO mAP evaluation, multi-label | Some(0), or None (30 000) | 0.001 |
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 multi_label(&self) -> bool
pub fn multi_label(&self) -> bool
Whether Decoder::decode and Decoder::decode_proto emit one box
per class above the score threshold instead of one per anchor.
Self::decode_for_tracking, and Decoder::decode_tracked /
Decoder::decode_proto_tracked (behind the tracker feature), ignore
this and always use one label per box.
§Examples
let decoder = DecoderBuilder::default()
.with_config_yaml_str(config_yaml)
.with_multi_label(true)
.build()?;
assert!(decoder.multi_label());Sourcepub fn pre_nms_top_k_for(&self, multi_label: bool) -> usize
pub fn pre_nms_top_k_for(&self, multi_label: bool) -> usize
The pre-NMS cap a decode uses: the explicit
pre_nms_top_k when set, else
MULTI_LABEL_PRE_NMS_TOP_K for a multi-label decode and
DEFAULT_PRE_NMS_TOP_K for an argmax one. 0 means unbounded.
Pass multi_label() for what decode /
decode_proto use, and false for the tracking entry points, which
always decode argmax.
§Examples
let mut decoder = DecoderBuilder::new()
.with_config_json_str(config_json)
.with_multi_label(true)
.build()?;
assert_eq!(decoder.pre_nms_top_k, None);
assert_eq!(decoder.pre_nms_top_k_for(true), 30_000);
assert_eq!(decoder.pre_nms_top_k_for(false), 300);
decoder.pre_nms_top_k = Some(30_000);
assert_eq!(decoder.pre_nms_top_k_for(false), 30_000);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_for_tracking(
&self,
outputs: &[&TensorDyn],
output_boxes: &mut Vec<DetectBox>,
output_masks: &mut Vec<Segmentation>,
) -> Result<(), DecoderError>
pub fn decode_for_tracking( &self, outputs: &[&TensorDyn], output_boxes: &mut Vec<DetectBox>, output_masks: &mut Vec<Segmentation>, ) -> Result<(), DecoderError>
Decode with one label per box, whatever Self::multi_label says.
Use this when the boxes feed an external tracker: trackers match on
IoU only, so per-class duplicates of one anchor become phantom tracks.
Logs a warning once per decoder when the decoder has multi-label
enabled. Decoder::decode_tracked and Decoder::decode_proto_tracked
(behind the tracker feature) apply the same rule. When the builder
raised Self::pre_nms_top_k to MULTI_LABEL_PRE_NMS_TOP_K only
because multi-label was on, this argmax decode uses
DEFAULT_PRE_NMS_TOP_K; an explicit cap is used as given.
§Errors
The same as Self::decode.
Sourcepub fn semantic(&self) -> Option<&SemanticConfig>
pub fn semantic(&self) -> Option<&SemanticConfig>
The model’s semantic segmentation output: a decoder: semantic
output, or the segmentation output of a ModelPack model. None when
the model has neither.
Sourcepub fn decode_semantic(
&self,
outputs: &[&TensorDyn],
dst: &mut TensorDyn,
confidence: Option<&mut TensorDyn>,
options: &SemanticOptions,
) -> Result<(), DecoderError>
pub fn decode_semantic( &self, outputs: &[&TensorDyn], dst: &mut TensorDyn, confidence: Option<&mut TensorDyn>, options: &SemanticOptions, ) -> Result<(), DecoderError>
Decodes the model’s semantic segmentation output into dst, an
[H, W] class map of dtype u8 (up to 256 classes) or u16, at any
resolution.
confidence, when given, receives the winning class’s probability as
an [H, W] map of f32 or u8 (scaled to 255); it is approximate
and meant for overlays. See crate::semantic for the decode modes
and crate::semantic::SemanticOptions for the letterbox and the
runtime mode override.
§Errors
DecoderError::NotSupported if the model has no semantic output;
otherwise the errors of crate::semantic::decode_semantic.
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
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
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