PyOPIA 2.1.1__tar.gz → 2.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyOPIA
3
- Version: 2.1.1
3
+ Version: 2.3.0
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.3.0'
@@ -15,6 +15,7 @@ import pandas as pd
15
15
  import pyopia.background
16
16
  import pyopia.instrument.silcam
17
17
  import pyopia.instrument.holo
18
+ import pyopia.instrument.uvp
18
19
  import pyopia.instrument.common
19
20
  import pyopia.io
20
21
  import pyopia.pipeline
@@ -80,7 +81,7 @@ def generate_config(instrument: str, raw_files: str, model_path: str, outfolder:
80
81
  Parameters
81
82
  ----------
82
83
  instrument : str
83
- either `silcam` or `holo`
84
+ either `silcam`, `holo` or `uvp`
84
85
  raw_files : str
85
86
  raw_files
86
87
  model_path : str
@@ -95,6 +96,8 @@ def generate_config(instrument: str, raw_files: str, model_path: str, outfolder:
95
96
  pipeline_config = pyopia.instrument.silcam.generate_config(raw_files, model_path, outfolder, output_prefix)
96
97
  case 'holo':
97
98
  pipeline_config = pyopia.instrument.holo.generate_config(raw_files, model_path, outfolder, output_prefix)
99
+ case 'uvp':
100
+ pipeline_config = pyopia.instrument.uvp.generate_config(raw_files, model_path, outfolder, output_prefix)
98
101
 
99
102
  config_filename = instrument + "-config.toml"
100
103
  with open(config_filename, "w") as toml_file:
@@ -34,9 +34,9 @@ def load_image(filename):
34
34
  Returns
35
35
  -------
36
36
  array
37
- raw image
37
+ raw image float between 0-1
38
38
  '''
39
- img = np.load(filename, allow_pickle=False).astype(np.float64)
39
+ img = np.load(filename, allow_pickle=False).astype(np.float64) / 255
40
40
  return img
41
41
 
42
42
 
@@ -68,7 +68,7 @@ class SilCamLoad():
68
68
 
69
69
  def __call__(self, data):
70
70
  timestamp = timestamp_from_filename(data['filename'])
71
- img = load_image(data['filename']).astype(np.float64)/255
71
+ img = load_image(data['filename'])
72
72
  data['timestamp'] = timestamp
73
73
  data['imraw'] = img
74
74
  return data
@@ -0,0 +1,127 @@
1
+ '''
2
+ Module containing UVP specific tools to enable compatability with the :mod:`pyopia.pipeline`
3
+ '''
4
+
5
+ import os
6
+ import numpy as np
7
+ import pandas as pd
8
+ import skimage.io
9
+
10
+
11
+ def timestamp_from_filename(filename):
12
+ '''get a pandas timestamp from a UVP vignette image filename
13
+
14
+ Args:
15
+ filename (string): UVP filename (.png)
16
+
17
+ Returns:
18
+ timestamp: timestamp from pandas.to_datetime()
19
+ '''
20
+
21
+ # get the timestamp of the image (in this case from the filename)
22
+ timestr = os.path.split(filename)[-1].strip('.png')
23
+ timestamp = pd.to_datetime(timestr)
24
+ return timestamp
25
+
26
+
27
+ def load_image(filename):
28
+ '''load a UVP .png file from disc
29
+
30
+ Parameters
31
+ ----------
32
+ filename : string
33
+ filename to load
34
+
35
+ Returns
36
+ -------
37
+ array
38
+ raw image float between 0-1, inverted so that particles are dark on a light background
39
+ '''
40
+ img_darkfield = skimage.io.imread(filename).astype(np.float64)
41
+ img_inverted = (255 - img_darkfield) / 255
42
+ return img_inverted
43
+
44
+
45
+ class UVPLoad():
46
+ '''PyOpia pipline-compatible class for loading a single UVP image
47
+ using :func:`pyopia.instrument.uvp.load_image`
48
+ and extracting the timestamp using
49
+ :func:`pyopia.instrument.uvp.timestamp_from_filename`
50
+
51
+ Pipeline input data:
52
+ ---------
53
+ :class:`pyopia.pipeline.Data`
54
+ containing the following keys:
55
+
56
+ :attr:`pyopia.pipeline.Data.filename`
57
+
58
+ Returns:
59
+ --------
60
+ :class:`pyopia.pipeline.Data`
61
+ containing the following new keys:
62
+
63
+ :attr:`pyopia.pipeline.Data.timestamp`
64
+
65
+ :attr:`pyopia.pipeline.Data.img`
66
+ '''
67
+
68
+ def __init__(self):
69
+ pass
70
+
71
+ def __call__(self, data):
72
+ timestamp = timestamp_from_filename(data['filename'])
73
+ img = load_image(data['filename'])
74
+ data['timestamp'] = timestamp
75
+ data['imraw'] = img
76
+ return data
77
+
78
+
79
+ def generate_config(raw_files: str, model_path: str, outfolder: str, output_prefix: str):
80
+ '''Generate example uvp config.toml as a dict
81
+
82
+ Parameters
83
+ ----------
84
+ raw_files : str
85
+ raw_files
86
+ model_path : str
87
+ model_path
88
+ outfolder : str
89
+ outfolder
90
+ output_prefix : str
91
+ output_prefix
92
+
93
+ Returns:
94
+ --------
95
+ dict
96
+ pipeline_config toml dict
97
+ '''
98
+ # define the configuration to use in the processing pipeline - given as a dictionary - with some values defined above
99
+ pipeline_config = {
100
+ 'general': {
101
+ 'raw_files': raw_files,
102
+ 'pixel_size': 80 # pixel size in um
103
+ },
104
+ 'steps': {
105
+ 'classifier': {
106
+ 'pipeline_class': 'pyopia.classify.Classify',
107
+ 'model_path': model_path
108
+ },
109
+ 'load': {
110
+ 'pipeline_class': 'pyopia.instrument.uvp.UVPLoad'
111
+ },
112
+ 'segmentation': {
113
+ 'pipeline_class': 'pyopia.process.Segment',
114
+ 'threshold': 0.95,
115
+ 'segment_source': 'imraw'
116
+ },
117
+ 'statextract': {
118
+ 'pipeline_class': 'pyopia.process.CalculateStats',
119
+ 'roi_source': 'imraw'
120
+ },
121
+ 'output': {
122
+ 'pipeline_class': 'pyopia.io.StatsH5',
123
+ 'output_datafile': os.path.join(outfolder, output_prefix)
124
+ }
125
+ }
126
+ }
127
+ return pipeline_config
@@ -0,0 +1,253 @@
1
+ '''
2
+ Module containing tools for assessing statistical reliability of silcam size distributions
3
+ '''
4
+ import numpy as np
5
+ from skimage.draw import disk
6
+ import matplotlib.pyplot as plt
7
+ import skimage.util
8
+ import pandas as pd
9
+
10
+ import pyopia.statistics
11
+ import pyopia.plotting
12
+ import pyopia.process
13
+ import pyopia.instrument.silcam
14
+ from pyopia.pipeline import Pipeline
15
+
16
+
17
+ class SilcamSimulator():
18
+ def __init__(self, total_volume_concentration=1000,
19
+ d50=1000,
20
+ MinD=10,
21
+ PIX_SIZE=28,
22
+ PATH_LENGTH=40,
23
+ imx=2048,
24
+ imy=2448,
25
+ nims=50):
26
+ '''SilCam simulator
27
+
28
+ Parameters
29
+ ----------
30
+ total_volume_concentration : int, optional
31
+ total volume concentration, by default 1000
32
+ d50 : int, optional
33
+ median particle size, by default 1000
34
+ MinD : int, optional
35
+ minimum diameter to simulate, by default 10
36
+ PIX_SIZE : int, optional
37
+ pixel size (um), by default 28
38
+ PATH_LENGTH : int, optional
39
+ path length (mm), by default 40
40
+ imx : int, optional
41
+ image x dimension, by default 2048
42
+ imy : int, optional
43
+ image y dimension, by default 2448
44
+ nims : int, optional
45
+ number of images to simulate, by default 50
46
+
47
+ Example:
48
+ --------
49
+
50
+ ```python
51
+ from pyopia.simulator.silcam import SilcamSimulator
52
+
53
+ sim = SilcamSimulator()
54
+ sim.check_convergence()
55
+ sim.synthesize()
56
+ sim.process_synthetic_image()
57
+ sim.plot()
58
+ ```
59
+
60
+ '''
61
+ self.total_volume_concentration = total_volume_concentration
62
+ self.d50 = d50
63
+ self.MinD = MinD
64
+ self.PIX_SIZE = PIX_SIZE
65
+ self.PATH_LENGTH = PATH_LENGTH
66
+ self.imx = imx
67
+ self.imy = imy
68
+ self.nims = nims
69
+
70
+ self.dias, self.bin_limits = pyopia.statistics.get_size_bins()
71
+
72
+ # calculate the sample volume of the SilCam specified
73
+ self.sample_volume = pyopia.statistics.get_sample_volume(self.PIX_SIZE,
74
+ path_length=self.PATH_LENGTH,
75
+ imx=self.imx, imy=self.imy)
76
+
77
+ self.data = dict()
78
+
79
+ def weibull_distribution(self, x):
80
+ '''calculate weibull distribution
81
+
82
+ Parameters
83
+ ----------
84
+ x : array
85
+ size bins of input
86
+
87
+ Returns
88
+ -------
89
+ array
90
+ weibull distribution
91
+ '''
92
+ a = 2.8
93
+ n = self.d50 * 1.5723270440251573 # scaling required for the log-spaced size bins to match the input d50
94
+ return (a / n) * (x / n) ** (a - 1) * np.exp(-(x / n) ** a)
95
+
96
+ def check_convergence(self):
97
+ self.data['weibull_x'] = np.linspace(np.min(self.dias), np.max(self.dias), 10000)
98
+ self.data['weibull_y'] = self.weibull_distribution(self.data['weibull_x'])
99
+
100
+ self.data['volume_distribution_input'] = self.weibull_distribution(self.dias)
101
+ self.data['volume_distribution_input'] = self.data['volume_distribution_input'] / \
102
+ np.sum(self.data['volume_distribution_input']) * \
103
+ self.total_volume_concentration # scale the distribution according to concentration
104
+
105
+ DropletVolume = ((4 / 3) * np.pi * ((self.dias * 1e-6) / 2) ** 3) # the volume of each droplet in m3
106
+ # the number distribution in each bin
107
+ self.data['number_distribution'] = self.data['volume_distribution_input'] / (DropletVolume * 1e9)
108
+ self.data['number_distribution'][self.dias < self.MinD] = 0 # remove small particles for speed purposes
109
+
110
+ # scale the number distribution by the sample volume so resulting units are #/L/bin
111
+ self.data['number_distribution'] = self.data['number_distribution'] * self.sample_volume
112
+ nc = int(sum(self.data['number_distribution'])) # calculate the total number concentration. must be integer number
113
+
114
+ # convert the number distribution to volume distribution in uL/L/bin
115
+ vd2 = pyopia.statistics.vd_from_nd(self.data['number_distribution'], self.dias, self.sample_volume)
116
+
117
+ # obtain the resulting concentration, now having remove small particles
118
+ self.data['initial_volume_concentration'] = sum(vd2)
119
+
120
+ # calculate the d50 in um
121
+ self.data['d50_theoretical_best'] = pyopia.statistics.d50_from_vd(vd2, self.dias)
122
+
123
+ # preallocate variables
124
+ self.data['volume_distribution'] = np.zeros((self.nims, len(self.dias)))
125
+ self.data['cumulative_volume_concentration'] = np.zeros(self.nims)
126
+ self.data['cumulative_d50'] = np.zeros(self.nims)
127
+
128
+ for i in range(self.nims):
129
+ # randomly select a droplet radius from the input distribution
130
+ # radius is in pixels
131
+ rad = np.random.choice(self.dias / 2,
132
+ size=nc,
133
+ p=self.data['number_distribution'] / sum(self.data['number_distribution'])) / self.PIX_SIZE
134
+ log_ecd = rad * 2 * self.PIX_SIZE # log this size as a diameter in um
135
+
136
+ necd, edges = np.histogram(log_ecd, self.bin_limits) # count particles into number distribution
137
+
138
+ # convert to volume distribution
139
+ self.data['volume_distribution'][i, :] = pyopia.statistics.vd_from_nd(necd,
140
+ self.dias,
141
+ sv=self.sample_volume)
142
+
143
+ # calculated the cumulate volume distribution over image number
144
+ self.data['cumulative_volume_concentration'][i] = np.sum(np.mean(self.data['volume_distribution'][0:i, :],
145
+ axis=0))
146
+
147
+ # calcualte the cumulate d50 over image number
148
+ self.data['cumulative_d50'][i] = pyopia.statistics.d50_from_vd(np.mean(self.data['volume_distribution'],
149
+ axis=0),
150
+ self.dias)
151
+
152
+ def synthesize(self):
153
+ '''synthesize an image and measure droplets
154
+ '''
155
+ nc = int(sum(self.data['number_distribution'])) # number concentration
156
+
157
+ # preallocate the image and logged volume distribution variables
158
+ img = np.zeros((self.imx, self.imy, 3), dtype=np.uint8()) + 230 # scale the initial brightness down a bit
159
+ log_ecd = np.zeros(nc)
160
+ # randomly select a droplet radii from the input distribution
161
+ # radius is in pixels
162
+ rad = np.random.choice(self.dias / 2,
163
+ size=nc,
164
+ p=self.data['number_distribution'] / sum(self.data['number_distribution'])) / self.PIX_SIZE
165
+ log_ecd = rad * 2 * self.PIX_SIZE # log these sizes as a diameter in um
166
+ for rad_ in rad:
167
+ # randomly decide where to put particles within the image
168
+ col = np.random.randint(1, high=self.imx - rad_)
169
+ row = np.random.randint(1, high=self.imy - rad_)
170
+
171
+ rr, cc = disk((col, row), rad_) # make a cirle of the radius selected from the distribution
172
+ img[rr, cc, :] = 0
173
+
174
+ necd, edges = np.histogram(log_ecd, self.bin_limits) # count the input diameters into a number distribution
175
+ log_vd = pyopia.statistics.vd_from_nd(necd, self.dias, sv=self.sample_volume) # convert to a volume distribution
176
+
177
+ # add some noise to the synthesized image
178
+ img = np.uint8(255 * skimage.util.random_noise(np.float64(img) / 255))
179
+
180
+ img = np.uint8(img) # convert to uint8
181
+ self.data['synthetic_image_data'] = dict()
182
+ self.data['synthetic_image_data']['image'] = img
183
+ self.data['synthetic_image_data']['input_volume_distribution'] = log_vd
184
+
185
+ def process_synthetic_image(self):
186
+ pipeline_config = {
187
+ 'general': {
188
+ 'raw_files': '',
189
+ 'pixel_size': 28 # pixel size in um
190
+ },
191
+ 'steps': {
192
+ 'imageprep': {
193
+ 'pipeline_class': 'pyopia.instrument.silcam.ImagePrep',
194
+ 'image_level': 'im_synthetic'
195
+ },
196
+ 'segmentation': {
197
+ 'pipeline_class': 'pyopia.process.Segment',
198
+ 'threshold': 0.85,
199
+ 'segment_source': 'im_minimum'
200
+ },
201
+ 'statextract': {
202
+ 'pipeline_class': 'pyopia.process.CalculateStats',
203
+ 'roi_source': 'im_synthetic'
204
+ }
205
+ }
206
+ }
207
+ pipeline = Pipeline(pipeline_config)
208
+ pipeline.data['im_synthetic'] = self.data['synthetic_image_data']['image']
209
+ pipeline.data['timestamp'] = pd.Timestamp.now()
210
+ pipeline.run('')
211
+ dias, vd = pyopia.statistics.vd_from_stats(pipeline.data['stats'], pipeline_config['general']['pixel_size'])
212
+ vd /= self.sample_volume
213
+ self.data['synthetic_image_data']['pyopia_processed_volume_distribution'] = vd
214
+
215
+ def plot(self):
216
+ f, a = plt.subplots(2, 2, figsize=(15, 10))
217
+
218
+ plt.sca(a[0, 0])
219
+ pyopia.plotting.show_image(self.data['synthetic_image_data']['image'], self.PIX_SIZE)
220
+ plt.title(f'Synthetic image. Path lengh: {self.PATH_LENGTH}')
221
+
222
+ plt.sca(a[0, 1])
223
+ plt.plot(self.dias, self.data['volume_distribution'].T, '0.8', alpha=0.2)
224
+ plt.plot(-10, 0, '0.8', alpha=0.2, label='Simulated')
225
+ plt.plot(self.dias, np.mean(self.data['volume_distribution'].T, axis=1), 'k', label=f'{self.nims} statistical average')
226
+ plt.plot(self.dias, self.data['synthetic_image_data']['input_volume_distribution'], 'b',
227
+ label='Best possible from synthetic image\n(without occlusion)')
228
+ plt.plot(self.dias, self.data['synthetic_image_data']['pyopia_processed_volume_distribution'], 'g',
229
+ label='PyOPIA processed from synthetic image\n')
230
+ plt.plot(self.dias, self.data['volume_distribution_input'], 'r', label='target')
231
+ plt.xscale('log')
232
+ plt.xlabel('Diameter [um]')
233
+ plt.ylabel('Volume concentration [uL/L]')
234
+ plt.legend()
235
+ plt.xlim(np.min(self.dias), np.max(self.dias))
236
+
237
+ plt.sca(a[1, 0])
238
+ plt.plot(self.data['cumulative_volume_concentration'], '0.8', label='simulated')
239
+ plt.hlines(self.total_volume_concentration, xmin=0, xmax=self.nims, colors='r', label='target')
240
+ plt.xlabel('n-images')
241
+ plt.ylabel('Volume concentration of n-images [uL/L]')
242
+ plt.xlim(0, self.nims)
243
+ plt.legend()
244
+
245
+ plt.sca(a[1, 1])
246
+ plt.plot(self.data['cumulative_d50'], '0.8', label='simulated')
247
+ plt.hlines(self.d50, xmin=0, xmax=self.nims, colors='r', label='target')
248
+ plt.xlabel('n-images')
249
+ plt.ylabel('D50 over n-images [um]')
250
+ plt.xlim(0, self.nims)
251
+ plt.legend()
252
+
253
+ plt.tight_layout()
@@ -277,16 +277,22 @@ def nd_from_stats(stats, pix_size):
277
277
 
278
278
 
279
279
  def vd_from_stats(stats, pix_size):
280
- ''' calculate volume distribution from stats
280
+ '''Calculate volume distribution from stats
281
281
  units of miro-litres per sample volume
282
282
 
283
- Args:
284
- stats (DataFrame) : particle statistics from silcam process
285
- pix_size (float) : pixel size in microns
283
+ Parameters
284
+ ----------
285
+ stats : DataFrame
286
+ particle statistics from silcam process
287
+ pix_size : float
288
+ pixel size in microns
286
289
 
287
- Returns:
288
- dias : mid-points of size bins
289
- vd : volume distribution in micro-litres/sample-volume
290
+ Returns
291
+ -------
292
+ dias : array
293
+ mid-points of size bins
294
+ vd : array
295
+ volume distribution in micro-litres/sample-volume
290
296
  '''
291
297
 
292
298
  # obtain the number distribution
@@ -1 +0,0 @@
1
- __version__ = '2.1.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