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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyOPIA
3
- Version: 2.3.0
3
+ Version: 2.4.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.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
- files = sorted(glob(pipeline_config['general']['raw_files']))
124
- nfiles = len(files)
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
- progress.console.print('[red]Renaming conflicting file to: ' +
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
- for filename in track(files, description=f'[blue]Processing progress through {nfiles} files:'):
145
- try:
146
- processing_pipeline.run(filename)
147
- except Exception as e:
148
- progress.console.print("[red]An error occured in processing, skipping rest of pipeline and moving to next image.")
149
- logger.error(e)
150
- logger.debug(''.join(traceback.format_tb(e.__traceback__)))
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
- print(log_level_name, log_level, log_file)
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, filename=log_file,
187
- datefmt='%Y-%m-%d %H:%M:%S')
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
- logger = logging.getLogger()
190
- logger.info(f'PyOPIA process started {pd.Timestamp.now()}')
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=False)
345
+ region_properties = measure.regionprops(iml, cache=True)
346
346
 
347
347
  return region_properties
348
348
 
@@ -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