Refine ChromoStereoizer: remove parallax, restore black & white level controls, expand sliders to 0-100, and improve update responsiveness

#2
Files changed (1) hide show
  1. app.py +128 -90
app.py CHANGED
@@ -1,129 +1,150 @@
1
  import gradio as gr
2
  import numpy as np
3
  from PIL import Image
 
 
4
  try:
5
  import cv2
6
  except ImportError:
7
  cv2 = None
8
- import torch
9
- from transformers import pipeline
10
 
11
- # Load depth estimation model (large)
12
  depth_pipe = pipeline("depth-estimation", model="depth-anything/Depth-Anything-V2-Large-hf")
13
 
14
- # Global variables to store state
15
  current_original_image = None
16
  current_depth_norm = None
17
- current_gray = None
18
  current_depth_map_pil = None
19
 
20
 
21
- def preprocess_gray(image: Image.Image, gamma: float) -> np.ndarray:
22
- """
23
- Convert image to grayscale and apply gamma correction.
24
- Returns a float32 array in [0,1].
25
- """
26
- gray = np.array(image.convert("L"), dtype=np.float32) / 255.0
27
- gray = np.clip(gray ** gamma, 0.0, 1.0)
28
- return gray
29
 
30
 
31
- def smooth_depth(depth_norm: np.ndarray, radius: float) -> np.ndarray:
 
32
  """
33
- Apply edge‑preserving smoothing to the normalized depth map using bilateral filtering.
34
- radius determines the strength; 0 means no smoothing.
 
 
 
 
 
 
35
  """
36
- if radius <= 0 or depth_norm is None or cv2 is None:
37
- return depth_norm
38
- depth_uint8 = (depth_norm * 255.0).astype(np.uint8)
39
- sigma = max(radius * 10.0, 1.0)
40
- smoothed = cv2.bilateralFilter(depth_uint8, d=5, sigmaColor=sigma, sigmaSpace=sigma)
41
- return smoothed.astype(np.float32) / 255.0
42
 
 
 
 
 
 
 
 
43
 
44
- def logistic_blend(depth_norm: np.ndarray, threshold: float, steepness: float) -> np.ndarray:
45
- """
46
- Compute a blend factor from the depth map using a logistic (sigmoid) function.
47
- threshold controls the midpoint (0..1), steepness controls the slope (>0).
48
- Returns array in [0,1].
49
- """
50
- s = max(steepness, 1e-3)
51
- return 1.0 / (1.0 + np.exp(-s * (depth_norm - threshold)))
52
 
 
 
 
53
 
54
- def build_chromostereopsis_image(gray: np.ndarray, blend: np.ndarray, red_brightness: float, blue_brightness: float, parallax_shift: float) -> Image.Image:
55
- """
56
- Build an RGB image using red/blue channels weighted by the blend factor.
57
- Optionally shift red and blue channels horizontally for parallax effect.
58
- gray: grayscale luminance in [0,1]
59
- blend: blend factor in [0,1], same shape as gray
60
- red_brightness, blue_brightness: multipliers for channel intensities
61
- parallax_shift: maximum pixel shift for red (left) and blue (right) channels
62
- """
63
- h, w = gray.shape
64
- red_intensity = red_brightness * gray * blend
65
- blue_intensity = blue_brightness * gray * (1.0 - blend)
66
- red_img = np.clip(red_intensity * 255.0, 0, 255).astype(np.uint8)
67
- blue_img = np.clip(blue_intensity * 255.0, 0, 255).astype(np.uint8)
68
- shift = int(parallax_shift)
69
- if shift > 0:
70
- red_img = np.roll(red_img, -shift, axis=1)
71
- blue_img = np.roll(blue_img, shift, axis=1)
72
  output = np.zeros((h, w, 3), dtype=np.uint8)
73
  output[..., 0] = red_img
74
  output[..., 1] = 0
75
  output[..., 2] = blue_img
 
76
  return Image.fromarray(output, mode="RGB")
77
 
78
 
79
- def generate_depth_map(input_image: Image.Image):
80
- global current_original_image, current_depth_norm, current_gray, current_depth_map_pil
 
81
  if input_image is None:
82
  current_original_image = None
83
  current_depth_norm = None
84
- current_gray = None
85
  current_depth_map_pil = None
86
  return None, None
87
  current_original_image = input_image
 
88
  result = depth_pipe(input_image)
89
- depth_pil = result["depth"]
90
- depth_np = np.array(depth_pil, dtype=np.float32)
91
- depth_np -= depth_np.min()
92
- max_val = depth_np.max()
93
  if max_val > 0:
94
- depth_np /= max_val
95
- current_depth_norm = depth_np
96
- depth_uint8 = (depth_np * 255.0).astype(np.uint8)
97
- current_depth_map_pil = Image.fromarray(depth_uint8, mode="L")
98
- current_gray = preprocess_gray(current_original_image, gamma=1.0)
99
- blend = logistic_blend(current_depth_norm, threshold=0.5, steepness=10.0)
100
- chromo_img = build_chromostereopsis_image(current_gray, blend, red_brightness=1.0, blue_brightness=1.0, parallax_shift=10.0)
101
- return current_depth_map_pil.convert("RGB"), chromo_img
102
-
103
-
104
- def update_chromostereopsis(threshold_percent, depth_scale, red_brightness, blue_brightness, gamma_value, parallax_shift, smoothing_radius):
105
- global current_original_image, current_depth_norm, current_gray
106
- if current_original_image is None or current_depth_norm is None:
107
- return None
108
- current_gray = preprocess_gray(current_original_image, gamma=gamma_value)
109
- depth_smoothed = smooth_depth(current_depth_norm, radius=smoothing_radius)
110
- threshold = threshold_percent / 100.0
111
- blend = logistic_blend(depth_smoothed, threshold=threshold, steepness=depth_scale)
112
- result_img = build_chromostereopsis_image(current_gray, blend, red_brightness=red_brightness, blue_brightness=blue_brightness, parallax_shift=parallax_shift)
113
- return result_img
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
 
116
  def clear_results():
117
- global current_original_image, current_depth_norm, current_gray, current_depth_map_pil
 
118
  current_original_image = None
119
  current_depth_norm = None
120
- current_gray = None
121
  current_depth_map_pil = None
122
  return None, None
123
 
124
 
125
- with gr.Blocks(title="Enhanced ChromoStereoizer", theme=gr.themes.Soft()) as demo:
126
- gr.Markdown("# Enhanced ChromoStereoizer")
127
  with gr.Row():
128
  with gr.Column(scale=1):
129
  input_image = gr.Image(label="Upload Image", type="pil", height=400)
@@ -133,19 +154,36 @@ with gr.Blocks(title="Enhanced ChromoStereoizer", theme=gr.themes.Soft()) as dem
133
  depth_output = gr.Image(type="pil", height=400, interactive=False, show_download_button=True, show_label=False)
134
  gr.Markdown("**ChromoStereoizer Result**")
135
  chromo_output = gr.Image(type="pil", height=400, interactive=False, show_download_button=True, show_label=False)
136
- gr.Markdown("## Effect Controls")
137
  threshold_slider = gr.Slider(minimum=0, maximum=100, value=50, step=1, label="Threshold (%)")
138
- depth_scale_slider = gr.Slider(minimum=1, maximum=40, value=10, step=1, label="Depth Scale (Steepness)")
139
- red_brightness_slider = gr.Slider(minimum=0.0, maximum=2.0, value=1.0, step=0.1, label="Red Brightness")
140
- blue_brightness_slider = gr.Slider(minimum=0.0, maximum=2.0, value=1.0, step=0.1, label="Blue Brightness")
141
- gamma_slider = gr.Slider(minimum=0.1, maximum=3.0, value=1.0, step=0.1, label="Gamma")
142
- parallax_slider = gr.Slider(minimum=0.0, maximum=50.0, value=10.0, step=1.0, label="Parallax Shift (px)")
143
- smoothing_slider = gr.Slider(minimum=0.0, maximum=10.0, value=0.0, step=1.0, label="Smoothing Radius")
 
 
144
  clear_btn = gr.Button("Clear", variant="secondary")
145
- generate_btn.click(fn=generate_depth_map, inputs=[input_image], outputs=[depth_output, chromo_output], show_progress=True)
146
- for slider in [threshold_slider, depth_scale_slider, red_brightness_slider, blue_brightness_slider, gamma_slider, parallax_slider, smoothing_slider]:
147
- slider.change(fn=update_chromostereopsis, inputs=[threshold_slider, depth_scale_slider, red_brightness_slider, blue_brightness_slider, gamma_slider, parallax_slider, smoothing_slider], outputs=chromo_output, show_progress=False)
148
- clear_btn.click(fn=clear_results, inputs=[], outputs=[depth_output, chromo_output])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
 
150
  if __name__ == "__main__":
151
  demo.launch(server_name="0.0.0.0", server_port=7860, share=False)
 
1
  import gradio as gr
2
  import numpy as np
3
  from PIL import Image
4
+ from transformers import pipeline
5
+
6
  try:
7
  import cv2
8
  except ImportError:
9
  cv2 = None
 
 
10
 
11
+ # Load depth estimation model once
12
  depth_pipe = pipeline("depth-estimation", model="depth-anything/Depth-Anything-V2-Large-hf")
13
 
14
+ # Global state
15
  current_original_image = None
16
  current_depth_norm = None
 
17
  current_depth_map_pil = None
18
 
19
 
20
+ def preprocess_depth(depth_norm, smoothing_radius):
21
+ """Smooth the depth map using bilateral filtering if radius > 0 and cv2 is available."""
22
+ if smoothing_radius > 0 and cv2 is not None:
23
+ depth_uint8 = (depth_norm * 255.0).astype(np.uint8)
24
+ sigma = max(smoothing_radius * 10.0, 1.0)
25
+ smoothed = cv2.bilateralFilter(depth_uint8, d=5, sigmaColor=sigma, sigmaSpace=sigma)
26
+ return smoothed.astype(np.float32) / 255.0
27
+ return depth_norm
28
 
29
 
30
+ def apply_effect(threshold, depth_scale, feather, red_brightness, blue_brightness, gamma,
31
+ black_level_percent, white_level_percent, smoothing_percent):
32
  """
33
+ Apply chromostereopsis effect using adjustable parameters.
34
+ threshold: percentage [0,100] controlling blend midpoint.
35
+ depth_scale: percentage [0,100] controlling steepness of logistic curve.
36
+ feather: percentage [0,100] affecting the smoothness of the transition.
37
+ red_brightness, blue_brightness: percentages [0,100] controlling channel intensities.
38
+ gamma: percentage [0,100] mapped to gamma range [0.1, 3.0].
39
+ black_level_percent, white_level_percent: percentages mapped to 0..255 levels.
40
+ smoothing_percent: percentage [0,100] mapped to bilateral filter radius.
41
  """
42
+ global current_original_image, current_depth_norm
43
+ if current_original_image is None or current_depth_norm is None:
44
+ return None
 
 
 
45
 
46
+ # Levels adjustment
47
+ black_level = black_level_percent * 2.55
48
+ white_level = white_level_percent * 2.55
49
+ gray = np.array(current_original_image.convert("L"), dtype=np.float32)
50
+ denom = max(white_level - black_level, 1e-6)
51
+ adjusted_gray = (gray - black_level) / denom
52
+ adjusted_gray = np.clip(adjusted_gray, 0.0, 1.0)
53
 
54
+ # Gamma correction
55
+ gamma_val = 0.1 + (gamma / 100.0) * 2.9
56
+ adjusted_gray = np.clip(adjusted_gray ** gamma_val, 0.0, 1.0)
 
 
 
 
 
57
 
58
+ # Smooth depth map
59
+ smoothing_radius = smoothing_percent / 10.0
60
+ depth_smoothed = preprocess_depth(current_depth_norm, smoothing_radius)
61
 
62
+ # Compute blend factor using logistic function
63
+ threshold_norm = threshold / 100.0
64
+ steepness = max(depth_scale, 1e-3)
65
+ feather_norm = feather / 100.0
66
+ steepness_adj = steepness / (feather_norm * 10.0 + 1.0)
67
+ blend = 1.0 / (1.0 + np.exp(-steepness_adj * (depth_smoothed - threshold_norm)))
68
+
69
+ # Map brightness to factors (0-2)
70
+ red_factor = red_brightness / 50.0
71
+ blue_factor = blue_brightness / 50.0
72
+
73
+ red_channel = red_factor * adjusted_gray * blend
74
+ blue_channel = blue_factor * adjusted_gray * (1.0 - blend)
75
+
76
+ red_img = np.clip(red_channel * 255.0, 0, 255).astype(np.uint8)
77
+ blue_img = np.clip(blue_channel * 255.0, 0, 255).astype(np.uint8)
78
+
79
+ h, w = red_img.shape
80
  output = np.zeros((h, w, 3), dtype=np.uint8)
81
  output[..., 0] = red_img
82
  output[..., 1] = 0
83
  output[..., 2] = blue_img
84
+
85
  return Image.fromarray(output, mode="RGB")
86
 
87
 
88
+ def generate_depth_map(input_image):
89
+ """Generate normalized depth map and initial effect image."""
90
+ global current_original_image, current_depth_norm, current_depth_map_pil
91
  if input_image is None:
92
  current_original_image = None
93
  current_depth_norm = None
 
94
  current_depth_map_pil = None
95
  return None, None
96
  current_original_image = input_image
97
+ # Run depth estimation
98
  result = depth_pipe(input_image)
99
+ depth = np.array(result["depth"], dtype=np.float32)
100
+ depth -= depth.min()
101
+ max_val = depth.max()
 
102
  if max_val > 0:
103
+ depth /= max_val
104
+ current_depth_norm = depth
105
+ current_depth_map_pil = Image.fromarray((depth * 255.0).astype(np.uint8), mode="L")
106
+ # Default effect parameters
107
+ effect = apply_effect(
108
+ threshold=50,
109
+ depth_scale=50,
110
+ feather=10,
111
+ red_brightness=50,
112
+ blue_brightness=50,
113
+ gamma=50,
114
+ black_level_percent=0,
115
+ white_level_percent=100,
116
+ smoothing_percent=0,
117
+ )
118
+ return current_depth_map_pil.convert("RGB"), effect
119
+
120
+
121
+ def update_effect(threshold, depth_scale, feather, red_brightness, blue_brightness,
122
+ gamma, black_level, white_level, smoothing):
123
+ """Update the effect when any slider changes."""
124
+ return apply_effect(
125
+ threshold=threshold,
126
+ depth_scale=depth_scale,
127
+ feather=feather,
128
+ red_brightness=red_brightness,
129
+ blue_brightness=blue_brightness,
130
+ gamma=gamma,
131
+ black_level_percent=black_level,
132
+ white_level_percent=white_level,
133
+ smoothing_percent=smoothing,
134
+ )
135
 
136
 
137
  def clear_results():
138
+ """Reset global state and clear outputs."""
139
+ global current_original_image, current_depth_norm, current_depth_map_pil
140
  current_original_image = None
141
  current_depth_norm = None
 
142
  current_depth_map_pil = None
143
  return None, None
144
 
145
 
146
+ with gr.Blocks(title="ChromoStereoizer Enhanced", theme=gr.themes.Soft()) as demo:
147
+ gr.Markdown("# ChromoStereoizer Enhanced")
148
  with gr.Row():
149
  with gr.Column(scale=1):
150
  input_image = gr.Image(label="Upload Image", type="pil", height=400)
 
154
  depth_output = gr.Image(type="pil", height=400, interactive=False, show_download_button=True, show_label=False)
155
  gr.Markdown("**ChromoStereoizer Result**")
156
  chromo_output = gr.Image(type="pil", height=400, interactive=False, show_download_button=True, show_label=False)
157
+ gr.Markdown("## Controls")
158
  threshold_slider = gr.Slider(minimum=0, maximum=100, value=50, step=1, label="Threshold (%)")
159
+ depth_scale_slider = gr.Slider(minimum=0, maximum=100, value=50, step=1, label="Depth Scale (Steepness)")
160
+ feather_slider = gr.Slider(minimum=0, maximum=100, value=10, step=1, label="Feather (%)")
161
+ red_slider = gr.Slider(minimum=0, maximum=100, value=50, step=1, label="Red Brightness")
162
+ blue_slider = gr.Slider(minimum=0, maximum=100, value=50, step=1, label="Blue Brightness")
163
+ gamma_slider = gr.Slider(minimum=0, maximum=100, value=50, step=1, label="Gamma")
164
+ black_slider = gr.Slider(minimum=0, maximum=100, value=0, step=1, label="Black Level (%)")
165
+ white_slider = gr.Slider(minimum=0, maximum=100, value=100, step=1, label="White Level (%)")
166
+ smoothing_slider = gr.Slider(minimum=0, maximum=100, value=0, step=1, label="Smoothing (%)")
167
  clear_btn = gr.Button("Clear", variant="secondary")
168
+ # Event bindings
169
+ generate_btn.click(
170
+ fn=generate_depth_map,
171
+ inputs=[input_image],
172
+ outputs=[depth_output, chromo_output],
173
+ show_progress=True,
174
+ )
175
+ for slider in [threshold_slider, depth_scale_slider, feather_slider, red_slider, blue_slider, gamma_slider, black_slider, white_slider, smoothing_slider]:
176
+ slider.change(
177
+ fn=update_effect,
178
+ inputs=[threshold_slider, depth_scale_slider, feather_slider, red_slider, blue_slider, gamma_slider, black_slider, white_slider, smoothing_slider],
179
+ outputs=chromo_output,
180
+ show_progress=False,
181
+ )
182
+ clear_btn.click(
183
+ fn=clear_results,
184
+ inputs=[],
185
+ outputs=[depth_output, chromo_output],
186
+ )
187
 
188
  if __name__ == "__main__":
189
  demo.launch(server_name="0.0.0.0", server_port=7860, share=False)