Created
May 11, 2026 06:22
-
-
Save me-suzy/fbaf10959a477bf447730f4ac55d9fb1 to your computer and use it in GitHub Desktop.
batch - font albastru senin.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """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