Skip to content
thedevmarkPublic

About

Put a film look on digital photos — color, grain, and halation. Self-hosted Python + Flask.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

39 Commits

Folders and files

Repository files navigation

Film Lab

Film Lab

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.

Run it

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.py

Open http://localhost:3100.

RAW support (.arw, .cr2, .cr3, .nef, .raf, .dng, …) comes from rawpy and is optional — JPEG/PNG/TIFF work without it.

What's in the box

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.

Where the color comes from

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.

Files

  • film.py — pipeline order, params, presets, routes
  • filmlab/ — loader (linear + scene/display state), tone (transfer functions, rolloff), lut (HaldCLUT + tetrahedral interpolation), effects (halation, grain), batch (folder-in/folder-out worker), blur
  • app.py — Flask host, port discovery, state paths
  • static/ — single-page UI
  • luts/ — HaldCLUT PNGs; private/ is gitignored
  • tools/make_hald.py — writes an identity HaldCLUT to render through an editor
  • tools/make_icon.py — regenerates the icon from static/img/app-icon.svg
  • tests/ — python -m unittest discover -s tests

Prior art worth reading


deutschmark's other apps

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

License

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.

About

Put a film look on digital photos — color, grain, and halation. Self-hosted Python + Flask.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages