Skip to content

Commit

Permalink
Merge pull request #5 from alasdairwilson/add_timeseries
Browse files Browse the repository at this point in the history
Add timeseries
  • Loading branch information
alasdairwilson authored Jul 11, 2024
2 parents ac22797 + 1dceff9 commit 478c890
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 1 deletion.
19 changes: 19 additions & 0 deletions docs/tutorial/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
************************

Expand Down
Binary file not shown.
46 changes: 46 additions & 0 deletions examples/visualisation/plotting_a_timeseries.py
Original file line number Diff line number Diff line change
@@ -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()
3 changes: 3 additions & 0 deletions ripplemapper/analyse.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
22 changes: 21 additions & 1 deletion ripplemapper/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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:
Expand Down
16 changes: 16 additions & 0 deletions ripplemapper/tests/test_class_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down
9 changes: 9 additions & 0 deletions ripplemapper/visualisation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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()

0 comments on commit 478c890

Please sign in to comment.