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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyOPIA
3
- Version: 2.3.0
3
+ Version: 2.4.1
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.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
- Args:
12
- bgfiles (list) : list of strings of filenames to be used in background creation
13
- load_function (function object) : this function should take a filename and return an image,
14
- for example: :func:`pyopia.instrument.silcam.load_image`
15
- Returns:
16
- bgstack (list) : list of all images in the background stack
17
- imbg (array) : background image
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
- Args:
37
- bgstack (list) : list of all images in the background stack
38
- imbg (uint8) : background image
39
- imnew (unit8) : new image to be added to stack
40
-
41
- Returns:
42
- bgstack (updated list of all background images)
43
- imbg (updated actual background image)
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
- Args:
60
- bgstack (list) : list of all images in the background stack
61
- imbg (uint8) : background image
62
- imnew (unit8) : new image to be added to stack
63
-
64
- Returns:
65
- bgstack (updated list of all background images)
66
- imbg (updated actual background image)
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
- Args:
86
- imbg (float64) : background averaged image
87
- imraw (float64) : raw image
88
- imbg (float64) : background averaged image
89
- imraw (float64) : raw image
90
-
91
- Returns:
92
- im_corrected (float64) : corrected image, same type as input
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
- Args:
112
- imraw (float64) : raw image
113
- imbg (float64) : background averaged image
114
-
115
- Returns:
116
- im_corrected (float64) : corrected image
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
- Args:
134
- bgstack (list) : list of all images in the background stack
135
- imbg (float64) : background image
136
- imraw (float64) : raw image
137
- stacklength (int) : unsed int here - just there to maintain the same behaviour as
138
- shift_bgstack_fast()
139
- real_time_stats=False (Bool) : if True use fast functions, if False use accurate functions
140
-
141
- Returns:
142
- bgstack (list) : list of all images in the background stack
143
- imbg (float64) : background averaged image
144
- im_corrected (float64) : corrected image
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
- 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__":
@@ -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
- Args:
36
- datafilename (str) : Filame prefix for -STATS.h5 file that may or may not include a path
37
- stats (DataFrame or xr.Dataset) : STATS dataframe
38
- export_name_len (int) : Max number of chars allowed for col 'export name'
39
- append (bool) : Append all processed data into one nc file.
40
- Defaults to True.
41
- If False, then one nc file will be generated per raw image,
42
- which can be loaded using :func:`pyopia.io.combine_stats_netcdf_files`
43
- This is useful for larger datasets,
44
- where appending causes substantial slowdown
45
- as the dataset gets larger.
46
- image_stats (xr.Dataset) : image_stats data
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.DataSet
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.DataArray
124
- image_stats
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
- tuple
174
- xstats STATS xarray dataset, image_stats dataset
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
- Args:
273
- h5file : h5 filename from exported data from pyopia.process
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
- Args:
290
- output_datafile (str): prefix path for output nc file
291
- dataformat (str): either 'nc' or 'h5
292
- append (bool): if to allow append to an existing STATS file. Defaults to True
293
- export_name_len (int): max number of chars allowed for col 'export name'. Defaults to 40
294
- append (bool): Append all processed data into one nc file.
295
- Defaults to True.
296
- If False, then one nc file will be generated per raw image,
297
- which can be loaded using :func:`pyopia.io.combine_stats_netcdf_files`
298
- This is useful for larger datasets, where appending causes substantial slowdown
299
- as the dataset gets larger.
300
-
301
- Returns:
302
- data (dict): data from pipeline
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
- Args:
86
- filename (str): file to be processed
87
+ Parameters
88
+ ----------
89
+ filename : str
90
+ file to be processed
87
91
 
88
- Returns:
89
- stats (DataFrame): stats DataFrame of particle statistics associated with 'filename'
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
- Args:
261
- steps (dict): pipeline steps dictionary
266
+ Parameters
267
+ ----------
268
+ steps : dict
269
+ pipeline steps dictionary
262
270
 
263
- Returns:
264
- str: human-readable string of the types and variables
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
- Plots a scaled figure (in mm) of an image
14
-
15
- Args:
16
- image (float) : Image (usually a corrected image, such as im_corrected)
17
- pixel_size (float) : the pixel size (um) of the imaging system used
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
- Args:
35
- montage (uint8) : a SilCam montage created with scpp.make_montage
36
- pixel_size (float) : the pixel size of the SilCam used, obtained from settings.PostProcess.pix_size in the
37
- config ini file
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.