from typing import TYPE_CHECKING import os import re import threading from copy import copy import numpy as np import gradio as gr from PIL import Image, ImageDraw from modules import shared, processing, devices, processing_class, ui_common, ui_components, ui_symbols, images, extra_networks, sd_models from modules.detailer import Detailer predefined = [ # 'https://github.com/ultralytics/assets/releases/download/v8.3.0/yolo11m.pt', 'https://huggingface.co/vladmandic/yolo-detailers/resolve/main/face-yolo8n.pt', 'https://huggingface.co/vladmandic/yolo-detailers/resolve/main/face-yolo8m.pt', 'https://huggingface.co/vladmandic/yolo-detailers/resolve/main/hand_yolov8n.pt', 'https://huggingface.co/vladmandic/yolo-detailers/resolve/main/person_yolov8n-seg.pt', 'https://huggingface.co/vladmandic/yolo-detailers/resolve/main/eyes-v1.pt', 'https://huggingface.co/vladmandic/yolo-detailers/resolve/main/eyes-full-v1.pt', 'https://huggingface.co/vladmandic/yolo-detailers/resolve/main/anzhc-eyes-seg.pt', 'https://huggingface.co/vladmandic/yolo-detailers/resolve/main/anzhc-face-1024-seg-8n.pt', 'https://huggingface.co/vladmandic/yolo-detailers/resolve/main/anzhc-head-seg-8n.pt', 'https://huggingface.co/netrunner-exe/Face-Upscalers-onnx/resolve/main/codeformer.fp16.onnx', 'https://huggingface.co/netrunner-exe/Face-Upscalers-onnx/resolve/main/restoreformer.fp16.onnx', 'https://huggingface.co/netrunner-exe/Face-Upscalers-onnx/resolve/main/GFPGANv1.4.fp16.onnx', 'https://huggingface.co/netrunner-exe/Face-Upscalers-onnx/resolve/main/GPEN-BFR-512.fp16.onnx', ] load_lock = threading.Lock() class YoloResult: def __init__(self, cls: int, label: str, score: float, box: list[int], mask: Image.Image = None, item: Image.Image = None, width = 0, height = 0, args = {}): self.cls = cls self.label = label self.score = score self.box = box self.mask = mask self.item = item self.width = width self.height = height self.args = args def __repl__(self): return f'cls={self.cls} label={self.label} score={self.score} box={self.box} mask={self.mask} item={self.item} size={self.width}x{self.height} args={self.args}' class YoloRestorer(Detailer): def __init__(self): super().__init__() self.models = {} # cache loaded models self.list = {} self.ui_mode = True self.cmd_dir = shared.opts.yolo_dir self.enumerate() def name(self): return "Detailer" def enumerate(self): self.list.clear() files = [] downloaded = 0 for m in predefined: name = os.path.splitext(os.path.basename(m))[0] self.list[name] = m files.append(name) if os.path.exists(shared.opts.yolo_dir): for f in os.listdir(shared.opts.yolo_dir): if f.endswith('.pt'): downloaded += 1 name = os.path.splitext(os.path.basename(f))[0] if name not in files: self.list[name] = os.path.join(shared.opts.yolo_dir, f) shared.log.info(f'Available Detailer: path="{shared.opts.yolo_dir}" items={len(list(self.list))} downloaded={downloaded}') return list(self.list) def dependencies(self): import installer installer.install('ultralytics==8.3.40', ignore=True, quiet=True) def predict( self, model, image: Image.Image, imgsz: int = 640, half: bool = True, device = devices.device, augment: bool = shared.opts.detailer_augment, agnostic: bool = False, retina: bool = False, mask: bool = True, offload: bool = shared.opts.detailer_unload, ) -> list[YoloResult]: if model is None or (isinstance(model, str) and len(model) == 0): model = 'yolo11m' result = [] if isinstance(model, str): cached = self.models.get(model, None) if cached is None: _, model = self.load(model) else: model = cached if model is None: return result args = { 'conf': shared.opts.detailer_conf, 'iou': shared.opts.detailer_iou, # 'max_det': shared.opts.detailer_max, } try: if TYPE_CHECKING: from ultralytics import YOLO # pylint: disable=import-outside-toplevel, unused-import model: YOLO = model.to(device) predictions = model.predict( source=[image], stream=False, verbose=False, imgsz=imgsz, half=half, device=device, augment=augment, agnostic_nms=agnostic, retina_masks=retina, **args ) if offload: model.to('cpu') except Exception as e: shared.log.error(f'Detailer predict: {e}') return result desired = shared.opts.detailer_classes.split(',') desired = [d.lower().strip() for d in desired] desired = [d for d in desired if len(d) > 0] 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 [] classes = prediction.boxes.cls.detach().float().cpu().numpy() if prediction.boxes is not None else [] masks = prediction.masks.data.cpu().float().numpy() if prediction.masks is not None else [] if len(masks) < len(classes): masks = len(classes) * [None] for score, box, cls, seg in zip(scores, boxes, classes, masks): if seg is not None: try: seg = (255 * seg).astype(np.uint8) seg = Image.fromarray(seg).resize(image.size).convert('L') except Exception: seg = None cls = int(cls) label = prediction.names[cls] if cls < len(prediction.names) else f'cls{cls}' if len(desired) > 0 and label.lower() not in desired: continue box = box.tolist() w, h = box[2] - box[0], box[3] - box[1] x_size, y_size = w/image.width, h/image.height min_size = shared.opts.detailer_min_size if shared.opts.detailer_min_size >= 0 and shared.opts.detailer_min_size <= 1 else 0 max_size = shared.opts.detailer_max_size if shared.opts.detailer_max_size >= 0 and shared.opts.detailer_max_size <= 1 else 1 if x_size >= min_size and y_size >=min_size and x_size <= max_size and y_size <= max_size: if mask: if shared.opts.detailer_seg and seg is not None: masked = seg else: masked = Image.new('L', image.size, 0) draw = ImageDraw.Draw(masked) draw.rectangle(box, fill="white", outline=None, width=0) cropped = image.crop(box) res = YoloResult( cls=cls, label=label, score=round(score, 2), box=box, mask=masked, item=cropped, width=w, height=h, args=args, ) result.append(res) if len(result) >= shared.opts.detailer_max: break return result def load(self, model_name: str = None): with load_lock: from modules import modelloader model = None if model_name is None: model_name = list(self.list)[0] if model_name in self.models: return model_name, self.models[model_name] else: model_url = self.list.get(model_name, None) if model_url is None: shared.log.error(f'Load: type=Detailer name="{model_name}" error="model not found"') return None, None file_name = os.path.basename(model_url) model_file = None try: model_file = modelloader.load_file_from_url(url=model_url, model_dir=shared.opts.yolo_dir, file_name=file_name) if model_file is None: shared.log.error(f'Load: type=Detailer name="{model_name}" url="{model_url}" error="failed to fetch model"') elif model_file.endswith('.onnx'): import onnxruntime as ort options = ort.SessionOptions() # options.graph_optimization_level = onnxruntime.GraphOptimizationLevel.ORT_ENABLE_ALL session = ort.InferenceSession(model_file, sess_options=options, providers=devices.onnx) self.models[model_name] = session return model_name, session else: self.dependencies() import ultralytics model = ultralytics.YOLO(model_file) classes = list(model.names.values()) shared.log.info(f'Load: type=Detailer name="{model_name}" model="{model_file}" ultralytics={ultralytics.__version__} classes={classes}') self.models[model_name] = model return model_name, model except Exception as e: shared.log.error(f'Load: type=Detailer name="{model_name}" error="{e}"') return None, None def merge(self, items: list[YoloResult]) -> list[YoloResult]: if items is None or len(items) == 0: return None box=[min(item.box[0] for item in items), min(item.box[1] for item in items), max(item.box[2] for item in items), max(item.box[3] for item in items)] mask = Image.new('L', items[0].mask.size, 0) for item in items: mask = Image.fromarray(np.maximum(np.array(mask), np.array(item.mask))) merged = YoloResult( cls=items[0].cls, label=items[0].label, score=sum(item.score for item in items) / len(items), box=box, mask=mask, item=None, width=box[2] - box[0], height=box[3] - box[1], ) return [merged] def draw_masks(self, image: Image.Image, items: list[YoloResult]) -> Image.Image: if not isinstance(image, Image.Image): image = Image.fromarray(image) image = image.convert('RGBA') size = min(image.width, image.height) // 32 font = images.get_font(size) color = (0, 190, 190) shared.log.debug(f'Detailer: draw={items}') for i, item in enumerate(items): if shared.opts.detailer_seg and item.mask is not None: mask = item.mask.convert('L') else: mask = Image.new('L', image.size, 0) draw_mask = ImageDraw.Draw(mask) draw_mask.rectangle(item.box, fill="white", outline=None, width=0) alpha = mask.point(lambda p: int(p * 0.5)) overlay = Image.new("RGBA", image.size, color + (0,)) overlay.putalpha(alpha) image = Image.alpha_composite(image, overlay) draw_text = ImageDraw.Draw(image) draw_text.text((item.box[0] + 2, item.box[1] - size - 2), f'{i+1} {item.label} {item.score:.2f}', fill="black", font=font) draw_text.text((item.box[0] + 0, item.box[1] - size - 4), f'{i+1} {item.label} {item.score:.2f}', fill="white", font=font) image = image.convert("RGB") return np.array(image) def restore(self, np_image, p: processing.StableDiffusionProcessing = None): if shared.state.interrupted or shared.state.skipped: return np_image if hasattr(p, 'recursion'): return np_image if not hasattr(p, 'detailer_active'): p.detailer_active = 0 if np_image is None or p.detailer_active >= p.batch_size * p.n_iter: return np_image shared.sd_model = sd_models.set_diffuser_pipe(shared.sd_model, sd_models.DiffusersTaskType.INPAINTING) if (sd_models.get_diffusers_task(shared.sd_model) != sd_models.DiffusersTaskType.INPAINTING) and (shared.sd_model.__class__.__name__ not in sd_models.pipe_switch_task_exclude): shared.log.error(f'Detailer: model="{shared.sd_model.__class__.__name__}" not compatible') return np_image models = [] if len(shared.opts.detailer_args) > 0: models = [m.strip() for m in re.split(r'[\n,;]+', shared.opts.detailer_args)] models = [m for m in models if len(m) > 0] if len(models) == 0: models = shared.opts.detailer_models if len(models) == 0: shared.log.warning('Detailer: model=None') return np_image shared.log.debug(f'Detailer: models={models}') # create backups orig_apply_overlay = shared.opts.mask_apply_overlay orig_p = p.__dict__.copy() orig_cls = p.__class__ models_used = [] np_images = [] annotated = Image.fromarray(np_image) image = None for i, model_val in enumerate(models): if ':' in model_val: model_name, model_args = model_val.split(':', 1) else: model_name, model_args = model_val, '' model_args = [m.strip() for m in model_args.split(':')] model_args = {k.strip(): v.strip() for k, v in (arg.split('=') for arg in model_args if '=' in arg)} name, model = self.load(model_name) if model is None: shared.log.warning(f'Detailer: model="{name}" not loaded') continue if name.endswith('.fp16'): # run gfpgan or codeformer directly and skip detailer processing from modules.postprocess import restorer np_image = restorer.restore(np_image, name, model, p.detailer_strength) image = Image.fromarray(np_image) continue if image is None: image = Image.fromarray(np_image) items = self.predict(model, image) if len(items) == 0: shared.log.info(f'Detailer: model="{name}" no items detected') continue if shared.opts.detailer_merge and len(items) > 1: shared.log.debug(f'Detailer: model="{name}" items={len(items)} merge') items = self.merge(items) shared.opts.data['mask_apply_overlay'] = True orig_prompt: str = orig_p.get('all_prompts', [''])[0] orig_negative: str = orig_p.get('all_negative_prompts', [''])[0] prompt: str = orig_p.get('detailer_prompt', '') negative: str = orig_p.get('detailer_negative', '') if prompt is None or len(prompt) == 0: prompt = orig_prompt else: prompt = prompt.replace('[PROMPT]', orig_prompt) prompt = prompt.replace('[prompt]', orig_prompt) if len(negative) == 0: negative = orig_negative else: negative = negative.replace('[PROMPT]', orig_negative) negative = negative.replace('[prompt]', orig_negative) prompt_lines = 99 * [p.strip() for p in prompt.split('\n')] negative_lines = 99 * [n.strip() for n in negative.split('\n')] args = { 'detailer': True, 'batch_size': 1, 'n_iter': 1, 'prompt': prompt, 'negative_prompt': negative, 'denoising_strength': p.detailer_strength, 'sampler_name': orig_p.get('hr_sampler_name', 'default'), 'steps': p.detailer_steps, 'styles': [], 'inpaint_full_res': True, 'inpainting_mask_invert': 0, 'mask_blur': shared.opts.detailer_blur, 'inpaint_full_res_padding': shared.opts.detailer_padding, 'width': p.detailer_resolution, 'height': p.detailer_resolution, 'vae_type': orig_p.get('vae_type', 'Full'), } args.update(model_args) if args['denoising_strength'] == 0: shared.log.debug(f'Detailer: model="{name}" strength=0 skip') return np_image control_pipeline = None orig_class = shared.sd_model.__class__ if getattr(p, 'is_control', False): from modules.control import run control_pipeline = shared.sd_model run.restore_pipeline() p = processing_class.switch_class(p, processing.StableDiffusionProcessingImg2Img, args) if hasattr(shared.sd_model, 'restore_pipeline'): shared.sd_model.restore_pipeline() p.detailer_active += 1 # set flag to avoid recursion if p.steps < 1: p.steps = orig_p.get('steps', 0) # report = [{'label': i.label, 'score': i.score, 'size': f'{i.width}x{i.height}' } for i in items] # shared.log.info(f'Detailer: model="{name}" items={report} args={args}') models_used.append(name) mask_all = [] p.state = '' pc = copy(p) pc.ops.append('detailer') orig_sigma_adjust: float = shared.opts.schedulers_sigma_adjust orig_sigma_end: float = shared.opts.schedulers_sigma_adjust_max shared.opts.schedulers_sigma_adjust = shared.opts.detailer_sigma_adjust shared.opts.schedulers_sigma_adjust_max = shared.opts.detailer_sigma_adjust_max if shared.opts.detailer_sort: items = sorted(items, key=lambda x: x.box[0]) # sort items left-to-right to improve consistency if shared.opts.detailer_save: annotated = self.draw_masks(annotated, items) for j, item in enumerate(items): if item.mask is None: continue pc.keep_prompts = True shared.sd_model.fail_on_switch_error = True pc.prompt = prompt_lines[i*len(items)+j] pc.negative_prompt = negative_lines[i*len(items)+j] pc.prompts = [pc.prompt] pc.negative_prompts = [pc.negative_prompt] pc.prompts, pc.network_data = extra_networks.parse_prompts(pc.prompts) extra_networks.activate(pc, pc.network_data) shared.log.debug(f'Detail: model="{i+1}:{name}" item={j+1}/{len(items)} box={item.box} label="{item.label}" score={item.score:.2f} seg={shared.opts.detailer_seg} prompt="{pc.prompt}"') pc.init_images = [image] pc.image_mask = [item.mask] pc.overlay_images = [] # explictly disable for detailer pass pc.enable_hr = False pc.do_not_save_samples = True pc.do_not_save_grid = True # set recursion flag to avoid nested detailer calls pc.recursion = True # process jobid = shared.state.begin('Detailer') pp = processing.process_images_inner(pc) extra_networks.deactivate(pc, force=True) shared.sd_model.fail_on_switch_error = False shared.state.end(jobid) del pc.recursion 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 item if len(pp.images) > 1: mask_all.append(pp.images[1]) shared.opts.schedulers_sigma_adjust = orig_sigma_adjust shared.opts.schedulers_sigma_adjust_max = orig_sigma_end # restore pipeline if control_pipeline is not None: shared.sd_model = control_pipeline else: shared.sd_model.__class__ = orig_class p = processing_class.switch_class(p, orig_cls, orig_p) p.init_images = orig_p.get('init_images', None) p.image_mask = orig_p.get('image_mask', None) p.state = orig_p.get('state', None) p.ops = orig_p.get('ops', []) shared.opts.data['mask_apply_overlay'] = orig_apply_overlay if len(mask_all) > 0 and shared.opts.include_mask: from modules.control.util import blend p.image_mask = blend([np.array(m) for m in mask_all]) p.image_mask = Image.fromarray(p.image_mask) if image is not None: np_images.append(np.array(image)) if shared.opts.detailer_save and annotated is not None: np_images.append(annotated) # save debug image with boxes return np_images def change_mode(self, dropdown, text): self.ui_mode = not self.ui_mode if self.ui_mode: value = [val.split(':')[0].strip() for val in text.split(',')] return gr.update(visible=True, value=value), gr.update(visible=False), gr.update(visible=True) else: value = ', '.join(dropdown) return gr.update(visible=False), gr.update(visible=True, value=value), gr.update(visible=False) def ui(self, tab: str): def ui_settings_change(merge, detailers, text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg): shared.opts.detailer_merge = merge shared.opts.detailer_models = detailers shared.opts.detailer_args = text if not self.ui_mode else '' shared.opts.detailer_classes = classes shared.opts.detailer_padding = padding shared.opts.detailer_blur = blur shared.opts.detailer_conf = min_confidence shared.opts.detailer_max = max_detected shared.opts.detailer_min_size = min_size shared.opts.detailer_max_size = max_size shared.opts.detailer_iou = iou shared.opts.detailer_sigma_adjust = renoise_value shared.opts.detailer_sigma_adjust_max = renoise_end shared.opts.detailer_save = save shared.opts.detailer_sort = sort shared.opts.detailer_seg = seg # shared.opts.detailer_resolution = resolution shared.opts.save(silent=True) shared.log.debug(f'Detailer settings: models={detailers} classes={classes} strength={strength} conf={min_confidence} max={max_detected} iou={iou} size={min_size}-{max_size} padding={padding} steps={steps} resolution={resolution} save={save} sort={sort} seg={seg}') if not self.ui_mode: shared.log.debug(f'Detailer expert: {text}') with gr.Accordion(open=False, label="Detailer", elem_id=f"{tab}_detailer_accordion", elem_classes=["small-accordion"]): with gr.Row(): enabled = gr.Checkbox(label="Enable detailer pass", elem_id=f"{tab}_detailer_enabled", value=False) with gr.Row(): seg = gr.Checkbox(label="Use segmentation", elem_id=f"{tab}_detailer_seg", value=shared.opts.detailer_seg, visible=True) save = gr.Checkbox(label="Include detections", elem_id=f"{tab}_detailer_save", value=shared.opts.detailer_save, visible=True) with gr.Row(): merge = gr.Checkbox(label="Merge detailers", elem_id=f"{tab}_detailer_merge", value=shared.opts.detailer_merge, visible=True) sort = gr.Checkbox(label="Sort detections", elem_id=f"{tab}_detailer_sort", value=shared.opts.detailer_sort, visible=True) with gr.Row(): detailers = gr.Dropdown(label="Detailer models", elem_id=f"{tab}_detailers", choices=list(self.list), value=shared.opts.detailer_models, multiselect=True, visible=True) detailers_text = gr.Textbox(label="Detailer list", elem_id=f"{tab}_detailers_text", placeholder="Comma separated list of detailer models", lines=2, visible=False, interactive=True) refresh_btn = ui_common.create_refresh_button(detailers, self.enumerate, lambda: {"choices": self.enumerate()}, 'yolo_models_refresh') ui_mode = ui_components.ToolButton(value=ui_symbols.view, elem_id=f'{tab}_yolo_models_list') ui_mode.click(fn=self.change_mode, inputs=[detailers, detailers_text], outputs=[detailers, detailers_text, refresh_btn]) with gr.Row(): classes = gr.Textbox(label="Detailer classes", placeholder="Classes", elem_id=f"{tab}_detailer_classes") with gr.Row(): prompt = gr.Textbox(label="Detailer prompt", value='', placeholder='detailer prompt or leave empty to use main prompt', lines=2, elem_id=f"{tab}_detailer_prompt", elem_classes=["prompt"]) with gr.Row(): negative = gr.Textbox(label="Detailer negative prompt", value='', placeholder='detailer prompt or leave empty to use main prompt', lines=2, elem_id=f"{tab}_detailer_negative", elem_classes=["prompt"]) with gr.Row(): steps = gr.Slider(label="Detailer steps", elem_id=f"{tab}_detailer_steps", value=10, minimum=0, maximum=99, step=1) strength = gr.Slider(label="Detailer strength", elem_id=f"{tab}_detailer_strength", value=0.3, minimum=0, maximum=1, step=0.01) with gr.Row(): resolution = gr.Slider(label="Detailer resolution", elem_id=f"{tab}_detailer_resolution", value=1024, minimum=256, maximum=4096, step=8) max_detected = gr.Slider(label="Max detected", elem_id=f"{tab}_detailer_max", value=shared.opts.detailer_max, minimum=1, maximum=10, step=1) with gr.Row(): padding = gr.Slider(label="Edge padding", elem_id=f"{tab}_detailer_padding", value=shared.opts.detailer_padding, minimum=0, maximum=100, step=1) blur = gr.Slider(label="Edge blur", elem_id=f"{tab}_detailer_blur", value=shared.opts.detailer_blur, minimum=0, maximum=100, step=1) with gr.Row(): min_confidence = gr.Slider(label="Min confidence", elem_id=f"{tab}_detailer_conf", value=shared.opts.detailer_conf, minimum=0.0, maximum=1.0, step=0.05) iou = gr.Slider(label="Max overlap", elem_id=f"{tab}_detailer_iou", value=shared.opts.detailer_iou, minimum=0, maximum=1.0, step=0.05) with gr.Row(): min_size = shared.opts.detailer_min_size if shared.opts.detailer_min_size < 1 else 0.0 min_size = gr.Slider(label="Min size", elem_id=f"{tab}_detailer_min_size", value=min_size, minimum=0.0, maximum=1.0, step=0.05) max_size = shared.opts.detailer_max_size if shared.opts.detailer_max_size < 1 and shared.opts.detailer_max_size > 0 else 1.0 max_size = gr.Slider(label="Max size", elem_id=f"{tab}_detailer_max_size", value=max_size, minimum=0.0, maximum=1.0, step=0.05) with gr.Row(elem_classes=['flex-break']): renoise_value = gr.Slider(minimum=0.5, maximum=1.5, step=0.01, label='Renoise', value=shared.opts.detailer_sigma_adjust, elem_id=f"{tab}_detailer_renoise") renoise_end = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, label='Renoise end', value=shared.opts.detailer_sigma_adjust_max, elem_id=f"{tab}_detailer_renoise_end") merge.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[]) detailers.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[]) detailers_text.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[]) classes.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[]) padding.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[]) blur.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[]) min_confidence.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[]) max_detected.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[]) min_size.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[]) max_size.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[]) iou.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[]) resolution.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[]) save.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[]) sort.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[]) seg.change(fn=ui_settings_change, inputs=[merge, detailers, detailers_text, classes, strength, padding, blur, min_confidence, max_detected, min_size, max_size, iou, steps, renoise_value, renoise_end, resolution, save, sort, seg], outputs=[]) return enabled, prompt, negative, steps, strength, resolution def initialize(): shared.yolo = YoloRestorer() shared.detailers.append(shared.yolo)