Spaces:
Sleeping
Sleeping
Your Name
Implement feature detection and editing capabilities using OpenCV, update UI for enhanced user experience, and add opencv-python dependency to requirements.txt.
851b7d3
| import gradio as gr | |
| import numpy as np | |
| from PIL import Image, ImageDraw, ImageFont | |
| import cv2 | |
| import os | |
| import io | |
| import base64 | |
| import time | |
| import random | |
| # Global variables | |
| FEATURE_TYPES = ["Eyes", "Nose", "Lips", "Face Shape", "Hair", "Body"] | |
| MODIFICATION_PRESETS = { | |
| "Eyes": ["Larger", "Smaller", "Change Color", "Change Shape"], | |
| "Nose": ["Refine", "Reshape", "Resize"], | |
| "Lips": ["Fuller", "Thinner", "Change Color"], | |
| "Face Shape": ["Slim", "Round", "Define Jawline", "Soften Features"], | |
| "Hair": ["Change Color", "Change Style", "Add Volume"], | |
| "Body": ["Slim", "Athletic", "Curvy", "Muscular"] | |
| } | |
| # Feature detection function | |
| def detect_features(image): | |
| """Detect facial features in the image using OpenCV.""" | |
| if image is None: | |
| return None, "Please upload an image first." | |
| # Convert to numpy array if it's a PIL Image | |
| if isinstance(image, Image.Image): | |
| img_array = np.array(image) | |
| else: | |
| img_array = image.copy() | |
| # Convert to grayscale for face detection | |
| gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY) | |
| # Load pre-trained face detector | |
| face_cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml' | |
| eye_cascade_path = cv2.data.haarcascades + 'haarcascade_eye.xml' | |
| face_cascade = cv2.CascadeClassifier(face_cascade_path) | |
| eye_cascade = cv2.CascadeClassifier(eye_cascade_path) | |
| # Detect faces | |
| faces = face_cascade.detectMultiScale(gray, 1.3, 5) | |
| # Create a copy for visualization | |
| visualization = img_array.copy() | |
| # Dictionary to store detected features | |
| detected_features = { | |
| "faces": [], | |
| "eyes": [], | |
| "nose": [], | |
| "lips": [] | |
| } | |
| # Draw rectangles around detected faces | |
| for (x, y, w, h) in faces: | |
| # Store face coordinates | |
| detected_features["faces"].append((x, y, w, h)) | |
| # Draw face rectangle | |
| cv2.rectangle(visualization, (x, y), (x+w, y+h), (0, 255, 0), 2) | |
| # Region of interest for the face | |
| roi_gray = gray[y:y+h, x:x+w] | |
| roi_color = visualization[y:y+h, x:x+w] | |
| # Detect eyes | |
| eyes = eye_cascade.detectMultiScale(roi_gray) | |
| for (ex, ey, ew, eh) in eyes: | |
| # Store eye coordinates (relative to the face) | |
| detected_features["eyes"].append((x+ex, y+ey, ew, eh)) | |
| # Draw eye rectangle | |
| cv2.rectangle(roi_color, (ex, ey), (ex+ew, ey+eh), (255, 0, 0), 2) | |
| # Approximate nose position (center of face) | |
| nose_x = x + w//2 - 15 | |
| nose_y = y + h//2 - 10 | |
| nose_w = 30 | |
| nose_h = 30 | |
| detected_features["nose"].append((nose_x, nose_y, nose_w, nose_h)) | |
| # Draw nose rectangle | |
| cv2.rectangle(visualization, (nose_x, nose_y), (nose_x+nose_w, nose_y+nose_h), (0, 0, 255), 2) | |
| # Approximate lips position (lower third of face) | |
| lips_x = x + w//4 | |
| lips_y = y + int(h * 0.7) | |
| lips_w = w//2 | |
| lips_h = h//6 | |
| detected_features["lips"].append((lips_x, lips_y, lips_w, lips_h)) | |
| # Draw lips rectangle | |
| cv2.rectangle(visualization, (lips_x, lips_y), (lips_x+lips_w, lips_y+lips_h), (255, 0, 255), 2) | |
| # Add labels | |
| font = cv2.FONT_HERSHEY_SIMPLEX | |
| if len(detected_features["faces"]) > 0: | |
| cv2.putText(visualization, 'Face', (faces[0][0], faces[0][1]-10), font, 0.8, (0, 255, 0), 2) | |
| if len(detected_features["eyes"]) > 0: | |
| cv2.putText(visualization, 'Eye', (detected_features["eyes"][0][0], detected_features["eyes"][0][1]-5), font, 0.5, (255, 0, 0), 2) | |
| if len(detected_features["nose"]) > 0: | |
| cv2.putText(visualization, 'Nose', (detected_features["nose"][0][0], detected_features["nose"][0][1]-5), font, 0.5, (0, 0, 255), 2) | |
| if len(detected_features["lips"]) > 0: | |
| cv2.putText(visualization, 'Lips', (detected_features["lips"][0][0], detected_features["lips"][0][1]-5), font, 0.5, (255, 0, 255), 2) | |
| return Image.fromarray(visualization), detected_features | |
| # Basic image editing function | |
| def edit_image(image, feature_type, modification_type, intensity, detected_features): | |
| """Apply basic image editing based on the selected feature and modification.""" | |
| if image is None or detected_features is None: | |
| return image | |
| # Convert to numpy array if it's a PIL Image | |
| if isinstance(image, Image.Image): | |
| img_array = np.array(image) | |
| else: | |
| img_array = image.copy() | |
| # Create a copy for editing | |
| edited_img = img_array.copy() | |
| # Apply different edits based on feature type | |
| if feature_type == "Eyes" and len(detected_features["eyes"]) > 0: | |
| for (x, y, w, h) in detected_features["eyes"]: | |
| # Get the eye region | |
| eye_region = edited_img[y:y+h, x:x+w] | |
| if modification_type == "Larger": | |
| # Scale the eye region | |
| scale_factor = 1.0 + (intensity * 0.5) # Scale up to 1.5x based on intensity | |
| new_h, new_w = int(h * scale_factor), int(w * scale_factor) | |
| # Resize the eye region | |
| resized_eye = cv2.resize(eye_region, (new_w, new_h)) | |
| # Calculate offsets to center the resized eye | |
| offset_y = (new_h - h) // 2 | |
| offset_x = (new_w - w) // 2 | |
| # Create a larger region to paste the resized eye | |
| y1 = max(0, y - offset_y) | |
| y2 = min(edited_img.shape[0], y + h + offset_y) | |
| x1 = max(0, x - offset_x) | |
| x2 = min(edited_img.shape[1], x + w + offset_x) | |
| # Blend the resized eye with the original image | |
| alpha = 0.7 # Blend factor | |
| try: | |
| # Crop the resized eye to fit the target region | |
| crop_y1 = max(0, offset_y - (y - y1)) | |
| crop_y2 = crop_y1 + (y2 - y1) | |
| crop_x1 = max(0, offset_x - (x - x1)) | |
| crop_x2 = crop_x1 + (x2 - x1) | |
| cropped_eye = resized_eye[crop_y1:crop_y2, crop_x1:crop_x2] | |
| # Ensure dimensions match before blending | |
| if cropped_eye.shape[0] == (y2 - y1) and cropped_eye.shape[1] == (x2 - x1): | |
| edited_img[y1:y2, x1:x2] = cv2.addWeighted( | |
| edited_img[y1:y2, x1:x2], 1-alpha, cropped_eye, alpha, 0 | |
| ) | |
| except Exception as e: | |
| print(f"Error resizing eye: {e}") | |
| elif modification_type == "Smaller": | |
| # Scale the eye region | |
| scale_factor = 1.0 - (intensity * 0.3) # Scale down to 0.7x based on intensity | |
| new_h, new_w = int(h * scale_factor), int(w * scale_factor) | |
| # Resize the eye region | |
| resized_eye = cv2.resize(eye_region, (new_w, new_h)) | |
| # Calculate offsets to center the resized eye | |
| offset_y = (h - new_h) // 2 | |
| offset_x = (w - new_w) // 2 | |
| # Create a background (use the surrounding area) | |
| background = edited_img[y:y+h, x:x+w].copy() | |
| # Paste the resized eye onto the background | |
| background[offset_y:offset_y+new_h, offset_x:offset_x+new_w] = resized_eye | |
| # Blend the result with the original image | |
| edited_img[y:y+h, x:x+w] = background | |
| elif modification_type == "Change Color": | |
| # Apply a color tint to the eye region | |
| # Generate a random color based on intensity | |
| blue = random.randint(0, 255) | |
| green = random.randint(0, 255) | |
| red = random.randint(0, 255) | |
| # Create a color overlay | |
| overlay = np.ones(eye_region.shape, dtype=np.uint8) * np.array([blue, green, red], dtype=np.uint8) | |
| # Blend the overlay with the eye region | |
| alpha = intensity * 0.7 # Adjust alpha based on intensity | |
| edited_img[y:y+h, x:x+w] = cv2.addWeighted(eye_region, 1-alpha, overlay, alpha, 0) | |
| elif feature_type == "Nose" and len(detected_features["nose"]) > 0: | |
| for (x, y, w, h) in detected_features["nose"]: | |
| # Get the nose region | |
| nose_region = edited_img[y:y+h, x:x+w] | |
| if modification_type == "Refine": | |
| # Apply a subtle blur to refine the nose | |
| blurred_nose = cv2.GaussianBlur(nose_region, (5, 5), 0) | |
| # Blend the blurred nose with the original | |
| alpha = intensity * 0.8 | |
| edited_img[y:y+h, x:x+w] = cv2.addWeighted(nose_region, 1-alpha, blurred_nose, alpha, 0) | |
| elif modification_type == "Reshape" or modification_type == "Resize": | |
| # Apply a subtle transformation | |
| scale_x = 1.0 + (intensity * 0.4 - 0.2) # Scale between 0.8x and 1.2x | |
| scale_y = 1.0 + (intensity * 0.4 - 0.2) | |
| # Create transformation matrix | |
| center = (w // 2, h // 2) | |
| M = cv2.getRotationMatrix2D(center, 0, scale_x) | |
| # Apply transformation | |
| transformed_nose = cv2.warpAffine(nose_region, M, (w, h)) | |
| # Blend the transformed nose with the original | |
| alpha = 0.7 | |
| edited_img[y:y+h, x:x+w] = cv2.addWeighted(nose_region, 1-alpha, transformed_nose, alpha, 0) | |
| elif feature_type == "Lips" and len(detected_features["lips"]) > 0: | |
| for (x, y, w, h) in detected_features["lips"]: | |
| # Get the lips region | |
| lips_region = edited_img[y:y+h, x:x+w] | |
| if modification_type == "Fuller": | |
| # Scale the lips region | |
| scale_factor = 1.0 + (intensity * 0.3) # Scale up to 1.3x based on intensity | |
| new_h, new_w = int(h * scale_factor), int(w * scale_factor) | |
| # Resize the lips region | |
| resized_lips = cv2.resize(lips_region, (new_w, new_h)) | |
| # Calculate offsets to center the resized lips | |
| offset_y = (new_h - h) // 2 | |
| offset_x = (new_w - w) // 2 | |
| # Create a larger region to paste the resized lips | |
| y1 = max(0, y - offset_y) | |
| y2 = min(edited_img.shape[0], y + h + offset_y) | |
| x1 = max(0, x - offset_x) | |
| x2 = min(edited_img.shape[1], x + w + offset_x) | |
| # Blend the resized lips with the original image | |
| alpha = 0.7 # Blend factor | |
| try: | |
| # Crop the resized lips to fit the target region | |
| crop_y1 = max(0, offset_y - (y - y1)) | |
| crop_y2 = crop_y1 + (y2 - y1) | |
| crop_x1 = max(0, offset_x - (x - x1)) | |
| crop_x2 = crop_x1 + (x2 - x1) | |
| cropped_lips = resized_lips[crop_y1:crop_y2, crop_x1:crop_x2] | |
| # Ensure dimensions match before blending | |
| if cropped_lips.shape[0] == (y2 - y1) and cropped_lips.shape[1] == (x2 - x1): | |
| edited_img[y1:y2, x1:x2] = cv2.addWeighted( | |
| edited_img[y1:y2, x1:x2], 1-alpha, cropped_lips, alpha, 0 | |
| ) | |
| except Exception as e: | |
| print(f"Error resizing lips: {e}") | |
| elif modification_type == "Thinner": | |
| # Scale the lips region | |
| scale_factor = 1.0 - (intensity * 0.3) # Scale down to 0.7x based on intensity | |
| new_h, new_w = int(h * scale_factor), int(w) # Only reduce height | |
| # Resize the lips region | |
| resized_lips = cv2.resize(lips_region, (new_w, new_h)) | |
| # Calculate offsets to center the resized lips | |
| offset_y = (h - new_h) // 2 | |
| offset_x = 0 | |
| # Create a background (use the surrounding area) | |
| background = edited_img[y:y+h, x:x+w].copy() | |
| # Paste the resized lips onto the background | |
| background[offset_y:offset_y+new_h, offset_x:offset_x+new_w] = resized_lips | |
| # Blend the result with the original image | |
| edited_img[y:y+h, x:x+w] = background | |
| elif modification_type == "Change Color": | |
| # Apply a color tint to the lips | |
| # Use a reddish color for lips | |
| red_tint = np.ones(lips_region.shape, dtype=np.uint8) * np.array([50, 50, 200], dtype=np.uint8) | |
| # Blend the tint with the lips region | |
| alpha = intensity * 0.6 # Adjust alpha based on intensity | |
| edited_img[y:y+h, x:x+w] = cv2.addWeighted(lips_region, 1-alpha, red_tint, alpha, 0) | |
| elif feature_type == "Face Shape" and len(detected_features["faces"]) > 0: | |
| for (x, y, w, h) in detected_features["faces"]: | |
| # Get the face region | |
| face_region = edited_img[y:y+h, x:x+w] | |
| if modification_type == "Slim": | |
| # Apply a slimming effect by squeezing horizontally | |
| scale_x = 1.0 - (intensity * 0.2) # Scale between 0.8x and 1.0x horizontally | |
| scale_y = 1.0 # Keep vertical scale the same | |
| # Create transformation matrix | |
| center = (w // 2, h // 2) | |
| M = cv2.getRotationMatrix2D(center, 0, 1.0) | |
| M[0, 0] = scale_x # Modify the horizontal scale | |
| # Apply transformation | |
| transformed_face = cv2.warpAffine(face_region, M, (w, h)) | |
| # Blend the transformed face with the original | |
| alpha = 0.7 | |
| edited_img[y:y+h, x:x+w] = cv2.addWeighted(face_region, 1-alpha, transformed_face, alpha, 0) | |
| elif modification_type == "Round": | |
| # Apply a rounding effect | |
| # Create a circular mask | |
| mask = np.zeros((h, w), dtype=np.uint8) | |
| center = (w // 2, h // 2) | |
| radius = min(w, h) // 2 | |
| cv2.circle(mask, center, radius, 255, -1) | |
| # Blur the edges of the face | |
| blurred_face = cv2.GaussianBlur(face_region, (21, 21), 0) | |
| # Blend based on the mask | |
| alpha = intensity * 0.5 | |
| for i in range(h): | |
| for j in range(w): | |
| if mask[i, j] == 0: | |
| # Outside the circle, blend more of the blurred face | |
| edited_img[y+i, x+j] = cv2.addWeighted( | |
| face_region[i, j].reshape(1, 3), 1-alpha, | |
| blurred_face[i, j].reshape(1, 3), alpha, 0 | |
| ).reshape(3) | |
| # For other features, apply a simpler effect | |
| else: | |
| # Add a visual indicator to show something happened | |
| # Draw a small colored rectangle in the corner to indicate processing | |
| color = (int(255 * intensity), 100, 200) | |
| cv2.rectangle(edited_img, (10, 10), (30, 30), color, -1) | |
| # Add text to indicate the modification | |
| font = cv2.FONT_HERSHEY_SIMPLEX | |
| cv2.putText( | |
| edited_img, | |
| f"{feature_type}: {modification_type}", | |
| (40, 25), | |
| font, | |
| 0.7, | |
| (255, 255, 255), | |
| 2 | |
| ) | |
| return Image.fromarray(edited_img) | |
| # Main processing function | |
| def process_image(image, feature_type, modification_type, intensity, custom_prompt="", use_custom_prompt=False): | |
| if image is None: | |
| return None, None, "Please upload an image first." | |
| # Step 1: Detect features and create visualization | |
| visualization, detected_features = detect_features(image) | |
| # Step 2: Apply edits based on detected features | |
| if isinstance(image, np.ndarray): | |
| processed_image = edit_image(image, feature_type, modification_type, intensity, detected_features) | |
| else: | |
| processed_image = edit_image(np.array(image), feature_type, modification_type, intensity, detected_features) | |
| # Get the instruction based on feature and modification | |
| if use_custom_prompt and custom_prompt: | |
| instruction = custom_prompt | |
| else: | |
| instruction = f"Applied {feature_type} modification: {modification_type} with intensity {intensity:.1f}" | |
| return processed_image, visualization, f"Edit applied: {instruction}\n\nNote: This is using CPU-based processing. For more advanced AI-powered edits, download the Pinokio local version which supports GPU acceleration." | |
| # UI Components | |
| def create_ui(): | |
| with gr.Blocks(title="PortraitPerfectAI - Facial & Body Feature Editor") as app: | |
| gr.Markdown("# PortraitPerfectAI - Facial & Body Feature Editor") | |
| gr.Markdown("Upload an image and use the controls to edit specific facial and body features.") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| # Input controls | |
| input_image = gr.Image(label="Upload Image", type="numpy") | |
| with gr.Group(): | |
| gr.Markdown("### Feature Selection") | |
| feature_type = gr.Dropdown( | |
| choices=FEATURE_TYPES, | |
| label="Select Feature", | |
| value="Eyes" | |
| ) | |
| # Initialize with choices for the default feature (Eyes) | |
| modification_type = gr.Dropdown( | |
| choices=MODIFICATION_PRESETS["Eyes"], | |
| label="Modification Type", | |
| value="Larger" | |
| ) | |
| intensity = gr.Slider( | |
| minimum=0.1, | |
| maximum=1.0, | |
| value=0.5, | |
| step=0.1, | |
| label="Intensity" | |
| ) | |
| with gr.Group(): | |
| gr.Markdown("### Custom Prompt (Advanced)") | |
| use_custom_prompt = gr.Checkbox( | |
| label="Use Custom Prompt", | |
| value=False | |
| ) | |
| custom_prompt = gr.Textbox( | |
| label="Custom Prompt", | |
| placeholder="e.g., make the eyes blue and add long eyelashes" | |
| ) | |
| edit_button = gr.Button("Apply Edit", variant="primary") | |
| reset_button = gr.Button("Reset") | |
| status_text = gr.Textbox(label="Status", interactive=False) | |
| with gr.Column(scale=1): | |
| # Output display | |
| with gr.Tab("Edited Image"): | |
| output_image = gr.Image(label="Edited Image", type="pil") | |
| with gr.Tab("Feature Detection"): | |
| feature_visualization = gr.Image(label="Detected Features", type="pil") | |
| # Download Pinokio package section | |
| with gr.Accordion("Download Full Version for Local Use", open=True): | |
| gr.Markdown(""" | |
| ### Get the Full AI-Powered Version | |
| For more advanced AI-powered editing with GPU acceleration: | |
| 1. Download the Pinokio package below | |
| 2. Install [Pinokio](https://pinokio.computer/) on your computer | |
| 3. Follow the instructions in the PINOKIO_GUIDE.md file | |
| [Download Pinokio Package](pinokio-package.zip) | |
| """) | |
| # Information about the application | |
| with gr.Accordion("About This Application", open=False): | |
| gr.Markdown(""" | |
| ### PortraitPerfectAI | |
| This application allows you to make precise edits to facial and body features in uploaded images. | |
| **Features:** | |
| - Edit facial features like eyes, nose, lips, and more | |
| - Modify body proportions and characteristics | |
| - Intuitive sliders and controls | |
| - Non-destructive editing workflow | |
| **Note:** The web version uses CPU-based processing. For more advanced AI-powered editing with GPU acceleration, download the Pinokio package. | |
| """) | |
| # Event handlers | |
| def update_modification_choices(feature): | |
| return gr.Dropdown(choices=MODIFICATION_PRESETS[feature]) | |
| feature_type.change( | |
| fn=update_modification_choices, | |
| inputs=feature_type, | |
| outputs=modification_type | |
| ) | |
| edit_button.click( | |
| fn=process_image, | |
| inputs=[ | |
| input_image, | |
| feature_type, | |
| modification_type, | |
| intensity, | |
| custom_prompt, | |
| use_custom_prompt | |
| ], | |
| outputs=[output_image, feature_visualization, status_text] | |
| ) | |
| def reset_image(): | |
| return None, None, "Image reset." | |
| reset_button.click( | |
| fn=reset_image, | |
| inputs=[], | |
| outputs=[output_image, feature_visualization, status_text] | |
| ) | |
| # Add ethical usage notice | |
| gr.Markdown(""" | |
| ## Ethical Usage Notice | |
| This tool is designed for creative and personal use. Please ensure: | |
| - You have appropriate rights to edit the images you upload | |
| - You use this tool responsibly and respect the dignity of individuals | |
| - You understand that AI-generated modifications are artificial and may not represent reality | |
| By using this application, you agree to these terms. | |
| """) | |
| return app | |
| # Launch the app | |
| if __name__ == "__main__": | |
| app = create_ui() | |
| app.launch(server_name="0.0.0.0", share=False) | |