How a model reads a picture

Ask Janus-Pro-1B about photos and text cards, see the 576 image tokens your picture becomes, and check how sure the model was of every word, right or wrong.

AdvancedExplained in How a model reads a picture

Janus-Pro-1B, from DeepSeek, reads a picture by turning it into 576 vectors, one for each 16 by 16 square of a 384 by 384 version of it, and putting them in front of your question as if they were words. The lesson explains each step.

How a model reads a picture

Janus-Pro turns the picture into 576 image tokens, puts them in front of your question, and answers one token at a time.

Photos and text cards

Your photos and camera frames are processed on this device and never uploaded.

Recorded from Janus-Pro-1B

Loading the model's recorded outputs for every listed question about the eight photos and four text cards.

About the download

Ask your own questions about the examples, your photos, or the camera. This uses Janus-Pro-1B, a model from DeepSeek (2025) that can both describe and make pictures, built on a language model of about 1.5 billion parameters. To read a picture it turns it into 576 image tokens with a SigLIP vision encoder.

The first time, your browser downloads about 1.6 GB of model files from Hugging Face, plus a 27 MB runtime from this site. Both are cached for later visits. Everything runs on your device; no photo, camera frame, or question you type is sent anywhere.

Manage downloads

Loading the recorded results

What the vision encoder sees: the long side shrunk to 384 pixels, gray padding to a square, and a 24 by 24 grid of 16-pixel squares, one image token each.

Answer

Pick a picture and a question.

Questions (recorded)
Image tokensn/a
Tokens read before answeringn/a
Reading the picturen/a
Each answer tokenn/a

Challenges

0 of 3 complete

  1. Find a picture where at least 4 in 10 of the 576 image tokens see nothing but gray padding. (not yet)
  2. Find a wrong detail the model stated with more than 50% probability, and tap it. (not yet)
  3. Ask about the text on a text card and about the writing in a photo, and compare. (not yet)

Try this

  • Pick the woodblock print and turn on the token grid. How many of the 576 tokens see any of the print?
  • Ask what is written on the motorcycle. Tap each word of the answer and read its probability.
  • Read the receipt, then the small print. Which one comes back word for word?
  • Ask how many umbrellas were sold in March, and check the bar yourself.
  • With the model downloaded, ask your own question, or use your own photo or the camera. Nothing leaves your device.
From a photo to the language model's input, in NumPyPython with NumPy
import math

import numpy as np

SIZE, PATCH = 384, 16  # SigLIP-L/16 at 384 x 384 pixels

# 1. The picture: long side to 384, gray padding to a square, 16 x 16 squares.
def image_tokens(width, height):
    scale = SIZE / max(width, height)
    w, h = int(width * scale), int(height * scale)
    x0, y0 = (SIZE - w) // 2, (SIZE - h) // 2
    grid = SIZE // PATCH
    padding = 0
    for r in range(grid):
        for c in range(grid):
            x, y = c * PATCH, r * PATCH
            if not (x < x0 + w and x + PATCH > x0 and y < y0 + h and y + PATCH > y0):
                padding += 1
    return grid * grid, padding

for name, (w, h) in {"phone photo": (4032, 3024), "square": (500, 500),
                     "tall print": (466, 1024), "huge panorama": (12000, 2000)}.items():
    total, pad = image_tokens(w, h)
    print(f"{name:13s} {w:5d} x {h:4d} -> {total} image tokens, {pad} only padding")

# 2. The vision encoder gives one 1,024-number vector per square; a small
#    two-layer network (the adaptor) maps each into the language model's
#    2,048-number embedding space. Random weights here, real shapes.
rng = np.random.default_rng(0)
features = rng.normal(size=(576, 1024))
W1, W2 = rng.normal(size=(1024, 2048)) * 0.03, rng.normal(size=(2048, 2048)) * 0.02
erf = np.vectorize(math.erf)
gelu = lambda x: 0.5 * x * (1 + erf(x / math.sqrt(2)))
image_embeds = gelu(features @ W1) @ W2
print("image embeddings:", image_embeds.shape)

# 3. The language model's input: text tokens, the picture's 576 vectors
#    between two marker tokens, then the question, all in one sequence.
system, question = 46, 8  # token counts (typical for the lab)
sequence = ["text"] * system + ["<begin_of_image>"] + ["image"] * 576 \
    + ["<end_of_image>"] + ["text"] * question
print("tokens read before answering:", len(sequence))
print("share that comes from the picture:", f"{578 / len(sequence):.0%}")
phone photo    4032 x 3024 -> 576 image tokens, 144 only padding
square          500 x  500 -> 576 image tokens, 0 only padding
tall print      466 x 1024 -> 576 image tokens, 288 only padding
huge panorama 12000 x 2000 -> 576 image tokens, 480 only padding
image embeddings: (576, 2048)
tokens read before answering: 632
share that comes from the picture: 91%

Sample photos, all CC0 on Wikimedia Commons: Dog by Joselodos; Cat by Vyshnavi medicharla; Cat on a motorcycle by CAPTAIN RAJU; Breakfast by Andy Li; Street by Mucklagh; Cafe by Andy Li; Corridor by Sindugab; Woodblock print by Utagawa Hiroshige (Metropolitan Museum of Art).

Try "embedding", "softmax", "overfitting", or "backpropagation".