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
Download visualize.py from Panorama-grounding/PANORAMA-4B: direct link, hf CLI and curl.
- Browser
- Download file 10.5 kB
-
https://huggingface.co/Panorama-grounding/PANORAMA-4B/resolve/main/visualize.py
- Command line
-
hf download hf://Panorama-grounding/PANORAMA-4B/visualize.py
-
curl -L -o visualize.py https://huggingface.co/Panorama-grounding/PANORAMA-4B/resolve/main/visualize.py
10.5 kB
| #!/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() | |