Your Name commited on
Commit
68e4b96
·
1 Parent(s): 1a46d63

Implement initial project structure and setup

Browse files
PINOKIO_GUIDE.md ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Pinokio Deployment Guide for PortraitPerfectAI
2
+
3
+ This guide explains how to deploy the AI-Powered Facial & Body Feature Editor application using Pinokio for local hosting.
4
+
5
+ ## What is Pinokio?
6
+
7
+ Pinokio is a browser-based platform that allows you to install, run, and manage AI applications locally on your computer. It provides a simple interface for installing and launching applications without dealing with complex command-line operations.
8
+
9
+ ## Prerequisites
10
+
11
+ - [Pinokio](https://pinokio.computer/) installed on your computer
12
+ - A computer with sufficient resources to run AI applications:
13
+ - At least 8GB RAM (16GB recommended)
14
+ - At least 10GB free disk space
15
+ - NVIDIA GPU with CUDA support (optional but recommended for better performance)
16
+
17
+ ## Installation Steps
18
+
19
+ 1. **Download the PortraitPerfectAI Pinokio Package**
20
+ - Extract the ZIP file to a location of your choice
21
+
22
+ 2. **Open Pinokio Browser**
23
+ - Launch the Pinokio application on your computer
24
+
25
+ 3. **Add the Application to Pinokio**
26
+ - In Pinokio, click on the "+" button to add a new application
27
+ - Navigate to the folder where you extracted the PortraitPerfectAI files
28
+ - Select the folder and click "Open"
29
+
30
+ 4. **Install the Application**
31
+ - Once added, you'll see "PortraitPerfectAI" in your Pinokio dashboard
32
+ - Click on the application
33
+ - Click the "Install" button
34
+ - Wait for the installation to complete (this may take several minutes as it installs Python dependencies)
35
+
36
+ 5. **Launch the Application**
37
+ - After installation is complete, click the "Launch" button
38
+ - The application will start and open in your default web browser
39
+
40
+ ## Using the Application
41
+
42
+ Once launched, you can:
43
+ - Upload images for editing
44
+ - Select facial and body features to modify
45
+ - Adjust settings using sliders and dropdowns
46
+ - Apply AI-powered edits to your images
47
+ - Download the edited results
48
+
49
+ ## Troubleshooting
50
+
51
+ If you encounter any issues:
52
+
53
+ 1. **Installation Fails**
54
+ - Ensure you have a stable internet connection
55
+ - Check that you have sufficient disk space
56
+ - Try restarting Pinokio and attempting installation again
57
+
58
+ 2. **Application Won't Launch**
59
+ - Check the Pinokio logs for any error messages
60
+ - Ensure Python is properly installed on your system
61
+ - Try reinstalling the application
62
+
63
+ 3. **Slow Performance**
64
+ - If you don't have a GPU, processing will be slower
65
+ - Try reducing the image size before uploading
66
+ - Adjust the processing parameters to lower values
67
+
68
+ ## Technical Details
69
+
70
+ The Pinokio package includes:
71
+ - `install.json` - Defines the installation process
72
+ - `run.json` - Defines how to run the application
73
+ - `pinokio.js` - Contains metadata and menu configuration
74
+ - `app.py` - The main application file
75
+ - Supporting modules in the `models/` and `utils/` directories
76
+
77
+ The application uses a Python virtual environment to isolate dependencies and ensure compatibility across different systems.
app.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gradio as gr
3
+ import torch
4
+ from PIL import Image
5
+ import numpy as np
6
+ from models.ledits_model import LEDITSModel
7
+ from utils.image_processing import preprocess_image, postprocess_image
8
+ from utils.feature_detection import detect_features, create_mask
9
+
10
+ # Initialize models
11
+ def initialize_models():
12
+ ledits_model = LEDITSModel()
13
+ return ledits_model
14
+
15
+ # Global variables
16
+ FEATURE_TYPES = ["Eyes", "Nose", "Lips", "Face Shape", "Hair", "Body"]
17
+ MODIFICATION_PRESETS = {
18
+ "Eyes": ["Larger", "Smaller", "Change Color", "Change Shape"],
19
+ "Nose": ["Refine", "Reshape", "Resize"],
20
+ "Lips": ["Fuller", "Thinner", "Change Color"],
21
+ "Face Shape": ["Slim", "Round", "Define Jawline", "Soften Features"],
22
+ "Hair": ["Change Color", "Change Style", "Add Volume"],
23
+ "Body": ["Slim", "Athletic", "Curvy", "Muscular"]
24
+ }
25
+
26
+ # Main editing function
27
+ def edit_image(image, feature_type, modification_type, intensity,
28
+ custom_prompt="", use_custom_prompt=False):
29
+ if image is None:
30
+ return None, "Please upload an image first."
31
+
32
+ try:
33
+ # Convert to numpy array if needed
34
+ if isinstance(image, Image.Image):
35
+ image_np = np.array(image)
36
+ else:
37
+ image_np = image
38
+
39
+ # Preprocess image
40
+ processed_image = preprocess_image(image_np)
41
+
42
+ # Detect features and create mask
43
+ features = detect_features(processed_image)
44
+ mask = create_mask(processed_image, feature_type, features)
45
+
46
+ # Get model
47
+ ledits_model = initialize_models()
48
+
49
+ # Prepare prompt
50
+ if use_custom_prompt and custom_prompt:
51
+ prompt = custom_prompt
52
+ else:
53
+ prompt = f"{feature_type} {modification_type}"
54
+
55
+ # Apply edit
56
+ edited_image = ledits_model.edit_image(
57
+ processed_image,
58
+ mask,
59
+ prompt,
60
+ intensity=intensity
61
+ )
62
+
63
+ # Postprocess
64
+ final_image = postprocess_image(edited_image, processed_image, mask)
65
+
66
+ return final_image, "Edit completed successfully."
67
+
68
+ except Exception as e:
69
+ return image, f"Error during editing: {str(e)}"
70
+
71
+ # UI Components
72
+ def create_ui():
73
+ with gr.Blocks(title="AI-Powered Facial & Body Feature Editor") as app:
74
+ gr.Markdown("# AI-Powered Facial & Body Feature Editor")
75
+ gr.Markdown("Upload an image and use the controls to edit specific facial and body features.")
76
+
77
+ with gr.Row():
78
+ with gr.Column(scale=1):
79
+ # Input controls
80
+ input_image = gr.Image(label="Upload Image", type="pil")
81
+
82
+ with gr.Group():
83
+ gr.Markdown("### Feature Selection")
84
+ feature_type = gr.Dropdown(
85
+ choices=FEATURE_TYPES,
86
+ label="Select Feature",
87
+ value="Eyes"
88
+ )
89
+
90
+ modification_type = gr.Dropdown(
91
+ choices=MODIFICATION_PRESETS["Eyes"],
92
+ label="Modification Type",
93
+ value="Larger"
94
+ )
95
+
96
+ intensity = gr.Slider(
97
+ minimum=0.1,
98
+ maximum=1.0,
99
+ value=0.5,
100
+ step=0.1,
101
+ label="Intensity"
102
+ )
103
+
104
+ with gr.Group():
105
+ gr.Markdown("### Custom Prompt (Advanced)")
106
+ use_custom_prompt = gr.Checkbox(
107
+ label="Use Custom Prompt",
108
+ value=False
109
+ )
110
+ custom_prompt = gr.Textbox(
111
+ label="Custom Prompt",
112
+ placeholder="e.g., blue eyes with long eyelashes"
113
+ )
114
+
115
+ edit_button = gr.Button("Apply Edit", variant="primary")
116
+ reset_button = gr.Button("Reset")
117
+ status_text = gr.Textbox(label="Status", interactive=False)
118
+
119
+ with gr.Column(scale=1):
120
+ # Output display
121
+ output_image = gr.Image(label="Edited Image", type="pil")
122
+
123
+ with gr.Accordion("Edit History", open=False):
124
+ edit_history = gr.State([])
125
+ history_gallery = gr.Gallery(label="Previous Edits")
126
+
127
+ # Event handlers
128
+ def update_modification_choices(feature):
129
+ return gr.Dropdown(choices=MODIFICATION_PRESETS[feature])
130
+
131
+ feature_type.change(
132
+ fn=update_modification_choices,
133
+ inputs=feature_type,
134
+ outputs=modification_type
135
+ )
136
+
137
+ edit_button.click(
138
+ fn=edit_image,
139
+ inputs=[
140
+ input_image,
141
+ feature_type,
142
+ modification_type,
143
+ intensity,
144
+ custom_prompt,
145
+ use_custom_prompt
146
+ ],
147
+ outputs=[output_image, status_text]
148
+ )
149
+
150
+ def reset_image():
151
+ return None, "Image reset."
152
+
153
+ reset_button.click(
154
+ fn=reset_image,
155
+ inputs=[],
156
+ outputs=[output_image, status_text]
157
+ )
158
+
159
+ # Add examples
160
+ gr.Examples(
161
+ examples=[
162
+ ["assets/example1.jpg", "Eyes", "Larger", 0.5, "", False],
163
+ ["assets/example2.jpg", "Lips", "Fuller", 0.4, "", False],
164
+ ["assets/example3.jpg", "Face Shape", "Slim", 0.6, "", False],
165
+ ],
166
+ inputs=[
167
+ input_image,
168
+ feature_type,
169
+ modification_type,
170
+ intensity,
171
+ custom_prompt,
172
+ use_custom_prompt
173
+ ],
174
+ outputs=[output_image, status_text],
175
+ fn=edit_image,
176
+ cache_examples=True,
177
+ )
178
+
179
+ # Add ethical usage notice
180
+ gr.Markdown("""
181
+ ## Ethical Usage Notice
182
+
183
+ This tool is designed for creative and personal use. Please ensure:
184
+
185
+ - You have appropriate rights to edit the images you upload
186
+ - You use this tool responsibly and respect the dignity of individuals
187
+ - You understand that AI-generated modifications are artificial and may not represent reality
188
+
189
+ By using this application, you agree to these terms.
190
+ """)
191
+
192
+ return app
193
+
194
+ # Launch the app
195
+ if __name__ == "__main__":
196
+ app = create_ui()
197
+ app.launch()
icon.png ADDED
image-edit-app-pinokio.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4493214b9975b54ff8860856d8a809b4e7092254c5b9df74d1e6159d16ad2b65
3
+ size 13301
install.json ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "run": [
3
+ {
4
+ "method": "shell.run",
5
+ "params": {
6
+ "message": "mkdir -p feature-editor"
7
+ }
8
+ },
9
+ {
10
+ "method": "shell.run",
11
+ "params": {
12
+ "message": "{{os.platform() === 'win32' ? 'python' : 'python3'}} -m venv env",
13
+ "path": "feature-editor"
14
+ }
15
+ },
16
+ {
17
+ "method": "shell.start",
18
+ "params": {
19
+ "path": "feature-editor"
20
+ }
21
+ },
22
+ {
23
+ "method": "shell.enter",
24
+ "params": {
25
+ "message": "{{os.platform() === 'win32' ? 'env\\\\Scripts\\\\activate' : 'source env/bin/activate'}}",
26
+ "on": [
27
+ {
28
+ "event": null,
29
+ "return": true
30
+ }
31
+ ]
32
+ }
33
+ },
34
+ {
35
+ "method": "shell.enter",
36
+ "params": {
37
+ "message": "pip install gradio diffusers transformers opencv-python pillow numpy torch torchvision",
38
+ "on": [
39
+ {
40
+ "event": null,
41
+ "return": true
42
+ }
43
+ ]
44
+ }
45
+ },
46
+ {
47
+ "method": "fs.copy",
48
+ "params": {
49
+ "from": "app.py",
50
+ "to": "feature-editor/app.py"
51
+ }
52
+ },
53
+ {
54
+ "method": "fs.copy",
55
+ "params": {
56
+ "from": "utils",
57
+ "to": "feature-editor/utils"
58
+ }
59
+ },
60
+ {
61
+ "method": "fs.copy",
62
+ "params": {
63
+ "from": "models",
64
+ "to": "feature-editor/models"
65
+ }
66
+ },
67
+ {
68
+ "method": "input",
69
+ "params": {
70
+ "title": "Installation Complete",
71
+ "description": "AI Facial & Body Feature Editor has been successfully installed. Go back to the dashboard and launch the app!"
72
+ }
73
+ }
74
+ ]
75
+ }
models/ledits_model.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import numpy as np
3
+ from diffusers import StableDiffusionInpaintPipeline, DDIMScheduler
4
+ from PIL import Image
5
+
6
+ class LEDITSModel:
7
+ """
8
+ Implementation of LEDITS++ model for localized image editing using Stable Diffusion.
9
+ """
10
+
11
+ def __init__(self, model_id="runwayml/stable-diffusion-inpainting", device=None):
12
+ """
13
+ Initialize the LEDITS++ model.
14
+
15
+ Args:
16
+ model_id (str): Hugging Face model ID for the Stable Diffusion inpainting model
17
+ device (str, optional): Device to run the model on ('cuda' or 'cpu')
18
+ """
19
+ self.model_id = model_id
20
+
21
+ # Determine device
22
+ if device is None:
23
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
24
+ else:
25
+ self.device = device
26
+
27
+ # Model will be loaded on first use to save memory
28
+ self.pipe = None
29
+
30
+ def load_model(self):
31
+ """
32
+ Load the Stable Diffusion inpainting model.
33
+ """
34
+ if self.pipe is None:
35
+ # Load the pipeline with DDIM scheduler for better quality
36
+ scheduler = DDIMScheduler.from_pretrained(
37
+ self.model_id,
38
+ subfolder="scheduler"
39
+ )
40
+
41
+ self.pipe = StableDiffusionInpaintPipeline.from_pretrained(
42
+ self.model_id,
43
+ scheduler=scheduler,
44
+ safety_checker=None # Disable safety checker for NSFW content as per user request
45
+ )
46
+
47
+ # Move to device
48
+ self.pipe = self.pipe.to(self.device)
49
+
50
+ # Enable memory optimization if on CUDA
51
+ if self.device == "cuda":
52
+ self.pipe.enable_attention_slicing()
53
+
54
+ def edit_image(self, image, mask, prompt, intensity=0.5, guidance_scale=7.5, num_inference_steps=30):
55
+ """
56
+ Edit an image using the LEDITS++ approach.
57
+
58
+ Args:
59
+ image (numpy.ndarray): Input image (normalized to [0, 1])
60
+ mask (numpy.ndarray): Mask indicating the region to edit (values in [0, 1])
61
+ prompt (str): Text prompt describing the desired edit
62
+ intensity (float): Strength of the edit (0.0 to 1.0)
63
+ guidance_scale (float): Guidance scale for diffusion model
64
+ num_inference_steps (int): Number of denoising steps
65
+
66
+ Returns:
67
+ numpy.ndarray: Edited image
68
+ """
69
+ # Load model if not already loaded
70
+ self.load_model()
71
+
72
+ # Convert numpy arrays to PIL Images
73
+ if isinstance(image, np.ndarray):
74
+ # Convert to uint8 if the image is float
75
+ if image.dtype == np.float32 or image.dtype == np.float64:
76
+ image_pil = Image.fromarray((image * 255).astype(np.uint8))
77
+ else:
78
+ image_pil = Image.fromarray(image)
79
+ else:
80
+ image_pil = image
81
+
82
+ if isinstance(mask, np.ndarray):
83
+ # Convert to uint8 if the mask is float
84
+ if mask.dtype == np.float32 or mask.dtype == np.float64:
85
+ mask_pil = Image.fromarray((mask * 255).astype(np.uint8))
86
+ else:
87
+ mask_pil = Image.fromarray(mask)
88
+
89
+ # Ensure mask is grayscale
90
+ if mask_pil.mode != 'L':
91
+ mask_pil = mask_pil.convert('L')
92
+ else:
93
+ mask_pil = mask
94
+
95
+ # Resize images to multiples of 8 (required by Stable Diffusion)
96
+ width, height = image_pil.size
97
+ new_width = width - (width % 8)
98
+ new_height = height - (height % 8)
99
+
100
+ if (new_width, new_height) != image_pil.size:
101
+ image_pil = image_pil.resize((new_width, new_height), Image.LANCZOS)
102
+ mask_pil = mask_pil.resize((new_width, new_height), Image.LANCZOS)
103
+
104
+ # Run the inpainting pipeline
105
+ with torch.no_grad():
106
+ output = self.pipe(
107
+ prompt=prompt,
108
+ image=image_pil,
109
+ mask_image=mask_pil,
110
+ guidance_scale=guidance_scale,
111
+ num_inference_steps=num_inference_steps,
112
+ strength=intensity,
113
+ ).images[0]
114
+
115
+ # Convert back to numpy array
116
+ output_np = np.array(output) / 255.0
117
+
118
+ return output_np
119
+
120
+ def __del__(self):
121
+ """
122
+ Clean up resources when the object is deleted.
123
+ """
124
+ if self.pipe is not None and self.device == "cuda":
125
+ try:
126
+ # Clear CUDA cache
127
+ torch.cuda.empty_cache()
128
+ except:
129
+ pass
130
+
131
+
132
+ class StableDiffusionModel:
133
+ """
134
+ Implementation of Stable Diffusion model for image generation and editing.
135
+ """
136
+
137
+ def __init__(self, model_id="runwayml/stable-diffusion-v1-5", device=None):
138
+ """
139
+ Initialize the Stable Diffusion model.
140
+
141
+ Args:
142
+ model_id (str): Hugging Face model ID for the Stable Diffusion model
143
+ device (str, optional): Device to run the model on ('cuda' or 'cpu')
144
+ """
145
+ self.model_id = model_id
146
+
147
+ # Determine device
148
+ if device is None:
149
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
150
+ else:
151
+ self.device = device
152
+
153
+ # Model will be loaded on first use to save memory
154
+ self.pipe = None
155
+
156
+ def load_model(self):
157
+ """
158
+ Load the Stable Diffusion model.
159
+ """
160
+ if self.pipe is None:
161
+ from diffusers import StableDiffusionPipeline
162
+
163
+ self.pipe = StableDiffusionPipeline.from_pretrained(
164
+ self.model_id,
165
+ safety_checker=None # Disable safety checker for NSFW content as per user request
166
+ )
167
+
168
+ # Move to device
169
+ self.pipe = self.pipe.to(self.device)
170
+
171
+ # Enable memory optimization if on CUDA
172
+ if self.device == "cuda":
173
+ self.pipe.enable_attention_slicing()
174
+
175
+ def generate_image(self, prompt, negative_prompt="", width=512, height=512, guidance_scale=7.5, num_inference_steps=30):
176
+ """
177
+ Generate an image using Stable Diffusion.
178
+
179
+ Args:
180
+ prompt (str): Text prompt describing the desired image
181
+ negative_prompt (str): Text prompt describing what to avoid
182
+ width (int): Width of the generated image
183
+ height (int): Height of the generated image
184
+ guidance_scale (float): Guidance scale for diffusion model
185
+ num_inference_steps (int): Number of denoising steps
186
+
187
+ Returns:
188
+ numpy.ndarray: Generated image
189
+ """
190
+ # Load model if not already loaded
191
+ self.load_model()
192
+
193
+ # Run the pipeline
194
+ with torch.no_grad():
195
+ output = self.pipe(
196
+ prompt=prompt,
197
+ negative_prompt=negative_prompt,
198
+ width=width,
199
+ height=height,
200
+ guidance_scale=guidance_scale,
201
+ num_inference_steps=num_inference_steps,
202
+ ).images[0]
203
+
204
+ # Convert to numpy array
205
+ output_np = np.array(output) / 255.0
206
+
207
+ return output_np
208
+
209
+ def __del__(self):
210
+ """
211
+ Clean up resources when the object is deleted.
212
+ """
213
+ if self.pipe is not None and self.device == "cuda":
214
+ try:
215
+ # Clear CUDA cache
216
+ torch.cuda.empty_cache()
217
+ except:
218
+ pass
pinokio.js ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ title: "PortraitPerfectAI",
3
+ description: "AI-Powered Facial & Body Feature Editor",
4
+ icon: "icon.png",
5
+ menu: [
6
+ {
7
+ html: '<i class="fa-solid fa-microchip"></i> Install',
8
+ href: "install.json"
9
+ },
10
+ {
11
+ html: '<i class="fa-solid fa-rocket"></i> Launch',
12
+ href: "run.json"
13
+ }
14
+ ]
15
+ }
run.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "run": [
3
+ {
4
+ "method": "python",
5
+ "params": {
6
+ "script": "feature-editor/app.py"
7
+ }
8
+ }
9
+ ]
10
+ }
utils/feature_detection.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import cv2
3
+ from PIL import Image
4
+
5
+ def detect_features(image):
6
+ """
7
+ Detect facial and body features in the input image.
8
+
9
+ Args:
10
+ image (numpy.ndarray): Input image in numpy array format
11
+
12
+ Returns:
13
+ dict: Dictionary containing detected features and their coordinates
14
+ """
15
+ # Convert to uint8 if the image is float
16
+ if image.dtype == np.float32 or image.dtype == np.float64:
17
+ image_uint8 = (image * 255).astype(np.uint8)
18
+ else:
19
+ image_uint8 = image
20
+
21
+ # Initialize feature dictionary
22
+ features = {
23
+ "Eyes": [],
24
+ "Nose": [],
25
+ "Lips": [],
26
+ "Face": [],
27
+ "Hair": [],
28
+ "Body": []
29
+ }
30
+
31
+ # Load pre-trained face detector
32
+ face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
33
+ eye_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_eye.xml')
34
+
35
+ # Convert to grayscale for detection
36
+ gray = cv2.cvtColor(image_uint8, cv2.COLOR_RGB2GRAY)
37
+
38
+ # Detect faces
39
+ faces = face_cascade.detectMultiScale(gray, 1.3, 5)
40
+
41
+ for (x, y, w, h) in faces:
42
+ # Add face to features
43
+ features["Face"].append((x, y, w, h))
44
+
45
+ # Define regions of interest for other facial features
46
+ face_roi = gray[y:y+h, x:x+w]
47
+
48
+ # Detect eyes
49
+ eyes = eye_cascade.detectMultiScale(face_roi)
50
+ for (ex, ey, ew, eh) in eyes:
51
+ features["Eyes"].append((x+ex, y+ey, ew, eh))
52
+
53
+ # Approximate nose position (center of face)
54
+ nose_w = w // 4
55
+ nose_h = h // 4
56
+ nose_x = x + w//2 - nose_w//2
57
+ nose_y = y + h//2 - nose_h//2
58
+ features["Nose"].append((nose_x, nose_y, nose_w, nose_h))
59
+
60
+ # Approximate lips position (lower third of face)
61
+ lips_w = w // 2
62
+ lips_h = h // 6
63
+ lips_x = x + w//2 - lips_w//2
64
+ lips_y = y + 2*h//3
65
+ features["Lips"].append((lips_x, lips_y, lips_w, lips_h))
66
+
67
+ # Approximate hair region (top of face and above)
68
+ hair_w = w
69
+ hair_h = h // 2
70
+ hair_x = x
71
+ hair_y = max(0, y - hair_h // 2)
72
+ features["Hair"].append((hair_x, hair_y, hair_w, hair_h))
73
+
74
+ # If no faces detected, use whole image as body
75
+ if len(faces) == 0:
76
+ h, w = image.shape[:2]
77
+ features["Body"].append((0, 0, w, h))
78
+ else:
79
+ # Approximate body region (below face)
80
+ for (x, y, w, h) in faces:
81
+ body_w = w * 2
82
+ body_h = h * 3
83
+ body_x = max(0, x - w//2)
84
+ body_y = y + h
85
+ body_w = min(body_w, image.shape[1] - body_x)
86
+ body_h = min(body_h, image.shape[0] - body_y)
87
+ features["Body"].append((body_x, body_y, body_w, body_h))
88
+
89
+ return features
90
+
91
+ def create_mask(image, feature_type, features):
92
+ """
93
+ Create a binary mask for the selected feature type.
94
+
95
+ Args:
96
+ image (numpy.ndarray): Input image
97
+ feature_type (str): Type of feature to mask
98
+ features (dict): Dictionary of detected features
99
+
100
+ Returns:
101
+ numpy.ndarray: Binary mask highlighting the selected feature
102
+ """
103
+ # Create empty mask
104
+ mask = np.zeros(image.shape[:2], dtype=np.float32)
105
+
106
+ # Map feature_type to the corresponding key in features dictionary
107
+ if feature_type == "Face Shape":
108
+ feature_key = "Face"
109
+ elif feature_type in features:
110
+ feature_key = feature_type
111
+ else:
112
+ # Default to Face if feature type not found
113
+ feature_key = "Face"
114
+
115
+ # Draw filled rectangles for the selected feature
116
+ for (x, y, w, h) in features[feature_key]:
117
+ # Create a filled rectangle
118
+ cv2.rectangle(mask, (x, y), (x+w, y+h), 1.0, -1)
119
+
120
+ # Apply Gaussian blur to soften the mask edges
121
+ mask = cv2.GaussianBlur(mask, (21, 21), 0)
122
+
123
+ # Normalize mask to range [0, 1]
124
+ if mask.max() > 0:
125
+ mask = mask / mask.max()
126
+
127
+ return mask
128
+
129
+ def refine_mask_with_segmentation(image, mask, feature_type):
130
+ """
131
+ Refine the initial mask using image segmentation for more precise feature isolation.
132
+
133
+ Args:
134
+ image (numpy.ndarray): Input image
135
+ mask (numpy.ndarray): Initial mask
136
+ feature_type (str): Type of feature to mask
137
+
138
+ Returns:
139
+ numpy.ndarray: Refined binary mask
140
+ """
141
+ # Convert to uint8 if the image is float
142
+ if image.dtype == np.float32 or image.dtype == np.float64:
143
+ image_uint8 = (image * 255).astype(np.uint8)
144
+ else:
145
+ image_uint8 = image
146
+
147
+ # Create a masked region to focus segmentation
148
+ masked_region = image_uint8.copy()
149
+ for c in range(3):
150
+ masked_region[:, :, c] = masked_region[:, :, c] * mask
151
+
152
+ # Apply GrabCut algorithm for better segmentation
153
+ # Create initial mask for GrabCut
154
+ grabcut_mask = np.zeros(image.shape[:2], dtype=np.uint8)
155
+
156
+ # Areas with high mask values (>0.5) are definitely foreground
157
+ grabcut_mask[mask > 0.5] = cv2.GC_PR_FGD
158
+
159
+ # Areas with some mask values (>0.1) are probably foreground
160
+ grabcut_mask[(mask > 0.1) & (mask <= 0.5)] = cv2.GC_PR_FGD
161
+
162
+ # Rest is probably background
163
+ grabcut_mask[mask <= 0.1] = cv2.GC_PR_BGD
164
+
165
+ # Create temporary arrays for GrabCut
166
+ bgd_model = np.zeros((1, 65), np.float64)
167
+ fgd_model = np.zeros((1, 65), np.float64)
168
+
169
+ # Apply GrabCut
170
+ try:
171
+ cv2.grabCut(
172
+ image_uint8,
173
+ grabcut_mask,
174
+ None,
175
+ bgd_model,
176
+ fgd_model,
177
+ 5,
178
+ cv2.GC_INIT_WITH_MASK
179
+ )
180
+ except:
181
+ # If GrabCut fails, return the original mask
182
+ return mask
183
+
184
+ # Create refined mask
185
+ refined_mask = np.zeros_like(mask)
186
+ refined_mask[grabcut_mask == cv2.GC_FGD] = 1.0
187
+ refined_mask[grabcut_mask == cv2.GC_PR_FGD] = 0.8
188
+
189
+ # Apply Gaussian blur to soften the mask edges
190
+ refined_mask = cv2.GaussianBlur(refined_mask, (15, 15), 0)
191
+
192
+ # Normalize mask to range [0, 1]
193
+ if refined_mask.max() > 0:
194
+ refined_mask = refined_mask / refined_mask.max()
195
+
196
+ return refined_mask
utils/image_processing.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import cv2
3
+ from PIL import Image
4
+
5
+ def preprocess_image(image):
6
+ """
7
+ Preprocess the input image for AI model processing.
8
+
9
+ Args:
10
+ image (numpy.ndarray): Input image in numpy array format
11
+
12
+ Returns:
13
+ numpy.ndarray: Preprocessed image
14
+ """
15
+ # Convert to RGB if needed
16
+ if len(image.shape) == 2:
17
+ image = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB)
18
+ elif image.shape[2] == 4:
19
+ # Handle RGBA images by removing alpha channel
20
+ image = image[:, :, :3]
21
+
22
+ # Resize if needed (models typically expect specific dimensions)
23
+ # Using 512x512 as a common size for diffusion models
24
+ height, width = image.shape[:2]
25
+ max_dim = 512
26
+
27
+ if height > max_dim or width > max_dim:
28
+ # Maintain aspect ratio
29
+ if height > width:
30
+ new_height = max_dim
31
+ new_width = int(width * (max_dim / height))
32
+ else:
33
+ new_width = max_dim
34
+ new_height = int(height * (max_dim / width))
35
+
36
+ image = cv2.resize(image, (new_width, new_height), interpolation=cv2.INTER_AREA)
37
+
38
+ # Normalize pixel values to [0, 1]
39
+ image = image.astype(np.float32) / 255.0
40
+
41
+ return image
42
+
43
+ def postprocess_image(edited_image, original_image, mask=None):
44
+ """
45
+ Postprocess the edited image, blending it with the original if needed.
46
+
47
+ Args:
48
+ edited_image (numpy.ndarray): Edited image from the AI model
49
+ original_image (numpy.ndarray): Original input image
50
+ mask (numpy.ndarray, optional): Mask used for blending
51
+
52
+ Returns:
53
+ PIL.Image: Final processed image
54
+ """
55
+ # Convert back to uint8 range [0, 255]
56
+ if edited_image.max() <= 1.0:
57
+ edited_image = (edited_image * 255.0).astype(np.uint8)
58
+
59
+ if original_image.max() <= 1.0:
60
+ original_image = (original_image * 255.0).astype(np.uint8)
61
+
62
+ # Resize edited image to match original if needed
63
+ if edited_image.shape[:2] != original_image.shape[:2]:
64
+ edited_image = cv2.resize(
65
+ edited_image,
66
+ (original_image.shape[1], original_image.shape[0]),
67
+ interpolation=cv2.INTER_LANCZOS4
68
+ )
69
+
70
+ # If mask is provided, blend the edited and original images
71
+ if mask is not None:
72
+ # Ensure mask is properly sized
73
+ if mask.shape[:2] != original_image.shape[:2]:
74
+ mask = cv2.resize(
75
+ mask,
76
+ (original_image.shape[1], original_image.shape[0]),
77
+ interpolation=cv2.INTER_LINEAR
78
+ )
79
+
80
+ # Ensure mask is in proper format (single channel, values between 0 and 1)
81
+ if len(mask.shape) > 2:
82
+ mask = mask[:, :, 0]
83
+
84
+ if mask.max() > 1.0:
85
+ mask = mask / 255.0
86
+
87
+ # Apply Gaussian blur to mask for smoother blending
88
+ mask = cv2.GaussianBlur(mask, (15, 15), 0)
89
+
90
+ # Expand mask dimensions for broadcasting
91
+ mask_3d = np.expand_dims(mask, axis=2)
92
+ mask_3d = np.repeat(mask_3d, 3, axis=2)
93
+
94
+ # Blend images
95
+ blended = (mask_3d * edited_image) + ((1 - mask_3d) * original_image)
96
+ final_image = blended.astype(np.uint8)
97
+ else:
98
+ final_image = edited_image
99
+
100
+ # Convert to PIL Image for Gradio
101
+ return Image.fromarray(final_image)
102
+
103
+ def apply_quality_matching(edited_image, reference_image):
104
+ """
105
+ Match the quality, lighting, and texture of the edited image to the reference image.
106
+
107
+ Args:
108
+ edited_image (numpy.ndarray): Edited image to adjust
109
+ reference_image (numpy.ndarray): Reference image to match quality with
110
+
111
+ Returns:
112
+ numpy.ndarray: Quality-matched image
113
+ """
114
+ # Convert to LAB color space for better color matching
115
+ edited_lab = cv2.cvtColor(edited_image, cv2.COLOR_RGB2LAB)
116
+ reference_lab = cv2.cvtColor(reference_image, cv2.COLOR_RGB2LAB)
117
+
118
+ # Split channels
119
+ edited_l, edited_a, edited_b = cv2.split(edited_lab)
120
+ reference_l, reference_a, reference_b = cv2.split(reference_lab)
121
+
122
+ # Match luminance histogram
123
+ matched_l = match_histogram(edited_l, reference_l)
124
+
125
+ # Recombine channels
126
+ matched_lab = cv2.merge([matched_l, edited_a, edited_b])
127
+ matched_rgb = cv2.cvtColor(matched_lab, cv2.COLOR_LAB2RGB)
128
+
129
+ # Ensure values are in valid range
130
+ matched_rgb = np.clip(matched_rgb, 0, 1.0)
131
+
132
+ return matched_rgb
133
+
134
+ def match_histogram(source, reference):
135
+ """
136
+ Match the histogram of the source image to the reference image.
137
+
138
+ Args:
139
+ source (numpy.ndarray): Source image channel
140
+ reference (numpy.ndarray): Reference image channel
141
+
142
+ Returns:
143
+ numpy.ndarray: Histogram-matched image channel
144
+ """
145
+ # Calculate histograms
146
+ src_hist, src_bins = np.histogram(source.flatten(), 256, [0, 256], density=True)
147
+ ref_hist, ref_bins = np.histogram(reference.flatten(), 256, [0, 256], density=True)
148
+
149
+ # Calculate cumulative distribution functions
150
+ src_cdf = src_hist.cumsum()
151
+ src_cdf = src_cdf / src_cdf[-1]
152
+
153
+ ref_cdf = ref_hist.cumsum()
154
+ ref_cdf = ref_cdf / ref_cdf[-1]
155
+
156
+ # Create lookup table
157
+ lookup_table = np.zeros(256)
158
+ for i in range(256):
159
+ # Find the closest value in ref_cdf to src_cdf[i]
160
+ lookup_table[i] = np.argmin(np.abs(ref_cdf - src_cdf[i]))
161
+
162
+ # Apply lookup table
163
+ result = lookup_table[source.astype(np.uint8)]
164
+
165
+ return result.astype(np.uint8)