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)