PyOPIA 2.3.0__tar.gz → 2.4.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.3.0 → pyopia-2.4.0}/PKG-INFO +1 -1
- pyopia-2.4.0/pyopia/__init__.py +1 -0
- {pyopia-2.3.0 → pyopia-2.4.0}/pyopia/cli.py +57 -22
- {pyopia-2.3.0 → pyopia-2.4.0}/pyopia/pipeline.py +87 -0
- {pyopia-2.3.0 → pyopia-2.4.0}/pyopia/process.py +1 -1
- pyopia-2.3.0/pyopia/__init__.py +0 -1
- {pyopia-2.3.0 → pyopia-2.4.0}/LICENSE +0 -0
- {pyopia-2.3.0 → pyopia-2.4.0}/README.md +0 -0
- {pyopia-2.3.0 → pyopia-2.4.0}/pyopia/background.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.0}/pyopia/classify.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.0}/pyopia/exampledata.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.0}/pyopia/instrument/__init__.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.0}/pyopia/instrument/common.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.0}/pyopia/instrument/holo.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.0}/pyopia/instrument/silcam.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.0}/pyopia/instrument/uvp.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.0}/pyopia/io.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.0}/pyopia/plotting.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.0}/pyopia/simulator/silcam.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.0}/pyopia/statistics.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.0}/pyopia/tests/__init__.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.0}/pyopia/tests/test_classify.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.0}/pyopia/tests/test_notebooks.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.0}/pyopia/tests/test_pipeline.py +0 -0
- {pyopia-2.3.0 → pyopia-2.4.0}/pyproject.toml +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '2.4.0'
|
|
@@ -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__":
|
|
@@ -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
|
|
|
@@ -322,3 +324,88 @@ def build_steps(toml_steps):
|
|
|
322
324
|
steps[step_name] = build_repr(toml_steps, step_name)
|
|
323
325
|
|
|
324
326
|
return steps
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class FilesToProcess:
|
|
330
|
+
def __init__(self, glob_pattern=None):
|
|
331
|
+
'''Build file list from glob pattern if specified.
|
|
332
|
+
Create FilesToProcess.chunked_files is chunks specified
|
|
333
|
+
File list from glob will be sorted.
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
glob_pattern : str, optional
|
|
338
|
+
Glob pattern, by default None
|
|
339
|
+
'''
|
|
340
|
+
self.files = None
|
|
341
|
+
self.background_files = []
|
|
342
|
+
self.chunked_files = []
|
|
343
|
+
if glob_pattern is not None:
|
|
344
|
+
self.files = sorted(glob(glob_pattern))
|
|
345
|
+
|
|
346
|
+
def from_filelist_file(self, path_to_filelist):
|
|
347
|
+
'''
|
|
348
|
+
Initialize explicit list of files to process from a text file.
|
|
349
|
+
The text file should contain one path to an image per line, which should be processed in order.
|
|
350
|
+
'''
|
|
351
|
+
with open(path_to_filelist, 'r') as fh:
|
|
352
|
+
self.files = list(fh.readlines())
|
|
353
|
+
|
|
354
|
+
def to_filelist_file(self, path_to_filelist):
|
|
355
|
+
'''Write file list to a txt file
|
|
356
|
+
|
|
357
|
+
Parameters
|
|
358
|
+
----------
|
|
359
|
+
path_to_filelist : str
|
|
360
|
+
Path to txt file to write
|
|
361
|
+
'''
|
|
362
|
+
with open(path_to_filelist, 'w') as fh:
|
|
363
|
+
[fh.writelines(L + '\n') for L in self.files]
|
|
364
|
+
|
|
365
|
+
def prepare_chunking(self, num_chunks, average_window, bgshift_function):
|
|
366
|
+
if num_chunks > len(self.files) // 2:
|
|
367
|
+
raise RuntimeError('Number of chunks exceeds more than half the number of files to process. Use less chunks.')
|
|
368
|
+
self.chunk_files(num_chunks)
|
|
369
|
+
self.build_initial_background_files(average_window=average_window)
|
|
370
|
+
self.insert_bg_files_into_chunks(bgshift_function=bgshift_function)
|
|
371
|
+
|
|
372
|
+
def chunk_files(self, num_chunks: int):
|
|
373
|
+
'''Chunk the file list and create FilesToProcess.chunked_files
|
|
374
|
+
|
|
375
|
+
Parameters
|
|
376
|
+
----------
|
|
377
|
+
chunks : int
|
|
378
|
+
number of chunks to produce (must be at least 1)
|
|
379
|
+
'''
|
|
380
|
+
if num_chunks < 1:
|
|
381
|
+
raise RuntimeError('You must have at least one chunk')
|
|
382
|
+
chunk_length = int(np.ceil(len(self.files) / num_chunks))
|
|
383
|
+
self.chunked_files = [self.files[i:i + chunk_length] for i in range(0, len(self.files), chunk_length)]
|
|
384
|
+
|
|
385
|
+
def insert_bg_files_into_chunks(self, bgshift_function='pass'):
|
|
386
|
+
average_window = len(self.background_files)
|
|
387
|
+
for i, chunk in enumerate(self.chunked_files):
|
|
388
|
+
if i > 0 and bgshift_function != 'pass':
|
|
389
|
+
# If the bgshift_function is not pass then we need to find a new set of
|
|
390
|
+
# background images for the start of next chunk. These will be the last
|
|
391
|
+
# average_window number of files from the previous chunk.
|
|
392
|
+
# If bgshift_function is 'pass', then we should use the same background files for all chunks
|
|
393
|
+
# so there is no need to extend the list of background files here
|
|
394
|
+
self.background_files.extend(self.chunked_files[i-1][-average_window:])
|
|
395
|
+
# we have to loop backwards over bg_files because we are inserting into the top of the chunk
|
|
396
|
+
chunk = [chunk.insert(0, bg_file) for bg_file in reversed(self.background_files[-average_window:])]
|
|
397
|
+
|
|
398
|
+
def build_initial_background_files(self, average_window=0):
|
|
399
|
+
'''
|
|
400
|
+
Create a list of files to use for initializing the background in the first chunk
|
|
401
|
+
'''
|
|
402
|
+
self.background_files = []
|
|
403
|
+
for f in self.files[0:average_window]:
|
|
404
|
+
self.background_files.append(f)
|
|
405
|
+
|
|
406
|
+
def __len__(self):
|
|
407
|
+
return len(self.files)
|
|
408
|
+
|
|
409
|
+
def __iter__(self):
|
|
410
|
+
for filename in self.files:
|
|
411
|
+
yield filename
|
|
@@ -342,7 +342,7 @@ def measure_particles(imbw, max_particles=5000):
|
|
|
342
342
|
raise RuntimeError('Too many particles. Refer to documentation on max_particles parameter in measure_particles()')
|
|
343
343
|
# @todo handle situation when too many particles are found
|
|
344
344
|
|
|
345
|
-
region_properties = measure.regionprops(iml, cache=
|
|
345
|
+
region_properties = measure.regionprops(iml, cache=True)
|
|
346
346
|
|
|
347
347
|
return region_properties
|
|
348
348
|
|
pyopia-2.3.0/pyopia/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '2.3.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
|
|
File without changes
|