Repository navigation
Description
Bug report
As has been previously reported, imshow misaligns grid lines and image data. This gives a tiny reproducible example and discusses the specific issues. The example is actually mild. I have larger data sets where the effect is worse, added at the end; but the following tiny example demonstrates the core issues.
Code for reproduction
import matplotlib.pyplot as mp
import numpy as np
matrix = np.zeros((40, 40), dtype='int32')
np.fill_diagonal(matrix, 1)
mp.imshow(matrix)
axis = mp.gca()
axis.set_yticks([10.5, 20.5, 30.5], minor='True')
axis.set_xticks([10.5, 20.5, 30.5], minor='True')
axis.yaxis.grid(True, which='minor')
axis.xaxis.grid(True, which='minor')
mp.savefig(fname='bug.png')Actual outcome
Elaboration
Due to rounding, each cell of the data can have one of two sizes in pixels the image. This is understandable and inevitable. I added a red border using the smaller size to demonstrate the rounding issues, and annotated points where the actual issues arise. You would want to zoom in on the image to properly see these issues.
Points:
-
The cell at (0, 0) is clipped by 2 pixels in the Y direction and 1 pixel in the X direction. Compare to the cell at (39, 39) where the grid line is completely outside the cell.
-
Grid lines placed exactly between cells must, of necessity, overlap one of the cells. However, for best visual results, if the grid line is between a "big" cell and a "small" cell, the result is much better-looking if the grid line overlaps the "big" cell. You could argue this is nit-picking, but if rounding issues are addressed, this should be at least in the "to be considered" issues list.
-
The grid line which should have been between the cells is actually inside a cell. This seems to be an actual bug; on its face, there's no reason that this should ever happen, rounding errors or no rounding errors. It is worth noting that this seems to always happen in the Y direction and never in the X direction. There seems to be different treatment of X and Y axes when it comes to grid placement and rounding, expressed both at this point and at the first point. On it face, the code should be identical in both cases, so such axis-dependent artifacts should "never happen". If/when someone actually debugs the code, I would suggest that the first step would be looking for reasons why there is actually a difference between the X and Y axes.
-
Neither the (0, 0) cell nor the (39, 39) cell are "big" (with an extra pixel), because "big" cells appear only every 2/3 cells, and the (0, 0) cell and (39, 39) cells are too close to an existing big cell. Also, none of the "big" cells are exactly 2 or 3 cells away from either (0, 0) or (39, 39). Since the decision of whether a cell is "big" depends on rounding errors, one would expect that if every 4th cell is a "big" cell, then the 4th cell (starting from (0, 0), say) would be the 1st "big" cell. Perhaps using "round" shifts the pattern by one-half the repeat rate? Something to ponder when comparing the position of grid lines and cells (perhaps one us using "round" and another is using "floor")?
Matplotlib version
- Operating system: Linux, CentOs 7.5
- Matplotlib version: 2.2.3 (installed using
pip). - Matplotlib backend: TkAgg
- Python version: 3.6
- Jupyter version (if applicable): NA
- Other libraries: NA
Example with worse effects
The following example is from my actual program. The grid line coordinates are identical in the X and Y axis; you can see in the top-right corner the top-most gray grid line is too low (purple pixels visible above it). Even worse, the matrix has an all-0 diagonal. This ends up being several pixels away from the top-right grid corner. This seems to go way beyond a +/- one pixel rounding error to point at a more systematic problem. Perhaps the problem is that the grid Y size does not match the data's Y size in some way.



Activity
jklymak commentedon Dec 5, 2018
The grid issue is pixel-snapping of the line, which the image does not do.
Its possible there is an off-by-one pixel error in the image plotting, and how
imshowdecides to center the data it plots is a mystery to me, but...If you care about this stuff, save at a higher dpi. You are saving 2600 bits of information over an image that is 640 pixels wide, of which you are probably only using 400 pixels. If you save the above real plot at 300 dpi it will look much better, and actually be of publication quality; no journal will accept a 72 dpi image. If you need to downsample to 72 dpi for some reason, there are image processing programs that will do this for you properly.
If you are unhappy with how
imshowcenters bins (which I admit I don't know what the algorothm is), you can also usepcolormeshwhich allows precise placement of the bin edges (again so long as you use enough dpi).ImportanceOfBeingErnest commentedon Dec 5, 2018
This just distracts from the fact that there is a problem with gridline stamping and that e.g. for pdf output the dpi workaround is useless.
orenbenkiki commentedon Dec 5, 2018
Higher DPI might have solved the line-inside-cell issue, at least in this case. But I note that it did not solve the clipping of the top-left cell. It also hasn't solved the fact there's a difference between the handling of the X axis and the Y axis. Any such difference is definitely a bug IMO, regardless of how the algorithm works. I would also argue that the line-in-a-cell is also a bug, regardless of the DPI.
That said, I'll try increasing the DPI as a quick workaround and will also give
pcolormesha try. Thanks!jklymak commentedon Dec 5, 2018
The gridline stamping is understood, and would require a good bit of work to fix.
I don't understand the pdf comment: the dpi argument definitely affects images in pdf...
bug50.pdf
bug200.pdf
anntzer commentedon Dec 5, 2018
FWIW mplcairo (https://github.com/anntzer/mplcairo) renders the png nearly without the misalignment issue (if you squint hard enough some spillover from antialiasing remains visible), but the pdf (or svg) exhibit the misalignment even more (from a quick look).
tacaswell commentedon Dec 5, 2018
I would not strictly call this rounding (which implies we could be more precise), it is due to the incommensurate ratio between the array size and the rendered image size. Using small numbers, if you want to put a 4x4 grid across 10 pixels, there is no way to make each 'data pixel' the same size in 'screen pixels'. Moving to higher DPI gets you more pixels which makes the size differences smaller relative to the total figure size (and relative to the 'screen pixel size' of each 'data pixel').
jklymak commentedon Dec 5, 2018
I wonder if
savefig.dpi = 'figure'is a bad choice for the default. I agree its sweeping stuff under the rug, and I welcome the day we switch to mplcairo as the backend, or someone rolls up their sleeves and gets Agg just right... But in 2018 most folks doing scientific visualiztion have 4k/5k monitors at 200 dpi and terabyte harddrives, and I don't think saving a figure at 100 dpi by default makes much sense any more.tacaswell commentedon Dec 5, 2018
We can actually get data on this! GA gives us reported screen size and we looked at this around the time of 2.0, should re-visit that...
tacaswell commentedon Dec 6, 2018
Please explain why you think this is an issue with Agg? The pixel snapping is (intentionally) turned on for the grid and spines because if it is not and the line happens to fall between two screen pixels you get a twice as wide gray line and people complain about that too.
jklymak commentedon Dec 6, 2018
I think there is also an issue with agg’s centring of the grid cells. As @anntzer says above, the issues aren’t as obvious for mplcairo, so I assumed there is something in agg that is the differnce...
tacaswell commentedon Dec 6, 2018
Can you be clearer here? I have been trying to use the terms "screen pixel" and "data pixel" to refer to the pixels in the final output and the input array respectively.
Drawing an image we have to resample the input array on to the screen pixels. The part of this we have been struggling with post 2.0 has been the "the size of the input array -> size of the output array", this issue seems to be related to "how big should the output be and where do we put it". The image is going to be implicitly snapped to the screen pixels (as once we have the re-sampled data array we color map it and composite it into the output buffer). If there is a bug here, I suspect the logic that snaps the image to screen pixels does not then go back and adjust the exent of the
Imageobject (which in turn gets fed into the transforms used in the resamping. I strongly suspect that if you tweak the size in pixels of the OP slightly you can make issue 1 and 3 show up or go away in both the x and y directions.If mplcairo is not showing the issue, I supect that is because it does not support pixel snapping on the grid lines not that it is getting the images more correct (as where I think the bug is is in
image.pyand on a shared code path).orenbenkiki commentedon Dec 6, 2018
I used the term "cell" for what (I think) you call "data pixel", and used "pixel" for what (I think) you call "screen pixel".
I agree all the artifacts can be explained by the grid lines plotting thinking the data is plotted on a region with some size (X, Y) "screen pixels", while the actual data content plotting thinks the region has a different size (X + Extra-X, Y + Extra-Y) "screen pixels". If this is indeed the issue, it is clearly a bug. Note Extra-X and Extra-Y have different values! They seem to have a value of Extra-X = 1, Extra-Y = 2 in my example. This makes this bug even more "interesting".
I assume that adding a lot of print statements bisecting the path between the top most call and the actual plotting should pinpoint where exactly the sizes diverge. But I have no familiarity with the code base.
anntzer commentedon Dec 6, 2018
@tacaswell Actually mplcairo does snapping too.
Comparing the snapping implementations suggests that the bug is because matplotlib's snapping does not average to zero; the following patch appears to fix the issue
(mplcairo's version is at https://github.com/anntzer/mplcairo/blob/master/src/_util.cpp#L209)
If this indeed works, feel free to pick up the patch to make a PR out of it.
jklymak commentedon Dec 6, 2018
Humph, well that works better for consistency between the imshow and the grid lines, except now the grid lines don't agree with the tick locations :eyeroll:
Old
New
orenbenkiki commentedon Dec 6, 2018
Actually the grid lines seem to be technically correct (the best kind of correct! :-)
Seems like you have a grid line every 4 cells ("data pixels") and ticks every 5. Also the grid lines are between cells and the ticks are in the middle of cells.
Having grid lines every 5 cells, and not adding the 0.5 offset, should do the trick...
12 remaining items
tacaswell commentedon Dec 6, 2018
@anntzer 🐑 sorry for casting dispersion, happy to be wrong about this one.
The markers fall under the "draw once, stamp everywhere" issue. I think the consensus of how to fix that is to make 9 stamps of the marker offset a fraction of a pixel in each direction and then pick the right one at stamp time.
anntzer commentedon Dec 6, 2018
No, I tried that (not stamping ticks) in mplcairo and they actually look horrible when no snapped (the combination of being very short and fuzzy is not nice -- as opposed to long lines, which are usually okayish depending on your taste). Per above, I think the correct fix is to not draw them as markers but as lines (either a big LineCollection if we have the courage to refactor everything, or just a bunch of individual short lines -- it's not as if marker stamping was helpful in that case as each tick is drawn separately right now).
(Marker subpixel positioning (done by mplcairo) is useful in other cases of course.)
jklymak commentedon Dec 6, 2018
Yeah, I don't see any reason for the ticks to be drawn as markers.
While I agree it'd be nice to make them a LineCollection, for now, it seems "easy" to make them 2-D lines.
WeatherGod commentedon Dec 6, 2018
anntzer commentedon Dec 7, 2018
Somewhat accidentally, mplcairo also avoids the tick misalignment problem because it disables marker stamping if there are fewer than 81 markers: otherwise, it would need to generate 81 subpixel-shifted copies of the marker first, which makes the optimization moot.
In the case of ticks, there's only one copy, so life is good.
(Actually it's not 81, it's
1/rcParams["path.simplify_threshold"]**2(bounded to 256), as I use that rcParam to also control the stamping accuracy.)So if someone feels adventurous they could also try to disable stamping of single markers in the Agg code...
github-actions commentedon Jun 7, 2023
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!
szsdk commentedon Jun 7, 2023
This bug still exists in version 3.7.1.
jklymak commentedon Jun 7, 2023
I actually don't think there is any action items here. If you don't want this sort of aliasing then use
interpolation='Hanning', otherwise, you are usinginterpolation='nearest'and "nearest" has this sort of dither in it.I'd vote to close this, unless someone has a suggested way forward.
anntzer commentedon Jun 8, 2023
I didn't really check again but from a quick re-reading we may want to try disabling marker stamping for the single-marker (e.g., ticks) case?
timhoffm commentedon Jun 8, 2023
That was sort of my impression (or more generally speking, a solution is fundamentally possible if mplcairo does not suffer from the problem).
Either way, this got recent user response and therefore should not be "closed as inactive". If anything this would need a "closed as won't fix" but I don't have the competency to judge that we don't want to improve on this. If anybody else does, go for it.
jklymak commentedon Jun 8, 2023
So many orthogonal inconsistencies have been brought up in here that I find the issue more confusing than useful.