Skip to content

fix(detection): preserve class_name string dtype on empty Detections.from_inference - #2270

Merged
Borda merged 10 commits into
roboflow:developfrom
Ace3Z:fix/from-inference-empty-class-name-dtype
May 27, 2026
Merged

fix(detection): preserve class_name string dtype on empty Detections.from_inference#2270
Borda merged 10 commits into
roboflow:developfrom
Ace3Z:fix/from-inference-empty-class-name-dtype

Conversation

@Ace3Z

@Ace3Z Ace3Z commented May 25, 2026

Copy link
Copy Markdown
Contributor

For non-empty inference results, Detections.from_inference stores class_name as a string array (e.g. <U3 for ["cat"]). On the empty short-circuit path it instead built {CLASS_NAME_DATA_FIELD: np.empty(0)}, which defaults to float64. That breaks downstream code that assumes the array is string-typed (e.g. np.isin(det["class_name"], ["cat"]) returns a mismatched dtype warning, indexing into a list of names crashes, etc).

process_roboflow_result already returns a correctly typed empty data dict ({CLASS_NAME_DATA_FIELD: np.empty(0, dtype=str)} on the empty short-circuit, line 71 of detection/utils/internal.py). The fix is to use that returned data directly instead of rebuilding a wrong-dtype dict.

Minimal repro:

import supervision as sv

empty = sv.Detections.from_inference({"predictions": [], "image": {"width": 100, "height": 100}})
non_empty = sv.Detections.from_inference({
    "predictions": [{"x": 50, "y": 50, "width": 20, "height": 20,
                     "confidence": 0.9, "class": "cat", "class_id": 0}],
    "image": {"width": 100, "height": 100},
})
print(empty["class_name"].dtype, non_empty["class_name"].dtype)
# before: float64 <U3
# after:  <U1 <U3

tests/detection/test_core.py::test_from_inference_empty_class_name_dtype_matches_non_empty asserts both paths produce string-kind arrays.

…from_inference

For non-empty results, Detections.from_inference stores class_name as a
string array (e.g. <U3 for ["cat"]). On the empty short-circuit path
it instead built {CLASS_NAME_DATA_FIELD: np.empty(0)}, which defaults to
float64. process_roboflow_result already returns a correctly typed empty
data dict ({CLASS_NAME_DATA_FIELD: np.empty(0, dtype=str)}), so assign it
directly instead of rebuilding a wrong-dtype dict.
@Ace3Z
Ace3Z requested a review from SkalskiP as a code owner May 25, 2026 09:47
@Borda
Borda requested a review from Copilot May 26, 2026 16:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes a dtype inconsistency in Detections.from_inference so that the class_name data field remains string-typed even when the Roboflow/Inference result contains zero predictions, preventing downstream dtype-mismatch warnings/errors.

Changes:

  • Reuse process_roboflow_result’s returned data dict in the empty short-circuit path instead of rebuilding it with np.empty(0) (which defaults to float64).
  • Add a regression test ensuring empty and non-empty inference results produce class_name arrays of the same string dtype kind.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/supervision/detection/core.py Uses process_roboflow_result’s data directly for empty inference results to preserve string dtype.
tests/detection/test_core.py Adds coverage asserting class_name dtype kind matches between empty and non-empty inference outputs.

@codecov

codecov Bot commented May 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 14.28571% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 78%. Comparing base (2fdb970) to head (2c8a7ab).

Additional details and impacted files
@@           Coverage Diff           @@
##           develop   #2270   +/-   ##
=======================================
  Coverage       78%     78%           
=======================================
  Files           66      66           
  Lines         8412    8416    +4     
=======================================
+ Hits          6580    6589    +9     
+ Misses        1832    1827    -5     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Borda and others added 8 commits May 27, 2026 09:42
…e-dtype

Bring PR branch up to date with upstream develop (HeatMapAnnotator divide-by-zero fix, roboflow#2269).

---
Co-authored-by: Claude Code <noreply@anthropic.com>
…roboflow_result

Update three parametrize entries to use np.empty(0, dtype=str) instead of np.empty(0)
for expected class_name on empty-predictions paths. Add explicit dtype equality assertion
alongside the existing np.array_equal check so future regressions at the helper layer
are caught (np.array_equal is dtype-blind on zero-length arrays).

[resolve roboflow#1] /review finding by foundry:qa-specialist (report: .reports/review/2026-05-27T06-06-27Z/review-report.md)

---
Co-authored-by: Claude Code <noreply@anthropic.com>
… Returns

Callers have no docstring contract for detections.data["class_name"]. Add note to
Returns section: field always present as string-dtype array, empty shape (0,) when
predictions absent.

[resolve roboflow#2] /review finding by foundry:doc-scribe (report: .reports/review/2026-05-27T06-06-27Z/review-report.md)

---
Co-authored-by: Claude Code <noreply@anthropic.com>
… and from_vlm paths

from_ultralytics empty path returned cls.empty() with data={} — detections["class_name"]
returned None instead of an empty string array. Same bug in from_vlm Florence-2 branch.
Both now return data={CLASS_NAME_DATA_FIELD: np.empty(0, dtype=str)}, consistent with
the from_inference fix.

[resolve roboflow#3] /review finding by foundry:challenger (report: .reports/review/2026-05-27T06-06-27Z/review-report.md)

---
Co-authored-by: Claude Code <noreply@anthropic.com>
Add null-safety assertion (class_name is not None), full key-set + dtype parity check
across all data keys, and concatenation assertion covering the downstream mixed-batch
scenario. Replaces single dtype.kind check with structural invariant that guards future
key or dtype drift.

[resolve roboflow#4] [resolve roboflow#5] [resolve roboflow#8] /review findings by foundry:qa-specialist and foundry:challenger (report: .reports/review/2026-05-27T06-06-27Z/review-report.md)

---
Co-authored-by: Claude Code <noreply@anthropic.com>
…_inference

No test exercised the duck-typed .dict() object path (the documented Inference SDK
usage) for empty predictions. Add test with minimal fake SDK object to confirm the
dtype fix applies through the .dict() conversion branch.

[resolve roboflow#6] /review finding by foundry:qa-specialist (report: .reports/review/2026-05-27T06-06-27Z/review-report.md)

---
Co-authored-by: Claude Code <noreply@anthropic.com>
Function had no docstring. Document: CLASS_NAME_DATA_FIELD always present and
string-dtype in returned data dict (load-bearing invariant for the empty-path
fix), 6-tuple return order, and an example showing the dtype guarantee.

[resolve roboflow#7] /review finding by foundry:doc-scribe (report: .reports/review/2026-05-27T06-06-27Z/review-report.md)

---
Co-authored-by: Claude Code <noreply@anthropic.com>
…name

Empty arrays produce <U1 while non-empty "cat" produces <U3; exact dtype
equality was too strict. Kind comparison ("U") is the correct contract.

---
Co-authored-by: Claude Code <noreply@anthropic.com>
Borda
Borda previously approved these changes May 27, 2026
Simplify example to use inline `>>>` syntax, ensuring clarity and consistency with best practices.
@Borda
Borda force-pushed the fix/from-inference-empty-class-name-dtype branch from 6234178 to 2c8a7ab Compare May 27, 2026 08:27
@Borda
Borda merged commit 2c3a2ef into roboflow:develop May 27, 2026
26 checks passed
@Borda Borda mentioned this pull request Jun 11, 2026
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.

3 participants