Skip to main content

op-img

The McLaren photo cycling through seam-carve, channel-swap, polar, pixel-sort, invert-lightness, wrong-stride and fold, each layered over the last

op-img is a composable image manipulation CLI:

op-img <patch> <input> [--args]

Quick start

Install op-img with pipx, which also installs Pillow, numpy and scipy:

pipx install op-img

Or run it from a clone. Clone the repository:

git clone https://github.com/service-dept/op-img.git

Move into it:

cd op-img

Install Python3 with Pillow, numpy and scipy:

pip3 install Pillow numpy scipy

Add op-img to your PATH (one-time setup, from the op-img folder):

ln -s "$(pwd)/op-img" /usr/local/bin/op-img

Use it from anywhere on your machine.

Show the usage and three patches at random:

op-img

Crush the colors to 1 bit per channel:

op-img bit-crush photo.jpg

Sort the pixels, saving photo-psort.jpg next to photo.jpg because no output is given:

op-img pixel-sort photo.jpg

Draw halftone dots 8 px apart and save them as out.png:

op-img dot-halftone photo.jpg out.png --spacing 8

Snap every pixel to black, white or red:

op-img closest-palette photo.jpg --palette "#000,#fff,#f00"

The input comes first, then an optional output. Omit the output to save the result next to the input with the patch's suffix.

Patches

All examples below use this image as input:

default input

bit-crush

Reduce color depth to N bits per channel, with a light dither that leaves flat, saturated blotches.

python3 ./patches/bit-crush/bit-crush.py <input> [output] [--bits N]

Default: --bits 1 (2 levels per channel, 8 colors)

bit-crush example

res-crush

Downscale to a tiny resolution and upscale back with nearest-neighbor for a chunky pixel look.

python3 ./patches/res-crush/res-crush.py <input> [output] [--size N]

Default: --size 64

res-crush example

channel-offset

Shift R, G, B channels by independent pixel amounts for a misregistered print / chromatic aberration look.

python3 ./patches/channel-offset/channel-offset.py <input> [output] [--r X,Y] [--g X,Y] [--b X,Y]

Default: --r 140,50 --g -20,40 --b -120,-40

channel-offset example

fold

Mirror or repeat one half of the image across a fold line.

python3 ./patches/fold/fold.py <input> [output] [--axis x|y] [--position N] [--mode mirror|repeat]

Default: --axis x --position center --mode mirror

fold example

pixel-sort

Sort contiguous runs of pixels by brightness, hue, or saturation.

python3 ./patches/pixel-sort/pixel-sort.py <input> [output] [--by brightness|hue|saturation] [--threshold N] [--direction row|column]

Default: --threshold 200

pixel-sort example

scan-glitch

Randomly shift horizontal slices of the image for a broken-signal effect.

python3 ./patches/scan-glitch/scan-glitch.py <input> [output] [--severity N] [--seed N]

scan-glitch example

echo

Composite the image on itself with offset and fade for a ghosting/echo effect.

python3 ./patches/echo/echo.py <input> [output] [--count N] [--offset-x N] [--offset-y N] [--decay N] [--blend additive|screen|multiply]

Default: --count 12 --offset-x 30 --offset-y 12 --decay 0.6 --blend additive

echo example

kaleidoscope

Extract a wedge from the image and mirror/rotate it around the center for a kaleidoscope effect.

python3 ./patches/kaleidoscope/kaleidoscope.py <input> [output] [--segments N] [--angle N]

Default: --segments 6 --angle 90

kaleidoscope example

polar

Remap image between Cartesian and polar coordinates. --center moves the pole, --rotate turns where the seam falls, and --radius sets how far out the rings reach. Use the same values for to-polar and from-polar to map back.

python3 ./patches/polar/polar.py <input> [output] [--mode to-polar|from-polar] [--center X,Y] [--rotate DEG] [--radius N]

Default: --mode to-polar --center 0.5,0.5 --rotate 0 --radius 1

polar example

raw-bend

Treat pixel data as a raw audio signal and apply echo, chorus, and bitcrush distortion.

python3 ./patches/raw-bend/raw-bend.py <input> [output] [--echo-strength N] [--echo-delay N] [--chorus N] [--bitcrush N]

Default: --echo-strength 0.8 --echo-delay 2000 --chorus 0.7 --bitcrush 0

raw-bend example

seam-carve

Content-aware image resizing by removing low-energy vertical seams.

python3 ./patches/seam-carve/seam-carve.py <input> [output] [--percent N] [--energy gradient|sobel]

Default: --percent 35 --energy sobel

seam-carve example

slit-scan

Take one column from each rotation of the image and stitch them together for a slit-scan effect.

python3 ./patches/slit-scan/slit-scan.py <input> [output] [--slits N] [--max-angle N]

Default: --slits <width> --max-angle 180

slit-scan example

tile-shuffle

Chop the image into an NxN grid and randomly permute the tiles.

python3 ./patches/tile-shuffle/tile-shuffle.py <input> [output] [--grid N] [--seed N]

Default: --grid 8

tile-shuffle example

wrong-stride

Flatten the pixel buffer and reshape with a wrong row width for a diagonal shear glitch.

python3 ./patches/wrong-stride/wrong-stride.py <input> [output] [--offset N]

Default: --offset 1

wrong-stride example

fft-phase

Keep each channel's Fourier magnitude and blend in random phase, so the image dissolves into a texture with the same spectrum.

python3 ./patches/fft-phase/fft-phase.py <input> [output] [--amount N] [--seed N]

Default: --amount 0.35

fft-phase example

zoom-blur

Average copies of the image scaled up about a center point, for radial warp-speed streaks.

python3 ./patches/zoom-blur/zoom-blur.py <input> [output] [--amount N] [--center X,Y] [--samples N]

Default: --amount 0.3 --center 0.5,0.5 --samples 32

zoom-blur example

swirl

Twist the image around a center, with the rotation fading out toward a radius.

python3 ./patches/swirl/swirl.py <input> [output] [--angle DEG] [--radius N] [--center X,Y]

Default: --angle 360 --radius 1.0 --center 0.5,0.5

swirl example

displace

Move each pixel along an angle by an amount taken from its own blurred brightness, so light and dark areas tear apart in opposite directions.

python3 ./patches/displace/displace.py <input> [output] [--amount PX] [--angle DEG] [--blur N]

Default: --amount 150 --angle 0 --blur 3

displace example

drip

Bleed bright pixels in one direction with a fading tail, like wet paint running.

python3 ./patches/drip/drip.py <input> [output] [--length PX] [--threshold N] [--direction down|up|left|right]

Default: --length 500 --threshold 120 --direction down

drip example

edge-glow

Turn edges into neon lines in each pixel's own hue, with a soft halo, over a darkened base.

python3 ./patches/edge-glow/edge-glow.py <input> [output] [--amount N] [--radius N]

Default: --amount 1 --radius 6

edge-glow example

contour

Draw lines where brightness crosses N levels, like a topographic map of the photo. Pink lines on black by default.

python3 ./patches/contour/contour.py <input> [output] [--levels N] [--blur N] [--width PX] [--color HEX] [--amount N]

Default: --levels 16 --blur 2 --width 1 --color #ec4899 --amount 1

contour example

bloom

Pull out the highlights, blur them at three radii and screen them back for a soft glow.

python3 ./patches/bloom/bloom.py <input> [output] [--amount N] [--threshold N] [--radius N]

Default: --amount 2 --threshold 110 --radius 16

bloom example

voronoi-mosaic

Split the image into irregular Voronoi cells filled with their average color, with optional dark leading like stained glass.

python3 ./patches/voronoi-mosaic/voronoi-mosaic.py <input> [output] [--size PX] [--jitter N] [--edges PX] [--seed N]

Default: --size 24 --jitter 1 --edges 0

voronoi-mosaic example

oil-paint

Kuwahara filter: smooth into flat painterly patches while keeping edges crisp.

python3 ./patches/oil-paint/oil-paint.py <input> [output] [--radius N]

Default: --radius 6

oil-paint example

tilt-shift

Blur away from a horizontal focus band and lift the color, so the scene looks like a miniature.

python3 ./patches/tilt-shift/tilt-shift.py <input> [output] [--blur N] [--focus N] [--band N]

Default: --blur 10 --focus 0.62 --band 0.25

tilt-shift example

flow-streak

Smear the image along its own contours for brushed, combed strokes.

python3 ./patches/flow-streak/flow-streak.py <input> [output] [--length N] [--sigma N]

Default: --length 36 --sigma 6

flow-streak example

dither

Dither to N levels per channel with a Bayer matrix, or with Floyd–Steinberg or Atkinson error diffusion. Error diffusion takes a few seconds on the README image.

python3 ./patches/dither/dither.py <input> [output] [--method bayer|floyd|atkinson] [--levels N] [--matrix 2|4|8]

Default: --method bayer --levels 2 --matrix 8

dither example

jpeg-rot

Re-save as a low-quality JPEG many times, shifting a pixel each time so the damage piles up instead of settling.

python3 ./patches/jpeg-rot/jpeg-rot.py <input> [output] [--quality N] [--generations N]

Default: --quality 5 --generations 80

jpeg-rot example

ascii

Replace each cell with a bold character chosen by brightness, stretched to the image's own range, drawn in the cell's hue on black.

python3 ./patches/ascii/ascii.py <input> [output] [--cell PX] [--charset CHARS]

Default: --cell 10 --charset " .:-=+*#%@"

ascii example

isolate-threshold

Keep the pixels brighter than a threshold as a flat color on a transparent background, optionally upscaled with nearest-neighbor. The default output is PNG; a JPEG output gets a white background.

python3 ./patches/isolate-threshold/isolate-threshold.py <input> [output] [--threshold N] [--color "#hex"] [--scale N]

Default: --threshold 50 --color "#ff0000" --scale 1

isolate-threshold example

closest-palette

Snap every pixel to its nearest color in a given palette. No dithering -- hard color boundaries.

python3 ./patches/closest-palette/closest-palette.py <input> [output] --palette "#hex,#hex,..."
python3 ./patches/closest-palette/closest-palette.py <input> [output] --from-image ref.png --colors N

closest-palette example

invert-lightness

Invert the lightness channel in LAB color space — dark becomes light and vice versa, while hue and saturation are preserved.

python3 ./patches/invert-lightness/invert-lightness.py <input> [output]

invert-lightness example

posterize-hsv

Quantize HSV channels independently for a posterized look with hue control.

python3 ./patches/posterize-hsv/posterize-hsv.py <input> [output] [--h-levels N] [--s-levels N] [--v-levels N]

Default: --h-levels 8 --s-levels 4 --v-levels 4

posterize-hsv example

thermal

Map brightness to a false-color thermal palette (black to blue to red to yellow to white).

python3 ./patches/thermal/thermal.py <input> [output]

thermal example

hue-isolate

Keep one hue band in full color and turn everything else gray.

python3 ./patches/hue-isolate/hue-isolate.py <input> [output] [--hue DEG] [--width DEG] [--amount N]

Default: --hue 25 --width 20 --amount 1 (orange)

hue-isolate example

channel-swap

Rearrange RGB channels — swap, duplicate, or reorder color channels.

python3 ./patches/channel-swap/channel-swap.py <input> [output] [--map B,G,R]

Default: --map B,G,R (swaps red and blue)

channel-swap example

recolor

Repaint the image's most prevalent colors with the colors you give, most prevalent first, keeping their light and shade. Grays, black and white are left alone.

python3 ./patches/recolor/recolor.py <input> [output] [--colors C1,C2] [--amount N] [--clusters N]

Default: --colors "#1e3a8a,#facc15" --amount 1 --clusters 6

recolor example

dot-halftone

Convert to a halftone dot grid where dot size varies with brightness. Pink dots on transparent background.

python3 ./patches/dot-halftone/dot-halftone.py <input> [output] [--spacing N] [--min-dot N] [--max-dot N] [--angle N]

dot-halftone example

line-halftone

Variable-width lines whose thickness maps to brightness. Pink lines on transparent background.

python3 ./patches/line-halftone/line-halftone.py <input> [output] [--spacing N] [--min-width N] [--max-width N] [--angle N]

line-halftone example

cross-hatch

Multiple line-halftone passes at different angles, each gated by a brightness threshold. Darker areas get more layers of hatching. Pink lines on transparent background.

python3 ./patches/cross-hatch/cross-hatch.py <input> [output] [--layers N] [--spacing N] [--thresholds N,N,N]

cross-hatch example

stipple

Random dot placement where density maps to brightness. Pink dots on transparent background.

python3 ./patches/stipple/stipple.py <input> [output] [--dots N] [--dot-size N] [--seed N]

stipple example

Stacking patches

Join patches with + to run them one after another, each one building on the previous result:

op-img <patch> <input> [output] [--args] + <patch> [--args] + ...
op-img pixel-sort photo.jpg + fold + polar            # writes photo-psort-fold-polar.jpg
op-img pixel-sort photo.jpg --by hue + channel-swap   # options follow the patch they belong to
op-img seam-carve photo.jpg out.jpg + thermal         # an output after the input names the final file

The input, and the output if you give one, come right after the first patch. Subsequent patches only take arguments. Omit the output path to save the result in the same directory as the input, with each patch's suffix applied in order.

contour + swirl

Contour traces the photo's brightness bands in pink lines, and swirl twists them into a vortex.

op-img contour photo.jpg + swirl

contour + swirl

fold + polar + pixel-sort + channel-swap

Fold mirrors the car, polar wraps it into an arch, pixel-sort streaks it, and channel-swap turns the orange blue.

op-img fold photo.jpg + polar --center 0.3,0.5 --rotate 200 --radius 0.9 + pixel-sort + channel-swap

fold + polar + pixel-sort + channel-swap

thermal + invert-lightness

Heat colors first, then the lightness flipped, which turns the car magenta and pink.

op-img thermal photo.jpg + invert-lightness

thermal + invert-lightness

invert-lightness + thermal

The same two patches the other way round: with the lightness flipped first, the paint reads cold and the tires and grass run white-hot.

op-img invert-lightness photo.jpg + thermal

invert-lightness + thermal

slit-scan + thermal

Slit-scan melts the car into curves, and thermal paints them in heat colors.

op-img slit-scan photo.jpg + thermal

slit-scan + thermal

polar + drip + polar

Drips run straight down in polar space, so they come back as rays bursting from an off-center pole.

op-img polar photo.jpg --center 0.62,0.4 --rotate 150 --radius 0.85 + drip --length 500 --threshold 120 + polar --mode from-polar --center 0.62,0.4 --rotate 150 --radius 0.85

polar + drip + polar

polar + tile-shuffle + polar

Tiles shuffled in polar space come back as rings of turned wedges.

op-img polar photo.jpg + tile-shuffle --grid 8 + polar --mode from-polar

polar + tile-shuffle + polar

dither + zoom-blur

Atkinson dithering, then a zoom blur that smears the dots into speed streaks.

op-img dither photo.jpg --method atkinson + zoom-blur

dither + zoom-blur

kaleidoscope + ascii

A kaleidoscope emblem, redrawn in characters.

op-img kaleidoscope photo.jpg + ascii

kaleidoscope + ascii

scan-glitch + polar

Scan-glitch's torn rows, wrapped into arches around a shifted pole.

op-img scan-glitch photo.jpg + polar --center 0.72,0.3 --rotate 105 --radius 0.7

scan-glitch + polar

swirl + kaleidoscope

A swirl, mirrored into a chrome mandala.

op-img swirl photo.jpg + kaleidoscope

swirl + kaleidoscope

res-crush + zoom-blur

Big pixels, then a zoom blur that streaks them outward.

op-img res-crush photo.jpg --size 32 + zoom-blur

res-crush + zoom-blur

bit-crush + flow-streak

A 1-bit crush, brushed back into painterly strokes.

op-img bit-crush photo.jpg --bits 1 + flow-streak

bit-crush + flow-streak

channel-offset + swirl

Split color channels, twisted into a swirl.

op-img channel-offset photo.jpg --r 140,50 --g -20,40 --b -120,-40 + swirl

channel-offset + swirl

pixel-sort + drip + invert-lightness

Columns sorted, bright pixels dripping upward, then the lightness flipped.

op-img pixel-sort photo.jpg --direction column + drip --length 500 --threshold 120 --direction up + invert-lightness

pixel-sort + drip + invert-lightness

Adding a patch

A patch is a directory in patches/, named after the patch. It holds either a Python script with its requirements.txt, or a shell script. op-img finds patches by name, so there's nothing to register. A missing input prints Error: file not found. Its own tests go in tests/test_<name>.py. The name goes in ALL_PATCHES in tests/test_op_cli.py, and its default output name in DEFAULT_NAMES in tests/test_conventions.py, which checks the shared conventions for every patch.

License

MIT. See LICENSE.

Release files for op-img 0.1.0

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

Source distribution (sdist)

Source distribution for op-img 0.1.0
File Size Uploaded
op_img-0.1.0.tar.gz 60.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for op-img 0.1.0
File Interpreter ABI Platform
op_img-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 137.6 kB

Release files / op_img-0.1.0.tar.gz

Download URL op_img-0.1.0.tar.gz
Size 60.9 kB
Tags Source
SHA-256 checksum
How to use checksums
23ab800c8fb4b2707e8ed4cb0057ec4b13a821e2e49cb3ec28e7dc00f6511fe5
BLAKE2b-256 checksum
How to use checksums
7af7c91d5a75b9813568b9d3d1f90a87e372af297e113c81ca456bc0e61bb251
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 27, 2026.

Transparency log

Release files / op_img-0.1.0-py3-none-any.whl

Download URL op_img-0.1.0-py3-none-any.whl
Size 76.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
49a736e6872329b99a10d6302f2f805e0134da253f9c0ba8ae8fc12c579c4eb3
BLAKE2b-256 checksum
How to use checksums
c18781d10567992abf94a6d3bd4257c878c8ded9ffde408cc481a4dbd06d2cbd
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 27, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

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