Spaces:
Sleeping
Sleeping
| 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) |