pymodaq_plugins_utils 5.0.2__tar.gz → 5.0.4__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.
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/PKG-INFO +1 -1
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/src/pymodaq_plugins_utils/hardware/camera_base_pylablib.py +145 -64
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/.gitattributes +0 -0
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/.github/workflows/Test.yml +0 -0
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/.github/workflows/Testbase.yml +0 -0
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/.github/workflows/compatibility.yml +0 -0
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/.github/workflows/python-publish.yml +0 -0
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/.github/workflows/updater.yml +0 -0
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/.gitignore +0 -0
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/LICENSE +0 -0
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/README.rst +0 -0
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/hatch_build.py +0 -0
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/icon.ico +0 -0
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/pyproject.toml +5 -5
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/src/pymodaq_plugins_utils/__init__.py +0 -0
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/src/pymodaq_plugins_utils/app/__init__.py +0 -0
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/src/pymodaq_plugins_utils/exporters/__init__.py +0 -0
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/src/pymodaq_plugins_utils/extensions/__init__.py +0 -0
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/src/pymodaq_plugins_utils/extensions/custom_extension_template.py +0 -0
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/src/pymodaq_plugins_utils/hardware/__init__.py +0 -0
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/src/pymodaq_plugins_utils/models/__init__.py +0 -0
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/src/pymodaq_plugins_utils/resources/__init__.py +0 -0
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/src/pymodaq_plugins_utils/resources/config_template.toml +0 -0
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/src/pymodaq_plugins_utils/scanners/__init__.py +0 -0
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/src/pymodaq_plugins_utils/utils.py +0 -0
- {pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/tests/test_plugin_package_structure.py +0 -0
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from typing import Type
|
|
3
|
+
|
|
1
4
|
import cv2
|
|
5
|
+
|
|
6
|
+
from pymodaq_data import DataToExport
|
|
2
7
|
from pymodaq_utils.logger import set_logger, get_module_name
|
|
3
8
|
from pymodaq_utils.utils import ThreadCommand
|
|
4
9
|
from pymodaq_gui.parameter import Parameter
|
|
@@ -10,15 +15,20 @@ except ImportError:
|
|
|
10
15
|
from pymodaq.utils.data import DataFromPlugins, Axis
|
|
11
16
|
from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters, main
|
|
12
17
|
|
|
18
|
+
from pylablib.devices.interface.camera import trim_frames
|
|
19
|
+
|
|
13
20
|
from qtpy import QtWidgets, QtCore
|
|
14
21
|
import numpy as np
|
|
15
22
|
from time import perf_counter
|
|
16
23
|
|
|
17
24
|
|
|
25
|
+
logger = set_logger(get_module_name(__file__))
|
|
26
|
+
|
|
27
|
+
|
|
18
28
|
cam_params = [
|
|
19
29
|
{'title': 'Camera name:', 'name': 'camera_name', 'type': 'str', 'value': '', 'readonly': True},
|
|
20
|
-
{'title': '
|
|
21
|
-
|
|
30
|
+
{'title': 'Color Conversion:', 'name': 'color_conversion', 'type': 'list',
|
|
31
|
+
'limits': ['None', 'RGB2GRAY', 'BAYER_BG2RGB', 'BAYER_BG2GRAY']},
|
|
22
32
|
{'title': 'ROI', 'name': 'roi', 'type': 'group', 'children': [
|
|
23
33
|
{'title': 'Update ROI from Viewer', 'name': 'update_roi', 'type': 'led', 'value': False},
|
|
24
34
|
{'title': 'Apply ROI', 'name': 'apply_roi', 'type': 'led', 'value': False},
|
|
@@ -42,6 +52,60 @@ cam_params = [
|
|
|
42
52
|
]
|
|
43
53
|
|
|
44
54
|
|
|
55
|
+
@dataclasses.dataclass
|
|
56
|
+
class Grab:
|
|
57
|
+
do_acquisition: bool = True
|
|
58
|
+
snap: bool = False
|
|
59
|
+
since: str = 'now'
|
|
60
|
+
nframes: int = 1
|
|
61
|
+
n_average: int = 1
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CameraCallback(QtCore.QObject):
|
|
65
|
+
"""Callback object """
|
|
66
|
+
data_sig = QtCore.Signal(np.ndarray)
|
|
67
|
+
error = QtCore.Signal()
|
|
68
|
+
|
|
69
|
+
def __init__(self, controller):
|
|
70
|
+
super().__init__()
|
|
71
|
+
# Set the wait function
|
|
72
|
+
self.controller = controller
|
|
73
|
+
self.do_acquisition = True
|
|
74
|
+
|
|
75
|
+
def set_do_grab(self, mode: Grab):
|
|
76
|
+
self.do_acquisition = mode.do_acquisition
|
|
77
|
+
if mode.do_acquisition:
|
|
78
|
+
self.wait_for_acquisition(mode)
|
|
79
|
+
|
|
80
|
+
def wait_for_acquisition(self, mode: Grab):
|
|
81
|
+
while self.do_acquisition:
|
|
82
|
+
try:
|
|
83
|
+
ind_average = 0
|
|
84
|
+
while ind_average < mode.n_average:
|
|
85
|
+
ind_frames = 0
|
|
86
|
+
while ind_frames < mode.nframes:
|
|
87
|
+
self.controller.wait_for_frame(since='now')
|
|
88
|
+
new_frames, rng = self.controller.read_multiple_images(missing_frame='skip', return_rng=True)
|
|
89
|
+
if ind_average == 0 and ind_frames == 0:
|
|
90
|
+
shape = list(new_frames.shape[1:])
|
|
91
|
+
shape = [mode.n_average, mode.nframes] + shape
|
|
92
|
+
frames = np.zeros(shape, dtype=new_frames.dtype)
|
|
93
|
+
nacq = rng[1] - rng[0]
|
|
94
|
+
frames[ind_average, ind_frames:nacq, ...] = new_frames
|
|
95
|
+
ind_frames += nacq
|
|
96
|
+
ind_average += 1
|
|
97
|
+
self.data_sig.emit(frames)
|
|
98
|
+
QtCore.QThread.msleep(10)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.exception(str(e))
|
|
101
|
+
self.error.emit()
|
|
102
|
+
break
|
|
103
|
+
QtWidgets.QApplication.processEvents()
|
|
104
|
+
if not self.do_acquisition or mode.snap:
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
45
109
|
class CameraBasePyLabLib(DAQ_Viewer_base):
|
|
46
110
|
"""
|
|
47
111
|
Base implementation for Camera using pylablib framework. Works for TSI and uc480 thorlabs camera and rpobaly others
|
|
@@ -52,13 +116,15 @@ class CameraBasePyLabLib(DAQ_Viewer_base):
|
|
|
52
116
|
|
|
53
117
|
params = comon_parameters + serial_params + cam_params
|
|
54
118
|
|
|
55
|
-
callback_signal = QtCore.Signal(
|
|
119
|
+
callback_signal = QtCore.Signal(Grab)
|
|
56
120
|
live_mode_available = True
|
|
57
|
-
|
|
121
|
+
hardware_averaging = True
|
|
58
122
|
|
|
59
123
|
def ini_attributes(self):
|
|
60
124
|
self.controller = None
|
|
61
125
|
self.callback_thread: QtCore.QThread = None
|
|
126
|
+
self.is_live: bool = False
|
|
127
|
+
self.Naverage: int = 1
|
|
62
128
|
|
|
63
129
|
self.x_axis: Axis = None
|
|
64
130
|
self.y_axis: Axis = None
|
|
@@ -135,16 +201,16 @@ class CameraBasePyLabLib(DAQ_Viewer_base):
|
|
|
135
201
|
if param.name() == "exposure_time":
|
|
136
202
|
self.controller.set_exposure(param.value()/1000)
|
|
137
203
|
|
|
138
|
-
|
|
204
|
+
elif param.name() == "fps_on":
|
|
139
205
|
self.settings.child('timing_opts', 'fps').setOpts(visible=param.value())
|
|
140
206
|
|
|
141
|
-
|
|
207
|
+
elif param.name() == "apply_roi":
|
|
142
208
|
if param.value(): # Switching on ROI
|
|
143
209
|
self.apply_roi()
|
|
144
210
|
else:
|
|
145
211
|
self.clear_roi()
|
|
146
212
|
|
|
147
|
-
|
|
213
|
+
elif param.name() in ['x_binning', 'y_binning']:
|
|
148
214
|
# We handle ROI and binning separately for clarity
|
|
149
215
|
(x0, w, y0, h, *_) = self.controller.get_roi() # Get current ROI
|
|
150
216
|
xbin = self.settings['roi', 'x_binning']
|
|
@@ -152,7 +218,7 @@ class CameraBasePyLabLib(DAQ_Viewer_base):
|
|
|
152
218
|
new_roi = (x0, w, xbin, y0, h, ybin)
|
|
153
219
|
self.update_rois(new_roi)
|
|
154
220
|
|
|
155
|
-
|
|
221
|
+
elif param.name() == "clear_roi":
|
|
156
222
|
if param.value(): # Switching on ROI
|
|
157
223
|
self.clear_roi()
|
|
158
224
|
param.setValue(False)
|
|
@@ -175,12 +241,13 @@ class CameraBasePyLabLib(DAQ_Viewer_base):
|
|
|
175
241
|
initialized: bool
|
|
176
242
|
False if initialization failed otherwise True
|
|
177
243
|
"""
|
|
244
|
+
|
|
178
245
|
self.ini_detector_custom(controller)
|
|
179
246
|
|
|
180
247
|
self.get_device_info()
|
|
181
|
-
self.get_set_color()
|
|
182
248
|
self.get_set_main_parameters()
|
|
183
249
|
self.setup_callback_thread()
|
|
250
|
+
self.controller.set_frame_format("array")
|
|
184
251
|
|
|
185
252
|
info = "Initialized camera"
|
|
186
253
|
initialized = True
|
|
@@ -196,11 +263,6 @@ class CameraBasePyLabLib(DAQ_Viewer_base):
|
|
|
196
263
|
elif hasattr(device_info, 'model'):
|
|
197
264
|
self.settings.child('camera_name').setValue(device_info.model)
|
|
198
265
|
|
|
199
|
-
def get_set_color(self):
|
|
200
|
-
if 'monochrome' in self.settings['sensor'].lower():
|
|
201
|
-
self.settings.child('output_color').setValue('MonoChrome')
|
|
202
|
-
self.settings.child('output_color').setOpts(visible=False)
|
|
203
|
-
|
|
204
266
|
def get_set_main_parameters(self):
|
|
205
267
|
# Set exposure time
|
|
206
268
|
self.controller.set_exposure(self.settings['timing_opts', 'exposure_time']/1000)
|
|
@@ -222,18 +284,28 @@ class CameraBasePyLabLib(DAQ_Viewer_base):
|
|
|
222
284
|
self.settings.child('roi', 'roi_slices').setValue(str(slices))
|
|
223
285
|
self.compute_axes()
|
|
224
286
|
|
|
287
|
+
@property
|
|
288
|
+
def callback(self) -> Type[CameraCallback]:
|
|
289
|
+
""" Return the class handling the wait for acquisition and signal emission
|
|
290
|
+
|
|
291
|
+
Should be reimplement as well as CameraCallback if needed
|
|
292
|
+
"""
|
|
293
|
+
return CameraCallback
|
|
294
|
+
|
|
225
295
|
def setup_callback_thread(self):
|
|
226
296
|
# Way to define a wait function with arguments
|
|
227
297
|
wait_func = lambda: self.controller.wait_for_frame(since=self.settings['buffer', 'mode'],
|
|
228
298
|
nframes=1, timeout=20.0)
|
|
229
|
-
callback = CameraCallback(
|
|
299
|
+
callback = CameraCallback(self.controller)
|
|
230
300
|
self.settings.child('buffer', 'mode').setReadonly(True)
|
|
231
301
|
|
|
232
302
|
|
|
233
303
|
self.callback_thread = QtCore.QThread() # creation of a Qt5 thread
|
|
234
304
|
callback.moveToThread(self.callback_thread) # callback object will live within this thread
|
|
305
|
+
|
|
235
306
|
callback.data_sig.connect(
|
|
236
307
|
self.emit_data) # when the wait for acquisition returns (with data taken), emit_data will be fired
|
|
308
|
+
callback.error.connect(self.handle_error)
|
|
237
309
|
|
|
238
310
|
self.callback_signal.connect(callback.set_do_grab)
|
|
239
311
|
self.callback_thread.callback = callback
|
|
@@ -241,6 +313,8 @@ class CameraBasePyLabLib(DAQ_Viewer_base):
|
|
|
241
313
|
|
|
242
314
|
self._prepare_view()
|
|
243
315
|
|
|
316
|
+
def handle_error(self):
|
|
317
|
+
self.stop()
|
|
244
318
|
|
|
245
319
|
def _prepare_view(self):
|
|
246
320
|
"""Preparing a data viewer by emitting temporary data. Typically, needs to be called whenever the
|
|
@@ -275,14 +349,20 @@ class CameraBasePyLabLib(DAQ_Viewer_base):
|
|
|
275
349
|
"""
|
|
276
350
|
try:
|
|
277
351
|
# Warning, acquisition_in_progress returns 1,0 and not a real bool
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
self.
|
|
352
|
+
self.is_live = kwargs.get('live', False)
|
|
353
|
+
self.Naverage = Naverage
|
|
354
|
+
|
|
355
|
+
self.n_frames = 1
|
|
356
|
+
|
|
357
|
+
if not self.controller.acquisition_in_progress():
|
|
358
|
+
self.controller.clear_acquisition()
|
|
359
|
+
self.controller.start_acquisition(nframes=self.n_frames)
|
|
360
|
+
#Then start the acquisition
|
|
361
|
+
self.callback_signal.emit(Grab(do_acquisition=True,
|
|
362
|
+
snap=not self.is_live,
|
|
363
|
+
n_average=Naverage,
|
|
364
|
+
nframes=self.n_frames,
|
|
365
|
+
since=self.settings['buffer', 'mode']))
|
|
286
366
|
|
|
287
367
|
except Exception as e:
|
|
288
368
|
self.emit_status(ThreadCommand('Update_Status', [str(e), "log"]))
|
|
@@ -304,21 +384,48 @@ class CameraBasePyLabLib(DAQ_Viewer_base):
|
|
|
304
384
|
if frame is None:
|
|
305
385
|
frame = self.controller.read_newest_image()
|
|
306
386
|
# Emit the frame.
|
|
307
|
-
if frame is not None:
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
387
|
+
if frame is not None:
|
|
388
|
+
conversion_str = self.settings['color_conversion']
|
|
389
|
+
if conversion_str != "None":
|
|
390
|
+
for ind_average in range(frame.shape[0]):
|
|
391
|
+
for ind_frame in range(frame.shape[1]):
|
|
392
|
+
if ind_frame == 0 and ind_average == 0:
|
|
393
|
+
new_frame = cv2.cvtColor(frame[ind_average, ind_frame, ...],
|
|
394
|
+
getattr(cv2, f'COLOR_{conversion_str}'))
|
|
395
|
+
shape = [frame.shape[0], frame.shape[1]] + list(new_frame.shape)
|
|
396
|
+
out_frames = np.zeros(shape, dtype=new_frame.dtype)
|
|
397
|
+
out_frames[ind_average, ind_frame, ...] = new_frame
|
|
398
|
+
else:
|
|
399
|
+
cv2.cvtColor(frame[ind_average, ind_frame, ...],
|
|
400
|
+
getattr(cv2, f'COLOR_{conversion_str}'),
|
|
401
|
+
out_frames[ind_average, ind_frame, ...])
|
|
402
|
+
else:
|
|
403
|
+
out_frames = frame
|
|
404
|
+
if self.Naverage > 1:
|
|
405
|
+
out_frames = np.sum(out_frames, axis=0) / self.Naverage
|
|
406
|
+
else:
|
|
407
|
+
out_frames = out_frames[0, ...]
|
|
408
|
+
|
|
409
|
+
if self.n_frames > 1:
|
|
410
|
+
pass
|
|
411
|
+
#todo handle chunks of frames in ND data
|
|
412
|
+
else:
|
|
413
|
+
out_frames = out_frames[0, ...]
|
|
414
|
+
|
|
415
|
+
if out_frames.shape[-1] == 3:
|
|
416
|
+
data_arrays = [np.atleast_1d(out_frames[..., ind]) for ind in range(3)]
|
|
417
|
+
labels = ['Red', 'Green', 'Blue']
|
|
311
418
|
else:
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
419
|
+
labels = ['Intensity']
|
|
420
|
+
data_arrays = [out_frames]
|
|
421
|
+
|
|
422
|
+
self.dte_signal.emit(
|
|
423
|
+
DataToExport('Camera',
|
|
424
|
+
data=[DataFromPlugins(name='Camera',
|
|
425
|
+
data=data_arrays,
|
|
426
|
+
dim=self.data_shape,
|
|
427
|
+
labels=labels,
|
|
428
|
+
axes=[self.y_axis, self.x_axis])]))
|
|
322
429
|
if self.settings.child('timing_opts', 'fps_on').value():
|
|
323
430
|
self.update_fps()
|
|
324
431
|
|
|
@@ -362,37 +469,11 @@ class CameraBasePyLabLib(DAQ_Viewer_base):
|
|
|
362
469
|
|
|
363
470
|
def stop(self):
|
|
364
471
|
"""Stop the acquisition."""
|
|
365
|
-
self.callback_signal.emit(False)
|
|
472
|
+
self.callback_signal.emit(Grab(do_acquisition=False))
|
|
366
473
|
QtWidgets.QApplication.processEvents()
|
|
367
|
-
|
|
368
474
|
self.controller.clear_acquisition()
|
|
369
475
|
return ''
|
|
370
476
|
|
|
371
477
|
|
|
372
|
-
class CameraCallback(QtCore.QObject):
|
|
373
|
-
"""Callback object """
|
|
374
|
-
data_sig = QtCore.Signal()
|
|
375
|
-
|
|
376
|
-
def __init__(self, wait_fn):
|
|
377
|
-
super().__init__()
|
|
378
|
-
# Set the wait function
|
|
379
|
-
self.wait_fn = wait_fn
|
|
380
|
-
self.do_grab = True
|
|
381
|
-
|
|
382
|
-
def set_do_grab(self, do_grab=True):
|
|
383
|
-
self.do_grab = do_grab
|
|
384
|
-
if do_grab:
|
|
385
|
-
self.wait_for_acquisition()
|
|
386
|
-
|
|
387
|
-
def wait_for_acquisition(self):
|
|
388
|
-
while self.do_grab:
|
|
389
|
-
try:
|
|
390
|
-
new_data = self.wait_fn()
|
|
391
|
-
if new_data is not False: # will be returned if the main thread called CancelWait
|
|
392
|
-
self.data_sig.emit()
|
|
393
|
-
except Exception as e:
|
|
394
|
-
pass
|
|
395
|
-
QtWidgets.QApplication.processEvents()
|
|
396
|
-
|
|
397
478
|
|
|
398
479
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/.github/workflows/compatibility.yml
RENAMED
|
File without changes
|
{pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/.github/workflows/python-publish.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -8,7 +8,10 @@ scanners = false # true if plugin contains custom scan layout (daq_scan extensi
|
|
|
8
8
|
[urls]
|
|
9
9
|
package-url = 'https://github.com/PyMoDAQ/pymodaq_plugins_utils'
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
[project.optional-dependencies]
|
|
12
|
+
serial = [
|
|
13
|
+
"pyvisa",
|
|
14
|
+
]
|
|
12
15
|
|
|
13
16
|
[project]
|
|
14
17
|
name = "pymodaq_plugins_utils"
|
|
@@ -50,10 +53,7 @@ classifiers = [
|
|
|
50
53
|
"Topic :: Software Development :: User Interfaces",
|
|
51
54
|
]
|
|
52
55
|
|
|
53
|
-
|
|
54
|
-
serial = [
|
|
55
|
-
"pyvisa",
|
|
56
|
-
]
|
|
56
|
+
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
[build-system]
|
{pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/src/pymodaq_plugins_utils/__init__.py
RENAMED
|
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
|
{pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/src/pymodaq_plugins_utils/utils.py
RENAMED
|
File without changes
|
{pymodaq_plugins_utils-5.0.2 → pymodaq_plugins_utils-5.0.4}/tests/test_plugin_package_structure.py
RENAMED
|
File without changes
|