Downloads · 30 days
15
100% of all-time downloads
jennis0/scene-atlas-medium
scene-atlas-medium is a zero-shot image classification model from jennis0. Use it for the zero-shot image classification task on the model card, and read the license before you ship it in a product. It is set up for transformers. The card lists the license as apache-2.0.
An image–text embedding model for searching tabletop RPG art: battlemaps, scene and environment illustrations, portraits, tokens, items and maps. It is the default model of Scene Atlas, a tool that helps Game Masters…
Downloads · 30 days
15
100% of all-time downloads
All-time downloads
15
Public
Parameters
375M
1.4 GB on disk
Likes
0
Public
Click a slice to open those files.
.safetensors751 MB · 81%
From the Hugging Face model README
An image–text embedding model for searching tabletop RPG art: battlemaps, scene and environment illustrations, portraits, tokens, items and maps. It is the default model of Scene Atlas, a tool that helps Game Masters organise and search their art libraries.
It is google/siglip2-base-patch16-384 fine-tuned on about 121,000 captioned TTRPG images. On a benchmark of Game Master search queries it scores nDCG@10 0.607, against 0.540 for SigLIP2 base-384 (zero-shot) and 0.505 for SigLIP2 base-224 (zero-shot, previous Scene Atlas medium).
This repo holds the same model in two formats:
| Variant | Location | Format | Size | nDCG@10 |
|---|---|---|---|---|
| Full | repo root | transformers checkpoint, fp16 safetensors | 716 MB | 0.607 |
| Quantised | onnx/ | ONNX: 4-bit weights, fp16 activations | 154 MB | 0.604 (-0.4%) |
Use the quantised variant to run the model: it is what Scene Atlas downloads, it needs about 235 MB of memory, and it runs on CPU, CUDA and DirectML with ONNX Runtime 1.24 or later. Use the full variant to fine-tune further, or to work in PyTorch.
The benchmark has 664 search queries written the way a Game Master would type them, such as "medieval village", "burning airship" or "frozen tundra under the northern lights". Each query searches a gallery of 8,190 held-out test images, restricted to the query's art category. Test images were held out of training together with their known variants. A Claude Haiku judge graded the relevance of every image that any model below ranked in its top 10 for a query; only clear matches count as relevant.
| Model | All | Tactical maps | Environments | Maps | Portraits | Tokens | Items |
|---|---|---|---|---|---|---|---|
| SigLIP2 base-384 (zero-shot) | 0.540 | 0.420 | 0.584 | 0.552 | 0.558 | 0.640 | 0.815 |
| SigLIP2 base-224 (zero-shot, previous Scene Atlas medium) | 0.505 | 0.366 | 0.554 | 0.502 | 0.522 | 0.640 | 0.807 |
| This model (full) | 0.607 | 0.518 | 0.648 | 0.679 | 0.597 | 0.642 | 0.818 |
| This model (quantised) | 0.604 | 0.520 | 0.645 | 0.659 | 0.601 | 0.627 | 0.817 |
The quantised variant was scored through ONNX Runtime with inputs fed exactly as Scene Atlas feeds them. Its difference from the full model is -0.43% (95% CI -1.58% to +0.71%, paired bootstrap over queries).
import json
import numpy as np
import onnxruntime as ort
from huggingface_hub import snapshot_download
from PIL import Image
from tokenizers import Tokenizer
d = snapshot_download("jennis0/scene-atlas-medium", allow_patterns=["onnx/*"]) + "/onnx"
m = json.load(open(f"{d}/manifest.json"))
vision = ort.InferenceSession(f"{d}/{m['vision_encoder']}")
text = ort.InferenceSession(f"{d}/{m['text_encoder']}")
tok = Tokenizer.from_file(f"{d}/tokenizer/tokenizer.json") # lowercases for you
tok.enable_truncation(max_length=m["tokenizer_max_length"])
tok.enable_padding(length=m["tokenizer_max_length"], pad_id=m["tokenizer_pad_id"])
ids = np.array([tok.encode("ruined dwarven forge with lava channels").ids], dtype=np.int64)
text_emb = text.run(["embeddings"], {"input_ids": ids, "attention_mask": np.ones_like(ids)})[0]
size = m["image_size"] # plain resize to a square, no crop
img = Image.open("map.jpg").convert("RGB").resize((size, size), Image.BILINEAR)
x = ((np.asarray(img, np.float32) / 255 - 0.5) / 0.5).transpose(2, 0, 1)[None].astype(np.float16)
image_emb = vision.run(["embeddings"], {"pixel_values": x})[0]
score = float(text_emb[0] @ image_emb[0]) # both outputs are L2-normalised
import torch
import torch.nn.functional as F
from PIL import Image
from transformers import AutoModel, AutoProcessor
model = AutoModel.from_pretrained("jennis0/scene-atlas-medium").eval()
processor = AutoProcessor.from_pretrained("jennis0/scene-atlas-medium")
# Pad text to 64 tokens and pass no attention mask: SigLIP2 is trained that way.
text = processor(text=["ruined dwarven forge with lava channels"],
padding="max_length", max_length=64, return_tensors="pt")
image = processor(images=[Image.open("map.jpg")], return_tensors="pt")
with torch.no_grad():
t = model.get_text_features(input_ids=text["input_ids"])
i = model.get_image_features(pixel_values=image["pixel_values"])
t, i = (F.normalize(getattr(x, "pooler_output", x), dim=-1) for x in (t, i))
score = (t @ i.T).item()
Two things that break SigLIP2 text embeddings:
attention_mask input, so callers that pass one are safe.The ONNX variant is about 79% smaller than the fp16 checkpoint:
Spec: vocab=wiki32k,table=int8,text=w4b16ag,vision=w4b32ag, built with the onnx-export compress command of the Scene Atlas training repo.
training/configs/siglip2_base_384_rc4.yaml.Apache 2.0, inherited from google/siglip2-base-patch16-384.