← back to Nova Recolor

batch_recolor_v2.py

66 lines

#!/usr/bin/env python3
"""Reinhard color-transfer batch: ruby drapery → every Novasuede color.

Source drapery: /Users/macstudio3/Projects/nova-recolor/novasuede-ruby.jpg
Color samples:  /tmp/novasuede_sources/*.jpg (downloaded from Shopify CDN)
Output:         /tmp/novasuede_drapery/{slug}.jpg

Reinhard 2001 color transfer: matches mean+std of source LAB to target LAB.
"""
import os, glob, json, sys, time
import numpy as np
import cv2

DRAPERY_SRC = '/Users/macstudio3/Projects/nova-recolor/novasuede-ruby.jpg'
SAMPLES_DIR = '/tmp/novasuede_sources'
OUT_DIR = '/tmp/novasuede_drapery'

os.makedirs(OUT_DIR, exist_ok=True)

def center_crop(img, frac=0.7):
    h, w = img.shape[:2]
    ch, cw = int(h*frac), int(w*frac)
    y0, x0 = (h-ch)//2, (w-cw)//2
    return img[y0:y0+ch, x0:x0+cw]

def lab_reinhard(src_bgr, tgt_bgr):
    """Reinhard 2001 color transfer in LAB space."""
    src_lab = cv2.cvtColor(src_bgr, cv2.COLOR_BGR2LAB).astype(np.float32)
    tgt_lab = cv2.cvtColor(tgt_bgr, cv2.COLOR_BGR2LAB).astype(np.float32)
    s = src_lab.reshape(-1, 3)
    t = tgt_lab.reshape(-1, 3)
    s_mean, s_std = s.mean(0), s.std(0)
    t_mean, t_std = t.mean(0), t.std(0)
    out = (src_lab - s_mean) * (t_std / (s_std + 1e-8)) + t_mean
    out = np.clip(out, 0, 255).astype(np.uint8)
    return cv2.cvtColor(out, cv2.COLOR_LAB2BGR)

src = cv2.imread(DRAPERY_SRC, cv2.IMREAD_COLOR)
assert src is not None, f"could not read {DRAPERY_SRC}"

samples = sorted(glob.glob(os.path.join(SAMPLES_DIR, '*.jpg')))
print(f"found {len(samples)} color samples")

manifest = []
t0 = time.time()
for s_path in samples:
    slug = os.path.splitext(os.path.basename(s_path))[0]
    out_path = os.path.join(OUT_DIR, f'{slug}.jpg')
    tgt = cv2.imread(s_path, cv2.IMREAD_COLOR)
    if tgt is None:
        print(f"SKIP unreadable: {s_path}")
        continue
    tgt_c = center_crop(tgt, 0.7)
    out = lab_reinhard(src, tgt_c)
    cv2.imwrite(out_path, out, [cv2.IMWRITE_JPEG_QUALITY, 88])
    # Also extract a single hex color from the swatch center for the chip
    cx, cy = tgt_c.shape[1]//2, tgt_c.shape[0]//2
    bgr = tgt_c[cy, cx]
    hex_color = '#{:02x}{:02x}{:02x}'.format(bgr[2], bgr[1], bgr[0])
    manifest.append({'slug': slug, 'hex': hex_color, 'drapery': f'{slug}.jpg'})
    print(f"OK {slug:30s} hex={hex_color}")

with open(os.path.join(OUT_DIR, 'manifest.json'), 'w') as f:
    json.dump(manifest, f, indent=2)
print(f"\ndone in {time.time()-t0:.1f}s — {len(manifest)} draperies + manifest.json")