Source code for cutcutcodec.core.generation.video.matplotlib
"""Generate a video wih a matplotlib figure."""
from fractions import Fraction
import math
import threading
import typing
import matplotlib.figure
import matplotlib.pyplot
import torch
from cutcutcodec.core.classes.colorspace import Colorspace
from cutcutcodec.core.classes.container import ContainerInput
from cutcutcodec.core.classes.stream import Stream
from cutcutcodec.core.classes.stream_video import StreamVideo
from cutcutcodec.core.filter.video.resize import resize_keep_ratio
[docs]
class GeneratorVideoMatplotlib(ContainerInput):
"""Generate a video from matplotlib figure.
Examples
--------
>>> from fractions import Fraction
>>> import matplotlib.pyplot as plt
>>> from cutcutcodec.core.generation.video.matplotlib import GeneratorVideoMatplotlib
>>>
>>> def func(timestamp: Fraction) -> plt.Figure:
... fig = plt.figure(layout="constrained", figsize=(7, 4)) # w, h
... fig.supxlabel("abscissa")
... fig.supylabel("ordinate")
... axe = fig.subplots(squeeze=True)
... axe.set_xlim([0, 1])
... axe.set_ylim([0, 1])
... axe.plot([0, 1], [timestamp, timestamp]) # horizontal line
... return fig
...
>>> (stream,) = GeneratorVideoMatplotlib(func).out_streams
>>> stream.snapshot(0, (9, 9))
>>>
"""
def __init__(self, func: typing.Callable[[Fraction], matplotlib.figure.Figure]):
"""Initialise and create the class.
Parameters
----------
func : callable
A user-defined function that takes in input the frame time as a rational number,
and returns the filled matplotlib figure as the image of the current frame.
"""
assert callable(func), func.__class__.__name__
self.func = func
super().__init__([_StreamVideoMatplotlib(self)])
def _getstate(self) -> dict:
return {"func": str(self.func)}
def _setstate(self, in_streams: typing.Iterable[Stream], state: dict) -> None:
assert state.keys() == {"func"}, set(state)
self.func = state["func"]
ContainerInput.__init__(self, [_StreamVideoNoiseUniform(self)])
class _StreamVideoMatplotlib(StreamVideo):
"""Matplotlib figure bases video stream."""
colorspace = Colorspace.from_default_target_rgb() # not working
def __init__(self, node: GeneratorVideoMatplotlib):
assert isinstance(node, GeneratorVideoMatplotlib), node.__class__.__name__
super().__init__(node)
def _snapshot(self, timestamp: Fraction, mask: torch.Tensor) -> torch.Tensor:
if timestamp < 0:
raise OutOfTimeRange(f"there is no video frame at timestamp {timestamp} (need >= 0)")
if threading.current_thread().name != "MainThread":
matplotlib.pyplot.switch_backend("agg") # to allow matplotlib in thread
fig = self.node.func(timestamp)
# find the best dpi value to export the figure with the best resolution
width_inches, height_inches = fig.get_size_inches()
dpi = min(mask.shape[0]/height_inches, mask.shape[1]/width_inches)
fig.set_dpi(dpi)
# convert fig into frame
fig.canvas.draw()
width, height = fig.canvas.get_width_height()
frame = (
torch.frombuffer(bytearray(fig.canvas.tostring_argb()), dtype=torch.uint8)
.reshape(height, width, 4)
[:, :, 1:] # argb to rgb
.to(torch.float32)
) / 255.0
matplotlib.pyplot.close(fig) # fig.clear()
return resize_keep_ratio(frame, mask.shape, copy=False) # to guaranty having the write shape
@property
def beginning(self) -> Fraction:
return Fraction(0)
@property
def duration(self) -> Fraction | float:
return math.inf