PyOPIA 2.5.10__tar.gz → 2.5.12__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 (26) hide show
  1. {pyopia-2.5.10 → pyopia-2.5.12}/PKG-INFO +1 -1
  2. pyopia-2.5.12/pyopia/__init__.py +1 -0
  3. {pyopia-2.5.10 → pyopia-2.5.12}/pyopia/cli.py +16 -2
  4. {pyopia-2.5.10 → pyopia-2.5.12}/pyopia/io.py +141 -17
  5. {pyopia-2.5.10 → pyopia-2.5.12}/pyopia/process.py +6 -8
  6. pyopia-2.5.10/pyopia/__init__.py +0 -1
  7. {pyopia-2.5.10 → pyopia-2.5.12}/LICENSE +0 -0
  8. {pyopia-2.5.10 → pyopia-2.5.12}/README.md +0 -0
  9. {pyopia-2.5.10 → pyopia-2.5.12}/pyopia/background.py +0 -0
  10. {pyopia-2.5.10 → pyopia-2.5.12}/pyopia/classify.py +0 -0
  11. {pyopia-2.5.10 → pyopia-2.5.12}/pyopia/exampledata.py +0 -0
  12. {pyopia-2.5.10 → pyopia-2.5.12}/pyopia/instrument/__init__.py +0 -0
  13. {pyopia-2.5.10 → pyopia-2.5.12}/pyopia/instrument/common.py +0 -0
  14. {pyopia-2.5.10 → pyopia-2.5.12}/pyopia/instrument/holo.py +0 -0
  15. {pyopia-2.5.10 → pyopia-2.5.12}/pyopia/instrument/silcam.py +0 -0
  16. {pyopia-2.5.10 → pyopia-2.5.12}/pyopia/instrument/uvp.py +0 -0
  17. {pyopia-2.5.10 → pyopia-2.5.12}/pyopia/pipeline.py +0 -0
  18. {pyopia-2.5.10 → pyopia-2.5.12}/pyopia/plotting.py +0 -0
  19. {pyopia-2.5.10 → pyopia-2.5.12}/pyopia/simulator/__init__.py +0 -0
  20. {pyopia-2.5.10 → pyopia-2.5.12}/pyopia/simulator/silcam.py +0 -0
  21. {pyopia-2.5.10 → pyopia-2.5.12}/pyopia/statistics.py +0 -0
  22. {pyopia-2.5.10 → pyopia-2.5.12}/pyopia/tests/__init__.py +0 -0
  23. {pyopia-2.5.10 → pyopia-2.5.12}/pyopia/tests/test_classify.py +0 -0
  24. {pyopia-2.5.10 → pyopia-2.5.12}/pyopia/tests/test_notebooks.py +0 -0
  25. {pyopia-2.5.10 → pyopia-2.5.12}/pyopia/tests/test_pipeline.py +0 -0
  26. {pyopia-2.5.10 → pyopia-2.5.12}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyOPIA
3
- Version: 2.5.10
3
+ Version: 2.5.12
4
4
  Summary: A Python Ocean Particle Image Analysis toolbox.
5
5
  Home-page: https://github.com/sintef/pyopia
6
6
  Keywords: Ocean,Particles,Imaging,Measurement,Size distribution
@@ -0,0 +1 @@
1
+ __version__ = '2.5.12'
@@ -186,7 +186,8 @@ def process(config_filename: str, num_chunks: int = 1):
186
186
 
187
187
 
188
188
  @app.command()
189
- def merge_mfdata(path_to_data: str, prefix='*'):
189
+ def merge_mfdata(path_to_data: str, prefix='*', overwrite_existing_partials: bool = True,
190
+ chunk_size: int = None):
190
191
  '''Combine a multi-file directory of STATS.nc files into a single '-STATS.nc' file
191
192
  that can then be loaded with {func}`pyopia.io.load_stats`
192
193
 
@@ -198,8 +199,21 @@ def merge_mfdata(path_to_data: str, prefix='*'):
198
199
  prefix : str
199
200
  Prefix to multi-file dataset (for replacing the wildcard in '*Image-D*-STATS.nc').
200
201
  Defaults to '*'
202
+
203
+ overwrite_existing_partials : bool
204
+ Do not reprocess existing merged netcdf files for each chunk if False.
205
+ Otherwise reprocess (load) and overwrite. This can be used to restart
206
+ or continue a previous merge operation as new files become available.
207
+
208
+ chunk_size : int
209
+ Process this many files together and store as partially merged netcdf files, which
210
+ are then merged at the end. Default: None, process all files together.
201
211
  '''
202
- pyopia.io.merge_and_save_mfdataset(path_to_data, prefix=prefix)
212
+ setup_logging({'general': {}})
213
+
214
+ pyopia.io.merge_and_save_mfdataset(path_to_data, prefix=prefix,
215
+ overwrite_existing_partials=overwrite_existing_partials,
216
+ chunk_size=chunk_size)
203
217
 
204
218
 
205
219
  def setup_logging(pipeline_config):
@@ -3,7 +3,7 @@ Module containing tools for datafile and metadata handling
3
3
  '''
4
4
 
5
5
  from datetime import datetime
6
-
6
+ import numpy as np
7
7
  import h5py
8
8
  import pandas as pd
9
9
  import toml
@@ -11,6 +11,7 @@ import xarray
11
11
  import os
12
12
  from glob import glob
13
13
  import xarray as xr
14
+ from tqdm.auto import tqdm
14
15
 
15
16
  from pyopia import __version__ as pyopia_version
16
17
 
@@ -83,14 +84,21 @@ def write_stats(stats,
83
84
  existing_stats = load_stats(datafilename + '-STATS.nc')
84
85
  xstats = xarray.concat([existing_stats, xstats], 'index')
85
86
  elif not append:
87
+ # When appending, only store the last row in the image_stats DataFrame
86
88
  datafilename += ('-Image-D' +
87
89
  str(xstats['timestamp'][0].values).replace('-', '').replace(':', '').replace('.', '-'))
90
+
88
91
  encoding = setup_xstats_encoding(xstats)
89
92
  xstats.to_netcdf(datafilename + '-STATS.nc', encoding=encoding, engine=NETCDF_ENGINE, format='NETCDF4')
90
93
 
91
94
  # If we have image statistics (summary data for each raw image), add the image_stats a group
92
95
  if image_stats is not None:
93
- image_stats.to_xarray().to_netcdf(datafilename + '-STATS.nc', group='image_stats', mode='a', engine=NETCDF_ENGINE)
96
+ if append:
97
+ ximage_stats = image_stats.to_xarray()
98
+ else:
99
+ ximage_stats = image_stats.loc[[image_stats.index[-1]], :].to_xarray()
100
+
101
+ ximage_stats.to_netcdf(datafilename + '-STATS.nc', group='image_stats', mode='a', engine=NETCDF_ENGINE)
94
102
 
95
103
 
96
104
  def setup_xstats_encoding(xstats, string_vars=['export name', 'holo_filename']):
@@ -178,7 +186,7 @@ def load_stats(datafilename):
178
186
  '''
179
187
 
180
188
  if datafilename.endswith('.nc'):
181
- with xarray.open_dataset(datafilename) as xstats:
189
+ with xarray.open_dataset(datafilename, engine=NETCDF_ENGINE) as xstats:
182
190
  xstats.load()
183
191
  elif datafilename.endswith('.h5'):
184
192
  logger.warning('In future, load_stats will only take .nc files')
@@ -194,7 +202,11 @@ def load_stats(datafilename):
194
202
 
195
203
 
196
204
  def combine_stats_netcdf_files(path_to_data, prefix='*'):
197
- '''Combine a multi-file directory of STATS.nc files into a 'stats' xarray dataset created by :func:`pyopia.io.write_stats`
205
+ '''.. deprecated:: 2.4.11
206
+ :class:`pyopia.io.combine_stats_netcdf_files` will be removed in version 3.0.0, it is replaced by
207
+ :class:`pyopia.io.concat_stats_netcdf_files`.
208
+
209
+ Combine a multi-file directory of STATS.nc files into a 'stats' xarray dataset created by :func:`pyopia.io.write_stats`
198
210
  when using 'append = false'
199
211
 
200
212
  Parameters
@@ -231,7 +243,63 @@ def combine_stats_netcdf_files(path_to_data, prefix='*'):
231
243
  return xstats, image_stats
232
244
 
233
245
 
234
- def merge_and_save_mfdataset(path_to_data, prefix='*'):
246
+ def concat_stats_netcdf_files(sorted_filelist):
247
+ '''Concatenate specified list of STATS.nc files into one 'xstats' xarray dataset
248
+ created by :func:`pyopia.io.write_stats when using 'append = false'.
249
+
250
+ Existing files are first loaded and then combined, so memory usage will go up with longer file lists.
251
+
252
+ Parameters
253
+ ----------
254
+ sorted_filelist : str
255
+ List of files to be combined into single dataset
256
+
257
+ Returns
258
+ -------
259
+ xstats : xarray.Dataset or None
260
+ Particle statistics and metatdata from processing steps
261
+ image_stats : xarray.Dataset or None
262
+ Summary statistics of each raw image (including those with no particles)
263
+ '''
264
+ if len(sorted_filelist) < 1:
265
+ logger.error('No files found to concatenate, doing nothing.')
266
+ return None, None
267
+
268
+ # We load one dataset at the time into a list for later merge
269
+ datasets = []
270
+ datasets_image_stats = []
271
+
272
+ # Check if we have image statistics in first file, if not, we skip checking the rest
273
+ skip_image_stats = False
274
+ try:
275
+ with xr.open_dataset(sorted_filelist[0], group='image_stats', engine=NETCDF_ENGINE) as ds:
276
+ ds.load()
277
+ except OSError:
278
+ logger.info('Could get image_stats from netcdf files for merging, returning None for this.')
279
+ skip_image_stats = True
280
+
281
+ # Load datasets from each file into the lists
282
+ for f in tqdm(sorted_filelist, desc='Loading datasets'):
283
+ with xr.open_dataset(f, engine=NETCDF_ENGINE) as ds:
284
+ ds.load()
285
+ datasets.append(ds)
286
+
287
+ if not skip_image_stats:
288
+ with xr.open_dataset(f, group='image_stats', engine=NETCDF_ENGINE) as ds:
289
+ ds.load()
290
+ datasets_image_stats.append(ds)
291
+
292
+ # Combine the individual datasets loaded above
293
+ logging.info('Combining datasets')
294
+ xstats = xr.concat(datasets, dim='index')
295
+ image_stats = None
296
+ if not skip_image_stats:
297
+ image_stats = xr.concat(datasets_image_stats, dim='timestamp')
298
+
299
+ return xstats, image_stats
300
+
301
+
302
+ def merge_and_save_mfdataset(path_to_data, prefix='*', overwrite_existing_partials=False, chunk_size=None):
235
303
  '''Combine a multi-file directory of STATS.nc files into a single '-STATS.nc' file
236
304
  that can then be loaded with :func:`pyopia.io.load_stats`
237
305
 
@@ -243,24 +311,80 @@ def merge_and_save_mfdataset(path_to_data, prefix='*'):
243
311
  prefix : str
244
312
  Prefix to multi-file dataset (for replacing the wildcard in '*Image-D*-STATS.nc').
245
313
  Defaults to '*'
314
+
315
+ overwrite_existing_partials : bool
316
+ Do not reprocess existing merged netcdf files for each chunk if False.
317
+ Otherwise reprocess (load) and overwrite. This can be used to restart
318
+ or continue a previous merge operation as new files become available.
319
+
320
+ chunk_size : int
321
+ Number of files to be loaded and merged in each step. Produces a number
322
+ of intermediate/partially merged netcdf files equal to the total number
323
+ of input files divided by chunk_size. The last chunk may contain less
324
+ files than specified, depending on the total number of files.
325
+ Default: None, which processes all files together.
246
326
  '''
327
+ logging.info(f'Combine stats netcdf files from {path_to_data}')
328
+ if (chunk_size is not None) and (chunk_size < 1):
329
+ raise ValueError(f'Invalid chunk size, must be greater than 0, was {chunk_size}')
247
330
 
248
- logging.info(f'combine stats netcdf files from {path_to_data}')
249
- xstats, image_stats = combine_stats_netcdf_files(path_to_data, prefix=prefix)
331
+ # Get sorted list of per-image stats netcdf files
332
+ sorted_filelist = sorted(glob(os.path.join(path_to_data, prefix + 'Image-D*-STATS.nc')))
250
333
 
334
+ # Chunk the file list into smaller parts if specified
335
+ num_files = len(sorted_filelist)
336
+ chunk_size_used = num_files if chunk_size is None else min(chunk_size, num_files)
337
+ num_chunks = int(np.ceil(num_files / chunk_size_used))
338
+ filelist_chunks = [sorted_filelist[i*chunk_size_used:min(num_files, (i+1)*chunk_size_used)] for i in range(num_chunks)]
339
+ infostr = f'Processing {num_chunks} partial file lists of {chunk_size_used} files each'
340
+ infostr += f', based on a total of {num_files} files.'
341
+ logging.info(infostr)
342
+
343
+ # Get config from first file in list
344
+ xstats = load_stats(sorted_filelist[0])
251
345
  settings = steps_from_xstats(xstats)
252
-
253
346
  prefix_out = os.path.basename(settings['steps']['output']['output_datafile'])
254
- output_name = os.path.join(path_to_data, prefix_out)
255
-
256
- logging.info(f'writing {output_name}')
257
- # save the particle statistics (xstats) to NetCDF
258
347
  encoding = setup_xstats_encoding(xstats)
259
- xstats.to_netcdf(output_name + '-STATS.nc', encoding=encoding, engine=NETCDF_ENGINE, format='NETCDF4')
260
- # if summary data for each raw image are available (image_stats), save this into the image_stats group
261
- if image_stats is not None:
262
- image_stats.to_netcdf(output_name + '-STATS.nc', group='image_stats', mode='a', engine=NETCDF_ENGINE)
263
- logging.info(f'writing {output_name} done.')
348
+
349
+ def process_store(i, filelist_):
350
+ output_name = os.path.join(path_to_data, f'part-{i:04d}-{prefix_out}-STATS.nc')
351
+
352
+ # Skip this chunk if the merged output file exists and overwrite is set to False
353
+ if os.path.exists(output_name) and not overwrite_existing_partials:
354
+ logging.info(f'File exists ({output_name}), skipping')
355
+ return output_name
356
+
357
+ # Load the individual datasets
358
+ xstats, image_stats = concat_stats_netcdf_files(filelist_)
359
+
360
+ # Save the particle statistics (xstats) to NetCDF
361
+ logging.info(f'Writing {output_name}')
362
+ if xstats is not None:
363
+ xstats.to_netcdf(output_name, mode='w', encoding=encoding, engine=NETCDF_ENGINE, format='NETCDF4')
364
+
365
+ # If summary data for each raw image are available (image_stats), save this into the image_stats group
366
+ if image_stats is not None:
367
+ image_stats.to_netcdf(output_name, group='image_stats', mode='a', engine=NETCDF_ENGINE)
368
+ logging.info(f'Writing {output_name} done.')
369
+
370
+ return output_name
371
+
372
+ # Loop over filelist chunkst and created merged netcdf files for each
373
+ merged_files = []
374
+ for i, filelist_ in enumerate(filelist_chunks):
375
+ output_name = process_store(i, filelist_)
376
+ merged_files.append(output_name)
377
+
378
+ # Finally, merge the partially merged files
379
+ logging.info('Doing final merge of partially merged files')
380
+ output_name = os.path.join(path_to_data, prefix_out + '-STATS.nc')
381
+ with xr.open_mfdataset(merged_files, concat_dim='index', combine='nested') as ds:
382
+ ds.to_netcdf(output_name, mode='w', encoding=encoding, engine=NETCDF_ENGINE, format='NETCDF4')
383
+
384
+ with xr.open_mfdataset(merged_files, group='image_stats', concat_dim='timestamp', combine='nested') as ds:
385
+ ds.to_netcdf(output_name, mode='a', group='image_stats', engine=NETCDF_ENGINE)
386
+
387
+ logging.info(f'Writing {output_name} done.')
264
388
 
265
389
 
266
390
  def steps_from_xstats(xstats):
@@ -259,14 +259,12 @@ def extract_particles(imc, timestamp, Classification, region_properties,
259
259
  stats : DataFrame
260
260
  List of particle statistics for every particle, according to Partstats class
261
261
  '''
262
- filenames = ['not_exported'] * len(region_properties)
262
+ num_rows = max(len(region_properties), 1)
263
+ filenames = ['not_exported'] * num_rows
263
264
 
264
265
  if Classification is not None:
265
266
  # pre-allocation
266
- predictions = np.zeros((len(region_properties),
267
- len(Classification.class_labels)),
268
- dtype='float64')
269
- predictions *= np.nan
267
+ predictions = np.nan * np.zeros((num_rows, len(Classification.class_labels)), dtype='float64')
270
268
 
271
269
  # obtain the original image filename from the timestamp
272
270
  filename = timestamp.strftime('D%Y%m%dT%H%M%S.%f')
@@ -292,8 +290,8 @@ def extract_particles(imc, timestamp, Classification, region_properties,
292
290
  HDF5File = None
293
291
 
294
292
  # pre-allocate some things
295
- data = np.zeros((len(region_properties), len(propnames)), dtype=np.float64)
296
- bboxes = np.zeros((len(region_properties), 4), dtype=np.float64)
293
+ data = np.nan * np.zeros((num_rows, len(propnames)), dtype=np.float64)
294
+ bboxes = np.nan * np.zeros((num_rows, 4), dtype=np.float64)
297
295
  nb_extractable_part = 0
298
296
 
299
297
  for i, el in enumerate(region_properties):
@@ -630,7 +628,7 @@ class CalculateImageStats():
630
628
  data['image_stats'].loc[data['timestamp'], 'saturation'] = image_saturation
631
629
 
632
630
  # Skip remaining calculations if no particles where found
633
- if data['stats'].size == 0:
631
+ if data['stats'].dropna().size == 0:
634
632
  return data
635
633
 
636
634
  # Calculate D50, nc and vc stats
@@ -1 +0,0 @@
1
- __version__ = '2.5.10'
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