File size: 15,091 Bytes
b3a4057
 
 
 
 
 
 
287b4fe
b3a4057
 
 
08979a0
 
 
b3a4057
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287b4fe
b3a4057
08979a0
 
 
 
 
 
 
 
 
b3a4057
 
 
08979a0
 
 
 
 
 
 
 
 
 
b3a4057
 
08979a0
 
 
 
 
 
 
 
 
 
 
 
 
b3a4057
 
 
08979a0
b3a4057
 
 
 
08979a0
b3a4057
42e16aa
 
 
b3a4057
 
 
 
 
08979a0
b3a4057
42e16aa
b3a4057
 
08979a0
 
b3a4057
 
08979a0
 
 
b3a4057
 
 
 
08979a0
b3a4057
08979a0
42e16aa
b3a4057
 
 
08979a0
 
 
 
b3a4057
 
 
 
 
42e16aa
08979a0
b3a4057
08979a0
 
b3a4057
08979a0
42e16aa
b3a4057
 
 
 
 
 
 
 
 
 
 
08979a0
b3a4057
08979a0
b3a4057
 
 
 
 
9a13f24
08979a0
9a13f24
 
b3a4057
 
287b4fe
9a13f24
 
08979a0
b3a4057
 
 
 
 
9a13f24
b3a4057
08979a0
 
b3a4057
 
 
08979a0
 
 
 
42e16aa
 
08979a0
b3a4057
42e16aa
 
 
b3a4057
 
42e16aa
08979a0
b3a4057
42e16aa
08979a0
b3a4057
 
08979a0
b3a4057
 
42e16aa
 
b3a4057
42e16aa
 
b3a4057
 
42e16aa
 
b3a4057
 
 
 
9a13f24
42e16aa
b3a4057
08979a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b3a4057
08979a0
 
 
 
 
 
 
 
 
9a13f24
b3a4057
287b4fe
08979a0
 
287b4fe
 
 
08979a0
 
 
b3a4057
 
9a13f24
b3a4057
287b4fe
b3a4057
 
 
 
08979a0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
import gradio as gr
from PIL import Image, ImageEnhance, ImageFilter, ImageOps
import numpy as np
import random
import cv2
import os

# --- Core Transformation Logic ---

def apply_transformations(
    image,
    # New parameter for resizing
    resize_percentage,
    # ---
    apply_mosaic_trigger,
    crop_box,
    scale_factor,
    rotation_angle,
    h_flip,
    v_flip,
    shear_x,
    shear_y,
    brightness,
    contrast,
    saturation,
    hue,
    gamma,
    grayscale,
    invert,
    blur_radius,
    sharpen_factor,
    noise_intensity,
    cutout_n_holes,
    cutout_ratio
):
    """
    Applies a series of transformations to an input image.
    """
    # BUG FIX: More robust input handling for gr.ImageEditor or standard np.array
    if image is None:
        return None, False  # Return nothing if no image is present

    # Get the background image from the ImageEditor's dictionary output, or use the array directly
    image_data = image.get("background") if isinstance(image, dict) else image
    
    if image_data is None:
        return None, False # Return nothing if image_data is still None

    img = Image.fromarray(image_data).convert("RGB")

    # --- FEATURE: Resize functionality added ---
    # Applied first to improve performance on subsequent operations.
    if resize_percentage != 100:
        new_width = int(img.width * resize_percentage / 100.0)
        new_height = int(img.height * resize_percentage / 100.0)
        # Ensure dimensions are at least 1x1
        if new_width > 0 and new_height > 0:
            img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)

    # Mosaic is a special one-shot trigger
    if apply_mosaic_trigger:
        w, h = img.size
        # Ensure dimensions are even for clean quadrants
        w, h = w - (w % 2), h - (h % 2)
        if w > 0 and h > 0:
            img = img.crop((0, 0, w, h))
            cx, cy = w // 2, h // 2
            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))]
            random.shuffle(crops)
            mosaic_img = Image.new('RGB', (w, h))
            mosaic_img.paste(crops[0], (0, 0))
            mosaic_img.paste(crops[1], (cx, 0))
            mosaic_img.paste(crops[2], (0, cy))
            mosaic_img.paste(crops[3], (cx, cy))
            img = mosaic_img

    if crop_box is not None:
        try:
            # Ensure crop coordinates are within image bounds
            x1, y1, x2, y2 = map(int, crop_box)
            if x1 < x2 and y1 < y2:
                img = img.crop((x1, y1, x2, y2))
        except (ValueError, TypeError):
            pass # Ignore if crop box is invalid

    if scale_factor != 1.0: img = img.resize((int(img.width * scale_factor), int(img.height * scale_factor)), Image.Resampling.LANCZOS)
    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)
    if rotation_angle != 0: img = img.rotate(rotation_angle, expand=True, fillcolor=(128, 128, 128))
    if h_flip: img = ImageOps.mirror(img)
    if v_flip: img = ImageOps.flip(img)
    if brightness != 1.0: img = ImageEnhance.Brightness(img).enhance(brightness)
    if contrast != 1.0: img = ImageEnhance.Contrast(img).enhance(contrast)
    if saturation != 1.0: img = ImageEnhance.Color(img).enhance(saturation)

    if hue != 0:
        hsv_img = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2HSV)
        hsv_img[:, :, 0] = (hsv_img[:, :, 0].astype(int) + hue) % 180
        img = Image.fromarray(cv2.cvtColor(hsv_img, cv2.COLOR_HSV2RGB))

    if gamma != 1.0 and gamma > 0:
        inv_gamma = 1.0 / gamma
        table = np.array([((i / 255.0) ** inv_gamma) * 255 for i in np.arange(0, 256)]).astype("uint8")
        # For RGB images, PIL's point operation applies the LUT to each channel
        img = Image.eval(img, lambda p: table[p])


    if sharpen_factor > 0:
        for _ in range(int(sharpen_factor)): img = img.filter(ImageFilter.SHARPEN)
    if blur_radius > 0: img = img.filter(ImageFilter.GaussianBlur(radius=blur_radius))

    if cutout_n_holes > 0 and cutout_ratio > 0:
        np_img = np.array(img).copy() # Use copy to avoid overwriting original array if needed elsewhere
        h, w, _ = np_img.shape
        hole_w, hole_h = int(w * cutout_ratio), int(h * cutout_ratio)
        if hole_w > 0 and hole_h > 0:
            for _ in range(cutout_n_holes):
                if h > hole_h and w > hole_w:
                    y1, x1 = random.randint(0, h - hole_h), random.randint(0, w - hole_w)
                    # BUG FIX: Fill with a consistent gray color instead of a calculated mean
                    np_img[y1:y1+hole_h, x1:x1+hole_w] = 128
            img = Image.fromarray(np_img)

    if noise_intensity > 0:
        np_img = np.array(img)
        noise = np.random.normal(0, noise_intensity, np_img.shape)
        img = Image.fromarray(np.clip(np_img + noise, 0, 255).astype(np.uint8))

    if grayscale: img = ImageOps.grayscale(img)
    # BUG FIX: Simplified invert logic since image is already converted to RGB at the start
    if invert: img = ImageOps.invert(img)

    # The second return value resets the mosaic_trigger state to False after one use
    return img, False

# --- UI Helper Functions ---
def process_selection(evt: gr.SelectData):
    return (evt.index[0], evt.index[1], evt.index[2], evt.index[3])

def update_slider(min_val, max_val, current_val):
    if min_val > max_val: min_val = max_val
    new_val = max(min_val, min(max_val, current_val))
    return gr.update(minimum=min_val, maximum=max_val, value=new_val)

def reset_all_controls():
    # Added reset value for the new resize slider (100)
    return (
        100, 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,
        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,
        -90, 90, 0.2, 2.2, 0.0, 15.0, 0, 50, 0, 50, 0.0, 0.5,
        None, None
    )

def on_upload():
    # Slicing is adjusted to correctly reset components without touching the image inputs
    return reset_all_controls()[:-2] + (None,)

# --- Gradio UI Layout ---

with gr.Blocks(theme=gr.themes.Soft()) as demo:
    gr.Markdown("# Advanced Image Augmentation Tool (Manual Control)")
    gr.Markdown("Set your parameters on the left, then click **Apply Transformations** to see the result.")

    crop_box_state = gr.State(None)
    mosaic_trigger = gr.State(False)

    with gr.Row(variant="panel"):
        with gr.Column(scale=1, min_width=400):
            gr.Markdown("### Control Panel")
            with gr.Accordion("Geometric Transformations", open=True):
                # NEW: Resize Slider
                resize_percent_slider = gr.Slider(1, 200, 100, step=1, label="Resize (%)")
                scale_slider = gr.Slider(0.1, 3.0, 1.0, step=0.05, label="Scale")
                rotation_slider = gr.Slider(-180, 180, 0, step=1, label="Rotation Angle")
                with gr.Row():
                    # BUG FIX: Buttons now set the angle directly for predictable behavior
                    rotate_90_btn = gr.Button("Rotate 90°")
                    rotate_180_btn = gr.Button("Rotate 180°")
                    rotate_270_btn = gr.Button("Rotate 270°")
                shear_x_slider, shear_y_slider = gr.Slider(-0.5, 0.5, 0.0, step=0.01, label="Shear X"), gr.Slider(-0.5, 0.5, 0.0, step=0.01, label="Shear Y")
                h_flip_check, v_flip_check = gr.Checkbox(label="Horizontal Flip"), gr.Checkbox(label="Vertical Flip")

            with gr.Accordion("Color & Tone Adjustments", open=True):
                brightness_slider, contrast_slider, saturation_slider = gr.Slider(0.0, 3.0, 1.0, step=0.05, label="Brightness"), gr.Slider(0.0, 3.0, 1.0, step=0.05, label="Contrast"), gr.Slider(0.0, 3.0, 1.0, step=0.05, label="Saturation")
                hue_slider, gamma_slider = gr.Slider(-90, 90, 0, step=1, label="Hue"), gr.Slider(0.2, 2.2, 1.0, step=0.05, label="Exposure (Gamma)")
                grayscale_check, invert_check = gr.Checkbox(label="Grayscale"), gr.Checkbox(label="Invert Colors")

            with gr.Accordion("Filters & Distortions", open=True):
                blur_slider, sharpen_slider, noise_slider = gr.Slider(0.0, 15.0, 0.0, step=0.1, label="Blur Radius"), gr.Slider(0, 5, 0, step=1, label="Sharpen Intensity"), gr.Slider(0, 50, 0, step=1, label="Add Noise")

            with gr.Accordion("Enhanced Augmentations", open=False):
                cutout_n_slider, cutout_ratio_slider = gr.Slider(0, 50, 0, step=1, label="Number of Holes"), gr.Slider(0.0, 0.5, 0.0, step=0.01, label="Hole Size Ratio")
                mosaic_btn = gr.Button("Apply Mosaic (then click Apply Transformations)")

            with gr.Accordion("Parameter Range Control (Advanced)", open=False):
                # This section remains the same, no new controls needed here
                with gr.Tabs():
                    with gr.TabItem("Geometric"):
                        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")
                        shear_x_min, shear_x_max, shear_y_min, shear_y_max = gr.Number(-0.5, label="Shear X Min"), gr.Number(0.5, label="Shear X Max"), gr.Number(-0.5, label="Shear Y Min"), gr.Number(0.5, label="Shear Y Max")
                    with gr.TabItem("Color"):
                        brightness_min, brightness_max, contrast_min, contrast_max = gr.Number(0.0, label="Brightness Min"), gr.Number(3.0, label="Brightness Max"), gr.Number(0.0, label="Contrast Min"), gr.Number(3.0, label="Contrast Max")
                        saturation_min, saturation_max, hue_min, hue_max = gr.Number(0.0, label="Saturation Min"), gr.Number(3.0, label="Saturation Max"), gr.Number(-90, label="Hue Min"), gr.Number(90, label="Hue Max")
                        gamma_min, gamma_max = gr.Number(0.2, label="Exposure Min"), gr.Number(2.2, label="Exposure Max")
                    with gr.TabItem("Filters/Other"):
                        blur_min, blur_max, noise_min, noise_max = gr.Number(0.0, label="Blur Min"), gr.Number(15.0, label="Blur Max"), gr.Number(0, label="Noise Min"), gr.Number(50, label="Noise Max")
                        cutout_n_min, cutout_n_max, cutout_ratio_min, cutout_ratio_max = gr.Number(0, label="Holes Min"), gr.Number(50, label="Holes Max"), gr.Number(0.0, label="Ratio Min"), gr.Number(0.5, label="Ratio Max")

            reset_btn = gr.Button("Reset All Settings", variant="stop", size="lg")

        with gr.Column(scale=3):
            apply_btn = gr.Button("Apply Transformations", variant="primary")
            image_input = gr.ImageEditor(type="numpy", label="Original Image (Select an area to crop)", interactive=True)
            image_output = gr.Image(label="Transformed Image", interactive=False)
            
            # Use a helper function to find example images if they exist
            def find_examples():
                example_files = ["cat.jpg", "cheetah.jpg", "lion.jpg"]
                existing_examples = []
                # __file__ might not work in all environments (like notebooks), so be defensive
                try:
                    script_dir = os.path.dirname(__file__)
                    for f in example_files:
                        path = os.path.join(script_dir, f)
                        if os.path.exists(path):
                            existing_examples.append(path)
                except NameError:
                    # __file__ is not defined, so skip examples
                    pass
                return existing_examples
                
            example_paths = find_examples()
            if example_paths:
                gr.Examples(examples=example_paths, inputs=image_input, label="Example Images")

    # --- Event Listeners ---
    
    # Updated list of inputs for the main function
    all_inputs_for_transform = [image_input, resize_percent_slider, mosaic_trigger, crop_box_state, scale_slider, rotation_slider, h_flip_check, v_flip_check, shear_x_slider, shear_y_slider, brightness_slider, contrast_slider, saturation_slider, hue_slider, gamma_slider, grayscale_check, invert_check, blur_slider, sharpen_slider, noise_slider, cutout_n_slider, cutout_ratio_slider]
    
    # Updated list of all components that can be reset
    all_resettable_components = [resize_percent_slider, mosaic_trigger, crop_box_state, scale_slider, rotation_slider, h_flip_check, v_flip_check, shear_x_slider, shear_y_slider, brightness_slider, contrast_slider, saturation_slider, hue_slider, gamma_slider, grayscale_check, invert_check, blur_slider, sharpen_slider, noise_slider, cutout_n_slider, cutout_ratio_slider, scale_min, scale_max, rotation_min, rotation_max, shear_x_min, shear_x_max, shear_y_min, shear_y_max, brightness_min, brightness_max, contrast_min, contrast_max, saturation_min, saturation_max, hue_min, hue_max, gamma_min, gamma_max, blur_min, blur_max, noise_min, noise_max, cutout_n_min, cutout_n_max, cutout_ratio_min, cutout_ratio_max, image_input, image_output]
    
    # A subset used for upload/clear events
    partial_resettable_components = all_resettable_components[:-2] + [image_output]

    apply_btn.click(fn=apply_transformations, inputs=all_inputs_for_transform, outputs=[image_output, mosaic_trigger], api_name="predict")

    image_input.select(fn=process_selection, inputs=None, outputs=[crop_box_state], show_progress="hidden")
    image_input.upload(fn=on_upload, inputs=None, outputs=partial_resettable_components)
    image_input.clear(fn=reset_all_controls, inputs=None, outputs=all_resettable_components)
    reset_btn.click(fn=reset_all_controls, inputs=None, outputs=all_resettable_components)

    # BUG FIX: Buttons now set a specific value, not increment
    rotate_90_btn.click(lambda: 90, inputs=None, outputs=[rotation_slider])
    rotate_180_btn.click(lambda: 180, inputs=None, outputs=[rotation_slider])
    rotate_270_btn.click(lambda: -90, inputs=None, outputs=[rotation_slider])
    mosaic_btn.click(lambda: True, None, mosaic_trigger)

    range_map = {(scale_min, scale_max): scale_slider, (rotation_min, rotation_max): rotation_slider, (shear_x_min, shear_x_max): shear_x_slider, (shear_y_min, shear_y_max): shear_y_slider, (brightness_min, brightness_max): brightness_slider, (contrast_min, contrast_max): contrast_slider, (saturation_min, saturation_max): saturation_slider, (hue_min, hue_max): hue_slider, (gamma_min, gamma_max): gamma_slider, (blur_min, blur_max): blur_slider, (noise_min, noise_max): noise_slider, (cutout_n_min, cutout_n_max): cutout_n_slider, (cutout_ratio_min, cutout_ratio_max): cutout_ratio_slider}
    for (min_comp, max_comp), slider in range_map.items():
        min_comp.change(fn=update_slider, inputs=[min_comp, max_comp, slider], outputs=[slider])
        max_comp.change(fn=update_slider, inputs=[min_comp, max_comp, slider], outputs=[slider])

demo.launch(debug=True)