A small, self-hosted tool for putting a film look on digital photographs.
Python + Flask + numpy. Runs in your browser, on your machine, on your files.
Drop in a JPEG or a RAW, pick a preset, get a rendered photo back. The pipeline models the parts of film that actually make it look like film: a color response, grain, and halation — the red glow that blooms around highlights when light punches through the emulsion and reflects off the film base.
Windows: grab film-lab.exe from the
latest release and
double-click it — no install, it opens as a standalone app in its own window.
Your own LUTs go in a luts/private/ folder next to the exe.
From source:
pip install -r requirements.txt
python app.pyOpen http://localhost:3100.
RAW support (.arw, .cr2, .cr3, .nef, .raf, .dng, …) comes from
rawpy and is optional — JPEG/PNG/TIFF work without it.
| Control | What it does |
|---|---|
| Grade strength | How far to blend in the film LUT |
| Exposure | EV stops — a multiply in linear light, so hue is preserved |
| Contrast | S-curve around mid-gray, applied after the LUT |
| Grain | Intensity, and size as a fraction of the short edge |
| Halation | Intensity, and radius as a fraction of the long edge |
Sizes are fractions rather than pixel counts so the look is the same on a downscaled preview and on a full-resolution export.
Presets ship as built-ins and can't be overwritten. Anything you dial in
yourself can be saved alongside them, and lives at
%LOCALAPPDATA%\film-lab\film_presets.json on Windows or ./.appstate/ elsewhere.
Batch a folder applies the look you've dialled in to every photo in a directory — folder in, folder out. It processes one file at a time and skips anything already rendered, so you can stop and pick up where you left off, and outputs are written atomically so an interrupted run never leaves a truncated file behind.
From a 3D LUT — a measured mapping from every input color to an output
color, applied with tetrahedral interpolation. It is the same thing DxO FilmPack
and darktable's lut3d module do. There is no hand-tuned color math in the
pipeline, by design: a guess at what film does is a guess, however pleasant.
The pipeline, in order:
load -> (linear, scene|display)
exposure linear x 2**EV
[if scene] grey-point scale — neutral, no look
halation linear, additive, red-dominant
highlights scene: soft shoulder display: hue-preserving clip
srgb encode + clip to [0,1]
LUT tetrahedral, strength blend
contrast post-LUT user finish, 0 in every preset
grain last — texture on a finished frame
-> JPEG
The highlight stage is the one place the scene/display tag changes the
render, and the two states need opposite things. A RAW is scene-linear, with real
headroom above 1.0 that nothing has rendered yet — it needs a soft shoulder, and
that shoulder is the scene-to-display render. A JPEG has already been rendered
by the camera: at EV=0 nothing in it exceeds 1.0, so the correct transform is the
identity, and there is no gentler option — any monotonic map that is the
identity on [0,1] and lands in [0,1] is min(x, 1). Only an exposure push can
send a JPEG over 1.0, and those values come back down by scaling the whole
triplet rather than clipping each channel, so hue survives. A shoulder here would
crush white to grey and stack a second tone curve in front of the LUT.
The grey-point scale runs before halation because halation's threshold and the shoulder's knee are both absolute linear values: they have to see one exposure.
Contrast sits after the LUT because the CLUTs were authored against a neutral, standard-contrast render; look-contrast in front of them would be counted twice.
Grain is seeded from a hash of the input file, so a photo renders identically
every time while no two photos in a shoot share a noise field. An explicit
non-zero seed overrides that.
Kodak Gold 200 ships with the repo (luts/open/), baked from
spektrafilm — a spectral
photochemical simulation built from measured datasheet density curves, not a
hand-tuned approximation. So it works out of the box.
You can add your own to luts/private/, which is gitignored — see
docs/extracting-a-lut.md. This repo will not
redistribute profiles extracted from commercial software. If you own a licensed
copy of an editor, extracting a LUT from it for your own use is your business;
publishing that LUT is not something this project will do for you.
film.py— pipeline order, params, presets, routesfilmlab/—loader(linear + scene/display state),tone(transfer functions, rolloff),lut(HaldCLUT + tetrahedral interpolation),effects(halation, grain),batch(folder-in/folder-out worker),blurapp.py— Flask host, port discovery, state pathsstatic/— single-page UIluts/— HaldCLUT PNGs;private/is gitignoredtools/make_hald.py— writes an identity HaldCLUT to render through an editortools/make_icon.py— regenerates the icon fromstatic/img/app-icon.svgtests/—python -m unittest discover -s tests
- darktable —
lut3dandgrainare the reference implementations - spektrafilm (formerly agx-emulsion) — spectral photochemical film simulation in numpy, built from measured datasheet density curves
- RawTherapee film simulation collection — the large open HaldCLUT set
| Pathos Worker-side job search with source-linked roles, evidence-checked resumes, and application tracking. | |
| Markskill A product-engineering skill for AI agents: trace behavior to its owner, fix root causes, shape interfaces around real tasks, and verify claims with evidence. | |
| Alert! Alert! Turn a video URL or local file into a cropped, trimmed stream alert. | |
| Auto iPhone Uploader Write a video's title and captions once on your PC, then post it from the real apps on your iPhone. Early preview. | |
| Streamer Online Build OBS scenes and browser-source overlays with connected streamer tools. | |
| ForgetMeNot A local-first Twitch bot that remembers regulars, callbacks, and stream lore. |
All projects → github.com/thedevmark
Code is MIT.
One exception: luts/open/kodak_gold_200.png is baked from
spektrafilm's measured film
profile and is CC BY-SA 4.0, not MIT — spektrafilm's
license explicitly treats a LUT as a direct encoding of its profile data, so
share-alike follows it. See luts/open/ATTRIBUTION.md,
and docs/baking-the-default-lut.md to reproduce it.
Photographs you make with it are yours. No copyleft reaches your images.
Any LUTs you add yourself carry their own licenses — check them.
Film stock names are trademarks of their respective owners. Nothing here is affiliated with, endorsed by, or derived from Kodak, Fujifilm, or DxO.
