-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
BUGFIX: use true bbox for rasters in backend_mixed #17182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
This looks far better! I don't see why anyone would care if their raster in a vector graphic was 300.5 dpi instead of 300, but perhaps we should make sure there are no practical reasons that is a problem. @jkseppan is the PDF backend expert and may have some thoughts. So do those methods all really need to be re-implimented? Before you do all that can we check that you are doing this the easiest way? I took a quick look at the code this morning, and it does seem something will have to bee added because |
Thanks for helping out Jody!
The methods don't need to be reimplemented, we just need to write small little wrappers that compute the bbox in each case, but would also be happy to hear if there is a cleaner way! It's unfortunate, because at the level of the individual For example the colorbar is a |
lib/matplotlib/image.py
Outdated
@@ -144,7 +144,11 @@ def flush_images(): | |||
gc = renderer.new_gc() | |||
gc.set_clip_rectangle(parent.bbox) | |||
gc.set_clip_path(parent.get_clip_path()) | |||
renderer.draw_image(gc, round(l), round(b), data) | |||
if type(renderer) == mpl.backends.backend_agg.RendererAgg: |
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.
why?
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.
Hadn't thought of third-party backends. With your suggestion, it would be something like...
if hasattr(renderer, 'draw_image_2'):
but this would effectively be deprecating a pretty core rendering function....so probably not a good idea?
@@ -217,7 +217,7 @@ def draw_markers(self, gc, marker_path, marker_trans, path, transform, | |||
self._fill_and_stroke( | |||
ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha()) | |||
|
|||
def draw_image(self, gc, x, y, im): | |||
def draw_image(self, gc, x, y, im, bbox=None): |
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.
What is supposed to happen if the bbox corner is not consistent with the (x, y)? Should this really rather be a new method draw_image_2(self, gc, bbox, im): ...
? This will make it easier to check whether third-party backends implement that method, too. (Or if we keep the same name, we may need signature-sniffing tricks to figure out signature to use... or something like option_scale_image.)
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.
Didn't think of third-party backends. I guess I could implement it as a separate method.
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.
Actually, to prevent having to do some weird deprecation stuff to draw_image
, I think the better solution is to just add optional width
and height
arguments instead, and yeah....a hasattr
trick to check what signature to use would work....even though it feels icky to write. Like option_exact_image_bbox
.
lib/matplotlib/image.py
Outdated
renderer.draw_image(gc, l, b, im) | ||
else: | ||
renderer.draw_image(gc, round(l), round(b), data, | ||
bbox=parent.bbox) |
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.
Turns out this is wrong (the test failure test_image.py:test_bbox_image_inverted is real), since BboxImages are allowed to fall outside the axes (parent) bbox.
There are two ways that draw_image
gets called. By stop_rasterizing
and by (_ImageBase).draw
(here). The latter is correctly dealt with in this PR already, but the former needs a more "correct" fix than passing the parent bbox.
Do you really need to pass in the true bbox explicitly? Can the vector backends not infer it by applying transform to |
As far as I can tell, the information about what the original size of the figure is has been lost by the time that you reach the vector backend. We could in principle hijack the I'm happy with whatever everyone agrees is the best/minimal change to the interface. However, the bigger issue is that it looks like I have to make an API change of some sort anyway in order to have the actual extents available to pass to There are two-ish places where What I currently do (use the Bbox of the Solutions I can think of:
Happy to hear if people approve and/or have better ideas? |
That said, maybe in terms of scope, we could leave |
May be easier to discuss this over a call (at least right now I have trouble keeping track of all moving parts); let's try to discuss this on Monday or possibly separately? |
Having a look at it again briefly (sorry for doing this in an on-and-off manner), I think you can indeed just smuggle the relevant information in the transform kwarg (and hence have no external API change) by doing the relevant work in make_image/_make_image? |
Not at all, thanks for looking into it! You're right, I'll push up a version with no external API change before the meeting tomorrow :) |
1b0a9d4
to
b99dec2
Compare
Decided that in order to keep this PR sane, I have to restrict it to only fix half of the "bug". The new version I just pushed fixes the colorbar bug, but leaves the imshow bug as-is. Separate PR for imshow bug incoming. My new fix seems to work great, unless someone requests a rasterized Path during a call to a MixedModeRenderer and the linewidth is larger than a few pixels (no idea why you would do such a thing, since the whole idea behind mixed mode renderers is to be able to have paths vectorized). This corner case has a regression (ironically, in The lines in the middle and on the right are supposed to look the same: The issue is that my new code uses the following trick:
That works great, except for in order to calculate the correct #17198 is a big PR. But until it gets merged, we need to accept this regression (the users that would trigger such a thing, I'm not very sympathetic to, personally, whereas I do think we should be able to render colorbars without them being super ugly) or come up with a whole new strategy, that doesn't involve pre-computing where each rasterization will be placed in the final vector image. The other |
b99dec2
to
66b0a11
Compare
@brunobeltran where is this PR at? |
ping @brunobeltran This will fix a lot of long-standing bugs. If you have abandoned it, please let us know and someone will pick it up... |
I'll ping again, and marking as stale... |
I've added to the 3.6 milestone and to the project board as I think this is quite high priority. Its a little ridiculous that our vector graphic rasters snap to virtual pixels. |
ping @brunobeltran - I'll take this over if you can't get back to this as its pretty high priority. We get bug reports every couple of months about this... |
66b0a11
to
5fb7665
Compare
pro: I think I got the rebase to work! |
ok understand one of the errors:
|
Unfortunately I think this is going to have to get punted a version again. |
36cbda3
to
1869fc3
Compare
Co-authored-by: Jody Klymak <jklymak@gmail.com> Co-authored-by: Thomas A Caswell <tcaswell@gmail.com>
3a091c7
to
bd3ff10
Compare
This version passes all tests, and adds one more test to ps backend. Obviously some image changes. It does not wrap markers so that is still a todo, but it doesn't make the situation worse. @anntzer suggest wrapping this in the |
I only looked at this briefly again. Looks like since my last comment (and probably since #17182 (comment)) the API changes are now limited to the addition of the true_size kwarg which effectively(?) serves as a private communication channel between MixedModeRenderer and the builtin vector backends, which is not so much of an issue (at least on mplcairo's side I just completely skip trying to work with MixedModeRenderer...). Still it's not entirely clear to me what the semantics of Minor review points: true_size should probably be true_shape (for consistency with ndarray.shape/ndarray.size), and option_true_bbox_image is no longer needed. |
I think that is unchanged. The new codepaths are only if |
gc, master_transform, meshWidth, meshHeight, coordinates, | ||
offsets, offsetTrans, facecolors, antialiased, edgecolors) | ||
|
||
def draw_gouraud_triangle(self, gc, points, colors, transform): |
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.
This should probably be draw_gouraud_triangles
:
matplotlib/lib/matplotlib/backend_bases.py
Lines 291 to 292 in d77801e
@_api.deprecated("3.7", alternative="draw_gouraud_triangles") | |
def draw_gouraud_triangle(self, gc, points, colors, transform): |
https://matplotlib.org/stable/api/prev_api_changes/api_changes_3.7.0.html#draw-gouraud-triangle
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.
This seems to be a significant change. Is this correct?
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.
No it's incorrect. Thanks for catching!
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.
This is actually a pretty big problem - the "True" bbox is calculated from the path, but the image bbox includes the full stroke. The the "True" bbox should really be calculated from the path+stroke.
My tendency is to just not do the fine tuning for paths - I expect they are rarely worth rasterizing anyhow.
Fair enough, but this should then be documented. |
PR Summary
WIP. Needs tests. Fixes #6827, and various other analogous bugs that may or may not have issues open.
Right now,
backend_mixed
does something equivalent to (excuse the abuse of notation)to get the final width at which to place rastered output. The
int()
cast is required since rasterizing can only happen into integer numbers of pixels.This PR adds some methods to
MixedModeRenderer
that wrap eachdraw_*
function, allowing us to manually keep around the original desired bbox of each rasterized component, allowing it to be scaled back to fit correctly.This means that for mixed-mode renders,
dpi=
is now a suggestion (the true dpi will typically be off by <1%, but can be off by quite a bit for very low dpi requests). However, most people using vector output care less about the exact dpi and care a lot about things not looking randomly out of alignment.This approach was suggested by @jklymak in the discussion of #16488.
The tests will stop failing once I've reimplented all of
for
MixedModeRenderer
.Comparison with previous behavior
Code
Old output
Bboxes added in post, for emphasis:

New output
Bboxes align exactly

PR Checklist