feat: add roboflow_core/detections_difference@v1 fusion block - #2765
Merged
PawelPeczek-Roboflow merged 4 commits intoAug 7, 2026
Merged
Conversation
Match object detections across before/after images of the same scene and report removed, persisted and new objects plus a verified flag. Cost is spatial (1 - IoU, DIoU-style normalised centre-distance fallback for disjoint boxes) plus an optional class-mismatch penalty; assignment is a dependency-free greedy lowest-cost pass (scipy is not in base requirements) with a reject-cost cutoff. Cross-time counterpart of overlap_analysis@v1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Exercise the block inside a compiled workflow via WorkflowBatchInput entries of kind object_detection_prediction (EE >=1.3.0), avoiding the model-weight and network dependencies of the model-step pattern used by the overlap_analysis integration test. Covers the removed/persisted/new split with detection_id propagation, verified gating on the no-change case, and serialize_results=True round-tripping of the detection outputs to the wire format. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
adawgwats
marked this pull request as ready for review
August 7, 2026 07:00
adawgwats
requested review from
PawelPeczek-Roboflow,
dkosowski87,
grzegorz-roboflow,
hansent,
probicheaux,
rafel-roboflow and
yeldarby
as code owners
August 7, 2026 07:00
PawelPeczek-Roboflow
approved these changes
Aug 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat: add
roboflow_core/detections_difference@v1fusion blockDescription
Adds a new fusion block that compares two sets of object-detection predictions of the
same scene at different times and reports the object-level set difference:
removed_detections— reference ("before") detections with no acceptable match in thecandidate ("after") set, returned in the reference image's coordinates
persisted_detections/new_detections— matched / unmatched candidate detections,returned in the candidate image's coordinates
removed_count,new_count— the corresponding countsverified— true when the reference set was non-empty and at leastmin_removed_to_verifydetections were removedMotivating use case: cleanup verification
The block was built for before/after cleanup verification: run a litter detector on a
photo of a littered area, run it again on an "after" photo taken from roughly the same
viewpoint once cleanup is claimed, and use
removed_detections+verifiedasphotographic evidence that the reported cleanup actually happened — per-object, not just
"the image changed". The same Workflow shape covers any before/after audit:
How it relates to existing blocks
roboflow_core/overlap_analysis@v1is the direct interface precedent: the manifestmirrors its
reference_predictions/candidate_predictionstwo-detection-set inputpair. Overlap Analysis relates two detection sets from the same image geometrically
and emits per-pair overlap records; this block is its cross-time counterpart — it
performs a one-to-one assignment between the two sets and emits the resulting set
difference as first-class
sv.Detectionsoutputs that downstream blocks (filters,visualizations, sinks, webhooks) can consume directly.
model_comparison) is the same-image cousin on thevisualization side: it paints where two prediction sets disagree on one image, but its
output is pixels.
detections_differenceproduces the structured equivalent —matched/unmatched detections and counts you can branch on (e.g. Continue-If on
verified) or persist.Matching model
Each reference detection is matched with at most one candidate detection (and vice
versa) by minimizing:
spatial_term = 1 - IoUfor overlapping pairs; for disjoint pairs it falls back to1 + centre_distance / enclosing_box_diagonal(a DIoU-style gradient), so anyoverlapping pair always beats any disjoint pair while disjoint pairs still rank by
proximity.
class_mismatch_penalty(default 0.15) is added when classes differ;class_strict=Trueforbids cross-class matches outright.reject_cost(default 0.75) are discarded — both sides thencount as removed / new respectively.
scipy.optimize. linear_sum_assignmentwould be optimal (Hungarian), but scipy is not among the baserequirements of this repository, so the block stays numpy-only; for the low detection
counts typical of before/after comparisons the assignments rarely differ. The trade-off
is documented in the
greedy_matchdocstring and pinned by a dedicated unit test, so afuture switch to an optimal solver shows up as a deliberate test change.
Outputs are produced by fancy-indexing the input
sv.Detections, so everydatakey(detection ids, parent coordinates, ...) is propagated and downstream coordinate
re-projection keeps working. No new kind is introduced — all outputs use existing
object_detection_prediction/integer/booleankinds with their registeredserializers.
Type of change
How has this change been tested
tests/workflows/unit_tests/core_steps/fusion/test_detections_difference.py(31 tests, mirrors
test_overlap_analysis.pyconventions) covers:DetectionsDifferencealias + defaults, out-of-boundsrejection for
spatial_weight,class_mismatch_penalty,reject_cost,min_removed_to_verifydescribe_outputs()names ==run()keys(all removed,
verified), partial cleanup (removed + persisted mix), new detections,output types
0.75 (rejected → removed + new) vs. the same geometry with the threshold raised by 1e-6
(matched); an IoU-driven variant; and exact-equality acceptance at
reject_costclass_strictwith anabsurdly permissive
reject_cost, class-less inputs matching purely spatiallyverifiedgating viamin_removed_to_verify; empty reference / empty candidate /both empty
global-cheapest-first order, and the documented greedy-vs-Hungarian suboptimality case
load_blocks())tests/workflows/integration_tests/execution/test_workflow_with_detections_difference_block.py(3 tests) runs the block inside a compiled workflow, hermetically: detections are
injected via
WorkflowBatchInputentries of kindobject_detection_prediction(EE >= 1.3.0) instead of chaining two model steps, so no model weights or network
access are needed. Covers the removed/persisted/new split with
detection_idpropagation,
verifiedgating on the no-change case, andserialize_results=Trueround-tripping of the detection outputs to the wireformat.
Results (local, Python 3.12.13, base requirements + repo on
PYTHONPATH):black --check/isort --checkclean on the touched source and test files.Docs
Block docs are autogenerated from the manifest
Fielddescriptions andLONG_DESCRIPTION— no hand-written docs page added, perdocs/workflows/create_workflow_block.md.Screenshot
(two Object Detection Model steps → Detections Difference → Continue-If on
verified)to be added here.
Follow-ups (not in this PR)
RoboflowObjectDetectionModelsteps (pattern:test_workflow_with_overlap_analysis_block.py, needs weights + network) +add_to_workflows_galleryentry. A hermeticWorkflowBatchInput-basedintegration test is already included in this PR.
Related: roboflow/supervision#2476 proposes the same primitive at the library level. This block is self-contained and does not depend on it — the cost matrix here is plain numpy, and the base requirements stay torch-free.