multimodalart HF Staff commited on
Commit
42ff634
·
verified ·
1 Parent(s): c1cad15

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
Files changed (1) hide show
  1. app.py +59 -20
app.py CHANGED
@@ -85,23 +85,59 @@ def _snap16(v):
85
  return max(512, min(1024, v))
86
 
87
 
88
- # Match the ComfyUI reference workflow's sizing: the pose image is scaled to
89
- # fit ~1 MP (FluxKontextImageScale), its size drives EmptyLatentImage
90
- # (GetImageSize), and the Krea-2 latent grid patchifies on a multiple of 16
91
- # (vae_scale_factor 8 * patch 2). Preserve aspect, fit under 1 MP, snap /16 —
92
- # NOT a hard 512/1024 clamp, which distorted the aspect and hurt pose fidelity.
93
- REF_MAX_PIXELS = 1024 * 1024
94
- REF_SNAP = 16
95
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
- def _fit_pose_size(w, h):
98
- """Scale (w, h) down to fit REF_MAX_PIXELS, keep aspect, snap to /16."""
99
- import math
 
 
 
 
 
 
 
 
100
 
101
- scale = min(1.0, math.sqrt(REF_MAX_PIXELS / float(w * h)))
102
- nw = max(int(round(w * scale / REF_SNAP)) * REF_SNAP, REF_SNAP)
103
- nh = max(int(round(h * scale / REF_SNAP)) * REF_SNAP, REF_SNAP)
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: fit the pose image under ~1 MP,
875
- # preserve aspect, snap to /16. The output grid then lines up with the pose
876
- # reference latent grid (index_timestep_zero placement) — strong adherence.
877
- w, h = pose_image.size
878
- out_w, out_h = _fit_pose_size(w, h)
 
 
 
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