teareduce 0.4.9__py3-none-any.whl → 0.5.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.
@@ -10,13 +10,20 @@
10
10
  """Define the ReviewCosmicRay class."""
11
11
 
12
12
  import tkinter as tk
13
+ from tkinter import messagebox
13
14
  from tkinter import simpledialog
14
15
 
15
16
  import matplotlib.pyplot as plt
16
17
  from matplotlib.backend_bases import key_press_handler
17
18
  from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
18
19
  import numpy as np
19
- from scipy import ndimage
20
+ from rich import print
21
+
22
+ from .definitions import MAX_PIXEL_DISTANCE_TO_CR
23
+ from .imagedisplay import ImageDisplay
24
+ from .interpolation_a import interpolation_a
25
+ from .interpolation_x import interpolation_x
26
+ from .interpolation_y import interpolation_y
20
27
 
21
28
  from ..imshow import imshow
22
29
  from ..sliceregion import SliceRegion2D
@@ -26,74 +33,182 @@ import matplotlib
26
33
  matplotlib.use("TkAgg")
27
34
 
28
35
 
29
- class ReviewCosmicRay():
36
+ class ReviewCosmicRay(ImageDisplay):
30
37
  """Class to review suspected cosmic ray pixels."""
31
38
 
32
- def __init__(self, root, data, mask_fixed, mask_crfound):
39
+ def __init__(self, root, data, auxdata, cleandata_lacosmic, cr_labels, num_features,
40
+ first_cr_index=1, single_cr=False,
41
+ last_dilation=None, last_npoints=None, last_degree=None):
33
42
  """Initialize the review window.
34
43
 
35
44
  Parameters
36
45
  ----------
37
- root : tk.Tk
38
- The main Tkinter window.
46
+ root : tk.Toplevel
47
+ The parent Tkinter root window.
39
48
  data : 2D numpy array
40
49
  The original image data.
50
+ auxdata : 2D numpy array or None
51
+ The auxiliary image data.
52
+ cleandata_lacosmic: 2D numpy array or None
53
+ The cleaned image data from L.A.Cosmic.
54
+ cr_labels : 2D numpy array
55
+ Labels of connected cosmic ray pixel groups.
56
+ num_features : int
57
+ Number of connected cosmic ray pixel groups.
58
+ first_cr_index : int, optional
59
+ The index of the first cosmic ray to review (default is 1).
60
+ single_cr : bool, optional
61
+ Whether to review a single cosmic ray (default is False).
62
+ If True, the review window will close after reviewing the
63
+ selected first cosmic ray.
64
+ last_dilation : int or None, optional
65
+ The last used dilation parameter employed after L.A.Cosmic
66
+ detection. If > 0, the replacement by the L.A.Cosmic cleaned
67
+ data will not be allowed.
68
+ last_npoints : int or None, optional
69
+ The last used number of points parameter for interpolation.
70
+ last_degree : int or None, optional
71
+ The last used degree parameter for interpolation.
72
+
73
+ Methods
74
+ -------
75
+ create_widgets()
76
+ Create the GUI widgets for the review window.
77
+ update_display(cleaned=False)
78
+ Update the display to show the current cosmic ray.
79
+ set_ndeg()
80
+ Set the Npoints and Degree parameters for interpolation.
81
+ interp_x()
82
+ Perform X-interpolation for the current cosmic ray.
83
+ interp_y()
84
+ Perform Y-interpolation for the current cosmic ray.
85
+ interp_a(method)
86
+ Perform interpolation using the specified method for the current cosmic ray.
87
+ use_lacosmic()
88
+ Replace cosmic ray pixels with L.A.Cosmic cleaned data.
89
+ use_auxdata()
90
+ Replace cosmic ray pixels with auxiliary data.
91
+ remove_crosses()
92
+ Remove all pixels of the current cosmic ray from the review.
93
+ restore_cr()
94
+ Restore all pixels of the current cosmic ray to their original values.
95
+ continue_cr()
96
+ Move to the next cosmic ray for review.
97
+ exit_review()
98
+ Close the review window.
99
+ on_key(event)
100
+ Handle key press events for shortcuts.
101
+ on_click(event)
102
+ Handle mouse click events to mark/unmark pixels as cosmic rays.
103
+
104
+ Attributes
105
+ ----------
106
+ root : tk.Toplevel
107
+ The parent Tkinter root window.
108
+ data : 2D numpy array
109
+ The original image data.
110
+ auxdata : 2D numpy array or None
111
+ The auxiliary image data.
112
+ cleandata_lacosmic: 2D numpy array or None
113
+ The cleaned image data from L.A.Cosmic.
114
+ cr_labels : 2D numpy array
115
+ Labels of connected cosmic ray pixel groups.
116
+ num_features : int
117
+ Number of connected cosmic ray pixel groups.
118
+ num_cr_cleaned : int
119
+ Number of cosmic rays cleaned during the review.
41
120
  mask_fixed : 2D numpy array
42
- Mask of previously corrected pixels.
43
- mask_crfound : 2D numpy array
44
- Mask of new pixels identified as cosmic rays.
121
+ Mask of pixels fixed during the review.
122
+ first_plot : bool
123
+ Flag to indicate if it's the first plot.
124
+ degree : int
125
+ Degree parameter for interpolation.
126
+ npoints : int
127
+ Number of points parameter for interpolation.
128
+ last_dilation : int or None
129
+ The last used dilation parameter employed after L.A.Cosmic
130
+ detection.
45
131
  """
46
132
  self.root = root
133
+ self.root.title("Review Cosmic Rays")
134
+ self.auxdata = auxdata
135
+ if self.auxdata is not None:
136
+ self.root.geometry("1200x700+100+100")
137
+ else:
138
+ self.root.geometry("800x700+100+100")
47
139
  self.data = data
140
+ self.cleandata_lacosmic = cleandata_lacosmic
48
141
  self.data_original = data.copy()
49
- self.mask_fixed = mask_fixed
50
- self.mask_crfound = mask_crfound
142
+ self.cr_labels = cr_labels
143
+ self.num_features = num_features
144
+ self.num_cr_cleaned = 0
145
+ self.mask_fixed = np.zeros(self.data.shape, dtype=bool) # Mask of pixels fixed during review
51
146
  self.first_plot = True
52
- self.degree = 1 # Degree of polynomial for interpolation
53
- self.npoints = 2 # Number of points at each side of the CR pixel for interpolation
54
- # Label connected components in the mask; note that by default,
55
- # structure is a cross [0,1,0;1,1,1;0,1,0], but we want to consider
56
- # diagonal connections too, so we define a 3x3 square.
57
- structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
58
- self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
147
+ self.degree = last_degree if last_degree is not None else 1
148
+ self.npoints = last_npoints if last_npoints is not None else 2
149
+ self.last_dilation = last_dilation
59
150
  # Make a copy of the original labels to allow pixel re-marking
60
151
  self.cr_labels_original = self.cr_labels.copy()
61
- print(f"Number of cosmic ray pixels detected: {np.sum(self.mask_crfound)}")
62
- print(f"Number of cosmic rays detected: {self.num_features}")
152
+ print(f"Number of cosmic ray pixels detected..: {np.sum(self.cr_labels > 0)}")
153
+ print(f"Number of cosmic rays (grouped pixels): {self.num_features}")
63
154
  if self.num_features == 0:
64
155
  print('No CR hits found!')
65
156
  else:
66
- self.cr_index = 1
157
+ self.cr_index = first_cr_index
158
+ self.single_cr = single_cr
67
159
  self.create_widgets()
68
160
 
69
161
  def create_widgets(self):
70
- self.review_window = tk.Toplevel(self.root)
71
- self.review_window.title("Review Cosmic Rays")
72
- self.review_window.geometry("800x700+100+100")
73
-
74
- self.button_frame1 = tk.Frame(self.review_window)
162
+ """Create the GUI widgets for the review window."""
163
+ # Row 1 of buttons
164
+ self.button_frame1 = tk.Frame(self.root)
75
165
  self.button_frame1.pack(pady=5)
76
- self.remove_crosses_button = tk.Button(self.button_frame1, text="remove all X's", command=self.remove_crosses)
166
+ self.ndeg_label = tk.Button(self.button_frame1, text=f"Npoints={self.npoints}, Degree={self.degree}",
167
+ command=self.set_ndeg)
168
+ self.ndeg_label.pack(side=tk.LEFT, padx=5)
169
+ self.remove_crosses_button = tk.Button(self.button_frame1, text="remove all x's", command=self.remove_crosses)
77
170
  self.remove_crosses_button.pack(side=tk.LEFT, padx=5)
78
- self.restore_cr_button = tk.Button(self.button_frame1, text="[r]estore CR", command=self.restore_cr)
171
+ self.restore_cr_button = tk.Button(self.button_frame1, text="[r]estore CR data", command=self.restore_cr)
79
172
  self.restore_cr_button.pack(side=tk.LEFT, padx=5)
80
173
  self.restore_cr_button.config(state=tk.DISABLED)
81
174
  self.next_button = tk.Button(self.button_frame1, text="[c]ontinue", command=self.continue_cr)
82
175
  self.next_button.pack(side=tk.LEFT, padx=5)
176
+ self.exit_button = tk.Button(self.button_frame1, text="[e]xit review", command=self.exit_review)
177
+ self.exit_button.pack(side=tk.LEFT, padx=5)
83
178
 
84
- self.button_frame2 = tk.Frame(self.review_window)
179
+ # Row 2 of buttons
180
+ self.button_frame2 = tk.Frame(self.root)
85
181
  self.button_frame2.pack(pady=5)
86
- self.ndeg_label = tk.Button(self.button_frame2, text=f"deg={self.degree}, n={self.npoints}",
87
- command=self.set_ndeg)
88
- self.ndeg_label.pack(side=tk.LEFT, padx=5)
89
182
  self.interp_x_button = tk.Button(self.button_frame2, text="[x] interp.", command=self.interp_x)
90
183
  self.interp_x_button.pack(side=tk.LEFT, padx=5)
91
184
  self.interp_y_button = tk.Button(self.button_frame2, text="[y] interp.", command=self.interp_y)
92
185
  self.interp_y_button.pack(side=tk.LEFT, padx=5)
93
- self.interp_s_button = tk.Button(self.button_frame2, text="[s] interp.", command=self.interp_s)
186
+ # it is important to use lambda here to pass the method argument correctly
187
+ # (avoiding the execution of the function at button creation time, which would happen
188
+ # if we didn't use lambda; in that case, the function would be called immediately and
189
+ # its return value (None) would be assigned to the command parameter; furthermore,
190
+ # the function is trying to deactivate the buttons before they are created, which
191
+ # would lead to an error; in addition, since I have two buttons calling the same function
192
+ # with different arguments, using lambda allows to differentiate them)
193
+ self.interp_s_button = tk.Button(self.button_frame2, text="[s]urface interp.",
194
+ command=lambda: self.interp_a('surface'))
94
195
  self.interp_s_button.pack(side=tk.LEFT, padx=5)
95
-
96
- self.button_frame3 = tk.Frame(self.review_window)
196
+ self.interp_m_button = tk.Button(self.button_frame2, text="[m]edian",
197
+ command=lambda: self.interp_a('median'))
198
+ self.interp_m_button.pack(side=tk.LEFT, padx=5)
199
+ self.interp_l_button = tk.Button(self.button_frame2, text="[l]acosmic", command=self.use_lacosmic)
200
+ self.interp_l_button.pack(side=tk.LEFT, padx=5)
201
+ if self.last_dilation is not None and self.last_dilation > 0:
202
+ self.interp_l_button.config(state=tk.DISABLED)
203
+ if self.cleandata_lacosmic is None:
204
+ self.interp_l_button.config(state=tk.DISABLED)
205
+ self.interp_aux_button = tk.Button(self.button_frame2, text="[a]ux. data", command=self.use_auxdata)
206
+ self.interp_aux_button.pack(side=tk.LEFT, padx=5)
207
+ if self.auxdata is None:
208
+ self.interp_aux_button.config(state=tk.DISABLED)
209
+
210
+ # Row 3 of buttons
211
+ self.button_frame3 = tk.Frame(self.root)
97
212
  self.button_frame3.pack(pady=5)
98
213
  vmin, vmax = zscale(self.data)
99
214
  self.vmin_button = tk.Button(self.button_frame3, text=f"vmin: {vmin:.2f}", command=self.set_vmin)
@@ -104,11 +219,15 @@ class ReviewCosmicRay():
104
219
  self.set_minmax_button.pack(side=tk.LEFT, padx=5)
105
220
  self.set_zscale_button = tk.Button(self.button_frame3, text="zscale [/]", command=self.set_zscale)
106
221
  self.set_zscale_button.pack(side=tk.LEFT, padx=5)
107
- self.exit_button = tk.Button(self.button_frame3, text="[e]xit review", command=self.exit_review)
108
- self.exit_button.pack(side=tk.LEFT, padx=5)
109
222
 
110
- self.fig, self.ax = plt.subplots(figsize=(8, 5))
111
- self.canvas = FigureCanvasTkAgg(self.fig, master=self.review_window)
223
+ # Figure
224
+ if self.auxdata is not None:
225
+ self.fig, (self.ax_aux, self.ax) = plt.subplots(
226
+ ncols=2, figsize=(10, 5), constrained_layout=True)
227
+ else:
228
+ self.fig, self.ax = plt.subplots(figsize=(8, 5), constrained_layout=True)
229
+ self.canvas = FigureCanvasTkAgg(self.fig, master=self.root)
230
+ self.canvas.get_tk_widget().pack(padx=5, pady=5)
112
231
  # The next two instructions prevent a segmentation fault when pressing "q"
113
232
  self.canvas.mpl_disconnect(self.canvas.mpl_connect("key_press_event", key_press_handler))
114
233
  self.canvas.mpl_connect("key_press_event", self.on_key)
@@ -117,286 +236,206 @@ class ReviewCosmicRay():
117
236
  self.canvas_widget.pack(fill=tk.BOTH, expand=True)
118
237
 
119
238
  # Matplotlib toolbar
120
- self.toolbar_frame = tk.Frame(self.review_window)
239
+ self.toolbar_frame = tk.Frame(self.root)
121
240
  self.toolbar_frame.pack(fill=tk.X, expand=False, pady=5)
122
241
  self.toolbar = NavigationToolbar2Tk(self.canvas, self.toolbar_frame)
123
242
  self.toolbar.update()
124
243
 
125
244
  self.update_display()
126
245
 
127
- self.root.wait_window(self.review_window)
246
+ def update_display(self, cleaned=False):
247
+ """Update the display to show the current cosmic ray.
128
248
 
129
- def update_display(self):
249
+ Parameters
250
+ ----------
251
+ cleaned : bool, optional
252
+ Whether the cosmic ray has been cleaned (default is False).
253
+ If True, the cosmic ray pixels will be marked differently.
254
+ """
130
255
  ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
131
256
  ycr_list_original, xcr_list_original = np.where(self.cr_labels_original == self.cr_index)
132
257
  if self.first_plot:
133
258
  print(f"Cosmic ray {self.cr_index}: "
134
259
  f"Number of pixels = {len(xcr_list)}, "
135
- f"Centroid = ({np.mean(xcr_list):.2f}, {np.mean(ycr_list):.2f})")
260
+ f"Centroid = ({np.mean(xcr_list)+1:.2f}, {np.mean(ycr_list)+1:.2f})")
136
261
  # Use original positions to define the region to display in order
137
262
  # to avoid image shifts when some pixels are unmarked or new ones are marked
138
263
  i0 = int(np.mean(ycr_list_original) + 0.5)
139
264
  j0 = int(np.mean(xcr_list_original) + 0.5)
140
- jmin = j0 - 15 if j0 - 15 >= 0 else 0
141
- jmax = j0 + 15 if j0 + 15 < self.data.shape[1] else self.data.shape[1] - 1
142
- imin = i0 - 15 if i0 - 15 >= 0 else 0
143
- imax = i0 + 15 if i0 + 15 < self.data.shape[0] else self.data.shape[0] - 1
265
+ semiwidth = MAX_PIXEL_DISTANCE_TO_CR
266
+ jmin = j0 - semiwidth if j0 - semiwidth >= 0 else 0
267
+ jmax = j0 + semiwidth if j0 + semiwidth < self.data.shape[1] else self.data.shape[1] - 1
268
+ imin = i0 - semiwidth if i0 - semiwidth >= 0 else 0
269
+ imax = i0 + semiwidth if i0 + semiwidth < self.data.shape[0] else self.data.shape[0] - 1
270
+ # Force the region to be of size (2*semiwidth + 1) x (2*semiwidth + 1)
271
+ if jmin == 0:
272
+ jmax = 2 * semiwidth
273
+ elif jmax == self.data.shape[1] - 1:
274
+ jmin = self.data.shape[1] - 1 - 2 * semiwidth
275
+ if imin == 0:
276
+ imax = 2 * semiwidth
277
+ elif imax == self.data.shape[0] - 1:
278
+ imin = self.data.shape[0] - 1 - 2 * semiwidth
144
279
  self.region = SliceRegion2D(f'[{jmin+1}:{jmax+1}, {imin+1}:{imax+1}]', mode='fits').python
145
280
  self.ax.clear()
146
281
  vmin = self.get_vmin()
147
282
  vmax = self.get_vmax()
148
283
  xlabel = 'X pixel (from 1 to NAXIS1)'
149
284
  ylabel = 'Y pixel (from 1 to NAXIS2)'
150
- self.image_review, _, _ = imshow(self.fig, self.ax, self.data[self.region], colorbar=False,
151
- xlabel=xlabel, ylabel=ylabel,
152
- vmin=vmin, vmax=vmax)
153
- self.image_review.set_extent([jmin + 0.5, jmax + 1.5, imin + 0.5, imax + 1.5])
285
+ self.image, _, _ = imshow(self.fig, self.ax, self.data[self.region], colorbar=False,
286
+ xlabel=xlabel, ylabel=ylabel,
287
+ vmin=vmin, vmax=vmax)
288
+ self.image.set_extent([jmin + 0.5, jmax + 1.5, imin + 0.5, imax + 1.5])
289
+ if self.auxdata is not None:
290
+ self.ax_aux.clear()
291
+ self.image_aux, _, _ = imshow(self.fig, self.ax_aux, self.auxdata[self.region],
292
+ colorbar=False,
293
+ xlabel=xlabel, ylabel=ylabel,
294
+ vmin=vmin, vmax=vmax)
295
+ self.image_aux.set_extent([jmin + 0.5, jmax + 1.5, imin + 0.5, imax + 1.5])
296
+ self.ax_aux.set_title("Auxiliary data")
297
+ # Overplot cosmic ray pixels
154
298
  xlim = self.ax.get_xlim()
155
299
  ylim = self.ax.get_ylim()
156
300
  for xcr, ycr in zip(xcr_list, ycr_list):
157
301
  xcr += 1 # from index to pixel
158
302
  ycr += 1 # from index to pixel
159
- self.ax.plot([xcr - 0.5, xcr + 0.5], [ycr + 0.5, ycr - 0.5], 'r-')
160
- self.ax.plot([xcr - 0.5, xcr + 0.5], [ycr - 0.5, ycr + 0.5], 'r-')
303
+ if cleaned:
304
+ self.ax.plot(xcr, ycr, 'C1o', markersize=4)
305
+ else:
306
+ self.ax.plot([xcr - 0.5, xcr + 0.5], [ycr + 0.5, ycr - 0.5], 'r-')
307
+ self.ax.plot([xcr - 0.5, xcr + 0.5], [ycr - 0.5, ycr + 0.5], 'r-')
161
308
  self.ax.set_xlim(xlim)
162
309
  self.ax.set_ylim(ylim)
163
310
  self.ax.set_title(f"Cosmic ray #{self.cr_index}/{self.num_features}")
164
311
  if self.first_plot:
165
312
  self.first_plot = False
166
- self.fig.tight_layout()
167
- self.canvas.draw()
168
-
169
- def set_vmin(self):
170
- old_vmin = self.get_vmin()
171
- new_vmin = simpledialog.askfloat("Set vmin", "Enter new vmin:", initialvalue=old_vmin)
172
- if new_vmin is None:
173
- return
174
- self.vmin_button.config(text=f"vmin: {new_vmin:.2f}")
175
- self.image_review.set_clim(vmin=new_vmin)
176
- self.canvas.draw()
177
-
178
- def set_vmax(self):
179
- old_vmax = self.get_vmax()
180
- new_vmax = simpledialog.askfloat("Set vmax", "Enter new vmax:", initialvalue=old_vmax)
181
- if new_vmax is None:
182
- return
183
- self.vmax_button.config(text=f"vmax: {new_vmax:.2f}")
184
- self.image_review.set_clim(vmax=new_vmax)
185
- self.canvas.draw()
186
-
187
- def get_vmin(self):
188
- return float(self.vmin_button.cget("text").split(":")[1])
189
-
190
- def get_vmax(self):
191
- return float(self.vmax_button.cget("text").split(":")[1])
192
-
193
- def set_minmax(self):
194
- vmin_new = np.min(self.data[self.region])
195
- vmax_new = np.max(self.data[self.region])
196
- self.vmin_button.config(text=f"vmin: {vmin_new:.2f}")
197
- self.vmax_button.config(text=f"vmax: {vmax_new:.2f}")
198
- self.image_review.set_clim(vmin=vmin_new)
199
- self.image_review.set_clim(vmax=vmax_new)
200
- self.canvas.draw()
201
-
202
- def set_zscale(self):
203
- vmin_new, vmax_new = zscale(self.data[self.region])
204
- self.vmin_button.config(text=f"vmin: {vmin_new:.2f}")
205
- self.vmax_button.config(text=f"vmax: {vmax_new:.2f}")
206
- self.image_review.set_clim(vmin=vmin_new)
207
- self.image_review.set_clim(vmax=vmax_new)
208
- self.canvas.draw()
313
+ self.canvas.draw_idle()
209
314
 
210
315
  def set_ndeg(self):
211
- new_degree = simpledialog.askinteger("Set degree", "Enter new degree (min=0):",
316
+ """Set the number of points and degree for interpolation."""
317
+ new_npoints = simpledialog.askinteger("Set Npoints", "Enter Npoints:",
318
+ initialvalue=self.npoints, minvalue=1)
319
+ if new_npoints is None:
320
+ return
321
+ new_degree = simpledialog.askinteger("Set degree", "Enter Degree (min=0):",
212
322
  initialvalue=self.degree, minvalue=0)
213
323
  if new_degree is None:
214
324
  return
215
- new_npoints = simpledialog.askinteger("Set n", f"Enter new n (min={2*new_degree}):",
216
- initialvalue=self.npoints, minvalue=2*new_degree)
217
- if new_npoints is None:
218
- return
219
325
  self.degree = new_degree
220
326
  self.npoints = new_npoints
221
- self.ndeg_label.config(text=f"deg={self.degree}, n={self.npoints}")
327
+ self.ndeg_label.config(text=f"Npoints={self.npoints}, Degree={self.degree}")
222
328
 
223
- def interp_x(self):
224
- print(f"X-interpolation of cosmic ray {self.cr_index}")
225
- ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
226
- ycr_min = np.min(ycr_list)
227
- ycr_max = np.max(ycr_list)
228
- xfit_all = []
229
- yfit_all = []
230
- for ycr in range(ycr_min, ycr_max + 1):
231
- xmarked = xcr_list[np.where(ycr_list == ycr)]
232
- if len(xmarked) > 0:
233
- jmin = np.min(xmarked)
234
- jmax = np.max(xmarked)
235
- # mark intermediate pixels too
236
- for ix in range(jmin, jmax + 1):
237
- self.cr_labels[ycr, ix] = self.cr_index
238
- xmarked = xcr_list[np.where(ycr_list == ycr)]
239
- xfit = []
240
- zfit = []
241
- for i in range(jmin - self.npoints, jmin):
242
- if 0 <= i < self.data.shape[1]:
243
- xfit.append(i)
244
- xfit_all.append(i)
245
- yfit_all.append(ycr)
246
- zfit.append(self.data[ycr, i])
247
- for i in range(jmax + 1, jmax + 1 + self.npoints):
248
- if 0 <= i < self.data.shape[1]:
249
- xfit.append(i)
250
- xfit_all.append(i)
251
- yfit_all.append(ycr)
252
- zfit.append(self.data[ycr, i])
253
- if len(xfit) > self.degree:
254
- p = np.polyfit(xfit, zfit, self.degree)
255
- for i in range(jmin, jmax + 1):
256
- if 0 <= i < self.data.shape[1]:
257
- self.data[ycr, i] = np.polyval(p, i)
258
- self.mask_fixed[ycr, i] = True
259
- else:
260
- print(f"Not enough points to fit at y={ycr+1}")
261
- self.update_display()
262
- return
329
+ def set_buttons_after_cleaning_cr(self):
330
+ """Set the state of buttons after cleaning a cosmic ray."""
263
331
  self.restore_cr_button.config(state=tk.NORMAL)
264
332
  self.remove_crosses_button.config(state=tk.DISABLED)
265
333
  self.interp_x_button.config(state=tk.DISABLED)
266
334
  self.interp_y_button.config(state=tk.DISABLED)
267
335
  self.interp_s_button.config(state=tk.DISABLED)
268
- self.update_display()
336
+ self.interp_m_button.config(state=tk.DISABLED)
337
+ self.interp_l_button.config(state=tk.DISABLED)
338
+ self.interp_aux_button.config(state=tk.DISABLED)
339
+
340
+ def interp_x(self):
341
+ """Perform x-direction interpolation to clean a cosmic ray."""
342
+ if 2 * self.npoints <= self.degree:
343
+ messagebox.showerror("Input Error", "2*Npoints must be greater than Degree for x interpolation.")
344
+ return
345
+ print(f"X-interpolation of cosmic ray {self.cr_index}")
346
+ interpolation_performed, xfit_all, yfit_all = interpolation_x(
347
+ data=self.data,
348
+ mask_fixed=self.mask_fixed,
349
+ cr_labels=self.cr_labels,
350
+ cr_index=self.cr_index,
351
+ npoints=self.npoints,
352
+ degree=self.degree
353
+ )
354
+ if interpolation_performed:
355
+ self.num_cr_cleaned += 1
356
+ self.set_buttons_after_cleaning_cr()
357
+ self.update_display(cleaned=interpolation_performed)
269
358
  if len(xfit_all) > 0:
270
359
  self.ax.plot(np.array(xfit_all) + 1, np.array(yfit_all) + 1, 'mo', markersize=4) # +1: from index to pixel
271
- self.canvas.draw()
360
+ self.canvas.draw_idle()
272
361
 
273
362
  def interp_y(self):
363
+ """Perform y-direction interpolation to clean a cosmic ray."""
364
+ if 2 * self.npoints <= self.degree:
365
+ messagebox.showerror("Input Error", "2*Npoints must be greater than Degree for y interpolation.")
366
+ return
274
367
  print(f"Y-interpolation of cosmic ray {self.cr_index}")
275
- ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
276
- xcr_min = np.min(xcr_list)
277
- xcr_max = np.max(xcr_list)
278
- xfit_all = []
279
- yfit_all = []
280
- for xcr in range(xcr_min, xcr_max + 1):
281
- ymarked = ycr_list[np.where(xcr_list == xcr)]
282
- if len(ymarked) > 0:
283
- imin = np.min(ymarked)
284
- imax = np.max(ymarked)
285
- # mark intermediate pixels too
286
- for iy in range(imin, imax + 1):
287
- self.cr_labels[iy, xcr] = self.cr_index
288
- ymarked = ycr_list[np.where(xcr_list == xcr)]
289
- yfit = []
290
- zfit = []
291
- for i in range(imin - self.npoints, imin):
292
- if 0 <= i < self.data.shape[0]:
293
- yfit.append(i)
294
- yfit_all.append(i)
295
- xfit_all.append(xcr)
296
- zfit.append(self.data[i, xcr])
297
- for i in range(imax + 1, imax + 1 + self.npoints):
298
- if 0 <= i < self.data.shape[0]:
299
- yfit.append(i)
300
- yfit_all.append(i)
301
- xfit_all.append(xcr)
302
- zfit.append(self.data[i, xcr])
303
- if len(yfit) > self.degree:
304
- p = np.polyfit(yfit, zfit, self.degree)
305
- for i in range(imin, imax + 1):
306
- if 0 <= i < self.data.shape[1]:
307
- self.data[i, xcr] = np.polyval(p, i)
308
- self.mask_fixed[i, xcr] = True
309
- else:
310
- print(f"Not enough points to fit at x={xcr+1}")
311
- self.update_display()
312
- return
313
- self.restore_cr_button.config(state=tk.NORMAL)
314
- self.remove_crosses_button.config(state=tk.DISABLED)
315
- self.interp_x_button.config(state=tk.DISABLED)
316
- self.interp_y_button.config(state=tk.DISABLED)
317
- self.interp_s_button.config(state=tk.DISABLED)
318
- self.update_display()
368
+ interpolation_performed, xfit_all, yfit_all = interpolation_y(
369
+ data=self.data,
370
+ mask_fixed=self.mask_fixed,
371
+ cr_labels=self.cr_labels,
372
+ cr_index=self.cr_index,
373
+ npoints=self.npoints,
374
+ degree=self.degree
375
+ )
376
+ if interpolation_performed:
377
+ self.num_cr_cleaned += 1
378
+ self.set_buttons_after_cleaning_cr()
379
+ self.update_display(cleaned=interpolation_performed)
319
380
  if len(xfit_all) > 0:
320
381
  self.ax.plot(np.array(xfit_all) + 1, np.array(yfit_all) + 1, 'mo', markersize=4) # +1: from index to pixel
321
- self.canvas.draw()
382
+ self.canvas.draw_idle()
322
383
 
323
- def interp_s(self):
324
- print(f"S-interpolation of cosmic ray {self.cr_index}")
325
- ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
326
- ycr_min = np.min(ycr_list)
327
- ycr_max = np.max(ycr_list)
328
- xfit_all = []
329
- yfit_all = []
330
- zfit_all = []
331
- # First do horizontal lines
332
- for ycr in range(ycr_min, ycr_max + 1):
333
- xmarked = xcr_list[np.where(ycr_list == ycr)]
334
- if len(xmarked) > 0:
335
- jmin = np.min(xmarked)
336
- jmax = np.max(xmarked)
337
- # mark intermediate pixels too
338
- for ix in range(jmin, jmax + 1):
339
- self.cr_labels[ycr, ix] = self.cr_index
340
- xmarked = xcr_list[np.where(ycr_list == ycr)]
341
- for i in range(jmin - self.npoints, jmin):
342
- if 0 <= i < self.data.shape[1]:
343
- xfit_all.append(i)
344
- yfit_all.append(ycr)
345
- zfit_all.append(self.data[ycr, i])
346
- for i in range(jmax + 1, jmax + 1 + self.npoints):
347
- if 0 <= i < self.data.shape[1]:
348
- xfit_all.append(i)
349
- yfit_all.append(ycr)
350
- zfit_all.append(self.data[ycr, i])
351
- xcr_min = np.min(xcr_list)
352
- # Now do vertical lines
353
- xcr_max = np.max(xcr_list)
354
- for xcr in range(xcr_min, xcr_max + 1):
355
- ymarked = ycr_list[np.where(xcr_list == xcr)]
356
- if len(ymarked) > 0:
357
- imin = np.min(ymarked)
358
- imax = np.max(ymarked)
359
- # mark intermediate pixels too
360
- for iy in range(imin, imax + 1):
361
- self.cr_labels[iy, xcr] = self.cr_index
362
- ymarked = ycr_list[np.where(xcr_list == xcr)]
363
- for i in range(imin - self.npoints, imin):
364
- if 0 <= i < self.data.shape[0]:
365
- yfit_all.append(i)
366
- xfit_all.append(xcr)
367
- zfit_all.append(self.data[i, xcr])
368
- for i in range(imax + 1, imax + 1 + self.npoints):
369
- if 0 <= i < self.data.shape[0]:
370
- yfit_all.append(i)
371
- xfit_all.append(xcr)
372
- zfit_all.append(self.data[i, xcr])
373
- if len(xfit_all) > 3:
374
- # Construct the design matrix for a 2D polynomial fit to a plane,
375
- # where each row corresponds to a point (x, y, z) and the model
376
- # is z = C[0]*x + C[1]*y + C[2]
377
- A = np.c_[xfit_all, yfit_all, np.ones(len(xfit_all))]
378
- # Least squares polynomial fit
379
- C, _, _, _ = np.linalg.lstsq(A, zfit_all, rcond=None)
380
- # recompute all CR pixels to take into account "holes" between marked pixels
381
- ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
382
- for iy, ix in zip(ycr_list, xcr_list):
383
- self.data[iy, ix] = C[0] * ix + C[1] * iy + C[2]
384
- self.mask_fixed[iy, ix] = True
385
- else:
386
- print("Not enough points to fit a plane")
387
- self.update_display()
388
- return
389
- self.restore_cr_button.config(state=tk.NORMAL)
390
- self.remove_crosses_button.config(state=tk.DISABLED)
391
- self.interp_x_button.config(state=tk.DISABLED)
392
- self.interp_y_button.config(state=tk.DISABLED)
393
- self.interp_s_button.config(state=tk.DISABLED)
394
- self.update_display()
384
+ def interp_a(self, method):
385
+ """Perform interpolation using the specified method to clean a cosmic ray.
386
+
387
+ Parameters
388
+ ----------
389
+ method : str
390
+ The interpolation method to use ('surface' or 'median').
391
+ """
392
+ print(f"{method} interpolation of cosmic ray {self.cr_index}")
393
+ interpolation_performed, xfit_all, yfit_all = interpolation_a(
394
+ data=self.data,
395
+ mask_fixed=self.mask_fixed,
396
+ cr_labels=self.cr_labels,
397
+ cr_index=self.cr_index,
398
+ npoints=self.npoints,
399
+ method=method
400
+ )
401
+ if interpolation_performed:
402
+ self.num_cr_cleaned += 1
403
+ self.set_buttons_after_cleaning_cr()
404
+ self.update_display(cleaned=interpolation_performed)
395
405
  if len(xfit_all) > 0:
396
406
  self.ax.plot(np.array(xfit_all) + 1, np.array(yfit_all) + 1, 'mo', markersize=4) # +1: from index to pixel
397
- self.canvas.draw()
407
+ self.canvas.draw_idle()
408
+
409
+ def use_lacosmic(self):
410
+ """Use L.A.Cosmic cleaned data to clean a cosmic ray."""
411
+ if self.cleandata_lacosmic is None:
412
+ print("L.A.Cosmic cleaned data not available.")
413
+ return
414
+ print(f"L.A.Cosmic interpolation of cosmic ray {self.cr_index}")
415
+ ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
416
+ for iy, ix in zip(ycr_list, xcr_list):
417
+ self.data[iy, ix] = self.cleandata_lacosmic[iy, ix]
418
+ self.mask_fixed[iy, ix] = True
419
+ self.num_cr_cleaned += 1
420
+ self.set_buttons_after_cleaning_cr()
421
+ self.update_display(cleaned=True)
422
+
423
+ def use_auxdata(self):
424
+ """Use auxiliary data to clean a cosmic ray."""
425
+ if self.auxdata is None:
426
+ print("Auxiliary data not available.")
427
+ return
428
+ print(f"Auxiliary data interpolation of cosmic ray {self.cr_index}")
429
+ ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
430
+ for iy, ix in zip(ycr_list, xcr_list):
431
+ self.data[iy, ix] = self.auxdata[iy, ix]
432
+ self.mask_fixed[iy, ix] = True
433
+ self.num_cr_cleaned += 1
434
+ self.set_buttons_after_cleaning_cr()
435
+ self.update_display(cleaned=True)
398
436
 
399
437
  def remove_crosses(self):
438
+ """Remove all pixels of the current cosmic ray from the review."""
400
439
  ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
401
440
  for iy, ix in zip(ycr_list, xcr_list):
402
441
  self.cr_labels[iy, ix] = 0
@@ -405,35 +444,61 @@ class ReviewCosmicRay():
405
444
  self.interp_x_button.config(state=tk.DISABLED)
406
445
  self.interp_y_button.config(state=tk.DISABLED)
407
446
  self.interp_s_button.config(state=tk.DISABLED)
447
+ self.interp_m_button.config(state=tk.DISABLED)
448
+ self.interp_l_button.config(state=tk.DISABLED)
449
+ self.interp_aux_button.config(state=tk.DISABLED)
408
450
  self.update_display()
409
451
 
410
452
  def restore_cr(self):
453
+ """Restore all pixels of the current cosmic ray to their original values."""
411
454
  ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
412
455
  for iy, ix in zip(ycr_list, xcr_list):
413
456
  self.data[iy, ix] = self.data_original[iy, ix]
457
+ self.mask_fixed[iy, ix] = False
414
458
  self.interp_x_button.config(state=tk.NORMAL)
415
459
  self.interp_y_button.config(state=tk.NORMAL)
416
460
  self.interp_s_button.config(state=tk.NORMAL)
461
+ self.interp_m_button.config(state=tk.NORMAL)
462
+ if self.cleandata_lacosmic is not None:
463
+ if self.last_dilation is None or self.last_dilation == 0:
464
+ self.interp_l_button.config(state=tk.NORMAL)
465
+ if self.auxdata is not None:
466
+ self.interp_aux_button.config(state=tk.NORMAL)
417
467
  print(f"Restored all pixels of cosmic ray {self.cr_index}")
468
+ self.num_cr_cleaned -= 1
418
469
  self.remove_crosses_button.config(state=tk.NORMAL)
419
470
  self.restore_cr_button.config(state=tk.DISABLED)
420
471
  self.update_display()
421
472
 
422
473
  def continue_cr(self):
474
+ """Move to the next cosmic ray for review."""
475
+ if self.single_cr:
476
+ self.exit_review()
477
+ return # important: do not remove (to avoid errors)
423
478
  self.cr_index += 1
424
479
  if self.cr_index > self.num_features:
425
- self.cr_index = 1
480
+ self.exit_review()
481
+ return # important: do not remove (to avoid errors)
426
482
  self.first_plot = True
427
483
  self.restore_cr_button.config(state=tk.DISABLED)
428
484
  self.interp_x_button.config(state=tk.NORMAL)
429
485
  self.interp_y_button.config(state=tk.NORMAL)
430
486
  self.interp_s_button.config(state=tk.NORMAL)
487
+ self.interp_m_button.config(state=tk.NORMAL)
488
+ if self.cleandata_lacosmic is not None:
489
+ if self.last_dilation is None or self.last_dilation == 0:
490
+ self.interp_l_button.config(state=tk.NORMAL)
491
+ if self.auxdata is not None:
492
+ self.interp_aux_button.config(state=tk.NORMAL)
493
+ self.remove_crosses_button.config(state=tk.NORMAL)
431
494
  self.update_display()
432
495
 
433
496
  def exit_review(self):
434
- self.review_window.destroy()
497
+ """Close the review window."""
498
+ self.root.destroy()
435
499
 
436
500
  def on_key(self, event):
501
+ """Handle key press events."""
437
502
  if event.key == 'q':
438
503
  pass # Ignore the "q" key to prevent closing the window
439
504
  elif event.key == 'r':
@@ -447,7 +512,16 @@ class ReviewCosmicRay():
447
512
  self.interp_y()
448
513
  elif event.key == 's':
449
514
  if self.interp_s_button.cget("state") != "disabled":
450
- self.interp_s()
515
+ self.interp_a('surface')
516
+ elif event.key == 'm':
517
+ if self.interp_m_button.cget("state") != "disabled":
518
+ self.interp_a('median')
519
+ elif event.key == 'l':
520
+ if self.interp_l_button.cget("state") != "disabled":
521
+ self.use_lacosmic()
522
+ elif event.key == 'a':
523
+ if self.interp_aux_button.cget("state") != "disabled":
524
+ self.use_auxdata()
451
525
  elif event.key == 'right' or event.key == 'c':
452
526
  self.continue_cr()
453
527
  elif event.key == ',':
@@ -456,13 +530,14 @@ class ReviewCosmicRay():
456
530
  self.set_zscale()
457
531
  elif event.key == 'e':
458
532
  self.exit_review()
533
+ return # important: do not remove (to avoid errors)
459
534
  else:
460
535
  print(f"Key pressed: {event.key}")
461
536
 
462
537
  def on_click(self, event):
463
- if event.inaxes:
538
+ """Handle mouse click events on the image."""
539
+ if event.inaxes == self.ax:
464
540
  x, y = event.xdata, event.ydata
465
- print(f"Clicked at image coordinates: ({x:.2f}, {y:.2f})")
466
541
  ix = int(x+0.5) - 1 # from pixel to index
467
542
  iy = int(y+0.5) - 1 # from pixel to index
468
543
  if int(self.cr_labels[iy, ix]) == self.cr_index:
@@ -470,17 +545,26 @@ class ReviewCosmicRay():
470
545
  print(f"Pixel ({ix+1}, {iy+1}) unmarked as cosmic ray.")
471
546
  else:
472
547
  self.cr_labels[iy, ix] = self.cr_index
473
- print(f"Pixel ({ix+1}, {iy+1}) marked as cosmic ray.")
548
+ print(f"Pixel ({ix+1}, {iy+1}), with signal {self.data[iy, ix]}, marked as cosmic ray.")
474
549
  xcr_list, ycr_list = np.where(self.cr_labels == self.cr_index)
475
550
  if len(xcr_list) == 0:
476
551
  self.interp_x_button.config(state=tk.DISABLED)
477
552
  self.interp_y_button.config(state=tk.DISABLED)
478
553
  self.interp_s_button.config(state=tk.DISABLED)
554
+ self.interp_m_button.config(state=tk.DISABLED)
555
+ self.interp_l_button.config(state=tk.DISABLED)
556
+ self.interp_aux_button.config(state=tk.DISABLED)
479
557
  self.remove_crosses_button.config(state=tk.DISABLED)
480
558
  else:
481
559
  self.interp_x_button.config(state=tk.NORMAL)
482
560
  self.interp_y_button.config(state=tk.NORMAL)
483
561
  self.interp_s_button.config(state=tk.NORMAL)
562
+ self.interp_m_button.config(state=tk.NORMAL)
563
+ if self.cleandata_lacosmic is not None:
564
+ if self.last_dilation is None or self.last_dilation == 0:
565
+ self.interp_l_button.config(state=tk.NORMAL)
566
+ if self.auxdata is not None:
567
+ self.interp_aux_button.config(state=tk.NORMAL)
484
568
  self.remove_crosses_button.config(state=tk.NORMAL)
485
569
  # Update the display to reflect the change
486
570
  self.update_display()