PyOPIA 2.16.4__tar.gz → 2.16.6__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 (38) hide show
  1. {pyopia-2.16.4 → pyopia-2.16.6}/PKG-INFO +1 -1
  2. pyopia-2.16.6/pyopia/__init__.py +1 -0
  3. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/instrument/holo.py +23 -11
  4. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/instrument/silcam.py +17 -6
  5. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/process.py +91 -7
  6. pyopia-2.16.4/pyopia/__init__.py +0 -1
  7. {pyopia-2.16.4 → pyopia-2.16.6}/.gitignore +0 -0
  8. {pyopia-2.16.4 → pyopia-2.16.6}/LICENSE +0 -0
  9. {pyopia-2.16.4 → pyopia-2.16.6}/README.md +0 -0
  10. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/auxillarydata.py +0 -0
  11. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/background.py +0 -0
  12. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/cf_metadata.json +0 -0
  13. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/classify.py +0 -0
  14. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/classify_torch.py +0 -0
  15. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/cli.py +0 -0
  16. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/dataexport/__init__.py +0 -0
  17. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/dataexport/ecotaxa.py +0 -0
  18. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/exampledata.py +0 -0
  19. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/instrument/__init__.py +0 -0
  20. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/instrument/common.py +0 -0
  21. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/instrument/uvp.py +0 -0
  22. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/io.py +0 -0
  23. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/metadata.py +0 -0
  24. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/pipeline.py +0 -0
  25. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/plotting.py +0 -0
  26. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/realtime.py +0 -0
  27. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/simulator/__init__.py +0 -0
  28. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/simulator/silcam.py +0 -0
  29. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/statistics.py +0 -0
  30. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/tests/__init__.py +0 -0
  31. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/tests/test_auxillarydata.py +0 -0
  32. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/tests/test_classify.py +0 -0
  33. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/tests/test_io.py +0 -0
  34. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/tests/test_notebooks.py +0 -0
  35. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/tests/test_pipeline.py +0 -0
  36. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/tests/test_process.py +0 -0
  37. {pyopia-2.16.4 → pyopia-2.16.6}/pyopia/tests/test_realtime.py +0 -0
  38. {pyopia-2.16.4 → pyopia-2.16.6}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyOPIA
3
- Version: 2.16.4
3
+ Version: 2.16.6
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
@@ -0,0 +1 @@
1
+ __version__ = "2.16.6"
@@ -21,7 +21,7 @@ from skimage.filters import sobel
21
21
  from skimage.morphology import disk, erosion, dilation
22
22
  import pyopia.process
23
23
  import struct
24
- from datetime import timedelta, datetime
24
+ from datetime import timedelta
25
25
  from glob import glob
26
26
 
27
27
  import logging
@@ -69,10 +69,10 @@ class Initial():
69
69
  self.filename = raw_files[0]
70
70
  imtmp = load_image(self.filename)
71
71
  self.pixel_size = data['settings']['general']['pixel_size']
72
- logger.info('Build kernel with pixel_size = ', self.pixel_size, 'um')
72
+ logger.info(f'Build kernel with pixel_size = {self.pixel_size} um')
73
73
  kern = create_kernel(imtmp, self.pixel_size, self.wavelength, self.n, self.offset, self.minZ, self.maxZ, self.stepZ)
74
74
  im_stack = np.zeros(np.shape(kern)).astype(np.float64)
75
- logger.info('HoloInitial done', datetime.now())
75
+ logger.info('HoloInitial done')
76
76
  data['kern'] = kern
77
77
  data['im_stack'] = im_stack
78
78
  return data
@@ -103,24 +103,27 @@ class Load():
103
103
  filename : string
104
104
  hologram filename (.pgm)
105
105
 
106
+ prefix_chars : int, optional
107
+ number of characters to ignore at start of filename when parsing timestamp,
108
+ by default 1 (e.g. to ignore 'D' in 'D20221101T120000.pgm')
109
+
106
110
  Returns
107
111
  -------
108
- timestamp : timestamp
109
- timestamp @todo
110
- imraw : np.arraym
112
+ timestamp : pandas.Timestamp
113
+ timestamp from filename
114
+ imraw : np.array
111
115
  hologram
112
116
  '''
113
117
 
114
- def __init__(self):
115
- pass
118
+ def __init__(self, prefix_chars=1):
119
+ self.prefix_chars = prefix_chars
116
120
 
117
121
  def __call__(self, data):
118
122
  logger.info(data['filename'])
119
123
  try:
120
124
  timestamp = read_lisst_holo_info(data['filename'])
121
125
  except ValueError:
122
- timestamp = pd.to_datetime(os.path.splitext(os.path.basename(data['filename']))[0][1:])
123
- logger.info(timestamp)
126
+ timestamp = pd.to_datetime(os.path.splitext(os.path.basename(data['filename']))[0][self.prefix_chars:])
124
127
  im = load_image(data['filename'])
125
128
  data['timestamp'] = timestamp
126
129
  data['imraw'] = im
@@ -559,6 +562,16 @@ class MergeStats():
559
562
  stack_rp = data['stack_rp']
560
563
  stack_ifocus = data['stack_ifocus']
561
564
 
565
+ # If there are no particles in the image, then stats has one row of nans
566
+ # This means that variables created here just need allocating and returning
567
+ if len(stack_ifocus) == 0:
568
+ logger.debug(f"stack_ifocus is empty. Allocating nan to ifocus and z. Filename: {data['filename']}")
569
+ stats['ifocus'] = np.nan
570
+ stats['z'] = np.nan
571
+ stats['holo_filename'] = data['filename']
572
+ data['stats'] = stats
573
+ return data
574
+
562
575
  bbox = np.empty((0, 4), int)
563
576
  for rp in stack_rp:
564
577
  bbox = np.append(bbox, [rp.bbox], axis=0)
@@ -603,7 +616,6 @@ def read_lisst_holo_info(filename):
603
616
  filenum = int(filenum.rsplit('.', 1)[0])
604
617
  timestamp = timestamp + timedelta(microseconds=filenum)
605
618
  timestamp = timestamp[0]
606
- logger.info(timestamp.strftime('D%Y%m%dT%H%M%S.%f'))
607
619
  f.close()
608
620
 
609
621
  return timestamp
@@ -14,21 +14,26 @@ from skimage.exposure import rescale_intensity
14
14
  import skimage.io
15
15
 
16
16
 
17
- def timestamp_from_filename(filename):
17
+ def timestamp_from_filename(filename, prefix_chars=1):
18
18
  '''get a pandas timestamp from a silcam filename
19
19
 
20
20
  Parameters
21
21
  ----------
22
- filename (string): silcam filename (.silc)
22
+ filename : string
23
+ silcam filename (.silc)
24
+
25
+ prefix_chars : int, optional
26
+ number of characters to ignore at start of filename when parsing timestamp,
27
+ by default 1 (e.g. to ignore 'D' in 'D20221101T120000.silc')
23
28
 
24
29
  Returns
25
30
  -------
26
- timestamp: timestamp
31
+ timestamp : pandas.Timestamp
27
32
  timestamp from pandas.to_datetime()
28
33
  '''
29
34
 
30
35
  # get the timestamp of the image (in this case from the filename)
31
- timestamp = pd.to_datetime(os.path.splitext(os.path.basename(filename))[0][1:])
36
+ timestamp = pd.to_datetime(os.path.splitext(os.path.basename(filename))[0][prefix_chars:])
32
37
  return timestamp
33
38
 
34
39
 
@@ -222,6 +227,10 @@ class SilCamLoad():
222
227
  image_format : str, optional
223
228
  Image file format. Can be either 'infer', 'rgb8', 'bayer_rg8' or 'mono8', by default 'infer'.
224
229
 
230
+ prefix_chars : int, optional
231
+ number of characters to ignore at start of filename when parsing timestamp,
232
+ by default 1 (e.g. to ignore 'D' in 'D20221101T120000.silc')
233
+
225
234
  Note
226
235
  ----
227
236
  'infer' uses the file extension to determine the image format using the following convention:
@@ -240,7 +249,8 @@ class SilCamLoad():
240
249
  :attr:`pyopia.pipeline.Data.img`
241
250
  '''
242
251
 
243
- def __init__(self, image_format='infer'):
252
+ def __init__(self, image_format='infer', prefix_chars=1):
253
+ self.prefix_chars = prefix_chars
244
254
  self.image_format = image_format
245
255
  self.extension_load = {'.silc': load_rgb8,
246
256
  '.msilc': load_mono8,
@@ -250,7 +260,8 @@ class SilCamLoad():
250
260
  'MONO8': load_mono8, 'BAYER_RG8': load_bayer_rgb8}
251
261
 
252
262
  def __call__(self, data):
253
- data['timestamp'] = timestamp_from_filename(data['filename'])
263
+ data['timestamp'] = timestamp_from_filename(data['filename'],
264
+ prefix_chars=self.prefix_chars)
254
265
  data['imraw'] = self.load_image(data['filename'])
255
266
  return data
256
267
 
@@ -204,6 +204,59 @@ def extract_roi(input_image, bbox):
204
204
  return roi
205
205
 
206
206
 
207
+ def expand_bbox(bbox, image_shape, fraction):
208
+ '''Expand a bounding box by a fraction of its width and height, clamped to image bounds.
209
+
210
+ The expansion is split evenly on each side, so a fraction of 0.10 grows the
211
+ bounding box by 5% on each side (total +10% width, +10% height). Coordinates
212
+ are clamped to remain inside the image. Useful for adding visual context
213
+ around exported particle ROIs without altering the underlying regionprops
214
+ measurements.
215
+
216
+ Parameters
217
+ ----------
218
+ bbox : array-like of int
219
+ [min_row, min_col, max_row, max_col], following the skimage regionprops
220
+ convention where ``max_row`` and ``max_col`` are exclusive.
221
+ image_shape : tuple
222
+ Shape of the full image. Only the first two elements (H, W) are used,
223
+ so passing ``imc.shape`` works for both 2-D and 3-D images.
224
+ fraction : float
225
+ Total fractional expansion of width and height. ``0.1`` = +10%.
226
+ ``0`` (or ``None``) returns the bbox unchanged. Must be non-negative.
227
+
228
+ Returns
229
+ -------
230
+ expanded : ndarray of int, shape (4,)
231
+ Expanded and clamped bounding box, integer-valued.
232
+
233
+ Raises
234
+ ------
235
+ ValueError
236
+ If ``fraction`` is negative.
237
+ '''
238
+ bbox_int = np.asarray(bbox, dtype=int)
239
+ if fraction is None or fraction == 0:
240
+ return bbox_int
241
+ if fraction < 0:
242
+ raise ValueError(f'bbox_expansion must be non-negative, got {fraction}')
243
+
244
+ r1, c1, r2, c2 = bbox_int
245
+ H, W = image_shape[0], image_shape[1]
246
+
247
+ h = r2 - r1
248
+ w = c2 - c1
249
+ pad_r = int(round(h * fraction / 2.0))
250
+ pad_c = int(round(w * fraction / 2.0))
251
+
252
+ return np.array([
253
+ max(0, r1 - pad_r),
254
+ max(0, c1 - pad_c),
255
+ min(H, r2 + pad_r),
256
+ min(W, c2 + pad_c),
257
+ ], dtype=int)
258
+
259
+
207
260
  def put_roi_in_h5(export_outputpath, HDF5File, roi, filename, i):
208
261
  '''Adds rois to an open hdf file if export_outputpath is not None.
209
262
  For use within {func}`pyopia.process.export_particles`
@@ -232,7 +285,8 @@ def put_roi_in_h5(export_outputpath, HDF5File, roi, filename, i):
232
285
 
233
286
  def extract_particles(imc, timestamp, Classification, region_properties,
234
287
  export_outputpath=None, min_length=0, propnames=['major_axis_length', 'minor_axis_length',
235
- 'equivalent_diameter']):
288
+ 'equivalent_diameter'],
289
+ bbox_expansion=0.0):
236
290
  '''Extracts the particles to build stats and export particle rois to HDF5 files
237
291
 
238
292
  Parameters
@@ -253,6 +307,13 @@ def extract_particles(imc, timestamp, Classification, region_properties,
253
307
  Specifies list of skimage regionprops to export to the output file.
254
308
  Must contain default values that can be appended to,
255
309
  by default ['major_axis_length', 'minor_axis_length', 'equivalent_diameter']
310
+ bbox_expansion : float, optional
311
+ Fractional expansion of the bounding box used when cropping each ROI for
312
+ export. ``0.0`` (default) preserves prior behaviour. ``0.1`` grows the
313
+ crop by 10% in width and height (5% on each side), clamped to image
314
+ bounds. Only the exported ROI image is affected; the ``minr/minc/maxr/
315
+ maxc`` columns saved in stats continue to report the un-expanded
316
+ regionprops bbox so that measurements are unchanged.
256
317
 
257
318
  Returns
258
319
  -------
@@ -304,8 +365,10 @@ def extract_particles(imc, timestamp, Classification, region_properties,
304
365
  if ((data[i, 0] > min_length) & (data[i, 1] > 2)):
305
366
 
306
367
  nb_extractable_part += 1
307
- # extract the region of interest from the corrected colour image
308
- roi = extract_roi(imc, bboxes[i, :].astype(int))
368
+ # extract the region of interest from the corrected colour image,
369
+ # optionally with the bbox expanded by `bbox_expansion` to add context
370
+ roi_bbox = expand_bbox(bboxes[i, :], imc.shape, bbox_expansion)
371
+ roi = extract_roi(imc, roi_bbox)
309
372
 
310
373
  if Classification is not None:
311
374
  # run a prediction on what type of particle this might be
@@ -426,7 +489,8 @@ def statextract(imbw, timestamp, imc,
426
489
  max_particles=5000,
427
490
  export_outputpath=None,
428
491
  min_length=0,
429
- propnames=['major_axis_length', 'minor_axis_length', 'equivalent_diameter']):
492
+ propnames=['major_axis_length', 'minor_axis_length', 'equivalent_diameter'],
493
+ bbox_expansion=0.0):
430
494
  '''Extracts statistics of particles in a binary images (imbw)
431
495
 
432
496
  Parameters
@@ -452,6 +516,9 @@ def statextract(imbw, timestamp, imc,
452
516
  Specifies list of skimage regionprops to export to the output file.
453
517
  Must contain default values that can be appended to,
454
518
  by default ['major_axis_length', 'minor_axis_length', 'equivalent_diameter']
519
+ bbox_expansion : float, optional
520
+ Fractional expansion of bounding boxes when cropping ROI images for export.
521
+ See :func:`extract_particles`. Defaults to 0.0 (no expansion).
455
522
 
456
523
  Returns
457
524
  -------
@@ -479,7 +546,8 @@ def statextract(imbw, timestamp, imc,
479
546
 
480
547
  stats = extract_particles(imc, timestamp, Classification, region_properties,
481
548
  export_outputpath=export_outputpath, min_length=min_length,
482
- propnames=propnames)
549
+ propnames=propnames,
550
+ bbox_expansion=bbox_expansion)
483
551
 
484
552
  return stats, saturation
485
553
 
@@ -558,6 +626,19 @@ class CalculateStats():
558
626
  roi_source: (str, optional)
559
627
  Key of an image in Pipeline.data that is used for outputting ROIs and passing to the classifier.
560
628
  Defaults to 'im_corrected'
629
+ bbox_expansion: (float, optional)
630
+ Fractional expansion applied to each particle bounding box before the
631
+ ROI is cropped and exported, e.g. ``0.1`` enlarges the crop by 10% in
632
+ width and height (5% on each side, clamped to image bounds). The
633
+ regionprops measurements and the ``minr/minc/maxr/maxc`` columns
634
+ written into stats are unaffected. Defaults to ``0.0`` (no expansion).
635
+
636
+ Configure from a TOML pipeline as::
637
+
638
+ [steps.statextract]
639
+ pipeline_class = "pyopia.process.CalculateStats"
640
+ export_outputpath = "/path/to/rois"
641
+ bbox_expansion = 0.1
561
642
 
562
643
  Returns
563
644
  -------
@@ -572,7 +653,8 @@ class CalculateStats():
572
653
  export_outputpath=None,
573
654
  min_length=0,
574
655
  propnames=['major_axis_length', 'minor_axis_length', 'equivalent_diameter'],
575
- roi_source='im_corrected'):
656
+ roi_source='im_corrected',
657
+ bbox_expansion=0.0):
576
658
 
577
659
  self.max_coverage = max_coverage
578
660
  self.max_particles = max_particles
@@ -580,6 +662,7 @@ class CalculateStats():
580
662
  self.min_length = min_length
581
663
  self.propnames = propnames
582
664
  self.roi_source = roi_source
665
+ self.bbox_expansion = bbox_expansion
583
666
 
584
667
  self.calc_image_stats = CalculateImageStats()
585
668
 
@@ -591,7 +674,8 @@ class CalculateStats():
591
674
  max_particles=self.max_particles,
592
675
  export_outputpath=self.export_outputpath,
593
676
  min_length=self.min_length,
594
- propnames=self.propnames)
677
+ propnames=self.propnames,
678
+ bbox_expansion=self.bbox_expansion)
595
679
  stats['timestamp'] = data['timestamp']
596
680
  stats['saturation'] = saturation
597
681
 
@@ -1 +0,0 @@
1
- __version__ = "2.16.4"
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