Spaces:
Running
Running
import hashlib | |
import os | |
from io import BytesIO | |
import gradio as gr | |
import grpc | |
from PIL import Image | |
from cachetools import LRUCache | |
from inference_pb2 import HairSwapRequest, HairSwapResponse | |
from inference_pb2_grpc import HairSwapServiceStub | |
from utils.shape_predictor import align_face | |
def get_bytes(img): | |
if img is None: | |
return img | |
buffered = BytesIO() | |
img.save(buffered, format="JPEG") | |
return buffered.getvalue() | |
def bytes_to_image(image: bytes) -> Image.Image: | |
image = Image.open(BytesIO(image)) | |
return image | |
def center_crop(img): | |
width, height = img.size | |
side = min(width, height) | |
left = (width - side) / 2 | |
top = (height - side) / 2 | |
right = (width + side) / 2 | |
bottom = (height + side) / 2 | |
img = img.crop((left, top, right, bottom)) | |
return img | |
def resize(name): | |
def resize_inner(img, align): | |
global align_cache | |
if name in align: | |
img_hash = hashlib.md5(get_bytes(img)).hexdigest() | |
if img_hash not in align_cache: | |
img = align_face(img, return_tensors=False)[0] | |
align_cache[img_hash] = img | |
else: | |
img = align_cache[img_hash] | |
elif img.size != (1024, 1024): | |
img = center_crop(img) | |
img = img.resize((1024, 1024), Image.Resampling.LANCZOS) | |
return img | |
return resize_inner | |
def swap_hair(face, shape, color, blending, poisson_iters, poisson_erosion): | |
if not face and not shape and not color: | |
return gr.update(visible=False), gr.update(value="Need to upload a face and at least a shape or color β", visible=True) | |
elif not face: | |
return gr.update(visible=False), gr.update(value="Need to upload a face β", visible=True) | |
elif not shape and not color: | |
return gr.update(visible=False), gr.update(value="Need to upload at least a shape or color β", visible=True) | |
face_bytes, shape_bytes, color_bytes = map(lambda item: get_bytes(item), (face, shape, color)) | |
if shape_bytes is None: | |
shape_bytes = b'face' | |
if color_bytes is None: | |
color_bytes = b'shape' | |
with grpc.insecure_channel(os.environ['SERVER']) as channel: | |
stub = HairSwapServiceStub(channel) | |
output: HairSwapResponse = stub.swap( | |
HairSwapRequest(face=face_bytes, shape=shape_bytes, color=color_bytes, blending=blending, | |
poisson_iters=poisson_iters, poisson_erosion=poisson_erosion, use_cache=True) | |
) | |
output = bytes_to_image(output.image) | |
return gr.update(value=output, visible=True), gr.update(visible=False) | |
def get_demo(): | |
with gr.Blocks() as demo: | |
gr.Markdown("## HairFastGan") | |
gr.Markdown( | |
'<div style="display: flex; align-items: center; gap: 10px;">' | |
'<span>Official HairFastGAN Gradio demo:</span>' | |
'<a href="https://arxiv.org/abs/2404.01094"><img src="https://img.shields.io/badge/arXiv-2404.01094-b31b1b.svg" height=22.5></a>' | |
'<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>' | |
'<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>' | |
'<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>' | |
'</div>' | |
) | |
with gr.Row(): | |
with gr.Column(): | |
source = gr.Image(label="Source photo to try on the hairstyle", type="pil") | |
with gr.Row(): | |
shape = gr.Image(label="Shape photo with desired hairstyle (optional)", type="pil") | |
color = gr.Image(label="Color photo with desired hair color (optional)", type="pil") | |
with gr.Accordion("Advanced Options", open=False): | |
blending = gr.Radio(["Article", "Alternative_v1", "Alternative_v2"], value='Article', | |
label="Color Encoder version", info="Selects a model for hair color transfer.") | |
poisson_iters = gr.Slider(0, 2500, value=0, step=1, label="Poisson iters", | |
info="The power of blending with the original image, helps to recover more details. Not included in the article, disabled by default.") | |
poisson_erosion = gr.Slider(1, 100, value=15, step=1, label="Poisson erosion", | |
info="Smooths out the blending area.") | |
align = gr.CheckboxGroup(["Face", "Shape", "Color"], value=["Face", "Shape", "Color"], | |
label="Image cropping [recommended]", | |
info="Selects which images to crop by face") | |
btn = gr.Button("Get the haircut") | |
with gr.Column(): | |
output = gr.Image(label="Your result") | |
error_message = gr.Textbox(label="β οΈ Error β οΈ", visible=False, elem_classes="error-message") | |
gr.Examples(examples=[["input/0.png", "input/1.png", "input/2.png"], ["input/6.png", "input/7.png", None], | |
["input/10.jpg", None, "input/11.jpg"]], | |
inputs=[source, shape, color], outputs=output) | |
source.upload(fn=resize('Face'), inputs=[source, align], outputs=source) | |
shape.upload(fn=resize('Shape'), inputs=[shape, align], outputs=shape) | |
color.upload(fn=resize('Color'), inputs=[color, align], outputs=color) | |
btn.click(fn=swap_hair, inputs=[source, shape, color, blending, poisson_iters, poisson_erosion], | |
outputs=[output, error_message]) | |
gr.Markdown('''To cite the paper by the authors | |
``` | |
@article{nikolaev2024hairfastgan, | |
title={HairFastGAN: Realistic and Robust Hair Transfer with a Fast Encoder-Based Approach}, | |
author={Nikolaev, Maxim and Kuznetsov, Mikhail and Vetrov, Dmitry and Alanov, Aibek}, | |
journal={arXiv preprint arXiv:2404.01094}, | |
year={2024} | |
} | |
``` | |
''') | |
return demo | |
if __name__ == '__main__': | |
align_cache = LRUCache(maxsize=10) | |
demo = get_demo() | |
demo.launch(server_name="0.0.0.0", server_port=7860) | |