Spaces:
Running on Zero
Running on Zero
File size: 8,441 Bytes
fed954e | 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 | """
specialists.py — the CPU assembly layer of the deterministic pipeline.
`Solids` holds one image's solidification primitives (detector boxes, seg masks, a
relative depth map, optional saliency, OCR, tags, class/style) — all in PIXEL space.
The `build_*` functions turn a `Solids` into each task's exact `tasks_vision` JSON, in
the task's declared coord space (via the existing `coords.from_canonical`), reusing the
`derive` engine for the INTEGRATE tasks.
The GPU half — loading the models and populating `Solids` — lives in the Colab runner
(`specialists_gpu`); this module is model-free and unit-tested on CPU.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Optional
import numpy as np
from . import derive
from .coords import BBox, CoordSpace, XYXY, _scale_for_space, from_canonical
from .tasks_vision import get_task
# ── coordinate helpers (pixel → task space) ──────────────────────────────────
def box_to_space(pix_xyxy, space: CoordSpace, size, ndigits: int = 2) -> list:
"""Pixel-abs [x1,y1,x2,y2] → the task's coord space."""
return [round(v, ndigits) for v in from_canonical(BBox(*map(float, pix_xyxy)), space, size, XYXY)]
def poly_to_space(pix_flat, space: CoordSpace, size, ndigits: int = 2) -> list:
"""Flat pixel polygon [x1,y1,x2,y2,...] → the task's coord space, per vertex."""
sx, sy = _scale_for_space(space, size) # raw = pixel / scale
out = []
for i in range(0, len(pix_flat) - 1, 2):
out.append(round(float(pix_flat[i]) / sx, ndigits))
out.append(round(float(pix_flat[i + 1]) / sy, ndigits))
return out
def quad_to_xyxy(quad) -> list:
"""A 4-point polygon (PaddleOCR) — [[x,y]*4] or flat [x,y,...] — → pixel xyxy."""
a = np.asarray(quad, dtype=float).reshape(-1, 2)
return [float(a[:, 0].min()), float(a[:, 1].min()), float(a[:, 0].max()), float(a[:, 1].max())]
# ── per-image primitives ─────────────────────────────────────────────────────
@dataclass
class Solids:
size: tuple # (W, H) pixels
boxes: list = field(default_factory=list) # [{label, box:[x1,y1,x2,y2] px, score, mask?}]
depth: Optional[np.ndarray] = None # HxW relative (Depth-Anything: higher = nearer)
saliency: Optional[np.ndarray] = None # HxW float in [0,1]
tags: list = field(default_factory=list) # [{label, score}] (RAM++/SigLIP2)
class_top: list = field(default_factory=list) # [{label, score}] top-k classification
ocr: Optional[dict] = None # {full_text, lines:[{text, box:[quad px], conf?}]}
style: Optional[str] = None # SigLIP2 style label
gray: Optional[np.ndarray] = None # HxW grayscale (for symmetry)
depth_higher_is_nearer: bool = True
attr_boxes: list = field(default_factory=list) # fusion tier: caption-phrase grounding
# [{phrase, matched_span, box:[x1,y1,x2,y2] px, score}] from ground_phrases()
# ── SOLID task builders (direct model outputs) ───────────────────────────────
# Emitted lists/strings are truncated to the registry caps (read from the spec,
# never hardcoded) — the generated pydantic models enforce max_items/max_length
# as hard constraints, so an uncapped emit would flunk schema validation on
# busy images even though the content is correct.
def build_bbox(s: Solids) -> dict:
spec = get_task("bbox_grounding")
cap = spec.fields["detections"].max_items
boxes = sorted(s.boxes, key=lambda b: -float(b.get("score", 1.0)))[:cap]
dets = [{"label": str(b["label"]),
"box": box_to_space(b["box"], spec.coord_space, s.size),
"score": round(float(b.get("score", 1.0)), 4)} for b in boxes]
return {"detections": dets, "count": len(dets)}
def build_segmentation(s: Solids) -> dict:
spec = get_task("segmentation")
cap = spec.fields["masks"].max_items
masked = [b for b in s.boxes if b.get("mask") is not None]
masked.sort(key=lambda b: -float(np.asarray(b["mask"]).sum())) # keep largest
masks = []
for b in masked:
if len(masks) >= cap:
break
poly = derive.outline_polygon(b["mask"], b["label"])["outline"]
if len(poly) >= 6:
masks.append({"label": str(b["label"]),
"polygon": poly_to_space(poly, spec.coord_space, s.size)})
return {"masks": masks}
def build_classification(s: Solids) -> dict:
top = s.class_top or ([{"label": s.boxes[0]["label"], "score": 1.0}] if s.boxes else
[{"label": "unknown", "score": 0.0}])
top = sorted(top, key=lambda t: -t["score"])[:5]
return {"label": str(top[0]["label"]), "confidence": round(float(top[0]["score"]), 4),
"top5": [{"label": str(t["label"]), "score": round(float(t["score"]), 4)} for t in top]}
def build_ocr(s: Solids) -> dict:
spec = get_task("ocr_text")
if not s.ocr:
return {"full_text": "", "lines": []}
lines_spec = spec.fields["lines"]
text_cap = next((f.max_str_length for f in lines_spec.nested_fields
if f.name == "text"), 512)
lines = []
for ln in s.ocr.get("lines", [])[:lines_spec.max_items]:
d = {"text": str(ln["text"])[:text_cap]}
if ln.get("box") is not None:
d["box"] = box_to_space(quad_to_xyxy(ln["box"]), spec.coord_space, s.size)
lines.append(d)
full_cap = spec.fields["full_text"].max_str_length
return {"full_text": str(s.ocr.get("full_text", ""))[:full_cap], "lines": lines}
# ── INTEGRATE task builders (derive engine + coord conversion) ───────────────
def build_spatial(s: Solids) -> dict:
return derive.spatial_relations(s.boxes, depth=s.depth,
higher_is_nearer=s.depth_higher_is_nearer)
def build_depth_order(s: Solids) -> dict:
if s.depth is None:
return {"nearest": "", "farthest": "", "relative_depth": []}
ents = [{"label": b["label"], "mask": b.get("mask"), "box": b["box"]} for b in s.boxes]
return derive.depth_order(ents, s.depth, higher_is_nearer=s.depth_higher_is_nearer)
def build_subject(s: Solids) -> dict:
space = get_task("subject_fixation").coord_space
out = derive.subject_fixation(s.boxes, s.size, saliency=s.saliency)
out["primary_subject"]["box"] = box_to_space(out["primary_subject"]["box"], space, s.size)
return out
def build_outline(s: Solids) -> dict:
space = get_task("outline_association").coord_space
masked = [b for b in s.boxes if b.get("mask") is not None]
if not masked:
return {"outline": [], "label": ""}
big = max(masked, key=lambda b: np.asarray(b["mask"]).sum())
o = derive.outline_polygon(big["mask"], big["label"])
o["outline"] = poly_to_space(o["outline"], space, s.size)
return o
def build_style(s: Solids) -> dict:
style = s.style if s.style in ("photo", "painting", "3d_render", "sketch", "anime", "other") else "other"
layout = derive.layout_kind(s.boxes, s.size)
symmetry = derive.symmetry_axis(s.gray) if s.gray is not None else "none"
return {"style": style, "layout": layout, "symmetry": symmetry}
def build_datatype_diff(s: Solids) -> dict:
return derive.detect_data_type(s.ocr.get("full_text", "") if s.ocr else "")
def build_datatype_util(s: Solids) -> tuple[dict, bool]:
"""Returns ({data_type, content}, parsed_ok). parsed_ok=False → caller may VLM-fallback."""
return derive.parse_data_type(s.ocr.get("full_text", "") if s.ocr else "")
# task → builder (SOLID + INTEGRATE only; semantic_association + vqa are VLM)
DETERMINISTIC_BUILDERS = {
"bbox_grounding": build_bbox,
"segmentation": build_segmentation,
"image_classification": build_classification,
"ocr_text": build_ocr,
"structural_spatial_awareness": build_spatial,
"depth_analysis": build_depth_order,
"subject_fixation": build_subject,
"outline_association": build_outline,
"style_structural_awareness": build_style,
"data_type_differentiation": build_datatype_diff,
# data_type_utilization handled specially (returns the parsed_ok flag)
}
|