mirror of
https://github.com/vladmandic/automatic
synced 2026-09-11 07:18:44 +02:00
f9ab0bf04d
Surface YoloRestorer.restore() as a standalone operation: a Detailer
postprocessing script in the Process tab and a thin /sdapi/v1/detail
endpoint, neither requiring a base generation pass.
- modules/postprocess/yolo.py: YoloRestorer.make_processing() builds the
synthetic Img2Img processing object both entry points feed to restore(),
resolving the seed so the inpaint passes are reproducible
- modules/api/process.py: post_detail handler exposes the full detailer
parameter set and returns the detailed image plus optional annotations
as base64
- scripts/postprocessing_detailer.py: reuses shared.yolo.ui('extras') and
runs through make_processing()
- modules/postprocessing.py: run_extras takes a per-script script_args
dict, also letting the extras API drive other scripts such as Remove
background; omitting it leaves existing callers unchanged
- modules/api/models.py: ReqDetail / ResDetail
- modules/processing_info.py: guard create_infotext's Image/Hires CFG
reporting against an unset (None) cfg_image, matching the is-not-None
checks the other cfg_image readers use; the detailer inpaint pass runs
with it unset
- test/test-detailer-api.py: covers both paths; effect tests measure the
diff inside the detected region with extreme isolated parameter values,
and the suite disables model quantization for the run and restores the
original settings afterward
652 lines
36 KiB
Python
652 lines
36 KiB
Python
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.logger import log
|
|
from modules import shared, processing, devices, processing_class, ui_common, ui_components, ui_symbols, images, extra_networks, sd_models
|
|
from modules.detailer import Detailer
|
|
|
|
|
|
def detailer_opt(p, attr, opts_attr=None):
|
|
"""Read detailer param from processing object if set, otherwise fall back to shared.opts."""
|
|
if p is not None:
|
|
val = getattr(p, attr, None)
|
|
if val is not None:
|
|
return val
|
|
return getattr(shared.opts, opts_attr or attr, None)
|
|
|
|
|
|
predefined = [ # <https://huggingface.co/vladmandic/yolo-detailers/tree/main>
|
|
'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',
|
|
]
|
|
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 = None):
|
|
if args is None:
|
|
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)
|
|
log.info(f'Available Detailer: path="{shared.opts.yolo_dir}" items={len(list(self.list))} downloaded={downloaded}')
|
|
return list(self.list)
|
|
|
|
def dependencies(self):
|
|
from installer import install
|
|
install('ultralytics==8.3.40', ignore=True, quiet=True)
|
|
install('omegaconf')
|
|
install('antlr4-python3-runtime')
|
|
|
|
def predict(
|
|
self,
|
|
model,
|
|
image: Image.Image,
|
|
imgsz: int = 640,
|
|
half: bool = True,
|
|
device = devices.device,
|
|
agnostic: bool = False,
|
|
retina: bool = False,
|
|
mask: bool = True,
|
|
augment: bool | None = None,
|
|
offload: bool | None = None,
|
|
p = None,
|
|
) -> list[YoloResult]:
|
|
if augment is None:
|
|
augment = detailer_opt(p, 'detailer_augment')
|
|
if offload is None:
|
|
offload = shared.opts.detailer_unload
|
|
|
|
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': detailer_opt(p, 'detailer_conf'),
|
|
'iou': detailer_opt(p, 'detailer_iou'),
|
|
# 'max_det': detailer_opt(p, '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:
|
|
log.error(f'Detailer predict: {e}')
|
|
return result
|
|
|
|
classes = detailer_opt(p, 'detailer_classes') or ''
|
|
desired = 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, strict=False):
|
|
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
|
|
opt_min = detailer_opt(p, 'detailer_min_size') or 0
|
|
opt_max = detailer_opt(p, 'detailer_max_size') or 1
|
|
min_size = opt_min if 0 <= opt_min <= 1 else 0
|
|
max_size = opt_max if 0 < opt_max <= 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 detailer_opt(p, 'detailer_segmentation') 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) >= (detailer_opt(p, 'detailer_max') or 2):
|
|
break
|
|
return result
|
|
|
|
def load(self, model_name: str | None = 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:
|
|
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:
|
|
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())
|
|
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:
|
|
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], p=None) -> 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)
|
|
log.debug(f'Detailer: draw={items}')
|
|
for i, item in enumerate(items):
|
|
if detailer_opt(p, 'detailer_segmentation') 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):
|
|
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 = detailer_opt(p, 'detailer_models') or []
|
|
if len(models) == 0:
|
|
log.warning('Detailer: model=None')
|
|
return np_image
|
|
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:
|
|
log.warning(f'Detailer: model="{name}" not loaded')
|
|
continue
|
|
|
|
if image is None:
|
|
image = Image.fromarray(np_image)
|
|
items = self.predict(model, image, p=p)
|
|
|
|
if len(items) == 0:
|
|
log.info(f'Detailer: model="{name}" no items detected')
|
|
continue
|
|
|
|
if detailer_opt(p, 'detailer_merge') and len(items) > 1:
|
|
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': detailer_opt(p, 'detailer_blur'),
|
|
'inpaint_full_res_padding': detailer_opt(p, '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:
|
|
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]
|
|
# 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 = detailer_opt(p, 'detailer_sigma_adjust')
|
|
shared.opts.schedulers_sigma_adjust_max = detailer_opt(p, 'detailer_sigma_adjust_max')
|
|
|
|
if detailer_opt(p, 'detailer_sort'):
|
|
items = sorted(items, key=lambda x: x.box[0]) # sort items left-to-right to improve consistency
|
|
if detailer_opt(p, 'detailer_include_detections', 'detailer_save'):
|
|
annotated = self.draw_masks(annotated, items, p=p)
|
|
|
|
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, pc.network_data)
|
|
pc.disable_extra_networks = True # disable processing_diffusers from handling network activation since its handled here
|
|
network_same = len(p.network_data.values()) == len(pc.network_data.values()) and all(x == y for x, y in zip(p.network_data.values(), pc.network_data.values()))
|
|
if not network_same:
|
|
extra_networks.activate(pc, pc.network_data)
|
|
log.debug(f'Detail: model="{i+1}:{name}" item={j+1}/{len(items)} box={item.box} label="{item.label}" score={item.score:.2f} seg={detailer_opt(p, "detailer_segmentation")} network={network_same} 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)
|
|
if not network_same:
|
|
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 detailer_opt(p, 'detailer_include_detections', 'detailer_save') and annotated is not None:
|
|
np_images.append(annotated) # save debug image with boxes
|
|
return np_images
|
|
|
|
def make_processing(self, image, prompt='', negative='', steps=10, strength=0.3, resolution=1024, seed=-1, overrides=None):
|
|
"""Build a synthetic Img2Img processing object to run restore() standalone, with no base generation pass.
|
|
|
|
The primary params map to the detailer_* fields restore() reads directly. overrides is an optional
|
|
dict of the remaining detailer_* settings; None values are skipped and fall through to shared.opts
|
|
via detailer_opt(). The seed is resolved here so restore()'s inpaint passes are reproducible and the
|
|
effective value can be reported back.
|
|
"""
|
|
from modules.processing_helpers import get_fixed_seed
|
|
from modules.paths import resolve_output_path
|
|
seed = int(get_fixed_seed(seed))
|
|
outpath = resolve_output_path(shared.opts.outdir_samples, shared.opts.outdir_extras_samples)
|
|
p = processing.StableDiffusionProcessingImg2Img(
|
|
sd_model=shared.sd_model,
|
|
prompt=prompt or '',
|
|
negative_prompt=negative or '',
|
|
init_images=[image],
|
|
outpath_samples=outpath,
|
|
outpath_grids=outpath,
|
|
batch_size=1,
|
|
n_iter=1,
|
|
seed=seed,
|
|
width=image.width,
|
|
height=image.height,
|
|
detailer_enabled=True,
|
|
detailer_prompt=prompt or '',
|
|
detailer_negative=negative or '',
|
|
detailer_steps=steps,
|
|
detailer_strength=strength,
|
|
detailer_resolution=resolution,
|
|
)
|
|
for attr, val in (overrides or {}).items():
|
|
if val is not None:
|
|
setattr(p, attr, val)
|
|
# restore() at yolo.py reads all_prompts[0]/all_negative_prompts[0]; the rest avoid AttributeError downstream
|
|
p.all_prompts = [p.detailer_prompt or '']
|
|
p.all_negative_prompts = [p.detailer_negative or '']
|
|
p.all_seeds = [seed]
|
|
p.all_subseeds = [-1]
|
|
p.scripts = None
|
|
p.is_control = False
|
|
p.do_not_save_samples = True
|
|
p.do_not_save_grid = True
|
|
return p
|
|
|
|
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_segmentation = seg
|
|
# shared.opts.detailer_resolution = resolution
|
|
shared.opts.save(silent=True)
|
|
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:
|
|
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_segmentation, 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")
|
|
if tab == 'extras': # Process tab is standalone, there is no base prompt to fall back to
|
|
prompt_placeholder = 'detailer prompt, leave empty for none'
|
|
negative_placeholder = 'detailer negative prompt, leave empty for none'
|
|
else:
|
|
prompt_placeholder = 'detailer prompt or leave empty to use main prompt'
|
|
negative_placeholder = 'detailer prompt or leave empty to use main prompt'
|
|
with gr.Row():
|
|
prompt = gr.Textbox(label="Detailer prompt", value='', placeholder=prompt_placeholder, lines=2, elem_id=f"{tab}_detailer_prompt", elem_classes=["prompt"])
|
|
with gr.Row():
|
|
negative = gr.Textbox(label="Detailer negative prompt", value='', placeholder=negative_placeholder, 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")
|
|
sampler_block = None
|
|
if tab == 'extras': # fold the standalone sampler settings into the detailer accordion; values applied per-job in make_processing, never global opts
|
|
from modules import sd_samplers
|
|
sd_samplers.set_samplers()
|
|
sampler_choices = [s.name for s in sd_samplers.samplers if s.name != 'Same as primary']
|
|
with gr.Accordion('Sampler', open=False, elem_id=f"{tab}_detailer_sampler_accordion", elem_classes=["small-accordion"]):
|
|
with gr.Row():
|
|
d_sampler = gr.Dropdown(label='Sampling method', choices=sampler_choices, value='Default', elem_id=f"{tab}_detailer_sampler")
|
|
d_prediction = gr.Dropdown(label='Prediction method', choices=['default', 'epsilon', 'sample', 'v_prediction', 'flow_prediction'], value='default', elem_id=f"{tab}_detailer_prediction")
|
|
with gr.Row():
|
|
d_shift = gr.Slider(label='Flow shift', minimum=0, maximum=10, step=0.1, value=shared.opts.schedulers_shift, elem_id=f"{tab}_detailer_shift")
|
|
d_cfg = gr.Slider(label='Guidance scale', minimum=0, maximum=30, step=0.1, value=6.0, elem_id=f"{tab}_detailer_cfg")
|
|
with gr.Row():
|
|
d_options = gr.CheckboxGroup(label='Options', choices=['low order', 'thresholding', 'dynamic', 'rescale'], value=['low order'], elem_id=f"{tab}_detailer_options")
|
|
with gr.Row():
|
|
d_seed = gr.Number(label='Seed', value=-1, precision=0, elem_id=f"{tab}_detailer_seed")
|
|
sampler_block = {'sampler': d_sampler, 'prediction': d_prediction, 'shift': d_shift, 'cfg_scale': d_cfg, 'options': d_options, 'seed': d_seed}
|
|
|
|
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=[])
|
|
if tab == 'extras':
|
|
return enabled, prompt, negative, steps, strength, resolution, sampler_block
|
|
return enabled, prompt, negative, steps, strength, resolution
|
|
|
|
|
|
def initialize():
|
|
shared.yolo = YoloRestorer()
|
|
shared.detailers.append(shared.yolo)
|