setiastrosuitepro 1.6.1__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.
Potentially problematic release.
This version of setiastrosuitepro might be problematic. Click here for more details.
- setiastro/__init__.py +2 -0
- setiastro/data/SASP_data.fits +0 -0
- setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
- setiastro/data/catalogs/astrobin_filters.csv +890 -0
- setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
- setiastro/data/catalogs/cali2.csv +63 -0
- setiastro/data/catalogs/cali2color.csv +65 -0
- setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
- setiastro/data/catalogs/celestial_catalog.csv +24031 -0
- setiastro/data/catalogs/detected_stars.csv +24784 -0
- setiastro/data/catalogs/fits_header_data.csv +46 -0
- setiastro/data/catalogs/test.csv +8 -0
- setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
- setiastro/images/Astro_Spikes.png +0 -0
- setiastro/images/HRDiagram.png +0 -0
- setiastro/images/LExtract.png +0 -0
- setiastro/images/LInsert.png +0 -0
- setiastro/images/Oxygenation-atm-2.svg.png +0 -0
- setiastro/images/RGB080604.png +0 -0
- setiastro/images/abeicon.png +0 -0
- setiastro/images/aberration.png +0 -0
- setiastro/images/andromedatry.png +0 -0
- setiastro/images/andromedatry_satellited.png +0 -0
- setiastro/images/annotated.png +0 -0
- setiastro/images/aperture.png +0 -0
- setiastro/images/astrosuite.ico +0 -0
- setiastro/images/astrosuite.png +0 -0
- setiastro/images/astrosuitepro.icns +0 -0
- setiastro/images/astrosuitepro.ico +0 -0
- setiastro/images/astrosuitepro.png +0 -0
- setiastro/images/background.png +0 -0
- setiastro/images/background2.png +0 -0
- setiastro/images/benchmark.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
- setiastro/images/blaster.png +0 -0
- setiastro/images/blink.png +0 -0
- setiastro/images/clahe.png +0 -0
- setiastro/images/collage.png +0 -0
- setiastro/images/colorwheel.png +0 -0
- setiastro/images/contsub.png +0 -0
- setiastro/images/convo.png +0 -0
- setiastro/images/copyslot.png +0 -0
- setiastro/images/cosmic.png +0 -0
- setiastro/images/cosmicsat.png +0 -0
- setiastro/images/crop1.png +0 -0
- setiastro/images/cropicon.png +0 -0
- setiastro/images/curves.png +0 -0
- setiastro/images/cvs.png +0 -0
- setiastro/images/debayer.png +0 -0
- setiastro/images/denoise_cnn_custom.png +0 -0
- setiastro/images/denoise_cnn_graph.png +0 -0
- setiastro/images/disk.png +0 -0
- setiastro/images/dse.png +0 -0
- setiastro/images/exoicon.png +0 -0
- setiastro/images/eye.png +0 -0
- setiastro/images/fliphorizontal.png +0 -0
- setiastro/images/flipvertical.png +0 -0
- setiastro/images/font.png +0 -0
- setiastro/images/freqsep.png +0 -0
- setiastro/images/functionbundle.png +0 -0
- setiastro/images/graxpert.png +0 -0
- setiastro/images/green.png +0 -0
- setiastro/images/gridicon.png +0 -0
- setiastro/images/halo.png +0 -0
- setiastro/images/hdr.png +0 -0
- setiastro/images/histogram.png +0 -0
- setiastro/images/hubble.png +0 -0
- setiastro/images/imagecombine.png +0 -0
- setiastro/images/invert.png +0 -0
- setiastro/images/isophote.png +0 -0
- setiastro/images/isophote_demo_figure.png +0 -0
- setiastro/images/isophote_demo_image.png +0 -0
- setiastro/images/isophote_demo_model.png +0 -0
- setiastro/images/isophote_demo_residual.png +0 -0
- setiastro/images/jwstpupil.png +0 -0
- setiastro/images/linearfit.png +0 -0
- setiastro/images/livestacking.png +0 -0
- setiastro/images/mask.png +0 -0
- setiastro/images/maskapply.png +0 -0
- setiastro/images/maskcreate.png +0 -0
- setiastro/images/maskremove.png +0 -0
- setiastro/images/morpho.png +0 -0
- setiastro/images/mosaic.png +0 -0
- setiastro/images/multiscale_decomp.png +0 -0
- setiastro/images/nbtorgb.png +0 -0
- setiastro/images/neutral.png +0 -0
- setiastro/images/nuke.png +0 -0
- setiastro/images/openfile.png +0 -0
- setiastro/images/pedestal.png +0 -0
- setiastro/images/pen.png +0 -0
- setiastro/images/pixelmath.png +0 -0
- setiastro/images/platesolve.png +0 -0
- setiastro/images/ppp.png +0 -0
- setiastro/images/pro.png +0 -0
- setiastro/images/project.png +0 -0
- setiastro/images/psf.png +0 -0
- setiastro/images/redo.png +0 -0
- setiastro/images/redoicon.png +0 -0
- setiastro/images/rescale.png +0 -0
- setiastro/images/rgbalign.png +0 -0
- setiastro/images/rgbcombo.png +0 -0
- setiastro/images/rgbextract.png +0 -0
- setiastro/images/rotate180.png +0 -0
- setiastro/images/rotateclockwise.png +0 -0
- setiastro/images/rotatecounterclockwise.png +0 -0
- setiastro/images/satellite.png +0 -0
- setiastro/images/script.png +0 -0
- setiastro/images/selectivecolor.png +0 -0
- setiastro/images/simbad.png +0 -0
- setiastro/images/slot0.png +0 -0
- setiastro/images/slot1.png +0 -0
- setiastro/images/slot2.png +0 -0
- setiastro/images/slot3.png +0 -0
- setiastro/images/slot4.png +0 -0
- setiastro/images/slot5.png +0 -0
- setiastro/images/slot6.png +0 -0
- setiastro/images/slot7.png +0 -0
- setiastro/images/slot8.png +0 -0
- setiastro/images/slot9.png +0 -0
- setiastro/images/spcc.png +0 -0
- setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
- setiastro/images/spinner.gif +0 -0
- setiastro/images/stacking.png +0 -0
- setiastro/images/staradd.png +0 -0
- setiastro/images/staralign.png +0 -0
- setiastro/images/starnet.png +0 -0
- setiastro/images/starregistration.png +0 -0
- setiastro/images/starspike.png +0 -0
- setiastro/images/starstretch.png +0 -0
- setiastro/images/statstretch.png +0 -0
- setiastro/images/supernova.png +0 -0
- setiastro/images/uhs.png +0 -0
- setiastro/images/undoicon.png +0 -0
- setiastro/images/upscale.png +0 -0
- setiastro/images/viewbundle.png +0 -0
- setiastro/images/whitebalance.png +0 -0
- setiastro/images/wimi_icon_256x256.png +0 -0
- setiastro/images/wimilogo.png +0 -0
- setiastro/images/wims.png +0 -0
- setiastro/images/wrench_icon.png +0 -0
- setiastro/images/xisfliberator.png +0 -0
- setiastro/saspro/__init__.py +20 -0
- setiastro/saspro/__main__.py +809 -0
- setiastro/saspro/_generated/__init__.py +7 -0
- setiastro/saspro/_generated/build_info.py +2 -0
- setiastro/saspro/abe.py +1295 -0
- setiastro/saspro/abe_preset.py +196 -0
- setiastro/saspro/aberration_ai.py +694 -0
- setiastro/saspro/aberration_ai_preset.py +224 -0
- setiastro/saspro/accel_installer.py +218 -0
- setiastro/saspro/accel_workers.py +30 -0
- setiastro/saspro/add_stars.py +621 -0
- setiastro/saspro/astrobin_exporter.py +1007 -0
- setiastro/saspro/astrospike.py +153 -0
- setiastro/saspro/astrospike_python.py +1839 -0
- setiastro/saspro/autostretch.py +196 -0
- setiastro/saspro/backgroundneutral.py +560 -0
- setiastro/saspro/batch_convert.py +325 -0
- setiastro/saspro/batch_renamer.py +519 -0
- setiastro/saspro/blemish_blaster.py +488 -0
- setiastro/saspro/blink_comparator_pro.py +2926 -0
- setiastro/saspro/bundles.py +61 -0
- setiastro/saspro/bundles_dock.py +114 -0
- setiastro/saspro/cheat_sheet.py +178 -0
- setiastro/saspro/clahe.py +342 -0
- setiastro/saspro/comet_stacking.py +1377 -0
- setiastro/saspro/common_tr.py +107 -0
- setiastro/saspro/config.py +38 -0
- setiastro/saspro/config_bootstrap.py +40 -0
- setiastro/saspro/config_manager.py +316 -0
- setiastro/saspro/continuum_subtract.py +1617 -0
- setiastro/saspro/convo.py +1397 -0
- setiastro/saspro/convo_preset.py +414 -0
- setiastro/saspro/copyastro.py +187 -0
- setiastro/saspro/cosmicclarity.py +1564 -0
- setiastro/saspro/cosmicclarity_preset.py +407 -0
- setiastro/saspro/crop_dialog_pro.py +956 -0
- setiastro/saspro/crop_preset.py +189 -0
- setiastro/saspro/curve_editor_pro.py +2544 -0
- setiastro/saspro/curves_preset.py +375 -0
- setiastro/saspro/debayer.py +670 -0
- setiastro/saspro/debug_utils.py +29 -0
- setiastro/saspro/dnd_mime.py +35 -0
- setiastro/saspro/doc_manager.py +2641 -0
- setiastro/saspro/exoplanet_detector.py +2166 -0
- setiastro/saspro/file_utils.py +284 -0
- setiastro/saspro/fitsmodifier.py +745 -0
- setiastro/saspro/fix_bom.py +32 -0
- setiastro/saspro/free_torch_memory.py +48 -0
- setiastro/saspro/frequency_separation.py +1343 -0
- setiastro/saspro/function_bundle.py +1594 -0
- setiastro/saspro/generate_translations.py +2378 -0
- setiastro/saspro/ghs_dialog_pro.py +660 -0
- setiastro/saspro/ghs_preset.py +284 -0
- setiastro/saspro/graxpert.py +634 -0
- setiastro/saspro/graxpert_preset.py +287 -0
- setiastro/saspro/gui/__init__.py +0 -0
- setiastro/saspro/gui/main_window.py +8567 -0
- setiastro/saspro/gui/mixins/__init__.py +33 -0
- setiastro/saspro/gui/mixins/dock_mixin.py +263 -0
- setiastro/saspro/gui/mixins/file_mixin.py +443 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
- setiastro/saspro/gui/mixins/header_mixin.py +441 -0
- setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
- setiastro/saspro/gui/mixins/menu_mixin.py +361 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +1457 -0
- setiastro/saspro/gui/mixins/update_mixin.py +309 -0
- setiastro/saspro/gui/mixins/view_mixin.py +435 -0
- setiastro/saspro/halobgon.py +462 -0
- setiastro/saspro/header_viewer.py +448 -0
- setiastro/saspro/headless_utils.py +88 -0
- setiastro/saspro/histogram.py +753 -0
- setiastro/saspro/history_explorer.py +939 -0
- setiastro/saspro/i18n.py +156 -0
- setiastro/saspro/image_combine.py +414 -0
- setiastro/saspro/image_peeker_pro.py +1601 -0
- setiastro/saspro/imageops/__init__.py +37 -0
- setiastro/saspro/imageops/mdi_snap.py +292 -0
- setiastro/saspro/imageops/scnr.py +36 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
- setiastro/saspro/imageops/stretch.py +244 -0
- setiastro/saspro/isophote.py +1179 -0
- setiastro/saspro/layers.py +208 -0
- setiastro/saspro/layers_dock.py +714 -0
- setiastro/saspro/lazy_imports.py +193 -0
- setiastro/saspro/legacy/__init__.py +2 -0
- setiastro/saspro/legacy/image_manager.py +2226 -0
- setiastro/saspro/legacy/numba_utils.py +3659 -0
- setiastro/saspro/legacy/xisf.py +1071 -0
- setiastro/saspro/linear_fit.py +534 -0
- setiastro/saspro/live_stacking.py +1830 -0
- setiastro/saspro/log_bus.py +5 -0
- setiastro/saspro/logging_config.py +460 -0
- setiastro/saspro/luminancerecombine.py +309 -0
- setiastro/saspro/main_helpers.py +201 -0
- setiastro/saspro/mask_creation.py +928 -0
- setiastro/saspro/masks_core.py +56 -0
- setiastro/saspro/mdi_widgets.py +353 -0
- setiastro/saspro/memory_utils.py +666 -0
- setiastro/saspro/metadata_patcher.py +75 -0
- setiastro/saspro/mfdeconv.py +3826 -0
- setiastro/saspro/mfdeconv_earlystop.py +71 -0
- setiastro/saspro/mfdeconvcudnn.py +3263 -0
- setiastro/saspro/mfdeconvsport.py +2382 -0
- setiastro/saspro/minorbodycatalog.py +567 -0
- setiastro/saspro/morphology.py +382 -0
- setiastro/saspro/multiscale_decomp.py +1290 -0
- setiastro/saspro/nbtorgb_stars.py +531 -0
- setiastro/saspro/numba_utils.py +3044 -0
- setiastro/saspro/numba_warmup.py +141 -0
- setiastro/saspro/ops/__init__.py +9 -0
- setiastro/saspro/ops/command_help_dialog.py +623 -0
- setiastro/saspro/ops/command_runner.py +217 -0
- setiastro/saspro/ops/commands.py +1594 -0
- setiastro/saspro/ops/script_editor.py +1102 -0
- setiastro/saspro/ops/scripts.py +1413 -0
- setiastro/saspro/ops/settings.py +679 -0
- setiastro/saspro/parallel_utils.py +554 -0
- setiastro/saspro/pedestal.py +121 -0
- setiastro/saspro/perfect_palette_picker.py +1070 -0
- setiastro/saspro/pipeline.py +110 -0
- setiastro/saspro/pixelmath.py +1600 -0
- setiastro/saspro/plate_solver.py +2444 -0
- setiastro/saspro/project_io.py +797 -0
- setiastro/saspro/psf_utils.py +136 -0
- setiastro/saspro/psf_viewer.py +549 -0
- setiastro/saspro/pyi_rthook_astroquery.py +95 -0
- setiastro/saspro/remove_green.py +314 -0
- setiastro/saspro/remove_stars.py +1625 -0
- setiastro/saspro/remove_stars_preset.py +404 -0
- setiastro/saspro/resources.py +477 -0
- setiastro/saspro/rgb_combination.py +207 -0
- setiastro/saspro/rgb_extract.py +19 -0
- setiastro/saspro/rgbalign.py +723 -0
- setiastro/saspro/runtime_imports.py +7 -0
- setiastro/saspro/runtime_torch.py +754 -0
- setiastro/saspro/save_options.py +72 -0
- setiastro/saspro/selective_color.py +1552 -0
- setiastro/saspro/sfcc.py +1430 -0
- setiastro/saspro/shortcuts.py +3043 -0
- setiastro/saspro/signature_insert.py +1099 -0
- setiastro/saspro/stacking_suite.py +18181 -0
- setiastro/saspro/star_alignment.py +7420 -0
- setiastro/saspro/star_alignment_preset.py +329 -0
- setiastro/saspro/star_metrics.py +49 -0
- setiastro/saspro/star_spikes.py +681 -0
- setiastro/saspro/star_stretch.py +470 -0
- setiastro/saspro/stat_stretch.py +506 -0
- setiastro/saspro/status_log_dock.py +78 -0
- setiastro/saspro/subwindow.py +3267 -0
- setiastro/saspro/supernovaasteroidhunter.py +1716 -0
- setiastro/saspro/swap_manager.py +99 -0
- setiastro/saspro/torch_backend.py +89 -0
- setiastro/saspro/torch_rejection.py +434 -0
- setiastro/saspro/translations/de_translations.py +3733 -0
- setiastro/saspro/translations/es_translations.py +3923 -0
- setiastro/saspro/translations/fr_translations.py +3842 -0
- setiastro/saspro/translations/integrate_translations.py +234 -0
- setiastro/saspro/translations/it_translations.py +3662 -0
- setiastro/saspro/translations/ja_translations.py +3585 -0
- setiastro/saspro/translations/pt_translations.py +3853 -0
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +253 -0
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +12520 -0
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +12514 -0
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +12520 -0
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +257 -0
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +257 -0
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +12520 -0
- setiastro/saspro/translations/zh_translations.py +3659 -0
- setiastro/saspro/versioning.py +71 -0
- setiastro/saspro/view_bundle.py +1555 -0
- setiastro/saspro/wavescale_hdr.py +624 -0
- setiastro/saspro/wavescale_hdr_preset.py +101 -0
- setiastro/saspro/wavescalede.py +658 -0
- setiastro/saspro/wavescalede_preset.py +230 -0
- setiastro/saspro/wcs_update.py +374 -0
- setiastro/saspro/whitebalance.py +456 -0
- setiastro/saspro/widgets/__init__.py +48 -0
- setiastro/saspro/widgets/common_utilities.py +306 -0
- setiastro/saspro/widgets/graphics_views.py +122 -0
- setiastro/saspro/widgets/image_utils.py +518 -0
- setiastro/saspro/widgets/preview_dialogs.py +280 -0
- setiastro/saspro/widgets/spinboxes.py +275 -0
- setiastro/saspro/widgets/themed_buttons.py +13 -0
- setiastro/saspro/widgets/wavelet_utils.py +299 -0
- setiastro/saspro/window_shelf.py +185 -0
- setiastro/saspro/xisf.py +1123 -0
- setiastrosuitepro-1.6.1.dist-info/METADATA +267 -0
- setiastrosuitepro-1.6.1.dist-info/RECORD +342 -0
- setiastrosuitepro-1.6.1.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.1.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.1.dist-info/licenses/LICENSE +674 -0
- setiastrosuitepro-1.6.1.dist-info/licenses/license.txt +2580 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
# pro/gui/mixins/header_mixin.py
|
|
2
|
+
"""
|
|
3
|
+
Header management mixin for AstroSuiteProMainWindow.
|
|
4
|
+
|
|
5
|
+
This mixin contains all functionality for viewing and managing FITS headers,
|
|
6
|
+
metadata, and WCS information.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
import re
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from PyQt6.QtCore import Qt
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HeaderMixin:
|
|
19
|
+
"""
|
|
20
|
+
Mixin for header/metadata management.
|
|
21
|
+
|
|
22
|
+
Provides methods for viewing, extracting, and manipulating document headers
|
|
23
|
+
and WCS (World Coordinate System) information.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Exact keys we always consider WCS
|
|
27
|
+
_WCS_KEY_SET = {
|
|
28
|
+
"WCSAXES", "CTYPE1", "CTYPE2", "CUNIT1", "CUNIT2",
|
|
29
|
+
"CRPIX1", "CRPIX2", "CRVAL1", "CRVAL2",
|
|
30
|
+
"CD1_1", "CD1_2", "CD2_1", "CD2_2",
|
|
31
|
+
"PC1_1", "PC1_2", "PC2_1", "PC2_2",
|
|
32
|
+
"CDELT1", "CDELT2",
|
|
33
|
+
"LONPOLE", "LATPOLE",
|
|
34
|
+
"RADESYS", "RADECSYS", "EQUINOX", "EPOCH",
|
|
35
|
+
"NAXIS1", "NAXIS2" # useful context for UIs/solvers
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def _ensure_header_map(self, doc):
|
|
39
|
+
"""Ensure doc has a header dictionary in metadata, return it."""
|
|
40
|
+
meta = getattr(doc, "metadata", None)
|
|
41
|
+
if meta is None:
|
|
42
|
+
return None
|
|
43
|
+
hdr = meta.get("original_header")
|
|
44
|
+
if not isinstance(hdr, dict):
|
|
45
|
+
hdr = {}
|
|
46
|
+
meta["original_header"] = hdr
|
|
47
|
+
return hdr
|
|
48
|
+
|
|
49
|
+
def _coerce_wcs_numbers(self, d: dict) -> dict:
|
|
50
|
+
"""Convert common WCS/SIP values to int/float where appropriate."""
|
|
51
|
+
numeric = {
|
|
52
|
+
"CRPIX1", "CRPIX2", "CRVAL1", "CRVAL2", "CDELT1", "CDELT2",
|
|
53
|
+
"CD1_1", "CD1_2", "CD2_1", "CD2_2", "PC1_1", "PC1_2", "PC2_1", "PC2_2",
|
|
54
|
+
"CROTA1", "CROTA2", "EQUINOX", "WCSAXES", "A_ORDER", "B_ORDER", "AP_ORDER", "BP_ORDER",
|
|
55
|
+
"LONPOLE", "LATPOLE"
|
|
56
|
+
}
|
|
57
|
+
out = {}
|
|
58
|
+
for k, v in d.items():
|
|
59
|
+
K = str(k).upper()
|
|
60
|
+
try:
|
|
61
|
+
if K in numeric or re.match(r"^(A|B|AP|BP)_\d+_\d+$", K or ""):
|
|
62
|
+
if isinstance(v, str):
|
|
63
|
+
s = v.strip()
|
|
64
|
+
# int if clean integer, else float
|
|
65
|
+
out[K] = int(s) if re.fullmatch(r"[+-]?\d+", s) else float(s)
|
|
66
|
+
else:
|
|
67
|
+
out[K] = v
|
|
68
|
+
else:
|
|
69
|
+
out[K] = v
|
|
70
|
+
except Exception:
|
|
71
|
+
out[K] = v
|
|
72
|
+
return out
|
|
73
|
+
|
|
74
|
+
def _extract_wcs_dict(self, doc) -> dict:
|
|
75
|
+
"""Collect a complete WCS/SIP dict from the doc's header/meta."""
|
|
76
|
+
if doc is None:
|
|
77
|
+
return {}
|
|
78
|
+
src = (getattr(doc, "metadata", {}) or {}).get("original_header")
|
|
79
|
+
|
|
80
|
+
wcs = {}
|
|
81
|
+
if src is None:
|
|
82
|
+
pass
|
|
83
|
+
else:
|
|
84
|
+
try:
|
|
85
|
+
for k, v in dict(src).items():
|
|
86
|
+
K = str(k).upper()
|
|
87
|
+
if (K.startswith(("CRPIX", "CRVAL", "CDELT", "CD", "PC", "CROTA", "CTYPE", "CUNIT",
|
|
88
|
+
"WCSAXES", "LONPOLE", "LATPOLE", "EQUINOX", "PV")) or
|
|
89
|
+
K in {"RADECSYS", "RADESYS", "NAXIS1", "NAXIS2"} or
|
|
90
|
+
K.startswith(("A_", "B_", "AP_", "BP_"))):
|
|
91
|
+
wcs[K] = v
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
# Also accept any mirror you previously stored
|
|
96
|
+
meta = getattr(doc, "metadata", {}) or {}
|
|
97
|
+
imgmeta = meta.get("image_meta") or meta.get("WCS") or {}
|
|
98
|
+
if isinstance(imgmeta, dict):
|
|
99
|
+
sub = imgmeta.get("WCS", imgmeta)
|
|
100
|
+
if isinstance(sub, dict):
|
|
101
|
+
for k, v in sub.items():
|
|
102
|
+
K = str(k).upper()
|
|
103
|
+
if (K.startswith(("CRPIX", "CRVAL", "CDELT", "CD", "PC", "CROTA", "CTYPE", "CUNIT",
|
|
104
|
+
"WCSAXES", "LONPOLE", "LATPOLE", "EQUINOX", "PV")) or
|
|
105
|
+
K in {"RADECSYS", "RADESYS", "NAXIS1", "NAXIS2"} or
|
|
106
|
+
K.startswith(("A_", "B_", "AP_", "BP_"))):
|
|
107
|
+
wcs.setdefault(K, v)
|
|
108
|
+
|
|
109
|
+
# sensible defaults/parity
|
|
110
|
+
if any(k.startswith(("A_", "B_", "AP_", "BP_")) for k in wcs):
|
|
111
|
+
wcs.setdefault("CUNIT1", "deg")
|
|
112
|
+
wcs.setdefault("CUNIT2", "deg")
|
|
113
|
+
# TAN-SIP labels if SIP present
|
|
114
|
+
c1 = str(wcs.get("CTYPE1", "RA---TAN"))
|
|
115
|
+
c2 = str(wcs.get("CTYPE2", "DEC--TAN"))
|
|
116
|
+
if not c1.endswith("-SIP"):
|
|
117
|
+
wcs["CTYPE1"] = "RA---TAN-SIP"
|
|
118
|
+
if not c2.endswith("-SIP"):
|
|
119
|
+
wcs["CTYPE2"] = "DEC--TAN-SIP"
|
|
120
|
+
|
|
121
|
+
if "RADECSYS" in wcs and "RADESYS" not in wcs:
|
|
122
|
+
wcs["RADESYS"] = wcs["RADECSYS"]
|
|
123
|
+
if "WCSAXES" not in wcs and {"CTYPE1", "CTYPE2"} <= wcs.keys():
|
|
124
|
+
wcs["WCSAXES"] = 2
|
|
125
|
+
|
|
126
|
+
return self._coerce_wcs_numbers(wcs)
|
|
127
|
+
|
|
128
|
+
def _ensure_header_for_doc(self, doc):
|
|
129
|
+
"""Return an astropy Header for doc.metadata['original_header'] (creating one if needed)."""
|
|
130
|
+
from astropy.io.fits import Header
|
|
131
|
+
import numpy as np
|
|
132
|
+
|
|
133
|
+
meta = getattr(doc, "metadata", None)
|
|
134
|
+
if not isinstance(meta, dict):
|
|
135
|
+
setattr(doc, "metadata", {})
|
|
136
|
+
meta = doc.metadata
|
|
137
|
+
|
|
138
|
+
hdr_like = meta.get("original_header")
|
|
139
|
+
|
|
140
|
+
# Already a Header?
|
|
141
|
+
if isinstance(hdr_like, Header):
|
|
142
|
+
hdr = hdr_like
|
|
143
|
+
elif isinstance(hdr_like, dict):
|
|
144
|
+
# coerce dict -> Header
|
|
145
|
+
hdr = Header()
|
|
146
|
+
int_keys = {"A_ORDER", "B_ORDER", "AP_ORDER", "BP_ORDER", "WCSAXES", "NAXIS", "NAXIS1", "NAXIS2", "NAXIS3"}
|
|
147
|
+
for k, v in dict(hdr_like).items():
|
|
148
|
+
K = str(k).upper()
|
|
149
|
+
try:
|
|
150
|
+
if K in int_keys:
|
|
151
|
+
hdr[K] = int(float(str(v).strip().split()[0]))
|
|
152
|
+
elif re.match(r"^(?:A|B|AP|BP)_\d+_\d+$", K) or \
|
|
153
|
+
re.match(r"^(?:CRPIX|CRVAL|CDELT|CD|PC|CROTA|LATPOLE|LONPOLE|EQUINOX)\d?_?\d*$", K):
|
|
154
|
+
hdr[K] = float(str(v).strip().split()[0])
|
|
155
|
+
else:
|
|
156
|
+
hdr[K] = v
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
else:
|
|
160
|
+
hdr = Header()
|
|
161
|
+
|
|
162
|
+
# Ensure basic axis cards exist (needed for non-FITS sources)
|
|
163
|
+
try:
|
|
164
|
+
img = getattr(doc, "image", None)
|
|
165
|
+
if img is not None:
|
|
166
|
+
a = np.asarray(img)
|
|
167
|
+
H = int(a.shape[0]) if a.ndim >= 2 else 1
|
|
168
|
+
W = int(a.shape[1]) if a.ndim >= 2 else 1
|
|
169
|
+
C = int(a.shape[2]) if a.ndim == 3 else 1
|
|
170
|
+
# Only set when missing (don't clobber real FITS headers)
|
|
171
|
+
if "NAXIS" not in hdr:
|
|
172
|
+
hdr["NAXIS"] = 2 if a.ndim != 3 else 3
|
|
173
|
+
if "NAXIS1" not in hdr:
|
|
174
|
+
hdr["NAXIS1"] = W
|
|
175
|
+
if "NAXIS2" not in hdr:
|
|
176
|
+
hdr["NAXIS2"] = H
|
|
177
|
+
if a.ndim == 3 and "NAXIS3" not in hdr:
|
|
178
|
+
hdr["NAXIS3"] = C
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
meta["original_header"] = hdr
|
|
183
|
+
return hdr
|
|
184
|
+
|
|
185
|
+
def _refresh_header_viewer(self, doc=None):
|
|
186
|
+
"""Rebuild the header dock from the given (or active) doc -- never raises."""
|
|
187
|
+
try:
|
|
188
|
+
doc = doc or self._active_doc()
|
|
189
|
+
hv = getattr(self, "header_viewer", None)
|
|
190
|
+
|
|
191
|
+
# If your dock widget has a native API, try it but don't trust it.
|
|
192
|
+
if hv and hasattr(hv, "set_document"):
|
|
193
|
+
try:
|
|
194
|
+
hv.set_document(doc)
|
|
195
|
+
return
|
|
196
|
+
except Exception as e:
|
|
197
|
+
print("[header] set_document suppressed:", e)
|
|
198
|
+
|
|
199
|
+
# Fallback path: extract -> populate, all guarded.
|
|
200
|
+
rows = self._extract_header_pairs(doc)
|
|
201
|
+
if not rows:
|
|
202
|
+
self._clear_header_viewer(self.tr("No header") if doc else self.tr("No image"))
|
|
203
|
+
else:
|
|
204
|
+
self._populate_header_viewer(rows)
|
|
205
|
+
except Exception as e:
|
|
206
|
+
print("[header] refresh suppressed:", e)
|
|
207
|
+
self._clear_header_viewer("")
|
|
208
|
+
|
|
209
|
+
def _extract_header_pairs(self, doc):
|
|
210
|
+
"""
|
|
211
|
+
Return list[(key, value, comment)].
|
|
212
|
+
Prefers a JSON-safe snapshot if present, otherwise best-effort parsing.
|
|
213
|
+
Never raises.
|
|
214
|
+
"""
|
|
215
|
+
try:
|
|
216
|
+
if not doc:
|
|
217
|
+
return []
|
|
218
|
+
|
|
219
|
+
meta = getattr(doc, "metadata", {}) or {}
|
|
220
|
+
|
|
221
|
+
# 1) Prefer a snapshot if any writer/loader provided it.
|
|
222
|
+
snap = meta.get("__header_snapshot__")
|
|
223
|
+
if isinstance(snap, dict):
|
|
224
|
+
fmt = snap.get("format")
|
|
225
|
+
if fmt == "fits-cards":
|
|
226
|
+
cards = snap.get("cards") or []
|
|
227
|
+
out = []
|
|
228
|
+
for it in cards:
|
|
229
|
+
try:
|
|
230
|
+
k, v, c = it
|
|
231
|
+
except Exception:
|
|
232
|
+
# tolerate weird shapes
|
|
233
|
+
k, v, c = (str(it[0]) if it else "",
|
|
234
|
+
"" if len(it) < 2 else str(it[1]),
|
|
235
|
+
"" if len(it) < 3 else str(it[2]))
|
|
236
|
+
out.append((str(k), str(v), str(c)))
|
|
237
|
+
return out
|
|
238
|
+
if fmt == "dict":
|
|
239
|
+
items = snap.get("items") or {}
|
|
240
|
+
out = []
|
|
241
|
+
for k, v in items.items():
|
|
242
|
+
if isinstance(v, dict):
|
|
243
|
+
out.append((str(k), str(v.get("value", "")), str(v.get("comment", ""))))
|
|
244
|
+
else:
|
|
245
|
+
out.append((str(k), str(v), ""))
|
|
246
|
+
return out
|
|
247
|
+
if fmt == "repr":
|
|
248
|
+
return [(self.tr("Header"), str(snap.get("text", "")), "")]
|
|
249
|
+
|
|
250
|
+
# 2) Live header object(s) (can be astropy, dict, or random).
|
|
251
|
+
hdr = (meta.get("original_header")
|
|
252
|
+
or meta.get("fits_header")
|
|
253
|
+
or meta.get("header"))
|
|
254
|
+
|
|
255
|
+
if hdr is None:
|
|
256
|
+
return []
|
|
257
|
+
|
|
258
|
+
# astropy.io.fits.Header (optional; no hard dependency)
|
|
259
|
+
try:
|
|
260
|
+
from astropy.io.fits import Header
|
|
261
|
+
except Exception:
|
|
262
|
+
Header = None
|
|
263
|
+
|
|
264
|
+
if Header is not None:
|
|
265
|
+
try:
|
|
266
|
+
if isinstance(hdr, Header):
|
|
267
|
+
out = []
|
|
268
|
+
for k in hdr.keys():
|
|
269
|
+
try:
|
|
270
|
+
val = hdr[k]
|
|
271
|
+
except Exception:
|
|
272
|
+
val = ""
|
|
273
|
+
try:
|
|
274
|
+
cmt = hdr.comments[k]
|
|
275
|
+
except Exception:
|
|
276
|
+
cmt = ""
|
|
277
|
+
out.append((str(k), str(val), str(cmt)))
|
|
278
|
+
return out
|
|
279
|
+
except Exception as e:
|
|
280
|
+
print("[header] astropy parse suppressed:", e)
|
|
281
|
+
|
|
282
|
+
# dict-ish header (e.g., XISF-like)
|
|
283
|
+
if isinstance(hdr, dict):
|
|
284
|
+
out = []
|
|
285
|
+
for k, v in hdr.items():
|
|
286
|
+
if isinstance(v, dict):
|
|
287
|
+
out.append((str(k), str(v.get("value", "")), str(v.get("comment", ""))))
|
|
288
|
+
else:
|
|
289
|
+
# avoid huge array dumps
|
|
290
|
+
try:
|
|
291
|
+
import numpy as _np
|
|
292
|
+
if isinstance(v, _np.ndarray):
|
|
293
|
+
v = f"ndarray{tuple(v.shape)}"
|
|
294
|
+
except Exception:
|
|
295
|
+
pass
|
|
296
|
+
out.append((str(k), str(v), ""))
|
|
297
|
+
return out
|
|
298
|
+
|
|
299
|
+
# Fallback: string repr
|
|
300
|
+
return [(self.tr("Header"), str(hdr), "")]
|
|
301
|
+
except Exception as e:
|
|
302
|
+
print("[header] extract suppressed:", e)
|
|
303
|
+
return []
|
|
304
|
+
|
|
305
|
+
def _populate_header_viewer(self, rows):
|
|
306
|
+
"""Render rows into whatever widget you expose; never raises."""
|
|
307
|
+
try:
|
|
308
|
+
w = self._header_widget()
|
|
309
|
+
except Exception as e:
|
|
310
|
+
print("[header] _header_widget suppressed:", e)
|
|
311
|
+
return
|
|
312
|
+
if w is None:
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
# Table widget path
|
|
316
|
+
try:
|
|
317
|
+
from PyQt6.QtWidgets import QTableWidget, QTableWidgetItem
|
|
318
|
+
if isinstance(w, QTableWidget):
|
|
319
|
+
try:
|
|
320
|
+
w.setRowCount(0)
|
|
321
|
+
w.setColumnCount(3)
|
|
322
|
+
w.setHorizontalHeaderLabels([self.tr("Key"), self.tr("Value"), self.tr("Comment")])
|
|
323
|
+
for r, (k, v, c) in enumerate(rows):
|
|
324
|
+
w.insertRow(r)
|
|
325
|
+
w.setItem(r, 0, QTableWidgetItem(k))
|
|
326
|
+
w.setItem(r, 1, QTableWidgetItem(v))
|
|
327
|
+
w.setItem(r, 2, QTableWidgetItem(c))
|
|
328
|
+
return
|
|
329
|
+
except Exception as e:
|
|
330
|
+
print("[header] table populate suppressed:", e)
|
|
331
|
+
except Exception:
|
|
332
|
+
pass
|
|
333
|
+
|
|
334
|
+
# List widget path
|
|
335
|
+
try:
|
|
336
|
+
from PyQt6.QtWidgets import QListWidget
|
|
337
|
+
if isinstance(w, QListWidget):
|
|
338
|
+
try:
|
|
339
|
+
w.clear()
|
|
340
|
+
for k, v, c in rows:
|
|
341
|
+
w.addItem(f"{k} = {v}" + (f" / {c}" if c else ""))
|
|
342
|
+
return
|
|
343
|
+
except Exception as e:
|
|
344
|
+
print("[header] list populate suppressed:", e)
|
|
345
|
+
except Exception:
|
|
346
|
+
pass
|
|
347
|
+
|
|
348
|
+
# Plain text-ish
|
|
349
|
+
try:
|
|
350
|
+
if hasattr(w, "setPlainText"):
|
|
351
|
+
w.setPlainText("\n".join(
|
|
352
|
+
f"{k} = {v}" + (f" / {c}" if c else "") for (k, v, c) in rows
|
|
353
|
+
))
|
|
354
|
+
return
|
|
355
|
+
if hasattr(w, "setText"):
|
|
356
|
+
w.setText("\n".join(
|
|
357
|
+
f"{k} = {v}" + (f" / {c}" if c else "") for (k, v, c) in rows
|
|
358
|
+
))
|
|
359
|
+
return
|
|
360
|
+
except Exception as e:
|
|
361
|
+
print("[header] text populate suppressed:", e)
|
|
362
|
+
|
|
363
|
+
def _clear_header_viewer(self, message: str = ""):
|
|
364
|
+
"""Clear header viewer content quietly--no dialogs."""
|
|
365
|
+
w = self._header_widget()
|
|
366
|
+
if w is None:
|
|
367
|
+
return
|
|
368
|
+
try:
|
|
369
|
+
from PyQt6.QtWidgets import QTableWidget
|
|
370
|
+
if isinstance(w, QTableWidget):
|
|
371
|
+
w.setRowCount(0)
|
|
372
|
+
w.setColumnCount(3)
|
|
373
|
+
w.setHorizontalHeaderLabels([self.tr("Key"), self.tr("Value"), self.tr("Comment")])
|
|
374
|
+
return
|
|
375
|
+
except Exception:
|
|
376
|
+
pass
|
|
377
|
+
try:
|
|
378
|
+
from PyQt6.QtWidgets import QListWidget
|
|
379
|
+
if isinstance(w, QListWidget):
|
|
380
|
+
w.clear()
|
|
381
|
+
if message:
|
|
382
|
+
w.addItem(message)
|
|
383
|
+
return
|
|
384
|
+
except Exception:
|
|
385
|
+
pass
|
|
386
|
+
# QTextEdit-like
|
|
387
|
+
if hasattr(w, "setPlainText"):
|
|
388
|
+
try:
|
|
389
|
+
w.setPlainText(message or "")
|
|
390
|
+
except Exception:
|
|
391
|
+
pass
|
|
392
|
+
|
|
393
|
+
def _header_widget(self):
|
|
394
|
+
"""
|
|
395
|
+
Find the concrete widget used to display header text/table.
|
|
396
|
+
Never raises; returns None if nothing sensible is found.
|
|
397
|
+
"""
|
|
398
|
+
hv = getattr(self, "header_viewer", None) or getattr(self, "metadata_dock", None)
|
|
399
|
+
if hv is None:
|
|
400
|
+
return None
|
|
401
|
+
|
|
402
|
+
# If it's a dock widget (QDockWidget-like), pull its child widget
|
|
403
|
+
try:
|
|
404
|
+
if hasattr(hv, "widget") and callable(hv.widget):
|
|
405
|
+
inner = hv.widget()
|
|
406
|
+
if inner is not None:
|
|
407
|
+
return inner
|
|
408
|
+
except Exception:
|
|
409
|
+
pass
|
|
410
|
+
|
|
411
|
+
# It might already be the actual widget
|
|
412
|
+
return hv
|
|
413
|
+
|
|
414
|
+
def _on_doc_added_for_header_sync(self, doc):
|
|
415
|
+
"""Update header when the *active* doc changes."""
|
|
416
|
+
try:
|
|
417
|
+
doc.changed.connect(self._maybe_refresh_header_on_doc_change)
|
|
418
|
+
except Exception:
|
|
419
|
+
pass
|
|
420
|
+
|
|
421
|
+
def _on_doc_removed_for_header_sync(self, doc):
|
|
422
|
+
"""If the removed doc was the active one, clear header."""
|
|
423
|
+
if doc is self._active_doc():
|
|
424
|
+
self._clear_header_viewer(self.tr("No image"))
|
|
425
|
+
hv = getattr(self, "header_viewer", None)
|
|
426
|
+
if hv and hasattr(hv, "set_document"):
|
|
427
|
+
try:
|
|
428
|
+
hv.set_document(None)
|
|
429
|
+
except Exception:
|
|
430
|
+
pass
|
|
431
|
+
|
|
432
|
+
# If there are no more subwindows, force a global clear too
|
|
433
|
+
if not self.mdi.subWindowList():
|
|
434
|
+
self.currentDocumentChanged.emit(None)
|
|
435
|
+
self._hdr_refresh_timer.start(0)
|
|
436
|
+
|
|
437
|
+
def _maybe_refresh_header_on_doc_change(self):
|
|
438
|
+
"""Refresh header if sender is the active doc."""
|
|
439
|
+
sender = self.sender()
|
|
440
|
+
if sender is self._active_doc():
|
|
441
|
+
self._hdr_refresh_timer.start(0)
|