Skip to main content

postfx

Theme-based image post-processing & film-emulation pipeline — pure Python, CPU, no GPU.

PyPI version Python versions License: MIT CI

postfx gives any image a consistent, reusable look — film stocks, cinematic grades, LUTs — defined entirely in YAML. Pick a theme, apply it to one image or a whole folder, and every output shares the same visual identity. It's a compact color/grain/lens pipeline (white balance, tone, split-toning, halation, grain, vignette, LUTs…) built on numpy + opencv that runs on the CPU — no GPU, no ML, no cloud.

The 15 signature themes applied to one photo
The 15 built-in signature themes, applied to one source image (python -m postfx sheet).

Highlights

  • Theme = YAML. Code and looks are fully decoupled. 30 themes built in (15 film-stock/industry signature looks, 15 experimental), plus .cube LUT support.
  • Pure & deterministic. Every operation is a pure (array, params) -> array function. Same image + theme = byte-identical output (grain seed derives from the filename).
  • Resolution-safe. Aspect ratio and resolution are preserved — no crop/resize. Grain and blur radii scale with image size, so a look is identical at any resolution.
  • Fast on CPU. ~0.5 s @ 2 MP on a single core; batch mode fans out across cores.
  • Three ways in: a postfx CLI, a tiny Python API, and drop-in .cube LUTs.

Install

pip install postfx

or with uv:

uv add postfx          # into a project
uv tool install postfx # as a standalone CLI tool

Quickstart

CLI

# One image or a whole folder (batch = auto-parallel across cores)
postfx run --input photo.jpg --theme portra_400 --condition day_outdoor --out out/

# Preview every signature theme on one image as a labeled grid
postfx sheet --input photo.jpg --out sheet.jpg

# List all themes (grouped by category) and conditions
postfx list

Python

import postfx
from postfx import Theme, Condition

# One-liner: load, process, save. Names are StrEnums — autocomplete + typo-safe.
# (Plain strings like "portra_400" still work everywhere too.)
postfx.process_file("photo.jpg", "graded.jpg",
                    theme=Theme.Signature.CINEMATIC_TEAL_ORANGE,
                    condition=Condition.INDOOR_EVENING, strength=1.0)

# Or work with arrays directly (float32 [0,1] RGB in, same out)
from postfx import load_theme, get_condition, imgio, process

rgb, alpha = imgio.load_image("photo.jpg")
out = process(rgb, load_theme(Theme.Signature.PORTRA_400), get_condition(Condition.DAY_OUTDOOR),
              strength=1.0, seed=imgio.seed_from_path("photo.jpg"))
imgio.save_image("graded.png", out, alpha)

Showcase

One source image (cat.jpg), four different themes — same pipeline, YAML-only difference:

Original vs three postfx themes

Themes

Themes live under postfx/themes/, grouped into categories. Names are unique across categories, so --theme <name> resolves globally.

Category What Count
signature Market-standard, reference-grounded looks — the primary set 15
luts LUT-based themes (a .cube file is a selectable look) 1+
experimental Earlier / exploratory looks 15

Signature (grounded in real film stocks & industry grades)

Theme Reference / character
portra_400 Kodak Portra 400 — creamy warm skin, soft highlights (portrait/wedding standard)
gold_200 Kodak Gold 200 — nostalgic golden-amber, punchy consumer film
pro_400h Fuji Pro 400H — soft airy pastel, minty cyan-green shadows
ektar_100 Kodak Ektar 100 — vivid saturation, deep blues, fine grain
cinestill_800t Cinestill 800T — tungsten teal cast + signature red halation
superia_xtra Fuji Superia X-TRA 400 — punchy, green-leaning shadows
kodachrome_64 Kodachrome 64 — rich reds/blues, hard micro-contrast, vintage slide
tri_x_400 Kodak Tri-X 400 — classic high-contrast B&W film grain
cinematic_teal_orange Hollywood blockbuster grade — teal shadows, orange skin
bleach_bypass Silver-retention — desaturated, high contrast, silvery, gritty
bright_airy Bright & Airy — high-key wedding/lifestyle staple
dark_moody Dark & Moody — rich, earthy, low-key staple
clean_commercial Neutral, crisp, accurate color, polished skin (e-comm/beauty)
golden_warm Golden-hour grade — warm, glowy, sun-kissed skin
mono_noir Cinematic high-contrast B&W noir, deep blacks, cool tone

experimental holds 15 earlier looks. Run postfx list for the full set with descriptions.

LUTs (a LUT is a theme)

A .cube 3D LUT is a selectable look like any other theme. Bundled LUTs live in postfx/themes/luts/ with a small YAML wrapper. To use your own, drop a .cube there with a YAML referencing it — or point any theme's lut.file at an external absolute path. The LUT is applied in display (sRGB) space, with grain/lens/texture on top. See postfx/themes/luts/README.md.

Conditions

A separate axis from the theme. A theme sets the look (color); a condition sets how strongly the capture/optical texture — grain, chroma noise, halation — reads for the situation a shot was taken in, as a multiplier on the theme's own values. So one theme stays consistent across shooting conditions. Select with --condition (default neutral — the theme's texture exactly as authored).

Condition grain chroma noise halation Character
neutral 1.0× 1.0× 1.0× identity / default — theme as authored
day_outdoor 0.2× 0.0× 0.7× clean daylight
overcast 0.4× 0.1× 0.5× soft, diffuse, low bloom
indoor_evening 1.0× 0.6× 1.0× tungsten interior
neon_night 1.4× 0.9× 1.7× neon-lit night, bloom-dominant
night_flash 2.2× 1.5× 1.4× direct flash, grittiest

Conditions live under postfx/conditions/ as YAML — add your own exactly the way you'd add a theme. neon_night deliberately blooms harder than night_flash while staying less grainy: the three multipliers are independent, not one slider.

Global strength

--strength 0.0–1.5 linearly blends the whole theme effect with the original: 0 = untouched, 1 = full theme, 1.5 = over. Additive effects like grain scale with it too.

Processing order (fixed)

1 sRGB→linear · 2 white balance (Bradford) · 3 exposure · 4 highlight rolloff · 5 lift/gamma/gain · 6 split toning · 7 tone curve · 8 vibrance+saturation · 9 halation (linear) · 10 linear→sRGB · 10b optional LUT (.cube) · 11 distortion · 12 chromatic aberration · 13 vignette · 14 black point lift · 15 clarity · 16 grain (luminance-weighted) · 17 sharpen · 18 JPEG re-encode (JPEG output only).

Halation must be in linear space, the LUT right after linear→sRGB, grain in sRGB — this order is deliberate and shouldn't be reordered.

Parameter reference

Each theme YAML carries the op blocks below. Omitted keys fall back to a neutral (no-op) default; an unknown key raises an error (so typos fail fast).

Op Parameters Effect
white_balance temp, tint temp>0 warm (red↑ blue↓); tint>0 magenta, <0 green
exposure stops Linear exposure; +1 = 2× light
highlight_rolloff knee, strength Soft-compress above knee (breaks clipped highlights)
lift_gamma_gain lift[3], gamma[3], gain[3] ASC-CDL: gain=slope, lift=offset, gamma=power
split_toning shadow[3], highlight[3], strength Separate shadow/highlight tint (luma-masked)
tone_curve strength, pivot S-curve contrast around the pivot
vibrance vibrance, saturation, skin_protect Vibrance lifts low-sat pixels & protects skin
halation threshold, radius_frac, tint[3], strength Highlight bloom; linear space, red-shifted, screen blend
lut file, intensity Optional 3D .cube LUT, display-referred
distortion k k<0 barrel, k>0 pincushion
chromatic_aberration amount Lateral CA; scales R/B about center
vignette strength, feather, roundness Radial darkening; feather = start radius
black_point lift (scalar or [3]) Lifts blacks (matte/faded); can be tinted
clarity amount, radius_frac Wide-radius local contrast; negative = soften
grain luma, chroma, size, floor Luminance-weighted, resolution-independent grain
sharpen amount, radius_frac, halo Narrow-radius unsharp + optional halo
jpeg quality, cycles JPEG re-encode loop (JPEG output only)

Determinism & constraints

  • Resolution and aspect ratio are preserved — no crop/resize.
  • All intermediate work is float32 in [0,1] RGB; conversion to 8-bit happens only on save. PNG keeps alpha and skips the JPEG step.
  • The grain seed derives from the filename hash, so the same image + theme is byte-identical every run.
  • No EXIF/metadata is written to outputs.

Adding a theme

Add postfx/themes/<category>/<name>.yaml. The first comment lines are the name / character / suited content; parameters follow (see the table above). Then verify with postfx sheet + pytest.

# my_look
# Warm, low-contrast, soft — a gentle everyday grade.
name: my_look
white_balance: { temp: 0.12, tint: 0.02 }
tone_curve: { strength: 0.15, pivot: 0.45 }
vibrance: { vibrance: 0.1, saturation: 1.03 }
grain: { luma: 0.015, size: 1.5 }

Development

git clone https://github.com/0xBeycan/postfx.git
cd postfx
uv sync --extra dev        # or: pip install -e ".[dev]"
uv run pytest -q           # or: pytest -q

License

MIT © 0xBeycan

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

postfx-1.0.0.tar.gz (239.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

postfx-1.0.0-py3-none-any.whl (258.6 kB view details)

Uploaded Python 3

File details

Details for the file postfx-1.0.0.tar.gz.

File metadata

  • Download URL: postfx-1.0.0.tar.gz
  • Upload date:
  • Size: 239.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for postfx-1.0.0.tar.gz
Algorithm Hash digest
SHA256 42aa163cecfe06ecd867837c677b112a841b54348930f3d0f14855eea1e3a422
MD5 3f3b97ac5d9535d42297a050e1a0e2f3
BLAKE2b-256 d119794e24b529bdb4e9d1385fcc22c4bb566880c29e2ab9d097efe2bbdae0cc

See more details on using hashes here.

Provenance

The following attestation bundles were made for postfx-1.0.0.tar.gz:

Publisher: publish.yml on 0xBeycan/postfx

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file postfx-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: postfx-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 258.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for postfx-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 56c468b39a0494bde3abb7499c69980843b4bc36a25d622b083f852d5ea00589
MD5 b102244984aeaf184792097da6af9a37
BLAKE2b-256 2046318d9405d5c7c541bd081f36a51e4df8eabe32df47b0731e7c6abd8f45c8

See more details on using hashes here.

Provenance

The following attestation bundles were made for postfx-1.0.0-py3-none-any.whl:

Publisher: publish.yml on 0xBeycan/postfx

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page