teareduce 0.4.5__tar.gz → 0.4.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {teareduce-0.4.5/src/teareduce.egg-info → teareduce-0.4.6}/PKG-INFO +1 -1
- {teareduce-0.4.5 → teareduce-0.4.6}/pyproject.toml +3 -0
- teareduce-0.4.6/src/teareduce/cleanest.py +694 -0
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/ctext.py +3 -4
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/draw_rectangle.py +5 -5
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/imshow.py +23 -12
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/peaks_spectrum.py +4 -4
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/sliceregion.py +3 -2
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/statsummary.py +5 -5
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/version.py +1 -1
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/wavecal.py +28 -31
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/zscale.py +4 -4
- {teareduce-0.4.5 → teareduce-0.4.6/src/teareduce.egg-info}/PKG-INFO +1 -1
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce.egg-info/SOURCES.txt +2 -0
- teareduce-0.4.6/src/teareduce.egg-info/entry_points.txt +2 -0
- {teareduce-0.4.5 → teareduce-0.4.6}/LICENSE.txt +0 -0
- {teareduce-0.4.5 → teareduce-0.4.6}/README.md +0 -0
- {teareduce-0.4.5 → teareduce-0.4.6}/setup.cfg +0 -0
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/__init__.py +0 -0
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/avoid_astropy_warnings.py +0 -0
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/correct_pincushion_distortion.py +0 -0
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/cosmicrays.py +0 -0
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/elapsed_time.py +0 -0
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/histogram1d.py +0 -0
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/numsplines.py +0 -0
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/polfit.py +0 -0
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/robust_std.py +0 -0
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/sdistortion.py +0 -0
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/simulateccdexposure.py +0 -0
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/tests/__init__.py +0 -0
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/tests/test_sliceregion.py +0 -0
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/tests/test_version.py +0 -0
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce/write_array_to_fits.py +0 -0
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce.egg-info/dependency_links.txt +0 -0
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce.egg-info/requires.txt +0 -0
- {teareduce-0.4.5 → teareduce-0.4.6}/src/teareduce.egg-info/top_level.txt +0 -0
|
@@ -46,6 +46,9 @@ test = [
|
|
|
46
46
|
Homepage = "https://github.com/nicocardiel/teareduce"
|
|
47
47
|
Repository = "https://github.com/nicocardiel/teareduce.git"
|
|
48
48
|
|
|
49
|
+
[project.scripts]
|
|
50
|
+
tea-cleanest = "teareduce.cleanest:main"
|
|
51
|
+
|
|
49
52
|
[tool.setuptools.dynamic]
|
|
50
53
|
version = {attr = "teareduce.version.VERSION"}
|
|
51
54
|
|
|
@@ -0,0 +1,694 @@
|
|
|
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
|
+
"""Interactive Cosmic Ray cleaning tool."""
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import tkinter as tk
|
|
14
|
+
from tkinter import filedialog
|
|
15
|
+
from tkinter import simpledialog
|
|
16
|
+
|
|
17
|
+
from astropy.io import fits
|
|
18
|
+
from ccdproc import cosmicray_lacosmic
|
|
19
|
+
import matplotlib.pyplot as plt
|
|
20
|
+
from matplotlib.backend_bases import key_press_handler
|
|
21
|
+
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
|
|
22
|
+
import numpy as np
|
|
23
|
+
import os
|
|
24
|
+
from scipy import ndimage
|
|
25
|
+
|
|
26
|
+
from .imshow import imshow
|
|
27
|
+
from .sliceregion import SliceRegion2D
|
|
28
|
+
from .zscale import zscale
|
|
29
|
+
|
|
30
|
+
import matplotlib
|
|
31
|
+
matplotlib.use("TkAgg")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ReviewCosmicRay():
|
|
35
|
+
"""Class to review suspected cosmic ray pixels."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, root, data, mask_fixed, mask_crfound):
|
|
38
|
+
"""Initialize the review window.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
root : tk.Tk
|
|
43
|
+
The main Tkinter window.
|
|
44
|
+
data : 2D numpy array
|
|
45
|
+
The original image data.
|
|
46
|
+
mask_fixed : 2D numpy array
|
|
47
|
+
Mask of previously corrected pixels.
|
|
48
|
+
mask_crfound : 2D numpy array
|
|
49
|
+
Mask of new pixels identified as cosmic rays.
|
|
50
|
+
"""
|
|
51
|
+
self.root = root
|
|
52
|
+
self.data = data
|
|
53
|
+
self.data_original = data.copy()
|
|
54
|
+
self.mask_fixed = mask_fixed
|
|
55
|
+
self.mask_crfound = mask_crfound
|
|
56
|
+
self.first_plot = True
|
|
57
|
+
self.degree = 1 # Degree of polynomial for interpolation
|
|
58
|
+
self.npoints = 2 # Number of points at each side of the CR pixel for interpolation
|
|
59
|
+
# Label connected components in the mask; note that by default,
|
|
60
|
+
# structure is a cross [0,1,0;1,1,1;0,1,0], but we want to consider
|
|
61
|
+
# diagonal connections too, so we define a 3x3 square.
|
|
62
|
+
structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
|
|
63
|
+
self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
|
|
64
|
+
# Make a copy of the original labels to allow pixel re-marking
|
|
65
|
+
self.cr_labels_original = self.cr_labels.copy()
|
|
66
|
+
print(f"Number of cosmic ray pixels detected: {np.sum(self.mask_crfound)}")
|
|
67
|
+
print(f"Number of cosmic rays detected: {self.num_features}")
|
|
68
|
+
if self.num_features == 0:
|
|
69
|
+
print('No CR hits found!')
|
|
70
|
+
else:
|
|
71
|
+
self.cr_index = 1
|
|
72
|
+
self.create_widgets()
|
|
73
|
+
|
|
74
|
+
def create_widgets(self):
|
|
75
|
+
self.review_window = tk.Toplevel(self.root)
|
|
76
|
+
self.review_window.title("Review Cosmic Rays")
|
|
77
|
+
self.review_window.geometry("800x700+100+100")
|
|
78
|
+
|
|
79
|
+
self.button_frame1 = tk.Frame(self.review_window)
|
|
80
|
+
self.button_frame1.pack(pady=5)
|
|
81
|
+
self.remove_crosses_button = tk.Button(self.button_frame1, text="remove all X's", command=self.remove_crosses)
|
|
82
|
+
self.remove_crosses_button.pack(side=tk.LEFT, padx=5)
|
|
83
|
+
self.restore_cr_button = tk.Button(self.button_frame1, text="[r]estore CR", command=self.restore_cr)
|
|
84
|
+
self.restore_cr_button.pack(side=tk.LEFT, padx=5)
|
|
85
|
+
self.restore_cr_button.config(state=tk.DISABLED)
|
|
86
|
+
self.next_button = tk.Button(self.button_frame1, text="[c]ontinue", command=self.continue_cr)
|
|
87
|
+
self.next_button.pack(side=tk.LEFT, padx=5)
|
|
88
|
+
|
|
89
|
+
self.button_frame2 = tk.Frame(self.review_window)
|
|
90
|
+
self.button_frame2.pack(pady=5)
|
|
91
|
+
self.ndeg_label = tk.Button(self.button_frame2, text=f"deg={self.degree}, n={self.npoints}",
|
|
92
|
+
command=self.set_ndeg)
|
|
93
|
+
self.ndeg_label.pack(side=tk.LEFT, padx=5)
|
|
94
|
+
self.interp_x_button = tk.Button(self.button_frame2, text="[x] interp.", command=self.interp_x)
|
|
95
|
+
self.interp_x_button.pack(side=tk.LEFT, padx=5)
|
|
96
|
+
self.interp_y_button = tk.Button(self.button_frame2, text="[y] interp.", command=self.interp_y)
|
|
97
|
+
self.interp_y_button.pack(side=tk.LEFT, padx=5)
|
|
98
|
+
self.interp_s_button = tk.Button(self.button_frame2, text="[s] interp.", command=self.interp_s)
|
|
99
|
+
self.interp_s_button.pack(side=tk.LEFT, padx=5)
|
|
100
|
+
|
|
101
|
+
self.button_frame3 = tk.Frame(self.review_window)
|
|
102
|
+
self.button_frame3.pack(pady=5)
|
|
103
|
+
vmin, vmax = zscale(self.data)
|
|
104
|
+
self.vmin_button = tk.Button(self.button_frame3, text=f"vmin: {vmin:.2f}", command=self.set_vmin)
|
|
105
|
+
self.vmin_button.pack(side=tk.LEFT, padx=5)
|
|
106
|
+
self.vmax_button = tk.Button(self.button_frame3, text=f"vmax: {vmax:.2f}", command=self.set_vmax)
|
|
107
|
+
self.vmax_button.pack(side=tk.LEFT, padx=5)
|
|
108
|
+
self.set_minmax_button = tk.Button(self.button_frame3, text="minmax [,]", command=self.set_minmax)
|
|
109
|
+
self.set_minmax_button.pack(side=tk.LEFT, padx=5)
|
|
110
|
+
self.set_zscale_button = tk.Button(self.button_frame3, text="zscale [/]", command=self.set_zscale)
|
|
111
|
+
self.set_zscale_button.pack(side=tk.LEFT, padx=5)
|
|
112
|
+
self.exit_button = tk.Button(self.button_frame3, text="[e]xit review", command=self.exit_review)
|
|
113
|
+
self.exit_button.pack(side=tk.LEFT, padx=5)
|
|
114
|
+
|
|
115
|
+
self.fig, self.ax = plt.subplots(figsize=(8, 5))
|
|
116
|
+
self.canvas = FigureCanvasTkAgg(self.fig, master=self.review_window)
|
|
117
|
+
# The next two instructions prevent a segmentation fault when pressing "q"
|
|
118
|
+
self.canvas.mpl_disconnect(self.canvas.mpl_connect("key_press_event", key_press_handler))
|
|
119
|
+
self.canvas.mpl_connect("key_press_event", self.on_key)
|
|
120
|
+
self.canvas.mpl_connect("button_press_event", self.on_click)
|
|
121
|
+
self.canvas_widget = self.canvas.get_tk_widget()
|
|
122
|
+
self.canvas_widget.pack(fill=tk.BOTH, expand=True)
|
|
123
|
+
|
|
124
|
+
# Matplotlib toolbar
|
|
125
|
+
self.toolbar_frame = tk.Frame(self.review_window)
|
|
126
|
+
self.toolbar_frame.pack(fill=tk.X, expand=False, pady=5)
|
|
127
|
+
self.toolbar = NavigationToolbar2Tk(self.canvas, self.toolbar_frame)
|
|
128
|
+
self.toolbar.update()
|
|
129
|
+
|
|
130
|
+
self.update_display()
|
|
131
|
+
|
|
132
|
+
self.root.wait_window(self.review_window)
|
|
133
|
+
|
|
134
|
+
def update_display(self):
|
|
135
|
+
ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
|
|
136
|
+
ycr_list_original, xcr_list_original = np.where(self.cr_labels_original == self.cr_index)
|
|
137
|
+
if self.first_plot:
|
|
138
|
+
print(f"Cosmic ray {self.cr_index}: "
|
|
139
|
+
f"Number of pixels = {len(xcr_list)}, "
|
|
140
|
+
f"Centroid = ({np.mean(xcr_list):.2f}, {np.mean(ycr_list):.2f})")
|
|
141
|
+
# Use original positions to define the region to display in order
|
|
142
|
+
# to avoid image shifts when some pixels are unmarked or new ones are marked
|
|
143
|
+
i0 = int(np.mean(ycr_list_original) + 0.5)
|
|
144
|
+
j0 = int(np.mean(xcr_list_original) + 0.5)
|
|
145
|
+
jmin = j0 - 15 if j0 - 15 >= 0 else 0
|
|
146
|
+
jmax = j0 + 15 if j0 + 15 < self.data.shape[1] else self.data.shape[1] - 1
|
|
147
|
+
imin = i0 - 15 if i0 - 15 >= 0 else 0
|
|
148
|
+
imax = i0 + 15 if i0 + 15 < self.data.shape[0] else self.data.shape[0] - 1
|
|
149
|
+
self.region = SliceRegion2D(f'[{jmin+1}:{jmax+1}, {imin+1}:{imax+1}]', mode='fits').python
|
|
150
|
+
self.ax.clear()
|
|
151
|
+
vmin = self.get_vmin()
|
|
152
|
+
vmax = self.get_vmax()
|
|
153
|
+
xlabel = 'X pixel (from 1 to NAXIS1)'
|
|
154
|
+
ylabel = 'Y pixel (from 1 to NAXIS2)'
|
|
155
|
+
self.image_review, _, _ = imshow(self.fig, self.ax, self.data[self.region], colorbar=False,
|
|
156
|
+
xlabel=xlabel, ylabel=ylabel,
|
|
157
|
+
vmin=vmin, vmax=vmax)
|
|
158
|
+
self.image_review.set_extent([jmin + 0.5, jmax + 1.5, imin + 0.5, imax + 1.5])
|
|
159
|
+
xlim = self.ax.get_xlim()
|
|
160
|
+
ylim = self.ax.get_ylim()
|
|
161
|
+
for xcr, ycr in zip(xcr_list, ycr_list):
|
|
162
|
+
xcr += 1 # from index to pixel
|
|
163
|
+
ycr += 1 # from index to pixel
|
|
164
|
+
self.ax.plot([xcr - 0.5, xcr + 0.5], [ycr + 0.5, ycr - 0.5], 'r-')
|
|
165
|
+
self.ax.plot([xcr - 0.5, xcr + 0.5], [ycr - 0.5, ycr + 0.5], 'r-')
|
|
166
|
+
self.ax.set_xlim(xlim)
|
|
167
|
+
self.ax.set_ylim(ylim)
|
|
168
|
+
self.ax.set_title(f"Cosmic ray #{self.cr_index}/{self.num_features}")
|
|
169
|
+
if self.first_plot:
|
|
170
|
+
self.first_plot = False
|
|
171
|
+
self.fig.tight_layout()
|
|
172
|
+
self.canvas.draw()
|
|
173
|
+
|
|
174
|
+
def set_vmin(self):
|
|
175
|
+
old_vmin = self.get_vmin()
|
|
176
|
+
new_vmin = simpledialog.askfloat("Set vmin", "Enter new vmin:", initialvalue=old_vmin)
|
|
177
|
+
if new_vmin is None:
|
|
178
|
+
return
|
|
179
|
+
self.vmin_button.config(text=f"vmin: {new_vmin:.2f}")
|
|
180
|
+
self.image_review.set_clim(vmin=new_vmin)
|
|
181
|
+
self.canvas.draw()
|
|
182
|
+
|
|
183
|
+
def set_vmax(self):
|
|
184
|
+
old_vmax = self.get_vmax()
|
|
185
|
+
new_vmax = simpledialog.askfloat("Set vmax", "Enter new vmax:", initialvalue=old_vmax)
|
|
186
|
+
if new_vmax is None:
|
|
187
|
+
return
|
|
188
|
+
self.vmax_button.config(text=f"vmax: {new_vmax:.2f}")
|
|
189
|
+
self.image_review.set_clim(vmax=new_vmax)
|
|
190
|
+
self.canvas.draw()
|
|
191
|
+
|
|
192
|
+
def get_vmin(self):
|
|
193
|
+
return float(self.vmin_button.cget("text").split(":")[1])
|
|
194
|
+
|
|
195
|
+
def get_vmax(self):
|
|
196
|
+
return float(self.vmax_button.cget("text").split(":")[1])
|
|
197
|
+
|
|
198
|
+
def set_minmax(self):
|
|
199
|
+
vmin_new = np.min(self.data[self.region])
|
|
200
|
+
vmax_new = np.max(self.data[self.region])
|
|
201
|
+
self.vmin_button.config(text=f"vmin: {vmin_new:.2f}")
|
|
202
|
+
self.vmax_button.config(text=f"vmax: {vmax_new:.2f}")
|
|
203
|
+
self.image_review.set_clim(vmin=vmin_new)
|
|
204
|
+
self.image_review.set_clim(vmax=vmax_new)
|
|
205
|
+
self.canvas.draw()
|
|
206
|
+
|
|
207
|
+
def set_zscale(self):
|
|
208
|
+
vmin_new, vmax_new = zscale(self.data[self.region])
|
|
209
|
+
self.vmin_button.config(text=f"vmin: {vmin_new:.2f}")
|
|
210
|
+
self.vmax_button.config(text=f"vmax: {vmax_new:.2f}")
|
|
211
|
+
self.image_review.set_clim(vmin=vmin_new)
|
|
212
|
+
self.image_review.set_clim(vmax=vmax_new)
|
|
213
|
+
self.canvas.draw()
|
|
214
|
+
|
|
215
|
+
def set_ndeg(self):
|
|
216
|
+
new_degree = simpledialog.askinteger("Set degree", "Enter new degree (min=0):",
|
|
217
|
+
initialvalue=self.degree, minvalue=0)
|
|
218
|
+
if new_degree is None:
|
|
219
|
+
return
|
|
220
|
+
new_npoints = simpledialog.askinteger("Set n", f"Enter new n (min={2*new_degree}):",
|
|
221
|
+
initialvalue=self.npoints, minvalue=2*new_degree)
|
|
222
|
+
if new_npoints is None:
|
|
223
|
+
return
|
|
224
|
+
self.degree = new_degree
|
|
225
|
+
self.npoints = new_npoints
|
|
226
|
+
self.ndeg_label.config(text=f"deg={self.degree}, n={self.npoints}")
|
|
227
|
+
|
|
228
|
+
def interp_x(self):
|
|
229
|
+
print(f"X-interpolation of cosmic ray {self.cr_index}")
|
|
230
|
+
ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
|
|
231
|
+
ycr_min = np.min(ycr_list)
|
|
232
|
+
ycr_max = np.max(ycr_list)
|
|
233
|
+
xfit_all = []
|
|
234
|
+
yfit_all = []
|
|
235
|
+
for ycr in range(ycr_min, ycr_max + 1):
|
|
236
|
+
xmarked = xcr_list[np.where(ycr_list == ycr)]
|
|
237
|
+
if len(xmarked) > 0:
|
|
238
|
+
jmin = np.min(xmarked)
|
|
239
|
+
jmax = np.max(xmarked)
|
|
240
|
+
# mark intermediate pixels too
|
|
241
|
+
for ix in range(jmin, jmax + 1):
|
|
242
|
+
self.cr_labels[ycr, ix] = self.cr_index
|
|
243
|
+
xmarked = xcr_list[np.where(ycr_list == ycr)]
|
|
244
|
+
xfit = []
|
|
245
|
+
zfit = []
|
|
246
|
+
for i in range(jmin - self.npoints, jmin):
|
|
247
|
+
if 0 <= i < self.data.shape[1]:
|
|
248
|
+
xfit.append(i)
|
|
249
|
+
xfit_all.append(i)
|
|
250
|
+
yfit_all.append(ycr)
|
|
251
|
+
zfit.append(self.data[ycr, i])
|
|
252
|
+
for i in range(jmax + 1, jmax + 1 + self.npoints):
|
|
253
|
+
if 0 <= i < self.data.shape[1]:
|
|
254
|
+
xfit.append(i)
|
|
255
|
+
xfit_all.append(i)
|
|
256
|
+
yfit_all.append(ycr)
|
|
257
|
+
zfit.append(self.data[ycr, i])
|
|
258
|
+
if len(xfit) > self.degree:
|
|
259
|
+
p = np.polyfit(xfit, zfit, self.degree)
|
|
260
|
+
for i in range(jmin, jmax + 1):
|
|
261
|
+
if 0 <= i < self.data.shape[1]:
|
|
262
|
+
self.data[ycr, i] = np.polyval(p, i)
|
|
263
|
+
self.mask_fixed[ycr, i] = True
|
|
264
|
+
else:
|
|
265
|
+
print(f"Not enough points to fit at y={ycr+1}")
|
|
266
|
+
self.update_display()
|
|
267
|
+
return
|
|
268
|
+
self.restore_cr_button.config(state=tk.NORMAL)
|
|
269
|
+
self.remove_crosses_button.config(state=tk.DISABLED)
|
|
270
|
+
self.interp_x_button.config(state=tk.DISABLED)
|
|
271
|
+
self.interp_y_button.config(state=tk.DISABLED)
|
|
272
|
+
self.interp_s_button.config(state=tk.DISABLED)
|
|
273
|
+
self.update_display()
|
|
274
|
+
if len(xfit_all) > 0:
|
|
275
|
+
self.ax.plot(np.array(xfit_all) + 1, np.array(yfit_all) + 1, 'mo', markersize=4) # +1: from index to pixel
|
|
276
|
+
self.canvas.draw()
|
|
277
|
+
|
|
278
|
+
def interp_y(self):
|
|
279
|
+
print(f"Y-interpolation of cosmic ray {self.cr_index}")
|
|
280
|
+
ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
|
|
281
|
+
xcr_min = np.min(xcr_list)
|
|
282
|
+
xcr_max = np.max(xcr_list)
|
|
283
|
+
xfit_all = []
|
|
284
|
+
yfit_all = []
|
|
285
|
+
for xcr in range(xcr_min, xcr_max + 1):
|
|
286
|
+
ymarked = ycr_list[np.where(xcr_list == xcr)]
|
|
287
|
+
if len(ymarked) > 0:
|
|
288
|
+
imin = np.min(ymarked)
|
|
289
|
+
imax = np.max(ymarked)
|
|
290
|
+
# mark intermediate pixels too
|
|
291
|
+
for iy in range(imin, imax + 1):
|
|
292
|
+
self.cr_labels[iy, xcr] = self.cr_index
|
|
293
|
+
ymarked = ycr_list[np.where(xcr_list == xcr)]
|
|
294
|
+
yfit = []
|
|
295
|
+
zfit = []
|
|
296
|
+
for i in range(imin - self.npoints, imin):
|
|
297
|
+
if 0 <= i < self.data.shape[0]:
|
|
298
|
+
yfit.append(i)
|
|
299
|
+
yfit_all.append(i)
|
|
300
|
+
xfit_all.append(xcr)
|
|
301
|
+
zfit.append(self.data[i, xcr])
|
|
302
|
+
for i in range(imax + 1, imax + 1 + self.npoints):
|
|
303
|
+
if 0 <= i < self.data.shape[0]:
|
|
304
|
+
yfit.append(i)
|
|
305
|
+
yfit_all.append(i)
|
|
306
|
+
xfit_all.append(xcr)
|
|
307
|
+
zfit.append(self.data[i, xcr])
|
|
308
|
+
if len(yfit) > self.degree:
|
|
309
|
+
p = np.polyfit(yfit, zfit, self.degree)
|
|
310
|
+
for i in range(imin, imax + 1):
|
|
311
|
+
if 0 <= i < self.data.shape[1]:
|
|
312
|
+
self.data[i, xcr] = np.polyval(p, i)
|
|
313
|
+
self.mask_fixed[i, xcr] = True
|
|
314
|
+
else:
|
|
315
|
+
print(f"Not enough points to fit at x={xcr+1}")
|
|
316
|
+
self.update_display()
|
|
317
|
+
return
|
|
318
|
+
self.restore_cr_button.config(state=tk.NORMAL)
|
|
319
|
+
self.remove_crosses_button.config(state=tk.DISABLED)
|
|
320
|
+
self.interp_x_button.config(state=tk.DISABLED)
|
|
321
|
+
self.interp_y_button.config(state=tk.DISABLED)
|
|
322
|
+
self.interp_s_button.config(state=tk.DISABLED)
|
|
323
|
+
self.update_display()
|
|
324
|
+
if len(xfit_all) > 0:
|
|
325
|
+
self.ax.plot(np.array(xfit_all) + 1, np.array(yfit_all) + 1, 'mo', markersize=4) # +1: from index to pixel
|
|
326
|
+
self.canvas.draw()
|
|
327
|
+
|
|
328
|
+
def interp_s(self):
|
|
329
|
+
print(f"S-interpolation of cosmic ray {self.cr_index}")
|
|
330
|
+
ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
|
|
331
|
+
ycr_min = np.min(ycr_list)
|
|
332
|
+
ycr_max = np.max(ycr_list)
|
|
333
|
+
xfit_all = []
|
|
334
|
+
yfit_all = []
|
|
335
|
+
zfit_all = []
|
|
336
|
+
# First do horizontal lines
|
|
337
|
+
for ycr in range(ycr_min, ycr_max + 1):
|
|
338
|
+
xmarked = xcr_list[np.where(ycr_list == ycr)]
|
|
339
|
+
if len(xmarked) > 0:
|
|
340
|
+
jmin = np.min(xmarked)
|
|
341
|
+
jmax = np.max(xmarked)
|
|
342
|
+
# mark intermediate pixels too
|
|
343
|
+
for ix in range(jmin, jmax + 1):
|
|
344
|
+
self.cr_labels[ycr, ix] = self.cr_index
|
|
345
|
+
xmarked = xcr_list[np.where(ycr_list == ycr)]
|
|
346
|
+
for i in range(jmin - self.npoints, jmin):
|
|
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
|
+
for i in range(jmax + 1, jmax + 1 + self.npoints):
|
|
352
|
+
if 0 <= i < self.data.shape[1]:
|
|
353
|
+
xfit_all.append(i)
|
|
354
|
+
yfit_all.append(ycr)
|
|
355
|
+
zfit_all.append(self.data[ycr, i])
|
|
356
|
+
xcr_min = np.min(xcr_list)
|
|
357
|
+
# Now do vertical lines
|
|
358
|
+
xcr_max = np.max(xcr_list)
|
|
359
|
+
for xcr in range(xcr_min, xcr_max + 1):
|
|
360
|
+
ymarked = ycr_list[np.where(xcr_list == xcr)]
|
|
361
|
+
if len(ymarked) > 0:
|
|
362
|
+
imin = np.min(ymarked)
|
|
363
|
+
imax = np.max(ymarked)
|
|
364
|
+
# mark intermediate pixels too
|
|
365
|
+
for iy in range(imin, imax + 1):
|
|
366
|
+
self.cr_labels[iy, xcr] = self.cr_index
|
|
367
|
+
ymarked = ycr_list[np.where(xcr_list == xcr)]
|
|
368
|
+
for i in range(imin - self.npoints, imin):
|
|
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
|
+
for i in range(imax + 1, imax + 1 + self.npoints):
|
|
374
|
+
if 0 <= i < self.data.shape[0]:
|
|
375
|
+
yfit_all.append(i)
|
|
376
|
+
xfit_all.append(xcr)
|
|
377
|
+
zfit_all.append(self.data[i, xcr])
|
|
378
|
+
if len(xfit_all) > 3:
|
|
379
|
+
# Construct the design matrix for a 2D polynomial fit to a plane,
|
|
380
|
+
# where each row corresponds to a point (x, y, z) and the model
|
|
381
|
+
# is z = C[0]*x + C[1]*y + C[2]
|
|
382
|
+
A = np.c_[xfit_all, yfit_all, np.ones(len(xfit_all))]
|
|
383
|
+
# Least squares polynomial fit
|
|
384
|
+
C, _, _, _ = np.linalg.lstsq(A, zfit_all, rcond=None)
|
|
385
|
+
# recompute all CR pixels to take into account "holes" between marked pixels
|
|
386
|
+
ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
|
|
387
|
+
for iy, ix in zip(ycr_list, xcr_list):
|
|
388
|
+
self.data[iy, ix] = C[0] * ix + C[1] * iy + C[2]
|
|
389
|
+
self.mask_fixed[iy, ix] = True
|
|
390
|
+
else:
|
|
391
|
+
print("Not enough points to fit a plane")
|
|
392
|
+
self.update_display()
|
|
393
|
+
return
|
|
394
|
+
self.restore_cr_button.config(state=tk.NORMAL)
|
|
395
|
+
self.remove_crosses_button.config(state=tk.DISABLED)
|
|
396
|
+
self.interp_x_button.config(state=tk.DISABLED)
|
|
397
|
+
self.interp_y_button.config(state=tk.DISABLED)
|
|
398
|
+
self.interp_s_button.config(state=tk.DISABLED)
|
|
399
|
+
self.update_display()
|
|
400
|
+
if len(xfit_all) > 0:
|
|
401
|
+
self.ax.plot(np.array(xfit_all) + 1, np.array(yfit_all) + 1, 'mo', markersize=4) # +1: from index to pixel
|
|
402
|
+
self.canvas.draw()
|
|
403
|
+
|
|
404
|
+
def remove_crosses(self):
|
|
405
|
+
ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
|
|
406
|
+
for iy, ix in zip(ycr_list, xcr_list):
|
|
407
|
+
self.cr_labels[iy, ix] = 0
|
|
408
|
+
print(f"Removed all pixels of cosmic ray {self.cr_index}")
|
|
409
|
+
self.remove_crosses_button.config(state=tk.DISABLED)
|
|
410
|
+
self.interp_x_button.config(state=tk.DISABLED)
|
|
411
|
+
self.interp_y_button.config(state=tk.DISABLED)
|
|
412
|
+
self.interp_s_button.config(state=tk.DISABLED)
|
|
413
|
+
self.update_display()
|
|
414
|
+
|
|
415
|
+
def restore_cr(self):
|
|
416
|
+
ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
|
|
417
|
+
for iy, ix in zip(ycr_list, xcr_list):
|
|
418
|
+
self.data[iy, ix] = self.data_original[iy, ix]
|
|
419
|
+
self.interp_x_button.config(state=tk.NORMAL)
|
|
420
|
+
self.interp_y_button.config(state=tk.NORMAL)
|
|
421
|
+
self.interp_s_button.config(state=tk.NORMAL)
|
|
422
|
+
print(f"Restored all pixels of cosmic ray {self.cr_index}")
|
|
423
|
+
self.remove_crosses_button.config(state=tk.NORMAL)
|
|
424
|
+
self.restore_cr_button.config(state=tk.DISABLED)
|
|
425
|
+
self.update_display()
|
|
426
|
+
|
|
427
|
+
def continue_cr(self):
|
|
428
|
+
self.cr_index += 1
|
|
429
|
+
if self.cr_index > self.num_features:
|
|
430
|
+
self.cr_index = 1
|
|
431
|
+
self.first_plot = True
|
|
432
|
+
self.restore_cr_button.config(state=tk.DISABLED)
|
|
433
|
+
self.interp_x_button.config(state=tk.NORMAL)
|
|
434
|
+
self.interp_y_button.config(state=tk.NORMAL)
|
|
435
|
+
self.interp_s_button.config(state=tk.NORMAL)
|
|
436
|
+
self.update_display()
|
|
437
|
+
|
|
438
|
+
def exit_review(self):
|
|
439
|
+
self.review_window.destroy()
|
|
440
|
+
|
|
441
|
+
def on_key(self, event):
|
|
442
|
+
if event.key == 'q':
|
|
443
|
+
pass # Ignore the "q" key to prevent closing the window
|
|
444
|
+
elif event.key == 'r':
|
|
445
|
+
if self.restore_cr_button.cget("state") != "disabled":
|
|
446
|
+
self.restore_cr()
|
|
447
|
+
elif event.key == 'x':
|
|
448
|
+
if self.interp_x_button.cget("state") != "disabled":
|
|
449
|
+
self.interp_x()
|
|
450
|
+
elif event.key == 'y':
|
|
451
|
+
if self.interp_y_button.cget("state") != "disabled":
|
|
452
|
+
self.interp_y()
|
|
453
|
+
elif event.key == 's':
|
|
454
|
+
if self.interp_s_button.cget("state") != "disabled":
|
|
455
|
+
self.interp_s()
|
|
456
|
+
elif event.key == 'right' or event.key == 'c':
|
|
457
|
+
self.continue_cr()
|
|
458
|
+
elif event.key == ',':
|
|
459
|
+
self.set_minmax()
|
|
460
|
+
elif event.key == '/':
|
|
461
|
+
self.set_zscale()
|
|
462
|
+
elif event.key == 'e':
|
|
463
|
+
self.exit_review()
|
|
464
|
+
else:
|
|
465
|
+
print(f"Key pressed: {event.key}")
|
|
466
|
+
|
|
467
|
+
def on_click(self, event):
|
|
468
|
+
if event.inaxes:
|
|
469
|
+
x, y = event.xdata, event.ydata
|
|
470
|
+
print(f"Clicked at image coordinates: ({x:.2f}, {y:.2f})")
|
|
471
|
+
ix = int(x+0.5) - 1 # from pixel to index
|
|
472
|
+
iy = int(y+0.5) - 1 # from pixel to index
|
|
473
|
+
if int(self.cr_labels[iy, ix]) == self.cr_index:
|
|
474
|
+
self.cr_labels[iy, ix] = 0
|
|
475
|
+
print(f"Pixel ({ix+1}, {iy+1}) unmarked as cosmic ray.")
|
|
476
|
+
else:
|
|
477
|
+
self.cr_labels[iy, ix] = self.cr_index
|
|
478
|
+
print(f"Pixel ({ix+1}, {iy+1}) marked as cosmic ray.")
|
|
479
|
+
xcr_list, ycr_list = np.where(self.cr_labels == self.cr_index)
|
|
480
|
+
if len(xcr_list) == 0:
|
|
481
|
+
self.interp_x_button.config(state=tk.DISABLED)
|
|
482
|
+
self.interp_y_button.config(state=tk.DISABLED)
|
|
483
|
+
self.interp_s_button.config(state=tk.DISABLED)
|
|
484
|
+
self.remove_crosses_button.config(state=tk.DISABLED)
|
|
485
|
+
else:
|
|
486
|
+
self.interp_x_button.config(state=tk.NORMAL)
|
|
487
|
+
self.interp_y_button.config(state=tk.NORMAL)
|
|
488
|
+
self.interp_s_button.config(state=tk.NORMAL)
|
|
489
|
+
self.remove_crosses_button.config(state=tk.NORMAL)
|
|
490
|
+
# Update the display to reflect the change
|
|
491
|
+
self.update_display()
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
class CosmicRayCleanerApp():
|
|
495
|
+
"""Main application class for cosmic ray cleaning."""
|
|
496
|
+
|
|
497
|
+
def __init__(self, root, input_fits, extension=0, output_fits=None):
|
|
498
|
+
"""
|
|
499
|
+
Initialize the application.
|
|
500
|
+
|
|
501
|
+
Parameters
|
|
502
|
+
----------
|
|
503
|
+
root : tk.Tk
|
|
504
|
+
The main Tkinter window.
|
|
505
|
+
input_fits : str
|
|
506
|
+
Path to the FITS file to be cleaned.
|
|
507
|
+
extension : int, optional
|
|
508
|
+
FITS extension to use (default is 0).
|
|
509
|
+
output_fits : str, optional
|
|
510
|
+
Path to save the cleaned FITS file (default is None, which prompts
|
|
511
|
+
for a save location).
|
|
512
|
+
"""
|
|
513
|
+
self.root = root
|
|
514
|
+
self.root.title("Cosmic Ray Cleaner")
|
|
515
|
+
self.root.geometry("800x700+50+0")
|
|
516
|
+
self.input_fits = input_fits
|
|
517
|
+
self.extension = extension
|
|
518
|
+
self.output_fits = output_fits
|
|
519
|
+
self.load_fits_file()
|
|
520
|
+
self.create_widgets()
|
|
521
|
+
|
|
522
|
+
def load_fits_file(self):
|
|
523
|
+
try:
|
|
524
|
+
with fits.open(self.input_fits, mode='readonly') as hdul:
|
|
525
|
+
self.data = hdul[self.extension].data
|
|
526
|
+
if 'CRMASK' in hdul:
|
|
527
|
+
self.mask_fixed = hdul['CRMASK'].data.astype(bool)
|
|
528
|
+
else:
|
|
529
|
+
self.mask_fixed = np.zeros(self.data.shape, dtype=bool)
|
|
530
|
+
except Exception as e:
|
|
531
|
+
print(f"Error loading FITS file: {e}")
|
|
532
|
+
|
|
533
|
+
def save_fits_file(self):
|
|
534
|
+
if self.output_fits is None:
|
|
535
|
+
base, ext = os.path.splitext(self.input_fits)
|
|
536
|
+
suggested_name = f"{base}_cleaned"
|
|
537
|
+
else:
|
|
538
|
+
suggested_name, _ = os.path.splitext(self.output_fits)
|
|
539
|
+
self.output_fits = filedialog.asksaveasfilename(
|
|
540
|
+
initialdir=os.getcwd(),
|
|
541
|
+
title="Save cleaned FITS file",
|
|
542
|
+
defaultextension=".fits",
|
|
543
|
+
filetypes=[("FITS files", "*.fits"), ("All files", "*.*")],
|
|
544
|
+
initialfile=suggested_name
|
|
545
|
+
)
|
|
546
|
+
try:
|
|
547
|
+
with fits.open(self.input_fits, mode='readonly') as hdul:
|
|
548
|
+
hdul[self.extension].data = self.data
|
|
549
|
+
if 'CRMASK' in hdul:
|
|
550
|
+
hdul['CRMASK'].data = self.mask_fixed.astype(np.uint8)
|
|
551
|
+
else:
|
|
552
|
+
crmask_hdu = fits.ImageHDU(self.mask_fixed.astype(np.uint8), name='CRMASK')
|
|
553
|
+
hdul.append(crmask_hdu)
|
|
554
|
+
hdul.writeto(self.output_fits, overwrite=True)
|
|
555
|
+
print(f"Cleaned data saved to {self.output_fits}")
|
|
556
|
+
except Exception as e:
|
|
557
|
+
print(f"Error saving FITS file: {e}")
|
|
558
|
+
|
|
559
|
+
def create_widgets(self):
|
|
560
|
+
# Row 1
|
|
561
|
+
self.button_frame1 = tk.Frame(self.root)
|
|
562
|
+
self.button_frame1.grid(row=0, column=0, pady=5)
|
|
563
|
+
self.run_lacosmic_button = tk.Button(self.button_frame1, text="Run L.A.Cosmic", command=self.run_lacosmic)
|
|
564
|
+
self.run_lacosmic_button.pack(side=tk.LEFT, padx=5)
|
|
565
|
+
self.save_button = tk.Button(self.button_frame1, text="Save cleaned FITS", command=self.save_fits_file)
|
|
566
|
+
self.save_button.pack(side=tk.LEFT, padx=5)
|
|
567
|
+
|
|
568
|
+
# Row 2
|
|
569
|
+
self.button_frame2 = tk.Frame(self.root)
|
|
570
|
+
self.button_frame2.grid(row=1, column=0, pady=5)
|
|
571
|
+
vmin, vmax = zscale(self.data)
|
|
572
|
+
self.vmin_button = tk.Button(self.button_frame2, text=f"vmin: {vmin:.2f}", command=self.set_vmin)
|
|
573
|
+
self.vmin_button.pack(side=tk.LEFT, padx=5)
|
|
574
|
+
self.vmax_button = tk.Button(self.button_frame2, text=f"vmax: {vmax:.2f}", command=self.set_vmax)
|
|
575
|
+
self.vmax_button.pack(side=tk.LEFT, padx=5)
|
|
576
|
+
self.stop_button = tk.Button(self.button_frame2, text="Stop program", command=self.stop_app)
|
|
577
|
+
self.stop_button.pack(side=tk.LEFT, padx=5)
|
|
578
|
+
|
|
579
|
+
# Main frame for figure and toolbar
|
|
580
|
+
self.main_frame = tk.Frame(self.root)
|
|
581
|
+
self.main_frame.grid(row=2, column=0, sticky="nsew")
|
|
582
|
+
self.root.grid_rowconfigure(2, weight=1)
|
|
583
|
+
self.root.grid_columnconfigure(0, weight=1)
|
|
584
|
+
self.main_frame.grid_rowconfigure(0, weight=1)
|
|
585
|
+
self.main_frame.grid_columnconfigure(0, weight=1)
|
|
586
|
+
|
|
587
|
+
# Create figure and axis
|
|
588
|
+
self.fig, self.ax = plt.subplots(figsize=(8, 6))
|
|
589
|
+
xlabel = 'X pixel (from 1 to NAXIS1)'
|
|
590
|
+
ylabel = 'Y pixel (from 1 to NAXIS2)'
|
|
591
|
+
extent = [0.5, self.data.shape[1] + 0.5, 0.5, self.data.shape[0] + 0.5]
|
|
592
|
+
self.image, _, _ = imshow(self.fig, self.ax, self.data, vmin=vmin, vmax=vmax,
|
|
593
|
+
xlabel=xlabel, ylabel=ylabel, extent=extent)
|
|
594
|
+
# Note: tight_layout should be called before defining the canvas
|
|
595
|
+
self.fig.tight_layout()
|
|
596
|
+
|
|
597
|
+
# Create canvas and toolbar
|
|
598
|
+
self.canvas = FigureCanvasTkAgg(self.fig, master=self.main_frame)
|
|
599
|
+
# The next two instructions prevent a segmentation fault when pressing "q"
|
|
600
|
+
self.canvas.mpl_disconnect(self.canvas.mpl_connect("key_press_event", key_press_handler))
|
|
601
|
+
self.canvas.mpl_connect("key_press_event", self.on_key)
|
|
602
|
+
self.canvas.mpl_connect("button_press_event", self.on_click)
|
|
603
|
+
canvas_widget = self.canvas.get_tk_widget()
|
|
604
|
+
canvas_widget.grid(row=0, column=0, sticky="nsew")
|
|
605
|
+
|
|
606
|
+
# Matplotlib toolbar
|
|
607
|
+
self.toolbar_frame = tk.Frame(self.main_frame)
|
|
608
|
+
self.toolbar_frame.grid(row=1, column=0, sticky="ew")
|
|
609
|
+
self.toolbar = NavigationToolbar2Tk(self.canvas, self.toolbar_frame)
|
|
610
|
+
self.toolbar.update()
|
|
611
|
+
|
|
612
|
+
def set_vmin(self):
|
|
613
|
+
old_vmin = self.get_vmin()
|
|
614
|
+
new_vmin = simpledialog.askfloat("Set vmin", "Enter new vmin:", initialvalue=old_vmin)
|
|
615
|
+
if new_vmin is None:
|
|
616
|
+
return
|
|
617
|
+
self.vmin_button.config(text=f"vmin: {new_vmin:.2f}")
|
|
618
|
+
self.image.set_clim(vmin=new_vmin)
|
|
619
|
+
self.canvas.draw()
|
|
620
|
+
|
|
621
|
+
def set_vmax(self):
|
|
622
|
+
old_vmax = self.get_vmax()
|
|
623
|
+
new_vmax = simpledialog.askfloat("Set vmax", "Enter new vmax:", initialvalue=old_vmax)
|
|
624
|
+
if new_vmax is None:
|
|
625
|
+
return
|
|
626
|
+
self.vmax_button.config(text=f"vmax: {new_vmax:.2f}")
|
|
627
|
+
self.image.set_clim(vmax=new_vmax)
|
|
628
|
+
self.canvas.draw()
|
|
629
|
+
|
|
630
|
+
def get_vmin(self):
|
|
631
|
+
return float(self.vmin_button.cget("text").split(":")[1])
|
|
632
|
+
|
|
633
|
+
def get_vmax(self):
|
|
634
|
+
return float(self.vmax_button.cget("text").split(":")[1])
|
|
635
|
+
|
|
636
|
+
def run_lacosmic(self):
|
|
637
|
+
self.run_lacosmic_button.config(state=tk.DISABLED)
|
|
638
|
+
self.stop_button.config(state=tk.DISABLED)
|
|
639
|
+
# Parameters for L.A.Cosmic can be adjusted as needed
|
|
640
|
+
_, mask_crfound = cosmicray_lacosmic(self.data, sigclip=4.5, sigfrac=0.3, objlim=5.0, verbose=True)
|
|
641
|
+
ReviewCosmicRay(
|
|
642
|
+
root=self.root,
|
|
643
|
+
data=self.data,
|
|
644
|
+
mask_fixed=self.mask_fixed,
|
|
645
|
+
mask_crfound=mask_crfound
|
|
646
|
+
)
|
|
647
|
+
print("L.A.Cosmic cleaning applied.")
|
|
648
|
+
self.run_lacosmic_button.config(state=tk.NORMAL)
|
|
649
|
+
self.stop_button.config(state=tk.NORMAL)
|
|
650
|
+
|
|
651
|
+
def stop_app(self):
|
|
652
|
+
self.root.quit()
|
|
653
|
+
self.root.destroy()
|
|
654
|
+
|
|
655
|
+
def on_key(self, event):
|
|
656
|
+
if event.key == 'q':
|
|
657
|
+
pass # Ignore the "q" key to prevent closing the window
|
|
658
|
+
else:
|
|
659
|
+
print(f"Key pressed: {event.key}")
|
|
660
|
+
|
|
661
|
+
def on_click(self, event):
|
|
662
|
+
if event.inaxes:
|
|
663
|
+
x, y = event.xdata, event.ydata
|
|
664
|
+
print(f"Clicked at image coordinates: ({x:.2f}, {y:.2f})")
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def main():
|
|
668
|
+
parser = argparse.ArgumentParser(description="Interactive cosmic ray cleaner for FITS images.")
|
|
669
|
+
parser.add_argument("input_fits", help="Path to the FITS file to be cleaned.")
|
|
670
|
+
parser.add_argument("--extension", type=int, default=0,
|
|
671
|
+
help="FITS extension to use (default: 0).")
|
|
672
|
+
parser.add_argument("--output_fits", type=str, default=None,
|
|
673
|
+
help="Path to save the cleaned FITS file")
|
|
674
|
+
args = parser.parse_args()
|
|
675
|
+
|
|
676
|
+
if not os.path.isfile(args.input_fits):
|
|
677
|
+
print(f"Error: File '{args.input_fits}' does not exist.")
|
|
678
|
+
return
|
|
679
|
+
if args.output_fits is not None and os.path.isfile(args.output_fits):
|
|
680
|
+
print(f"Error: Output file '{args.output_fits}' already exists.")
|
|
681
|
+
return
|
|
682
|
+
|
|
683
|
+
# Initialize Tkinter root
|
|
684
|
+
root = tk.Tk()
|
|
685
|
+
|
|
686
|
+
# Create and run the application
|
|
687
|
+
CosmicRayCleanerApp(root, args.input_fits, args.extension, args.output_fits)
|
|
688
|
+
|
|
689
|
+
# Execute
|
|
690
|
+
root.mainloop()
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
if __name__ == "__main__":
|
|
694
|
+
main()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#
|
|
2
|
-
# Copyright 2022-
|
|
2
|
+
# Copyright 2022-2025 Universidad Complutense de Madrid
|
|
3
3
|
#
|
|
4
4
|
# This file is part of teareduce
|
|
5
5
|
#
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def ctext(s=None,
|
|
12
|
-
fg=None,
|
|
13
|
-
bg=None,
|
|
12
|
+
fg=None,
|
|
13
|
+
bg=None,
|
|
14
14
|
under=False,
|
|
15
15
|
rev=False,
|
|
16
16
|
faint=False,
|
|
@@ -95,4 +95,3 @@ def ctext(s=None,
|
|
|
95
95
|
result = f'{final_style}{s}\x1B[0m'
|
|
96
96
|
|
|
97
97
|
return result
|
|
98
|
-
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#
|
|
2
|
-
# Copyright 2022-
|
|
2
|
+
# Copyright 2022-2025 Universidad Complutense de Madrid
|
|
3
3
|
#
|
|
4
4
|
# This file is part of teareduce
|
|
5
5
|
#
|
|
@@ -56,11 +56,11 @@ def draw_rectangle(ax, image_data, x1, x2, y1, y2,
|
|
|
56
56
|
ax.plot((x1, x2), (y2, y2), color, lw=1)
|
|
57
57
|
|
|
58
58
|
if text:
|
|
59
|
-
ax.text((x1+x2)/2, y1+(y2-y1)/8,
|
|
60
|
-
'{:.{prec}f}'.format(mean, prec=ndigits),
|
|
61
|
-
ha='center', va='center', color=color, fontsize=fontsize)
|
|
59
|
+
ax.text((x1+x2)/2, y1+(y2-y1)/8,
|
|
60
|
+
'{:.{prec}f}'.format(mean, prec=ndigits),
|
|
61
|
+
ha='center', va='center', color=color, fontsize=fontsize)
|
|
62
62
|
ax.text((x1+x2)/2, y2-(y2-y1)/8,
|
|
63
|
-
'{:.{prec}f}'.format(std, prec=ndigits),
|
|
63
|
+
'{:.{prec}f}'.format(std, prec=ndigits),
|
|
64
64
|
ha='center', va='top', color=color, fontsize=fontsize)
|
|
65
65
|
|
|
66
66
|
return mean, std
|
|
@@ -41,7 +41,7 @@ def imshowme(data, **kwargs):
|
|
|
41
41
|
return fig, ax, img, cax, cbar
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
def imshow(fig=None, ax=None, data=None,
|
|
44
|
+
def imshow(fig=None, ax=None, data=None, ds9mode=False,
|
|
45
45
|
crpix1=1, crval1=None, cdelt1=None, cunit1=None, cunitx=Unit('Angstrom'),
|
|
46
46
|
xlabel=None, ylabel=None, title=None,
|
|
47
47
|
colorbar=True, cblabel='Number of counts',
|
|
@@ -60,6 +60,10 @@ def imshow(fig=None, ax=None, data=None,
|
|
|
60
60
|
Instance of Axes.
|
|
61
61
|
data : numpy array
|
|
62
62
|
2D array to be displayed.
|
|
63
|
+
ds9mode : bool
|
|
64
|
+
If True, the extent parameter is set to
|
|
65
|
+
[0.5, NAXIS1+0.5, 0.5, NAXIS2+0.5]
|
|
66
|
+
to mimic the DS9 display.
|
|
63
67
|
crpix1 : astropy.units.Quantity
|
|
64
68
|
Float number providing the CRPIX1 value: the reference pixel
|
|
65
69
|
for which CRVAL1 is given.
|
|
@@ -103,13 +107,6 @@ def imshow(fig=None, ax=None, data=None,
|
|
|
103
107
|
if not isinstance(ax, Axes):
|
|
104
108
|
raise ValueError("Unexpected 'ax' argument")
|
|
105
109
|
|
|
106
|
-
# default labels
|
|
107
|
-
if xlabel is None:
|
|
108
|
-
xlabel = 'X axis (array index)'
|
|
109
|
-
|
|
110
|
-
if ylabel is None:
|
|
111
|
-
ylabel = 'Y axis (array index)'
|
|
112
|
-
|
|
113
110
|
wavecalib = False
|
|
114
111
|
if crpix1 is not None and crval1 is not None and cdelt1 is not None and cunit1 is not None:
|
|
115
112
|
if 'extent' in kwargs:
|
|
@@ -129,11 +126,25 @@ def imshow(fig=None, ax=None, data=None,
|
|
|
129
126
|
aspect = 'auto'
|
|
130
127
|
wavecalib = True
|
|
131
128
|
else:
|
|
132
|
-
if
|
|
133
|
-
extent
|
|
134
|
-
|
|
129
|
+
if ds9mode:
|
|
130
|
+
if 'extent' in kwargs:
|
|
131
|
+
raise ValueError('extent parameter can not be used with ds9mode=True')
|
|
132
|
+
naxis2, naxis1 = data.shape
|
|
133
|
+
extent = [0.5, naxis1 + 0.5, 0.5, naxis2 + 0.5]
|
|
134
|
+
if xlabel is None:
|
|
135
|
+
xlabel = 'X pixel (from 1 to NAXIS1)'
|
|
136
|
+
if ylabel is None:
|
|
137
|
+
ylabel = 'Y pixel (from 1 to NAXIS2)'
|
|
135
138
|
else:
|
|
136
|
-
|
|
139
|
+
if xlabel is None:
|
|
140
|
+
xlabel = 'X axis (array index)'
|
|
141
|
+
if ylabel is None:
|
|
142
|
+
ylabel = 'Y axis (array index)'
|
|
143
|
+
if 'extent' in kwargs:
|
|
144
|
+
extent = kwargs['extent']
|
|
145
|
+
del kwargs['extent']
|
|
146
|
+
else:
|
|
147
|
+
extent = None
|
|
137
148
|
if 'aspect' in kwargs:
|
|
138
149
|
aspect = kwargs['aspect']
|
|
139
150
|
del kwargs['aspect']
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
#
|
|
3
|
-
# Copyright 2015-
|
|
3
|
+
# Copyright 2015-2025 Universidad Complutense de Madrid
|
|
4
4
|
#
|
|
5
5
|
# This file is part of teareduce
|
|
6
6
|
#
|
|
@@ -58,12 +58,12 @@ def find_peaks_spectrum(sx, nwinwidth, deltaflux=0, threshold=0, debugplot=False
|
|
|
58
58
|
'pixels will be ignored')
|
|
59
59
|
|
|
60
60
|
xpeaks = [] # list to store the peaks
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
if sx_shape[0] < nwinwidth:
|
|
63
63
|
print('find_peaks_spectrum> sx shape......:', sx_shape)
|
|
64
64
|
print('find_peaks_spectrum> nwinwidth.....:', nwinwidth)
|
|
65
65
|
raise ValueError('sx.shape < nwinwidth')
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
i = nmed
|
|
68
68
|
while i < sx_shape[0] - nmed:
|
|
69
69
|
if sx[i] > threshold:
|
|
@@ -93,7 +93,7 @@ def find_peaks_spectrum(sx, nwinwidth, deltaflux=0, threshold=0, debugplot=False
|
|
|
93
93
|
i += 1
|
|
94
94
|
else:
|
|
95
95
|
i += 1
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
ixpeaks = np.array(xpeaks)
|
|
98
98
|
|
|
99
99
|
if debugplot:
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
#
|
|
9
9
|
"""Auxiliary classes to handle slicing regions in 1D, 2D, and 3D.
|
|
10
10
|
|
|
11
|
-
These classes provide a way to define and manipulate slices in a
|
|
11
|
+
These classes provide a way to define and manipulate slices in a
|
|
12
12
|
consistent manner, following both FITS and Python conventions.
|
|
13
13
|
"""
|
|
14
14
|
|
|
@@ -16,9 +16,10 @@ import re
|
|
|
16
16
|
|
|
17
17
|
import numpy as np
|
|
18
18
|
|
|
19
|
+
|
|
19
20
|
class SliceRegion1D:
|
|
20
21
|
"""Store indices for slicing of 1D regions.
|
|
21
|
-
|
|
22
|
+
|
|
22
23
|
The attributes .python and .fits provide the indices following
|
|
23
24
|
the Python and the FITS convention, respectively.
|
|
24
25
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#
|
|
2
|
-
# Copyright 2022-
|
|
2
|
+
# Copyright 2022-2025 Universidad Complutense de Madrid
|
|
3
3
|
#
|
|
4
4
|
# This file is part of teareduce
|
|
5
5
|
#
|
|
@@ -22,7 +22,7 @@ def statsummary(x=None, rm_nan=False, show=True):
|
|
|
22
22
|
Parameters
|
|
23
23
|
----------
|
|
24
24
|
x : numpy array or None
|
|
25
|
-
Input array with values which statistical properties are
|
|
25
|
+
Input array with values which statistical properties are
|
|
26
26
|
requested.
|
|
27
27
|
rm_nan : bool
|
|
28
28
|
If True, filter out NaN values before computing statistics.
|
|
@@ -42,9 +42,9 @@ def statsummary(x=None, rm_nan=False, show=True):
|
|
|
42
42
|
|
|
43
43
|
# protections
|
|
44
44
|
if x is None:
|
|
45
|
-
return ['npoints', 'minimum', 'maximum',
|
|
46
|
-
'mean', 'median', 'std', 'robust_std',
|
|
47
|
-
'percentile16', 'percentile25', 'percentile75', 'percentile84']
|
|
45
|
+
return ['npoints', 'minimum', 'maximum',
|
|
46
|
+
'mean', 'median', 'std', 'robust_std',
|
|
47
|
+
'percentile16', 'percentile25', 'percentile75', 'percentile84']
|
|
48
48
|
|
|
49
49
|
if isinstance(x, np.ndarray):
|
|
50
50
|
xx = np.copy(x.flatten())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
#
|
|
3
|
-
# Copyright 2015-
|
|
3
|
+
# Copyright 2015-2025 Universidad Complutense de Madrid
|
|
4
4
|
#
|
|
5
5
|
# This file is part of teareduce
|
|
6
6
|
#
|
|
@@ -30,10 +30,10 @@ from .sliceregion import SliceRegion1D
|
|
|
30
30
|
|
|
31
31
|
class TeaWaveCalibration:
|
|
32
32
|
"""Auxiliary class to compute and apply wavelength calibration.
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
It is assumed that the wavelength and spatial directions correspond
|
|
35
35
|
to the X (array columns) and Y (array rows) axes, respectively.
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
Attributes
|
|
38
38
|
----------
|
|
39
39
|
ns_window : int
|
|
@@ -44,7 +44,7 @@ class TeaWaveCalibration:
|
|
|
44
44
|
Standard deviation for Gaussian kernel to smooth median spectrum.
|
|
45
45
|
A value of 0 means that no smoothing is performed.
|
|
46
46
|
nx_window : int
|
|
47
|
-
Number of pixels (spectral direction) of the window where the
|
|
47
|
+
Number of pixels (spectral direction) of the window where the
|
|
48
48
|
peaks are sought. It must be odd.
|
|
49
49
|
delta_flux : float
|
|
50
50
|
Minimum difference between the flux at the line center and the
|
|
@@ -293,7 +293,7 @@ class TeaWaveCalibration:
|
|
|
293
293
|
Standard deviation for Gaussian kernel to smooth median spectrum.
|
|
294
294
|
A value of 0 means that no smoothing is performed.
|
|
295
295
|
nx_window : int or None
|
|
296
|
-
Number of pixels (spectral direction) of the window where the
|
|
296
|
+
Number of pixels (spectral direction) of the window where the
|
|
297
297
|
peaks are sought. It must be odd.
|
|
298
298
|
delta_flux : float
|
|
299
299
|
Minimum difference between the flux at the line center and the
|
|
@@ -335,7 +335,7 @@ class TeaWaveCalibration:
|
|
|
335
335
|
self.delta_flux = delta_flux
|
|
336
336
|
if method is not None:
|
|
337
337
|
self.method = method
|
|
338
|
-
|
|
338
|
+
|
|
339
339
|
naxis2, naxis1 = data.shape
|
|
340
340
|
|
|
341
341
|
if self._naxis1 is None:
|
|
@@ -343,7 +343,7 @@ class TeaWaveCalibration:
|
|
|
343
343
|
else:
|
|
344
344
|
if naxis1 != self._naxis1:
|
|
345
345
|
raise ValueError(f'Unexpected naxis1: {naxis1}')
|
|
346
|
-
|
|
346
|
+
|
|
347
347
|
if self._naxis2 is None:
|
|
348
348
|
self._naxis2 = naxis2
|
|
349
349
|
else:
|
|
@@ -363,7 +363,7 @@ class TeaWaveCalibration:
|
|
|
363
363
|
|
|
364
364
|
# initial median spectrum
|
|
365
365
|
xpeaks, ixpeaks, sp_median_smooth = self._find_peaks_scan(
|
|
366
|
-
data=data,
|
|
366
|
+
data=data,
|
|
367
367
|
ns1=ns1,
|
|
368
368
|
ns2=ns2,
|
|
369
369
|
plot_peaks=plot_peaks,
|
|
@@ -420,12 +420,12 @@ class TeaWaveCalibration:
|
|
|
420
420
|
xpeaks_reference=None,
|
|
421
421
|
ns_range=None,
|
|
422
422
|
direction='up',
|
|
423
|
-
ns_window
|
|
424
|
-
threshold
|
|
425
|
-
sigma_smooth
|
|
426
|
-
nx_window
|
|
427
|
-
delta_flux
|
|
428
|
-
method
|
|
423
|
+
ns_window=None,
|
|
424
|
+
threshold=None,
|
|
425
|
+
sigma_smooth=None,
|
|
426
|
+
nx_window=None,
|
|
427
|
+
delta_flux=None,
|
|
428
|
+
method=None,
|
|
429
429
|
plots=False,
|
|
430
430
|
title=None,
|
|
431
431
|
pdf_output=None,
|
|
@@ -455,7 +455,7 @@ class TeaWaveCalibration:
|
|
|
455
455
|
Standard deviation for Gaussian kernel to smooth median spectrum.
|
|
456
456
|
A value of 0 means that no smoothing is performed.
|
|
457
457
|
nx_window : int or None
|
|
458
|
-
Number of pixels (spectral direction) of the window where the
|
|
458
|
+
Number of pixels (spectral direction) of the window where the
|
|
459
459
|
peaks are sought. It must be odd.
|
|
460
460
|
delta_flux : float
|
|
461
461
|
Minimum difference between the flux at the line center and the
|
|
@@ -540,7 +540,7 @@ class TeaWaveCalibration:
|
|
|
540
540
|
color_previous = ['blue', 'cyan']
|
|
541
541
|
fig, ax = plt.subplots(figsize=(15, 15*naxis2/naxis1))
|
|
542
542
|
vmin, vmax = np.percentile(data, [5, 95])
|
|
543
|
-
|
|
543
|
+
imshow(fig, ax, data, vmin=vmin, vmax=vmax, cmap='gray', title=title, aspect='auto')
|
|
544
544
|
# display previously identified lines
|
|
545
545
|
yplot = np.arange(naxis2)[self._valid_scans]
|
|
546
546
|
for i in range(self._nlines_reference):
|
|
@@ -549,18 +549,17 @@ class TeaWaveCalibration:
|
|
|
549
549
|
else:
|
|
550
550
|
fig = None
|
|
551
551
|
ax = None
|
|
552
|
-
img = None
|
|
553
552
|
|
|
554
553
|
# search for peaks in the 2D image
|
|
555
554
|
dict_xpeaks = dict()
|
|
556
|
-
for ns in tqdm(range(ns_min_fits, ns_max_fits + ns_step, ns_step),
|
|
555
|
+
for ns in tqdm(range(ns_min_fits, ns_max_fits + ns_step, ns_step),
|
|
557
556
|
desc='Finding peaks', disable=disable_tqdm):
|
|
558
557
|
ns1 = ns - self.ns_window // 2
|
|
559
558
|
ns1 = max([ns1, min(ns_min_fits, ns_max_fits)])
|
|
560
559
|
ns2 = ns + self.ns_window // 2
|
|
561
560
|
ns2 = min([ns2, max(ns_min_fits, ns_max_fits)])
|
|
562
561
|
xpeaks, ixpeaks, sp_median_smooth = self._find_peaks_scan(
|
|
563
|
-
data=data,
|
|
562
|
+
data=data,
|
|
564
563
|
ns1=ns1,
|
|
565
564
|
ns2=ns2,
|
|
566
565
|
plot_peaks=False,
|
|
@@ -585,7 +584,7 @@ class TeaWaveCalibration:
|
|
|
585
584
|
xpeaks_predicted = np.median(self._xpeaks_all_lines_array[(ns1-1):ns2, :], axis=0)
|
|
586
585
|
for i in range(self._nlines_reference):
|
|
587
586
|
value = xpeaks_predicted[i]
|
|
588
|
-
# if there is no peak near to the expected location
|
|
587
|
+
# if there is no peak near to the expected location
|
|
589
588
|
# (within a distance given by self.nx_window) the line
|
|
590
589
|
# is probably weak and we need to avoid jumping into
|
|
591
590
|
# another line
|
|
@@ -661,7 +660,7 @@ class TeaWaveCalibration:
|
|
|
661
660
|
self.peak_wavelengths = wavelengths
|
|
662
661
|
|
|
663
662
|
@u.quantity_input(xpeaks=u.pixel)
|
|
664
|
-
def overplot_identified_lines(self, xpeaks, spectrum,
|
|
663
|
+
def overplot_identified_lines(self, xpeaks, spectrum,
|
|
665
664
|
title=None, fontsize_title=16, fontsize_wave=10,
|
|
666
665
|
pdf_output=None, pdf_only=False):
|
|
667
666
|
"""Overplot identified lines
|
|
@@ -790,7 +789,7 @@ class TeaWaveCalibration:
|
|
|
790
789
|
xfit = np.arange(self._naxis2)[self._valid_scans]
|
|
791
790
|
if len(xfit) <= self.degree_cdistortion:
|
|
792
791
|
raise ValueError(f'Insufficient number of points to fit a polynomial of degree {self.degree_cdistortion}')
|
|
793
|
-
for i in tqdm(range(self._nlines_reference),
|
|
792
|
+
for i in tqdm(range(self._nlines_reference),
|
|
794
793
|
desc='Fitting C distortion', disable=disable_tqdm):
|
|
795
794
|
yfit = self._xpeaks_all_lines_array[self._valid_scans, i]
|
|
796
795
|
poly, yres, reject = polfit_residuals_with_sigma_rejection(
|
|
@@ -1124,11 +1123,11 @@ class TeaWaveCalibration:
|
|
|
1124
1123
|
raise ValueError('You must set plots=True to make use of pdf_output')
|
|
1125
1124
|
|
|
1126
1125
|
return poly_fits_yx, residual_std_yx, \
|
|
1127
|
-
|
|
1128
|
-
|
|
1126
|
+
poly_fits_xy, residual_std_xy, \
|
|
1127
|
+
crval1_linear, cdelt1_linear, crmax1_linear
|
|
1129
1128
|
|
|
1130
1129
|
def fit_wavelengths(self, degree_wavecalib=None,
|
|
1131
|
-
output_filename=None, history_list=None,
|
|
1130
|
+
output_filename=None, history_list=None,
|
|
1132
1131
|
plots=False, title=None,
|
|
1133
1132
|
pdf_output=None, pdf_only=False,
|
|
1134
1133
|
silent_mode=False, disable_tqdm=True):
|
|
@@ -1320,9 +1319,9 @@ class TeaWaveCalibration:
|
|
|
1320
1319
|
# crval1, cdelt1, crmax1, residual_std
|
|
1321
1320
|
fig, axarr = plt.subplots(nrows=1, ncols=5, figsize=(15, 3))
|
|
1322
1321
|
axarr = axarr.flatten()
|
|
1323
|
-
for i, item in enumerate(['_array_crval1_linear',
|
|
1322
|
+
for i, item in enumerate(['_array_crval1_linear',
|
|
1324
1323
|
'_array_cdelt1_linear',
|
|
1325
|
-
'_array_crmax1_linear',
|
|
1324
|
+
'_array_crmax1_linear',
|
|
1326
1325
|
'_array_residual_std_wav',
|
|
1327
1326
|
'_array_residual_std_pix']):
|
|
1328
1327
|
ax = axarr[i]
|
|
@@ -1357,8 +1356,7 @@ class TeaWaveCalibration:
|
|
|
1357
1356
|
nrows = int(ncoeff / npprow)
|
|
1358
1357
|
if ncoeff % npprow != 0:
|
|
1359
1358
|
nrows += 1
|
|
1360
|
-
fig, axarr = plt.subplots(nrows=nrows, ncols=npprow,
|
|
1361
|
-
figsize=(figwidth, 3*nrows))
|
|
1359
|
+
fig, axarr = plt.subplots(nrows=nrows, ncols=npprow, figsize=(figwidth, 3*nrows))
|
|
1362
1360
|
axarr = axarr.flatten()
|
|
1363
1361
|
for ax in axarr:
|
|
1364
1362
|
ax.axis('off')
|
|
@@ -1435,7 +1433,7 @@ class TeaWaveCalibration:
|
|
|
1435
1433
|
|
|
1436
1434
|
old_x_borders_fits = np.arange(naxis1 + 1) + 0.5 # FITS convention
|
|
1437
1435
|
|
|
1438
|
-
for k in tqdm(range(naxis2),
|
|
1436
|
+
for k in tqdm(range(naxis2),
|
|
1439
1437
|
desc='Applying wavelength calibration',
|
|
1440
1438
|
disable=disable_tqdm):
|
|
1441
1439
|
poly = Polynomial(self._array_poly_wav[k])
|
|
@@ -1678,4 +1676,3 @@ def apply_wavecal_ccddata(infile, wcalibfile, outfile,
|
|
|
1678
1676
|
cdelt1=cdelt1,
|
|
1679
1677
|
title=f'{title_}(UNCERT extension)'
|
|
1680
1678
|
)
|
|
1681
|
-
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#
|
|
2
|
-
# Copyright 2022-
|
|
2
|
+
# Copyright 2022-2025 Universidad Complutense de Madrid
|
|
3
3
|
#
|
|
4
4
|
# This file is part of teareduce
|
|
5
5
|
#
|
|
@@ -12,8 +12,8 @@ import numpy as np
|
|
|
12
12
|
|
|
13
13
|
def zscale(image, factor=0.25):
|
|
14
14
|
"""Compute z1 and z2 cuts in a similar way to Iraf.
|
|
15
|
-
|
|
16
|
-
If the total number of pixels is less than 10, the function simply
|
|
15
|
+
|
|
16
|
+
If the total number of pixels is less than 10, the function simply
|
|
17
17
|
returns the minimum and the maximum values.
|
|
18
18
|
|
|
19
19
|
Parameters
|
|
@@ -48,5 +48,5 @@ def zscale(image, factor=0.25):
|
|
|
48
48
|
z1 = max(z1, q000)
|
|
49
49
|
z2 = q500+(zslope*npixels/2)/factor
|
|
50
50
|
z2 = min(z2, q1000)
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
return z1, z2
|
|
@@ -3,6 +3,7 @@ README.md
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
src/teareduce/__init__.py
|
|
5
5
|
src/teareduce/avoid_astropy_warnings.py
|
|
6
|
+
src/teareduce/cleanest.py
|
|
6
7
|
src/teareduce/correct_pincushion_distortion.py
|
|
7
8
|
src/teareduce/cosmicrays.py
|
|
8
9
|
src/teareduce/ctext.py
|
|
@@ -25,6 +26,7 @@ src/teareduce/zscale.py
|
|
|
25
26
|
src/teareduce.egg-info/PKG-INFO
|
|
26
27
|
src/teareduce.egg-info/SOURCES.txt
|
|
27
28
|
src/teareduce.egg-info/dependency_links.txt
|
|
29
|
+
src/teareduce.egg-info/entry_points.txt
|
|
28
30
|
src/teareduce.egg-info/requires.txt
|
|
29
31
|
src/teareduce.egg-info/top_level.txt
|
|
30
32
|
src/teareduce/tests/__init__.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|