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.
- {pyopia-2.1.0 → pyopia-2.2.0}/PKG-INFO +1 -1
- pyopia-2.2.0/pyopia/__init__.py +1 -0
- {pyopia-2.1.0 → pyopia-2.2.0}/pyopia/background.py +0 -1
- pyopia-2.2.0/pyopia/simulator/silcam.py +253 -0
- {pyopia-2.1.0 → pyopia-2.2.0}/pyopia/statistics.py +13 -7
- pyopia-2.1.0/pyopia/__init__.py +0 -1
- {pyopia-2.1.0 → pyopia-2.2.0}/LICENSE +0 -0
- {pyopia-2.1.0 → pyopia-2.2.0}/README.md +0 -0
- {pyopia-2.1.0 → pyopia-2.2.0}/pyopia/classify.py +0 -0
- {pyopia-2.1.0 → pyopia-2.2.0}/pyopia/cli.py +0 -0
- {pyopia-2.1.0 → pyopia-2.2.0}/pyopia/exampledata.py +0 -0
- {pyopia-2.1.0 → pyopia-2.2.0}/pyopia/instrument/__init__.py +0 -0
- {pyopia-2.1.0 → pyopia-2.2.0}/pyopia/instrument/common.py +0 -0
- {pyopia-2.1.0 → pyopia-2.2.0}/pyopia/instrument/holo.py +0 -0
- {pyopia-2.1.0 → pyopia-2.2.0}/pyopia/instrument/silcam.py +0 -0
- {pyopia-2.1.0 → pyopia-2.2.0}/pyopia/io.py +0 -0
- {pyopia-2.1.0 → pyopia-2.2.0}/pyopia/pipeline.py +0 -0
- {pyopia-2.1.0 → pyopia-2.2.0}/pyopia/plotting.py +0 -0
- {pyopia-2.1.0 → pyopia-2.2.0}/pyopia/process.py +0 -0
- {pyopia-2.1.0 → pyopia-2.2.0}/pyopia/tests/__init__.py +0 -0
- {pyopia-2.1.0 → pyopia-2.2.0}/pyopia/tests/test_classify.py +0 -0
- {pyopia-2.1.0 → pyopia-2.2.0}/pyopia/tests/test_notebooks.py +0 -0
- {pyopia-2.1.0 → pyopia-2.2.0}/pyopia/tests/test_pipeline.py +0 -0
- {pyopia-2.1.0 → pyopia-2.2.0}/pyproject.toml +0 -0
|
@@ -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
|
-
'''
|
|
280
|
+
'''Calculate volume distribution from stats
|
|
281
281
|
units of miro-litres per sample volume
|
|
282
282
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
289
|
-
|
|
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
|
pyopia-2.1.0/pyopia/__init__.py
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|