mirror of
https://github.com/Bing-su/adetailer.git
synced 2026-01-26 11:19:53 +00:00
Merge branch 'dev'
This commit is contained in:
4
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -26,8 +26,6 @@ body:
|
||||
label: Steps to reproduce
|
||||
description: |
|
||||
Description of how we can reproduce this issue.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
@@ -38,7 +36,7 @@ body:
|
||||
attributes:
|
||||
label: Console logs, from start to end.
|
||||
description: |
|
||||
The full console log of your terminal.
|
||||
The FULL console log of your terminal.
|
||||
placeholder: |
|
||||
Python ...
|
||||
Version: ...
|
||||
|
||||
23
.github/workflows/lgtm.yml
vendored
23
.github/workflows/lgtm.yml
vendored
@@ -1,23 +0,0 @@
|
||||
name: Empirical Implementation of JDD
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body: |
|
||||

|
||||
|
||||
LGTM
|
||||
reactions: hooray
|
||||
4
.github/workflows/pypi.yml
vendored
4
.github/workflows/pypi.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
jobs:
|
||||
test:
|
||||
name: test
|
||||
runs-on: macos-14
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
uv pip install --system . pytest
|
||||
uv pip install --system ".[test]"
|
||||
|
||||
- name: Run tests
|
||||
run: pytest -v
|
||||
|
||||
34
.github/workflows/test.yml
vendored
Normal file
34
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Test on PR
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "adetailer/**.py"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test on python ${{ matrix.python-version }}
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- uses: yezz123/setup-uv@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
uv pip install --system ".[test]"
|
||||
|
||||
- name: Run tests
|
||||
run: pytest -v
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -195,3 +195,4 @@ pyrightconfig.json
|
||||
# End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode
|
||||
*.ipynb
|
||||
node_modules
|
||||
modules
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
ci:
|
||||
autoupdate_branch: "dev"
|
||||
|
||||
exclude: ^modules/
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
@@ -22,7 +24,7 @@ repos:
|
||||
- id: prettier
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.5.6
|
||||
rev: v0.5.7
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## 2024-09-02
|
||||
|
||||
- v24.9.0
|
||||
- Dynamic Denoising, Inpaint bbox sizing 기능 (PR #678)
|
||||
- `ad_save_images_dir` 옵션 추가 - ad 이미지를 저장하는 장소 지정 (PR #689)
|
||||
|
||||
- forge와 관련된 버그 몇 개 수정
|
||||
- pydantic validation에 실패해도 에러를 일으키지 않고 넘어가도록 수정
|
||||
|
||||
## 2024-08-03
|
||||
|
||||
- v24.8.0
|
||||
|
||||
@@ -25,7 +25,7 @@ tasks:
|
||||
|
||||
update:
|
||||
cmds:
|
||||
- "{{.PYTHON}} -m uv 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 hypothesis"
|
||||
|
||||
update-torch:
|
||||
cmds:
|
||||
|
||||
@@ -5,6 +5,8 @@ from copy import copy
|
||||
from typing import TYPE_CHECKING, Any, Union
|
||||
|
||||
import torch
|
||||
from PIL import Image
|
||||
from typing_extensions import Protocol
|
||||
|
||||
from modules import safe
|
||||
from modules.shared import opts
|
||||
@@ -57,3 +59,7 @@ def preserve_prompts(p: PT):
|
||||
|
||||
def copy_extra_params(extra_params: dict[str, Any]) -> dict[str, Any]:
|
||||
return {k: v for k, v in extra_params.items() if not callable(v)}
|
||||
|
||||
|
||||
class PPImage(Protocol):
|
||||
image: Image.Image
|
||||
|
||||
@@ -133,7 +133,7 @@ def get_table(title: str, data: dict[str, Any]) -> Table:
|
||||
table.add_column("Value")
|
||||
for key, value in data.items():
|
||||
if not isinstance(value, str):
|
||||
value = repr(value)
|
||||
value = repr(value) # noqa: PLW2901
|
||||
table.add_row(key, value)
|
||||
|
||||
return table
|
||||
|
||||
@@ -219,6 +219,7 @@ def one_ui_group(n: int, is_img2img: bool, webui_info: WebuiInfo):
|
||||
with gr.Group():
|
||||
with gr.Row(elem_id=eid("ad_toprow_prompt")):
|
||||
w.ad_prompt = gr.Textbox(
|
||||
value="",
|
||||
label="ad_prompt" + suffix(n),
|
||||
show_label=False,
|
||||
lines=3,
|
||||
@@ -230,6 +231,7 @@ def one_ui_group(n: int, is_img2img: bool, webui_info: WebuiInfo):
|
||||
|
||||
with gr.Row(elem_id=eid("ad_toprow_negative_prompt")):
|
||||
w.ad_negative_prompt = gr.Textbox(
|
||||
value="",
|
||||
label="ad_negative_prompt" + suffix(n),
|
||||
show_label=False,
|
||||
lines=2,
|
||||
@@ -369,7 +371,7 @@ def mask_preprocessing(w: Widgets, n: int, is_img2img: bool):
|
||||
)
|
||||
|
||||
|
||||
def inpainting(w: Widgets, n: int, is_img2img: bool, webui_info: WebuiInfo):
|
||||
def inpainting(w: Widgets, n: int, is_img2img: bool, webui_info: WebuiInfo): # noqa: PLR0915
|
||||
eid = partial(elem_id, n=n, is_img2img=is_img2img)
|
||||
|
||||
with gr.Group():
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "24.8.0"
|
||||
__version__ = "24.9.0"
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from collections import UserList
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from functools import cached_property, partial
|
||||
from typing import Any, Literal, NamedTuple, Optional
|
||||
|
||||
@@ -262,6 +263,7 @@ BBOX_SORTBY = [
|
||||
"Position (center to edge)",
|
||||
"Area (large to small)",
|
||||
]
|
||||
|
||||
MASK_MERGE_INVERT = ["None", "Merge", "Merge and Invert"]
|
||||
|
||||
_script_default = (
|
||||
@@ -276,3 +278,16 @@ SCRIPT_DEFAULT = ",".join(sorted(_script_default))
|
||||
|
||||
_builtin_script = ("soft_inpainting", "hypertile_script")
|
||||
BUILTIN_SCRIPT = ",".join(sorted(_builtin_script))
|
||||
|
||||
|
||||
class InpaintBBoxMatchMode(Enum):
|
||||
OFF = "Off"
|
||||
STRICT = "Strict (SDXL only)"
|
||||
FREE = "Free"
|
||||
|
||||
|
||||
INPAINT_BBOX_MATCH_MODES = [
|
||||
InpaintBBoxMatchMode.OFF.value,
|
||||
InpaintBBoxMatchMode.STRICT.value,
|
||||
InpaintBBoxMatchMode.FREE.value,
|
||||
]
|
||||
|
||||
@@ -163,7 +163,7 @@ def create_bbox_from_mask(
|
||||
"""
|
||||
bboxes = []
|
||||
for mask in masks:
|
||||
mask = mask.resize(shape)
|
||||
mask = mask.resize(shape) # noqa: PLW2901
|
||||
bbox = mask.getbbox()
|
||||
if bbox is not None:
|
||||
bboxes.append(list(bbox))
|
||||
|
||||
101
adetailer/opts.py
Normal file
101
adetailer/opts.py
Normal file
@@ -0,0 +1,101 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import ClassVar, TypeVar
|
||||
|
||||
import numpy as np
|
||||
|
||||
T = TypeVar("T", int, float)
|
||||
|
||||
|
||||
def dynamic_denoise_strength(
|
||||
denoise_power: float,
|
||||
denoise_strength: float,
|
||||
bbox: Sequence[T],
|
||||
image_size: tuple[int, int],
|
||||
) -> float:
|
||||
if len(bbox) != 4:
|
||||
msg = f"bbox length must be 4, got {len(bbox)}"
|
||||
raise ValueError(msg)
|
||||
|
||||
if np.isclose(denoise_power, 0.0) or len(bbox) != 4:
|
||||
return denoise_strength
|
||||
|
||||
width, height = image_size
|
||||
|
||||
image_pixels = width * height
|
||||
bbox_pixels = (bbox[2] - bbox[0]) * (bbox[3] - bbox[1])
|
||||
|
||||
normalized_area = bbox_pixels / image_pixels
|
||||
denoise_modifier = (1.0 - normalized_area) ** denoise_power
|
||||
|
||||
return denoise_strength * denoise_modifier
|
||||
|
||||
|
||||
class _OptimalCropSize:
|
||||
sdxl_res: ClassVar[list[tuple[int, int]]] = [
|
||||
(1024, 1024),
|
||||
(1152, 896),
|
||||
(896, 1152),
|
||||
(1216, 832),
|
||||
(832, 1216),
|
||||
(1344, 768),
|
||||
(768, 1344),
|
||||
(1536, 640),
|
||||
(640, 1536),
|
||||
]
|
||||
|
||||
def sdxl(
|
||||
self, inpaint_width: int, inpaint_height: int, bbox: Sequence[T]
|
||||
) -> tuple[int, int]:
|
||||
if len(bbox) != 4:
|
||||
msg = f"bbox length must be 4, got {len(bbox)}"
|
||||
raise ValueError(msg)
|
||||
|
||||
bbox_width = bbox[2] - bbox[0]
|
||||
bbox_height = bbox[3] - bbox[1]
|
||||
bbox_aspect_ratio = bbox_width / bbox_height
|
||||
|
||||
resolutions = [
|
||||
res
|
||||
for res in self.sdxl_res
|
||||
if (res[0] >= bbox_width and res[1] >= bbox_height)
|
||||
and (res[0] >= inpaint_width or res[1] >= inpaint_height)
|
||||
]
|
||||
|
||||
if not resolutions:
|
||||
return inpaint_width, inpaint_height
|
||||
|
||||
return min(
|
||||
resolutions,
|
||||
key=lambda res: abs((res[0] / res[1]) - bbox_aspect_ratio),
|
||||
)
|
||||
|
||||
def free(
|
||||
self, inpaint_width: int, inpaint_height: int, bbox: Sequence[T]
|
||||
) -> tuple[int, int]:
|
||||
if len(bbox) != 4:
|
||||
msg = f"bbox length must be 4, got {len(bbox)}"
|
||||
raise ValueError(msg)
|
||||
|
||||
bbox_width = bbox[2] - bbox[0]
|
||||
bbox_height = bbox[3] - bbox[1]
|
||||
bbox_aspect_ratio = bbox_width / bbox_height
|
||||
|
||||
scale_size = max(inpaint_width, inpaint_height)
|
||||
|
||||
if bbox_aspect_ratio > 1:
|
||||
optimal_width = scale_size
|
||||
optimal_height = scale_size / bbox_aspect_ratio
|
||||
else:
|
||||
optimal_width = scale_size * bbox_aspect_ratio
|
||||
optimal_height = scale_size
|
||||
|
||||
# Round up to the nearest multiple of 8 to make the dimensions friendly for upscaling/diffusion.
|
||||
optimal_width = ((optimal_width + 8 - 1) // 8) * 8
|
||||
optimal_height = ((optimal_height + 8 - 1) // 8) * 8
|
||||
|
||||
return int(optimal_width), int(optimal_height)
|
||||
|
||||
|
||||
optimal_crop_size = _OptimalCropSize()
|
||||
@@ -49,7 +49,7 @@ class ControlNetExt:
|
||||
models = self.external_cn.get_models()
|
||||
self.cn_models.extend(m for m in models if cn_model_regex.search(m))
|
||||
|
||||
def update_scripts_args(
|
||||
def update_scripts_args( # noqa: PLR0913
|
||||
self,
|
||||
p,
|
||||
model: str,
|
||||
@@ -78,6 +78,7 @@ class ControlNetExt:
|
||||
guidance_start=guidance_start,
|
||||
guidance_end=guidance_end,
|
||||
pixel_perfect=True,
|
||||
enabled=True,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ class ControlNetExt:
|
||||
def init_controlnet(self):
|
||||
self.cn_available = True
|
||||
|
||||
def update_scripts_args(
|
||||
def update_scripts_args( # noqa: PLR0913
|
||||
self,
|
||||
p,
|
||||
model: str,
|
||||
|
||||
@@ -27,6 +27,10 @@ dynamic = ["version"]
|
||||
[project.urls]
|
||||
repository = "https://github.com/Bing-su/adetailer"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = ["ruff", "pre-commit", "devtools"]
|
||||
test = ["pytest", "hypothesis"]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
@@ -40,6 +44,7 @@ known_first_party = ["launch", "modules"]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py39"
|
||||
extend-exclude = ["modules"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
@@ -56,6 +61,7 @@ select = [
|
||||
"N",
|
||||
"PD",
|
||||
"PERF",
|
||||
"PL",
|
||||
"PIE",
|
||||
"PT",
|
||||
"PTH",
|
||||
@@ -67,7 +73,7 @@ select = [
|
||||
"UP",
|
||||
"W",
|
||||
]
|
||||
ignore = ["B905", "E501"]
|
||||
ignore = ["B905", "E501", "PLR2004", "PLW0603"]
|
||||
unfixable = ["F401"]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
|
||||
@@ -4,10 +4,10 @@ import platform
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
from collections.abc import Sequence
|
||||
from copy import copy
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple, cast
|
||||
|
||||
import gradio as gr
|
||||
@@ -17,6 +17,7 @@ from rich import print
|
||||
import modules
|
||||
from aaaaaa.conditional import create_binary_mask, schedulers
|
||||
from aaaaaa.helper import (
|
||||
PPImage,
|
||||
change_torch_load,
|
||||
copy_extra_params,
|
||||
pause_total_tqdm,
|
||||
@@ -42,8 +43,10 @@ from adetailer import (
|
||||
from adetailer.args import (
|
||||
BBOX_SORTBY,
|
||||
BUILTIN_SCRIPT,
|
||||
INPAINT_BBOX_MATCH_MODES,
|
||||
SCRIPT_DEFAULT,
|
||||
ADetailerArgs,
|
||||
InpaintBBoxMatchMode,
|
||||
SkipImg2ImgOrig,
|
||||
)
|
||||
from adetailer.common import PredictOutput, ensure_pil_image, safe_mkdir
|
||||
@@ -55,6 +58,7 @@ from adetailer.mask import (
|
||||
mask_preprocess,
|
||||
sort_bboxes,
|
||||
)
|
||||
from adetailer.opts import dynamic_denoise_strength, optimal_crop_size
|
||||
from controlnet_ext import (
|
||||
CNHijackRestore,
|
||||
ControlNetExt,
|
||||
@@ -172,25 +176,23 @@ class AfterDetailerScript(scripts.Script):
|
||||
guidance_end=args.ad_controlnet_guidance_end,
|
||||
)
|
||||
|
||||
def is_ad_enabled(self, *args_) -> bool:
|
||||
arg_list = [arg for arg in args_ if isinstance(arg, dict)]
|
||||
if not args_ or not arg_list:
|
||||
message = f"""
|
||||
[-] ADetailer: Invalid arguments passed to ADetailer.
|
||||
input: {args_!r}
|
||||
ADetailer disabled.
|
||||
"""
|
||||
print(dedent(message), file=sys.stderr)
|
||||
def is_ad_enabled(self, *args) -> bool:
|
||||
arg_list = [arg for arg in args if isinstance(arg, dict)]
|
||||
if not arg_list:
|
||||
return False
|
||||
|
||||
ad_enabled = args_[0] if isinstance(args_[0], bool) else True
|
||||
pydantic_args = []
|
||||
ad_enabled = args[0] if isinstance(args[0], bool) else True
|
||||
|
||||
not_none = False
|
||||
for arg in arg_list:
|
||||
try:
|
||||
pydantic_args.append(ADetailerArgs(**arg))
|
||||
adarg = ADetailerArgs(**arg)
|
||||
except ValueError: # noqa: PERF203
|
||||
continue
|
||||
not_none = not all(arg.need_skip() for arg in pydantic_args)
|
||||
else:
|
||||
if not adarg.need_skip():
|
||||
not_none = True
|
||||
break
|
||||
return ad_enabled and not_none
|
||||
|
||||
def set_skip_img2img(self, p, *args_) -> None:
|
||||
@@ -227,9 +229,6 @@ class AfterDetailerScript(scripts.Script):
|
||||
p.height = 128
|
||||
|
||||
def get_args(self, p, *args_) -> list[ADetailerArgs]:
|
||||
"""
|
||||
`args_` is at least 1 in length by `is_ad_enabled` immediately above
|
||||
"""
|
||||
args = [arg for arg in args_ if isinstance(arg, dict)]
|
||||
|
||||
if not args:
|
||||
@@ -239,21 +238,21 @@ class AfterDetailerScript(scripts.Script):
|
||||
if hasattr(p, "_ad_xyz"):
|
||||
args[0] = {**args[0], **p._ad_xyz}
|
||||
|
||||
all_inputs = []
|
||||
all_inputs: list[ADetailerArgs] = []
|
||||
|
||||
for n, arg_dict in enumerate(args, 1):
|
||||
try:
|
||||
inp = ADetailerArgs(**arg_dict)
|
||||
except ValueError as e:
|
||||
msg = f"[-] ADetailer: ValidationError when validating {ordinal(n)} arguments"
|
||||
if hasattr(e, "add_note"):
|
||||
e.add_note(msg)
|
||||
else:
|
||||
print(msg, file=sys.stderr)
|
||||
raise
|
||||
except ValueError:
|
||||
msg = f"[-] ADetailer: ValidationError when validating {ordinal(n)} arguments:"
|
||||
print(msg, arg_dict, file=sys.stderr)
|
||||
continue
|
||||
|
||||
all_inputs.append(inp)
|
||||
|
||||
if not all_inputs:
|
||||
msg = "[-] ADetailer: No valid arguments found."
|
||||
raise ValueError(msg)
|
||||
return all_inputs
|
||||
|
||||
def extra_params(self, arg_list: list[ADetailerArgs]) -> dict:
|
||||
@@ -565,9 +564,14 @@ class AfterDetailerScript(scripts.Script):
|
||||
seed, _ = self.get_seed(p)
|
||||
|
||||
if opts.data.get(condition, False):
|
||||
ad_save_images_dir: str = opts.data.get("ad_save_images_dir", "")
|
||||
|
||||
if not ad_save_images_dir.strip():
|
||||
ad_save_images_dir = p.outpath_samples
|
||||
|
||||
images.save_image(
|
||||
image=image,
|
||||
path=p.outpath_samples,
|
||||
path=ad_save_images_dir,
|
||||
basename="",
|
||||
seed=seed,
|
||||
prompt=save_prompt,
|
||||
@@ -633,7 +637,7 @@ class AfterDetailerScript(scripts.Script):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_i2i_init_image(p, pp):
|
||||
def get_i2i_init_image(p, pp: PPImage):
|
||||
if is_skip_img2img(p):
|
||||
return p.init_images[0]
|
||||
return pp.image
|
||||
@@ -654,6 +658,7 @@ class AfterDetailerScript(scripts.Script):
|
||||
@staticmethod
|
||||
def get_image_mask(p) -> Image.Image:
|
||||
mask = p.image_mask
|
||||
mask = ensure_pil_image(mask, "L")
|
||||
if getattr(p, "inpainting_mask_invert", False):
|
||||
mask = ImageChops.invert(mask)
|
||||
mask = create_binary_mask(mask)
|
||||
@@ -668,6 +673,93 @@ class AfterDetailerScript(scripts.Script):
|
||||
width, height = p.width, p.height
|
||||
return images.resize_image(p.resize_mode, mask, width, height)
|
||||
|
||||
@staticmethod
|
||||
def get_dynamic_denoise_strength(
|
||||
denoise_strength: float, bbox: Sequence[Any], image_size: tuple[int, int]
|
||||
):
|
||||
denoise_power = opts.data.get("ad_dynamic_denoise_power", 0)
|
||||
if denoise_power == 0:
|
||||
return denoise_strength
|
||||
|
||||
modified_strength = dynamic_denoise_strength(
|
||||
denoise_power=denoise_power,
|
||||
denoise_strength=denoise_strength,
|
||||
bbox=bbox,
|
||||
image_size=image_size,
|
||||
)
|
||||
|
||||
print(
|
||||
f"[-] ADetailer: dynamic denoising -- {denoise_strength:.2f} -> {modified_strength:.2f}"
|
||||
)
|
||||
|
||||
return modified_strength
|
||||
|
||||
@staticmethod
|
||||
def get_optimal_crop_image_size(
|
||||
inpaint_width: int, inpaint_height: int, bbox: Sequence[Any]
|
||||
) -> tuple[int, int]:
|
||||
calculate_optimal_crop = opts.data.get(
|
||||
"ad_match_inpaint_bbox_size", InpaintBBoxMatchMode.OFF.value
|
||||
)
|
||||
|
||||
optimal_resolution: tuple[int, int] | None = None
|
||||
|
||||
# Off
|
||||
if calculate_optimal_crop == InpaintBBoxMatchMode.OFF.value:
|
||||
return (inpaint_width, inpaint_height)
|
||||
|
||||
# Strict (SDXL only)
|
||||
if calculate_optimal_crop == InpaintBBoxMatchMode.STRICT.value:
|
||||
if not shared.sd_model.is_sdxl:
|
||||
msg = "[-] ADetailer: strict inpaint bounding box size matching is only available for SDXL. Use Free mode instead."
|
||||
print(msg)
|
||||
return (inpaint_width, inpaint_height)
|
||||
|
||||
optimal_resolution = optimal_crop_size.sdxl(
|
||||
inpaint_width, inpaint_height, bbox
|
||||
)
|
||||
|
||||
# Free
|
||||
elif calculate_optimal_crop == InpaintBBoxMatchMode.FREE.value:
|
||||
optimal_resolution = optimal_crop_size.free(
|
||||
inpaint_width, inpaint_height, bbox
|
||||
)
|
||||
|
||||
if optimal_resolution is None:
|
||||
msg = "[-] ADetailer: unsupported inpaint bounding box match mode. Original inpainting dimensions will be used."
|
||||
print(msg)
|
||||
return (inpaint_width, inpaint_height)
|
||||
|
||||
# Only use optimal dimensions if they're different enough to current inpaint dimensions.
|
||||
if (
|
||||
abs(optimal_resolution[0] - inpaint_width) > inpaint_width * 0.1
|
||||
or abs(optimal_resolution[1] - inpaint_height) > inpaint_height * 0.1
|
||||
):
|
||||
print(
|
||||
f"[-] ADetailer: inpaint dimensions optimized -- {inpaint_width}x{inpaint_height} -> {optimal_resolution[0]}x{optimal_resolution[1]}"
|
||||
)
|
||||
|
||||
return optimal_resolution
|
||||
|
||||
def fix_p2( # noqa: PLR0913
|
||||
self, p, p2, pp: PPImage, args: ADetailerArgs, pred: PredictOutput, j: int
|
||||
):
|
||||
seed, subseed = self.get_seed(p)
|
||||
p2.seed = self.get_each_tab_seed(seed, j)
|
||||
p2.subseed = self.get_each_tab_seed(subseed, j)
|
||||
p2.denoising_strength = self.get_dynamic_denoise_strength(
|
||||
p2.denoising_strength, pred.bboxes[j], pp.image.size
|
||||
)
|
||||
|
||||
p2.cached_c = [None, None]
|
||||
p2.cached_uc = [None, None]
|
||||
|
||||
# Don't override user-defined dimensions.
|
||||
if not args.ad_use_inpaint_width_height:
|
||||
p2.width, p2.height = self.get_optimal_crop_image_size(
|
||||
p2.width, p2.height, pred.bboxes[j]
|
||||
)
|
||||
|
||||
@rich_traceback
|
||||
def process(self, p, *args_):
|
||||
if getattr(p, "_ad_disabled", False):
|
||||
@@ -703,7 +795,7 @@ class AfterDetailerScript(scripts.Script):
|
||||
p.extra_generation_params.update(extra_params)
|
||||
|
||||
def _postprocess_image_inner(
|
||||
self, p, pp, args: ADetailerArgs, *, n: int = 0
|
||||
self, p, pp: PPImage, args: ADetailerArgs, *, n: int = 0
|
||||
) -> bool:
|
||||
"""
|
||||
Returns
|
||||
@@ -718,23 +810,23 @@ class AfterDetailerScript(scripts.Script):
|
||||
i = get_i(p)
|
||||
|
||||
i2i = self.get_i2i_p(p, args, pp.image)
|
||||
seed, subseed = self.get_seed(p)
|
||||
ad_prompts, ad_negatives = self.get_prompt(p, args)
|
||||
|
||||
is_mediapipe = args.is_mediapipe()
|
||||
|
||||
kwargs = {}
|
||||
if is_mediapipe:
|
||||
predictor = mediapipe_predict
|
||||
ad_model = args.ad_model
|
||||
else:
|
||||
predictor = ultralytics_predict
|
||||
ad_model = self.get_ad_model(args.ad_model)
|
||||
kwargs["device"] = self.ultralytics_device
|
||||
kwargs["classes"] = args.ad_model_classes
|
||||
pred = mediapipe_predict(args.ad_model, pp.image, args.ad_confidence)
|
||||
|
||||
with change_torch_load():
|
||||
pred = predictor(ad_model, pp.image, args.ad_confidence, **kwargs)
|
||||
else:
|
||||
with change_torch_load():
|
||||
ad_model = self.get_ad_model(args.ad_model)
|
||||
pred = ultralytics_predict(
|
||||
ad_model,
|
||||
image=pp.image,
|
||||
confidence=args.ad_confidence,
|
||||
device=self.ultralytics_device,
|
||||
classes=args.ad_model_classes,
|
||||
)
|
||||
|
||||
if pred.preview is None:
|
||||
print(
|
||||
@@ -768,11 +860,8 @@ class AfterDetailerScript(scripts.Script):
|
||||
if re.match(r"^\s*\[SKIP\]\s*$", p2.prompt):
|
||||
continue
|
||||
|
||||
p2.seed = self.get_each_tab_seed(seed, j)
|
||||
p2.subseed = self.get_each_tab_seed(subseed, j)
|
||||
self.fix_p2(p, p2, pp, args, pred, j)
|
||||
|
||||
p2.cached_c = [None, None]
|
||||
p2.cached_uc = [None, None]
|
||||
try:
|
||||
processed = process_images(p2)
|
||||
except NansException as e:
|
||||
@@ -782,6 +871,10 @@ class AfterDetailerScript(scripts.Script):
|
||||
finally:
|
||||
p2.close()
|
||||
|
||||
if not processed.images:
|
||||
processed = None
|
||||
break
|
||||
|
||||
self.compare_prompt(p.extra_generation_params, processed, n=n)
|
||||
p2 = copy(i2i)
|
||||
p2.init_images = [processed.images[0]]
|
||||
@@ -793,7 +886,7 @@ class AfterDetailerScript(scripts.Script):
|
||||
return False
|
||||
|
||||
@rich_traceback
|
||||
def postprocess_image(self, p, pp, *args_):
|
||||
def postprocess_image(self, p, pp: PPImage, *args_):
|
||||
if getattr(p, "_ad_disabled", False) or not self.is_ad_enabled(*args_):
|
||||
return
|
||||
|
||||
@@ -864,6 +957,16 @@ def on_ui_settings():
|
||||
.needs_reload_ui(),
|
||||
)
|
||||
|
||||
shared.opts.add_option(
|
||||
"ad_save_images_dir",
|
||||
shared.OptionInfo(
|
||||
default="",
|
||||
label="Output directory for adetailer images",
|
||||
component=gr.Textbox,
|
||||
section=section,
|
||||
),
|
||||
)
|
||||
|
||||
shared.opts.add_option(
|
||||
"ad_save_previews",
|
||||
shared.OptionInfo(False, "Save mask previews", section=section),
|
||||
@@ -915,6 +1018,32 @@ def on_ui_settings():
|
||||
),
|
||||
)
|
||||
|
||||
shared.opts.add_option(
|
||||
"ad_dynamic_denoise_power",
|
||||
shared.OptionInfo(
|
||||
default=0,
|
||||
label="Power scaling for dynamic denoise strength based on bounding box size",
|
||||
component=gr.Slider,
|
||||
component_args={"minimum": -10, "maximum": 10, "step": 0.01},
|
||||
section=section,
|
||||
).info(
|
||||
"Smaller areas get higher denoising, larger areas less. Maximum denoise strength is set by 'Inpaint denoising strength'. 0 = disabled; 1 = linear; 2-4 = recommended"
|
||||
),
|
||||
)
|
||||
|
||||
shared.opts.add_option(
|
||||
"ad_match_inpaint_bbox_size",
|
||||
shared.OptionInfo(
|
||||
default=InpaintBBoxMatchMode.OFF.value, # Off
|
||||
component=gr.Radio,
|
||||
component_args={"choices": INPAINT_BBOX_MATCH_MODES},
|
||||
label="Try to match inpainting size to bounding box size, if 'Use separate width/height' is not set",
|
||||
section=section,
|
||||
).info(
|
||||
"Strict is for SDXL only, and matches exactly to trained SDXL resolutions. Free works with any model, but will use potentially unsupported dimensions."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# xyz_grid
|
||||
|
||||
|
||||
93
tests/test_opts.py
Normal file
93
tests/test_opts.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from hypothesis import assume, given
|
||||
from hypothesis import strategies as st
|
||||
|
||||
from adetailer.opts import dynamic_denoise_strength, optimal_crop_size
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("denoise_power", "denoise_strength", "bbox", "image_size", "expected_result"),
|
||||
[
|
||||
(0.001, 0.5, [0, 0, 100, 100], (200, 200), 0.4998561796520339),
|
||||
(1.5, 0.3, [0, 0, 100, 100], (200, 200), 0.1948557158514987),
|
||||
(-0.001, 0.7, [0, 0, 100, 100], (1000, 1000), 0.7000070352704507),
|
||||
(-0.5, 0.5, [0, 0, 100, 100], (1000, 1000), 0.502518907629606),
|
||||
],
|
||||
)
|
||||
def test_dynamic_denoise_strength(
|
||||
denoise_power: float,
|
||||
denoise_strength: float,
|
||||
bbox: list[int],
|
||||
image_size: tuple[int, int],
|
||||
expected_result: float,
|
||||
):
|
||||
result = dynamic_denoise_strength(denoise_power, denoise_strength, bbox, image_size)
|
||||
assert np.isclose(result, expected_result)
|
||||
|
||||
|
||||
@given(denoise_strength=st.floats(allow_nan=False))
|
||||
def test_dynamic_denoise_strength_no_bbox(denoise_strength: float):
|
||||
with pytest.raises(ValueError, match="bbox length must be 4, got 0"):
|
||||
dynamic_denoise_strength(0.5, denoise_strength, [], (1000, 1000))
|
||||
|
||||
|
||||
@given(denoise_strength=st.floats(allow_nan=False))
|
||||
def test_dynamic_denoise_strength_zero_power(denoise_strength: float):
|
||||
result = dynamic_denoise_strength(
|
||||
0.0, denoise_strength, [0, 0, 100, 100], (1000, 1000)
|
||||
)
|
||||
assert np.isclose(result, denoise_strength)
|
||||
|
||||
|
||||
@given(
|
||||
inpaint_width=st.integers(1),
|
||||
inpaint_height=st.integers(1),
|
||||
bbox=st.tuples(
|
||||
st.integers(0, 500),
|
||||
st.integers(0, 500),
|
||||
st.integers(501, 1000),
|
||||
st.integers(501, 1000),
|
||||
),
|
||||
)
|
||||
def test_optimal_crop_size_sdxl(
|
||||
inpaint_width: int, inpaint_height: int, bbox: tuple[int, int, int, int]
|
||||
):
|
||||
bbox_width = bbox[2] - bbox[0]
|
||||
bbox_height = bbox[3] - bbox[1]
|
||||
assume(bbox_width > 0 and bbox_height > 0)
|
||||
|
||||
result = optimal_crop_size.sdxl(inpaint_width, inpaint_height, bbox)
|
||||
assert (result in optimal_crop_size.sdxl_res) or result == (
|
||||
inpaint_width,
|
||||
inpaint_height,
|
||||
)
|
||||
|
||||
if result != (inpaint_width, inpaint_height):
|
||||
assert result[0] >= bbox_width
|
||||
assert result[1] >= bbox_height
|
||||
assert result[0] >= inpaint_width or result[1] >= inpaint_height
|
||||
|
||||
|
||||
@given(
|
||||
inpaint_width=st.integers(1),
|
||||
inpaint_height=st.integers(1),
|
||||
bbox=st.tuples(
|
||||
st.integers(0, 500),
|
||||
st.integers(0, 500),
|
||||
st.integers(501, 1000),
|
||||
st.integers(501, 1000),
|
||||
),
|
||||
)
|
||||
def test_optimal_crop_size_free(
|
||||
inpaint_width: int, inpaint_height: int, bbox: tuple[int, int, int, int]
|
||||
):
|
||||
bbox_width = bbox[2] - bbox[0]
|
||||
bbox_height = bbox[3] - bbox[1]
|
||||
assume(bbox_width > 0 and bbox_height > 0)
|
||||
|
||||
result = optimal_crop_size.free(inpaint_width, inpaint_height, bbox)
|
||||
assert result[0] % 8 == 0
|
||||
assert result[1] % 8 == 0
|
||||
Reference in New Issue
Block a user