Merge branch 'dev'

This commit is contained in:
Dowon
2024-11-10 18:24:16 +09:00
14 changed files with 155 additions and 89 deletions

View File

@@ -7,30 +7,7 @@ on:
jobs:
test:
name: test
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
uses: ./.github/workflows/test.yml
build:
name: build

View File

@@ -1,9 +1,12 @@
name: Test on PR
name: Test
on:
pull_request:
paths:
- "adetailer/**.py"
workflow_call:
schedule:
- cron: "0 0 * * 0"
jobs:
test:
@@ -24,7 +27,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- uses: yezz123/setup-uv@v4
- uses: astral-sh/setup-uv@v3
- name: Install dependencies
run: |

View File

@@ -5,7 +5,7 @@ exclude: ^modules/
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: check-added-large-files
args: [--maxkb=100]
@@ -24,7 +24,7 @@ repos:
- id: prettier
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.7
rev: v0.7.2
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]

View File

@@ -1,5 +1,15 @@
# Changelog
## 2024-11-10
- v24.11.0
- `disable_controlnet_units` 함수가 `script_args`의 상태를 변경된 상태로 저장하는 문제 수정
- XYZ Grid에 CFG Scale, scheduler, noise multiplier 추가
- Area 또는 Confidence를 기준으로 마스크 최대 갯수를 지정할 수 있도록 함 (PR #720)
- `ADetailer detector classes`의 element id를 `ad_classes`에서 `ad_model_classes`로 변경
- `mediapipe` 최대 버전을 0.10.15로 제한
## 2024-09-02
- v24.9.0

View File

@@ -25,8 +25,8 @@ tasks:
update:
cmds:
- "{{.PYTHON}} -m uv pip install -U ultralytics mediapipe ruff pre-commit black devtools pytest hypothesis"
- "{{.PYTHON}} -m uv pip install -U ultralytics mediapipe ruff pre-commit-uv black devtools pytest hypothesis"
update-torch:
cmds:
- "{{.PYTHON}} -m uv pip install -U torch torchvision torchaudio -f https://download.pytorch.org/whl/torch_stable.html"
- "{{.PYTHON}} -m uv pip install -U torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu124"

View File

@@ -204,7 +204,7 @@ def one_ui_group(n: int, is_img2img: bool, webui_info: WebuiInfo):
label="ADetailer detector classes" + suffix(n),
value="",
visible=False,
elem_id=eid("ad_classes"),
elem_id=eid("ad_model_classes"),
)
w.ad_model.change(
@@ -294,14 +294,22 @@ def detection(w: Widgets, n: int, is_img2img: bool):
visible=True,
elem_id=eid("ad_confidence"),
)
w.ad_mask_k_largest = gr.Slider(
label="Mask only the top k largest (0 to disable)" + suffix(n),
w.ad_mask_filter_method = gr.Radio(
choices=["Area", "Confidence"],
value="Area",
label="Method to filter top k masks by (confidence or area)"
+ suffix(n),
visible=True,
elem_id=eid("ad_mask_filter_method"),
)
w.ad_mask_k = gr.Slider(
label="Mask only the top k (0 to disable)" + suffix(n),
minimum=0,
maximum=10,
step=1,
value=0,
visible=True,
elem_id=eid("ad_mask_k_largest"),
elem_id=eid("ad_mask_k"),
)
with gr.Column(variant="compact"):

View File

@@ -1 +1 @@
__version__ = "24.9.0"
__version__ = "24.11.0"

View File

@@ -60,7 +60,8 @@ class ADetailerArgs(BaseModel, extra=Extra.forbid):
ad_prompt: str = ""
ad_negative_prompt: str = ""
ad_confidence: confloat(ge=0.0, le=1.0) = 0.3
ad_mask_k_largest: NonNegativeInt = 0
ad_mask_filter_method: Literal["Area", "Confidence"] = "Area"
ad_mask_k: NonNegativeInt = 0
ad_mask_min_ratio: confloat(ge=0.0, le=1.0) = 0.0
ad_mask_max_ratio: confloat(ge=0.0, le=1.0) = 1.0
ad_dilate_erode: int = 4
@@ -131,7 +132,11 @@ class ADetailerArgs(BaseModel, extra=Extra.forbid):
ppop("ADetailer prompt")
ppop("ADetailer negative prompt")
p.pop("ADetailer tab enable", None) # always pop
ppop("ADetailer mask only top k largest", cond=0)
ppop(
"ADetailer mask only top k",
["ADetailer mask only top k", "ADetailer method to decide top k masks"],
cond=0,
)
ppop("ADetailer mask min ratio", cond=0.0)
ppop("ADetailer mask max ratio", cond=1.0)
ppop("ADetailer x offset", cond=0)
@@ -217,7 +222,8 @@ _all_args = [
("ad_prompt", "ADetailer prompt"),
("ad_negative_prompt", "ADetailer negative prompt"),
("ad_confidence", "ADetailer confidence"),
("ad_mask_k_largest", "ADetailer mask only top k largest"),
("ad_mask_filter_method", "ADetailer method to decide top k masks"),
("ad_mask_k", "ADetailer mask only top k"),
("ad_mask_min_ratio", "ADetailer mask min ratio"),
("ad_mask_max_ratio", "ADetailer mask max ratio"),
("ad_x_offset", "ADetailer x offset"),

View File

@@ -22,6 +22,7 @@ T = TypeVar("T", int, float)
class PredictOutput(Generic[T]):
bboxes: list[list[T]] = field(default_factory=list)
masks: list[Image.Image] = field(default_factory=list)
confidences: list[float] = field(default_factory=list)
preview: Optional[Image.Image] = None

View File

@@ -225,6 +225,7 @@ def filter_by_ratio(
idx = [i for i in range(items) if is_in_ratio(pred.bboxes[i], low, high, orig_area)]
pred.bboxes = [pred.bboxes[i] for i in idx]
pred.masks = [pred.masks[i] for i in idx]
pred.confidences = [pred.confidences[i] for i in idx]
return pred
@@ -236,9 +237,31 @@ def filter_k_largest(pred: PredictOutput[T], k: int = 0) -> PredictOutput[T]:
idx = idx[::-1]
pred.bboxes = [pred.bboxes[i] for i in idx]
pred.masks = [pred.masks[i] for i in idx]
pred.confidences = [pred.confidences[i] for i in idx]
return pred
def filter_k_most_confident(pred: PredictOutput[T], k: int = 0) -> PredictOutput[T]:
if not pred.bboxes or not pred.confidences or k == 0:
return pred
idx = np.argsort(pred.confidences)[-k:]
idx = idx[::-1]
pred.bboxes = [pred.bboxes[i] for i in idx]
pred.masks = [pred.masks[i] for i in idx]
pred.confidences = [pred.confidences[i] for i in idx]
return pred
def filter_k_by(
pred: PredictOutput[T], k: int = 0, by: str = "Area"
) -> PredictOutput[T]:
if by == "Area":
return filter_k_largest(pred, k)
if by == "Confidence":
return filter_k_most_confident(pred, k)
raise RuntimeError
# Merge / Invert
def mask_merge(masks: list[Image.Image]) -> list[Image.Image]:
arrs = [np.array(m) for m in masks]

View File

@@ -52,6 +52,7 @@ def mediapipe_face_detection(
preview_array = img_array.copy()
bboxes = []
confidences = []
for detection in pred.detections:
draw_util.draw_detection(preview_array, detection)
@@ -63,12 +64,15 @@ def mediapipe_face_detection(
x2 = x1 + w
y2 = y1 + h
confidences.append(detection.score)
bboxes.append([x1, y1, x2, y2])
masks = create_mask_from_bbox(bboxes, image.size)
preview = Image.fromarray(preview_array)
return PredictOutput(bboxes=bboxes, masks=masks, preview=preview)
return PredictOutput(
bboxes=bboxes, masks=masks, confidences=confidences, preview=preview
)
def mediapipe_face_mesh(
@@ -141,7 +145,6 @@ def mediapipe_face_mesh_eyes_only(
preview = image.copy()
masks = []
for landmarks in pred.multi_face_landmarks:
points = np.array(
[[land.x * w, land.y * h] for land in landmarks.landmark], dtype=int

View File

@@ -37,11 +37,16 @@ def ultralytics_predict(
masks = create_mask_from_bbox(bboxes, image.size)
else:
masks = mask_to_pil(pred[0].masks.data, image.size)
confidences = pred[0].boxes.conf.cpu().numpy().tolist()
preview = pred[0].plot()
preview = cv2.cvtColor(preview, cv2.COLOR_BGR2RGB)
preview = Image.fromarray(preview)
return PredictOutput(bboxes=bboxes, masks=masks, preview=preview)
return PredictOutput(
bboxes=bboxes, masks=masks, confidences=confidences, preview=preview
)
def apply_classes(model: YOLO | YOLOWorld, model_path: str | Path, classes: str):

View File

@@ -45,7 +45,7 @@ def install():
deps = [
# requirements
("ultralytics", "8.2.0", None),
("mediapipe", "0.10.13", None),
("mediapipe", "0.10.13", "0.10.15"),
("rich", "13.0.0", None),
]

View File

@@ -52,7 +52,7 @@ from adetailer.args import (
from adetailer.common import PredictOutput, ensure_pil_image, safe_mkdir
from adetailer.mask import (
filter_by_ratio,
filter_k_largest,
filter_k_by,
has_intersection,
is_all_black,
mask_preprocess,
@@ -392,7 +392,7 @@ class AfterDetailerScript(scripts.Script):
value = args.ad_scheduler
return {"scheduler": value}
def get_override_settings(self, p, args: ADetailerArgs) -> dict[str, Any]:
def get_override_settings(self, _p, args: ADetailerArgs) -> dict[str, Any]:
d = {}
if args.ad_use_clip_skip:
@@ -413,7 +413,7 @@ class AfterDetailerScript(scripts.Script):
d["sd_vae"] = args.ad_vae
return d
def get_initial_noise_multiplier(self, p, args: ADetailerArgs) -> float | None:
def get_initial_noise_multiplier(self, _p, args: ADetailerArgs) -> float | None:
return args.ad_noise_multiplier if args.ad_use_noise_multiplier else None
@staticmethod
@@ -474,20 +474,30 @@ class AfterDetailerScript(scripts.Script):
script_runner.alwayson_scripts = filtered_alwayson
return script_runner, script_args
def disable_controlnet_units(
self, script_args: list[Any] | tuple[Any, ...]
) -> None:
for obj in script_args:
if "controlnet" in obj.__class__.__name__.lower():
if hasattr(obj, "enabled"):
obj.enabled = False
if hasattr(obj, "input_mode"):
obj.input_mode = getattr(obj.input_mode, "SIMPLE", "simple")
def disable_controlnet_units(self, script_args: Sequence[Any]) -> list[Any]:
new_args = []
for arg in script_args:
if "controlnet" in arg.__class__.__name__.lower():
new = copy(arg)
if hasattr(new, "enabled"):
new.enabled = False
if hasattr(new, "input_mode"):
new.input_mode = getattr(new.input_mode, "SIMPLE", "simple")
new_args.append(new)
elif isinstance(obj, dict) and "module" in obj:
obj["enabled"] = False
elif isinstance(arg, dict) and "module" in arg:
new = copy(arg)
new["enabled"] = False
new_args.append(new)
def get_i2i_p(self, p, args: ADetailerArgs, image):
else:
new_args.append(arg)
return new_args
def get_i2i_p(
self, p, args: ADetailerArgs, image: Image.Image
) -> StableDiffusionProcessingImg2Img:
seed, subseed = self.get_seed(p)
width, height = self.get_width_height(p, args)
steps = self.get_steps(p, args)
@@ -545,7 +555,7 @@ class AfterDetailerScript(scripts.Script):
i2i._ad_inner = True
if args.ad_controlnet_model != "Passthrough" and controlnet_type != "forge":
self.disable_controlnet_units(i2i.script_args)
i2i.script_args = self.disable_controlnet_units(i2i.script_args)
if args.ad_controlnet_model not in ["None", "Passthrough"]:
self.update_controlnet_args(i2i, args)
@@ -555,6 +565,9 @@ class AfterDetailerScript(scripts.Script):
return i2i
def save_image(self, p, image, *, condition: str, suffix: str) -> None:
if not opts.data.get(condition, False):
return
i = get_i(p)
if p.all_prompts:
i %= len(p.all_prompts)
@@ -563,23 +576,22 @@ class AfterDetailerScript(scripts.Script):
save_prompt = p.prompt
seed, _ = self.get_seed(p)
if opts.data.get(condition, False):
ad_save_images_dir: str = opts.data.get("ad_save_images_dir", "")
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
if not ad_save_images_dir.strip():
ad_save_images_dir = p.outpath_samples
images.save_image(
image=image,
path=ad_save_images_dir,
basename="",
seed=seed,
prompt=save_prompt,
extension=opts.samples_format,
info=self.infotext(p),
p=p,
suffix=suffix,
)
images.save_image(
image=image,
path=ad_save_images_dir,
basename="",
seed=seed,
prompt=save_prompt,
extension=opts.samples_format,
info=self.infotext(p),
p=p,
suffix=suffix,
)
def get_ad_model(self, name: str):
if name not in model_mapping:
@@ -596,7 +608,7 @@ class AfterDetailerScript(scripts.Script):
pred = filter_by_ratio(
pred, low=args.ad_mask_min_ratio, high=args.ad_mask_max_ratio
)
pred = filter_k_largest(pred, k=args.ad_mask_k_largest)
pred = filter_k_by(pred, k=args.ad_mask_k, by=args.ad_mask_filter_method)
pred = self.sort_bboxes(pred)
masks = mask_preprocess(
pred.masks,
@@ -652,7 +664,7 @@ class AfterDetailerScript(scripts.Script):
img2img_mask: Image.Image, ad_mask: list[Image.Image]
) -> list[Image.Image]:
if ad_mask and img2img_mask.size != ad_mask[0].size:
img2img_mask = img2img_mask.resize(ad_mask[0].size, resample=images.LANCZOS)
img2img_mask = img2img_mask.resize(ad_mask[0].size, resample=Image.LANCZOS)
return [mask for mask in ad_mask if has_intersection(img2img_mask, mask)]
@staticmethod
@@ -663,14 +675,9 @@ class AfterDetailerScript(scripts.Script):
mask = ImageChops.invert(mask)
mask = create_binary_mask(mask)
if is_skip_img2img(p):
if hasattr(p, "init_images") and p.init_images:
width, height = p.init_images[0].size
else:
msg = "[-] ADetailer: no init_images."
raise RuntimeError(msg)
else:
width, height = p.width, p.height
width, height = p.width, p.height
if is_skip_img2img(p) and hasattr(p, "init_images") and p.init_images:
width, height = p.init_images[0].size
return images.resize_image(p.resize_mode, mask, width, height)
@staticmethod
@@ -969,18 +976,22 @@ def on_ui_settings():
shared.opts.add_option(
"ad_save_previews",
shared.OptionInfo(False, "Save mask previews", section=section),
shared.OptionInfo(default=False, label="Save mask previews", section=section),
)
shared.opts.add_option(
"ad_save_images_before",
shared.OptionInfo(False, "Save images before ADetailer", section=section),
shared.OptionInfo(
default=False, label="Save images before ADetailer", section=section
),
)
shared.opts.add_option(
"ad_only_selected_scripts",
shared.OptionInfo(
True, "Apply only selected scripts to ADetailer", section=section
default=True,
label="Apply only selected scripts to ADetailer",
section=section,
),
)
@@ -1014,7 +1025,9 @@ def on_ui_settings():
shared.opts.add_option(
"ad_same_seed_for_each_tab",
shared.OptionInfo(
False, "Use same seed for each tab in adetailer", section=section
default=False,
label="Use same seed for each tab in adetailer",
section=section,
),
)
@@ -1080,7 +1093,8 @@ def make_axis_on_xyz_grid():
return
model_list = ["None", *model_mapping.keys()]
samplers = [sampler.name for sampler in all_samplers]
xyz_samplers = [sampler.name for sampler in all_samplers]
xyz_schedulers = [scheduler.label for scheduler in schedulers]
axis = [
xyz_grid.AxisOption(
@@ -1119,6 +1133,11 @@ def make_axis_on_xyz_grid():
float,
partial(set_value, field="ad_denoising_strength"),
),
xyz_grid.AxisOption(
"[ADetailer] CFG scale 1st",
float,
partial(set_value, field="ad_cfg_scale"),
),
xyz_grid.AxisOption(
"[ADetailer] Inpaint only masked 1st",
str,
@@ -1134,7 +1153,18 @@ def make_axis_on_xyz_grid():
"[ADetailer] ADetailer sampler 1st",
str,
partial(set_value, field="ad_sampler"),
choices=lambda: samplers,
choices=lambda: xyz_samplers,
),
xyz_grid.AxisOption(
"[ADetailer] ADetailer scheduler 1st",
str,
partial(set_value, field="ad_scheduler"),
choices=lambda: xyz_schedulers,
),
xyz_grid.AxisOption(
"[ADetailer] noise multiplier 1st",
float,
partial(set_value, field="ad_noise_multiplier"),
),
xyz_grid.AxisOption(
"[ADetailer] ControlNet model 1st",