A codec you can import.

Lossy and lossless image compression in pure Python, running entirely on your own machine. And a second thing worth more than the codec: train your models on the compressed representation instead of on pixels, and the steps run about nine times faster.

Install

Three dependencies. Both trained models ship inside the package.

Terminal
pip install git+https://github.com/Mubby03/json-camera
It runs on your machine, not ours

There is no service to call, no API key, no account and no telemetry. The package is torch, numpy and pillow, with no network code anywhere in it — verified by running a full encode and decode with every socket blocked. Your images never leave your computer and it works with the wifi off.

Basic use
import jsoncam

doc = jsoncam.encode("photo.jpg")          # learned codec, about 60x
jsoncam.decode(doc, "restored.png")

doc = jsoncam.encode_lossless("photo.png") # bit exact, keeps alpha
jsoncam.decode(doc, "exact.png")           # format detected for you

Training on latents instead of pixels

The part worth stealing, and the part with a real caveat attached.

A network does not have to see pixels. The codec's latent grid is 128×14×14 where the image was 3×224×224 — six times fewer values — so every layer downstream of the input does less work. You convert the dataset once and train on it forever after.

Convert once, then train as usual
import jsoncam
from torch.utils.data import DataLoader

jsoncam.prepare_dataset("photos/", "train.jcl", size=224)  # once, ~27ms/image
ds = jsoncam.LatentDataset("train.jcl")                    # yields (latent, label)
dl = DataLoader(ds, batch_size=64, num_workers=4, shuffle=True)

Throughput and storage, measured

PipelineInput tensorValuesThroughputDisk / image
Pixels3 × 224 × 224150,52850 img/s20.1 KB
Latents128 × 14 × 1425,088462 img/s3.1 KB
Difference6.0× fewer9.2× faster6.5× smaller
Same architecture, same batch size, same machine. Reproduce with scripts/benchmark_latents.py.

But does the model actually learn? Cats vs dogs, 300 each

ApproachTest accuracyTimeVerdict
CNN from scratch, on pixels57.5%273 sat chance
CNN from scratch, on latents52.5%179 sat chance, but faster
Pretrained ResNet18, frozen92.5%87 swins outright
Chance is 50%. On a set this small, both from-scratch runs learn nothing, so 9× faster is 9× faster at learning nothing — and a latent cannot be fed to a pretrained backbone. The full notebook is in the repo.
Where this leaves you

Compressed-domain training is worth it when you were going to train from scratch on a large dataset anyway. If transfer learning is an option for your task, it will almost certainly beat this, and it cannot be combined with it: a pretrained backbone expects 3 channels at 224×224 and a latent is 128 at 14×14.

Working with a coding agent

Paste one of these into Claude Code, Cursor or whatever you use. Each one points the agent at the machine-readable docs first, so it learns the caveats before it writes anything.

Two files exist for this

AGENTS.md is the full brief: the API, the measured numbers with their conditions, and a table of requests this library is the wrong answer to. llms.txt is the short version. Both are served from the site and live in the repo, so an agent can fetch either without cloning.

1 — Should I even use this?
Read https://json-camera.fly.dev/AGENTS.md, then tell me honestly
whether json-camera is the right tool for what I am building. Work
through its "Should you use it at all" section against my actual
project. If WebP, AVIF or transfer learning would serve me better,
say so plainly and explain why rather than finding a way to use it.
Do not write any code yet.
2 — Lossless archival in a pipeline
Read https://json-camera.fly.dev/AGENTS.md. Add json-camera lossless
compression to my archival pipeline. Requirements: verify every round
trip with np.array_equal and fail loudly if it ever differs, since a
lossless codec that is quietly lossy is worse than none. Preserve the
alpha channel and the original filename. Warn me if any input is
16-bit or CMYK, because those are silently converted. Compare the
output size against PNG and WebP lossless and report all three.
3 — Compressed-domain training
Read https://json-camera.fly.dev/AGENTS.md, especially the five
conditions for compressed-domain training. First check my project
against all five and tell me if any fail. If they all hold: convert my
dataset with jsoncam.prepare_dataset, swap my DataLoader to
jsoncam.LatentDataset, and adapt only the input stem of my model to
take the latent shape. Then benchmark BOTH accuracy and throughput
against the pixel pipeline, because accuracy on latents is unmeasured
and speed alone does not tell me whether this was a good idea.
4 — Build-time placeholders
Read https://json-camera.fly.dev/AGENTS.md. I want low-quality image
placeholders generated at build time. Note the constraint up front:
browsers cannot decode json-camera files, so everything must be
decoded during the build and emitted as ordinary images or data URIs,
never fetched and decoded client side. Build a script that walks my
image directory, produces a tiny placeholder for each, reports the
byte size, and compares it against just using a small WebP. If WebP
wins, tell me and stop.
5 — Build on the internals
Read https://json-camera.fly.dev/AGENTS.md. I want to use
json-camera's components on their own rather than the codec as a
whole: jsoncam.rans for entropy coding my own symbol stream,
jsoncam.entropy for a learned prior, or jsoncam.lossless for the
reversible colour transform and MED predictor. Show me each one
working standalone with a round-trip assertion, then help me wire the
one I need into my own model. JSONCamera is an ordinary nn.Module, so
subclassing or replacing it is fine.

Should you use it?

Five questions. If any answer is no, the honest advice is don't.

Decision checklist

ConditionWhy it matters
Training from scratch No pretrained backbone fits a 128-channel 14×14 input. If you can fine-tune an existing model, do that instead.
Large dataset Conversion costs about 27 ms an image, paid once. Break-even is ~1.4 epochs, so the conversion is cheap — but on 600 images the whole question is moot.
Disk or I/O hurts 6× less data is real money on cloud storage and real time on a network-mounted dataset. If your data already fits in RAM, you gain little.
Task tolerates a 16× downsample Classification, probably fine. Segmentation, detection, OCR, anything needing fine spatial precision — probably not.
You can live without augmentation The finest crop is 16 px, colour jitter is impossible, and even a horizontal flip is not exact in latent space. On small datasets, losing augmentation costs more than 9× speed is worth.
Accuracy on your task is unmeasured by us. Validate it before you commit.

Other things it is good for

Places where a small file matters more than a perfect one.

Placeholders and blur-ups

Ship a tiny version inline in the HTML that renders instantly while the real image loads. At very low rates the codec produces a soft but structurally correct picture, which is exactly what a placeholder wants.

Avatars and profile pictures

Small, numerous, and nobody inspects them at 100%. A few kilobytes each across a large user table adds up, and the quality floor is low.

Thumbnail grids

Catalogue and gallery views load dozens of images at once. The bytes on the wire dominate the experience, not the fidelity of any single tile.

Dataset archival

Cold storage for image sets you may want again. Lossless mode keeps them bit exact; lossy keeps them at a fraction of the size if approximate is enough.

Embedding images in JSON APIs

The payload is already text, so it drops into a JSON response, a config file, a database text column or a git repository without a separate binary asset or base64 wrapper.

Teaching and demos

The whole pipeline is readable Python: a range coder, an entropy model, a predictor. Every component imports and runs on its own, which is hard to find in a production codec.

A caution on the web cases

A browser cannot decode these files. The model weights are the format, so decoding needs Python and the checkpoint — which means a server round trip, or decoding ahead of time. For placeholders that is fine, since you generate them during a build. For anything decoded live in a browser, use WebP or AVIF instead. Saying so is more useful than selling you a dead end.

The surface

Plain Python. No framework, no registry, no plugin system.

jsoncam.encode(img)
Learned codec. Path or PIL image in, container dict out. Pass out= to write it. Roughly 60× smaller than raw, and beats JPEG at a matched file size on the default model.
jsoncam.encode_lossless(img)
Bit exact, no network involved, keeps the alpha channel. About 20% under PNG on the bitstream. Needs no checkpoint, so the file is self contained.
jsoncam.decode(doc)
Rebuilds either format — it reads which one it is, so callers never have to care.
jsoncam.prepare_dataset(dir, out)
Encodes a folder into one shard. ImageFolder layout works unchanged; subdirectories become integer labels.
jsoncam.LatentDataset(shard)
A real torch.utils.data.Dataset, so Subset, ConcatDataset, samplers and transform= all apply as normal.
jsoncam.psnr / ms_ssim
The two quality metrics, usable on their own against any pair of images.

Everything is importable on its own

ComponentImportWhat it is
Range coderjsoncam.ransVectorised interleaved rANS, 512 lanes. Works on any symbol stream.
Entropy modeljsoncam.entropyFactorised prior with monotonic per-channel CDFs.
Networksjsoncam.modelJSONCamera is an ordinary nn.Module. Subclass it or replace it.
Colour transformjsoncam.losslessYCoCg-R, reversible in integers, plus the MED predictor.
Metricsjsoncam.metricsMS-SSIM with a separable Gaussian.
codec.encode_image(your_model, img) takes any model of the right shape. The file's fingerprint follows your weights automatically.

What it does not do

Stated here rather than discovered by you later.

The weights are the format

A file is only decodable by the exact checkpoint that wrote it. Every file carries a fingerprint and decoding refuses on a mismatch. Retrain the codec and old files stop opening.

No browser decoding

Decoding needs Python and torch. There is no JavaScript build, so anything client-side needs a server round trip or a build step.

CPU bound

About 1.3 s to encode and 1.7 s to decode a 3 megapixel image on one core. Fine for batch work, too slow for a request path without care.

Lossy drops transparency

The network has three input channels. The alpha channel is discarded and the header records it. Use lossless mode, which codes alpha as a fourth plane.

Silent conversions

Greyscale becomes three channels, CMYK becomes RGB, and 16-bit is narrowed to 8-bit without comment. Know this before feeding it scientific imagery.

JSON costs 25%

Text carries about 6.1 bits per character in base85, so the file is a quarter larger than the bitstream. On lossless that cancels the win against PNG entirely.

Have a look at the code

Roughly 4,000 lines of Python and 31 tests, all of it readable.

GitHub Try the codec first