PyOPIA 2.2.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.2.0 → pyopia-2.4.0}/PKG-INFO +1 -1
- pyopia-2.4.0/pyopia/__init__.py +1 -0
- {pyopia-2.2.0 → pyopia-2.4.0}/pyopia/cli.py +61 -23
- {pyopia-2.2.0 → pyopia-2.4.0}/pyopia/instrument/silcam.py +3 -3
- pyopia-2.4.0/pyopia/instrument/uvp.py +127 -0
- {pyopia-2.2.0 → pyopia-2.4.0}/pyopia/pipeline.py +87 -0
- {pyopia-2.2.0 → pyopia-2.4.0}/pyopia/process.py +1 -1
- pyopia-2.2.0/pyopia/__init__.py +0 -1
- {pyopia-2.2.0 → pyopia-2.4.0}/LICENSE +0 -0
- {pyopia-2.2.0 → pyopia-2.4.0}/README.md +0 -0
- {pyopia-2.2.0 → pyopia-2.4.0}/pyopia/background.py +0 -0
- {pyopia-2.2.0 → pyopia-2.4.0}/pyopia/classify.py +0 -0
- {pyopia-2.2.0 → pyopia-2.4.0}/pyopia/exampledata.py +0 -0
- {pyopia-2.2.0 → pyopia-2.4.0}/pyopia/instrument/__init__.py +0 -0
- {pyopia-2.2.0 → pyopia-2.4.0}/pyopia/instrument/common.py +0 -0
- {pyopia-2.2.0 → pyopia-2.4.0}/pyopia/instrument/holo.py +0 -0
- {pyopia-2.2.0 → pyopia-2.4.0}/pyopia/io.py +0 -0
- {pyopia-2.2.0 → pyopia-2.4.0}/pyopia/plotting.py +0 -0
- {pyopia-2.2.0 → pyopia-2.4.0}/pyopia/simulator/silcam.py +0 -0
- {pyopia-2.2.0 → pyopia-2.4.0}/pyopia/statistics.py +0 -0
- {pyopia-2.2.0 → pyopia-2.4.0}/pyopia/tests/__init__.py +0 -0
- {pyopia-2.2.0 → pyopia-2.4.0}/pyopia/tests/test_classify.py +0 -0
- {pyopia-2.2.0 → pyopia-2.4.0}/pyopia/tests/test_notebooks.py +0 -0
- {pyopia-2.2.0 → pyopia-2.4.0}/pyopia/tests/test_pipeline.py +0 -0
- {pyopia-2.2.0 → pyopia-2.4.0}/pyproject.toml +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '2.4.0'
|
|
@@ -4,17 +4,19 @@ 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
|
|
17
18
|
import pyopia.instrument.holo
|
|
19
|
+
import pyopia.instrument.uvp
|
|
18
20
|
import pyopia.instrument.common
|
|
19
21
|
import pyopia.io
|
|
20
22
|
import pyopia.pipeline
|
|
@@ -80,7 +82,7 @@ def generate_config(instrument: str, raw_files: str, model_path: str, outfolder:
|
|
|
80
82
|
Parameters
|
|
81
83
|
----------
|
|
82
84
|
instrument : str
|
|
83
|
-
either `silcam` or `
|
|
85
|
+
either `silcam`, `holo` or `uvp`
|
|
84
86
|
raw_files : str
|
|
85
87
|
raw_files
|
|
86
88
|
model_path : str
|
|
@@ -95,6 +97,8 @@ def generate_config(instrument: str, raw_files: str, model_path: str, outfolder:
|
|
|
95
97
|
pipeline_config = pyopia.instrument.silcam.generate_config(raw_files, model_path, outfolder, output_prefix)
|
|
96
98
|
case 'holo':
|
|
97
99
|
pipeline_config = pyopia.instrument.holo.generate_config(raw_files, model_path, outfolder, output_prefix)
|
|
100
|
+
case 'uvp':
|
|
101
|
+
pipeline_config = pyopia.instrument.uvp.generate_config(raw_files, model_path, outfolder, output_prefix)
|
|
98
102
|
|
|
99
103
|
config_filename = instrument + "-config.toml"
|
|
100
104
|
with open(config_filename, "w") as toml_file:
|
|
@@ -102,23 +106,35 @@ def generate_config(instrument: str, raw_files: str, model_path: str, outfolder:
|
|
|
102
106
|
|
|
103
107
|
|
|
104
108
|
@app.command()
|
|
105
|
-
def process(config_filename: str):
|
|
109
|
+
def process(config_filename: str, num_chunks: int = 1):
|
|
106
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
|
+
|
|
107
119
|
'''
|
|
108
120
|
from pyopia.io import load_toml
|
|
109
121
|
from pyopia.pipeline import Pipeline
|
|
110
122
|
|
|
111
|
-
logger = logging.getLogger()
|
|
112
|
-
|
|
113
123
|
with Progress(transient=True) as progress:
|
|
114
124
|
progress.console.print("[blue]LOAD CONFIG")
|
|
115
125
|
pipeline_config = load_toml(config_filename)
|
|
116
126
|
|
|
117
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)
|
|
118
132
|
|
|
119
133
|
progress.console.print("[blue]OBTAIN FILE LIST")
|
|
120
|
-
|
|
121
|
-
|
|
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)
|
|
122
138
|
|
|
123
139
|
progress.console.print('[blue]PREPARE FOLDERS')
|
|
124
140
|
if 'output' not in pipeline_config['steps']:
|
|
@@ -131,20 +147,32 @@ def process(config_filename: str):
|
|
|
131
147
|
if os.path.isfile(output_datafile + '-STATS.nc'):
|
|
132
148
|
dt_now = datetime.datetime.now().strftime('D%Y%m%dT%H%M%S')
|
|
133
149
|
newname = output_datafile + '-conflict-' + str(dt_now) + '-STATS.nc'
|
|
134
|
-
|
|
135
|
-
newname)
|
|
150
|
+
logger.warning(f'Renaming conflicting file to: {newname}')
|
|
136
151
|
os.rename(output_datafile + '-STATS.nc', newname)
|
|
137
152
|
|
|
138
153
|
progress.console.print("[blue]INITIALISE PIPELINE")
|
|
139
|
-
processing_pipeline = Pipeline(pipeline_config)
|
|
140
154
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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()
|
|
148
176
|
|
|
149
177
|
|
|
150
178
|
@app.command()
|
|
@@ -176,15 +204,25 @@ def setup_logging(pipeline_config):
|
|
|
176
204
|
log_file = pipeline_config['general'].get('log_file', None)
|
|
177
205
|
log_level_name = pipeline_config['general'].get('log_level', 'INFO')
|
|
178
206
|
log_level = getattr(logging, log_level_name)
|
|
179
|
-
|
|
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')]
|
|
180
213
|
|
|
181
214
|
# Configure logger
|
|
182
|
-
log_format = '%(asctime)s %(levelname)s [%(module)s.%(funcName)s] %(message)s'
|
|
183
|
-
logging.basicConfig(level=log_level, format=log_format,
|
|
184
|
-
|
|
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')
|
|
185
222
|
|
|
186
|
-
|
|
187
|
-
|
|
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')
|
|
188
226
|
|
|
189
227
|
|
|
190
228
|
if __name__ == "__main__":
|
|
@@ -34,9 +34,9 @@ def load_image(filename):
|
|
|
34
34
|
Returns
|
|
35
35
|
-------
|
|
36
36
|
array
|
|
37
|
-
raw image
|
|
37
|
+
raw image float between 0-1
|
|
38
38
|
'''
|
|
39
|
-
img = np.load(filename, allow_pickle=False).astype(np.float64)
|
|
39
|
+
img = np.load(filename, allow_pickle=False).astype(np.float64) / 255
|
|
40
40
|
return img
|
|
41
41
|
|
|
42
42
|
|
|
@@ -68,7 +68,7 @@ class SilCamLoad():
|
|
|
68
68
|
|
|
69
69
|
def __call__(self, data):
|
|
70
70
|
timestamp = timestamp_from_filename(data['filename'])
|
|
71
|
-
img = load_image(data['filename'])
|
|
71
|
+
img = load_image(data['filename'])
|
|
72
72
|
data['timestamp'] = timestamp
|
|
73
73
|
data['imraw'] = img
|
|
74
74
|
return data
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Module containing UVP specific tools to enable compatability with the :mod:`pyopia.pipeline`
|
|
3
|
+
'''
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import skimage.io
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def timestamp_from_filename(filename):
|
|
12
|
+
'''get a pandas timestamp from a UVP vignette image filename
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
filename (string): UVP filename (.png)
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
timestamp: timestamp from pandas.to_datetime()
|
|
19
|
+
'''
|
|
20
|
+
|
|
21
|
+
# get the timestamp of the image (in this case from the filename)
|
|
22
|
+
timestr = os.path.split(filename)[-1].strip('.png')
|
|
23
|
+
timestamp = pd.to_datetime(timestr)
|
|
24
|
+
return timestamp
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_image(filename):
|
|
28
|
+
'''load a UVP .png file from disc
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
filename : string
|
|
33
|
+
filename to load
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
array
|
|
38
|
+
raw image float between 0-1, inverted so that particles are dark on a light background
|
|
39
|
+
'''
|
|
40
|
+
img_darkfield = skimage.io.imread(filename).astype(np.float64)
|
|
41
|
+
img_inverted = (255 - img_darkfield) / 255
|
|
42
|
+
return img_inverted
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class UVPLoad():
|
|
46
|
+
'''PyOpia pipline-compatible class for loading a single UVP image
|
|
47
|
+
using :func:`pyopia.instrument.uvp.load_image`
|
|
48
|
+
and extracting the timestamp using
|
|
49
|
+
:func:`pyopia.instrument.uvp.timestamp_from_filename`
|
|
50
|
+
|
|
51
|
+
Pipeline input data:
|
|
52
|
+
---------
|
|
53
|
+
:class:`pyopia.pipeline.Data`
|
|
54
|
+
containing the following keys:
|
|
55
|
+
|
|
56
|
+
:attr:`pyopia.pipeline.Data.filename`
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
--------
|
|
60
|
+
:class:`pyopia.pipeline.Data`
|
|
61
|
+
containing the following new keys:
|
|
62
|
+
|
|
63
|
+
:attr:`pyopia.pipeline.Data.timestamp`
|
|
64
|
+
|
|
65
|
+
:attr:`pyopia.pipeline.Data.img`
|
|
66
|
+
'''
|
|
67
|
+
|
|
68
|
+
def __init__(self):
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
def __call__(self, data):
|
|
72
|
+
timestamp = timestamp_from_filename(data['filename'])
|
|
73
|
+
img = load_image(data['filename'])
|
|
74
|
+
data['timestamp'] = timestamp
|
|
75
|
+
data['imraw'] = img
|
|
76
|
+
return data
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def generate_config(raw_files: str, model_path: str, outfolder: str, output_prefix: str):
|
|
80
|
+
'''Generate example uvp config.toml as a dict
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
raw_files : str
|
|
85
|
+
raw_files
|
|
86
|
+
model_path : str
|
|
87
|
+
model_path
|
|
88
|
+
outfolder : str
|
|
89
|
+
outfolder
|
|
90
|
+
output_prefix : str
|
|
91
|
+
output_prefix
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
--------
|
|
95
|
+
dict
|
|
96
|
+
pipeline_config toml dict
|
|
97
|
+
'''
|
|
98
|
+
# define the configuration to use in the processing pipeline - given as a dictionary - with some values defined above
|
|
99
|
+
pipeline_config = {
|
|
100
|
+
'general': {
|
|
101
|
+
'raw_files': raw_files,
|
|
102
|
+
'pixel_size': 80 # pixel size in um
|
|
103
|
+
},
|
|
104
|
+
'steps': {
|
|
105
|
+
'classifier': {
|
|
106
|
+
'pipeline_class': 'pyopia.classify.Classify',
|
|
107
|
+
'model_path': model_path
|
|
108
|
+
},
|
|
109
|
+
'load': {
|
|
110
|
+
'pipeline_class': 'pyopia.instrument.uvp.UVPLoad'
|
|
111
|
+
},
|
|
112
|
+
'segmentation': {
|
|
113
|
+
'pipeline_class': 'pyopia.process.Segment',
|
|
114
|
+
'threshold': 0.95,
|
|
115
|
+
'segment_source': 'imraw'
|
|
116
|
+
},
|
|
117
|
+
'statextract': {
|
|
118
|
+
'pipeline_class': 'pyopia.process.CalculateStats',
|
|
119
|
+
'roi_source': 'imraw'
|
|
120
|
+
},
|
|
121
|
+
'output': {
|
|
122
|
+
'pipeline_class': 'pyopia.io.StatsH5',
|
|
123
|
+
'output_datafile': os.path.join(outfolder, output_prefix)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return pipeline_config
|
|
@@ -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.2.0/pyopia/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '2.2.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
|