Skip to content

Round halves up when converting crop coordinates to integers - #10243

Open
sungbin1015 wants to merge 3 commits into
python-pillow:mainfrom
sungbin1015:crop-round-half-up
Open

sungbin1015 wants to merge 3 commits into
python-pillow:mainfrom
sungbin1015:crop-round-half-up

Conversation

@sungbin1015

Copy link
Copy Markdown
Contributor

Follows on from #10242.

Changes proposed in this pull request:

  • Round crop coordinates ending in .5 up, instead of to the nearest even integer
  • Add test_crop_float_half
  • Add a 13.1.0 release note

_crop() uses round(), which since Python 3 sends halves to the nearest even integer. So the same box can come out a pixel larger or smaller depending on where it is:

from PIL import Image

im = Image.new("L", (200, 10))
for left in (84.5, 85.5):
    print(im.crop((left, 0, left + 31, 1)).size)
print(im.crop((1.5, 0, 2.5, 1)).size)
(32, 1)
(30, 1)
(0, 1)

With math.floor(v + 0.5) these are (31, 1), (31, 1) and (1, 1). Coordinates that aren't exactly .5 round as before, so test_crop_float from #1745 is unchanged. #10229 is close in spirit, but it changes how ImageDraw truncates negative coordinates in C and doesn't touch crop().

The full test suite and pre-commit pass locally.

@Muguett-DBY Muguett-DBY left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I checked the exact base and PR _crop implementations against the Pillow 13.0.0 core. The intended half-coordinate cases work, but a float just below a half can now lose a pixel; the inline comment has a small reproduction.

This was a targeted crop check, rather than a full build of the PR. The installed 13.0.0 wheel also passed the 59 upstream self-tests.

Comment thread src/PIL/Image.py Outdated
"""

x0, y0, x1, y1 = map(int, map(round, box))
x0, y0, x1, y1 = (math.floor(v + 0.5) for v in box)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we keep non-tie coordinates rounding as before? math.nextafter(0.5, 0.0) is below 0.5, but adding 0.5 rounds the intermediate value to exactly 1.0:

v = math.nextafter(0.5, 0.0)
im = Image.new("L", (4, 1))
im.putdata([10, 20, 30, 40])
out = im.crop((v, 0, 3, 1))
print(out.size, list(out.get_flattened_data()))

Base: (3, 1) [10, 20, 30]; this change: (2, 1) [20, 30].

Computing n = math.floor(v) first, then using n + (v - n >= 0.5), avoids that intermediate rounding. I checked that alternative against exact-ratio rounding for 288 cases across all four box edges and L/RGB/RGBA; it also preserves the intended half-up behavior. A regression case just below 0.5 would catch this.

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.

I've pushed a commit to address this scenario.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the update. Rechecking the example above at 59cf86c, the size now matches the base, but the pixels still shift: base [10, 20, 30]; this head [20, 30, 40].

Rounding the width separately also changes some boxes without .5 coordinates. With the image above, im.crop((0.49, 0, 1.51, 1)) returns (2, 1) / [10, 20] on the base and (1, 1) / [10] on this head. Is that broader change intentional? The description currently says non-half coordinates round as before.

I checked the exact _crop methods from base 1295b5c and head 59cf86c against Pillow 13.0.0's core on Windows / Python 3.15; this wasn't a full PR build.

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.

Ok, I've pushed another commit that should meet your expectations now.

@radarhere
radarhere requested a review from hugovk November 10, 2026 04:36
@codspeed

codspeed Bot commented Nov 15, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 10.77%

1 warning

⚠️ 3 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

❌ 1 regressed benchmark
✅ 667 untouched benchmarks
⏩ 352 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ test_crop[1237x811-L-scale0] 1.6 ms 1.8 ms -10.77%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing sungbin1015:crop-round-half-up (1904155) with main (b60880a)

Open in CodSpeed

Footnotes

  1. 352 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants