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

Release files for postfx 1.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for postfx 1.0.1
File Size Uploaded
postfx-1.0.1.tar.gz 239.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for postfx 1.0.1
File Interpreter ABI Platform
postfx-1.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 498.0 kB

Release files / postfx-1.0.1.tar.gz

Download URL postfx-1.0.1.tar.gz
Size 239.4 kB
Tags Source
SHA-256 checksum
How to use checksums
2ff37b4f045cefcb2658dd8a01616f338443a0d660d0fcfb2b40a5b695da8b95
BLAKE2b-256 checksum
How to use checksums
44ac192d7a0bd3aebb646b8f467fa0c162b9b6eeca7e2854ea30a60a56cd506a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release files / postfx-1.0.1-py3-none-any.whl

Download URL postfx-1.0.1-py3-none-any.whl
Size 258.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7cf5d4b2ad3a1a519ade4373af682099db2956ca59e1d6b2f382018ee5817d10
BLAKE2b-256 checksum
How to use checksums
d6f6568ec27dc2ea4c8e59c16a5543d0eb0eb88e7a2d170db8ddb9f39eaa9af5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release history Release notifications | RSS feed

1.1.0

2 release files

This release

1.0.1 This release

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page