2828from __future__ import annotations
2929
3030import logging
31+ import re
3132from pathlib import Path
3233from typing import TYPE_CHECKING , Any , ClassVar
3334
5354_ERR_ENCODE_NOT_SUPPORTED = "GroundingDINOAdapter does not support encode(). Use extract() instead."
5455
5556
57+ _WORD_RE = re .compile (r"\w+" )
58+
59+
60+ def _canonical_label (phrase : str , labels : list [str ] | None ) -> str :
61+ """The caller's label a grounded phrase came from, or the phrase itself.
62+
63+ Scores each label by the share of its words the phrase contains, then by
64+ how many of the phrase's words it explains ("red handbag" beats "handbag"
65+ for the phrase "red handbag"); the earlier label breaks a full tie. The
66+ phrase is kept verbatim only when no label shares a word with it, which
67+ is the ``instruction`` prompt case (no labels) or a decode the caller
68+ never asked for.
69+ """
70+ if not labels :
71+ return phrase
72+ phrase_words = set (_WORD_RE .findall (phrase .lower ()))
73+ best_label , best_key = phrase , (0.0 , 0 )
74+ for label in labels :
75+ words = set (_WORD_RE .findall (label .lower ()))
76+ if not words :
77+ continue
78+ matched = len (words & phrase_words )
79+ key = (matched / len (words ), matched )
80+ if key > best_key :
81+ best_label , best_key = label , key
82+ return best_label
83+
84+
5685class GroundingDINOAdapter (BaseAdapter ):
5786 """Adapter for GroundingDINO open-vocabulary object detection.
5887
@@ -225,6 +254,7 @@ def extract(
225254 pixel_values = pixel_values ,
226255 pixel_mask = pixel_mask ,
227256 original_sizes = original_sizes ,
257+ labels = labels ,
228258 )
229259 else :
230260 # Fallback: decode images inline
@@ -246,6 +276,7 @@ def extract(
246276 box_threshold = box_threshold ,
247277 text_threshold = text_threshold ,
248278 images = images ,
279+ labels = labels ,
249280 )
250281
251282 # Map results back to original item positions
@@ -265,6 +296,7 @@ def _detect_batch(
265296 pixel_mask : torch .Tensor | None = None ,
266297 original_sizes : list [tuple [int , int ]] | None = None ,
267298 images : list [Image ] | None = None ,
299+ labels : list [str ] | None = None ,
268300 ) -> list [list [DetectedObject ]]:
269301 """Run batched detection on multiple images.
270302
@@ -279,6 +311,8 @@ def _detect_batch(
279311 pixel_values: Preprocessed image tensor [B, C, H, W] (optional).
280312 original_sizes: List of (width, height) tuples for bbox denormalization.
281313 images: List of PIL Images (fallback if pixel_values not provided).
314+ labels: The caller's labels, verbatim, when the prompt was built from
315+ them; every detection's label is mapped back onto one of these.
282316
283317 Returns:
284318 List of detected object lists, one per input image.
@@ -345,10 +379,19 @@ def _detect_batch(
345379 )
346380
347381 # Convert results to DetectedObject format
348- return [self ._results_to_objects (result ) for result in results ]
349-
350- def _results_to_objects (self , result : dict [str , Any ]) -> list [DetectedObject ]:
351- """Convert post-processed detection result to DetectedObject list."""
382+ return [self ._results_to_objects (result , labels ) for result in results ]
383+
384+ def _results_to_objects (self , result : dict [str , Any ], labels : list [str ] | None = None ) -> list [DetectedObject ]:
385+ """Convert post-processed detection result to DetectedObject list.
386+
387+ The post-processor decodes, per box, the prompt tokens above
388+ ``text_threshold``, so a caller's label comes back lowercased and
389+ possibly as a fragment or a span merged across two labels ("leather
390+ handbag" for "Red Leather Handbag"). Callers match detections against
391+ the labels they sent, so each phrase is mapped back onto the caller's
392+ label it overlaps most; a free-text ``instruction`` prompt has no
393+ labels and keeps the phrase.
394+ """
352395 boxes = result ["boxes" ]
353396 scores = result ["scores" ]
354397 result_labels = result .get ("text_labels" , result .get ("labels" , []))
@@ -364,7 +407,7 @@ def _results_to_objects(self, result: dict[str, Any]) -> list[DetectedObject]:
364407 for i in range (n_detections ):
365408 x1 , y1 , x2 , y2 = boxes_list [i ]
366409 score = scores_list [i ]
367- label_text = result_labels [i ]
410+ label_text = _canonical_label ( result_labels [i ], labels )
368411
369412 objects .append (
370413 DetectedObject (
0 commit comments