Skip to content

Rasterization creates multiple bitmap elements and large file sizes #17149

Description

@samtygier

Bug summary

I often work with plots have have a large number of lines, for example showing the trajectories of a large number of particles in a physics simulation. For publication it is good for the axis to be in vector format, but keeping all the tracks as vector can create large file sizes.

Rasterization of some elements in the plot can solve this. Matplotlib offers rasterized kwarg to plot(), or Artist.set_rasterized(). But for this use case it results in a separate bitmap element for each line, making the file size even larger.

I'm not the first person to have issues related to this #13718

Code for reproduction

For example, this creates a 2.6MB file:

N = 20
rast = True

s = np.arange(100)
tracks = []

for a in np.logspace(0.001, 0.5, N):
	for t in np.linspace(0.1, pi, N):
		t = a*np.sin(s*0.05 + t)
		tracks.append(t)

for t in tracks:
	plt.plot(s, t, "-k", alpha=0.5, lw=0.5, rasterized=rast)
plt.savefig("tracks1.svg")

grep "<image" tracks1.svg | wc -l shows that there are 400 image elements.

Calling plot just once with all the tracks makes no difference.

tracks_na = np.array(tracks).T
plt.plot(s, tracks_na, "-k", alpha=0.5, lw=0.5, rasterized=rast)

RFC

This happens due to the way start_rasterizing() and stop_rasterizing() get called the from allow_rasterization() wrapper. If rasterized is set on the Line2D objects, then start and stop are called around each Line2D.draw(). stop_rasterizing() renders out to bitmap and calls _renderer.draw_image(). The only way to stay in rasterizing mode is set_rasterized() on the parent artist of the lines, which would be the axes, but that means that the axes labels and everything else get rasterized.

I have thought of a few solutions, but I would like some feedback before starting.

I think an ideal solution would be automatic for the user. matplotlib would just merge the bitmaps in an optimal way. This would need not effect draw order, so would need to be smart about zorder. It might avoid merging 2 non-overlapping bitmaps if that increases total area of bitmap.

A possible implementation would be changes to the allow_rasterization wrapper so that the rasterization does not stop between consecutive rasterized artists.

A more manual approach is to create an object that can contain multiple artists, and therefor keep the render in rasterization mode for drawing those artists. Collections don't fit this role as they have some limitations, e.g. LineCollection does not have markers. I also looked at Container but that seems designed for specific use cases. So this could be ArtistGroup or ArtistCollection. It would derive from Artist, and its draw() method would iterate through its children and draw them.

I have a minimal prototype, that lets me do:

ag = ArtistGroup()
for t in tracks:
	ag.add_child(matplotlib.lines.Line2D(s, t, color="k", linestyle="-", alpha=0.5, lw=0.5))
ag.set_rasterized(True)
plt.gca().add_artist(ag)
plt.savefig("tracks3.svg")

This results in a 304 kB file with a single <image> element (compared with 2.6 MB with original code). Increasing N makes the file size difference even larger.

If this is a good approach it could maybe be used inside the plot() and similar commands, so that lines drawn with a single plot would end up in a single rasterization (when requested) with no further user input.

Matplotlib version

  • Operating system: Linux
  • Matplotlib version: 3.0.3 and git master
  • Matplotlib backend: svg
  • Python version: 3.7.6

Note, I have been testing with the SVG backend, because its easier to see what is going on the output files. Judging from file size and loading times all this is true in the PDF backend too.

Activity

  1. changed the title [-]Rasterization creates multiple bitmaps and large file sizes[/-] [+]Rasterization creates multiple bitmap elements and large file sizes[/+] on Apr 15, 2020
  2. jklymak commented on Apr 15, 2020

    @jklymak
    Member

    This seems reasonable - you could, during draw, split the artists to draw into two groups and do the vector and rasterized draws all at once. Then you would know how large your bitmap is. The problem though is with zorder/draw order if you have some vector objects mixed in the draw order with the rasterized objects.

    Another problem, I bet, is going to be animation and blitting.

  3. samtygier commented on Apr 15, 2020

    @samtygier
    ContributorAuthor

    So the first option of making changes in the draw() routines so rasterization can be combined where appropriate with out need any extra work from the user.

    Yes, it needs to not modify the draw ordering in ways that change the output. For example

    t = np.arange(0,100) * (2.3)
    x = np.cos(t)
    y = np.sin(t)
    
    def do_plot_1(ax):
    	ax.plot(x,y, "-", c="r", rasterized=True)
    	ax.plot(x+1,y, "-", c="b", rasterized=False)
    	ax.plot(x+2,y, "-", c="g", rasterized=True)
    	ax.plot(x+3,y, "-", c="m", rasterized=True)
    
    def do_plot_2(ax):
    	ax.plot(x,y, "-", c="r", rasterized=True, zorder=1)
    	ax.plot(x+2,y, "-", c="g", rasterized=True, zorder=3)
    	ax.plot(x+3,y, "-", c="m", rasterized=True, zorder=4)
    	ax.plot(x+1,y, "-", c="b", rasterized=False, zorder=2)
    

    Both need to give the same output, so it would only be possible to combine green and magenta elements.
    overlap1

  4. samtygier commented on Apr 15, 2020

    @samtygier
    ContributorAuthor

    Also, I have been looking at making some image comparison tests to help me through this. I saw #16447 , does that mean I should refrain from adding any comparison tests, or just keep them in a separate commit?

    It is possible to point multiple tests at the same baseline image? For example the 2 functions in my previous comment should give identical output. I can put the same baseline image name into 2 tests which sort of works, but it is also the name used for the result image and the diff, so those get over written.

  5. jklymak commented on Apr 15, 2020

    @jklymak
    Member

    You can use the figures_equal decorator, but a few image tests are OK if needed.

    Your approach seems fine and this would be a worthwhile change I think, but again, you may need to think about blitting for animations?

  6. added this to the v3.4.0 milestone on Apr 16, 2020
  7. added a commit that references this issue on Apr 16, 2020
    56c6d5d
  8. samtygier commented on Apr 16, 2020

    @samtygier
    ContributorAuthor

    Here is a first pass at this. It only really touches start_rasterizing() and stop_rasterizing() and adds flush_rasterizing(). It passes the existing test suite, and adds a couple of tests. I have not looked at animations yet.

    The tracks example is now down to 243 KB.

    check_figures_equal() decorator was just what needed. So no new baseline images.

    I'd still need to do the doc side of this.

  9. added a commit that references this issue on Apr 16, 2020
    3ae9d44
  10. samtygier commented on Apr 16, 2020

    @samtygier
    ContributorAuthor

    All the examples in the animation directory work, apart from double_pendulum_sgskip.py, which also fails on master with _tkinter.TclError: bad argument error. Is there anything else you can suggest I test?

  11. jklymak commented on Apr 17, 2020

    @jklymak
    Member

    #17160 wasn't really fixing this issue, I don't think

  12. QuLogic commented on Apr 17, 2020

    @QuLogic
    Member

    Right, it was only fixing the bug in the last comment.

  13. added 3 commits that reference this issue on Apr 21, 2020
    2f67719
    92c9242
    2ab32eb
  14. added 2 commits that reference this issue on May 3, 2020
    add7333
    b6a2739
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions