Skip to content

Commit a8a91b2

Browse files
authored
feat: add ADetailer support (#1785)
1 parent c00a9e9 commit a8a91b2

19 files changed

Lines changed: 1874 additions & 14 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ API and command-line option may change frequently.***
6868
- [LingBot-Video](./docs/lingbot_video.md)
6969
- [PhotoMaker](./docs/photo_maker.md) support.
7070
- Control Net support with SD 1.5
71+
- [ADetailer](./docs/adetailer.md)
7172
- LoRA support, same as [stable-diffusion-webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Features#lora)
7273
- Latent Consistency Models support (LCM/LCM-LoRA)
7374
- Faster and memory efficient latent decoding with [TAESD](./docs/taesd.md)

docs/adetailer.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# ADetailer
2+
3+
`sd-cli` can run a YOLOv8 object detector on an existing or newly generated
4+
image and perform a cropped inpaint pass for every detected object. The first
5+
implementation supports YOLOv8 detection checkpoints. YOLOv8 segmentation and
6+
MediaPipe models are not supported yet.
7+
8+
## Convert a detector
9+
10+
Ultralytics checkpoints must be converted before use. The converter fuses
11+
BatchNorm into convolution layers and writes a safetensors file with the weight
12+
names expected by the native GGML implementation.
13+
14+
```bash
15+
python scripts/convert_yolov8_to_safetensors.py face_yolov8n.pt face_yolov8n.safetensors
16+
```
17+
18+
The converter requires Python packages `ultralytics`, `torch`, and
19+
`safetensors`.
20+
Only YOLOv8 detection checkpoints are accepted.
21+
PyTorch checkpoints use pickle internally, so only convert `.pt` files from a
22+
trusted source.
23+
24+
## Repair an existing image
25+
26+
Use the dedicated `adetailer` mode to detect and repair objects in an existing
27+
image:
28+
29+
```bash
30+
./bin/sd-cli \
31+
-M adetailer \
32+
-m model.safetensors \
33+
-i input.png \
34+
-o repaired.png \
35+
-p "detailed portrait photo" \
36+
--negative-prompt "deformed face" \
37+
--steps 24 \
38+
--cfg-scale 6 \
39+
--strength 0.4 \
40+
--sampling-method dpm++2m \
41+
--scheduler karras \
42+
--ad-model face_yolov8n.safetensors \
43+
--extra-ad-args "confidence=0.3,inpaint_padding=32,mask_blur=4"
44+
```
45+
46+
This mode reuses the normal image-generation options for the detail pass:
47+
48+
- `--init-img`, `--output`, `--prompt`, and `--negative-prompt`
49+
- `--steps`, `--cfg-scale`, `--sampling-method`, and `--scheduler`
50+
- `--strength`, `--seed`, LoRA settings, VAE tiling, and backend assignments
51+
- `--width` and `--height`, which also resize the input when specified
52+
53+
`--ad-prompt` and `--ad-negative-prompt` optionally override the normal prompts.
54+
Values provided in `--extra-ad-args`, such as `steps`, `cfg_scale`,
55+
`denoising_strength`, or `inpaint_width`, take precedence over inherited values.
56+
57+
## Repair generated images
58+
59+
ADetailer can also run automatically after normal image generation:
60+
61+
```bash
62+
./bin/sd-cli \
63+
-m model.safetensors \
64+
-p "portrait photo" \
65+
--ad-model face_yolov8n.safetensors \
66+
--ad-prompt "[PROMPT], detailed face" \
67+
--ad-negative-prompt "" \
68+
--extra-ad-args "confidence=0.3,denoising_strength=0.4,inpaint_width=512,inpaint_height=512"
69+
```
70+
71+
An empty ADetailer prompt inherits the main prompt. `[PROMPT]` inserts the main
72+
prompt, `[SEP]` assigns different prompts to consecutive masks, and `[SKIP]`
73+
skips the corresponding mask.
74+
75+
All settings other than the detector path and prompts are passed through
76+
`--extra-ad-args` as a comma-separated `key=value` list:
77+
78+
| Key | Default | Description |
79+
| --- | ---: | --- |
80+
| `input_size` | `640` | Square YOLO input size; must be a multiple of 32 |
81+
| `confidence` | `0.3` | Detection confidence threshold |
82+
| `nms` | `0.45` | NMS IoU threshold |
83+
| `max_detections` | `100` | Maximum detections retained after NMS |
84+
| `mask_k_largest` | `0` | Keep only the largest K masks; zero keeps all |
85+
| `mask_min_ratio` | `0` | Minimum bbox area relative to the image |
86+
| `mask_max_ratio` | `1` | Maximum bbox area relative to the image |
87+
| `dilate_erode` | `4` | Positive values dilate; negative values erode |
88+
| `x_offset`, `y_offset` | `0` | Mask offset in pixels; positive Y moves upward |
89+
| `mask_mode` | `none` | `none`, `merge`, or `merge_invert` |
90+
| `merge_masks`, `invert_mask` | `false` | Boolean alternatives to `mask_mode` |
91+
| `mask_blur` | `4` | Final composite feather radius |
92+
| `inpaint_padding` | `32` | Padding around the detected region |
93+
| `inpaint_width`, `inpaint_height` | mode-specific | `512x512` after generation; input/output size in `adetailer` mode |
94+
| `denoising_strength` | mode-specific | `0.4` after generation; inherits `--strength` in `adetailer` mode |
95+
| `steps` | `0` | Detail steps; zero inherits the main generation |
96+
| `cfg_scale` | `-1` | Detail CFG; a negative value inherits the main generation |
97+
| `sample_method` | inherited | Detail sampler name |
98+
| `scheduler` | inherited | Detail scheduler name |
99+
| `sort_by` | `none` | `none`, `left_to_right`, `center_to_edge`, or `area` |
100+
101+
Multiple masks are processed serially. Each completed inpaint becomes the input
102+
for the next mask, and the seed is incremented by the mask index. Use
103+
`mask_mode=merge` to process all detections in one inpaint pass.
104+
105+
The detector uses the `detector` backend module. For example, keep detection on
106+
the CPU while diffusion runs on CUDA:
107+
108+
```bash
109+
--backend "diffusion=cuda0,detector=cpu"
110+
```

docs/backend.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ still runs out of memory, tiling is enabled and the decode retried once.
153153
| `controlnet` | ControlNet | `controlnet`, `control` |
154154
| `photomaker` | PhotoMaker ID encoder and PhotoMaker LoRA | `photomaker`, `photomakerid`, `pmid`, `photo` |
155155
| `upscaler` | ESRGAN upscaler | `upscaler`, `esrgan`, `hires` |
156+
| `detector` | ADetailer YOLOv8 detector | `detector`, `adetailer`, `yolo` |
156157

157158
`te` is the preferred module name for text encoders. `clip` is kept as an accepted alias because many existing commands and model names use CLIP terminology.
158159

examples/cli/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ For detailed command-line arguments, run:
66
./bin/sd-cli -h
77
```
88

9+
For direct image repair or automatic post-generation YOLOv8 detection followed by cropped inpainting, see
10+
[ADetailer](../../docs/adetailer.md).
11+
912
Metadata mode inspects PNG/JPEG container metadata without loading any model:
1013

1114
```bash

examples/cli/main.cpp

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ struct SDCliParams {
199199
options.manual_options = {
200200
{"-M",
201201
"--mode",
202-
"run mode, one of [img_gen, vid_gen, upscale, convert, metadata], default: img_gen",
202+
"run mode, one of [img_gen, adetailer, vid_gen, upscale, convert, metadata], default: img_gen",
203203
on_mode_arg},
204204
{"",
205205
"--preview",
@@ -566,6 +566,65 @@ bool save_results(const SDCliParams& cli_params,
566566
return sucessful_reults != 0;
567567
}
568568

569+
static bool apply_adetailer(sd_ctx_t* sd_ctx,
570+
const sd_ctx_params_t& sd_ctx_params,
571+
const SDContextParams& ctx_params,
572+
const SDGenerationParams& gen_params,
573+
const sd_img_gen_params_t& img_gen_params,
574+
SDMode mode,
575+
SDImageVec& results,
576+
int num_results) {
577+
if (gen_params.ad_model_path.empty()) {
578+
return true;
579+
}
580+
581+
sd_adetailer_params_t ad_params{};
582+
ad_params.prompt = gen_params.ad_prompt.empty() ? nullptr : gen_params.ad_prompt.c_str();
583+
ad_params.negative_prompt = gen_params.ad_negative_prompt.empty() ? nullptr : gen_params.ad_negative_prompt.c_str();
584+
ad_params.extra_ad_args = gen_params.extra_ad_args.c_str();
585+
586+
ADetailerCtxPtr ad_ctx(new_adetailer_ctx(gen_params.ad_model_path.c_str(),
587+
ctx_params.n_threads,
588+
sd_ctx_params.backend,
589+
sd_ctx_params.params_backend));
590+
if (ad_ctx == nullptr) {
591+
LOG_ERROR("new_adetailer_ctx failed");
592+
return false;
593+
}
594+
595+
for (int i = 0; i < num_results; ++i) {
596+
if (results[i].data == nullptr) {
597+
continue;
598+
}
599+
sd_img_gen_params_t ad_generation_params = img_gen_params;
600+
ad_generation_params.seed = img_gen_params.seed + i;
601+
if (mode == IMG_GEN) {
602+
ad_generation_params.width = 512;
603+
ad_generation_params.height = 512;
604+
ad_generation_params.strength = 0.4f;
605+
}
606+
sd_image_t* detailed_images = nullptr;
607+
int detailed_count = 0;
608+
if (!adetail_image(ad_ctx.get(),
609+
sd_ctx,
610+
results[i],
611+
&ad_params,
612+
&ad_generation_params,
613+
&detailed_images,
614+
&detailed_count) ||
615+
detailed_count <= 0 || detailed_images == nullptr || detailed_images[0].data == nullptr) {
616+
free_sd_images(detailed_images, detailed_count);
617+
LOG_ERROR("ADetailer failed for image %d", i + 1);
618+
return false;
619+
}
620+
free(results[i].data);
621+
results[i] = detailed_images[0];
622+
detailed_images[0] = {0, 0, 0, nullptr};
623+
free_sd_images(detailed_images, detailed_count);
624+
}
625+
return true;
626+
}
627+
569628
int main(int argc, const char* argv[]) {
570629
if (argc > 1 && std::string(argv[1]) == "--version") {
571630
std::cout << version_string() << "\n";
@@ -598,6 +657,11 @@ int main(int argc, const char* argv[]) {
598657
return 0;
599658
}
600659

660+
if (!gen_params.ad_model_path.empty() && cli_params.mode != IMG_GEN && cli_params.mode != ADETAILER) {
661+
LOG_ERROR("--ad-model is only supported in image generation and adetailer modes");
662+
return 1;
663+
}
664+
601665
if (gen_params.video_frames > 4) {
602666
size_t last_dot_pos = cli_params.preview_path.find_last_of(".");
603667
std::string base_path = cli_params.preview_path;
@@ -806,15 +870,22 @@ int main(int argc, const char* argv[]) {
806870
gen_params.sample_params.scheduler = sd_get_default_scheduler(sd_ctx.get(), gen_params.sample_params.sample_method);
807871
}
808872

809-
if (cli_params.mode == IMG_GEN) {
810-
sd_img_gen_params_t img_gen_params = gen_params.to_sd_img_gen_params_t();
873+
sd_img_gen_params_t img_gen_params{};
874+
const bool use_img_gen_params = cli_params.mode == IMG_GEN || cli_params.mode == ADETAILER;
875+
if (use_img_gen_params) {
876+
img_gen_params = gen_params.to_sd_img_gen_params_t();
877+
}
811878

879+
if (cli_params.mode == IMG_GEN) {
812880
sd_image_t* generated_images = nullptr;
813881
if (!generate_image(sd_ctx.get(), &img_gen_params, &generated_images, &num_results)) {
814882
generated_images = nullptr;
815883
num_results = 0;
816884
}
817885
results.adopt(generated_images, num_results);
886+
} else if (cli_params.mode == ADETAILER) {
887+
num_results = 1;
888+
results.push_back(gen_params.init_image.release());
818889
} else if (cli_params.mode == VID_GEN) {
819890
sd_vid_gen_params_t vid_gen_params = gen_params.to_sd_vid_gen_params_t();
820891
sd_image_t* generated_video = nullptr;
@@ -828,6 +899,18 @@ int main(int argc, const char* argv[]) {
828899
LOG_ERROR("generate failed");
829900
return 1;
830901
}
902+
903+
if (use_img_gen_params &&
904+
!apply_adetailer(sd_ctx.get(),
905+
sd_ctx_params,
906+
ctx_params,
907+
gen_params,
908+
img_gen_params,
909+
cli_params.mode,
910+
results,
911+
num_results)) {
912+
return 1;
913+
}
831914
}
832915

833916
int upscale_factor = 4; // unused for RealESRGAN_x4plus_anime_6B.pth

examples/common/common.cpp

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ namespace fs = std::filesystem;
3030

3131
const char* const modes_str[] = {
3232
"img_gen",
33+
"adetailer",
3334
"vid_gen",
3435
"convert",
3536
"upscale",
@@ -922,6 +923,26 @@ ArgOptions SDGenerationParams::get_options() {
922923
"the negative prompt (default: \"\")",
923924
0,
924925
&negative_prompt},
926+
{"",
927+
"--ad-model",
928+
"path to a converted YOLOv8 detection model for ADetailer",
929+
0,
930+
&ad_model_path},
931+
{"",
932+
"--ad-prompt",
933+
"ADetailer prompt; empty inherits the main prompt, supports [PROMPT], [SEP], and [SKIP]",
934+
0,
935+
&ad_prompt},
936+
{"",
937+
"--ad-negative-prompt",
938+
"ADetailer negative prompt; empty inherits the main negative prompt, supports [PROMPT] and [SEP]",
939+
0,
940+
&ad_negative_prompt},
941+
{"",
942+
"--extra-ad-args",
943+
"extra ADetailer args, key=value list. Supports input_size, confidence, nms, max_detections, mask_k_largest, mask_min_ratio, mask_max_ratio, dilate_erode, x_offset, y_offset, mask_mode, merge_masks, invert_mask, mask_blur, inpaint_padding, inpaint_width, inpaint_height, denoising_strength, steps, cfg_scale, sample_method, scheduler, sort_by",
944+
(int)',',
945+
&extra_ad_args},
925946
{"-i",
926947
"--init-img",
927948
"path to the init image",
@@ -1842,6 +1863,10 @@ bool SDGenerationParams::from_json_str(
18421863

18431864
load_if_exists("prompt", prompt);
18441865
load_if_exists("negative_prompt", negative_prompt);
1866+
load_if_exists("ad_model", ad_model_path);
1867+
load_if_exists("ad_prompt", ad_prompt);
1868+
load_if_exists("ad_negative_prompt", ad_negative_prompt);
1869+
load_if_exists("extra_ad_args", extra_ad_args);
18451870
load_if_exists("cache_mode", cache_mode);
18461871
load_if_exists("cache_option", cache_option);
18471872
load_if_exists("scm_mask", scm_mask);
@@ -2358,13 +2383,19 @@ bool SDGenerationParams::validate(SDMode mode) {
23582383
}
23592384
}
23602385

2361-
if (mode == UPSCALE) {
2386+
if (mode == UPSCALE || mode == ADETAILER) {
23622387
if (init_image_path.length() == 0) {
2363-
LOG_ERROR("error: upscale mode needs an init image (--init-img)\n");
2388+
LOG_ERROR("error: %s mode needs an init image (--init-img)\n",
2389+
mode == UPSCALE ? "upscale" : "adetailer");
23642390
return false;
23652391
}
23662392
}
23672393

2394+
if (mode == ADETAILER && ad_model_path.empty()) {
2395+
LOG_ERROR("error: adetailer mode needs a detector model (--ad-model)\n");
2396+
return false;
2397+
}
2398+
23682399
return true;
23692400
}
23702401

@@ -2587,6 +2618,10 @@ std::string SDGenerationParams::to_string() const {
25872618
<< " high_noise_loras: \"" << high_noise_loras_str << "\",\n"
25882619
<< " prompt: \"" << prompt << "\",\n"
25892620
<< " negative_prompt: \"" << negative_prompt << "\",\n"
2621+
<< " ad_model_path: \"" << ad_model_path << "\",\n"
2622+
<< " ad_prompt: \"" << ad_prompt << "\",\n"
2623+
<< " ad_negative_prompt: \"" << ad_negative_prompt << "\",\n"
2624+
<< " extra_ad_args: \"" << extra_ad_args << "\",\n"
25902625
<< " clip_skip: " << clip_skip << ",\n"
25912626
<< " width: " << width << ",\n"
25922627
<< " height: " << height << ",\n"
@@ -2701,8 +2736,13 @@ std::string build_sdcpp_image_metadata_json(const SDContextParams& ctx_params,
27012736
int64_t seed,
27022737
SDMode mode) {
27032738
json root;
2704-
root["schema"] = "sdcpp.image.params/v1";
2705-
root["mode"] = mode == VID_GEN ? "vid_gen" : "img_gen";
2739+
root["schema"] = "sdcpp.image.params/v1";
2740+
root["mode"] = "img_gen";
2741+
if (mode == VID_GEN) {
2742+
root["mode"] = "vid_gen";
2743+
} else if (mode == ADETAILER) {
2744+
root["mode"] = "adetailer";
2745+
}
27062746
root["generator"] = {
27072747
{"name", "stable-diffusion.cpp"},
27082748
{"version", safe_json_string(sd_version())},
@@ -2716,6 +2756,14 @@ std::string build_sdcpp_image_metadata_json(const SDContextParams& ctx_params,
27162756
{"positive", gen_params.prompt},
27172757
{"negative", gen_params.negative_prompt},
27182758
};
2759+
if (!gen_params.ad_model_path.empty()) {
2760+
root["adetailer"] = {
2761+
{"model", sd_basename(gen_params.ad_model_path)},
2762+
{"prompt", gen_params.ad_prompt},
2763+
{"negative_prompt", gen_params.ad_negative_prompt},
2764+
{"extra_args", gen_params.extra_ad_args},
2765+
};
2766+
}
27192767
root["sampling"] = build_sampling_metadata_json(gen_params.sample_params,
27202768
gen_params.skip_layers,
27212769
&gen_params.custom_sigmas);
@@ -2870,6 +2918,18 @@ std::string get_image_params(const SDContextParams& ctx_params,
28702918
if (!gen_params.extra_sample_args.empty()) {
28712919
parameter_string += "Extra sample args: " + gen_params.extra_sample_args + ", ";
28722920
}
2921+
if (!gen_params.ad_model_path.empty()) {
2922+
parameter_string += "ADetailer model: " + sd_basename(gen_params.ad_model_path) + ", ";
2923+
if (!gen_params.ad_prompt.empty()) {
2924+
parameter_string += "ADetailer prompt: " + gen_params.ad_prompt + ", ";
2925+
}
2926+
if (!gen_params.ad_negative_prompt.empty()) {
2927+
parameter_string += "ADetailer negative prompt: " + gen_params.ad_negative_prompt + ", ";
2928+
}
2929+
if (!gen_params.extra_ad_args.empty()) {
2930+
parameter_string += "ADetailer args: " + gen_params.extra_ad_args + ", ";
2931+
}
2932+
}
28732933
parameter_string += "Seed: " + std::to_string(seed) + ", ";
28742934
parameter_string += "Size: " + std::to_string(gen_params.get_resolved_width()) + "x" + std::to_string(gen_params.get_resolved_height()) + ", ";
28752935
parameter_string += "Model: " + sd_basename(ctx_params.model_path) + ", ";

0 commit comments

Comments
 (0)