AbstractPhil's picture
Deploy: 12-task vision extraction + fusion ZeroGPU showcase
fed954e verified
Raw
History Blame Contribute Delete
5.78 kB
"""
coords.py — Coordinate normalization (the correctness centerpiece).
Different VLMs report boxes in different spaces:
* Qwen3-VL emits RELATIVE coordinates in 0..1000 (integers).
* Qwen3.5 / many checkpoints emit 0..1 floats.
* COCO / LVIS ground truth is ABSOLUTE pixels.
If predictions and ground truth are compared in mismatched spaces, every IoU is
silently wrong and detection/3D/segmentation metrics collapse. To prevent that,
the canonical internal form is ALWAYS pixel-absolute xyxy. GT is converted to
canonical at load time; predictions are converted at score time. No metric ever
sees a non-canonical coordinate (enforced by tests).
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Sequence
class CoordSpace(str, Enum):
"""The space a set of raw coordinates lives in."""
PIXEL_ABS = "pixel_abs" # absolute pixels, image-sized
NORM_0_1 = "norm_0_1" # 0..1 floats
NORM_0_1000 = "norm_0_1000" # 0..1000 ints (Qwen3-VL native)
# Box coordinate layouts a model might emit / GT might store.
XYXY = "xyxy" # [x1, y1, x2, y2]
XYWH = "xywh" # [x, y, w, h] (COCO GT layout)
def xywh_to_xyxy(box: Sequence[float]) -> list[float]:
x, y, w, h = box
return [x, y, x + w, y + h]
def xyxy_to_xywh(box: Sequence[float]) -> list[float]:
x1, y1, x2, y2 = box
return [x1, y1, x2 - x1, y2 - y1]
@dataclass(frozen=True)
class BBox:
"""A bounding box in the canonical form: pixel-absolute xyxy."""
x1: float
y1: float
x2: float
y2: float
def area(self) -> float:
return max(0.0, self.x2 - self.x1) * max(0.0, self.y2 - self.y1)
def clip(self, size: tuple[int, int]) -> "BBox":
"""Clip to image bounds. size = (W, H)."""
w, h = size
return BBox(
x1=min(max(self.x1, 0.0), w),
y1=min(max(self.y1, 0.0), h),
x2=min(max(self.x2, 0.0), w),
y2=min(max(self.y2, 0.0), h),
)
def iou(self, other: "BBox") -> float:
ix1 = max(self.x1, other.x1)
iy1 = max(self.y1, other.y1)
ix2 = min(self.x2, other.x2)
iy2 = min(self.y2, other.y2)
iw = max(0.0, ix2 - ix1)
ih = max(0.0, iy2 - iy1)
inter = iw * ih
if inter <= 0.0:
return 0.0
union = self.area() + other.area() - inter
return inter / union if union > 0 else 0.0
def as_list(self) -> list[float]:
return [self.x1, self.y1, self.x2, self.y2]
def _scale_for_space(space: CoordSpace, size: tuple[int, int]) -> tuple[float, float]:
"""Return (x_scale, y_scale) that maps a raw coord in `space` to pixels."""
w, h = size
if space == CoordSpace.PIXEL_ABS:
return 1.0, 1.0
if space == CoordSpace.NORM_0_1:
return float(w), float(h)
if space == CoordSpace.NORM_0_1000:
return w / 1000.0, h / 1000.0
raise ValueError(f"unknown coord space: {space!r}")
def to_canonical(
raw: Sequence[float],
space: CoordSpace,
size: tuple[int, int],
fmt: str = XYXY,
) -> BBox:
"""Convert a raw 4-tuple in `space`/`fmt` to a canonical pixel-abs xyxy BBox.
size = (W, H) in pixels. `fmt` is XYXY or XYWH.
"""
if len(raw) != 4:
raise ValueError(f"bbox must have 4 values, got {len(raw)}: {raw!r}")
coords = list(map(float, raw))
if fmt == XYWH:
coords = xywh_to_xyxy(coords)
elif fmt != XYXY:
raise ValueError(f"unknown bbox fmt: {fmt!r}")
sx, sy = _scale_for_space(space, size)
return BBox(coords[0] * sx, coords[1] * sy, coords[2] * sx, coords[3] * sy).clip(size)
def from_canonical(box: BBox, space: CoordSpace, size: tuple[int, int], fmt: str = XYXY) -> list[float]:
"""Inverse of to_canonical: canonical BBox → raw coords in `space`/`fmt`."""
sx, sy = _scale_for_space(space, size)
xyxy = [box.x1 / sx, box.y1 / sy, box.x2 / sx, box.y2 / sy]
return xyxy_to_xywh(xyxy) if fmt == XYWH else xyxy
def detect_space(raw_values: Sequence[float], size: tuple[int, int]) -> CoordSpace:
"""Defensive fallback for models that ignore the requested space.
Used ONLY when a model's output space can't be trusted; the caller logs
`coord_space_inferred=True` (itself a robustness signal). Heuristic:
* all values <= 1.0 → NORM_0_1
* all values <= 1000 and the image is larger than 1000 px on a side → NORM_0_1000
* otherwise → PIXEL_ABS
"""
vals = [abs(float(v)) for v in raw_values if v is not None]
if not vals:
return CoordSpace.PIXEL_ABS
mx = max(vals)
w, h = size
if mx <= 1.0:
return CoordSpace.NORM_0_1
if mx <= 1000.0 and max(w, h) > 1000:
return CoordSpace.NORM_0_1000
if mx <= 1000.0 and max(w, h) <= 1000:
# ambiguous: 0..1000 ints vs small-image pixels. Prefer NORM_0_1000 only
# if values clearly exceed the image dimensions.
if mx > max(w, h):
return CoordSpace.NORM_0_1000
return CoordSpace.PIXEL_ABS
return CoordSpace.PIXEL_ABS
def prompt_hint_for(space: CoordSpace) -> str:
"""A sentence appended to the system prompt telling the model which space to use."""
if space == CoordSpace.PIXEL_ABS:
return "Report bounding boxes as [x1, y1, x2, y2] in absolute pixel coordinates."
if space == CoordSpace.NORM_0_1:
return "Report bounding boxes as [x1, y1, x2, y2] normalized to 0..1 of the image dimensions."
if space == CoordSpace.NORM_0_1000:
return (
"Report bounding boxes as [x1, y1, x2, y2] integers in 0..1000, "
"relative to the image width and height."
)
raise ValueError(f"unknown coord space: {space!r}")