@@ -568,6 +568,37 @@ def clear(self):
568
568
for subplot in self :
569
569
subplot .clear ()
570
570
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
+
571
602
def export (self , uri : str | Path | bytes , ** kwargs ):
572
603
"""
573
604
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):
593
624
"conda install -c conda-forge imageio\n "
594
625
)
595
626
else :
596
- snapshot = self .renderer .snapshot ()
597
- remove_alpha = True
598
-
599
627
# image formats that support alpha channel:
600
628
# https://en.wikipedia.org/wiki/Alpha_compositing#Image_formats_supporting_alpha_channels
601
629
alpha_support = [".png" , ".exr" , ".tiff" , ".tif" , ".gif" , ".jxl" , ".svg" ]
602
630
603
- if isinstance (uri , str ):
604
- if any ([uri .endswith (ext ) for ext in alpha_support ]):
605
- remove_alpha = False
631
+ uri = Path (uri )
606
632
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
610
637
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 )
614
639
615
640
return iio .imwrite (uri , snapshot , ** kwargs )
616
641
@@ -660,157 +685,3 @@ def __repr__(self):
660
685
f"\t { newline .join (subplot .__str__ () for subplot in self )} "
661
686
f"\n "
662
687
)
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