コード例 #1
0
    def __init__(self, scene=None, title='FURY', size=(300, 300),
                 png_magnify=1, reset_camera=True, order_transparent=False,
                 interactor_style='custom', stereo='off',
                 multi_samples=8, max_peels=4, occlusion_ratio=0.0):
        """Manage the visualization pipeline.

        Parameters
        ----------
        scene : Scene() or vtkRenderer()
            The scene that holds all the actors.
        title : string
            A string for the window title bar.
        size : (int, int)
            ``(width, height)`` of the window. Default is (300, 300).
        png_magnify : int
            Number of times to magnify the screenshot. This can be used to save
            high resolution screenshots when pressing 's' inside the window.
        reset_camera : bool
            Default is True. You can change this option to False if you want to
            keep the camera as set before calling this function.
        order_transparent : bool
            True is useful when you want to order transparent
            actors according to their relative position to the camera. The
            default option which is False will order the actors according to
            the order of their addition to the Scene().
        interactor_style : str or vtkInteractorStyle
            If str then if 'trackball' then vtkInteractorStyleTrackballCamera()
            is used, if 'image' then vtkInteractorStyleImage() is used (no
            rotation) or if 'custom' then CustomInteractorStyle is used.
            Otherwise you can input your own interactor style.
        stereo: string
            Set the stereo type. Default is 'off'. Other types include:

            * 'opengl': OpenGL frame-sequential stereo. Referred to as
              'CrystalEyes' by VTK.
            * 'anaglyph': For use with red/blue glasses. See VTK docs to
              use different colors.
            * 'interlaced': Line interlaced.
            * 'checkerboard': Checkerboard interlaced.
            * 'left': Left eye only.
            * 'right': Right eye only.
            * 'horizontal': Side-by-side.

        multi_samples : int
            Number of samples for anti-aliazing (Default 8).
            For no anti-aliasing use 0.
        max_peels : int
            Maximum number of peels for depth peeling (Default 4).
        occlusion_ratio : float
            Occlusion ration for depth peeling (Default 0 - exact image).

        Attributes
        ----------
        scene : Scene() or vtkRenderer()
        iren : vtkRenderWindowInteractor()
        style : vtkInteractorStyle()
        window : vtkRenderWindow()

        Methods
        -------
        initialize()
        render()
        start()
        add_window_callback()

        Examples
        --------
        >>> from fury import actor, window
        >>> scene = window.Scene()
        >>> scene.add(actor.axes())
        >>> showm = window.ShowManager(scene)
        >>> # showm.initialize()
        >>> # showm.render()
        >>> # showm.start()

        """
        if scene is None:
            scene = Scene()
        self.scene = scene
        self.title = title
        self.size = size
        self.png_magnify = png_magnify
        self.reset_camera = reset_camera
        self.order_transparent = order_transparent
        self.interactor_style = interactor_style
        self.stereo = stereo
        self.timers = []

        if self.reset_camera:
            self.scene.ResetCamera()

        self.window = RenderWindow()

        if self.stereo.lower() != 'off':
            enable_stereo(self.window, self.stereo)

        self.window.AddRenderer(scene)

        self.window.SetSize(size[0], size[1])

        if self.order_transparent:
            occlusion_ratio = occlusion_ratio or 0.1
            antialiasing(self.scene, self.window,
                         multi_samples=0, max_peels=max_peels,
                         occlusion_ratio=occlusion_ratio)

        if self.interactor_style == 'image':
            self.style = InteractorStyleImage()
        elif self.interactor_style == 'trackball':
            self.style = InteractorStyleTrackballCamera()
        elif self.interactor_style == 'custom':
            self.style = CustomInteractorStyle()
        else:
            self.style = interactor_style

        self.iren = RenderWindowInteractor()
        self.style.SetCurrentRenderer(self.scene)
        # Hack: below, we explicitly call the Python version of SetInteractor.
        self.style.SetInteractor(self.iren)
        self.iren.SetInteractorStyle(self.style)
        self.iren.SetRenderWindow(self.window)
コード例 #2
0
    def __init__(self,
                 scene=None,
                 title='FURY',
                 size=(300, 300),
                 png_magnify=1,
                 reset_camera=True,
                 order_transparent=False,
                 interactor_style='custom'):
        """Manage the visualization pipeline.

        Parameters
        ----------
        scene : Scene() or vtkRenderer()
            The scene that holds all the actors.
        title : string
            A string for the window title bar.
        size : (int, int)
            ``(width, height)`` of the window. Default is (300, 300).
        png_magnify : int
            Number of times to magnify the screenshot. This can be used to save
            high resolution screenshots when pressing 's' inside the window.
        reset_camera : bool
            Default is True. You can change this option to False if you want to
            keep the camera as set before calling this function.
        order_transparent : bool
            True is useful when you want to order transparent
            actors according to their relative position to the camera. The
            default option which is False will order the actors according to
            the order of their addition to the Scene().
        interactor_style : str or vtkInteractorStyle
            If str then if 'trackball' then vtkInteractorStyleTrackballCamera()
            is used, if 'image' then vtkInteractorStyleImage() is used (no
            rotation) or if 'custom' then CustomInteractorStyle is used.
            Otherwise you can input your own interactor style.

        Attributes
        ----------
        scene : Scene() or vtkRenderer()
        iren : vtkRenderWindowInteractor()
        style : vtkInteractorStyle()
        window : vtkRenderWindow()

        Methods
        -------
        initialize()
        render()
        start()
        add_window_callback()

        Notes
        -----
        Default interaction keys for

        * 3d navigation are with left, middle and right mouse dragging
        * resetting the camera press 'r'
        * saving a screenshot press 's'
        * for quiting press 'q'

        Examples
        --------
        >>> from fury import actor, window
        >>> scene = window.Scene()
        >>> scene.add(actor.axes())
        >>> showm = window.ShowManager(scene)
        >>> # showm.initialize()
        >>> # showm.render()
        >>> # showm.start()

        """
        if scene is None:
            scene = Scene()
        self.scene = scene
        self.title = title
        self.size = size
        self.png_magnify = png_magnify
        self.reset_camera = reset_camera
        self.order_transparent = order_transparent
        self.interactor_style = interactor_style
        self.timers = []

        if self.reset_camera:
            self.scene.ResetCamera()

        self.window = vtk.vtkRenderWindow()
        self.window.AddRenderer(scene)

        if self.title == 'FURY':
            self.window.SetWindowName(title + ' ' + fury_version)
        else:
            self.window.SetWindowName(title)
        self.window.SetSize(size[0], size[1])

        if self.order_transparent:

            # Use a render window with alpha bits
            # as default is 0 (false))
            self.window.SetAlphaBitPlanes(True)

            # Force to not pick a framebuffer with a multisample buffer
            # (default is 8)
            self.window.SetMultiSamples(0)

            # Choose to use depth peeling (if supported)
            # (default is 0 (false)):
            self.scene.UseDepthPeelingOn()

            # Set depth peeling parameters
            # Set the maximum number of rendering passes (default is 4)
            scene.SetMaximumNumberOfPeels(4)

            # Set the occlusion ratio (initial value is 0.0, exact image):
            scene.SetOcclusionRatio(0.0)

        if self.interactor_style == 'image':
            self.style = vtk.vtkInteractorStyleImage()
        elif self.interactor_style == 'trackball':
            self.style = vtk.vtkInteractorStyleTrackballCamera()
        elif self.interactor_style == 'custom':
            self.style = CustomInteractorStyle()
        else:
            self.style = interactor_style

        self.iren = vtk.vtkRenderWindowInteractor()
        self.style.SetCurrentRenderer(self.scene)
        # Hack: below, we explicitly call the Python version of SetInteractor.
        self.style.SetInteractor(self.iren)
        self.iren.SetInteractorStyle(self.style)
        self.iren.SetRenderWindow(self.window)
コード例 #3
0
class ShowManager(object):
    """Class interface between the scene, the window and the interactor."""

    def __init__(self, scene=None, title='FURY', size=(300, 300),
                 png_magnify=1, reset_camera=True, order_transparent=False,
                 interactor_style='custom', stereo='off',
                 multi_samples=8, max_peels=4, occlusion_ratio=0.0):
        """Manage the visualization pipeline.

        Parameters
        ----------
        scene : Scene() or vtkRenderer()
            The scene that holds all the actors.
        title : string
            A string for the window title bar.
        size : (int, int)
            ``(width, height)`` of the window. Default is (300, 300).
        png_magnify : int
            Number of times to magnify the screenshot. This can be used to save
            high resolution screenshots when pressing 's' inside the window.
        reset_camera : bool
            Default is True. You can change this option to False if you want to
            keep the camera as set before calling this function.
        order_transparent : bool
            True is useful when you want to order transparent
            actors according to their relative position to the camera. The
            default option which is False will order the actors according to
            the order of their addition to the Scene().
        interactor_style : str or vtkInteractorStyle
            If str then if 'trackball' then vtkInteractorStyleTrackballCamera()
            is used, if 'image' then vtkInteractorStyleImage() is used (no
            rotation) or if 'custom' then CustomInteractorStyle is used.
            Otherwise you can input your own interactor style.
        stereo: string
            Set the stereo type. Default is 'off'. Other types include:

            * 'opengl': OpenGL frame-sequential stereo. Referred to as
              'CrystalEyes' by VTK.
            * 'anaglyph': For use with red/blue glasses. See VTK docs to
              use different colors.
            * 'interlaced': Line interlaced.
            * 'checkerboard': Checkerboard interlaced.
            * 'left': Left eye only.
            * 'right': Right eye only.
            * 'horizontal': Side-by-side.

        multi_samples : int
            Number of samples for anti-aliazing (Default 8).
            For no anti-aliasing use 0.
        max_peels : int
            Maximum number of peels for depth peeling (Default 4).
        occlusion_ratio : float
            Occlusion ration for depth peeling (Default 0 - exact image).

        Attributes
        ----------
        scene : Scene() or vtkRenderer()
        iren : vtkRenderWindowInteractor()
        style : vtkInteractorStyle()
        window : vtkRenderWindow()

        Methods
        -------
        initialize()
        render()
        start()
        add_window_callback()

        Examples
        --------
        >>> from fury import actor, window
        >>> scene = window.Scene()
        >>> scene.add(actor.axes())
        >>> showm = window.ShowManager(scene)
        >>> # showm.initialize()
        >>> # showm.render()
        >>> # showm.start()

        """
        if scene is None:
            scene = Scene()
        self.scene = scene
        self.title = title
        self.size = size
        self.png_magnify = png_magnify
        self.reset_camera = reset_camera
        self.order_transparent = order_transparent
        self.interactor_style = interactor_style
        self.stereo = stereo
        self.timers = []

        if self.reset_camera:
            self.scene.ResetCamera()

        self.window = RenderWindow()

        if self.stereo.lower() != 'off':
            enable_stereo(self.window, self.stereo)

        self.window.AddRenderer(scene)

        self.window.SetSize(size[0], size[1])

        if self.order_transparent:
            occlusion_ratio = occlusion_ratio or 0.1
            antialiasing(self.scene, self.window,
                         multi_samples=0, max_peels=max_peels,
                         occlusion_ratio=occlusion_ratio)

        if self.interactor_style == 'image':
            self.style = InteractorStyleImage()
        elif self.interactor_style == 'trackball':
            self.style = InteractorStyleTrackballCamera()
        elif self.interactor_style == 'custom':
            self.style = CustomInteractorStyle()
        else:
            self.style = interactor_style

        self.iren = RenderWindowInteractor()
        self.style.SetCurrentRenderer(self.scene)
        # Hack: below, we explicitly call the Python version of SetInteractor.
        self.style.SetInteractor(self.iren)
        self.iren.SetInteractorStyle(self.style)
        self.iren.SetRenderWindow(self.window)

    def initialize(self):
        """Initialize interaction."""
        self.iren.Initialize()

    def render(self):
        """Render only once."""
        self.window.Render()

    def start(self):
        """Start interaction."""
        try:
            self.render()
            if self.title.upper() == "FURY":
                self.window.SetWindowName(self.title + " " + fury_version)
            else:
                self.window.SetWindowName(self.title)
            self.iren.Start()
        except AttributeError:
            self.__init__(self.scene, self.title, size=self.size,
                          png_magnify=self.png_magnify,
                          reset_camera=self.reset_camera,
                          order_transparent=self.order_transparent,
                          interactor_style=self.interactor_style)
            self.initialize()
            self.render()
            if self.title.upper() == "FURY":
                self.window.SetWindowName(self.title + " " + fury_version)
            else:
                self.window.SetWindowName(self.title)
            self.iren.Start()

        self.window.RemoveRenderer(self.scene)
        self.scene.SetRenderWindow(None)
        self.window.Finalize()
        del self.iren
        del self.window

    def record_events(self):
        """Record events during the interaction.

        The recording is represented as a list of VTK events that happened
        during the interaction. The recorded events are then returned.

        Returns
        -------
        events : str
            Recorded events (one per line).

        Notes
        -----
        Since VTK only allows recording events to a file, we use a
        temporary file from which we then read the events.

        """
        with InTemporaryDirectory():
            filename = "recorded_events.log"
            recorder = InteractorEventRecorder()
            recorder.SetInteractor(self.iren)
            recorder.SetFileName(filename)

            def _stop_recording_and_close(_obj, _evt):
                if recorder:
                    recorder.Stop()
                self.iren.TerminateApp()

            self.iren.AddObserver("ExitEvent", _stop_recording_and_close)

            recorder.EnabledOn()
            recorder.Record()

            self.initialize()
            self.render()
            self.iren.Start()
            # Deleting this object is the unique way
            # to close the file.
            recorder = None
            # Retrieved recorded events.
            with open(filename, 'r') as f:
                events = f.read()
        return events

    def record_events_to_file(self, filename="record.log"):
        """Record events during the interaction.

        The recording is represented as a list of VTK events
        that happened during the interaction. The recording is
        going to be saved into `filename`.

        Parameters
        ----------
        filename : str
            Name of the file that will contain the recording (.log|.log.gz).

        """
        events = self.record_events()

        # Compress file if needed
        if filename.endswith(".gz"):
            with gzip.open(filename, 'wb') as fgz:
                fgz.write(asbytes(events))
        else:
            with open(filename, 'w') as f:
                f.write(events)

    def play_events(self, events):
        """Play recorded events of a past interaction.

        The VTK events that happened during the recorded interaction will be
        played back.

        Parameters
        ----------
        events : str
            Recorded events (one per line).

        """
        recorder = InteractorEventRecorder()
        recorder.SetInteractor(self.iren)

        recorder.SetInputString(events)
        recorder.ReadFromInputStringOn()
        self.initialize()
        # self.render()
        recorder.Play()

        # self.window.RemoveRenderer(self.scene)
        # self.scene.SetRenderWindow(None)

        # Finalize seems very important otherwise
        # the recording window will not close.
        self.window.Finalize()
        self.exit()
        # print('After Finalize and Exit')

        # del self.iren
        # del self.window

    def play_events_from_file(self, filename):
        """Play recorded events of a past interaction.

        The VTK events that happened during the recorded interaction will be
        played back from `filename`.

        Parameters
        ----------
        filename : str
            Name of the file containing the recorded events (.log|.log.gz).

        """
        # Uncompress file if needed.
        if filename.endswith(".gz"):
            with gzip.open(filename, 'r') as f:
                events = f.read()
        else:
            with open(filename) as f:
                events = f.read()

        self.play_events(events)

    def add_window_callback(self, win_callback,
                            event=Command.ModifiedEvent):
        """Add window callbacks."""
        self.window.AddObserver(event, win_callback)
        self.window.Render()

    def add_timer_callback(self, repeat, duration, timer_callback):
        self.iren.AddObserver("TimerEvent", timer_callback)

        if repeat:
            timer_id = self.iren.CreateRepeatingTimer(duration)
        else:
            timer_id = self.iren.CreateOneShotTimer(duration)
        self.timers.append(timer_id)

    def add_iren_callback(self, iren_callback, event="MouseMoveEvent"):
        self.iren.AddObserver(event, iren_callback)

    def destroy_timer(self, timer_id):
        self.iren.DestroyTimer(timer_id)
        del self.timers[self.timers.index(timer_id)]

    def destroy_timers(self):
        for timer_id in self.timers:
            self.destroy_timer(timer_id)

    def exit(self):
        """Close window and terminate interactor."""
        # if is_osx and self.timers:
            # OSX seems to not destroy correctly timers
            # segfault 11 appears sometimes if we do not do it manually.

        # self.iren.GetRenderWindow().Finalize()
        self.iren.TerminateApp()
        self.destroy_timers()
        self.timers.clear()

    def save_screenshot(self, fname, magnification=1, size=None, stereo=None):
        """Save a screenshot of the current window in the specified filename.

        Parameters
        ----------
        fname : str or None
            File name where to save the screenshot.
        magnification : int, optional
            Applies a magnification factor to the scene before taking the
            screenshot which improves the quality. A value greater than 1
            increases the quality of the image. However, the output size will
            be larger. For example, 200x200 image with magnification of 2 will
            result in a 400x400 image. Default is 1.
        size : tuple of 2 ints, optional
            Size of the output image in pixels. If None, the size of the scene
            will be used. If magnification > 1, then the size will be
            determined by the magnification factor. Default is None.
        stereo : str, optional
            Set the type of stereo for the screenshot. Supported values are:

                * 'opengl': OpenGL frame-sequential stereo. Referred to as
                  'CrystalEyes' by VTK.
                * 'anaglyph': For use with red/blue glasses. See VTK docs to
                  use different colors.
                * 'interlaced': Line interlaced.
                * 'checkerboard': Checkerboard interlaced.
                * 'left': Left eye only.
                * 'right': Right eye only.
                * 'horizontal': Side-by-side.

        """
        if size is None:
            size = self.size
        if stereo is None:
            stereo = self.stereo.lower()
        record(scene=self.scene, out_path=fname, magnification=magnification,
               size=size, stereo=stereo)
コード例 #4
0
class ShowManager(object):
    """Class interface between the scene, the window and the interactor."""
    def __init__(self,
                 scene=None,
                 title='FURY',
                 size=(300, 300),
                 png_magnify=1,
                 reset_camera=True,
                 order_transparent=False,
                 interactor_style='custom'):
        """Manage the visualization pipeline.

        Parameters
        ----------
        scene : Scene() or vtkRenderer()
            The scene that holds all the actors.
        title : string
            A string for the window title bar.
        size : (int, int)
            ``(width, height)`` of the window. Default is (300, 300).
        png_magnify : int
            Number of times to magnify the screenshot. This can be used to save
            high resolution screenshots when pressing 's' inside the window.
        reset_camera : bool
            Default is True. You can change this option to False if you want to
            keep the camera as set before calling this function.
        order_transparent : bool
            True is useful when you want to order transparent
            actors according to their relative position to the camera. The
            default option which is False will order the actors according to
            the order of their addition to the Scene().
        interactor_style : str or vtkInteractorStyle
            If str then if 'trackball' then vtkInteractorStyleTrackballCamera()
            is used, if 'image' then vtkInteractorStyleImage() is used (no
            rotation) or if 'custom' then CustomInteractorStyle is used.
            Otherwise you can input your own interactor style.

        Attributes
        ----------
        scene : Scene() or vtkRenderer()
        iren : vtkRenderWindowInteractor()
        style : vtkInteractorStyle()
        window : vtkRenderWindow()

        Methods
        -------
        initialize()
        render()
        start()
        add_window_callback()

        Notes
        -----
        Default interaction keys for

        * 3d navigation are with left, middle and right mouse dragging
        * resetting the camera press 'r'
        * saving a screenshot press 's'
        * for quiting press 'q'

        Examples
        --------
        >>> from fury import actor, window
        >>> scene = window.Scene()
        >>> scene.add(actor.axes())
        >>> showm = window.ShowManager(scene)
        >>> # showm.initialize()
        >>> # showm.render()
        >>> # showm.start()

        """
        if scene is None:
            scene = Scene()
        self.scene = scene
        self.title = title
        self.size = size
        self.png_magnify = png_magnify
        self.reset_camera = reset_camera
        self.order_transparent = order_transparent
        self.interactor_style = interactor_style
        self.timers = []

        if self.reset_camera:
            self.scene.ResetCamera()

        self.window = vtk.vtkRenderWindow()
        self.window.AddRenderer(scene)

        if self.title == 'FURY':
            self.window.SetWindowName(title + ' ' + fury_version)
        else:
            self.window.SetWindowName(title)
        self.window.SetSize(size[0], size[1])

        if self.order_transparent:

            # Use a render window with alpha bits
            # as default is 0 (false))
            self.window.SetAlphaBitPlanes(True)

            # Force to not pick a framebuffer with a multisample buffer
            # (default is 8)
            self.window.SetMultiSamples(0)

            # Choose to use depth peeling (if supported)
            # (default is 0 (false)):
            self.scene.UseDepthPeelingOn()

            # Set depth peeling parameters
            # Set the maximum number of rendering passes (default is 4)
            scene.SetMaximumNumberOfPeels(4)

            # Set the occlusion ratio (initial value is 0.0, exact image):
            scene.SetOcclusionRatio(0.0)

        if self.interactor_style == 'image':
            self.style = vtk.vtkInteractorStyleImage()
        elif self.interactor_style == 'trackball':
            self.style = vtk.vtkInteractorStyleTrackballCamera()
        elif self.interactor_style == 'custom':
            self.style = CustomInteractorStyle()
        else:
            self.style = interactor_style

        self.iren = vtk.vtkRenderWindowInteractor()
        self.style.SetCurrentRenderer(self.scene)
        # Hack: below, we explicitly call the Python version of SetInteractor.
        self.style.SetInteractor(self.iren)
        self.iren.SetInteractorStyle(self.style)
        self.iren.SetRenderWindow(self.window)

    def initialize(self):
        """Initialize interaction."""
        self.iren.Initialize()

    def render(self):
        """Render only once."""
        self.window.Render()

    def start(self):
        """Start interaction."""
        try:
            self.iren.Start()
        except AttributeError:
            self.__init__(self.scene,
                          self.title,
                          size=self.size,
                          png_magnify=self.png_magnify,
                          reset_camera=self.reset_camera,
                          order_transparent=self.order_transparent,
                          interactor_style=self.interactor_style)
            self.initialize()
            self.render()
            self.iren.Start()

        self.window.RemoveRenderer(self.scene)
        self.scene.SetRenderWindow(None)
        del self.iren
        del self.window

    def record_events(self):
        """Record events during the interaction.

        The recording is represented as a list of VTK events that happened
        during the interaction. The recorded events are then returned.

        Returns
        -------
        events : str
            Recorded events (one per line).

        Notes
        -----
        Since VTK only allows recording events to a file, we use a
        temporary file from which we then read the events.

        """
        with InTemporaryDirectory():
            filename = "recorded_events.log"
            recorder = vtk.vtkInteractorEventRecorder()
            recorder.SetInteractor(self.iren)
            recorder.SetFileName(filename)

            def _stop_recording_and_close(obj, evt):
                if recorder:
                    recorder.Stop()
                self.iren.TerminateApp()

            self.iren.AddObserver("ExitEvent", _stop_recording_and_close)

            recorder.EnabledOn()
            recorder.Record()

            self.initialize()
            self.render()
            self.iren.Start()
            # Deleting this object is the unique way
            # to close the file.
            recorder = None
            # Retrieved recorded events.
            with open(filename, 'r') as f:
                events = f.read()
        return events

    def record_events_to_file(self, filename="record.log"):
        """Record events during the interaction.

        The recording is represented as a list of VTK events
        that happened during the interaction. The recording is
        going to be saved into `filename`.

        Parameters
        ----------
        filename : str
            Name of the file that will contain the recording (.log|.log.gz).

        """
        events = self.record_events()

        # Compress file if needed
        if filename.endswith(".gz"):
            with gzip.open(filename, 'wb') as fgz:
                fgz.write(asbytes(events))
        else:
            with open(filename, 'w') as f:
                f.write(events)

    def play_events(self, events):
        """Play recorded events of a past interaction.

        The VTK events that happened during the recorded interaction will be
        played back.

        Parameters
        ----------
        events : str
            Recorded events (one per line).

        """
        recorder = vtk.vtkInteractorEventRecorder()
        recorder.SetInteractor(self.iren)

        recorder.SetInputString(events)
        recorder.ReadFromInputStringOn()

        self.initialize()
        self.render()
        recorder.Play()

    def play_events_from_file(self, filename):
        """Play recorded events of a past interaction.

        The VTK events that happened during the recorded interaction will be
        played back from `filename`.

        Parameters
        ----------
        filename : str
            Name of the file containing the recorded events (.log|.log.gz).

        """
        # Uncompress file if needed.
        if filename.endswith(".gz"):
            with gzip.open(filename, 'r') as f:
                events = f.read()
        else:
            with open(filename) as f:
                events = f.read()

        self.play_events(events)

    def add_window_callback(self, win_callback):
        """Add window callbacks."""
        self.window.AddObserver(vtk.vtkCommand.ModifiedEvent, win_callback)
        self.window.Render()

    def add_timer_callback(self, repeat, duration, timer_callback):
        self.iren.AddObserver("TimerEvent", timer_callback)

        if repeat:
            timer_id = self.iren.CreateRepeatingTimer(duration)
        else:
            timer_id = self.iren.CreateOneShotTimer(duration)
        self.timers.append(timer_id)

    def destroy_timer(self, timer_id):
        self.iren.DestroyTimer(timer_id)
        del self.timers[self.timers.index(timer_id)]

    def destroy_timers(self):
        for timer_id in self.timers:
            self.destroy_timer(timer_id)

    def exit(self):
        """Close window and terminate interactor."""
        self.iren.GetRenderWindow().Finalize()
        self.iren.TerminateApp()