PyOPIA 2.12.1__tar.gz → 2.13.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 (34) hide show
  1. {pyopia-2.12.1 → pyopia-2.13.0}/PKG-INFO +20 -2
  2. {pyopia-2.12.1 → pyopia-2.13.0}/README.md +19 -1
  3. pyopia-2.13.0/pyopia/__init__.py +1 -0
  4. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/cli.py +44 -0
  5. pyopia-2.13.0/pyopia/dataexport/__init__.py +0 -0
  6. pyopia-2.13.0/pyopia/dataexport/ecotaxa.py +239 -0
  7. pyopia-2.12.1/pyopia/__init__.py +0 -1
  8. {pyopia-2.12.1 → pyopia-2.13.0}/.gitignore +0 -0
  9. {pyopia-2.12.1 → pyopia-2.13.0}/LICENSE +0 -0
  10. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/auxillarydata.py +0 -0
  11. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/background.py +0 -0
  12. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/cf_metadata.json +0 -0
  13. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/classify.py +0 -0
  14. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/exampledata.py +0 -0
  15. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/instrument/__init__.py +0 -0
  16. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/instrument/common.py +0 -0
  17. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/instrument/holo.py +0 -0
  18. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/instrument/silcam.py +0 -0
  19. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/instrument/uvp.py +0 -0
  20. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/io.py +0 -0
  21. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/metadata.py +0 -0
  22. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/pipeline.py +0 -0
  23. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/plotting.py +0 -0
  24. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/process.py +0 -0
  25. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/simulator/__init__.py +0 -0
  26. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/simulator/silcam.py +0 -0
  27. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/statistics.py +0 -0
  28. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/tests/__init__.py +0 -0
  29. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/tests/test_auxillarydata.py +0 -0
  30. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/tests/test_classify.py +0 -0
  31. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/tests/test_io.py +0 -0
  32. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/tests/test_notebooks.py +0 -0
  33. {pyopia-2.12.1 → pyopia-2.13.0}/pyopia/tests/test_pipeline.py +0 -0
  34. {pyopia-2.12.1 → pyopia-2.13.0}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyOPIA
3
- Version: 2.12.1
3
+ Version: 2.13.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
@@ -57,11 +57,29 @@ A Python Ocean Particle Image Analysis toolbox
57
57
  1) Install [uv](https://docs.astral.sh/uv/getting-started/installation)
58
58
  2) Initialize PyOPIA project with a small example image dataset and run processing
59
59
  ```bash
60
- uvx --python 3.12 --from pyopia[classification] pyopia --init-project pyopiatest --example-data
60
+ uvx --python 3.12 --from pyopia[classification] pyopia init-project pyopiatest --example-data
61
+ ```
62
+ ```bash
61
63
  cd pyopiatest
64
+ ```
65
+ ```bash
62
66
  uvx --python 3.12 --from pyopia[classification] pyopia process config.toml
63
67
  ```
64
68
  3) Inspect the processed particle statistics in the processed/ folder
69
+ 4) Merge the individual processed image STATS files into a single STATS.nc file, then create a montage of the processed STATS.nc
70
+
71
+ ```bash
72
+ uvx --python 3.12 --from pyopia[classification] pyopia merge-mfdata processed
73
+ ```
74
+
75
+ ```bash
76
+ uvx --python 3.12 --from pyopia[classification] pyopia make-montage processed\pyopiatest-STATS.nc
77
+ ```
78
+ 5) Visualise the monatge of all processed singular particle images in one
79
+ ```bash
80
+ montage.png
81
+ ```
82
+ Will show you a montage of all the processed particle images in one.
65
83
 
66
84
  See the documentation for more information on how to install and use PyOPIA.
67
85
 
@@ -8,11 +8,29 @@ A Python Ocean Particle Image Analysis toolbox
8
8
  1) Install [uv](https://docs.astral.sh/uv/getting-started/installation)
9
9
  2) Initialize PyOPIA project with a small example image dataset and run processing
10
10
  ```bash
11
- uvx --python 3.12 --from pyopia[classification] pyopia --init-project pyopiatest --example-data
11
+ uvx --python 3.12 --from pyopia[classification] pyopia init-project pyopiatest --example-data
12
+ ```
13
+ ```bash
12
14
  cd pyopiatest
15
+ ```
16
+ ```bash
13
17
  uvx --python 3.12 --from pyopia[classification] pyopia process config.toml
14
18
  ```
15
19
  3) Inspect the processed particle statistics in the processed/ folder
20
+ 4) Merge the individual processed image STATS files into a single STATS.nc file, then create a montage of the processed STATS.nc
21
+
22
+ ```bash
23
+ uvx --python 3.12 --from pyopia[classification] pyopia merge-mfdata processed
24
+ ```
25
+
26
+ ```bash
27
+ uvx --python 3.12 --from pyopia[classification] pyopia make-montage processed\pyopiatest-STATS.nc
28
+ ```
29
+ 5) Visualise the monatge of all processed singular particle images in one
30
+ ```bash
31
+ montage.png
32
+ ```
33
+ Will show you a montage of all the processed particle images in one.
16
34
 
17
35
  See the documentation for more information on how to install and use PyOPIA.
18
36
 
@@ -0,0 +1 @@
1
+ __version__ = "2.13.0"
@@ -41,6 +41,7 @@ import pyopia.statistics
41
41
  import pyopia.auxillarydata
42
42
  import pyopia.exampledata
43
43
  import pyopia.metadata
44
+ import pyopia.dataexport.ecotaxa
44
45
 
45
46
  app = typer.Typer()
46
47
 
@@ -473,6 +474,49 @@ def make_montage(
473
474
  plt.savefig(output_filename, dpi=300, bbox_inches="tight")
474
475
 
475
476
 
477
+ @app.command()
478
+ def export_to_ecotaxa(
479
+ stats_filename: pathlib.Path,
480
+ export_filename: pathlib.Path,
481
+ make_label_folders: bool = False,
482
+ filter_variable: Annotated[Tuple[str, float, float], typer.Option()] = [
483
+ None,
484
+ None,
485
+ None,
486
+ ],
487
+ ):
488
+ """EcoTaxa export: Create a zip file with particle images and statistics csv
489
+
490
+ Parameters
491
+ ----------
492
+ config_filename : pathlib.Path
493
+ Config filename
494
+ output_filename: pathlib.Path
495
+ Name of zip file to contain exported particle images and statistics
496
+ make_label_folders : bool, optional, default False
497
+ If True, store particle images in sub-folders with label names.
498
+ NB: This must be False to create an EcoTaxa compatible zip file.
499
+ filter_variable: list
500
+ Variables to filter on (name, min, max), e.g. ['depth', 5, None]
501
+ """
502
+ print("[blue]LOAD STATS")
503
+ xstats = pyopia.io.load_stats(str(stats_filename))
504
+
505
+ # Filter the stats
506
+ if filter_variable[0] is not None:
507
+ print(filter_variable)
508
+ xstats = xstats.where(
509
+ (xstats[filter_variable[0]] <= filter_variable[2])
510
+ & (xstats[filter_variable[0]] >= filter_variable[1])
511
+ ).dropna(dim="index")
512
+
513
+ print("[blue]CREATING ECOTAXA BUNDLE")
514
+ ecotaxa_export = pyopia.dataexport.ecotaxa.EcotaxaExporter()
515
+ ecotaxa_export.create_bundle(
516
+ xstats, export_filename, make_label_folders=make_label_folders
517
+ )
518
+
519
+
476
520
  def process_file_list(file_list, pipeline_config, c):
477
521
  """Run a PyOPIA processing pipeline for a chuncked list of files based on a given config.toml
478
522
 
File without changes
@@ -0,0 +1,239 @@
1
+ """
2
+ Create data bundle from PyOPIA stats that can be imported into EcoTaxa.
3
+
4
+ EcoTaxa: https://ecotaxa.obs-vlfr.fr/
5
+ """
6
+
7
+ import numpy as np
8
+ import pyopia
9
+ import pyopia.io
10
+ import pyopia.statistics
11
+ import zipfile
12
+ import io
13
+ import xarray as xr
14
+ import pandas as pd
15
+ import matplotlib.pyplot as plt
16
+ from typing import Optional
17
+ from pathlib import Path
18
+ from tqdm.rich import tqdm
19
+
20
+ # Define the mapping from EcoTaxa variable names to PyOPIA stats column names.
21
+ # The type expected by EcoTaxa is also defined.
22
+ # Direct mapping from PyOPIA stats row values (statsrows) is possible in many cases.
23
+ # Where no direct mapping from a single PyOPIA stats variable exists, a combination
24
+ # or retrieval of other relevant metadata is used to construct the EcoTaxa value.
25
+ _ecotaxa_dict = {
26
+ "img_file_name": ("export_name", "str"), # source: statsrow
27
+ "img_rank": ("img_rank", "float"), # source: we will make it exist
28
+ "object_id": ("export_name", "str"), # source: statsrow
29
+ "object_lat": (
30
+ "latitude",
31
+ "float",
32
+ ), # needs to be in decimal degrees, source: auxillary data
33
+ "object_lon": (
34
+ "longitude",
35
+ "float",
36
+ ), # needs to be in decimal degrees, source: auxillary data
37
+ "object_date": (
38
+ "timestamp",
39
+ "str",
40
+ ), # UTC, format: YYYYMMDD, source: statsrow timestamp.dt.strftime(%Y%m%d)
41
+ "object_time": (
42
+ "timestamp",
43
+ "str",
44
+ ), # UTC, format: HHMMSS, source: statsrow timestamp.dt.strftime(%H%M%S)
45
+ "object_depth_min": ("depth", "float"), # source: statsrow
46
+ "object_depth_max": ("depth", "float"), # source: statsrow
47
+ "object_major": ("major_axis_length", "float"), # source: statsrow
48
+ "object_minor": ("minor_axis_length", "float"), # source: statsrow
49
+ "object_circ.": ("equivalent_diameter", "float"), # source: statsrow
50
+ "process_id": (
51
+ "process_id",
52
+ "float",
53
+ ), # If missing will be added by EcoTaxa
54
+ "process_img_software_version": ("pyopia.__version__", "str"),
55
+ "process_img_resolution": ("process_img_resolution", "float"),
56
+ "process_particle_pixel_size_?m": (
57
+ "pixel_size",
58
+ "float",
59
+ ), # Needs to be converted from pixels to micrometers, the ? is a choice of EcoTaxa
60
+ "process_date": (
61
+ "process_date",
62
+ "str",
63
+ ), # Added by custom step in create_bundle, from Modified time in PyOPIA netcdf (UTC)
64
+ "process_time": (
65
+ "process_time",
66
+ "str",
67
+ ), # Same as process_date. datetime.now(timezone.utc).strftime("%H%M%S").
68
+ "acq_id": ("acq_id", "float"), # If missing will be added by EcoTaxa
69
+ "sample_id": ("sample_id", "float"), # If missing will be added by EcoTaxa
70
+ "sample_stationid": (
71
+ "sample_stationid",
72
+ "str",
73
+ ), # Will be constructed from project name and station (PyOPIA netcdf)
74
+ }
75
+
76
+ # Some columns requires formatting or transformations
77
+ _ecotaxa_formatters = {
78
+ "object_date": lambda x: x.strftime("%Y%m%d"),
79
+ "object_time": lambda x: x.strftime("%H%M%S"),
80
+ "img_file_name": lambda x: x + ".png",
81
+ }
82
+
83
+ _ecotaxa_types = {
84
+ "float": "[f]",
85
+ "str": "[t]",
86
+ }
87
+
88
+
89
+ class EcotaxaExporter:
90
+ """Export particle statistics (xstats) and images (ROIs) to a zip file for EcoTaxa import"""
91
+
92
+ def statsrow_to_ecotaxarow(self, statsrow, xstats_attrs):
93
+ """Create a row in the EcoTaxa import file from PyOPIA stats row"""
94
+ ecotaxarow = dict()
95
+ for k, (statsname, statstype) in _ecotaxa_dict.items():
96
+ formatter = _ecotaxa_formatters.get(k, lambda x: x)
97
+ if statsname in statsrow.index:
98
+ ecotaxarow[k] = formatter(statsrow.get(statsname))
99
+ elif statsname in xstats_attrs:
100
+ ecotaxarow[k] = formatter(xstats_attrs.get(statsname))
101
+ else:
102
+ ecotaxarow[k] = np.nan
103
+ # type casting
104
+ return ecotaxarow
105
+
106
+ def create_bundle(
107
+ self,
108
+ xstats: xr.Dataset,
109
+ export_filename: Path,
110
+ roi_dir: Optional[Path] = None,
111
+ make_label_folders=False,
112
+ ):
113
+ """
114
+ Create an EcoTaxa import bundle file containing particle images and stats csv.
115
+
116
+ Parameters
117
+ ----------
118
+ xstats : xr.Dataset
119
+ A dataset containing particle statistics and metadata.
120
+ export_filename : pathlib.Path
121
+ Path to the output `.zip` file that will contain the bundle.
122
+ roi_dir : pathlib.Path, optional
123
+ Directory containing particle image files to include in the bundle.
124
+ If None, the roi_dir in the xstats steps metadata will be used.
125
+ make_label_folders : bool, optional, default False
126
+ If True, store particle images in sub-folders with label names.
127
+ NB: This must be False to create an EcoTaxa compatible zip file.
128
+ Returns
129
+ -------
130
+ None
131
+ This method does not return a value. It writes a zip file.
132
+ """
133
+
134
+ # Reconstruct PyOPIA config from xstats
135
+ self.config = pyopia.io.steps_from_xstats(xstats)
136
+
137
+ self.export_filename = export_filename
138
+
139
+ # Name of particle roi directory from config, unless overridden by input argument
140
+ self.roi_dir = (
141
+ Path(self.config["steps"]["statextract"]["export_outputpath"])
142
+ if roi_dir is None
143
+ else roi_dir
144
+ )
145
+
146
+ # Create statistics DataFrame, add classification best guess info for all particles
147
+ self.df_stats_export = pyopia.statistics.add_best_guesses_to_stats(
148
+ xstats.to_pandas()
149
+ )
150
+
151
+ buffer = io.BytesIO()
152
+ with zipfile.ZipFile(self.export_filename, mode="w") as zip:
153
+ # Export particle statistics dataframe to buffer as csv
154
+ buffer.seek(0)
155
+ buffer.truncate(0)
156
+ self.df_stats_export.to_csv(buffer)
157
+
158
+ ecotaxarows = []
159
+
160
+ # Write particle statistics csv to zip file, only if labelled folders are enabled
161
+ # This file is not used by EcoTaxa import
162
+ if make_label_folders:
163
+ buffer.seek(0)
164
+ zip.writestr("particle_statistics.csv", buffer.read())
165
+
166
+ # create it outside here
167
+ for idx, (_, row) in enumerate(
168
+ tqdm(
169
+ self.df_stats_export.iterrows(), total=self.df_stats_export.shape[0]
170
+ )
171
+ ):
172
+ # Get particle image
173
+ export_name = row["export_name"]
174
+ if export_name == "not_exported":
175
+ continue
176
+ particle_image = pyopia.statistics.roi_from_export_name(
177
+ export_name, self.roi_dir
178
+ )
179
+
180
+ # Convert PyOPIA stats row to EcoTaxa import row
181
+ ecotaxarows.append(self.statsrow_to_ecotaxarow(row, xstats.attrs))
182
+
183
+ # Save the particle image to the buffer
184
+ buffer.seek(0)
185
+ buffer.truncate(0)
186
+ plt.imsave(buffer, particle_image, format="png")
187
+
188
+ # Write the buffer to the zip file with a filename
189
+ label_folder = ""
190
+ if make_label_folders:
191
+ label_folder = row["best guess"].replace("probability_", "") + "/"
192
+ buffer.seek(0)
193
+ zip.writestr(f"{label_folder}{export_name}.png", buffer.read())
194
+
195
+ # Convert export table to Pandas DataFrame
196
+ df_ecotaxa_export_table = pd.DataFrame(ecotaxarows)
197
+ df_ecotaxa_types = pd.DataFrame(
198
+ [
199
+ {
200
+ k: _ecotaxa_types[ettype]
201
+ for k, (_, ettype) in _ecotaxa_dict.items()
202
+ }
203
+ ]
204
+ )
205
+
206
+ # Add some EcoTaxa required information manually here.
207
+ # Construct from multiple PyOPIA data values or metadata as needed.
208
+ pixel_size = self.config["general"]["pixel_size"]
209
+ df_ecotaxa_export_table["process_img_software_version"] = (
210
+ f"PyOPIA {xstats.attrs['PyOPIA_version']}"
211
+ )
212
+ df_ecotaxa_export_table["img_rank"] = 0
213
+ df_ecotaxa_export_table["process_particle_pixel_size_?m"] = pixel_size
214
+ df_ecotaxa_export_table["sample_stationid"] = "-".join(
215
+ [xstats.attrs.get(el, "") for el in ["project_name", "station"]]
216
+ )
217
+ processing_datetime = pd.to_datetime(xstats.attrs["Modified"])
218
+ df_ecotaxa_export_table["process_time"] = processing_datetime.strftime(
219
+ "%H%M%S"
220
+ )
221
+ df_ecotaxa_export_table["process_date"] = processing_datetime.strftime(
222
+ "%Y%m%d"
223
+ )
224
+
225
+ for col in ["object_major", "object_minor", "object_circ."]:
226
+ df_ecotaxa_export_table[col] *= pixel_size
227
+
228
+ # Insert type defintion as first row in exported table
229
+ df_ecotaxa_export_table = pd.concat(
230
+ [df_ecotaxa_types, df_ecotaxa_export_table],
231
+ axis=0,
232
+ )
233
+
234
+ # Write to zip archive
235
+ buffer.seek(0)
236
+ buffer.truncate(0)
237
+ df_ecotaxa_export_table.to_csv(buffer, index=False, sep="\t")
238
+ buffer.seek(0)
239
+ zip.writestr("ecotaxa_particle_statistics.tsv", buffer.read())
@@ -1 +0,0 @@
1
- __version__ = "2.12.1"
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