visualens 0.1.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.
- visualens/__init__.py +5 -0
- visualens/catalog.py +512 -0
- visualens/fits_image.py +604 -0
- visualens/gui.py +313 -0
- visualens/lenstool_model/im2source.py +145 -0
- visualens/lenstool_model/lenstool_model.py +800 -0
- visualens/lenstool_model/simulate_image/simulate_image.py +256 -0
- visualens/lenstool_model/simulate_image/utils.py +301 -0
- visualens/lenstool_model/utils/file_makers.py +204 -0
- visualens/lenstool_model/utils/lenstool_to_lenstronomy.py +102 -0
- visualens/lenstool_model/utils/operations.py +32 -0
- visualens/lenstool_model/utils/param_extractors.py +622 -0
- visualens/lenstool_model/utils/redshift_extractors.py +81 -0
- visualens/lenstool_model/utils/utils_general.py +617 -0
- visualens/main.py +19 -0
- visualens/sliders_TEST.py +213 -0
- visualens/source_extraction/source_extract.py +23 -0
- visualens/utils/__init__.py +0 -0
- visualens/utils/utils_Qt/drag_widgets.py +339 -0
- visualens/utils/utils_Qt/selectable_classes.py +268 -0
- visualens/utils/utils_Qt/utils_general.py +104 -0
- visualens/utils/utils_astro/cat_manip.py +293 -0
- visualens/utils/utils_astro/get_cosmology.py +13 -0
- visualens/utils/utils_astro/utils_general.py +205 -0
- visualens/utils/utils_astro/utils_lensing.py +16 -0
- visualens/utils/utils_fits/utils_fits_image.py +111 -0
- visualens/utils/utils_general/sort_points.py +293 -0
- visualens/utils/utils_general/utils_general.py +222 -0
- visualens/utils/utils_plots/plot_utils_general.py +230 -0
- visualens/utils/utils_plots/plot_utils_specific.py +48 -0
- visualens/utils/utils_plots/plt_framework.py +78 -0
- visualens-0.1.0.dist-info/METADATA +24 -0
- visualens-0.1.0.dist-info/RECORD +35 -0
- visualens-0.1.0.dist-info/WHEEL +5 -0
- visualens-0.1.0.dist-info/top_level.txt +1 -0
visualens/fits_image.py
ADDED
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import glob
|
|
4
|
+
import numpy as np
|
|
5
|
+
from matplotlib import pyplot as plt
|
|
6
|
+
from matplotlib.patches import Ellipse, Polygon, Circle, Rectangle
|
|
7
|
+
from astropy.io import fits
|
|
8
|
+
from astropy.wcs import WCS
|
|
9
|
+
import astropy.units as u
|
|
10
|
+
import astropy.constants as c
|
|
11
|
+
from reproject import reproject_interp
|
|
12
|
+
#from astropy.visualization.wcsaxes import *
|
|
13
|
+
from astropy.coordinates import SkyCoord
|
|
14
|
+
from tqdm import tqdm
|
|
15
|
+
from speclite import filters as specfilters
|
|
16
|
+
|
|
17
|
+
import PyQt5
|
|
18
|
+
from PyQt5.QtWidgets import QMainWindow, QSplitter
|
|
19
|
+
import pyqtgraph as pg
|
|
20
|
+
from PyQt5.QtCore import Qt
|
|
21
|
+
from astropy.table import Table
|
|
22
|
+
|
|
23
|
+
from .catalog import catalog
|
|
24
|
+
from .lenstool_model.lenstool_model import lenstool_model
|
|
25
|
+
from .source_extraction.source_extract import source_extract, source_extract_DIM
|
|
26
|
+
from .utils.utils_fits.utils_fits_image import open_image
|
|
27
|
+
from .utils.utils_plots.plot_utils_general import *
|
|
28
|
+
from .utils.utils_Qt.selectable_classes import *
|
|
29
|
+
from .utils.utils_Qt.drag_widgets import DragWidget
|
|
30
|
+
from .utils.utils_Qt.utils_general import *
|
|
31
|
+
from .utils.utils_astro.get_cosmology import get_cosmo
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
pg.setConfigOption('imageAxisOrder', 'row-major')
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class fits_image :
|
|
41
|
+
def __init__(self, image_path, main_window=None, plot_image=True) :
|
|
42
|
+
self.image_path = image_path
|
|
43
|
+
# If an external QMainWindow is provided (e.g. from the GUI), use it
|
|
44
|
+
# instead of spawning a new independent window.
|
|
45
|
+
self.main_window = main_window # type: ignore[assignment]
|
|
46
|
+
if os.path.isfile(self.image_path[:-8] + 'wht.fits') :
|
|
47
|
+
print("Weight file found: " + self.image_path[:-8] + 'wht.fits')
|
|
48
|
+
self.weight_path = self.image_path[:-8] + 'wht.fits'
|
|
49
|
+
else :
|
|
50
|
+
self.weight_path = None
|
|
51
|
+
self.image_data, self.pix_deg_scale, self.orientation, self.wcs, self.header = open_image(self.image_path)
|
|
52
|
+
self.sources = None
|
|
53
|
+
self.fig = None
|
|
54
|
+
self.ax = None
|
|
55
|
+
self.multiple_images = None
|
|
56
|
+
self.galaxy_selection = None
|
|
57
|
+
self.imported_cat = None
|
|
58
|
+
self.imported_cat_list = []
|
|
59
|
+
self.qt_image = None
|
|
60
|
+
self.qtItems_dict = {'sources': None,
|
|
61
|
+
'potfile_cat': None,
|
|
62
|
+
'imported_cat': None,
|
|
63
|
+
'multiple_images': None}
|
|
64
|
+
self.ax = None
|
|
65
|
+
self.redshift = None
|
|
66
|
+
self.boosted_image_path = self.image_path[:-5] + '_boosted.fits'
|
|
67
|
+
if os.path.isfile(self.boosted_image_path) :
|
|
68
|
+
print("Boosted image found: " + self.boosted_image_path)
|
|
69
|
+
self.boosted_image, _, _, _, _ = open_image(self.boosted_image_path)
|
|
70
|
+
else :
|
|
71
|
+
self.boosted_image = None
|
|
72
|
+
self.boosted = False
|
|
73
|
+
self.extra_qt_images = []
|
|
74
|
+
self.extra_windows = []
|
|
75
|
+
if self.orientation==None :
|
|
76
|
+
self.orientation = 0.
|
|
77
|
+
self.cosmo = get_cosmo()
|
|
78
|
+
|
|
79
|
+
if plot_image :
|
|
80
|
+
self.plot_image()
|
|
81
|
+
self.filters = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def set_cosmo(self, cosmo_name) :
|
|
85
|
+
self.cosmo = get_cosmo(cosmo_name)
|
|
86
|
+
|
|
87
|
+
def create_qt_image(self) :
|
|
88
|
+
to_plot = np.flip(self.image_data, axis=0) if not self.boosted else np.flip(self.boosted_image, axis=0)
|
|
89
|
+
|
|
90
|
+
#qt_image = pg.image(to_plot)
|
|
91
|
+
qt_image = pg.ImageView()
|
|
92
|
+
qt_image.setImage(to_plot)
|
|
93
|
+
#qt_image.autoLevels()
|
|
94
|
+
|
|
95
|
+
image_widget = DragWidget(qt_image)
|
|
96
|
+
|
|
97
|
+
qt_layout = QSplitter(Qt.Horizontal)
|
|
98
|
+
qt_layout.addWidget(image_widget)
|
|
99
|
+
|
|
100
|
+
if self.main_window is None:
|
|
101
|
+
window = QMainWindow()
|
|
102
|
+
window.setWindowTitle(os.path.basename(self.image_path))
|
|
103
|
+
window.setCentralWidget(qt_layout)
|
|
104
|
+
window.show()
|
|
105
|
+
else:
|
|
106
|
+
# Reuse the provided main window.
|
|
107
|
+
window = self.main_window
|
|
108
|
+
# Replace any existing central widget.
|
|
109
|
+
window.setCentralWidget(qt_layout)
|
|
110
|
+
window.setWindowTitle(os.path.basename(self.image_path))
|
|
111
|
+
# Ensure the window is visible (may already be shown).
|
|
112
|
+
window.show()
|
|
113
|
+
|
|
114
|
+
return qt_image, qt_layout, image_widget, window
|
|
115
|
+
|
|
116
|
+
def plot_image(self) :
|
|
117
|
+
#to_plot = self.image_data
|
|
118
|
+
#to_plot = np.transpose(self.image_data, axes=[1,0,2])
|
|
119
|
+
if self.qt_image is None or not self.qt_image.isVisible() :
|
|
120
|
+
print('Creating main window...')
|
|
121
|
+
self.qt_image, self.qt_layout, self.image_widget, self.window = self.create_qt_image()
|
|
122
|
+
print('Done')
|
|
123
|
+
to_return = self.qt_image
|
|
124
|
+
else :
|
|
125
|
+
print('Creating secondary window...')
|
|
126
|
+
extra_qt_image, _, _, extra_window = self.create_qt_image()
|
|
127
|
+
extra_window.setWindowTitle(os.path.basename(self.image_path) + ' (' + str(len(self.extra_qt_images)+2) + ')')
|
|
128
|
+
extra_window.show()
|
|
129
|
+
self.extra_qt_images.append(extra_qt_image)
|
|
130
|
+
self.extra_windows.append(extra_window)
|
|
131
|
+
print('Done')
|
|
132
|
+
to_return = extra_qt_image
|
|
133
|
+
return to_return
|
|
134
|
+
|
|
135
|
+
def boost(self, boost=[2,1.5,1]) :
|
|
136
|
+
if self.boosted_image is None :
|
|
137
|
+
print('Adjusting contrast...')
|
|
138
|
+
adjusted_image = adjust_contrast(self.image_data, boost[0], pivot=boost[1])
|
|
139
|
+
print('Adjusting luminosity...')
|
|
140
|
+
self.boosted_image = adjust_luminosity(adjusted_image, boost[2])
|
|
141
|
+
print('Writing to memory...')
|
|
142
|
+
hdul = fits.HDUList([fits.PrimaryHDU()] + [ fits.ImageHDU(data=self.boosted_image[:,:,i], header=self.wcs.to_header(), name=name) for i, name in enumerate(['RED','GREEN','BLUE']) ])
|
|
143
|
+
hdul.writeto(self.boosted_image_path, overwrite=True)
|
|
144
|
+
if not self.boosted :
|
|
145
|
+
print('Plotting...')
|
|
146
|
+
self.qt_image.setImage(np.flip(self.boosted_image, axis=0))
|
|
147
|
+
#self.qt_image.autoLevels()
|
|
148
|
+
self.boosted = True
|
|
149
|
+
for extra_qt_image in self.extra_qt_images :
|
|
150
|
+
extra_qt_image.setImage(np.flip(self.boosted_image, axis=0))
|
|
151
|
+
print('Done')
|
|
152
|
+
|
|
153
|
+
def unboost(self) :
|
|
154
|
+
if self.boosted :
|
|
155
|
+
print('Plotting...')
|
|
156
|
+
self.qt_image.setImage(np.flip(self.image_data, axis=0))
|
|
157
|
+
#self.qt_image.autoLevels()
|
|
158
|
+
self.boosted = False
|
|
159
|
+
for extra_qt_image in self.extra_qt_images :
|
|
160
|
+
extra_qt_image.setImage(np.flip(self.image_data, axis=0))
|
|
161
|
+
print('Done')
|
|
162
|
+
|
|
163
|
+
def set_weight(self, weight_path) :
|
|
164
|
+
self.weight_path = weight_path
|
|
165
|
+
|
|
166
|
+
def extract_sources(self, image_path=None, weight_path=None, DIM_ref_path=None, rerun=False, reproject=True) :
|
|
167
|
+
print("Source extraction not yet available. Work in progress.")
|
|
168
|
+
if False :
|
|
169
|
+
if image_path is None :
|
|
170
|
+
image_path = self.image_path
|
|
171
|
+
weight_path = self.weight_path
|
|
172
|
+
|
|
173
|
+
out_dir = os.path.join( os.path.dirname(image_path), 'source_extraction/' )
|
|
174
|
+
if not os.path.exists(out_dir) :
|
|
175
|
+
os.mkdir(out_dir)
|
|
176
|
+
|
|
177
|
+
outfile_name = 'SExtractor_cat.fits' if DIM_ref_path is None else 'SExtractor_cat_DIM.fits'
|
|
178
|
+
out_path = os.path.join( out_dir, outfile_name )
|
|
179
|
+
if os.path.isfile(out_path) and not rerun :
|
|
180
|
+
print('Previous SExtractor catalog found.')
|
|
181
|
+
with fits.open(out_path) as hdu :
|
|
182
|
+
self.sources_all = Table(hdu[1].data)
|
|
183
|
+
elif DIM_ref_path is None :
|
|
184
|
+
self.sources_all = source_extract(image_path, weight_path=weight_path, pixel_scale=self.pix_deg_scale*3600, zero_point=None, out_dir=out_dir,
|
|
185
|
+
outfile_name=outfile_name, return_sources=True)
|
|
186
|
+
else :
|
|
187
|
+
if type(DIM_ref_path) is list :
|
|
188
|
+
if reproject :
|
|
189
|
+
reprojected_image_path = self.reproject(DIM_ref_path[0], image_path)
|
|
190
|
+
reprojected_weight_path = self.reproject(DIM_ref_path[0], weight_path)
|
|
191
|
+
reprojected_image_path = [reprojected_image_path, reprojected_weight_path]
|
|
192
|
+
else :
|
|
193
|
+
reprojected_image_path = [image_path, weight_path]
|
|
194
|
+
else :
|
|
195
|
+
if reproject :
|
|
196
|
+
reprojected_image_path = self.reproject(DIM_ref_path, image_path)
|
|
197
|
+
else :
|
|
198
|
+
reprojected_image_path = image_path
|
|
199
|
+
|
|
200
|
+
self.sources_all = source_extract_DIM(DIM_ref_path, reprojected_image_path, pixel_scale=self.pix_deg_scale*3600, zero_point=None, out_dir=out_dir,
|
|
201
|
+
outfile_name=outfile_name, return_sources=True)
|
|
202
|
+
|
|
203
|
+
# This next part doesn't make sense here as the purpose of extract_sources() is to extract sources from the imported image,
|
|
204
|
+
# but the code should be added to import_cat()/make_catalog()
|
|
205
|
+
# THE WAY TO CONVERT ANGLES IS MORE COMPLICATED!!!
|
|
206
|
+
x, y = self.world_to_image(self.sources_all['RA'], self.sources_all['DEC'], unit='deg')
|
|
207
|
+
if x[0] != self.sources_all['X_IMAGE'][0] :
|
|
208
|
+
#print('Catalog sextracted from different image: replacing X_IMAGE, Y_IMAGE and THETA_IMAGE columns with current image coordinates.')
|
|
209
|
+
#self.sources_all['X_IMAGE'], self.sources_all['Y_IMAGE'] = x, y
|
|
210
|
+
print('Catalog SExtracted from different image: keeping X_IMAGE, Y_IMAGE and THETA_IMAGE and adding x, y, theta columns from current image coordinates.')
|
|
211
|
+
self.sources_all.add_column(x, name='x')
|
|
212
|
+
self.sources_all.add_column(y, name='y')
|
|
213
|
+
ref_image_angle = ( np.arctan2(self.wcs.wcs.get_pc()[1, 0], self.wcs.wcs.get_pc()[0, 0]) %np.pi ) * 360/np.pi
|
|
214
|
+
print('Overall angle of imported image: ' + str(ref_image_angle))
|
|
215
|
+
#self.sources_all.add_column(self.sources_all['THETA_WORLD'] + ref_image_angle, name='prout')
|
|
216
|
+
|
|
217
|
+
#mask_mag = self.sources_all['MAG_AUTO']<-10.
|
|
218
|
+
#mask = self.sources['KRON_RADIUS']
|
|
219
|
+
#mask_galstar = self.sources_all['CLASS_STAR']<0.4
|
|
220
|
+
#mask_size = self.sources_all['A_IMAGE']*self.sources_all['B_IMAGE']*np.pi>1000.
|
|
221
|
+
#mask = mask_mag & mask_galstar & mask_size
|
|
222
|
+
|
|
223
|
+
#self.sources = self.sources_all #[mask]
|
|
224
|
+
self.make_photometry(self.sources_all)
|
|
225
|
+
self.sources = self.make_catalog(self.sources_all)
|
|
226
|
+
return str(len(self.sources.cat)) + ' sources found.'
|
|
227
|
+
|
|
228
|
+
def reproject(self, ref_image_path, image_path) :
|
|
229
|
+
reprojected_image_path = image_path[:-len('.fits')] + '_reprojected.fits'
|
|
230
|
+
if os.path.isfile(reprojected_image_path) :
|
|
231
|
+
print('Previous reprojected image found.')
|
|
232
|
+
else :
|
|
233
|
+
with fits.open(ref_image_path) as hdu :
|
|
234
|
+
reference_header = hdu[0].header
|
|
235
|
+
with fits.open(image_path) as hdu :
|
|
236
|
+
print('Reprojecting image ' + image_path + ' onto reference ' + ref_image_path)
|
|
237
|
+
reprojected_data, footprint = reproject_interp(hdu[0], reference_header)
|
|
238
|
+
fits.writeto(reprojected_image_path, reprojected_data, reference_header)
|
|
239
|
+
return reprojected_image_path
|
|
240
|
+
|
|
241
|
+
def make_photometry(self, cat) :
|
|
242
|
+
print("################")
|
|
243
|
+
print("Figuring out the photometry:")
|
|
244
|
+
print("Assuming instrument HST/ACS")
|
|
245
|
+
print("PHOTFLAM = " + str(self.header['PHOTFLAM']))
|
|
246
|
+
print("Pivot wavelength = " + str(self.header['PHOTPLAM']))
|
|
247
|
+
print("################")
|
|
248
|
+
flux_lambda = self.header['PHOTFLAM'] * cat['FLUX_ISO'] * u.erg/u.cm**2/u.s/u.AA #FLUX_AUTO, FLUX_ISO, FLUX_APER
|
|
249
|
+
magST = -2.5*np.log10(flux_lambda.value) - 21.1
|
|
250
|
+
pivot_wavelength = self.header['PHOTPLAM'] * u.AA
|
|
251
|
+
flux_nu = flux_lambda.to(u.erg/u.cm**2/u.s/u.Hz, u.spectral_density(pivot_wavelength))
|
|
252
|
+
magAB = -2.5*np.log10(flux_nu.value) - 48.6
|
|
253
|
+
if 'FILTER2' in self.header.keys() :
|
|
254
|
+
print("filter " + self.header['FILTER2'] + " found")
|
|
255
|
+
cat.add_column(magAB, name='magAB_' + self.header['FILTER2'])
|
|
256
|
+
cat.add_column(magST, name='magST_' + self.header['FILTER2'])
|
|
257
|
+
else :
|
|
258
|
+
cat.add_column(magAB, name='magAB')
|
|
259
|
+
cat.add_column(magST, name='magST')
|
|
260
|
+
return 'Magnitudes calculated'
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def make_catalog(self, cat, color=[1., 1., 0], mag_colnames=['magAB_F814W', 'magAB_F435W'], units=None) :
|
|
264
|
+
if self.qt_image is None :
|
|
265
|
+
self.plot_image()
|
|
266
|
+
#to_return = catalog(cat, self.image_data, self.wcs, self.qt_image, window=self.window, image_path=self.image_path,
|
|
267
|
+
# image_widget = self.image_widget, qt_layout=self.qt_layout, color=color,
|
|
268
|
+
# mag_colnames=mag_colnames, mpl_fig=self.fig, mpl_ax=self.ax,
|
|
269
|
+
# pix_deg_scale=self.pix_deg_scale, units=units)
|
|
270
|
+
to_return = catalog(cat, self, color=color, mag_colnames=mag_colnames, units=units)
|
|
271
|
+
return to_return
|
|
272
|
+
|
|
273
|
+
def import_catalog(self, cat, color=None, mag_colnames=['magAB_F814W', 'magAB_F435W'], units='pixel') :
|
|
274
|
+
self.imported_cat = self.make_catalog(cat, color=color, mag_colnames=mag_colnames, units=units)
|
|
275
|
+
self.imported_cat_list.append(self.imported_cat)
|
|
276
|
+
|
|
277
|
+
###########################################################################
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def world_to_image(self, ra, dec, unit='deg') :
|
|
281
|
+
coord = SkyCoord(ra, dec, unit=unit)
|
|
282
|
+
image_coord = WCS.world_to_pixel(self.wcs, coord)
|
|
283
|
+
if len(image_coord[0].shape)==0 :
|
|
284
|
+
image_coord = (image_coord[0]*1., image_coord[1]*1.)
|
|
285
|
+
return image_coord
|
|
286
|
+
|
|
287
|
+
def image_to_world(self, x, y, unit='deg') :
|
|
288
|
+
world_coord = WCS.pixel_to_world(self.wcs, x, y)
|
|
289
|
+
return world_coord.ra.deg, world_coord.dec.deg
|
|
290
|
+
|
|
291
|
+
def clear_Items(self) :
|
|
292
|
+
for key in self.qtItems_dict.keys() :
|
|
293
|
+
if self.qtItems_dict[key] is not None :
|
|
294
|
+
for i in tqdm( range(len(self.qtItems_dict[key])) ) :
|
|
295
|
+
self.qt_image.removeItem(self.qtItems_dict[key][i])
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def import_lenstool(self, model_dir, use_wrapper=None) :
|
|
300
|
+
#self.lt_dir = model_dir
|
|
301
|
+
#if hasattr(self, 'lt'):
|
|
302
|
+
# del self.lt
|
|
303
|
+
self.lt = lenstool_model(model_dir, self, use_wrapper=use_wrapper)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def start_hand_select(self) :
|
|
308
|
+
cat_dict = {'id': [], 'ra': [], 'dec': [], 'x': [], 'y': [], 'a': [], 'b': [], 'theta': []}
|
|
309
|
+
cat = Table(cat_dict)
|
|
310
|
+
self.hand_made_cat = catalog(cat, self, units='pixel')
|
|
311
|
+
|
|
312
|
+
def mouse_clicked(evt):
|
|
313
|
+
if evt.double():
|
|
314
|
+
pos = evt.scenePos()
|
|
315
|
+
if self.qt_image.getView().sceneBoundingRect().contains(pos):
|
|
316
|
+
mouse_point = self.qt_image.getView().mapSceneToView(pos)
|
|
317
|
+
x, y_flipped = mouse_point.x(), mouse_point.y()
|
|
318
|
+
x, y = x, self.image_data.shape[0] - y_flipped
|
|
319
|
+
ra, dec = self.image_to_world(x, y)
|
|
320
|
+
self.hand_made_cat.cat.add_row( {'id': [len(self.hand_made_cat.cat) + 1], 'ra': [ra], 'dec': [dec], 'x': [x], 'y': [y], 'a': [10], 'b': [10], 'theta': [0]} )
|
|
321
|
+
self.hand_made_cat.qtItems.append(PyQt5.QtWidgets.QGraphicsEllipseItem())
|
|
322
|
+
self.hand_made_cat.plot(color=[1,1,1,0])
|
|
323
|
+
|
|
324
|
+
self._doubleclick_connection = self.qt_image.scene.sigMouseClicked.connect(mouse_clicked)
|
|
325
|
+
|
|
326
|
+
def keyPressEvent(event):
|
|
327
|
+
#print('Hand selection stopped.')
|
|
328
|
+
if event.key() == Qt.Key_Escape or event.key() == Qt.Key_Space :
|
|
329
|
+
if hasattr(self, '_doubleclick_connection'):
|
|
330
|
+
self.qt_image.scene.sigMouseClicked.disconnect(self._doubleclick_connection)
|
|
331
|
+
del self._doubleclick_connection
|
|
332
|
+
self.hand_made_cat.clear()
|
|
333
|
+
self.window.keyPressEvent = self._original_keyPressEvent
|
|
334
|
+
print('Hand selection stopped.')
|
|
335
|
+
|
|
336
|
+
self._original_keyPressEvent = self.window.keyPressEvent
|
|
337
|
+
self.window.keyPressEvent = keyPressEvent
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def plot_image_mpl(self, wcs_projection=True, units='pixel', pos=111, make_axes_labels=True, make_grid=True, crop=None, replace_image=True, extra_pad=None) :
|
|
342
|
+
fig, ax = plot_image_mpl(self.image_data, wcs=self.wcs, wcs_projection=wcs_projection, units=units, pos=pos, \
|
|
343
|
+
make_axes_labels=make_axes_labels, make_grid=make_grid, crop=crop, extra_pad=extra_pad)
|
|
344
|
+
plot_NE_arrows(ax, self.wcs)
|
|
345
|
+
if replace_image :
|
|
346
|
+
self.fig, self.ax = fig, ax
|
|
347
|
+
return fig, ax
|
|
348
|
+
|
|
349
|
+
def plot_sub_region(self, ra, dec, size=3):
|
|
350
|
+
"""
|
|
351
|
+
Plots a square region around given RA and Dec coordinates.
|
|
352
|
+
|
|
353
|
+
Parameters:
|
|
354
|
+
ra (float): Right Ascension of the center in degrees.
|
|
355
|
+
dec (float): Declination of the center in degrees.
|
|
356
|
+
size (float): Size of the square region in arcseconds (default is 10).
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
x_center, y_center = self.world_to_image(ra, dec, unit='deg')
|
|
360
|
+
|
|
361
|
+
size_pix = int( size / (self.pix_deg_scale*3600) / 2 )
|
|
362
|
+
|
|
363
|
+
fig, axs = plt.subplots(1,3)
|
|
364
|
+
for i in range(len(x_center)) :
|
|
365
|
+
x_min = int(x_center[i]) - size_pix
|
|
366
|
+
x_max = int(x_center[i]) + size_pix
|
|
367
|
+
y_min = int(y_center[i]) - size_pix
|
|
368
|
+
y_max = int(y_center[i]) + size_pix
|
|
369
|
+
region = self.image_data[y_min:y_max, x_min:x_max, :]
|
|
370
|
+
axs[i].imshow(region, origin='lower')
|
|
371
|
+
axs[i].axis('off')
|
|
372
|
+
|
|
373
|
+
return fig, axs
|
|
374
|
+
|
|
375
|
+
def load_filters(self, filter_dir=None):
|
|
376
|
+
if filter_dir==None :
|
|
377
|
+
listdir = os.listdir( os.path.dirname(self.image_path) )
|
|
378
|
+
listdir_lower = [ name.lower() for name in os.listdir( os.path.dirname(self.image_path) ) ]
|
|
379
|
+
indices = np.where([ 'filter' in name for name in listdir_lower ])[0]
|
|
380
|
+
filters = []
|
|
381
|
+
if len(indices)>0 :
|
|
382
|
+
for i in indices :
|
|
383
|
+
filter_dir = os.path.join(os.path.dirname(self.image_path), listdir[i])
|
|
384
|
+
print('Looking for filters in directory: ' + filter_dir)
|
|
385
|
+
f = load_filters(filter_dir)
|
|
386
|
+
filters.append(f)
|
|
387
|
+
l = np.array( [ len(f) for f in filters ] )
|
|
388
|
+
idx = np.argmax(l)
|
|
389
|
+
self.filters = filters[idx]
|
|
390
|
+
print( str(len(self.filters)) + ' filters loaded from directory: ' + listdir[indices[idx]] )
|
|
391
|
+
else :
|
|
392
|
+
raise ValueError('No filter directory found in image directory.')
|
|
393
|
+
else :
|
|
394
|
+
self.filters = load_filters(filter_dir)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
class FilterImage(fits_image) :
|
|
401
|
+
def __init__(self, image_path, main_window=None, plot_image=False) :
|
|
402
|
+
super().__init__(image_path, main_window=main_window, plot_image=plot_image)
|
|
403
|
+
self.filter = self._get_filter_name()
|
|
404
|
+
self.wavelength = self._get_pivot_wavelength()
|
|
405
|
+
psf_path = self._get_psf_path()
|
|
406
|
+
if psf_path is not None :
|
|
407
|
+
self.psf = PSF(psf_path)
|
|
408
|
+
else :
|
|
409
|
+
self.psf = None
|
|
410
|
+
|
|
411
|
+
# ------------------ internal methods ------------------ #
|
|
412
|
+
|
|
413
|
+
def _get_filter_name(self):
|
|
414
|
+
"""Extract filter name from header or filename."""
|
|
415
|
+
# Try header first
|
|
416
|
+
for key in ['FILTER', 'FILTER1', 'FILTER2']:
|
|
417
|
+
if key in self.header and isinstance(self.header[key], str):
|
|
418
|
+
val = self.header[key].strip().upper()
|
|
419
|
+
if re.match(r'F\d{3,4}[WMN]*', val):
|
|
420
|
+
return val
|
|
421
|
+
|
|
422
|
+
# Try filename
|
|
423
|
+
m = re.search(r'F\d{3,4}[WMN]*', os.path.basename(self.path).upper())
|
|
424
|
+
if m:
|
|
425
|
+
return m.group(0)
|
|
426
|
+
|
|
427
|
+
print(f"Could not determine filter name for {os.path.basename(self.path)}.")
|
|
428
|
+
return "UNKNOWN"
|
|
429
|
+
|
|
430
|
+
def _get_pivot_wavelength(self):
|
|
431
|
+
"""Find pivot/effective wavelength using speclite or FITS header."""
|
|
432
|
+
lam = None
|
|
433
|
+
f_lower = self.filter.lower()
|
|
434
|
+
|
|
435
|
+
for prefix in ['jwst_nircam_', 'jwst_miri_', 'jwst_niriss_',
|
|
436
|
+
'hst_acs_', 'hst_wfc3_', 'hst_wfpc2_']:
|
|
437
|
+
try:
|
|
438
|
+
fobj = specfilters.load_filter(prefix + f_lower)
|
|
439
|
+
lam = fobj.effective_wavelength.value
|
|
440
|
+
break
|
|
441
|
+
except Exception:
|
|
442
|
+
continue
|
|
443
|
+
|
|
444
|
+
# Fallback: header info
|
|
445
|
+
if lam is None:
|
|
446
|
+
for key in ['PHOTPLAM', 'PIVOTWL', 'WAVELENGTH']:
|
|
447
|
+
if key in self.header:
|
|
448
|
+
lam = self.header[key]
|
|
449
|
+
break
|
|
450
|
+
|
|
451
|
+
if lam is None:
|
|
452
|
+
lam = np.nan
|
|
453
|
+
print(f"Could not determine wavelength for {self.filter} ({self.path}).")
|
|
454
|
+
|
|
455
|
+
return lam
|
|
456
|
+
|
|
457
|
+
def _get_psf_path(self):
|
|
458
|
+
# Search first in parent of image dir, then in image dir
|
|
459
|
+
search_dirs = [
|
|
460
|
+
os.path.dirname(os.path.dirname(self.image_path)),
|
|
461
|
+
os.path.dirname(self.image_path)
|
|
462
|
+
]
|
|
463
|
+
|
|
464
|
+
for base in search_dirs:
|
|
465
|
+
if not os.path.isdir(base):
|
|
466
|
+
continue
|
|
467
|
+
try:
|
|
468
|
+
listdir = os.listdir(base)
|
|
469
|
+
except Exception:
|
|
470
|
+
continue
|
|
471
|
+
|
|
472
|
+
listdir_lower = [name.lower() for name in listdir]
|
|
473
|
+
indices = [i for i, name in enumerate(listdir_lower) if 'psf' in name]
|
|
474
|
+
if len(indices) == 0:
|
|
475
|
+
continue
|
|
476
|
+
|
|
477
|
+
i = indices[0]
|
|
478
|
+
psf_dir = os.path.join(base, listdir[i])
|
|
479
|
+
print('Looking for psf in directory: ' + psf_dir)
|
|
480
|
+
possible_psf_files = []
|
|
481
|
+
for f in glob.glob(os.path.join(psf_dir, '*.fits')):
|
|
482
|
+
if self.filter.lower() in os.path.basename(f).lower():
|
|
483
|
+
possible_psf_files.append(f)
|
|
484
|
+
if len(possible_psf_files) > 0:
|
|
485
|
+
print(f"PSF file found for filter {self.filter}: {os.path.basename(possible_psf_files[0])}")
|
|
486
|
+
return possible_psf_files[0]
|
|
487
|
+
else:
|
|
488
|
+
print(f"No PSF file found for filter {self.filter} in {psf_dir}.")
|
|
489
|
+
|
|
490
|
+
return None
|
|
491
|
+
|
|
492
|
+
# ------------------ user-facing methods ------------------ #
|
|
493
|
+
|
|
494
|
+
def plot_throughput(self):
|
|
495
|
+
"""Plot the filter's transmission curve using speclite."""
|
|
496
|
+
f_lower = self.filter.lower()
|
|
497
|
+
for prefix in ['jwst_nircam_', 'jwst_miri_', 'jwst_niriss_',
|
|
498
|
+
'hst_acs_', 'hst_wfc3_', 'hst_wfpc2_']:
|
|
499
|
+
try:
|
|
500
|
+
fobj = specfilters.load_filter(prefix + f_lower)
|
|
501
|
+
lam = fobj.wavelength.value
|
|
502
|
+
trans = fobj.throughput
|
|
503
|
+
plt.figure(figsize=(6, 4))
|
|
504
|
+
plt.plot(lam, trans, label=f"{self.filter}")
|
|
505
|
+
plt.xlabel("Wavelength [Å]")
|
|
506
|
+
plt.ylabel("Throughput")
|
|
507
|
+
plt.title(f"Filter Throughput: {self.filter}")
|
|
508
|
+
plt.grid(True, alpha=0.3)
|
|
509
|
+
plt.legend()
|
|
510
|
+
plt.tight_layout()
|
|
511
|
+
plt.show()
|
|
512
|
+
return
|
|
513
|
+
except Exception:
|
|
514
|
+
continue
|
|
515
|
+
|
|
516
|
+
print(f"Could not plot throughput for {self.filter} (not found in speclite).")
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def load_filters(path):
|
|
522
|
+
"""
|
|
523
|
+
Load all FITS files in `path` and return a dict {filter_name: FilterImage},
|
|
524
|
+
ordered by increasing pivot wavelength.
|
|
525
|
+
If multiple files correspond to the same filter, selects the one most likely
|
|
526
|
+
to be the science image (e.g., containing 'SCI' in header or filename).
|
|
527
|
+
"""
|
|
528
|
+
fits_files = [f for f in os.listdir(path) if f.lower().endswith('.fits')]
|
|
529
|
+
filter_groups = {}
|
|
530
|
+
|
|
531
|
+
# --- Group files by detected filter name ---
|
|
532
|
+
for fname in fits_files:
|
|
533
|
+
full_path = os.path.join(path, fname)
|
|
534
|
+
try:
|
|
535
|
+
# Just read header to find filter (faster)
|
|
536
|
+
with fits.open(full_path) as hdul:
|
|
537
|
+
hdr = hdul[0].header
|
|
538
|
+
filt = None
|
|
539
|
+
for key in ['FILTER', 'FILTER1', 'FILTER2']:
|
|
540
|
+
if key in hdr and isinstance(hdr[key], str):
|
|
541
|
+
val = hdr[key].strip().upper()
|
|
542
|
+
if re.match(r'F\d{3,4}[WMN]*', val):
|
|
543
|
+
filt = val
|
|
544
|
+
break
|
|
545
|
+
if filt is None:
|
|
546
|
+
m = re.search(r'F\d{3,4}[WMN]*', fname.upper())
|
|
547
|
+
if m:
|
|
548
|
+
filt = m.group(0)
|
|
549
|
+
except Exception:
|
|
550
|
+
print(f"Could not read header for {fname}. Skipping.")
|
|
551
|
+
continue
|
|
552
|
+
|
|
553
|
+
if filt is None:
|
|
554
|
+
continue
|
|
555
|
+
|
|
556
|
+
filter_groups.setdefault(filt, []).append(full_path)
|
|
557
|
+
|
|
558
|
+
# --- For each filter, pick best file if multiple ---
|
|
559
|
+
selected_files = {}
|
|
560
|
+
for filt, files in filter_groups.items():
|
|
561
|
+
if len(files) == 1:
|
|
562
|
+
selected_files[filt] = files[0]
|
|
563
|
+
else:
|
|
564
|
+
sci_candidates = []
|
|
565
|
+
for f in files:
|
|
566
|
+
try:
|
|
567
|
+
with fits.open(f) as hdul:
|
|
568
|
+
hdr = hdul[0].header
|
|
569
|
+
for key in ['EXTNAME', 'FILETYPE', 'IMAGETYP']:
|
|
570
|
+
if key in hdr and 'SCI' in str(hdr[key]).upper():
|
|
571
|
+
sci_candidates.append(f)
|
|
572
|
+
break
|
|
573
|
+
if 'SCI' in os.path.basename(f).upper():
|
|
574
|
+
sci_candidates.append(f)
|
|
575
|
+
except Exception:
|
|
576
|
+
continue
|
|
577
|
+
|
|
578
|
+
if sci_candidates:
|
|
579
|
+
selected_files[filt] = sci_candidates[0]
|
|
580
|
+
print(f"Selected {os.path.basename(sci_candidates[0])} for {filt}")
|
|
581
|
+
else:
|
|
582
|
+
print(f"Multiple files for {filt}, picking first arbitrarily.")
|
|
583
|
+
selected_files[filt] = files[0]
|
|
584
|
+
|
|
585
|
+
# --- Create FilterImage instances ---
|
|
586
|
+
filters_list = [FilterImage(fpath, plot_image=False) for fpath in selected_files.values()]
|
|
587
|
+
filters_list.sort(key=lambda f: np.inf if np.isnan(f.wavelength) else f.wavelength)
|
|
588
|
+
|
|
589
|
+
return {f.filter: f for f in filters_list}
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
class PSF :
|
|
597
|
+
def __init__(self, psf_path) :
|
|
598
|
+
with fits.open(psf_path) as hdu :
|
|
599
|
+
self.data = hdu[0].data
|
|
600
|
+
self.wcs = WCS(hdu[0].header)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
|