Merge pull request #1 from KarmaSwint/feat/new-post-processing-nodes

feat: Add Karma Lens FX, Tone Curves, and Film Emulation nodes
This commit is contained in:
KarmaSwint
2026-03-29 08:26:46 +00:00
committed by GitHub
5 changed files with 1438 additions and 2 deletions
+604
View File
@@ -0,0 +1,604 @@
"""
ComfyUI node for cinematic film stock emulation.
This module provides a specialized node that emulates the color science and
tonal characteristics of iconic analog film stocks. Each preset replicates
the unique look of a specific film, including its color response, contrast
curve, grain structure, and highlight/shadow behavior.
"""
import torch
from PIL import Image, ImageFilter, ImageEnhance
import numpy as np
def tensor2pil(image):
"""Convert tensor to PIL image."""
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
def pil2tensor(image):
"""Convert PIL image to tensor."""
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
# Film stock preset definitions
# Each preset defines the color science of a specific film stock:
# temperature - white balance shift (negative=cool, positive=warm)
# tint - green/magenta tint shift
# contrast - overall contrast adjustment
# saturation - color saturation multiplier
# shadows_hue - hue of shadow tinting (0-1)
# shadows_sat - strength of shadow tinting
# highlights_hue - hue of highlight tinting (0-1)
# highlights_sat - strength of highlight tinting
# gamma - midtone brightness (>1 = brighter, <1 = darker)
# black_lift - raise black point for faded look
# grain - film grain intensity
# grain_size - film grain particle size
# halation - highlight bloom intensity
FILM_PRESETS = {
"Kodak Portra 400": {
"description": "Natural skin tones, soft contrast, warm pastels. The gold standard for portrait photography.",
"temperature": 0.04,
"tint": 0.01,
"contrast": 0.95,
"saturation": 0.88,
"shadows_hue": 0.58,
"shadows_sat": 0.06,
"highlights_hue": 0.10,
"highlights_sat": 0.05,
"gamma": 1.05,
"black_lift": 0.02,
"grain": 0.06,
"grain_size": 1.2,
"halation": 0.0,
},
"Kodak Ektar 100": {
"description": "Ultra-vivid colors, fine grain, high saturation. Ideal for landscapes and travel.",
"temperature": 0.02,
"tint": 0.0,
"contrast": 1.15,
"saturation": 1.25,
"shadows_hue": 0.60,
"shadows_sat": 0.03,
"highlights_hue": 0.08,
"highlights_sat": 0.02,
"gamma": 0.98,
"black_lift": 0.0,
"grain": 0.03,
"grain_size": 0.8,
"halation": 0.0,
},
"Kodak Gold 200": {
"description": "Warm, saturated consumer film. Golden highlights, nostalgic everyday look.",
"temperature": 0.06,
"tint": 0.01,
"contrast": 1.05,
"saturation": 1.10,
"shadows_hue": 0.08,
"shadows_sat": 0.05,
"highlights_hue": 0.12,
"highlights_sat": 0.08,
"gamma": 1.02,
"black_lift": 0.01,
"grain": 0.08,
"grain_size": 1.3,
"halation": 0.0,
},
"Fuji Velvia 50": {
"description": "Extreme saturation, deep contrast, vivid greens and blues. Legendary landscape film.",
"temperature": -0.02,
"tint": 0.0,
"contrast": 1.25,
"saturation": 1.40,
"shadows_hue": 0.55,
"shadows_sat": 0.04,
"highlights_hue": 0.05,
"highlights_sat": 0.02,
"gamma": 0.95,
"black_lift": 0.0,
"grain": 0.02,
"grain_size": 0.7,
"halation": 0.0,
},
"Fuji Pro 400H": {
"description": "Soft, pastel rendering with subtle greens. Bright, airy skin tones. Wedding favorite.",
"temperature": -0.01,
"tint": 0.02,
"contrast": 0.90,
"saturation": 0.85,
"shadows_hue": 0.42,
"shadows_sat": 0.05,
"highlights_hue": 0.15,
"highlights_sat": 0.04,
"gamma": 1.08,
"black_lift": 0.03,
"grain": 0.05,
"grain_size": 1.0,
"halation": 0.0,
},
"Fuji Superia 400": {
"description": "Cool tones, strong greens and blues, punchy contrast. Classic consumer film.",
"temperature": -0.03,
"tint": 0.01,
"contrast": 1.08,
"saturation": 1.05,
"shadows_hue": 0.55,
"shadows_sat": 0.06,
"highlights_hue": 0.42,
"highlights_sat": 0.04,
"gamma": 1.0,
"black_lift": 0.01,
"grain": 0.09,
"grain_size": 1.4,
"halation": 0.0,
},
"CineStill 800T": {
"description": "Tungsten-balanced cinema film. Teal shadows, warm highlights, halation around lights.",
"temperature": -0.08,
"tint": -0.02,
"contrast": 1.05,
"saturation": 0.95,
"shadows_hue": 0.52,
"shadows_sat": 0.10,
"highlights_hue": 0.08,
"highlights_sat": 0.08,
"gamma": 1.02,
"black_lift": 0.02,
"grain": 0.10,
"grain_size": 1.5,
"halation": 0.15,
},
"Kodak Tri-X 400": {
"description": "Iconic black & white film. Rich tones, beautiful grain, deep blacks. Street photography legend.",
"temperature": 0.0,
"tint": 0.0,
"contrast": 1.20,
"saturation": 0.0,
"shadows_hue": 0.0,
"shadows_sat": 0.0,
"highlights_hue": 0.0,
"highlights_sat": 0.0,
"gamma": 0.98,
"black_lift": 0.01,
"grain": 0.12,
"grain_size": 1.4,
"halation": 0.0,
},
"Ilford HP5 Plus": {
"description": "Versatile black & white film. Smooth tones, moderate grain, excellent latitude.",
"temperature": 0.0,
"tint": 0.0,
"contrast": 1.10,
"saturation": 0.0,
"shadows_hue": 0.0,
"shadows_sat": 0.0,
"highlights_hue": 0.0,
"highlights_sat": 0.0,
"gamma": 1.02,
"black_lift": 0.02,
"grain": 0.08,
"grain_size": 1.2,
"halation": 0.0,
},
"Kodak Vision3 500T": {
"description": "Professional cinema negative film. Refined color, tungsten-balanced, modern movie look.",
"temperature": -0.05,
"tint": -0.01,
"contrast": 1.0,
"saturation": 0.92,
"shadows_hue": 0.55,
"shadows_sat": 0.07,
"highlights_hue": 0.10,
"highlights_sat": 0.05,
"gamma": 1.03,
"black_lift": 0.015,
"grain": 0.05,
"grain_size": 1.0,
"halation": 0.05,
},
}
class Karma_Film_Emulation:
"""
Film stock emulation node for one-click cinematic color grading.
This node applies the color science, tonal characteristics, and texture
of iconic analog film stocks to digital images. Each preset is carefully
calibrated to replicate the unique rendering of a specific film, including
its color response curves, contrast behavior, grain structure, and special
characteristics like CineStill's halation.
The intensity slider allows blending between the original image and the
full film emulation, making it easy to dial in exactly the right amount
of analog character.
Supported film stocks:
Color Negative:
- Kodak Portra 400 (portraits, natural skin tones)
- Kodak Ektar 100 (landscapes, vivid color)
- Kodak Gold 200 (warm, nostalgic everyday)
- Fuji Velvia 50 (extreme saturation, landscapes)
- Fuji Pro 400H (soft pastels, weddings)
- Fuji Superia 400 (cool tones, consumer)
Cinema:
- CineStill 800T (tungsten, halation, night photography)
- Kodak Vision3 500T (professional cinema)
Black & White:
- Kodak Tri-X 400 (classic, rich grain)
- Ilford HP5 Plus (smooth, versatile)
"""
@classmethod
def INPUT_TYPES(cls):
"""
Define the input parameters for the film emulation node.
Returns:
Dictionary containing required and optional input specifications
"""
film_options = list(FILM_PRESETS.keys())
return {
"required": {
"image": ("IMAGE", {"tooltip": "Input image to apply film emulation to"}),
"film_stock": (film_options, {
"default": "Kodak Portra 400",
"tooltip": "Film stock to emulate"
}),
"intensity": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 1.5,
"step": 0.05,
"tooltip": "Blend intensity (0 = original, 1 = full emulation, >1 = exaggerated)"
}),
"grain_override": ("FLOAT", {
"default": -1.0,
"min": -1.0,
"max": 1.0,
"step": 0.01,
"tooltip": "Override grain amount (-1 = use film default, 0-1 = custom strength)"
}),
"seed": ("INT", {
"default": 0,
"min": 0,
"max": 2**31 - 1,
"tooltip": "Random seed for reproducible grain patterns"
}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "apply_film_emulation"
CATEGORY = "KarmaNodes/Post-Processing"
def apply_film_emulation(self, image: torch.Tensor, film_stock: str,
intensity: float, grain_override: float,
seed: int) -> tuple:
"""
Apply film stock emulation to the input image.
The emulation pipeline applies effects in the correct order to replicate
how analog film actually works: color response first (how the film
captures light), then contrast/tone (development characteristics),
then physical artifacts (grain, halation).
Args:
image: Input image tensor
film_stock: Name of the film stock preset to apply
intensity: Blend strength (0=original, 1=full, >1=exaggerated)
grain_override: Custom grain strength (-1 = use preset default)
seed: Random seed for grain reproducibility
Returns:
Tuple containing the processed image tensor
"""
preset = FILM_PRESETS[film_stock]
pil_image = tensor2pil(image)
original_array = np.array(pil_image, dtype=np.float32) / 255.0
img_array = original_array.copy()
# Step 1: Color temperature and tint
if abs(preset["temperature"]) > 0.001 or abs(preset["tint"]) > 0.001:
img_array = self.apply_temperature_tint(
img_array, preset["temperature"] * intensity, preset["tint"] * intensity
)
# Step 2: Saturation (including B&W conversion for monochrome films)
effective_sat = 1.0 + (preset["saturation"] - 1.0) * intensity
if effective_sat < 0.01:
# Black and white film
img_array = self.convert_to_bw(img_array)
elif abs(effective_sat - 1.0) > 0.01:
img_array = self.adjust_saturation(img_array, effective_sat)
# Step 3: Gamma / midtone brightness
effective_gamma = 1.0 + (preset["gamma"] - 1.0) * intensity
if abs(effective_gamma - 1.0) > 0.005:
img_array = self.apply_gamma(img_array, effective_gamma)
# Step 4: Contrast
effective_contrast = 1.0 + (preset["contrast"] - 1.0) * intensity
if abs(effective_contrast - 1.0) > 0.01:
img_array = self.apply_contrast(img_array, effective_contrast)
# Step 5: Black point lift (faded film look)
effective_lift = preset["black_lift"] * intensity
if effective_lift > 0.001:
img_array = np.clip(img_array * (1.0 - effective_lift) + effective_lift, 0, 1)
# Step 6: Split toning
if preset["shadows_sat"] > 0 and intensity > 0:
img_array = self.apply_split_tone(
img_array, preset["shadows_hue"],
preset["shadows_sat"] * intensity, zone="shadows"
)
if preset["highlights_sat"] > 0 and intensity > 0:
img_array = self.apply_split_tone(
img_array, preset["highlights_hue"],
preset["highlights_sat"] * intensity, zone="highlights"
)
# Step 7: Halation (for CineStill and cinema stocks)
effective_halation = preset["halation"] * intensity
if effective_halation > 0.005:
img_array = self.apply_halation(img_array, effective_halation)
# Step 8: Film grain
grain_amount = preset["grain"] if grain_override < 0 else grain_override
grain_amount *= intensity
if grain_amount > 0.005:
valid_seed = int(seed) % (2**32)
np.random.seed(valid_seed)
img_array = self.apply_grain(
img_array, grain_amount, preset["grain_size"]
)
result = Image.fromarray(np.clip(img_array * 255, 0, 255).astype(np.uint8))
result_tensor = pil2tensor(result)
return (result_tensor,)
@staticmethod
def apply_temperature_tint(img: np.ndarray, temperature: float,
tint: float) -> np.ndarray:
"""
Adjust color temperature and green/magenta tint.
Temperature shifts the blue-yellow axis (positive = warmer/yellow,
negative = cooler/blue). Tint shifts the green-magenta axis
(positive = more green, negative = more magenta).
Args:
img: Image array in 0-1 float range
temperature: Temperature shift (-0.2 to 0.2)
tint: Tint shift (-0.1 to 0.1)
Returns:
Color-adjusted image array
"""
result = img.copy()
if len(result.shape) == 3 and result.shape[2] >= 3:
# Warm: boost red, reduce blue
result[:, :, 0] = np.clip(result[:, :, 0] + temperature * 0.5, 0, 1)
result[:, :, 2] = np.clip(result[:, :, 2] - temperature * 0.5, 0, 1)
# Tint: adjust green channel
result[:, :, 1] = np.clip(result[:, :, 1] + tint * 0.5, 0, 1)
return result
@staticmethod
def convert_to_bw(img: np.ndarray) -> np.ndarray:
"""
Convert image to black and white using luminance weighting.
Uses standard BT.601 luminance coefficients for natural-looking
monochrome conversion that matches how the human eye perceives
brightness.
Args:
img: Image array in 0-1 float range
Returns:
Grayscale image array (still 3-channel for compatibility)
"""
if len(img.shape) == 3 and img.shape[2] >= 3:
luminance = 0.299 * img[:, :, 0] + 0.587 * img[:, :, 1] + 0.114 * img[:, :, 2]
result = np.stack([luminance, luminance, luminance], axis=2)
return result
return img
@staticmethod
def adjust_saturation(img: np.ndarray, factor: float) -> np.ndarray:
"""
Adjust color saturation.
Blends between the luminance (grayscale) version and the original
image. Factor > 1 increases saturation, < 1 decreases.
Args:
img: Image array in 0-1 float range
factor: Saturation multiplier
Returns:
Saturation-adjusted image array
"""
if len(img.shape) == 3 and img.shape[2] >= 3:
luminance = 0.299 * img[:, :, 0] + 0.587 * img[:, :, 1] + 0.114 * img[:, :, 2]
luminance = luminance[:, :, np.newaxis]
result = luminance + (img - luminance) * factor
return np.clip(result, 0, 1)
return img
@staticmethod
def apply_gamma(img: np.ndarray, gamma: float) -> np.ndarray:
"""
Apply gamma correction for midtone brightness adjustment.
Gamma > 1 brightens midtones (lifts the curve), gamma < 1 darkens
them. Black and white points are preserved.
Args:
img: Image array in 0-1 float range
gamma: Gamma value (typically 0.8-1.2)
Returns:
Gamma-corrected image array
"""
# Inverse gamma: gamma > 1 should brighten
inv_gamma = 1.0 / max(gamma, 0.01)
return np.clip(np.power(np.clip(img, 0.0001, 1.0), inv_gamma), 0, 1)
@staticmethod
def apply_contrast(img: np.ndarray, factor: float) -> np.ndarray:
"""
Adjust image contrast around the midpoint.
Scales pixel values relative to 0.5 (middle gray). Factor > 1
increases contrast, < 1 decreases it.
Args:
img: Image array in 0-1 float range
factor: Contrast multiplier
Returns:
Contrast-adjusted image array
"""
return np.clip(0.5 + (img - 0.5) * factor, 0, 1)
@staticmethod
def apply_split_tone(img: np.ndarray, hue: float, strength: float,
zone: str = "shadows") -> np.ndarray:
"""
Apply color tinting to shadows or highlights.
Args:
img: Image array in 0-1 float range
hue: Color hue (0-1)
strength: Tint intensity
zone: "shadows" or "highlights"
Returns:
Tinted image array
"""
if len(img.shape) < 3 or img.shape[2] < 3:
return img
luminance = 0.299 * img[:, :, 0] + 0.587 * img[:, :, 1] + 0.114 * img[:, :, 2]
if zone == "shadows":
mask = np.clip(1.0 - luminance * 2.0, 0, 1)
else:
mask = np.clip((luminance - 0.5) * 2.0, 0, 1)
# Hue to RGB
hue_360 = hue * 6.0
x = 1.0 - abs(hue_360 % 2.0 - 1.0)
if hue_360 < 1:
color = np.array([1.0, x, 0.0])
elif hue_360 < 2:
color = np.array([x, 1.0, 0.0])
elif hue_360 < 3:
color = np.array([0.0, 1.0, x])
elif hue_360 < 4:
color = np.array([0.0, x, 1.0])
elif hue_360 < 5:
color = np.array([x, 0.0, 1.0])
else:
color = np.array([1.0, 0.0, x])
tint = color[np.newaxis, np.newaxis, :] * np.ones_like(img)
blend = mask[:, :, np.newaxis] * strength
return np.clip(img * (1.0 - blend) + tint * blend, 0, 1)
@staticmethod
def apply_halation(img: np.ndarray, strength: float) -> np.ndarray:
"""
Apply halation (highlight bloom) effect.
Simulates the light-scatter phenomenon where bright highlights bleed
into surrounding areas. Characteristic of CineStill and some cinema
film stocks where the anti-halation layer is removed.
Args:
img: Image array in 0-1 float range
strength: Halation intensity
Returns:
Image array with halation applied
"""
if len(img.shape) == 3 and img.shape[2] >= 3:
luminance = 0.299 * img[:, :, 0] + 0.587 * img[:, :, 1] + 0.114 * img[:, :, 2]
else:
luminance = img[:, :, 0] if len(img.shape) == 3 else img
# Extract bright areas
threshold = 0.75
highlights = np.clip((luminance - threshold) / (1.0 - threshold + 0.001), 0, 1)
if len(img.shape) == 3:
highlight_img = img * highlights[:, :, np.newaxis]
else:
highlight_img = img * highlights
# Blur highlights using PIL
h_pil = Image.fromarray(np.clip(highlight_img * 255, 0, 255).astype(np.uint8))
h_blurred = h_pil.filter(ImageFilter.GaussianBlur(radius=15))
h_array = np.array(h_blurred, dtype=np.float32) / 255.0
# Screen blend
result = 1.0 - (1.0 - img) * (1.0 - h_array * strength)
return np.clip(result, 0, 1)
@staticmethod
def apply_grain(img: np.ndarray, strength: float,
grain_size: float) -> np.ndarray:
"""
Apply realistic film grain with luminance-based intensity.
Grain is more pronounced in darker areas and less visible in bright
highlights, mimicking real analog film behavior. Multiple noise
layers at different frequencies create a more organic texture.
Args:
img: Image array in 0-1 float range
strength: Grain intensity
grain_size: Grain particle size
Returns:
Image array with grain applied
"""
h, w = img.shape[:2]
# Calculate luminance for intensity modulation
if len(img.shape) == 3 and img.shape[2] >= 3:
luminance = 0.299 * img[:, :, 0] + 0.587 * img[:, :, 1] + 0.114 * img[:, :, 2]
else:
luminance = img[:, :, 0] if len(img.shape) == 3 else img
# Grain is stronger in shadows, weaker in highlights
grain_mask = 1.0 - luminance * 0.5
# Generate grain at reduced resolution for larger grain size
grain_h = max(int(h / grain_size), 1)
grain_w = max(int(w / grain_size), 1)
# Multi-layer grain for organic texture
grain = np.random.normal(0, 1, (grain_h, grain_w)).astype(np.float32)
# Upscale grain to image size if needed
if grain_size > 1.0:
grain_pil = Image.fromarray(
np.clip((grain + 3) / 6 * 255, 0, 255).astype(np.uint8)
)
grain_pil = grain_pil.resize((w, h), Image.BILINEAR)
grain = (np.array(grain_pil, dtype=np.float32) / 255.0 * 6 - 3)
# Apply grain modulated by luminance
grain_final = grain * grain_mask * strength
if len(img.shape) == 3:
grain_final = grain_final[:, :, np.newaxis]
return np.clip(img + grain_final, 0, 1)
+352
View File
@@ -0,0 +1,352 @@
"""
ComfyUI node for realistic lens effect simulation.
This module provides a specialized node that applies authentic optical
imperfections to images, simulating the characteristics of real camera lenses.
Effects include chromatic aberration, vignetting, barrel/pincushion distortion,
and halation (highlight bloom).
"""
import torch
from PIL import Image, ImageFilter
import numpy as np
def tensor2pil(image):
"""Convert tensor to PIL image."""
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
def pil2tensor(image):
"""Convert PIL image to tensor."""
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
class Karma_Lens_FX:
"""
Professional lens effects node that simulates real-world optical imperfections.
This node recreates the optical characteristics of physical camera lenses by
applying chromatic aberration, vignetting, barrel/pincushion distortion, and
halation effects. Each effect can be independently controlled for precise
cinematic styling.
Features:
- Chromatic aberration with per-channel offset control
- Smooth radial vignette with adjustable falloff
- Barrel and pincushion lens distortion
- Halation (bloom/glow on highlights) with threshold control
- All effects composable and independently adjustable
"""
@classmethod
def INPUT_TYPES(cls):
"""
Define the input parameters for the lens effects node.
Returns:
Dictionary containing required and optional input specifications
"""
return {
"required": {
"image": ("IMAGE", {"tooltip": "Input image to apply lens effects to"}),
"chromatic_aberration": ("FLOAT", {
"default": 0.0,
"min": 0.0,
"max": 20.0,
"step": 0.5,
"tooltip": "Strength of color fringing at image edges (in pixels)"
}),
"vignette_strength": ("FLOAT", {
"default": 0.0,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"tooltip": "Intensity of edge darkening (0 = none, 1 = maximum)"
}),
"vignette_falloff": ("FLOAT", {
"default": 2.0,
"min": 0.5,
"max": 5.0,
"step": 0.1,
"tooltip": "Controls how gradually the vignette fades (higher = tighter center)"
}),
"distortion": ("FLOAT", {
"default": 0.0,
"min": -1.0,
"max": 1.0,
"step": 0.01,
"tooltip": "Lens distortion: positive = barrel, negative = pincushion"
}),
"halation_strength": ("FLOAT", {
"default": 0.0,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"tooltip": "Intensity of highlight bloom/glow effect"
}),
"halation_threshold": ("FLOAT", {
"default": 0.8,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"tooltip": "Brightness threshold above which halation is applied"
}),
"halation_radius": ("FLOAT", {
"default": 10.0,
"min": 1.0,
"max": 50.0,
"step": 1.0,
"tooltip": "Spread radius of the halation glow (in pixels)"
}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "apply_lens_fx"
CATEGORY = "KarmaNodes/Post-Processing"
def apply_lens_fx(self, image: torch.Tensor, chromatic_aberration: float,
vignette_strength: float, vignette_falloff: float,
distortion: float, halation_strength: float,
halation_threshold: float, halation_radius: float) -> tuple:
"""
Apply lens effects to the input image.
Effects are applied in optical order: distortion first (physical lens
geometry), then chromatic aberration (light separation), halation
(light scatter), and finally vignetting (light falloff).
Args:
image: Input image tensor
chromatic_aberration: Strength of color fringing in pixels
vignette_strength: Intensity of edge darkening (0-1)
vignette_falloff: Vignette gradient steepness (0.5-5.0)
distortion: Barrel (+) or pincushion (-) distortion (-1 to 1)
halation_strength: Intensity of highlight bloom (0-1)
halation_threshold: Brightness threshold for halation (0-1)
halation_radius: Spread of halation glow in pixels
Returns:
Tuple containing the processed image tensor
"""
pil_image = tensor2pil(image)
# Apply effects in optical order
if abs(distortion) > 0.001:
pil_image = self.apply_distortion(pil_image, distortion)
if chromatic_aberration > 0.1:
pil_image = self.apply_chromatic_aberration(pil_image, chromatic_aberration)
if halation_strength > 0.001:
pil_image = self.apply_halation(pil_image, halation_strength,
halation_threshold, halation_radius)
if vignette_strength > 0.001:
pil_image = self.apply_vignette(pil_image, vignette_strength,
vignette_falloff)
result_tensor = pil2tensor(pil_image)
return (result_tensor,)
@staticmethod
def apply_chromatic_aberration(image: Image.Image, strength: float) -> Image.Image:
"""
Apply chromatic aberration by offsetting color channels.
Simulates the failure of a lens to focus all colors to the same point,
creating color fringing that increases toward image edges. The red channel
is shifted outward and the blue channel inward, mimicking real lateral
chromatic aberration.
Args:
image: Input PIL Image
strength: Offset strength in pixels
Returns:
Image with chromatic aberration applied
"""
img_array = np.array(image, dtype=np.float32)
h, w = img_array.shape[:2]
is_color = len(img_array.shape) == 3 and img_array.shape[2] >= 3
if not is_color:
return image
# Create coordinate grids for radial-weighted shifts
cy, cx = h / 2.0, w / 2.0
y_coords, x_coords = np.mgrid[0:h, 0:w].astype(np.float32)
# Radial distance from center (normalized to 0-1)
max_radius = np.sqrt(cx ** 2 + cy ** 2)
dx = (x_coords - cx) / max_radius
dy = (y_coords - cy) / max_radius
radius = np.sqrt(dx ** 2 + dy ** 2)
# Scale shift by radial distance (more shift at edges)
shift_scale = radius * strength
# Shift red channel outward, blue channel inward
result = img_array.copy()
# Red channel - shift away from center
r_x = x_coords + dx * shift_scale
r_y = y_coords + dy * shift_scale
r_x = np.clip(r_x, 0, w - 1).astype(np.int32)
r_y = np.clip(r_y, 0, h - 1).astype(np.int32)
result[:, :, 0] = img_array[r_y, r_x, 0]
# Blue channel - shift toward center
b_x = x_coords - dx * shift_scale
b_y = y_coords - dy * shift_scale
b_x = np.clip(b_x, 0, w - 1).astype(np.int32)
b_y = np.clip(b_y, 0, h - 1).astype(np.int32)
result[:, :, 2] = img_array[b_y, b_x, 2]
return Image.fromarray(np.clip(result, 0, 255).astype(np.uint8))
@staticmethod
def apply_vignette(image: Image.Image, strength: float,
falloff: float) -> Image.Image:
"""
Apply radial vignette darkening to image edges.
Creates a smooth radial gradient that darkens the image toward its
edges, simulating the natural light falloff of camera lenses. The
falloff parameter controls how tight the bright center area is.
Args:
image: Input PIL Image
strength: Vignette intensity (0-1)
falloff: Gradient steepness (higher = tighter center)
Returns:
Image with vignette applied
"""
img_array = np.array(image, dtype=np.float32)
h, w = img_array.shape[:2]
# Create radial distance map (0 at center, 1 at corners)
cy, cx = h / 2.0, w / 2.0
y_coords, x_coords = np.mgrid[0:h, 0:w].astype(np.float32)
# Normalize to elliptical distance so vignette follows image shape
dx = (x_coords - cx) / cx
dy = (y_coords - cy) / cy
radius = np.sqrt(dx ** 2 + dy ** 2)
# Apply falloff curve and strength
# radius of ~1.0 at edges, ~1.41 at corners
vignette_mask = 1.0 - strength * np.clip(radius ** falloff, 0, 1)
vignette_mask = np.clip(vignette_mask, 0, 1)
# Apply to all channels
if len(img_array.shape) == 3:
vignette_mask = vignette_mask[:, :, np.newaxis]
result = img_array * vignette_mask
return Image.fromarray(np.clip(result, 0, 255).astype(np.uint8))
@staticmethod
def apply_distortion(image: Image.Image, strength: float) -> Image.Image:
"""
Apply barrel or pincushion lens distortion.
Simulates the geometric distortion of real camera lenses. Barrel
distortion (positive values) bulges the image center outward, while
pincushion distortion (negative values) pinches it inward.
Args:
image: Input PIL Image
strength: Distortion amount (positive = barrel, negative = pincushion)
Returns:
Image with lens distortion applied
"""
img_array = np.array(image, dtype=np.float32)
h, w = img_array.shape[:2]
# Create normalized coordinate grid centered at image center
cy, cx = h / 2.0, w / 2.0
y_coords, x_coords = np.mgrid[0:h, 0:w].astype(np.float32)
# Normalize coordinates to -1..1 range
nx = (x_coords - cx) / cx
ny = (y_coords - cy) / cy
# Radial distance from center
r = np.sqrt(nx ** 2 + ny ** 2)
# Apply distortion formula: r_distorted = r * (1 + k * r^2)
k = strength * 0.5 # Scale for reasonable range
r_distorted = r * (1.0 + k * r ** 2)
# Avoid division by zero
safe_r = np.where(r > 0.0001, r, 1.0)
scale = r_distorted / safe_r
scale = np.where(r > 0.0001, scale, 1.0)
# Map back to pixel coordinates
new_x = cx + nx * scale * cx
new_y = cy + ny * scale * cy
# Clip to valid range
new_x = np.clip(new_x, 0, w - 1).astype(np.int32)
new_y = np.clip(new_y, 0, h - 1).astype(np.int32)
# Remap image
if len(img_array.shape) == 3:
result = img_array[new_y, new_x, :]
else:
result = img_array[new_y, new_x]
return Image.fromarray(np.clip(result, 0, 255).astype(np.uint8))
@staticmethod
def apply_halation(image: Image.Image, strength: float,
threshold: float, radius: float) -> Image.Image:
"""
Apply halation (highlight bloom) effect.
Simulates the light-scatter phenomenon in analog film where bright
highlights bleed into surrounding areas with a soft glow. The effect
is isolated to pixels above the brightness threshold and blurred to
create a natural bloom.
Args:
image: Input PIL Image
strength: Intensity of the glow (0-1)
threshold: Brightness threshold for affected pixels (0-1)
radius: Blur radius for the glow spread
Returns:
Image with halation applied
"""
img_array = np.array(image, dtype=np.float32) / 255.0
is_color = len(img_array.shape) == 3 and img_array.shape[2] >= 3
# Calculate luminance
if is_color:
luminance = 0.299 * img_array[:, :, 0] + 0.587 * img_array[:, :, 1] + 0.114 * img_array[:, :, 2]
else:
luminance = img_array.copy()
# Create highlight mask (pixels above threshold)
highlight_mask = np.clip((luminance - threshold) / (1.0 - threshold + 0.001), 0, 1)
# Extract highlight colors and blur them
if is_color:
highlight_image = img_array * highlight_mask[:, :, np.newaxis]
else:
highlight_image = img_array * highlight_mask
# Convert to PIL for Gaussian blur
highlight_pil = Image.fromarray(np.clip(highlight_image * 255, 0, 255).astype(np.uint8))
blurred_highlight = highlight_pil.filter(ImageFilter.GaussianBlur(radius=radius))
blurred_array = np.array(blurred_highlight, dtype=np.float32) / 255.0
# Blend: screen-like compositing for natural glow
# Screen blend: 1 - (1 - a) * (1 - b)
result = 1.0 - (1.0 - img_array) * (1.0 - blurred_array * strength)
return Image.fromarray(np.clip(result * 255, 0, 255).astype(np.uint8))
+342
View File
@@ -0,0 +1,342 @@
"""
ComfyUI node for professional tone curve adjustments.
This module provides a specialized node for surgical tonal control,
including independent shadow, midtone, and highlight adjustments,
split toning, and black/white point management. Designed to complement
the Karma Kolors node by offering finer-grained control over the tonal
range of an image.
"""
import torch
from PIL import Image
import numpy as np
def tensor2pil(image):
"""Convert tensor to PIL image."""
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
def pil2tensor(image):
"""Convert PIL image to tensor."""
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
class Karma_Tone_Curves:
"""
Professional tone curve adjustment node for fine-grained tonal control.
This node provides independent control over shadows, midtones, and highlights,
along with split toning capabilities and black/white point adjustment. It
operates like a simplified version of Lightroom's tone curve panel, giving
photographers and artists precise control over the luminance distribution
of their images.
Features:
- Independent shadow, midtone, and highlight brightness control
- Midtone contrast adjustment (S-curve)
- Shadow and highlight split toning with hue and saturation
- Black point and white point clipping
- Smooth tonal transitions with no banding
"""
@classmethod
def INPUT_TYPES(cls):
"""
Define the input parameters for the tone curves node.
Returns:
Dictionary containing required and optional input specifications
"""
return {
"required": {
"image": ("IMAGE", {"tooltip": "Input image to apply tone adjustments to"}),
"shadows": ("FLOAT", {
"default": 0.0,
"min": -1.0,
"max": 1.0,
"step": 0.01,
"tooltip": "Shadow brightness adjustment (-1 = crush, +1 = lift)"
}),
"midtones": ("FLOAT", {
"default": 0.0,
"min": -1.0,
"max": 1.0,
"step": 0.01,
"tooltip": "Midtone brightness adjustment (gamma correction)"
}),
"highlights": ("FLOAT", {
"default": 0.0,
"min": -1.0,
"max": 1.0,
"step": 0.01,
"tooltip": "Highlight brightness adjustment (-1 = pull down, +1 = push up)"
}),
"midtone_contrast": ("FLOAT", {
"default": 0.0,
"min": -1.0,
"max": 1.0,
"step": 0.01,
"tooltip": "Midtone contrast (S-curve): positive = more contrast, negative = flatter"
}),
"black_point": ("FLOAT", {
"default": 0.0,
"min": 0.0,
"max": 0.3,
"step": 0.005,
"tooltip": "Raise the black point to clip shadows (0 = pure black)"
}),
"white_point": ("FLOAT", {
"default": 1.0,
"min": 0.7,
"max": 1.0,
"step": 0.005,
"tooltip": "Lower the white point to clip highlights (1 = pure white)"
}),
"shadow_tint_hue": ("FLOAT", {
"default": 0.6,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"tooltip": "Hue for shadow split tone (0=red, 0.33=green, 0.6=blue, 0.83=magenta)"
}),
"shadow_tint_strength": ("FLOAT", {
"default": 0.0,
"min": 0.0,
"max": 0.5,
"step": 0.01,
"tooltip": "Intensity of shadow color tinting"
}),
"highlight_tint_hue": ("FLOAT", {
"default": 0.1,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"tooltip": "Hue for highlight split tone (0=red, 0.1=orange, 0.17=yellow)"
}),
"highlight_tint_strength": ("FLOAT", {
"default": 0.0,
"min": 0.0,
"max": 0.5,
"step": 0.01,
"tooltip": "Intensity of highlight color tinting"
}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "apply_tone_curves"
CATEGORY = "KarmaNodes/Post-Processing"
def apply_tone_curves(self, image: torch.Tensor, shadows: float, midtones: float,
highlights: float, midtone_contrast: float,
black_point: float, white_point: float,
shadow_tint_hue: float, shadow_tint_strength: float,
highlight_tint_hue: float, highlight_tint_strength: float) -> tuple:
"""
Apply tone curve adjustments to the input image.
Adjustments are applied in a specific order to maintain image quality:
black/white point clipping, then shadow/midtone/highlight adjustments,
then midtone contrast (S-curve), and finally split toning.
Args:
image: Input image tensor
shadows: Shadow brightness adjustment (-1 to 1)
midtones: Midtone brightness / gamma adjustment (-1 to 1)
highlights: Highlight brightness adjustment (-1 to 1)
midtone_contrast: S-curve contrast for midtones (-1 to 1)
black_point: Black point clipping level (0-0.3)
white_point: White point clipping level (0.7-1.0)
shadow_tint_hue: Hue value for shadow tint (0-1)
shadow_tint_strength: Strength of shadow tint (0-0.5)
highlight_tint_hue: Hue value for highlight tint (0-1)
highlight_tint_strength: Strength of highlight tint (0-0.5)
Returns:
Tuple containing the processed image tensor
"""
pil_image = tensor2pil(image)
img_array = np.array(pil_image, dtype=np.float32) / 255.0
is_color = len(img_array.shape) == 3 and img_array.shape[2] >= 3
# Step 1: Black/white point adjustment
if black_point > 0.001 or white_point < 0.999:
img_array = self.apply_point_clipping(img_array, black_point, white_point)
# Step 2: Shadow, midtone, highlight adjustments
if abs(shadows) > 0.001 or abs(midtones) > 0.001 or abs(highlights) > 0.001:
img_array = self.apply_zone_adjustments(img_array, shadows, midtones, highlights)
# Step 3: Midtone contrast (S-curve)
if abs(midtone_contrast) > 0.001:
img_array = self.apply_s_curve(img_array, midtone_contrast)
# Step 4: Split toning
if is_color:
if shadow_tint_strength > 0.001:
img_array = self.apply_split_tone(img_array, shadow_tint_hue,
shadow_tint_strength, zone="shadows")
if highlight_tint_strength > 0.001:
img_array = self.apply_split_tone(img_array, highlight_tint_hue,
highlight_tint_strength, zone="highlights")
result = Image.fromarray(np.clip(img_array * 255, 0, 255).astype(np.uint8))
result_tensor = pil2tensor(result)
return (result_tensor,)
@staticmethod
def apply_point_clipping(img: np.ndarray, black_point: float,
white_point: float) -> np.ndarray:
"""
Adjust black and white points by remapping the tonal range.
This compresses the full tonal range into the window defined by
the black and white points, effectively clipping the deepest
shadows and brightest highlights.
Args:
img: Image array in 0-1 float range
black_point: New minimum value (0-0.3)
white_point: New maximum value (0.7-1.0)
Returns:
Remapped image array
"""
range_width = max(white_point - black_point, 0.01)
result = (img - black_point) / range_width
return np.clip(result, 0, 1)
@staticmethod
def apply_zone_adjustments(img: np.ndarray, shadows: float,
midtones: float, highlights: float) -> np.ndarray:
"""
Apply independent brightness adjustments to shadow, midtone, and highlight zones.
Uses smooth weighting functions to isolate tonal zones and apply
adjustments only to the relevant range. The weighting functions
overlap smoothly to prevent visible banding or transitions.
Args:
img: Image array in 0-1 float range
shadows: Shadow adjustment (-1 to 1)
midtones: Midtone adjustment (-1 to 1)
highlights: Highlight adjustment (-1 to 1)
Returns:
Adjusted image array
"""
# Calculate luminance for zone detection
if len(img.shape) == 3 and img.shape[2] >= 3:
luminance = 0.299 * img[:, :, 0] + 0.587 * img[:, :, 1] + 0.114 * img[:, :, 2]
else:
luminance = img.copy() if len(img.shape) == 2 else img[:, :, 0]
# Smooth zone weight functions using cosine-based transitions
# Shadows: strongest at 0, fades to 0 by ~0.5
shadow_weight = np.clip(1.0 - luminance * 2.5, 0, 1) ** 1.5
# Highlights: 0 until ~0.5, full strength at 1.0
highlight_weight = np.clip((luminance - 0.4) * 2.5, 0, 1) ** 1.5
# Midtones: bell curve peaking at 0.5
midtone_weight = 1.0 - shadow_weight - highlight_weight
midtone_weight = np.clip(midtone_weight, 0, 1)
# Calculate combined adjustment
adjustment = (shadow_weight * shadows * 0.3 +
midtone_weight * midtones * 0.3 +
highlight_weight * highlights * 0.3)
if len(img.shape) == 3:
adjustment = adjustment[:, :, np.newaxis]
return np.clip(img + adjustment, 0, 1)
@staticmethod
def apply_s_curve(img: np.ndarray, strength: float) -> np.ndarray:
"""
Apply an S-curve contrast adjustment to the midtones.
Positive strength increases contrast in the midtone range (steepens
the curve around 0.5), while negative strength reduces contrast
(flattens the curve). The curve is anchored at the black and white
points to avoid clipping.
Args:
img: Image array in 0-1 float range
strength: S-curve intensity (-1 to 1)
Returns:
Contrast-adjusted image array
"""
# Use a sigmoid-based S-curve centered at 0.5
# The strength parameter controls the steepness
contrast_factor = 1.0 + strength * 2.0
# Apply power-based S-curve: simple and effective
if contrast_factor > 0:
# Remap around 0.5 pivot point
centered = img - 0.5
# Apply contrast
result = 0.5 + centered * contrast_factor
# Smooth clipping using tanh to avoid hard edges
result = 0.5 + 0.5 * np.tanh((result - 0.5) * 2.0) / np.tanh(1.0)
else:
result = img
return np.clip(result, 0, 1)
@staticmethod
def apply_split_tone(img: np.ndarray, hue: float, strength: float,
zone: str = "shadows") -> np.ndarray:
"""
Apply color tinting to a specific tonal zone.
Split toning adds a color cast to either shadows or highlights
independently. This is a classic photographic technique used to
create mood, such as cool blue shadows with warm golden highlights.
Args:
img: Image array in 0-1 float range (must be RGB)
hue: Color hue to apply (0-1, where 0=red, 0.33=green, 0.67=blue)
strength: Tint intensity (0-0.5)
zone: Which zone to tint: "shadows" or "highlights"
Returns:
Tinted image array
"""
# Calculate luminance
luminance = 0.299 * img[:, :, 0] + 0.587 * img[:, :, 1] + 0.114 * img[:, :, 2]
# Create zone mask
if zone == "shadows":
mask = np.clip(1.0 - luminance * 2.0, 0, 1)
else:
mask = np.clip((luminance - 0.5) * 2.0, 0, 1)
# Convert hue to RGB color
# Simple hue-to-RGB conversion (fully saturated colors)
hue_360 = hue * 6.0
x = 1.0 - abs(hue_360 % 2.0 - 1.0)
if hue_360 < 1:
tint_color = np.array([1.0, x, 0.0])
elif hue_360 < 2:
tint_color = np.array([x, 1.0, 0.0])
elif hue_360 < 3:
tint_color = np.array([0.0, 1.0, x])
elif hue_360 < 4:
tint_color = np.array([0.0, x, 1.0])
elif hue_360 < 5:
tint_color = np.array([x, 0.0, 1.0])
else:
tint_color = np.array([1.0, 0.0, x])
# Apply tint: blend toward tint color based on mask and strength
tint_layer = tint_color[np.newaxis, np.newaxis, :] * np.ones_like(img)
blend_mask = mask[:, :, np.newaxis] * strength
result = img * (1.0 - blend_mask) + tint_layer * blend_mask
return np.clip(result, 0, 1)
+130 -1
View File
@@ -9,6 +9,9 @@ ComfyUI-KarmaNodes provides a comprehensive suite of nodes for advanced image ge
- **Karma KSampler Cycle**: Specialized KSampler that performs multiple sampling cycles with progressive upscaling between cycles, enabling high-quality, high-resolution image generation with better detail preservation
- **Karma Film Grain**: Professional film grain simulation for authentic analog film texture and cinematic aesthetics
- **Karma Kolors**: Advanced color grading and correction tools for professional-grade color enhancement
- **Karma Lens FX**: Realistic lens effect simulation — chromatic aberration, vignetting, barrel/pincushion distortion, and halation
- **Karma Tone Curves**: Surgical tonal control — independent shadow/midtone/highlight adjustments, S-curve contrast, and split toning
- **Karma Film Emulation**: One-click cinematic film stock emulation with 10 iconic presets from Kodak, Fuji, CineStill, and Ilford
## Features
@@ -35,6 +38,9 @@ ComfyUI-KarmaNodes provides a comprehensive suite of nodes for advanced image ge
- **Sharpening Filter**: Optional unsharp mask filter between cycles
- **Film Grain Effects**: Realistic analog film grain simulation
- **Color Grading**: Professional color correction and enhancement
- **Lens Effects**: Chromatic aberration, vignetting, distortion, and halation
- **Tone Curves**: Independent shadow/midtone/highlight control with split toning
- **Film Emulation**: One-click cinematic looks from iconic film stocks
- **Tiled VAE Support**: Handle large images with tiled VAE processing
- **Memory Management**: Automatic device management for optimal performance
@@ -228,6 +234,104 @@ Professional color grading and correction tools for precise image enhancement.
- **Landscape Vibrancy**: Increase saturation (+5-10%) with slight contrast boost
- **Vintage Look**: Warm temperature (3000K-4000K) with reduced saturation (-5-10%)
### 🔍 Karma Lens FX
Simulate real-world optical imperfections for authentic photographic character. Recreates the optical characteristics of physical camera lenses.
#### Features
- **Chromatic Aberration**: Color fringing that increases toward image edges, simulating lateral CA
- **Vignetting**: Smooth radial edge darkening with adjustable falloff curve
- **Lens Distortion**: Barrel (positive) and pincushion (negative) geometric distortion
- **Halation**: Highlight bloom/glow effect that simulates light scatter in film and lenses
- **Independent Controls**: Each effect can be dialed in precisely without affecting others
#### Parameters
- **Chromatic Aberration** (0-20): Color fringing strength in pixels
- **Vignette Strength** (0-1): Edge darkening intensity
- **Vignette Falloff** (0.5-5.0): Gradient steepness (higher = tighter bright center)
- **Distortion** (-1 to 1): Barrel (+) or pincushion (-) distortion amount
- **Halation Strength** (0-1): Highlight bloom intensity
- **Halation Threshold** (0-1): Brightness level above which bloom is applied
- **Halation Radius** (1-50): Spread of the bloom glow in pixels
#### Usage Tips
- **Subtle Realism**: Chromatic aberration 1-3, vignette 0.1-0.2 for natural lens character
- **Vintage Lens Look**: Chromatic aberration 5-10, vignette 0.3-0.5, distortion 0.1-0.2
- **Dreamy/Ethereal**: Halation strength 0.3-0.5 with lower threshold (0.6-0.7)
- **Night Photography**: Halation strength 0.2-0.4 with high threshold (0.85-0.95) for light glow
- **Cinematic**: Combine vignette (0.2-0.3) with subtle halation (0.1-0.2) for movie look
### 📈 Karma Tone Curves
Surgical tonal control with independent shadow, midtone, and highlight adjustments — like a simplified version of Lightroom's tone curve panel.
#### Features
- **Zone-Based Adjustments**: Independent shadow, midtone, and highlight brightness control
- **S-Curve Contrast**: Midtone contrast adjustment for punchy or flat looks
- **Black/White Point**: Clip shadows and highlights for controlled dynamic range
- **Split Toning**: Independently tint shadows and highlights with any hue
- **Smooth Transitions**: Cosine-weighted zone blending prevents banding
#### Parameters
- **Shadows** (-1 to 1): Shadow brightness (negative = crush blacks, positive = lift shadows)
- **Midtones** (-1 to 1): Midtone brightness (gamma-like adjustment)
- **Highlights** (-1 to 1): Highlight brightness (negative = recover, positive = blow out)
- **Midtone Contrast** (-1 to 1): S-curve contrast (positive = punchier, negative = flatter)
- **Black Point** (0-0.3): Raise the darkest level for a faded/matte look
- **White Point** (0.7-1.0): Lower the brightest level to tame highlights
- **Shadow Tint Hue** (0-1): Color hue for shadow tinting
- **Shadow Tint Strength** (0-0.5): Intensity of shadow color tint
- **Highlight Tint Hue** (0-1): Color hue for highlight tinting
- **Highlight Tint Strength** (0-0.5): Intensity of highlight color tint
#### Usage Tips
- **Faded Film Look**: Black point 0.05-0.1, shadows +0.1, contrast -0.1
- **Punchy Modern**: Midtone contrast +0.3-0.5, shadows -0.1
- **Teal & Orange Split Tone**: Shadow tint hue 0.52, highlight tint hue 0.08, both at strength 0.1-0.15
- **Matte Finish**: Black point 0.05-0.08, white point 0.92-0.95
- **High Key**: Shadows +0.2, midtones +0.1, highlights +0.1
### 🎞️ Karma Film Emulation
One-click cinematic film stock emulation with 10 carefully calibrated presets. Each preset replicates the complete color science of an iconic analog film — color response, contrast curve, grain, and special characteristics.
#### Supported Film Stocks
**Color Negative:**
| Film Stock | Character |
|---|---|
| **Kodak Portra 400** | Natural skin tones, soft contrast, warm pastels. Portrait gold standard. |
| **Kodak Ektar 100** | Ultra-vivid colors, fine grain, high saturation. Landscape and travel. |
| **Kodak Gold 200** | Warm, golden highlights, nostalgic everyday look. |
| **Fuji Velvia 50** | Extreme saturation, deep contrast, vivid greens/blues. Landscape legend. |
| **Fuji Pro 400H** | Soft pastels, airy skin tones, subtle greens. Wedding favorite. |
| **Fuji Superia 400** | Cool tones, strong greens/blues, punchy contrast. Classic consumer film. |
**Cinema:**
| Film Stock | Character |
|---|---|
| **CineStill 800T** | Tungsten-balanced, teal shadows, warm highlights, halation on lights. Night photography icon. |
| **Kodak Vision3 500T** | Professional cinema negative. Refined color, modern movie look. |
**Black & White:**
| Film Stock | Character |
|---|---|
| **Kodak Tri-X 400** | Rich tones, beautiful grain, deep blacks. Street photography legend. |
| **Ilford HP5 Plus** | Smooth tones, moderate grain, excellent latitude. Versatile B&W workhorse. |
#### Parameters
- **Film Stock**: Select the film to emulate from the dropdown
- **Intensity** (0-1.5): Blend strength (0 = original, 1 = full emulation, >1 = exaggerated)
- **Grain Override** (-1 to 1): Override grain amount (-1 = use film's default, 0+ = custom)
- **Seed** (0-2³¹-1): Random seed for reproducible grain patterns
#### Usage Tips
- **Natural Look**: Intensity 0.7-0.9 for believable film emulation
- **Heavy Stylization**: Intensity 1.2-1.5 for exaggerated film character
- **Clean Film Color**: Use grain override of 0 to get film color without grain
- **CineStill Night Shots**: Works best with images that have bright light sources (halation glow)
- **B&W Conversion**: Tri-X or HP5 presets automatically convert to monochrome with proper grain
### Post-Processing Workflow Examples
#### Basic Enhancement Chain
@@ -254,6 +358,28 @@ Professional color grading and correction tools for precise image enhancement.
Saturation: -3% Seed: 42
```
#### Full Camera Simulation Pipeline
```
[Generated Image] → [Karma Tone Curves] → [Karma Kolors] → [Karma Lens FX] → [Karma Film Grain] → [Save Image]
```
Apply tonal adjustments first, then color grading, then optical effects, and finally grain — mimicking the order of a real camera and film pipeline.
#### One-Click Film Emulation
```
[Generated Image] → [Karma Film Emulation] → [Save Image]
↓
Film Stock: CineStill 800T
Intensity: 0.85
Grain Override: -1 (use default)
```
For quick cinematic looks without manual tuning.
#### Stacked Precision + Emulation
```
[Generated Image] → [Karma Tone Curves] → [Karma Film Emulation] → [Karma Lens FX] → [Save Image]
```
Use Tone Curves for surgical adjustments, then Film Emulation for the overall look, and Lens FX for optical character.
## Requirements
- **Python**: 3.9+
@@ -310,6 +436,9 @@ Professional color grading and correction tools for precise image enhancement.
- **Color corrections too harsh**: Use smaller adjustment increments (±1-3%)
- **White balance not working**: Ensure temperature value is within 2000K-10000K range
- **Scipy warning for film grain**: Install scipy for better grain quality: `pip install scipy`
- **Lens FX chromatic aberration not visible**: Increase value above 2-3 for noticeable effect
- **Tone Curves banding**: Use smaller adjustment values; the node uses smooth blending to minimize this
- **Film Emulation too intense**: Lower the intensity slider to 0.6-0.8 for a more subtle look
## Contributing
@@ -347,4 +476,4 @@ Thank you for being part of the ComfyUI-KarmaNodes community! 🙏
---
**Note**: This node is designed for advanced users familiar with ComfyUI workflows. Basic knowledge of diffusion model sampling is recommended.
**Note**: This node is designed for advanced users familiar with ComfyUI workflows. Basic knowledge of diffusion model sampling is recommended.
+10 -1
View File
@@ -1,15 +1,24 @@
from .KarmaKSamplerCycle import Karma_KSampler_Cycle
from .KarmaFilmGrain import Karma_Film_Grain
from .KarmaKolors import Karma_Kolors
from .KarmaLensFX import Karma_Lens_FX
from .KarmaToneCurves import Karma_Tone_Curves
from .KarmaFilmEmulation import Karma_Film_Emulation
NODE_CLASS_MAPPINGS = {
"Karma-KSampler-Cycle": Karma_KSampler_Cycle,
"Karma-Film-Grain": Karma_Film_Grain,
"Karma-Kolors": Karma_Kolors,
"Karma-Lens-FX": Karma_Lens_FX,
"Karma-Tone-Curves": Karma_Tone_Curves,
"Karma-Film-Emulation": Karma_Film_Emulation,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Karma-KSampler-Cycle": "Karma KSampler Cycle",
"Karma-Film-Grain": "Karma Film Grain",
"Karma-Kolors": "Karma Kolors",
}
"Karma-Lens-FX": "Karma Lens FX",
"Karma-Tone-Curves": "Karma Tone Curves",
"Karma-Film-Emulation": "Karma Film Emulation",
}