PyOPIA 2.9.3__tar.gz → 2.11.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.11.0}/PKG-INFO +14 -6
- {pyopia-2.9.3 → pyopia-2.11.0}/README.md +7 -2
- pyopia-2.11.0/pyopia/__init__.py +1 -0
- pyopia-2.11.0/pyopia/auxillarydata.py +106 -0
- {pyopia-2.9.3 → pyopia-2.11.0}/pyopia/cf_metadata.json +8 -13
- {pyopia-2.9.3 → pyopia-2.11.0}/pyopia/classify.py +66 -30
- pyopia-2.11.0/pyopia/cli.py +571 -0
- pyopia-2.11.0/pyopia/exampledata.py +150 -0
- {pyopia-2.9.3 → pyopia-2.11.0}/pyopia/instrument/silcam.py +14 -3
- {pyopia-2.9.3 → pyopia-2.11.0}/pyopia/io.py +88 -19
- pyopia-2.11.0/pyopia/metadata.py +46 -0
- pyopia-2.11.0/pyopia/plotting.py +221 -0
- pyopia-2.11.0/pyopia/tests/test_auxillarydata.py +102 -0
- {pyopia-2.9.3 → pyopia-2.11.0}/pyopia/tests/test_classify.py +88 -63
- {pyopia-2.9.3 → pyopia-2.11.0}/pyopia/tests/test_io.py +5 -1
- pyopia-2.11.0/pyopia/tests/test_notebooks.py +26 -0
- {pyopia-2.9.3 → pyopia-2.11.0}/pyproject.toml +6 -3
- pyopia-2.9.3/pyopia/__init__.py +0 -1
- pyopia-2.9.3/pyopia/cli.py +0 -309
- 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.11.0}/.gitignore +0 -0
- {pyopia-2.9.3 → pyopia-2.11.0}/LICENSE +0 -0
- {pyopia-2.9.3 → pyopia-2.11.0}/pyopia/background.py +0 -0
- {pyopia-2.9.3 → pyopia-2.11.0}/pyopia/instrument/__init__.py +0 -0
- {pyopia-2.9.3 → pyopia-2.11.0}/pyopia/instrument/common.py +0 -0
- {pyopia-2.9.3 → pyopia-2.11.0}/pyopia/instrument/holo.py +0 -0
- {pyopia-2.9.3 → pyopia-2.11.0}/pyopia/instrument/uvp.py +0 -0
- {pyopia-2.9.3 → pyopia-2.11.0}/pyopia/pipeline.py +0 -0
- {pyopia-2.9.3 → pyopia-2.11.0}/pyopia/process.py +0 -0
- {pyopia-2.9.3 → pyopia-2.11.0}/pyopia/simulator/__init__.py +0 -0
- {pyopia-2.9.3 → pyopia-2.11.0}/pyopia/simulator/silcam.py +0 -0
- {pyopia-2.9.3 → pyopia-2.11.0}/pyopia/statistics.py +0 -0
- {pyopia-2.9.3 → pyopia-2.11.0}/pyopia/tests/__init__.py +0 -0
- {pyopia-2.9.3 → pyopia-2.11.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.11.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,14 +22,16 @@ 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
|
|
28
|
+
Requires-Dist: pydantic>=2.11.7
|
|
27
29
|
Requires-Dist: pytest-error-for-skips<3,>=2.0.2
|
|
28
30
|
Requires-Dist: pytest>=7.2.0
|
|
29
31
|
Requires-Dist: readthedocs-sphinx-search<0.4,>=0.3.2
|
|
30
32
|
Requires-Dist: scikit-image<0.25,>=0.24.0
|
|
31
33
|
Requires-Dist: scipy<2,>=1.11.2
|
|
34
|
+
Requires-Dist: seaborn>=0.13.2
|
|
32
35
|
Requires-Dist: sphinx-copybutton<0.6,>=0.5.2
|
|
33
36
|
Requires-Dist: sphinx-rtd-theme>=0.5.0
|
|
34
37
|
Requires-Dist: sphinx-togglebutton<0.4,>=0.3.2
|
|
@@ -40,8 +43,8 @@ Requires-Dist: typer[all]<0.10,>=0.9.0
|
|
|
40
43
|
Requires-Dist: urllib3<2.0
|
|
41
44
|
Requires-Dist: xarray<2024,>=2023.12.0
|
|
42
45
|
Provides-Extra: classification
|
|
43
|
-
Requires-Dist: keras==3.
|
|
44
|
-
Requires-Dist: tensorflow
|
|
46
|
+
Requires-Dist: keras==3.9.1; extra == 'classification'
|
|
47
|
+
Requires-Dist: tensorflow>=2.19.0; extra == 'classification'
|
|
45
48
|
Description-Content-Type: text/markdown
|
|
46
49
|
|
|
47
50
|
PyOPIA
|
|
@@ -52,10 +55,15 @@ A Python Ocean Particle Image Analysis toolbox
|
|
|
52
55
|
# Quick tryout of PyOPIA
|
|
53
56
|
|
|
54
57
|
1) Install [uv](https://docs.astral.sh/uv/getting-started/installation)
|
|
55
|
-
2)
|
|
58
|
+
2) Initialize PyOPIA project with a small example image dataset and run processing
|
|
56
59
|
```bash
|
|
57
|
-
|
|
60
|
+
uvx --python 3.12 --from pyopia[classification] pyopia --init-project pyopiatest --example-data
|
|
61
|
+
cd pyopiatest
|
|
62
|
+
uvx --python 3.12 --from pyopia[classification] pyopia process config.toml
|
|
58
63
|
```
|
|
64
|
+
3) Inspect the processed particle statistics in the processed/ folder
|
|
65
|
+
|
|
66
|
+
See the documentation for more information on how to install and use PyOPIA.
|
|
59
67
|
|
|
60
68
|
# Documentation:
|
|
61
69
|
|
|
@@ -6,10 +6,15 @@ A Python Ocean Particle Image Analysis toolbox
|
|
|
6
6
|
# Quick tryout of PyOPIA
|
|
7
7
|
|
|
8
8
|
1) Install [uv](https://docs.astral.sh/uv/getting-started/installation)
|
|
9
|
-
2)
|
|
9
|
+
2) Initialize PyOPIA project with a small example image dataset and run processing
|
|
10
10
|
```bash
|
|
11
|
-
|
|
11
|
+
uvx --python 3.12 --from pyopia[classification] pyopia --init-project pyopiatest --example-data
|
|
12
|
+
cd pyopiatest
|
|
13
|
+
uvx --python 3.12 --from pyopia[classification] pyopia process config.toml
|
|
12
14
|
```
|
|
15
|
+
3) Inspect the processed particle statistics in the processed/ folder
|
|
16
|
+
|
|
17
|
+
See the documentation for more information on how to install and use PyOPIA.
|
|
13
18
|
|
|
14
19
|
# Documentation:
|
|
15
20
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.11.0"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import logging
|
|
3
|
+
import xarray as xr
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger()
|
|
6
|
+
|
|
7
|
+
AUXILLARY_DATA_FILE_TEMPLATE = """% COMMENT LINE: PLEASE UPDATE THIS FILE WITH PROJECT RELEVANT DATA. EACH COLUMN WILL BECOME A NETCDF VARIABLE.
|
|
8
|
+
% COMMENT LINE: ONE LINE PER MEASUREMENT, TIME IS INTERPOLATED TO IMAGE DATA TIMES IN PYOPIA. FOLLOWING LINES ARE UNITS, DESCRIPTION AND VARIABLE NAME.
|
|
9
|
+
,metres,degC
|
|
10
|
+
Time of measurement,Depth at measurement location,Temperature at measurement location
|
|
11
|
+
time,depth,temperature
|
|
12
|
+
2022-06-08T18:40:00.00000,0.0,5.0
|
|
13
|
+
2022-06-08T18:41:00.00000,5.0,6.0
|
|
14
|
+
2022-06-08T18:42:00.00000,10.0,7.0
|
|
15
|
+
2022-06-08T18:43:00.00000,20.0,8.0
|
|
16
|
+
""" # noqa: E501
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AuxillaryData:
|
|
20
|
+
"""
|
|
21
|
+
Handle auxillary data for PyOPIA particle statistics file.
|
|
22
|
+
|
|
23
|
+
Auxillary data variables may include (image) depth, longitude, latitude, etc.
|
|
24
|
+
This class parses a well-defined defined .csv input format, see example below.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
auxillary_data_path : str
|
|
29
|
+
Path to auxillary data files .csv creates by the enduser
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
Example of auxillary data file
|
|
33
|
+
------------------------------
|
|
34
|
+
% COMMENT LINE:
|
|
35
|
+
% COMMENT LINE:
|
|
36
|
+
,m,degC,psu
|
|
37
|
+
Time of measurement,Depth at measurement location,Temperature at measurement location
|
|
38
|
+
time,depth,temperature,salinity
|
|
39
|
+
2025-03-19T16:59:29.950729,0.0,5.0,34
|
|
40
|
+
2025-03-19T17:59:29.950729,0.0,5.0,34
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
Note
|
|
44
|
+
----
|
|
45
|
+
Each column will become a netCDF variable
|
|
46
|
+
The two first lines are comments, and are ignored
|
|
47
|
+
Note that the third and fourth rows contain units and description for each variable
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, auxillary_data_path=None):
|
|
51
|
+
self.auxillary_data_path = auxillary_data_path
|
|
52
|
+
|
|
53
|
+
# Create empty dataframe for cases where no file was specified, or an error occured reading it
|
|
54
|
+
self.auxillary_data = pd.DataFrame(index=pd.Index([], name="time")).to_xarray()
|
|
55
|
+
if auxillary_data_path is not None:
|
|
56
|
+
try:
|
|
57
|
+
self.auxillary_data = self.load_auxillary_data(auxillary_data_path)
|
|
58
|
+
except RuntimeError as e:
|
|
59
|
+
print(f"Failed to load auxillary data from file: {self.auxillary_data}")
|
|
60
|
+
logging.error(
|
|
61
|
+
f"Failed to load auxillary data from file: {self.auxillary_data}"
|
|
62
|
+
)
|
|
63
|
+
logging.error(e)
|
|
64
|
+
|
|
65
|
+
def load_auxillary_data(self, auxillary_data_path):
|
|
66
|
+
"""Load and format uxillary data from .csv file"""
|
|
67
|
+
|
|
68
|
+
# Load in the auxillary data file
|
|
69
|
+
auxillary_data = pd.read_csv(auxillary_data_path, skiprows=4)
|
|
70
|
+
|
|
71
|
+
# Load units and description rows
|
|
72
|
+
units = pd.read_csv(auxillary_data_path, skiprows=2, nrows=0).columns
|
|
73
|
+
long_names = pd.read_csv(auxillary_data_path, skiprows=3, nrows=0).columns
|
|
74
|
+
|
|
75
|
+
# Set time as the index and make sure its type is datetime64[ns]
|
|
76
|
+
auxillary_data["time"] = auxillary_data["time"].astype("datetime64[ns]")
|
|
77
|
+
auxillary_data = auxillary_data.set_index("time")
|
|
78
|
+
|
|
79
|
+
# Transform into xarray, add units
|
|
80
|
+
auxillary_data = auxillary_data.to_xarray()
|
|
81
|
+
for i, col in enumerate(auxillary_data.data_vars): # Iterate over each column
|
|
82
|
+
auxillary_data[col].attrs["units"] = units[i + 1]
|
|
83
|
+
auxillary_data[col].attrs["long_name"] = long_names[i + 1]
|
|
84
|
+
|
|
85
|
+
logging.info(auxillary_data)
|
|
86
|
+
|
|
87
|
+
return auxillary_data
|
|
88
|
+
|
|
89
|
+
def add_auxillary_data_to_xstats(self, xstats):
|
|
90
|
+
"""Add auxillary data to a PyOPIA xstats object"""
|
|
91
|
+
logging.info("Adding auxillary data to xstats and storing to new file")
|
|
92
|
+
|
|
93
|
+
# Add each auxillary data variable to xstats, interpolated to xstats times
|
|
94
|
+
for (
|
|
95
|
+
data_var
|
|
96
|
+
) in self.auxillary_data.data_vars: # Iterate over each data variable
|
|
97
|
+
xstats[data_var] = xr.DataArray(
|
|
98
|
+
data=self.auxillary_data[data_var]
|
|
99
|
+
.astype(float)
|
|
100
|
+
.interp(time=xstats["timestamp"]),
|
|
101
|
+
dims=xstats.dims,
|
|
102
|
+
coords=xstats.coords,
|
|
103
|
+
attrs=self.auxillary_data[data_var].attrs,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return xstats
|
|
@@ -1,49 +1,49 @@
|
|
|
1
1
|
{"major_axis_length": {
|
|
2
2
|
"standard_name": "major_axis_length",
|
|
3
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": "
|
|
4
|
+
"units": "Pixels",
|
|
5
5
|
"calculation_method": "Computed using skimage.measure.regionprops (axis_major_length)",
|
|
6
6
|
"pyopia_process_level": 1},
|
|
7
7
|
"minor_axis_length": {
|
|
8
8
|
"standard_name": "minor_axis_length",
|
|
9
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": "
|
|
10
|
+
"units": "Pixels",
|
|
11
11
|
"calculation_method": "Computed using skimage.measure.regionprops (axis_minor_length)",
|
|
12
12
|
"pyopia_process_level": 1},
|
|
13
13
|
"equivalent_diameter": {
|
|
14
14
|
"standard_name": "equivalent_circular_diameter",
|
|
15
15
|
"long_name": "Diameter of a circle with the same area as the particle",
|
|
16
|
-
"units": "
|
|
16
|
+
"units": "Pixels",
|
|
17
17
|
"calculation_method": "Computed using skimage.measure.regionprops (equivalent_diameter)",
|
|
18
18
|
"pyopia_process_level": 1},
|
|
19
19
|
"minr": {
|
|
20
20
|
"standard_name": "minimum_row_index",
|
|
21
21
|
"long_name": "Minimum row index of the particle bounding box",
|
|
22
|
-
"units": "
|
|
22
|
+
"units": "Pixels",
|
|
23
23
|
"calculation_method": "Extracted from skimage.measure.regionprops (bbox[0])",
|
|
24
24
|
"pyopia_process_level": 1},
|
|
25
25
|
"maxr": {
|
|
26
26
|
"standard_name": "maximum_row_index",
|
|
27
27
|
"long_name": "Maximum row index of the particle bounding box",
|
|
28
|
-
"units": "
|
|
28
|
+
"units": "Pixels",
|
|
29
29
|
"calculation_method": "Extracted from skimage.measure.regionprops (bbox[2])",
|
|
30
30
|
"pyopia_process_level": 1},
|
|
31
31
|
"minc": {
|
|
32
32
|
"standard_name": "minimum_column_index",
|
|
33
33
|
"long_name": "Minimum column index of the particle bounding box",
|
|
34
|
-
"units": "
|
|
34
|
+
"units": "Pixels",
|
|
35
35
|
"calculation_method": "Extracted from skimage.measure.regionprops (bbox[1])",
|
|
36
36
|
"pyopia_process_level": 1},
|
|
37
37
|
"maxc": {
|
|
38
38
|
"standard_name": "maximum_column_index",
|
|
39
39
|
"long_name": "Maximum column index of the particle bounding box",
|
|
40
|
-
"units": "
|
|
40
|
+
"units": "Pixels",
|
|
41
41
|
"calculation_method": "Extracted from skimage.measure.regionprops (bbox[3])",
|
|
42
42
|
"pyopia_process_level": 1},
|
|
43
43
|
"saturation": {
|
|
44
44
|
"standard_name": "image_saturation",
|
|
45
45
|
"long_name": "Percentage saturation of the image",
|
|
46
|
-
"units": "
|
|
46
|
+
"units": "Percent",
|
|
47
47
|
"calculation_method": "Computed as the percentage of the image covered by particles relative to the maximum acceptable coverage",
|
|
48
48
|
"pyopia_process_level": 1},
|
|
49
49
|
"index": {
|
|
@@ -58,11 +58,6 @@
|
|
|
58
58
|
"units": "",
|
|
59
59
|
"calculation_method": "Generated during particle export",
|
|
60
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
61
|
"timestamp": {
|
|
67
62
|
"standard_name": "timestamp",
|
|
68
63
|
"long_name": "Timestamp of particle observation",
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
2
|
Module containing tools for classifying particle ROIs
|
|
3
|
-
|
|
3
|
+
"""
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
import hashlib
|
|
6
7
|
import numpy as np
|
|
7
8
|
import pandas as pd
|
|
8
9
|
import logging
|
|
10
|
+
from skimage.exposure import rescale_intensity
|
|
11
|
+
|
|
9
12
|
logger = logging.getLogger()
|
|
10
13
|
|
|
11
14
|
# import tensorflow here. It must be imported on the processor where it will be used!
|
|
@@ -15,16 +18,16 @@ try:
|
|
|
15
18
|
from tensorflow import keras
|
|
16
19
|
import tensorflow as tf
|
|
17
20
|
except ImportError:
|
|
18
|
-
info_str =
|
|
19
|
-
info_str +=
|
|
20
|
-
info_str +=
|
|
21
|
-
info_str +=
|
|
22
|
-
info_str +=
|
|
21
|
+
info_str = "ERROR: Could not import Keras. Classify will not work"
|
|
22
|
+
info_str += " until you install tensorflow.\n"
|
|
23
|
+
info_str += "Use: pip install pyopia[classification]\n"
|
|
24
|
+
info_str += " or: pip install pyopia[classification-arm64]"
|
|
25
|
+
info_str += " for tensorflow-macos (silicon chips)"
|
|
23
26
|
raise ImportError(info_str)
|
|
24
27
|
|
|
25
28
|
|
|
26
|
-
class Classify
|
|
27
|
-
|
|
29
|
+
class Classify:
|
|
30
|
+
"""
|
|
28
31
|
A classifier class for PyOPIA workflow.
|
|
29
32
|
This is intended as a parent class that can be used as a template for flexible classification methods
|
|
30
33
|
|
|
@@ -32,6 +35,10 @@ class Classify():
|
|
|
32
35
|
----------
|
|
33
36
|
model_path : str
|
|
34
37
|
path to particle-classifier e.g. '/testdata/model_name/particle_classifier.keras'
|
|
38
|
+
normalize_intensity : bool
|
|
39
|
+
Scale input image intensity to [0-1] range before classification
|
|
40
|
+
correct_whitebalance : bool
|
|
41
|
+
Perform whitebalance correction before classification
|
|
35
42
|
|
|
36
43
|
Example
|
|
37
44
|
-------
|
|
@@ -71,23 +78,36 @@ class Classify():
|
|
|
71
78
|
import pyopia.exampledata
|
|
72
79
|
model_path = exampledata.get_example_model()
|
|
73
80
|
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
model_path=None,
|
|
86
|
+
normalize_intensity=True,
|
|
87
|
+
correct_whitebalance=False,
|
|
88
|
+
):
|
|
76
89
|
self.model_path = model_path
|
|
77
90
|
self.load_model()
|
|
78
91
|
|
|
79
92
|
# Get config for image resizing from the model
|
|
80
|
-
_, self.img_height, self.img_width, _ = self.model.get_config()[
|
|
81
|
-
|
|
93
|
+
_, self.img_height, self.img_width, _ = self.model.get_config()["layers"][0][
|
|
94
|
+
"config"
|
|
95
|
+
]["batch_shape"]
|
|
96
|
+
self.pad_to_aspect_ratio = getattr(
|
|
97
|
+
self.model.layers[0], "pad_to_aspect_ratio", False
|
|
98
|
+
)
|
|
82
99
|
|
|
83
100
|
# Enable this to perform whitebalance correction in the preprocessing step
|
|
84
|
-
self.correct_whitebalance =
|
|
101
|
+
self.correct_whitebalance = correct_whitebalance
|
|
102
|
+
|
|
103
|
+
# Enable this to rescale intensity in the preprocessing step
|
|
104
|
+
self.normalize_intensity = normalize_intensity
|
|
85
105
|
|
|
86
106
|
def __call__(self):
|
|
87
107
|
return self
|
|
88
108
|
|
|
89
109
|
def load_model(self):
|
|
90
|
-
|
|
110
|
+
"""
|
|
91
111
|
Load a trained Keras model into the Classify class.
|
|
92
112
|
|
|
93
113
|
Parameters
|
|
@@ -96,34 +116,41 @@ class Classify():
|
|
|
96
116
|
loaded Keras model
|
|
97
117
|
class_names: list
|
|
98
118
|
names for the model output classes
|
|
99
|
-
|
|
119
|
+
"""
|
|
100
120
|
model_path = self.model_path
|
|
101
121
|
|
|
102
|
-
os.environ[
|
|
122
|
+
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
|
|
103
123
|
keras.backend.clear_session()
|
|
104
124
|
|
|
105
125
|
# Instantiate Keras model from file
|
|
106
126
|
path, filename = os.path.split(model_path)
|
|
107
127
|
self.model = keras.models.load_model(model_path)
|
|
108
128
|
|
|
129
|
+
# Create a hash of the model weights file
|
|
130
|
+
with open(model_path, "rb") as f:
|
|
131
|
+
digest = hashlib.file_digest(f, "sha256")
|
|
132
|
+
self.model_hash = digest.hexdigest()
|
|
133
|
+
|
|
109
134
|
# Try to create model output class name list from last model layer name
|
|
110
135
|
class_labels = None
|
|
111
136
|
try:
|
|
112
|
-
class_labels = self.model.layers[-1].name.split(
|
|
137
|
+
class_labels = self.model.layers[-1].name.split(".")
|
|
113
138
|
except: # noqa E722
|
|
114
|
-
logger.info(
|
|
139
|
+
logger.info(
|
|
140
|
+
"Could not get class names from model layer name, reverting to old method with header file."
|
|
141
|
+
)
|
|
115
142
|
|
|
116
143
|
# If we could not create correct class names above, revert to old header file method
|
|
117
144
|
expected_class_number = self.model.layers[-1].output.shape[1]
|
|
118
145
|
if class_labels is None or len(class_labels) != expected_class_number:
|
|
119
|
-
header = pd.read_csv(os.path.join(path,
|
|
146
|
+
header = pd.read_csv(os.path.join(path, "header.tfl.txt"))
|
|
120
147
|
class_labels = header.columns
|
|
121
148
|
|
|
122
149
|
self.class_labels = class_labels
|
|
123
150
|
logger.info(self.class_labels)
|
|
124
151
|
|
|
125
152
|
def preprocessing(self, img_input):
|
|
126
|
-
|
|
153
|
+
"""
|
|
127
154
|
Preprocess ROI ready for prediction. example here based on the pysilcam network setup
|
|
128
155
|
|
|
129
156
|
Parameters
|
|
@@ -135,7 +162,7 @@ class Classify():
|
|
|
135
162
|
-------
|
|
136
163
|
img_preprocessed : float
|
|
137
164
|
A particle ROI with range 0.-255., corrected and preprocessed, ready for prediction
|
|
138
|
-
|
|
165
|
+
"""
|
|
139
166
|
|
|
140
167
|
whitebalanced = img_input.astype(np.float64)
|
|
141
168
|
|
|
@@ -143,10 +170,16 @@ class Classify():
|
|
|
143
170
|
if self.correct_whitebalance:
|
|
144
171
|
p = 99
|
|
145
172
|
for c in range(3):
|
|
146
|
-
whitebalanced[:, :, c] += (p/100) - np.percentile(
|
|
173
|
+
whitebalanced[:, :, c] += (p / 100) - np.percentile(
|
|
174
|
+
whitebalanced[:, :, c], p
|
|
175
|
+
)
|
|
147
176
|
whitebalanced[whitebalanced > 1] = 1
|
|
148
177
|
whitebalanced[whitebalanced < 0] = 0
|
|
149
178
|
|
|
179
|
+
# Rescale intensity to 0-1 range
|
|
180
|
+
if self.normalize_intensity:
|
|
181
|
+
whitebalanced = rescale_intensity(whitebalanced)
|
|
182
|
+
|
|
150
183
|
# convert back to 0-255 scaling (because of this layer in the network:
|
|
151
184
|
# layers.Rescaling(1./255, input_shape=(img_height, img_width, 3)))
|
|
152
185
|
# This is useful because it allows training to use tf.keras.utils.image_dataset_from_directory,
|
|
@@ -154,9 +187,12 @@ class Classify():
|
|
|
154
187
|
img = keras.utils.img_to_array(whitebalanced * 255)
|
|
155
188
|
|
|
156
189
|
# resize to match the dimentions expected by the network
|
|
157
|
-
img = tf.image.resize(
|
|
158
|
-
|
|
159
|
-
|
|
190
|
+
img = tf.image.resize(
|
|
191
|
+
img,
|
|
192
|
+
[self.img_height, self.img_width],
|
|
193
|
+
method=tf.image.ResizeMethod.BILINEAR,
|
|
194
|
+
preserve_aspect_ratio=self.pad_to_aspect_ratio,
|
|
195
|
+
)
|
|
160
196
|
|
|
161
197
|
img_array = tf.keras.utils.img_to_array(img)
|
|
162
198
|
img_preprocessed = tf.expand_dims(img_array, 0) # Create a batch
|
|
@@ -164,7 +200,7 @@ class Classify():
|
|
|
164
200
|
|
|
165
201
|
@tf.function
|
|
166
202
|
def predict(self, img_preprocessed):
|
|
167
|
-
|
|
203
|
+
"""
|
|
168
204
|
Use tensorflow model to classify particles. example here based on the pysilcam network setup.
|
|
169
205
|
|
|
170
206
|
Parameters
|
|
@@ -177,14 +213,14 @@ class Classify():
|
|
|
177
213
|
-------
|
|
178
214
|
prediction : array
|
|
179
215
|
The probability of the roi belonging to each class
|
|
180
|
-
|
|
216
|
+
"""
|
|
181
217
|
|
|
182
218
|
prediction = self.model(img_preprocessed, training=False)
|
|
183
219
|
prediction = tf.nn.softmax(prediction[0])
|
|
184
220
|
return prediction
|
|
185
221
|
|
|
186
222
|
def proc_predict(self, img_input):
|
|
187
|
-
|
|
223
|
+
"""
|
|
188
224
|
Run pre-processing (:meth:`Classify.preprocessing`) and prediction (:meth:`Classify.predict`)
|
|
189
225
|
using tensorflow model to classify particles. example here based on the pysilcam network setup.
|
|
190
226
|
|
|
@@ -197,7 +233,7 @@ class Classify():
|
|
|
197
233
|
-------
|
|
198
234
|
prediction : array
|
|
199
235
|
The probability of the roi belonging to each class
|
|
200
|
-
|
|
236
|
+
"""
|
|
201
237
|
img_preprocessed = self.preprocessing(img_input)
|
|
202
238
|
prediction = self.predict(img_preprocessed)
|
|
203
239
|
|