Skip to content

Error at save animation with pillow #15678

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

Closed
Zachary4biz opened this issue Nov 12, 2019 · 14 comments · Fixed by #15691
Closed

Error at save animation with pillow #15678

Zachary4biz opened this issue Nov 12, 2019 · 14 comments · Fixed by #15691

Comments

@Zachary4biz
Copy link

Bug report

Bug summary

line 575 of animation.py use Image.frombytes to load a BytesIO, this will lead to an exception -- not enough image data.
seems we can't feed bytes of .png to frombytes from the first question-comment of this SO question

Code for reproduction

import numpy as np
from matplotlib import animation
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

emb = np.random.random_sample((1000,3))

fig = plt.figure("bug_report")
ax = Axes3D(fig)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
ax.set_xlim3d(left=-1,right=1)
ax.set_ylim3d(bottom=-1,top=1)
ax.set_zlim3d(bottom=-1,top=1)

batch=50
def animate(i):
    emb_part = emb[i*batch:(i+1)*batch]
    ax.scatter(xs=emb_part[:, 0], ys=emb_part[:, 1], zs=emb_part[:, 2])
    return ax

ani = animation.FuncAnimation(fig=fig, func=animate, frames=len(emb)//batch + 1, interval=0.01*1000, blit=False, repeat=False)

ani.save("/Users/zac/Downloads/ani.gif", fps=30, writer='pillow')

Actual outcome

# ....
ValueError: not enough image data

During handling of the above exception, another exception occurred:
# ....

actually, if we use plt.show() before ani.save, this exception wont appear, but we will get only some last parts of the animation, example as this

Expected outcome

normally saved gif pic
example as this

Matplotlib version

  • Operating system: MacOSX
  • Matplotlib version: 3.0.3
  • Matplotlib backend (print(matplotlib.get_backend())): Qt5Agg
  • Python version: 3.7.3
  • Jupyter version (if applicable):
  • Other libraries: numpy
@anntzer
Copy link
Contributor

anntzer commented Nov 12, 2019

I cannot repro with either 3.0.3 or master, and in any case the frombytes call was removed in #14242.

@dopplershift
Copy link
Contributor

dopplershift commented Nov 12, 2019

It works if I don't specify the backend, but it fails for me if I do: MPLBACKEND="Qt5Agg" python test_anim.py:

Attribute Qt::AA_EnableHighDpiScaling must be set before QCoreApplication is created.
Traceback (most recent call last):
  File "/Users/rmay/miniconda3/envs/py37/lib/python3.7/site-packages/matplotlib/animation.py", line 230, in saving
    yield self
  File "/Users/rmay/miniconda3/envs/py37/lib/python3.7/site-packages/matplotlib/animation.py", line 1156, in save
    writer.grab_frame(**savefig_kwargs)
  File "/Users/rmay/miniconda3/envs/py37/lib/python3.7/site-packages/matplotlib/animation.py", line 572, in grab_frame
    buf.getvalue()))
  File "/Users/rmay/miniconda3/envs/py37/lib/python3.7/site-packages/PIL/Image.py", line 2542, in frombytes
    im.frombytes(data, decoder_name, args)
  File "/Users/rmay/miniconda3/envs/py37/lib/python3.7/site-packages/PIL/Image.py", line 829, in frombytes
    raise ValueError("not enough image data")
ValueError: not enough image data

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "test_anim.py", line 25, in <module>
    ani.save("/Users/rmay/Downloads/ani.gif", fps=30, writer='pillow')
  File "/Users/rmay/miniconda3/envs/py37/lib/python3.7/site-packages/matplotlib/animation.py", line 1156, in save
    writer.grab_frame(**savefig_kwargs)
  File "/Users/rmay/miniconda3/envs/py37/lib/python3.7/contextlib.py", line 130, in __exit__
    self.gen.throw(type, value, traceback)
  File "/Users/rmay/miniconda3/envs/py37/lib/python3.7/site-packages/matplotlib/animation.py", line 232, in saving
    self.finish()
  File "/Users/rmay/miniconda3/envs/py37/lib/python3.7/site-packages/matplotlib/animation.py", line 575, in finish
    self._frames[0].save(
IndexError: list index out of range
Exception ignored in: <function TimerBase.__del__ at 0x11c218950>
Traceback (most recent call last):
  File "/Users/rmay/miniconda3/envs/py37/lib/python3.7/site-packages/matplotlib/backend_bases.py", line 1100, in __del__
  File "/Users/rmay/miniconda3/envs/py37/lib/python3.7/site-packages/matplotlib/backends/backend_qt5.py", line 210, in _timer_stop
RuntimeError: Internal C++ object (PySide2.QtCore.QTimer) already deleted.

I should note that this is using conda-forge PySide2 5.13.1, matplotlib 3.1.0, and Python 3.7.3 on macOS 10.15.

@dopplershift
Copy link
Contributor

On master with python 3.7.3 I get a slightly different error:

Traceback (most recent call last):
  File "/Users/rmay/repos/matplotlib/lib/matplotlib/animation.py", line 230, in saving
    yield self
  File "/Users/rmay/repos/matplotlib/lib/matplotlib/animation.py", line 1148, in save
    writer.grab_frame(**savefig_kwargs)
  File "/Users/rmay/repos/matplotlib/lib/matplotlib/animation.py", line 567, in grab_frame
    "raw", "RGBA", 0, 1))
  File "/Users/rmay/miniconda3/envs/test-mpl/lib/python3.7/site-packages/PIL/Image.py", line 2605, in frombuffer
    im = im._new(core.map_buffer(data, size, decoder_name, None, 0, args))
ValueError: buffer is not large enough

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "test_anim.py", line 25, in <module>
    ani.save("/Users/rmay/Downloads/ani.gif", fps=30, writer='pillow')
  File "/Users/rmay/repos/matplotlib/lib/matplotlib/animation.py", line 1148, in save
    writer.grab_frame(**savefig_kwargs)
  File "/Users/rmay/miniconda3/envs/test-mpl/lib/python3.7/contextlib.py", line 130, in __exit__
    self.gen.throw(type, value, traceback)
  File "/Users/rmay/repos/matplotlib/lib/matplotlib/animation.py", line 232, in saving
    self.finish()
  File "/Users/rmay/repos/matplotlib/lib/matplotlib/animation.py", line 570, in finish
    self._frames[0].save(
IndexError: list index out of range
Exception ignored in: <function TimerBase.__del__ at 0x113a83d08>
Traceback (most recent call last):
  File "/Users/rmay/repos/matplotlib/lib/matplotlib/backend_bases.py", line 1094, in __del__
  File "/Users/rmay/repos/matplotlib/lib/matplotlib/backends/backend_qt5.py", line 213, in _timer_stop
RuntimeError: Internal C++ object (PySide2.QtCore.QTimer) already deleted.

@anntzer
Copy link
Contributor

anntzer commented Nov 12, 2019

Still can't repro (pyside2+qt5agg+master), except the "Internal C++ object already deleted" error which I do repro but that doesn't affect the correct creation of the gif.

@dopplershift
Copy link
Contributor

Oh fun, probably mac-specific.

@dopplershift
Copy link
Contributor

I think it's a DPI scaling issue. If I print out the renderer height, width, and length of the image buffer (BytesIO), I get:

Without Qt5Agg:

640.0 480.0 1228800

With Qt5Agg:

1280.0 960.0 1228800

@Zachary4biz
Copy link
Author

I see, maybe this is just some occasional error.
in matplotlib 3.1.3, it works fine for my case.
thx!

@Zachary4biz
Copy link
Author

btw, maybe you guys have some idea about generating this kind of pic?

I was tackling some image-cluster-visualization stuff, and got stuck at controlling the position of plt.imshow

@dopplershift
Copy link
Contributor

dopplershift commented Nov 13, 2019

@anntzer Can you reproduce with this, even without specifying backend?

import numpy as np
from matplotlib import animation
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

emb = np.random.random_sample((1000,3))

fig = plt.figure("bug_report")
ax = Axes3D(fig)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
ax.set_xlim3d(left=-1,right=1)
ax.set_ylim3d(bottom=-1,top=1)
ax.set_zlim3d(bottom=-1,top=1)

batch=50
def animate(i):
    emb_part = emb[i*batch:(i+1)*batch]
    ax.scatter(xs=emb_part[:, 0], ys=emb_part[:, 1], zs=emb_part[:, 2])
    return ax

ani = animation.FuncAnimation(fig=fig, func=animate, frames=len(emb)//batch + 1, interval=0.01*1000, blit=False, repeat=False)

ani.save("ani.gif", fps=30, writer='pillow', savefig_kwargs={'dpi':50})

I think there's a fundamental problem in the Pillow writer (and who knows where else) that assumes the number of pixels written in savefig() matches that found by querying renderer afterwards. Empirically, that's not the case--but I'm not sure what the right way to get that information is.

@dopplershift
Copy link
Contributor

def grab_frame(self, **savefig_kwargs):
from PIL import Image
buf = BytesIO()
self._fig.savefig(buf, **dict(savefig_kwargs, format="rgba"))
renderer = self._fig.canvas.get_renderer()
self._frames.append(Image.frombuffer(
"RGBA",
(int(renderer.width), int(renderer.height)), buf.getbuffer(),
"raw", "RGBA", 0, 1))

@anntzer
Copy link
Contributor

anntzer commented Nov 13, 2019

OK, I can repro by faking high-dpi with QT_SCALE_FACTOR; can you check whether #15691 fixes this?

@QuLogic QuLogic added this to the v3.3.0 milestone Nov 15, 2019
@Svito-zar
Copy link

This issue is still there for me on Mac using Matplotlib v 3.3.4

@jklymak
Copy link
Member

jklymak commented Dec 7, 2022

Please open a new issue

@Svito-zar
Copy link

I realized it was a bug in my code :)

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

Successfully merging a pull request may close this issue.

6 participants