Skip to content
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

Handle floating point round-off error when converting to pixels for h264 animations #8253

Merged
merged 7 commits into from
Apr 3, 2017

Conversation

ngoldbaum
Copy link
Contributor

@ngoldbaum ngoldbaum commented Mar 10, 2017

Rather than handling the rescaling in matplotlib, tell ffmpeg to do the rescaling for us. This avoids issues described in #8250 where the rescaling is susceptible to round-off error. It also allows us to delete some code.

EDIT:

Now let's try rounding the output of adjusted_figsize. In addition I found that to get this to work properly with yt (and I guess in general any other library that calls set_size_inches after matplotlib does) I needed another call to _adjust_frame_size before actually writing each frame. This avoids the crazy corrupted video I saw in the original issue I opened about this.

@ngoldbaum
Copy link
Contributor Author

@efiring I believe you originally added the code I'm deleting. Would you mind taking a look?

# The h264 codec requires that frames passed to it must have a
# a width and height that are even numbers of pixels. This tells
# ffmpeg to rescale the frames to the nearest even number of pixels
# see http://stackoverflow.com/a/20848224/1382869
Copy link
Member

Choose a reason for hiding this comment

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

https:

@ngoldbaum ngoldbaum force-pushed the simplify-figsize-adjust branch 2 times, most recently from 6b9dd12 to e0c212b Compare March 10, 2017 01:09
@efiring
Copy link
Member

efiring commented Mar 10, 2017

I don't think this is a good approach. If I understand correctly, what happens with this approach is that each image is generated by mpl (using Agg) with, for example, 501x301 pixels, and then ffmpeg does a massive interpolation to a 500x300 grid. This undoes whatever pixel-snapping and other operations mpl/agg did that involved knowing what the pixel grid would be. I think it fundamentally degrades the image, and I don't see any benefit to it. In the approach I used, the figure size is tweaked so that the desired pixel grid is produced from the start, and never modified. If there is a problem with the floating-point math, I suspect that can be corrected in some more direct way, but I haven't looked at your example in #8250.

wo, ho = self.fig.get_size_inches()
w, h = adjusted_figsize(wo, ho, self.dpi, 2)
if not (wo, ho) == (w, h):
self.fig.set_size_inches(w, h, forward=True)
Copy link
Member

Choose a reason for hiding this comment

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

The fix might be to set forward=False here. Some of the backends will feed-back and mostly go to the size you want (which is another set of things we need to fix, but).

@tacaswell
Copy link
Member

I agree with @efiring , we should handle this on our side by outputting the right sized-frame.

@tacaswell tacaswell added this to the 2.0.1 (next bug fix release) milestone Mar 10, 2017
@ngoldbaum
Copy link
Contributor Author

@efiring @tacaswell ok, I've rewritten this pull request in what is hopefully a more copacetic way. Let me know if you have additional comments.

@@ -62,8 +63,8 @@


def adjusted_figsize(w, h, dpi, n):
wnew = int(w * dpi / n) * n / dpi
hnew = int(h * dpi / n) * n / dpi
wnew = np.round(int(w * dpi / n) * n / dpi)
Copy link
Member

Choose a reason for hiding this comment

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

This rounds to the nearest inch which is not what we want.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ugh, good point!

Unfortunately putting the rounding in frame_size produces a corrupted movie:

http://imgur.com/a/CjftT

@tacaswell
Copy link
Member

If we are having trouble getting this to round trip nicely (and this is a perineal request, the ability to specify the pixel size, not the inch size of the png), then we should fall back to just trimming a single row / column off the image before handing it to ffmpeg.

@ngoldbaum
Copy link
Contributor Author

fall back to just trimming a single row / column off the image before handing it to ffmpeg

Is there a straightforward way to do this as an argument to e.g. savefig? I think I could do it using pillow but not easily using pure matplotlib.

@ngoldbaum
Copy link
Contributor Author

OK, third (fourth?) time's the charm? I think this one is correct. Now we use np.nextafter to correct the results of adjusted_figsize if there's any floating point jitter.

In other news, I now am very annoyed with floating point!

@njsmith
Copy link

njsmith commented Mar 11, 2017

It seems like the underlying problem here is that the agg backend (or whatever) converts figsize to pixels by doing int(width * dpi), and the floating point error + int() truncation introduce some nasty jitter. Rather than trying to model this jitter and correct for it like the PR does right now, maybe it would make more sense to fix the backend code so it computes pixels as round(width * dpi)? This should produce more accurate and predictable results in general, as well as avoiding the super-tricky and fragile code here...

@ngoldbaum ngoldbaum changed the title Let ffmpeg handle image rescaling for h264 Handle floating point round-off error when converting to pixels for h264 animations Mar 12, 2017
@@ -337,6 +347,7 @@ def grab_frame(self, **savefig_kwargs):
verbose.report('MovieWriter.grab_frame: Grabbing frame.',
level='debug')
try:
self._adjust_frame_size()
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What if the animation callback function calls set_size_inches? This happens indirectly with any animation using a yt plot when the figure gets updated. yt assumes it can control the figure size, which is a reasonable assumption everywhere else in matplotlib, except for here when it's trying to work around this h264 issue.

Copy link
Member

Choose a reason for hiding this comment

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

This only partially prevents the problem if the callbacks always set the same size. Probably should cache the size (and dpi) in setup and then re-set them here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thanks for the suggestion, makes the intent clearer


# Combine FFMpeg options with pipe-based writing
@writers.register('ffmpeg')
class FFMpegWriter(MovieWriter, FFMpegBase):
Copy link
Member

Choose a reason for hiding this comment

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

why change the mro?

Copy link
Member

Choose a reason for hiding this comment

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

nevemind, I see why ❤️ MI.

stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
creationflags=subprocess_creation_flags)
if not cls._handle_subprocess(p):
Copy link
Member

Choose a reason for hiding this comment

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

could this just bereturn cls._handle_subprocess(p)?

@ngoldbaum
Copy link
Contributor Author

OK, I think the test I added is finally passing now.

I had to add some extra code to fix an issue that the travis builds on Ubuntu 12.04 ran into. On that version of Ubuntu they shipped an ffmpeg compatibility binary that wraps avconv. This ffmpeg is not fully compatible with the real ffmpeg (for the purposes of this pull request, it doesn't work with the h264 codec), which caused the test I added to fail. The fix for that is to update the logic that searches for ffmpeg to ignore an ffmpeg that is really a wrapper for avconv.

In principle in the future this code can be removed since Ubuntu brought back the real ffmpeg in 16.04 and Ubuntu 12.04 is going EOL very soon.

@QuLogic
Copy link
Member

QuLogic commented Mar 13, 2017

Hmm, it may soon be time to switch Travis to the trusty builds. If master is not going to come out for another month at least, then maybe that time is now.

Copy link
Contributor

@dopplershift dopplershift left a comment

Choose a reason for hiding this comment

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

Generally 👍 on the approach. (There almost seems as much effort here to fix tests on 12.04 as there is fixing the actual bug.)

if int(x*dpi) % n != 0:
if int(np.nextafter(x, np.inf)*dpi) % n == 0:
x = np.nextafter(x, np.inf)
if int(np.nextafter(x, -np.inf)*dpi) % n == 0:
Copy link
Contributor

Choose a reason for hiding this comment

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

Not elif?

@anntzer
Copy link
Contributor

anntzer commented Mar 15, 2017

+1 for not supporting more than the oldest, still Canonical-supported Ubuntu LTS (and that's only a lower bound :-)).

@ngoldbaum
Copy link
Contributor Author

@dopplershift yeah, moving travis to use a 14.04 image seems like the way to go come April :)

@tacaswell tacaswell added the Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. label Mar 21, 2017
except OSError:
return False

@classmethod
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason for using @classmethod rather than @staticmethod here, even though the method doesn't use its cls argument?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No particular reason I think I was under the false impression that can't override a staticmethod in a subclass. In any case I think it makes sense to leave it as is just in case anyone needs access to the data in cls in the future.

@efiring
Copy link
Member

efiring commented Mar 26, 2017

As a reminder, we still have the open question as to whether consistently rounding rather than truncating (see #8265) would be a good approach instead of, or in addition to, the floating point adjustment done here.

Copy link
Member

@tacaswell tacaswell left a comment

Choose a reason for hiding this comment

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

Contingent on tests passing.

I added a commit rather than asking @ngoldbaum to make some minor changes.

@ngoldbaum
Copy link
Contributor Author

Thanks Tom!

@efiring efiring merged commit 44fcb87 into matplotlib:master Apr 3, 2017
tacaswell pushed a commit that referenced this pull request May 3, 2017
Handle floating point round-off error when converting to pixels for h264 animations
Conflicts:
	lib/matplotlib/tests/test_animation.py
	  - conflicts in tests
@tacaswell
Copy link
Member

backported to v2.0.x as cab1abd

Sorry this missed 2.0.1 :(

@QuLogic QuLogic modified the milestones: 2.0.2 (next bug fix release), 2.0.1 (next bug fix release) May 3, 2017
@tacaswell tacaswell modified the milestones: 2.0.3 (next bug fix release), 2.0.2 (critical bug fixes from 2.0.1) May 7, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. topic: animation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants