V2.0.0 - Rasterix - photo papers

This commit is contained in:
DESKTOP-TVBJISQ\Primere
2026-04-01 17:52:25 +02:00
parent ab01da5cbf
commit bb3ca6c871
3 changed files with 141 additions and 6 deletions
+44 -6
View File
@@ -30,6 +30,8 @@ from ..components.images import img_filmic_curve as img_filmic_curve
from ..components.images import img_lut3d as img_lut3d
from ..components.images import img_edge_jitter as img_edge_jitter
from ..components.images import img_depth_blur as img_depth_blur
from ..components.images import img_photo_paper as img_photo_paper
from ..components.images.img_photo_paper import PAPER_PRESETS
from ..components import utility
from .Dashboard import PrimereModelConceptSelector as PrimereModelConceptSelector
@@ -69,7 +71,8 @@ class PrimereRasterix:
{"after": "blend_mode", "name": "rasterix_local_laplacian", "title": "🧱 Tone: Edge-Aware Pyramid", "color": "#405985", "text_color": "#EAF1F8", "label": "Local Laplacian contrast/detail enhancement with strong edge preservation."},
{"after": "levels", "name": "rasterix_analog_film", "title": "🎞 Creative: Analog Film / CCD", "color": "#3B5E68", "text_color": "#EAF1F8", "label": "Stylized film and sensor-era rendering for mood, palette, and texture character. Inspired by DxO."},
{"after": "photo_paper", "name": "rasterix_lut_reader", "title": "📷 Creative: LUT .cube Reader", "color": "#3B5E68", "text_color": "#EAF1F8", "label": "Load and blend LUT looks for fast creative direction and consistent show style. Inspired by Blackmagic DaVinci Resolve and DxO."},
{"after": "expiration_years", "name": "rasterix_photo_paper", "title": "🧪 Creative: Photo Paper Simulation", "color": "#3B5E68", "text_color": "#EAF1F8", "label": "Darkroom-inspired paper response with selectable grade, RC/FB base, color/B&W mode, and controlled print intensity."},
{"after": "paper_intensity", "name": "rasterix_lut_reader", "title": "📷 Creative: LUT .cube Reader", "color": "#3B5E68", "text_color": "#EAF1F8", "label": "Load and blend LUT looks for fast creative direction and consistent show style. Inspired by Blackmagic DaVinci Resolve and DxO."},
{"after": "color_space", "name": "rasterix_filmic_camera", "title": "🎥 Creative: Filmic Camera Curve", "color": "#3B5E68", "text_color": "#EAF1F8", "label": "Camera-like highlight roll-off and tonal response for cinematic dynamic range behavior. Inspired by Adobe Camera Raw."},
{"after": "pivot", "name": "rasterix_selective_tone", "title": "🎛 Color: Selective Tone Zones", "color": "#6A5636", "text_color": "#EAF1F8", "label": "Zone-based tonal pushes for highlights, midtones, shadows, and blacks. Inspired by DxO Photolab"},
@@ -176,8 +179,12 @@ class PrimereRasterix:
"halation": ("BOOLEAN", {"default": False, "label_off": "Ignore halation", "label_on": "Add halation"}),
"expiration_years": ("INT", {"default": 0, "min": 0, "max": 30, "step": 1}),
"use_photo_paper": ("BOOLEAN", {"default": False, "label_off": "Ignore H/B photo paper", "label_on": "Apply H/B photo paper"}),
"photo_paper": ("BOOLEAN", {"default": False, "label_off": "Soft paper", "label_on": "Hard paper"}),
"use_photo_paper": ("BOOLEAN", {"default": False, "label_off": "Ignore photo paper", "label_on": "Apply photo paper"}),
"photo_paper": (list(PAPER_PRESETS.keys()), {"default": list(PAPER_PRESETS.keys())[0]}),
"color_paper": ("BOOLEAN", {"default": False, "label_off": "B&W paper", "label_on": "Color paper"}),
"paper_base": (["RC", "FB"], {"default": "RC"}),
"paper_expiration_years": ("FLOAT", {"default": 0, "min": 0, "max": 30, "step": 0.1}),
"paper_intensity": ("FLOAT", {"default": 100, "min": 0, "max": 200, "step": 1}),
"use_lut": ("BOOLEAN", {"default": False, "label_off": "Ignore LUT", "label_on": "Apply LUT"}),
"lut_file": (cls._list_luts(),),
@@ -339,7 +346,11 @@ class PrimereRasterix:
halation = kwargs.get('halation', False)
expiration_years = kwargs.get('expiration_years', 0)
use_photo_paper = kwargs.get('use_photo_paper', False)
photo_paper = kwargs.get('photo_paper', False)
photo_paper = kwargs.get('photo_paper', "N (ISO R 90, normal)")
color_paper = kwargs.get('color_paper', False)
paper_base = kwargs.get('paper_base', "RC")
paper_expiration_years = kwargs.get('paper_expiration_years', 0)
paper_intensity = kwargs.get('paper_intensity', 100)
use_filmic = kwargs.get('use_filmic', False)
curve_type = kwargs.get('curve_type', "filmic")
filmic_contrast = kwargs.get('filmic_contrast', 1.0)
@@ -427,8 +438,8 @@ class PrimereRasterix:
if use_filmic:
pil_img = img_filmic_curve.img_filmic_curve(image=pil_img, curve_type=curve_type, contrast=filmic_contrast, highlight_rolloff=highlight_rolloff, shadow_lift=shadow_lift, pivot=pivot)
if use_photo_paper:
pil_img = img_solarization_bw.img_solarization_bw(image=pil_img, color_mode=False, strength=0.00, pivot=0.00, sigma=0.01, edge_boost=0.00, edge_radius=0.5, contrast=1, precision=precision, hard_paper=photo_paper, grain_modulation=False, grain_strength=0, grain_scale=1, seed=1)
if use_photo_paper and paper_intensity != 0:
pil_img = img_photo_paper.img_photo_paper(image=pil_img, paper_type=photo_paper, color_paper=color_paper, paper_base=paper_base, paper_intensity=paper_intensity, expiration_years=paper_expiration_years)
st_data = rasterix_data.get('selective_tone', {})
if use_selective_tone and st_data:
@@ -1378,4 +1389,31 @@ class PrimereDepthBlur:
if use_depth_blur:
pil_img = img_depth_blur.img_depth_blur(image=pil_img, focus_depth=focus_depth, depth_range=depth_range, max_blur=max_blur, depth_gamma=depth_gamma, auto_optimize=auto_optimize, use_v3=use_DA_v3)
return (utility.image_to_tensor(pil_img),)
class PrimerePhotoPaper:
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("IMAGE",)
FUNCTION = "primere_photo_paper"
CATEGORY = TREE_RASTERIX
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE", {"forceInput": True}),
"use_photo_paper": ("BOOLEAN", {"default": False, "label_off": "Ignore photo paper", "label_on": "Apply photo paper"}),
"photo_paper": (list(PAPER_PRESETS.keys()), {"default": list(PAPER_PRESETS.keys())[0]}),
"color_paper": ("BOOLEAN", {"default": False, "label_off": "B&W paper", "label_on": "Color paper"}),
"paper_base": (["RC", "FB"], {"default": "RC"}),
"paper_expiration_years": ("FLOAT", {"default": 0, "min": 0, "max": 30, "step": 0.1}),
"paper_intensity": ("FLOAT", {"default": 100, "min": 0, "max": 200, "step": 1}),
}
}
def primere_photo_paper(self, image, use_photo_paper, photo_paper, color_paper, paper_base, paper_intensity, paper_expiration_years):
pil_img = utility.tensor_to_image(image)
if use_photo_paper and paper_intensity != 0:
pil_img = img_photo_paper.img_photo_paper(image=pil_img, paper_type=photo_paper, color_paper=color_paper, paper_base=paper_base, paper_intensity=paper_intensity, expiration_years=paper_expiration_years)
return (utility.image_to_tensor(pil_img),)
+2
View File
@@ -67,6 +67,7 @@ NODE_CLASS_MAPPINGS = {
"PrimereBlur": Rasterix.PrimereBlur,
"PrimereBrightnessContrast": Rasterix.PrimereBrightnessContrast,
"PrimereFilmRendering": Rasterix.PrimereFilmRendering,
"PrimerePhotoPaper": Rasterix.PrimerePhotoPaper,
"PrimereSelectiveTone": Rasterix.PrimereSelectiveTone,
"PrimereColorBalance": Rasterix.PrimereColorBalance,
"PrimereHSL": Rasterix.PrimereHSL,
@@ -174,6 +175,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"PrimereBlur": "Primere Rasterix (Blur)",
"PrimereBrightnessContrast": "Primere Rasterix (Brightness Contrast)",
"PrimereFilmRendering": "Primere Rasterix (Film Rendering)",
"PrimerePhotoPaper": "Primere Rasterix (Photo Paper)",
"PrimereSelectiveTone": "Primere Rasterix (Selective Tone)",
"PrimereColorBalance": "Primere Rasterix (Color Balance)",
"PrimereHSL": "Primere Rasterix (HSL)",
+95
View File
@@ -0,0 +1,95 @@
import numpy as np
from PIL import Image
PAPER_PRESETS = {
"BN (ISO R 130, soft)": {"contrast": 0.88, "toe": 0.10, "shoulder": 0.04},
"B (ISO R 110, medium-soft)": {"contrast": 0.96, "toe": 0.08, "shoulder": 0.05},
"N (ISO R 90, normal)": {"contrast": 1.04, "toe": 0.06, "shoulder": 0.06},
"H (ISO R 70, hard)": {"contrast": 1.18, "toe": 0.05, "shoulder": 0.08},
"HD (ISO R 50, extra hard)": {"contrast": 1.32, "toe": 0.03, "shoulder": 0.10},
"Grade 00 (very soft)": {"contrast": 0.84, "toe": 0.12, "shoulder": 0.04},
"Grade 0 (soft)": {"contrast": 0.92, "toe": 0.09, "shoulder": 0.05},
"Grade 2 (normal)": {"contrast": 1.00, "toe": 0.07, "shoulder": 0.06},
"Grade 3 (normal-hard)": {"contrast": 1.12, "toe": 0.06, "shoulder": 0.07},
"Grade 4 (hard)": {"contrast": 1.24, "toe": 0.05, "shoulder": 0.09},
"Grade 5 (ultra hard)": {"contrast": 1.36, "toe": 0.03, "shoulder": 0.12},
}
def _apply_tone_curve(x, contrast, toe, shoulder):
x = np.clip(0.5 + (x - 0.5) * contrast, 0.0, 1.0)
if toe > 0:
x = x * (1.0 - toe) + np.power(x, 1.25) * toe
if shoulder > 0:
inv = 1.0 - x
x = 1.0 - (inv * (1.0 - shoulder) + np.power(inv, 1.25) * shoulder)
return np.clip(x, 0.0, 1.0)
def _apply_paper_expiration(arr, years):
out = arr.copy()
fog = np.clip(years * 0.006, 0, 0.18)
out = out + fog * (1.0 - out)
blue_fade = np.clip(1.0 - (years * 0.012), 0.65, 1.0)
out[..., 2] *= blue_fade
contrast = np.clip(1.0 - (years * 0.01), 0.7, 1.0)
out = np.clip((out - 0.5) * contrast + 0.5, 0.0, 1.0)
return out
def img_photo_paper(
image: Image.Image,
paper_type: str = "N (ISO R 90, normal)",
color_paper: bool = False,
paper_base: str = "RC",
paper_intensity: float = 100.0,
expiration_years: float = 0.0,
) -> Image.Image:
src = np.asarray(image.convert("RGB"), dtype=np.float32) / 255.0
preset = PAPER_PRESETS.get(paper_type, PAPER_PRESETS["N (ISO R 90, normal)"])
contrast = preset["contrast"]
toe = preset["toe"]
shoulder = preset["shoulder"]
if paper_base == "FB":
toe += 0.02
shoulder += 0.01
contrast *= 0.98
elif paper_base == "RC":
contrast *= 1.02
intensity = float(np.clip(paper_intensity, 0.0, 200.0))
strength = (intensity - 100.0) / 100.0
contrast = np.clip(contrast * (1.0 + 0.25 * strength), 0.5, 2.5)
toe = np.clip(toe * (1.0 + 0.35 * strength), 0.0, 0.35)
shoulder = np.clip(shoulder * (1.0 + 0.35 * strength), 0.0, 0.35)
if color_paper:
out = _apply_tone_curve(src, contrast, toe, shoulder)
sat = 1.02 if paper_base == "RC" else 0.98
mean = out.mean(axis=2, keepdims=True)
out = np.clip(mean + (out - mean) * sat, 0.0, 1.0)
else:
# In B&W mode, force grayscale source before blending to avoid color casts
# at any intensity value (including > 100).
luma = 0.299 * src[..., 0] + 0.587 * src[..., 1] + 0.114 * src[..., 2]
src = np.stack([luma, luma, luma], axis=-1)
tone = _apply_tone_curve(luma, contrast, toe, shoulder)
out = np.stack([tone, tone, tone], axis=-1)
out = _apply_paper_expiration(out, expiration_years)
if intensity <= 100.0:
amount = (intensity / 100.0) * 0.90
else:
amount = 1.0 + ((intensity - 100.0) / 100.0) * 1.10
amount = np.clip(amount, 0.0, 2.0)
blended = np.clip(src + (out - src) * amount, 0.0, 1.0)
return Image.fromarray((blended * 255.0).astype(np.uint8), mode="RGB")