Skip to content

Release v0.24.1#81

Merged
sebastient merged 10 commits into
mainfrom
release/0.24.1
May 26, 2026
Merged

Release v0.24.1#81
sebastient merged 10 commits into
mainfrom
release/0.24.1

Conversation

@sebastient
Copy link
Copy Markdown
Contributor

Summary

Patch release fixing the Decoder::normalized_boxes() accessor contract leak: the public flag now describes the post-decode coordinate space the caller actually receives, not the raw schema annotation.

When a schema declared normalized: false and the decoder also knew input_dims, every decode path internally divided bbox channels by (W, H) via yolo::maybe_normalize_boxes_in_place (EDGEAI-1303) — but normalized_boxes() still returned Some(false) ("pixel-space"). Downstream callers that trusted the flag (notably the GStreamer bridge) re-normalized themselves and collapsed detections to ~0.

The accessor now returns Some(true) whenever the internal helper runs, and Some(false) only when input_dims is unknown and pixel-space genuinely leaks out. No control-flow changes — the in-place helper still triggers on the same input policy. Only the public accessor and surrounding docs move; the C / Python equivalents inherit the fix automatically.

Three existing assertions that enshrined the buggy state flipped to expect Some(true):

  • crates/decoder/tests/decoder_normalized_flag.rs (combined-Detection and SplitSegDet)
  • crates/decoder/src/decoder/tests.rs (Hailo YOLOv8-seg schema)
  • crates/decoder/tests/per_scale_parity.rs (per-scale parity helper)

New regression normalized_false_without_input_dims_reports_false pins the Some(false) branch via a programmatic builder that leaves input_dims unset.

Test plan

  • CI: full Rust + Python test matrix on PR open
  • CI: SBOM + license policy gate
  • On-target: re-run the gst bridge smoke test on the platform that triggered the original coords ≈ 0 regression and confirm segmentation boxes land in [0, 1]

Signed-off-by: Sébastien Taylor <[email protected]>
Copilot AI review requested due to automatic review settings May 26, 2026 04:38
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Patch release v0.24.1 intended to fix a contract mismatch where Decoder::normalized_boxes() (and C/Python equivalents) reported the schema annotation rather than the post-decode coordinate space actually returned to callers.

Changes:

  • Update Decoder::normalized_boxes() semantics/docs to report the post-decode coordinate space.
  • Update Rust/C/Python documentation to describe the corrected contract.
  • Update/extend tests and bump workspace + Python package versions to 0.24.1.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
NOTICE Update crate version attributions to 0.24.1.
crates/python/src/decoder.rs Update Python API docs for normalized_boxes / input_dims.
crates/python/pyproject.toml Bump Python package version to 0.24.1.
crates/decoder/tests/per_scale_parity.rs Update per-scale parity assertion to new normalized_boxes() contract.
crates/decoder/tests/decoder_normalized_flag.rs Update assertions for new contract and add regression test for input_dims == None.
crates/decoder/src/decoder/tests.rs Update internal test expectation for Hailo YOLOv8-seg schema.
crates/decoder/src/decoder/mod.rs Implement new normalized_boxes() logic + updated rustdoc.
crates/decoder/src/decoder/builder.rs Adjust per-scale normalization commentary to match new contract.
crates/capi/src/decoder.rs Update C API docs describing hal_decoder_normalized_boxes.
crates/capi/include/edgefirst/hal.h Update public header docs for hal_decoder_normalized_boxes.
CHANGELOG.md Add v0.24.1 changelog entry describing the contract fix.
Cargo.toml Bump workspace + internal crate versions to 0.24.1.
Cargo.lock Lockfile updates reflecting 0.24.1 versions.

Comment on lines 302 to +339
@@ -316,21 +325,35 @@ impl Decoder {
/// # }
/// ```
pub fn normalized_boxes(&self) -> Option<bool> {
self.normalized
// Report the effective post-decode coordinate space. The
// internal `self.normalized` is the *input* policy fed to
// `yolo::maybe_normalize_boxes_in_place`; that helper divides
// by `input_dims` exactly when `normalized == Some(false)` and
// `input_dims` is a non-zero `(w, h)`. The output the caller
// sees is therefore normalized in that case — surface that.
match (self.normalized, self.input_dims) {
(Some(true), _) => Some(true),
(Some(false), Some((w, h))) if w != 0 && h != 0 => Some(true),
(Some(false), _) => Some(false),
(None, _) => None,
}
Comment thread crates/python/src/decoder.rs Outdated
Comment on lines +961 to +966
/// Reports the **post-decode** state, not the raw schema annotation:
/// when the schema declares pixel-space outputs but
/// :attr:`input_dims` is known, the decoder internally divides bbox
/// channels by ``(W, H)`` before returning, so the boxes the caller
/// receives are in ``[0, 1]`` and this getter returns ``True``.
/// Callers must not re-normalize in that case.
Comment thread crates/capi/include/edgefirst/hal.h Outdated
Comment on lines +1337 to +1346
* Reports the **post-decode** state, not the raw schema annotation: when
* the schema declares pixel-space outputs but `hal_decoder_input_dims()`
* is known, the decoder internally divides bbox channels by `(W, H)`
* before returning, so the boxes the caller receives are in `[0, 1]`
* and this function returns `1`. Only when no input dimensions are
* available does pixel-space leak out and this function return `0`.
*
* Callers MUST NOT re-normalize when this function returns `1`;
* dividing already-normalized coordinates by `(W, H)` again collapses
* detections to ~0.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 26, 2026

Test Results (x86_64)

162 tests  ±0   150 ✅ ±0   1m 25s ⏱️ +2s
  1 suites ±0    12 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit c252df5. ± Comparison against base commit 72294b1.

♻️ This comment has been updated with latest results.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 26, 2026

Test Results (aarch64)

1 260 tests  +7   1 248 ✅ +7   43s ⏱️ ±0s
    2 suites ±0      12 💤 ±0 
    2 files   ±0       0 ❌ ±0 

Results for commit c252df5. ± Comparison against base commit 72294b1.

♻️ This comment has been updated with latest results.

Per-scale decoders: accessor reports post-decode coordinate space
(Some(false) + valid input_dims upgrades to Some(true)). All other
decoders: raw schema annotation returned verbatim. C header regenerated
via cbindgen from updated Rust doc. CHANGELOG entry tightened to
match actual fix scope.

Signed-off-by: Sébastien Taylor <[email protected]>
- Flip accessor assertions in pixel_space_input_with_normalized_false_decodes
  and split_schema_pixel_space_with_normalized_false_decodes from Some(true)
  to Some(false): non-per-scale paths surface the raw schema flag.
- Rewrite file-level doc-block to describe the per-path contract and point
  readers at Decoder::normalized_boxes rustdoc; note which sub-claims each
  test section pins.
- Update normalized_false_without_input_dims_reports_false rationale from
  "input_dims absent so helper cannot run" to "non-per-scale always returns
  raw flag".
- Add normalized_none_reports_none (non-per-scale + None -> None).
- Add normalized_true_reports_true (non-per-scale + Some(true) -> Some(true)).
- Add non_per_scale_normalized_false_with_input_dims_reports_false_but_normalizes:
  accessor returns Some(false) AND decoded boxes land in [0,1], pinning both
  the under-claiming flag and the internal normalizer firing.

Signed-off-by: Sébastien Taylor <[email protected]>
@sebastient
Copy link
Copy Markdown
Contributor Author

PR feedback addressed (6 follow-up commits, 1780caa53ba8ad)

The original Release v0.24.1 commit over-claimed normalization — every decode path was assumed to normalize, but EDGEAI-1303's maybe_normalize_boxes_in_place only fires on a subset. Three rounds of work tightened the contract and extended the implementation so the accessor's report is accurate for every path it claims to cover.

Round 1 — 1780caa / b36f639 / 9b4d140 (conservative tightening)

  • Accessor restricted to upgrading only self.per_scale.is_some(); all other paths pass self.normalized raw.
  • Rationale: legacy ModelTypes have entry-point asymmetry (some paths normalize, others don't), so a single bool can't honestly describe them.
  • Docs, CHANGELOG, and tests realigned to the conservative contract.

Round 2 — e18873d (scope expanded per reviewer feedback)

  • YoloSegDet's tracker dispatch was the missing case: decode/decode_proto normalized but decode_tracked/decode_tracked_proto didn't, leaving the same gst-bridge double-normalize bug class for tracker users.
  • Fix: added maybe_normalize_boxes_in_place calls in process_tracked_yolo_segmentation! (postprocess.rs:1748) and process_tracked_yolo_segdet_float (postprocess.rs:2115) so YoloSegDet now normalizes uniformly across all four entry points.
  • Accessor extended via legacy_path_normalizes_uniformly() predicate (matches ModelType::YoloSegDet { .. }) so its report matches the new uniform behavior.
  • Helper call-site count: 4 (postprocess.rs) + 2 (per_scale_bridge.rs) + 5 (yolo.rs) = 11.

Round 3 — 82b180e / 53ba8ad (docs + tests for extended contract)

  • C / Python / CHANGELOG prose names per-scale AND ModelType::YoloSegDet as uniformly-normalizing; explicitly names YoloSplitSegDet and YoloSegDet2Way as remaining asymmetric so callers know what's covered.
  • C header regenerated via cbindgen.
  • New regression yolo_segdet_tracker_path_normalizes pins both the accessor flip AND the new tracker helper call.
  • Renamed non_per_scale_non_yolo_segdet_normalized_false_with_input_dims_reports_false to keep the under-claim regression honest after YoloSegDet left that bucket.

Test summary

Suite Passed Failed Ignored
--lib --features tracker 390 0 4
decoder_normalized_flag 8 0 0
per_scale_parity 29 0 2
doc-tests 61 0 0
edgefirst-hal-capi 1 0 0

All gates green locally: cargo check --workspace, make format lint, verify_version, make sbom.

Known remaining gaps (potential follow-up tickets)

  • ModelType::YoloSplitSegDet: helper fires on decode (yolo.rs:1282) but NOT on the tracker macro process_tracked_yolo_segmentation_split! (~postprocess.rs:1773) or on _proto variants. Asymmetric — accessor correctly under-claims Some(false). Same-pattern fix would mirror what landed for YoloSegDet.
  • ModelType::YoloSegDet2Way: helper not called anywhere in either tracker or non-tracker paths. Likely emits pixel-space when normalized: false. Architect's investigation surfaced this as a likely-pre-existing bug independent of the original contract leak. Worth a JIRA ticket.

Both can be deferred — they don't regress against the pre-0.24.1 behavior; the accessor simply doesn't claim a contract it can't honor.

Test plan

  • CI: full Rust + Python test matrix
  • CI: SBOM + license policy gate
  • On-target: gst bridge smoke test on the platform that triggered the original coords ≈ 0 regression, exercising both per-scale and YoloSegDet decoders (including tracked variants)

Four paths now invoke maybe_normalize_boxes_in_place uniformly: per-scale,
YoloSegDet, YoloSplitSegDet, YoloSegDet2Way. Update Doxygen, Python
docstrings, and CHANGELOG to name all four and drop the stale two-path
claim. Regenerate C header via cbindgen.

Signed-off-by: Sébastien Taylor <[email protected]>
Update decoder_normalized_flag.rs for fa8e919: flip
split_schema_pixel_space_with_normalized_false_decodes to assert
Some(true) now that YoloSplitSegDet normalizes uniformly; replace the
stale non_per_scale_non_yolo_segdet test (premise invalidated) with
detection_only_normalized_false_with_input_dims_reports_false_raw using
a YoloDet schema (still returns raw flag); add
yolo_split_segdet_tracker_path_normalizes and
yolo_segdet_2way_decode_normalizes to pin the new uniform-normalization
contract for the tracker and decode entry points on those types; tighten
the file-level doc-block to reflect the complete coverage.

Signed-off-by: Sébastien Taylor <[email protected]>
@sebastient
Copy link
Copy Markdown
Contributor Author

Round 4 — full uniform-normalization closure (commits fa8e919c252df5)

After deeper investigation the remaining seg/mask/proto asymmetries flagged in the prior comment have been closed. Per the user's "fix this properly" instruction, every entry point of every seg/mask/proto ModelType now uniformly invokes maybe_normalize_boxes_in_place.

Implementation (fa8e919)

Added the helper call in 7 new locations across postprocess.rs (quant _proto decoders, both tracker macros for split and 2way, the split-float tracker helper, the 2way-float inline decoders) plus 1 signature extension in yolo.rs (impl_yolo_split_segdet_float_proto now accepts normalized + input_dims).

Audit table per ModelType × entry point — every cell that touched seg/mask/proto outputs is now uniformly Y:

ModelType decode decode_proto decode_tracked decode_tracked_proto
Per-scale Y Y Y Y
YoloSegDet Y Y Y Y
YoloSplitSegDet Y Y (new) Y (new) Y (new)
YoloSegDet2Way Y (new) Y (new) Y (new) Y (new)

Total helper callsites: 19 (was 11 at the start of this round).

Accessor (fa8e919)

fn legacy_path_normalizes_uniformly(&self) -> bool {
    matches!(
        self.model_type,
        ModelType::YoloSegDet { .. }
            | ModelType::YoloSplitSegDet { .. }
            | ModelType::YoloSegDet2Way { .. }
    )
}

Tests (c252df5)

  • Flipped two now-stale assertions (split_schema_pixel_space_with_normalized_false_decodes, the YoloSplitSegDet under-claim regression).
  • Refactored non_per_scale_non_yolo_segdet_normalized_false_with_input_dims_reports_false into detection_only_normalized_false_with_input_dims_reports_false_raw using a YoloDet schema (which intentionally stays at the raw-flag contract).
  • Added two new tracker-path regressions:
    • yolo_split_segdet_tracker_path_normalizes — pins YoloSplitSegDet decode_tracked upgrade + boxes in [0,1].
    • yolo_segdet_2way_decode_normalizes — pins YoloSegDet2Way decode upgrade + boxes in [0,1].
  • Updated file-level doc-block to describe the now-complete four-path uniform contract.

Docs (73d04bc)

  • C Doxygen / regenerated C header: rewritten from "two normalizing paths" to "four normalizing paths"; explicitly names the still-raw-flag families (YoloDet, YoloSplitDet, YoloEndToEnd*, ModelPack*).
  • Python getter docstrings: same widening, using :attr: cross-refs.
  • CHANGELOG rewritten in place to describe the complete fix: implementation + accessor halves, and the intentional raw-flag exceptions.

Test summary

Suite Passed Failed Ignored
--lib --features tracker (unit) 390 0 4
decoder_normalized_flag --features tracker 10 0 0
per_scale_parity 29 0 2
Integration (decoder_from_edgefirst_json, decoder_capacity, decode_vs_proto_parity) 7 0 0
doc-tests 61 0 0
CAPI test_decoder_normalized_boxes 1 0 0

All gates green: cargo check --workspace, make format lint, verify_version (after Round 1 bump), SBOM + license policy clean from round 1.

Confirmed remaining gaps (intentional; suitable for follow-up tickets, NOT release blockers)

  1. YoloEndToEnd*: schemas declaring normalized: false paired with these models would still receive pixel-space boxes — the embedded-NMS coordinate contract makes the right answer unclear. Worth a JIRA ticket to clarify whether YoloEndToEnd* schemas can legitimately declare normalized: false and, if so, whether the helper should fire there too.
  2. ModelPack*: separate model family with its own conventions. Same situation — needs a clarification ticket.
  3. YoloSegDet2Way decode_tracked test: the implementation is in place (macro at postprocess.rs:1974 now calls the helper), but no synthetic test exercises this entry point because constructing a 2-way schema + ByteTrack stack programmatically is non-trivial. The decode path IS pinned by the new yolo_segdet_2way_decode_normalizes test. Low priority — covered implicitly by source-level audit.

Test plan

  • CI: full Rust + Python test matrix
  • CI: SBOM + license policy gate
  • On-target: gst bridge smoke test exercising all four ModelType families (per-scale, YoloSegDet, YoloSplitSegDet, YoloSegDet2Way) via both decode and decode_tracked to confirm coordinates land in [0, 1] for all of them.

@sonarqubecloud
Copy link
Copy Markdown

@sebastient sebastient merged commit 3363557 into main May 26, 2026
15 checks passed
@sebastient sebastient deleted the release/0.24.1 branch May 26, 2026 13:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants