PyOPIA 2.3.0__tar.gz → 2.4.1__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.3.0 → pyopia-2.4.1}/PKG-INFO +1 -1
- pyopia-2.4.1/pyopia/__init__.py +1 -0
- {pyopia-2.3.0 → pyopia-2.4.1}/pyopia/background.py +86 -49
- {pyopia-2.3.0 → pyopia-2.4.1}/pyopia/cli.py +57 -22
- {pyopia-2.3.0 → pyopia-2.4.1}/pyopia/io.py +52 -36
- {pyopia-2.3.0 → pyopia-2.4.1}/pyopia/pipeline.py +103 -8
- {pyopia-2.3.0 → pyopia-2.4.1}/pyopia/plotting.py +14 -10
- {pyopia-2.3.0 → pyopia-2.4.1}/pyopia/process.py +142 -101
- {pyopia-2.3.0 → pyopia-2.4.1}/pyopia/simulator/silcam.py +3 -2
- {pyopia-2.3.0 → pyopia-2.4.1}/pyopia/statistics.py +413 -280
- pyopia-2.3.0/pyopia/__init__.py +0 -1
- {pyopia-2.3.0 → pyopia-2.4.1}/LICENSE +0 -0
- {pyopia-2.3.0 → pyopia-2.4.1}/README.md +0 -0
- {pyopia-2.3.0 → pyopia-2.4.1}/pyopia/classify.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.1}/pyopia/exampledata.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.1}/pyopia/instrument/__init__.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.1}/pyopia/instrument/common.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.1}/pyopia/instrument/holo.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.1}/pyopia/instrument/silcam.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.1}/pyopia/instrument/uvp.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.1}/pyopia/tests/__init__.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.1}/pyopia/tests/test_classify.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.1}/pyopia/tests/test_notebooks.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.1}/pyopia/tests/test_pipeline.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.1}/pyproject.toml +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '2.4.1'
|
|
@@ -8,13 +8,19 @@ def ini_background(bgfiles, load_function):
|
|
|
8
8
|
'''
|
|
9
9
|
Create and initial background stack and average image
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
Parameters:
|
|
12
|
+
-----------
|
|
13
|
+
bgfiles : list
|
|
14
|
+
List of strings of filenames to be used in background creation
|
|
15
|
+
load_function : object
|
|
16
|
+
This function should take a filename and return an image, for example: :func:`pyopia.instrument.silcam.load_image`
|
|
17
|
+
|
|
18
|
+
Returns
|
|
19
|
+
-------
|
|
20
|
+
bgstack : list
|
|
21
|
+
list of all images in the background stack
|
|
22
|
+
imbg : array
|
|
23
|
+
background image
|
|
18
24
|
'''
|
|
19
25
|
bgstack = []
|
|
20
26
|
for f in bgfiles:
|
|
@@ -33,14 +39,21 @@ def shift_bgstack_accurate(bgstack, imbg, imnew):
|
|
|
33
39
|
The new background is calculated slowly by computing the mean of all images
|
|
34
40
|
in the background stack.
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
Parameters:
|
|
43
|
+
-----------
|
|
44
|
+
bgstack : list
|
|
45
|
+
list of all images in the background stack
|
|
46
|
+
imbg : array
|
|
47
|
+
background image
|
|
48
|
+
imnew : array
|
|
49
|
+
new image to be added to stack
|
|
50
|
+
|
|
51
|
+
Returns
|
|
52
|
+
-------
|
|
53
|
+
bgstack : list
|
|
54
|
+
updated list of all background images
|
|
55
|
+
imbg : array
|
|
56
|
+
updated actual background image
|
|
44
57
|
'''
|
|
45
58
|
bgstack.pop(0) # pop the oldest image from the stack,
|
|
46
59
|
bgstack.append(imnew) # append the new image to the stack
|
|
@@ -56,14 +69,21 @@ def shift_bgstack_fast(bgstack, imbg, imnew):
|
|
|
56
69
|
adding the new image (both scaled by the stacklength).
|
|
57
70
|
This is close to a running mean, but not quite.
|
|
58
71
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
Parameters:
|
|
73
|
+
-----------
|
|
74
|
+
bgstac : list
|
|
75
|
+
list of all images in the background stack
|
|
76
|
+
imbg : uint8
|
|
77
|
+
background image
|
|
78
|
+
imnew : unit8
|
|
79
|
+
new image to be added to stack
|
|
80
|
+
|
|
81
|
+
Returns
|
|
82
|
+
-------
|
|
83
|
+
bgstack : list
|
|
84
|
+
updated list of all background images
|
|
85
|
+
imbg : array
|
|
86
|
+
updated actual background image
|
|
67
87
|
'''
|
|
68
88
|
stacklength = len(bgstack)
|
|
69
89
|
imold = bgstack.pop(0) # pop the oldest image from the stack,
|
|
@@ -82,14 +102,17 @@ def correct_im_accurate(imbg, imraw):
|
|
|
82
102
|
There is a small chance of clipping of imc in both crushed blacks and blown
|
|
83
103
|
highlights if the background or raw images are very poorly obtained
|
|
84
104
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
105
|
+
Parameters:
|
|
106
|
+
-----------
|
|
107
|
+
imbg : float64
|
|
108
|
+
background averaged image
|
|
109
|
+
imraw : float64
|
|
110
|
+
raw image
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
im_corrected : float64
|
|
115
|
+
corrected image, same type as input
|
|
93
116
|
'''
|
|
94
117
|
|
|
95
118
|
im_corrected = imraw - imbg
|
|
@@ -108,12 +131,17 @@ def correct_im_fast(imbg, imraw):
|
|
|
108
131
|
There is high potential for clipping of imc in both crushed blacks an blown
|
|
109
132
|
highlights, especially if the background or raw images are not properly obtained
|
|
110
133
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
134
|
+
Parameters:
|
|
135
|
+
-----------
|
|
136
|
+
imraw : array
|
|
137
|
+
raw image
|
|
138
|
+
imbg : array
|
|
139
|
+
background averaged image
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
im_corrected : array
|
|
144
|
+
corrected image
|
|
117
145
|
'''
|
|
118
146
|
im_corrected = imraw - imbg
|
|
119
147
|
|
|
@@ -130,18 +158,27 @@ def shift_and_correct(bgstack, imbg, imraw, stacklength, real_time_stats=False):
|
|
|
130
158
|
|
|
131
159
|
This is a wrapper for shift_bgstack and correct_im
|
|
132
160
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
161
|
+
Parameters:
|
|
162
|
+
-----------
|
|
163
|
+
bgstack : list
|
|
164
|
+
list of all images in the background stack
|
|
165
|
+
imbg : float64
|
|
166
|
+
background image
|
|
167
|
+
imraw : float64
|
|
168
|
+
raw image
|
|
169
|
+
stacklength : int
|
|
170
|
+
unused int here - just there to maintain the same behaviour as shift_bgstack_fast()
|
|
171
|
+
real_time_stats : Bool, optional
|
|
172
|
+
True use fast functions, if False use accurate functions., by default False
|
|
173
|
+
|
|
174
|
+
Returns
|
|
175
|
+
-------
|
|
176
|
+
bgstack : list
|
|
177
|
+
list of all images in the background stack
|
|
178
|
+
imbg : float64
|
|
179
|
+
background averaged image
|
|
180
|
+
im_corrected : float64
|
|
181
|
+
corrected image
|
|
145
182
|
'''
|
|
146
183
|
|
|
147
184
|
if real_time_stats:
|
|
@@ -4,13 +4,14 @@ PyOPIA top-level code primarily for managing cmd line entry points
|
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
6
|
import toml
|
|
7
|
-
from glob import glob
|
|
8
7
|
import os
|
|
9
8
|
import datetime
|
|
10
9
|
import traceback
|
|
11
10
|
import logging
|
|
12
11
|
from rich.progress import track, Progress
|
|
12
|
+
from rich.logging import RichHandler
|
|
13
13
|
import pandas as pd
|
|
14
|
+
import threading
|
|
14
15
|
|
|
15
16
|
import pyopia.background
|
|
16
17
|
import pyopia.instrument.silcam
|
|
@@ -105,23 +106,35 @@ def generate_config(instrument: str, raw_files: str, model_path: str, outfolder:
|
|
|
105
106
|
|
|
106
107
|
|
|
107
108
|
@app.command()
|
|
108
|
-
def process(config_filename: str):
|
|
109
|
+
def process(config_filename: str, num_chunks: int = 1):
|
|
109
110
|
'''Run a PyOPIA processing pipeline based on given a config.toml
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
config_filename : str
|
|
115
|
+
config filename
|
|
116
|
+
numchunks : int, optional
|
|
117
|
+
split the dataset into chucks, and process in parallell, by default 1
|
|
118
|
+
|
|
110
119
|
'''
|
|
111
120
|
from pyopia.io import load_toml
|
|
112
121
|
from pyopia.pipeline import Pipeline
|
|
113
122
|
|
|
114
|
-
logger = logging.getLogger()
|
|
115
|
-
|
|
116
123
|
with Progress(transient=True) as progress:
|
|
117
124
|
progress.console.print("[blue]LOAD CONFIG")
|
|
118
125
|
pipeline_config = load_toml(config_filename)
|
|
119
126
|
|
|
120
127
|
setup_logging(pipeline_config)
|
|
128
|
+
logger = logging.getLogger('rich')
|
|
129
|
+
logger.info(f'PyOPIA process started {pd.Timestamp.now()}')
|
|
130
|
+
|
|
131
|
+
check_chunks(num_chunks, pipeline_config)
|
|
121
132
|
|
|
122
133
|
progress.console.print("[blue]OBTAIN FILE LIST")
|
|
123
|
-
|
|
124
|
-
|
|
134
|
+
raw_files = pyopia.pipeline.FilesToProcess(pipeline_config['general']['raw_files'])
|
|
135
|
+
average_window = pipeline_config['steps']['correctbackground'].get('average_window', 0)
|
|
136
|
+
bgshift_function = pipeline_config['steps']['correctbackground'].get('bgshift_function', 'pass')
|
|
137
|
+
raw_files.prepare_chunking(num_chunks, average_window, bgshift_function)
|
|
125
138
|
|
|
126
139
|
progress.console.print('[blue]PREPARE FOLDERS')
|
|
127
140
|
if 'output' not in pipeline_config['steps']:
|
|
@@ -134,20 +147,32 @@ def process(config_filename: str):
|
|
|
134
147
|
if os.path.isfile(output_datafile + '-STATS.nc'):
|
|
135
148
|
dt_now = datetime.datetime.now().strftime('D%Y%m%dT%H%M%S')
|
|
136
149
|
newname = output_datafile + '-conflict-' + str(dt_now) + '-STATS.nc'
|
|
137
|
-
|
|
138
|
-
newname)
|
|
150
|
+
logger.warning(f'Renaming conflicting file to: {newname}')
|
|
139
151
|
os.rename(output_datafile + '-STATS.nc', newname)
|
|
140
152
|
|
|
141
153
|
progress.console.print("[blue]INITIALISE PIPELINE")
|
|
142
|
-
processing_pipeline = Pipeline(pipeline_config)
|
|
143
154
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
155
|
+
def process_file_list(file_list, c):
|
|
156
|
+
processing_pipeline = Pipeline(pipeline_config)
|
|
157
|
+
for filename in track(file_list, description=f'[blue]Processing progress (chunk {c})',
|
|
158
|
+
disable=c != 0):
|
|
159
|
+
try:
|
|
160
|
+
logger.debug(f'Chunk {c} starting to process {filename}')
|
|
161
|
+
processing_pipeline.run(filename)
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.warning('[red]An error occured in processing, ' +
|
|
164
|
+
'skipping rest of pipeline and moving to next image.' +
|
|
165
|
+
f'(chunk {c})')
|
|
166
|
+
logger.error(e)
|
|
167
|
+
logger.debug(''.join(traceback.format_tb(e.__traceback__)))
|
|
168
|
+
|
|
169
|
+
# With one chunk we keep the non-threaded functionality to ensure backwards compatibility
|
|
170
|
+
if num_chunks == 1:
|
|
171
|
+
process_file_list(raw_files, 0)
|
|
172
|
+
else:
|
|
173
|
+
for c, chunk in enumerate(raw_files.chunked_files):
|
|
174
|
+
job = threading.Thread(target=process_file_list, args=(chunk, c, ))
|
|
175
|
+
job.start()
|
|
151
176
|
|
|
152
177
|
|
|
153
178
|
@app.command()
|
|
@@ -179,15 +204,25 @@ def setup_logging(pipeline_config):
|
|
|
179
204
|
log_file = pipeline_config['general'].get('log_file', None)
|
|
180
205
|
log_level_name = pipeline_config['general'].get('log_level', 'INFO')
|
|
181
206
|
log_level = getattr(logging, log_level_name)
|
|
182
|
-
|
|
207
|
+
|
|
208
|
+
# Either log to file (silent console) or to console with Rich
|
|
209
|
+
if log_file is None:
|
|
210
|
+
handlers = [RichHandler(show_time=True, show_level=False)]
|
|
211
|
+
else:
|
|
212
|
+
handlers = [logging.FileHandler(log_file, mode='a')]
|
|
183
213
|
|
|
184
214
|
# Configure logger
|
|
185
|
-
log_format = '%(asctime)s %(levelname)s [%(module)s.%(funcName)s] %(message)s'
|
|
186
|
-
logging.basicConfig(level=log_level, format=log_format,
|
|
187
|
-
|
|
215
|
+
log_format = '%(asctime)s %(levelname)s %(threadName)s [%(module)s.%(funcName)s] %(message)s'
|
|
216
|
+
logging.basicConfig(level=log_level, datefmt='%Y-%m-%d %H:%M:%S', format=log_format, handlers=handlers)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def check_chunks(chunks, pipeline_config):
|
|
220
|
+
if chunks < 1:
|
|
221
|
+
raise RuntimeError('You must have at least 1 chunk')
|
|
188
222
|
|
|
189
|
-
|
|
190
|
-
|
|
223
|
+
append_enabled = pipeline_config['steps']['output'].get('append', True)
|
|
224
|
+
if chunks > 1 and append_enabled:
|
|
225
|
+
raise RuntimeError('Output mode must be set to "append = false" in "output" step when using more than one chunk')
|
|
191
226
|
|
|
192
227
|
|
|
193
228
|
if __name__ == "__main__":
|
|
@@ -32,18 +32,24 @@ def write_stats(stats,
|
|
|
32
32
|
Writes particle stats into the ouput file.
|
|
33
33
|
Appends if file already exists.
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
datafilename : str
|
|
38
|
+
Filame prefix for -STATS.h5 file that may or may not include a path
|
|
39
|
+
stats : DataFrame or xr.Dataset
|
|
40
|
+
particle statistics
|
|
41
|
+
export_name_len : int
|
|
42
|
+
Max number of chars allowed for col 'export name'
|
|
43
|
+
append : bool
|
|
44
|
+
Append all processed data into one nc file.
|
|
45
|
+
Defaults to True.
|
|
46
|
+
If False, then one nc file will be generated per raw image,
|
|
47
|
+
which can be loaded using :func:`pyopia.io.combine_stats_netcdf_files`
|
|
48
|
+
This is useful for larger datasets,
|
|
49
|
+
where appending causes substantial slowdown
|
|
50
|
+
as the dataset gets larger.
|
|
51
|
+
image_stats : xr.Dataset
|
|
52
|
+
summary statistics of each raw image (including those with no particles)
|
|
47
53
|
'''
|
|
48
54
|
|
|
49
55
|
if len(stats) == 0: # to avoid issue with wrong time datatypes in xarray
|
|
@@ -99,7 +105,7 @@ def make_xstats(stats, toml_steps):
|
|
|
99
105
|
|
|
100
106
|
Returns
|
|
101
107
|
-------
|
|
102
|
-
xarray.
|
|
108
|
+
xstats : xarray.Dataset
|
|
103
109
|
Xarray version of stats dataframe, including metadata
|
|
104
110
|
'''
|
|
105
111
|
xstats = stats.to_xarray()
|
|
@@ -120,8 +126,8 @@ def load_image_stats(datafilename):
|
|
|
120
126
|
|
|
121
127
|
Returns
|
|
122
128
|
-------
|
|
123
|
-
xarray.
|
|
124
|
-
|
|
129
|
+
image_stats : xarray.Dataset
|
|
130
|
+
summary statistics of each raw image (including those with no particles)
|
|
125
131
|
'''
|
|
126
132
|
with xarray.open_dataset(datafilename, engine=NETCDF_ENGINE, group='image_stats') as image_stats:
|
|
127
133
|
image_stats.load()
|
|
@@ -138,7 +144,7 @@ def load_stats(datafilename):
|
|
|
138
144
|
|
|
139
145
|
Returns
|
|
140
146
|
-------
|
|
141
|
-
DataFrame
|
|
147
|
+
stats : DataFrame
|
|
142
148
|
STATS DataFrame / xarray dataset
|
|
143
149
|
'''
|
|
144
150
|
|
|
@@ -170,8 +176,10 @@ def combine_stats_netcdf_files(path_to_data, prefix='*'):
|
|
|
170
176
|
|
|
171
177
|
Returns
|
|
172
178
|
-------
|
|
173
|
-
|
|
174
|
-
|
|
179
|
+
xstats : xarray.Dataset
|
|
180
|
+
Particle statistics and metatdata from processing steps
|
|
181
|
+
image_stats : xarray.Dataset
|
|
182
|
+
summary statistics of each raw image (including those with no particles)
|
|
175
183
|
'''
|
|
176
184
|
|
|
177
185
|
sorted_filelist = sorted(glob(os.path.join(path_to_data, prefix + 'Image-D*-STATS.nc')))
|
|
@@ -234,7 +242,7 @@ def steps_from_xstats(xstats):
|
|
|
234
242
|
|
|
235
243
|
Returns
|
|
236
244
|
-------
|
|
237
|
-
dict
|
|
245
|
+
steps : dict
|
|
238
246
|
TOML-formatted dictionary of pipeline steps
|
|
239
247
|
'''
|
|
240
248
|
steps = toml.loads(xstats.__getattr__('steps'))
|
|
@@ -251,7 +259,7 @@ def load_stats_as_dataframe(stats_file):
|
|
|
251
259
|
|
|
252
260
|
Returns
|
|
253
261
|
-------
|
|
254
|
-
DataFrame
|
|
262
|
+
stats : DataFrame
|
|
255
263
|
stats pandas dataframe
|
|
256
264
|
'''
|
|
257
265
|
# obtain particle statistics from the stats file
|
|
@@ -269,8 +277,10 @@ def show_h5_meta(h5file):
|
|
|
269
277
|
'''
|
|
270
278
|
prints metadata from an exported hdf5 file created from pyopia.process
|
|
271
279
|
|
|
272
|
-
|
|
273
|
-
|
|
280
|
+
Parameters
|
|
281
|
+
----------
|
|
282
|
+
h5file : str
|
|
283
|
+
h5 filename from exported data from pyopia.process
|
|
274
284
|
'''
|
|
275
285
|
|
|
276
286
|
with h5py.File(h5file, 'r') as f:
|
|
@@ -286,20 +296,26 @@ class StatsToDisc():
|
|
|
286
296
|
|
|
287
297
|
Replaces the old StatsH5 class
|
|
288
298
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
299
|
+
Parameters
|
|
300
|
+
----------
|
|
301
|
+
output_datafile : str
|
|
302
|
+
prefix path for output nc file
|
|
303
|
+
dataformat : str
|
|
304
|
+
either 'nc' or 'h5
|
|
305
|
+
export_name_len : int
|
|
306
|
+
max number of chars allowed for col 'export name'. Defaults to 40
|
|
307
|
+
append : bool
|
|
308
|
+
Append all processed data into one nc file.
|
|
309
|
+
Defaults to True.
|
|
310
|
+
If False, then one nc file will be generated per raw image,
|
|
311
|
+
which can be loaded using :func:`pyopia.io.combine_stats_netcdf_files`
|
|
312
|
+
This is useful for larger datasets, where appending causes substantial slowdown
|
|
313
|
+
as the dataset gets larger.
|
|
314
|
+
|
|
315
|
+
Returns
|
|
316
|
+
-------
|
|
317
|
+
data : dict
|
|
318
|
+
data from pipeline
|
|
303
319
|
|
|
304
320
|
Example config for pipeline useage:
|
|
305
321
|
|
|
@@ -10,6 +10,8 @@ from operator import methodcaller
|
|
|
10
10
|
import sys
|
|
11
11
|
from pyopia.io import steps_from_xstats as steps_from_xstats # noqa: E(F401)
|
|
12
12
|
import logging
|
|
13
|
+
from glob import glob
|
|
14
|
+
import numpy as np
|
|
13
15
|
|
|
14
16
|
logger = logging.getLogger()
|
|
15
17
|
|
|
@@ -82,11 +84,15 @@ class Pipeline():
|
|
|
82
84
|
def run(self, filename):
|
|
83
85
|
'''Method for executing the processing pipeline.
|
|
84
86
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
filename : str
|
|
90
|
+
file to be processed
|
|
87
91
|
|
|
88
|
-
Returns
|
|
89
|
-
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
stats : DataFrame
|
|
95
|
+
particle statistics associated with 'filename'
|
|
90
96
|
|
|
91
97
|
Note: the returned stats from this function are single-image only and not appended
|
|
92
98
|
if you loop through several filenames! It is recommended to use this step in the pipeline
|
|
@@ -257,11 +263,15 @@ class Data(TypedDict):
|
|
|
257
263
|
def steps_to_string(steps):
|
|
258
264
|
'''Deprecated. Convert pipeline steps dictionary to a human-readable string
|
|
259
265
|
|
|
260
|
-
|
|
261
|
-
|
|
266
|
+
Parameters
|
|
267
|
+
----------
|
|
268
|
+
steps : dict
|
|
269
|
+
pipeline steps dictionary
|
|
262
270
|
|
|
263
|
-
Returns
|
|
264
|
-
|
|
271
|
+
Returns
|
|
272
|
+
-------
|
|
273
|
+
steps_str : str
|
|
274
|
+
human-readable string of the types and variables
|
|
265
275
|
'''
|
|
266
276
|
|
|
267
277
|
steps_str = '\n'
|
|
@@ -322,3 +332,88 @@ def build_steps(toml_steps):
|
|
|
322
332
|
steps[step_name] = build_repr(toml_steps, step_name)
|
|
323
333
|
|
|
324
334
|
return steps
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class FilesToProcess:
|
|
338
|
+
def __init__(self, glob_pattern=None):
|
|
339
|
+
'''Build file list from glob pattern if specified.
|
|
340
|
+
Create FilesToProcess.chunked_files is chunks specified
|
|
341
|
+
File list from glob will be sorted.
|
|
342
|
+
|
|
343
|
+
Parameters
|
|
344
|
+
----------
|
|
345
|
+
glob_pattern : str, optional
|
|
346
|
+
Glob pattern, by default None
|
|
347
|
+
'''
|
|
348
|
+
self.files = None
|
|
349
|
+
self.background_files = []
|
|
350
|
+
self.chunked_files = []
|
|
351
|
+
if glob_pattern is not None:
|
|
352
|
+
self.files = sorted(glob(glob_pattern))
|
|
353
|
+
|
|
354
|
+
def from_filelist_file(self, path_to_filelist):
|
|
355
|
+
'''
|
|
356
|
+
Initialize explicit list of files to process from a text file.
|
|
357
|
+
The text file should contain one path to an image per line, which should be processed in order.
|
|
358
|
+
'''
|
|
359
|
+
with open(path_to_filelist, 'r') as fh:
|
|
360
|
+
self.files = list(fh.readlines())
|
|
361
|
+
|
|
362
|
+
def to_filelist_file(self, path_to_filelist):
|
|
363
|
+
'''Write file list to a txt file
|
|
364
|
+
|
|
365
|
+
Parameters
|
|
366
|
+
----------
|
|
367
|
+
path_to_filelist : str
|
|
368
|
+
Path to txt file to write
|
|
369
|
+
'''
|
|
370
|
+
with open(path_to_filelist, 'w') as fh:
|
|
371
|
+
[fh.writelines(L + '\n') for L in self.files]
|
|
372
|
+
|
|
373
|
+
def prepare_chunking(self, num_chunks, average_window, bgshift_function):
|
|
374
|
+
if num_chunks > len(self.files) // 2:
|
|
375
|
+
raise RuntimeError('Number of chunks exceeds more than half the number of files to process. Use less chunks.')
|
|
376
|
+
self.chunk_files(num_chunks)
|
|
377
|
+
self.build_initial_background_files(average_window=average_window)
|
|
378
|
+
self.insert_bg_files_into_chunks(bgshift_function=bgshift_function)
|
|
379
|
+
|
|
380
|
+
def chunk_files(self, num_chunks: int):
|
|
381
|
+
'''Chunk the file list and create FilesToProcess.chunked_files
|
|
382
|
+
|
|
383
|
+
Parameters
|
|
384
|
+
----------
|
|
385
|
+
chunks : int
|
|
386
|
+
number of chunks to produce (must be at least 1)
|
|
387
|
+
'''
|
|
388
|
+
if num_chunks < 1:
|
|
389
|
+
raise RuntimeError('You must have at least one chunk')
|
|
390
|
+
chunk_length = int(np.ceil(len(self.files) / num_chunks))
|
|
391
|
+
self.chunked_files = [self.files[i:i + chunk_length] for i in range(0, len(self.files), chunk_length)]
|
|
392
|
+
|
|
393
|
+
def insert_bg_files_into_chunks(self, bgshift_function='pass'):
|
|
394
|
+
average_window = len(self.background_files)
|
|
395
|
+
for i, chunk in enumerate(self.chunked_files):
|
|
396
|
+
if i > 0 and bgshift_function != 'pass':
|
|
397
|
+
# If the bgshift_function is not pass then we need to find a new set of
|
|
398
|
+
# background images for the start of next chunk. These will be the last
|
|
399
|
+
# average_window number of files from the previous chunk.
|
|
400
|
+
# If bgshift_function is 'pass', then we should use the same background files for all chunks
|
|
401
|
+
# so there is no need to extend the list of background files here
|
|
402
|
+
self.background_files.extend(self.chunked_files[i-1][-average_window:])
|
|
403
|
+
# we have to loop backwards over bg_files because we are inserting into the top of the chunk
|
|
404
|
+
chunk = [chunk.insert(0, bg_file) for bg_file in reversed(self.background_files[-average_window:])]
|
|
405
|
+
|
|
406
|
+
def build_initial_background_files(self, average_window=0):
|
|
407
|
+
'''
|
|
408
|
+
Create a list of files to use for initializing the background in the first chunk
|
|
409
|
+
'''
|
|
410
|
+
self.background_files = []
|
|
411
|
+
for f in self.files[0:average_window]:
|
|
412
|
+
self.background_files.append(f)
|
|
413
|
+
|
|
414
|
+
def __len__(self):
|
|
415
|
+
return len(self.files)
|
|
416
|
+
|
|
417
|
+
def __iter__(self):
|
|
418
|
+
for filename in self.files:
|
|
419
|
+
yield filename
|
|
@@ -9,12 +9,14 @@ import numpy as np
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def show_image(image, pixel_size):
|
|
12
|
-
'''
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
'''Plots a scaled figure (in mm) of an image
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
image : float
|
|
17
|
+
Image (usually a corrected image, such as im_corrected)
|
|
18
|
+
pixel_size : float
|
|
19
|
+
the pixel size (um) of the imaging system used
|
|
18
20
|
'''
|
|
19
21
|
r, c = np.shape(image[:, :, 0])
|
|
20
22
|
|
|
@@ -31,10 +33,12 @@ def montage_plot(montage, pixel_size):
|
|
|
31
33
|
'''
|
|
32
34
|
Plots a SilCam particle montage with a 1mm scale reference
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
montage : uint8
|
|
39
|
+
a montage created with scpp.make_montage
|
|
40
|
+
pixel_size : float
|
|
41
|
+
the pixel size (um) of the imaging system used
|
|
38
42
|
'''
|
|
39
43
|
msize = np.shape(montage)[0]
|
|
40
44
|
ex = pixel_size * np.float64(msize) / 1000.
|