PyOPIA 2.9.3__tar.gz → 2.10.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {pyopia-2.9.3 → pyopia-2.10.0}/PKG-INFO +6 -4
  2. pyopia-2.10.0/pyopia/__init__.py +1 -0
  3. {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/classify.py +60 -30
  4. pyopia-2.10.0/pyopia/exampledata.py +150 -0
  5. pyopia-2.10.0/pyopia/plotting.py +221 -0
  6. {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/tests/test_classify.py +88 -63
  7. pyopia-2.10.0/pyopia/tests/test_notebooks.py +26 -0
  8. {pyopia-2.9.3 → pyopia-2.10.0}/pyproject.toml +5 -3
  9. pyopia-2.9.3/pyopia/__init__.py +0 -1
  10. pyopia-2.9.3/pyopia/exampledata.py +0 -143
  11. pyopia-2.9.3/pyopia/plotting.py +0 -51
  12. pyopia-2.9.3/pyopia/tests/test_notebooks.py +0 -22
  13. {pyopia-2.9.3 → pyopia-2.10.0}/.gitignore +0 -0
  14. {pyopia-2.9.3 → pyopia-2.10.0}/LICENSE +0 -0
  15. {pyopia-2.9.3 → pyopia-2.10.0}/README.md +0 -0
  16. {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/background.py +0 -0
  17. {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/cf_metadata.json +0 -0
  18. {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/cli.py +0 -0
  19. {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/instrument/__init__.py +0 -0
  20. {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/instrument/common.py +0 -0
  21. {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/instrument/holo.py +0 -0
  22. {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/instrument/silcam.py +0 -0
  23. {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/instrument/uvp.py +0 -0
  24. {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/io.py +0 -0
  25. {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/pipeline.py +0 -0
  26. {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/process.py +0 -0
  27. {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/simulator/__init__.py +0 -0
  28. {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/simulator/silcam.py +0 -0
  29. {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/statistics.py +0 -0
  30. {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/tests/__init__.py +0 -0
  31. {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/tests/test_io.py +0 -0
  32. {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/tests/test_pipeline.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyOPIA
3
- Version: 2.9.3
3
+ Version: 2.10.0
4
4
  Summary: A Python Ocean Particle Image Analysis toolbox.
5
5
  Project-URL: Repository, https://github.com/sintef/pyopia
6
6
  Project-URL: Documentation, https://pyopia.readthedocs.io
@@ -8,6 +8,7 @@ Author-email: Emlyn Davies <emlyn.davies@sintef.no>, "Alex Nimmo Smith@plymouth.
8
8
  License-File: LICENSE
9
9
  Keywords: Imaging,Measurement,Ocean,Particles,Size distribution
10
10
  Requires-Python: ~=3.12
11
+ Requires-Dist: click<8.2.0
11
12
  Requires-Dist: cmocean<4,>=3.0.3
12
13
  Requires-Dist: dask>=2024.8.1
13
14
  Requires-Dist: flake8<7,>=6.1.0
@@ -21,7 +22,7 @@ Requires-Dist: matplotlib>=3.7
21
22
  Requires-Dist: myst-nb<0.18,>=0.17.2
22
23
  Requires-Dist: nbclient==0.7
23
24
  Requires-Dist: nbconvert<8,>=7.16.4
24
- Requires-Dist: numpy<2,>=1.24.0
25
+ Requires-Dist: numpy>=1.24.0
25
26
  Requires-Dist: pandas[computation]<3,>=2.1.1
26
27
  Requires-Dist: poetry-version-plugin<0.3,>=0.2.0
27
28
  Requires-Dist: pytest-error-for-skips<3,>=2.0.2
@@ -29,6 +30,7 @@ Requires-Dist: pytest>=7.2.0
29
30
  Requires-Dist: readthedocs-sphinx-search<0.4,>=0.3.2
30
31
  Requires-Dist: scikit-image<0.25,>=0.24.0
31
32
  Requires-Dist: scipy<2,>=1.11.2
33
+ Requires-Dist: seaborn>=0.13.2
32
34
  Requires-Dist: sphinx-copybutton<0.6,>=0.5.2
33
35
  Requires-Dist: sphinx-rtd-theme>=0.5.0
34
36
  Requires-Dist: sphinx-togglebutton<0.4,>=0.3.2
@@ -40,8 +42,8 @@ Requires-Dist: typer[all]<0.10,>=0.9.0
40
42
  Requires-Dist: urllib3<2.0
41
43
  Requires-Dist: xarray<2024,>=2023.12.0
42
44
  Provides-Extra: classification
43
- Requires-Dist: keras==3.5.0; extra == 'classification'
44
- Requires-Dist: tensorflow<3,>=2.16.2; extra == 'classification'
45
+ Requires-Dist: keras==3.9.1; extra == 'classification'
46
+ Requires-Dist: tensorflow>=2.19.0; extra == 'classification'
45
47
  Description-Content-Type: text/markdown
46
48
 
47
49
  PyOPIA
@@ -0,0 +1 @@
1
+ __version__ = "2.10.0"
@@ -1,11 +1,13 @@
1
- '''
1
+ """
2
2
  Module containing tools for classifying particle ROIs
3
- '''
3
+ """
4
4
 
5
5
  import os
6
6
  import numpy as np
7
7
  import pandas as pd
8
8
  import logging
9
+ from skimage.exposure import rescale_intensity
10
+
9
11
  logger = logging.getLogger()
10
12
 
11
13
  # import tensorflow here. It must be imported on the processor where it will be used!
@@ -15,16 +17,16 @@ try:
15
17
  from tensorflow import keras
16
18
  import tensorflow as tf
17
19
  except ImportError:
18
- info_str = 'ERROR: Could not import Keras. Classify will not work'
19
- info_str += ' until you install tensorflow.\n'
20
- info_str += 'Use: pip install pyopia[classification]\n'
21
- info_str += ' or: pip install pyopia[classification-arm64]'
22
- info_str += ' for tensorflow-macos (silicon chips)'
20
+ info_str = "ERROR: Could not import Keras. Classify will not work"
21
+ info_str += " until you install tensorflow.\n"
22
+ info_str += "Use: pip install pyopia[classification]\n"
23
+ info_str += " or: pip install pyopia[classification-arm64]"
24
+ info_str += " for tensorflow-macos (silicon chips)"
23
25
  raise ImportError(info_str)
24
26
 
25
27
 
26
- class Classify():
27
- '''
28
+ class Classify:
29
+ """
28
30
  A classifier class for PyOPIA workflow.
29
31
  This is intended as a parent class that can be used as a template for flexible classification methods
30
32
 
@@ -32,6 +34,10 @@ class Classify():
32
34
  ----------
33
35
  model_path : str
34
36
  path to particle-classifier e.g. '/testdata/model_name/particle_classifier.keras'
37
+ normalize_intensity : bool
38
+ Scale input image intensity to [0-1] range before classification
39
+ correct_whitebalance : bool
40
+ Perform whitebalance correction before classification
35
41
 
36
42
  Example
37
43
  -------
@@ -71,23 +77,36 @@ class Classify():
71
77
  import pyopia.exampledata
72
78
  model_path = exampledata.get_example_model()
73
79
 
74
- '''
75
- def __init__(self, model_path=None):
80
+ """
81
+
82
+ def __init__(
83
+ self,
84
+ model_path=None,
85
+ normalize_intensity=True,
86
+ correct_whitebalance=False,
87
+ ):
76
88
  self.model_path = model_path
77
89
  self.load_model()
78
90
 
79
91
  # Get config for image resizing from the model
80
- _, self.img_height, self.img_width, _ = self.model.get_config()['layers'][0]['config']['batch_shape']
81
- self.pad_to_aspect_ratio = getattr(self.model.layers[0], 'pad_to_aspect_ratio', False)
92
+ _, self.img_height, self.img_width, _ = self.model.get_config()["layers"][0][
93
+ "config"
94
+ ]["batch_shape"]
95
+ self.pad_to_aspect_ratio = getattr(
96
+ self.model.layers[0], "pad_to_aspect_ratio", False
97
+ )
82
98
 
83
99
  # Enable this to perform whitebalance correction in the preprocessing step
84
- self.correct_whitebalance = False
100
+ self.correct_whitebalance = correct_whitebalance
101
+
102
+ # Enable this to rescale intensity in the preprocessing step
103
+ self.normalize_intensity = normalize_intensity
85
104
 
86
105
  def __call__(self):
87
106
  return self
88
107
 
89
108
  def load_model(self):
90
- '''
109
+ """
91
110
  Load a trained Keras model into the Classify class.
92
111
 
93
112
  Parameters
@@ -96,10 +115,10 @@ class Classify():
96
115
  loaded Keras model
97
116
  class_names: list
98
117
  names for the model output classes
99
- '''
118
+ """
100
119
  model_path = self.model_path
101
120
 
102
- os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
121
+ os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
103
122
  keras.backend.clear_session()
104
123
 
105
124
  # Instantiate Keras model from file
@@ -109,21 +128,23 @@ class Classify():
109
128
  # Try to create model output class name list from last model layer name
110
129
  class_labels = None
111
130
  try:
112
- class_labels = self.model.layers[-1].name.split('.')
131
+ class_labels = self.model.layers[-1].name.split(".")
113
132
  except: # noqa E722
114
- logger.info('Could not get class names from model layer name, reverting to old method with header file.')
133
+ logger.info(
134
+ "Could not get class names from model layer name, reverting to old method with header file."
135
+ )
115
136
 
116
137
  # If we could not create correct class names above, revert to old header file method
117
138
  expected_class_number = self.model.layers[-1].output.shape[1]
118
139
  if class_labels is None or len(class_labels) != expected_class_number:
119
- header = pd.read_csv(os.path.join(path, 'header.tfl.txt'))
140
+ header = pd.read_csv(os.path.join(path, "header.tfl.txt"))
120
141
  class_labels = header.columns
121
142
 
122
143
  self.class_labels = class_labels
123
144
  logger.info(self.class_labels)
124
145
 
125
146
  def preprocessing(self, img_input):
126
- '''
147
+ """
127
148
  Preprocess ROI ready for prediction. example here based on the pysilcam network setup
128
149
 
129
150
  Parameters
@@ -135,7 +156,7 @@ class Classify():
135
156
  -------
136
157
  img_preprocessed : float
137
158
  A particle ROI with range 0.-255., corrected and preprocessed, ready for prediction
138
- '''
159
+ """
139
160
 
140
161
  whitebalanced = img_input.astype(np.float64)
141
162
 
@@ -143,10 +164,16 @@ class Classify():
143
164
  if self.correct_whitebalance:
144
165
  p = 99
145
166
  for c in range(3):
146
- whitebalanced[:, :, c] += (p/100) - np.percentile(whitebalanced[:, :, c], p)
167
+ whitebalanced[:, :, c] += (p / 100) - np.percentile(
168
+ whitebalanced[:, :, c], p
169
+ )
147
170
  whitebalanced[whitebalanced > 1] = 1
148
171
  whitebalanced[whitebalanced < 0] = 0
149
172
 
173
+ # Rescale intensity to 0-1 range
174
+ if self.normalize_intensity:
175
+ whitebalanced = rescale_intensity(whitebalanced)
176
+
150
177
  # convert back to 0-255 scaling (because of this layer in the network:
151
178
  # layers.Rescaling(1./255, input_shape=(img_height, img_width, 3)))
152
179
  # This is useful because it allows training to use tf.keras.utils.image_dataset_from_directory,
@@ -154,9 +181,12 @@ class Classify():
154
181
  img = keras.utils.img_to_array(whitebalanced * 255)
155
182
 
156
183
  # resize to match the dimentions expected by the network
157
- img = tf.image.resize(img, [self.img_height, self.img_width],
158
- method=tf.image.ResizeMethod.BILINEAR,
159
- preserve_aspect_ratio=self.pad_to_aspect_ratio)
184
+ img = tf.image.resize(
185
+ img,
186
+ [self.img_height, self.img_width],
187
+ method=tf.image.ResizeMethod.BILINEAR,
188
+ preserve_aspect_ratio=self.pad_to_aspect_ratio,
189
+ )
160
190
 
161
191
  img_array = tf.keras.utils.img_to_array(img)
162
192
  img_preprocessed = tf.expand_dims(img_array, 0) # Create a batch
@@ -164,7 +194,7 @@ class Classify():
164
194
 
165
195
  @tf.function
166
196
  def predict(self, img_preprocessed):
167
- '''
197
+ """
168
198
  Use tensorflow model to classify particles. example here based on the pysilcam network setup.
169
199
 
170
200
  Parameters
@@ -177,14 +207,14 @@ class Classify():
177
207
  -------
178
208
  prediction : array
179
209
  The probability of the roi belonging to each class
180
- '''
210
+ """
181
211
 
182
212
  prediction = self.model(img_preprocessed, training=False)
183
213
  prediction = tf.nn.softmax(prediction[0])
184
214
  return prediction
185
215
 
186
216
  def proc_predict(self, img_input):
187
- '''
217
+ """
188
218
  Run pre-processing (:meth:`Classify.preprocessing`) and prediction (:meth:`Classify.predict`)
189
219
  using tensorflow model to classify particles. example here based on the pysilcam network setup.
190
220
 
@@ -197,7 +227,7 @@ class Classify():
197
227
  -------
198
228
  prediction : array
199
229
  The probability of the roi belonging to each class
200
- '''
230
+ """
201
231
  img_preprocessed = self.preprocessing(img_input)
202
232
  prediction = self.predict(img_preprocessed)
203
233
 
@@ -0,0 +1,150 @@
1
+ import urllib.request
2
+ import zipfile
3
+ import os
4
+ import gdown
5
+ from pathlib import Path
6
+
7
+ import logging
8
+
9
+ logger = logging.getLogger()
10
+
11
+
12
+ def get_classifier_database_from_pysilcam_blob(download_directory="./"):
13
+ """Downloads a specified filename from the pysilcam.blob into the working dir. if it doesn't already exist
14
+
15
+ only works for known filenames that are on this blob
16
+
17
+ Parameters
18
+ ----------
19
+ filename : string
20
+ known filename on the blob
21
+
22
+ """
23
+ if os.path.exists(os.path.join(download_directory)):
24
+ logger.info(download_directory, "already exists. Returning nothing")
25
+ return download_directory
26
+ os.makedirs(download_directory, exist_ok=False)
27
+ url = "https://pysilcam.blob.core.windows.net/test-data/silcam_database.zip"
28
+ logger.info("Downloading....")
29
+ urllib.request.urlretrieve(url, download_directory + "/silcam_database.zip")
30
+ logger.info("Unzipping....")
31
+ with zipfile.ZipFile(
32
+ os.path.join(download_directory, "silcam_database.zip"), "r"
33
+ ) as zipit:
34
+ zipit.extractall(os.path.join(download_directory, "../"))
35
+ logger.info("Removing zip file")
36
+ os.remove(os.path.join(download_directory, "silcam_database.zip"))
37
+ logger.info("Done.")
38
+ return download_directory
39
+
40
+
41
+ def get_file_from_pysilcam_blob(filename, download_directory="./"):
42
+ """Downloads a specified filename from the pysilcam.blob into the working dir. if it doesn't already exist
43
+
44
+ only works for known filenames that are on this blob
45
+
46
+ Parameters
47
+ ----------
48
+ filename : string
49
+ known filename on the blob
50
+
51
+ """
52
+ if os.path.exists(os.path.join(download_directory, filename)):
53
+ return filename
54
+ url = "https://pysilcam.blob.core.windows.net/test-data/" + filename
55
+ urllib.request.urlretrieve(url, os.path.join(download_directory, filename))
56
+ return download_directory
57
+
58
+
59
+ def get_example_silc_image(download_directory="./"):
60
+ """calls `get_file_from_pysilcam_blob` for a silcam iamge
61
+
62
+ Returns
63
+ -------
64
+ string
65
+ filename
66
+ """
67
+ filename = "D20181101T142731.838206.silc"
68
+ if os.path.isfile(filename):
69
+ logger.info("Example image already exists. Skipping download.")
70
+ return filename
71
+ get_file_from_pysilcam_blob(filename, download_directory)
72
+ return filename
73
+
74
+
75
+ def get_example_model(download_directory="./"):
76
+ """Download PyOPIA default CNN model classifier
77
+
78
+ Download from the pysilcam blob storage into the working dir.
79
+ If the file exists, skip the download.
80
+
81
+ Returns
82
+ -------
83
+ string
84
+ model_filename
85
+ """
86
+ model_filename = "pyopia-default-classifier-20250409.keras"
87
+ model_path = Path(download_directory, model_filename)
88
+ model_url = (
89
+ f"https://pysilcam.blob.core.windows.net/test-data/{str(model_filename)}"
90
+ )
91
+ if not model_path.exists():
92
+ logger.info("Downloading example model...")
93
+ urllib.request.urlretrieve(model_url, model_path)
94
+ return str(model_path)
95
+
96
+
97
+ def get_example_hologram_and_background(download_directory="./"):
98
+ """calls `get_file_from_pysilcam_blob` for a raw hologram, and its associated background image.
99
+
100
+ Returns
101
+ -------
102
+ string
103
+ holo_filename
104
+
105
+ string
106
+ holo_background_filename
107
+ """
108
+ holo_filename = "001-2082.pgm"
109
+ holo_background_filename = "imbg-" + holo_filename
110
+ get_file_from_pysilcam_blob(holo_filename, download_directory)
111
+ get_file_from_pysilcam_blob(holo_background_filename, download_directory)
112
+
113
+ holo_filename = os.path.join(download_directory, holo_filename)
114
+ holo_background_filename = os.path.join(
115
+ download_directory, holo_background_filename
116
+ )
117
+
118
+ return holo_filename, holo_background_filename
119
+
120
+
121
+ def get_folder_from_holo_repository(foldername="holo_test_data_01", existsok=False):
122
+ """Downloads a specified folder from the holo testing repository into the working dir. if it doesn't already exist
123
+
124
+ only works for known folders that are on the GoogleDrive repository
125
+ by default will download a known-good folder. Additional elif statements can be added to implement additional folders.
126
+
127
+ Parameters
128
+ ----------
129
+ foldername : string
130
+ known filename on the blob
131
+ existsok : (bool, optional)
132
+ if True, then don't download if the specified folder already exists, defaults to False
133
+
134
+ """
135
+ if foldername == "holo_test_data_01":
136
+ url = "https://drive.google.com/drive/folders/1yNatOaKdWwYQp-5WVEDItoibr-k0lGsP?usp=share_link"
137
+
138
+ elif foldername == "holo_test_data_02":
139
+ url = "https://drive.google.com/drive/folders/1E5iNSyfeKcVMLVe4PNEwF2Q2mo3WVjF5?usp=share_link"
140
+
141
+ else:
142
+ foldername == "holo_test_data_01"
143
+ url = "https://drive.google.com/drive/folders/1yNatOaKdWwYQp-5WVEDItoibr-k0lGsP?usp=share_link"
144
+
145
+ if os.path.exists(foldername) and existsok:
146
+ logger.info(foldername + " already exists. Skipping download.")
147
+ return foldername
148
+
149
+ gdown.download_folder(url, quiet=True, use_cookies=False)
150
+ return foldername
@@ -0,0 +1,221 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Particle plotting functionality for standardised figures
4
+ e.g. image presentation, size distributions, montages etc.
5
+ """
6
+
7
+ import matplotlib.pyplot as plt
8
+ import numpy as np
9
+ import seaborn as sns
10
+ import pandas as pd
11
+ import pyopia.statistics
12
+
13
+
14
+ def show_image(image, pixel_size):
15
+ """Plots a scaled figure (in mm) of an image
16
+
17
+ Parameters
18
+ ----------
19
+ image : float
20
+ Image (usually a corrected image, such as im_corrected)
21
+ pixel_size : float
22
+ the pixel size (um) of the imaging system used
23
+ """
24
+ r, c = np.shape(image[:, :, 0])
25
+
26
+ plt.imshow(
27
+ image,
28
+ extent=[0, c * pixel_size / 1000, 0, r * pixel_size / 1000],
29
+ interpolation="nearest",
30
+ )
31
+ plt.xlabel("mm")
32
+ plt.ylabel("mm")
33
+
34
+ return
35
+
36
+
37
+ def montage_plot(montage, pixel_size):
38
+ """
39
+ Plots a SilCam particle montage with a 1mm scale reference
40
+
41
+ Parameters
42
+ ----------
43
+ montage : uint8
44
+ a montage created with scpp.make_montage
45
+ pixel_size : float
46
+ the pixel size (um) of the imaging system used
47
+ """
48
+ msize = np.shape(montage)[0]
49
+ ex = pixel_size * np.float64(msize) / 1000.0
50
+
51
+ ax = plt.gca()
52
+ ax.imshow(montage, extent=[0, ex, 0, ex], cmap="grey")
53
+ ax.set_xticks([1, 2], [])
54
+ ax.set_xticklabels([" 1mm", ""])
55
+ ax.set_yticks([], [])
56
+ ax.xaxis.set_ticks_position("bottom")
57
+
58
+
59
+ def classify_rois(roilist, classifier):
60
+ """Classify list of single-object images
61
+
62
+ If true_class is specified, mark ROIs not matching this class in the figure.
63
+
64
+ Parameters
65
+ ----------
66
+ roilist: list
67
+ List of ROI images to classify
68
+
69
+ classifier: pyopia.classify.Classify
70
+ Used to classify ROIs
71
+
72
+ Returns
73
+ -------
74
+ df: pd.DataFrame
75
+ Class probabilities for each item in roifiles
76
+ """
77
+
78
+ # Get class labels from classifier
79
+ class_labels = [f"probability_{cl}" for cl in classifier.class_labels]
80
+
81
+ # Classify all ROIs
82
+ classify_data = []
83
+ for img in roilist:
84
+ prediction = classifier.proc_predict(img).numpy()
85
+ classify_data.append(prediction)
86
+ df = pd.DataFrame(columns=class_labels, data=classify_data)
87
+
88
+ return df
89
+
90
+
91
+ def plot_classified_rois(roilist, df_class_labels, true_class=None):
92
+ """Plot classified single-object images and show them in a figure grid with classification info
93
+
94
+ If true_class is specified, mark ROIs not matching this class in the figure.
95
+
96
+ Parameters
97
+ ----------
98
+ roilist: list
99
+ List of ROI image to classify
100
+
101
+ df_class_labels: pd.DataFrame
102
+ Class label for each image in roilist in a column named "best guess"
103
+
104
+ true_class: str
105
+ True class of listed ROIs
106
+
107
+
108
+ Returns
109
+ -------
110
+ fig: matplotlib figure
111
+ ax: matplotlib axes
112
+ """
113
+ if "best guess" not in df_class_labels:
114
+ raise RuntimeError(
115
+ "df_class_clabels must contain column 'best guess'. "
116
+ "See e.g. pyopia.statistics.add_best_guesses_to_stats"
117
+ )
118
+
119
+ # Get class labels and class index for each ROI
120
+ label_maxprob_list = df_class_labels["best guess"].values
121
+ class_maxprob_list = pd.Categorical(
122
+ df_class_labels["best guess"].values,
123
+ categories=[
124
+ cl
125
+ for cl in df_class_labels.columns
126
+ if cl not in ["best guess", "best guess value"]
127
+ ],
128
+ ordered=True,
129
+ ).codes
130
+
131
+ # Set up figure with 15 axes columns
132
+ N = len(roilist)
133
+ ncols = img_per_col = min(N, 15)
134
+ nrows = 1
135
+ if N > img_per_col:
136
+ ncols = img_per_col
137
+ nrows = N // ncols + int((N % img_per_col) > 0)
138
+ fig, axes = plt.subplots(nrows, ncols, figsize=(1 * ncols, 1 * nrows))
139
+
140
+ # Plot ROIs in an ncols x nrows grid
141
+ colors = sns.color_palette()
142
+ for i, (img, ax) in enumerate(zip(roilist, axes.flatten())):
143
+ ax.imshow(img)
144
+ ax.set_xticks([])
145
+ ax.set_yticks([])
146
+ plt.setp(ax.spines.values(), color=colors[class_maxprob_list[i]], lw=4)
147
+ ax.text(
148
+ 0,
149
+ 1,
150
+ f"{class_maxprob_list[i]} {label_maxprob_list[i][:4].upper()}",
151
+ ha="left",
152
+ va="top",
153
+ fontsize=8,
154
+ transform=ax.transAxes,
155
+ )
156
+ if true_class and (label_maxprob_list[i] != true_class):
157
+ ax.text(
158
+ 0.5,
159
+ 0.5,
160
+ "X",
161
+ ha="center",
162
+ va="center",
163
+ fontsize=20,
164
+ color="red",
165
+ alpha=0.5,
166
+ transform=ax.transAxes,
167
+ )
168
+
169
+ # Hide non-used axes in the grid
170
+ for ax in axes.flatten()[len(roilist):]:
171
+ ax.set_visible(False)
172
+
173
+ fig.patch.set_linewidth(10)
174
+ fig.patch.set_edgecolor("k")
175
+
176
+ return fig, ax
177
+
178
+
179
+ def classify_plot_class_rois(class_name, classifier, filelist):
180
+ """Classify single-object (ROI) images and plot images in a grid with best guess class.
181
+
182
+ Parameters
183
+ ----------
184
+ class_name: str
185
+ Name of class ROI files belong to (e.g. 'copepod')
186
+ classifier: pyopia.classify.Classify
187
+ PyOPIA classifier instance
188
+ filelist: list
189
+ List of single-object (ROI) files
190
+
191
+ Returns
192
+ -------
193
+ df_: pandas.DataFrame
194
+ Classification results for each image
195
+ """
196
+ # Load single-object images (ROIs)
197
+ roilist = [np.float64(plt.imread(f)) / 255.0 for f in filelist]
198
+
199
+ # Classify ROIs
200
+ df_ = classify_rois(roilist, classifier)
201
+ df_ = pyopia.statistics.add_best_guesses_to_stats(df_)
202
+
203
+ # Remove "_probability" from labels
204
+ df_ = df_.replace("probability_", "", regex=True)
205
+ df_.columns = df_.columns.str.replace("probability_", "", regex=True)
206
+
207
+ # Plot
208
+ fig, ax = plot_classified_rois(roilist, df_, true_class=class_name)
209
+
210
+ # Print classification info
211
+ num_correct = (df_["best guess"] == class_name).sum()
212
+ num_images = len(roilist)
213
+ frac_class = num_correct / num_images
214
+ print(
215
+ f"Correctly identified {class_name} was {100 * frac_class:.1f}% ({num_correct}/{num_images})"
216
+ )
217
+ fig.suptitle(
218
+ f"Class: {class_name} ({num_correct}/{num_images}, {100 * frac_class:.1f}%)"
219
+ )
220
+
221
+ return df_.style.format(precision=1, decimal=".")
@@ -1,7 +1,7 @@
1
- '''
1
+ """
2
2
  A high level test for the basic processing pipeline.
3
3
 
4
- '''
4
+ """
5
5
 
6
6
  from glob import glob
7
7
  import tempfile
@@ -21,48 +21,48 @@ import numpy as np
21
21
  import pyopia.instrument.silcam
22
22
 
23
23
 
24
- ACCURACY = 39
24
+ ACCURACY = 60
25
25
 
26
26
 
27
27
  def test_match_to_database():
28
- '''
28
+ """
29
29
  Basic check of classification prediction against the training database.
30
30
  Therefore, if correct positive matches are not high percentages, then something is wrong with the prediction.
31
31
 
32
32
  @todo include more advanced testing of the classification feks. assert values in a confusion matrix.
33
- '''
33
+ """
34
34
 
35
35
  # location of the training data
36
36
  with tempfile.TemporaryDirectory() as tempdir:
37
37
  # location of the training data
38
- database_path = os.path.join(tempdir, 'silcam_classification_database')
38
+ database_path = os.path.join(tempdir, "silcam_classification_database")
39
39
 
40
40
  exampledata.get_classifier_database_from_pysilcam_blob(database_path)
41
- os.makedirs(os.path.join(tempdir, 'model'), exist_ok=True)
42
- model_path = exampledata.get_example_model(os.path.join(tempdir, 'model'))
41
+ os.makedirs(os.path.join(tempdir, "model"), exist_ok=True)
42
+ model_path = exampledata.get_example_model(os.path.join(tempdir, "model"))
43
43
 
44
44
  # Load the trained tensorflow model and class names
45
45
  cl = pyopia.classify.Classify(model_path=model_path)
46
46
  class_labels = cl.class_labels
47
47
 
48
48
  # class_labels should match the training data
49
- classes = sorted(glob(os.path.join(database_path, '*')))
49
+ classes = sorted(glob(os.path.join(database_path, "*")))
50
50
 
51
51
  # @todo write a quick check that classes and class_labels agree before doing the proper test.
52
52
 
53
53
  def correct_positives(category):
54
- '''
54
+ """
55
55
  calculate the percentage positive matches for a given category
56
- '''
57
- print('Checking', category)
56
+ """
57
+ print("Checking", category)
58
58
  # list the files in this category of the training data
59
- files = glob(os.path.join(database_path, category, '*.tiff'))
59
+ files = glob(os.path.join(database_path, category, "*.tiff"))
60
60
 
61
- assert len(files) > 50, 'less then 50 files in test data.'
61
+ assert len(files) > 50, "less then 50 files in test data."
62
62
 
63
63
  # start a counter of incorrectly classified images
64
64
  failed = 0
65
- time_limit = len(files) * 0.01
65
+ time_limit = len(files) * 0.02
66
66
  t1 = pd.Timestamp.now()
67
67
 
68
68
  # loop through the database images
@@ -83,7 +83,7 @@ def test_match_to_database():
83
83
 
84
84
  t2 = pd.Timestamp.now()
85
85
  td = t2 - t1
86
- assert td < pd.to_timedelta(time_limit, 's'), 'Processing time too long.'
86
+ assert td < pd.to_timedelta(time_limit, "s"), "Processing time too long."
87
87
 
88
88
  return success
89
89
 
@@ -92,35 +92,37 @@ def test_match_to_database():
92
92
  name = os.path.split(cat)[-1]
93
93
  success = correct_positives(name)
94
94
  print(name, success)
95
- assert success > ACCURACY, (name + ' was poorly classified at only ' + str(success) + 'percent.')
95
+ assert success > ACCURACY, (
96
+ name + " was poorly classified at only " + str(success) + "percent."
97
+ )
96
98
 
97
99
 
98
100
  def test_pipeline_classification():
99
- '''Check that the pipeline doesn't change the outcome of the classification.
101
+ """Check that the pipeline doesn't change the outcome of the classification.
100
102
  Do this by putting rois of know classificion (which we know get correctly classified independintly),
101
103
  and then use the same model in a pipeline analysing the synthetic image.
102
- '''
104
+ """
103
105
 
104
106
  with tempfile.TemporaryDirectory() as tempdir:
105
107
  # location of the training data
106
- database_path = os.path.join(tempdir, 'silcam_classification_database')
108
+ database_path = os.path.join(tempdir, "silcam_classification_database")
107
109
 
108
110
  exampledata.get_classifier_database_from_pysilcam_blob(database_path)
109
- os.makedirs('model', exist_ok=True)
110
- model_path = exampledata.get_example_model('model')
111
+ os.makedirs("model", exist_ok=True)
112
+ model_path = exampledata.get_example_model("model")
111
113
 
112
114
  # Load the trained tensorflow model and class names
113
115
  cl = pyopia.classify.Classify(model_path=model_path)
114
116
 
115
117
  def get_good_roi(category):
116
- '''
118
+ """
117
119
  calculate the percentage positive matches for a given category
118
- '''
120
+ """
119
121
 
120
- print('category', category)
122
+ print("category", category)
121
123
  # list the files in this category of the training data
122
- files = sorted(glob(os.path.join(database_path, category, '*.tiff')))
123
- print(len(files), 'files')
124
+ files = sorted(glob(os.path.join(database_path, category, "*.tiff")))
125
+ print(len(files), "files")
124
126
 
125
127
  found_match = 0
126
128
  # loop through the database images
@@ -136,16 +138,18 @@ def test_pipeline_classification():
136
138
 
137
139
  # check if the highest score matches the correct category
138
140
  if cl.class_labels[ind] == category:
139
- print('roi file', file)
141
+ print("roi file", file)
140
142
  return img, category
141
- assert found_match == 1, f'classifier not finding matching particle for {category}'
143
+ assert found_match == 1, (
144
+ f"classifier not finding matching particle for {category}"
145
+ )
142
146
 
143
147
  canvas = np.ones((2048, 2448, 3), np.float64)
144
148
 
145
- rc_shift = int(2048/len(cl.class_labels)/1.5)
149
+ rc_shift = int(2048 / len(cl.class_labels) / 1.5)
146
150
  rc = rc_shift
147
151
 
148
- classes = sorted(glob(os.path.join(database_path, '*')))
152
+ classes = sorted(glob(os.path.join(database_path, "*")))
149
153
 
150
154
  categories = []
151
155
 
@@ -155,75 +159,96 @@ def test_pipeline_classification():
155
159
  categories.append(category)
156
160
  img_shape = np.shape(img)
157
161
  rc += rc_shift
158
- canvas[rc:rc + img_shape[0], rc: rc + img_shape[1], :] = np.float64(img)
162
+ canvas[rc: rc + img_shape[0], rc: rc + img_shape[1], :] = np.float64(img)
159
163
 
160
- settings = {'general': {'raw_files': None,
161
- 'pixel_size': 24},
162
- 'steps': {'note': 'non-standard pipeline.'}
163
- }
164
+ settings = {
165
+ "general": {"raw_files": None, "pixel_size": 24},
166
+ "steps": {"note": "non-standard pipeline."},
167
+ }
164
168
 
165
169
  # Initialise the pipeline class without running anything
166
- MyPipeline = pyopia.pipeline.Pipeline(settings=settings, initial_steps='')
170
+ MyPipeline = pyopia.pipeline.Pipeline(settings=settings, initial_steps="")
167
171
 
168
172
  # Get the example trained model
169
173
  model_path = pyopia.exampledata.get_example_model(os.getcwd())
170
174
 
171
175
  # Add the classifier step description to settings (i.e. metadata)
172
- MyPipeline.settings['steps'].update({'classifier':
173
- {'pipeline_class': 'pyopia.classify.Classify',
174
- 'model_path': model_path}})
176
+ MyPipeline.settings["steps"].update(
177
+ {
178
+ "classifier": {
179
+ "pipeline_class": "pyopia.classify.Classify",
180
+ "model_path": model_path,
181
+ }
182
+ }
183
+ )
175
184
 
176
185
  # Execute the classifier step we defined above
177
- MyPipeline.run_step('classifier')
186
+ MyPipeline.run_step("classifier")
178
187
  # This is the same as running:
179
188
  # MyPipeline.data['cl'] = pyopia.classify.Classify(model_path=model_path)
180
189
  # Note: the classifier step is special in that it's output is specifically data['cl'], rather than other new keys in data
181
190
 
182
- MyPipeline.data['imraw'] = canvas
183
- MyPipeline.data['timestamp'] = pd.Timestamp.now()
184
- MyPipeline.data['filename'] = ''
191
+ MyPipeline.data["imraw"] = canvas
192
+ MyPipeline.data["timestamp"] = pd.Timestamp.now()
193
+ MyPipeline.data["filename"] = ""
185
194
 
186
195
  # Add the imageprep step description
187
- MyPipeline.settings['steps'].update({'imageprep':
188
- {'pipeline_class': 'pyopia.instrument.silcam.ImagePrep',
189
- 'image_level': 'imraw'}})
196
+ MyPipeline.settings["steps"].update(
197
+ {
198
+ "imageprep": {
199
+ "pipeline_class": "pyopia.instrument.silcam.ImagePrep",
200
+ "image_level": "imraw",
201
+ }
202
+ }
203
+ )
190
204
  # Run the step
191
- MyPipeline.run_step('imageprep')
205
+ MyPipeline.run_step("imageprep")
192
206
  # This is the same as running:
193
207
  # ImagePrep = pyopia.instrument.silcam.ImagePrep(image_level='imraw')
194
208
  # MyPipeline.data = ImagePrep(MyPipeline.data)
195
209
 
196
210
  # Add the segmentation step description
197
- MyPipeline.settings['steps'].update({'segmentation':
198
- {'pipeline_class': 'pyopia.process.Segment',
199
- 'threshold': 1,
200
- 'segment_source': 'im_minimum'}})
211
+ MyPipeline.settings["steps"].update(
212
+ {
213
+ "segmentation": {
214
+ "pipeline_class": "pyopia.process.Segment",
215
+ "threshold": 1,
216
+ "segment_source": "im_minimum",
217
+ }
218
+ }
219
+ )
201
220
  # Run the step
202
- MyPipeline.run_step('segmentation')
221
+ MyPipeline.run_step("segmentation")
203
222
  # This is the same as running:
204
223
  # Segment = pyopia.process.Segment(threshold=settings['steps']['segmentation']['threshold'])
205
224
  # data = Segment(data)
206
225
 
207
226
  # Add the segmentation step description
208
- MyPipeline.settings['steps'].update({'statextract':
209
- {'pipeline_class': 'pyopia.process.CalculateStats',
210
- 'roi_source': 'imref'}}
211
- )
227
+ MyPipeline.settings["steps"].update(
228
+ {
229
+ "statextract": {
230
+ "pipeline_class": "pyopia.process.CalculateStats",
231
+ "roi_source": "imref",
232
+ }
233
+ }
234
+ )
212
235
 
213
236
  # Run the step
214
- MyPipeline.run_step('statextract')
237
+ MyPipeline.run_step("statextract")
215
238
  # This is the same as running:
216
239
  # CalculateStats = pyopia.process.CalculateStats()
217
240
  # data = CalculateStats(data)
218
241
 
219
- stats = pyopia.statistics.add_best_guesses_to_stats(MyPipeline.data['stats'])
242
+ stats = pyopia.statistics.add_best_guesses_to_stats(MyPipeline.data["stats"])
220
243
 
221
- out = [x[12:] for x in stats['best guess'].values]
244
+ out = [x[12:] for x in stats["best guess"].values]
222
245
 
223
- print('classes input', categories)
224
- print('classes measured', out)
246
+ print("classes input", categories)
247
+ print("classes measured", out)
225
248
 
226
- assert categories == out, 'Classes returned from classifier do not match what was given to the pipeline'
249
+ assert categories == out, (
250
+ "Classes returned from classifier do not match what was given to the pipeline"
251
+ )
227
252
 
228
253
 
229
254
  if __name__ == "__main__":
@@ -0,0 +1,26 @@
1
+ """
2
+ A high level test for executing the ipynb notebooks in the notebooks folder.
3
+
4
+ Can only be run from top-level directory (i.e. with 'poetry run pytest -v')
5
+
6
+ """
7
+
8
+ from nbconvert.preprocessors import ExecutePreprocessor
9
+ import nbformat
10
+ from pathlib import Path
11
+
12
+
13
+ def test_notebooks():
14
+ notebooks = sorted(Path("notebooks/").rglob("*.ipynb"))
15
+ notebooks.append("docs/notebooks/background_correction.ipynb")
16
+ for notebook_filename in notebooks:
17
+ with open(notebook_filename, encoding="utf8") as f:
18
+ nb = nbformat.read(f, as_version=4)
19
+ ep = ExecutePreprocessor()
20
+ print("running", notebook_filename)
21
+ ep.preprocess(nb, {"metadata": {"path": "notebooks/"}})
22
+ print(notebook_filename, "complete")
23
+
24
+
25
+ if __name__ == "__main__":
26
+ test_notebooks()
@@ -17,7 +17,7 @@ keywords = [
17
17
  ]
18
18
  dependencies = [
19
19
  "flake8>=6.1.0,<7",
20
- "numpy>=1.24.0,<2",
20
+ "numpy>=1.24.0",
21
21
  "scipy>=1.11.2,<2",
22
22
  "pytest>=7.2.0",
23
23
  "imageio>=2.31.3,<3",
@@ -47,13 +47,15 @@ dependencies = [
47
47
  "nbconvert>=7.16.4,<8",
48
48
  "h5netcdf>= 1.3.0",
49
49
  "scikit-image>=0.24.0,<0.25",
50
+ "click<8.2.0",
51
+ "seaborn>=0.13.2",
50
52
  ]
51
53
 
52
54
  [project.optional-dependencies]
53
55
 
54
56
  classification = [
55
- "tensorflow>=2.16.2,<3",
56
- "keras==3.5.0",
57
+ "tensorflow>=2.19.0",
58
+ "keras==3.9.1",
57
59
  ]
58
60
 
59
61
  [project.urls]
@@ -1 +0,0 @@
1
- __version__ = "2.9.3"
@@ -1,143 +0,0 @@
1
- import urllib.request
2
- import zipfile
3
- import os
4
- import gdown
5
-
6
- import logging
7
- logger = logging.getLogger()
8
-
9
-
10
- def get_classifier_database_from_pysilcam_blob(download_directory='./'):
11
- '''Downloads a specified filename from the pysilcam.blob into the working dir. if it doesn't already exist
12
-
13
- only works for known filenames that are on this blob
14
-
15
- Parameters
16
- ----------
17
- filename : string
18
- known filename on the blob
19
-
20
- '''
21
- if os.path.exists(os.path.join(download_directory)):
22
- logger.info(download_directory, 'already exists. Returning nothing')
23
- return download_directory
24
- os.makedirs(download_directory, exist_ok=False)
25
- url = 'https://pysilcam.blob.core.windows.net/test-data/silcam_database.zip'
26
- logger.info('Downloading....')
27
- urllib.request.urlretrieve(url, download_directory + '/silcam_database.zip')
28
- logger.info('Unzipping....')
29
- with zipfile.ZipFile(os.path.join(download_directory, 'silcam_database.zip'), 'r') as zipit:
30
- zipit.extractall(os.path.join(download_directory, '../'))
31
- logger.info('Removing zip file')
32
- os.remove(os.path.join(download_directory, 'silcam_database.zip'))
33
- logger.info('Done.')
34
- return download_directory
35
-
36
-
37
- def get_file_from_pysilcam_blob(filename, download_directory='./'):
38
- '''Downloads a specified filename from the pysilcam.blob into the working dir. if it doesn't already exist
39
-
40
- only works for known filenames that are on this blob
41
-
42
- Parameters
43
- ----------
44
- filename : string
45
- known filename on the blob
46
-
47
- '''
48
- if os.path.exists(os.path.join(download_directory, filename)):
49
- return filename
50
- url = 'https://pysilcam.blob.core.windows.net/test-data/' + filename
51
- urllib.request.urlretrieve(url, os.path.join(download_directory, filename))
52
- return download_directory
53
-
54
-
55
- def get_example_silc_image(download_directory='./'):
56
- '''calls `get_file_from_pysilcam_blob` for a silcam iamge
57
-
58
- Returns
59
- -------
60
- string
61
- filename
62
- '''
63
- filename = 'D20181101T142731.838206.silc'
64
- if os.path.isfile(filename):
65
- logger.info('Example image already exists. Skipping download.')
66
- return filename
67
- get_file_from_pysilcam_blob(filename, download_directory)
68
- return filename
69
-
70
-
71
- def get_example_model(download_directory='./'):
72
- '''Downloads and unzips an example trained CNN model from the pysilcam.blob
73
- into the working dir. if it doesn't already exist.
74
-
75
- Returns
76
- -------
77
- string
78
- model_filename
79
- '''
80
- model_filename = 'silcam-classification_database_20240822-200-20240829T091048-best-epoch.keras'
81
- if os.path.exists(model_filename):
82
- return model_filename
83
- # -- Download and unzip the model --#
84
- url = 'https://pysilcam.blob.core.windows.net/test-data/silcam-classification_database_20240822-200-20240829T091048.zip'
85
- zip_filename = 'silcam-classification_database_20240822-200-20240829T091048.zip'
86
- urllib.request.urlretrieve(url, os.path.join(download_directory, zip_filename))
87
- with zipfile.ZipFile(os.path.join(download_directory, zip_filename), 'r') as zipit:
88
- zipit.extractall(download_directory)
89
- return os.path.join(download_directory, model_filename.replace('-best-epoch.keras', ''), model_filename)
90
-
91
-
92
- def get_example_hologram_and_background(download_directory='./'):
93
- '''calls `get_file_from_pysilcam_blob` for a raw hologram, and its associated background image.
94
-
95
- Returns
96
- -------
97
- string
98
- holo_filename
99
-
100
- string
101
- holo_background_filename
102
- '''
103
- holo_filename = '001-2082.pgm'
104
- holo_background_filename = 'imbg-' + holo_filename
105
- get_file_from_pysilcam_blob(holo_filename, download_directory)
106
- get_file_from_pysilcam_blob(holo_background_filename, download_directory)
107
-
108
- holo_filename = os.path.join(download_directory, holo_filename)
109
- holo_background_filename = os.path.join(download_directory, holo_background_filename)
110
-
111
- return holo_filename, holo_background_filename
112
-
113
-
114
- def get_folder_from_holo_repository(foldername="holo_test_data_01", existsok=False):
115
- '''Downloads a specified folder from the holo testing repository into the working dir. if it doesn't already exist
116
-
117
- only works for known folders that are on the GoogleDrive repository
118
- by default will download a known-good folder. Additional elif statements can be added to implement additional folders.
119
-
120
- Parameters
121
- ----------
122
- foldername : string
123
- known filename on the blob
124
- existsok : (bool, optional)
125
- if True, then don't download if the specified folder already exists, defaults to False
126
-
127
- '''
128
- if foldername == "holo_test_data_01":
129
- url = 'https://drive.google.com/drive/folders/1yNatOaKdWwYQp-5WVEDItoibr-k0lGsP?usp=share_link'
130
-
131
- elif foldername == "holo_test_data_02":
132
- url = "https://drive.google.com/drive/folders/1E5iNSyfeKcVMLVe4PNEwF2Q2mo3WVjF5?usp=share_link"
133
-
134
- else:
135
- foldername == "holo_test_data_01"
136
- url = 'https://drive.google.com/drive/folders/1yNatOaKdWwYQp-5WVEDItoibr-k0lGsP?usp=share_link'
137
-
138
- if os.path.exists(foldername) and existsok:
139
- logger.info(foldername + ' already exists. Skipping download.')
140
- return foldername
141
-
142
- gdown.download_folder(url, quiet=True, use_cookies=False)
143
- return foldername
@@ -1,51 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- '''
3
- Particle plotting functionality for standardised figures
4
- e.g. image presentation, size distributions, montages etc.
5
- '''
6
-
7
- import matplotlib.pyplot as plt
8
- import numpy as np
9
-
10
-
11
- def show_image(image, pixel_size):
12
- '''Plots a scaled figure (in mm) of an image
13
-
14
- Parameters
15
- ----------
16
- image : float
17
- Image (usually a corrected image, such as im_corrected)
18
- pixel_size : float
19
- the pixel size (um) of the imaging system used
20
- '''
21
- r, c = np.shape(image[:, :, 0])
22
-
23
- plt.imshow(image,
24
- extent=[0, c * pixel_size / 1000, 0, r * pixel_size / 1000],
25
- interpolation='nearest')
26
- plt.xlabel('mm')
27
- plt.ylabel('mm')
28
-
29
- return
30
-
31
-
32
- def montage_plot(montage, pixel_size):
33
- '''
34
- Plots a SilCam particle montage with a 1mm scale reference
35
-
36
- Parameters
37
- ----------
38
- montage : uint8
39
- a montage created with scpp.make_montage
40
- pixel_size : float
41
- the pixel size (um) of the imaging system used
42
- '''
43
- msize = np.shape(montage)[0]
44
- ex = pixel_size * np.float64(msize) / 1000.
45
-
46
- ax = plt.gca()
47
- ax.imshow(montage, extent=[0, ex, 0, ex], cmap='grey')
48
- ax.set_xticks([1, 2], [])
49
- ax.set_xticklabels([' 1mm', ''])
50
- ax.set_yticks([], [])
51
- ax.xaxis.set_ticks_position('bottom')
@@ -1,22 +0,0 @@
1
- '''
2
- A high level test for executing the ipynb notebooks in the notebooks folder.
3
-
4
- Can only be run from top-level directory (i.e. with 'poetry run pytest -v')
5
-
6
- '''
7
-
8
- from nbconvert.preprocessors import ExecutePreprocessor
9
- import nbformat
10
- from glob import glob
11
-
12
-
13
- def test_notebooks():
14
- notebooks = sorted(glob('notebooks/*.ipynb'))
15
- notebooks.append('docs/notebooks/background_correction.ipynb')
16
- for notebook_filename in notebooks:
17
- with open(notebook_filename) as f:
18
- nb = nbformat.read(f, as_version=4)
19
- ep = ExecutePreprocessor()
20
- print('running', notebook_filename)
21
- ep.preprocess(nb, {'metadata': {'path': 'notebooks/'}})
22
- print(notebook_filename, 'complete')
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes