jefrisuparjanaAI commited on
Commit
eba8a48
·
verified ·
1 Parent(s): 21b8f1b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +334 -151
app.py CHANGED
@@ -1,151 +1,334 @@
1
- import hashlib
2
- import os
3
- from io import BytesIO
4
-
5
- import gradio as gr
6
- import grpc
7
- from PIL import Image
8
- from cachetools import LRUCache
9
-
10
- from inference_pb2 import HairSwapRequest, HairSwapResponse
11
- from inference_pb2_grpc import HairSwapServiceStub
12
- from utils.shape_predictor import align_face
13
-
14
-
15
- def get_bytes(img):
16
- if img is None:
17
- return img
18
-
19
- buffered = BytesIO()
20
- img.save(buffered, format="JPEG")
21
- return buffered.getvalue()
22
-
23
-
24
- def bytes_to_image(image: bytes) -> Image.Image:
25
- image = Image.open(BytesIO(image))
26
- return image
27
-
28
-
29
- def center_crop(img):
30
- width, height = img.size
31
- side = min(width, height)
32
-
33
- left = (width - side) / 2
34
- top = (height - side) / 2
35
- right = (width + side) / 2
36
- bottom = (height + side) / 2
37
-
38
- img = img.crop((left, top, right, bottom))
39
- return img
40
-
41
-
42
- def resize(name):
43
- def resize_inner(img, align):
44
- global align_cache
45
-
46
- if name in align:
47
- img_hash = hashlib.md5(get_bytes(img)).hexdigest()
48
-
49
- if img_hash not in align_cache:
50
- img = align_face(img, return_tensors=False)[0]
51
- align_cache[img_hash] = img
52
- else:
53
- img = align_cache[img_hash]
54
-
55
- elif img.size != (1024, 1024):
56
- img = center_crop(img)
57
- img = img.resize((1024, 1024), Image.Resampling.LANCZOS)
58
-
59
- return img
60
-
61
- return resize_inner
62
-
63
-
64
- def swap_hair(face, shape, color, blending, poisson_iters, poisson_erosion):
65
- if not face and not shape and not color:
66
- return gr.update(visible=False), gr.update(value="Need to upload a face and at least a shape or color ❗", visible=True)
67
- elif not face:
68
- return gr.update(visible=False), gr.update(value="Need to upload a face ❗", visible=True)
69
- elif not shape and not color:
70
- return gr.update(visible=False), gr.update(value="Need to upload at least a shape or color ❗", visible=True)
71
-
72
- face_bytes, shape_bytes, color_bytes = map(lambda item: get_bytes(item), (face, shape, color))
73
-
74
- if shape_bytes is None:
75
- shape_bytes = b'face'
76
- if color_bytes is None:
77
- color_bytes = b'shape'
78
-
79
- with grpc.insecure_channel(os.environ['SERVER']) as channel:
80
- stub = HairSwapServiceStub(channel)
81
-
82
- output: HairSwapResponse = stub.swap(
83
- HairSwapRequest(face=face_bytes, shape=shape_bytes, color=color_bytes, blending=blending,
84
- poisson_iters=poisson_iters, poisson_erosion=poisson_erosion, use_cache=True)
85
- )
86
-
87
- output = bytes_to_image(output.image)
88
- return gr.update(value=output, visible=True), gr.update(visible=False)
89
-
90
-
91
- def get_demo():
92
- with gr.Blocks() as demo:
93
- gr.Markdown("## HairFastGan")
94
- gr.Markdown(
95
- '<div style="display: flex; align-items: center; gap: 10px;">'
96
- '<span>Official HairFastGAN Gradio demo:</span>'
97
- '<a href="https://arxiv.org/abs/2404.01094"><img src="https://img.shields.io/badge/arXiv-2404.01094-b31b1b.svg" height=22.5></a>'
98
- '<a href="https://github.com/AIRI-Institute/HairFastGAN"><img src="https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white" height=22.5></a>'
99
- '<a href="https://huggingface.co/AIRI-Institute/HairFastGAN"><img src="https://huggingface.co/datasets/huggingface/badges/resolve/main/model-on-hf-md.svg" height=22.5></a>'
100
- '<a href="https://colab.research.google.com/#fileId=https://huggingface.co/AIRI-Institute/HairFastGAN/blob/main/notebooks/HairFast_inference.ipynb"><img src="https://colab.research.google.com/assets/colab-badge.svg" height=22.5></a>'
101
- '</div>'
102
- )
103
- with gr.Row():
104
- with gr.Column():
105
- source = gr.Image(label="Source photo to try on the hairstyle", type="pil")
106
- with gr.Row():
107
- shape = gr.Image(label="Shape photo with desired hairstyle (optional)", type="pil")
108
- color = gr.Image(label="Color photo with desired hair color (optional)", type="pil")
109
- with gr.Accordion("Advanced Options", open=False):
110
- blending = gr.Radio(["Article", "Alternative_v1", "Alternative_v2"], value='Article',
111
- label="Color Encoder version", info="Selects a model for hair color transfer.")
112
- poisson_iters = gr.Slider(0, 2500, value=0, step=1, label="Poisson iters",
113
- info="The power of blending with the original image, helps to recover more details. Not included in the article, disabled by default.")
114
- poisson_erosion = gr.Slider(1, 100, value=15, step=1, label="Poisson erosion",
115
- info="Smooths out the blending area.")
116
- align = gr.CheckboxGroup(["Face", "Shape", "Color"], value=["Face", "Shape", "Color"],
117
- label="Image cropping [recommended]",
118
- info="Selects which images to crop by face")
119
- btn = gr.Button("Get the haircut")
120
- with gr.Column():
121
- output = gr.Image(label="Your result")
122
- error_message = gr.Textbox(label="⚠️ Error ⚠️", visible=False, elem_classes="error-message")
123
-
124
- gr.Examples(examples=[["input/0.png", "input/1.png", "input/2.png"], ["input/6.png", "input/7.png", None],
125
- ["input/10.jpg", None, "input/11.jpg"]],
126
- inputs=[source, shape, color], outputs=output)
127
-
128
- source.upload(fn=resize('Face'), inputs=[source, align], outputs=source)
129
- shape.upload(fn=resize('Shape'), inputs=[shape, align], outputs=shape)
130
- color.upload(fn=resize('Color'), inputs=[color, align], outputs=color)
131
-
132
- btn.click(fn=swap_hair, inputs=[source, shape, color, blending, poisson_iters, poisson_erosion],
133
- outputs=[output, error_message])
134
-
135
- gr.Markdown('''To cite the paper by the authors
136
- ```
137
- @article{nikolaev2024hairfastgan,
138
- title={HairFastGAN: Realistic and Robust Hair Transfer with a Fast Encoder-Based Approach},
139
- author={Nikolaev, Maxim and Kuznetsov, Mikhail and Vetrov, Dmitry and Alanov, Aibek},
140
- journal={arXiv preprint arXiv:2404.01094},
141
- year={2024}
142
- }
143
- ```
144
- ''')
145
- return demo
146
-
147
-
148
- if __name__ == '__main__':
149
- align_cache = LRUCache(maxsize=10)
150
- demo = get_demo()
151
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib
2
+ import os
3
+ import base64
4
+ from io import BytesIO
5
+ import threading
6
+
7
+ import gradio as gr
8
+ from flask import Flask, request, jsonify
9
+ from flask_cors import CORS
10
+ from PIL import Image
11
+ from cachetools import LRUCache
12
+ import grpc
13
+
14
+ from inference_pb2 import HairSwapRequest, HairSwapResponse
15
+ from inference_pb2_grpc import HairSwapServiceStub
16
+ from utils.shape_predictor import align_face
17
+
18
+ # Initialize Flask app
19
+ flask_app = Flask(__name__)
20
+ CORS(flask_app)
21
+
22
+ # Global cache
23
+ align_cache = LRUCache(maxsize=10)
24
+
25
+
26
+ def get_bytes(img):
27
+ if img is None:
28
+ return img
29
+ buffered = BytesIO()
30
+ img.save(buffered, format="JPEG")
31
+ return buffered.getvalue()
32
+
33
+
34
+ def bytes_to_image(image: bytes) -> Image.Image:
35
+ image = Image.open(BytesIO(image))
36
+ return image
37
+
38
+
39
+ def base64_to_image(base64_string: str) -> Image.Image:
40
+ """Convert base64 string to PIL Image"""
41
+ image_data = base64.b64decode(base64_string.split(',')[-1])
42
+ return Image.open(BytesIO(image_data))
43
+
44
+
45
+ def image_to_base64(img: Image.Image) -> str:
46
+ """Convert PIL Image to base64 string"""
47
+ buffered = BytesIO()
48
+ img.save(buffered, format="JPEG")
49
+ img_str = base64.b64encode(buffered.getvalue()).decode()
50
+ return f"data:image/jpeg;base64,{img_str}"
51
+
52
+
53
+ def center_crop(img):
54
+ width, height = img.size
55
+ side = min(width, height)
56
+ left = (width - side) / 2
57
+ top = (height - side) / 2
58
+ right = (width + side) / 2
59
+ bottom = (height + side) / 2
60
+ img = img.crop((left, top, right, bottom))
61
+ return img
62
+
63
+
64
+ def resize(name):
65
+ def resize_inner(img, align):
66
+ global align_cache
67
+
68
+ if name in align:
69
+ img_hash = hashlib.md5(get_bytes(img)).hexdigest()
70
+
71
+ if img_hash not in align_cache:
72
+ img = align_face(img, return_tensors=False)[0]
73
+ align_cache[img_hash] = img
74
+ else:
75
+ img = align_cache[img_hash]
76
+
77
+ elif img.size != (1024, 1024):
78
+ img = center_crop(img)
79
+ img = img.resize((1024, 1024), Image.Resampling.LANCZOS)
80
+
81
+ return img
82
+
83
+ return resize_inner
84
+
85
+
86
+ def swap_hair_core(face, shape, color, blending, poisson_iters, poisson_erosion, align_settings):
87
+ """Core hair swap function used by both Gradio and Flask"""
88
+ if not face:
89
+ return None, "Need to upload a face ❗"
90
+
91
+ if not shape and not color:
92
+ return None, "Need to upload at least a shape or color ❗"
93
+
94
+ # Process images
95
+ face_img = face
96
+ shape_img = shape
97
+ color_img = color
98
+
99
+ # Apply alignment if needed
100
+ if 'Face' in align_settings:
101
+ img_hash = hashlib.md5(get_bytes(face_img)).hexdigest()
102
+ if img_hash not in align_cache:
103
+ face_img = align_face(face_img, return_tensors=False)[0]
104
+ align_cache[img_hash] = face_img
105
+ else:
106
+ face_img = align_cache[img_hash]
107
+
108
+ if shape_img and 'Shape' in align_settings:
109
+ img_hash = hashlib.md5(get_bytes(shape_img)).hexdigest()
110
+ if img_hash not in align_cache:
111
+ shape_img = align_face(shape_img, return_tensors=False)[0]
112
+ align_cache[img_hash] = shape_img
113
+ else:
114
+ shape_img = align_cache[img_hash]
115
+
116
+ if color_img and 'Color' in align_settings:
117
+ img_hash = hashlib.md5(get_bytes(color_img)).hexdigest()
118
+ if img_hash not in align_cache:
119
+ color_img = align_face(color_img, return_tensors=False)[0]
120
+ align_cache[img_hash] = color_img
121
+ else:
122
+ color_img = align_cache[img_hash]
123
+
124
+ # Resize if needed
125
+ if face_img.size != (1024, 1024):
126
+ face_img = center_crop(face_img)
127
+ face_img = face_img.resize((1024, 1024), Image.Resampling.LANCZOS)
128
+
129
+ if shape_img and shape_img.size != (1024, 1024):
130
+ shape_img = center_crop(shape_img)
131
+ shape_img = shape_img.resize((1024, 1024), Image.Resampling.LANCZOS)
132
+
133
+ if color_img and color_img.size != (1024, 1024):
134
+ color_img = center_crop(color_img)
135
+ color_img = color_img.resize((1024, 1024), Image.Resampling.LANCZOS)
136
+
137
+ # Convert to bytes
138
+ face_bytes = get_bytes(face_img)
139
+ shape_bytes = get_bytes(shape_img) if shape_img else b'face'
140
+ color_bytes = get_bytes(color_img) if color_img else b'shape'
141
+
142
+ try:
143
+ with grpc.insecure_channel(os.environ.get('SERVER', 'localhost:50051')) as channel:
144
+ stub = HairSwapServiceStub(channel)
145
+ output: HairSwapResponse = stub.swap(
146
+ HairSwapRequest(
147
+ face=face_bytes,
148
+ shape=shape_bytes,
149
+ color=color_bytes,
150
+ blending=blending,
151
+ poisson_iters=poisson_iters,
152
+ poisson_erosion=poisson_erosion,
153
+ use_cache=True
154
+ )
155
+ )
156
+
157
+ output_img = bytes_to_image(output.image)
158
+ return output_img, None
159
+
160
+ except Exception as e:
161
+ return None, f"Error: {str(e)}"
162
+
163
+
164
+ def swap_hair(face, shape, color, blending, poisson_iters, poisson_erosion, align):
165
+ """Gradio interface function"""
166
+ result, error = swap_hair_core(
167
+ face, shape, color, blending,
168
+ poisson_iters, poisson_erosion, align
169
+ )
170
+
171
+ if error:
172
+ return gr.update(visible=False), gr.update(value=error, visible=True)
173
+
174
+ return gr.update(value=result, visible=True), gr.update(visible=False)
175
+
176
+
177
+ # Flask API Endpoints
178
+ @flask_app.route('/health', methods=['GET'])
179
+ def health_check():
180
+ """Health check endpoint"""
181
+ return jsonify({"status": "healthy", "service": "HairFastGAN API"}), 200
182
+
183
+
184
+ @flask_app.route('/api/swap-hair', methods=['POST'])
185
+ def api_swap_hair():
186
+ """Flask API endpoint for hair swap"""
187
+ try:
188
+ data = request.get_json()
189
+
190
+ if not data:
191
+ return jsonify({"error": "No JSON data provided"}), 400
192
+
193
+ if 'face' not in data:
194
+ return jsonify({"error": "Face image is required"}), 400
195
+
196
+ if 'shape' not in data and 'color' not in data:
197
+ return jsonify({"error": "At least shape or color image is required"}), 400
198
+
199
+ # Parse images
200
+ face_img = base64_to_image(data['face'])
201
+ shape_img = base64_to_image(data['shape']) if 'shape' in data and data['shape'] else None
202
+ color_img = base64_to_image(data['color']) if 'color' in data and data['color'] else None
203
+
204
+ # Get options
205
+ blending = data.get('blending', 'Article')
206
+ poisson_iters = int(data.get('poisson_iters', 0))
207
+ poisson_erosion = int(data.get('poisson_erosion', 15))
208
+
209
+ # Build align settings
210
+ align_settings = []
211
+ if data.get('align_face', True):
212
+ align_settings.append('Face')
213
+ if data.get('align_shape', True):
214
+ align_settings.append('Shape')
215
+ if data.get('align_color', True):
216
+ align_settings.append('Color')
217
+
218
+ # Process
219
+ result_img, error = swap_hair_core(
220
+ face_img, shape_img, color_img,
221
+ blending, poisson_iters, poisson_erosion,
222
+ align_settings
223
+ )
224
+
225
+ if error:
226
+ return jsonify({"error": error}), 500
227
+
228
+ # Convert to base64
229
+ result_base64 = image_to_base64(result_img)
230
+
231
+ return jsonify({
232
+ "success": True,
233
+ "result": result_base64,
234
+ "message": "Hair swap completed successfully"
235
+ }), 200
236
+
237
+ except Exception as e:
238
+ return jsonify({"error": f"Internal server error: {str(e)}"}), 500
239
+
240
+
241
+ @flask_app.route('/', methods=['GET'])
242
+ def api_root():
243
+ """API documentation"""
244
+ return jsonify({
245
+ "service": "HairFastGAN API",
246
+ "version": "1.0",
247
+ "endpoints": {
248
+ "/health": "GET - Health check",
249
+ "/api/swap-hair": "POST - Hair swap endpoint"
250
+ }
251
+ }), 200
252
+
253
+
254
+ # Gradio Interface
255
+ def get_demo():
256
+ with gr.Blocks() as demo:
257
+ gr.Markdown("## HairFastGan")
258
+ gr.Markdown(
259
+ '<div style="display: flex; align-items: center; gap: 10px;">'
260
+ '<span>Official HairFastGAN Gradio demo:</span>'
261
+ '<a href="https://arxiv.org/abs/2404.01094"><img src="https://img.shields.io/badge/arXiv-2404.01094-b31b1b.svg" height=22.5></a>'
262
+ '<a href="https://github.com/AIRI-Institute/HairFastGAN"><img src="https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white" height=22.5></a>'
263
+ '<a href="https://huggingface.co/AIRI-Institute/HairFastGAN"><img src="https://huggingface.co/datasets/huggingface/badges/resolve/main/model-on-hf-md.svg" height=22.5></a>'
264
+ '</div>'
265
+ )
266
+
267
+ gr.Markdown("### 🔗 API Endpoint Available!")
268
+ gr.Markdown("Use `/api/swap-hair` endpoint for programmatic access. See API docs at `/` endpoint.")
269
+
270
+ with gr.Row():
271
+ with gr.Column():
272
+ source = gr.Image(label="Source photo to try on the hairstyle", type="pil")
273
+ with gr.Row():
274
+ shape = gr.Image(label="Shape photo with desired hairstyle (optional)", type="pil")
275
+ color = gr.Image(label="Color photo with desired hair color (optional)", type="pil")
276
+ with gr.Accordion("Advanced Options", open=False):
277
+ blending = gr.Radio(["Article", "Alternative_v1", "Alternative_v2"], value='Article',
278
+ label="Color Encoder version", info="Selects a model for hair color transfer.")
279
+ poisson_iters = gr.Slider(0, 2500, value=0, step=1, label="Poisson iters",
280
+ info="The power of blending with the original image, helps to recover more details. Not included in the article, disabled by default.")
281
+ poisson_erosion = gr.Slider(1, 100, value=15, step=1, label="Poisson erosion",
282
+ info="Smooths out the blending area.")
283
+ align = gr.CheckboxGroup(["Face", "Shape", "Color"], value=["Face", "Shape", "Color"],
284
+ label="Image cropping [recommended]",
285
+ info="Selects which images to crop by face")
286
+ btn = gr.Button("Get the haircut")
287
+ with gr.Column():
288
+ output = gr.Image(label="Your result")
289
+ error_message = gr.Textbox(label="⚠️ Error ⚠️", visible=False, elem_classes="error-message")
290
+
291
+ gr.Examples(examples=[["input/0.png", "input/1.png", "input/2.png"], ["input/6.png", "input/7.png", None],
292
+ ["input/10.jpg", None, "input/11.jpg"]],
293
+ inputs=[source, shape, color], outputs=output)
294
+
295
+ source.upload(fn=resize('Face'), inputs=[source, align], outputs=source)
296
+ shape.upload(fn=resize('Shape'), inputs=[shape, align], outputs=shape)
297
+ color.upload(fn=resize('Color'), inputs=[color, align], outputs=color)
298
+
299
+ btn.click(fn=swap_hair, inputs=[source, shape, color, blending, poisson_iters, poisson_erosion, align],
300
+ outputs=[output, error_message])
301
+
302
+ gr.Markdown('''To cite the paper by the authors
303
+ ```
304
+ @article{nikolaev2024hairfastgan,
305
+ title={HairFastGAN: Realistic and Robust Hair Transfer with a Fast Encoder-Based Approach},
306
+ author={Nikolaev, Maxim and Kuznetsov, Mikhail and Vetrov, Dmitry and Alanov, Aibek},
307
+ journal={arXiv preprint arXiv:2404.01094},
308
+ year={2024}
309
+ }
310
+ ```
311
+ ''')
312
+ return demo
313
+
314
+
315
+ def run_flask():
316
+ """Run Flask in a separate thread"""
317
+ flask_app.run(host='0.0.0.0', port=5000, debug=False, use_reloader=False)
318
+
319
+
320
+ if __name__ == '__main__':
321
+ # Start Flask API in background thread
322
+ flask_thread = threading.Thread(target=run_flask, daemon=True)
323
+ flask_thread.start()
324
+
325
+ print("🚀 Flask API running on http://0.0.0.0:5000")
326
+ print("🎨 Gradio UI starting on http://0.0.0.0:7860")
327
+
328
+ # Start Gradio
329
+ demo = get_demo()
330
+ demo.launch(
331
+ server_name="0.0.0.0",
332
+ server_port=7860,
333
+ share=False
334
+ )