Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Draft
brunobeltran wants to merge1 commit intomatplotlib:main
base:main
Choose a base branch
Loading
frombrunobeltran:colorbar-rounding-fix

Conversation

brunobeltran
Copy link
Contributor

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)

bbox = int(bbox/72*image_dpi)*72/image_dpi

to get the final width at which to place rastered output. Theint() cast is required since rasterizing can only happen into integer numbers of pixels.

This PR adds some methods toMixedModeRenderer 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 theexact dpi and care alot 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

    * :meth:`draw_path`    * :meth:`draw_image`    * :meth:`draw_gouraud_triangle`    * :meth:`draw_text`    * :meth:`draw_markers`    * :meth:`draw_path_collection`    * :meth:`draw_quad_mesh`

forMixedModeRenderer.

Comparison with previous behavior

Code

import matplotlib as mplimport matplotlib.pyplot as pltimport matplotlib.cmimport numpy as npmpl.rcParams['figure.dpi'] = 20fig, ax = plt.subplots(figsize=(2, 1.5))ax.imshow(np.array([[0, 1], [1, 0]]))ax.axis('off')sm = matplotlib.cm.ScalarMappable()sm.set_array([])plt.colorbar(sm)plt.savefig('test.svg')

Old output

Bboxes added in post, for emphasis:
test7 svg

New output

Bboxes align exactly
ttest

PR Checklist

  • Has Pytest style unit tests
  • Code isFlake 8 compliant
  • New features are documented, with examples if plot related
  • Documentation is sphinx and numpydoc compliant
  • Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)
  • Documented in doc/api/api_changes.rst if API changed in a backward-incompatible way

jrbergen reacted with thumbs up emojilgbouma reacted with rocket emoji
@jklymakjklymak requested a review fromanntzerApril 17, 2020 23:26
@jklymak
Copy link
Member

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 seemsomething will have to bee added becausedraw_image loses track of how exactly how wide the image is meant to be once it figures out how many "pixels" wide it will be. That makes perfect sense for actual raster outputs, but is not applicable here.

@jklymakjklymak marked this pull request as draftApril 17, 2020 23:32
@jklymakjklymak added this to thev3.4.0 milestoneApr 17, 2020
@brunobeltran
Copy link
ContributorAuthor

Thanks for helping out Jody!

So do those methods all really need to be re-implemented? Before you do all that can we check that you are doing this the easiest way?

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 individualArtist.draw calls, the artist always has aget_clipbox that could be read directly to grab the "real" bbox. But the first call that touches the renderer is arenderer.draw_* call, and none of the parameters of these methods have information about the size of the object (so I have to recalculate it in my wrappers.

For example the colorbar is aQuadMesh, so even though we know its absolute size inside ofQuadMesh.draw (via e.g.QuadMesh.get_clip_box), onceQuadMesh.draw callsrenderer.draw_quad_mesh, (which I wrap), I have to recompute theBbox by hand, which is very wasteful.

@@ -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:
Copy link
Contributor

Choose a reason for hiding this comment

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

why?

Copy link
ContributorAuthor

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):
Copy link
Contributor

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 methoddraw_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.)

Copy link
ContributorAuthor

@brunobeltranbrunobeltranApr 18, 2020
edited
Loading

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.

Copy link
ContributorAuthor

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 todraw_image, I think the better solution is to just add optionalwidth andheight arguments instead, and yeah....ahasattr trick to check what signature to use would work....even though it feels icky to write. Likeoption_exact_image_bbox.

renderer.draw_image(gc, l, b, im)
else:
renderer.draw_image(gc, round(l), round(b), data,
bbox=parent.bbox)
Copy link
ContributorAuthor

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 thatdraw_image gets called. Bystop_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.

@anntzer
Copy link
Contributor

Do you really need to pass in the true bbox explicitly? Can the vector backends not infer it by applyingtransform to(x, y, im.width, im.height)? Possibly the caller may need to adjusttransform a bit, though. But then no need for signature changes.

@brunobeltran
Copy link
ContributorAuthor

Do you really need to pass in the true bbox explicitly? Can the vector backends not infer it by applyingtransform to(x, y, im.width, im.height)? Possibly the caller may need to adjusttransform a bit, though. But then no need for signature changes.

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 thetransform kwarg to get that information in there....it's not hard to construct the correct translation+scaling (as in the current PR's version ofbackend_svg.draw_image).

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 todraw_image in the first place.

There are two-ish places wheredraw_image is called (inimage.py:_ImageBase.draw andbackend_mixed.py:MixedModeRenderer.stop_rasterizing). This PR already correctly deals with the latter case, but the former case actually not quite.

What I currently do (use the Bbox of theparent passed to_draw_list_compositing_images) works forFigureImage andAxesImage objects because in those casesparent is theFigure/Axes). However, forBboxImage's, that is not necessarily the correct bounding box.

Solutions I can think of:

  1. Bumpget_window_extent to the superclass_ImageBase, so that we can call it in_ImageBase.draw to get the correct pre-rounding bbox to pass to the vectorized renderers

Happy to hear if people approve and/or have better ideas?

@brunobeltran
Copy link
ContributorAuthor

That said, maybe in terms of scope, we could leaveimage.py as-is in this PR, and only modify thedraw_image call inMixedModeRenderer.stop_rasterizing. Then, any_BaseImage subclass will still be drawn not aligned to its edge, but at least we'll have fixed the bug for every other rasterized object thatMixedModeRenderer deals with (color bars in particular)?

@anntzer
Copy link
Contributor

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?

brunobeltran reacted with thumbs up emoji

@brunobeltranbrunobeltran changed the titleBUGFIX: use true bbox for rasters in backend_mixedFix mixed-mode rasters to align with intended bounding boxes in vectorized outputApr 19, 2020
@brunobeltranbrunobeltran changed the titleFix mixed-mode rasters to align with intended bounding boxes in vectorized outputBUGFIX: use true bbox for rasters in backend_mixedApr 19, 2020
@anntzer
Copy link
Contributor

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 thetransform kwarg (and hence have no external API change) by doing the relevant work in make_image/_make_image?

@brunobeltranOctobox
Copy link
ContributorAuthor

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 :)

@brunobeltranbrunobeltran mentioned this pull requestApr 20, 2020
6 tasks
@brunobeltran
Copy link
ContributorAuthor

brunobeltran commentedApr 20, 2020
edited
Loading

we could leaveimage.py as-is in this PR, and only modify thedraw_image call inMixedModeRenderer.stop_rasterizing

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, intests/test_image.py:rasterize_10dpi_pdf).

The lines in the middle and on the right are supposed to look the same:

rasterize_10dpi_pdf

The issue is that my new code uses the following trick:

  1. keep track of the bbox of each rasterized area by wrapping calls todraw_marker,draw_path,draw_quad_mesh, etc. inMixedModeRenderer.
  2. at the end of each rasterization, scale the rendered pixels to the final "theoretical" bbox we've calculated.

That works great, except for in order to calculate the correctbbox for a stroked path, we need to wait for the code in#17198. Right now, although we merged some fixes to path extents calculations, we still assume a "zero" thickness path when computing its extents.

#17198 is abig 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 otherbaseline_images changes are "more" correct than they used to be.

@jklymak
Copy link
Member

@brunobeltran where is this PR at?

@jklymak
Copy link
Member

ping@brunobeltran This will fix alot of long-standing bugs. If you have abandoned it, please let us know and someone will pick it up...

@jklymak
Copy link
Member

I'll ping again, and marking as stale...

@jklymak
Copy link
Member

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.

@jklymak
Copy link
Member

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...

rgbmrc, Spl1x, and lgbouma reacted with thumbs up emoji

@tacaswell
Copy link
Member

pro: I think I got the rebase to work!
con: there are a bunch of test failures I do not understand locally

@tacaswell
Copy link
Member

ok understand one of the errors:

  • this PR does not wrapdraw_markers yet

@tacaswelltacaswell modified the milestones:v3.7.0,v3.8.0Dec 16, 2022
@tacaswell
Copy link
Member

Unfortunately I think this is going to have to get punted a version again.

narahma2 reacted with confused emoji

Co-authored-by: Jody Klymak <jklymak@gmail.com>Co-authored-by: Thomas A Caswell <tcaswell@gmail.com>
@jklymakjklymakforce-pushed thecolorbar-rounding-fix branch from3a091c7 tobd3ff10CompareApril 23, 2023 15:27
@jklymak
Copy link
Member

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 thetransform object. I think thats true, but seems more confusing than handling this directly.

@jklymakjklymak marked this pull request as ready for reviewApril 23, 2023 16:48
@anntzer
Copy link
Contributor

@anntzer suggest wrapping this in the transform object. I think thats true, but seems more confusing than handling this directly.

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 ofdraw_image(gc, x, y, im, transform, true_size) now really means. Previouslydraw_image(gc, x, y, im, transform) meant "draw the image at position x, y (in pixels); the image pixels (0..m, 0..n) should be transformed by transform first and that also indicates the size of the rendered image". How does that now interact with true_size? Perhaps that's simple enough, but should be documented.


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.

@jklymak
Copy link
Member

Still it's not entirely clear to me what the semantics ofdraw_image(gc, x, y, im, transform, true_size) now really means.

I think that is unchanged. The new codepaths are only iftransform is None.

gc, master_transform, meshWidth, meshHeight, coordinates,
offsets, offsetTrans, facecolors, antialiased, edgecolors)

def draw_gouraud_triangle(self, gc, points, colors, transform):
Copy link
Member

Choose a reason for hiding this comment

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

This should probably bedraw_gouraud_triangles:

@_api.deprecated("3.7",alternative="draw_gouraud_triangles")
defdraw_gouraud_triangle(self,gc,points,colors,transform):

https://matplotlib.org/stable/api/prev_api_changes/api_changes_3.7.0.html#draw-gouraud-triangle

Copy link
Member

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?

Copy link
Member

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!

Copy link
Member

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.

@anntzer
Copy link
Contributor

I think that is unchanged. The new codepaths are only if transform is None.

Fair enough, but this should then be documented.

@jklymakjklymak marked this pull request as draftApril 24, 2023 15:12
@ksundenksunden modified the milestones:v3.8.0,future releasesAug 8, 2023
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@anntzeranntzeranntzer left review comments

@jklymakjklymakjklymak left review comments

@oscargusoscargusoscargus left review comments

At least 1 approving review is required to merge this pull request.

Assignees
No one assigned
Projects
None yet
Milestone
future releases
Development

Successfully merging this pull request may close these issues.

Bug with colorbar and low DPI output in PDF
7 participants
@brunobeltran@jklymak@anntzer@QuLogic@tacaswell@oscargus@ksunden

[8]ページ先頭

©2009-2025 Movatter.jp