Skip to content

Commit d4af1a9

Browse files
authored
reorganized Figure export stuff (#710)
1 parent 497f574 commit d4af1a9

File tree

1 file changed

+37
-166
lines changed

1 file changed

+37
-166
lines changed

fastplotlib/layouts/_figure.py

Lines changed: 37 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,37 @@ def clear(self):
568568
for subplot in self:
569569
subplot.clear()
570570

571+
def export_numpy(self, rgb: bool = False) -> np.ndarray:
572+
"""
573+
Export a snapshot of the Figure as numpy array.
574+
575+
Parameters
576+
----------
577+
rgb: bool, default ``False``
578+
if True, use alpha blending to return an RGB image.
579+
if False, returns an RGBA array
580+
581+
Returns
582+
-------
583+
np.ndarray
584+
[n_rows, n_cols, 3] for RGB or [n_rows, n_cols, 4] for RGBA
585+
"""
586+
snapshot = self.renderer.snapshot()
587+
588+
if rgb:
589+
bg = np.zeros(snapshot.shape).astype(np.uint8)
590+
bg[:, :, -1] = 255
591+
592+
img_alpha = snapshot[..., -1] / 255
593+
594+
rgb = snapshot[..., :-1] * img_alpha[..., None] + bg[..., :-1] * np.ones(
595+
img_alpha.shape
596+
)[..., None] * (1 - img_alpha[..., None])
597+
598+
return rgb.astype(np.uint8)
599+
600+
return snapshot
601+
571602
def export(self, uri: str | Path | bytes, **kwargs):
572603
"""
573604
Use ``imageio`` for writing the current Figure to a file, or return a byte string.
@@ -593,24 +624,18 @@ def export(self, uri: str | Path | bytes, **kwargs):
593624
"conda install -c conda-forge imageio\n"
594625
)
595626
else:
596-
snapshot = self.renderer.snapshot()
597-
remove_alpha = True
598-
599627
# image formats that support alpha channel:
600628
# https://en.wikipedia.org/wiki/Alpha_compositing#Image_formats_supporting_alpha_channels
601629
alpha_support = [".png", ".exr", ".tiff", ".tif", ".gif", ".jxl", ".svg"]
602630

603-
if isinstance(uri, str):
604-
if any([uri.endswith(ext) for ext in alpha_support]):
605-
remove_alpha = False
631+
uri = Path(uri)
606632

607-
elif isinstance(uri, Path):
608-
if uri.suffix in alpha_support:
609-
remove_alpha = False
633+
if uri.suffix in alpha_support:
634+
rgb = False
635+
else:
636+
rgb = True
610637

611-
if remove_alpha:
612-
# remove alpha channel if it's not supported
613-
snapshot = snapshot[..., :-1].shape
638+
snapshot = self.export_numpy(rgb=rgb)
614639

615640
return iio.imwrite(uri, snapshot, **kwargs)
616641

@@ -660,157 +685,3 @@ def __repr__(self):
660685
f"\t{newline.join(subplot.__str__() for subplot in self)}"
661686
f"\n"
662687
)
663-
664-
665-
class FigureRecorder:
666-
def __init__(self, figure: Figure):
667-
self._figure = figure
668-
self._video_writer: VideoWriterAV = None
669-
self._video_writer_queue = Queue()
670-
self._record_fps = 25
671-
self._record_timer = 0
672-
self._record_start_time = 0
673-
674-
def _record(self):
675-
"""
676-
Sends frame to VideoWriter through video writer queue
677-
"""
678-
# current time
679-
t = time()
680-
681-
# put frame in queue only if enough time as passed according to the desired framerate
682-
# otherwise it tries to record EVERY frame on every rendering cycle, which just blocks the rendering
683-
if t - self._record_timer < (1 / self._record_fps):
684-
return
685-
686-
# reset timer
687-
self._record_timer = t
688-
689-
if self._video_writer is not None:
690-
ss = self._figure.canvas.snapshot()
691-
# exclude alpha channel
692-
self._video_writer_queue.put(ss.data[..., :-1])
693-
694-
def start(
695-
self,
696-
path: str | Path,
697-
fps: int = 25,
698-
codec: str = "mpeg4",
699-
pixel_format: str = "yuv420p",
700-
options: dict = None,
701-
):
702-
"""
703-
Start a recording, experimental. Call ``record_end()`` to end a recording.
704-
Note: playback duration does not exactly match recording duration.
705-
706-
Requires PyAV: https://github.com/PyAV-Org/PyAV
707-
708-
**Do not resize canvas during a recording, the width and height must remain constant!**
709-
710-
Parameters
711-
----------
712-
path: str or Path
713-
path to save the recording
714-
715-
fps: int, default ``25``
716-
framerate, do not use > 25 within jupyter
717-
718-
codec: str, default "mpeg4"
719-
codec to use, see ``ffmpeg`` list: https://www.ffmpeg.org/ffmpeg-codecs.html .
720-
In general, ``"mpeg4"`` should work on most systems. ``"libx264"`` is a
721-
better option if you have it installed.
722-
723-
pixel_format: str, default "yuv420p"
724-
pixel format
725-
726-
options: dict, optional
727-
Codec options. For example, if using ``"mpeg4"`` you can use ``{"q:v": "20"}`` to set the quality between
728-
1-31, where "1" is highest and "31" is lowest. If using ``"libx264"``` you can use ``{"crf": "30"}`` where
729-
the "crf" value is between "0" (highest quality) and "50" (lowest quality). See ``ffmpeg`` docs for more
730-
info on codec options
731-
732-
Examples
733-
--------
734-
735-
With ``"mpeg4"``
736-
737-
.. code-block:: python
738-
739-
# start recording video
740-
figure.recorder.start("./video.mp4", options={"q:v": "20"}
741-
742-
# do stuff like interacting with the plot, change things, etc.
743-
744-
# end recording
745-
figure.recorder.stop()
746-
747-
With ``"libx264"``
748-
749-
.. code-block:: python
750-
751-
# start recording video
752-
figure.recorder.start("./vid_x264.mp4", codec="libx264", options={"crf": "25"})
753-
754-
# do stuff like interacting with the plot, change things, etc.
755-
756-
# end recording
757-
figure.recorder.stop()
758-
759-
"""
760-
761-
if Path(path).exists():
762-
raise FileExistsError(f"File already exists at given path: {path}")
763-
764-
# queue for sending frames to VideoWriterAV process
765-
self._video_writer_queue = Queue()
766-
767-
# snapshot to get canvas width height
768-
ss = self._figure.canvas.snapshot()
769-
770-
# writer process
771-
self._video_writer = VideoWriterAV(
772-
path=str(path),
773-
queue=self._video_writer_queue,
774-
fps=int(fps),
775-
width=ss.width,
776-
height=ss.height,
777-
codec=codec,
778-
pixel_format=pixel_format,
779-
options=options,
780-
)
781-
782-
# start writer process
783-
self._video_writer.start()
784-
785-
# 1.3 seems to work well to reduce that difference between playback time and recording time
786-
# will properly investigate later
787-
self._record_fps = fps * 1.3
788-
self._record_start_time = time()
789-
790-
# record timer used to maintain desired framerate
791-
self._record_timer = time()
792-
793-
self._figure.add_animations(self._record)
794-
795-
def stop(self) -> float:
796-
"""
797-
End a current recording. Returns the real duration of the recording
798-
799-
Returns
800-
-------
801-
float
802-
recording duration
803-
"""
804-
805-
# tell video writer that recording has finished
806-
self._video_writer_queue.put(None)
807-
808-
# wait for writer to finish
809-
self._video_writer.join(timeout=5)
810-
811-
self._video_writer = None
812-
813-
# so self._record() is no longer called on every render cycle
814-
self._figure.remove_animation(self._record)
815-
816-
return time() - self._record_start_time

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy