film-simulation / film_simulation.py
sinanonur's picture
Update film_simulation.py
03404bd verified
import numpy as np
from PIL import Image, ImageEnhance, ImageOps, ImageFilter
import colour
import json
import os
import psutil
from scipy.interpolate import interp1d
import piexif
import argparse
import multiprocessing
from functools import partial
class FilmProfile:
def __init__(self, name, color_curves, contrast, saturation, chromatic_aberration, blur, base_color, grain_amount, grain_size, advanced_curve=None):
self.name = name
self.color_curves = color_curves
self.contrast = contrast
self.saturation = saturation
self.chromatic_aberration = chromatic_aberration
self.blur = blur
self.base_color = base_color
self.grain_amount = grain_amount
self.grain_size = grain_size
self.advanced_curve = advanced_curve
def create_curve(curve_data):
x = np.array(curve_data['x'])
y = np.array(curve_data['y'])
return interp1d(x, y, kind='cubic', bounds_error=False, fill_value=(y[0], y[-1]))
def load_film_profiles_from_json(json_path):
with open(json_path, 'r') as f:
profiles_data = json.load(f)
profiles = {}
for name, data in profiles_data.items():
color_curves = {
channel: create_curve(curve_data)
for channel, curve_data in data['color_curves'].items()
}
advanced_curve = data.get('advanced_curve', None)
profiles[name] = FilmProfile(
name,
color_curves=color_curves,
contrast=data['contrast'],
saturation=data['saturation'],
chromatic_aberration=data.get('chromatic_aberration', 0),
blur=data.get('blur', 0),
base_color=tuple(data.get('base_color', (255, 255, 255))),
grain_amount=data.get('grain_amount', 0),
grain_size=data.get('grain_size', 1),
advanced_curve=advanced_curve
)
return profiles
def load_default_profiles():
default_json_path = '12-film-profiles.json'
return load_film_profiles_from_json(default_json_path)
def apply_color_curves(image, curves):
result = np.zeros_like(image)
for i, channel in enumerate(['R', 'G', 'B']):
result[:,:,i] = curves[channel](image[:,:,i])
return result
def interpolate_circular(x, y, new_x):
x_extended = np.concatenate((x, x + 360))
y_extended = np.concatenate((y, y))
interp_func = interp1d(x_extended, y_extended, kind='cubic')
return interp_func(new_x % 360)
def apply_advanced_curve(image, advanced_curve):
hsv_image = colour.RGB_to_HSV(image)
hue_values = np.array(advanced_curve['hue_values'])
saturation_multipliers = np.array(advanced_curve['saturation_multipliers'])
hue_shifts = np.array(advanced_curve['hue_shifts'])
value_multipliers = np.array(advanced_curve['value_multipliers'])
hue = hsv_image[:,:,0] * 360
saturation = hsv_image[:,:,1]
value = hsv_image[:,:,2]
interp_saturation_multipliers = interpolate_circular(hue_values, saturation_multipliers, hue)
max_saturation = 1.0
interp_saturation_multipliers = np.clip(interp_saturation_multipliers, 0, max_saturation / saturation)
saturation *= interp_saturation_multipliers
interp_hue_shifts = interpolate_circular(hue_values, hue_shifts, hue)
hue = (hue + interp_hue_shifts) % 360
interp_value_multipliers = interpolate_circular(hue_values, value_multipliers, hue)
max_value = 1.0
interp_value_multipliers = np.clip(interp_value_multipliers, 0, max_value / value)
value *= interp_value_multipliers
hsv_image[:,:,0] = hue / 360
hsv_image[:,:,1] = saturation
hsv_image[:,:,2] = value
return colour.HSV_to_RGB(hsv_image)
def apply_chromatic_aberration_pil(img, strength):
width, height = img.size
center_x, center_y = width // 2, height // 2
r, g, b = img.split()
def create_displacement(x, y):
return int(strength * ((x - center_x) ** 2 + (y - center_y) ** 2) ** 0.5 / (width + height))
r = r.transform(img.size, Image.AFFINE, (1, 0, create_displacement(0, 0), 0, 1, 0))
b = b.transform(img.size, Image.AFFINE, (1, 0, -create_displacement(0, 0), 0, 1, 0))
return Image.merge("RGB", (r, g, b))
def add_film_grain(image, amount=0.1, size=1):
width, height = image.size
grain = np.random.normal(0, amount, (height//size + 1, width//size + 1, 3))
grain = np.repeat(np.repeat(grain, size, axis=0), size, axis=1)
grain = grain[:height, :width, :]
img_array = np.array(image).astype(np.float32) / 255.0
grainy_image = np.clip(img_array + grain, 0, 1) * 255
return Image.fromarray(grainy_image.astype(np.uint8))
def adjust_color_temperature(image, temperature):
r_multiplier = 1 + (temperature - 6500) / 100 * 0.01
b_multiplier = 1 - (temperature - 6500) / 100 * 0.01
g_multiplier = 1
r, g, b = image.split()
r = r.point(lambda i: min(255, int(i * r_multiplier)))
g = g.point(lambda i: min(255, int(i * g_multiplier)))
b = b.point(lambda i: min(255, int(i * b_multiplier)))
return Image.merge('RGB', (r, g, b))
def apply_base_color(image, base_color):
base = Image.new('RGB', image.size, base_color)
return Image.blend(image, base, 0.1)
def cross_process(image):
contrast_enhancer = ImageEnhance.Contrast(image)
image = contrast_enhancer.enhance(1.5)
r, g, b = image.split()
r = r.point(lambda i: min(255, int(i * 1.2)))
g = g.point(lambda i: int(i * 0.9))
b = b.point(lambda i: min(255, int(i * 1.1)))
image = Image.merge('RGB', (r, g, b))
saturation_enhancer = ImageEnhance.Color(image)
image = saturation_enhancer.enhance(1.3)
return image
def apply_film_profile(img, profile, chroma_override=None, blur_override=None, color_temp=6500, cross_process_flag=False, curve_type="auto"):
img_array = np.array(img).astype(np.float32) / 255.0
img_linear = colour.models.eotf_sRGB(img_array)
if curve_type == "advanced" or (curve_type == "auto" and profile.advanced_curve):
img_color_adjusted = apply_advanced_curve(img_linear, profile.advanced_curve)
elif curve_type == "color" or (curve_type == "auto" and not profile.advanced_curve):
img_color_adjusted = apply_color_curves(img_linear, profile.color_curves)
elif curve_type == "both":
if profile.color_curves:
img_color_adjusted = apply_color_curves(img_linear, profile.color_curves)
if profile.advanced_curve:
img_color_adjusted = apply_advanced_curve(img_color_adjusted, profile.advanced_curve)
img_srgb = colour.models.eotf_inverse_sRGB(img_color_adjusted)
img_pil = Image.fromarray((img_srgb * 255).astype(np.uint8))
enhancer = ImageEnhance.Contrast(img_pil)
img_contrast = enhancer.enhance(profile.contrast)
enhancer = ImageEnhance.Color(img_contrast)
img_saturated = enhancer.enhance(profile.saturation)
chroma_strength = chroma_override if chroma_override is not None else profile.chromatic_aberration
if chroma_strength > 0:
img_saturated = apply_chromatic_aberration_pil(img_saturated, chroma_strength)
blur_amount = blur_override if blur_override is not None else profile.blur
if blur_amount > 0:
img_saturated = img_saturated.filter(ImageFilter.GaussianBlur(radius=blur_amount))
img_saturated = apply_base_color(img_saturated, profile.base_color)
img_saturated = add_film_grain(img_saturated, amount=profile.grain_amount, size=profile.grain_size)
if color_temp is not None:
img_saturated = adjust_color_temperature(img_saturated, color_temp)
if cross_process_flag:
img_saturated = cross_process(img_saturated)
return img_saturated
def process_images(image, profiles_json=None, selected_profile=None, chroma_override=None, blur_override=None, color_temp=6500, cross_process_flag=False, curve_type="auto"):
if profiles_json:
film_profiles = load_film_profiles_from_json(profiles_json)
else:
film_profiles = load_default_profiles()
input_path_base = "output"
processed_images = []
for profile_name, profile in film_profiles.items():
if selected_profile and selected_profile != "All" and selected_profile != profile_name:
continue
processed_image = apply_film_profile(image, profile, chroma_override, blur_override, color_temp, cross_process_flag, curve_type)
processed_images.append(processed_image)
return processed_images