Skip to content

fix: align rasterized artists with get_extent #742

Merged
timtreis merged 2 commits into
mainfrom
fix/issue-216-pixel-shift
Jun 22, 2026
Merged

fix: align rasterized artists with get_extent #742
timtreis merged 2 commits into
mainfrom
fix/issue-216-pixel-shift

Conversation

@timtreis

@timtreis timtreis commented Jun 22, 2026

Copy link
Copy Markdown
Member

Closes #216.

Problem

Rasterized artists (images, labels, datashader output) used matplotlib's default pixel-center imshow extent (-0.5, W-0.5, ...), while axis limits (get_extent), the element affine, and point/shape overlays use the pixel-edge convention. So every image sat half a pixel off — and the affine amplifies it (a Scale(1000) image shifted by 500 world units).

Fix

Use pixel-edge extent (0, W, H, 0) so each artist's data box is the [0, shape] box get_extent transforms — they coincide under any affine.

  • _datashader.py _ax_show_and_transform — shared by images + datashader.
  • render.py _render_labels — labels bypass that helper via a direct imshow(origin="lower"), so they get an explicit extent=(0, W, 0, H).

This also removes the residual datashader-points offset relative to the matplotlib backend.

Why pixel-edge

It's spatialdata's own convention: get_extent reports an image as occupying [0, shape], and get_centroids puts pixel [0,0]'s centroid at (0.5, 0.5). The fix matches the data model — a get_centroids overlay now lands dead-center on its label pixel instead of on the corner.

@timtreis timtreis changed the title fix: align rasterized artists with get_extent (resolve #216 pixel shift) fix: align rasterized artists with get_extent Jun 22, 2026
@timtreis timtreis force-pushed the fix/issue-216-pixel-shift branch from 1bdf93c to bf0d385 Compare June 22, 2026 11:53
…ent)

Images, labels, and datashader output were drawn with matplotlib's default
pixel-center imshow extent (-0.5, W-0.5, ...), placing them half a pixel off
the world coordinates used for the axis limits (get_extent), the affine box,
and matplotlib point/shape overlays. The affine's linear part amplifies the
constant 0.5 offset, so a Scale(1000) image shifted by 500 world units (#216).

Switch to the pixel-edge convention (0, W, H, 0) so each artist's data box is
the same [0, shape] box get_extent transforms — they now coincide under any
affine. Labels are drawn outside the shared _ax_show_and_transform helper via a
direct imshow(origin="lower"), so they get an explicit extent=(0, W, 0, H)
(origin-lower order) to match. This also removes the residual half-canvas-pixel
offset of datashader points relative to the matplotlib backend.

The pixel-edge convention is spatialdata's own: get_extent reports an image as
occupying [0, shape], and get_centroids places a single pixel's centroid at its
half-integer center — so a get_centroids overlay now lands dead-center on its
label pixel instead of on the corner.

Tests:
- Non-visual regressions (test_utils.py) asserting the rendered world box equals
  get_extent for images and labels (Identity + Scale), and that the datashader
  points image occupies the points' extent.
- Visual regression (test_render_labels.py) overlaying get_centroids on a small
  label grid, where the half-pixel shift is large in display pixels: dots sit at
  pixel centers after the fix, at pixel corners before.
@timtreis timtreis force-pushed the fix/issue-216-pixel-shift branch from bf0d385 to bfe97ea Compare June 22, 2026 12:03
Datashader points/shapes, image-extent, and a few image baselines shift by the
half-pixel correction (sub-tolerance for large images, hence only 32 of them).
Adds the baseline for the new centroid-alignment visual test. Regenerated from
CI artifacts (py3.11-stable; 31/32 byte-identical to py3.14-stable).
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.59%. Comparing base (50a6606) to head (6eafd21).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #742   +/-   ##
=======================================
  Coverage   79.59%   79.59%           
=======================================
  Files          17       17           
  Lines        4641     4641           
  Branches     1029     1029           
=======================================
  Hits         3694     3694           
  Misses        598      598           
  Partials      349      349           
Files with missing lines Coverage Δ
src/spatialdata_plot/pl/_datashader.py 87.10% <100.00%> (ø)
src/spatialdata_plot/pl/render.py 89.62% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@timtreis

Copy link
Copy Markdown
Member Author

FYI @LucaMarconato

ignored until now because I didn't think it'd matter much on real data, but I guess good that it's eventually fixed

@timtreis timtreis merged commit 293d41c into main Jun 22, 2026
7 of 8 checks passed
@timtreis timtreis deleted the fix/issue-216-pixel-shift branch June 22, 2026 19:09
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.

Pixel shift in render_images

2 participants