Spaces:
Runtime error
Runtime error
import os | |
import numpy as np | |
from PIL import Image, ImageDraw | |
from modules import shared, processing | |
from modules.face_restoration import FaceRestoration | |
class YoLoResult: | |
def __init__(self, score: float, box: list[int], mask: Image.Image = None, size: float = 0): | |
self.score = score | |
self.box = box | |
self.mask = mask | |
self.size = size | |
class FaceRestorerYolo(FaceRestoration): | |
def name(self): | |
return "Face HiRes" | |
def __init__(self): | |
from modules import paths | |
self.model = None | |
self.model_dir = os.path.join(paths.models_path, 'yolo') | |
self.model_name = 'yolov8n-face.pt' | |
self.model_url = 'https://github.com/akanametov/yolov8-face/releases/download/v0.0.0/yolov8n-face.pt' | |
# self.model_name = 'yolov9-c-face.pt' | |
# self.model_url = 'https://github.com/akanametov/yolov9-face/releases/download/1.0/yolov9-c-face.pt' | |
def dependencies(self): | |
import installer | |
installer.install('ultralytics', ignore=False) | |
def predict( | |
self, | |
image: Image.Image, | |
offload: bool = False, | |
conf: float = 0.5, | |
iou: float = 0.5, | |
imgsz: int = 640, | |
half: bool = True, | |
device = 'cuda', | |
n: int = 5, | |
augment: bool = True, | |
agnostic: bool = False, | |
retina: bool = False, | |
mask: bool = True, | |
) -> list[YoLoResult]: | |
self.model.to(device) | |
predictions = self.model.predict( | |
source=[image], | |
stream=False, | |
verbose=False, | |
conf=conf, | |
iou=iou, | |
imgsz=imgsz, | |
half=half, | |
device=device, | |
max_det=n, | |
augment=augment, | |
agnostic_nms=agnostic, | |
retina_masks=retina, | |
) | |
if offload: | |
self.model.to('cpu') | |
result = [] | |
for prediction in predictions: | |
boxes = prediction.boxes.xyxy.detach().int().cpu().numpy() if prediction.boxes is not None else [] | |
scores = prediction.boxes.conf.detach().float().cpu().numpy() if prediction.boxes is not None else [] | |
for score, box in zip(scores, boxes): | |
box = box.tolist() | |
mask_image = None | |
size = (box[2] - box[0]) * (box[3] - box[1]) / (image.width * image.height) | |
if mask: | |
mask_image = image.copy() | |
mask_image = Image.new('L', image.size, 0) | |
draw = ImageDraw.Draw(mask_image) | |
draw.rectangle(box, fill="white", outline=None, width=0) | |
result.append(YoLoResult(score=score, box=box, mask=mask_image, size=size)) | |
return result | |
def load(self): | |
from modules import modelloader | |
self.dependencies() | |
if self.model is None: | |
model_file = modelloader.load_file_from_url(url=self.model_url, model_dir=self.model_dir, file_name=self.model_name) | |
if model_file is not None: | |
shared.log.info(f'Loading: type=FaceHires model={model_file}') | |
from ultralytics import YOLO # pylint: disable=import-outside-toplevel | |
self.model = YOLO(model_file) | |
def restore(self, np_image, p: processing.StableDiffusionProcessing = None): | |
from modules import devices, processing_class | |
if not hasattr(p, 'facehires'): | |
p.facehires = 0 | |
if np_image is None or p.facehires >= p.batch_size * p.n_iter: | |
return np_image | |
self.load() | |
if self.model is None: | |
shared.log.error(f"Model load: type=FaceHires model='{self.model_name}' dir={self.model_dir} url={self.model_url}") | |
return np_image | |
image = Image.fromarray(np_image) | |
faces = self.predict(image, mask=True, device=devices.device, offload=shared.opts.face_restoration_unload) | |
if len(faces) == 0: | |
return np_image | |
# create backups | |
orig_apply_overlay = shared.opts.mask_apply_overlay | |
orig_p = p.__dict__.copy() | |
orig_cls = p.__class__ | |
pp = None | |
shared.opts.data['mask_apply_overlay'] = True | |
args = { | |
'batch_size': 1, | |
'n_iter': 1, | |
'inpaint_full_res': True, | |
'inpainting_mask_invert': 0, | |
'inpainting_fill': 1, # no fill | |
'sampler_name': orig_p.get('hr_sampler_name', 'default'), | |
'steps': orig_p.get('hr_second_pass_steps', 0), | |
'negative_prompt': orig_p.get('refiner_negative', ''), | |
'denoising_strength': shared.opts.facehires_strength if shared.opts.facehires_strength > 0 else orig_p.get('denoising_strength', 0.3), | |
'styles': [], | |
'prompt': orig_p.get('refiner_prompt', ''), | |
# TODO facehires expose as tunable | |
'mask_blur': 10, | |
'inpaint_full_res_padding': 15, | |
'restore_faces': True, | |
} | |
p = processing_class.switch_class(p, processing.StableDiffusionProcessingImg2Img, args) | |
p.facehires += 1 # set flag to avoid recursion | |
if p.steps < 1: | |
p.steps = orig_p.get('steps', 0) | |
if len(p.prompt) == 0: | |
p.prompt = orig_p.get('all_prompts', [''])[0] | |
if len(p.negative_prompt) == 0: | |
p.negative_prompt = orig_p.get('all_negative_prompts', [''])[0] | |
shared.log.debug(f'Face HiRes: faces={[f.__dict__ for f in faces]} strength={p.denoising_strength} blur={p.mask_blur} padding={p.inpaint_full_res_padding} steps={p.steps}') | |
for face in faces: | |
if face.mask is None: | |
continue | |
if face.size < 0.0002 or face.size > 0.8: | |
shared.log.debug(f'Face HiRes skip: {face.__dict__}') | |
continue | |
p.init_images = [image] | |
p.image_mask = [face.mask] | |
p.recursion = True | |
pp = processing.process_images_inner(p) | |
del p.recursion | |
p.overlay_images = None # skip applying overlay twice | |
if pp is not None and pp.images is not None and len(pp.images) > 0: | |
image = pp.images[0] # update image to be reused for next face | |
# restore pipeline | |
p = processing_class.switch_class(p, orig_cls, orig_p) | |
p.init_images = getattr(orig_p, 'init_images', None) | |
p.image_mask = getattr(orig_p, 'image_mask', None) | |
shared.opts.data['mask_apply_overlay'] = orig_apply_overlay | |
np_image = np.array(image) | |
# shared.log.debug(f'Face HiRes complete: faces={len(faces)} time={t1-t0:.3f}') | |
return np_image | |
yolo = FaceRestorerYolo() | |
shared.face_restorers.append(yolo) | |