Downloads · 30 days
1
100% of all-time downloads
jennis0/scene-atlas-large
scene-atlas-large 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 high-accuracy model of Scene Atlas, a tool that helps Game Ma…
Downloads · 30 days
1
100% of all-time downloads
All-time downloads
1
Public
Parameters
882M
4.8 GB on disk
Likes
0
Public
Click a slice to open those files.
.safetensors1.8 GB · 71%
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 high-accuracy model of Scene Atlas, a tool that helps Game Masters organise and search their art libraries, for computers with a GPU.
It is google/siglip2-large-patch16-384 fine-tuned on about 123,000 captioned TTRPG images. On a benchmark of Game Master search queries it scores nDCG@10 0.640, against 0.585 for SigLIP2 L-384 (zero-shot) and 0.589 for SigLIP2 so400m (zero-shot, previous Scene Atlas large).
This repo holds the same model in two formats:
| Variant | Location | Format | Size | nDCG@10 |
|---|---|---|---|---|
| Full | repo root | transformers checkpoint, fp16 safetensors | 1682 MB | 0.640 |
| Quantised | onnx/ | ONNX: 8-bit weights, fp16 activations | 680 MB | 0.640 (-0.1%) |
Use the quantised variant to run the model: it is what Scene Atlas downloads, it needs about 760 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 L-384 (zero-shot) | 0.585 | 0.483 | 0.640 | 0.596 | 0.594 | 0.666 | 0.760 |
| SigLIP2 so400m (zero-shot, previous Scene Atlas large) | 0.589 | 0.514 | 0.625 | 0.556 | 0.602 | 0.630 | 0.785 |
| This model (full) | 0.640 | 0.550 | 0.699 | 0.698 | 0.633 | 0.669 | 0.802 |
| This model (quantised) | 0.640 | 0.549 | 0.699 | 0.699 | 0.634 | 0.670 | 0.800 |
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.09% (95% CI -0.30% to +0.13%, 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-large", 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"]
img = Image.open("map.jpg").convert("RGB").resize((size, size), Image.BILINEAR) # plain resize, no crop
mean, std = np.array(m["image_mean"], np.float32), np.array(m["image_std"], np.float32)
x = ((np.asarray(img, np.float32) / 255 - mean) / std).transpose(2, 0, 1)[None] # float32 in, float32 out
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-large").eval()
processor = AutoProcessor.from_pretrained("jennis0/scene-atlas-large")
# 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 60% smaller than the fp16 checkpoint:
GPTQ was calibrated on captions and 256 thumbnails of training images only. Activations stay fp16.
Spec: vocab=wiki32k,table=int8b32,text=w8,vision=w8b32g, built with the onnx-export compress command of the Scene Atlas training repo.
training/configs/siglip2_l_384_rc4.yaml.Apache 2.0, inherited from google/siglip2-large-patch16-384.