wykonos commited on
Commit
9a13f24
·
verified ·
1 Parent(s): 42e16aa

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +58 -39
app.py CHANGED
@@ -1,14 +1,3 @@
1
- #
2
- # Advanced Image Augmentation Tool for Gradio
3
- #
4
- # This script creates a comprehensive web interface for applying various image transformations.
5
- # It uses the modern gr.ImageEditor component for interactive cropping.
6
- #
7
- # CRITICAL FIX: The event handling logic has been revised to prevent feedback loops.
8
- # Instead of using a broad .change() event on the ImageEditor, this version uses
9
- # specific .upload(), .clear(), and .select() events. Transformation triggers
10
- # now ONLY update the output image, not the input, which resolves the flickering/resetting issue.
11
- #
12
 
13
  import gradio as gr
14
  from PIL import Image, ImageEnhance, ImageFilter, ImageOps
@@ -17,7 +6,6 @@ import random
17
  import cv2
18
  import os
19
 
20
- # --- Core Transformation Logic ---
21
 
22
  def apply_transformations(
23
  image,
@@ -43,7 +31,8 @@ def apply_transformations(
43
  cutout_ratio
44
  ):
45
  """
46
- Applies a series of transformations to an input image based on user-defined parameters.
 
47
  """
48
  # Handle the dictionary input from gr.ImageEditor, which contains the background image
49
  if isinstance(image, dict) and image.get("background") is not None:
@@ -70,7 +59,7 @@ def apply_transformations(
70
  mosaic_img.paste(crops[3].resize((cx, cy), Image.Resampling.LANCZOS), (cx, cy))
71
  img = mosaic_img
72
 
73
- # Geometric Augmentation: Crop
74
  if crop_box is not None:
75
  try:
76
  x1, y1, x2, y2 = map(int, crop_box)
@@ -79,7 +68,7 @@ def apply_transformations(
79
  except (ValueError, TypeError):
80
  pass
81
 
82
- # Apply other transformations...
83
  if scale_factor != 1.0: img = img.resize((int(img.width * scale_factor), int(img.height * scale_factor)), Image.Resampling.LANCZOS)
84
  if shear_x != 0 or shear_y != 0: img = img.transform(img.size, Image.Transform.AFFINE, (1, shear_x, 0, shear_y, 1, 0), Image.Resampling.BICUBIC)
85
  if rotation_angle != 0: img = img.rotate(rotation_angle, expand=True, fillcolor=(128, 128, 128))
@@ -127,6 +116,7 @@ def apply_transformations(
127
  # --- UI Helper Functions ---
128
 
129
  def process_selection(evt: gr.SelectData):
 
130
  return (evt.index[0], evt.index[1], evt.index[2], evt.index[3])
131
 
132
  def update_slider(min_val, max_val, current_val):
@@ -135,6 +125,7 @@ def update_slider(min_val, max_val, current_val):
135
  return gr.update(minimum=min_val, maximum=max_val, value=new_val)
136
 
137
  def reset_all_controls():
 
138
  return (
139
  False, None, 1.0, 0, False, False, 0.0, 0.0, 1.0, 1.0, 1.0, 0, 1.0, False, False, 0.0, 0, 0, 0, 0.0,
140
  0.1, 3.0, -180, 180, -0.5, 0.5, -0.5, 0.5, 0.0, 3.0, 0.0, 3.0, 0.0, 3.0,
@@ -142,21 +133,25 @@ def reset_all_controls():
142
  None, None
143
  )
144
 
145
- def on_upload_or_clear():
146
- return reset_all_controls()
 
 
 
 
147
 
148
  # --- Gradio UI Layout ---
149
 
150
  with gr.Blocks(theme=gr.themes.Soft(), css="footer {display: none !important}") as demo:
151
- gr.Markdown("# Advanced Image Augmentation Tool")
152
- gr.Markdown("Upload an image or select an example. Apply transformations from the control panel and even define your own parameter ranges.")
153
 
154
  crop_box_state = gr.State(None)
155
  mosaic_trigger = gr.State(False)
156
 
157
  with gr.Row(variant="panel"):
158
  with gr.Column(scale=1, min_width=400):
159
- gr.Markdown("### Main Control Panel")
160
  # All control panels remain the same...
161
  with gr.Accordion("Geometric Transformations", open=True):
162
  scale_slider = gr.Slider(0.1, 3.0, 1.0, step=0.05, label="Scale")
@@ -179,6 +174,7 @@ with gr.Blocks(theme=gr.themes.Soft(), css="footer {display: none !important}")
179
  mosaic_btn = gr.Button("Apply Mosaic")
180
 
181
  with gr.Accordion("Parameter Range Control (Advanced)", open=False):
 
182
  with gr.Tabs():
183
  with gr.TabItem("Geometric"):
184
  scale_min, scale_max, rotation_min, rotation_max = gr.Number(0.1, label="Scale Min"), gr.Number(3.0, label="Scale Max"), gr.Number(-180, label="Rotation Min"), gr.Number(180, label="Rotation Max")
@@ -194,6 +190,8 @@ with gr.Blocks(theme=gr.themes.Soft(), css="footer {display: none !important}")
194
  reset_btn = gr.Button("Reset All Settings", variant="stop", size="lg")
195
 
196
  with gr.Column(scale=3):
 
 
197
  image_input = gr.ImageEditor(type="numpy", label="Original Image (Select an area to crop)", interactive=True)
198
  image_output = gr.Image(label="Transformed Image", interactive=False)
199
  gr.Examples(
@@ -201,14 +199,15 @@ with gr.Blocks(theme=gr.themes.Soft(), css="footer {display: none !important}")
201
  inputs=image_input, label="Example Images"
202
  )
203
 
204
- all_inputs = [
 
205
  image_input, mosaic_trigger, crop_box_state, scale_slider, rotation_slider, h_flip_check, v_flip_check,
206
  shear_x_slider, shear_y_slider, brightness_slider, contrast_slider, saturation_slider,
207
  hue_slider, gamma_slider, grayscale_check, invert_check, blur_slider, sharpen_slider, noise_slider,
208
  cutout_n_slider, cutout_ratio_slider
209
  ]
210
 
211
- all_resettable_outputs = [
212
  mosaic_trigger, crop_box_state, scale_slider, rotation_slider, h_flip_check, v_flip_check,
213
  shear_x_slider, shear_y_slider, brightness_slider, contrast_slider, saturation_slider,
214
  hue_slider, gamma_slider, grayscale_check, invert_check, blur_slider, sharpen_slider, noise_slider,
@@ -219,32 +218,52 @@ with gr.Blocks(theme=gr.themes.Soft(), css="footer {display: none !important}")
219
  cutout_n_min, cutout_n_max, cutout_ratio_min, cutout_ratio_max,
220
  image_input, image_output
221
  ]
 
 
222
 
223
- # STABLE EVENT HANDLING
224
- # 1. Any change in a control slider/checkbox triggers the transformation function.
225
- # CRITICAL: The output is ONLY the output image, not the input. This prevents the loop.
226
- for component in all_inputs:
227
- if component not in [image_input, crop_box_state, mosaic_trigger]:
228
- component.change(fn=apply_transformations, inputs=all_inputs, outputs=[image_output, mosaic_trigger])
 
 
229
 
230
- # 2. Selecting a crop area updates the crop state, then triggers transformation.
231
- image_input.select(fn=process_selection, inputs=None, outputs=[crop_box_state]).then(
232
- fn=apply_transformations, inputs=all_inputs, outputs=[image_output, mosaic_trigger])
 
 
 
233
 
234
- # 3. Use SPECIFIC events for upload/clear to reset the entire UI.
235
- image_input.upload(fn=on_upload_or_clear, inputs=None, outputs=all_resettable_outputs)
236
- image_input.clear(fn=on_upload_or_clear, inputs=None, outputs=all_resettable_outputs)
237
- reset_btn.click(fn=on_upload_or_clear, inputs=None, outputs=all_resettable_outputs)
 
 
 
 
 
 
 
 
 
 
 
 
 
238
 
239
- # Other event handlers
240
  rotate_90_btn.click(lambda x: (x + 90) % 360, inputs=[rotation_slider], outputs=[rotation_slider])
241
  rotate_180_btn.click(lambda: 180, inputs=None, outputs=[rotation_slider])
242
  rotate_270_btn.click(lambda: -90, inputs=None, outputs=[rotation_slider])
243
 
244
- mosaic_btn.click(lambda: True, None, mosaic_trigger).then(
245
- fn=apply_transformations, inputs=all_inputs, outputs=[image_output, mosaic_trigger])
246
 
247
- # Range control handlers remain the same
248
  range_map = {
249
  (scale_min, scale_max): scale_slider, (rotation_min, rotation_max): rotation_slider,
250
  (shear_x_min, shear_x_max): shear_x_slider, (shear_y_min, shear_y_max): shear_y_slider,
 
 
 
 
 
 
 
 
 
 
 
 
1
 
2
  import gradio as gr
3
  from PIL import Image, ImageEnhance, ImageFilter, ImageOps
 
6
  import cv2
7
  import os
8
 
 
9
 
10
  def apply_transformations(
11
  image,
 
31
  cutout_ratio
32
  ):
33
  """
34
+ Applies a series of transformations to an input image. This function is now
35
+ called ONLY when the 'Apply Transformations' button is clicked.
36
  """
37
  # Handle the dictionary input from gr.ImageEditor, which contains the background image
38
  if isinstance(image, dict) and image.get("background") is not None:
 
59
  mosaic_img.paste(crops[3].resize((cx, cy), Image.Resampling.LANCZOS), (cx, cy))
60
  img = mosaic_img
61
 
62
+ # Geometric Augmentation: Crop (uses the stored state)
63
  if crop_box is not None:
64
  try:
65
  x1, y1, x2, y2 = map(int, crop_box)
 
68
  except (ValueError, TypeError):
69
  pass
70
 
71
+ # Apply all other transformations...
72
  if scale_factor != 1.0: img = img.resize((int(img.width * scale_factor), int(img.height * scale_factor)), Image.Resampling.LANCZOS)
73
  if shear_x != 0 or shear_y != 0: img = img.transform(img.size, Image.Transform.AFFINE, (1, shear_x, 0, shear_y, 1, 0), Image.Resampling.BICUBIC)
74
  if rotation_angle != 0: img = img.rotate(rotation_angle, expand=True, fillcolor=(128, 128, 128))
 
116
  # --- UI Helper Functions ---
117
 
118
  def process_selection(evt: gr.SelectData):
119
+ """Callback function to CAPTURE the coordinates of a user's selection, to be used later."""
120
  return (evt.index[0], evt.index[1], evt.index[2], evt.index[3])
121
 
122
  def update_slider(min_val, max_val, current_val):
 
125
  return gr.update(minimum=min_val, maximum=max_val, value=new_val)
126
 
127
  def reset_all_controls():
128
+ """Returns a tuple of default values to reset all UI components to their initial state."""
129
  return (
130
  False, None, 1.0, 0, False, False, 0.0, 0.0, 1.0, 1.0, 1.0, 0, 1.0, False, False, 0.0, 0, 0, 0, 0.0,
131
  0.1, 3.0, -180, 180, -0.5, 0.5, -0.5, 0.5, 0.0, 3.0, 0.0, 3.0, 0.0, 3.0,
 
133
  None, None
134
  )
135
 
136
+ def on_upload():
137
+ """Returns a partial reset tuple, specifically clearing the output image and resetting controls, but leaves the input image alone."""
138
+ # Slices the full reset tuple to exclude the last two items (image_input, image_output)
139
+ # and then adds a None for image_output.
140
+ return reset_all_controls()[:-2] + (None,)
141
+
142
 
143
  # --- Gradio UI Layout ---
144
 
145
  with gr.Blocks(theme=gr.themes.Soft(), css="footer {display: none !important}") as demo:
146
+ gr.Markdown("# Advanced Image Augmentation Tool (Manual Control)")
147
+ gr.Markdown("Set your parameters on the left, then click **Apply Transformations** to see the result.")
148
 
149
  crop_box_state = gr.State(None)
150
  mosaic_trigger = gr.State(False)
151
 
152
  with gr.Row(variant="panel"):
153
  with gr.Column(scale=1, min_width=400):
154
+ gr.Markdown("### Control Panel")
155
  # All control panels remain the same...
156
  with gr.Accordion("Geometric Transformations", open=True):
157
  scale_slider = gr.Slider(0.1, 3.0, 1.0, step=0.05, label="Scale")
 
174
  mosaic_btn = gr.Button("Apply Mosaic")
175
 
176
  with gr.Accordion("Parameter Range Control (Advanced)", open=False):
177
+ # This panel remains unchanged
178
  with gr.Tabs():
179
  with gr.TabItem("Geometric"):
180
  scale_min, scale_max, rotation_min, rotation_max = gr.Number(0.1, label="Scale Min"), gr.Number(3.0, label="Scale Max"), gr.Number(-180, label="Rotation Min"), gr.Number(180, label="Rotation Max")
 
190
  reset_btn = gr.Button("Reset All Settings", variant="stop", size="lg")
191
 
192
  with gr.Column(scale=3):
193
+ apply_btn = gr.Button("Apply Transformations", variant="primary")
194
+
195
  image_input = gr.ImageEditor(type="numpy", label="Original Image (Select an area to crop)", interactive=True)
196
  image_output = gr.Image(label="Transformed Image", interactive=False)
197
  gr.Examples(
 
199
  inputs=image_input, label="Example Images"
200
  )
201
 
202
+ # --- LIST OF ALL INPUTS AND RESETTABLE COMPONENTS ---
203
+ all_inputs_for_transform = [
204
  image_input, mosaic_trigger, crop_box_state, scale_slider, rotation_slider, h_flip_check, v_flip_check,
205
  shear_x_slider, shear_y_slider, brightness_slider, contrast_slider, saturation_slider,
206
  hue_slider, gamma_slider, grayscale_check, invert_check, blur_slider, sharpen_slider, noise_slider,
207
  cutout_n_slider, cutout_ratio_slider
208
  ]
209
 
210
+ all_resettable_components = [
211
  mosaic_trigger, crop_box_state, scale_slider, rotation_slider, h_flip_check, v_flip_check,
212
  shear_x_slider, shear_y_slider, brightness_slider, contrast_slider, saturation_slider,
213
  hue_slider, gamma_slider, grayscale_check, invert_check, blur_slider, sharpen_slider, noise_slider,
 
218
  cutout_n_min, cutout_n_max, cutout_ratio_min, cutout_ratio_max,
219
  image_input, image_output
220
  ]
221
+
222
+ partial_resettable_components = all_resettable_components[:-2] + [image_output]
223
 
224
+ # --- STABLE EVENT HANDLING (MANUAL TRIGGER) ---
225
+
226
+ # 1. THE MAIN TRIGGER: Apply button click
227
+ apply_btn.click(
228
+ fn=apply_transformations,
229
+ inputs=all_inputs_for_transform,
230
+ outputs=[image_output, mosaic_trigger]
231
+ )
232
 
233
+ # 2. Selecting a crop area ONLY updates the crop state silently. No transformation is run.
234
+ image_input.select(
235
+ fn=process_selection,
236
+ inputs=None,
237
+ outputs=[crop_box_state]
238
+ )
239
 
240
+ # 3. Use SPECIFIC events for upload/clear to reset the UI in a controlled way.
241
+ # These do NOT trigger any transformations.
242
+ image_input.upload(
243
+ fn=on_upload,
244
+ inputs=None,
245
+ outputs=partial_resettable_components
246
+ )
247
+ image_input.clear(
248
+ fn=reset_all_controls,
249
+ inputs=None,
250
+ outputs=all_resettable_components
251
+ )
252
+ reset_btn.click(
253
+ fn=reset_all_controls,
254
+ inputs=None,
255
+ outputs=all_resettable_components
256
+ )
257
 
258
+ # 4. Other helper buttons also have no effect on images until "Apply" is clicked.
259
  rotate_90_btn.click(lambda x: (x + 90) % 360, inputs=[rotation_slider], outputs=[rotation_slider])
260
  rotate_180_btn.click(lambda: 180, inputs=None, outputs=[rotation_slider])
261
  rotate_270_btn.click(lambda: -90, inputs=None, outputs=[rotation_slider])
262
 
263
+ # Mosaic button only updates the state, to be used when "Apply" is clicked.
264
+ mosaic_btn.click(lambda: True, None, mosaic_trigger)
265
 
266
+ # 5. Range control handlers update the sliders, but do not trigger transformations.
267
  range_map = {
268
  (scale_min, scale_max): scale_slider, (rotation_min, rotation_max): rotation_slider,
269
  (shear_x_min, shear_x_max): shear_x_slider, (shear_y_min, shear_y_max): shear_y_slider,