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.
- {pyopia-2.9.3 → pyopia-2.10.0}/PKG-INFO +6 -4
- pyopia-2.10.0/pyopia/__init__.py +1 -0
- {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/classify.py +60 -30
- pyopia-2.10.0/pyopia/exampledata.py +150 -0
- pyopia-2.10.0/pyopia/plotting.py +221 -0
- {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/tests/test_classify.py +88 -63
- pyopia-2.10.0/pyopia/tests/test_notebooks.py +26 -0
- {pyopia-2.9.3 → pyopia-2.10.0}/pyproject.toml +5 -3
- pyopia-2.9.3/pyopia/__init__.py +0 -1
- pyopia-2.9.3/pyopia/exampledata.py +0 -143
- pyopia-2.9.3/pyopia/plotting.py +0 -51
- pyopia-2.9.3/pyopia/tests/test_notebooks.py +0 -22
- {pyopia-2.9.3 → pyopia-2.10.0}/.gitignore +0 -0
- {pyopia-2.9.3 → pyopia-2.10.0}/LICENSE +0 -0
- {pyopia-2.9.3 → pyopia-2.10.0}/README.md +0 -0
- {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/background.py +0 -0
- {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/cf_metadata.json +0 -0
- {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/cli.py +0 -0
- {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/instrument/__init__.py +0 -0
- {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/instrument/common.py +0 -0
- {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/instrument/holo.py +0 -0
- {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/instrument/silcam.py +0 -0
- {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/instrument/uvp.py +0 -0
- {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/io.py +0 -0
- {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/pipeline.py +0 -0
- {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/process.py +0 -0
- {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/simulator/__init__.py +0 -0
- {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/simulator/silcam.py +0 -0
- {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/statistics.py +0 -0
- {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/tests/__init__.py +0 -0
- {pyopia-2.9.3 → pyopia-2.10.0}/pyopia/tests/test_io.py +0 -0
- {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.
|
|
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
|
|
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.
|
|
44
|
-
Requires-Dist: tensorflow
|
|
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 =
|
|
19
|
-
info_str +=
|
|
20
|
-
info_str +=
|
|
21
|
-
info_str +=
|
|
22
|
-
info_str +=
|
|
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
|
-
|
|
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()[
|
|
81
|
-
|
|
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 =
|
|
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[
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
158
|
-
|
|
159
|
-
|
|
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 =
|
|
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,
|
|
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,
|
|
42
|
-
model_path = exampledata.get_example_model(os.path.join(tempdir,
|
|
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(
|
|
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,
|
|
59
|
+
files = glob(os.path.join(database_path, category, "*.tiff"))
|
|
60
60
|
|
|
61
|
-
assert len(files) > 50,
|
|
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.
|
|
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,
|
|
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, (
|
|
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
|
-
|
|
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,
|
|
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(
|
|
110
|
-
model_path = exampledata.get_example_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(
|
|
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,
|
|
123
|
-
print(len(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(
|
|
141
|
+
print("roi file", file)
|
|
140
142
|
return img, category
|
|
141
|
-
assert found_match == 1,
|
|
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 = {
|
|
161
|
-
|
|
162
|
-
|
|
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[
|
|
173
|
-
|
|
174
|
-
|
|
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(
|
|
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[
|
|
183
|
-
MyPipeline.data[
|
|
184
|
-
MyPipeline.data[
|
|
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[
|
|
188
|
-
|
|
189
|
-
|
|
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(
|
|
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[
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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(
|
|
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[
|
|
209
|
-
|
|
210
|
-
|
|
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(
|
|
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[
|
|
242
|
+
stats = pyopia.statistics.add_best_guesses_to_stats(MyPipeline.data["stats"])
|
|
220
243
|
|
|
221
|
-
out = [x[12:] for x in stats[
|
|
244
|
+
out = [x[12:] for x in stats["best guess"].values]
|
|
222
245
|
|
|
223
|
-
print(
|
|
224
|
-
print(
|
|
246
|
+
print("classes input", categories)
|
|
247
|
+
print("classes measured", out)
|
|
225
248
|
|
|
226
|
-
assert categories == out,
|
|
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
|
|
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.
|
|
56
|
-
"keras==3.
|
|
57
|
+
"tensorflow>=2.19.0",
|
|
58
|
+
"keras==3.9.1",
|
|
57
59
|
]
|
|
58
60
|
|
|
59
61
|
[project.urls]
|
pyopia-2.9.3/pyopia/__init__.py
DELETED
|
@@ -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
|
pyopia-2.9.3/pyopia/plotting.py
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|