File size: 11,715 Bytes
b8b15c0
1f87340
b8b15c0
1f87340
26d8d83
d62d3eb
1f87340
fb2df62
0669d7b
26d8d83
0669d7b
26d8d83
b8b15c0
a1c07cd
cd0403b
1f87340
0ab1a3e
b8b15c0
cd0403b
1f87340
 
a1c07cd
cd0403b
b8b15c0
cd0403b
1f87340
0ab1a3e
b8b15c0
cd0403b
1f87340
cd0403b
 
 
 
 
1f87340
cd0403b
0ab1a3e
cd0403b
1f87340
 
 
0ab1a3e
 
1f87340
0ab1a3e
 
1f87340
 
 
 
 
0ab1a3e
1f87340
 
 
 
 
 
0ab1a3e
 
 
 
 
 
a1c07cd
0ab1a3e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32ff1ee
a1c07cd
 
 
 
 
0ab1a3e
0669d7b
a1c07cd
0669d7b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a1c07cd
 
 
 
 
 
 
 
 
 
 
 
0ab1a3e
a1c07cd
 
 
 
 
 
 
eea28cb
a1c07cd
 
 
 
 
 
 
 
 
 
 
 
 
 
ce8d5bc
 
 
 
0ab1a3e
ce8d5bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ab1a3e
 
ce8d5bc
 
d62d3eb
 
1f87340
d62d3eb
 
0ab1a3e
a1c07cd
0ab1a3e
 
 
ce8d5bc
 
 
 
a1c07cd
0ab1a3e
a1c07cd
ce8d5bc
d62d3eb
ce8d5bc
1f87340
 
fb2df62
a1c07cd
 
1f87340
a1c07cd
cd0403b
b8b15c0
1f87340
a1c07cd
 
955e31b
a1c07cd
 
 
ce8d5bc
 
26d8d83
b8b15c0
 
 
a1c07cd
26d8d83
b8b15c0
 
1f87340
 
0ab1a3e
b8b15c0
 
 
0ab1a3e
a1c07cd
0ab1a3e
955e31b
eea28cb
32ff1ee
dc80f01
eea28cb
955e31b
0ab1a3e
a1c07cd
 
 
 
 
 
eea28cb
a1c07cd
 
 
 
 
 
eea28cb
a1c07cd
eea28cb
 
a1c07cd
 
eea28cb
a1c07cd
eea28cb
 
a1c07cd
 
 
1abb8b0
b8b15c0
 
dc13b68
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
import gradio as gr
from PIL import Image, ImageOps, ImageDraw
import numpy as np
import cv2 # Using OpenCV for fast resizing and filtering
import mediapipe as mp
import math

# --- Asset Setup ---
# Initialize MediaPipe Face Mesh for landmark detection, now supporting up to 2 faces.
mp_face_mesh = mp.solutions.face_mesh
face_mesh = mp_face_mesh.FaceMesh(static_image_mode=True, max_num_faces=2, min_detection_confidence=0.5)


# --- Standard Filter Functions ---
def apply_grayscale(img_np):
    if img_np is None: return None
    return cv2.cvtColor(cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY), cv2.COLOR_GRAY2RGB)

def apply_sepia(img_np):
    if img_np is None: return None
    sepia_matrix = np.array([[0.393, 0.769, 0.189], [0.349, 0.686, 0.168], [0.272, 0.534, 0.131]]).T
    sepia_img = np.dot(img_np[...,:3], sepia_matrix)
    return np.clip(sepia_img, 0, 255).astype(np.uint8)

def apply_invert(img_np):
    if img_np is None: return None
    return cv2.bitwise_not(img_np)

def apply_posterize(img_np):
    if img_np is None: return None
    bits = 4
    shift = 8 - bits
    return ((img_np >> shift) << shift).astype(np.uint8)

def apply_solarize(img_np):
    if img_np is None: return None
    threshold = 128
    return np.where(img_np > threshold, 255 - img_np, img_np).astype(np.uint8)

def apply_vignette(img_np):
    if img_np is None: return None
    rows, cols = img_np.shape[:2]
    kernel_x = cv2.getGaussianKernel(cols, int(cols * 0.5))
    kernel_y = cv2.getGaussianKernel(rows, int(rows * 0.5))
    kernel = kernel_y * kernel_x.T
    mask = 255 * kernel / np.max(kernel)
    return np.clip(img_np * (mask[:, :, np.newaxis] / 255.0), 0, 255).astype(np.uint8)

def apply_contour(img_np):
    if img_np is None: return None
    gray = cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY)
    edges = cv2.Canny(gray, 100, 200)
    return cv2.cvtColor(255 - edges, cv2.COLOR_GRAY2RGB)

def apply_sharpen(img_np):
    if img_np is None: return None
    kernel = np.array([[-1, -1, -1], [-1,  9, -1], [-1, -1, -1]])
    return cv2.filter2D(img_np, -1, kernel)

def apply_cartoon(img_np):
    if img_np is None: return None
    gray = cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY)
    gray = cv2.medianBlur(gray, 5)
    edges = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 9, 9)
    color = cv2.bilateralFilter(img_np, 9, 250, 250)
    return cv2.bitwise_and(color, color, mask=edges)

def apply_sketch(img_np):
    if img_np is None: return None
    gray_img = cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY)
    invert_img = 255 - gray_img
    blur_img = cv2.GaussianBlur(invert_img, (21, 21), 0)
    invert_blur_img = 255 - blur_img
    sketch_img = cv2.divide(gray_img, invert_blur_img, scale=256.0)
    return cv2.cvtColor(sketch_img, cv2.COLOR_GRAY2RGB)

def apply_pixelate(img_np):
    if img_np is None: return None
    h, w = img_np.shape[:2]
    pixel_size = 16
    temp = cv2.resize(img_np, (w // pixel_size, h // pixel_size), interpolation=cv2.INTER_NEAREST)
    return cv2.resize(temp, (w, h), interpolation=cv2.INTER_NEAREST)

# --- Advanced Filters ---

def apply_hdr_effect(img_np):
    """Simulates an HDR effect by enhancing details."""
    if img_np is None: return None
    return cv2.detailEnhance(img_np, sigma_s=12, sigma_r=0.15)

def apply_color_splash(img_np, color_str):
    """Keeps a selected color and converts the rest of the image to grayscale."""
    if img_np is None or color_str is None: return img_np

    try:
        if color_str.startswith('#'):
            h = color_str.lstrip('#')
            rgb_color = tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
        elif color_str.startswith('rgb'):
            parts = color_str.strip('rgb()').split(',')
            rgb_color = tuple(int(p.strip()) for p in parts)
        else:
            rgb_color = (0, 0, 0)
    except (ValueError, IndexError):
        print(f"Warning: Could not parse color '{color_str}'. Defaulting to black.")
        rgb_color = (0, 0, 0)

    hsv_img = cv2.cvtColor(img_np, cv2.COLOR_RGB2HSV)
    hsv_color = cv2.cvtColor(np.uint8([[rgb_color]]), cv2.COLOR_RGB2HSV)[0][0]
    hue_tolerance = 10
    lower_bound = np.array([max(0, hsv_color[0] - hue_tolerance), 50, 50])
    upper_bound = np.array([min(179, hsv_color[0] + hue_tolerance), 255, 255])
    mask = cv2.inRange(hsv_img, lower_bound, upper_bound)
    mask_inv = cv2.bitwise_not(mask)
    gray_img = cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY)
    gray_img_3_channel = cv2.cvtColor(gray_img, cv2.COLOR_GRAY2RGB)
    colored_part = cv2.bitwise_and(img_np, img_np, mask=mask)
    grayscale_part = cv2.bitwise_and(gray_img_3_channel, gray_img_3_channel, mask=mask_inv)
    return cv2.add(colored_part, grayscale_part)

def apply_sunburst_glow(img_np):
    """Adds a sunburst/lens flare effect from the brightest point."""
    if img_np is None: return None
    gray = cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY)
    minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(gray)
    overlay = img_np.copy()
    for i in range(12):
        angle = i * 30 * np.pi / 180
        length = np.random.randint(int(maxVal/2), int(maxVal*1.5))
        pt2_x = int(maxLoc[0] + length * np.cos(angle))
        pt2_y = int(maxLoc[1] + length * np.sin(angle))
        cv2.line(overlay, maxLoc, (pt2_x, pt2_y), (255, 255, 220), 1)
    glow = cv2.GaussianBlur(overlay, (0,0), sigmaX=30, sigmaY=30)
    return cv2.addWeighted(img_np, 0.8, glow, 0.4, 0)

def apply_dreamy_glow(img_np):
    """Adds a soft, dreamy glow effect to the image."""
    if img_np is None: return None
    blurred = cv2.GaussianBlur(img_np, (0,0), sigmaX=15, sigmaY=15)
    return cv2.addWeighted(img_np, 1.0, blurred, 0.6, 0)


# --- Sunglasses Functions ---
def _create_sunglasses_mask(style="aviator"):
    """Creates a PIL image mask for a given sunglass style."""
    sunglasses = Image.new('RGBA', (300, 150), (0, 0, 0, 0))
    draw = ImageDraw.Draw(sunglasses)

    if style == "aviator":
        # Left Lens
        draw.polygon([(20, 50), (130, 40), (120, 110), (30, 110)], fill=(20, 20, 20, 200), outline='gold', width=3)
        # Right Lens
        draw.polygon([(170, 40), (280, 50), (270, 110), (180, 110)], fill=(20, 20, 20, 200), outline='gold', width=3)
        # Bridge
        draw.line((130, 45, 170, 45), fill='gold', width=5)
        draw.line((130, 55, 170, 55), fill='gold', width=5)
    
    elif style == "retro_square":
        # Left Lens
        draw.rectangle((20, 40, 130, 110), fill=(10, 10, 10, 210), outline='white', width=6)
        # Right Lens
        draw.rectangle((170, 40, 280, 110), fill=(10, 10, 10, 210), outline='white', width=6)
        # Bridge
        draw.rectangle((130, 60, 170, 75), fill='white')

    return sunglasses

def apply_sunglasses(img_np, style="aviator"):
    """Applies a specific style of sunglasses to all detected faces (up to 2)."""
    if img_np is None: return img_np
    results = face_mesh.process(img_np)
    pil_image = Image.fromarray(img_np)
    if results.multi_face_landmarks:
        for face_landmarks in results.multi_face_landmarks:
            landmarks = np.array([(lm.x * img_np.shape[1], lm.y * img_np.shape[0]) for lm in face_landmarks.landmark])
            left_eye, right_eye = landmarks[33], landmarks[263]
            eye_center = (left_eye + right_eye) / 2
            eye_width = np.linalg.norm(left_eye - right_eye)
            angle = math.degrees(math.atan2(right_eye[1] - left_eye[1], right_eye[0] - left_eye[0]))
            
            sunglasses_img = _create_sunglasses_mask(style)
            
            w, h = int(eye_width * 1.8), int(eye_width * 1.8 * sunglasses_img.height / sunglasses_img.width)
            resized_sunglasses = sunglasses_img.resize((w, h), Image.Resampling.LANCZOS)
            rotated_sunglasses = resized_sunglasses.rotate(angle, expand=True, resample=Image.Resampling.BICUBIC)
            pos_x, pos_y = int(eye_center[0] - rotated_sunglasses.width / 2), int(eye_center[1] - rotated_sunglasses.height / 2)
            
            pil_image.paste(rotated_sunglasses, (pos_x, pos_y), rotated_sunglasses)
            
    return np.array(pil_image)


# --- Main Processing Function ---
def process_image(image, filter_name, splash_color):
    if image is None: return None
    img_np = np.array(image.convert("RGB")) if isinstance(image, Image.Image) else image
    
    filter_map = {
        "Grayscale": apply_grayscale, "Sepia": apply_sepia, "Invert": apply_invert,
        "Posterize": apply_posterize, "Solarize": apply_solarize, "Vignette": apply_vignette,
        "Contour": apply_contour, "Sharpen": apply_sharpen, "Cartoon": apply_cartoon,
        "Sketch": apply_sketch, "Pixelate": apply_pixelate,
        "HDR Effect": apply_hdr_effect, "Sunburst Glow": apply_sunburst_glow,
        "Dreamy Glow": apply_dreamy_glow,
        "Color Splash": lambda img: apply_color_splash(img, splash_color),
        "Aviator Sunglasses": lambda img: apply_sunglasses(img, style="aviator"),
        "Retro Square Sunglasses": lambda img: apply_sunglasses(img, style="retro_square"),
        "None": lambda img: img
    }
    
    filter_function = filter_map.get(filter_name, lambda img: img)
    return filter_function(img_np.copy())

# --- Gradio UI ---
css = """
#title { text-align: center; color: #1d1e22; font-size: 2.8em; font-weight: 700; }
#subtitle { text-align: center; color: #57606a; font-size: 1.2em; }
.gradio-container { max-width: 1280px !important; margin: auto !important; }
"""

with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
    gr.Markdown("# Advanced Image & Face Filter Studio", elem_id="title")
    gr.Markdown("Apply classic, artistic, and face-aware effects to your images.", elem_id="subtitle")

    filters_standard = ["Grayscale", "Sepia", "Invert", "Posterize", "Solarize", "Vignette", "Contour", "Sharpen"]
    filters_artistic = ["Cartoon", "Sketch", "Pixelate"]
    filters_advanced = ["HDR Effect", "Color Splash", "Sunburst Glow", "Dreamy Glow"]
    filters_face = ["Aviator Sunglasses", "Retro Square Sunglasses"]
    
    all_filters = ["None"] + filters_standard + filters_artistic + filters_advanced + filters_face

    with gr.Row(equal_height=False):
        with gr.Column(scale=2):
            input_image = gr.Image(sources=["upload", "webcam"], type="pil", label="Input Image")
        
        with gr.Column(scale=1):
            gr.Markdown("### Filter Controls")
            filter_radio = gr.Radio(all_filters, label="Select a Filter", value="None")
            color_picker = gr.ColorPicker(label="Color to Keep (for Color Splash)", value="#FF0000", visible=False)
            apply_button = gr.Button("Apply Filter", variant="primary")

        with gr.Column(scale=2):
            output_image = gr.Image(label="Filtered Output")

    def master_update_function(img, selected_filter, splash_color):
        """This function is the single point of truth for applying filters."""
        processed_img = process_image(img, selected_filter, splash_color)
        color_picker_visibility = gr.update(visible=True) if selected_filter == "Color Splash" else gr.update(visible=False)
        return processed_img, color_picker_visibility

    trigger_inputs = [input_image, filter_radio, color_picker]
    trigger_outputs = [output_image, color_picker]

    filter_radio.change(master_update_function, inputs=trigger_inputs, outputs=trigger_outputs)
    apply_button.click(master_update_function, inputs=trigger_inputs, outputs=trigger_outputs)
    input_image.change(master_update_function, inputs=trigger_inputs, outputs=trigger_outputs)
    color_picker.change(master_update_function, inputs=trigger_inputs, outputs=trigger_outputs)


if __name__ == "__main__":
    demo.launch(debug=True)