Spaces:
Paused
Paused
File size: 7,861 Bytes
0127c21 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | """
Text-guided region grounding specialist model.
Phase 6 (P0). Satisfies PS requirement:
- "Each solution must additionally implement either captioning/scene
description OR text-guided region grounding." β we chose grounding
(see README Β§4 rationale: stronger map/demo fit, produces the
bounding-box evidence the PS explicitly calls for: "Highlight the
water body referred to in the query.")
MODEL CHOICE β google/owlvit-base-patch32
- Verified supports: open-vocabulary object detection. Give it an image
+ arbitrary text phrase(s), it returns bounding boxes + a match score
per phrase. This is exactly "text-guided region grounding" β no
detection-head retraining needed to add new object classes.
- Modality: input = 1 RGB image + 1-N text phrases. Output = boxes
(pixel coords) + confidence score per phrase.
- Integration complexity: LOW. Pure `transformers`
(OwlViTProcessor/OwlViTForObjectDetection), same dependency footprint
as Phase 5's VQA model β no new packages required.
- License: Apache 2.0 (OWL-ViT / Google). Free for hackathon/research use.
- NOT remote-sensing-native. It was trained on natural-image/web data,
so expect it to do well on generic categories ("water", "building",
"road", "vehicle", "vegetation") and poorly on RS-specific phrasing
("built-up area", "paddy field") until adapted. This model is a
strong Phase-6 candidate for the SAME LoRA-style domain adaptation
approach as Phase 5's VQA model β not yet built; would be a Phase 6.5
follow-up if time allows, not required for the PS baseline.
- Confidence returned is the detector's own softmax match score for the
best box, NOT a validated grounding-accuracy benchmark figure.
"""
from __future__ import annotations
import re
import threading
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
from preprocessing.raster_to_rgb import load_as_rgb
MODEL_ID = "google/owlvit-base-patch32"
# Same confidence-bucket policy as Phase 5 (master brief Β§10) β kept
# consistent across specialist models so the UI doesn't need per-task
# thresholds.
_HIGH_THRESHOLD = 0.30 # OWL-ViT match scores run lower than VQA token
_MEDIUM_THRESHOLD = 0.12 # probs by nature of open-vocab detection; these
# are a prototype policy, not a validated cutoff.
# Rule-based phrase extraction β strips grounding-intent scaffolding off
# the user's natural-language query so OWL-ViT gets a clean noun phrase.
# Deterministic on purpose, consistent with agent/router.py's approach.
_STRIP_PREFIXES = [
"where is the", "where is", "highlight the", "highlight", "locate the",
"locate", "point to the", "point to", "show me the", "show me",
"draw a box around the", "draw a box around", "mark the", "mark",
"find the", "find",
]
_STRIP_SUFFIXES = ["referred to in the query", "in this image", "in the image"]
@dataclass
class BoundingBox:
x_min: float
y_min: float
x_max: float
y_max: float
@dataclass
class GroundingResult:
phrase: str
found: bool
box: Optional[BoundingBox]
score: float
confidence_level: str # "high" | "medium" | "insufficient"
confidence_note: str
image_width: int
image_height: int
model_id: str
adapted: bool
def extract_referring_phrase(query: str) -> str:
"""Turn a grounding question into a short noun phrase for the detector.
e.g. "Where is the water body?" -> "water body"
"Highlight the flooded area." -> "flooded area"
Purely rule-based (strip known prefixes/suffixes + punctuation) β
no LLM call, consistent with the router's deterministic classification.
If nothing matches, falls back to the raw query so grounding still
attempts something rather than failing silently.
"""
q = query.strip().rstrip("?.!").lower()
for prefix in sorted(_STRIP_PREFIXES, key=len, reverse=True):
if q.startswith(prefix):
q = q[len(prefix):].strip()
break
for suffix in _STRIP_SUFFIXES:
if q.endswith(suffix):
q = q[: -len(suffix)].strip()
q = re.sub(r"^(the|a|an)\s+", "", q).strip()
return q or query.strip().rstrip("?.!")
class _GroundingModel:
"""Lazy-loaded singleton, mirrors models/vqa.py's pattern."""
def __init__(self) -> None:
self._lock = threading.Lock()
self._processor = None
self._model = None
self._device = "cpu"
self._adapted = False # no grounding adapter exists yet β see docstring
def _ensure_loaded(self) -> None:
if self._model is not None:
return
with self._lock:
if self._model is not None:
return
import torch
from transformers import OwlViTForObjectDetection, OwlViTProcessor
self._device = "cuda" if torch.cuda.is_available() else "cpu"
self._processor = OwlViTProcessor.from_pretrained(MODEL_ID)
self._model = OwlViTForObjectDetection.from_pretrained(MODEL_ID).to(self._device).eval()
def ground(self, image_path: Path, phrase: str) -> GroundingResult:
self._ensure_loaded()
import torch
image = load_as_rgb(image_path)
width, height = image.size
inputs = self._processor(text=[[phrase]], images=image, return_tensors="pt").to(self._device)
with torch.no_grad():
outputs = self._model(**inputs)
target_sizes = torch.tensor([[height, width]])
results = self._processor.post_process_grounded_object_detection(
outputs=outputs, target_sizes=target_sizes, threshold=0.0
)[0]
boxes, scores = results["boxes"], results["scores"]
if len(scores) == 0:
level, note = "insufficient", "Detector returned no candidate region at all for this phrase."
return GroundingResult(
phrase=phrase, found=False, box=None, score=0.0,
confidence_level=level, confidence_note=note,
image_width=width, image_height=height,
model_id=self._model_id_label(), adapted=self._adapted,
)
best_idx = int(torch.argmax(scores))
best_score = float(scores[best_idx])
x_min, y_min, x_max, y_max = (float(v) for v in boxes[best_idx])
level, note = _bucket_confidence(best_score)
return GroundingResult(
phrase=phrase,
found=True,
box=BoundingBox(x_min=x_min, y_min=y_min, x_max=x_max, y_max=y_max),
score=best_score,
confidence_level=level,
confidence_note=note,
image_width=width,
image_height=height,
model_id=self._model_id_label(),
adapted=self._adapted,
)
def _model_id_label(self) -> str:
return MODEL_ID + (" + RS LoRA adapter" if self._adapted else " (base, not RS-adapted)")
def _bucket_confidence(score: float) -> tuple[str, str]:
if score >= _HIGH_THRESHOLD:
return "high", "Detector was strongly confident this region matches the phrase."
if score >= _MEDIUM_THRESHOLD:
return "medium", "Detector found a plausible region but with moderate uncertainty β verify visually."
return "insufficient", "Best candidate region had low match confidence β treat as unreliable."
_singleton: Optional[_GroundingModel] = None
_singleton_lock = threading.Lock()
def get_grounding_model() -> _GroundingModel:
global _singleton
if _singleton is None:
with _singleton_lock:
if _singleton is None:
_singleton = _GroundingModel()
return _singleton
def run_grounding(image_path: Path, query: str) -> GroundingResult:
phrase = extract_referring_phrase(query)
return get_grounding_model().ground(image_path, phrase)
|