Skip to content

Instantly share code, notes, and snippets.

@me-suzy
Created May 11, 2026 06:22
Show Gist options
  • Select an option

  • Save me-suzy/fbaf10959a477bf447730f4ac55d9fb1 to your computer and use it in GitHub Desktop.

Select an option

Save me-suzy/fbaf10959a477bf447730f4ac55d9fb1 to your computer and use it in GitHub Desktop.
batch - font albastru senin.py
"""Script standalone generat din setarile PhotoScape: batch - font albastru senin.
Citeste imaginile din INPUT_DIR si scrie JPG in OUTPUT_DIR.
Setarile originale sunt embeduite in SETTINGS.
Corectii importante fata de generarea anterioara:
- multe valori PhotoScape la 100 sunt valori de lucru/neutre pentru acel control,
nu comenzi de colorare agresiva;
- in Curves se aplica doar curba RGB/master, fiindca in PhotoScape canalele
Red/Green/Blue nu sunt bifate in presetul de referinta;
- filtrul Film/Duotone este aproximat prin fundaluri pale pentru documente scanate.
"""
import json
from pathlib import Path
import cv2
import numpy as np
INPUT_DIR = Path(r"g:\Colectia EMINESCIANA")
OUTPUT_DIR = Path(r"g:\Colectia EMINESCIANA\Output")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
PRESET_NAME = "batch - font albastru senin"
JPEG_QUALITY = 90
SETTINGS = json.loads(r"""{
"color": {
"Color": {
"autoColor": 3,
"autoContrast": 3,
"autoLevel": 3,
"blue": 100,
"brightness": 0,
"burn": 100,
"bw/blackAmount": 0,
"bw/whiteAmount": -100,
"clarity": 100,
"colorFilter": "",
"contrast": 100,
"dodge": 100,
"exposure": 0,
"gamma": 0,
"green": 100,
"hdr": true,
"hdr/drago": 100,
"hdr/ldrtonemap": 0,
"highlight": 0,
"hue": 0,
"magiccolor": true,
"magiccolor/amountBrt": 100,
"magiccolor/amountDrk": 0,
"multiply": 100,
"negative": false,
"overlay": 100,
"red": 100,
"saturation": -100,
"screen": 100,
"shadow": 0,
"skintones": true,
"temperature": 15000,
"tint": 100,
"vibrance": 100,
"vivid": 100
},
"enabled": true
},
"crop": {
"bottom": 0,
"enabled": true,
"left": 0,
"right": 0,
"top": 0
},
"curve": {
"enabled": true,
"parameters": {
"amount": 100.0,
"curve": {
"c": [
"0,0,219,46,255,255",
"0,0,105,153,255,255",
"0,0,187,98,255,255",
"0,0,236,35,255,255"
],
"n": 4
}
}
},
"decoration": {
"enabled": false
},
"film": [
{
"action": "Film",
"actionName": "Film > I04",
"enabled": true,
"parameters": {
"mode": "Film",
"path": ":/res/curves/pro/I-04.acv",
"strength": 0.5,
"title": "I04"
}
},
{
"action": "",
"actionName": "",
"enabled": false,
"parameters": {}
},
{
"action": "",
"actionName": "",
"enabled": false,
"parameters": {}
},
{
"action": "",
"actionName": "",
"enabled": false,
"parameters": {}
},
{
"action": "",
"actionName": "",
"enabled": false,
"parameters": {}
},
{
"action": "",
"actionName": "",
"enabled": false,
"parameters": {}
}
],
"filter": {
"Bloom": {
"amount": 0
},
"Blur": {
"amount": 0
},
"DenoiseColor": {
"amount": 0
},
"DenoiseLuminance": {
"amount": 0
},
"FilmGrain": {
"amount": 6
},
"Sharpen": {
"amount": 0
},
"Vignette": {
"amount": 0
},
"enabled": false
},
"light": [
{
"action": "",
"actionName": "",
"enabled": false,
"parameters": {}
},
{
"action": "",
"actionName": "",
"enabled": false,
"parameters": {}
},
{
"action": "",
"actionName": "",
"enabled": false,
"parameters": {}
},
{
"action": "",
"actionName": "",
"enabled": false,
"parameters": {}
},
{
"action": "",
"actionName": "",
"enabled": false,
"parameters": {}
},
{
"action": "",
"actionName": "",
"enabled": false,
"parameters": {}
}
],
"object": [],
"resize": {
"axis": "original",
"height": 800,
"interpolation": "",
"longer": 640,
"noenlargement": false,
"ratio": 100,
"shorter": 640,
"size": "1280,1280",
"width": 1280
},
"version": 2
}""")
SCRIPT_SETTINGS = {
"variant": "attempt_15_hybrid_strong_1600",
"paperSigma": 17,
"blackPoint": 135,
"whitePoint": 246,
"gamma": 0.54,
"unsharpAmount": 0.95,
"unsharpSigma": 0.90,
"adaptiveBlock": 47,
"adaptiveC": 8,
"maskBlur": 0.45,
"textBlack": 5,
"textDilateIterations": 1,
"minTextComponentArea": 4,
"maskStrength": 0.92,
"outputMaxSize": [1600, 1600],
}
def _clip8(img):
return np.clip(img, 0, 255).astype(np.uint8)
def _ensure_bgr(img):
if img.ndim == 2:
return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
if img.shape[2] == 4:
bgr = img[:, :, :3].astype(np.float32)
alpha = img[:, :, 3:4].astype(np.float32) / 255.0
white = np.full_like(bgr, 255.0)
return _clip8(bgr * alpha + white * (1.0 - alpha))
return img[:, :, :3]
def _apply_crop(img):
crop = SETTINGS.get("crop") or {}
if not crop.get("enabled"):
return img
h, w = img.shape[:2]
left = max(0, int(crop.get("left", 0) or 0))
top = max(0, int(crop.get("top", 0) or 0))
right = max(0, int(crop.get("right", 0) or 0))
bottom = max(0, int(crop.get("bottom", 0) or 0))
return img[top:max(top + 1, h - bottom), left:max(left + 1, w - right)]
def _apply_resize(img):
resize = SETTINGS.get("resize") or {}
size = str(resize.get("size", "") or "")
width = int(resize.get("width", 0) or 0)
height = int(resize.get("height", 0) or 0)
if "," in size:
try:
width, height = [int(x.strip()) for x in size.split(",", 1)]
except ValueError:
pass
if width <= 0 or height <= 0:
return img
h, w = img.shape[:2]
scale = min(width / float(w), height / float(h))
if resize.get("noenlargement") and scale > 1.0:
return img
new_size = (max(1, int(round(w * scale))), max(1, int(round(h * scale))))
if new_size == (w, h):
return img
interpolation = cv2.INTER_AREA if scale < 1.0 else cv2.INTER_CUBIC
return cv2.resize(img, new_size, interpolation=interpolation)
def _film_names():
names = []
for item in SETTINGS.get("film") or []:
if item.get("enabled"):
params = item.get("parameters") or {}
names.append(" ".join(str(x or "") for x in [item.get("actionName"), params.get("mode"), params.get("title")]))
return " ".join(names).lower()
def _color_settings():
return ((SETTINGS.get("color") or {}).get("Color") or {})
def _preset_text():
return (PRESET_NAME + " " + _film_names()).lower()
def _background_tint_bgr():
text = _preset_text()
color = _color_settings()
if "font albastru senin" in text or " i04" in text:
return np.array([255, 255, 236], dtype=np.float32) # cyan pal, ca in PhotoScape I04
if "galben" in text or "primavera" in text or " d00" in text:
return np.array([150, 255, 255], dtype=np.float32) # galben deschis
if " l05" in text:
return np.array([190, 248, 255], dtype=np.float32)
if " b01" in text:
return np.array([210, 232, 248], dtype=np.float32)
if "delamere" in text:
return np.array([210, 235, 225], dtype=np.float32)
if "cross process" in text:
return np.array([245, 255, 235], dtype=np.float32)
if "vintage" in text:
return np.array([225, 238, 245], dtype=np.float32)
temp = float(color.get("temperature", 6500) or 6500)
tint = float(color.get("tint", 0) or 0)
if temp >= 10000 and tint >= 50:
return np.array([255, 255, 236], dtype=np.float32)
if temp <= 4000 and tint >= 50:
return np.array([150, 255, 255], dtype=np.float32)
return np.array([255, 255, 255], dtype=np.float32)
def _parse_curve_points(spec):
try:
nums = [float(x.strip()) for x in str(spec).split(",") if x.strip()]
except ValueError:
return None
if len(nums) < 4 or len(nums) % 2:
return None
pts = np.array(nums, dtype=np.float32).reshape(-1, 2)
pts = pts[np.argsort(pts[:, 0])]
merged = {}
for x, y in pts:
merged[int(np.clip(round(x), 0, 255))] = float(np.clip(y, 0, 255))
xs = np.array(sorted(merged.keys()), dtype=np.float32)
ys = np.array([merged[int(x)] for x in xs], dtype=np.float32)
if xs[0] > 0:
xs = np.insert(xs, 0, 0.0)
ys = np.insert(ys, 0, ys[0])
if xs[-1] < 255:
xs = np.append(xs, 255.0)
ys = np.append(ys, ys[-1])
return np.interp(np.arange(256, dtype=np.float32), xs, ys).clip(0, 255).astype(np.uint8)
def _soft_master_curve(gray, amount):
curve_settings = SETTINGS.get("curve") or {}
if not curve_settings.get("enabled"):
return gray
curves = (((curve_settings.get("parameters") or {}).get("curve") or {}).get("c") or [])
if not curves:
return gray
lut = _parse_curve_points(curves[0])
if lut is None:
return gray
curved = cv2.LUT(gray, lut)
amount = float(np.clip(amount, 0.0, 1.0))
return _clip8(gray.astype(np.float32) * (1.0 - amount) + curved.astype(np.float32) * amount)
def _paper_normalize(gray, sigma=15, median=3):
# Echivalent practic pentru Auto Levels/Auto Contrast pe scanari: scoate umbrele mari
# fara sa coloreze pagina.
background = cv2.GaussianBlur(gray, (0, 0), sigmaX=sigma, sigmaY=sigma)
normalized = cv2.divide(gray, background, scale=255)
if median and median > 1:
return cv2.medianBlur(normalized, median)
return normalized
def _derive_levels():
color = _color_settings()
text = _preset_text()
# Presetul I04 din captura ta: acesta este calibrat pentru fundal cyan pal si text negru.
if "font albastru senin" in text or " i04" in text:
return 170, 245, 0.70, 1
contrast = float(color.get("contrast", 0) or 0)
clarity = float(color.get("clarity", 0) or 0)
blacks = float(color.get("bw/blackAmount", 0) or 0)
saturation = float(color.get("saturation", 0) or 0)
auto = max(float(color.get("autoColor", 0) or 0), float(color.get("autoContrast", 0) or 0), float(color.get("autoLevel", 0) or 0))
bp = 85.0
if auto:
bp += 12.0 * min(auto, 3.0) / 3.0
if contrast >= 100:
bp += 35.0
if clarity >= 80:
bp += 12.0
if blacks > 0:
bp += min(30.0, blacks * 0.25)
elif blacks < 0:
bp -= min(25.0, abs(blacks) * 0.18)
if saturation <= -50:
bp += 8.0
if "remove-shadows" in text or "shadows" in text:
bp += 15.0
bp = int(np.clip(round(bp), 55, 175))
wp = 245
gamma = 0.65 if bp < 145 else 0.70
erode_iters = 1 if contrast >= 100 or clarity >= 80 else 0
return bp, wp, gamma, erode_iters
def _levels(gray, bp, wp, gamma):
f = gray.astype(np.float32)
norm = np.clip((f - float(bp)) / max(1.0, float(wp - bp)), 0.0, 1.0)
return _clip8(np.power(norm, 1.0 / float(gamma)) * 255.0)
def _colorize_document(gray):
tint = _background_tint_bgr().reshape(1, 1, 3)
mask = gray.astype(np.float32) / 255.0
return _clip8(mask[:, :, None] * tint)
def _resize_to_max(img, max_size):
max_w, max_h = max_size
h, w = img.shape[:2]
scale = min(float(max_w) / float(w), float(max_h) / float(h))
new_size = (max(1, int(round(w * scale))), max(1, int(round(h * scale))))
if new_size == (w, h):
return img
interpolation = cv2.INTER_AREA if scale < 1.0 else cv2.INTER_CUBIC
return cv2.resize(img, new_size, interpolation=interpolation)
def _unsharp_gray(gray, amount, sigma):
blur = cv2.GaussianBlur(gray, (0, 0), sigmaX=sigma, sigmaY=sigma)
return _clip8(cv2.addWeighted(gray, 1.0 + amount, blur, -amount, 0))
def _remove_tiny_components(mask, min_area):
count, labels, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=8)
keep = np.zeros(count, dtype=np.uint8)
keep[1:] = (stats[1:, cv2.CC_STAT_AREA] >= min_area).astype(np.uint8)
return (keep[labels] * 255).astype(np.uint8)
def _readable_hybrid_gray(gray):
settings = SCRIPT_SETTINGS
gray = _paper_normalize(gray, sigma=settings["paperSigma"], median=3)
gray = _levels(gray, settings["blackPoint"], settings["whitePoint"], settings["gamma"])
gray = _unsharp_gray(gray, settings["unsharpAmount"], settings["unsharpSigma"])
mask = cv2.adaptiveThreshold(
gray,
255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY_INV,
settings["adaptiveBlock"],
settings["adaptiveC"],
)
mask = _remove_tiny_components(mask, settings["minTextComponentArea"])
dilate_iters = settings["textDilateIterations"]
if dilate_iters:
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))
mask = cv2.dilate(mask, kernel, iterations=dilate_iters)
soft_mask = cv2.GaussianBlur(
mask,
(0, 0),
sigmaX=settings["maskBlur"],
sigmaY=settings["maskBlur"],
).astype(np.float32) / 255.0
base = gray.astype(np.float32)
black = float(settings["textBlack"])
strength = float(settings["maskStrength"])
reinforced = np.minimum(base, black + (base - black) * (1.0 - soft_mask * strength))
return _clip8(reinforced)
def proc_photoscape(img):
"""Aplica presetul PhotoScape ca filtru pentru documente scanate."""
bgr = _apply_crop(_ensure_bgr(img))
gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)
if SCRIPT_SETTINGS.get("variant") == "attempt_15_hybrid_strong_1600":
gray = _readable_hybrid_gray(gray)
out = _colorize_document(gray)
return _resize_to_max(out, SCRIPT_SETTINGS["outputMaxSize"])
gray = _paper_normalize(gray)
bp, wp, gamma, erode_iters = _derive_levels()
gray = _levels(gray, bp, wp, gamma)
# Aplicam doar curba RGB/master, foarte bland. Canalele R/G/B din JSON raman stocate,
# dar in presetul din captura nu sunt bifate si nu trebuie aplicate automat.
if " i04" not in _preset_text() and "font albastru senin" not in _preset_text():
gray = _soft_master_curve(gray, 0.20)
if erode_iters:
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))
gray = cv2.erode(gray, kernel, iterations=erode_iters)
out = _colorize_document(gray)
return _apply_resize(out)
def main():
extensions = {".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff", ".webp"}
files = sorted(f for f in INPUT_DIR.iterdir() if f.is_file() and f.suffix.lower() in extensions)
if not files:
print(f"Nu am gasit imagini in {INPUT_DIR}")
return
print(f"Preset: {PRESET_NAME}")
print(f"Procesez {len(files)} imagini din {INPUT_DIR}")
for i, p in enumerate(files, 1):
print(f" [{i}/{len(files)}] {p.name}")
data = np.fromfile(str(p), dtype=np.uint8)
img = cv2.imdecode(data, cv2.IMREAD_UNCHANGED)
if img is None:
print(" SKIP - nu pot citi")
continue
out = proc_photoscape(img)
out_path = OUTPUT_DIR / (p.stem + ".jpg")
ok, buf = cv2.imencode(".jpg", out, [cv2.IMWRITE_JPEG_QUALITY, JPEG_QUALITY])
if ok:
buf.tofile(str(out_path))
else:
print(" SKIP - nu pot salva")
print(f"Gata! Iesire: {OUTPUT_DIR}")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment