wykonos commited on
Commit
b3a4057
·
verified ·
1 Parent(s): 5bfa9e2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +323 -387
app.py CHANGED
@@ -1,388 +1,324 @@
1
- import gradio as gr
2
- from PIL import Image, ImageEnhance, ImageFilter, ImageOps
3
- import numpy as np
4
- import random
5
- import cv2
6
- import os
7
-
8
- def apply_transformations(
9
- image,
10
- apply_mosaic_trigger,
11
- crop_box,
12
- scale_factor,
13
- rotation_angle,
14
- h_flip,
15
- v_flip,
16
- shear_x,
17
- shear_y,
18
- brightness,
19
- contrast,
20
- saturation,
21
- hue,
22
- gamma,
23
- grayscale,
24
- invert,
25
- blur_radius,
26
- sharpen_factor,
27
- noise_intensity,
28
- cutout_n_holes,
29
- cutout_ratio
30
- ):
31
- """
32
- Applies a series of transformations to an input image based on user-defined parameters.
33
-
34
- Args:
35
- image (np.array): The input image in NumPy array format from Gradio.
36
- apply_mosaic_trigger (bool): A flag to trigger the mosaic augmentation.
37
- crop_box (tuple): Coordinates (x1, y1, x2, y2) for cropping.
38
- scale_factor (float): Factor by which to scale the image.
39
- rotation_angle (float): Angle in degrees for rotation.
40
- h_flip (bool): Whether to flip horizontally.
41
- v_flip (bool): Whether to flip vertically.
42
- shear_x (float): Horizontal shear factor.
43
- shear_y (float): Vertical shear factor.
44
- brightness (float): Brightness enhancement factor (1.0 is original).
45
- contrast (float): Contrast enhancement factor (1.0 is original).
46
- saturation (float): Color saturation factor (1.0 is original).
47
- hue (int): Hue shift value (-90 to 90).
48
- gamma (float): Gamma correction value for exposure (1.0 is original).
49
- grayscale (bool): Whether to convert to grayscale.
50
- invert (bool): Whether to invert image colors.
51
- blur_radius (float): Radius for Gaussian blur.
52
- sharpen_factor (int): Number of times to apply the sharpen filter.
53
- noise_intensity (float): Standard deviation for Gaussian noise.
54
- cutout_n_holes (int): Number of rectangular holes to create.
55
- cutout_ratio (float): Size of the cutout holes relative to image size.
56
-
57
- Returns:
58
- tuple: A tuple containing the original image preview, the transformed image,
59
- and a reset boolean for the mosaic trigger.
60
- """
61
- if image is None:
62
- return None, None, False
63
-
64
- img = Image.fromarray(image).convert("RGB")
65
- original_for_preview = img.copy()
66
-
67
- # Enhanced Augmentation: Mosaic
68
- if apply_mosaic_trigger:
69
- w, h = img.size
70
- cx, cy = w // 2, h // 2
71
- crops = [img.crop((0, 0, cx, cy)), img.crop((cx, 0, w, cy)), img.crop((0, cy, w, h)), img.crop((cx, cy, w, h))]
72
- random.shuffle(crops)
73
- mosaic_img = Image.new('RGB', (w, h))
74
- mosaic_img.paste(crops[0].resize((cx, cy), Image.Resampling.LANCZOS), (0, 0))
75
- mosaic_img.paste(crops[1].resize((cx, cy), Image.Resampling.LANCZOS), (cx, 0))
76
- mosaic_img.paste(crops[2].resize((cx, cy), Image.Resampling.LANCZOS), (0, cy))
77
- mosaic_img.paste(crops[3].resize((cx, cy), Image.Resampling.LANCZOS), (cx, cy))
78
- img = mosaic_img
79
-
80
- # Geometric Augmentation: Crop
81
- if crop_box is not None:
82
- try:
83
- x1, y1, x2, y2 = map(int, crop_box)
84
- if x1 < x2 and y1 < y2:
85
- img = img.crop((x1, y1, x2, y2))
86
- except (ValueError, TypeError):
87
- pass
88
-
89
- # Geometric Augmentation: Scale
90
- if scale_factor != 1.0:
91
- w, h = img.size
92
- new_size = (int(w * scale_factor), int(h * scale_factor))
93
- img = img.resize(new_size, Image.Resampling.LANCZOS)
94
-
95
- # Geometric Augmentation: Shear
96
- if shear_x != 0 or shear_y != 0:
97
- img = img.transform(img.size, Image.Transform.AFFINE, (1, shear_x, 0, shear_y, 1, 0), Image.Resampling.BICUBIC)
98
-
99
- # Geometric Augmentation: Rotate
100
- if rotation_angle != 0:
101
- img = img.rotate(rotation_angle, expand=True, fillcolor=(128, 128, 128))
102
-
103
- # Geometric Augmentation: Flip
104
- if h_flip:
105
- img = ImageOps.mirror(img)
106
- if v_flip:
107
- img = ImageOps.flip(img)
108
-
109
- # Color Augmentation: Brightness, Contrast, Saturation
110
- if brightness != 1.0: img = ImageEnhance.Brightness(img).enhance(brightness)
111
- if contrast != 1.0: img = ImageEnhance.Contrast(img).enhance(contrast)
112
- if saturation != 1.0: img = ImageEnhance.Color(img).enhance(saturation)
113
-
114
- # Color Augmentation: Hue (using OpenCV for robust HSV conversion)
115
- if hue != 0:
116
- np_img_rgb = np.array(img)
117
- hsv_img = cv2.cvtColor(np_img_rgb, cv2.COLOR_RGB2HSV)
118
- hsv_img[:, :, 0] = (hsv_img[:, :, 0].astype(int) + hue) % 180
119
- img = Image.fromarray(cv2.cvtColor(hsv_img, cv2.COLOR_HSV2RGB))
120
-
121
- # Color Augmentation: Exposure (Gamma Correction)
122
- if gamma != 1.0:
123
- inv_gamma = 1.0 / gamma
124
- table = np.array([((i / 255.0) ** inv_gamma) * 255 for i in np.arange(0, 256)]).astype("uint8")
125
- if img.mode == 'L':
126
- img = img.point(table)
127
- else:
128
- r, g, b = img.split()
129
- r, g, b = r.point(table), g.point(table), b.point(table)
130
- img = Image.merge('RGB', (r, g, b))
131
-
132
- # Filter Augmentation: Sharpen
133
- if sharpen_factor > 0:
134
- for _ in range(int(sharpen_factor)):
135
- img = img.filter(ImageFilter.SHARPEN)
136
-
137
- # Filter Augmentation: Blur
138
- if blur_radius > 0:
139
- img = img.filter(ImageFilter.GaussianBlur(radius=blur_radius))
140
-
141
- # Enhanced Augmentation: Cutout
142
- if cutout_n_holes > 0 and cutout_ratio > 0:
143
- w, h = img.size
144
- np_img = np.array(img)
145
- mask_value = int(np.mean(np_img))
146
- hole_w, hole_h = int(w * cutout_ratio), int(h * cutout_ratio)
147
- if hole_w > 0 and hole_h > 0:
148
- for _ in range(cutout_n_holes):
149
- y1, x1 = random.randint(0, h - hole_h), random.randint(0, w - hole_w)
150
- y2, x2 = y1 + hole_h, x1 + hole_w
151
- np_img[y1:y2, x1:x2] = mask_value
152
- img = Image.fromarray(np_img)
153
-
154
- # Filter Augmentation: Noise
155
- if noise_intensity > 0:
156
- np_img = np.array(img)
157
- noise = np.random.normal(0, noise_intensity, np_img.shape)
158
- np_img = np.clip(np_img + noise, 0, 255).astype(np.uint8)
159
- img = Image.fromarray(np_img)
160
-
161
- # Color Augmentation: Grayscale and Invert
162
- if grayscale:
163
- img = ImageOps.grayscale(img)
164
- if invert:
165
- img = ImageOps.invert(img.convert('RGB') if img.mode != 'L' else img)
166
-
167
- # Return final images and reset the mosaic trigger to False
168
- return original_for_preview, img, False
169
-
170
-
171
- # --- UI Helper Functions ---
172
-
173
- def process_selection(evt: gr.SelectData):
174
- """Callback function to capture the coordinates of a user's selection on the input image for cropping."""
175
- return (evt.index[0], evt.index[1], evt.index[2], evt.index[3])
176
-
177
-
178
- def update_slider(min_val, max_val, current_val):
179
- """
180
- Updates a Gradio slider's properties (min, max, value) dynamically.
181
- It ensures the current value is clamped within the new min/max bounds.
182
- """
183
- if min_val > max_val:
184
- min_val = max_val
185
- new_val = max(min_val, min(max_val, current_val))
186
- return gr.update(minimum=min_val, maximum=max_val, value=new_val)
187
-
188
-
189
- def reset_all_controls():
190
- """Returns a tuple of default values to reset all UI components to their initial state."""
191
- return (
192
- # Main controls
193
- 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,
194
- # Range controls
195
- 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,
196
- -90, 90, 0.2, 2.2, 0.0, 15.0, 0, 50, 0, 50, 0.0, 0.5,
197
- # Image outputs
198
- None, None
199
- )
200
-
201
-
202
- def on_upload_or_clear(image):
203
- """Handles the event of uploading or clearing an image, resetting all controls."""
204
- outputs = reset_all_controls()
205
- if image is None: return outputs
206
- # Replace the last two None values with the new image and a cleared output
207
- return outputs[:-2] + (image, None)
208
-
209
-
210
- # --- Gradio UI Layout ---
211
-
212
- with gr.Blocks(theme=gr.themes.Soft(), css="footer {display: none !important}") as demo:
213
- gr.Markdown("# Advanced Image Augmentation Tool")
214
- gr.Markdown(
215
- "Upload an image or select an example. Apply transformations from the control panel and even define your own parameter ranges.")
216
-
217
- # State objects to hold data that persists between interactions without being a visible component
218
- crop_box_state = gr.State(None)
219
- mosaic_trigger = gr.State(False)
220
-
221
- with gr.Row(variant="panel"):
222
- # --- Left Column: All Control Panels ---
223
- with gr.Column(scale=1, min_width=400):
224
- gr.Markdown("### Main Control Panel")
225
-
226
- with gr.Accordion("Geometric Transformations", open=True):
227
- scale_slider = gr.Slider(minimum=0.1, maximum=3.0, value=1.0, step=0.05, label="Scale")
228
- rotation_slider = gr.Slider(minimum=-180, maximum=180, value=0, step=1, label="Rotation Angle")
229
- with gr.Row():
230
- rotate_90_btn = gr.Button("Rotate 90°")
231
- rotate_180_btn = gr.Button("Rotate 180°")
232
- rotate_270_btn = gr.Button("Rotate 270°")
233
- shear_x_slider = gr.Slider(minimum=-0.5, maximum=0.5, value=0.0, step=0.01, label="Shear X")
234
- shear_y_slider = gr.Slider(minimum=-0.5, maximum=0.5, value=0.0, step=0.01, label="Shear Y")
235
- with gr.Row():
236
- h_flip_check = gr.Checkbox(label="Horizontal Flip")
237
- v_flip_check = gr.Checkbox(label="Vertical Flip")
238
-
239
- with gr.Accordion("Color & Tone Adjustments", open=True):
240
- brightness_slider = gr.Slider(minimum=0.0, maximum=3.0, value=1.0, step=0.05, label="Brightness")
241
- contrast_slider = gr.Slider(minimum=0.0, maximum=3.0, value=1.0, step=0.05, label="Contrast")
242
- saturation_slider = gr.Slider(minimum=0.0, maximum=3.0, value=1.0, step=0.05, label="Saturation")
243
- hue_slider = gr.Slider(minimum=-90, maximum=90, value=0, step=1, label="Hue")
244
- gamma_slider = gr.Slider(minimum=0.2, maximum=2.2, value=1.0, step=0.05, label="Exposure (Gamma)")
245
- with gr.Row():
246
- grayscale_check = gr.Checkbox(label="Grayscale")
247
- invert_check = gr.Checkbox(label="Invert Colors")
248
-
249
- with gr.Accordion("Filters & Distortions", open=True):
250
- blur_slider = gr.Slider(minimum=0.0, maximum=15.0, value=0.0, step=0.1, label="Blur Radius")
251
- sharpen_slider = gr.Slider(minimum=0, maximum=5, value=0, step=1, label="Sharpen Intensity")
252
- noise_slider = gr.Slider(minimum=0, maximum=50, value=0, step=1, label="Add Noise")
253
-
254
- with gr.Accordion("Enhanced Augmentations", open=False):
255
- gr.Markdown("**Cutout**: Erases random rectangular patches from the image.")
256
- cutout_n_slider = gr.Slider(minimum=0, maximum=50, value=0, step=1, label="Number of Holes")
257
- cutout_ratio_slider = gr.Slider(minimum=0.0, maximum=0.5, value=0.0, step=0.01, label="Hole Size Ratio")
258
- gr.Markdown("**Mosaic**: Splits the image into 4 quadrants and shuffles them.")
259
- mosaic_btn = gr.Button("Apply Mosaic")
260
-
261
- # --- User-Defined Range Control Panel ---
262
- with gr.Accordion("Parameter Range Control (Advanced)", open=False):
263
- gr.Markdown("Define the min/max for the sliders in the main panel.")
264
- with gr.Tabs():
265
- with gr.TabItem("Geometric"):
266
- with gr.Row():
267
- scale_min = gr.Number(0.1, label="Scale Min")
268
- scale_max = gr.Number(3.0, label="Scale Max")
269
- with gr.Row():
270
- rotation_min = gr.Number(-180, label="Rotation Min")
271
- rotation_max = gr.Number(180, label="Rotation Max")
272
- with gr.Row():
273
- shear_x_min = gr.Number(-0.5, label="Shear X Min")
274
- shear_x_max = gr.Number(0.5, label="Shear X Max")
275
- with gr.Row():
276
- shear_y_min = gr.Number(-0.5, label="Shear Y Min")
277
- shear_y_max = gr.Number(0.5, label="Shear Y Max")
278
- with gr.TabItem("Color"):
279
- with gr.Row():
280
- brightness_min = gr.Number(0.0, label="Brightness Min")
281
- brightness_max = gr.Number(3.0, label="Brightness Max")
282
- with gr.Row():
283
- contrast_min = gr.Number(0.0, label="Contrast Min")
284
- contrast_max = gr.Number(3.0, label="Contrast Max")
285
- with gr.Row():
286
- saturation_min = gr.Number(0.0, label="Saturation Min")
287
- saturation_max = gr.Number(3.0, label="Saturation Max")
288
- with gr.Row():
289
- hue_min = gr.Number(-90, label="Hue Min")
290
- hue_max = gr.Number(90, label="Hue Max")
291
- with gr.Row():
292
- gamma_min = gr.Number(0.2, label="Exposure Min")
293
- gamma_max = gr.Number(2.2, label="Exposure Max")
294
- with gr.TabItem("Filters/Other"):
295
- with gr.Row():
296
- blur_min = gr.Number(0.0, label="Blur Min")
297
- blur_max = gr.Number(15.0, label="Blur Max")
298
- with gr.Row():
299
- noise_min = gr.Number(0, label="Noise Min")
300
- noise_max = gr.Number(50, label="Noise Max")
301
- with gr.Row():
302
- cutout_n_min = gr.Number(0, label="Holes Min")
303
- cutout_n_max = gr.Number(50, label="Holes Max")
304
- with gr.Row():
305
- cutout_ratio_min = gr.Number(0.0, label="Ratio Min")
306
- cutout_ratio_max = gr.Number(0.5, label="Ratio Max")
307
-
308
- reset_btn = gr.Button("Reset All Settings", variant="stop", size="lg")
309
-
310
- # --- Right Column: Image Display ---
311
- with gr.Column(scale=3):
312
- image_input = gr.Image(type="numpy", label="Original Image (Select an area to crop)", tool="select")
313
- image_output = gr.Image(label="Transformed Image", interactive=False)
314
- gr.Examples(
315
- examples=[os.path.join(os.path.dirname(__file__), f) for f in ["cat.jpg", "cheetah.jpg", "lion.jpg"] if
316
- os.path.exists(os.path.join(os.path.dirname(__file__), f))],
317
- inputs=image_input,
318
- label="Example Images"
319
- )
320
-
321
- # --- Event Handling Logic ---
322
-
323
- # Define all inputs for the main transformation function
324
- all_inputs = [
325
- image_input, mosaic_trigger, crop_box_state, scale_slider, rotation_slider, h_flip_check, v_flip_check,
326
- shear_x_slider, shear_y_slider, brightness_slider, contrast_slider, saturation_slider,
327
- hue_slider, gamma_slider, grayscale_check, invert_check, blur_slider, sharpen_slider, noise_slider,
328
- cutout_n_slider, cutout_ratio_slider
329
- ]
330
-
331
- # This list defines all components that need to be reset
332
- all_resettable_outputs = [
333
- mosaic_trigger, crop_box_state, scale_slider, rotation_slider, h_flip_check, v_flip_check,
334
- shear_x_slider, shear_y_slider, brightness_slider, contrast_slider, saturation_slider,
335
- hue_slider, gamma_slider, grayscale_check, invert_check, blur_slider, sharpen_slider, noise_slider,
336
- cutout_n_slider, cutout_ratio_slider,
337
- scale_min, scale_max, rotation_min, rotation_max, shear_x_min, shear_x_max, shear_y_min, shear_y_max,
338
- brightness_min, brightness_max, contrast_min, contrast_max, saturation_min, saturation_max,
339
- hue_min, hue_max, gamma_min, gamma_max, blur_min, blur_max, noise_min, noise_max,
340
- cutout_n_min, cutout_n_max, cutout_ratio_min, cutout_ratio_max,
341
- image_input, image_output
342
- ]
343
-
344
- # Link every main control component to the transformation function
345
- for component in all_inputs:
346
- if component not in [image_input, crop_box_state, mosaic_trigger]:
347
- component.change(fn=apply_transformations, inputs=all_inputs,
348
- outputs=[image_input, image_output, mosaic_trigger])
349
-
350
- # Handle specific events like image selection, upload, and button clicks
351
- image_input.select(fn=process_selection, inputs=None, outputs=[crop_box_state]).then(
352
- fn=apply_transformations, inputs=all_inputs, outputs=[image_input, image_output, mosaic_trigger])
353
-
354
- image_input.upload(fn=on_upload_or_clear, inputs=[image_input], outputs=all_resettable_outputs)
355
- image_input.clear(fn=on_upload_or_clear, inputs=[image_input], outputs=all_resettable_outputs)
356
- reset_btn.click(fn=on_upload_or_clear, inputs=[image_input], outputs=all_resettable_outputs)
357
-
358
- # Handle 90-degree rotation buttons
359
- rotate_90_btn.click(lambda x: (x + 90) % 360, inputs=[rotation_slider], outputs=[rotation_slider])
360
- rotate_180_btn.click(lambda: 180, inputs=None, outputs=[rotation_slider])
361
- rotate_270_btn.click(lambda: -90, inputs=None, outputs=[rotation_slider])
362
-
363
- # Handle mosaic button click
364
- mosaic_btn.click(lambda: True, None, mosaic_trigger).then(
365
- fn=apply_transformations, inputs=all_inputs, outputs=[image_input, image_output, mosaic_trigger])
366
-
367
- # Link the range control inputs to update the main sliders
368
- range_map = {
369
- (scale_min, scale_max): scale_slider,
370
- (rotation_min, rotation_max): rotation_slider,
371
- (shear_x_min, shear_x_max): shear_x_slider,
372
- (shear_y_min, shear_y_max): shear_y_slider,
373
- (brightness_min, brightness_max): brightness_slider,
374
- (contrast_min, contrast_max): contrast_slider,
375
- (saturation_min, saturation_max): saturation_slider,
376
- (hue_min, hue_max): hue_slider,
377
- (gamma_min, gamma_max): gamma_slider,
378
- (blur_min, blur_max): blur_slider,
379
- (noise_min, noise_max): noise_slider,
380
- (cutout_n_min, cutout_n_max): cutout_n_slider,
381
- (cutout_ratio_min, cutout_ratio_max): cutout_ratio_slider
382
- }
383
- for (min_comp, max_comp), slider in range_map.items():
384
- min_comp.change(fn=update_slider, inputs=[min_comp, max_comp, slider], outputs=[slider])
385
- max_comp.change(fn=update_slider, inputs=[min_comp, max_comp, slider], outputs=[slider])
386
-
387
- # Launch the Gradio interface
388
  demo.launch()
 
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, ensuring
6
+ # compatibility with recent versions of the Gradio library.
7
+ # Users can upload an image, interactively adjust parameters for augmentations,
8
+ # and dynamically define the operational range (min/max values) for each slider.
9
+ #
10
+
11
+ import gradio as gr
12
+ from PIL import Image, ImageEnhance, ImageFilter, ImageOps
13
+ import numpy as np
14
+ import random
15
+ import cv2
16
+ import os
17
+
18
+ # --- Core Transformation Logic ---
19
+
20
+ def apply_transformations(
21
+ image,
22
+ apply_mosaic_trigger,
23
+ crop_box,
24
+ scale_factor,
25
+ rotation_angle,
26
+ h_flip,
27
+ v_flip,
28
+ shear_x,
29
+ shear_y,
30
+ brightness,
31
+ contrast,
32
+ saturation,
33
+ hue,
34
+ gamma,
35
+ grayscale,
36
+ invert,
37
+ blur_radius,
38
+ sharpen_factor,
39
+ noise_intensity,
40
+ cutout_n_holes,
41
+ cutout_ratio
42
+ ):
43
+ """
44
+ Applies a series of transformations to an input image based on user-defined parameters.
45
+
46
+ Args:
47
+ image (dict or np.array): The input from gr.ImageEditor, which can be a dict
48
+ containing 'background' or a direct np.array.
49
+ apply_mosaic_trigger (bool): A flag to trigger the mosaic augmentation.
50
+ crop_box (tuple): Coordinates (x1, y1, x2, y2) for cropping.
51
+ ... (all other args are the same)
52
+
53
+ Returns:
54
+ tuple: A tuple containing the original image preview, the transformed image,
55
+ and a reset boolean for the mosaic trigger.
56
+ """
57
+ # Handle the input from gr.ImageEditor, which is a dictionary
58
+ if isinstance(image, dict) and image.get("background") is not None:
59
+ image_data = image["background"]
60
+ elif isinstance(image, np.ndarray):
61
+ image_data = image
62
+ else:
63
+ return None, None, False
64
+
65
+ # Convert NumPy array to PIL Image
66
+ img = Image.fromarray(image_data).convert("RGB")
67
+ original_for_preview = img.copy()
68
+
69
+ # Enhanced Augmentation: Mosaic
70
+ if apply_mosaic_trigger:
71
+ w, h = img.size
72
+ cx, cy = w // 2, h // 2
73
+ crops = [img.crop((0, 0, cx, cy)), img.crop((cx, 0, w, cy)), img.crop((0, cy, w, h)), img.crop((cx, cy, w, h))]
74
+ random.shuffle(crops)
75
+ mosaic_img = Image.new('RGB', (w, h))
76
+ mosaic_img.paste(crops[0].resize((cx, cy), Image.Resampling.LANCZOS), (0, 0))
77
+ mosaic_img.paste(crops[1].resize((cx, cy), Image.Resampling.LANCZOS), (cx, 0))
78
+ mosaic_img.paste(crops[2].resize((cx, cy), Image.Resampling.LANCZOS), (0, cy))
79
+ mosaic_img.paste(crops[3].resize((cx, cy), Image.Resampling.LANCZOS), (cx, cy))
80
+ img = mosaic_img
81
+
82
+ # Geometric Augmentation: Crop
83
+ if crop_box is not None:
84
+ try:
85
+ x1, y1, x2, y2 = map(int, crop_box)
86
+ if x1 < x2 and y1 < y2:
87
+ img = img.crop((x1, y1, x2, y2))
88
+ except (ValueError, TypeError):
89
+ pass
90
+
91
+ # Geometric Augmentation: Scale
92
+ if scale_factor != 1.0:
93
+ w, h = img.size
94
+ new_size = (int(w * scale_factor), int(h * scale_factor))
95
+ img = img.resize(new_size, Image.Resampling.LANCZOS)
96
+
97
+ # Geometric Augmentation: Shear
98
+ if shear_x != 0 or shear_y != 0:
99
+ img = img.transform(img.size, Image.Transform.AFFINE, (1, shear_x, 0, shear_y, 1, 0), Image.Resampling.BICUBIC)
100
+
101
+ # Geometric Augmentation: Rotate
102
+ if rotation_angle != 0:
103
+ img = img.rotate(rotation_angle, expand=True, fillcolor=(128, 128, 128))
104
+
105
+ # Geometric Augmentation: Flip
106
+ if h_flip: img = ImageOps.mirror(img)
107
+ if v_flip: img = ImageOps.flip(img)
108
+
109
+ # Color Augmentation: Brightness, Contrast, Saturation
110
+ if brightness != 1.0: img = ImageEnhance.Brightness(img).enhance(brightness)
111
+ if contrast != 1.0: img = ImageEnhance.Contrast(img).enhance(contrast)
112
+ if saturation != 1.0: img = ImageEnhance.Color(img).enhance(saturation)
113
+
114
+ # Color Augmentation: Hue (using OpenCV for robust HSV conversion)
115
+ if hue != 0:
116
+ np_img_rgb = np.array(img)
117
+ hsv_img = cv2.cvtColor(np_img_rgb, cv2.COLOR_RGB2HSV)
118
+ hsv_img[:, :, 0] = (hsv_img[:, :, 0].astype(int) + hue) % 180
119
+ img = Image.fromarray(cv2.cvtColor(hsv_img, cv2.COLOR_HSV2RGB))
120
+
121
+ # Color Augmentation: Exposure (Gamma Correction)
122
+ if gamma != 1.0:
123
+ inv_gamma = 1.0 / gamma
124
+ table = np.array([((i / 255.0) ** inv_gamma) * 255 for i in np.arange(0, 256)]).astype("uint8")
125
+ if img.mode == 'L':
126
+ img = img.point(table)
127
+ else:
128
+ r, g, b = img.split()
129
+ r, g, b = r.point(table), g.point(table), b.point(table)
130
+ img = Image.merge('RGB', (r, g, b))
131
+
132
+ # Filter Augmentation: Sharpen
133
+ if sharpen_factor > 0:
134
+ for _ in range(int(sharpen_factor)): img = img.filter(ImageFilter.SHARPEN)
135
+
136
+ # Filter Augmentation: Blur
137
+ if blur_radius > 0: img = img.filter(ImageFilter.GaussianBlur(radius=blur_radius))
138
+
139
+ # Enhanced Augmentation: Cutout
140
+ if cutout_n_holes > 0 and cutout_ratio > 0:
141
+ w, h = img.size
142
+ np_img = np.array(img)
143
+ mask_value = int(np.mean(np_img))
144
+ hole_w, hole_h = int(w * cutout_ratio), int(h * cutout_ratio)
145
+ if hole_w > 0 and hole_h > 0:
146
+ for _ in range(cutout_n_holes):
147
+ y1, x1 = random.randint(0, h - hole_h), random.randint(0, w - hole_w)
148
+ y2, x2 = y1 + hole_h, x1 + hole_w
149
+ np_img[y1:y2, x1:x2] = mask_value
150
+ img = Image.fromarray(np_img)
151
+
152
+ # Filter Augmentation: Noise
153
+ if noise_intensity > 0:
154
+ np_img = np.array(img)
155
+ noise = np.random.normal(0, noise_intensity, np_img.shape)
156
+ np_img = np.clip(np_img + noise, 0, 255).astype(np.uint8)
157
+ img = Image.fromarray(np_img)
158
+
159
+ # Color Augmentation: Grayscale and Invert
160
+ if grayscale: img = ImageOps.grayscale(img)
161
+ if invert: img = ImageOps.invert(img.convert('RGB') if img.mode != 'L' else img)
162
+
163
+ # Return final images and reset the mosaic trigger to False
164
+ return original_for_preview, img, False
165
+
166
+ # --- UI Helper Functions ---
167
+
168
+ def process_selection(evt: gr.SelectData):
169
+ """Callback function to capture the coordinates of a user's selection on the input image for cropping."""
170
+ return (evt.index[0], evt.index[1], evt.index[2], evt.index[3])
171
+
172
+ def update_slider(min_val, max_val, current_val):
173
+ """Updates a Gradio slider's properties (min, max, value) dynamically."""
174
+ if min_val > max_val: min_val = max_val
175
+ new_val = max(min_val, min(max_val, current_val))
176
+ return gr.update(minimum=min_val, maximum=max_val, value=new_val)
177
+
178
+ def reset_all_controls():
179
+ """Returns a tuple of default values to reset all UI components to their initial state."""
180
+ return (
181
+ 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,
182
+ 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,
183
+ -90, 90, 0.2, 2.2, 0.0, 15.0, 0, 50, 0, 50, 0.0, 0.5,
184
+ None, None
185
+ )
186
+
187
+ def on_upload_or_clear(image):
188
+ """Handles the event of uploading or clearing an image, resetting all controls."""
189
+ outputs = reset_all_controls()
190
+ if image is None: return outputs
191
+ return outputs[:-2] + (image, None)
192
+
193
+ # --- Gradio UI Layout ---
194
+
195
+ with gr.Blocks(theme=gr.themes.Soft(), css="footer {display: none !important}") as demo:
196
+ gr.Markdown("# Advanced Image Augmentation Tool")
197
+ gr.Markdown("Upload an image or select an example. Apply transformations from the control panel and even define your own parameter ranges.")
198
+
199
+ crop_box_state = gr.State(None)
200
+ mosaic_trigger = gr.State(False)
201
+
202
+ with gr.Row(variant="panel"):
203
+ with gr.Column(scale=1, min_width=400):
204
+ gr.Markdown("### Main Control Panel")
205
+
206
+ with gr.Accordion("Geometric Transformations", open=True):
207
+ scale_slider = gr.Slider(0.1, 3.0, 1.0, step=0.05, label="Scale")
208
+ rotation_slider = gr.Slider(-180, 180, 0, step=1, label="Rotation Angle")
209
+ with gr.Row():
210
+ rotate_90_btn = gr.Button("Rotate 90°")
211
+ rotate_180_btn = gr.Button("Rotate 180°")
212
+ rotate_270_btn = gr.Button("Rotate 270°")
213
+ shear_x_slider = gr.Slider(-0.5, 0.5, 0.0, step=0.01, label="Shear X")
214
+ shear_y_slider = gr.Slider(-0.5, 0.5, 0.0, step=0.01, label="Shear Y")
215
+ with gr.Row():
216
+ h_flip_check = gr.Checkbox(label="Horizontal Flip")
217
+ v_flip_check = gr.Checkbox(label="Vertical Flip")
218
+
219
+ with gr.Accordion("Color & Tone Adjustments", open=True):
220
+ brightness_slider = gr.Slider(0.0, 3.0, 1.0, step=0.05, label="Brightness")
221
+ contrast_slider = gr.Slider(0.0, 3.0, 1.0, step=0.05, label="Contrast")
222
+ saturation_slider = gr.Slider(0.0, 3.0, 1.0, step=0.05, label="Saturation")
223
+ hue_slider = gr.Slider(-90, 90, 0, step=1, label="Hue")
224
+ gamma_slider = gr.Slider(0.2, 2.2, 1.0, step=0.05, label="Exposure (Gamma)")
225
+ with gr.Row():
226
+ grayscale_check = gr.Checkbox(label="Grayscale")
227
+ invert_check = gr.Checkbox(label="Invert Colors")
228
+
229
+ with gr.Accordion("Filters & Distortions", open=True):
230
+ blur_slider = gr.Slider(0.0, 15.0, 0.0, step=0.1, label="Blur Radius")
231
+ sharpen_slider = gr.Slider(0, 5, 0, step=1, label="Sharpen Intensity")
232
+ noise_slider = gr.Slider(0, 50, 0, step=1, label="Add Noise")
233
+
234
+ with gr.Accordion("Enhanced Augmentations", open=False):
235
+ gr.Markdown("**Cutout**: Erases random rectangular patches from the image.")
236
+ cutout_n_slider = gr.Slider(0, 50, 0, step=1, label="Number of Holes")
237
+ cutout_ratio_slider = gr.Slider(0.0, 0.5, 0.0, step=0.01, label="Hole Size Ratio")
238
+ gr.Markdown("**Mosaic**: Splits the image into 4 quadrants and shuffles them.")
239
+ mosaic_btn = gr.Button("Apply Mosaic")
240
+
241
+ with gr.Accordion("Parameter Range Control (Advanced)", open=False):
242
+ gr.Markdown("Define the min/max for the sliders in the main panel.")
243
+ with gr.Tabs():
244
+ with gr.TabItem("Geometric"):
245
+ scale_min, scale_max = gr.Number(0.1, label="Scale Min"), gr.Number(3.0, label="Scale Max")
246
+ rotation_min, rotation_max = gr.Number(-180, label="Rotation Min"), gr.Number(180, label="Rotation Max")
247
+ shear_x_min, shear_x_max = gr.Number(-0.5, label="Shear X Min"), gr.Number(0.5, label="Shear X Max")
248
+ shear_y_min, shear_y_max = gr.Number(-0.5, label="Shear Y Min"), gr.Number(0.5, label="Shear Y Max")
249
+ with gr.TabItem("Color"):
250
+ brightness_min, brightness_max = gr.Number(0.0, label="Brightness Min"), gr.Number(3.0, label="Brightness Max")
251
+ contrast_min, contrast_max = gr.Number(0.0, label="Contrast Min"), gr.Number(3.0, label="Contrast Max")
252
+ saturation_min, saturation_max = gr.Number(0.0, label="Saturation Min"), gr.Number(3.0, label="Saturation Max")
253
+ hue_min, hue_max = gr.Number(-90, label="Hue Min"), gr.Number(90, label="Hue Max")
254
+ gamma_min, gamma_max = gr.Number(0.2, label="Exposure Min"), gr.Number(2.2, label="Exposure Max")
255
+ with gr.TabItem("Filters/Other"):
256
+ blur_min, blur_max = gr.Number(0.0, label="Blur Min"), gr.Number(15.0, label="Blur Max")
257
+ noise_min, noise_max = gr.Number(0, label="Noise Min"), gr.Number(50, label="Noise Max")
258
+ cutout_n_min, cutout_n_max = gr.Number(0, label="Holes Min"), gr.Number(50, label="Holes Max")
259
+ cutout_ratio_min, cutout_ratio_max = gr.Number(0.0, label="Ratio Min"), gr.Number(0.5, label="Ratio Max")
260
+
261
+ reset_btn = gr.Button("Reset All Settings", variant="stop", size="lg")
262
+
263
+ with gr.Column(scale=3):
264
+ # FIXED: Replaced gr.Image with gr.ImageEditor
265
+ image_input = gr.ImageEditor(type="numpy", label="Original Image (Select an area to crop)")
266
+ image_output = gr.Image(label="Transformed Image", interactive=False)
267
+ gr.Examples(
268
+ examples=[os.path.join(os.path.dirname(__file__), f) for f in ["cat.jpg", "cheetah.jpg", "lion.jpg"] if os.path.exists(os.path.join(os.path.dirname(__file__), f))],
269
+ inputs=image_input,
270
+ label="Example Images"
271
+ )
272
+
273
+ all_inputs = [
274
+ image_input, mosaic_trigger, crop_box_state, scale_slider, rotation_slider, h_flip_check, v_flip_check,
275
+ shear_x_slider, shear_y_slider, brightness_slider, contrast_slider, saturation_slider,
276
+ hue_slider, gamma_slider, grayscale_check, invert_check, blur_slider, sharpen_slider, noise_slider,
277
+ cutout_n_slider, cutout_ratio_slider
278
+ ]
279
+
280
+ all_resettable_outputs = [
281
+ mosaic_trigger, crop_box_state, scale_slider, rotation_slider, h_flip_check, v_flip_check,
282
+ shear_x_slider, shear_y_slider, brightness_slider, contrast_slider, saturation_slider,
283
+ hue_slider, gamma_slider, grayscale_check, invert_check, blur_slider, sharpen_slider, noise_slider,
284
+ cutout_n_slider, cutout_ratio_slider,
285
+ scale_min, scale_max, rotation_min, rotation_max, shear_x_min, shear_x_max, shear_y_min, shear_y_max,
286
+ brightness_min, brightness_max, contrast_min, contrast_max, saturation_min, saturation_max,
287
+ hue_min, hue_max, gamma_min, gamma_max, blur_min, blur_max, noise_min, noise_max,
288
+ cutout_n_min, cutout_n_max, cutout_ratio_min, cutout_ratio_max,
289
+ image_input, image_output
290
+ ]
291
+
292
+ for component in all_inputs:
293
+ if component not in [image_input, crop_box_state, mosaic_trigger]:
294
+ component.change(fn=apply_transformations, inputs=all_inputs, outputs=[image_input, image_output, mosaic_trigger])
295
+
296
+ # The 'select' event is built into ImageEditor
297
+ image_input.select(fn=process_selection, inputs=None, outputs=[crop_box_state]).then(
298
+ fn=apply_transformations, inputs=all_inputs, outputs=[image_input, image_output, mosaic_trigger])
299
+
300
+ # The 'change' event in ImageEditor fires on upload and clear
301
+ image_input.change(fn=on_upload_or_clear, inputs=[image_input], outputs=all_resettable_outputs)
302
+ reset_btn.click(fn=on_upload_or_clear, inputs=[image_input], outputs=all_resettable_outputs)
303
+
304
+ rotate_90_btn.click(lambda x: (x + 90) % 360, inputs=[rotation_slider], outputs=[rotation_slider])
305
+ rotate_180_btn.click(lambda: 180, inputs=None, outputs=[rotation_slider])
306
+ rotate_270_btn.click(lambda: -90, inputs=None, outputs=[rotation_slider])
307
+
308
+ mosaic_btn.click(lambda: True, None, mosaic_trigger).then(
309
+ fn=apply_transformations, inputs=all_inputs, outputs=[image_input, image_output, mosaic_trigger])
310
+
311
+ range_map = {
312
+ (scale_min, scale_max): scale_slider, (rotation_min, rotation_max): rotation_slider,
313
+ (shear_x_min, shear_x_max): shear_x_slider, (shear_y_min, shear_y_max): shear_y_slider,
314
+ (brightness_min, brightness_max): brightness_slider, (contrast_min, contrast_max): contrast_slider,
315
+ (saturation_min, saturation_max): saturation_slider, (hue_min, hue_max): hue_slider,
316
+ (gamma_min, gamma_max): gamma_slider, (blur_min, blur_max): blur_slider,
317
+ (noise_min, noise_max): noise_slider, (cutout_n_min, cutout_n_max): cutout_n_slider,
318
+ (cutout_ratio_min, cutout_ratio_max): cutout_ratio_slider
319
+ }
320
+ for (min_comp, max_comp), slider in range_map.items():
321
+ min_comp.change(fn=update_slider, inputs=[min_comp, max_comp, slider], outputs=[slider])
322
+ max_comp.change(fn=update_slider, inputs=[min_comp, max_comp, slider], outputs=[slider])
323
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  demo.launch()