Skip to content

Instantly share code, notes, and snippets.

@motebaya
Last active September 3, 2025 12:31
Show Gist options
  • Save motebaya/b0567cc8bac93320150ae146419585bc to your computer and use it in GitHub Desktop.
Save motebaya/b0567cc8bac93320150ae146419585bc to your computer and use it in GitHub Desktop.
Bulk/single image/s convert to Pink × Green duotone
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @gist.github.com/motebaya - 3/9/2025
# rewrite from TS - https://lovable.dev/projects/8354edf5-ed78-4f83-8860-5ac9d2f067ff
import numpy as np
import os
from PIL import Image
import sys
OPTIMIZED_SHADOW = np.array([22, 80, 39], dtype=np.float32) #165027
OPTIMIZED_HIGHLIGHT = np.array([249, 159, 210], dtype=np.float32) #F99FD2
CLASSIC_SHADOW = np.array([27, 96, 47], dtype=np.float32) #1b602f
CLASSIC_HIGHLIGHT = np.array([247, 132, 197], dtype=np.float32) #f784c5
def duotone(
imgp: str,
classic: bool = False,
reverse: bool = False,
out: str = "",
) -> None:
img = Image.open(imgp).convert("RGB")
arr = np.asarray(img, dtype=np.float32)
# using rec. 709 formula
luminance = 0.2126 * arr[...,0] + 0.7152 * arr[...,1] + 0.0722 * arr[...,2]
min_l, max_l = luminance.min(), luminance.max()
norm = (luminance - min_l) / (max_l - min_l + 1e-6)
# contrast
norm = np.where(
norm < 0.5,
np.power(norm * 2, 1.8) / 2,
1 - np.power((1 - norm) * 2, 1.8) / 2
)
f = "duotone_"
if classic:
f += "classic"
s, h = CLASSIC_SHADOW, CLASSIC_HIGHLIGHT
else:
s, h = OPTIMIZED_SHADOW, OPTIMIZED_HIGHLIGHT
f += "optimized"
if reverse:
f += "_reverse"
s, h = h, s
f = f + "_" + os.path.basename(imgp)
norm = norm[..., None]
Image.fromarray(
np.clip(
s + norm * (h - s),
0, 255
).astype(np.uint8),
"RGB"
).save(os.path.join(out, f))
print(f" * ok: {os.path.join(out, f)}")
return
def fromdof(f: str, **kwargs) -> None:
if os.path.isfile(f):
duotone(f, **kwargs)
return
if os.path.isdir(f):
out = os.path.join(f, "duotone_" + "reverse" if kwargs["reverse"] else "classic" if kwargs["classic"] else "original")
os.makedirs(out, exist_ok=True)
kwargs['out'] = out
for x, file in enumerate(os.listdir(f), 1):
print(f"{x:02d} | {file}")
duotone(
os.path.join(f, file),
**kwargs,
)
return
if __name__ == "__main__":
arg = sys.argv
if len(arg) < 2:
print("Usage: python pg_duotone.py <image_path> <mode>")
sys.exit(1)
mode = arg[2].split(",") if len(arg) > 2 else []
assert all(
m in (
"classic",
"reverse",
) for m in mode), "wrong mode!"
fromdof(
arg[1],
reverse="reverse" in mode,
classic="classic" in mode,
)
@motebaya
Copy link
Author

motebaya commented Sep 3, 2025

Original Classic Reverse Classic & Reverse

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment