mirror of
https://github.com/Bing-su/adetailer.git
synced 2026-01-26 19:29:54 +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
|
label: Steps to reproduce
|
||||||
description: |
|
description: |
|
||||||
Description of how we can reproduce this issue.
|
Description of how we can reproduce this issue.
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
@@ -38,7 +36,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Console logs, from start to end.
|
label: Console logs, from start to end.
|
||||||
description: |
|
description: |
|
||||||
The full console log of your terminal.
|
The FULL console log of your terminal.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Python ...
|
Python ...
|
||||||
Version: ...
|
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:
|
jobs:
|
||||||
test:
|
test:
|
||||||
name: test
|
name: test
|
||||||
runs-on: macos-14
|
runs-on: macos-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version:
|
python-version:
|
||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
uv pip install --system . pytest
|
uv pip install --system ".[test]"
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: pytest -v
|
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
|
# End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode
|
||||||
*.ipynb
|
*.ipynb
|
||||||
node_modules
|
node_modules
|
||||||
|
modules
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
ci:
|
ci:
|
||||||
autoupdate_branch: "dev"
|
autoupdate_branch: "dev"
|
||||||
|
|
||||||
|
exclude: ^modules/
|
||||||
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.6.0
|
rev: v4.6.0
|
||||||
@@ -22,7 +24,7 @@ repos:
|
|||||||
- id: prettier
|
- id: prettier
|
||||||
|
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.5.6
|
rev: v0.5.7
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: [--fix, --exit-non-zero-on-fix]
|
args: [--fix, --exit-non-zero-on-fix]
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# 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
|
## 2024-08-03
|
||||||
|
|
||||||
- v24.8.0
|
- v24.8.0
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ tasks:
|
|||||||
|
|
||||||
update:
|
update:
|
||||||
cmds:
|
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:
|
update-torch:
|
||||||
cmds:
|
cmds:
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ from copy import copy
|
|||||||
from typing import TYPE_CHECKING, Any, Union
|
from typing import TYPE_CHECKING, Any, Union
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
|
from PIL import Image
|
||||||
|
from typing_extensions import Protocol
|
||||||
|
|
||||||
from modules import safe
|
from modules import safe
|
||||||
from modules.shared import opts
|
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]:
|
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)}
|
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")
|
table.add_column("Value")
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
value = repr(value)
|
value = repr(value) # noqa: PLW2901
|
||||||
table.add_row(key, value)
|
table.add_row(key, value)
|
||||||
|
|
||||||
return table
|
return table
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ def one_ui_group(n: int, is_img2img: bool, webui_info: WebuiInfo):
|
|||||||
with gr.Group():
|
with gr.Group():
|
||||||
with gr.Row(elem_id=eid("ad_toprow_prompt")):
|
with gr.Row(elem_id=eid("ad_toprow_prompt")):
|
||||||
w.ad_prompt = gr.Textbox(
|
w.ad_prompt = gr.Textbox(
|
||||||
|
value="",
|
||||||
label="ad_prompt" + suffix(n),
|
label="ad_prompt" + suffix(n),
|
||||||
show_label=False,
|
show_label=False,
|
||||||
lines=3,
|
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")):
|
with gr.Row(elem_id=eid("ad_toprow_negative_prompt")):
|
||||||
w.ad_negative_prompt = gr.Textbox(
|
w.ad_negative_prompt = gr.Textbox(
|
||||||
|
value="",
|
||||||
label="ad_negative_prompt" + suffix(n),
|
label="ad_negative_prompt" + suffix(n),
|
||||||
show_label=False,
|
show_label=False,
|
||||||
lines=2,
|
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)
|
eid = partial(elem_id, n=n, is_img2img=is_img2img)
|
||||||
|
|
||||||
with gr.Group():
|
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 collections import UserList
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
from functools import cached_property, partial
|
from functools import cached_property, partial
|
||||||
from typing import Any, Literal, NamedTuple, Optional
|
from typing import Any, Literal, NamedTuple, Optional
|
||||||
|
|
||||||
@@ -262,6 +263,7 @@ BBOX_SORTBY = [
|
|||||||
"Position (center to edge)",
|
"Position (center to edge)",
|
||||||
"Area (large to small)",
|
"Area (large to small)",
|
||||||
]
|
]
|
||||||
|
|
||||||
MASK_MERGE_INVERT = ["None", "Merge", "Merge and Invert"]
|
MASK_MERGE_INVERT = ["None", "Merge", "Merge and Invert"]
|
||||||
|
|
||||||
_script_default = (
|
_script_default = (
|
||||||
@@ -276,3 +278,16 @@ SCRIPT_DEFAULT = ",".join(sorted(_script_default))
|
|||||||
|
|
||||||
_builtin_script = ("soft_inpainting", "hypertile_script")
|
_builtin_script = ("soft_inpainting", "hypertile_script")
|
||||||
BUILTIN_SCRIPT = ",".join(sorted(_builtin_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 = []
|
bboxes = []
|
||||||
for mask in masks:
|
for mask in masks:
|
||||||
mask = mask.resize(shape)
|
mask = mask.resize(shape) # noqa: PLW2901
|
||||||
bbox = mask.getbbox()
|
bbox = mask.getbbox()
|
||||||
if bbox is not None:
|
if bbox is not None:
|
||||||
bboxes.append(list(bbox))
|
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()
|
models = self.external_cn.get_models()
|
||||||
self.cn_models.extend(m for m in models if cn_model_regex.search(m))
|
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,
|
self,
|
||||||
p,
|
p,
|
||||||
model: str,
|
model: str,
|
||||||
@@ -78,6 +78,7 @@ class ControlNetExt:
|
|||||||
guidance_start=guidance_start,
|
guidance_start=guidance_start,
|
||||||
guidance_end=guidance_end,
|
guidance_end=guidance_end,
|
||||||
pixel_perfect=True,
|
pixel_perfect=True,
|
||||||
|
enabled=True,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class ControlNetExt:
|
|||||||
def init_controlnet(self):
|
def init_controlnet(self):
|
||||||
self.cn_available = True
|
self.cn_available = True
|
||||||
|
|
||||||
def update_scripts_args(
|
def update_scripts_args( # noqa: PLR0913
|
||||||
self,
|
self,
|
||||||
p,
|
p,
|
||||||
model: str,
|
model: str,
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ dynamic = ["version"]
|
|||||||
[project.urls]
|
[project.urls]
|
||||||
repository = "https://github.com/Bing-su/adetailer"
|
repository = "https://github.com/Bing-su/adetailer"
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = ["ruff", "pre-commit", "devtools"]
|
||||||
|
test = ["pytest", "hypothesis"]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["hatchling"]
|
requires = ["hatchling"]
|
||||||
build-backend = "hatchling.build"
|
build-backend = "hatchling.build"
|
||||||
@@ -40,6 +44,7 @@ known_first_party = ["launch", "modules"]
|
|||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
target-version = "py39"
|
target-version = "py39"
|
||||||
|
extend-exclude = ["modules"]
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
select = [
|
select = [
|
||||||
@@ -56,6 +61,7 @@ select = [
|
|||||||
"N",
|
"N",
|
||||||
"PD",
|
"PD",
|
||||||
"PERF",
|
"PERF",
|
||||||
|
"PL",
|
||||||
"PIE",
|
"PIE",
|
||||||
"PT",
|
"PT",
|
||||||
"PTH",
|
"PTH",
|
||||||
@@ -67,7 +73,7 @@ select = [
|
|||||||
"UP",
|
"UP",
|
||||||
"W",
|
"W",
|
||||||
]
|
]
|
||||||
ignore = ["B905", "E501"]
|
ignore = ["B905", "E501", "PLR2004", "PLW0603"]
|
||||||
unfixable = ["F401"]
|
unfixable = ["F401"]
|
||||||
|
|
||||||
[tool.ruff.lint.isort]
|
[tool.ruff.lint.isort]
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import platform
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
from collections.abc import Sequence
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from textwrap import dedent
|
|
||||||
from typing import TYPE_CHECKING, Any, NamedTuple, cast
|
from typing import TYPE_CHECKING, Any, NamedTuple, cast
|
||||||
|
|
||||||
import gradio as gr
|
import gradio as gr
|
||||||
@@ -17,6 +17,7 @@ from rich import print
|
|||||||
import modules
|
import modules
|
||||||
from aaaaaa.conditional import create_binary_mask, schedulers
|
from aaaaaa.conditional import create_binary_mask, schedulers
|
||||||
from aaaaaa.helper import (
|
from aaaaaa.helper import (
|
||||||
|
PPImage,
|
||||||
change_torch_load,
|
change_torch_load,
|
||||||
copy_extra_params,
|
copy_extra_params,
|
||||||
pause_total_tqdm,
|
pause_total_tqdm,
|
||||||
@@ -42,8 +43,10 @@ from adetailer import (
|
|||||||
from adetailer.args import (
|
from adetailer.args import (
|
||||||
BBOX_SORTBY,
|
BBOX_SORTBY,
|
||||||
BUILTIN_SCRIPT,
|
BUILTIN_SCRIPT,
|
||||||
|
INPAINT_BBOX_MATCH_MODES,
|
||||||
SCRIPT_DEFAULT,
|
SCRIPT_DEFAULT,
|
||||||
ADetailerArgs,
|
ADetailerArgs,
|
||||||
|
InpaintBBoxMatchMode,
|
||||||
SkipImg2ImgOrig,
|
SkipImg2ImgOrig,
|
||||||
)
|
)
|
||||||
from adetailer.common import PredictOutput, ensure_pil_image, safe_mkdir
|
from adetailer.common import PredictOutput, ensure_pil_image, safe_mkdir
|
||||||
@@ -55,6 +58,7 @@ from adetailer.mask import (
|
|||||||
mask_preprocess,
|
mask_preprocess,
|
||||||
sort_bboxes,
|
sort_bboxes,
|
||||||
)
|
)
|
||||||
|
from adetailer.opts import dynamic_denoise_strength, optimal_crop_size
|
||||||
from controlnet_ext import (
|
from controlnet_ext import (
|
||||||
CNHijackRestore,
|
CNHijackRestore,
|
||||||
ControlNetExt,
|
ControlNetExt,
|
||||||
@@ -172,25 +176,23 @@ class AfterDetailerScript(scripts.Script):
|
|||||||
guidance_end=args.ad_controlnet_guidance_end,
|
guidance_end=args.ad_controlnet_guidance_end,
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_ad_enabled(self, *args_) -> bool:
|
def is_ad_enabled(self, *args) -> bool:
|
||||||
arg_list = [arg for arg in args_ if isinstance(arg, dict)]
|
arg_list = [arg for arg in args if isinstance(arg, dict)]
|
||||||
if not args_ or not arg_list:
|
if not arg_list:
|
||||||
message = f"""
|
|
||||||
[-] ADetailer: Invalid arguments passed to ADetailer.
|
|
||||||
input: {args_!r}
|
|
||||||
ADetailer disabled.
|
|
||||||
"""
|
|
||||||
print(dedent(message), file=sys.stderr)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
ad_enabled = args_[0] if isinstance(args_[0], bool) else True
|
ad_enabled = args[0] if isinstance(args[0], bool) else True
|
||||||
pydantic_args = []
|
|
||||||
|
not_none = False
|
||||||
for arg in arg_list:
|
for arg in arg_list:
|
||||||
try:
|
try:
|
||||||
pydantic_args.append(ADetailerArgs(**arg))
|
adarg = ADetailerArgs(**arg)
|
||||||
except ValueError: # noqa: PERF203
|
except ValueError: # noqa: PERF203
|
||||||
continue
|
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
|
return ad_enabled and not_none
|
||||||
|
|
||||||
def set_skip_img2img(self, p, *args_) -> None:
|
def set_skip_img2img(self, p, *args_) -> None:
|
||||||
@@ -227,9 +229,6 @@ class AfterDetailerScript(scripts.Script):
|
|||||||
p.height = 128
|
p.height = 128
|
||||||
|
|
||||||
def get_args(self, p, *args_) -> list[ADetailerArgs]:
|
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)]
|
args = [arg for arg in args_ if isinstance(arg, dict)]
|
||||||
|
|
||||||
if not args:
|
if not args:
|
||||||
@@ -239,21 +238,21 @@ class AfterDetailerScript(scripts.Script):
|
|||||||
if hasattr(p, "_ad_xyz"):
|
if hasattr(p, "_ad_xyz"):
|
||||||
args[0] = {**args[0], **p._ad_xyz}
|
args[0] = {**args[0], **p._ad_xyz}
|
||||||
|
|
||||||
all_inputs = []
|
all_inputs: list[ADetailerArgs] = []
|
||||||
|
|
||||||
for n, arg_dict in enumerate(args, 1):
|
for n, arg_dict in enumerate(args, 1):
|
||||||
try:
|
try:
|
||||||
inp = ADetailerArgs(**arg_dict)
|
inp = ADetailerArgs(**arg_dict)
|
||||||
except ValueError as e:
|
except ValueError:
|
||||||
msg = f"[-] ADetailer: ValidationError when validating {ordinal(n)} arguments"
|
msg = f"[-] ADetailer: ValidationError when validating {ordinal(n)} arguments:"
|
||||||
if hasattr(e, "add_note"):
|
print(msg, arg_dict, file=sys.stderr)
|
||||||
e.add_note(msg)
|
continue
|
||||||
else:
|
|
||||||
print(msg, file=sys.stderr)
|
|
||||||
raise
|
|
||||||
|
|
||||||
all_inputs.append(inp)
|
all_inputs.append(inp)
|
||||||
|
|
||||||
|
if not all_inputs:
|
||||||
|
msg = "[-] ADetailer: No valid arguments found."
|
||||||
|
raise ValueError(msg)
|
||||||
return all_inputs
|
return all_inputs
|
||||||
|
|
||||||
def extra_params(self, arg_list: list[ADetailerArgs]) -> dict:
|
def extra_params(self, arg_list: list[ADetailerArgs]) -> dict:
|
||||||
@@ -565,9 +564,14 @@ class AfterDetailerScript(scripts.Script):
|
|||||||
seed, _ = self.get_seed(p)
|
seed, _ = self.get_seed(p)
|
||||||
|
|
||||||
if opts.data.get(condition, False):
|
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(
|
images.save_image(
|
||||||
image=image,
|
image=image,
|
||||||
path=p.outpath_samples,
|
path=ad_save_images_dir,
|
||||||
basename="",
|
basename="",
|
||||||
seed=seed,
|
seed=seed,
|
||||||
prompt=save_prompt,
|
prompt=save_prompt,
|
||||||
@@ -633,7 +637,7 @@ class AfterDetailerScript(scripts.Script):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_i2i_init_image(p, pp):
|
def get_i2i_init_image(p, pp: PPImage):
|
||||||
if is_skip_img2img(p):
|
if is_skip_img2img(p):
|
||||||
return p.init_images[0]
|
return p.init_images[0]
|
||||||
return pp.image
|
return pp.image
|
||||||
@@ -654,6 +658,7 @@ class AfterDetailerScript(scripts.Script):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_image_mask(p) -> Image.Image:
|
def get_image_mask(p) -> Image.Image:
|
||||||
mask = p.image_mask
|
mask = p.image_mask
|
||||||
|
mask = ensure_pil_image(mask, "L")
|
||||||
if getattr(p, "inpainting_mask_invert", False):
|
if getattr(p, "inpainting_mask_invert", False):
|
||||||
mask = ImageChops.invert(mask)
|
mask = ImageChops.invert(mask)
|
||||||
mask = create_binary_mask(mask)
|
mask = create_binary_mask(mask)
|
||||||
@@ -668,6 +673,93 @@ class AfterDetailerScript(scripts.Script):
|
|||||||
width, height = p.width, p.height
|
width, height = p.width, p.height
|
||||||
return images.resize_image(p.resize_mode, mask, width, 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
|
@rich_traceback
|
||||||
def process(self, p, *args_):
|
def process(self, p, *args_):
|
||||||
if getattr(p, "_ad_disabled", False):
|
if getattr(p, "_ad_disabled", False):
|
||||||
@@ -703,7 +795,7 @@ class AfterDetailerScript(scripts.Script):
|
|||||||
p.extra_generation_params.update(extra_params)
|
p.extra_generation_params.update(extra_params)
|
||||||
|
|
||||||
def _postprocess_image_inner(
|
def _postprocess_image_inner(
|
||||||
self, p, pp, args: ADetailerArgs, *, n: int = 0
|
self, p, pp: PPImage, args: ADetailerArgs, *, n: int = 0
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns
|
Returns
|
||||||
@@ -718,23 +810,23 @@ class AfterDetailerScript(scripts.Script):
|
|||||||
i = get_i(p)
|
i = get_i(p)
|
||||||
|
|
||||||
i2i = self.get_i2i_p(p, args, pp.image)
|
i2i = self.get_i2i_p(p, args, pp.image)
|
||||||
seed, subseed = self.get_seed(p)
|
|
||||||
ad_prompts, ad_negatives = self.get_prompt(p, args)
|
ad_prompts, ad_negatives = self.get_prompt(p, args)
|
||||||
|
|
||||||
is_mediapipe = args.is_mediapipe()
|
is_mediapipe = args.is_mediapipe()
|
||||||
|
|
||||||
kwargs = {}
|
|
||||||
if is_mediapipe:
|
if is_mediapipe:
|
||||||
predictor = mediapipe_predict
|
pred = mediapipe_predict(args.ad_model, pp.image, args.ad_confidence)
|
||||||
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
|
|
||||||
|
|
||||||
with change_torch_load():
|
else:
|
||||||
pred = predictor(ad_model, pp.image, args.ad_confidence, **kwargs)
|
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:
|
if pred.preview is None:
|
||||||
print(
|
print(
|
||||||
@@ -768,11 +860,8 @@ class AfterDetailerScript(scripts.Script):
|
|||||||
if re.match(r"^\s*\[SKIP\]\s*$", p2.prompt):
|
if re.match(r"^\s*\[SKIP\]\s*$", p2.prompt):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
p2.seed = self.get_each_tab_seed(seed, j)
|
self.fix_p2(p, p2, pp, args, pred, j)
|
||||||
p2.subseed = self.get_each_tab_seed(subseed, j)
|
|
||||||
|
|
||||||
p2.cached_c = [None, None]
|
|
||||||
p2.cached_uc = [None, None]
|
|
||||||
try:
|
try:
|
||||||
processed = process_images(p2)
|
processed = process_images(p2)
|
||||||
except NansException as e:
|
except NansException as e:
|
||||||
@@ -782,6 +871,10 @@ class AfterDetailerScript(scripts.Script):
|
|||||||
finally:
|
finally:
|
||||||
p2.close()
|
p2.close()
|
||||||
|
|
||||||
|
if not processed.images:
|
||||||
|
processed = None
|
||||||
|
break
|
||||||
|
|
||||||
self.compare_prompt(p.extra_generation_params, processed, n=n)
|
self.compare_prompt(p.extra_generation_params, processed, n=n)
|
||||||
p2 = copy(i2i)
|
p2 = copy(i2i)
|
||||||
p2.init_images = [processed.images[0]]
|
p2.init_images = [processed.images[0]]
|
||||||
@@ -793,7 +886,7 @@ class AfterDetailerScript(scripts.Script):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@rich_traceback
|
@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_):
|
if getattr(p, "_ad_disabled", False) or not self.is_ad_enabled(*args_):
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -864,6 +957,16 @@ def on_ui_settings():
|
|||||||
.needs_reload_ui(),
|
.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(
|
shared.opts.add_option(
|
||||||
"ad_save_previews",
|
"ad_save_previews",
|
||||||
shared.OptionInfo(False, "Save mask previews", section=section),
|
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
|
# 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