Spaces:
Runtime error
Runtime error
updated script to be used as library
Browse files- film_simulation.py +41 -44
film_simulation.py
CHANGED
|
@@ -31,7 +31,7 @@ def create_curve(curve_data):
|
|
| 31 |
def load_film_profiles_from_json(json_path):
|
| 32 |
with open(json_path, 'r') as f:
|
| 33 |
profiles_data = json.load(f)
|
| 34 |
-
|
| 35 |
profiles = {}
|
| 36 |
for name, data in profiles_data.items():
|
| 37 |
color_curves = {
|
|
@@ -60,7 +60,6 @@ def apply_color_curves(image, curves):
|
|
| 60 |
return result
|
| 61 |
|
| 62 |
def interpolate_circular(x, y, new_x):
|
| 63 |
-
# Interpolate considering the circular nature of hue values (0-360 degrees)
|
| 64 |
x_extended = np.concatenate((x, x + 360))
|
| 65 |
y_extended = np.concatenate((y, y))
|
| 66 |
interp_func = interp1d(x_extended, y_extended, kind='cubic')
|
|
@@ -73,33 +72,24 @@ def apply_advanced_curve(image, advanced_curve):
|
|
| 73 |
hue_shifts = np.array(advanced_curve['hue_shifts'])
|
| 74 |
value_multipliers = np.array(advanced_curve['value_multipliers'])
|
| 75 |
|
| 76 |
-
hue = hsv_image[:,:,0] * 360
|
| 77 |
saturation = hsv_image[:,:,1]
|
| 78 |
value = hsv_image[:,:,2]
|
| 79 |
|
| 80 |
-
# Interpolate saturation multipliers
|
| 81 |
interp_saturation_multipliers = interpolate_circular(hue_values, saturation_multipliers, hue)
|
| 82 |
-
|
| 83 |
-
# Apply saturation multipliers with a curve to prevent blowout
|
| 84 |
max_saturation = 1.0
|
| 85 |
interp_saturation_multipliers = np.clip(interp_saturation_multipliers, 0, max_saturation / saturation)
|
| 86 |
-
|
| 87 |
saturation *= interp_saturation_multipliers
|
| 88 |
|
| 89 |
-
# Interpolate hue shifts and apply them
|
| 90 |
interp_hue_shifts = interpolate_circular(hue_values, hue_shifts, hue)
|
| 91 |
hue = (hue + interp_hue_shifts) % 360
|
| 92 |
|
| 93 |
-
# Interpolate value multipliers and apply them
|
| 94 |
interp_value_multipliers = interpolate_circular(hue_values, value_multipliers, hue)
|
| 95 |
-
|
| 96 |
-
# Apply value multipliers with a curve to prevent blowout
|
| 97 |
max_value = 1.0
|
| 98 |
interp_value_multipliers = np.clip(interp_value_multipliers, 0, max_value / value)
|
| 99 |
-
|
| 100 |
value *= interp_value_multipliers
|
| 101 |
|
| 102 |
-
hsv_image[:,:,0] = hue / 360
|
| 103 |
hsv_image[:,:,1] = saturation
|
| 104 |
hsv_image[:,:,2] = value
|
| 105 |
|
|
@@ -108,23 +98,23 @@ def apply_advanced_curve(image, advanced_curve):
|
|
| 108 |
def apply_chromatic_aberration_pil(img, strength):
|
| 109 |
width, height = img.size
|
| 110 |
center_x, center_y = width // 2, height // 2
|
| 111 |
-
|
| 112 |
r, g, b = img.split()
|
| 113 |
-
|
| 114 |
def create_displacement(x, y):
|
| 115 |
return int(strength * ((x - center_x) ** 2 + (y - center_y) ** 2) ** 0.5 / (width + height))
|
| 116 |
-
|
| 117 |
r = r.transform(img.size, Image.AFFINE, (1, 0, create_displacement(0, 0), 0, 1, 0))
|
| 118 |
b = b.transform(img.size, Image.AFFINE, (1, 0, -create_displacement(0, 0), 0, 1, 0))
|
| 119 |
-
|
| 120 |
return Image.merge("RGB", (r, g, b))
|
| 121 |
|
| 122 |
def add_film_grain(image, amount=0.1, size=1):
|
| 123 |
width, height = image.size
|
| 124 |
-
grain = np.random.normal(0, amount, (height//size, width//size, 3))
|
| 125 |
grain = np.repeat(np.repeat(grain, size, axis=0), size, axis=1)
|
| 126 |
grain = grain[:height, :width, :]
|
| 127 |
-
|
| 128 |
img_array = np.array(image).astype(np.float32) / 255.0
|
| 129 |
grainy_image = np.clip(img_array + grain, 0, 1) * 255
|
| 130 |
return Image.fromarray(grainy_image.astype(np.uint8))
|
|
@@ -133,40 +123,39 @@ def adjust_color_temperature(image, temperature):
|
|
| 133 |
r_multiplier = 1 + (temperature - 6500) / 100 * 0.01
|
| 134 |
b_multiplier = 1 - (temperature - 6500) / 100 * 0.01
|
| 135 |
g_multiplier = 1
|
| 136 |
-
|
| 137 |
r, g, b = image.split()
|
| 138 |
-
|
| 139 |
r = r.point(lambda i: min(255, int(i * r_multiplier)))
|
| 140 |
g = g.point(lambda i: min(255, int(i * g_multiplier)))
|
| 141 |
b = b.point(lambda i: min(255, int(i * b_multiplier)))
|
| 142 |
-
|
| 143 |
return Image.merge('RGB', (r, g, b))
|
| 144 |
|
| 145 |
def apply_base_color(image, base_color):
|
| 146 |
base = Image.new('RGB', image.size, base_color)
|
| 147 |
-
return Image.blend(image, base, 0.1)
|
| 148 |
|
| 149 |
def cross_process(image):
|
| 150 |
contrast_enhancer = ImageEnhance.Contrast(image)
|
| 151 |
image = contrast_enhancer.enhance(1.5)
|
| 152 |
-
|
| 153 |
r, g, b = image.split()
|
| 154 |
r = r.point(lambda i: min(255, int(i * 1.2)))
|
| 155 |
g = g.point(lambda i: int(i * 0.9))
|
| 156 |
b = b.point(lambda i: min(255, int(i * 1.1)))
|
| 157 |
-
|
| 158 |
image = Image.merge('RGB', (r, g, b))
|
| 159 |
-
|
| 160 |
saturation_enhancer = ImageEnhance.Color(image)
|
| 161 |
image = saturation_enhancer.enhance(1.3)
|
| 162 |
-
|
| 163 |
return image
|
| 164 |
|
| 165 |
def apply_film_profile(img, profile, chroma_override=None, blur_override=None, color_temp=6500, cross_process_flag=False, curve_type="auto"):
|
| 166 |
img_array = np.array(img).astype(np.float32) / 255.0
|
| 167 |
-
|
| 168 |
img_linear = colour.models.eotf_sRGB(img_array)
|
| 169 |
-
|
| 170 |
if curve_type == "advanced" or (curve_type == "auto" and profile.advanced_curve):
|
| 171 |
img_color_adjusted = apply_advanced_curve(img_linear, profile.advanced_curve)
|
| 172 |
elif curve_type == "color" or (curve_type == "auto" and not profile.advanced_curve):
|
|
@@ -176,45 +165,53 @@ def apply_film_profile(img, profile, chroma_override=None, blur_override=None, c
|
|
| 176 |
img_color_adjusted = apply_color_curves(img_linear, profile.color_curves)
|
| 177 |
if profile.advanced_curve:
|
| 178 |
img_color_adjusted = apply_advanced_curve(img_color_adjusted, profile.advanced_curve)
|
| 179 |
-
|
| 180 |
img_srgb = colour.models.eotf_inverse_sRGB(img_color_adjusted)
|
| 181 |
-
|
| 182 |
img_pil = Image.fromarray((img_srgb * 255).astype(np.uint8))
|
| 183 |
-
|
| 184 |
enhancer = ImageEnhance.Contrast(img_pil)
|
| 185 |
img_contrast = enhancer.enhance(profile.contrast)
|
| 186 |
-
|
| 187 |
enhancer = ImageEnhance.Color(img_contrast)
|
| 188 |
img_saturated = enhancer.enhance(profile.saturation)
|
| 189 |
-
|
| 190 |
chroma_strength = chroma_override if chroma_override is not None else profile.chromatic_aberration
|
| 191 |
if chroma_strength > 0:
|
| 192 |
img_saturated = apply_chromatic_aberration_pil(img_saturated, chroma_strength)
|
| 193 |
-
|
| 194 |
blur_amount = blur_override if blur_override is not None else profile.blur
|
| 195 |
if blur_amount > 0:
|
| 196 |
img_saturated = img_saturated.filter(ImageFilter.GaussianBlur(radius=blur_amount))
|
| 197 |
-
|
| 198 |
img_saturated = apply_base_color(img_saturated, profile.base_color)
|
| 199 |
-
|
| 200 |
img_saturated = add_film_grain(img_saturated, amount=profile.grain_amount, size=profile.grain_size)
|
| 201 |
-
|
| 202 |
img_saturated = adjust_color_temperature(img_saturated, color_temp)
|
| 203 |
-
|
| 204 |
if cross_process_flag:
|
| 205 |
img_saturated = cross_process(img_saturated)
|
| 206 |
-
|
| 207 |
return img_saturated
|
| 208 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
def process_image(args):
|
| 210 |
image_path, profile, chroma_override, blur_override, color_temp, cross_process_flag, curve_type, output_filename = args
|
| 211 |
with Image.open(image_path) as img:
|
| 212 |
exif_data = img.getexif()
|
| 213 |
-
|
| 214 |
orientation = exif_data.get(274, 1)
|
| 215 |
if orientation in [3, 6, 8]:
|
| 216 |
img = img.rotate({3: 180, 6: 270, 8: 90}[orientation], expand=True)
|
| 217 |
-
|
| 218 |
processed_image = apply_film_profile(img, profile, chroma_override, blur_override, color_temp, cross_process_flag, curve_type)
|
| 219 |
|
| 220 |
if exif_data:
|
|
@@ -231,12 +228,12 @@ def get_memory_usage():
|
|
| 231 |
def get_optimal_pool_size(target_memory_usage=75):
|
| 232 |
available_memory = 100 - get_memory_usage()
|
| 233 |
cpu_count = multiprocessing.cpu_count()
|
| 234 |
-
|
| 235 |
for i in range(cpu_count, 0, -1):
|
| 236 |
estimated_memory_usage = get_memory_usage() + (available_memory / cpu_count) * i
|
| 237 |
if estimated_memory_usage <= target_memory_usage:
|
| 238 |
return i
|
| 239 |
-
|
| 240 |
return 1
|
| 241 |
|
| 242 |
if __name__ == "__main__":
|
|
|
|
| 31 |
def load_film_profiles_from_json(json_path):
|
| 32 |
with open(json_path, 'r') as f:
|
| 33 |
profiles_data = json.load(f)
|
| 34 |
+
|
| 35 |
profiles = {}
|
| 36 |
for name, data in profiles_data.items():
|
| 37 |
color_curves = {
|
|
|
|
| 60 |
return result
|
| 61 |
|
| 62 |
def interpolate_circular(x, y, new_x):
|
|
|
|
| 63 |
x_extended = np.concatenate((x, x + 360))
|
| 64 |
y_extended = np.concatenate((y, y))
|
| 65 |
interp_func = interp1d(x_extended, y_extended, kind='cubic')
|
|
|
|
| 72 |
hue_shifts = np.array(advanced_curve['hue_shifts'])
|
| 73 |
value_multipliers = np.array(advanced_curve['value_multipliers'])
|
| 74 |
|
| 75 |
+
hue = hsv_image[:,:,0] * 360
|
| 76 |
saturation = hsv_image[:,:,1]
|
| 77 |
value = hsv_image[:,:,2]
|
| 78 |
|
|
|
|
| 79 |
interp_saturation_multipliers = interpolate_circular(hue_values, saturation_multipliers, hue)
|
|
|
|
|
|
|
| 80 |
max_saturation = 1.0
|
| 81 |
interp_saturation_multipliers = np.clip(interp_saturation_multipliers, 0, max_saturation / saturation)
|
|
|
|
| 82 |
saturation *= interp_saturation_multipliers
|
| 83 |
|
|
|
|
| 84 |
interp_hue_shifts = interpolate_circular(hue_values, hue_shifts, hue)
|
| 85 |
hue = (hue + interp_hue_shifts) % 360
|
| 86 |
|
|
|
|
| 87 |
interp_value_multipliers = interpolate_circular(hue_values, value_multipliers, hue)
|
|
|
|
|
|
|
| 88 |
max_value = 1.0
|
| 89 |
interp_value_multipliers = np.clip(interp_value_multipliers, 0, max_value / value)
|
|
|
|
| 90 |
value *= interp_value_multipliers
|
| 91 |
|
| 92 |
+
hsv_image[:,:,0] = hue / 360
|
| 93 |
hsv_image[:,:,1] = saturation
|
| 94 |
hsv_image[:,:,2] = value
|
| 95 |
|
|
|
|
| 98 |
def apply_chromatic_aberration_pil(img, strength):
|
| 99 |
width, height = img.size
|
| 100 |
center_x, center_y = width // 2, height // 2
|
| 101 |
+
|
| 102 |
r, g, b = img.split()
|
| 103 |
+
|
| 104 |
def create_displacement(x, y):
|
| 105 |
return int(strength * ((x - center_x) ** 2 + (y - center_y) ** 2) ** 0.5 / (width + height))
|
| 106 |
+
|
| 107 |
r = r.transform(img.size, Image.AFFINE, (1, 0, create_displacement(0, 0), 0, 1, 0))
|
| 108 |
b = b.transform(img.size, Image.AFFINE, (1, 0, -create_displacement(0, 0), 0, 1, 0))
|
| 109 |
+
|
| 110 |
return Image.merge("RGB", (r, g, b))
|
| 111 |
|
| 112 |
def add_film_grain(image, amount=0.1, size=1):
|
| 113 |
width, height = image.size
|
| 114 |
+
grain = np.random.normal(0, amount, (height//size + 1, width//size + 1, 3))
|
| 115 |
grain = np.repeat(np.repeat(grain, size, axis=0), size, axis=1)
|
| 116 |
grain = grain[:height, :width, :]
|
| 117 |
+
|
| 118 |
img_array = np.array(image).astype(np.float32) / 255.0
|
| 119 |
grainy_image = np.clip(img_array + grain, 0, 1) * 255
|
| 120 |
return Image.fromarray(grainy_image.astype(np.uint8))
|
|
|
|
| 123 |
r_multiplier = 1 + (temperature - 6500) / 100 * 0.01
|
| 124 |
b_multiplier = 1 - (temperature - 6500) / 100 * 0.01
|
| 125 |
g_multiplier = 1
|
| 126 |
+
|
| 127 |
r, g, b = image.split()
|
| 128 |
+
|
| 129 |
r = r.point(lambda i: min(255, int(i * r_multiplier)))
|
| 130 |
g = g.point(lambda i: min(255, int(i * g_multiplier)))
|
| 131 |
b = b.point(lambda i: min(255, int(i * b_multiplier)))
|
| 132 |
+
|
| 133 |
return Image.merge('RGB', (r, g, b))
|
| 134 |
|
| 135 |
def apply_base_color(image, base_color):
|
| 136 |
base = Image.new('RGB', image.size, base_color)
|
| 137 |
+
return Image.blend(image, base, 0.1)
|
| 138 |
|
| 139 |
def cross_process(image):
|
| 140 |
contrast_enhancer = ImageEnhance.Contrast(image)
|
| 141 |
image = contrast_enhancer.enhance(1.5)
|
| 142 |
+
|
| 143 |
r, g, b = image.split()
|
| 144 |
r = r.point(lambda i: min(255, int(i * 1.2)))
|
| 145 |
g = g.point(lambda i: int(i * 0.9))
|
| 146 |
b = b.point(lambda i: min(255, int(i * 1.1)))
|
| 147 |
+
|
| 148 |
image = Image.merge('RGB', (r, g, b))
|
| 149 |
+
|
| 150 |
saturation_enhancer = ImageEnhance.Color(image)
|
| 151 |
image = saturation_enhancer.enhance(1.3)
|
| 152 |
+
|
| 153 |
return image
|
| 154 |
|
| 155 |
def apply_film_profile(img, profile, chroma_override=None, blur_override=None, color_temp=6500, cross_process_flag=False, curve_type="auto"):
|
| 156 |
img_array = np.array(img).astype(np.float32) / 255.0
|
|
|
|
| 157 |
img_linear = colour.models.eotf_sRGB(img_array)
|
| 158 |
+
|
| 159 |
if curve_type == "advanced" or (curve_type == "auto" and profile.advanced_curve):
|
| 160 |
img_color_adjusted = apply_advanced_curve(img_linear, profile.advanced_curve)
|
| 161 |
elif curve_type == "color" or (curve_type == "auto" and not profile.advanced_curve):
|
|
|
|
| 165 |
img_color_adjusted = apply_color_curves(img_linear, profile.color_curves)
|
| 166 |
if profile.advanced_curve:
|
| 167 |
img_color_adjusted = apply_advanced_curve(img_color_adjusted, profile.advanced_curve)
|
| 168 |
+
|
| 169 |
img_srgb = colour.models.eotf_inverse_sRGB(img_color_adjusted)
|
|
|
|
| 170 |
img_pil = Image.fromarray((img_srgb * 255).astype(np.uint8))
|
| 171 |
+
|
| 172 |
enhancer = ImageEnhance.Contrast(img_pil)
|
| 173 |
img_contrast = enhancer.enhance(profile.contrast)
|
| 174 |
+
|
| 175 |
enhancer = ImageEnhance.Color(img_contrast)
|
| 176 |
img_saturated = enhancer.enhance(profile.saturation)
|
| 177 |
+
|
| 178 |
chroma_strength = chroma_override if chroma_override is not None else profile.chromatic_aberration
|
| 179 |
if chroma_strength > 0:
|
| 180 |
img_saturated = apply_chromatic_aberration_pil(img_saturated, chroma_strength)
|
| 181 |
+
|
| 182 |
blur_amount = blur_override if blur_override is not None else profile.blur
|
| 183 |
if blur_amount > 0:
|
| 184 |
img_saturated = img_saturated.filter(ImageFilter.GaussianBlur(radius=blur_amount))
|
| 185 |
+
|
| 186 |
img_saturated = apply_base_color(img_saturated, profile.base_color)
|
|
|
|
| 187 |
img_saturated = add_film_grain(img_saturated, amount=profile.grain_amount, size=profile.grain_size)
|
|
|
|
| 188 |
img_saturated = adjust_color_temperature(img_saturated, color_temp)
|
| 189 |
+
|
| 190 |
if cross_process_flag:
|
| 191 |
img_saturated = cross_process(img_saturated)
|
| 192 |
+
|
| 193 |
return img_saturated
|
| 194 |
|
| 195 |
+
def process_images(image, profiles_json, chroma_override=None, blur_override=None, color_temp=6500, cross_process_flag=False, curve_type="auto"):
|
| 196 |
+
film_profiles = load_film_profiles_from_json(profiles_json)
|
| 197 |
+
input_path_base = "output"
|
| 198 |
+
|
| 199 |
+
processed_images = []
|
| 200 |
+
for profile_name, profile in film_profiles.items():
|
| 201 |
+
processed_image = apply_film_profile(image, profile, chroma_override, blur_override, color_temp, cross_process_flag, curve_type)
|
| 202 |
+
processed_images.append(processed_image)
|
| 203 |
+
|
| 204 |
+
return processed_images
|
| 205 |
+
|
| 206 |
def process_image(args):
|
| 207 |
image_path, profile, chroma_override, blur_override, color_temp, cross_process_flag, curve_type, output_filename = args
|
| 208 |
with Image.open(image_path) as img:
|
| 209 |
exif_data = img.getexif()
|
| 210 |
+
|
| 211 |
orientation = exif_data.get(274, 1)
|
| 212 |
if orientation in [3, 6, 8]:
|
| 213 |
img = img.rotate({3: 180, 6: 270, 8: 90}[orientation], expand=True)
|
| 214 |
+
|
| 215 |
processed_image = apply_film_profile(img, profile, chroma_override, blur_override, color_temp, cross_process_flag, curve_type)
|
| 216 |
|
| 217 |
if exif_data:
|
|
|
|
| 228 |
def get_optimal_pool_size(target_memory_usage=75):
|
| 229 |
available_memory = 100 - get_memory_usage()
|
| 230 |
cpu_count = multiprocessing.cpu_count()
|
| 231 |
+
|
| 232 |
for i in range(cpu_count, 0, -1):
|
| 233 |
estimated_memory_usage = get_memory_usage() + (available_memory / cpu_count) * i
|
| 234 |
if estimated_memory_usage <= target_memory_usage:
|
| 235 |
return i
|
| 236 |
+
|
| 237 |
return 1
|
| 238 |
|
| 239 |
if __name__ == "__main__":
|