SDXL's img2img and inpaint pipelines silently ignore the documented
negative_crops_coords_top_left argument on the base model. The method that packs the
negative micro-conditioning vector, _get_add_time_ids, has two branches. The aesthetic-score
branch (used by the refiner) builds the negative vector from negative_crops_coords_top_left.
The other branch, taken whenever requires_aesthetics_score is False, which is the default for
the SDXL base model, packs the positive crops_coords_top_left into the negative vector
instead. So the negative crop conditioning a caller asks for has no effect on the
classifier-free-guidance negative branch.
src/diffusers/pipelines/stable_diffusion_xl/pipeline_stable_diffusion_xl_img2img.py,
_get_add_time_ids, current main (lines 856-863); the identical branch is in
pipeline_stable_diffusion_xl_inpaint.py (lines 961-968):
if self.config.requires_aesthetics_score:
add_time_ids = list(original_size + crops_coords_top_left + (aesthetic_score,))
add_neg_time_ids = list(
negative_original_size + negative_crops_coords_top_left + (negative_aesthetic_score,)
)
else:
add_time_ids = list(original_size + crops_coords_top_left + target_size)
add_neg_time_ids = list(negative_original_size + crops_coords_top_left + negative_target_size)The __call__ docstring documents negative_crops_coords_top_left as a way "to negatively
condition the generation process based on a specific crop coordinates," and threads it into
this method. The aesthetic branch honors it. The else branch does not: its negative vector uses
negative_original_size for the first field and negative_target_size for the last, but
crops_coords_top_left, the positive one, for the crop field in the middle.
Requesting a negative crop of (256, 256) while the positive crop is (0, 0):
negative crop (256,256) requested: diffusers [1024, 1024, 0, 0, 1024, 1024], correct [1024, 1024, 256, 256, 1024, 1024], ignored True
other negative fields honored: original_size True, target_size True (so the crop field is an omission)
base case (negative crop == positive crop): buggy == correct True
The negative vector keeps the positive crop (0, 0) instead of the requested (256, 256), so
the parameter is silently discarded. That the same line does honor negative_original_size and
negative_target_size is what makes the crop field an omission rather than a deliberate choice:
two of the three negative fields are wired through, the crop is not, and the sibling
aesthetic-score branch wires all three. The base case confirms the harness: when the negative
crop equals the positive crop (the default, both (0, 0)), the buggy and correct vectors are
identical, so the divergence appears only when the parameter is actually set, which is its
entire purpose.
All defects are on current main. requires_aesthetics_score defaults to False, and the SDXL
base model does not enable it, so base-model img2img and inpaint, the mainstream SDXL
image-editing paths, take the affected branch. The refiner, which sets
requires_aesthetics_score=True, takes the correct aesthetic branch. Users who leave
negative_crops_coords_top_left at its default are unaffected; those who set it get no effect.
The fix is one field per branch: crops_coords_top_left to negative_crops_coords_top_left.
The same leak is present in the SDXL ControlNet img2img pipeline,
pipelines/controlnet/pipeline_controlnet_sd_xl_img2img.py, _get_add_time_ids line 1009,
which exposes and documents negative_crops_coords_top_left in its __call__ and then packs
the positive crop into the negative vector in the same else branch. Three pipelines are
affected: SDXL img2img, SDXL inpaint, and SDXL ControlNet img2img. The SDXL ControlNet inpaint
pipeline hard-codes the positive crop into the negative vector as well, but it never exposes
negative_crops_coords_top_left in its signature, so that is a missing feature rather than an
ignored parameter, and is not counted here.
excerpt.py: the two branches quoted with line numbers and the two flags stating which crop each uses (Apache-2.0).timeids.py: the else-branch construction as current main builds it and as the aesthetic branch would, differing only in the negative crop field.consequence.py: the ignored negative crop, the confirmation that the other two negative fields are used, and the base case where the crops are equal.test_negcrop.py: the negative crop is ignored, the positive vector is unchanged, the other negative fields are honored, and the base case matches.
python timeids.py
python consequence.py
python test_negcrop.py
The branches are quoted from current main; the fix is to use negative_crops_coords_top_left
for the negative vector's crop field, matching the two negative fields around it and the
aesthetic-score branch.