PyOPIA 2.1.0__tar.gz → 2.2.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.0
3
+ Version: 2.2.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.2.0'
@@ -253,7 +253,6 @@ class CorrectBackgroundAccurate():
253
253
  return data
254
254
 
255
255
  data['im_corrected'] = correct_im_accurate(data['imbg'], data[self.image_source])
256
- data['im_corrected'] = correct_im_accurate(data['imbg'], data[self.image_source])
257
256
 
258
257
  match self.bgshift_function:
259
258
  case 'pass':
@@ -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.0'
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