diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index 3d44912..6f00f1a 100644 --- a/docs/tutorial/index.rst +++ b/docs/tutorial/index.rst @@ -89,8 +89,27 @@ All of these functions also work with `RippleImageSeries` objects, e.g. series.images[0].plot() plt.show() +Note, here we have passed the overwrite keyword since those contour methods already exist. + For a more detailed explanation of how to use these functions, see the :ref:`reference` section. +Visualising an Image Series +**************************** + +We have multiple methods attached to rimg and rimgs that help visualise the interface. + +First we can produce animated .gif files easily: + +.. code-block:: python + + series.animate("example_series.gif") + +We can also produce a timeseries plot of any of the contours in our series: + +.. code-block:: python + + series.timeseries("Upper Boundary") + Saving and loading data ************************ diff --git a/examples/visualisation/1_00052_sample.tif_animation.gif b/examples/visualisation/1_00052_sample.tif_animation.gif deleted file mode 100644 index 70d6f9e..0000000 Binary files a/examples/visualisation/1_00052_sample.tif_animation.gif and /dev/null differ diff --git a/examples/visualisation/plotting_a_timeseries.py b/examples/visualisation/plotting_a_timeseries.py new file mode 100644 index 0000000..d21fc0c --- /dev/null +++ b/examples/visualisation/plotting_a_timeseries.py @@ -0,0 +1,46 @@ +""" +=================================== +Plotting a timeseries of a contour +=================================== + +A RippleImageSeries is simply a container class for a list of RippleImages. + +This example demonstrates how to plot a RippleImageSeries using inbuilt plotting methods. +""" + +from matplotlib import pyplot as plt + +from ripplemapper.analyse import (add_a_star_contours, add_boundary_contours, + add_chan_vese_contours) +from ripplemapper.classes import RippleImageSeries +from ripplemapper.data.example import example_dir +from ripplemapper.io import load_dir_to_obj + +################################################################# +# +# We can create a list of RippleImages from a list of image files. +# In this example we use the load_dir_to_obj method to load all images in a directory into RippleImage objects. +# +# Passing this list to the RippleImageSeries constructor will create a RippleImageSeries object. + +ripple_images = load_dir_to_obj(example_dir) +series = RippleImageSeries(ripple_images) +add_boundary_contours(series, sigma=2) +add_a_star_contours(series) +add_chan_vese_contours(series) + +################################################################# +# +# We can plot a timeseries of this object, showing how the same contour evolves over time. +# +# We can refer to the contour via index: + +series.timeseries(1) +plt.show() + +################################################################# +# +# or via the method name: + +series.timeseries('A* traversal') +plt.show() diff --git a/ripplemapper/analyse.py b/ripplemapper/analyse.py index 13b6b49..bb9c011 100644 --- a/ripplemapper/analyse.py +++ b/ripplemapper/analyse.py @@ -20,6 +20,7 @@ def add_boundary_contours(ripple_images: list[RippleImage] | RippleImage | Rippl ripple_images = [ripple_images] for ripple_image in ripple_images: if len(ripple_image.contours) > 0: + # TODO: refactor to use new get_contour method indexes = [] for i in range(len(ripple_image.contours)): if ripple_image.contours[i].method == 'Upper Boundary': @@ -57,6 +58,7 @@ def add_a_star_contours(ripple_images: list[RippleImage] | RippleImage | RippleI if len(ripple_image.contours) < 2: warnings.warn(f"RippleImage object must have at least two contours, skipping image: {ripple_image.source_file}") continue + # TODO: refactor to use new get_contour method methods = [contour.method for contour in ripple_image.contours] if 'A* traversal' in methods: if overwrite: @@ -92,6 +94,7 @@ def add_chan_vese_contours(ripple_images: list[RippleImage] | RippleImage | Ripp ripple_images = [ripple_images] for ripple_image in ripple_images: if len(ripple_image.contours) > 0: + # TODO: refactor to use new get_contour method methods = [contour.method for contour in ripple_image.contours] if 'Chan-Vese' in methods: if overwrite: diff --git a/ripplemapper/classes.py b/ripplemapper/classes.py index 0ead011..57780d3 100644 --- a/ripplemapper/classes.py +++ b/ripplemapper/classes.py @@ -11,7 +11,8 @@ from ripplemapper.contour import smooth_bumps from ripplemapper.image import preprocess_image from ripplemapper.io import load_image -from ripplemapper.visualisation import plot_contours, plot_image +from ripplemapper.visualisation import (plot_contours, plot_image, + plot_timeseries) __all__ = ['RippleContour', 'RippleImage', 'RippleImageSeries'] @@ -100,6 +101,18 @@ def add_contour(self, *args): contour = RippleContour(*args, image=self) self.contours.append(contour) + def get_contour(self, contour: str | int): + """Return a given contour for the image.""" + if isinstance(contour, int): + return self.contours[contour] + elif isinstance(contour, str): + for cont in self.contours: + if contour.lower() in cont.method.lower(): + return cont + else: + raise ValueError("Invalid input, expected an integer or method string") + + def smooth_contours(self, **kwargs): """Smooth all the contours in the image.""" self.contours = [contour.smooth(**kwargs) for contour in self.contours] @@ -194,6 +207,13 @@ def save(self, fname: str = False, save_image_data: bool = False): image.save(fname=image_fname, save_image_data=save_image_data) return fname + def timeseries(self, contour: str | int = 0, **kwargs): + """Plot a timeseries of the same contour.""" + contours = [img.get_contour(contour) for img in self.images] + labels = [img.source_file.split('/')[-1] for img in self.images] + plot_timeseries(contours, labels) + + def _load(self, file: str): """Load the image series from a file.""" with gzip.open(file, 'rb') as f: diff --git a/ripplemapper/tests/test_class_methods.py b/ripplemapper/tests/test_class_methods.py index dcfe9af..6ebfc18 100644 --- a/ripplemapper/tests/test_class_methods.py +++ b/ripplemapper/tests/test_class_methods.py @@ -3,9 +3,20 @@ import matplotlib.pyplot as plt import pytest +from ripplemapper.analyse import add_boundary_contours from ripplemapper.classes import RippleContour +def test_get_contour_index(loaded_example_image_with_contours): + contour = loaded_example_image_with_contours.get_contour(0) + assert contour is not None + +def test_get_contour_method(loaded_example_image_with_contours): + method = "Lower Boundary" + contour = loaded_example_image_with_contours.get_contour(method) + assert contour is not None + assert contour.method == method + def test_ripple_contour_to_physical(loaded_example_contour): # Assuming the function is not yet implemented loaded_example_contour.to_physical() @@ -19,6 +30,11 @@ def test_ripple_contour_plot(loaded_example_contour): loaded_example_contour.plot() plt.close() +def test_timeseries_plot(loaded_example_image_series): + add_boundary_contours(loaded_example_image_series) + loaded_example_image_series.timeseries(0) + plt.close() + def test_ripple_contour_smooth(loaded_example_contour): loaded_example_contour.smooth() # Assuming the smooth function does not return anything but modifies in place diff --git a/ripplemapper/visualisation.py b/ripplemapper/visualisation.py index 8604aed..200e4bf 100644 --- a/ripplemapper/visualisation.py +++ b/ripplemapper/visualisation.py @@ -3,6 +3,7 @@ import matplotlib.pyplot as plt import numpy as np +__all__ = ['plot_contours', 'plot_image', 'plot_timeseries'] def plot_contours(ripple_contours, *args, **kwargs): """Plot the contour.""" @@ -56,3 +57,11 @@ def plot_image(ripple_image, include_contours: bool=True, cmap: str='gray', **k for contour in ripple_image.contours: plt.plot(contour.values[:][1], contour.values[:][0], label=contour.method) plt.legend() + + +def plot_timeseries(contours, labels, **kwargs): + """Plot a timeseries of contours.""" + for i, contour in enumerate(contours): + plt.plot(contour.values[1], contour.values[0], label=labels[i], **kwargs) + plt.gca().invert_yaxis() + plt.legend()