From d3a38aa33123bfa041dd10a078ebabb6117d9480 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 15 Aug 2024 19:44:07 +0900 Subject: [PATCH 01/21] [pre-commit.ci] pre-commit autoupdate (#681) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.5.6 → v0.5.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.6...v0.5.7) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 667ab5d..d566db7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,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] From 38db25b483725cb713ecc9f591d6243d5b04e4eb Mon Sep 17 00:00:00 2001 From: LoganBooker Date: Sun, 18 Aug 2024 15:48:48 +1000 Subject: [PATCH 02/21] Add dynamic denoising and inpaint bbox sizing (#678) * Added dynamic denoising and inpaint bbox sizing * Dynamic denoising: Once bboxes are available from the predictor, it is possible to calculate the size of the crop region relative to the original image size. Using this value, we can modulate the "Inpaint denoising strength" based on the region size, with smaller regions getting higher denoising, and smaller areas less. * Several algorithms were tested, ultimately, a configurable power value worked best. Values between 2-4 are recommended (1 is equivalent to linear). * Try match inpaint/bbox size: Again, using bbox sizes, we can determine more optimal dimensions and aspect ratio for the inpaint width and height. * Only active for SDXL, as the model natively handles various dimensions and aspect ratios. * Don't use inpaint/bbox matching if user has specified their own width and height * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Remove math.isclose. * Remove math import * Remove unneeded formatting * Better descriptions for new features in settings. * Tidy up bbox matching, filter out more resolutions earlier * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add strict and free inpaint bbox size matching * Strict: SDXL only, same as original implementation * Free (prefer smaller or larger): Theoretically works with any model. Adjusts the inpaint region to match the aspect ratio of the bbox exactly, favouring either the smaller dimension or larger dimension of the original inpaint region. We also round up (if needed) to the closest 8 pixels to make the dimensions nicer to diffusion/upscalers. "Prefer smaller" is the better option, as it will usually very closely match the original inpaint sizes. * Also added a threshold to the difference between the original inpaint size and adjusted size, and ignore the adjusted size if it's very similar. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Use or for checking thresholds on new inpaint dimensions * Rework free mode to a single setting Should now always pick optimal dimensions --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- adetailer/args.py | 5 ++ scripts/!adetailer.py | 135 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) diff --git a/adetailer/args.py b/adetailer/args.py index ebfd2c0..abe31b1 100644 --- a/adetailer/args.py +++ b/adetailer/args.py @@ -262,6 +262,11 @@ BBOX_SORTBY = [ "Position (center to edge)", "Area (large to small)", ] +INPAINT_BBOX_MATCH_MODES = [ + "Off", + "Strict (SDXL only)", + "Free", +] MASK_MERGE_INVERT = ["None", "Merge", "Merge and Invert"] _script_default = ( diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index b655e6e..64ed90e 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -42,6 +42,7 @@ from adetailer import ( from adetailer.args import ( BBOX_SORTBY, BUILTIN_SCRIPT, + INPAINT_BBOX_MATCH_MODES, SCRIPT_DEFAULT, ADetailerArgs, SkipImg2ImgOrig, @@ -668,6 +669,103 @@ 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, bbox, image): + denoise_power = opts.data.get("ad_dynamic_denoise_power", 0) + if denoise_power == 0: + return denoise_strength + + image_pixels = image.width * image.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 + + print( + f"[-] ADetailer: dynamic denoising -- {denoise_modifier:.2f} * {denoise_strength:.2f} = {denoise_strength * denoise_modifier:.2f}" + ) + + return denoise_strength * denoise_modifier + + @staticmethod + def get_optimal_crop_image_size(inpaint_width, inpaint_height, bbox): + calculate_optimal_crop = opts.data.get("ad_match_inpaint_bbox_size", "Off") + if calculate_optimal_crop == "Off": + return (inpaint_width, inpaint_height) + + optimal_resolution = None + + bbox_width = bbox[2] - bbox[0] + bbox_height = bbox[3] - bbox[1] + bbox_aspect_ratio = bbox_width / bbox_height + + if calculate_optimal_crop == "Strict (SDXL only)": + 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) + + # Limit resolutions to those SDXL was trained on. + resolutions = [ + (1024, 1024), + (1152, 896), + (896, 1152), + (1216, 832), + (832, 1216), + (1344, 768), + (768, 1344), + (1536, 640), + (640, 1536), + ] + + # Filter resolutions smaller than bbox, and any that could result in a total pixel size smaller than the current inpaint dimensions. + resolutions = [ + res + for res in resolutions + 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) + + optimal_resolution = min( + resolutions, + key=lambda res: abs((res[0] / res[1]) - bbox_aspect_ratio), + ) + elif calculate_optimal_crop == "Free": + 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 + + optimal_resolution = (int(optimal_width), int(optimal_height)) + else: + msg = "[-] ADetailer: unsupported inpaint bounding box match mode. Original inpainting dimensions will be used." + print(msg) + + if optimal_resolution is None: + 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 + @rich_traceback def process(self, p, *args_): if getattr(p, "_ad_disabled", False): @@ -773,6 +871,17 @@ class AfterDetailerScript(scripts.Script): p2.cached_c = [None, None] p2.cached_uc = [None, None] + + p2.denoising_strength = self.get_dynamic_denoise_strength( + p2.denoising_strength, pred.bboxes[j], pp.image + ) + + # 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] + ) + try: processed = process_images(p2) except NansException as e: @@ -915,6 +1024,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="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 From 7c8c103be2a6cf0df5fa684ea4bd0f90bad14f9e Mon Sep 17 00:00:00 2001 From: Dowon Date: Wed, 21 Aug 2024 21:44:08 +0900 Subject: [PATCH 03/21] chore: include hypothesis --- Taskfile.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Taskfile.yml b/Taskfile.yml index 03a4e8a..834d82e 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -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: From a00a4cf12d0eb442dad62442a21a5b8f05c51a76 Mon Sep 17 00:00:00 2001 From: Dowon Date: Wed, 21 Aug 2024 21:44:30 +0900 Subject: [PATCH 04/21] feat: #678 opts --- adetailer/opts.py | 95 ++++++++++++++++++++++++++++++++++++++++++++++ tests/test_opts.py | 93 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 adetailer/opts.py create mode 100644 tests/test_opts.py diff --git a/adetailer/opts.py b/adetailer/opts.py new file mode 100644 index 0000000..e7f0c59 --- /dev/null +++ b/adetailer/opts.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +from collections.abc import Sequence +from typing import ClassVar, TypeVar + +import numpy as np + +T = TypeVar("T", int, float) + + +def get_dynamic_denoise_strength( + denoise_power: float, + denoise_strength: float, + bbox: Sequence[T], + image_size: tuple[int, int], +) -> float: + 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: + return inpaint_width, inpaint_height + + 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: + return inpaint_width, inpaint_height + + 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() diff --git a/tests/test_opts.py b/tests/test_opts.py new file mode 100644 index 0000000..347e3b9 --- /dev/null +++ b/tests/test_opts.py @@ -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 get_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_get_dynamic_denoise_strength( + denoise_power: float, + denoise_strength: float, + bbox: list[int], + image_size: tuple[int, int], + expected_result: float, +): + result = get_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_get_dynamic_denoise_strength_no_bbox(denoise_strength: float): + result = get_dynamic_denoise_strength(0.5, denoise_strength, [], (1000, 1000)) + assert result == denoise_strength + + +@given(denoise_strength=st.floats(allow_nan=False)) +def test_get_dynamic_denoise_strength_zero_power(denoise_strength: float): + result = get_dynamic_denoise_strength(0.0, denoise_strength, [], (1000, 1000)) + assert 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 From cc0a11393c725e9e970146b7b5dad86da2b6c332 Mon Sep 17 00:00:00 2001 From: bluelovers Date: Sat, 24 Aug 2024 12:16:12 +0800 Subject: [PATCH 05/21] feat: add `ad_save_images_dir` (#689) * feat: add `ad-save-images` https://github.com/Bing-su/adetailer/issues/688 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * feat: only enable `ad_save_images_dir` when not empty https://github.com/Bing-su/adetailer/pull/689#issuecomment-2300057990 * fix: F401 `pathlib.PurePath` imported but unused https://results.pre-commit.ci/run/github/632823919/1724229072.nxpHBQU4QvaIJ8IOFayT1Q --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- scripts/!adetailer.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index 64ed90e..53d18d7 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -566,9 +566,14 @@ class AfterDetailerScript(scripts.Script): seed, _ = self.get_seed(p) if opts.data.get(condition, False): + ad_save_images_dir = opts.data.get("ad_save_images_dir", None) + + if not bool(ad_save_images_dir and 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, @@ -973,6 +978,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), From d705c6e30a2a806970c4c4942c01dacce6507334 Mon Sep 17 00:00:00 2001 From: Dowon Date: Sat, 24 Aug 2024 13:34:20 +0900 Subject: [PATCH 06/21] feat(scripts): update pr options --- adetailer/args.py | 20 ++++++--- adetailer/opts.py | 12 +++-- scripts/!adetailer.py | 100 +++++++++++++++--------------------------- tests/test_opts.py | 22 +++++----- 4 files changed, 71 insertions(+), 83 deletions(-) diff --git a/adetailer/args.py b/adetailer/args.py index abe31b1..4efcdd7 100644 --- a/adetailer/args.py +++ b/adetailer/args.py @@ -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,11 +263,7 @@ BBOX_SORTBY = [ "Position (center to edge)", "Area (large to small)", ] -INPAINT_BBOX_MATCH_MODES = [ - "Off", - "Strict (SDXL only)", - "Free", -] + MASK_MERGE_INVERT = ["None", "Merge", "Merge and Invert"] _script_default = ( @@ -281,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, +] diff --git a/adetailer/opts.py b/adetailer/opts.py index e7f0c59..a355eac 100644 --- a/adetailer/opts.py +++ b/adetailer/opts.py @@ -8,12 +8,16 @@ import numpy as np T = TypeVar("T", int, float) -def get_dynamic_denoise_strength( +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 @@ -45,7 +49,8 @@ class _OptimalCropSize: self, inpaint_width: int, inpaint_height: int, bbox: Sequence[T] ) -> tuple[int, int]: if len(bbox) != 4: - return inpaint_width, inpaint_height + 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] @@ -70,7 +75,8 @@ class _OptimalCropSize: self, inpaint_width: int, inpaint_height: int, bbox: Sequence[T] ) -> tuple[int, int]: if len(bbox) != 4: - return inpaint_width, inpaint_height + 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] diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index 53d18d7..cfd3b0b 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -4,6 +4,7 @@ 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 @@ -45,6 +46,7 @@ from adetailer.args import ( INPAINT_BBOX_MATCH_MODES, SCRIPT_DEFAULT, ADetailerArgs, + InpaintBBoxMatchMode, SkipImg2ImgOrig, ) from adetailer.common import PredictOutput, ensure_pil_image, safe_mkdir @@ -56,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, @@ -675,84 +678,54 @@ class AfterDetailerScript(scripts.Script): return images.resize_image(p.resize_mode, mask, width, height) @staticmethod - def get_dynamic_denoise_strength(denoise_strength, bbox, image): + 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 - image_pixels = image.width * image.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 - - print( - f"[-] ADetailer: dynamic denoising -- {denoise_modifier:.2f} * {denoise_strength:.2f} = {denoise_strength * denoise_modifier:.2f}" + modified_strength = dynamic_denoise_strength( + denoise_power=denoise_power, + denoise_strength=denoise_strength, + bbox=bbox, + image_size=image_size, ) - return denoise_strength * denoise_modifier + print( + f"[-] ADetailer: dynamic denoising -- {denoise_strength:.2f} -> {modified_strength:.2f}" + ) + + return modified_strength @staticmethod - def get_optimal_crop_image_size(inpaint_width, inpaint_height, bbox): - calculate_optimal_crop = opts.data.get("ad_match_inpaint_bbox_size", "Off") - if calculate_optimal_crop == "Off": + def get_optimal_crop_image_size( + inpaint_width: int, inpaint_height: int, bbox: Sequence[Any] + ): + calculate_optimal_crop = opts.data.get( + "ad_match_inpaint_bbox_size", InpaintBBoxMatchMode.OFF.value + ) + + if calculate_optimal_crop == InpaintBBoxMatchMode.OFF.value: return (inpaint_width, inpaint_height) - optimal_resolution = None + optimal_resolution: tuple[int, int] | None = None - bbox_width = bbox[2] - bbox[0] - bbox_height = bbox[3] - bbox[1] - bbox_aspect_ratio = bbox_width / bbox_height - - if calculate_optimal_crop == "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) - # Limit resolutions to those SDXL was trained on. - resolutions = [ - (1024, 1024), - (1152, 896), - (896, 1152), - (1216, 832), - (832, 1216), - (1344, 768), - (768, 1344), - (1536, 640), - (640, 1536), - ] - - # Filter resolutions smaller than bbox, and any that could result in a total pixel size smaller than the current inpaint dimensions. - resolutions = [ - res - for res in resolutions - 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) - - optimal_resolution = min( - resolutions, - key=lambda res: abs((res[0] / res[1]) - bbox_aspect_ratio), + optimal_resolution = optimal_crop_size.sdxl( + inpaint_width, inpaint_height, bbox ) - elif calculate_optimal_crop == "Free": - 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 + elif calculate_optimal_crop == InpaintBBoxMatchMode.FREE.value: + optimal_resolution = optimal_crop_size.free( + inpaint_width, inpaint_height, bbox + ) - # 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 - - optimal_resolution = (int(optimal_width), int(optimal_height)) else: msg = "[-] ADetailer: unsupported inpaint bounding box match mode. Original inpainting dimensions will be used." print(msg) @@ -873,14 +846,13 @@ class AfterDetailerScript(scripts.Script): 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] - p2.denoising_strength = self.get_dynamic_denoise_strength( - p2.denoising_strength, pred.bboxes[j], pp.image - ) - # Don't override user-defined dimensions. if not args.ad_use_inpaint_width_height: p2.width, p2.height = self.get_optimal_crop_image_size( @@ -1055,7 +1027,7 @@ def on_ui_settings(): shared.opts.add_option( "ad_match_inpaint_bbox_size", shared.OptionInfo( - default="Off", + 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", diff --git a/tests/test_opts.py b/tests/test_opts.py index 347e3b9..fd37b25 100644 --- a/tests/test_opts.py +++ b/tests/test_opts.py @@ -5,7 +5,7 @@ import pytest from hypothesis import assume, given from hypothesis import strategies as st -from adetailer.opts import get_dynamic_denoise_strength, optimal_crop_size +from adetailer.opts import dynamic_denoise_strength, optimal_crop_size @pytest.mark.parametrize( @@ -17,29 +17,29 @@ from adetailer.opts import get_dynamic_denoise_strength, optimal_crop_size (-0.5, 0.5, [0, 0, 100, 100], (1000, 1000), 0.502518907629606), ], ) -def test_get_dynamic_denoise_strength( +def test_dynamic_denoise_strength( denoise_power: float, denoise_strength: float, bbox: list[int], image_size: tuple[int, int], expected_result: float, ): - result = get_dynamic_denoise_strength( - denoise_power, denoise_strength, bbox, image_size - ) + 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_get_dynamic_denoise_strength_no_bbox(denoise_strength: float): - result = get_dynamic_denoise_strength(0.5, denoise_strength, [], (1000, 1000)) - assert result == denoise_strength +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_get_dynamic_denoise_strength_zero_power(denoise_strength: float): - result = get_dynamic_denoise_strength(0.0, denoise_strength, [], (1000, 1000)) - assert result == denoise_strength +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( From 18d8db995fc9769296131a61ce2f7623e478967c Mon Sep 17 00:00:00 2001 From: Dowon Date: Sat, 24 Aug 2024 13:35:37 +0900 Subject: [PATCH 07/21] fix(scripts): fix ad_save_images_dir --- scripts/!adetailer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index cfd3b0b..b203e3e 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -569,9 +569,9 @@ class AfterDetailerScript(scripts.Script): seed, _ = self.get_seed(p) if opts.data.get(condition, False): - ad_save_images_dir = opts.data.get("ad_save_images_dir", None) + ad_save_images_dir: str = opts.data.get("ad_save_images_dir", "") - if not bool(ad_save_images_dir and ad_save_images_dir.strip()): + if not ad_save_images_dir.strip(): ad_save_images_dir = p.outpath_samples images.save_image( From 79a74819cbd7ae167f5d0b91a7eecd7877c1b3d9 Mon Sep 17 00:00:00 2001 From: Dowon Date: Sat, 24 Aug 2024 13:51:11 +0900 Subject: [PATCH 08/21] refactor(script): reduce complexity --- aaaaaa/helper.py | 6 ++++ scripts/!adetailer.py | 65 +++++++++++++++++++++++++------------------ 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/aaaaaa/helper.py b/aaaaaa/helper.py index c9c50c9..ae19003 100644 --- a/aaaaaa/helper.py +++ b/aaaaaa/helper.py @@ -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 diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index b203e3e..52141e5 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -18,6 +18,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, @@ -744,6 +745,25 @@ class AfterDetailerScript(scripts.Script): return optimal_resolution + def fix_p2( + 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): @@ -779,7 +799,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 @@ -794,23 +814,22 @@ 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(): + pred = ultralytics_predict( + args.ad_model, + image=pp.image, + confidence=args.ad_confidence, + device=self.ultralytics_device, + classes=args.ad_model_classes, + ) if pred.preview is None: print( @@ -844,20 +863,7 @@ 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) - 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] - ) + self.fix_p2(p, p2, pp, args, pred, j) try: processed = process_images(p2) @@ -870,6 +876,11 @@ class AfterDetailerScript(scripts.Script): self.compare_prompt(p.extra_generation_params, processed, n=n) p2 = copy(i2i) + + if not processed.images: + processed = None + break + p2.init_images = [processed.images[0]] if processed is not None: @@ -879,7 +890,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 From 652da215ac06d460dbd79eb4af7efa7660602471 Mon Sep 17 00:00:00 2001 From: Dowon Date: Sat, 24 Aug 2024 13:52:39 +0900 Subject: [PATCH 09/21] chore: dev version --- adetailer/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adetailer/__version__.py b/adetailer/__version__.py index 45adada..0ce23c3 100644 --- a/adetailer/__version__.py +++ b/adetailer/__version__.py @@ -1 +1 @@ -__version__ = "24.8.0" +__version__ = "24.9.0-dev.0" From fbbe127c9ec1763287b0d1f30818996b45da0bd4 Mon Sep 17 00:00:00 2001 From: Dowon Date: Sat, 24 Aug 2024 13:58:08 +0900 Subject: [PATCH 10/21] fix: add PL rule --- aaaaaa/traceback.py | 2 +- aaaaaa/ui.py | 2 +- adetailer/common.py | 2 +- controlnet_ext/controlnet_ext.py | 2 +- controlnet_ext/controlnet_ext_forge.py | 2 +- pyproject.toml | 3 ++- scripts/!adetailer.py | 2 +- 7 files changed, 8 insertions(+), 7 deletions(-) diff --git a/aaaaaa/traceback.py b/aaaaaa/traceback.py index 3f7e44a..4119a9b 100644 --- a/aaaaaa/traceback.py +++ b/aaaaaa/traceback.py @@ -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 diff --git a/aaaaaa/ui.py b/aaaaaa/ui.py index e4efb54..d7b5391 100644 --- a/aaaaaa/ui.py +++ b/aaaaaa/ui.py @@ -369,7 +369,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(): diff --git a/adetailer/common.py b/adetailer/common.py index f9e42fc..0a4fb7a 100644 --- a/adetailer/common.py +++ b/adetailer/common.py @@ -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)) diff --git a/controlnet_ext/controlnet_ext.py b/controlnet_ext/controlnet_ext.py index bcf7130..6fb557e 100644 --- a/controlnet_ext/controlnet_ext.py +++ b/controlnet_ext/controlnet_ext.py @@ -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, diff --git a/controlnet_ext/controlnet_ext_forge.py b/controlnet_ext/controlnet_ext_forge.py index ef59e32..5fba119 100644 --- a/controlnet_ext/controlnet_ext_forge.py +++ b/controlnet_ext/controlnet_ext_forge.py @@ -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, diff --git a/pyproject.toml b/pyproject.toml index 87f609f..cd3a2fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,7 @@ select = [ "N", "PD", "PERF", + "PL", "PIE", "PT", "PTH", @@ -67,7 +68,7 @@ select = [ "UP", "W", ] -ignore = ["B905", "E501"] +ignore = ["B905", "E501", "PLR2004", "PLW0603"] unfixable = ["F401"] [tool.ruff.lint.isort] diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index 52141e5..22f3190 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -745,7 +745,7 @@ class AfterDetailerScript(scripts.Script): return optimal_resolution - def fix_p2( + def fix_p2( # noqa: PLR0913 self, p, p2, pp: PPImage, args: ADetailerArgs, pred: PredictOutput, j: int ): seed, subseed = self.get_seed(p) From cbd60739e8954c0021882b616c70957611efd975 Mon Sep 17 00:00:00 2001 From: Dowon Date: Sat, 24 Aug 2024 13:59:34 +0900 Subject: [PATCH 11/21] fix(scripts): move empty check --- scripts/!adetailer.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index 22f3190..1836cf5 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -874,13 +874,12 @@ class AfterDetailerScript(scripts.Script): finally: p2.close() - self.compare_prompt(p.extra_generation_params, processed, n=n) - p2 = copy(i2i) - 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]] if processed is not None: From a3935fcc4fe8aec4292fbbd476d293c87ddad520 Mon Sep 17 00:00:00 2001 From: Dowon Date: Sat, 24 Aug 2024 14:48:13 +0900 Subject: [PATCH 12/21] chore(ci): fix pypi action --- .github/workflows/pypi.yml | 4 ++-- pyproject.toml | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index fee205c..a8d10cf 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -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 diff --git a/pyproject.toml b/pyproject.toml index cd3a2fb..7bf3640 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" From 6090dcdaa9e77cd2c4cf39c5d55f352ef54f0152 Mon Sep 17 00:00:00 2001 From: Dowon Date: Sat, 24 Aug 2024 19:14:10 +0900 Subject: [PATCH 13/21] fix(scripts): fix is ad enabled --- aaaaaa/ui.py | 2 ++ scripts/!adetailer.py | 46 +++++++++++++++++++------------------------ 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/aaaaaa/ui.py b/aaaaaa/ui.py index d7b5391..4b9b484 100644 --- a/aaaaaa/ui.py +++ b/aaaaaa/ui.py @@ -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, diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index 1836cf5..91245ce 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -8,7 +8,6 @@ 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 @@ -177,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: @@ -232,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: @@ -244,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: @@ -643,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 From 63b0d2bbde7d961c5337d72c58b301fd47e7735f Mon Sep 17 00:00:00 2001 From: Dowon Date: Sat, 24 Aug 2024 19:20:29 +0900 Subject: [PATCH 14/21] chore: exclude modules dir --- .gitignore | 1 + .pre-commit-config.yaml | 2 ++ pyproject.toml | 1 + 3 files changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index aefb818..009cc5d 100644 --- a/.gitignore +++ b/.gitignore @@ -195,3 +195,4 @@ pyrightconfig.json # End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode *.ipynb node_modules +modules diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d566db7..6d11532 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,8 @@ ci: autoupdate_branch: "dev" +exclude: ^modules/ + repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 diff --git a/pyproject.toml b/pyproject.toml index 7bf3640..f1be122 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ known_first_party = ["launch", "modules"] [tool.ruff] target-version = "py39" +extend-exclude = ["modules"] [tool.ruff.lint] select = [ From 47cc6b907056a947bd54e00305b53092b1d45d8d Mon Sep 17 00:00:00 2001 From: Dowon Date: Sun, 25 Aug 2024 14:51:05 +0900 Subject: [PATCH 15/21] fix(scripts): misc --- scripts/!adetailer.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index 91245ce..908c1ea 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -696,16 +696,18 @@ class AfterDetailerScript(scripts.Script): @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) - optimal_resolution: tuple[int, int] | None = None - + # 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." @@ -716,16 +718,15 @@ class AfterDetailerScript(scripts.Script): inpaint_width, inpaint_height, bbox ) + # Free elif calculate_optimal_crop == InpaintBBoxMatchMode.FREE.value: optimal_resolution = optimal_crop_size.free( inpaint_width, inpaint_height, bbox ) - else: + if optimal_resolution is None: msg = "[-] ADetailer: unsupported inpaint bounding box match mode. Original inpainting dimensions will be used." print(msg) - - if optimal_resolution is None: return (inpaint_width, inpaint_height) # Only use optimal dimensions if they're different enough to current inpaint dimensions. @@ -874,7 +875,7 @@ class AfterDetailerScript(scripts.Script): self.compare_prompt(p.extra_generation_params, processed, n=n) p2 = copy(i2i) - p2.init_images = [processed.images[0]] + p2.init_images = processed.images if processed is not None: pp.image = processed.images[0] From 67793f076c9f580d15044e583d9cf9946dcc77e6 Mon Sep 17 00:00:00 2001 From: Dowon Date: Sun, 25 Aug 2024 14:59:58 +0900 Subject: [PATCH 16/21] chore: update .github --- .github/ISSUE_TEMPLATE/bug_report.yaml | 4 +-- .github/workflows/lgtm.yml | 23 ----------------- .github/workflows/test.yml | 34 ++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 26 deletions(-) delete mode 100644 .github/workflows/lgtm.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 8093934..70a7b32 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -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: ... diff --git a/.github/workflows/lgtm.yml b/.github/workflows/lgtm.yml deleted file mode 100644 index 08c0fad..0000000 --- a/.github/workflows/lgtm.yml +++ /dev/null @@ -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: | - ![Imgur](https://i.imgur.com/ESow3BL.png) - - LGTM - reactions: hooray diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..f345788 --- /dev/null +++ b/.github/workflows/test.yml @@ -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 From fabb7ded2b7297c63b4138f9c521efa2ad0d1f5a Mon Sep 17 00:00:00 2001 From: Dowon Date: Sun, 25 Aug 2024 15:13:25 +0900 Subject: [PATCH 17/21] fix(script): missing get_ad_model --- scripts/!adetailer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index 908c1ea..b4156fc 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -818,8 +818,9 @@ class AfterDetailerScript(scripts.Script): else: with change_torch_load(): + ad_model = self.get_ad_model(args.ad_model) pred = ultralytics_predict( - args.ad_model, + ad_model, image=pp.image, confidence=args.ad_confidence, device=self.ultralytics_device, From bb070be96111ccd1aac58e655a84be4bb6867f9b Mon Sep 17 00:00:00 2001 From: Dowon Date: Sun, 25 Aug 2024 15:43:27 +0900 Subject: [PATCH 18/21] fix(scripts): controlnet enabled --- controlnet_ext/controlnet_ext.py | 1 + 1 file changed, 1 insertion(+) diff --git a/controlnet_ext/controlnet_ext.py b/controlnet_ext/controlnet_ext.py index 6fb557e..e1f0724 100644 --- a/controlnet_ext/controlnet_ext.py +++ b/controlnet_ext/controlnet_ext.py @@ -78,6 +78,7 @@ class ControlNetExt: guidance_start=guidance_start, guidance_end=guidance_end, pixel_perfect=True, + enabled=True, ) ] From 91f7d8617a3de408a099c81a8435e9412d3354c1 Mon Sep 17 00:00:00 2001 From: Dowon Date: Tue, 27 Aug 2024 19:18:00 +0900 Subject: [PATCH 19/21] fix(scripts): init image controlnet error --- scripts/!adetailer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index b4156fc..27ddb7c 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -876,7 +876,7 @@ class AfterDetailerScript(scripts.Script): self.compare_prompt(p.extra_generation_params, processed, n=n) p2 = copy(i2i) - p2.init_images = processed.images + p2.init_images = [processed.images[0]] if processed is not None: pp.image = processed.images[0] From 0a1fd3a4af0667d1a996068774f21fd63037755d Mon Sep 17 00:00:00 2001 From: Dowon Date: Fri, 30 Aug 2024 20:12:18 +0900 Subject: [PATCH 20/21] fix(scripts): get image mask ensure L mode --- scripts/!adetailer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index 27ddb7c..52baf15 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -658,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) From 5ff72ad76dbf00760ac7d5eda33428052a398a66 Mon Sep 17 00:00:00 2001 From: Dowon Date: Mon, 2 Sep 2024 23:07:52 +0900 Subject: [PATCH 21/21] chore: v24.9.0 --- CHANGELOG.md | 9 +++++++++ adetailer/__version__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2095da5..b70e4e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/adetailer/__version__.py b/adetailer/__version__.py index 0ce23c3..ca0cfd9 100644 --- a/adetailer/__version__.py +++ b/adetailer/__version__.py @@ -1 +1 @@ -__version__ = "24.9.0-dev.0" +__version__ = "24.9.0"