PyOPIA 2.9.2__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.2 → pyopia-2.10.0}/PKG-INFO +6 -4
  2. pyopia-2.10.0/pyopia/__init__.py +1 -0
  3. pyopia-2.10.0/pyopia/cf_metadata.json +70 -0
  4. {pyopia-2.9.2 → pyopia-2.10.0}/pyopia/classify.py +60 -30
  5. pyopia-2.10.0/pyopia/exampledata.py +150 -0
  6. {pyopia-2.9.2 → pyopia-2.10.0}/pyopia/io.py +268 -139
  7. pyopia-2.10.0/pyopia/plotting.py +221 -0
  8. {pyopia-2.9.2 → pyopia-2.10.0}/pyopia/process.py +1 -1
  9. {pyopia-2.9.2 → pyopia-2.10.0}/pyopia/statistics.py +4 -4
  10. {pyopia-2.9.2 → pyopia-2.10.0}/pyopia/tests/test_classify.py +88 -63
  11. pyopia-2.10.0/pyopia/tests/test_io.py +64 -0
  12. pyopia-2.10.0/pyopia/tests/test_notebooks.py +26 -0
  13. {pyopia-2.9.2 → pyopia-2.10.0}/pyproject.toml +5 -3
  14. pyopia-2.9.2/pyopia/__init__.py +0 -1
  15. pyopia-2.9.2/pyopia/exampledata.py +0 -143
  16. pyopia-2.9.2/pyopia/plotting.py +0 -51
  17. pyopia-2.9.2/pyopia/tests/test_notebooks.py +0 -22
  18. {pyopia-2.9.2 → pyopia-2.10.0}/.gitignore +0 -0
  19. {pyopia-2.9.2 → pyopia-2.10.0}/LICENSE +0 -0
  20. {pyopia-2.9.2 → pyopia-2.10.0}/README.md +0 -0
  21. {pyopia-2.9.2 → pyopia-2.10.0}/pyopia/background.py +0 -0
  22. {pyopia-2.9.2 → pyopia-2.10.0}/pyopia/cli.py +0 -0
  23. {pyopia-2.9.2 → pyopia-2.10.0}/pyopia/instrument/__init__.py +0 -0
  24. {pyopia-2.9.2 → pyopia-2.10.0}/pyopia/instrument/common.py +0 -0
  25. {pyopia-2.9.2 → pyopia-2.10.0}/pyopia/instrument/holo.py +0 -0
  26. {pyopia-2.9.2 → pyopia-2.10.0}/pyopia/instrument/silcam.py +0 -0
  27. {pyopia-2.9.2 → pyopia-2.10.0}/pyopia/instrument/uvp.py +0 -0
  28. {pyopia-2.9.2 → pyopia-2.10.0}/pyopia/pipeline.py +0 -0
  29. {pyopia-2.9.2 → pyopia-2.10.0}/pyopia/simulator/__init__.py +0 -0
  30. {pyopia-2.9.2 → pyopia-2.10.0}/pyopia/simulator/silcam.py +0 -0
  31. {pyopia-2.9.2 → pyopia-2.10.0}/pyopia/tests/__init__.py +0 -0
  32. {pyopia-2.9.2 → 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.2
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"
@@ -0,0 +1,70 @@
1
+ {"major_axis_length": {
2
+ "standard_name": "major_axis_length",
3
+ "long_name": "The length of the major axis of the ellipse that has the same normalized second central moments as the region",
4
+ "units": "micrometer",
5
+ "calculation_method": "Computed using skimage.measure.regionprops (axis_major_length)",
6
+ "pyopia_process_level": 1},
7
+ "minor_axis_length": {
8
+ "standard_name": "minor_axis_length",
9
+ "long_name": "The length of the minor axis of the ellipse that has the same normalized second central moments as the region",
10
+ "units": "micrometer",
11
+ "calculation_method": "Computed using skimage.measure.regionprops (axis_minor_length)",
12
+ "pyopia_process_level": 1},
13
+ "equivalent_diameter": {
14
+ "standard_name": "equivalent_circular_diameter",
15
+ "long_name": "Diameter of a circle with the same area as the particle",
16
+ "units": "micrometer",
17
+ "calculation_method": "Computed using skimage.measure.regionprops (equivalent_diameter)",
18
+ "pyopia_process_level": 1},
19
+ "minr": {
20
+ "standard_name": "minimum_row_index",
21
+ "long_name": "Minimum row index of the particle bounding box",
22
+ "units": "pixels",
23
+ "calculation_method": "Extracted from skimage.measure.regionprops (bbox[0])",
24
+ "pyopia_process_level": 1},
25
+ "maxr": {
26
+ "standard_name": "maximum_row_index",
27
+ "long_name": "Maximum row index of the particle bounding box",
28
+ "units": "pixels",
29
+ "calculation_method": "Extracted from skimage.measure.regionprops (bbox[2])",
30
+ "pyopia_process_level": 1},
31
+ "minc": {
32
+ "standard_name": "minimum_column_index",
33
+ "long_name": "Minimum column index of the particle bounding box",
34
+ "units": "pixels",
35
+ "calculation_method": "Extracted from skimage.measure.regionprops (bbox[1])",
36
+ "pyopia_process_level": 1},
37
+ "maxc": {
38
+ "standard_name": "maximum_column_index",
39
+ "long_name": "Maximum column index of the particle bounding box",
40
+ "units": "pixels",
41
+ "calculation_method": "Extracted from skimage.measure.regionprops (bbox[3])",
42
+ "pyopia_process_level": 1},
43
+ "saturation": {
44
+ "standard_name": "image_saturation",
45
+ "long_name": "Percentage saturation of the image",
46
+ "units": "percent",
47
+ "calculation_method": "Computed as the percentage of the image covered by particles relative to the maximum acceptable coverage",
48
+ "pyopia_process_level": 1},
49
+ "index": {
50
+ "standard_name": "index",
51
+ "long_name": "Index of the particle in the dataset",
52
+ "units": "",
53
+ "calculation_method": "Sequential numbering of particles in the dataset",
54
+ "pyopia_process_level": 1},
55
+ "export_name": {
56
+ "standard_name": "export_name",
57
+ "long_name": "Name of the exported particle ROI file",
58
+ "units": "",
59
+ "calculation_method": "Generated during particle export",
60
+ "pyopia_process_level": 1},
61
+ "time": {
62
+ "standard_name": "time",
63
+ "long_name": "Time of particle observation",
64
+ "calculation_method": "Extracted from the timestamp of the observation",
65
+ "pyopia_process_level": 0},
66
+ "timestamp": {
67
+ "standard_name": "timestamp",
68
+ "long_name": "Timestamp of particle observation",
69
+ "calculation_method": "Recorded during particle observation",
70
+ "pyopia_process_level": 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