Spaces:
Running on Zero
Running on Zero
Fix pose fidelity/quality: replicate ComfyUI FluxKontextImageScale (nearest ~1MP bucket + center-crop + Lanczos) for pose sizing so ref-latent grid == output grid at native ~1MP resolution
Browse files
app.py
CHANGED
|
@@ -85,23 +85,59 @@ def _snap16(v):
|
|
| 85 |
return max(512, min(1024, v))
|
| 86 |
|
| 87 |
|
| 88 |
-
#
|
| 89 |
-
#
|
| 90 |
-
#
|
| 91 |
-
#
|
| 92 |
-
#
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
return nw, nh
|
| 105 |
|
| 106 |
|
| 107 |
class InteractivePoseSkeleton(gr.HTML):
|
|
@@ -871,11 +907,14 @@ def _run_pipeline(pose_image, prompt, lora_scale, num_inference_steps,
|
|
| 871 |
"""
|
| 872 |
generator = torch.Generator("cuda").manual_seed(int(seed))
|
| 873 |
|
| 874 |
-
# Match the ComfyUI workflow's sizing
|
| 875 |
-
#
|
| 876 |
-
#
|
| 877 |
-
|
| 878 |
-
|
|
|
|
|
|
|
|
|
|
| 879 |
|
| 880 |
# The pose LoRA (an ai-toolkit base-transformer LoRA, keys
|
| 881 |
# diffusion_model.blocks.*.lora_{A,B}) is applied once at `lora_scale` via
|
|
|
|
| 85 |
return max(512, min(1024, v))
|
| 86 |
|
| 87 |
|
| 88 |
+
# Exact replica of the ComfyUI reference workflow's sizing node
|
| 89 |
+
# (FluxKontextImageScale, node 28). In the workflow the DWPose skeleton is run
|
| 90 |
+
# through FluxKontextImageScale, and that single scaled image drives BOTH:
|
| 91 |
+
# * node 35 image1 -> the VAE reference latents, and
|
| 92 |
+
# * node 6 EmptyLatentImage (via GetImageSize) -> the output canvas.
|
| 93 |
+
# So the reference-latent grid and the output grid are guaranteed identical.
|
| 94 |
+
#
|
| 95 |
+
# The previous "fit under 1 MP, snap /16, never upscale" heuristic diverged:
|
| 96 |
+
# small pose images (e.g. the 512x768 editor canvas or typical skeleton PNGs)
|
| 97 |
+
# were left at ~0.4 MP, whereas FluxKontextImageScale always normalizes to a
|
| 98 |
+
# ~1 MP preferred bucket. Krea-2-Turbo is trained at ~1 MP, so running the
|
| 99 |
+
# pose reference + generation at a third of that resolution destroyed both
|
| 100 |
+
# generation quality and pose fidelity (the model saw a coarse skeleton on a
|
| 101 |
+
# small grid). This replicates FluxKontextImageScale precisely.
|
| 102 |
+
PREFERRED_KONTEXT_RESOLUTIONS = [
|
| 103 |
+
(672, 1568), (688, 1504), (720, 1456), (752, 1392), (800, 1328),
|
| 104 |
+
(832, 1248), (880, 1184), (944, 1104), (1024, 1024), (1104, 944),
|
| 105 |
+
(1184, 880), (1248, 832), (1328, 800), (1392, 752), (1456, 720),
|
| 106 |
+
(1504, 688), (1568, 672),
|
| 107 |
+
]
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _flux_kontext_scale(pose_image):
|
| 111 |
+
"""Scale a pose image exactly like ComfyUI's FluxKontextImageScale node.
|
| 112 |
+
|
| 113 |
+
Picks the preferred (~1 MP) resolution bucket with the nearest aspect
|
| 114 |
+
ratio, center-crops to that aspect, then Lanczos-resizes to the exact
|
| 115 |
+
bucket size. Returns (scaled_rgb_image, width, height). Both bucket dims
|
| 116 |
+
are multiples of 16, so the Krea-2 latent grid patchifies cleanly and the
|
| 117 |
+
output grid lines up with the pose reference-latent grid.
|
| 118 |
+
"""
|
| 119 |
+
pose_image = pose_image.convert("RGB")
|
| 120 |
+
w, h = pose_image.size
|
| 121 |
+
aspect = w / h
|
| 122 |
+
_, bw, bh = min(
|
| 123 |
+
(abs(aspect - tw / th), tw, th) for tw, th in PREFERRED_KONTEXT_RESOLUTIONS
|
| 124 |
+
)
|
| 125 |
|
| 126 |
+
# Center-crop the source to the target aspect (crop only, never pad),
|
| 127 |
+
# matching comfy.utils.common_upscale(crop="center").
|
| 128 |
+
old_aspect = w / h
|
| 129 |
+
new_aspect = bw / bh
|
| 130 |
+
x = y = 0
|
| 131 |
+
if old_aspect > new_aspect:
|
| 132 |
+
x = round((w - w * (new_aspect / old_aspect)) / 2)
|
| 133 |
+
elif old_aspect < new_aspect:
|
| 134 |
+
y = round((h - h * (old_aspect / new_aspect)) / 2)
|
| 135 |
+
if x > 0 or y > 0:
|
| 136 |
+
pose_image = pose_image.crop((x, y, w - x, h - y))
|
| 137 |
|
| 138 |
+
# Lanczos resize to the exact bucket size (PIL LANCZOS, as ComfyUI does).
|
| 139 |
+
pose_image = pose_image.resize((bw, bh), resample=Image.LANCZOS)
|
| 140 |
+
return pose_image, bw, bh
|
|
|
|
| 141 |
|
| 142 |
|
| 143 |
class InteractivePoseSkeleton(gr.HTML):
|
|
|
|
| 907 |
"""
|
| 908 |
generator = torch.Generator("cuda").manual_seed(int(seed))
|
| 909 |
|
| 910 |
+
# Match the ComfyUI workflow's sizing EXACTLY (FluxKontextImageScale, node
|
| 911 |
+
# 28): snap the pose skeleton to the nearest ~1 MP preferred bucket via
|
| 912 |
+
# center-crop + Lanczos. The SAME scaled image is then used both as the VAE
|
| 913 |
+
# reference (image=) and as the output canvas (height/width) — so the pose
|
| 914 |
+
# reference-latent grid and the output grid share identical dimensions
|
| 915 |
+
# (index_timestep_zero placement), giving strong pose adherence, and the
|
| 916 |
+
# model runs at its native ~1 MP resolution for full quality.
|
| 917 |
+
pose_image, out_w, out_h = _flux_kontext_scale(pose_image)
|
| 918 |
|
| 919 |
# The pose LoRA (an ai-toolkit base-transformer LoRA, keys
|
| 920 |
# diffusion_model.blocks.*.lora_{A,B}) is applied once at `lora_scale` via
|