Skip to content

Fix BOX downscale dropping boundary source pixels - #9949

Open
Dmao233 wants to merge 12 commits into
python-pillow:mainfrom
Dmao233:fix-9939-box-resize-edge-pixels
Open

Dmao233 wants to merge 12 commits into
python-pillow:mainfrom
Dmao233:fix-9939-box-resize-edge-pixels

Conversation

@Dmao233

@Dmao233 Dmao233 commented Sep 3, 2026

Copy link
Copy Markdown

Fixes #9939.

Changes proposed in this pull request:

  • BOX downscale assigns a source pixel whose center lands on an output bin edge to exactly one destination pixel
  • Add regression tests for the 13→6 boundary case and the issue sizes (2755→688, 1837→306)
  • Note the fix in docs/releasenotes/13.0.0.rst

Dmao233 and others added 2 commits September 3, 2026 01:23
BOX coefficient ranges used (int)(center ± support + 0.5) plus a
discontinuous kernel, so a source pixel whose center landed on an
output bin edge could get weight 0 in every output. Tile downscale
bins as (left, right] so each source pixel maps to one destination.

Co-authored-by: CenFangyu <Dmao233@users.noreply.github.com>
Avoid the deprecated Image.getdata API in the python-pillow#9939 test.

Co-authored-by: CenFangyu <Dmao233@users.noreply.github.com>
Comment thread docs/releasenotes/13.0.0.rst Outdated
Comment thread Tests/test_image_resample.py Outdated
Comment thread Tests/test_image_resample.py Outdated
Co-authored-by: CenFangyu <Dmao233@users.noreply.github.com>
@akx

akx commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

cc @AlttiRi as the reporter of #9939, you said in that issue you have a BOX down-sampling implementation of your own, so I suspect you have more domain knowledge, can you take a look at this? :)

Comment thread src/libImaging/Resample.c Outdated
Co-authored-by: CenFangyu <Dmao233@users.noreply.github.com>
@radarhere

Copy link
Copy Markdown
Member

I've created Dmao233#2 with a suggestion.

Took radarhere's Simplify code suggestion.
@Dmao233

Dmao233 commented Sep 14, 2026

Copy link
Copy Markdown
Author

Took the Simplify code suggestion from Dmao233#2.

Dmao233 and others added 2 commits September 18, 2026 01:07
Keep the BOX downscale note and the HarfBuzz wheel note.

Co-authored-by: CenFangyu <Dmao233@users.noreply.github.com>
@Dmao233

Dmao233 commented Sep 22, 2026

Copy link
Copy Markdown
Author

Merged main; releasenotes conflict in 13.0.0.rst resolved (kept BOX note). e7be905.

@MohammediYunus

Copy link
Copy Markdown
Contributor

I checked e7be905 against 5835da6 in isolated native builds. This also fixes a duplication case that would be useful beside the dropped-pixel regressions: shrinking 15 pixels to 14 can count the central source pixel twice on the old implementation.

from PIL import Image

im = Image.new("L", (15, 1))
im.putpixel((7, 0), 255)
out = im.resize((14, 1), Image.Resampling.BOX)
assert [value for value in out.get_flattened_data() if value] == [128]

The base produces two nonzero values, [128, 128]; this head produces [128]. The vertical 1×15 → 1×14 case agrees. That assertion checks exactly one contribution without prescribing which side owns the boundary tie.

On this head, all 59 selftests passed and the focused resampling suite had 103 passes and 2 expected failures. I also verified the small example above on both native builds. I haven’t run the full suite.

@Dmao233

Dmao233 commented Oct 8, 2026

Copy link
Copy Markdown
Author

Thanks, added it as a test for both directions in f1670bf; fails on main, passes here.

@MohammediYunus

Copy link
Copy Markdown
Contributor

Thanks for adding both directions! I rechecked f1670bf: the new regression passes, while the original baseline (5835da6) still duplicates the centre pixel. The resampling suite also passes: 104 passed, 2 expected failures.

@radarhere

Copy link
Copy Markdown
Member

What do you think of #10165? It passes all of the tests from this PR, and certainly looks simpler.

@Dmao233

Dmao233 commented Oct 11, 2026

Copy link
Copy Markdown
Author

Checked #10165 against the tests here (boundary drop + 15→14 double-count, both axes): all pass.

Prefer this PR on the approach: BOX downscale should tile (left, right] so each source pixel maps to exactly one output by construction. A 1e-9 nudge on the kernel and rounding is simpler, but it is a tolerance that happens to work for the sizes we hit; it also changes box_filter for every use, not only downscale.

If you still prefer #10165 for the smaller diff, I can close this.

@aclark4life aclark4life added the Bug Any unexpected behavior, until confirmed feature. label Oct 11, 2026
@radarhere
radarhere requested a review from homm October 22, 2026 10:15
Comment thread docs/releasenotes/13.1.0.rst Outdated
Comment on lines +55 to +58
:data:`PIL.Image.Resampling.BOX` downscaling could skip a source pixel whose
center landed on an output bin edge, due to range rounding and the discontinuous
box kernel. Those pixels are now assigned to exactly one output pixel, matching
the documented "each source pixel contributes to one destination pixel" behavior.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:data:`PIL.Image.Resampling.BOX` downscaling could skip a source pixel whose
center landed on an output bin edge, due to range rounding and the discontinuous
box kernel. Those pixels are now assigned to exactly one output pixel, matching
the documented "each source pixel contributes to one destination pixel" behavior.
:data:`PIL.Image.Resampling.BOX` downscaling could skip a source pixel whose center
landed on an output bin edge, due to range rounding and the discontinuous box kernel.
Those pixels are now assigned to exactly one output pixel, matching the documented
behavior that "Each pixel of the source image contributes to one pixel of the
destination image with identical weights"

I suspect you're quoting https://pillow.readthedocs.io/en/latest/handbook/concepts.html#filters. If so, let's make what's inside the quote marks exact.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took in 7720f83.

Comment thread src/libImaging/Resample.c Outdated
Comment on lines +253 to +256
/* BOX maps each source pixel to exactly one output with equal weights.
When downscaling, (int)(center ± support + 0.5) and the discontinuous
box kernel can both miss a pixel whose center lands on a bin edge
(issue #9939). Tile bins as (left, right] instead. */

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/* BOX maps each source pixel to exactly one output with equal weights.
When downscaling, (int)(center ± support + 0.5) and the discontinuous
box kernel can both miss a pixel whose center lands on a bin edge
(issue #9939). Tile bins as (left, right] instead. */
/* For BOX, each source pixel contributes to one output pixel with equal weights.
When downscaling, (int)(center ± support + 0.5) and the discontinuous box kernel
can both miss a pixel whose center lands on a bin edge (issue #9939).
Tile bins as (left, right] instead. */

Maybe it's just me, but 'maps each source pixel to exactly one output' sounds like a one-to-one mapping, which it isn't.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took in 7720f83.

@Dmao233

Dmao233 commented Oct 23, 2026

Copy link
Copy Markdown
Author

Took both wording suggestions in 7720f83.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Any unexpected behavior, until confirmed feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BOX filter missed some edge-case pixels

5 participants