Merge branch 'dev'

This commit is contained in:
Dowon
2024-05-19 17:03:45 +09:00
14 changed files with 159 additions and 49 deletions

View File

@@ -8,6 +8,7 @@ on:
jobs:
lint:
permissions:
issues: write
pull-requests: write
runs-on: ubuntu-latest

1
.gitignore vendored
View File

@@ -194,3 +194,4 @@ pyrightconfig.json
# End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode
*.ipynb
node_modules

View File

@@ -16,13 +16,13 @@ repos:
- id: end-of-file-fixer
- id: mixed-line-ending
- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v4.0.0-alpha.8"
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.2.5
hooks:
- id: prettier
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.7
rev: v0.4.4
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]

View File

@@ -1,5 +1,15 @@
# Changelog
## 2024-05-19
- v24.5.0
- 개별 탭 활성화/비활성화 체크박스 추가
- ad_extra_model_dir 옵션에 |로 구분된 여러 디렉토리를 추가할 수 있게 함 (PR #596)
- `hypertile` 빌트인 확장이 지원되도록 함
- 항상 cond 캐시를 비움
- 설치 스크립트에 uv를 사용함
- mediapipe 최소 버전을 올려 protobuf 버전 4를 사용하게 함
## 2024-04-17
- v24.4.2

View File

@@ -10,6 +10,7 @@ tasks:
cmds:
- echo "$PYTHON"
- echo "$WEBUI"
- echo "$UV_PYTHON"
silent: true
launch:
@@ -24,8 +25,8 @@ tasks:
update:
cmds:
- "{{.PYTHON}} -m pip install -U ultralytics mediapipe ruff pre-commit black devtools pytest"
- "{{.PYTHON}} -m uv pip install -U ultralytics mediapipe ruff pre-commit black devtools pytest"
update-torch:
cmds:
- "{{.PYTHON}} -m pip install -U torch torchvision torchaudio -f https://download.pytorch.org/whl/torch_stable.html"
- "{{.PYTHON}} -m uv pip install -U torch torchvision torchaudio -f https://download.pytorch.org/whl/torch_stable.html"

View File

@@ -28,3 +28,7 @@ def get_i(p) -> int:
bs = p.batch_size
i = p.batch_index
return it * bs + i
def is_skip_img2img(p) -> bool:
return getattr(p, "_ad_skip_img2img", False)

View File

@@ -12,6 +12,7 @@ from rich.table import Table
from rich.traceback import Traceback
from adetailer.__version__ import __version__
from adetailer.args import ADetailerArgs
def processing(*args: Any) -> dict[str, Any]:
@@ -66,23 +67,30 @@ def sd_models() -> dict[str, str]:
def ad_args(*args: Any) -> dict[str, Any]:
ad_args = [
arg
for arg in args
if isinstance(arg, dict) and arg.get("ad_model", "None") != "None"
]
ad_args = []
for arg in args:
if not isinstance(arg, dict):
continue
try:
a = ADetailerArgs(**arg)
except ValueError:
continue
if not a.need_skip():
ad_args.append(a)
if not ad_args:
return {}
arg0 = ad_args[0]
is_api = arg0.get("is_api", True)
return {
"version": __version__,
"ad_model": arg0["ad_model"],
"ad_prompt": arg0.get("ad_prompt", ""),
"ad_negative_prompt": arg0.get("ad_negative_prompt", ""),
"ad_controlnet_model": arg0.get("ad_controlnet_model", "None"),
"is_api": type(is_api) is not tuple,
"ad_model": arg0.ad_model,
"ad_prompt": arg0.ad_prompt,
"ad_negative_prompt": arg0.ad_negative_prompt,
"ad_controlnet_model": arg0.ad_controlnet_model,
"is_api": arg0.is_api,
}

View File

@@ -162,7 +162,7 @@ def adui(
states.append(state)
infotext_fields.extend(infofields)
# components: [bool, dict, dict, ...]
# components: [bool, bool, dict, dict, ...]
components = [ad_enable, ad_skip_img2img, *states]
return components, infotext_fields
@@ -171,26 +171,35 @@ def one_ui_group(n: int, is_img2img: bool, webui_info: WebuiInfo):
w = Widgets()
eid = partial(elem_id, n=n, is_img2img=is_img2img)
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]
with gr.Row(variant="compact"):
w.ad_tap_enable = gr.Checkbox(
label=f"Enable this tap ({ordinal(n + 1)})",
value=True,
visible=True,
elem_id=eid("ad_tap_enable"),
)
with gr.Row():
w.ad_model = gr.Dropdown(
label="ADetailer model" + suffix(n),
label="ADetailer detector" + suffix(n),
choices=model_choices,
value=model_choices[0],
visible=True,
type="value",
elem_id=eid("ad_model"),
info="Select a model to use for detection.",
)
with gr.Row():
w.ad_model_classes = gr.Textbox(
label="ADetailer model classes" + suffix(n),
label="ADetailer detector classes" + suffix(n),
value="",
visible=False,
elem_id=eid("ad_classes"),
@@ -354,6 +363,7 @@ def mask_preprocessing(w: Widgets, n: int, is_img2img: bool):
choices=MASK_MERGE_INVERT,
value="None",
elem_id=eid("ad_mask_merge_invert"),
info="None: do nothing, Merge: merge masks, Merge and Invert: merge all masks and invert",
)

View File

@@ -1 +1 @@
__version__ = "24.4.2"
__version__ = "24.5.0"

View File

@@ -55,6 +55,7 @@ class ArgsList(UserList):
class ADetailerArgs(BaseModel, extra=Extra.forbid):
ad_model: str = "None"
ad_model_classes: str = ""
ad_tap_enable: bool = True
ad_prompt: str = ""
ad_negative_prompt: str = ""
ad_confidence: confloat(ge=0.0, le=1.0) = 0.3
@@ -119,7 +120,7 @@ class ADetailerArgs(BaseModel, extra=Extra.forbid):
p.pop(k, None)
def extra_params(self, suffix: str = "") -> dict[str, Any]:
if self.ad_model == "None":
if self.need_skip():
return {}
p = {name: getattr(self, attr) for attr, name in ALL_ARGS}
@@ -128,6 +129,7 @@ class ADetailerArgs(BaseModel, extra=Extra.forbid):
ppop("ADetailer model classes")
ppop("ADetailer prompt")
ppop("ADetailer negative prompt")
p.pop("ADetailer tap enable", None) # always pop
ppop("ADetailer mask only top k largest", cond=0)
ppop("ADetailer mask min ratio", cond=0.0)
ppop("ADetailer mask max ratio", cond=1.0)
@@ -200,10 +202,17 @@ class ADetailerArgs(BaseModel, extra=Extra.forbid):
return p
def is_mediapipe(self) -> bool:
return self.ad_model.lower().startswith("mediapipe")
def need_skip(self) -> bool:
return self.ad_model == "None" or self.ad_tap_enable is False
_all_args = [
("ad_model", "ADetailer model"),
("ad_model_classes", "ADetailer model classes"),
("ad_tap_enable", "ADetailer tap enable"),
("ad_prompt", "ADetailer prompt"),
("ad_negative_prompt", "ADetailer negative prompt"),
("ad_confidence", "ADetailer confidence"),
@@ -262,6 +271,8 @@ _script_default = (
"wildcards",
"lora_block_weight",
"negpip",
"soft_inpainting",
)
SCRIPT_DEFAULT = ",".join(sorted(_script_default))
_builtin_script = ("soft_inpainting", "hypertile_script")
BUILTIN_SCRIPT = ",".join(sorted(_builtin_script))

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import importlib.util
import os
import subprocess
import sys
from importlib.metadata import version # python >= 3.8
@@ -38,19 +39,29 @@ def is_installed(
def run_pip(*args):
subprocess.run([sys.executable, "-m", "pip", "install", *args])
subprocess.run([sys.executable, "-m", "pip", "install", *args], check=False)
def run_uv_pip(*args):
subprocess.run([sys.executable, "-m", "uv", "pip", "install", *args], check=False)
def install():
deps = [
# requirements
("ultralytics", "8.1.29", None),
("mediapipe", "0.10.9", None),
("ultralytics", "8.2.0", None),
("mediapipe", "0.10.13", None),
("rich", "13.0.0", None),
# mediapipe
("protobuf", "3.20", "3.9999"),
("protobuf", "4.25.3", "4.9999"),
]
if not is_installed("uv", "0.1.44", None):
run_pip("uv>=0.1.44")
os.environ["UV_PYTHON"] = sys.executable
pkgs = []
for pkg, low, high in deps:
if not is_installed(pkg, low, high):
if low and high:
@@ -61,8 +72,9 @@ def install():
cmd = f"{pkg}<={high}"
else:
cmd = pkg
pkgs.append(cmd)
run_pip("-U", cmd)
run_uv_pip(*pkgs)
try:

View File

@@ -2,11 +2,11 @@
name = "adetailer"
description = "An object detection and auto-mask extension for stable diffusion webui."
authors = [{ name = "dowon", email = "ks2515@naver.com" }]
requires-python = ">=3.8,<3.13"
requires-python = ">=3.8"
readme = "README.md"
license = { text = "AGPL-3.0" }
dependencies = [
"ultralytics>=8.1",
"ultralytics>=8.2",
"mediapipe>=0.10",
"pydantic<3",
"rich>=13",

View File

@@ -26,9 +26,12 @@ from aaaaaa.p_method import (
get_i,
is_img2img_inpaint,
is_inpaint_only_masked,
is_skip_img2img,
need_call_postprocess,
need_call_process,
)
from aaaaaa.traceback import rich_traceback
from aaaaaa.ui import WebuiInfo, adui, ordinal, suffix
from adetailer import (
AFTER_DETAILER,
__version__,
@@ -36,7 +39,13 @@ from adetailer import (
mediapipe_predict,
ultralytics_predict,
)
from adetailer.args import BBOX_SORTBY, SCRIPT_DEFAULT, ADetailerArgs, SkipImg2ImgOrig
from adetailer.args import (
BBOX_SORTBY,
BUILTIN_SCRIPT,
SCRIPT_DEFAULT,
ADetailerArgs,
SkipImg2ImgOrig,
)
from adetailer.common import PredictOutput, ensure_pil_image, safe_mkdir
from adetailer.mask import (
filter_by_ratio,
@@ -46,8 +55,6 @@ from adetailer.mask import (
mask_preprocess,
sort_bboxes,
)
from adetailer.traceback import rich_traceback
from adetailer.ui import WebuiInfo, adui, ordinal, suffix
from controlnet_ext import (
CNHijackRestore,
ControlNetExt,
@@ -76,10 +83,10 @@ no_huggingface = getattr(cmd_opts, "ad_no_huggingface", False)
adetailer_dir = Path(paths.models_path, "adetailer")
safe_mkdir(adetailer_dir)
extra_models_dir = shared.opts.data.get("ad_extra_models_dir", "")
extra_models_dirs = shared.opts.data.get("ad_extra_models_dir", "")
model_mapping = get_models(
adetailer_dir,
extra_models_dir,
*extra_models_dirs.split("|"),
huggingface=not no_huggingface,
)
@@ -180,7 +187,13 @@ class AfterDetailerScript(scripts.Script):
return False
ad_enabled = args_[0] if isinstance(args_[0], bool) else True
not_none = any(arg.get("ad_model", "None") != "None" for arg in arg_list)
pydantic_args = []
for arg in arg_list:
try:
pydantic_args.append(ADetailerArgs(**arg))
except ValueError: # noqa: PERF203
continue
not_none = not all(arg.need_skip() for arg in pydantic_args)
return ad_enabled and not_none
def set_skip_img2img(self, p, *args_) -> None:
@@ -441,10 +454,11 @@ class AfterDetailerScript(scripts.Script):
if not ad_only_seleted_scripts:
return script_runner, script_args
ad_script_names = opts.data.get("ad_script_names", SCRIPT_DEFAULT)
ad_script_names_string: str = opts.data.get("ad_script_names", SCRIPT_DEFAULT)
ad_script_names = ad_script_names_string.split(",") + BUILTIN_SCRIPT.split(",")
script_names_set = {
name
for script_name in ad_script_names.split(",")
for script_name in ad_script_names
for name in (script_name, script_name.strip())
}
@@ -625,7 +639,7 @@ class AfterDetailerScript(scripts.Script):
@staticmethod
def get_i2i_init_image(p, pp):
if getattr(p, "_ad_skip_img2img", False):
if is_skip_img2img(p):
return p.init_images[0]
return pp.image
@@ -649,7 +663,7 @@ class AfterDetailerScript(scripts.Script):
mask = ImageChops.invert(mask)
mask = create_binary_mask(mask)
if getattr(p, "_ad_skip_img2img", False):
if is_skip_img2img(p):
if hasattr(p, "init_images") and p.init_images:
width, height = p.init_images[0].size
else:
@@ -712,7 +726,7 @@ class AfterDetailerScript(scripts.Script):
seed, subseed = self.get_seed(p)
ad_prompts, ad_negatives = self.get_prompt(p, args)
is_mediapipe = args.ad_model.lower().startswith("mediapipe")
is_mediapipe = args.is_mediapipe()
kwargs = {}
if is_mediapipe:
@@ -762,6 +776,8 @@ class AfterDetailerScript(scripts.Script):
p2.seed = self.get_each_tap_seed(seed, j)
p2.subseed = self.get_each_tap_seed(subseed, j)
p2.cached_c = [None, None]
p2.cached_uc = [None, None]
try:
processed = process_images(p2)
except NansException as e:
@@ -800,11 +816,11 @@ class AfterDetailerScript(scripts.Script):
is_processed = False
with CNHijackRestore(), pause_total_tqdm(), cn_allow_script_control():
for n, args in enumerate(arg_list):
if args.ad_model == "None":
if args.need_skip():
continue
is_processed |= self._postprocess_image_inner(p, pp, args, n=n)
if is_processed and not getattr(p, "_ad_skip_img2img", False):
if is_processed and not is_skip_img2img(p):
self.save_image(
p, init_image, condition="ad_save_images_before", suffix="-ad-before"
)
@@ -849,10 +865,12 @@ def on_ui_settings():
"ad_extra_models_dir",
shared.OptionInfo(
default="",
label="Extra path to scan adetailer models",
label="Extra paths to scan adetailer models seperated by vertical bars(|)",
component=gr.Textbox,
section=section,
),
)
.info("eg. path\\to\\models|C:\\path\\to\\models|another/path/to/models")
.needs_reload_ui(),
)
shared.opts.add_option(

View File

@@ -1,5 +1,7 @@
from __future__ import annotations
import pytest
from adetailer.args import ALL_ARGS, ADetailerArgs
@@ -12,3 +14,35 @@ def test_all_args() -> None:
if attr == "is_api":
continue
assert attr in ALL_ARGS.attrs, attr
@pytest.mark.parametrize(
("ad_model", "expect"),
[("mediapipe_face_full", True), ("face_yolov8n.pt", False)],
)
def test_is_mediapipe(ad_model: str, expect: bool) -> None:
args = ADetailerArgs(ad_model=ad_model)
assert args.is_mediapipe() is expect
@pytest.mark.parametrize(
("ad_model", "expect"),
[("mediapipe_face_full", False), ("face_yolov8n.pt", False), ("None", True)],
)
def test_need_skip(ad_model: str, expect: bool) -> None:
args = ADetailerArgs(ad_model=ad_model)
assert args.need_skip() is expect
@pytest.mark.parametrize(
("ad_model", "ad_tap_enable", "expect"),
[
("face_yolov8n.pt", False, True),
("mediapipe_face_full", False, True),
("None", True, True),
("ace_yolov8s.pt", True, False),
],
)
def test_need_skip_tap_enable(ad_model: str, ad_tap_enable: bool, expect: bool) -> None:
args = ADetailerArgs(ad_model=ad_model, ad_tap_enable=ad_tap_enable)
assert args.need_skip() is expect