teareduce 0.5.1__py3-none-any.whl → 0.5.3__py3-none-any.whl
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.
- teareduce/cleanest/__init__.py +10 -0
- teareduce/cleanest/cleanest.py +139 -0
- teareduce/cleanest/cosmicraycleanerapp.py +85 -29
- teareduce/cleanest/definitions.py +20 -9
- teareduce/cleanest/dilatemask.py +41 -0
- teareduce/cleanest/interpolation_a.py +29 -7
- teareduce/cleanest/interpolation_x.py +18 -0
- teareduce/cleanest/interpolation_y.py +18 -0
- teareduce/cleanest/interpolationeditor.py +4 -0
- teareduce/cleanest/parametereditor.py +67 -22
- teareduce/cleanest/reviewcosmicray.py +24 -8
- teareduce/tests/test_cleanest.py +131 -0
- teareduce/version.py +1 -1
- {teareduce-0.5.1.dist-info → teareduce-0.5.3.dist-info}/METADATA +2 -1
- {teareduce-0.5.1.dist-info → teareduce-0.5.3.dist-info}/RECORD +19 -16
- {teareduce-0.5.1.dist-info → teareduce-0.5.3.dist-info}/WHEEL +0 -0
- {teareduce-0.5.1.dist-info → teareduce-0.5.3.dist-info}/entry_points.txt +0 -0
- {teareduce-0.5.1.dist-info → teareduce-0.5.3.dist-info}/licenses/LICENSE.txt +0 -0
- {teareduce-0.5.1.dist-info → teareduce-0.5.3.dist-info}/top_level.txt +0 -0
teareduce/cleanest/__init__.py
CHANGED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2025 Universidad Complutense de Madrid
|
|
3
|
+
#
|
|
4
|
+
# This file is part of teareduce
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: GPL-3.0+
|
|
7
|
+
# License-Filename: LICENSE.txt
|
|
8
|
+
#
|
|
9
|
+
|
|
10
|
+
"""Interpolate pixels identified in a mask."""
|
|
11
|
+
|
|
12
|
+
from scipy import ndimage
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from .dilatemask import dilatemask
|
|
16
|
+
from .interpolation_x import interpolation_x
|
|
17
|
+
from .interpolation_y import interpolation_y
|
|
18
|
+
from .interpolation_a import interpolation_a
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def cleanest(data, mask_crfound, dilation=0,
|
|
22
|
+
interp_method=None, npoints=None, degree=None,
|
|
23
|
+
debug=False):
|
|
24
|
+
"""Interpolate pixels identified in a mask.
|
|
25
|
+
|
|
26
|
+
The original data and mask are not modified. A copy of both
|
|
27
|
+
arrays are created and returned with the interpolated pixels.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
data : 2D numpy.ndarray
|
|
32
|
+
The image data array to be processed.
|
|
33
|
+
mask_crfound : 2D numpy.ndarray of bool
|
|
34
|
+
A boolean mask array indicating which pixels are affected by
|
|
35
|
+
cosmic rays.
|
|
36
|
+
dilation : int, optional
|
|
37
|
+
The number of pixels to dilate the masked pixels before
|
|
38
|
+
interpolation.
|
|
39
|
+
interp_method : str, optional
|
|
40
|
+
The interpolation method to use. Options are:
|
|
41
|
+
'x' : Polynomial interpolation in the X direction.
|
|
42
|
+
'y' : Polynomial interpolation in the Y direction.
|
|
43
|
+
's' : Surface fit (degree 1) interpolation.
|
|
44
|
+
'd' : Median of border pixels interpolation.
|
|
45
|
+
'm' : Mean of border pixels interpolation.
|
|
46
|
+
npoints : int, optional
|
|
47
|
+
The number of points to use for interpolation. This
|
|
48
|
+
parameter is relevant for 'x', 'y', 's', 'd', and 'm' methods.
|
|
49
|
+
degree : int, optional
|
|
50
|
+
The degree of the polynomial to fit. This parameter is
|
|
51
|
+
relevant for 'x' and 'y' methods.
|
|
52
|
+
debug : bool, optional
|
|
53
|
+
If True, print debug information.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
cleaned_data : 2D numpy.ndarray
|
|
58
|
+
The image data array with cosmic rays cleaned.
|
|
59
|
+
mask_fixed : 2D numpy.ndarray of bool
|
|
60
|
+
The updated boolean mask array indicating which pixels
|
|
61
|
+
have been fixed.
|
|
62
|
+
|
|
63
|
+
Notes
|
|
64
|
+
-----
|
|
65
|
+
This function has been created to clean cosmic rays without
|
|
66
|
+
the need of a GUI interaction. It can be used in scripts
|
|
67
|
+
or batch processing of images.
|
|
68
|
+
"""
|
|
69
|
+
if interp_method is None:
|
|
70
|
+
raise ValueError("interp_method must be specified.")
|
|
71
|
+
if interp_method not in ['x', 'y', 's', 'd', 'm']:
|
|
72
|
+
raise ValueError(f"Unknown interp_method: {interp_method}")
|
|
73
|
+
if npoints is None:
|
|
74
|
+
raise ValueError("npoints must be specified.")
|
|
75
|
+
if degree is None and interp_method in ['x', 'y']:
|
|
76
|
+
raise ValueError("degree must be specified for the chosen interp_method.")
|
|
77
|
+
|
|
78
|
+
# Apply dilation to the cosmic ray mask if needed
|
|
79
|
+
if dilation > 0:
|
|
80
|
+
updated_mask_crfound = dilatemask(mask_crfound, dilation)
|
|
81
|
+
else:
|
|
82
|
+
updated_mask_crfound = mask_crfound.copy()
|
|
83
|
+
|
|
84
|
+
# Create a mask to keep track of cleaned pixels
|
|
85
|
+
mask_fixed = np.zeros_like(mask_crfound, dtype=bool)
|
|
86
|
+
|
|
87
|
+
# Determine number of CR features
|
|
88
|
+
structure = [[1, 1, 1],
|
|
89
|
+
[1, 1, 1],
|
|
90
|
+
[1, 1, 1]]
|
|
91
|
+
cr_labels, num_features = ndimage.label(updated_mask_crfound, structure=structure)
|
|
92
|
+
if debug:
|
|
93
|
+
print(f"Number of cosmic ray pixels to be cleaned: {np.sum(updated_mask_crfound)}")
|
|
94
|
+
print(f"Number of cosmic rays (grouped pixels)...: {num_features}")
|
|
95
|
+
|
|
96
|
+
# Fix cosmic rays using the specified interpolation method
|
|
97
|
+
cleaned_data = data.copy()
|
|
98
|
+
num_cr_cleaned = 0
|
|
99
|
+
for cr_index in range(1, num_features + 1):
|
|
100
|
+
if interp_method in ['x', 'y']:
|
|
101
|
+
if 2 * npoints <= degree:
|
|
102
|
+
raise ValueError("2*npoints must be greater than degree for polynomial interpolation.")
|
|
103
|
+
if interp_method == 'x':
|
|
104
|
+
interp_func = interpolation_x
|
|
105
|
+
else:
|
|
106
|
+
interp_func = interpolation_y
|
|
107
|
+
interpolation_performed, _, _ = interp_func(
|
|
108
|
+
data=cleaned_data,
|
|
109
|
+
mask_fixed=mask_fixed,
|
|
110
|
+
cr_labels=cr_labels,
|
|
111
|
+
cr_index=cr_index,
|
|
112
|
+
npoints=npoints,
|
|
113
|
+
degree=degree)
|
|
114
|
+
if interpolation_performed:
|
|
115
|
+
num_cr_cleaned += 1
|
|
116
|
+
elif interp_method in ['s', 'd', 'm']:
|
|
117
|
+
if interp_method == 's':
|
|
118
|
+
method = 'surface'
|
|
119
|
+
elif interp_method == 'd':
|
|
120
|
+
method = 'median'
|
|
121
|
+
elif interp_method == 'm':
|
|
122
|
+
method = 'mean'
|
|
123
|
+
interpolation_performed, _, _ = interpolation_a(
|
|
124
|
+
data=cleaned_data,
|
|
125
|
+
mask_fixed=mask_fixed,
|
|
126
|
+
cr_labels=cr_labels,
|
|
127
|
+
cr_index=cr_index,
|
|
128
|
+
npoints=npoints,
|
|
129
|
+
method=method
|
|
130
|
+
)
|
|
131
|
+
if interpolation_performed:
|
|
132
|
+
num_cr_cleaned += 1
|
|
133
|
+
else:
|
|
134
|
+
raise ValueError(f"Unknown interpolation method: {interp_method}")
|
|
135
|
+
|
|
136
|
+
if debug:
|
|
137
|
+
print(f"Number of cosmic rays cleaned............: {num_cr_cleaned}")
|
|
138
|
+
|
|
139
|
+
return cleaned_data, mask_fixed
|
|
@@ -19,15 +19,17 @@ from ccdproc import cosmicray_lacosmic
|
|
|
19
19
|
import matplotlib.pyplot as plt
|
|
20
20
|
from matplotlib.backend_bases import key_press_handler
|
|
21
21
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
|
|
22
|
+
from scipy import ndimage
|
|
22
23
|
import numpy as np
|
|
23
24
|
import os
|
|
24
25
|
from rich import print
|
|
25
|
-
from
|
|
26
|
+
from tqdm import tqdm
|
|
26
27
|
|
|
27
28
|
from .definitions import lacosmic_default_dict
|
|
28
29
|
from .definitions import DEFAULT_NPOINTS_INTERP
|
|
29
30
|
from .definitions import DEFAULT_DEGREE_INTERP
|
|
30
31
|
from .definitions import MAX_PIXEL_DISTANCE_TO_CR
|
|
32
|
+
from .dilatemask import dilatemask
|
|
31
33
|
from .find_closest_true import find_closest_true
|
|
32
34
|
from .interpolation_a import interpolation_a
|
|
33
35
|
from .interpolation_x import interpolation_x
|
|
@@ -135,7 +137,7 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
135
137
|
"""
|
|
136
138
|
self.root = root
|
|
137
139
|
self.root.title("Cosmic Ray Cleaner")
|
|
138
|
-
self.root.geometry("
|
|
140
|
+
self.root.geometry("800x800+50+0")
|
|
139
141
|
self.lacosmic_params = lacosmic_default_dict.copy()
|
|
140
142
|
self.input_fits = input_fits
|
|
141
143
|
self.extension = extension
|
|
@@ -357,41 +359,81 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
357
359
|
self.last_xmax = updated_params['xmax']['value']
|
|
358
360
|
self.last_ymin = updated_params['ymin']['value']
|
|
359
361
|
self.last_ymax = updated_params['ymax']['value']
|
|
362
|
+
usefulregion = SliceRegion2D(f"[{self.last_xmin}:{self.last_xmax},{self.last_ymin}:{self.last_ymax}]",
|
|
363
|
+
mode="fits").python
|
|
364
|
+
usefulmask = np.zeros_like(self.data)
|
|
365
|
+
usefulmask[usefulregion] = 1.0
|
|
360
366
|
# Update parameter dictionary with new values
|
|
361
367
|
self.lacosmic_params = updated_params
|
|
362
368
|
print("Parameters updated:")
|
|
363
369
|
for key, info in self.lacosmic_params.items():
|
|
364
370
|
print(f" {key}: {info['value']}")
|
|
371
|
+
if self.lacosmic_params['nruns']['value'] not in [1, 2]:
|
|
372
|
+
raise ValueError("nruns must be 1 or 2")
|
|
365
373
|
# Execute L.A.Cosmic with updated parameters
|
|
366
374
|
cleandata_lacosmic, mask_crfound = cosmicray_lacosmic(
|
|
367
375
|
self.data,
|
|
368
|
-
gain=self.lacosmic_params['
|
|
369
|
-
readnoise=self.lacosmic_params['
|
|
370
|
-
sigclip=self.lacosmic_params['
|
|
371
|
-
sigfrac=self.lacosmic_params['
|
|
372
|
-
objlim=self.lacosmic_params['
|
|
373
|
-
niter=self.lacosmic_params['
|
|
374
|
-
verbose=self.lacosmic_params['
|
|
376
|
+
gain=self.lacosmic_params['run1_gain']['value'],
|
|
377
|
+
readnoise=self.lacosmic_params['run1_readnoise']['value'],
|
|
378
|
+
sigclip=self.lacosmic_params['run1_sigclip']['value'],
|
|
379
|
+
sigfrac=self.lacosmic_params['run1_sigfrac']['value'],
|
|
380
|
+
objlim=self.lacosmic_params['run1_objlim']['value'],
|
|
381
|
+
niter=self.lacosmic_params['run1_niter']['value'],
|
|
382
|
+
verbose=self.lacosmic_params['run1_verbose']['value']
|
|
375
383
|
)
|
|
384
|
+
# Apply usefulmask to consider only selected region
|
|
385
|
+
cleandata_lacosmic *= usefulmask
|
|
386
|
+
mask_crfound = mask_crfound & (usefulmask.astype(bool))
|
|
387
|
+
# Second execution if nruns == 2
|
|
388
|
+
if self.lacosmic_params['nruns']['value'] == 2:
|
|
389
|
+
cleandata_lacosmic2, mask_crfound2 = cosmicray_lacosmic(
|
|
390
|
+
self.data,
|
|
391
|
+
gain=self.lacosmic_params['run2_gain']['value'],
|
|
392
|
+
readnoise=self.lacosmic_params['run2_readnoise']['value'],
|
|
393
|
+
sigclip=self.lacosmic_params['run2_sigclip']['value'],
|
|
394
|
+
sigfrac=self.lacosmic_params['run2_sigfrac']['value'],
|
|
395
|
+
objlim=self.lacosmic_params['run2_objlim']['value'],
|
|
396
|
+
niter=self.lacosmic_params['run2_niter']['value'],
|
|
397
|
+
verbose=self.lacosmic_params['run2_verbose']['value']
|
|
398
|
+
)
|
|
399
|
+
# Apply usefulmask to consider only selected region
|
|
400
|
+
cleandata_lacosmic2 *= usefulmask
|
|
401
|
+
mask_crfound2 = mask_crfound2 & (usefulmask.astype(bool))
|
|
402
|
+
# Combine results from both runs
|
|
403
|
+
if np.any(mask_crfound):
|
|
404
|
+
print(f"Number of cosmic ray pixels (run1).......: {np.sum(mask_crfound)}")
|
|
405
|
+
print(f"Number of cosmic ray pixels (run2).......: {np.sum(mask_crfound2)}")
|
|
406
|
+
# find features in second run
|
|
407
|
+
structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
|
|
408
|
+
cr_labels2, num_features2 = ndimage.label(mask_crfound2, structure=structure)
|
|
409
|
+
# generate mask of ones at CR pixels found in first run
|
|
410
|
+
mask_peaks = np.zeros(mask_crfound.shape, dtype=float)
|
|
411
|
+
mask_peaks[mask_crfound] = 1.0
|
|
412
|
+
# preserve only those CR pixels in second run that are in the first run
|
|
413
|
+
cr_labels2_preserved = mask_peaks * cr_labels2
|
|
414
|
+
# generate new mask with preserved CR pixels from second run
|
|
415
|
+
mask_crfound = np.zeros_like(mask_crfound, dtype=bool)
|
|
416
|
+
for icr in np.unique(cr_labels2_preserved):
|
|
417
|
+
if icr > 0:
|
|
418
|
+
mask_crfound[cr_labels2 == icr] = True
|
|
419
|
+
print(f'Number of cosmic ray pixels (run1 & run2): {np.sum(mask_crfound)}')
|
|
420
|
+
# Use the cleandata from the second run
|
|
421
|
+
cleandata_lacosmic = cleandata_lacosmic2
|
|
376
422
|
# Select the image region to process
|
|
377
|
-
fits_region = f"[{updated_params['xmin']['value']}:{updated_params['xmax']['value']}"
|
|
378
|
-
fits_region += f",{updated_params['ymin']['value']}:{updated_params['ymax']['value']}]"
|
|
379
|
-
region = SliceRegion2D(fits_region, mode="fits").python
|
|
380
423
|
self.cleandata_lacosmic = self.data.copy()
|
|
381
|
-
self.cleandata_lacosmic[
|
|
424
|
+
self.cleandata_lacosmic[usefulregion] = cleandata_lacosmic[usefulregion]
|
|
382
425
|
self.mask_crfound = np.zeros_like(self.data, dtype=bool)
|
|
383
|
-
self.mask_crfound[
|
|
426
|
+
self.mask_crfound[usefulregion] = mask_crfound[usefulregion]
|
|
384
427
|
# Process the mask: dilation and labeling
|
|
385
428
|
if np.any(self.mask_crfound):
|
|
386
429
|
num_cr_pixels_before_dilation = np.sum(self.mask_crfound)
|
|
387
430
|
dilation = self.lacosmic_params['dilation']['value']
|
|
388
431
|
if dilation > 0:
|
|
389
432
|
# Dilate the mask by the specified number of pixels
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
self.
|
|
393
|
-
|
|
394
|
-
iterations=self.lacosmic_params['dilation']['value']
|
|
433
|
+
self.mask_crfound = dilatemask(
|
|
434
|
+
mask=self.mask_crfound,
|
|
435
|
+
iterations=self.lacosmic_params['dilation']['value'],
|
|
436
|
+
connectivity=1
|
|
395
437
|
)
|
|
396
438
|
num_cr_pixels_after_dilation = np.sum(self.mask_crfound)
|
|
397
439
|
sdum = str(num_cr_pixels_after_dilation)
|
|
@@ -407,7 +449,7 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
407
449
|
# diagonal connections too, so we define a 3x3 square.
|
|
408
450
|
structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
|
|
409
451
|
self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
|
|
410
|
-
print(f"Number of cosmic rays features (grouped pixels)...: {self.num_features
|
|
452
|
+
print(f"Number of cosmic rays features (grouped pixels)...: {self.num_features:>{len(sdum)}}")
|
|
411
453
|
self.apply_lacosmic_button.config(state=tk.NORMAL)
|
|
412
454
|
self.examine_detected_cr_button.config(state=tk.NORMAL)
|
|
413
455
|
self.update_cr_overlay()
|
|
@@ -452,10 +494,13 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
452
494
|
"""Apply the selected cleaning method to the detected cosmic rays."""
|
|
453
495
|
if np.any(self.mask_crfound):
|
|
454
496
|
# recalculate labels and number of features
|
|
455
|
-
structure = [[1, 1, 1],
|
|
497
|
+
structure = [[1, 1, 1],
|
|
498
|
+
[1, 1, 1],
|
|
499
|
+
[1, 1, 1]]
|
|
456
500
|
self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
|
|
457
|
-
|
|
458
|
-
print(f"Number of cosmic
|
|
501
|
+
sdum = str(np.sum(self.mask_crfound))
|
|
502
|
+
print(f"Number of cosmic ray pixels detected by L.A.Cosmic...........: {sdum}")
|
|
503
|
+
print(f"Number of cosmic rays (grouped pixels) detected by L.A.Cosmic: {self.num_features:>{len(sdum)}}")
|
|
459
504
|
# Define parameters for L.A.Cosmic from default dictionary
|
|
460
505
|
editor_window = tk.Toplevel(self.root)
|
|
461
506
|
editor = InterpolationEditor(
|
|
@@ -463,7 +508,7 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
463
508
|
last_dilation=self.lacosmic_params['dilation']['value'],
|
|
464
509
|
last_npoints=self.last_npoints,
|
|
465
510
|
last_degree=self.last_degree,
|
|
466
|
-
auxdata=self.auxdata
|
|
511
|
+
auxdata=self.auxdata,
|
|
467
512
|
)
|
|
468
513
|
# Make it modal (blocks interaction with main window)
|
|
469
514
|
editor_window.transient(self.root)
|
|
@@ -498,7 +543,7 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
498
543
|
self.mask_crfound[self.mask_crfound] = False
|
|
499
544
|
num_cr_cleaned = self.num_features
|
|
500
545
|
else:
|
|
501
|
-
for i in range(1, self.num_features + 1):
|
|
546
|
+
for i in tqdm(range(1, self.num_features + 1)):
|
|
502
547
|
tmp_mask_fixed = np.zeros_like(self.data, dtype=bool)
|
|
503
548
|
if cleaning_method == 'x':
|
|
504
549
|
interpolation_performed, _, _ = interpolation_x(
|
|
@@ -536,6 +581,15 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
536
581
|
npoints=editor.npoints,
|
|
537
582
|
method='median'
|
|
538
583
|
)
|
|
584
|
+
elif cleaning_method == 'a-mean':
|
|
585
|
+
interpolation_performed, _, _ = interpolation_a(
|
|
586
|
+
data=self.data,
|
|
587
|
+
mask_fixed=tmp_mask_fixed,
|
|
588
|
+
cr_labels=self.cr_labels,
|
|
589
|
+
cr_index=i,
|
|
590
|
+
npoints=editor.npoints,
|
|
591
|
+
method='mean'
|
|
592
|
+
)
|
|
539
593
|
else:
|
|
540
594
|
raise ValueError(f"Unknown cleaning method: {cleaning_method}")
|
|
541
595
|
if interpolation_performed:
|
|
@@ -548,8 +602,9 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
548
602
|
# recalculate labels and number of features
|
|
549
603
|
structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
|
|
550
604
|
self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
|
|
551
|
-
|
|
552
|
-
print(f"Remaining number of cosmic
|
|
605
|
+
sdum = str(np.sum(self.mask_crfound))
|
|
606
|
+
print(f"Remaining number of cosmic ray pixels...........: {sdum}")
|
|
607
|
+
print(f"Remaining number of cosmic rays (grouped pixels): {self.num_features:>{len(sdum)}}")
|
|
553
608
|
# redraw image to show the changes
|
|
554
609
|
self.image.set_data(self.data)
|
|
555
610
|
self.canvas.draw_idle()
|
|
@@ -612,8 +667,9 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
612
667
|
# recalculate labels and number of features
|
|
613
668
|
structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
|
|
614
669
|
self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
|
|
615
|
-
|
|
616
|
-
print(f"Remaining number of cosmic
|
|
670
|
+
sdum = str(np.sum(self.mask_crfound))
|
|
671
|
+
print(f"Remaining number of cosmic ray pixels...........: {sdum}")
|
|
672
|
+
print(f"Remaining number of cosmic rays (grouped pixels): {self.num_features:>{len(sdum)}}")
|
|
617
673
|
# redraw image to show the changes
|
|
618
674
|
self.image.set_data(self.data)
|
|
619
675
|
self.canvas.draw_idle()
|
|
@@ -14,21 +14,31 @@
|
|
|
14
14
|
# using the intrinsic Python types, so that they can be easily cast
|
|
15
15
|
# when reading user input.
|
|
16
16
|
lacosmic_default_dict = {
|
|
17
|
-
# L.A.Cosmic parameters
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
17
|
+
# L.A.Cosmic parameters for run 1
|
|
18
|
+
'run1_gain': {'value': 1.0, 'type': float, 'positive': True},
|
|
19
|
+
'run1_readnoise': {'value': 6.5, 'type': float, 'positive': True},
|
|
20
|
+
'run1_sigclip': {'value': 5.0, 'type': float, 'positive': True},
|
|
21
|
+
'run1_sigfrac': {'value': 0.3, 'type': float, 'positive': True},
|
|
22
|
+
'run1_objlim': {'value': 5.0, 'type': float, 'positive': True},
|
|
23
|
+
'run1_niter': {'value': 4, 'type': int, 'positive': True},
|
|
24
|
+
'run1_verbose': {'value': False, 'type': bool},
|
|
25
|
+
# L.A.Cosmic parameters for run 2
|
|
26
|
+
'run2_gain': {'value': 1.0, 'type': float, 'positive': True},
|
|
27
|
+
'run2_readnoise': {'value': 6.5, 'type': float, 'positive': True},
|
|
28
|
+
'run2_sigclip': {'value': 3.0, 'type': float, 'positive': True},
|
|
29
|
+
'run2_sigfrac': {'value': 0.3, 'type': float, 'positive': True},
|
|
30
|
+
'run2_objlim': {'value': 5.0, 'type': float, 'positive': True},
|
|
31
|
+
'run2_niter': {'value': 4, 'type': int, 'positive': True},
|
|
32
|
+
'run2_verbose': {'value': False, 'type': bool},
|
|
25
33
|
# Dilation of the mask
|
|
26
34
|
'dilation': {'value': 0, 'type': int, 'positive': True},
|
|
27
35
|
# Limits for the image section to process (pixels start at 1)
|
|
28
36
|
'xmin': {'value': 1, 'type': int, 'positive': True},
|
|
29
37
|
'xmax': {'value': None, 'type': int, 'positive': True},
|
|
30
38
|
'ymin': {'value': 1, 'type': int, 'positive': True},
|
|
31
|
-
'ymax': {'value': None, 'type': int, 'positive': True}
|
|
39
|
+
'ymax': {'value': None, 'type': int, 'positive': True},
|
|
40
|
+
# Number of runs to execute L.A.Cosmic
|
|
41
|
+
'nruns': {'value': 1, 'type': int, 'positive': True}
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
# Default parameters for cleaning methods
|
|
@@ -37,6 +47,7 @@ VALID_CLEANING_METHODS = [
|
|
|
37
47
|
'y interp.',
|
|
38
48
|
'surface interp.',
|
|
39
49
|
'median',
|
|
50
|
+
'mean',
|
|
40
51
|
'lacosmic',
|
|
41
52
|
'auxdata'
|
|
42
53
|
]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2025 Universidad Complutense de Madrid
|
|
3
|
+
#
|
|
4
|
+
# This file is part of teareduce
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: GPL-3.0+
|
|
7
|
+
# License-Filename: LICENSE.txt
|
|
8
|
+
#
|
|
9
|
+
|
|
10
|
+
"""Dilate cosmic ray mask"""
|
|
11
|
+
|
|
12
|
+
from scipy import ndimage
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def dilatemask(mask, iterations, rank=None, connectivity=1):
|
|
16
|
+
"""Dilate mask by a given number of points.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
mask : numpy.ndarray of bool
|
|
21
|
+
A boolean mask array indicating cosmic ray affected pixels.
|
|
22
|
+
iterations : int
|
|
23
|
+
The number of dilation iterations to perform. Each iteration
|
|
24
|
+
expands the mask by one pixel in the specified connectivity.
|
|
25
|
+
rank : int, optional
|
|
26
|
+
The rank of the array. If None, it is inferred from crmask.
|
|
27
|
+
See scipy.ndimage.generate_binary_structure for details.
|
|
28
|
+
connectivity : int, optional
|
|
29
|
+
The connectivity for the structuring element. Default is 1.
|
|
30
|
+
See scipy.ndimage.generate_binary_structure for details.
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
dilated_mask : numpy.ndarray of bool
|
|
35
|
+
The dilated mask.
|
|
36
|
+
"""
|
|
37
|
+
if rank is None:
|
|
38
|
+
rank = mask.ndim
|
|
39
|
+
structure = ndimage.generate_binary_structure(rank, connectivity)
|
|
40
|
+
dilated_mask = ndimage.binary_dilation(mask, structure=structure, iterations=iterations)
|
|
41
|
+
return dilated_mask
|
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
"""Surface interpolation (plane fit) or median interpolation"""
|
|
11
11
|
|
|
12
12
|
import numpy as np
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
from .dilatemask import dilatemask
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def interpolation_a(data, mask_fixed, cr_labels, cr_index, npoints, method):
|
|
17
|
-
"""
|
|
18
|
+
"""Fix cosmic ray pixels using surface fit, median or mean.
|
|
18
19
|
|
|
19
20
|
Parameters
|
|
20
21
|
----------
|
|
@@ -29,7 +30,7 @@ def interpolation_a(data, mask_fixed, cr_labels, cr_index, npoints, method):
|
|
|
29
30
|
npoints : int
|
|
30
31
|
The number of points to use for interpolation.
|
|
31
32
|
method : str
|
|
32
|
-
The interpolation method to use ('surface' or '
|
|
33
|
+
The interpolation method to use ('surface', 'median' or 'mean').
|
|
33
34
|
|
|
34
35
|
Returns
|
|
35
36
|
-------
|
|
@@ -39,11 +40,27 @@ def interpolation_a(data, mask_fixed, cr_labels, cr_index, npoints, method):
|
|
|
39
40
|
X-coordinates of border pixels used for interpolation.
|
|
40
41
|
yfit_all : list
|
|
41
42
|
Y-coordinates of border pixels used for interpolation.
|
|
43
|
+
|
|
44
|
+
Notes
|
|
45
|
+
-----
|
|
46
|
+
The `data` array is modified in place with interpolated values for the
|
|
47
|
+
cosmic ray pixels. This function also returns an updated `mask_fixed`
|
|
48
|
+
array with interpolated pixels marked as fixed.
|
|
49
|
+
|
|
50
|
+
It is important to highlight that contrary to what is performed when
|
|
51
|
+
using the X- and Y-interpolation, this function does not fill the gaps
|
|
52
|
+
between the marked pixels. Only the pixels explicitly marked as affected
|
|
53
|
+
by cosmic rays are interpolated.
|
|
42
54
|
"""
|
|
43
55
|
# Mask of CR pixels
|
|
44
56
|
mask = (cr_labels == cr_index)
|
|
45
57
|
# Dilate the mask to find border pixels
|
|
46
|
-
dilated_mask = binary_dilation(mask, structure=np.ones((3, 3)), iterations=npoints)
|
|
58
|
+
# dilated_mask = binary_dilation(mask, structure=np.ones((3, 3)), iterations=npoints)
|
|
59
|
+
dilated_mask = dilatemask(
|
|
60
|
+
mask=mask,
|
|
61
|
+
iterations=npoints,
|
|
62
|
+
connectivity=1
|
|
63
|
+
)
|
|
47
64
|
# Border pixels are those in the dilated mask but not in the original mask
|
|
48
65
|
border_mask = dilated_mask & (~mask)
|
|
49
66
|
# Get coordinates of border pixels
|
|
@@ -67,16 +84,21 @@ def interpolation_a(data, mask_fixed, cr_labels, cr_index, npoints, method):
|
|
|
67
84
|
interpolation_performed = True
|
|
68
85
|
else:
|
|
69
86
|
print("Not enough points to fit a plane")
|
|
70
|
-
elif method
|
|
87
|
+
elif method in ['median', 'mean']:
|
|
71
88
|
# Compute median of all surrounding points
|
|
72
89
|
if len(zfit_all) > 0:
|
|
73
|
-
|
|
90
|
+
if method == 'median':
|
|
91
|
+
zval = np.median(zfit_all)
|
|
92
|
+
else:
|
|
93
|
+
zval = np.mean(zfit_all)
|
|
74
94
|
# recompute all CR pixels to take into account "holes" between marked pixels
|
|
75
95
|
ycr_list, xcr_list = np.where(cr_labels == cr_index)
|
|
76
96
|
for iy, ix in zip(ycr_list, xcr_list):
|
|
77
|
-
data[iy, ix] =
|
|
97
|
+
data[iy, ix] = zval
|
|
78
98
|
mask_fixed[iy, ix] = True
|
|
79
99
|
interpolation_performed = True
|
|
100
|
+
else:
|
|
101
|
+
print("No surrounding points found for median interpolation")
|
|
80
102
|
else:
|
|
81
103
|
print(f"Unknown interpolation method: {method}")
|
|
82
104
|
|
|
@@ -37,6 +37,24 @@ def interpolation_x(data, mask_fixed, cr_labels, cr_index, npoints, degree):
|
|
|
37
37
|
X-coordinates of border pixels used for interpolation.
|
|
38
38
|
yfit_all : list
|
|
39
39
|
Y-coordinates of border pixels used for interpolation.
|
|
40
|
+
|
|
41
|
+
Notes
|
|
42
|
+
-----
|
|
43
|
+
The `data` array is modified in place with interpolated values for the
|
|
44
|
+
cosmic ray pixels. This function also returns an updated `mask_fixed`
|
|
45
|
+
array with interpolated pixels marked as fixed.
|
|
46
|
+
|
|
47
|
+
It is important to highlight that this function assumes that at
|
|
48
|
+
every y-coordinate where cosmic ray pixels are found, the pixels
|
|
49
|
+
form a contiguous horizontal segment. In this sense, gaps in the
|
|
50
|
+
x-direction are assumed to be also part of the same cosmic ray
|
|
51
|
+
feature, and the `cr_labels` array is updated accordingly. In other
|
|
52
|
+
words, all the pixels between the minimum and maximum x-coordinates
|
|
53
|
+
of the cosmic ray pixels at a given y-coordinate are treated as
|
|
54
|
+
affected by the cosmic ray. This simplyfies the interactive marking
|
|
55
|
+
of cosmic rays, as the user does not need to ensure that all pixels
|
|
56
|
+
in a horizontal segment are marked; marking just the extreme pixels
|
|
57
|
+
is sufficient.
|
|
40
58
|
"""
|
|
41
59
|
ycr_list, xcr_list = np.where(cr_labels == cr_index)
|
|
42
60
|
ycr_min = np.min(ycr_list)
|
|
@@ -37,6 +37,24 @@ def interpolation_y(data, mask_fixed, cr_labels, cr_index, npoints, degree):
|
|
|
37
37
|
X-coordinates of border pixels used for interpolation.
|
|
38
38
|
yfit_all : list
|
|
39
39
|
Y-coordinates of border pixels used for interpolation.
|
|
40
|
+
|
|
41
|
+
Notes
|
|
42
|
+
-----
|
|
43
|
+
The `data` array is modified in place with interpolated values for the
|
|
44
|
+
cosmic ray pixels. This function also returns an updated `mask_fixed`
|
|
45
|
+
array with interpolated pixels marked as fixed.
|
|
46
|
+
|
|
47
|
+
It is important to highlight that this function assumes that at
|
|
48
|
+
every x-coordinate where cosmic ray pixels are found, the pixels
|
|
49
|
+
form a contiguous vertical segment. In this sense, gaps in the
|
|
50
|
+
y-direction are assumed to be also part of the same cosmic ray
|
|
51
|
+
feature, and the `cr_labels` array is updated accordingly. In other
|
|
52
|
+
words, all the pixels between the minimum and maximum y-coordinates
|
|
53
|
+
of the cosmic ray pixels at a given x-coordinate are treated as
|
|
54
|
+
affected by the cosmic ray. This simplyfies the interactive marking
|
|
55
|
+
of cosmic rays, as the user does not need to ensure that all pixels
|
|
56
|
+
in a vertical segment are marked; marking just the extreme pixels
|
|
57
|
+
is sufficient.
|
|
40
58
|
"""
|
|
41
59
|
ycr_list, xcr_list = np.where(cr_labels == cr_index)
|
|
42
60
|
xcr_min = np.min(xcr_list)
|
|
@@ -72,6 +72,7 @@ class InterpolationEditor:
|
|
|
72
72
|
"y interp.": "y",
|
|
73
73
|
"surface interp.": "a-plane",
|
|
74
74
|
"median": "a-median",
|
|
75
|
+
"mean": "a-mean",
|
|
75
76
|
"lacosmic": "lacosmic",
|
|
76
77
|
"auxdata": "auxdata"
|
|
77
78
|
}
|
|
@@ -190,6 +191,9 @@ class InterpolationEditor:
|
|
|
190
191
|
elif selected_method == 'median':
|
|
191
192
|
self.entry_npoints.config(state='normal')
|
|
192
193
|
self.entry_degree.config(state='disabled')
|
|
194
|
+
elif selected_method == 'mean':
|
|
195
|
+
self.entry_npoints.config(state='normal')
|
|
196
|
+
self.entry_degree.config(state='disabled')
|
|
193
197
|
elif selected_method == 'lacosmic':
|
|
194
198
|
self.entry_npoints.config(state='disabled')
|
|
195
199
|
self.entry_degree.config(state='disabled')
|
|
@@ -75,7 +75,7 @@ class ParameterEditor:
|
|
|
75
75
|
self.param_dict['ymin']['value'] = ymin
|
|
76
76
|
self.param_dict['ymax']['value'] = ymax
|
|
77
77
|
self.imgshape = imgshape
|
|
78
|
-
self.entries = {} # dictionary to hold entry widgets
|
|
78
|
+
self.entries = {'run1': {}, 'run2': {}} # dictionary to hold entry widgets
|
|
79
79
|
self.result_dict = None
|
|
80
80
|
|
|
81
81
|
# Create the form
|
|
@@ -91,33 +91,48 @@ class ParameterEditor:
|
|
|
91
91
|
|
|
92
92
|
# Subtitle for L.A.Cosmic parameters
|
|
93
93
|
subtitle_label = tk.Label(main_frame, text="L.A.Cosmic Parameters", font=("Arial", 14, "bold"))
|
|
94
|
-
subtitle_label.grid(row=row, column=0, columnspan=
|
|
94
|
+
subtitle_label.grid(row=row, column=0, columnspan=4, pady=(0, 15))
|
|
95
95
|
row += 1
|
|
96
96
|
|
|
97
|
-
# Create labels and entry fields for each parameter
|
|
97
|
+
# Create labels and entry fields for each parameter.
|
|
98
|
+
# Note: here we are using entry_vars to trace changes in the entries
|
|
99
|
+
# so that we can update the color of run2 entries if they differ from run1.
|
|
100
|
+
self.entry_vars = {}
|
|
98
101
|
for key, info in self.param_dict.items():
|
|
99
|
-
if key.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
102
|
+
if not key.startswith('run1_'):
|
|
103
|
+
continue
|
|
104
|
+
# Parameter name label
|
|
105
|
+
label = tk.Label(main_frame, text=f"{key[5:]}:", anchor='e', width=15)
|
|
106
|
+
label.grid(row=row, column=0, sticky='w', pady=5)
|
|
107
|
+
# Entry field for run1
|
|
108
|
+
self.entry_vars[key] = tk.StringVar()
|
|
109
|
+
self.entry_vars[key].trace_add('write', lambda *args: self.update_colour_param_run1_run2())
|
|
110
|
+
entry = tk.Entry(main_frame, textvariable=self.entry_vars[key], width=10)
|
|
111
|
+
entry.insert(0, str(info['value']))
|
|
112
|
+
entry.grid(row=row, column=1, padx=10, pady=5)
|
|
113
|
+
self.entries[key] = entry # dictionary to hold entry widgets
|
|
114
|
+
# Entry field for run2
|
|
115
|
+
key2 = 'run2_' + key[5:]
|
|
116
|
+
self.entry_vars[key2] = tk.StringVar()
|
|
117
|
+
self.entry_vars[key2].trace_add('write', lambda *args: self.update_colour_param_run1_run2())
|
|
118
|
+
entry = tk.Entry(main_frame, textvariable=self.entry_vars[key2], width=10)
|
|
119
|
+
entry.insert(0, str(self.param_dict[key2]['value']))
|
|
120
|
+
entry.grid(row=row, column=2, padx=10, pady=5)
|
|
121
|
+
self.entries['run2_'+key[5:]] = entry # dictionary to hold entry widgets
|
|
122
|
+
# Type label
|
|
123
|
+
type_label = tk.Label(main_frame, text=f"({info['type'].__name__})", fg='gray', anchor='w', width=10)
|
|
124
|
+
type_label.grid(row=row, column=3, sticky='w', pady=5)
|
|
125
|
+
row += 1
|
|
126
|
+
# self.update_colour_param_run1_run2()
|
|
112
127
|
|
|
113
128
|
# Separator
|
|
114
129
|
separator1 = ttk.Separator(main_frame, orient='horizontal')
|
|
115
|
-
separator1.grid(row=row, column=0, columnspan=
|
|
130
|
+
separator1.grid(row=row, column=0, columnspan=4, sticky='ew', pady=(10, 10))
|
|
116
131
|
row += 1
|
|
117
132
|
|
|
118
133
|
# Subtitle for additional parameters
|
|
119
134
|
subtitle_label = tk.Label(main_frame, text="Additional Parameters", font=("Arial", 14, "bold"))
|
|
120
|
-
subtitle_label.grid(row=row, column=0, columnspan=
|
|
135
|
+
subtitle_label.grid(row=row, column=0, columnspan=4, pady=(0, 15))
|
|
121
136
|
row += 1
|
|
122
137
|
|
|
123
138
|
# Dilation label and entry
|
|
@@ -134,12 +149,12 @@ class ParameterEditor:
|
|
|
134
149
|
|
|
135
150
|
# Separator
|
|
136
151
|
separator2 = ttk.Separator(main_frame, orient='horizontal')
|
|
137
|
-
separator2.grid(row=row, column=0, columnspan=
|
|
152
|
+
separator2.grid(row=row, column=0, columnspan=4, sticky='ew', pady=(10, 10))
|
|
138
153
|
row += 1
|
|
139
154
|
|
|
140
155
|
# Subtitle for region to be examined
|
|
141
156
|
subtitle_label = tk.Label(main_frame, text="Region to be Examined", font=("Arial", 14, "bold"))
|
|
142
|
-
subtitle_label.grid(row=row, column=0, columnspan=
|
|
157
|
+
subtitle_label.grid(row=row, column=0, columnspan=4, pady=(0, 15))
|
|
143
158
|
row += 1
|
|
144
159
|
|
|
145
160
|
# Region to be examined label and entries
|
|
@@ -165,12 +180,12 @@ class ParameterEditor:
|
|
|
165
180
|
|
|
166
181
|
# Separator
|
|
167
182
|
separator3 = ttk.Separator(main_frame, orient='horizontal')
|
|
168
|
-
separator3.grid(row=row, column=0, columnspan=
|
|
183
|
+
separator3.grid(row=row, column=0, columnspan=4, sticky='ew', pady=(10, 10))
|
|
169
184
|
row += 1
|
|
170
185
|
|
|
171
186
|
# Button frame
|
|
172
187
|
button_frame = tk.Frame(main_frame)
|
|
173
|
-
button_frame.grid(row=row, column=0, columnspan=
|
|
188
|
+
button_frame.grid(row=row, column=0, columnspan=4, pady=(5, 0))
|
|
174
189
|
|
|
175
190
|
# OK button
|
|
176
191
|
ok_button = tk.Button(button_frame, text="OK", width=5, command=self.on_ok)
|
|
@@ -190,6 +205,8 @@ class ParameterEditor:
|
|
|
190
205
|
updated_dict = {}
|
|
191
206
|
|
|
192
207
|
for key, info in self.param_dict.items():
|
|
208
|
+
if key == 'nruns':
|
|
209
|
+
continue
|
|
193
210
|
entry_value = self.entries[key].get()
|
|
194
211
|
value_type = info['type']
|
|
195
212
|
|
|
@@ -212,6 +229,17 @@ class ParameterEditor:
|
|
|
212
229
|
'type': value_type
|
|
213
230
|
}
|
|
214
231
|
|
|
232
|
+
# Check whether any run1 and run2 parameters differ
|
|
233
|
+
nruns = 1
|
|
234
|
+
for key in self.param_dict.keys():
|
|
235
|
+
if key.startswith('run1_'):
|
|
236
|
+
parname = key[5:]
|
|
237
|
+
key2 = 'run2_' + parname
|
|
238
|
+
if updated_dict[key]['value'] != updated_dict[key2]['value']:
|
|
239
|
+
nruns = 2
|
|
240
|
+
print(f"Parameter '{parname}' differs between run1 and run2: "
|
|
241
|
+
f"{updated_dict[key]['value']} (run1) vs {updated_dict[key2]['value']} (run2)")
|
|
242
|
+
|
|
215
243
|
# Additional validation for region limits
|
|
216
244
|
try:
|
|
217
245
|
if updated_dict['xmax']['value'] <= updated_dict['xmin']['value']:
|
|
@@ -219,6 +247,9 @@ class ParameterEditor:
|
|
|
219
247
|
if updated_dict['ymax']['value'] <= updated_dict['ymin']['value']:
|
|
220
248
|
raise ValueError("ymax must be greater than ymin")
|
|
221
249
|
self.result_dict = updated_dict
|
|
250
|
+
self.result_dict['nruns'] = {'value': nruns, 'type': int, 'positive': True}
|
|
251
|
+
if nruns not in [1, 2]:
|
|
252
|
+
raise ValueError("nruns must be 1 or 2")
|
|
222
253
|
self.root.destroy()
|
|
223
254
|
except ValueError as e:
|
|
224
255
|
messagebox.showerror("Invalid Inputs",
|
|
@@ -243,9 +274,23 @@ class ParameterEditor:
|
|
|
243
274
|
self.param_dict['ymin']['value'] = 1
|
|
244
275
|
self.param_dict['ymax']['value'] = self.imgshape[0]
|
|
245
276
|
for key, info in self.param_dict.items():
|
|
277
|
+
if key == 'nruns':
|
|
278
|
+
continue
|
|
246
279
|
self.entries[key].delete(0, tk.END)
|
|
247
280
|
self.entries[key].insert(0, str(info['value']))
|
|
248
281
|
|
|
249
282
|
def get_result(self):
|
|
250
283
|
"""Return the updated dictionary"""
|
|
251
284
|
return self.result_dict
|
|
285
|
+
|
|
286
|
+
def update_colour_param_run1_run2(self):
|
|
287
|
+
"""Update the foreground color of run1 and run2 entries."""
|
|
288
|
+
# Highlight run2 parameter if different from run1
|
|
289
|
+
for key in self.param_dict.keys():
|
|
290
|
+
if key.startswith('run1_'):
|
|
291
|
+
parname = key[5:]
|
|
292
|
+
if key in self.entries and 'run2_'+parname in self.entries:
|
|
293
|
+
if self.entries[key].get() != self.entries['run2_'+parname].get():
|
|
294
|
+
self.entries['run2_'+parname].config(fg='red')
|
|
295
|
+
else:
|
|
296
|
+
self.entries['run2_'+parname].config(fg='black')
|
|
@@ -193,8 +193,11 @@ class ReviewCosmicRay(ImageDisplay):
|
|
|
193
193
|
self.interp_s_button = tk.Button(self.button_frame2, text="[s]urface interp.",
|
|
194
194
|
command=lambda: self.interp_a('surface'))
|
|
195
195
|
self.interp_s_button.pack(side=tk.LEFT, padx=5)
|
|
196
|
-
self.
|
|
196
|
+
self.interp_d_button = tk.Button(self.button_frame2, text="me[d]ian",
|
|
197
197
|
command=lambda: self.interp_a('median'))
|
|
198
|
+
self.interp_d_button.pack(side=tk.LEFT, padx=5)
|
|
199
|
+
self.interp_m_button = tk.Button(self.button_frame2, text="[m]ean",
|
|
200
|
+
command=lambda: self.interp_a('mean'))
|
|
198
201
|
self.interp_m_button.pack(side=tk.LEFT, padx=5)
|
|
199
202
|
self.interp_l_button = tk.Button(self.button_frame2, text="[l]acosmic", command=self.use_lacosmic)
|
|
200
203
|
self.interp_l_button.pack(side=tk.LEFT, padx=5)
|
|
@@ -262,20 +265,24 @@ class ReviewCosmicRay(ImageDisplay):
|
|
|
262
265
|
# to avoid image shifts when some pixels are unmarked or new ones are marked
|
|
263
266
|
i0 = int(np.mean(ycr_list_original) + 0.5)
|
|
264
267
|
j0 = int(np.mean(xcr_list_original) + 0.5)
|
|
265
|
-
|
|
268
|
+
max_distance_from_center = np.max(
|
|
269
|
+
[np.max(np.abs(ycr_list_original - i0)),
|
|
270
|
+
np.max(np.abs(xcr_list_original - j0))]
|
|
271
|
+
)
|
|
272
|
+
semiwidth = int(np.max([max_distance_from_center, MAX_PIXEL_DISTANCE_TO_CR]))
|
|
266
273
|
jmin = j0 - semiwidth if j0 - semiwidth >= 0 else 0
|
|
267
274
|
jmax = j0 + semiwidth if j0 + semiwidth < self.data.shape[1] else self.data.shape[1] - 1
|
|
268
275
|
imin = i0 - semiwidth if i0 - semiwidth >= 0 else 0
|
|
269
276
|
imax = i0 + semiwidth if i0 + semiwidth < self.data.shape[0] else self.data.shape[0] - 1
|
|
270
277
|
# Force the region to be of size (2*semiwidth + 1) x (2*semiwidth + 1)
|
|
271
278
|
if jmin == 0:
|
|
272
|
-
jmax = 2 * semiwidth
|
|
279
|
+
jmax = np.min([2 * semiwidth, self.data.shape[1] - 1])
|
|
273
280
|
elif jmax == self.data.shape[1] - 1:
|
|
274
|
-
jmin = self.data.shape[1] - 1 - 2 * semiwidth
|
|
281
|
+
jmin = np.max([0, self.data.shape[1] - 1 - 2 * semiwidth])
|
|
275
282
|
if imin == 0:
|
|
276
|
-
imax = 2 * semiwidth
|
|
283
|
+
imax = np.min([2 * semiwidth, self.data.shape[0] - 1])
|
|
277
284
|
elif imax == self.data.shape[0] - 1:
|
|
278
|
-
imin = self.data.shape[0] - 1 - 2 * semiwidth
|
|
285
|
+
imin = np.max([0, self.data.shape[0] - 1 - 2 * semiwidth])
|
|
279
286
|
self.region = SliceRegion2D(f'[{jmin+1}:{jmax+1}, {imin+1}:{imax+1}]', mode='fits').python
|
|
280
287
|
self.ax.clear()
|
|
281
288
|
vmin = self.get_vmin()
|
|
@@ -333,6 +340,7 @@ class ReviewCosmicRay(ImageDisplay):
|
|
|
333
340
|
self.interp_x_button.config(state=tk.DISABLED)
|
|
334
341
|
self.interp_y_button.config(state=tk.DISABLED)
|
|
335
342
|
self.interp_s_button.config(state=tk.DISABLED)
|
|
343
|
+
self.interp_d_button.config(state=tk.DISABLED)
|
|
336
344
|
self.interp_m_button.config(state=tk.DISABLED)
|
|
337
345
|
self.interp_l_button.config(state=tk.DISABLED)
|
|
338
346
|
self.interp_aux_button.config(state=tk.DISABLED)
|
|
@@ -387,7 +395,7 @@ class ReviewCosmicRay(ImageDisplay):
|
|
|
387
395
|
Parameters
|
|
388
396
|
----------
|
|
389
397
|
method : str
|
|
390
|
-
The interpolation method to use ('surface' or '
|
|
398
|
+
The interpolation method to use ('surface', 'median' or 'mean').
|
|
391
399
|
"""
|
|
392
400
|
print(f"{method} interpolation of cosmic ray {self.cr_index}")
|
|
393
401
|
interpolation_performed, xfit_all, yfit_all = interpolation_a(
|
|
@@ -444,6 +452,7 @@ class ReviewCosmicRay(ImageDisplay):
|
|
|
444
452
|
self.interp_x_button.config(state=tk.DISABLED)
|
|
445
453
|
self.interp_y_button.config(state=tk.DISABLED)
|
|
446
454
|
self.interp_s_button.config(state=tk.DISABLED)
|
|
455
|
+
self.interp_d_button.config(state=tk.DISABLED)
|
|
447
456
|
self.interp_m_button.config(state=tk.DISABLED)
|
|
448
457
|
self.interp_l_button.config(state=tk.DISABLED)
|
|
449
458
|
self.interp_aux_button.config(state=tk.DISABLED)
|
|
@@ -458,6 +467,7 @@ class ReviewCosmicRay(ImageDisplay):
|
|
|
458
467
|
self.interp_x_button.config(state=tk.NORMAL)
|
|
459
468
|
self.interp_y_button.config(state=tk.NORMAL)
|
|
460
469
|
self.interp_s_button.config(state=tk.NORMAL)
|
|
470
|
+
self.interp_d_button.config(state=tk.NORMAL)
|
|
461
471
|
self.interp_m_button.config(state=tk.NORMAL)
|
|
462
472
|
if self.cleandata_lacosmic is not None:
|
|
463
473
|
if self.last_dilation is None or self.last_dilation == 0:
|
|
@@ -484,6 +494,7 @@ class ReviewCosmicRay(ImageDisplay):
|
|
|
484
494
|
self.interp_x_button.config(state=tk.NORMAL)
|
|
485
495
|
self.interp_y_button.config(state=tk.NORMAL)
|
|
486
496
|
self.interp_s_button.config(state=tk.NORMAL)
|
|
497
|
+
self.interp_d_button.config(state=tk.NORMAL)
|
|
487
498
|
self.interp_m_button.config(state=tk.NORMAL)
|
|
488
499
|
if self.cleandata_lacosmic is not None:
|
|
489
500
|
if self.last_dilation is None or self.last_dilation == 0:
|
|
@@ -513,9 +524,12 @@ class ReviewCosmicRay(ImageDisplay):
|
|
|
513
524
|
elif event.key == 's':
|
|
514
525
|
if self.interp_s_button.cget("state") != "disabled":
|
|
515
526
|
self.interp_a('surface')
|
|
527
|
+
elif event.key == 'd':
|
|
528
|
+
if self.interp_d_button.cget("state") != "disabled":
|
|
529
|
+
self.interp_a('median')
|
|
516
530
|
elif event.key == 'm':
|
|
517
531
|
if self.interp_m_button.cget("state") != "disabled":
|
|
518
|
-
self.interp_a('
|
|
532
|
+
self.interp_a('mean')
|
|
519
533
|
elif event.key == 'l':
|
|
520
534
|
if self.interp_l_button.cget("state") != "disabled":
|
|
521
535
|
self.use_lacosmic()
|
|
@@ -551,6 +565,7 @@ class ReviewCosmicRay(ImageDisplay):
|
|
|
551
565
|
self.interp_x_button.config(state=tk.DISABLED)
|
|
552
566
|
self.interp_y_button.config(state=tk.DISABLED)
|
|
553
567
|
self.interp_s_button.config(state=tk.DISABLED)
|
|
568
|
+
self.interp_d_button.config(state=tk.DISABLED)
|
|
554
569
|
self.interp_m_button.config(state=tk.DISABLED)
|
|
555
570
|
self.interp_l_button.config(state=tk.DISABLED)
|
|
556
571
|
self.interp_aux_button.config(state=tk.DISABLED)
|
|
@@ -559,6 +574,7 @@ class ReviewCosmicRay(ImageDisplay):
|
|
|
559
574
|
self.interp_x_button.config(state=tk.NORMAL)
|
|
560
575
|
self.interp_y_button.config(state=tk.NORMAL)
|
|
561
576
|
self.interp_s_button.config(state=tk.NORMAL)
|
|
577
|
+
self.interp_d_button.config(state=tk.NORMAL)
|
|
562
578
|
self.interp_m_button.config(state=tk.NORMAL)
|
|
563
579
|
if self.cleandata_lacosmic is not None:
|
|
564
580
|
if self.last_dilation is None or self.last_dilation == 0:
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2025 Universidad Complutense de Madrid
|
|
4
|
+
#
|
|
5
|
+
# This file is part of teareduce
|
|
6
|
+
#
|
|
7
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
8
|
+
# License-Filename: LICENSE.txt
|
|
9
|
+
#
|
|
10
|
+
"""Tests for the cleanest module."""
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
from ..cleanest.cleanest import cleanest
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_cleanest_no_cr():
|
|
19
|
+
"""Test cleanest function with no cosmic rays."""
|
|
20
|
+
data = np.array([[1, 2, 3],
|
|
21
|
+
[4, 5, 6],
|
|
22
|
+
[7, 8, 9]], dtype=float)
|
|
23
|
+
mask_crfound = np.zeros_like(data, dtype=bool)
|
|
24
|
+
|
|
25
|
+
for interp_method in ['x', 'y', 's', 'd', 'm']:
|
|
26
|
+
cleaned_data, mask_fixed = cleanest(data, mask_crfound, interp_method=interp_method, npoints=2, degree=1)
|
|
27
|
+
|
|
28
|
+
assert np.array_equal(cleaned_data, data), "Data should remain unchanged when no cosmic rays are present."
|
|
29
|
+
assert np.array_equal(mask_fixed, mask_crfound), "Mask should remain unchanged when no cosmic rays are present."
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_cleanest_interpolation_x():
|
|
33
|
+
"""Test cleanest function with interpolation in X direction."""
|
|
34
|
+
data = np.array([[1, 1, 2, 1, 1],
|
|
35
|
+
[2, 2, 3, 2, 2],
|
|
36
|
+
[3, 3, 4, 3, 3]], dtype=float)
|
|
37
|
+
mask_crfound = np.array([[False, False, True, False, False],
|
|
38
|
+
[False, False, True, False, False],
|
|
39
|
+
[False, False, True, False, False]], dtype=bool)
|
|
40
|
+
|
|
41
|
+
cleaned_data, mask_fixed = cleanest(data, mask_crfound,
|
|
42
|
+
interp_method='x', npoints=2, degree=1)
|
|
43
|
+
|
|
44
|
+
expected_data = np.array([[1, 1, 1, 1, 1],
|
|
45
|
+
[2, 2, 2, 2, 2],
|
|
46
|
+
[3, 3, 3, 3, 3]], dtype=float)
|
|
47
|
+
|
|
48
|
+
assert np.allclose(cleaned_data, expected_data), "Interpolation in X direction failed."
|
|
49
|
+
assert np.array_equal(mask_fixed, mask_crfound), "Mask should remain unchanged after interpolation."
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_cleanest_interpolation_y():
|
|
53
|
+
"""Test cleanest function with interpolation in Y direction."""
|
|
54
|
+
data = np.array([[1, 2, 3],
|
|
55
|
+
[1, 2, 3],
|
|
56
|
+
[2, 3, 4],
|
|
57
|
+
[1, 2, 3],
|
|
58
|
+
[1, 2, 3]], dtype=float)
|
|
59
|
+
mask_crfound = np.array([[False, False, False],
|
|
60
|
+
[False, False, False],
|
|
61
|
+
[True, True, True],
|
|
62
|
+
[False, False, False],
|
|
63
|
+
[False, False, False]], dtype=bool)
|
|
64
|
+
|
|
65
|
+
cleaned_data, mask_fixed = cleanest(data, mask_crfound,
|
|
66
|
+
interp_method='y', npoints=2, degree=1)
|
|
67
|
+
|
|
68
|
+
expected_data = np.array([[1, 2, 3],
|
|
69
|
+
[1, 2, 3],
|
|
70
|
+
[1, 2, 3],
|
|
71
|
+
[1, 2, 3],
|
|
72
|
+
[1, 2, 3]], dtype=float)
|
|
73
|
+
assert np.allclose(cleaned_data, expected_data), "Interpolation in Y direction failed."
|
|
74
|
+
assert np.array_equal(mask_fixed, mask_crfound), "Mask should remain unchanged after interpolation."
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_cleanest_interpolation_surface():
|
|
78
|
+
"""Test cleanest function with surface interpolation."""
|
|
79
|
+
data = np.array([[1, 2, 3],
|
|
80
|
+
[4, 100, 6],
|
|
81
|
+
[7, 8, 9]], dtype=float)
|
|
82
|
+
mask_crfound = np.array([[False, False, False],
|
|
83
|
+
[False, True, False],
|
|
84
|
+
[False, False, False]], dtype=bool)
|
|
85
|
+
|
|
86
|
+
cleaned_data, mask_fixed = cleanest(data, mask_crfound,
|
|
87
|
+
interp_method='s', npoints=1)
|
|
88
|
+
|
|
89
|
+
expected_data = np.array([[1, 2, 3],
|
|
90
|
+
[4, 5, 6],
|
|
91
|
+
[7, 8, 9]], dtype=float)
|
|
92
|
+
assert np.allclose(cleaned_data, expected_data), "Surface interpolation failed."
|
|
93
|
+
assert np.array_equal(mask_fixed, mask_crfound), "Mask should remain unchanged after interpolation."
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_cleanest_interpolation_median():
|
|
97
|
+
"""Test cleanest function with median border pixel interpolation."""
|
|
98
|
+
data = np.array([[1, 2, 3],
|
|
99
|
+
[4, 100, 6],
|
|
100
|
+
[7, 8, 9]], dtype=float)
|
|
101
|
+
mask_crfound = np.array([[False, False, False],
|
|
102
|
+
[False, True, False],
|
|
103
|
+
[False, False, False]], dtype=bool)
|
|
104
|
+
|
|
105
|
+
cleaned_data, mask_fixed = cleanest(data, mask_crfound,
|
|
106
|
+
interp_method='d', npoints=1)
|
|
107
|
+
|
|
108
|
+
expected_data = np.array([[1, 2, 3],
|
|
109
|
+
[4, 5, 6],
|
|
110
|
+
[7, 8, 9]], dtype=float)
|
|
111
|
+
assert np.allclose(cleaned_data, expected_data), "Median border pixel interpolation failed."
|
|
112
|
+
assert np.array_equal(mask_fixed, mask_crfound), "Mask should remain unchanged after interpolation."
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_cleanest_interpolation_mean():
|
|
116
|
+
"""Test cleanest function with mean border pixel interpolation."""
|
|
117
|
+
data = np.array([[1, 2, 3],
|
|
118
|
+
[4, 100, 6],
|
|
119
|
+
[7, 8, 9]], dtype=float)
|
|
120
|
+
mask_crfound = np.array([[False, False, False],
|
|
121
|
+
[False, True, False],
|
|
122
|
+
[False, False, False]], dtype=bool)
|
|
123
|
+
|
|
124
|
+
cleaned_data, mask_fixed = cleanest(data, mask_crfound,
|
|
125
|
+
interp_method='m', npoints=1)
|
|
126
|
+
|
|
127
|
+
expected_data = np.array([[1, 2, 3],
|
|
128
|
+
[4, 5, 6],
|
|
129
|
+
[7, 8, 9]], dtype=float)
|
|
130
|
+
assert np.allclose(cleaned_data, expected_data), "Mean border pixel interpolation failed."
|
|
131
|
+
assert np.array_equal(mask_fixed, mask_crfound), "Mask should remain unchanged after interpolation."
|
teareduce/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: teareduce
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: Utilities for astronomical data reduction
|
|
5
5
|
Author-email: Nicolás Cardiel <cardiel@ucm.es>
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -20,6 +20,7 @@ Requires-Python: >=3.10
|
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE.txt
|
|
22
22
|
Requires-Dist: astropy
|
|
23
|
+
Requires-Dist: ccdproc
|
|
23
24
|
Requires-Dist: lmfit
|
|
24
25
|
Requires-Dist: matplotlib
|
|
25
26
|
Requires-Dist: numpy>=1.22
|
|
@@ -15,30 +15,33 @@ teareduce/sdistortion.py,sha256=5ZsZn4vD5Sw2aoqO8-NIOH7H89Zmh7ZDkow6YbAotHU,5916
|
|
|
15
15
|
teareduce/simulateccdexposure.py,sha256=cdbpca6GVuM3d7R1LGzlIZZvjTq_jzrlkk_Cli7aouQ,24636
|
|
16
16
|
teareduce/sliceregion.py,sha256=Jdf8XvmGaY_vaY1cneTaRtSOYPxpUsJm9cXJDDMa0YM,18626
|
|
17
17
|
teareduce/statsummary.py,sha256=VTNAnBV8z6suqiyB2Lhw3YjUUOjlmwUPX3enkOKRF54,5422
|
|
18
|
-
teareduce/version.py,sha256=
|
|
18
|
+
teareduce/version.py,sha256=PhV5vwOfjd2-SjStecfYtye4EUgepCl7-M0rx5QAJA0,419
|
|
19
19
|
teareduce/wavecal.py,sha256=2MewWz5Y3ms41c305UtWOM_LaLNdk1mugDXCtzf-fSw,68586
|
|
20
20
|
teareduce/write_array_to_fits.py,sha256=kWDrEH9coJ1yIu56oQJpWtDqJL4c8HGmssE9jle4e94,617
|
|
21
21
|
teareduce/zscale.py,sha256=SDgmcDD2N5GvDn46JADCgTQJPBF_N_jSKMHoeTz9Nsw,1152
|
|
22
|
-
teareduce/cleanest/__init__.py,sha256=
|
|
22
|
+
teareduce/cleanest/__init__.py,sha256=3O-eptEAQareN504tLALMrJVQPGCuBx7YTfQGqMKjmc,205
|
|
23
23
|
teareduce/cleanest/__main__.py,sha256=mobw6PW6JauOBKt-0eNvUH0UCC_D-AjrbTlzfAu7joY,1732
|
|
24
|
-
teareduce/cleanest/
|
|
25
|
-
teareduce/cleanest/
|
|
24
|
+
teareduce/cleanest/cleanest.py,sha256=tf1RdKRZfSXkGXbMVkN-Y0eudFb_XUfCl8Va2mJ0kK8,5070
|
|
25
|
+
teareduce/cleanest/cosmicraycleanerapp.py,sha256=gYP9KaueOPaUX2iLnHZc5rWBeiAm8u-2Lxl3t83KBls,36409
|
|
26
|
+
teareduce/cleanest/definitions.py,sha256=pcq0gk_EnDVkRfatWseQvRIyF445jIarICkuWIW3oqE,2343
|
|
27
|
+
teareduce/cleanest/dilatemask.py,sha256=I5tHAv5VEO6V0Wed8Ar20uLt4F9P-tgjmLL5BAaFvgM,1276
|
|
26
28
|
teareduce/cleanest/find_closest_true.py,sha256=mWdIvhipzAXDRKfePDrP7f0lP4U48cckpHiKwiB4jHI,1320
|
|
27
29
|
teareduce/cleanest/imagedisplay.py,sha256=4FhoRcyskj_F6gaza4U37sLODpe-Sud3sunM8_-p5iY,5877
|
|
28
|
-
teareduce/cleanest/interpolation_a.py,sha256=
|
|
29
|
-
teareduce/cleanest/interpolation_x.py,sha256=
|
|
30
|
-
teareduce/cleanest/interpolation_y.py,sha256=
|
|
31
|
-
teareduce/cleanest/interpolationeditor.py,sha256=
|
|
32
|
-
teareduce/cleanest/parametereditor.py,sha256=
|
|
33
|
-
teareduce/cleanest/reviewcosmicray.py,sha256=
|
|
30
|
+
teareduce/cleanest/interpolation_a.py,sha256=upbfZBU0i7YyXR4EJj-WdnMkWbajgDbnUDvJaoLAeEg,4039
|
|
31
|
+
teareduce/cleanest/interpolation_x.py,sha256=D5hKbobT6lJk18XktP3PXhzEN12zqed6U18yfQ-reLQ,3744
|
|
32
|
+
teareduce/cleanest/interpolation_y.py,sha256=O6yw5nKKlTdV6qsP1Ku6CwGhXB6o3j0_YQirXyILi8c,3759
|
|
33
|
+
teareduce/cleanest/interpolationeditor.py,sha256=F5_FyOLHgtvoan22Lfaoa9j-kdKrygaQARi1SYsDfPA,8179
|
|
34
|
+
teareduce/cleanest/parametereditor.py,sha256=wfl1Zem_FI02YAVawq8r1FLGz5dXB5-S8RF1d_v6ZEQ,12495
|
|
35
|
+
teareduce/cleanest/reviewcosmicray.py,sha256=tL1iutMrWlp0Ep5eDnuXlBPaIsFgYzy_c7BGU8_Gcd0,27775
|
|
34
36
|
teareduce/cookbook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
37
|
teareduce/cookbook/get_cookbook_file.py,sha256=vde-iNii2lm1QII8GmLRsFsKNxkdsd7njCBE-8Z7io0,1088
|
|
36
38
|
teareduce/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
|
+
teareduce/tests/test_cleanest.py,sha256=0uiun1Uloh3rt5ppkv1aG04KUbOJvYZ9C5BvqNizLuI,5562
|
|
37
40
|
teareduce/tests/test_sliceregion.py,sha256=S7Zoh2eEBFIEbfsXgWBEMCf7pottjw2oLhqlZJQkAwg,3785
|
|
38
41
|
teareduce/tests/test_version.py,sha256=mKLnbXyvVNc1pATq5PxR8qeoFMPAFL_GilFV6IHLOi0,172
|
|
39
|
-
teareduce-0.5.
|
|
40
|
-
teareduce-0.5.
|
|
41
|
-
teareduce-0.5.
|
|
42
|
-
teareduce-0.5.
|
|
43
|
-
teareduce-0.5.
|
|
44
|
-
teareduce-0.5.
|
|
42
|
+
teareduce-0.5.3.dist-info/licenses/LICENSE.txt,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
43
|
+
teareduce-0.5.3.dist-info/METADATA,sha256=4CHBLx7SPJxE9Yh6gaxL5dpfxPkqH-g5DFcucCNzn9k,3113
|
|
44
|
+
teareduce-0.5.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
45
|
+
teareduce-0.5.3.dist-info/entry_points.txt,sha256=6yBvig5jTL2ugqz5SF767AiszzrHKGRASsX1II84kqA,66
|
|
46
|
+
teareduce-0.5.3.dist-info/top_level.txt,sha256=7OkwtX9zNRkGJ7ACgjk4ESgC74qUYcS5O2qcO0v-Si4,10
|
|
47
|
+
teareduce-0.5.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|