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 @@ def need_skip(self) -> bool: "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.args import ( BBOX_SORTBY, BUILTIN_SCRIPT, + INPAINT_BBOX_MATCH_MODES, SCRIPT_DEFAULT, ADetailerArgs, SkipImg2ImgOrig, @@ -668,6 +669,103 @@ def get_image_mask(p) -> Image.Image: 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 @@ def _postprocess_image_inner( 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