Skip to content

[Bug]: Rounding to pixel boundary misaligns images in vector graphics outputs #20964

Closed

Description

@lschr

Bug summary

When using Axes.imshow(), images are scaled to integer pixel size (_ImageBase._make_image). This makes images notably too large when using vector graphics output, misaligning them with tick marks and plotted lines.

Code for reproduction

import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt

img = np.zeros((100, 400))
img[::5, ::5] = 1
idx = np.indices(img.shape)
idx = [idx[0][::5].flatten(), idx[1][:, ::5].flatten()]

fig, ax = plt.subplots(constrained_layout=True)
ax.imshow(img, interpolation="None", origin="lower")
ax.scatter(idx[1], idx[0], marker="x", lw=0.2, s=1)
fig.savefig("imshow.pdf")

Actual outcome

The image does not align with plot markers, tick marks. Towards the top-right corner, they are even placed in adjacent pixels.
image
Full PDF output

Expected outcome

This can be worked around by calling _ImageBase._make_image with round_to_pixel_border=False:

class NotRoundedAxesImage(mpl.image.AxesImage):
    """AxesImage subclass which does not round image extents to fit frame

    This rounding leads to improper alignment between data and image
    """
    def make_image(self, renderer, magnification=1.0, unsampled=False):
        # docstring inherited
        trans = self.get_transform()
        # image is created in the canvas coordinate.
        x1, x2, y1, y2 = self.get_extent()
        bbox = mpl.transforms.Bbox(np.array([[x1, y1], [x2, y2]]))
        transformed_bbox = mpl.transforms.TransformedBbox(bbox, trans)
        clip = ((self.get_clip_box() or self.axes.bbox) if self.get_clip_on()
                else self.figure.bbox)
        # Turn off round_to_pixel_border!
        return self._make_image(self._A, bbox, transformed_bbox, clip,
                                magnification, unsampled=unsampled,
                                round_to_pixel_border=False)
    
fig, ax = plt.subplots(constrained_layout=True)
# Replicate Axes.imshow
ax.set_aspect(mpl.rcParams["image.aspect"])
ai = NotRoundedAxesImage(ax, interpolation="none")
ai.set_data(img)
ai.set_extent(ai.get_extent())
ax.add_image(ai)
ax.scatter(idx[1], idx[0], marker="x", lw=0.2, s=1)
fig.savefig("custom_image.pdf")

image
Full PDF output

Operating system

Ubuntu 20.04

Matplotlib Version

3.4.3

Matplotlib Backend

PDF, SVG, PGF, probably others

Python version

3.7

Jupyter version

No response

Other libraries

No response

Installation

conda

Conda channel

conda-forge

Activity

  1. lschr commented on Sep 1, 2021

    @lschr
    ContributorAuthor

    This may be related to #8981.

  2. jklymak commented on Sep 1, 2021

    @jklymak
    Member

    Currently away from my computer but there is more discussion of this and a fix (basically don't force vector backends to have integer dpi)

  3. Spl1x commented on Feb 23, 2022

    @Spl1x

    This is the same issue, which also leads to a misalignment of axis ticks:

    import numpy as np
    import matplotlib as mpl
    import matplotlib.pyplot as plt
    
    fig, ax = plt.subplots()
    
    # note missalignment at 0.5 tick
    ax.imshow(np.array([[0, 1], [1, 0]]))
    fig.savefig("current.pdf")

    image
    Full output: current.pdf

    The problem is fixed, when implementing the solution suggested by @lschr:

    import numpy as np
    import matplotlib as mpl
    import matplotlib.pyplot as plt
    
    
    class NotRoundedAxesImage(mpl.image.AxesImage):
        """AxesImage subclass which does not round image extents to fit frame
    
        This rounding leads to improper alignment between data and image
        """
        def make_image(self, renderer, magnification=1.0, unsampled=False):
            # docstring inherited
            trans = self.get_transform()
            # image is created in the canvas coordinate.
            x1, x2, y1, y2 = self.get_extent()
            bbox = mpl.transforms.Bbox(np.array([[x1, y1], [x2, y2]]))
            transformed_bbox = mpl.transforms.TransformedBbox(bbox, trans)
            clip = ((self.get_clip_box() or self.axes.bbox) if self.get_clip_on()
                    else self.figure.bbox)
            # Turn off round_to_pixel_border!
            return self._make_image(self._A, bbox, transformed_bbox, clip,
                                    magnification, unsampled=unsampled,
                                    round_to_pixel_border=False)
    
    
    fig, ax = plt.subplots()
    # Replicate Axes.imshow
    ax.set_aspect(mpl.rcParams["image.aspect"])
    ai = NotRoundedAxesImage(ax, interpolation="none")
    ai.set_data(np.array([[0, 1], [1, 0]]))
    ai.set_extent(ai.get_extent())
    ax.add_image(ai)
    
    fig.savefig("custom_image.pdf")

    Ticks now align perfectly:
    image
    Full output: custom_image.pdf

    This issue may also be related to #17182.

    Discussions about this issue seem to be going on for quite some time, cf.
    #16378
    #12934
    #6827
    #7341

    After some closer investigation, it turns out that the misalignment issue is still present and 'only' significantly smaller.

  4. github-actions commented on Nov 28, 2025

    @github-actions

    This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear. Thanks for your help!

  5. added
    status: closed as inactiveIssues closed by the "Stale" Github Action. Please comment on any you think should still be open.
    on Dec 30, 2025
  6. added
    keepItems to be ignored by the “Stale” Github Action
    on Dec 30, 2025
  7. removed
    status: closed as inactiveIssues closed by the "Stale" Github Action. Please comment on any you think should still be open.
    on Dec 30, 2025
  8. QuLogic commented on Feb 5, 2026

    @QuLogic
    Member

    I can't reproduce this with 3.10.6, but there have also been many fixes for image resampling for 3.11 that will be coming soon as well.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    keepItems to be ignored by the “Stale” Github Action

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions