From afb9cd0b661886a18ce8e0a9cc3ac36d4a3af7d9 Mon Sep 17 00:00:00 2001 From: whiteSkar Date: Mon, 26 Feb 2024 03:08:53 -0800 Subject: [PATCH 01/27] Fix ADetailer prompt in PNG Info showing the original prompt when ADetailer S/R script is applied (#489) --- scripts/!adetailer.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index 6e4c7ea..c16d665 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -648,6 +648,16 @@ def process(self, p, *args_): if self.is_ad_enabled(*args_): arg_list = self.get_args(p, *args_) self.check_skip_img2img(p, *args_) + + # The extra_generation_params has the ADetailer prompt that will be saved in the png info + # In case S/R script has been applied, we need to return the replaced prompt and not the original one + # batch_index exists on p if the p is for the Adetailer p. + # If the p is for the image before Adetailer, batch_index does not seem to exist. + if hasattr(p, "_ad_xyz_prompt_sr") and hasattr(p, "batch_index"): + replaced_positive_prompt, replaced_negative_prompt = self.get_prompt(p, arg_list[0]) + arg_list[0].ad_prompt = replaced_positive_prompt[0] + arg_list[0].ad_negative_prompt = replaced_negative_prompt[0] + extra_params = self.extra_params(arg_list) p.extra_generation_params.update(extra_params) else: From 4db56f43692c2af5601b2ef268fc1e7137cdae60 Mon Sep 17 00:00:00 2001 From: Dowon Date: Mon, 26 Feb 2024 20:11:09 +0900 Subject: [PATCH 02/27] chore: update pre-commit --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bb5e142..ea055e6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,12 +9,12 @@ repos: - id: mixed-line-ending - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.14 + rev: v0.2.2 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.12.1 + rev: 24.2.0 hooks: - id: black From b8fe935ae8b2f0f835d193db5b014016a1cd75c5 Mon Sep 17 00:00:00 2001 From: Dowon Date: Mon, 26 Feb 2024 20:11:29 +0900 Subject: [PATCH 03/27] fix: remove prompt sr comments --- scripts/!adetailer.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index c16d665..42eb374 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -649,12 +649,10 @@ def process(self, p, *args_): arg_list = self.get_args(p, *args_) self.check_skip_img2img(p, *args_) - # The extra_generation_params has the ADetailer prompt that will be saved in the png info - # In case S/R script has been applied, we need to return the replaced prompt and not the original one - # batch_index exists on p if the p is for the Adetailer p. - # If the p is for the image before Adetailer, batch_index does not seem to exist. - if hasattr(p, "_ad_xyz_prompt_sr") and hasattr(p, "batch_index"): - replaced_positive_prompt, replaced_negative_prompt = self.get_prompt(p, arg_list[0]) + if hasattr(p, "_ad_xyz_prompt_sr"): + replaced_positive_prompt, replaced_negative_prompt = self.get_prompt( + p, arg_list[0] + ) arg_list[0].ad_prompt = replaced_positive_prompt[0] arg_list[0].ad_negative_prompt = replaced_negative_prompt[0] From e49667ddc7d5b52e3841aedb056d91e5deb18ee2 Mon Sep 17 00:00:00 2001 From: Dowon Date: Mon, 26 Feb 2024 20:11:49 +0900 Subject: [PATCH 04/27] chore: ultralytics min version -> 8.1.18 --- install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.py b/install.py index cc7325d..c7de57d 100644 --- a/install.py +++ b/install.py @@ -44,7 +44,7 @@ def run_pip(*args): def install(): deps = [ # requirements - ("ultralytics", "8.1.0", None), + ("ultralytics", "8.1.18", None), ("mediapipe", "0.10.9", None), ("rich", "13.0.0", None), # mediapipe From 1506361898184d797b0d4f59ab2c8c6663d5fe3b Mon Sep 17 00:00:00 2001 From: Dowon Date: Mon, 26 Feb 2024 21:20:11 +0900 Subject: [PATCH 05/27] feat: ultralytics yolo world model --- adetailer/ultralytics.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/adetailer/ultralytics.py b/adetailer/ultralytics.py index 36062b7..091872e 100644 --- a/adetailer/ultralytics.py +++ b/adetailer/ultralytics.py @@ -15,10 +15,12 @@ def ultralytics_predict( image: Image.Image, confidence: float = 0.3, device: str = "", + classes: str = "", ) -> PredictOutput: from ultralytics import YOLO model = YOLO(model_path) + apply_classes(model, model_path, classes) pred = model(image, conf=confidence, device=device) bboxes = pred[0].boxes.xyxy.cpu().numpy() @@ -37,6 +39,13 @@ def ultralytics_predict( return PredictOutput(bboxes=bboxes, masks=masks, preview=preview) +def apply_classes(model, model_path: str | Path, classes: str): + if not classes or not Path(model_path).stem.endswith("world"): + return + parsed = [c.strip() for c in classes.split(",")] + model.set_classes(parsed) + + def mask_to_pil(masks, shape: tuple[int, int]) -> list[Image.Image]: """ Parameters From ac0bb9ab00e6475f91f8a9ba1462d24f8f36ceb9 Mon Sep 17 00:00:00 2001 From: Dowon Date: Mon, 26 Feb 2024 21:20:32 +0900 Subject: [PATCH 06/27] test: pytests --- tests/__init__.py | 0 tests/conftest.py | 29 +++++++++++++++++++++++ tests/test_mediapipe.py | 18 +++++++++++++++ tests/test_ultralytics.py | 48 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_mediapipe.py create mode 100644 tests/test_ultralytics.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..fc3e22d --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,29 @@ +from functools import cache + +import pytest +import requests +from PIL import Image + + +@cache +def _sample_image(): + url = "https://i.imgur.com/E5OVXvn.png" + resp = requests.get(url, stream=True) + return Image.open(resp.raw) + + +@cache +def _sample_image2(): + url = "https://i.imgur.com/px5UT7T.png" + resp = requests.get(url, stream=True) + return Image.open(resp.raw) + + +@pytest.fixture() +def sample_image(): + return _sample_image() + + +@pytest.fixture() +def sample_image2(): + return _sample_image2() diff --git a/tests/test_mediapipe.py b/tests/test_mediapipe.py new file mode 100644 index 0000000..7ddcdfe --- /dev/null +++ b/tests/test_mediapipe.py @@ -0,0 +1,18 @@ +import pytest +from PIL import Image + +from adetailer.mediapipe import mediapipe_predict + + +@pytest.mark.parametrize( + "model_name", + [ + "mediapipe_face_short", + "mediapipe_face_full", + "mediapipe_face_mesh", + "mediapipe_face_mesh_eyes_only", + ], +) +def test_mediapipe(sample_image2: Image.Image, model_name: str): + result = mediapipe_predict(model_name, sample_image2) + assert result.preview is not None diff --git a/tests/test_ultralytics.py b/tests/test_ultralytics.py new file mode 100644 index 0000000..ad855ad --- /dev/null +++ b/tests/test_ultralytics.py @@ -0,0 +1,48 @@ +import pytest +from huggingface_hub import hf_hub_download +from PIL import Image + +from adetailer.ultralytics import ultralytics_predict + +repo_id = "Bingsu/adetailer" + + +@pytest.mark.parametrize( + "model_name", + [ + "face_yolov8n.pt", + "face_yolov8n_v2.pt", + "face_yolov8s.pt", + "hand_yolov8n.pt", + "hand_yolov8s.pt", + "person_yolov8n-seg.pt", + "person_yolov8s-seg.pt", + "person_yolov8m-seg.pt", + "deepfashion2_yolov8s-seg.pt", + ], +) +def test_ultralytics_hf_models(sample_image: Image.Image, model_name: str): + model_path = hf_hub_download(repo_id, model_name) + result = ultralytics_predict(model_path, sample_image) + assert result.preview is not None + + +def test_yolo_world_default(sample_image: Image.Image): + result = ultralytics_predict("yolov8l-world.pt", sample_image) + assert result.preview is not None + + +@pytest.mark.parametrize( + "klass", + [ + "person", + "bird", + "yellow bird", + "person,glasses,headphone", + "person,bird", + "glasses,yellow bird", + ], +) +def test_yolo_world(sample_image2: Image.Image, klass: str): + result = ultralytics_predict("yolov8l-world.pt", sample_image2, classes=klass) + assert result.preview is not None From 1441b55d6f2e8e06af76c2152eb8ac82fc0d7296 Mon Sep 17 00:00:00 2001 From: catboxanon <122327233+catboxanon@users.noreply.github.com> Date: Tue, 27 Feb 2024 07:19:36 -0500 Subject: [PATCH 07/27] Forge webui support (#517) * Forge support * Separate Forge implementation * Remove debug print --- adetailer/ui.py | 49 ++++++++------ controlnet_ext/__init__.py | 16 ++++- controlnet_ext/common.py | 11 +++ controlnet_ext/controlnet_ext.py | 14 +--- controlnet_ext/controlnet_ext_forge.py | 94 ++++++++++++++++++++++++++ scripts/!adetailer.py | 26 +++++-- 6 files changed, 172 insertions(+), 38 deletions(-) create mode 100644 controlnet_ext/common.py create mode 100644 controlnet_ext/controlnet_ext_forge.py diff --git a/adetailer/ui.py b/adetailer/ui.py index b6318ff..8e044cc 100644 --- a/adetailer/ui.py +++ b/adetailer/ui.py @@ -9,25 +9,36 @@ from adetailer import AFTER_DETAILER, __version__ from adetailer.args import ALL_ARGS, MASK_MERGE_INVERT -from controlnet_ext import controlnet_exists, get_cn_models - -cn_module_choices = { - "inpaint": [ - "inpaint_global_harmonious", - "inpaint_only", - "inpaint_only+lama", - ], - "lineart": [ - "lineart_coarse", - "lineart_realistic", - "lineart_anime", - "lineart_anime_denoise", - ], - "openpose": ["openpose_full", "dw_openpose_full"], - "tile": ["tile_resample", "tile_colorfix", "tile_colorfix+sharp"], - "scribble": ["t2ia_sketch_pidi"], - "depth": ["depth_midas", "depth_hand_refiner"], -} +from controlnet_ext import controlnet_exists, controlnet_forge, get_cn_models + +if controlnet_forge: + from lib_controlnet import global_state + cn_module_choices = { + "inpaint": list(m for m in global_state.get_filtered_preprocessors("Inpaint")), + "lineart": list(m for m in global_state.get_filtered_preprocessors("Lineart")), + "openpose": list(m for m in global_state.get_filtered_preprocessors("OpenPose")), + "tile": list(m for m in global_state.get_filtered_preprocessors("Tile")), + "scribble": list(m for m in global_state.get_filtered_preprocessors("Scribble")), + "depth": list(m for m in global_state.get_filtered_preprocessors("Depth")), + } +else: + cn_module_choices = { + "inpaint": [ + "inpaint_global_harmonious", + "inpaint_only", + "inpaint_only+lama", + ], + "lineart": [ + "lineart_coarse", + "lineart_realistic", + "lineart_anime", + "lineart_anime_denoise", + ], + "openpose": ["openpose_full", "dw_openpose_full"], + "tile": ["tile_resample", "tile_colorfix", "tile_colorfix+sharp"], + "scribble": ["t2ia_sketch_pidi"], + "depth": ["depth_midas", "depth_hand_refiner"], + } class Widgets(SimpleNamespace): diff --git a/controlnet_ext/__init__.py b/controlnet_ext/__init__.py index 0ab6668..32efe17 100644 --- a/controlnet_ext/__init__.py +++ b/controlnet_ext/__init__.py @@ -1,7 +1,21 @@ -from .controlnet_ext import ControlNetExt, controlnet_exists, get_cn_models +try: + from .controlnet_ext_forge import ( + ControlNetExt, + controlnet_exists, + controlnet_forge, + get_cn_models, + ) +except ImportError: + from .controlnet_ext import ( + ControlNetExt, + controlnet_exists, + controlnet_forge, + get_cn_models, + ) __all__ = [ "ControlNetExt", "controlnet_exists", + "controlnet_forge", "get_cn_models", ] diff --git a/controlnet_ext/common.py b/controlnet_ext/common.py new file mode 100644 index 0000000..beeb60e --- /dev/null +++ b/controlnet_ext/common.py @@ -0,0 +1,11 @@ +import re + +cn_model_module = { + "inpaint": "inpaint_global_harmonious", + "scribble": "t2ia_sketch_pidi", + "lineart": "lineart_coarse", + "openpose": "openpose_full", + "tile": "tile_resample", + "depth": "depth_midas", +} +cn_model_regex = re.compile("|".join(cn_model_module.keys())) diff --git a/controlnet_ext/controlnet_ext.py b/controlnet_ext/controlnet_ext.py index 9f54f12..b6785cb 100644 --- a/controlnet_ext/controlnet_ext.py +++ b/controlnet_ext/controlnet_ext.py @@ -1,7 +1,6 @@ from __future__ import annotations import importlib -import re import sys from functools import lru_cache from pathlib import Path @@ -9,6 +8,8 @@ from modules import extensions, sd_models, shared +from .common import cn_model_regex + try: from modules.paths import extensions_builtin_dir, extensions_dir, models_path except ImportError as e: @@ -22,6 +23,7 @@ ext_path = Path(extensions_dir) ext_builtin_path = Path(extensions_builtin_dir) controlnet_exists = False +controlnet_forge = False controlnet_path = None cn_base_path = "" @@ -42,16 +44,6 @@ if target_path not in sys.path: sys.path.append(target_path) -cn_model_module = { - "inpaint": "inpaint_global_harmonious", - "scribble": "t2ia_sketch_pidi", - "lineart": "lineart_coarse", - "openpose": "openpose_full", - "tile": "tile_resample", - "depth": "depth_midas", -} -cn_model_regex = re.compile("|".join(cn_model_module.keys())) - class ControlNetExt: def __init__(self): diff --git a/controlnet_ext/controlnet_ext_forge.py b/controlnet_ext/controlnet_ext_forge.py new file mode 100644 index 0000000..0c0331b --- /dev/null +++ b/controlnet_ext/controlnet_ext_forge.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import copy +import sys + +import numpy as np +from lib_controlnet import external_code, global_state +from lib_controlnet.external_code import ControlNetUnit + +from modules import scripts +from modules.processing import StableDiffusionProcessing + +from .common import cn_model_regex + +controlnet_exists = True +controlnet_forge = True + +def find_script(p : StableDiffusionProcessing, script_title : str) -> scripts.Script: + script = next((s for s in p.scripts.scripts if s.title() == script_title ), None) + if not script: + raise Exception("Script not found: " + script_title) + return script + +def add_forge_script_to_adetailer_run(p: StableDiffusionProcessing, script_title : str, script_args : list): + p.scripts = copy.copy(scripts.scripts_img2img) + p.scripts.alwayson_scripts = [] + p.script_args_value = [] + + script = copy.copy(find_script(p, script_title)) + script.args_from = len(p.script_args_value) + script.args_to = len(p.script_args_value) + len(script_args) + p.scripts.alwayson_scripts.append(script) + p.script_args_value.extend(script_args) + +class ControlNetExt: + def __init__(self): + self.cn_available = False + self.external_cn = external_code + + def init_controlnet(self): + self.cn_available = True + + def update_scripts_args( + self, + p, + model: str, + module: str | None, + weight: float, + guidance_start: float, + guidance_end: float, + ): + if (not self.cn_available) or model == "None": + return + + if controlnet_forge: + image = np.asarray(p.init_images[0]) + mask = np.zeros_like(image) + mask[:] = 255 + + cnet_image = { + "image": image, + "mask": mask + } + + pres = external_code.pixel_perfect_resolution( + image, + target_H=p.height, + target_W=p.width, + resize_mode=external_code.resize_mode_from_value(p.resize_mode) + ) + + add_forge_script_to_adetailer_run( + p, + "ControlNet", + [ + ControlNetUnit( + enabled=True, + image=cnet_image, + model=model, + module=module, + weight=weight, + guidance_start=guidance_start, + guidance_end=guidance_end, + processor_res=pres + ) + ] + ) + + return + + +def get_cn_models() -> list[str]: + models = global_state.get_all_controlnet_names() + return [m for m in models if cn_model_regex.search(m)] diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index 42eb374..ece36e8 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -36,7 +36,12 @@ ) from adetailer.traceback import rich_traceback from adetailer.ui import WebuiInfo, adui, ordinal, suffix -from controlnet_ext import ControlNetExt, controlnet_exists, get_cn_models +from controlnet_ext import ( + ControlNetExt, + controlnet_exists, + controlnet_forge, + get_cn_models, +) from controlnet_ext.restore import ( CNHijackRestore, cn_allow_script_control, @@ -517,13 +522,14 @@ def get_i2i_p(self, p, args: ADetailerArgs, image): i2i._ad_disabled = True i2i._ad_inner = True - if args.ad_controlnet_model != "Passthrough": - self.disable_controlnet_units(i2i.script_args) + if not controlnet_forge: + if args.ad_controlnet_model != "Passthrough": + self.disable_controlnet_units(i2i.script_args) - if args.ad_controlnet_model not in ["None", "Passthrough"]: - self.update_controlnet_args(i2i, args) - elif args.ad_controlnet_model == "None": - i2i.control_net_enabled = False + if args.ad_controlnet_model not in ["None", "Passthrough"]: + self.update_controlnet_args(i2i, args) + elif args.ad_controlnet_model == "None": + i2i.control_net_enabled = False return i2i @@ -729,6 +735,12 @@ def _postprocess_image_inner( p2.seed = self.get_each_tap_seed(seed, j) p2.subseed = self.get_each_tap_seed(subseed, j) + if controlnet_forge: + if args.ad_controlnet_model not in "None": + self.update_controlnet_args(p2, args) + else: + p2.control_net_enabled = False + try: processed = process_images(p2) except NansException as e: From 9248b79504d2df0f78f04f3fd7eeb5b9b3644b7b Mon Sep 17 00:00:00 2001 From: Dowon Date: Tue, 27 Feb 2024 21:32:40 +0900 Subject: [PATCH 08/27] style: apply black, fix by ruff --- adetailer/ui.py | 13 +++-- controlnet_ext/controlnet_ext.py | 2 +- controlnet_ext/controlnet_ext_forge.py | 79 +++++++++++++------------- scripts/!adetailer.py | 2 +- 4 files changed, 48 insertions(+), 48 deletions(-) diff --git a/adetailer/ui.py b/adetailer/ui.py index 8e044cc..8fbf263 100644 --- a/adetailer/ui.py +++ b/adetailer/ui.py @@ -13,13 +13,14 @@ if controlnet_forge: from lib_controlnet import global_state + cn_module_choices = { - "inpaint": list(m for m in global_state.get_filtered_preprocessors("Inpaint")), - "lineart": list(m for m in global_state.get_filtered_preprocessors("Lineart")), - "openpose": list(m for m in global_state.get_filtered_preprocessors("OpenPose")), - "tile": list(m for m in global_state.get_filtered_preprocessors("Tile")), - "scribble": list(m for m in global_state.get_filtered_preprocessors("Scribble")), - "depth": list(m for m in global_state.get_filtered_preprocessors("Depth")), + "inpaint": list(global_state.get_filtered_preprocessors("Inpaint")), + "lineart": list(global_state.get_filtered_preprocessors("Lineart")), + "openpose": list(global_state.get_filtered_preprocessors("OpenPose")), + "tile": list(global_state.get_filtered_preprocessors("Tile")), + "scribble": list(global_state.get_filtered_preprocessors("Scribble")), + "depth": list(global_state.get_filtered_preprocessors("Depth")), } else: cn_module_choices = { diff --git a/controlnet_ext/controlnet_ext.py b/controlnet_ext/controlnet_ext.py index b6785cb..717d11b 100644 --- a/controlnet_ext/controlnet_ext.py +++ b/controlnet_ext/controlnet_ext.py @@ -8,7 +8,7 @@ from modules import extensions, sd_models, shared -from .common import cn_model_regex +from .common import cn_model_module, cn_model_regex try: from modules.paths import extensions_builtin_dir, extensions_dir, models_path diff --git a/controlnet_ext/controlnet_ext_forge.py b/controlnet_ext/controlnet_ext_forge.py index 0c0331b..e1ac6ed 100644 --- a/controlnet_ext/controlnet_ext_forge.py +++ b/controlnet_ext/controlnet_ext_forge.py @@ -1,7 +1,6 @@ from __future__ import annotations import copy -import sys import numpy as np from lib_controlnet import external_code, global_state @@ -15,13 +14,18 @@ controlnet_exists = True controlnet_forge = True -def find_script(p : StableDiffusionProcessing, script_title : str) -> scripts.Script: - script = next((s for s in p.scripts.scripts if s.title() == script_title ), None) + +def find_script(p: StableDiffusionProcessing, script_title: str) -> scripts.Script: + script = next((s for s in p.scripts.scripts if s.title() == script_title), None) if not script: - raise Exception("Script not found: " + script_title) + msg = f"Script not found: {script_title!r}" + raise RuntimeError(msg) return script -def add_forge_script_to_adetailer_run(p: StableDiffusionProcessing, script_title : str, script_args : list): + +def add_forge_script_to_adetailer_run( + p: StableDiffusionProcessing, script_title: str, script_args: list +): p.scripts = copy.copy(scripts.scripts_img2img) p.scripts.alwayson_scripts = [] p.script_args_value = [] @@ -32,6 +36,7 @@ def add_forge_script_to_adetailer_run(p: StableDiffusionProcessing, script_title p.scripts.alwayson_scripts.append(script) p.script_args_value.extend(script_args) + class ControlNetExt: def __init__(self): self.cn_available = False @@ -52,41 +57,35 @@ def update_scripts_args( if (not self.cn_available) or model == "None": return - if controlnet_forge: - image = np.asarray(p.init_images[0]) - mask = np.zeros_like(image) - mask[:] = 255 - - cnet_image = { - "image": image, - "mask": mask - } - - pres = external_code.pixel_perfect_resolution( - image, - target_H=p.height, - target_W=p.width, - resize_mode=external_code.resize_mode_from_value(p.resize_mode) - ) - - add_forge_script_to_adetailer_run( - p, - "ControlNet", - [ - ControlNetUnit( - enabled=True, - image=cnet_image, - model=model, - module=module, - weight=weight, - guidance_start=guidance_start, - guidance_end=guidance_end, - processor_res=pres - ) - ] - ) - - return + image = np.asarray(p.init_images[0]) + mask = np.zeros_like(image) + mask[:] = 255 + + cnet_image = {"image": image, "mask": mask} + + pres = external_code.pixel_perfect_resolution( + image, + target_H=p.height, + target_W=p.width, + resize_mode=external_code.resize_mode_from_value(p.resize_mode), + ) + + add_forge_script_to_adetailer_run( + p, + "ControlNet", + [ + ControlNetUnit( + enabled=True, + image=cnet_image, + model=model, + module=module, + weight=weight, + guidance_start=guidance_start, + guidance_end=guidance_end, + processor_res=pres, + ) + ], + ) def get_cn_models() -> list[str]: diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index ece36e8..fb599ad 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -667,7 +667,7 @@ def process(self, p, *args_): else: p._ad_disabled = True - def _postprocess_image_inner( + def _postprocess_image_inner( # noqa: C901 self, p, pp, args: ADetailerArgs, *, n: int = 0 ) -> bool: """ From 03f4c44d95dd417e830dfcc471b3802c43fc1194 Mon Sep 17 00:00:00 2001 From: Dowon Date: Tue, 27 Feb 2024 21:33:15 +0900 Subject: [PATCH 09/27] fix: np.zeros_like -> np.full_like --- controlnet_ext/controlnet_ext_forge.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/controlnet_ext/controlnet_ext_forge.py b/controlnet_ext/controlnet_ext_forge.py index e1ac6ed..f829182 100644 --- a/controlnet_ext/controlnet_ext_forge.py +++ b/controlnet_ext/controlnet_ext_forge.py @@ -58,8 +58,7 @@ def update_scripts_args( return image = np.asarray(p.init_images[0]) - mask = np.zeros_like(image) - mask[:] = 255 + mask = np.full_like(image, fill_value=255) cnet_image = {"image": image, "mask": mask} From 32c3e9cf0919f6c01e577b129fc9d4ac721b8092 Mon Sep 17 00:00:00 2001 From: Dowon Date: Tue, 27 Feb 2024 21:35:05 +0900 Subject: [PATCH 10/27] chore: fix pyproject ruff lint --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 31b6342..c900464 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ repository = "https://github.com/Bing-su/adetailer" profile = "black" known_first_party = ["launch", "modules"] -[tool.ruff] +[tool.ruff.lint] select = [ "A", "B", @@ -38,5 +38,5 @@ select = [ ] ignore = ["B008", "B905", "E501", "F401", "UP007"] -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ["launch", "modules"] From 699227aa3871ccc24db80b4e74b4d4379422843b Mon Sep 17 00:00:00 2001 From: Dowon Date: Tue, 27 Feb 2024 21:36:19 +0900 Subject: [PATCH 11/27] ci: update stale action version --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 79ab8fa..6d3e128 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -7,7 +7,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v8 + - uses: actions/stale@v9 with: days-before-stale: 23 days-before-close: 3 From 4e4726c71c60b93ba41c6c11c55e6c036d0b3674 Mon Sep 17 00:00:00 2001 From: Dowon Date: Tue, 27 Feb 2024 21:58:41 +0900 Subject: [PATCH 12/27] ci: add lint action --- .github/workflows/lint.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..405efe5 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,35 @@ +name: Lint + +on: + pull_request: + paths: + - "**.py" + +jobs: + lint: + runs-on: ubuntu-latest + if: github.repository == 'Bing-su/adetailer' || github.repository == '' + + steps: + - uses: actions/checkout@v4 + + - name: Setup python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install python packages + run: pip install black ruff pre-commit-hooks + + - name: Run pre-commit-hooks + run: | + check-ast + trailing-whitespace-fixer --markdown-linebreak-ext=md + end-of-file-fixer + mixed-line-ending + + - name: Run black + run: black --check . + + - name: Run ruff + run: ruff check . From cfc9f98e566f1bf223c2540da05fbaad514fb750 Mon Sep 17 00:00:00 2001 From: Dowon Date: Wed, 28 Feb 2024 20:56:17 +0900 Subject: [PATCH 13/27] fix: disable controlnet unit forge, fix C901 --- scripts/!adetailer.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index fb599ad..50ba314 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -522,14 +522,13 @@ def get_i2i_p(self, p, args: ADetailerArgs, image): i2i._ad_disabled = True i2i._ad_inner = True - if not controlnet_forge: - if args.ad_controlnet_model != "Passthrough": - self.disable_controlnet_units(i2i.script_args) + if args.ad_controlnet_model != "Passthrough" and not controlnet_forge: + self.disable_controlnet_units(i2i.script_args) - if args.ad_controlnet_model not in ["None", "Passthrough"]: - self.update_controlnet_args(i2i, args) - elif args.ad_controlnet_model == "None": - i2i.control_net_enabled = False + if args.ad_controlnet_model not in ["None", "Passthrough"]: + self.update_controlnet_args(i2i, args) + elif args.ad_controlnet_model == "None": + i2i.control_net_enabled = False return i2i @@ -667,7 +666,7 @@ def process(self, p, *args_): else: p._ad_disabled = True - def _postprocess_image_inner( # noqa: C901 + def _postprocess_image_inner( self, p, pp, args: ADetailerArgs, *, n: int = 0 ) -> bool: """ @@ -735,12 +734,6 @@ def _postprocess_image_inner( # noqa: C901 p2.seed = self.get_each_tap_seed(seed, j) p2.subseed = self.get_each_tap_seed(subseed, j) - if controlnet_forge: - if args.ad_controlnet_model not in "None": - self.update_controlnet_args(p2, args) - else: - p2.control_net_enabled = False - try: processed = process_images(p2) except NansException as e: From 4c02019d582e97663cade008ce4d97a23ec1126a Mon Sep 17 00:00:00 2001 From: Dowon Date: Wed, 28 Feb 2024 20:58:35 +0900 Subject: [PATCH 14/27] fix: controlnet_forge boolean -> controlnet_type --- controlnet_ext/__init__.py | 6 +++--- controlnet_ext/controlnet_ext.py | 2 +- controlnet_ext/controlnet_ext_forge.py | 2 +- scripts/!adetailer.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/controlnet_ext/__init__.py b/controlnet_ext/__init__.py index 32efe17..06cc3a1 100644 --- a/controlnet_ext/__init__.py +++ b/controlnet_ext/__init__.py @@ -2,20 +2,20 @@ from .controlnet_ext_forge import ( ControlNetExt, controlnet_exists, - controlnet_forge, + controlnet_type, get_cn_models, ) except ImportError: from .controlnet_ext import ( ControlNetExt, controlnet_exists, - controlnet_forge, + controlnet_type, get_cn_models, ) __all__ = [ "ControlNetExt", "controlnet_exists", - "controlnet_forge", + "controlnet_type", "get_cn_models", ] diff --git a/controlnet_ext/controlnet_ext.py b/controlnet_ext/controlnet_ext.py index 717d11b..134a7a4 100644 --- a/controlnet_ext/controlnet_ext.py +++ b/controlnet_ext/controlnet_ext.py @@ -23,7 +23,7 @@ ext_path = Path(extensions_dir) ext_builtin_path = Path(extensions_builtin_dir) controlnet_exists = False -controlnet_forge = False +controlnet_type = "standard" controlnet_path = None cn_base_path = "" diff --git a/controlnet_ext/controlnet_ext_forge.py b/controlnet_ext/controlnet_ext_forge.py index f829182..ef59e32 100644 --- a/controlnet_ext/controlnet_ext_forge.py +++ b/controlnet_ext/controlnet_ext_forge.py @@ -12,7 +12,7 @@ from .common import cn_model_regex controlnet_exists = True -controlnet_forge = True +controlnet_type = "forge" def find_script(p: StableDiffusionProcessing, script_title: str) -> scripts.Script: diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index 50ba314..7e4f436 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -39,7 +39,7 @@ from controlnet_ext import ( ControlNetExt, controlnet_exists, - controlnet_forge, + controlnet_type, get_cn_models, ) from controlnet_ext.restore import ( @@ -522,7 +522,7 @@ def get_i2i_p(self, p, args: ADetailerArgs, image): i2i._ad_disabled = True i2i._ad_inner = True - if args.ad_controlnet_model != "Passthrough" and not controlnet_forge: + if args.ad_controlnet_model != "Passthrough" and controlnet_type != "forge": self.disable_controlnet_units(i2i.script_args) if args.ad_controlnet_model not in ["None", "Passthrough"]: From a7e67770673d31ce9107b73ffc9083b3fde8d1f5 Mon Sep 17 00:00:00 2001 From: Dowon Date: Wed, 28 Feb 2024 21:18:44 +0900 Subject: [PATCH 15/27] fix: async rest api endpoints --- scripts/!adetailer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index 7e4f436..11f6f11 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -995,15 +995,15 @@ def on_before_ui(): def add_api_endpoints(_: gr.Blocks, app: FastAPI): @app.get("/adetailer/v1/version") - def version(): + async def version(): return {"version": __version__} @app.get("/adetailer/v1/schema") - def schema(): + async def schema(): return ADetailerArgs.schema() @app.get("/adetailer/v1/ad_model") - def ad_model(): + async def ad_model(): return {"ad_model": list(model_mapping)} From 70bcec36224ebf5deab67483cf3fab5bd64da708 Mon Sep 17 00:00:00 2001 From: Dowon Date: Thu, 29 Feb 2024 07:57:13 +0900 Subject: [PATCH 16/27] fix: ui controlnet_forge --- adetailer/ui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adetailer/ui.py b/adetailer/ui.py index 8fbf263..a044a53 100644 --- a/adetailer/ui.py +++ b/adetailer/ui.py @@ -9,9 +9,9 @@ from adetailer import AFTER_DETAILER, __version__ from adetailer.args import ALL_ARGS, MASK_MERGE_INVERT -from controlnet_ext import controlnet_exists, controlnet_forge, get_cn_models +from controlnet_ext import controlnet_exists, controlnet_type, get_cn_models -if controlnet_forge: +if controlnet_type == "forge": from lib_controlnet import global_state cn_module_choices = { From e607041589d53c7faffe80ee80765f3cdadbdfb7 Mon Sep 17 00:00:00 2001 From: Dowon Date: Thu, 29 Feb 2024 20:25:51 +0900 Subject: [PATCH 17/27] feat: add soft_inpainting to default scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 소급 적용 안됨 --- scripts/!adetailer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index 11f6f11..0161180 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -67,7 +67,7 @@ adetailer_dir, extra_dir=extra_models_dir, huggingface=not no_huggingface ) txt2img_submit_button = img2img_submit_button = None -SCRIPT_DEFAULT = "dynamic_prompting,dynamic_thresholding,wildcard_recursive,wildcards,lora_block_weight,negpip" +SCRIPT_DEFAULT = "dynamic_prompting,dynamic_thresholding,wildcard_recursive,wildcards,lora_block_weight,negpip,soft_inpainting" if ( not adetailer_dir.exists() From f21ac7b28f6e9a51b25a435a2d2a0e2f5d3aa0f5 Mon Sep 17 00:00:00 2001 From: Dowon Date: Thu, 29 Feb 2024 20:27:04 +0900 Subject: [PATCH 18/27] fix: xyz grid controlnet model passthrough option --- scripts/!adetailer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index 0161180..747798c 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -971,7 +971,7 @@ def make_axis_on_xyz_grid(): "[ADetailer] ControlNet model 1st", str, partial(set_value, field="ad_controlnet_model"), - choices=lambda: ["None", *get_cn_models()], + choices=lambda: ["None", "Passthrough", *get_cn_models()], ), ] From 6dcad30b64ea3208d8bf593b6e289bc95d3e0c25 Mon Sep 17 00:00:00 2001 From: Dowon Date: Thu, 29 Feb 2024 20:30:44 +0900 Subject: [PATCH 19/27] stype: fix ruff UP007 rule, typings --- adetailer/args.py | 6 ++---- adetailer/common.py | 2 +- pyproject.toml | 5 ++++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/adetailer/args.py b/adetailer/args.py index 130bdea..b0f311e 100644 --- a/adetailer/args.py +++ b/adetailer/args.py @@ -5,7 +5,6 @@ from functools import cached_property, partial from typing import Any, Literal, NamedTuple, Optional -import pydantic from pydantic import ( BaseModel, Extra, @@ -14,7 +13,6 @@ PositiveInt, confloat, conint, - constr, validator, ) @@ -34,11 +32,11 @@ class Arg(NamedTuple): class ArgsList(UserList): @cached_property - def attrs(self) -> tuple[str]: + def attrs(self) -> tuple[str, ...]: return tuple(attr for attr, _ in self) @cached_property - def names(self) -> tuple[str]: + def names(self) -> tuple[str, ...]: return tuple(name for _, name in self) diff --git a/adetailer/common.py b/adetailer/common.py index 9b69782..7d0460d 100644 --- a/adetailer/common.py +++ b/adetailer/common.py @@ -3,7 +3,7 @@ from collections import OrderedDict from dataclasses import dataclass, field from pathlib import Path -from typing import Optional, Union +from typing import Optional from huggingface_hub import hf_hub_download from PIL import Image, ImageDraw diff --git a/pyproject.toml b/pyproject.toml index c900464..90ca5dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,10 @@ select = [ "UP", "W", ] -ignore = ["B008", "B905", "E501", "F401", "UP007"] +ignore = ["B008", "B905", "E501", "F401"] [tool.ruff.lint.isort] known-first-party = ["launch", "modules"] + +[tool.ruff.lint.pyupgrade] +keep-runtime-typing = true From 6018184ce4203bcbc750ba49daefa370d358b759 Mon Sep 17 00:00:00 2001 From: Dowon Date: Thu, 29 Feb 2024 20:34:35 +0900 Subject: [PATCH 20/27] fix: sync with ultralytics implement https://github.com/ultralytics/ultralytics/blob/f8e681c2be251562633d424f4a1a1cc7da526bed/ultralytics/models/yolo/model.py#L17 --- adetailer/ultralytics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adetailer/ultralytics.py b/adetailer/ultralytics.py index 091872e..8483b12 100644 --- a/adetailer/ultralytics.py +++ b/adetailer/ultralytics.py @@ -40,7 +40,7 @@ def ultralytics_predict( def apply_classes(model, model_path: str | Path, classes: str): - if not classes or not Path(model_path).stem.endswith("world"): + if not classes or "-world" not in Path(model_path).stem: return parsed = [c.strip() for c in classes.split(",")] model.set_classes(parsed) From 692b052cfb2a6d77659c3af580030f011f9da62e Mon Sep 17 00:00:00 2001 From: Dowon Date: Thu, 29 Feb 2024 20:48:56 +0900 Subject: [PATCH 21/27] test: fix sample image requests headers --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index fc3e22d..0c382bd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,14 +8,14 @@ @cache def _sample_image(): url = "https://i.imgur.com/E5OVXvn.png" - resp = requests.get(url, stream=True) + resp = requests.get(url, stream=True, headers={"User-Agent": "Mozilla/5.0"}) return Image.open(resp.raw) @cache def _sample_image2(): url = "https://i.imgur.com/px5UT7T.png" - resp = requests.get(url, stream=True) + resp = requests.get(url, stream=True, headers={"User-Agent": "Mozilla/5.0"}) return Image.open(resp.raw) From 9e9dcd5bca6559078fcc3ead0715e31c337c660b Mon Sep 17 00:00:00 2001 From: Dowon Date: Fri, 1 Mar 2024 10:54:19 +0900 Subject: [PATCH 22/27] feat: add yolo world model --- adetailer/__version__.py | 2 +- adetailer/args.py | 3 +++ adetailer/common.py | 1 + adetailer/ui.py | 22 ++++++++++++++++++++++ adetailer/ultralytics.py | 11 ++++++++--- scripts/!adetailer.py | 1 + tests/test_ultralytics.py | 4 ++-- 7 files changed, 38 insertions(+), 6 deletions(-) diff --git a/adetailer/__version__.py b/adetailer/__version__.py index 869aead..9d86149 100644 --- a/adetailer/__version__.py +++ b/adetailer/__version__.py @@ -1 +1 @@ -__version__ = "24.1.2" +__version__ = "24.3.0.dev0" diff --git a/adetailer/args.py b/adetailer/args.py index b0f311e..bda899d 100644 --- a/adetailer/args.py +++ b/adetailer/args.py @@ -42,6 +42,7 @@ def names(self) -> tuple[str, ...]: class ADetailerArgs(BaseModel, extra=Extra.forbid): ad_model: str = "None" + ad_model_classes: str = "" ad_prompt: str = "" ad_negative_prompt: str = "" ad_confidence: confloat(ge=0.0, le=1.0) = 0.3 @@ -111,6 +112,7 @@ def extra_params(self, suffix: str = "") -> dict[str, Any]: p = {name: getattr(self, attr) for attr, name in ALL_ARGS} ppop = partial(self.ppop, p) + ppop("ADetailer model classes") ppop("ADetailer prompt") ppop("ADetailer negative prompt") ppop("ADetailer mask only top k largest", cond=0) @@ -183,6 +185,7 @@ def extra_params(self, suffix: str = "") -> dict[str, Any]: _all_args = [ ("ad_model", "ADetailer model"), + ("ad_model_classes", "ADetailer model classes"), ("ad_prompt", "ADetailer prompt"), ("ad_negative_prompt", "ADetailer negative prompt"), ("ad_confidence", "ADetailer confidence"), diff --git a/adetailer/common.py b/adetailer/common.py index 7d0460d..5a8b1ab 100644 --- a/adetailer/common.py +++ b/adetailer/common.py @@ -60,6 +60,7 @@ def get_models( ) models.update( { + "yolov8x-world.pt": "yolov8x-world.pt", "mediapipe_face_full": None, "mediapipe_face_short": None, "mediapipe_face_mesh": None, diff --git a/adetailer/ui.py b/adetailer/ui.py index a044a53..cfd665d 100644 --- a/adetailer/ui.py +++ b/adetailer/ui.py @@ -85,6 +85,14 @@ def on_generate_click(state: dict, *values: Any): return state +def on_ad_model_update(model: str): + if "-world" in model: + return gr.update( + visible=True, placeholder="Comma separated class names to detect." + ) + return gr.update(visible=False, placeholder="") + + def on_cn_model_update(cn_model_name: str): cn_model_name = cn_model_name.replace("inpaint_depth", "depth") for t in cn_module_choices: @@ -177,6 +185,20 @@ def one_ui_group(n: int, is_img2img: bool, webui_info: WebuiInfo): elem_id=eid("ad_model"), ) + w.ad_model_classes = gr.Textbox( + label="ADetailer model classes" + suffix(n), + value="", + visible=False, + elem_id=eid("ad_classes"), + ) + + w.ad_model_classes.change( + on_ad_model_update, + inputs=w.ad_model, + outputs=w.ad_model_classes, + queue=False, + ) + with gr.Group(): with gr.Row(elem_id=eid("ad_toprow_prompt")): w.ad_prompt = gr.Textbox( diff --git a/adetailer/ultralytics.py b/adetailer/ultralytics.py index 8483b12..de19ff7 100644 --- a/adetailer/ultralytics.py +++ b/adetailer/ultralytics.py @@ -1,6 +1,7 @@ from __future__ import annotations from pathlib import Path +from typing import TYPE_CHECKING import cv2 from PIL import Image @@ -9,6 +10,10 @@ from adetailer import PredictOutput from adetailer.common import create_mask_from_bbox +if TYPE_CHECKING: + import torch + from ultralytics import YOLO, YOLOWorld + def ultralytics_predict( model_path: str | Path, @@ -39,14 +44,14 @@ def ultralytics_predict( return PredictOutput(bboxes=bboxes, masks=masks, preview=preview) -def apply_classes(model, model_path: str | Path, classes: str): +def apply_classes(model: YOLO | YOLOWorld, model_path: str | Path, classes: str): if not classes or "-world" not in Path(model_path).stem: return parsed = [c.strip() for c in classes.split(",")] model.set_classes(parsed) -def mask_to_pil(masks, shape: tuple[int, int]) -> list[Image.Image]: +def mask_to_pil(masks: torch.Tensor, shape: tuple[int, int]) -> list[Image.Image]: """ Parameters ---------- @@ -54,7 +59,7 @@ def mask_to_pil(masks, shape: tuple[int, int]) -> list[Image.Image]: The device can be CUDA, but `to_pil_image` takes care of that. shape: tuple[int, int] - (width, height) of the original image + (W, H) of the original image """ n = masks.shape[0] return [to_pil_image(masks[i], mode="L").resize(shape) for i in range(n)] diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index 747798c..8fa8352 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -695,6 +695,7 @@ def _postprocess_image_inner( predictor = ultralytics_predict ad_model = self.get_ad_model(args.ad_model) kwargs["device"] = self.ultralytics_device + kwargs["classes"] = args.ad_model_classes with change_torch_load(): pred = predictor(ad_model, pp.image, args.ad_confidence, **kwargs) diff --git a/tests/test_ultralytics.py b/tests/test_ultralytics.py index ad855ad..f3885de 100644 --- a/tests/test_ultralytics.py +++ b/tests/test_ultralytics.py @@ -28,7 +28,7 @@ def test_ultralytics_hf_models(sample_image: Image.Image, model_name: str): def test_yolo_world_default(sample_image: Image.Image): - result = ultralytics_predict("yolov8l-world.pt", sample_image) + result = ultralytics_predict("yolov8x-world.pt", sample_image) assert result.preview is not None @@ -44,5 +44,5 @@ def test_yolo_world_default(sample_image: Image.Image): ], ) def test_yolo_world(sample_image2: Image.Image, klass: str): - result = ultralytics_predict("yolov8l-world.pt", sample_image2, classes=klass) + result = ultralytics_predict("yolov8x-world.pt", sample_image2, classes=klass) assert result.preview is not None From 9d46fcd714fc84dd8489e1b396e7b6b4ba73bdda Mon Sep 17 00:00:00 2001 From: Dowon Date: Fri, 1 Mar 2024 11:02:42 +0900 Subject: [PATCH 23/27] fix: download yolo world from hf --- adetailer/common.py | 8 +++++--- tests/test_ultralytics.py | 10 +++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/adetailer/common.py b/adetailer/common.py index 5a8b1ab..317d9c6 100644 --- a/adetailer/common.py +++ b/adetailer/common.py @@ -9,7 +9,7 @@ from PIL import Image, ImageDraw from rich import print -repo_id = "Bingsu/adetailer" +REPO_ID = "Bingsu/adetailer" _download_failed = False @@ -20,7 +20,7 @@ class PredictOutput: preview: Optional[Image.Image] = None -def hf_download(file: str): +def hf_download(file: str, repo_id: str = REPO_ID) -> str | None: global _download_failed if _download_failed: @@ -56,11 +56,13 @@ def get_models( "hand_yolov8n.pt": hf_download("hand_yolov8n.pt"), "person_yolov8n-seg.pt": hf_download("person_yolov8n-seg.pt"), "person_yolov8s-seg.pt": hf_download("person_yolov8s-seg.pt"), + "yolov8x-world.pt": hf_download( + "yolov8x-world.pt", repo_id="Bingsu/yolo-world-mirror" + ), } ) models.update( { - "yolov8x-world.pt": "yolov8x-world.pt", "mediapipe_face_full": None, "mediapipe_face_short": None, "mediapipe_face_mesh": None, diff --git a/tests/test_ultralytics.py b/tests/test_ultralytics.py index f3885de..fe40097 100644 --- a/tests/test_ultralytics.py +++ b/tests/test_ultralytics.py @@ -4,8 +4,6 @@ from adetailer.ultralytics import ultralytics_predict -repo_id = "Bingsu/adetailer" - @pytest.mark.parametrize( "model_name", @@ -22,13 +20,14 @@ ], ) def test_ultralytics_hf_models(sample_image: Image.Image, model_name: str): - model_path = hf_hub_download(repo_id, model_name) + model_path = hf_hub_download("Bingsu/adetailer", model_name) result = ultralytics_predict(model_path, sample_image) assert result.preview is not None def test_yolo_world_default(sample_image: Image.Image): - result = ultralytics_predict("yolov8x-world.pt", sample_image) + model_path = hf_hub_download("Bingsu/yolo-world-mirror", "yolov8x-world.pt") + result = ultralytics_predict(model_path, sample_image) assert result.preview is not None @@ -44,5 +43,6 @@ def test_yolo_world_default(sample_image: Image.Image): ], ) def test_yolo_world(sample_image2: Image.Image, klass: str): - result = ultralytics_predict("yolov8x-world.pt", sample_image2, classes=klass) + model_path = hf_hub_download("Bingsu/yolo-world-mirror", "yolov8x-world.pt") + result = ultralytics_predict(model_path, sample_image2, classes=klass) assert result.preview is not None From aa9e4553e9b65bd7b0bd94f8256c2daa5606bd71 Mon Sep 17 00:00:00 2001 From: Dowon Date: Fri, 1 Mar 2024 11:34:26 +0900 Subject: [PATCH 24/27] fix: ad classes ui --- adetailer/ui.py | 59 +++++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/adetailer/ui.py b/adetailer/ui.py index cfd665d..04379d5 100644 --- a/adetailer/ui.py +++ b/adetailer/ui.py @@ -88,7 +88,8 @@ def on_generate_click(state: dict, *values: Any): def on_ad_model_update(model: str): if "-world" in model: return gr.update( - visible=True, placeholder="Comma separated class names to detect." + visible=True, + placeholder="Comma separated class names to detect, ex: 'person,cat'. default: COCO 80 classes", ) return gr.update(visible=False, placeholder="") @@ -169,35 +170,39 @@ def one_ui_group(n: int, is_img2img: bool, webui_info: WebuiInfo): w = Widgets() eid = partial(elem_id, n=n, is_img2img=is_img2img) - with gr.Row(): - model_choices = ( - [*webui_info.ad_model_list, "None"] - if n == 0 - else ["None", *webui_info.ad_model_list] - ) + with gr.Group(): + with gr.Row(): + model_choices = ( + [*webui_info.ad_model_list, "None"] + if n == 0 + else ["None", *webui_info.ad_model_list] + ) - w.ad_model = gr.Dropdown( - label="ADetailer model" + suffix(n), - choices=model_choices, - value=model_choices[0], - visible=True, - type="value", - elem_id=eid("ad_model"), - ) + w.ad_model = gr.Dropdown( + label="ADetailer model" + suffix(n), + choices=model_choices, + value=model_choices[0], + visible=True, + type="value", + elem_id=eid("ad_model"), + ) - w.ad_model_classes = gr.Textbox( - label="ADetailer model classes" + suffix(n), - value="", - visible=False, - elem_id=eid("ad_classes"), - ) + with gr.Row(): + w.ad_model_classes = gr.Textbox( + label="ADetailer model classes" + suffix(n), + value="", + visible=False, + elem_id=eid("ad_classes"), + ) - w.ad_model_classes.change( - on_ad_model_update, - inputs=w.ad_model, - outputs=w.ad_model_classes, - queue=False, - ) + w.ad_model.change( + on_ad_model_update, + inputs=w.ad_model, + outputs=w.ad_model_classes, + queue=False, + ) + + gr.HTML("
") with gr.Group(): with gr.Row(elem_id=eid("ad_toprow_prompt")): From 55557c7959e57216fa358f737854fe12e71ece21 Mon Sep 17 00:00:00 2001 From: Dowon Date: Fri, 1 Mar 2024 11:34:54 +0900 Subject: [PATCH 25/27] fix: ignore empty string when parsing --- adetailer/ultralytics.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/adetailer/ultralytics.py b/adetailer/ultralytics.py index de19ff7..0c9fb86 100644 --- a/adetailer/ultralytics.py +++ b/adetailer/ultralytics.py @@ -47,8 +47,9 @@ def ultralytics_predict( def apply_classes(model: YOLO | YOLOWorld, model_path: str | Path, classes: str): if not classes or "-world" not in Path(model_path).stem: return - parsed = [c.strip() for c in classes.split(",")] - model.set_classes(parsed) + parsed = [c.strip() for c in classes.split(",") if c.strip()] + if parsed: + model.set_classes(parsed) def mask_to_pil(masks: torch.Tensor, shape: tuple[int, int]) -> list[Image.Image]: From a310294ad9dd8b7d9127fcd3de8729c96202fe2d Mon Sep 17 00:00:00 2001 From: Dowon Date: Fri, 1 Mar 2024 11:44:09 +0900 Subject: [PATCH 26/27] docs: update readme ad model classes, controlnet Passthrough, ultralytics --- README.md | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 3a00116..06ce487 100644 --- a/README.md +++ b/README.md @@ -18,15 +18,16 @@ You can now install it directly from the Extensions tab. ![image](https://i.imgur.com/g6GdRBT.png) -You **DON'T** need to download any model from huggingface. +You **DON'T** need to download any base model from huggingface. ## Options -| Model, Prompts | | | -| --------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------- | -| ADetailer model | Determine what to detect. | `None` = disable | -| ADetailer prompt, negative prompt | Prompts and negative prompts to apply | If left blank, it will use the same as the input. | -| Skip img2img | Skip img2img. In practice, this works by changing the step count of img2img to 1. | img2img only | +| Model, Prompts | | | +| --------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| ADetailer model | Determine what to detect. | `None` = disable | +| ADetailer model classes | Comma separated class names to detect. only available when using YOLO World models | If blank, use default values.
default = [COCO 80 classes](https://github.com/ultralytics/ultralytics/blob/main/ultralytics/cfg/datasets/coco.yaml) | +| ADetailer prompt, negative prompt | Prompts and negative prompts to apply | If left blank, it will use the same as the input. | +| Skip img2img | Skip img2img. In practice, this works by changing the step count of img2img to 1. | img2img only | | Detection | | | | ------------------------------------ | -------------------------------------------------------------------------------------------- | ------------ | @@ -52,7 +53,9 @@ Each option corresponds to a corresponding option on the inpaint tab. Therefore, You can use the ControlNet extension if you have ControlNet installed and ControlNet models. -Support `inpaint, scribble, lineart, openpose, tile` controlnet models. Once you choose a model, the preprocessor is set automatically. It works separately from the model set by the Controlnet extension. +Support `inpaint, scribble, lineart, openpose, tile, depth` controlnet models. Once you choose a model, the preprocessor is set automatically. It works separately from the model set by the Controlnet extension. + +If you select `Passthrough`, the controlnet settings you set outside of ADetailer will be used. ## Advanced Options @@ -80,11 +83,15 @@ API request example: [wiki/API](https://github.com/Bing-su/adetailer/wiki/API) | mediapipe_face_short | realistic face | - | - | | mediapipe_face_mesh | realistic face | - | - | -The yolo models can be found on huggingface [Bingsu/adetailer](https://huggingface.co/Bingsu/adetailer). +The YOLO models can be found on huggingface [Bingsu/adetailer](https://huggingface.co/Bingsu/adetailer). + +For a detailed description of the YOLO8 model, see: https://docs.ultralytics.com/models/yolov8/#overview + +YOLO World model: https://docs.ultralytics.com/models/yolo-world/ ### Additional Model -Put your [ultralytics](https://github.com/ultralytics/ultralytics) yolo model in `webui/models/adetailer`. The model name should end with `.pt` or `.pth`. +Put your [ultralytics](https://github.com/ultralytics/ultralytics) yolo model in `webui/models/adetailer`. The model name should end with `.pt`. It must be a bbox detection or segment model and use all label. From 302540dac11c9ef2c627633261b4f19087bc2161 Mon Sep 17 00:00:00 2001 From: Dowon Date: Fri, 1 Mar 2024 11:50:57 +0900 Subject: [PATCH 27/27] chore: v24.3.0 --- CHANGELOG.md | 11 +++++++++++ adetailer/__version__.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d109778..cb36ab2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 2024-03-01 + +- v24.3.0 +- YOLO World 모델 추가: 가장 큰 yolov8x-world.pt 모델만 기본적으로 선택할 수 있게 함. +- lllyasviel/stable-diffusion-webui-forge에서 컨트롤넷을 사용가능하게 함 (PR #517) +- 기본 스크립트 목록에 soft_inpainting 추가 (https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14208) + - 기존에 설치한 사람에게 소급적용되지는 않음 + +- 감지모델에 대한 간단한 pytest 추가함 +- xyz grid 컨트롤넷 모델 옵션에 `Passthrough` 추가함 + ## 2024-01-23 - v24.1.2 diff --git a/adetailer/__version__.py b/adetailer/__version__.py index 9d86149..98cbf02 100644 --- a/adetailer/__version__.py +++ b/adetailer/__version__.py @@ -1 +1 @@ -__version__ = "24.3.0.dev0" +__version__ = "24.3.0"