teareduce 0.6.8__py3-none-any.whl → 0.7.0__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/__main__.py +74 -22
- teareduce/cleanest/choose_index_with_three_buttons.py +126 -0
- teareduce/cleanest/cosmicraycleanerapp.py +192 -68
- teareduce/cleanest/definitions.py +1 -1
- teareduce/cleanest/interpolationeditor.py +73 -10
- teareduce/cleanest/reviewcosmicray.py +182 -25
- teareduce/simulateccdexposure.py +3 -1
- teareduce/version.py +2 -2
- {teareduce-0.6.8.dist-info → teareduce-0.7.0.dist-info}/METADATA +1 -1
- {teareduce-0.6.8.dist-info → teareduce-0.7.0.dist-info}/RECORD +14 -13
- {teareduce-0.6.8.dist-info → teareduce-0.7.0.dist-info}/WHEEL +1 -1
- {teareduce-0.6.8.dist-info → teareduce-0.7.0.dist-info}/entry_points.txt +0 -0
- {teareduce-0.6.8.dist-info → teareduce-0.7.0.dist-info}/licenses/LICENSE.txt +0 -0
- {teareduce-0.6.8.dist-info → teareduce-0.7.0.dist-info}/top_level.txt +0 -0
teareduce/cleanest/__main__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#
|
|
2
|
-
# Copyright 2025 Universidad Complutense de Madrid
|
|
2
|
+
# Copyright 2025-2026 Universidad Complutense de Madrid
|
|
3
3
|
#
|
|
4
4
|
# This file is part of teareduce
|
|
5
5
|
#
|
|
@@ -10,11 +10,10 @@
|
|
|
10
10
|
"""Interactive Cosmic Ray cleaning tool."""
|
|
11
11
|
|
|
12
12
|
import argparse
|
|
13
|
+
import glob
|
|
13
14
|
import tkinter as tk
|
|
14
15
|
from tkinter import filedialog
|
|
15
|
-
from tkinter import simpledialog
|
|
16
16
|
import os
|
|
17
|
-
from pathlib import Path
|
|
18
17
|
import platform
|
|
19
18
|
from rich import print
|
|
20
19
|
from rich_argparse import RichHelpFormatter
|
|
@@ -38,14 +37,7 @@ def main():
|
|
|
38
37
|
formatter_class=RichHelpFormatter,
|
|
39
38
|
)
|
|
40
39
|
parser.add_argument("input_fits", nargs="?", default=None, help="Path to the FITS file to be cleaned.")
|
|
41
|
-
parser.add_argument("--
|
|
42
|
-
parser.add_argument("--auxfile", type=str, default=None, help="Auxiliary FITS file")
|
|
43
|
-
parser.add_argument(
|
|
44
|
-
"--extension_auxfile",
|
|
45
|
-
type=str,
|
|
46
|
-
default="0",
|
|
47
|
-
help="FITS extension for auxiliary file (default: 0).",
|
|
48
|
-
)
|
|
40
|
+
parser.add_argument("--auxfile", type=str, default=None, help="Auxiliary FITS files (comma-separated if several).")
|
|
49
41
|
parser.add_argument(
|
|
50
42
|
"--fontfamily",
|
|
51
43
|
type=str,
|
|
@@ -114,17 +106,77 @@ def main():
|
|
|
114
106
|
use_auxfile = True
|
|
115
107
|
if use_auxfile:
|
|
116
108
|
print(f"Selected auxiliary FITS file: {args.auxfile}")
|
|
117
|
-
|
|
109
|
+
extension_auxfile = ask_extension_input_image(args.auxfile, imgshape=None)
|
|
110
|
+
args.auxfile = f"{args.auxfile}[{extension_auxfile}]"
|
|
111
|
+
else:
|
|
112
|
+
args.auxfile = None
|
|
118
113
|
root.destroy()
|
|
119
114
|
|
|
120
|
-
# Check that input
|
|
121
|
-
if
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
115
|
+
# Check that input file exists
|
|
116
|
+
if "[" in args.input_fits:
|
|
117
|
+
input_fits = args.input_fits[: args.input_fits.index("[")]
|
|
118
|
+
extension = args.input_fits[args.input_fits.index("[") + 1 : args.input_fits.index("]")]
|
|
119
|
+
else:
|
|
120
|
+
input_fits = args.input_fits
|
|
121
|
+
extension = "0"
|
|
122
|
+
if not os.path.isfile(input_fits):
|
|
123
|
+
print(f"Error: File '{input_fits}' does not exist.")
|
|
126
124
|
exit(1)
|
|
127
125
|
|
|
126
|
+
# Process auxiliary files if provided
|
|
127
|
+
auxfile_list = []
|
|
128
|
+
extension_auxfile_list = []
|
|
129
|
+
if args.auxfile is not None:
|
|
130
|
+
# Check several auxiliary files separated by commas
|
|
131
|
+
if "\n" in args.auxfile:
|
|
132
|
+
if "?" in args.auxfile or "*" in args.auxfile:
|
|
133
|
+
print("Error: Cannot combine newlines and wildcards in --auxfile specification.")
|
|
134
|
+
exit(1)
|
|
135
|
+
# Replace newlines by commas to allow:
|
|
136
|
+
# --auxfile="`ls file??.fits`" -> without extensions
|
|
137
|
+
# --auxfile="`for f in file??.fits; do echo "${f}[primary]"; done`" -> with extension name (the same for all files!)
|
|
138
|
+
args.auxfile = args.auxfile.replace("\n", ",")
|
|
139
|
+
elif "?" in args.auxfile or "*" in args.auxfile:
|
|
140
|
+
# Handle wildcards to allow:
|
|
141
|
+
# --auxfile="file??.fits" -> without extensions
|
|
142
|
+
# --auxfile="file??.fits[primary]" -> with extension name (the same for all files!)
|
|
143
|
+
if "," in args.auxfile:
|
|
144
|
+
print("Error: Cannot combine wildcards with commas in --auxfile specification.")
|
|
145
|
+
exit(1)
|
|
146
|
+
# Expand possible wildcards in the auxiliary file specification
|
|
147
|
+
if "[" in args.auxfile:
|
|
148
|
+
s = args.auxfile.strip()
|
|
149
|
+
ext = None
|
|
150
|
+
if s.endswith("]"):
|
|
151
|
+
ext = s[s.rfind("[") + 1 : s.rfind("]")]
|
|
152
|
+
s = s[: s.rfind("[")]
|
|
153
|
+
matched_files = sorted(glob.glob(s))
|
|
154
|
+
if len(matched_files) == 0:
|
|
155
|
+
print(f"Error: No files matched the pattern '{s}'.")
|
|
156
|
+
exit(1)
|
|
157
|
+
args.auxfile = ",".join([f"{fname}[{ext}]" if ext is not None else fname for fname in matched_files])
|
|
158
|
+
else:
|
|
159
|
+
matched_files = sorted(glob.glob(args.auxfile))
|
|
160
|
+
if len(matched_files) == 0:
|
|
161
|
+
print(f"Error: No files matched the pattern '{args.auxfile}'.")
|
|
162
|
+
exit(1)
|
|
163
|
+
args.auxfile = ",".join(matched_files)
|
|
164
|
+
# Now process the comma-separated auxiliary files
|
|
165
|
+
for item in args.auxfile.split(","):
|
|
166
|
+
# Extract possible [ext] from filename
|
|
167
|
+
if "[" in item:
|
|
168
|
+
# Separate filename and extension, removing blanks
|
|
169
|
+
fname = item[: item.index("[")].strip()
|
|
170
|
+
# Check that file exists
|
|
171
|
+
if not os.path.isfile(fname):
|
|
172
|
+
print(f"Error: File '{fname}' does not exist.")
|
|
173
|
+
exit(1)
|
|
174
|
+
auxfile_list.append(fname)
|
|
175
|
+
extension_auxfile_list.append(item[item.index("[") + 1 : item.index("]")])
|
|
176
|
+
else:
|
|
177
|
+
auxfile_list.append(item.strip())
|
|
178
|
+
extension_auxfile_list.append("0")
|
|
179
|
+
|
|
128
180
|
# Initialize Tkinter root
|
|
129
181
|
try:
|
|
130
182
|
root = tk.Tk()
|
|
@@ -149,10 +201,10 @@ def main():
|
|
|
149
201
|
# Create and run the application
|
|
150
202
|
CosmicRayCleanerApp(
|
|
151
203
|
root=root,
|
|
152
|
-
input_fits=
|
|
153
|
-
extension=
|
|
154
|
-
|
|
155
|
-
|
|
204
|
+
input_fits=input_fits,
|
|
205
|
+
extension=extension,
|
|
206
|
+
auxfile_list=auxfile_list,
|
|
207
|
+
extension_auxfile_list=extension_auxfile_list,
|
|
156
208
|
fontfamily=args.fontfamily,
|
|
157
209
|
fontsize=args.fontsize,
|
|
158
210
|
width=args.width,
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 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
|
+
import tkinter as tk
|
|
11
|
+
from tkinter import simpledialog, ttk
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ThreeButtonListDialog(simpledialog.Dialog):
|
|
15
|
+
"""
|
|
16
|
+
A modal dialog that shows a list of strings and returns:
|
|
17
|
+
- 1..N : the highlighted item index (1-based) when pressing OK (or double-click / Enter)
|
|
18
|
+
- 0 : when pressing Cancel or closing the window (Esc or window X)
|
|
19
|
+
- N+1 : when pressing New (N = len(items))
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, parent, title, prompt, items):
|
|
23
|
+
self.prompt = prompt
|
|
24
|
+
self.items = list(items)
|
|
25
|
+
self.result = None
|
|
26
|
+
self._force_new = False # used to bypass selection validation for "New"
|
|
27
|
+
super().__init__(parent, title)
|
|
28
|
+
|
|
29
|
+
# ---------- Core layout ----------
|
|
30
|
+
def body(self, master):
|
|
31
|
+
ttk.Label(master, text=self.prompt).grid(row=0, column=0, columnspan=1, sticky="w", padx=8, pady=(8, 4))
|
|
32
|
+
|
|
33
|
+
self.listbox = tk.Listbox(master, height=min(12, max(4, len(self.items))), activestyle="dotbox")
|
|
34
|
+
for s in self.items:
|
|
35
|
+
self.listbox.insert(tk.END, s)
|
|
36
|
+
self.listbox.grid(row=1, column=0, sticky="nsew", padx=(8, 0), pady=4)
|
|
37
|
+
|
|
38
|
+
scrollbar = ttk.Scrollbar(master, orient="vertical", command=self.listbox.yview)
|
|
39
|
+
self.listbox.configure(yscrollcommand=scrollbar.set)
|
|
40
|
+
scrollbar.grid(row=1, column=1, sticky="ns", pady=4, padx=(0, 8))
|
|
41
|
+
|
|
42
|
+
# Keyboard bindings
|
|
43
|
+
self.listbox.bind("<Double-Button-1>", lambda e: self.ok()) # OK on double-click
|
|
44
|
+
self.bind("<Return>", lambda e: self.ok()) # Enter = OK
|
|
45
|
+
self.bind("<Escape>", lambda e: self.cancel()) # Esc = Cancel (→ 0)
|
|
46
|
+
|
|
47
|
+
# Resize behavior
|
|
48
|
+
master.grid_columnconfigure(0, weight=1)
|
|
49
|
+
master.grid_rowconfigure(1, weight=1)
|
|
50
|
+
|
|
51
|
+
if self.items:
|
|
52
|
+
self.listbox.selection_set(0)
|
|
53
|
+
self.listbox.activate(0)
|
|
54
|
+
|
|
55
|
+
return self.listbox # initial focus on listbox
|
|
56
|
+
|
|
57
|
+
# ---------- Buttons (OK, Cancel, New) ----------
|
|
58
|
+
def buttonbox(self):
|
|
59
|
+
box = ttk.Frame(self)
|
|
60
|
+
|
|
61
|
+
self.ok_button = ttk.Button(box, text="OK", width=10, command=self.ok)
|
|
62
|
+
self.ok_button.grid(row=0, column=0, padx=5, pady=8)
|
|
63
|
+
|
|
64
|
+
self.cancel_button = ttk.Button(box, text="Cancel", width=10, command=self.cancel)
|
|
65
|
+
self.cancel_button.grid(row=0, column=1, padx=5, pady=8)
|
|
66
|
+
|
|
67
|
+
self.new_button = ttk.Button(box, text="New", width=10, command=self._on_new)
|
|
68
|
+
self.new_button.grid(row=0, column=2, padx=5, pady=8)
|
|
69
|
+
|
|
70
|
+
# Bindings for Return/Escape already set in body()
|
|
71
|
+
self.bind("<Return>", lambda e: self.ok())
|
|
72
|
+
self.bind("<Escape>", lambda e: self.cancel())
|
|
73
|
+
|
|
74
|
+
box.pack(side="bottom", fill="x")
|
|
75
|
+
|
|
76
|
+
# ---------- Dialog flow ----------
|
|
77
|
+
def validate(self):
|
|
78
|
+
"""
|
|
79
|
+
Called by OK to decide whether dialog can close.
|
|
80
|
+
- If 'New' was pressed, allow closing without selection.
|
|
81
|
+
- Otherwise, ensure there is a highlighted item.
|
|
82
|
+
"""
|
|
83
|
+
if self._force_new:
|
|
84
|
+
return True
|
|
85
|
+
cur = self.listbox.curselection()
|
|
86
|
+
if not cur:
|
|
87
|
+
self.bell()
|
|
88
|
+
return False
|
|
89
|
+
self._selected_index = cur[0]
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
def apply(self):
|
|
93
|
+
"""
|
|
94
|
+
Called after validate(). Set the final result here.
|
|
95
|
+
"""
|
|
96
|
+
if self._force_new:
|
|
97
|
+
self.result = len(self.items) + 1 # N+1 (New)
|
|
98
|
+
else:
|
|
99
|
+
self.result = self._selected_index + 1 # 1..N (index of selection)
|
|
100
|
+
|
|
101
|
+
def _on_new(self):
|
|
102
|
+
"""
|
|
103
|
+
Trigger New: returns N+1.
|
|
104
|
+
This must bypass selection validation.
|
|
105
|
+
"""
|
|
106
|
+
self._force_new = True
|
|
107
|
+
self.ok()
|
|
108
|
+
|
|
109
|
+
# Important: Don't overwrite OK/New results when closing via Dialog.ok().
|
|
110
|
+
# Dialog.ok() calls validate() -> apply() -> cancel().
|
|
111
|
+
# Only set 0 in cancel() if result wasn't already set.
|
|
112
|
+
def cancel(self, event=None):
|
|
113
|
+
if self.result is None:
|
|
114
|
+
self.result = 0
|
|
115
|
+
super().cancel(event)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def choose_index_with_three_buttons(parent, title, prompt, items):
|
|
119
|
+
"""
|
|
120
|
+
Show the dialog and return:
|
|
121
|
+
- 1..N : highlighted item index (OK)
|
|
122
|
+
- 0 : Cancel/close
|
|
123
|
+
- N+1 : New
|
|
124
|
+
"""
|
|
125
|
+
dlg = ThreeButtonListDialog(parent, title, prompt, items)
|
|
126
|
+
return dlg.result
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#
|
|
2
|
-
# Copyright 2025 Universidad Complutense de Madrid
|
|
2
|
+
# Copyright 2025-2026 Universidad Complutense de Madrid
|
|
3
3
|
#
|
|
4
4
|
# This file is part of teareduce
|
|
5
5
|
#
|
|
@@ -69,6 +69,7 @@ from rich import print
|
|
|
69
69
|
|
|
70
70
|
from .askextension import ask_extension_input_image
|
|
71
71
|
from .centerchildparent import center_on_parent
|
|
72
|
+
from .choose_index_with_three_buttons import choose_index_with_three_buttons
|
|
72
73
|
from .definitions import cosmicconn_default_dict
|
|
73
74
|
from .definitions import deepcr_default_dict
|
|
74
75
|
from .definitions import lacosmic_default_dict
|
|
@@ -117,8 +118,8 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
117
118
|
root,
|
|
118
119
|
input_fits,
|
|
119
120
|
extension="0",
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
auxfile_list=[],
|
|
122
|
+
extension_auxfile_list=[],
|
|
122
123
|
fontfamily=DEFAULT_FONT_FAMILY,
|
|
123
124
|
fontsize=DEFAULT_FONT_SIZE,
|
|
124
125
|
width=DEFAULT_TK_WINDOW_SIZE_X,
|
|
@@ -136,10 +137,10 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
136
137
|
Path to the FITS file to be cleaned.
|
|
137
138
|
extension : str, optional
|
|
138
139
|
FITS extension to use (default is "0").
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
FITS
|
|
140
|
+
auxfile_list : list of str, optional
|
|
141
|
+
List of paths to auxiliary FITS files (default is empty list).
|
|
142
|
+
extension_auxfile_list : list of str, optional
|
|
143
|
+
List of FITS extensions for auxiliary files (default is empty list).
|
|
143
144
|
fontfamily : str, optional
|
|
144
145
|
Font family for the GUI (default is "Helvetica").
|
|
145
146
|
fontsize : int, optional
|
|
@@ -222,12 +223,18 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
222
223
|
FITS extension to use.
|
|
223
224
|
data : np.ndarray
|
|
224
225
|
The image data from the FITS file.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
FITS
|
|
229
|
-
auxdata : np.ndarray
|
|
230
|
-
The image data from the auxiliary FITS
|
|
226
|
+
auxfile_list : list of str
|
|
227
|
+
List of Paths to auxiliary FITS files.
|
|
228
|
+
extension_auxfile_list : list of int
|
|
229
|
+
List of FITS extensions for auxiliary files.
|
|
230
|
+
auxdata : list of np.ndarray
|
|
231
|
+
The list of image data from the auxiliary FITS files.
|
|
232
|
+
auxdata_mean : np.ndarray
|
|
233
|
+
The mean image data computed from the auxiliary FITS files.
|
|
234
|
+
auxdata_median : np.ndarray
|
|
235
|
+
The median image data computed from the auxiliary FITS files.
|
|
236
|
+
auxdata_min : np.ndarray
|
|
237
|
+
The minimum image data computed from the auxiliary FITS files.
|
|
231
238
|
overplot_cr_pixels : bool
|
|
232
239
|
Flag to indicate whether to overlay cosmic ray pixels.
|
|
233
240
|
mask_crfound : np.ndarray
|
|
@@ -300,9 +307,12 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
300
307
|
self.input_fits = input_fits
|
|
301
308
|
self.extension = extension
|
|
302
309
|
self.data = None
|
|
303
|
-
self.
|
|
304
|
-
self.
|
|
305
|
-
self.auxdata =
|
|
310
|
+
self.auxfile_list = auxfile_list
|
|
311
|
+
self.extension_auxfile_list = extension_auxfile_list
|
|
312
|
+
self.auxdata = []
|
|
313
|
+
self.auxdata_mean = None
|
|
314
|
+
self.auxdata_median = None
|
|
315
|
+
self.auxdata_min = None
|
|
306
316
|
self.overplot_cr_pixels = True
|
|
307
317
|
self.mask_crfound = None
|
|
308
318
|
self.load_fits_file()
|
|
@@ -452,7 +462,7 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
452
462
|
help_text="Toggle the display of auxiliary data.",
|
|
453
463
|
)
|
|
454
464
|
self.toggle_auxdata_button.pack(side=tk.LEFT, padx=5)
|
|
455
|
-
if self.auxdata is None:
|
|
465
|
+
if len(self.auxdata) == 0 or self.auxdata is None:
|
|
456
466
|
self.toggle_auxdata_button.config(state=tk.DISABLED)
|
|
457
467
|
else:
|
|
458
468
|
self.toggle_auxdata_button.config(state=tk.NORMAL)
|
|
@@ -578,14 +588,14 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
578
588
|
ylabel = "Y pixel (from 1 to NAXIS2)"
|
|
579
589
|
extent = [0.5, self.data.shape[1] + 0.5, 0.5, self.data.shape[0] + 0.5]
|
|
580
590
|
self.image_aspect = "equal"
|
|
581
|
-
self.displaying_auxdata =
|
|
591
|
+
self.displaying_auxdata = 0 # 0: main data, > 0: auxdata
|
|
582
592
|
self.image, _, _ = imshow(
|
|
583
593
|
fig=self.fig,
|
|
584
594
|
ax=self.ax,
|
|
585
595
|
data=self.data,
|
|
586
596
|
vmin=vmin,
|
|
587
597
|
vmax=vmax,
|
|
588
|
-
title=f"data: {os.path.basename(self.input_fits)}",
|
|
598
|
+
title=f"data: {os.path.basename(self.input_fits)}[{self.extension}]",
|
|
589
599
|
xlabel=xlabel,
|
|
590
600
|
ylabel=ylabel,
|
|
591
601
|
extent=extent,
|
|
@@ -694,8 +704,8 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
694
704
|
Notes
|
|
695
705
|
-----
|
|
696
706
|
This method loads the FITS file specified by `self.input_fits` and
|
|
697
|
-
reads the data from the specified extension. If
|
|
698
|
-
provided, it also loads the auxiliary data from the specified
|
|
707
|
+
reads the data from the specified extension. If auxiliary files are
|
|
708
|
+
provided, it also loads the auxiliary data from the specified extensions.
|
|
699
709
|
The loaded data is stored in `self.data` and `self.auxdata` attributes.
|
|
700
710
|
"""
|
|
701
711
|
# check if extension is compatible with an integer
|
|
@@ -725,43 +735,67 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
725
735
|
self.mask_crfound = np.zeros(self.data.shape, dtype=bool)
|
|
726
736
|
naxis2, naxis1 = self.data.shape
|
|
727
737
|
self.region = SliceRegion2D(f"[1:{naxis1}, 1:{naxis2}]", mode="fits").python
|
|
738
|
+
|
|
728
739
|
# Read auxiliary file if provided
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
740
|
+
naux = len(self.auxfile_list)
|
|
741
|
+
if naux > 0:
|
|
742
|
+
for i in range(naux):
|
|
743
|
+
# check if extension_auxfile is compatible with an integer
|
|
744
|
+
try:
|
|
745
|
+
extnum_aux = int(self.extension_auxfile_list[i])
|
|
746
|
+
self.extension_auxfile_list[i] = extnum_aux
|
|
747
|
+
except ValueError:
|
|
748
|
+
# Keep as string (delaying checking until opening the file)
|
|
749
|
+
self.extension_auxfile_list[i] = self.extension_auxfile_list[i].upper() # Convert to uppercase
|
|
750
|
+
try:
|
|
751
|
+
with fits.open(self.auxfile_list[i], mode="readonly") as hdul_aux:
|
|
752
|
+
if isinstance(self.extension_auxfile_list[i], int):
|
|
753
|
+
if self.extension_auxfile_list[i] < 0 or self.extension_auxfile_list[i] >= len(hdul_aux):
|
|
754
|
+
raise IndexError(f"Extension index {self.extension_auxfile_list[i]} out of range.")
|
|
755
|
+
else:
|
|
756
|
+
if self.extension_auxfile_list[i] not in hdul_aux:
|
|
757
|
+
raise KeyError(f"Extension name '{self.extension_auxfile_list[i]}' not found.")
|
|
758
|
+
print(
|
|
759
|
+
f"Reading auxiliary file [bold green]{self.auxfile_list[i]}[/bold green], extension {self.extension_auxfile_list[i]}"
|
|
760
|
+
)
|
|
761
|
+
self.auxdata.append(hdul_aux[self.extension_auxfile_list[i]].data)
|
|
762
|
+
if self.auxdata[i].shape != self.data.shape:
|
|
763
|
+
print(f"data shape...: {self.data.shape}")
|
|
764
|
+
print(f"auxdata shape: {self.auxdata[i].shape}")
|
|
765
|
+
raise ValueError("Auxiliary file has different shape.")
|
|
766
|
+
except Exception as e:
|
|
767
|
+
sys.exit(f"Error loading auxiliary FITS file {self.auxfile_list[i]}:\n{e}")
|
|
768
|
+
# Compute mean, median, min if multiple auxdata are loaded
|
|
769
|
+
if naux > 1:
|
|
770
|
+
data3d = np.zeros((len(self.auxdata),) + self.data.shape, dtype=self.data.dtype)
|
|
771
|
+
for i in range(len(self.auxdata)):
|
|
772
|
+
data3d[i, :, :] = self.auxdata[i]
|
|
773
|
+
self.auxdata_mean = np.mean(data3d, axis=0)
|
|
774
|
+
self.auxdata_median = np.median(data3d, axis=0)
|
|
775
|
+
self.auxdata_min = np.min(data3d, axis=0)
|
|
776
|
+
else:
|
|
777
|
+
self.auxdata_mean = self.auxdata[0].copy()
|
|
778
|
+
self.auxdata_median = self.auxdata[0].copy()
|
|
779
|
+
self.auxdata_min = self.auxdata[0].copy()
|
|
755
780
|
|
|
756
781
|
def load_auxdata_from_file(self):
|
|
757
782
|
"""Load auxiliary data from a FITS file."""
|
|
758
|
-
if self.
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
783
|
+
if len(self.auxfile_list) > 0:
|
|
784
|
+
nchoice = choose_index_with_three_buttons(
|
|
785
|
+
parent=self.root,
|
|
786
|
+
title="Select Auxiliary Data Index",
|
|
787
|
+
prompt="Select the auxiliary data to overwrite\nor create new auxiliary data:",
|
|
788
|
+
items=[
|
|
789
|
+
f"Aux. data #{i+1}: {os.path.basename(self.auxfile_list[i])}[{self.extension_auxfile_list[i]}]"
|
|
790
|
+
for i in range(len(self.auxfile_list))
|
|
791
|
+
],
|
|
762
792
|
)
|
|
763
|
-
if
|
|
793
|
+
if nchoice == 0:
|
|
764
794
|
return
|
|
795
|
+
else:
|
|
796
|
+
aux_number = nchoice - 1
|
|
797
|
+
else:
|
|
798
|
+
aux_number = 0
|
|
765
799
|
auxfile = filedialog.askopenfilename(
|
|
766
800
|
initialdir=os.getcwd(),
|
|
767
801
|
title="Select auxiliary FITS file",
|
|
@@ -782,9 +816,26 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
782
816
|
print(f"data shape...: {self.data.shape}")
|
|
783
817
|
print(f"auxdata shape: {auxdata_loaded.shape}")
|
|
784
818
|
raise ValueError("Auxiliary file has different shape.")
|
|
785
|
-
self.
|
|
786
|
-
|
|
787
|
-
|
|
819
|
+
if aux_number == len(self.auxfile_list):
|
|
820
|
+
self.auxfile_list.append(auxfile)
|
|
821
|
+
self.extension_auxfile_list.append(extension)
|
|
822
|
+
self.auxdata.append(auxdata_loaded)
|
|
823
|
+
else:
|
|
824
|
+
self.auxfile_list[aux_number] = auxfile
|
|
825
|
+
self.auxdata[aux_number] = auxdata_loaded
|
|
826
|
+
self.extension_auxfile_list[aux_number] = extension
|
|
827
|
+
# Compute mean, median, min if multiple auxdata are loaded
|
|
828
|
+
if len(self.auxdata) > 1:
|
|
829
|
+
data3d = np.zeros((len(self.auxdata),) + self.data.shape, dtype=self.data.dtype)
|
|
830
|
+
for i in range(len(self.auxdata)):
|
|
831
|
+
data3d[i, :, :] = self.auxdata[i]
|
|
832
|
+
self.auxdata_mean = np.mean(data3d, axis=0)
|
|
833
|
+
self.auxdata_median = np.median(data3d, axis=0)
|
|
834
|
+
self.auxdata_min = np.min(data3d, axis=0)
|
|
835
|
+
else:
|
|
836
|
+
self.auxdata_mean = self.auxdata[0].copy()
|
|
837
|
+
self.auxdata_median = self.auxdata[0].copy()
|
|
838
|
+
self.auxdata_min = self.auxdata[0].copy()
|
|
788
839
|
print(f"Loaded auxiliary data from {auxfile}")
|
|
789
840
|
self.toggle_auxdata_button.config(state=tk.NORMAL)
|
|
790
841
|
except Exception as e:
|
|
@@ -863,23 +914,61 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
863
914
|
self.use_cursor_button.config(text="[c]ursor: OFF")
|
|
864
915
|
|
|
865
916
|
def toggle_auxdata(self):
|
|
866
|
-
"""Toggle between main data and auxiliary data for display.
|
|
867
|
-
|
|
917
|
+
"""Toggle between main data and auxiliary data for display.
|
|
918
|
+
|
|
919
|
+
If multiple auxiliary data are loaded, cycle through them,
|
|
920
|
+
including mean, median, and min of all auxiliary data.
|
|
921
|
+
"""
|
|
922
|
+
naux = len(self.auxdata)
|
|
923
|
+
if naux == 0:
|
|
924
|
+
self.displaying_auxdata = 0
|
|
925
|
+
elif naux == 1:
|
|
926
|
+
if self.displaying_auxdata == 0:
|
|
927
|
+
self.displaying_auxdata = 1
|
|
928
|
+
else:
|
|
929
|
+
self.displaying_auxdata = 0
|
|
930
|
+
else:
|
|
931
|
+
self.displaying_auxdata += 1
|
|
932
|
+
if self.displaying_auxdata > naux + 3:
|
|
933
|
+
self.displaying_auxdata = 0
|
|
934
|
+
if self.displaying_auxdata == 0:
|
|
868
935
|
# Switch to main data
|
|
869
936
|
vmin = self.get_vmin()
|
|
870
937
|
vmax = self.get_vmax()
|
|
871
938
|
self.image.set_data(self.data)
|
|
872
939
|
self.image.set_clim(vmin=vmin, vmax=vmax)
|
|
873
|
-
self.displaying_auxdata =
|
|
874
|
-
self.ax.set_title(f"data: {os.path.basename(self.input_fits)}")
|
|
875
|
-
|
|
940
|
+
self.displaying_auxdata = 0
|
|
941
|
+
self.ax.set_title(f"data: {os.path.basename(self.input_fits)}[{self.extension}]")
|
|
942
|
+
elif self.displaying_auxdata > 0 and self.displaying_auxdata <= naux:
|
|
876
943
|
# Switch to auxiliary data
|
|
877
944
|
vmin = self.get_vmin()
|
|
878
945
|
vmax = self.get_vmax()
|
|
879
|
-
self.image.set_data(self.auxdata)
|
|
946
|
+
self.image.set_data(self.auxdata[self.displaying_auxdata - 1])
|
|
947
|
+
self.image.set_clim(vmin=vmin, vmax=vmax)
|
|
948
|
+
self.ax.set_title(
|
|
949
|
+
f"auxdata: {os.path.basename(self.auxfile_list[self.displaying_auxdata - 1])}[{self.extension_auxfile_list[self.displaying_auxdata - 1]}]"
|
|
950
|
+
)
|
|
951
|
+
elif self.displaying_auxdata == naux + 1:
|
|
952
|
+
# Switch to auxiliary mean data
|
|
953
|
+
vmin = self.get_vmin()
|
|
954
|
+
vmax = self.get_vmax()
|
|
955
|
+
self.image.set_data(self.auxdata_mean)
|
|
956
|
+
self.image.set_clim(vmin=vmin, vmax=vmax)
|
|
957
|
+
self.ax.set_title("auxdata: MEAN of all loaded auxiliary data")
|
|
958
|
+
elif self.displaying_auxdata == naux + 2:
|
|
959
|
+
# Switch to auxiliary median data
|
|
960
|
+
vmin = self.get_vmin()
|
|
961
|
+
vmax = self.get_vmax()
|
|
962
|
+
self.image.set_data(self.auxdata_median)
|
|
963
|
+
self.image.set_clim(vmin=vmin, vmax=vmax)
|
|
964
|
+
self.ax.set_title("auxdata: MEDIAN of all loaded auxiliary data")
|
|
965
|
+
elif self.displaying_auxdata == naux + 3:
|
|
966
|
+
# Switch to auxiliary min data
|
|
967
|
+
vmin = self.get_vmin()
|
|
968
|
+
vmax = self.get_vmax()
|
|
969
|
+
self.image.set_data(self.auxdata_min)
|
|
880
970
|
self.image.set_clim(vmin=vmin, vmax=vmax)
|
|
881
|
-
self.
|
|
882
|
-
self.ax.set_title(f"auxdata: {os.path.basename(self.auxfile)}")
|
|
971
|
+
self.ax.set_title("auxdata: MIN of all loaded auxiliary data")
|
|
883
972
|
self.canvas.draw_idle()
|
|
884
973
|
|
|
885
974
|
def toggle_aspect(self):
|
|
@@ -1337,7 +1426,8 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
1337
1426
|
last_maskfill_operator=self.last_maskfill_operator,
|
|
1338
1427
|
last_maskfill_smooth=self.last_maskfill_smooth,
|
|
1339
1428
|
last_maskfill_verbose=self.last_maskfill_verbose,
|
|
1340
|
-
|
|
1429
|
+
auxfile_list=self.auxfile_list,
|
|
1430
|
+
extension_auxfile_list=self.extension_auxfile_list,
|
|
1341
1431
|
cleandata_lacosmic=self.cleandata_lacosmic,
|
|
1342
1432
|
cleandata_pycosmic=self.cleandata_pycosmic,
|
|
1343
1433
|
cleandata_deepcr=self.cleandata_deepcr,
|
|
@@ -1422,11 +1512,33 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
1422
1512
|
self.mask_crfound[mask_crfound_region] = False
|
|
1423
1513
|
data_has_been_modified = True
|
|
1424
1514
|
elif cleaning_method == "auxdata":
|
|
1425
|
-
if self.auxdata
|
|
1515
|
+
if len(self.auxdata) == 0:
|
|
1426
1516
|
print("No auxiliary data available. Cleaning skipped!")
|
|
1427
1517
|
return
|
|
1428
|
-
|
|
1429
|
-
|
|
1518
|
+
inum = editor.auxiliary_data_index # 1-based index of auxiliary data
|
|
1519
|
+
if inum <= len(self.auxdata):
|
|
1520
|
+
print(
|
|
1521
|
+
f"Using auxiliary data: "
|
|
1522
|
+
f"{os.path.basename(self.auxfile_list[inum - 1])}"
|
|
1523
|
+
f"[{self.extension_auxfile_list[inum - 1]}]"
|
|
1524
|
+
)
|
|
1525
|
+
# Replace detected CR pixels with auxiliary data values
|
|
1526
|
+
self.data[mask_crfound_region] = self.auxdata[inum - 1][mask_crfound_region]
|
|
1527
|
+
elif inum == len(self.auxdata) + 1:
|
|
1528
|
+
print("Using auxiliary data: MEAN of all loaded auxiliary data")
|
|
1529
|
+
# Replace detected CR pixels with auxiliary mean data values
|
|
1530
|
+
self.data[mask_crfound_region] = self.auxdata_mean[mask_crfound_region]
|
|
1531
|
+
elif inum == len(self.auxdata) + 2:
|
|
1532
|
+
print("Using auxiliary data: MEDIAN of all loaded auxiliary data")
|
|
1533
|
+
# Replace detected CR pixels with auxiliary median data values
|
|
1534
|
+
self.data[mask_crfound_region] = self.auxdata_median[mask_crfound_region]
|
|
1535
|
+
elif inum == len(self.auxdata) + 3:
|
|
1536
|
+
print("Using auxiliary data: MIN of all loaded auxiliary data")
|
|
1537
|
+
# Replace detected CR pixels with auxiliary min data values
|
|
1538
|
+
self.data[mask_crfound_region] = self.auxdata_min[mask_crfound_region]
|
|
1539
|
+
else:
|
|
1540
|
+
print(f"Invalid auxiliary data index: {inum}. Cleaning skipped!")
|
|
1541
|
+
return
|
|
1430
1542
|
# update mask_fixed to include the newly fixed pixels
|
|
1431
1543
|
self.mask_fixed[mask_crfound_region] = True
|
|
1432
1544
|
# upate mask_crfound by eliminating the cleaned pixels
|
|
@@ -1533,8 +1645,14 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
1533
1645
|
root=review_window,
|
|
1534
1646
|
root_width=self.width,
|
|
1535
1647
|
root_height=self.height,
|
|
1648
|
+
input_fits=self.input_fits,
|
|
1536
1649
|
data=self.data,
|
|
1537
1650
|
auxdata=self.auxdata,
|
|
1651
|
+
auxfile_list=self.auxfile_list,
|
|
1652
|
+
extension_auxfile_list=self.extension_auxfile_list,
|
|
1653
|
+
auxdata_mean=self.auxdata_mean,
|
|
1654
|
+
auxdata_median=self.auxdata_median,
|
|
1655
|
+
auxdata_min=self.auxdata_min,
|
|
1538
1656
|
cleandata_lacosmic=self.cleandata_lacosmic,
|
|
1539
1657
|
cleandata_pycosmic=self.cleandata_pycosmic,
|
|
1540
1658
|
cleandata_deepcr=self.cleandata_deepcr,
|
|
@@ -1551,8 +1669,14 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
1551
1669
|
root=review_window,
|
|
1552
1670
|
root_width=self.width,
|
|
1553
1671
|
root_height=self.height,
|
|
1672
|
+
input_fits=self.input_fits,
|
|
1554
1673
|
data=self.data,
|
|
1555
1674
|
auxdata=self.auxdata,
|
|
1675
|
+
auxfile_list=self.auxfile_list,
|
|
1676
|
+
extension_auxfile_list=self.extension_auxfile_list,
|
|
1677
|
+
auxdata_mean=self.auxdata_mean,
|
|
1678
|
+
auxdata_median=self.auxdata_median,
|
|
1679
|
+
auxdata_min=self.auxdata_min,
|
|
1556
1680
|
cleandata_lacosmic=self.cleandata_lacosmic,
|
|
1557
1681
|
cleandata_pycosmic=self.cleandata_pycosmic,
|
|
1558
1682
|
cleandata_deepcr=self.cleandata_deepcr,
|
|
@@ -1616,7 +1740,7 @@ class CosmicRayCleanerApp(ImageDisplay):
|
|
|
1616
1740
|
self.set_cursor_onoff()
|
|
1617
1741
|
elif event.key == "a":
|
|
1618
1742
|
self.toggle_aspect()
|
|
1619
|
-
elif event.key == "t" and self.auxdata
|
|
1743
|
+
elif event.key == "t" and len(self.auxdata) > 0:
|
|
1620
1744
|
self.toggle_auxdata()
|
|
1621
1745
|
elif event.key == ",":
|
|
1622
1746
|
self.set_minmax()
|