Downloads · 30 days
16
100% of all-time downloads
jennis0/scene-atlas-small
scene-atlas-small 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 mit.
An image–text embedding model for searching tabletop RPG art: battlemaps, scene and environment illustrations, portraits, tokens, items and maps. It is the lightweight model of Scene Atlas, a tool that helps Game Mast…
Downloads · 30 days
16
100% of all-time downloads
All-time downloads
16
Public
Parameters
151M
674 MB on disk
Likes
0
Public
Click a slice to open those files.
.safetensors303 MB · 74%
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 lightweight model of Scene Atlas, a tool that helps Game Masters organise and search their art libraries, and its default on computers without a GPU.
It is laion/CLIP-ViT-B-32-laion2B-s34B-b79K fine-tuned on about 121,000 captioned TTRPG images. On a benchmark of Game Master search queries it scores nDCG@10 0.470, against 0.376 for CLIP ViT-B/32 LAION-2B (zero-shot, previous Scene Atlas small).
This repo holds the same model in two formats:
| Variant | Location | Format | Size | nDCG@10 |
|---|---|---|---|---|
| Full | repo root | transformers checkpoint (converted from OpenCLIP), fp16 safetensors | 289 MB | 0.470 |
| Quantised | onnx/ | ONNX: 4-bit weights, fp16 activations | 99 MB | 0.468 (-0.6%) |
Use the quantised variant to run the model: it is what Scene Atlas downloads, it needs about 160 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 |
|---|---|---|---|---|---|---|---|
| CLIP ViT-B/32 LAION-2B (zero-shot, previous Scene Atlas small) | 0.376 | 0.249 | 0.466 | 0.424 | 0.357 | 0.389 | 0.677 |
| This model (full) | 0.470 | 0.395 | 0.514 | 0.481 | 0.416 | 0.541 | 0.707 |
| This model (quantised) | 0.468 | 0.402 | 0.510 | 0.484 | 0.401 | 0.519 | 0.710 |
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.59% (95% CI -2.05% to +0.85%, 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-small", 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") # short side to `size` (bicubic), then centre crop
w, h = img.size
img = img.resize((max(size, round(w * size / h)), max(size, round(h * size / w))), Image.BICUBIC)
left, top = (img.width - size) // 2, (img.height - size) // 2
img = img.crop((left, top, left + size, top + size))
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-small").eval()
processor = AutoProcessor.from_pretrained("jennis0/scene-atlas-small")
text = processor(text=["ruined dwarven forge with lava channels"], padding=True, return_tensors="pt")
image = processor(images=[Image.open("map.jpg")], return_tensors="pt")
with torch.no_grad():
t = model.get_text_features(**text)
i = model.get_image_features(**image)
t, i = (F.normalize(getattr(x, "pooler_output", x), dim=-1) for x in (t, i))
score = (t @ i.T).item()
The ONNX variant is about 66% smaller than the fp16 checkpoint:
Spec: vocab=clip_wiki16k,table=int8,text=w4b16ao,vision=w4b16ao,ncal=1024, built with the onnx-export compress command of the Scene Atlas training repo.
training/configs/openclip_vitb32_rc3.yaml.MIT, inherited from laion/CLIP-ViT-B-32-laion2B-s34B-b79K.