GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Fixed inaccurate image placement and even more resampling bugs by ayshih · Pull Request #31021 · matplotlib/matplotlib

Conversation

@ayshih

@ayshih ayshih commented Jan 23, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

PR summary

This PR now fixes a bunch of bugs:

  • Misaligned placement of AxesImage, PcolorImage, and NonUniformImage
  • Interpolation inaccuracies when affine resampling under nearest-neighbor interpolation
  • Edge effects when affine resampling under nearest-neighbor interpolation
  • Reflected data beyond the image bounds when nonaffine resampling under nearest-neighbor interpolation
  • Alpha/color issues at the edges when affine resampling under non-nearest-neighbor interpolation in RGBA space

Original post:

This PR fixes a bug with how images are resampled for drawing when they are positioned at a fractional pixel (which is nearly always the case). The existing code resamples the image to the (mostly) correct width/height in display pixels, but essentially assumes that the beginning edge of the display array is aligned with the beginning edge of the image array. This resampled image is then placed at the nearest display pixel, which combined with above, means that the drawn image can be up to a pixel off from the optimal location.

The "mostly" above is because the above bug means that the drawn image can leave a gap to the axes at the end edge of the drawn image, so there's additional, discomforting code to slightly enlarge the resampled image to cover up that gap. This additional code runs only for affine transformations, so you can see the gap when using a nonaffine transformation.

Before this PR:

before

This PR fixes the resampling logic so that it anticipates the rounding that will be used in the later drawing and defines the transform appropriately with the rounding baked in. That way, each display pixel has been resampled exactly correctly to match the eventual drawing. Furthermore, this means there are should no longer any gaps at the end edge of the drawn image, so there no longer needs to be an enlarging step.

The resampling code for nonaffine transforms has already been scrubbed extensively (#30054 and #30184), so drawing using a nonaffine transform now gives optimal results with this PR.

Unfortunately, while drawing using an affine transform is also broadly improved, there are still cases where the results are not optimal due to inaccuracies in the image-resampling code for affine transforms. See #30184 (comment) for some details. I'm mulling over whether to greatly expand the solution described in #30184 (comment), that is, taking the switching to the nonaffine-transform code path for extreme zooms (>128x) and extending it down to minor zooms (>~5x).

Update: I've made changes to how the Agg resampler handles affine transforms when using nearest-neighbor interpolation, and now drawing using an affine transform also gives optimal results.

After this PR:

after

Generating script:

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.transforms import Transform

# Create a nonaffine identity to easily convert an affine transform to its nonaffine equivalent
class NonAffineIdentityTransform(Transform):
    input_dims = 2
    output_dims = 2

    def inverted(self):
        return self
nonaffine_identity = NonAffineIdentityTransform()

fig, axs = plt.subplots(2, 3)

N = [3, 7, 11]

for j in range(3):
    data = np.arange(N[j]**2).reshape((N[j], N[j])) % 4
    seps = np.arange(-0.5, N[j])

    for i in range(2):
        if i == 0:
            axs[i, j].imshow(data, cmap='Grays')
            axs[i, j].set_title('affine')
        else:
            axs[i, j].imshow(data, cmap='Grays', transform=nonaffine_identity + axs[i, j].transData)
            axs[i, j].set_title('nonaffine')

        axs[i, j].set_axis_off()
        axs[i, j].vlines(seps, -1, N[j], linewidth=0.5, color='red', linestyle='dashed')
        axs[i, j].hlines(seps, -1, N[j], linewidth=0.5, color='red', linestyle='dashed')

plt.show()

Fixes #31009

PR checklist

  • "closes #0000" is in the body of the PR description to link the related issue
  • new and changed code is tested
  • [N/A] Plotting related features are demonstrated in an example
  • New Features and API Changes are noted with a directive and release note
  • [N/A] Documentation complies with general and docstring guidelines

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

adeak and scottshambaugh reacted with hooray emoji
All reactions
  • 🎉 2 reactions
@github-actions github-actions Bot added the topic: images label Jan 23, 2026
@ayshih ayshih mentioned this pull request Jan 23, 2026
Fix: prevent imshow pixel overlap with grid lines #31015
Closed
@ayshih
ayshih force-pushed the make_image_fix branch from 59e9389 to 8b0224e Compare January 23, 2026 15:52
@QuLogic

QuLogic commented Jan 24, 2026

Copy link
Copy Markdown
Member

Does this replace #30175?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ayshih

ayshih commented Jan 24, 2026

Copy link
Copy Markdown
Contributor Author

Yes, so I'll close #30175

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ayshih ayshih changed the title Fixed image resampling when the image is placed at a fractional pixel Fixed image resampling when the image is positioned at a fractional pixel Jan 24, 2026
@ayshih
ayshih force-pushed the make_image_fix branch 5 times, most recently from 3b36edb to af951be Compare January 29, 2026 04:07
@github-actions github-actions Bot removed the topic: images label Jan 29, 2026
@ayshih
ayshih force-pushed the make_image_fix branch from 1f49307 to a3764d8 Compare January 29, 2026 04:58
@github-actions github-actions Bot added the topic: images label Jan 29, 2026
@ayshih

ayshih commented Jan 29, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor Author

I've made changes to how the Agg resampler handles affine transforms when using nearest-neighbor interpolation, and now drawing using an affine transform also gives optimal results.

To elaborate, the issue at hand is that the Agg resampler uses an integer-based interpolator for affine resampling (see some details in #30184 (comment)). The resulting inaccuracies can be obvious when using nearest-neighbor interpolation. This PR replaces that interpolator with a floating-point-based interpolator, and qualitatively there is no discernable performance hit.

As a nice benefit of this change, the solution described in #30184 (comment) – falling back to nonaffine resampling under extreme zooms – is no longer necessary to preserve the alignment of pixel edges.

To be safe performance-wise, affine resampling under non-nearest-neighbor interpolation continues to use the integer-based interpolator. Unlike with nearest-neighbor interpolation, it's very difficult to tell if there are slight inaccuracies with the interpolator. This can be revisited in the future.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ayshih
ayshih force-pushed the make_image_fix branch from a3764d8 to a9da7a6 Compare January 29, 2026 16:44
@ayshih ayshih changed the title Fixed image resampling when the image is positioned at a fractional pixel Fixed inaccurate image placement and even more resampling bugs Jan 29, 2026
@ayshih
ayshih force-pushed the make_image_fix branch 2 times, most recently from 104077d to e919599 Compare January 30, 2026 05:25
@ayshih

ayshih commented Jan 30, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor Author

Even with the fixing the affine resampling, the resampled image continued to sometimes have a gap at the end. It turns out that a rendering box was being applied to affine resampling, even under nearest-neighbor interpolation, and that could hinder the rendering of the edges. Under nearest-neighbor interpolation, the interpolation is now calculated exactly correctly, so there is no longer a need to apply a rendering box, which avoids the occasional appearance of gaps between the image and the axes spines.

The reason for the rendering box was for non-nearest-neighbor interpolation. To resample up to the edge of the image, the code uses the assumption of reflection wrapping on all edges. For nearest-neighbor interpolation, this PR now fills to zero beyond the bounds of the source image instead of reflection wrapping, which allows us to avoid a rendering box.

For arbitrary nonaffine resampling, a rendering box cannot be reliably defined, so nonaffine resampling has had the long-standing bug of always showing the reflection wrapping.

Before this PR

before

The same removal of reflection wrapping done for affine resampling under nearest-neighbor interpolation can be done for nonaffine resampling under nearest-neighbor interpolation,

After this PR

after

Fixing the nonaffine resampling under non-nearest-neighbor interpolation is a more challenging problem, and must be deferred to a future effort.

Generating code

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Rectangle
from matplotlib.transforms import Affine2D, Transform

# Create a nonaffine identity to easily convert an affine transform to its nonaffine equivalent
class NonAffineIdentityTransform(Transform):
    input_dims = 2
    output_dims = 2

    def inverted(self):
        return self
nonaffine_identity = NonAffineIdentityTransform()

fig, axs = plt.subplots(2, 3)

N = 11

interpolation = ['nearest', 'hanning', 'bilinear']

for j in range(3):
    data = np.arange(N**2).reshape((N, N))
    data = data / N**2 + (data % 4) / 6
    rotation = Affine2D().rotate_around(N/2-0.5, N/2-0.5, 1)

    for i in range(2):
        if i == 0:
            axs[i, j].imshow(data, cmap='Grays', interpolation=interpolation[j],
                             transform=rotation + axs[i, j].transData)
            axs[i, j].set_title(f"{interpolation[j]}\naffine")
        else:
            axs[i, j].imshow(data, cmap='Grays', interpolation=interpolation[j],
                             transform=nonaffine_identity + rotation + axs[i, j].transData)
            axs[i, j].set_title('nonaffine')

        axs[i, j].set_axis_off()
        box = Rectangle((-0.5, -0.5), N, N,
                        edgecolor='red', facecolor='none', lw=0.5, ls='dashed',
                        transform=rotation + axs[i, j].transData)
        axs[i, j].add_artist(box)

plt.show()
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ayshih
ayshih force-pushed the make_image_fix branch from e919599 to b413c6c Compare January 30, 2026 05:55
@github-actions github-actions Bot added the topic: mpl_toolkit label Jan 30, 2026
@ayshih
ayshih force-pushed the make_image_fix branch 2 times, most recently from f5088a9 to 7472d60 Compare January 30, 2026 14:46
@ayshih

ayshih commented Jan 30, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor Author

This PR now also fixes the pixel alignment for PcolorImage and NonUniformImage. The coordinates are specified differently (edges versus centers) for these two image types, so the two types are not supposed to agree. Just look at the pixel alignment to grid lines in the upper right of each panel (away from the yellow/red box).

Before this PR

before

After this PR

after

Generating code

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.image import NonUniformImage
from matplotlib.patches import Rectangle

fig, axs = plt.subplots(2, 3)

N = [4, 9, 11]

for j in range(3):
    data = np.arange(N[j]**2).reshape((N[j], N[j])) % 5
    seps = np.arange(-0.5, N[j] + 1)

    x_edges = np.arange(-0.5, N[j])
    x_edges[2:] += 1
    y_edges = np.arange(-0.5, N[j])
    y_edges[3:] += 1

    x_mids = (x_edges[:-1] + x_edges[1:]) / 2
    y_mids = (y_edges[:-1] + y_edges[1:]) / 2

    for i in range(2):
        if i == 0:
            axs[i, j].pcolorfast(x_edges, y_edges, data, cmap='Grays', interpolation='nearest')
            axs[i, j].set_title('PcolorImage')
        else:
            im = NonUniformImage(axs[i, j], interpolation='nearest', cmap='Grays')
            im.set_data(x_mids, y_mids, data)
            axs[i, j].add_image(im)
            axs[i, j].set_title('NonUniformImage')

        axs[i, j].set_aspect('equal')
        axs[i, j].set_axis_off()
        axs[i, j].vlines(seps, -1, N[j] + 1, lw=0.5, color='red', ls='dashed')
        axs[i, j].hlines(seps, -1, N[j] + 1, lw=0.5, color='red', ls='dashed')

        box = Rectangle((0.5, 1.5), 2, 2, edgecolor='yellow', facecolor='none', lw=1)
        axs[i, j].add_artist(box)

plt.show()
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ayshih
ayshih force-pushed the make_image_fix branch 2 times, most recently from a034951 to 50b6ed3 Compare January 31, 2026 04:46
@ayshih

This comment was marked as outdated.

Sign in to view
@ayshih
ayshih force-pushed the make_image_fix branch 2 times, most recently from c752389 to 6fdf13e Compare January 31, 2026 05:37
tacaswell
tacaswell reviewed Feb 27, 2026
View reviewed changes
Comment thread src/_image_resample.h Resolved

Uh oh!

There was an error while loading. Please reload this page.

tacaswell
tacaswell approved these changes Feb 27, 2026
View reviewed changes

@tacaswell tacaswell left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

modulo fixing the c++ style issue.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
QuLogic
QuLogic reviewed Feb 27, 2026
View reviewed changes

@QuLogic QuLogic left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Some minor nits, but also a small ask: please add a bit more context to the commit messages; it is helpful to find out the why of a change without having to search out the PR. Maybe not the entirety of the test images you have here, but at least more than the single-line messages.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread src/agg_workaround.h Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread lib/matplotlib/tests/test_image.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread lib/matplotlib/tests/test_image.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

@tacaswell tacaswell mentioned this pull request Feb 27, 2026
[Bug]: alpha array-type not working with RGB image in imshow() #26092
Closed
@ayshih
ayshih force-pushed the make_image_fix branch 3 times, most recently from 992b375 to 72dc4aa Compare February 28, 2026 05:38
@ayshih

ayshih commented Feb 28, 2026

Copy link
Copy Markdown
Contributor Author

please add a bit more context to the commit messages; it is helpful to find out the why of a change without having to search out the PR.

Sure, I've added a bunch of sentences to the commit messages

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@QuLogic

QuLogic commented Mar 1, 2026

Copy link
Copy Markdown
Member

I'd like to keep the individual commits, so please squash the fixups into the originals. I assume that a lot of the commits affect test images repeatedly, so it's probably fine to keep those updates in a single separate commit.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

ayshih added 9 commits March 1, 2026 21:57
@ayshih
Fix making an image that is to be placed at a fractional pixel
89bbd1c
The code now specifies the resampling with the foreknowledge that the
resampled image will be placed at the nearest display pixel.  This
fixes situations where the rendered image can appear to be shifted by up
to one display pixel.
@ayshih
Fix affine resampling when using nearest-neighbor interpolation
0ecf660
The built-in Agg interpolator for determining the nearest-neighbor
pixels under an affine transform uses integer-based math, but that
results in slight inaccuracies that can become visible at even a
minor level of magnification.  A custom interpolator that uses
floating-point-based math is now used instead.
@ayshih
Fix the handling of image resampling beyond the image bounds
d9d7d15
For nearest-neighbor interpolation, the code no longer applies any
antialiasing of edges under an affine transform for more accurate edges
and no longer shows data beyond the image bounds under a non-affine
transform.
@ayshih
Fix pixel alignment for PcolorImage and NonUniformImage
2d37c64
Calculations now take into account that rendering will be to the nearest
display pixel.
@ayshih
Switch to premultiplied space inside Agg for RGBA blending
dd24e05
Premultiplied image data is passed to the Agg resampler so that the
interpolation is handled correctly.  This code now tells Agg that the
image is premultiplied so that the antialiasing of edges is handled
correctly.
@ayshih
Adjust to favor the left input pixel for nearest-neighbor interpolation
35ccd4c
This fixes a subtle issue where a resampled pixel falling exactly
between two input pixels would normally round half up, but would also be
rendered with rounding half up, resulting in the resampled pixel
having the wrong value.  The code now functionally rounds half down,
which cancels out the effect of rendering rounding half up in these
situations, and otherwise makes no difference.
@ayshih
Bump rendering edges to fill axes when rendering affine non-NN interp…
f1a1fb4
…olation

This fixes a visual issue where there can appear to be a slight gap
between an image and the axes spines even when it is supposed to fill
the axes.  The code now expands the rendering edges to fill the axes
without degrading the placement accuracy within the image.
@ayshih
Add four figure tests and changelog entry
ae239ea
@ayshih
Adjust existing figure test and update baseline images
800185c
@ayshih
ayshih force-pushed the make_image_fix branch from 72dc4aa to 800185c Compare March 2, 2026 02:58
@ayshih

ayshih commented Mar 2, 2026

Copy link
Copy Markdown
Contributor Author

Sure, I've squashed the commits down to nine commits

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@QuLogic
QuLogic merged commit 86f65d7 into matplotlib:main Mar 2, 2026
36 of 38 checks passed

Uh oh!

There was an error while loading. Please reload this page.

@QuLogic QuLogic added this to the v3.11.0 milestone Mar 2, 2026
@ayshih
ayshih deleted the make_image_fix branch March 2, 2026 13:38
pllim added a commit to pllim/astropy that referenced this pull request Mar 3, 2026
@pllim
TST: Update hash for mpldev image tests
2f61b05
due to upstream changes at matplotlib/matplotlib#31021
@pllim pllim mentioned this pull request Mar 3, 2026
TST: Update hash for mpldev image tests astropy/astropy#19354
Merged
1 task
@ayshih ayshih mentioned this pull request Mar 4, 2026
Fix figure-test CI sunpy/sunpy#8533
Merged
1 of 5 tasks
@rcomer rcomer mentioned this pull request Mar 10, 2026
[TST] Upcoming dependency test failures SciTools/cartopy#2634
Closed
@ayshih ayshih mentioned this pull request Mar 16, 2026
Fixed lingering bugs with image rendering related to exact half display pixels #31313
Merged
1 task done
@ayshih ayshih mentioned this pull request Jul 23, 2026
Fix alpha handling when flattening images for vector backends #32107
Merged
2 tasks done
@ayshih ayshih mentioned this pull request Aug 24, 2026
[Bug]: AxesImage redraws ~2x slower in 3.11 than in 3.10 #32240
Closed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@QuLogic QuLogic QuLogic left review comments
@scottshambaugh scottshambaugh scottshambaugh approved these changes
@tacaswell tacaswell tacaswell approved these changes

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

topic: images topic: mpl_toolkit

Projects

None yet

Milestone

v3.11.0

Development

Successfully merging this pull request may close these issues.

[Bug]: Large pixels may overlap when using imshow()

Uh oh!

There was an error while loading. Please reload this page.

4 participants

@ayshih @QuLogic @scottshambaugh @tacaswell
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.