Image-Text-to-Text
Transformers
Safetensors
panorama_chat
feature-extraction
panoptic-grounded-captioning
grounded-captioning
segmentation
multimodal
conversational
custom_code
Instructions to use Panorama-grounding/PANORAMA-4B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Panorama-grounding/PANORAMA-4B with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="Panorama-grounding/PANORAMA-4B", trust_remote_code=True) messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] pipe(text=messages)# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("Panorama-grounding/PANORAMA-4B", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use Panorama-grounding/PANORAMA-4B with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "Panorama-grounding/PANORAMA-4B" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Panorama-grounding/PANORAMA-4B", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker
docker model run hf.co/Panorama-grounding/PANORAMA-4B
- SGLang
How to use Panorama-grounding/PANORAMA-4B with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "Panorama-grounding/PANORAMA-4B" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Panorama-grounding/PANORAMA-4B", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "Panorama-grounding/PANORAMA-4B" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Panorama-grounding/PANORAMA-4B", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }' - Docker Model Runner
How to use Panorama-grounding/PANORAMA-4B with Docker Model Runner:
docker model run hf.co/Panorama-grounding/PANORAMA-4B
File size: 10,462 Bytes
f488763 | 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 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | #!/usr/bin/env python3
"""Visualise PANORAMA's masks: one panel per phrase, plus a combined overlay.
python visualize.py --image test.png --pred ./out # after predict_panocaps.py
python visualize.py --image ./my_images/ --pred ./out # a whole folder
For each image it writes a folder <stem>_viz/ containing:
00_<phrase>.png ... the image with ONLY that phrase's mask(s) lit; everything else dimmed
overlay.png all masks tinted per phrase + a colour->phrase legend
caption.txt the caption verbatim, with the <p>...</p>[SEG] markers
caption_clean.txt the same caption with the markers stripped
Per-phrase panels dim the background instead of tinting the mask, so you judge the mask against the
object's real colours (a 50% tint turns a red mug green). The overlay is for coverage; the panels
are for correctness.
Pillow + numpy only — no GPU, no extra libraries.
"""
import argparse
import json
import os
import re
import numpy as np
from PIL import Image, ImageDraw, ImageFont
# Colour is per PHRASE, not per mask: one phrase can own several masks ("three chairs"), and they
# belong together. masks_per_phrase in the json gives the split.
# Okabe-Ito (colour-blind friendly), extended; cycles if a caption names more regions than colours.
# Names are the canonical Okabe-Ito ones for the first 8; the rest are our tints/extensions.
# Index == the phrase number printed in the per-phrase panels, so "phrase 03" is always yellow.
PALETTE_NAMES = [
"orange", # 0 (230, 159, 0) Okabe-Ito
"sky-blue", # 1 ( 86, 180, 233) Okabe-Ito
"bluish-green", # 2 ( 0, 158, 115) Okabe-Ito
"yellow", # 3 (240, 228, 66) Okabe-Ito
"blue", # 4 ( 0, 114, 178) Okabe-Ito
"vermillion", # 5 (213, 94, 0) Okabe-Ito
"reddish-purple", # 6 (204, 121, 167) Okabe-Ito
"grey", # 7 (153, 153, 153) Okabe-Ito (black slot, lightened)
"salmon", # 8 (255, 128, 128) extension
"mint", # 9 (128, 255, 128) extension
"periwinkle", # 10 (128, 128, 255) extension
"apricot", # 11 (255, 200, 100) extension
"amethyst", # 12 (170, 110, 200) extension
"teal", # 13 (100, 200, 190) extension
"raspberry", # 14 (200, 90, 140) extension
]
PALETTE = [
(230, 159, 0), (86, 180, 233), (0, 158, 115), (240, 228, 66), (0, 114, 178),
(213, 94, 0), (204, 121, 167), (153, 153, 153), (255, 128, 128), (128, 255, 128),
(128, 128, 255), (255, 200, 100), (170, 110, 200), (100, 200, 190), (200, 90, 140),
]
assert len(PALETTE_NAMES) == len(PALETTE), "palette names and colours out of sync"
def colour_name(i):
"""Name of the colour phrase `i` is drawn in (wraps like PALETTE does)."""
return PALETTE_NAMES[i % len(PALETTE_NAMES)]
OVERLAY_ALPHA = 0.45 # combined view: strong enough to read coverage
DIM = 0.28 # per-phrase panels: how much of the image survives outside the mask
def phrase_of_mask(counts, n_masks):
"""mask index -> phrase index, from masks_per_phrase (falls back to 1:1)."""
if not counts:
return list(range(n_masks))
out = []
for pi, c in enumerate(counts):
out.extend([pi] * int(c))
if len(out) < n_masks: # trust the masks, not the counts
out.extend([len(counts) - 1] * (n_masks - len(out)))
return out[:n_masks]
def outline(mask):
"""1px boundary of a boolean mask."""
e = np.zeros_like(mask)
e[:-1, :] |= mask[:-1, :] ^ mask[1:, :]
e[:, :-1] |= mask[:, :-1] ^ mask[:, 1:]
return e
def _font(size=15):
for p in ('DejaVuSans.ttf', '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf'):
try:
return ImageFont.truetype(p, size)
except Exception:
pass
return ImageFont.load_default()
def panel(img_arr, masks, colour, label):
"""One phrase: its mask(s) at full brightness, everything else dimmed."""
m = np.zeros(img_arr.shape[:2], bool)
for k in masks:
m |= k
out = (img_arr * DIM).astype(np.float32) # dim everything
out[m] = img_arr[m] # restore the mask region
out[outline(m)] = colour # trace it
im = Image.fromarray(out.astype(np.uint8))
d = ImageDraw.Draw(im)
f = _font(16)
px = int(m.sum())
txt = f'{label} — {px:,} px' + (f' ({len(masks)} masks)' if len(masks) > 1 else '')
x0, y0, x1, y1 = d.textbbox((0, 0), txt, font=f)
d.rectangle([6, 6, 14 + (x1 - x0), 14 + (y1 - y0)], fill=(0, 0, 0))
d.rectangle([6, 6, 12, 14 + (y1 - y0)], fill=colour) # colour tab
d.text((16, 10), txt, fill=(255, 255, 255), font=f)
return im
def _wrap(draw, text, font, width):
"""Greedy word-wrap to `width` px."""
lines, cur = [], ''
for w in text.split():
t = f'{cur} {w}'.strip()
if draw.textlength(t, font=font) <= width or not cur:
cur = t
else:
lines.append(cur)
cur = w
if cur:
lines.append(cur)
return lines
def overlay(img_arr, masks, phrases, m2p, caption=''):
arr = img_arr.astype(np.float32)
for i, m in enumerate(masks):
if not m.any():
continue
c = np.array(PALETTE[m2p[i] % len(PALETTE)], np.float32)
arr[m] = (1 - OVERLAY_ALPHA) * arr[m] + OVERLAY_ALPHA * c
for i, m in enumerate(masks):
if m.any():
arr[outline(m)] = PALETTE[m2p[i] % len(PALETTE)]
im = Image.fromarray(arr.astype(np.uint8))
f, fc = _font(14), _font(15)
probe = ImageDraw.Draw(im)
# caption block: the [SEG] markers are noise on a picture that already shows the masks
cap = re.sub(r'\s*\[SEG\]', '', caption).replace('<p>', '').replace('</p>', '')
cap = re.sub(r'\s+', ' ', cap).strip()
cap_lines = _wrap(probe, cap, fc, im.width - 24) if cap else []
cap_h = (len(cap_lines) * 20 + 16) if cap_lines else 0
lh = 20
strip_h = cap_h + max(lh * len(phrases) + 12, 24)
strip = Image.new('RGB', (im.width, strip_h), (255, 255, 255))
sd = ImageDraw.Draw(strip)
y = 8
for ln in cap_lines:
sd.text((12, y), ln, fill=(0, 0, 0), font=fc)
y += 20
if cap_lines:
y += 8
sd.line([12, y - 5, im.width - 12, y - 5], fill=(210, 210, 210))
for pi, ph in enumerate(phrases):
sd.rectangle([6, y + 3, 20, y + 15], fill=PALETTE[pi % len(PALETTE)], outline=(0, 0, 0))
n = m2p.count(pi)
sd.text((28, y), f'{pi:02d} {ph.strip()}' + (f' ({n} masks)' if n > 1 else ''),
fill=(0, 0, 0), font=f)
y += lh
out = Image.new('RGB', (im.width, im.height + strip_h), (255, 255, 255))
out.paste(im, (0, 0))
out.paste(strip, (0, im.height))
return out
def slug(s, n=40):
s = re.sub(r'[^a-z0-9]+', '_', s.strip().lower()).strip('_')
return (s[:n] or 'phrase')
def run(image_path, pred_dir):
stem = os.path.splitext(os.path.basename(image_path))[0]
jf, mf = os.path.join(pred_dir, f'{stem}.json'), os.path.join(pred_dir, f'{stem}_masks.npz')
if not (os.path.isfile(jf) and os.path.isfile(mf)):
miss = [os.path.basename(f) for f in (jf, mf) if not os.path.isfile(f)]
print(f' skip {stem}: missing {", ".join(miss)} in {pred_dir} — run predict_panocaps.py '
f'with --out {pred_dir} first')
return
d = json.load(open(jf))
masks = np.load(mf)['masks'].astype(bool)
phrases = d['phrases']
img = np.asarray(Image.open(image_path).convert('RGB'))
if masks.size and masks.shape[1:] != img.shape[:2]:
print(f' !! {stem}: masks are {masks.shape[1:]} but the image is {img.shape[:2]}')
return
m2p = phrase_of_mask(d.get('masks_per_phrase'), len(masks))
outdir = os.path.join(pred_dir, f'{stem}_viz')
os.makedirs(outdir, exist_ok=True)
# The caption, next to the panels, in both forms:
# caption.txt — verbatim, with the <p>...</p>[SEG] markers. This is the alignment
# record: the i-th <p>...</p> is the i-th phrase, hence panel 00, 01, ...
# caption_clean.txt — the markers stripped, i.e. the prose on its own, for reading or for
# feeding somewhere that does not want the markup.
raw = d['caption']
clean = re.sub(r'\s*\[SEG\]', '', raw).replace('<p>', '').replace('</p>', '')
clean = re.sub(r'\s+', ' ', clean)
# the model writes "<p> red mug </p> [SEG]." — the spaces live INSIDE the tags, so removing them
# strands a space before the punctuation ("a red mug ."). Close it up.
clean = re.sub(r'\s+([,.;:!?%)\]])', r'\1', clean)
clean = re.sub(r'([(\[])\s+', r'\1', clean).strip()
open(os.path.join(outdir, 'caption.txt'), 'w').write(raw.strip() + '\n')
open(os.path.join(outdir, 'caption_clean.txt'), 'w').write(clean + '\n')
for pi, ph in enumerate(phrases):
mine = [masks[i] for i in range(len(masks)) if m2p[i] == pi]
if not mine:
continue
panel(img, mine, PALETTE[pi % len(PALETTE)], f'{pi:02d} {ph.strip()}').save(
os.path.join(outdir, f'{pi:02d}_{slug(ph)}.png'))
overlay(img, masks, phrases, m2p).save(os.path.join(outdir, 'overlay.png'))
print(f'{stem}: {len(masks)} mask(s), {len(phrases)} phrase(s) -> {outdir}/ '
f'({len(phrases)} panels + overlay.png + caption.txt/caption_clean.txt)')
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--image', required=True, help='the image file, or the directory you predicted on')
ap.add_argument('--pred', default='./out',
help='the --out DIRECTORY predict_panocaps.py wrote (not a file)')
a = ap.parse_args()
if os.path.isfile(a.pred): # easy slip: passing <stem>_masks.npz
a.pred = os.path.dirname(os.path.abspath(a.pred))
print(f'--pred is a file; using its directory: {a.pred}')
elif not os.path.isdir(a.pred):
raise SystemExit(f'--pred: no such directory: {a.pred}')
imgs = ([os.path.join(a.image, f) for f in sorted(os.listdir(a.image))
if f.lower().endswith(('.jpg', '.jpeg', '.png', '.webp', '.bmp'))]
if os.path.isdir(a.image) else [a.image])
for p in imgs:
run(p, a.pred)
if __name__ == '__main__':
main()
|