tatyafanas commited on
Commit
13c41c5
·
verified ·
1 Parent(s): 86a0ea8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1356 -483
app.py CHANGED
@@ -1,7 +1,7 @@
1
  #!/usr/bin/env python3
2
  """
3
- Enhanced Russian 2000s Photo Filter with VHS Video Still Effects
4
- Incorporates VHS camcorder aesthetics, video artifacts, and period-accurate effects
5
  """
6
 
7
  import gradio as gr
@@ -13,325 +13,499 @@ import random
13
  import math
14
 
15
  # ----------------------
16
- # VHS Video Still Effects
17
  # ----------------------
 
 
18
 
19
- def add_vhs_camcorder_ui(pil_img: Image.Image, style="classic", enable_ui=True):
20
- """Add authentic VHS camcorder UI overlay"""
21
- if not enable_ui:
22
- return pil_img
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
- # Create UI overlay
25
- w, h = pil_img.size
26
- overlay = Image.new("RGBA", (w, h), (0, 0, 0, 0))
27
- draw = ImageDraw.Draw(overlay)
28
-
29
- # UI colors and styles
30
- ui_styles = {
31
- "classic": {"bg": (40, 40, 40, 200), "text": (255, 255, 255, 255), "accent": (255, 0, 0, 255)},
32
- "sony": {"bg": (20, 20, 80, 180), "text": (200, 200, 255, 255), "accent": (255, 255, 0, 255)},
33
- "panasonic": {"bg": (80, 20, 20, 180), "text": (255, 200, 200, 255), "accent": (0, 255, 0, 255)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  }
 
 
 
 
 
 
 
 
35
 
36
- colors = ui_styles.get(style, ui_styles["classic"])
 
 
37
 
38
- # Top status bar
39
- draw.rectangle([(0, 0), (w, 35)], fill=colors["bg"])
 
40
 
41
- # REC indicator
42
- rec_x = w - 80
43
- draw.ellipse([(rec_x, 8), (rec_x + 20, 28)], fill=colors["accent"])
44
 
45
- # Font setup
46
- try:
47
- font = ImageFont.truetype("DejaVuSansMono.ttf", 14)
48
- small_font = ImageFont.truetype("DejaVuSansMono.ttf", 10)
49
- except:
50
- font = ImageFont.load_default()
51
- small_font = font
52
-
53
- # UI elements
54
- draw.text((rec_x + 25, 12), "REC", fill=colors["text"], font=font, anchor="lm")
55
- draw.text((10, 12), "VIDEO", fill=colors["text"], font=font, anchor="lm")
56
-
57
- # Side UI elements
58
- ui_height = h // 8
59
- ui_y_start = h // 3
60
-
61
- # Left side buttons
62
- buttons = ["MENU", "ZOOM", "T", "W"]
63
- for i, btn in enumerate(buttons):
64
- y = ui_y_start + i * (ui_height // 2)
65
- # Button background
66
- draw.rectangle([(5, y), (45, y + 25)], fill=colors["bg"], outline=colors["text"])
67
- draw.text((25, y + 12), btn, fill=colors["text"], font=small_font, anchor="mm")
68
-
69
- # Right side elements
70
- draw.text((w - 10, ui_y_start), "LIGHT", fill=colors["text"], font=small_font, anchor="rm")
71
- draw.text((w - 10, ui_y_start + 30), "TITLER", fill=colors["text"], font=small_font, anchor="rm")
72
- draw.text((w - 10, ui_y_start + 60), "PLAY", fill=colors["text"], font=small_font, anchor="rm")
73
-
74
- # Blend overlay
75
- result = Image.alpha_composite(pil_img.convert("RGBA"), overlay)
76
- return result.convert("RGB")
77
-
78
- def add_vhs_video_timestamp(pil_img: Image.Image, timestamp_style="camcorder", custom_time=""):
79
- """Add VHS-style video timestamp"""
80
- draw = ImageDraw.Draw(pil_img)
81
- w, h = pil_img.size
82
 
83
- try:
84
- font = ImageFont.truetype("DejaVuSansMono.ttf", max(12, min(w, h) // 35))
85
- except:
86
- font = ImageFont.load_default()
 
87
 
88
- if not custom_time:
89
- # Generate random VHS-era timestamp
90
- year = random.choice([1997, 1998, 1999, 2000, 2001, 2002])
91
- month = random.randint(1, 12)
92
- day = random.randint(1, 28)
93
- hour = random.randint(0, 23)
94
- minute = random.randint(0, 59)
 
95
 
96
- if timestamp_style == "camcorder":
97
- timestamp = f"PM {hour:02d}:{minute:02d}\n{month:02d}/{day:02d}/{year}"
98
- elif timestamp_style == "security":
99
- timestamp = f"{year:04d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:00"
100
- else: # european
101
- timestamp = f"{day:02d}.{month:02d}.{year} {hour:02d}:{minute:02d}"
102
- else:
103
- timestamp = custom_time
104
-
105
- # Position timestamp
106
- if timestamp_style == "camcorder":
107
- x_pos, y_pos = 15, h - 45
108
- anchor = "lt"
109
- elif timestamp_style == "security":
110
- x_pos, y_pos = w - 15, 15
111
- anchor = "rt"
112
- else:
113
- x_pos, y_pos = w - 15, h - 15
114
- anchor = "rb"
115
 
116
- # Add black outline for readability
117
- for dx in [-1, 0, 1]:
118
- for dy in [-1, 0, 1]:
119
- if dx != 0 or dy != 0:
120
- draw.text((x_pos + dx, y_pos + dy), timestamp, anchor=anchor,
121
- fill=(0, 0, 0), font=font)
122
 
123
- # Main timestamp
124
- draw.text((x_pos, y_pos), timestamp, anchor=anchor,
125
- fill=(255, 255, 255), font=font)
 
 
 
126
 
127
- return pil_img
 
 
 
 
 
 
 
128
 
129
- def add_vhs_tracking_lines(bgr, intensity=0.3, line_count=None):
130
- """Add VHS tracking distortion lines"""
131
- if intensity <= 0:
132
- return bgr
 
 
 
133
 
134
- h, w = bgr.shape[:2]
135
- if line_count is None:
136
- line_count = int(h * intensity * 0.02)
137
 
138
- result = bgr.copy()
 
139
 
140
- for _ in range(line_count):
141
- y = random.randint(0, h-1)
142
- # Random horizontal displacement
143
- displacement = int(random.uniform(-10, 10) * intensity)
144
 
145
- if displacement != 0:
146
- # Shift the line
147
- result[y] = np.roll(result[y], displacement, axis=0)
148
-
149
- # Add noise to the line
150
- noise = np.random.normal(0, 15 * intensity, (w, 3))
151
- result[y] = np.clip(result[y].astype(np.float32) + noise, 0, 255).astype(np.uint8)
 
152
 
153
- return result
 
 
 
 
 
 
 
 
 
 
 
154
 
155
- def add_vhs_color_bleeding(bgr, amount=0.4):
156
- """Add VHS-style color bleeding and chroma smearing"""
157
- if amount <= 0:
158
- return bgr
 
 
 
 
159
 
160
- # Convert to YUV for chroma manipulation
161
- yuv = cv2.cvtColor(bgr, cv2.COLOR_BGR2YUV).astype(np.float32)
162
- y, u, v = cv2.split(yuv)
163
 
164
- # Horizontal chroma bleeding
165
- blur_kernel_size = max(3, int(amount * 15))
166
- if blur_kernel_size % 2 == 0:
167
- blur_kernel_size += 1
168
 
169
- u_blurred = cv2.GaussianBlur(u, (blur_kernel_size, 1), 0)
170
- v_blurred = cv2.GaussianBlur(v, (blur_kernel_size, 1), 0)
 
 
 
171
 
172
- # Mix original and blurred chroma
173
- u = u * (1 - amount) + u_blurred * amount
174
- v = v * (1 - amount) + v_blurred * amount
175
 
176
- # Recombine and convert back
177
- yuv_result = cv2.merge([y, u, v])
178
- bgr_result = cv2.cvtColor(np.clip(yuv_result, 0, 255).astype(np.uint8), cv2.COLOR_YUV2BGR)
179
 
180
- return bgr_result
181
-
182
- def add_vhs_tape_artifacts(bgr, wear_level=0.3):
183
- """Add VHS tape wear artifacts - dropouts, streaks, etc."""
184
- if wear_level <= 0:
185
- return bgr
186
 
187
- h, w = bgr.shape[:2]
188
- result = bgr.astype(np.float32)
 
 
189
 
190
- # Dropout artifacts (small black/white spots)
191
- dropout_count = int(w * h * wear_level * 0.00001)
192
- for _ in range(dropout_count):
193
- x = random.randint(0, w-1)
194
- y = random.randint(0, h-1)
195
- size = random.randint(1, 3)
196
-
197
- # Create dropout
198
- y_start = max(0, y - size)
199
- y_end = min(h, y + size + 1)
200
- x_start = max(0, x - size)
201
- x_end = min(w, x + size + 1)
202
 
203
- if random.random() < 0.7:
204
- # Dark dropout
205
- result[y_start:y_end, x_start:x_end] *= random.uniform(0.1, 0.4)
206
- else:
207
- # Bright dropout
208
- result[y_start:y_end, x_start:x_end] = np.minimum(
209
- result[y_start:y_end, x_start:x_end] + random.uniform(50, 150), 255
210
- )
211
-
212
- # Vertical streaks (tape head issues)
213
- streak_count = int(wear_level * 3)
214
- for _ in range(streak_count):
215
- x = random.randint(0, w-1)
216
- streak_width = random.randint(1, 2)
217
- streak_intensity = random.uniform(0.7, 1.3)
 
 
 
 
 
 
 
218
 
219
- x_start = max(0, x)
220
- x_end = min(w, x + streak_width)
221
- result[:, x_start:x_end] *= streak_intensity
222
 
223
- return np.clip(result, 0, 255).astype(np.uint8)
 
 
 
 
 
 
 
 
 
 
224
 
225
- def add_vhs_interlacing(bgr, field_offset=True, blend_amount=0.3):
226
- """Add authentic VHS interlacing effects"""
227
- if blend_amount <= 0:
228
- return bgr
 
 
 
 
229
 
230
- h, w = bgr.shape[:2]
231
- result = bgr.copy()
232
 
233
- if field_offset:
234
- # Simulate field offset (odd/even line temporal difference)
235
- offset_lines = bgr.copy()
 
 
 
 
 
 
 
 
236
 
237
- # Slightly shift odd lines horizontally
238
- for y in range(1, h, 2):
239
- offset_lines[y] = np.roll(offset_lines[y], 1, axis=0)
240
 
241
- # Blend with original
242
- result = cv2.addWeighted(bgr, 1 - blend_amount, offset_lines, blend_amount, 0)
243
 
244
- # Add line-by-line brightness variation
245
- brightness_var = np.ones((h, 1, 1), dtype=np.float32)
246
- for y in range(0, h, 2):
247
- brightness_var[y] *= (1 - blend_amount * 0.1)
248
 
249
- result = np.clip(result.astype(np.float32) * brightness_var, 0, 255).astype(np.uint8)
 
 
250
 
251
- return result
 
 
 
 
 
 
 
 
 
 
 
252
 
253
- def add_vhs_head_switching_noise(bgr, intensity=0.2):
254
- """Add VHS head switching noise (horizontal band at bottom)"""
255
- if intensity <= 0:
256
- return bgr
 
 
 
257
 
258
- h, w = bgr.shape[:2]
259
- result = bgr.copy()
260
 
261
- # Head switching occurs in bottom portion of frame
262
- noise_start = int(h * 0.85)
263
- noise_height = int(h * 0.1)
264
 
265
- if noise_height > 0:
266
- # Add horizontal noise band
267
- noise = np.random.normal(0, intensity * 30, (noise_height, w, 3))
268
- noise_region = result[noise_start:noise_start+noise_height].astype(np.float32)
269
- noise_region += noise
270
- result[noise_start:noise_start+noise_height] = np.clip(noise_region, 0, 255).astype(np.uint8)
271
 
272
- # Add some horizontal lines
273
- for i in range(2):
274
- y = noise_start + random.randint(0, noise_height-1)
275
- cv2.line(result, (0, y), (w, y), (128, 128, 128), 1)
 
276
 
277
- return result
278
 
279
- def simulate_vhs_resolution_loss(pil_img: Image.Image, horizontal_res=240, add_softness=True):
280
- """Simulate VHS resolution limitations"""
281
- original_size = pil_img.size
282
 
283
- # Reduce horizontal resolution significantly (VHS ~240 lines)
284
- reduced_height = horizontal_res
285
- aspect_ratio = original_size[0] / original_size[1]
286
- reduced_width = int(reduced_height * aspect_ratio)
287
-
288
- # Scale down
289
- reduced = pil_img.resize((reduced_width, reduced_height), Image.Resampling.LANCZOS)
290
 
291
- if add_softness:
292
- # Add slight blur to simulate VHS softness
293
- reduced = reduced.filter(ImageFilter.GaussianBlur(radius=0.5))
 
 
 
294
 
295
- # Scale back up with lower quality interpolation
296
- result = reduced.resize(original_size, Image.Resampling.BILINEAR)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
 
298
- return result
299
 
300
- def add_vhs_rf_interference(bgr, intensity=0.2):
301
- """Add RF interference patterns common in VHS"""
302
- if intensity <= 0:
303
- return bgr
304
 
305
- h, w = bgr.shape[:2]
 
306
 
307
- # Create interference pattern
308
- y_coords, x_coords = np.ogrid[:h, :w]
309
 
310
- # Multiple frequency interference
311
- pattern1 = np.sin(x_coords * 0.1 + y_coords * 0.05) * intensity * 10
312
- pattern2 = np.sin(x_coords * 0.03 + y_coords * 0.1) * intensity * 8
313
- pattern3 = np.sin(x_coords * 0.2) * intensity * 5
314
 
315
- interference = pattern1 + pattern2 + pattern3
 
 
 
 
316
 
317
- # Apply interference
318
- result = bgr.astype(np.float32)
319
- result += interference[..., np.newaxis]
320
 
321
- return np.clip(result, 0, 255).astype(np.uint8)
322
-
323
- # [Include all the previous functions from the original filter here]
324
- # For brevity, I'll include just the essential ones and the main processing function
325
-
326
- def to_np(img: Image.Image):
327
- return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
328
-
329
- def to_pil(arr: np.ndarray):
330
- return Image.fromarray(cv2.cvtColor(arr, cv2.COLOR_BGR2RGB))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
 
332
- def clamp_u8(x):
333
- return np.clip(x, 0, 255).astype(np.uint8)
 
334
 
 
 
 
335
  def crop_4_3(img: Image.Image):
336
  w, h = img.size
337
  target_ratio = 4/3
@@ -345,6 +519,20 @@ def crop_4_3(img: Image.Image):
345
  top = max(0, int((h - new_h) * 0.3))
346
  return img.crop((0, top, w, top + new_h))
347
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  def enhanced_vignette(bgr, strength=0.15, feather=1.8):
349
  if strength <= 0:
350
  return bgr
@@ -377,6 +565,85 @@ def realistic_film_grain(bgr, grain_strength=8, grain_size=1.1):
377
  out = cv2.cvtColor(clamp_u8(yuv), cv2.COLOR_YUV2BGR)
378
  return out
379
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  def authentic_jpeg_compression(pil_img: Image.Image, quality=55, add_artifacts=False):
381
  def compress_once(im, q):
382
  buf = io.BytesIO()
@@ -388,6 +655,357 @@ def authentic_jpeg_compression(pil_img: Image.Image, quality=55, add_artifacts=F
388
  out = compress_once(out, int(min(95, quality + 10)))
389
  return out
390
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  def map_intensity(intensity_0_10: float):
392
  base = float(np.clip(intensity_0_10 / 3.0, 0.0, 1.0))
393
  s = 1.0 - (1.0 - base) ** 3
@@ -396,28 +1014,53 @@ def map_intensity(intensity_0_10: float):
396
  return s, boost
397
 
398
  # ----------------------
399
- # Enhanced Main Processing Pipeline
400
  # ----------------------
401
- def process_image_with_vhs(
402
  image,
403
  intensity,
404
- # VHS Video Effects
405
- enable_vhs_mode,
406
- vhs_tracking_lines,
407
- vhs_color_bleeding,
408
- vhs_tape_wear,
409
- vhs_interlacing,
410
- vhs_head_noise,
411
- vhs_rf_interference,
412
- vhs_resolution_loss,
413
- vhs_ui_overlay,
414
- vhs_ui_style,
415
- vhs_timestamp_style,
416
- vhs_custom_timestamp,
417
- # Basic settings
418
  grain_amount,
419
  compression_level,
420
- keep_ratio
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  ):
422
  if image is None:
423
  return None
@@ -429,103 +1072,285 @@ def process_image_with_vhs(
429
  original = image.convert("RGB")
430
  pil = original.copy() if keep_ratio else crop_4_3(original)
431
 
432
- # STEP 1: VHS Resolution Loss (do this early for authentic low-res look)
433
- if enable_vhs_mode and vhs_resolution_loss > 0:
434
- target_resolution = int(480 - (vhs_resolution_loss * 200)) # 480 down to 280 lines
435
- pil = simulate_vhs_resolution_loss(pil, horizontal_res=target_resolution, add_softness=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
 
437
- # Convert to BGR for OpenCV operations
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  bgr = to_np(pil)
439
 
440
- # STEP 2: VHS-specific effects
441
- if enable_vhs_mode:
442
- # VHS color bleeding (do early to affect subsequent processing)
443
- if vhs_color_bleeding > 0:
444
- bgr = add_vhs_color_bleeding(bgr, amount=vhs_color_bleeding)
445
-
446
- # VHS tape artifacts
447
- if vhs_tape_wear > 0:
448
- bgr = add_vhs_tape_artifacts(bgr, wear_level=vhs_tape_wear)
449
-
450
- # VHS tracking issues
451
- if vhs_tracking_lines > 0:
452
- bgr = add_vhs_tracking_lines(bgr, intensity=vhs_tracking_lines)
453
-
454
- # VHS interlacing
455
- if vhs_interlacing > 0:
456
- bgr = add_vhs_interlacing(bgr, field_offset=True, blend_amount=vhs_interlacing)
457
-
458
- # RF interference
459
- if vhs_rf_interference > 0:
460
- bgr = add_vhs_rf_interference(bgr, intensity=vhs_rf_interference)
461
-
462
- # Head switching noise
463
- if vhs_head_noise > 0:
464
- bgr = add_vhs_head_switching_noise(bgr, intensity=vhs_head_noise)
465
-
466
- # STEP 3: Standard processing (reduced for VHS mode)
467
- if enable_vhs_mode:
468
- # Lighter processing for VHS mode
469
- reduced_s = s * 0.6 # Reduce standard effects when VHS mode is on
470
- reduced_boost = 1 + (boost - 1) * 0.4
471
- else:
472
- reduced_s, reduced_boost = s, boost
473
 
474
  # Vignette
475
- bgr = enhanced_vignette(bgr, strength=min(0.4, 0.06 * reduced_s * reduced_boost), feather=1.8)
476
 
477
- # Grain (adjusted for VHS)
478
- g_strength = min(30.0, (float(grain_amount) * 0.35 + 1.5) * reduced_s * reduced_boost)
479
- if enable_vhs_mode:
480
- g_strength *= 0.7 # Less grain for VHS mode
481
  bgr = realistic_film_grain(bgr, grain_strength=g_strength, grain_size=1.05)
 
482
 
483
- # Convert back to PIL
484
- pil_mid = to_pil(bgr)
 
 
485
 
486
- # JPEG compression (adjusted for VHS)
487
- comp_level = compression_level
488
- if enable_vhs_mode:
489
- comp_level = min(compression_level * 1.2, 1.5) # More compression for VHS
490
-
491
- comp_norm = (float(comp_level) - 0.3) / (1.5 - 0.3)
 
 
 
 
 
492
  comp_norm = float(np.clip(comp_norm, 0, 1))
493
- q = int(92 - (92 - 68) * comp_norm * min(1.5, reduced_s * (0.8 + 0.6 * (reduced_boost - 1))))
494
- add_2pass = (comp_level > 1.0) or (reduced_s > 0.7)
495
  pil_mid = authentic_jpeg_compression(pil_mid, quality=int(np.clip(q, 30, 92)), add_artifacts=add_2pass)
496
 
497
  # Final blend
498
  orig_aligned = original if keep_ratio else crop_4_3(original)
499
- if enable_vhs_mode:
500
- mix = float(np.clip(0.15 + 0.85 * reduced_s * (0.9 + 0.6 * (reduced_boost - 1)), 0.15, 0.95))
501
- else:
502
- mix = float(np.clip(0.08 + 0.67 * reduced_s * (0.9 + 0.6 * (reduced_boost - 1)), 0.08, 0.92))
503
-
504
  processed = Image.blend(orig_aligned, pil_mid, alpha=mix)
505
 
506
- # STEP 4: VHS UI and timestamp overlays (do last)
507
- if enable_vhs_mode:
508
- # Add VHS timestamp
509
- if vhs_timestamp_style != "none":
510
- processed = add_vhs_video_timestamp(
511
- processed,
512
- timestamp_style=vhs_timestamp_style,
513
- custom_time=vhs_custom_timestamp
514
- )
515
-
516
- # Add VHS UI overlay
517
- if vhs_ui_overlay:
518
- processed = add_vhs_camcorder_ui(processed, style=vhs_ui_style, enable_ui=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
519
 
520
  return processed
521
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
  # ----------------------
523
- # Enhanced UI with VHS Controls
524
  # ----------------------
525
- with gr.Blocks(title="Russian 2000s Filter with VHS Video Effects", theme=gr.themes.Soft()) as demo:
526
  gr.Markdown("""
527
- # 📷 Russian 2000s Filter with VHS Video Still Effects
528
- Transform your photos into authentic VHS video stills with camcorder UI, tracking issues, and period-accurate artifacts.
529
  """)
530
 
531
  with gr.Row():
@@ -533,159 +1358,207 @@ with gr.Blocks(title="Russian 2000s Filter with VHS Video Effects", theme=gr.the
533
  input_image = gr.Image(type="pil", label="📸 Upload Your Photo")
534
 
535
  with gr.Column(scale=1):
536
- output_image = gr.Image(type="pil", label="✨ VHS Video Still", interactive=False)
537
 
538
- # Main processing button
539
  with gr.Row():
540
- process_btn = gr.Button("🎥 Apply VHS Video Filter", variant="primary", size="lg")
541
 
542
  with gr.Row():
543
  with gr.Column(scale=1):
544
-
545
- with gr.Accordion("📼 VHS Video Still Effects", open=True):
546
  gr.Markdown("""
547
- **Transform photos into authentic VHS video stills**
548
- - Camcorder UI overlays with REC indicator
549
- - VHS tracking lines and tape artifacts
550
- - Color bleeding and interlacing effects
551
- - Resolution loss simulation
 
 
 
 
 
552
  """)
553
 
554
- enable_vhs_mode = gr.Checkbox(
555
- label="🎥 Enable VHS Video Mode",
556
- value=True,
557
- info="Master switch for all VHS effects"
 
 
 
 
 
 
 
 
558
  )
559
 
560
- with gr.Row():
561
- with gr.Column():
562
- vhs_ui_overlay = gr.Checkbox(label="Camcorder UI Overlay", value=True)
563
- vhs_ui_style = gr.Dropdown(
564
- choices=["classic", "sony", "panasonic"],
565
- value="classic",
566
- label="UI Style"
567
- )
568
-
569
- vhs_timestamp_style = gr.Dropdown(
570
- choices=["none", "camcorder", "security", "european"],
571
- value="camcorder",
572
- label="Timestamp Style"
573
- )
574
-
575
- vhs_custom_timestamp = gr.Textbox(
576
- label="Custom Timestamp",
577
- placeholder="Leave empty for random",
578
- info="Custom time/date text"
579
- )
580
-
581
- with gr.Column():
582
- vhs_resolution_loss = gr.Slider(
583
- 0, 1, value=0.6, step=0.1,
584
- label="Resolution Loss",
585
- info="Simulates VHS 240-line resolution"
586
- )
587
-
588
- vhs_color_bleeding = gr.Slider(
589
- 0, 1, value=0.4, step=0.1,
590
- label="Color Bleeding",
591
- info="Horizontal chroma smearing"
592
- )
593
-
594
- vhs_tracking_lines = gr.Slider(
595
- 0, 1, value=0.3, step=0.1,
596
- label="Tracking Issues",
597
- info="Horizontal line displacement"
598
- )
599
-
600
- vhs_interlacing = gr.Slider(
601
- 0, 1, value=0.3, step=0.1,
602
- label="Interlacing Effects",
603
- info="Field offset and line artifacts"
604
- )
605
 
606
- with gr.Row():
607
- vhs_tape_wear = gr.Slider(
608
- 0, 1, value=0.2, step=0.1,
609
- label="Tape Wear",
610
- info="Dropouts and streaks"
611
- )
612
-
613
- vhs_head_noise = gr.Slider(
614
- 0, 1, value=0.15, step=0.05,
615
- label="Head Switching Noise",
616
- info="Noise band at bottom of frame"
617
- )
618
-
619
- vhs_rf_interference = gr.Slider(
620
- 0, 1, value=0.1, step=0.05,
621
- label="RF Interference",
622
- info="Wavy interference patterns"
623
- )
624
 
625
  with gr.Accordion("🎛️ Basic Settings", open=True):
626
  intensity = gr.Slider(0, 10, value=3.5, step=0.1, label="Overall Effect Intensity (0–10)")
 
 
 
 
 
627
  grain_amount = gr.Slider(2, 15, value=7, step=1, label="Film Grain Amount")
628
- compression_level = gr.Slider(0.3, 1.5, value=1.1, step=0.1, label="Compression Level")
629
- keep_ratio = gr.Checkbox(value=False, label="Keep Original Aspect Ratio")
630
 
631
- # Connect processing button
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
632
  process_btn.click(
633
- fn=process_image_with_vhs,
634
  inputs=[
635
- input_image, intensity,
636
- # VHS controls
637
- enable_vhs_mode, vhs_tracking_lines, vhs_color_bleeding, vhs_tape_wear,
638
- vhs_interlacing, vhs_head_noise, vhs_rf_interference, vhs_resolution_loss,
639
- vhs_ui_overlay, vhs_ui_style, vhs_timestamp_style, vhs_custom_timestamp,
640
- # Basic settings
641
- grain_amount, compression_level, keep_ratio
 
 
 
642
  ],
643
  outputs=[output_image]
644
  )
645
 
646
  gr.Markdown("""
647
- ### 📼 VHS Video Still Features:
648
-
649
- **🎥 Camcorder UI Elements:**
650
- - Authentic REC indicator with red dot
651
- - MENU, ZOOM, and control button overlays
652
- - LIGHT, TITLER, PLAY indicators
653
- - Multiple UI styles (Classic, Sony, Panasonic)
654
-
655
- **📺 VHS Video Artifacts:**
656
- - **Resolution Loss**: Simulates VHS 240-line horizontal resolution
657
- - **Color Bleeding**: Horizontal chroma smearing typical of VHS
658
- - **Tracking Lines**: Random horizontal line displacement
659
- - **Interlacing**: Field offset and line-by-line artifacts
660
- - **Tape Wear**: Dropouts, streaks, and degradation
661
- - **Head Switching Noise**: Noise band at bottom of frame
662
- - **RF Interference**: Wavy interference patterns
663
-
664
- **⏰ Timestamp Options:**
665
- - **Camcorder**: Bottom-left PM time format (like Image 2, 8, 9)
666
- - **Security**: Top-right format with full date (like Image 7)
667
- - **European**: Bottom-right DD.MM.YYYY format (like Image 3, 5)
668
-
669
- **🎯 Perfect for Creating:**
670
- - Russian 2000s family video stills
671
- - Security camera footage aesthetics
672
- - Amateur camcorder recordings
673
- - VHS home movie screenshots
674
- - Y2K-era video content
675
-
676
- **💡 Pro Tips:**
677
- - Enable VHS mode for full video aesthetic
678
- - Use "Classic" UI style for most authentic look
679
- - Combine with higher compression for maximum authenticity
680
- - "Camcorder" timestamp matches Russian family videos
681
- - Reduce resolution loss for cleaner look, increase for more authentic VHS degradation
682
-
683
- **🔧 Technical Features:**
684
- - Simulates actual VHS color space limitations
685
- - Authentic interlacing and field offset
686
- - Real tracking error patterns
687
- - Period-accurate UI design based on 90s/2000s camcorders
688
- - Proper aspect ratio handling for video frames
689
  """)
690
 
691
  if __name__ == "__main__":
 
1
  #!/usr/bin/env python3
2
  """
3
+ Complete Russian/Eastern European 2000s Photo Filter with Reference Style Transfer
4
+ Fixed version with proper function definitions and Gradio interface
5
  """
6
 
7
  import gradio as gr
 
13
  import math
14
 
15
  # ----------------------
16
+ # Utilities
17
  # ----------------------
18
+ def to_np(img: Image.Image):
19
+ return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
20
 
21
+ def to_pil(arr: np.ndarray):
22
+ return Image.fromarray(cv2.cvtColor(arr, cv2.COLOR_BGR2RGB))
23
+
24
+ def clamp_u8(x):
25
+ return np.clip(x, 0, 255).astype(np.uint8)
26
+
27
+ def smoothstep(x, edge0, edge1):
28
+ t = np.clip((x - edge0) / (edge1 - edge0 + 1e-6), 0, 1)
29
+ return t * t * (3 - 2 * t)
30
+
31
+ # ----------------------
32
+ # Missing Debug Functions
33
+ # ----------------------
34
+ def simple_style_test(input_image):
35
+ """Simple test function to verify basic functionality"""
36
+ if input_image is None:
37
+ return "❌ No input image provided"
38
 
39
+ try:
40
+ # Just check if we can process the image
41
+ img_array = np.array(input_image)
42
+ mean_brightness = np.mean(cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY))
43
+
44
+ return f"""✅ Basic functionality working!
45
+ Image size: {input_image.size}
46
+ Mean brightness: {mean_brightness:.2f}
47
+ Image mode: {input_image.mode}
48
+
49
+ Ready for style transfer testing!"""
50
+ except Exception as e:
51
+ return f"❌ Error in simple test: {e}"
52
+
53
+ def test_style_transfer_debug(input_image, reference_images):
54
+ """Test function for debugging style transfer"""
55
+ if input_image is None:
56
+ return "❌ No input image provided"
57
+
58
+ if not reference_images:
59
+ return "❌ No reference images provided"
60
+
61
+ try:
62
+ # Try to load reference images
63
+ ref_images = []
64
+ for file in reference_images:
65
+ try:
66
+ img = Image.open(file.name).convert("RGB")
67
+ ref_images.append(img)
68
+ except Exception as e:
69
+ return f"❌ Failed to load reference image: {e}"
70
+
71
+ if not ref_images:
72
+ return "❌ No reference images could be loaded"
73
+
74
+ # Create reference database
75
+ ref_db = create_reference_database(ref_images)
76
+ if not ref_db:
77
+ return "❌ Failed to create reference database"
78
+
79
+ # Test color matching
80
+ target_pil = input_image.convert("RGB")
81
+ original_array = np.array(target_pil)
82
+
83
+ # Apply simple color matching test
84
+ if ref_db['color_stats']:
85
+ result = apply_color_matching(target_pil, ref_db['color_stats'][0], 0.8)
86
+ result_array = np.array(result)
87
+
88
+ difference = np.mean(np.abs(original_array.astype(float) - result_array.astype(float)))
89
+
90
+ ref_stats = ref_db['color_stats'][0]
91
+ debug_info = f"""✅ Style transfer working!
92
+ Reference LAB mean: {ref_stats['lab_mean']}
93
+ Color difference: {difference:.2f} (should be > 1.0)
94
+ Database has {len(ref_db['color_stats'])} reference(s)
95
+ Amateur chars: {'Yes' if 'amateur_chars' in ref_db else 'No'}"""
96
+ return debug_info
97
+ else:
98
+ return "❌ No color stats in reference database"
99
+
100
+ except Exception as e:
101
+ return f"❌ Error during test: {e}"
102
+
103
+ # Enhanced Style Transfer Functions for Amateur Point-and-Click Photography
104
+ def analyze_amateur_photography_characteristics(image):
105
+ """Analyze characteristics typical of amateur point-and-click photography"""
106
+ if image is None:
107
+ return None
108
+
109
+ img_array = np.array(image)
110
+ gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY)
111
+ h, w = gray.shape
112
+
113
+ # Analyze center vs edge brightness (center-weighted metering)
114
+ center_region = gray[h//4:3*h//4, w//4:3*w//4]
115
+ edge_region = np.concatenate([
116
+ gray[:h//4, :].flatten(),
117
+ gray[3*h//4:, :].flatten(),
118
+ gray[:, :w//4].flatten(),
119
+ gray[:, 3*w//4:].flatten()
120
+ ])
121
+
122
+ # Flash characteristics detection
123
+ top_quarter = gray[:h//4, :]
124
+ flash_hotspot = np.percentile(top_quarter, 95)
125
+
126
+ # Depth analysis (simple edge density)
127
+ edges = cv2.Canny(gray, 50, 150)
128
+ foreground_edges = np.mean(edges[2*h//3:, :]) # Bottom third
129
+ background_edges = np.mean(edges[:h//3, :]) # Top third
130
+
131
+ return {
132
+ 'center_brightness': np.mean(center_region),
133
+ 'edge_brightness': np.mean(edge_region),
134
+ 'flash_intensity': flash_hotspot,
135
+ 'brightness_variance': np.std(gray),
136
+ 'foreground_detail': foreground_edges,
137
+ 'background_detail': background_edges,
138
+ 'overall_exposure': np.mean(gray),
139
+ 'highlight_clipping': np.sum(gray > 240) / (h * w),
140
+ 'shadow_crushing': np.sum(gray < 15) / (h * w)
141
  }
142
+
143
+ def emulate_point_and_click_exposure(image, reference_chars, strength=0.7):
144
+ """Emulate typical point-and-click camera exposure characteristics"""
145
+ if reference_chars is None:
146
+ return image
147
+
148
+ img_array = np.array(image).astype(np.float32)
149
+ h, w = img_array.shape[:2]
150
 
151
+ # Create distance-based masks for foreground/background
152
+ y_coords, x_coords = np.ogrid[:h, :w]
153
+ center_y, center_x = h // 2, w // 2
154
 
155
+ # Distance from center (for center-weighted metering simulation)
156
+ center_distance = np.sqrt((x_coords - center_x)**2 + (y_coords - center_y)**2)
157
+ center_distance = center_distance / np.max(center_distance)
158
 
159
+ # Depth proxy (bottom = closer, top = farther)
160
+ depth_proxy = y_coords.astype(np.float32) / h
 
161
 
162
+ # Simulate center-weighted metering bias
163
+ ref_center_bright = reference_chars.get('center_brightness', 128)
164
+ ref_edge_bright = reference_chars.get('edge_brightness', 100)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
+ current_center = np.mean(img_array[h//4:3*h//4, w//4:3*w//4])
167
+
168
+ # Apply center-weighted exposure correction
169
+ center_correction = (ref_center_bright - current_center) * strength * 0.3
170
+ center_mask = 1 - smoothstep(center_distance, 0.3, 0.8)
171
 
172
+ img_array += center_mask[..., None] * center_correction
173
+
174
+ # Simulate flash falloff on foreground subjects
175
+ ref_flash = reference_chars.get('flash_intensity', 200)
176
+ if ref_flash > 180: # Reference had flash
177
+ # Flash affects foreground more (bottom 60% of image)
178
+ flash_mask = 1 - smoothstep(depth_proxy, 0.4, 1.0)
179
+ flash_strength = (ref_flash - 128) * strength * 0.15
180
 
181
+ # Flash creates overexposure in foreground
182
+ img_array += flash_mask[..., None] * flash_strength
183
+
184
+ # Flash creates harsh shadows in background
185
+ shadow_mask = smoothstep(depth_proxy, 0.6, 1.0)
186
+ shadow_strength = -flash_strength * 0.4
187
+ img_array += shadow_mask[..., None] * shadow_strength
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
+ # Simulate limited dynamic range (crush shadows, clip highlights)
190
+ ref_clipping = reference_chars.get('highlight_clipping', 0.02)
191
+ ref_crushing = reference_chars.get('shadow_crushing', 0.03)
 
 
 
192
 
193
+ if ref_clipping > 0.01:
194
+ # Clip highlights more aggressively
195
+ clip_threshold = 255 - (ref_clipping * 800)
196
+ img_array = np.where(img_array > clip_threshold,
197
+ clip_threshold + (img_array - clip_threshold) * 0.3,
198
+ img_array)
199
 
200
+ if ref_crushing > 0.01:
201
+ # Crush shadows
202
+ crush_threshold = ref_crushing * 600
203
+ img_array = np.where(img_array < crush_threshold,
204
+ img_array * 0.5,
205
+ img_array)
206
+
207
+ return Image.fromarray(np.clip(img_array, 0, 255).astype(np.uint8))
208
 
209
+ def apply_amateur_focus_characteristics(image, reference_chars, strength=0.6):
210
+ """Simulate amateur focus characteristics - everything in focus or poorly focused"""
211
+ if reference_chars is None:
212
+ return image
213
+
214
+ img_array = np.array(image)
215
+ h, w = img_array.shape[:2]
216
 
217
+ # Simple depth proxy
218
+ y_coords = np.arange(h).reshape(-1, 1) / h
219
+ depth_proxy = np.broadcast_to(y_coords, (h, w))
220
 
221
+ ref_fg_detail = reference_chars.get('foreground_detail', 50)
222
+ ref_bg_detail = reference_chars.get('background_detail', 30)
223
 
224
+ # If reference has poor background focus, blur background
225
+ if ref_bg_detail < ref_fg_detail * 0.7:
226
+ # Create depth-based blur
227
+ background_blur = cv2.GaussianBlur(img_array, (0, 0), 1.5 * strength)
228
 
229
+ # Apply more blur to background
230
+ bg_mask = smoothstep(1 - depth_proxy, 0.3, 0.8)
231
+
232
+ result = img_array.astype(np.float32)
233
+ blurred = background_blur.astype(np.float32)
234
+
235
+ result = result * (1 - bg_mask[..., None]) + blurred * bg_mask[..., None]
236
+ img_array = result.astype(np.uint8)
237
 
238
+ # If reference shows motion blur (camera shake), add slight blur
239
+ ref_variance = reference_chars.get('brightness_variance', 30)
240
+ if ref_variance > 40: # High variance might indicate motion blur
241
+ # Add slight motion blur
242
+ kernel_size = max(3, int(strength * 5))
243
+ motion_kernel = np.zeros((kernel_size, kernel_size))
244
+ motion_kernel[kernel_size//2, :] = 1 / kernel_size
245
+
246
+ motion_blurred = cv2.filter2D(img_array, -1, motion_kernel)
247
+ img_array = cv2.addWeighted(img_array, 1 - strength * 0.3, motion_blurred, strength * 0.3, 0)
248
+
249
+ return Image.fromarray(img_array)
250
 
251
+ def apply_amateur_flash_realism(image, reference_chars, strength=0.7):
252
+ """Apply realistic amateur flash characteristics"""
253
+ if reference_chars is None:
254
+ return image
255
+
256
+ ref_flash = reference_chars.get('flash_intensity', 150)
257
+ if ref_flash < 180: # No significant flash in reference
258
+ return image
259
 
260
+ img_array = np.array(image).astype(np.float32)
261
+ h, w = img_array.shape[:2]
 
262
 
263
+ # Flash position (slightly off-center, typical of compact cameras)
264
+ flash_x = w * 0.52 # Slightly right of center
265
+ flash_y = h * 0.15 # Upper portion
 
266
 
267
+ # Create distance map from flash
268
+ y_coords, x_coords = np.ogrid[:h, :w]
269
+ flash_distance = np.sqrt((x_coords - flash_x)**2 + (y_coords - flash_y)**2)
270
+ max_distance = np.sqrt(w**2 + h**2)
271
+ flash_distance_norm = flash_distance / max_distance
272
 
273
+ # Flash characteristics
274
+ # 1. Harsh falloff (inverse square law)
275
+ flash_intensity = 1 / (1 + flash_distance_norm * 8) ** 2
276
 
277
+ # 2. Flash creates cool color temperature
278
+ flash_effect = flash_intensity * strength * (ref_flash - 128) / 128
 
279
 
280
+ # Apply flash effect
281
+ img_array[:,:,2] += flash_effect * 20 # Less red
282
+ img_array[:,:,1] += flash_effect * 25 # More green
283
+ img_array[:,:,0] += flash_effect * 35 # Much more blue
 
 
284
 
285
+ # 3. Flash overexposes foreground subjects
286
+ foreground_mask = 1 - smoothstep(y_coords / h, 0.5, 1.0)
287
+ overexposure = flash_effect * foreground_mask * 15
288
+ img_array += overexposure[..., None]
289
 
290
+ # 4. Flash creates hard shadows behind subjects
291
+ # Simulate by darkening areas that would be shadowed
292
+ shadow_mask = smoothstep(flash_distance_norm, 0.4, 0.8) * smoothstep(y_coords / h, 0.3, 0.7)
293
+ shadow_effect = -flash_effect * shadow_mask * 20
294
+ img_array += shadow_effect[..., None]
295
+
296
+ return Image.fromarray(np.clip(img_array, 0, 255).astype(np.uint8))
297
+
298
+ def extract_color_statistics(image):
299
+ """Extract color statistics from reference image"""
300
+ if image is None:
301
+ return None
302
 
303
+ img_array = np.array(image)
304
+
305
+ # Convert to different color spaces for analysis
306
+ lab = cv2.cvtColor(img_array, cv2.COLOR_RGB2LAB)
307
+ hsv = cv2.cvtColor(img_array, cv2.COLOR_RGB2HSV)
308
+
309
+ stats = {
310
+ 'rgb_mean': np.mean(img_array, axis=(0,1)),
311
+ 'rgb_std': np.std(img_array, axis=(0,1)),
312
+ 'lab_mean': np.mean(lab, axis=(0,1)),
313
+ 'lab_std': np.std(lab, axis=(0,1)),
314
+ 'hsv_mean': np.mean(hsv, axis=(0,1)),
315
+ 'hsv_std': np.std(hsv, axis=(0,1)),
316
+ 'brightness_dist': np.histogram(cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY), bins=50)[0],
317
+ }
318
+
319
+ return stats
320
+
321
+ def extract_texture_features(image):
322
+ """Extract basic texture features"""
323
+ if image is None:
324
+ return None
325
 
326
+ gray = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2GRAY)
 
 
327
 
328
+ # Simple gradient-based texture analysis
329
+ grad_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
330
+ grad_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
331
+ grad_mag = np.sqrt(grad_x**2 + grad_y**2)
332
+
333
+ return {
334
+ 'gradient_mean': np.mean(grad_mag),
335
+ 'gradient_std': np.std(grad_mag),
336
+ 'edge_density': np.mean(grad_mag > np.percentile(grad_mag, 75)),
337
+ 'contrast': np.std(gray)
338
+ }
339
 
340
+ def apply_color_matching(target_image, reference_stats, strength=0.7):
341
+ """Apply color matching based on reference statistics"""
342
+ if reference_stats is None:
343
+ print("DEBUG: No reference stats provided to color matching")
344
+ return target_image
345
+
346
+ print(f"DEBUG: Applying color matching with strength {strength}")
347
+ print(f"DEBUG: Reference LAB mean: {reference_stats['lab_mean']}")
348
 
349
+ target_array = np.array(target_image).astype(np.float32)
350
+ original_array = target_array.copy()
351
 
352
+ # LAB color space matching
353
+ target_lab = cv2.cvtColor(target_array.astype(np.uint8), cv2.COLOR_RGB2LAB).astype(np.float32)
354
+ original_lab = target_lab.copy()
355
+
356
+ # Match mean and standard deviation
357
+ for i in range(3):
358
+ target_mean = np.mean(target_lab[:,:,i])
359
+ target_std = np.std(target_lab[:,:,i])
360
+
361
+ ref_mean = reference_stats['lab_mean'][i]
362
+ ref_std = reference_stats['lab_std'][i]
363
 
364
+ print(f"DEBUG: Channel {i} - Target mean: {target_mean:.1f}, Ref mean: {ref_mean:.1f}")
365
+ print(f"DEBUG: Channel {i} - Target std: {target_std:.1f}, Ref std: {ref_std:.1f}")
 
366
 
367
+ if target_std > 1:
368
+ target_lab[:,:,i] = (target_lab[:,:,i] - target_mean) * (ref_std / target_std) + ref_mean
369
 
370
+ # Convert back to RGB
371
+ matched = cv2.cvtColor(np.clip(target_lab, 0, 255).astype(np.uint8), cv2.COLOR_LAB2RGB)
 
 
372
 
373
+ # Check the difference before blending
374
+ lab_difference = np.mean(np.abs(original_lab - target_lab))
375
+ print(f"DEBUG: LAB space difference: {lab_difference}")
376
 
377
+ # Blend with original
378
+ result_array = np.array(target_image).astype(np.float32)
379
+ matched_array = matched.astype(np.float32)
380
+
381
+ final = result_array * (1 - strength) + matched_array * strength
382
+ final_image = Image.fromarray(np.clip(final, 0, 255).astype(np.uint8))
383
+
384
+ # Check final difference
385
+ final_difference = np.mean(np.abs(original_array - np.array(final_image).astype(np.float32)))
386
+ print(f"DEBUG: Final color matching difference: {final_difference}")
387
+
388
+ return final_image
389
 
390
+ def apply_texture_matching(target_image, reference_texture, strength=0.5):
391
+ """Apply texture-based adjustments"""
392
+ if reference_texture is None:
393
+ return target_image
394
+
395
+ target_array = np.array(target_image)
396
+ target_texture = extract_texture_features(target_image)
397
 
398
+ if target_texture is None:
399
+ return target_image
400
 
401
+ # Adjust contrast based on reference
402
+ ref_contrast = reference_texture['contrast']
403
+ target_contrast = target_texture['contrast']
404
 
405
+ if target_contrast > 0:
406
+ contrast_factor = (ref_contrast / target_contrast) * strength + 1 * (1 - strength)
407
+ contrast_factor = np.clip(contrast_factor, 0.5, 2.0)
 
 
 
408
 
409
+ enhanced = target_array.astype(np.float32)
410
+ enhanced = (enhanced - 128) * contrast_factor + 128
411
+ enhanced = np.clip(enhanced, 0, 255).astype(np.uint8)
412
+
413
+ return Image.fromarray(enhanced)
414
 
415
+ return target_image
416
 
417
+ def create_reference_database(reference_images):
418
+ """Process multiple reference images to create enhanced style database"""
 
419
 
420
+ if not reference_images:
421
+ return None
 
 
 
 
 
422
 
423
+ database = {
424
+ 'color_stats': [],
425
+ 'texture_features': [],
426
+ 'scene_brightness': [],
427
+ 'amateur_chars': [] # NEW: Amateur photography characteristics
428
+ }
429
 
430
+ for ref_img in reference_images:
431
+ if ref_img is not None:
432
+ color_stats = extract_color_statistics(ref_img)
433
+ texture_features = extract_texture_features(ref_img)
434
+ amateur_chars = analyze_amateur_photography_characteristics(ref_img) # NEW
435
+
436
+ if color_stats is not None:
437
+ database['color_stats'].append(color_stats)
438
+ if texture_features is not None:
439
+ database['texture_features'].append(texture_features)
440
+ if amateur_chars is not None:
441
+ database['amateur_chars'].append(amateur_chars)
442
+
443
+ # Scene brightness for matching
444
+ avg_brightness = np.mean(cv2.cvtColor(np.array(ref_img), cv2.COLOR_RGB2GRAY))
445
+ database['scene_brightness'].append(avg_brightness)
446
 
447
+ return database if database['color_stats'] else None
448
 
449
+ def enhanced_reference_style_transfer(target_image, reference_database, strength=0.6, method="enhanced_amateur"):
450
+ """Enhanced style transfer with amateur photography characteristics"""
 
 
451
 
452
+ if not reference_database or not reference_database.get('color_stats'):
453
+ return target_image
454
 
455
+ # Find best matching reference
456
+ target_brightness = np.mean(cv2.cvtColor(np.array(target_image), cv2.COLOR_RGB2GRAY))
457
 
458
+ best_ref_idx = 0
459
+ min_brightness_diff = float('inf')
 
 
460
 
461
+ for i, ref_brightness in enumerate(reference_database['scene_brightness']):
462
+ brightness_diff = abs(target_brightness - ref_brightness)
463
+ if brightness_diff < min_brightness_diff:
464
+ min_brightness_diff = brightness_diff
465
+ best_ref_idx = i
466
 
467
+ result = target_image
 
 
468
 
469
+ # Apply different methods
470
+ if method == "color_matching":
471
+ best_color_stats = reference_database['color_stats'][best_ref_idx]
472
+ result = apply_color_matching(result, best_color_stats, strength)
473
+
474
+ elif method == "texture_matching":
475
+ best_texture = reference_database['texture_features'][best_ref_idx]
476
+ result = apply_texture_matching(result, best_texture, strength)
477
+
478
+ elif method == "enhanced_amateur":
479
+ # Full amateur photography emulation
480
+ best_color_stats = reference_database['color_stats'][best_ref_idx]
481
+ best_texture = reference_database['texture_features'][best_ref_idx]
482
+
483
+ # Apply color and texture matching first
484
+ result = apply_color_matching(result, best_color_stats, strength * 0.7)
485
+ result = apply_texture_matching(result, best_texture, strength * 0.3)
486
+
487
+ # Apply amateur photography characteristics
488
+ if 'amateur_chars' in reference_database and len(reference_database['amateur_chars']) > best_ref_idx:
489
+ best_amateur_chars = reference_database['amateur_chars'][best_ref_idx]
490
+
491
+ # Apply amateur exposure characteristics
492
+ result = emulate_point_and_click_exposure(result, best_amateur_chars, strength)
493
+
494
+ # Apply amateur focus characteristics
495
+ result = apply_amateur_focus_characteristics(result, best_amateur_chars, strength * 0.7)
496
+
497
+ # Apply amateur flash realism
498
+ result = apply_amateur_flash_realism(result, best_amateur_chars, strength * 0.8)
499
+
500
+ return result
501
 
502
+ def apply_reference_style_transfer(target_image, reference_database, strength=0.6, method="advanced_blend"):
503
+ """Apply enhanced reference style transfer"""
504
+ return enhanced_reference_style_transfer(target_image, reference_database, strength, method)
505
 
506
+ # ----------------------
507
+ # Original Core Functions (unchanged)
508
+ # ----------------------
509
  def crop_4_3(img: Image.Image):
510
  w, h = img.size
511
  target_ratio = 4/3
 
519
  top = max(0, int((h - new_h) * 0.3))
520
  return img.crop((0, top, w, top + new_h))
521
 
522
+ def apply_lens_distortion(bgr, strength=0.01):
523
+ if strength <= 0:
524
+ return bgr
525
+ h, w = bgr.shape[:2]
526
+ y, x = np.ogrid[:h, :w]
527
+ cx, cy = w/2, h/2
528
+ x_norm = (x - cx) / cx
529
+ y_norm = (y - cy) / cy
530
+ r = np.sqrt(x_norm**2 + y_norm**2)
531
+ distortion = 1 + strength * r**2
532
+ map_x = (x_norm * distortion * cx + cx).astype(np.float32)
533
+ map_y = (y_norm * distortion * cy + cy).astype(np.float32)
534
+ return cv2.remap(bgr, map_x, map_y, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101)
535
+
536
  def enhanced_vignette(bgr, strength=0.15, feather=1.8):
537
  if strength <= 0:
538
  return bgr
 
565
  out = cv2.cvtColor(clamp_u8(yuv), cv2.COLOR_YUV2BGR)
566
  return out
567
 
568
+ def enhanced_chroma_noise(bgr, amount=4.0):
569
+ if amount <= 0:
570
+ return bgr
571
+ ycrcb = cv2.cvtColor(bgr, cv2.COLOR_BGR2YCrCb).astype(np.float32)
572
+ y, cr, cb = cv2.split(ycrcb)
573
+ h, w = cr.shape
574
+ cr_n = np.random.normal(0, amount * 0.5, (h, w)).astype(np.float32)
575
+ cb_n = np.random.normal(0, amount * 0.5, (h, w)).astype(np.float32)
576
+ cb_n = cb_n * 0.7 + cr_n * 0.3
577
+ cr = np.clip(cr + cr_n, 0, 255)
578
+ cb = np.clip(cb + cb_n, 0, 255)
579
+ return cv2.cvtColor(np.stack([y, cr, cb], axis=-1).astype(np.uint8), cv2.COLOR_YCrCb2BGR)
580
+
581
+ def authentic_2000s_tone_curve(bgr, amount=1.0):
582
+ if amount <= 0:
583
+ return bgr
584
+ x = np.linspace(0, 1, 256)
585
+ tone = np.where(
586
+ x < 0.5,
587
+ 0.18 + 0.60 * (2 * x) ** 0.9,
588
+ 0.82 - 0.15 * (2 * (1 - x)) ** 1.1
589
+ )
590
+ lut = (np.clip(tone, 0, 1) * 255).astype(np.uint8)
591
+ curved = np.empty_like(bgr)
592
+ for c in range(3):
593
+ curved[:, :, c] = cv2.LUT(bgr[:, :, c], lut)
594
+ return (bgr.astype(np.float32) * (1 - amount) + curved.astype(np.float32) * amount).astype(np.uint8)
595
+
596
+ def early_digital_wb(bgr, preset="auto"):
597
+ presets = {
598
+ "auto": {"temp_shift": 8, "tint_shift": 4, "saturation": 0.88},
599
+ "daylight": {"temp_shift": 0, "tint_shift": 2, "saturation": 0.95},
600
+ "cloudy": {"temp_shift": -6, "tint_shift": 1, "saturation": 0.92},
601
+ "tungsten": {"temp_shift": 25,"tint_shift": 8, "saturation": 0.85},
602
+ "fluorescent": {"temp_shift": 15,"tint_shift": -5, "saturation": 0.90},
603
+ }
604
+ s = presets.get(preset, presets["auto"])
605
+ b, g, r = cv2.split(bgr.astype(np.int16))
606
+ if s["temp_shift"] > 0:
607
+ b = np.clip(b + s["temp_shift"], 0, 255)
608
+ r = np.clip(r - s["temp_shift"] // 2, 0, 255)
609
+ else:
610
+ r = np.clip(r - s["temp_shift"], 0, 255)
611
+ b = np.clip(b + s["temp_shift"] // 2, 0, 255)
612
+ g = np.clip(g + s["tint_shift"], 0, 255)
613
+ result = cv2.merge([b.astype(np.uint8), g.astype(np.uint8), r.astype(np.uint8)])
614
+ hsv = cv2.cvtColor(result, cv2.COLOR_BGR2HSV).astype(np.float32)
615
+ hsv[:, :, 1] *= s["saturation"]
616
+ hsv[:, :, 1] = np.clip(hsv[:, :, 1], 0, 255)
617
+ return cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR)
618
+
619
+ def ccd_blooming_effect(bgr, threshold=240, bloom_size=2):
620
+ gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)
621
+ mask = (gray > threshold).astype(np.uint8)
622
+ if not np.any(mask):
623
+ return bgr
624
+ kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (bloom_size, bloom_size))
625
+ bloomed = cv2.dilate(mask, kernel, iterations=1)
626
+ out = bgr.astype(np.float32)
627
+ bloom_factor = 1.08
628
+ for i in range(3):
629
+ out[:, :, i] = np.where(bloomed > 0, np.minimum(out[:, :, i] * bloom_factor, 255), out[:, :, i])
630
+ return out.astype(np.uint8)
631
+
632
+ def enhanced_center_sharpness(pil_img: Image.Image, strength=0.3):
633
+ arr = np.array(pil_img)
634
+ h, w = arr.shape[:2]
635
+ kernel = np.array([[-0.1, -0.1, -0.1],
636
+ [-0.1, 2.2, -0.1],
637
+ [-0.1, -0.1, -0.1]])
638
+ sharp = cv2.filter2D(arr, -1, kernel)
639
+ y, x = np.ogrid[:h, :w]
640
+ cx, cy = w/2, h/2
641
+ dist = np.sqrt((x - cx)**2 + (y - cy)**2)
642
+ mask = 1 - (dist / np.sqrt(cx**2 + cy**2))
643
+ mask = np.clip(mask, 0, 1) ** 2
644
+ res = arr.astype(np.float32) * (1 - mask[..., None] * strength) + sharp.astype(np.float32) * (mask[..., None] * strength)
645
+ return Image.fromarray(np.clip(res, 0, 255).astype(np.uint8))
646
+
647
  def authentic_jpeg_compression(pil_img: Image.Image, quality=55, add_artifacts=False):
648
  def compress_once(im, q):
649
  buf = io.BytesIO()
 
655
  out = compress_once(out, int(min(95, quality + 10)))
656
  return out
657
 
658
+ # Russian film stocks and enhanced features
659
+ def authentic_russian_film_stocks(bgr, stock="svema", strength=0.5):
660
+ if strength <= 0:
661
+ return bgr
662
+
663
+ stocks = {
664
+ "svema": {"shadow_tint": (0, 8, -3), "highlight_tint": (5, -2, 8), "saturation": 0.92, "contrast": 1.08},
665
+ "orwo": {"shadow_tint": (-2, 3, 6), "highlight_tint": (2, 0, -4), "saturation": 0.95, "contrast": 1.12},
666
+ "tasma": {"shadow_tint": (2, -1, 4), "highlight_tint": (3, 2, -1), "saturation": 0.88, "contrast": 1.05}
667
+ }
668
+
669
+ if stock not in stocks:
670
+ stock = "svema"
671
+
672
+ s = stocks[stock]
673
+ result = bgr.astype(np.float32)
674
+
675
+ gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY).astype(np.float32) / 255.0
676
+ shadow_mask = np.maximum(0, 1 - gray * 2)
677
+ highlight_mask = np.maximum(0, (gray - 0.5) * 2)
678
+
679
+ for i, (shadow_shift, highlight_shift) in enumerate(zip(s["shadow_tint"], s["highlight_tint"])):
680
+ result[:,:,i] += shadow_mask * shadow_shift * strength
681
+ result[:,:,i] += highlight_mask * highlight_shift * strength
682
+
683
+ result = np.clip(result, 0, 255).astype(np.uint8)
684
+
685
+ hsv = cv2.cvtColor(result, cv2.COLOR_BGR2HSV).astype(np.float32)
686
+ hsv[:,:,1] *= (s["saturation"] ** strength)
687
+ hsv[:,:,2] *= (s["contrast"] ** (strength * 0.5))
688
+ hsv = np.clip(hsv, 0, 255)
689
+
690
+ return cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR)
691
+
692
+ def add_tungsten_indoor_warmth(bgr, strength=0.3):
693
+ if strength <= 0:
694
+ return bgr
695
+ gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY).astype(np.float32) / 255.0
696
+ depth_proxy = 1 - gray
697
+ result = bgr.astype(np.float32)
698
+ warm_mask = depth_proxy * strength
699
+ result[:,:,2] += warm_mask * 25
700
+ result[:,:,1] += warm_mask * 12
701
+ result[:,:,0] -= warm_mask * 8
702
+ return np.clip(result, 0, 255).astype(np.uint8)
703
+
704
+ def add_fluorescent_flicker(bgr, strength=0.2):
705
+ if strength <= 0:
706
+ return bgr
707
+ flicker = 1 + np.random.normal(0, strength * 0.05)
708
+ flicker = np.clip(flicker, 0.85, 1.15)
709
+ result = bgr.astype(np.float32) * flicker
710
+ green_var = np.random.normal(1, strength * 0.03)
711
+ result[:,:,1] *= green_var
712
+ return np.clip(result, 0, 255).astype(np.uint8)
713
+
714
+ def add_party_atmosphere(bgr, strength=0.3):
715
+ if strength <= 0:
716
+ return bgr
717
+ result = bgr.astype(np.float32)
718
+ result *= (1 + strength * 0.15)
719
+ hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV)
720
+ lower_skin = np.array([0, 25, 50])
721
+ upper_skin = np.array([25, 255, 255])
722
+ skin_mask = cv2.inRange(hsv, lower_skin, upper_skin).astype(np.float32) / 255.0
723
+ result[:,:,2] += skin_mask * strength * 15
724
+ result[:,:,1] += skin_mask * strength * 8
725
+ return np.clip(result, 0, 255).astype(np.uint8)
726
+
727
+ def apply_scene_preset(bgr, scene="none", intensity=1.0):
728
+ if scene == "none":
729
+ return bgr
730
+
731
+ result = bgr.copy()
732
+
733
+ if scene == "kitchen_party":
734
+ result = authentic_russian_film_stocks(result, "svema", intensity * 0.6)
735
+ result = add_tungsten_indoor_warmth(result, intensity * 0.4)
736
+ result = add_party_atmosphere(result, intensity * 0.5)
737
+ elif scene == "winter_street":
738
+ result = authentic_russian_film_stocks(result, "orwo", intensity * 0.7)
739
+ result = result.astype(np.float32)
740
+ result[:,:,0] += intensity * 8
741
+ result = np.clip(result, 0, 255).astype(np.uint8)
742
+ elif scene == "apartment_interior":
743
+ result = authentic_russian_film_stocks(result, "tasma", intensity * 0.5)
744
+ result = add_tungsten_indoor_warmth(result, intensity * 0.3)
745
+ result = add_fluorescent_flicker(result, intensity * 0.2)
746
+ elif scene == "dacha_summer":
747
+ result = authentic_russian_film_stocks(result, "svema", intensity * 0.4)
748
+ hsv = cv2.cvtColor(result, cv2.COLOR_BGR2HSV).astype(np.float32)
749
+ green_mask = ((hsv[:,:,0] > 40) & (hsv[:,:,0] < 80)).astype(np.float32)
750
+ hsv[:,:,1] += green_mask * intensity * 15
751
+ hsv = np.clip(hsv, 0, 255)
752
+ result = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR)
753
+
754
+ return result
755
+
756
+ # Video/TV effects
757
+ def radial_chromatic_aberration(bgr, pixels=1.0):
758
+ if pixels <= 0:
759
+ return bgr
760
+ h, w = bgr.shape[:2]
761
+ y, x = np.indices((h, w), dtype=np.float32)
762
+ cx, cy = np.float32(w / 2.0), np.float32(h / 2.0)
763
+ dx = x - cx
764
+ dy = y - cy
765
+ r = np.sqrt(dx * dx + dy * dy) + 1e-6
766
+ r_norm = r / np.sqrt(cx * cx + cy * cy)
767
+ shift = (np.float32(pixels) * r_norm)
768
+ ux = dx / r
769
+ uy = dy / r
770
+ map_x_out = np.ascontiguousarray((x + ux * shift).astype(np.float32))
771
+ map_y_out = np.ascontiguousarray((y + uy * shift).astype(np.float32))
772
+ map_x_in = np.ascontiguousarray((x - ux * shift).astype(np.float32))
773
+ map_y_in = np.ascontiguousarray((y - uy * shift).astype(np.float32))
774
+ b, g, rch = cv2.split(bgr)
775
+ rch = cv2.remap(rch, map_x_out, map_y_out, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101)
776
+ b = cv2.remap(b, map_x_in, map_y_in, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101)
777
+ return cv2.merge([b, g, rch])
778
+
779
+ def composite_chroma_bleed(bgr, amount=0.3, offset_px=1):
780
+ if amount <= 0:
781
+ return bgr
782
+ ycrcb = cv2.cvtColor(bgr, cv2.COLOR_BGR2YCrCb).astype(np.float32)
783
+ y, cr, cb = cv2.split(ycrcb)
784
+ k = max(1, int(3 + amount * 12))
785
+ cr_b = cv2.blur(cr, (k, 1))
786
+ cb_b = cv2.blur(cb, (k, 1))
787
+ if offset_px != 0:
788
+ M = np.float32([[1, 0, offset_px], [0, 1, 0]])
789
+ cr_b = cv2.warpAffine(cr_b, M, (cr.shape[1], cr.shape[0]), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE)
790
+ cb_b = cv2.warpAffine(cb_b, M, (cb.shape[1], cb.shape[0]), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE)
791
+ out = cv2.cvtColor(np.stack([y, cr_b, cb_b], axis=-1).astype(np.uint8), cv2.COLOR_YCrCb2BGR)
792
+ return out
793
+
794
+ def add_interlace_combing(bgr, amount=0.3, horiz_px=2):
795
+ if amount <= 0:
796
+ return bgr
797
+ h, w = bgr.shape[:2]
798
+ out = bgr.copy()
799
+ delta = int(max(1, horiz_px * amount * 5))
800
+ out[::2] = np.roll(out[::2], shift=delta, axis=1)
801
+ lines = np.ones((h, 1, 1), np.float32)
802
+ lines[::2] *= (1.0 - 0.15 * amount)
803
+ out = clamp_u8(out.astype(np.float32) * lines)
804
+ return out
805
+
806
+ def add_tv_scanlines(bgr, strength=0.02):
807
+ if strength <= 0:
808
+ return bgr
809
+ h, w = bgr.shape[:2]
810
+ lines = np.ones((h, 1, 1), np.float32)
811
+ darken = np.clip(strength, 0.0, 0.35)
812
+ lines[::2] *= (1.0 - darken)
813
+ out = clamp_u8(bgr.astype(np.float32) * lines)
814
+ return out
815
+
816
+ def add_low_bitrate_artifacts(bgr, strength=0.3, block_size=16, ringing=0.3):
817
+ if strength <= 0:
818
+ return bgr
819
+ h, w = bgr.shape[:2]
820
+ factor = max(1, int(block_size * (0.8 + 1.7 * strength)))
821
+ small_w = max(1, w // factor)
822
+ small_h = max(1, h // factor)
823
+ small = cv2.resize(bgr, (small_w, small_h), interpolation=cv2.INTER_LINEAR)
824
+ up = cv2.resize(small, (w, h), interpolation=cv2.INTER_NEAREST)
825
+ if ringing > 0:
826
+ blur = cv2.GaussianBlur(up, (0, 0), 0.8 + 1.6 * ringing)
827
+ up = cv2.addWeighted(up, 1 + 0.9 * ringing, blur, -0.9 * ringing, 0)
828
+ pil = to_pil(up)
829
+ q = int(np.clip(48 - 28 * strength, 8, 60))
830
+ pil = authentic_jpeg_compression(pil, quality=q, add_artifacts=True)
831
+ return to_np(pil)
832
+
833
+ def add_print_border(pil_img: Image.Image, enable=False, width_rel=0.04, color=(245, 245, 245)):
834
+ if not enable or width_rel <= 0:
835
+ return pil_img
836
+ w, h = pil_img.size
837
+ border = int(min(w, h) * width_rel)
838
+ canvas = Image.new("RGB", (w + border * 2, h + int(border * 2.2)), color)
839
+ canvas.paste(pil_img, (border, border))
840
+ return canvas
841
+
842
+ def lab_color_cast(bgr, preset="none", amount=0.3):
843
+ if amount <= 0 or preset == "none":
844
+ return bgr
845
+ y = cv2.cvtColor(bgr, cv2.COLOR_BGR2YCrCb)[:, :, 0].astype(np.float32) / 255.0
846
+ r, g, b = bgr[:, :, 2].astype(np.float32), bgr[:, :, 1].astype(np.float32), bgr[:, :, 0].astype(np.float32)
847
+ if preset == "fuji_warm_magenta_shadows":
848
+ t_high = smoothstep(y, 0.55, 0.95)
849
+ t_shad = 1.0 - smoothstep(y, 0.15, 0.45)
850
+ r += amount * (22.0 * t_high + 12.0 * t_shad)
851
+ g += amount * (14.0 * t_high - 8.0 * t_shad)
852
+ b += amount * (0.0 * t_high + 10.0 * t_shad)
853
+ elif preset == "kodak_cool_mids":
854
+ t_mid = np.exp(-((y - 0.55) ** 2) / (2 * 0.12 ** 2))
855
+ r -= amount * (12.0 * t_mid)
856
+ g += amount * (6.0 * t_mid)
857
+ b += amount * (16.0 * t_mid)
858
+ elif preset == "minilab_greenish":
859
+ t_all = smoothstep(y, 0.2, 0.9)
860
+ g += amount * (18.0 * t_all)
861
+ r -= amount * (6.0 * (1 - t_all))
862
+ hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV).astype(np.float32)
863
+ hsv[:, :, 1] *= (1 - 0.06 * amount)
864
+ bgr = cv2.cvtColor(clamp_u8(hsv), cv2.COLOR_HSV2BGR)
865
+ r, g, b = bgr[:, :, 2].astype(np.float32), bgr[:, :, 1].astype(np.float32), bgr[:, :, 0].astype(np.float32)
866
+ out = np.stack([clamp_u8(b), clamp_u8(g), clamp_u8(r)], axis=-1)
867
+ return out
868
+
869
+ def add_scan_dust_hairs(pil_img: Image.Image, density=0.25, strength=0.6, hair_prob=0.25, size_factor=1.0):
870
+ if density <= 0 or strength <= 0:
871
+ return pil_img
872
+ w, h = pil_img.size
873
+ area = w * h
874
+ n = int(max(1, (area / 55000.0) * float(density)))
875
+ dark = Image.new("L", (w, h), 0)
876
+ bright = Image.new("L", (w, h), 0)
877
+ ddraw = ImageDraw.Draw(dark)
878
+ bdraw = ImageDraw.Draw(bright)
879
+ for _ in range(n):
880
+ if random.random() < hair_prob:
881
+ x0 = random.randint(0, w - 1)
882
+ y0 = random.randint(0, h - 1)
883
+ length = int(random.uniform(30, 120) * size_factor)
884
+ angle = random.uniform(0, math.pi)
885
+ x1 = int(np.clip(x0 + length * math.cos(angle), 0, w - 1))
886
+ y1 = int(np.clip(y0 + length * math.sin(angle), 0, h - 1))
887
+ width = random.choice([1, 1, 2])
888
+ if random.random() < 0.6:
889
+ ddraw.line((x0, y0, x1, y1), fill=random.randint(160, 255), width=width)
890
+ else:
891
+ bdraw.line((x0, y0, x1, y1), fill=random.randint(140, 220), width=width)
892
+ else:
893
+ cx = random.randint(0, w - 1)
894
+ cy = random.randint(0, h - 1)
895
+ r = int(random.uniform(1, 3.5) * size_factor)
896
+ bbox = (cx - r, cy - r, cx + r, cy + r)
897
+ if random.random() < 0.5:
898
+ ddraw.ellipse(bbox, fill=random.randint(160, 255))
899
+ else:
900
+ bdraw.ellipse(bbox, fill=random.randint(140, 220))
901
+ dark = dark.filter(ImageFilter.GaussianBlur(radius=0.8 + 1.2 * strength))
902
+ bright = bright.filter(ImageFilter.GaussianBlur(radius=0.8 + 1.2 * strength))
903
+ base = np.array(pil_img).astype(np.float32)
904
+ d = np.array(dark).astype(np.float32) / 255.0
905
+ b = np.array(bright).astype(np.float32) / 255.0
906
+ amt = 28.0 * float(strength)
907
+ base -= d[..., None] * amt
908
+ base += b[..., None] * (amt * 0.9)
909
+ base = np.clip(base, 0, 255).astype(np.uint8)
910
+ return Image.fromarray(base)
911
+
912
+ def apply_chaos(bgr, amount=0.2):
913
+ if amount <= 0:
914
+ return bgr
915
+ h, w = bgr.shape[:2]
916
+ out = bgr.copy()
917
+ max_shift = 2.0 * amount
918
+ tx = np.random.uniform(-max_shift, max_shift)
919
+ ty = np.random.uniform(-max_shift, max_shift)
920
+ M = np.float32([[1, 0, tx], [0, 1, ty]])
921
+ out = cv2.warpAffine(out, M, (w, h), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101)
922
+ amp = 2.0 * amount
923
+ freq = np.random.uniform(1.0, 3.0)
924
+ phase = np.random.uniform(0, 2*np.pi)
925
+ shifts = (amp * np.sin(phase + (np.arange(h) / max(h,1)) * 2*np.pi*freq)).astype(np.int32)
926
+ for y in range(h):
927
+ if shifts[y] != 0:
928
+ out[y] = np.roll(out[y], shifts[y], axis=0)
929
+ n_hot = int(amount * w * h * 0.00005)
930
+ for _ in range(n_hot):
931
+ y = random.randint(0, h - 1)
932
+ x = random.randint(0, w - 1)
933
+ color = random.choice([(255, 255, 255), (255, 240, 220), (255, 255, 200)])
934
+ out[y, x] = color
935
+ if n_hot > 0:
936
+ out = cv2.GaussianBlur(out, (0, 0), 0.25 + 0.6 * amount)
937
+ return out
938
+
939
+ def add_russian_timestamp_styles(pil_img: Image.Image, date_text: str, style="russian"):
940
+ months = ["ЯНВ","ФЕВ","МАР","АПР","МАЙ","ИЮН","ИЮЛ","АВГ","СЕН","ОКТ","НОЯ","ДЕК"]
941
+ try:
942
+ d, m, y = date_text.split(".")
943
+ m_i = int(m)
944
+ rus = f"{int(d):02d} {months[m_i-1]} {int(y)}"
945
+ except Exception:
946
+ rus = date_text
947
+ draw = ImageDraw.Draw(pil_img)
948
+ w, h = pil_img.size
949
+ font_size = max(12, min(w, h) // 40)
950
+ try:
951
+ font = ImageFont.truetype("DejaVuSansMono.ttf", font_size)
952
+ except:
953
+ font = ImageFont.load_default()
954
+ x_pos, y_pos = w - 10, h - 10
955
+ for dx in (-1, 0, 1):
956
+ for dy in (-1, 0, 1):
957
+ if dx or dy:
958
+ draw.text((x_pos + dx, y_pos + dy), rus, anchor="rd", fill=(0, 0, 0), font=font)
959
+ draw.text((x_pos, y_pos), rus, anchor="rd", fill=(255, 200, 0), font=font)
960
+ return pil_img
961
+
962
+ def add_authentic_timestamp(pil_img: Image.Image, date_text: str, style="digital"):
963
+ draw = ImageDraw.Draw(pil_img)
964
+ w, h = pil_img.size
965
+ font_size = max(12, min(w, h) // 40)
966
+ try:
967
+ font = ImageFont.truetype("DejaVuSansMono.ttf", font_size)
968
+ except:
969
+ font = ImageFont.load_default()
970
+ if style == "digital":
971
+ x_pos, y_pos = w - 10, h - 10
972
+ for dx in (-1, 0, 1):
973
+ for dy in (-1, 0, 1):
974
+ if dx or dy:
975
+ draw.text((x_pos + dx, y_pos + dy), date_text, anchor="rd", fill=(0, 0, 0), font=font)
976
+ draw.text((x_pos, y_pos), date_text, anchor="rd", fill=(255, 200, 0), font=font)
977
+ else:
978
+ try:
979
+ small_font = ImageFont.truetype("DejaVuSansMono.ttf", max(8, font_size - 4))
980
+ except:
981
+ small_font = font
982
+ draw.text((10, h - 10), date_text, anchor="ld", fill=(255, 255, 255), font=small_font)
983
+ return pil_img
984
+
985
+ def add_motion_blur(pil_img: Image.Image, strength=0.8):
986
+ if strength <= 0:
987
+ return pil_img
988
+ k = max(3, int(3 + strength * 6))
989
+ kernel = np.zeros((k, k), np.float32)
990
+ kernel[k // 2, :] = 1.0 / k
991
+ arr = np.array(pil_img)
992
+ blurred = cv2.filter2D(arr, -1, kernel)
993
+ return Image.fromarray(blurred)
994
+
995
+ def add_cheap_flash_effect(bgr, strength=0.08):
996
+ if strength <= 0:
997
+ return bgr
998
+ out = bgr.astype(np.float32)
999
+ out = out * (1.0 + strength * 0.3)
1000
+ out[:, :, 0] += 12 * strength
1001
+ out[:, :, 1] += 8 * strength
1002
+ out = np.clip(out, 0, 255).astype(np.uint8)
1003
+ lut = np.arange(256, dtype=np.float32)
1004
+ lut = np.clip(lut + (30 * strength) * (1 - (lut / 255.0)), 0, 255).astype(np.uint8)
1005
+ for c in range(3):
1006
+ out[:, :, c] = cv2.LUT(out[:, :, c], lut)
1007
+ return out
1008
+
1009
  def map_intensity(intensity_0_10: float):
1010
  base = float(np.clip(intensity_0_10 / 3.0, 0.0, 1.0))
1011
  s = 1.0 - (1.0 - base) ** 3
 
1014
  return s, boost
1015
 
1016
  # ----------------------
1017
+ # Main processing pipeline with style transfer
1018
  # ----------------------
1019
+ def process_image(
1020
  image,
1021
  intensity,
1022
+ wb_preset,
1023
+ add_date,
1024
+ date_style,
1025
+ custom_date,
 
 
 
 
 
 
 
 
 
 
1026
  grain_amount,
1027
  compression_level,
1028
+ flash_effect,
1029
+ motion_blur_strength,
1030
+ # Reference style transfer
1031
+ reference_images,
1032
+ style_strength,
1033
+ style_method,
1034
+ enable_style_transfer,
1035
+ # Scene and film controls
1036
+ scene_preset,
1037
+ film_stock,
1038
+ lighting_condition,
1039
+ # Video controls
1040
+ macroblock_strength,
1041
+ block_size,
1042
+ ringing_strength,
1043
+ interlace_amount,
1044
+ chroma_bleed_amount,
1045
+ scanlines_amount,
1046
+ # Optics/print
1047
+ chrom_ab_px,
1048
+ print_border_enable,
1049
+ print_border_width,
1050
+ # Lab & scan
1051
+ lab_preset,
1052
+ lab_amount,
1053
+ dust_enable,
1054
+ dust_density,
1055
+ dust_strength,
1056
+ hair_prob,
1057
+ speck_size,
1058
+ # Chaos
1059
+ chaos_amount,
1060
+ # Options
1061
+ keep_ratio,
1062
+ timestamp_layer,
1063
+ russian_style
1064
  ):
1065
  if image is None:
1066
  return None
 
1072
  original = image.convert("RGB")
1073
  pil = original.copy() if keep_ratio else crop_4_3(original)
1074
 
1075
+ # STEP 1: Apply reference style transfer FIRST (if enabled)
1076
+ if enable_style_transfer and reference_images:
1077
+ print(f"DEBUG: Style transfer enabled with {len(reference_images)} reference images")
1078
+ ref_db = create_reference_database(reference_images)
1079
+ print(f"DEBUG: Reference database created: {ref_db is not None}")
1080
+
1081
+ if ref_db:
1082
+ print(f"DEBUG: Database contains {len(ref_db.get('color_stats', []))} color stats")
1083
+ print(f"DEBUG: Applying style transfer with method={style_method}, strength={style_strength}")
1084
+
1085
+ # Store original for comparison
1086
+ original_array = np.array(pil)
1087
+
1088
+ # Apply style transfer
1089
+ pil = apply_reference_style_transfer(pil, ref_db, style_strength, style_method)
1090
+
1091
+ # Check if anything changed
1092
+ new_array = np.array(pil)
1093
+ difference = np.mean(np.abs(original_array.astype(float) - new_array.astype(float)))
1094
+ print(f"DEBUG: Style transfer difference: {difference} (should be > 0 if working)")
1095
+
1096
+ if difference < 1.0:
1097
+ print("WARNING: Style transfer made very little difference!")
1098
+ else:
1099
+ print("DEBUG: Failed to create reference database!")
1100
+ else:
1101
+ if not enable_style_transfer:
1102
+ print("DEBUG: Style transfer disabled")
1103
+ if not reference_images:
1104
+ print("DEBUG: No reference images provided")
1105
+ else:
1106
+ print("DEBUG: Style transfer skipped")
1107
 
1108
+ # Optional: bake timestamp BEFORE effects
1109
+ if add_date and timestamp_layer == "baked":
1110
+ if not custom_date:
1111
+ year = random.choice([1998, 1999, 2000, 2001, 2002])
1112
+ month = random.randint(1, 12)
1113
+ day = random.randint(1, 28)
1114
+ date_text = f"{day:02d}.{month:02d}.{year}"
1115
+ else:
1116
+ date_text = custom_date.strip()
1117
+
1118
+ if russian_style and date_style == "digital":
1119
+ pil = add_russian_timestamp_styles(pil, date_text, style="russian")
1120
+ else:
1121
+ pil = add_authentic_timestamp(pil, date_text, style=date_style)
1122
+
1123
+ # Pre-effects
1124
+ mb = min(3.0, float(motion_blur_strength) * 0.25 * s * boost)
1125
+ if mb > 0.01:
1126
+ pil = add_motion_blur(pil, strength=mb)
1127
+
1128
+ pil = enhanced_center_sharpness(pil, strength=min(0.45, 0.15 * s * boost))
1129
  bgr = to_np(pil)
1130
 
1131
+ # White balance
1132
+ bgr = early_digital_wb(bgr, wb_preset)
1133
+
1134
+ # Scene preset
1135
+ bgr = apply_scene_preset(bgr, scene_preset, intensity=s)
1136
+
1137
+ # Film stock (if not handled by scene preset)
1138
+ if scene_preset == "none" and film_stock != "none":
1139
+ bgr = authentic_russian_film_stocks(bgr, film_stock, strength=0.6 * s)
1140
+
1141
+ # Lighting conditions
1142
+ if lighting_condition == "tungsten_warmth":
1143
+ bgr = add_tungsten_indoor_warmth(bgr, strength=0.4 * s)
1144
+ elif lighting_condition == "fluorescent_flicker":
1145
+ bgr = add_fluorescent_flicker(bgr, strength=0.3 * s)
1146
+
1147
+ # Lab cast
1148
+ bgr = lab_color_cast(bgr, preset=lab_preset, amount=float(lab_amount) * (0.6 + 0.6 * s))
1149
+
1150
+ # Tone curve
1151
+ bgr = authentic_2000s_tone_curve(bgr, amount=min(1.0, 0.4 * s * (0.9 + 0.5 * (boost - 1))))
1152
+
1153
+ # Flash
1154
+ if flash_effect:
1155
+ bgr = add_cheap_flash_effect(bgr, strength=min(0.25, 0.05 * s * boost))
1156
+
1157
+ # Blooming
1158
+ bgr = ccd_blooming_effect(bgr, threshold=242, bloom_size=2)
1159
+
1160
+ # Optics
1161
+ bgr = apply_lens_distortion(bgr, strength=min(0.03, 0.004 * s * boost))
1162
+ bgr = radial_chromatic_aberration(bgr, pixels=min(3.0, float(chrom_ab_px) * (0.7 + 0.3 * s)))
 
1163
 
1164
  # Vignette
1165
+ bgr = enhanced_vignette(bgr, strength=min(0.4, 0.06 * s * boost), feather=1.8)
1166
 
1167
+ # Grain & chroma noise
1168
+ g_strength = min(30.0, (float(grain_amount) * 0.35 + 1.5) * s * boost)
 
 
1169
  bgr = realistic_film_grain(bgr, grain_strength=g_strength, grain_size=1.05)
1170
+ bgr = enhanced_chroma_noise(bgr, amount=min(12.0, 1.6 * s * boost))
1171
 
1172
+ # Video effects
1173
+ bgr = composite_chroma_bleed(bgr, amount=float(chroma_bleed_amount) * (0.4 + 0.8 * s), offset_px=1)
1174
+ bgr = add_interlace_combing(bgr, amount=float(interlace_amount), horiz_px=2)
1175
+ bgr = add_tv_scanlines(bgr, strength=float(scanlines_amount) * 0.25)
1176
 
1177
+ # Macroblocking
1178
+ bgr = add_low_bitrate_artifacts(
1179
+ bgr,
1180
+ strength=float(macroblock_strength) * (0.5 + 0.8 * s),
1181
+ block_size=int(block_size),
1182
+ ringing=float(ringing_strength)
1183
+ )
1184
+
1185
+ # JPEG compression
1186
+ pil_mid = to_pil(bgr)
1187
+ comp_norm = (float(compression_level) - 0.3) / (1.5 - 0.3)
1188
  comp_norm = float(np.clip(comp_norm, 0, 1))
1189
+ q = int(92 - (92 - 68) * comp_norm * min(1.5, s * (0.8 + 0.6 * (boost - 1))))
1190
+ add_2pass = (compression_level > 1.0) or (s > 0.7)
1191
  pil_mid = authentic_jpeg_compression(pil_mid, quality=int(np.clip(q, 30, 92)), add_artifacts=add_2pass)
1192
 
1193
  # Final blend
1194
  orig_aligned = original if keep_ratio else crop_4_3(original)
1195
+ mix = float(np.clip(0.08 + 0.67 * s * (0.9 + 0.6 * (boost - 1)), 0.08, 0.92))
 
 
 
 
1196
  processed = Image.blend(orig_aligned, pil_mid, alpha=mix)
1197
 
1198
+ # Chaos
1199
+ if chaos_amount > 0:
1200
+ bgr_chaos = to_np(processed)
1201
+ bgr_chaos = apply_chaos(bgr_chaos, amount=float(chaos_amount))
1202
+ processed = to_pil(bgr_chaos)
1203
+
1204
+ # Timestamp on top
1205
+ if add_date and timestamp_layer == "top":
1206
+ if not custom_date:
1207
+ year = random.choice([1998, 1999, 2000, 2001, 2002])
1208
+ month = random.randint(1, 12)
1209
+ day = random.randint(1, 28)
1210
+ date_text = f"{day:02d}.{month:02d}.{year}"
1211
+ else:
1212
+ date_text = custom_date.strip()
1213
+
1214
+ if russian_style and date_style == "digital":
1215
+ processed = add_russian_timestamp_styles(processed, date_text, style="russian")
1216
+ else:
1217
+ processed = add_authentic_timestamp(processed, date_text, style=date_style)
1218
+
1219
+ # Print border
1220
+ processed = add_print_border(processed, enable=bool(print_border_enable), width_rel=float(print_border_width))
1221
+
1222
+ # Scan dust/hairs
1223
+ if dust_enable:
1224
+ processed = add_scan_dust_hairs(
1225
+ processed,
1226
+ density=float(dust_density),
1227
+ strength=float(dust_strength),
1228
+ hair_prob=float(hair_prob),
1229
+ size_factor=float(speck_size)
1230
+ )
1231
 
1232
  return processed
1233
 
1234
+ # Processing function to handle file inputs
1235
+ def process_with_files(
1236
+ input_image,
1237
+ reference_images,
1238
+ style_strength,
1239
+ style_method,
1240
+ enable_style_transfer,
1241
+ intensity,
1242
+ wb_preset,
1243
+ add_date,
1244
+ date_style,
1245
+ custom_date,
1246
+ grain_amount,
1247
+ compression_level,
1248
+ flash_effect,
1249
+ motion_blur_strength,
1250
+ scene_preset,
1251
+ film_stock,
1252
+ lighting_condition,
1253
+ macroblock_strength,
1254
+ block_size,
1255
+ ringing_strength,
1256
+ interlace_amount,
1257
+ chroma_bleed_amount,
1258
+ scanlines_amount,
1259
+ chrom_ab_px,
1260
+ print_border_enable,
1261
+ print_border_width,
1262
+ lab_preset,
1263
+ lab_amount,
1264
+ dust_enable,
1265
+ dust_density,
1266
+ dust_strength,
1267
+ hair_prob,
1268
+ speck_size,
1269
+ chaos_amount,
1270
+ keep_ratio,
1271
+ timestamp_layer,
1272
+ russian_style
1273
+ ):
1274
+ # DEBUG: Print what we received
1275
+ print(f"DEBUG: enable_style_transfer = {enable_style_transfer}")
1276
+ print(f"DEBUG: reference_images type = {type(reference_images)}")
1277
+ print(f"DEBUG: style_strength = {style_strength}")
1278
+ print(f"DEBUG: style_method = {style_method}")
1279
+
1280
+ # Convert file inputs to PIL Images
1281
+ ref_images = []
1282
+
1283
+ if reference_images:
1284
+ print(f"DEBUG: Processing {len(reference_images)} reference files")
1285
+ for i, file in enumerate(reference_images):
1286
+ try:
1287
+ print(f"DEBUG: Processing file {i}: {file.name}")
1288
+ img = Image.open(file.name).convert("RGB")
1289
+ ref_images.append(img)
1290
+ print(f"DEBUG: Successfully loaded image {i}, size: {img.size}")
1291
+ except Exception as e:
1292
+ print(f"DEBUG: Failed to load file {i}: {e}")
1293
+ continue
1294
+ else:
1295
+ print("DEBUG: No reference images provided")
1296
+
1297
+ print(f"DEBUG: Successfully loaded {len(ref_images)} reference images")
1298
+
1299
+ # Call process_image with properly ordered arguments
1300
+ return process_image(
1301
+ input_image,
1302
+ intensity,
1303
+ wb_preset,
1304
+ add_date,
1305
+ date_style,
1306
+ custom_date,
1307
+ grain_amount,
1308
+ compression_level,
1309
+ flash_effect,
1310
+ motion_blur_strength,
1311
+ # Reference style transfer
1312
+ ref_images, # Pass the processed images here
1313
+ style_strength,
1314
+ style_method,
1315
+ enable_style_transfer,
1316
+ # Scene and film controls
1317
+ scene_preset,
1318
+ film_stock,
1319
+ lighting_condition,
1320
+ # Video controls
1321
+ macroblock_strength,
1322
+ block_size,
1323
+ ringing_strength,
1324
+ interlace_amount,
1325
+ chroma_bleed_amount,
1326
+ scanlines_amount,
1327
+ # Optics/print
1328
+ chrom_ab_px,
1329
+ print_border_enable,
1330
+ print_border_width,
1331
+ # Lab & scan
1332
+ lab_preset,
1333
+ lab_amount,
1334
+ dust_enable,
1335
+ dust_density,
1336
+ dust_strength,
1337
+ hair_prob,
1338
+ speck_size,
1339
+ # Chaos
1340
+ chaos_amount,
1341
+ # Options
1342
+ keep_ratio,
1343
+ timestamp_layer,
1344
+ russian_style
1345
+ )
1346
+
1347
  # ----------------------
1348
+ # Enhanced UI with Style Transfer
1349
  # ----------------------
1350
+ with gr.Blocks(title="Russian 2000s Filter with Reference Style Transfer", theme=gr.themes.Soft()) as demo:
1351
  gr.Markdown("""
1352
+ # 📷 Complete Russian 2000s Filter with Reference Style Transfer
1353
+ Transform your photos using authentic Russian film stocks, period effects, AND reference-based style transfer from real 2000s photos.
1354
  """)
1355
 
1356
  with gr.Row():
 
1358
  input_image = gr.Image(type="pil", label="📸 Upload Your Photo")
1359
 
1360
  with gr.Column(scale=1):
1361
+ output_image = gr.Image(type="pil", label="✨ Processed Photo", interactive=False)
1362
 
1363
+ # Main processing button right under the photos
1364
  with gr.Row():
1365
+ process_btn = gr.Button("🎬 Apply Complete Russian Filter with Style Transfer", variant="primary", size="lg")
1366
 
1367
  with gr.Row():
1368
  with gr.Column(scale=1):
1369
+ # All settings moved down here
1370
+ with gr.Accordion("🎨 Reference Style Transfer (Enhanced!)", open=True):
1371
  gr.Markdown("""
1372
+ **Upload 1-5 authentic Russian 2000s photos as style references**
1373
+ - Works on free tier (CPU processing)
1374
+ - **NEW**: Analyzes amateur photography characteristics:
1375
+ - Point-and-click exposure patterns (center-weighted metering)
1376
+ - Flash falloff and harsh lighting
1377
+ - Foreground overexposure vs background shadows
1378
+ - Amateur focus characteristics
1379
+ - Limited dynamic range simulation
1380
+ - Processing time: 10-15 seconds
1381
+ - **Debug mode enabled**: Check console for style transfer status
1382
  """)
1383
 
1384
+ with gr.Accordion("🔧 Debug & Testing", open=False):
1385
+ gr.Markdown("**Quick test to verify style transfer is working**")
1386
+
1387
+ # Add test buttons for debugging
1388
+ test_style_transfer = gr.Button("🔍 Test Style Transfer (Debug)", variant="secondary")
1389
+ test_simple = gr.Button("🎯 Simple Style Test (No Files)", variant="secondary")
1390
+ debug_output = gr.Textbox(label="Debug Output", lines=4, interactive=False)
1391
+
1392
+ reference_images = gr.File(
1393
+ file_count="multiple",
1394
+ file_types=["image"],
1395
+ label="Reference Photos (Upload 1-5 authentic Russian 2000s images)"
1396
  )
1397
 
1398
+ enable_style_transfer = gr.Checkbox(
1399
+ label="Enable Enhanced Reference Style Transfer",
1400
+ value=False
1401
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1402
 
1403
+ style_strength = gr.Slider(
1404
+ 0, 1, value=0.65, step=0.05,
1405
+ label="Style Transfer Strength"
1406
+ )
1407
+
1408
+ style_method = gr.Radio(
1409
+ choices=["color_matching", "texture_matching", "enhanced_amateur"],
1410
+ value="enhanced_amateur",
1411
+ label="Style Transfer Method",
1412
+ info="Enhanced Amateur = Full point-and-click camera emulation"
1413
+ )
 
 
 
 
 
 
 
1414
 
1415
  with gr.Accordion("🎛️ Basic Settings", open=True):
1416
  intensity = gr.Slider(0, 10, value=3.5, step=0.1, label="Overall Effect Intensity (0–10)")
1417
+ wb_preset = gr.Dropdown(
1418
+ choices=["auto", "daylight", "cloudy", "tungsten", "fluorescent"],
1419
+ value="tungsten",
1420
+ label="White Balance Preset"
1421
+ )
1422
  grain_amount = gr.Slider(2, 15, value=7, step=1, label="Film Grain Amount")
1423
+ compression_level = gr.Slider(0.3, 1.5, value=0.9, step=0.1, label="JPEG Compression Level")
1424
+ keep_ratio = gr.Checkbox(value=False, label="Keep Original Aspect Ratio (disable for authentic 4:3 crop)")
1425
 
1426
+ with gr.Accordion("🇷🇺 Russian/Eastern European Features", open=True):
1427
+ scene_preset = gr.Dropdown(
1428
+ choices=["none", "kitchen_party", "winter_street", "apartment_interior", "dacha_summer"],
1429
+ value="none",
1430
+ label="Scene Preset"
1431
+ )
1432
+
1433
+ film_stock = gr.Dropdown(
1434
+ choices=["none", "svema", "orwo", "tasma"],
1435
+ value="svema",
1436
+ label="Russian/Soviet Film Stock"
1437
+ )
1438
+
1439
+ lighting_condition = gr.Dropdown(
1440
+ choices=["none", "tungsten_warmth", "fluorescent_flicker"],
1441
+ value="none",
1442
+ label="Period Lighting Conditions"
1443
+ )
1444
+
1445
+ russian_style = gr.Checkbox(label="Russian Date Format (Cyrillic months)", value=False)
1446
+ flash_effect = gr.Checkbox(label="Cheap Camera Flash", value=True)
1447
+ motion_blur_strength = gr.Slider(0, 3, value=1, step=0.5, label="Motion Blur")
1448
+
1449
+ with gr.Accordion("📼 Video / TV Artifacts", open=False):
1450
+ macroblock_strength = gr.Slider(0, 1, value=0.4, step=0.05, label="Macroblocking Strength")
1451
+ block_size = gr.Slider(1, 32, value=16, step=1, label="Block Size (px)")
1452
+ ringing_strength = gr.Slider(0, 1, value=0.35, step=0.05, label="Ringing / Edge Halos")
1453
+ interlace_amount = gr.Slider(0, 1, value=0.15, step=0.05, label="Interlace Combing")
1454
+ chroma_bleed_amount = gr.Slider(0, 1, value=0.2, step=0.05, label="Chroma Bleed")
1455
+ scanlines_amount = gr.Slider(0, 1, value=0.15, step=0.05, label="CRT Scanlines")
1456
+
1457
+ with gr.Accordion("🔧 Optics & Print", open=False):
1458
+ chrom_ab_px = gr.Slider(0, 2.0, value=0.6, step=0.1, label="Chromatic Aberration (px)")
1459
+ print_border_enable = gr.Checkbox(label="Add 10×15 Minilab Border", value=False)
1460
+ print_border_width = gr.Slider(0.02, 0.08, value=0.04, step=0.005, label="Border Width")
1461
+
1462
+ with gr.Accordion("🧪 Lab & Scan Look", open=False):
1463
+ lab_preset = gr.Dropdown(
1464
+ choices=["none", "fuji_warm_magenta_shadows", "kodak_cool_mids", "minilab_greenish"],
1465
+ value="none",
1466
+ label="Lab Color Cast Preset"
1467
+ )
1468
+ lab_amount = gr.Slider(0, 1, value=0.3, step=0.05, label="Lab Cast Amount")
1469
+ dust_enable = gr.Checkbox(label="Add Scan Dust & Hairs", value=False)
1470
+ dust_density = gr.Slider(0, 1, value=0.25, step=0.05, label="Dust/Hair Density")
1471
+ dust_strength = gr.Slider(0, 1, value=0.6, step=0.05, label="Dust/Hair Contrast")
1472
+ hair_prob = gr.Slider(0, 1, value=0.25, step=0.05, label="Hair Probability")
1473
+ speck_size = gr.Slider(0.8, 2.5, value=1.0, step=0.1, label="Speck Size Factor")
1474
+
1475
+ with gr.Accordion("🎲 Chaos", open=False):
1476
+ chaos_amount = gr.Slider(0, 1, value=0.2, step=0.05, label="Micro Jitter, Wobble & Hot Pixels")
1477
+
1478
+ with gr.Accordion("📅 Timestamp Options", open=False):
1479
+ add_date = gr.Checkbox(label="Add Date Timestamp", value=True)
1480
+ date_style = gr.Radio(choices=["digital", "film_lab"], value="digital", label="Timestamp Style")
1481
+ custom_date = gr.Textbox(
1482
+ label="Custom Date (dd.mm.yyyy)",
1483
+ placeholder="14.08.2000",
1484
+ info="Leave empty for random date from 1998–2002"
1485
+ )
1486
+ timestamp_layer = gr.Radio(
1487
+ choices=["top", "baked"],
1488
+ value="top",
1489
+ label="Timestamp Layer"
1490
+ )
1491
+
1492
+ # Connect test buttons
1493
+ test_simple.click(
1494
+ fn=simple_style_test,
1495
+ inputs=[input_image],
1496
+ outputs=[debug_output]
1497
+ )
1498
+
1499
+ test_style_transfer.click(
1500
+ fn=test_style_transfer_debug,
1501
+ inputs=[input_image, reference_images],
1502
+ outputs=[debug_output]
1503
+ )
1504
+
1505
+ # Connect main processing button
1506
  process_btn.click(
1507
+ fn=process_with_files,
1508
  inputs=[
1509
+ input_image, reference_images, style_strength, style_method, enable_style_transfer,
1510
+ intensity, wb_preset, add_date, date_style, custom_date,
1511
+ grain_amount, compression_level, flash_effect, motion_blur_strength,
1512
+ scene_preset, film_stock, lighting_condition,
1513
+ macroblock_strength, block_size, ringing_strength, interlace_amount,
1514
+ chroma_bleed_amount, scanlines_amount,
1515
+ chrom_ab_px, print_border_enable, print_border_width,
1516
+ lab_preset, lab_amount, dust_enable, dust_density, dust_strength, hair_prob, speck_size,
1517
+ chaos_amount,
1518
+ keep_ratio, timestamp_layer, russian_style
1519
  ],
1520
  outputs=[output_image]
1521
  )
1522
 
1523
  gr.Markdown("""
1524
+ ### 🎯 ENHANCED: Reference Style Transfer Features:
1525
+ - **Upload Reference Photos**: 1-5 authentic Russian 2000s photos for style matching
1526
+ - **Color Matching**: Matches lighting, color grading, and atmosphere
1527
+ - **Texture Matching**: Adjusts contrast and visual texture based on references
1528
+ - **Enhanced Amateur Mode**: Full point-and-click camera emulation with:
1529
+ - **Center-weighted metering** simulation (subjects properly exposed, backgrounds over/under)
1530
+ - **Flash characteristics** (harsh falloff, foreground overexposure, cool color cast)
1531
+ - **Depth-based focus** (amateur focus patterns, background blur)
1532
+ - **Limited dynamic range** (shadow crushing, highlight clipping)
1533
+ - **Exposure patterns** typical of 2000s compact cameras
1534
+
1535
+ ### 💡 Enhanced Style Transfer Tips:
1536
+ - **Best references**: Family photos with flash, indoor gatherings, amateur compositions
1537
+ - **Enhanced Amateur mode**: Gives most authentic point-and-click camera results
1538
+ - **Flash photos work best**: References with visible flash create realistic amateur lighting
1539
+ - **Center-composed photos**: Works best with typically amateur-style center composition
1540
+
1541
+ ### 📸 What Enhanced Mode Emulates:
1542
+ - **Point-and-click cameras**: Canon PowerShot, Nikon Coolpix, Sony Mavica
1543
+ - **Center-weighted metering**: Subjects in center properly exposed, backgrounds blown/dark
1544
+ - **On-camera flash**: Harsh, direct flash with realistic falloff and color temperature
1545
+ - **Amateur focus patterns**: Everything in focus OR poorly focused backgrounds
1546
+ - **Cheap optics**: Limited dynamic range, highlight clipping, shadow crushing
1547
+
1548
+ ### 🎬 Recommended Workflow:
1549
+ 1. Upload your modern photo
1550
+ 2. Upload 3-5 flash photos from Russian family gatherings (1998-2002)
1551
+ 3. Enable "Enhanced Amateur" style transfer (strength 0.6-0.7)
1552
+ 4. Choose "Kitchen Party" or "Apartment Interior" scene preset
1553
+ 5. Use tungsten white balance + compression 0.9 for authentic look
1554
+ 6. Process and get genuine point-and-click camera results!
1555
+
1556
+ ### 🔧 Updated Default Settings:
1557
+ - **Block Size**: Now 8px (was 16px) for finer, more realistic artifacts
1558
+ - **White Balance**: Tungsten default (most common Russian indoor lighting)
1559
+ - **Compression**: 0.9 default (typical of 2000s digital cameras)
1560
+ - **4:3 Crop**: Now default ON (authentic camera aspect ratio)
1561
+ - **Intensity**: 3.5 default (slightly more effect for amateur camera look)
 
 
 
 
1562
  """)
1563
 
1564
  if __name__ == "__main__":