PyOPIA 2.10.0__tar.gz → 2.12.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.10.0 → pyopia-2.12.0}/PKG-INFO +9 -3
- {pyopia-2.10.0 → pyopia-2.12.0}/README.md +7 -2
- pyopia-2.12.0/pyopia/__init__.py +1 -0
- pyopia-2.12.0/pyopia/auxillarydata.py +106 -0
- {pyopia-2.10.0 → pyopia-2.12.0}/pyopia/cf_metadata.json +8 -13
- {pyopia-2.10.0 → pyopia-2.12.0}/pyopia/classify.py +6 -0
- pyopia-2.12.0/pyopia/cli.py +571 -0
- pyopia-2.12.0/pyopia/instrument/common.py +150 -0
- {pyopia-2.10.0 → pyopia-2.12.0}/pyopia/instrument/silcam.py +14 -3
- {pyopia-2.10.0 → pyopia-2.12.0}/pyopia/io.py +88 -19
- pyopia-2.12.0/pyopia/metadata.py +46 -0
- pyopia-2.12.0/pyopia/tests/test_auxillarydata.py +102 -0
- {pyopia-2.10.0 → pyopia-2.12.0}/pyopia/tests/test_io.py +5 -1
- {pyopia-2.10.0 → pyopia-2.12.0}/pyproject.toml +1 -0
- pyopia-2.10.0/pyopia/__init__.py +0 -1
- pyopia-2.10.0/pyopia/cli.py +0 -309
- pyopia-2.10.0/pyopia/instrument/common.py +0 -54
- {pyopia-2.10.0 → pyopia-2.12.0}/.gitignore +0 -0
- {pyopia-2.10.0 → pyopia-2.12.0}/LICENSE +0 -0
- {pyopia-2.10.0 → pyopia-2.12.0}/pyopia/background.py +0 -0
- {pyopia-2.10.0 → pyopia-2.12.0}/pyopia/exampledata.py +0 -0
- {pyopia-2.10.0 → pyopia-2.12.0}/pyopia/instrument/__init__.py +0 -0
- {pyopia-2.10.0 → pyopia-2.12.0}/pyopia/instrument/holo.py +0 -0
- {pyopia-2.10.0 → pyopia-2.12.0}/pyopia/instrument/uvp.py +0 -0
- {pyopia-2.10.0 → pyopia-2.12.0}/pyopia/pipeline.py +0 -0
- {pyopia-2.10.0 → pyopia-2.12.0}/pyopia/plotting.py +0 -0
- {pyopia-2.10.0 → pyopia-2.12.0}/pyopia/process.py +0 -0
- {pyopia-2.10.0 → pyopia-2.12.0}/pyopia/simulator/__init__.py +0 -0
- {pyopia-2.10.0 → pyopia-2.12.0}/pyopia/simulator/silcam.py +0 -0
- {pyopia-2.10.0 → pyopia-2.12.0}/pyopia/statistics.py +0 -0
- {pyopia-2.10.0 → pyopia-2.12.0}/pyopia/tests/__init__.py +0 -0
- {pyopia-2.10.0 → pyopia-2.12.0}/pyopia/tests/test_classify.py +0 -0
- {pyopia-2.10.0 → pyopia-2.12.0}/pyopia/tests/test_notebooks.py +0 -0
- {pyopia-2.10.0 → pyopia-2.12.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.12.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
|
|
@@ -25,6 +25,7 @@ Requires-Dist: nbconvert<8,>=7.16.4
|
|
|
25
25
|
Requires-Dist: numpy>=1.24.0
|
|
26
26
|
Requires-Dist: pandas[computation]<3,>=2.1.1
|
|
27
27
|
Requires-Dist: poetry-version-plugin<0.3,>=0.2.0
|
|
28
|
+
Requires-Dist: pydantic>=2.11.7
|
|
28
29
|
Requires-Dist: pytest-error-for-skips<3,>=2.0.2
|
|
29
30
|
Requires-Dist: pytest>=7.2.0
|
|
30
31
|
Requires-Dist: readthedocs-sphinx-search<0.4,>=0.3.2
|
|
@@ -54,10 +55,15 @@ A Python Ocean Particle Image Analysis toolbox
|
|
|
54
55
|
# Quick tryout of PyOPIA
|
|
55
56
|
|
|
56
57
|
1) Install [uv](https://docs.astral.sh/uv/getting-started/installation)
|
|
57
|
-
2)
|
|
58
|
+
2) Initialize PyOPIA project with a small example image dataset and run processing
|
|
58
59
|
```bash
|
|
59
|
-
|
|
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
|
|
60
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.
|
|
61
67
|
|
|
62
68
|
# Documentation:
|
|
63
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.12.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",
|
|
@@ -3,6 +3,7 @@ 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
|
|
@@ -125,6 +126,11 @@ class Classify:
|
|
|
125
126
|
path, filename = os.path.split(model_path)
|
|
126
127
|
self.model = keras.models.load_model(model_path)
|
|
127
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
|
+
|
|
128
134
|
# Try to create model output class name list from last model layer name
|
|
129
135
|
class_labels = None
|
|
130
136
|
try:
|