setiastrosuitepro 1.6.7__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/Background_startup.jpg +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/acv_icon.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/first_quarter.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/full_moon.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/last_quarter.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/new_moon.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/rotatearbitrary.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/waning_crescent_1.png +0 -0
- setiastro/images/waning_crescent_2.png +0 -0
- setiastro/images/waning_crescent_3.png +0 -0
- setiastro/images/waning_crescent_4.png +0 -0
- setiastro/images/waning_crescent_5.png +0 -0
- setiastro/images/waning_gibbous_1.png +0 -0
- setiastro/images/waning_gibbous_2.png +0 -0
- setiastro/images/waning_gibbous_3.png +0 -0
- setiastro/images/waning_gibbous_4.png +0 -0
- setiastro/images/waning_gibbous_5.png +0 -0
- setiastro/images/waxing_crescent_1.png +0 -0
- setiastro/images/waxing_crescent_2.png +0 -0
- setiastro/images/waxing_crescent_3.png +0 -0
- setiastro/images/waxing_crescent_4.png +0 -0
- setiastro/images/waxing_crescent_5.png +0 -0
- setiastro/images/waxing_gibbous_1.png +0 -0
- setiastro/images/waxing_gibbous_2.png +0 -0
- setiastro/images/waxing_gibbous_3.png +0 -0
- setiastro/images/waxing_gibbous_4.png +0 -0
- setiastro/images/waxing_gibbous_5.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/qml/ResourceMonitor.qml +128 -0
- setiastro/saspro/__init__.py +20 -0
- setiastro/saspro/__main__.py +964 -0
- setiastro/saspro/_generated/__init__.py +7 -0
- setiastro/saspro/_generated/build_info.py +3 -0
- setiastro/saspro/abe.py +1379 -0
- setiastro/saspro/abe_preset.py +196 -0
- setiastro/saspro/aberration_ai.py +910 -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/acv_exporter.py +379 -0
- setiastro/saspro/add_stars.py +627 -0
- setiastro/saspro/astrobin_exporter.py +1010 -0
- setiastro/saspro/astrospike.py +153 -0
- setiastro/saspro/astrospike_python.py +1841 -0
- setiastro/saspro/autostretch.py +198 -0
- setiastro/saspro/backgroundneutral.py +639 -0
- setiastro/saspro/batch_convert.py +328 -0
- setiastro/saspro/batch_renamer.py +522 -0
- setiastro/saspro/blemish_blaster.py +494 -0
- setiastro/saspro/blink_comparator_pro.py +3149 -0
- setiastro/saspro/bundles.py +61 -0
- setiastro/saspro/bundles_dock.py +114 -0
- setiastro/saspro/cheat_sheet.py +213 -0
- setiastro/saspro/clahe.py +371 -0
- setiastro/saspro/comet_stacking.py +1442 -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 +1620 -0
- setiastro/saspro/convo.py +1403 -0
- setiastro/saspro/convo_preset.py +414 -0
- setiastro/saspro/copyastro.py +190 -0
- setiastro/saspro/cosmicclarity.py +1593 -0
- setiastro/saspro/cosmicclarity_preset.py +407 -0
- setiastro/saspro/crop_dialog_pro.py +1005 -0
- setiastro/saspro/crop_preset.py +189 -0
- setiastro/saspro/curve_editor_pro.py +2608 -0
- setiastro/saspro/curves_preset.py +375 -0
- setiastro/saspro/debayer.py +673 -0
- setiastro/saspro/debug_utils.py +29 -0
- setiastro/saspro/dnd_mime.py +35 -0
- setiastro/saspro/doc_manager.py +2727 -0
- setiastro/saspro/exoplanet_detector.py +2258 -0
- setiastro/saspro/file_utils.py +284 -0
- setiastro/saspro/fitsmodifier.py +748 -0
- setiastro/saspro/fix_bom.py +32 -0
- setiastro/saspro/free_torch_memory.py +48 -0
- setiastro/saspro/frequency_separation.py +1352 -0
- setiastro/saspro/function_bundle.py +1596 -0
- setiastro/saspro/generate_translations.py +3092 -0
- setiastro/saspro/ghs_dialog_pro.py +728 -0
- setiastro/saspro/ghs_preset.py +284 -0
- setiastro/saspro/graxpert.py +638 -0
- setiastro/saspro/graxpert_preset.py +287 -0
- setiastro/saspro/gui/__init__.py +0 -0
- setiastro/saspro/gui/main_window.py +8928 -0
- setiastro/saspro/gui/mixins/__init__.py +33 -0
- setiastro/saspro/gui/mixins/dock_mixin.py +375 -0
- setiastro/saspro/gui/mixins/file_mixin.py +450 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +503 -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 +391 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +1824 -0
- setiastro/saspro/gui/mixins/update_mixin.py +323 -0
- setiastro/saspro/gui/mixins/view_mixin.py +477 -0
- setiastro/saspro/gui/statistics_dialog.py +47 -0
- setiastro/saspro/halobgon.py +492 -0
- setiastro/saspro/header_viewer.py +448 -0
- setiastro/saspro/headless_utils.py +88 -0
- setiastro/saspro/histogram.py +760 -0
- setiastro/saspro/history_explorer.py +941 -0
- setiastro/saspro/i18n.py +168 -0
- setiastro/saspro/image_combine.py +421 -0
- setiastro/saspro/image_peeker_pro.py +1608 -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 +236 -0
- setiastro/saspro/isophote.py +1186 -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 +2360 -0
- setiastro/saspro/legacy/numba_utils.py +3676 -0
- setiastro/saspro/legacy/xisf.py +1213 -0
- setiastro/saspro/linear_fit.py +537 -0
- setiastro/saspro/live_stacking.py +1854 -0
- setiastro/saspro/log_bus.py +5 -0
- setiastro/saspro/logging_config.py +460 -0
- setiastro/saspro/luminancerecombine.py +510 -0
- setiastro/saspro/main_helpers.py +201 -0
- setiastro/saspro/mask_creation.py +1090 -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 +3909 -0
- setiastro/saspro/mfdeconv_earlystop.py +71 -0
- setiastro/saspro/mfdeconvcudnn.py +3312 -0
- setiastro/saspro/mfdeconvsport.py +2459 -0
- setiastro/saspro/minorbodycatalog.py +567 -0
- setiastro/saspro/morphology.py +411 -0
- setiastro/saspro/multiscale_decomp.py +1751 -0
- setiastro/saspro/nbtorgb_stars.py +541 -0
- setiastro/saspro/numba_utils.py +3145 -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 +1105 -0
- setiastro/saspro/ops/scripts.py +1476 -0
- setiastro/saspro/ops/settings.py +637 -0
- setiastro/saspro/parallel_utils.py +554 -0
- setiastro/saspro/pedestal.py +121 -0
- setiastro/saspro/perfect_palette_picker.py +1105 -0
- setiastro/saspro/pipeline.py +110 -0
- setiastro/saspro/pixelmath.py +1604 -0
- setiastro/saspro/plate_solver.py +2480 -0
- setiastro/saspro/project_io.py +797 -0
- setiastro/saspro/psf_utils.py +136 -0
- setiastro/saspro/psf_viewer.py +631 -0
- setiastro/saspro/pyi_rthook_astroquery.py +95 -0
- setiastro/saspro/remove_green.py +331 -0
- setiastro/saspro/remove_stars.py +1599 -0
- setiastro/saspro/remove_stars_preset.py +446 -0
- setiastro/saspro/resources.py +570 -0
- setiastro/saspro/rgb_combination.py +208 -0
- setiastro/saspro/rgb_extract.py +19 -0
- setiastro/saspro/rgbalign.py +727 -0
- setiastro/saspro/runtime_imports.py +7 -0
- setiastro/saspro/runtime_torch.py +754 -0
- setiastro/saspro/save_options.py +73 -0
- setiastro/saspro/selective_color.py +1614 -0
- setiastro/saspro/sfcc.py +1530 -0
- setiastro/saspro/shortcuts.py +3125 -0
- setiastro/saspro/signature_insert.py +1106 -0
- setiastro/saspro/stacking_suite.py +19069 -0
- setiastro/saspro/star_alignment.py +7383 -0
- setiastro/saspro/star_alignment_preset.py +329 -0
- setiastro/saspro/star_metrics.py +49 -0
- setiastro/saspro/star_spikes.py +769 -0
- setiastro/saspro/star_stretch.py +542 -0
- setiastro/saspro/stat_stretch.py +554 -0
- setiastro/saspro/status_log_dock.py +78 -0
- setiastro/saspro/subwindow.py +3523 -0
- setiastro/saspro/supernovaasteroidhunter.py +1719 -0
- setiastro/saspro/swap_manager.py +134 -0
- setiastro/saspro/torch_backend.py +89 -0
- setiastro/saspro/torch_rejection.py +434 -0
- setiastro/saspro/translations/all_source_strings.json +4726 -0
- setiastro/saspro/translations/ar_translations.py +4096 -0
- setiastro/saspro/translations/de_translations.py +3728 -0
- setiastro/saspro/translations/es_translations.py +4169 -0
- setiastro/saspro/translations/fr_translations.py +4090 -0
- setiastro/saspro/translations/hi_translations.py +3803 -0
- setiastro/saspro/translations/integrate_translations.py +271 -0
- setiastro/saspro/translations/it_translations.py +4728 -0
- setiastro/saspro/translations/ja_translations.py +3834 -0
- setiastro/saspro/translations/pt_translations.py +3847 -0
- setiastro/saspro/translations/ru_translations.py +3082 -0
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +16019 -0
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +14548 -0
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +16202 -0
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +15870 -0
- setiastro/saspro/translations/saspro_hi.qm +0 -0
- setiastro/saspro/translations/saspro_hi.ts +14855 -0
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +19046 -0
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +14980 -0
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +15024 -0
- setiastro/saspro/translations/saspro_ru.qm +0 -0
- setiastro/saspro/translations/saspro_ru.ts +11835 -0
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +15237 -0
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +15248 -0
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +15289 -0
- setiastro/saspro/translations/sw_translations.py +3897 -0
- setiastro/saspro/translations/uk_translations.py +3929 -0
- setiastro/saspro/translations/zh_translations.py +3910 -0
- setiastro/saspro/versioning.py +77 -0
- setiastro/saspro/view_bundle.py +1558 -0
- setiastro/saspro/wavescale_hdr.py +648 -0
- setiastro/saspro/wavescale_hdr_preset.py +101 -0
- setiastro/saspro/wavescalede.py +683 -0
- setiastro/saspro/wavescalede_preset.py +230 -0
- setiastro/saspro/wcs_update.py +374 -0
- setiastro/saspro/whitebalance.py +540 -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/minigame/game.js +991 -0
- setiastro/saspro/widgets/minigame/index.html +53 -0
- setiastro/saspro/widgets/minigame/style.css +241 -0
- setiastro/saspro/widgets/preview_dialogs.py +280 -0
- setiastro/saspro/widgets/resource_monitor.py +313 -0
- setiastro/saspro/widgets/spinboxes.py +290 -0
- setiastro/saspro/widgets/themed_buttons.py +13 -0
- setiastro/saspro/widgets/wavelet_utils.py +331 -0
- setiastro/saspro/wimi.py +7367 -0
- setiastro/saspro/wims.py +588 -0
- setiastro/saspro/window_shelf.py +185 -0
- setiastro/saspro/xisf.py +1213 -0
- setiastrosuitepro-1.6.7.dist-info/METADATA +279 -0
- setiastrosuitepro-1.6.7.dist-info/RECORD +394 -0
- setiastrosuitepro-1.6.7.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.7.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.7.dist-info/licenses/LICENSE +674 -0
- setiastrosuitepro-1.6.7.dist-info/licenses/license.txt +2580 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import numpy as np
|
|
3
|
+
from PyQt6.QtWidgets import QDialog, QFormLayout, QDialogButtonBox, QSpinBox, QDoubleSpinBox, QMessageBox
|
|
4
|
+
from .wavescalede import compute_wavescale_dse
|
|
5
|
+
|
|
6
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
# Preset editor
|
|
8
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
class WaveScaleDSEPresetDialog(QDialog):
|
|
10
|
+
def __init__(self, parent=None, initial: dict | None = None):
|
|
11
|
+
super().__init__(parent)
|
|
12
|
+
self.setWindowTitle(self.tr("WaveScale Dark Enhancer — Preset"))
|
|
13
|
+
p = dict(initial or {})
|
|
14
|
+
f = QFormLayout(self)
|
|
15
|
+
|
|
16
|
+
self.n_scales = QSpinBox()
|
|
17
|
+
self.n_scales.setRange(2, 10)
|
|
18
|
+
self.n_scales.setValue(int(p.get("n_scales", 6)))
|
|
19
|
+
|
|
20
|
+
self.boost = QDoubleSpinBox()
|
|
21
|
+
self.boost.setRange(0.10, 10.00)
|
|
22
|
+
self.boost.setDecimals(2)
|
|
23
|
+
self.boost.setSingleStep(0.05)
|
|
24
|
+
self.boost.setValue(float(p.get("boost_factor", 5.0)))
|
|
25
|
+
|
|
26
|
+
self.gamma = QDoubleSpinBox()
|
|
27
|
+
self.gamma.setRange(0.10, 10.00)
|
|
28
|
+
self.gamma.setDecimals(2)
|
|
29
|
+
self.gamma.setSingleStep(0.10)
|
|
30
|
+
self.gamma.setValue(float(p.get("mask_gamma", 1.0)))
|
|
31
|
+
|
|
32
|
+
self.iters = QSpinBox()
|
|
33
|
+
self.iters.setRange(1, 10)
|
|
34
|
+
self.iters.setValue(int(p.get("iterations", 2)))
|
|
35
|
+
|
|
36
|
+
f.addRow(self.tr("Number of Scales:"), self.n_scales)
|
|
37
|
+
f.addRow(self.tr("Boost Factor:"), self.boost)
|
|
38
|
+
f.addRow(self.tr("Mask Gamma:"), self.gamma)
|
|
39
|
+
f.addRow(self.tr("Iterations:"), self.iters)
|
|
40
|
+
|
|
41
|
+
btns = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, parent=self)
|
|
42
|
+
btns.accepted.connect(self.accept); btns.rejected.connect(self.reject)
|
|
43
|
+
f.addRow(btns)
|
|
44
|
+
|
|
45
|
+
def result_dict(self) -> dict:
|
|
46
|
+
return {
|
|
47
|
+
"n_scales": int(self.n_scales.value()),
|
|
48
|
+
"boost_factor": float(self.boost.value()),
|
|
49
|
+
"mask_gamma": float(self.gamma.value()),
|
|
50
|
+
"iterations": int(self.iters.value()),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
# Headless runner (exactly like UI: compute → blend by active mask → apply)
|
|
58
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
59
|
+
def run_wavescalede_via_preset(main, preset: dict | None = None, target_doc=None):
|
|
60
|
+
import numpy as np
|
|
61
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
62
|
+
|
|
63
|
+
p = dict(preset or {})
|
|
64
|
+
|
|
65
|
+
# --- sanitize to UI limits so replay is clean ---
|
|
66
|
+
try:
|
|
67
|
+
n_scales = int(np.clip(p.get("n_scales", 6), 2, 10))
|
|
68
|
+
boost = float(np.clip(p.get("boost_factor", 5.0), 0.10, 10.00))
|
|
69
|
+
mgamma = float(np.clip(p.get("mask_gamma", 1.0), 0.10, 10.00))
|
|
70
|
+
iters = int(np.clip(p.get("iterations", 2), 1, 10))
|
|
71
|
+
except Exception:
|
|
72
|
+
n_scales = int(p.get("n_scales", 6))
|
|
73
|
+
boost = float(p.get("boost_factor", 5.0))
|
|
74
|
+
mgamma = float(p.get("mask_gamma", 1.0))
|
|
75
|
+
iters = int(p.get("iterations", 2))
|
|
76
|
+
|
|
77
|
+
params = {
|
|
78
|
+
"n_scales": n_scales,
|
|
79
|
+
"boost_factor": boost,
|
|
80
|
+
"mask_gamma": mgamma,
|
|
81
|
+
"iterations": iters,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# --- store for Replay (prefer unified helper) ---
|
|
85
|
+
try:
|
|
86
|
+
remember = getattr(main, "remember_last_headless_command", None)
|
|
87
|
+
if remember is None:
|
|
88
|
+
remember = getattr(main, "_remember_last_headless_command", None)
|
|
89
|
+
if callable(remember):
|
|
90
|
+
remember("wavescale_dark_enhance", params, description="WaveScale Dark Enhance")
|
|
91
|
+
else:
|
|
92
|
+
setattr(main, "_last_headless_command", {
|
|
93
|
+
"command_id": "wavescale_dark_enhance",
|
|
94
|
+
"preset": dict(params),
|
|
95
|
+
})
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
# resolve target doc
|
|
100
|
+
from setiastro.saspro.headless_utils import normalize_headless_main, unwrap_docproxy
|
|
101
|
+
|
|
102
|
+
main, doc, _dm = normalize_headless_main(main, target_doc)
|
|
103
|
+
if doc is None or getattr(doc, "image", None) is None:
|
|
104
|
+
from PyQt6.QtCore import QCoreApplication
|
|
105
|
+
QMessageBox.warning(main or None, QCoreApplication.translate("WaveScaleDSEPresetDialog", "WaveScale Dark Enhancer"), QCoreApplication.translate("WaveScaleDSEPresetDialog", "Load an image first."))
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
# pull & normalize image like the dialog
|
|
109
|
+
base = np.asarray(doc.image, dtype=np.float32)
|
|
110
|
+
was_mono = False
|
|
111
|
+
mono_shape = None
|
|
112
|
+
if base.ndim == 2:
|
|
113
|
+
was_mono = True
|
|
114
|
+
mono_shape = base.shape
|
|
115
|
+
img = np.repeat(base[:, :, None], 3, axis=2)
|
|
116
|
+
elif base.ndim == 3 and base.shape[2] == 1:
|
|
117
|
+
was_mono = True
|
|
118
|
+
mono_shape = base.shape
|
|
119
|
+
img = np.repeat(base, 3, axis=2)
|
|
120
|
+
else:
|
|
121
|
+
img = base[:, :, :3]
|
|
122
|
+
|
|
123
|
+
if base.dtype.kind in "ui":
|
|
124
|
+
mx = float(np.nanmax(img)) or 1.0
|
|
125
|
+
img = img / max(1.0, mx)
|
|
126
|
+
img = np.clip(img, 0.0, 1.0).astype(np.float32, copy=False)
|
|
127
|
+
|
|
128
|
+
# fetch active doc mask → 2D [0..1], resized to image (same logic as dialog)
|
|
129
|
+
doc_mask = _get_doc_active_mask_2d(doc, target_hw=img.shape[:2])
|
|
130
|
+
|
|
131
|
+
# compute (limit enhancement with external mask, same as UI preview)
|
|
132
|
+
out, _mask_used = compute_wavescale_dse(
|
|
133
|
+
img,
|
|
134
|
+
n_scales=n_scales,
|
|
135
|
+
boost_factor=boost,
|
|
136
|
+
mask_gamma=mgamma,
|
|
137
|
+
iterations=iters,
|
|
138
|
+
external_mask=doc_mask
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# if a doc mask exists, blend final result with original (exactly like dialog)
|
|
142
|
+
if doc_mask is not None:
|
|
143
|
+
m3 = np.repeat(doc_mask[:, :, None], 3, axis=2).astype(np.float32)
|
|
144
|
+
blended = img * (1.0 - m3) + out * m3
|
|
145
|
+
else:
|
|
146
|
+
blended = out
|
|
147
|
+
|
|
148
|
+
# collapse back to mono if needed (like dialog apply)
|
|
149
|
+
result = blended
|
|
150
|
+
if was_mono:
|
|
151
|
+
mono = np.mean(result, axis=2, dtype=np.float32)
|
|
152
|
+
if mono_shape and len(mono_shape) == 3 and mono_shape[2] == 1:
|
|
153
|
+
mono = mono[:, :, None]
|
|
154
|
+
result = mono
|
|
155
|
+
|
|
156
|
+
result = np.clip(result, 0.0, 1.0).astype(np.float32, copy=False)
|
|
157
|
+
|
|
158
|
+
# apply to document (undoable + metadata)
|
|
159
|
+
meta = {
|
|
160
|
+
"step_name": "WaveScale Dark Enhance",
|
|
161
|
+
"wavescale_dark_enhance": dict(params),
|
|
162
|
+
"masked": bool(doc_mask is not None),
|
|
163
|
+
"mask_blend": "m*out + (1-m)*src" if doc_mask is not None else "none",
|
|
164
|
+
"bit_depth": "32-bit floating point",
|
|
165
|
+
"is_mono": (result.ndim == 2 or (result.ndim == 3 and result.shape[2] == 1)),
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
if hasattr(doc, "apply_edit"):
|
|
170
|
+
doc.apply_edit(result, step_name="WaveScale Dark Enhance", metadata=meta)
|
|
171
|
+
elif hasattr(doc, "set_image"):
|
|
172
|
+
doc.set_image(result, step_name="WaveScale Dark Enhance")
|
|
173
|
+
elif hasattr(doc, "apply_numpy"):
|
|
174
|
+
doc.apply_numpy(result, step_name="WaveScale Dark Enhance")
|
|
175
|
+
else:
|
|
176
|
+
doc.image = result
|
|
177
|
+
except Exception as e:
|
|
178
|
+
from PyQt6.QtCore import QCoreApplication
|
|
179
|
+
QMessageBox.critical(main, QCoreApplication.translate("WaveScaleDSEPresetDialog", "WaveScale Dark Enhancer"), QCoreApplication.translate("WaveScaleDSEPresetDialog", "Failed to write to document:\n{0}").format(e))
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
184
|
+
# Helpers (mirror dialog’s mask resolution)
|
|
185
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
186
|
+
def _get_doc_active_mask_2d(doc, *, target_hw):
|
|
187
|
+
"""
|
|
188
|
+
Return the document's active mask as 2-D float32 [0..1], resized to target_hw.
|
|
189
|
+
"""
|
|
190
|
+
mid = getattr(doc, "active_mask_id", None)
|
|
191
|
+
if not mid:
|
|
192
|
+
return None
|
|
193
|
+
masks = getattr(doc, "masks", {}) or {}
|
|
194
|
+
layer = masks.get(mid)
|
|
195
|
+
if layer is None:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
data = None
|
|
199
|
+
for attr in ("data", "mask", "image", "array"):
|
|
200
|
+
if hasattr(layer, attr):
|
|
201
|
+
val = getattr(layer, attr)
|
|
202
|
+
if val is not None:
|
|
203
|
+
data = val
|
|
204
|
+
break
|
|
205
|
+
if data is None and isinstance(layer, dict):
|
|
206
|
+
for key in ("data", "mask", "image", "array"):
|
|
207
|
+
if key in layer and layer[key] is not None:
|
|
208
|
+
data = layer[key]
|
|
209
|
+
break
|
|
210
|
+
if data is None and isinstance(layer, np.ndarray):
|
|
211
|
+
data = layer
|
|
212
|
+
if data is None:
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
m = np.asarray(data)
|
|
216
|
+
if m.ndim == 3:
|
|
217
|
+
m = m.mean(axis=2)
|
|
218
|
+
|
|
219
|
+
m = m.astype(np.float32, copy=False)
|
|
220
|
+
mx = float(m.max()) if m.size else 1.0
|
|
221
|
+
if mx > 1.0:
|
|
222
|
+
m /= mx
|
|
223
|
+
m = np.clip(m, 0.0, 1.0)
|
|
224
|
+
|
|
225
|
+
H, W = target_hw
|
|
226
|
+
if m.shape != (H, W):
|
|
227
|
+
yi = (np.linspace(0, m.shape[0] - 1, H)).astype(np.int32)
|
|
228
|
+
xi = (np.linspace(0, m.shape[1] - 1, W)).astype(np.int32)
|
|
229
|
+
m = m[yi][:, xi]
|
|
230
|
+
return m
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# pro/wcs_update.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _get_header_from_meta(meta: dict):
|
|
7
|
+
return (
|
|
8
|
+
meta.get("original_header")
|
|
9
|
+
or meta.get("fits_header")
|
|
10
|
+
or meta.get("header")
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _has_sip(header) -> bool:
|
|
15
|
+
"""
|
|
16
|
+
Return True if the header carries any TAN-SIP style distortion keywords.
|
|
17
|
+
"""
|
|
18
|
+
try:
|
|
19
|
+
return any(k in header for k in ("A_ORDER", "B_ORDER", "AP_ORDER", "BP_ORDER"))
|
|
20
|
+
except Exception:
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _strip_wcs_keys(hdr):
|
|
25
|
+
"""
|
|
26
|
+
Remove all WCS-related cards from a header so we can re-write them cleanly.
|
|
27
|
+
"""
|
|
28
|
+
wcs_prefixes = (
|
|
29
|
+
"CTYPE", "CUNIT", "CDELT", "CRPIX", "CRVAL", "PC", "CD",
|
|
30
|
+
"PV", "PS", "LONPOLE", "LATPOLE", "PROJP", "RADESYS", "EQUINOX",
|
|
31
|
+
"A_", "B_", "AP_", "BP_",
|
|
32
|
+
"WCSAXES"
|
|
33
|
+
)
|
|
34
|
+
keys = list(hdr.keys())
|
|
35
|
+
for k in keys:
|
|
36
|
+
up = str(k).upper()
|
|
37
|
+
if any(up.startswith(p) for p in wcs_prefixes):
|
|
38
|
+
del hdr[k]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _pixscale_rot_from_wcs(w):
|
|
42
|
+
"""
|
|
43
|
+
Return (scale_x, scale_y) arcsec/pixel and rotation angle (deg).
|
|
44
|
+
Works for CD or PC+CDELT. Assumes celestial 2D.
|
|
45
|
+
"""
|
|
46
|
+
import numpy as _np
|
|
47
|
+
# build CD matrix
|
|
48
|
+
if w.wcs.has_cd():
|
|
49
|
+
CD = _np.array(w.wcs.cd)
|
|
50
|
+
else:
|
|
51
|
+
# CDELT + PC
|
|
52
|
+
CDELT = _np.array(w.wcs.cdelt)
|
|
53
|
+
PC = _np.array(w.wcs.pc) if w.wcs.pc is not None else _np.eye(2)
|
|
54
|
+
CD = PC @ _np.diag(CDELT)
|
|
55
|
+
# scales are sqrt of column norms; convert deg/pix -> arcsec/pix
|
|
56
|
+
sx = float(np.hypot(CD[0, 0], CD[1, 0])) * 3600.0
|
|
57
|
+
sy = float(np.hypot(CD[0, 1], CD[1, 1])) * 3600.0
|
|
58
|
+
# rotation is atan2 of -CD10, CD00 (TAN convention; aligns with "east-left" images)
|
|
59
|
+
theta = float(np.degrees(np.arctan2(-CD[1, 0], CD[0, 0])))
|
|
60
|
+
return sx, sy, theta
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _needs_2d_coercion(hdr) -> bool:
|
|
64
|
+
"""
|
|
65
|
+
True if the header is effectively 3-D (NAXIS>2 or WCSAXES>2).
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
naxis = int(hdr.get("NAXIS", 2))
|
|
69
|
+
except Exception:
|
|
70
|
+
naxis = 2
|
|
71
|
+
try:
|
|
72
|
+
wcsaxes = int(hdr.get("WCSAXES", naxis))
|
|
73
|
+
except Exception:
|
|
74
|
+
wcsaxes = naxis
|
|
75
|
+
return (naxis > 2) or (wcsaxes > 2)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _coerce_header_to_2d(hdr):
|
|
79
|
+
"""
|
|
80
|
+
Make a 2-D view of a 3-D header so SIP/TAN WCS can be built.
|
|
81
|
+
|
|
82
|
+
We:
|
|
83
|
+
- set NAXIS=2 and WCSAXES=2
|
|
84
|
+
- drop axis-3 specific cards (CRPIX3, CTYPE3, CD3_*, PC3_*, etc.)
|
|
85
|
+
"""
|
|
86
|
+
from astropy.io import fits
|
|
87
|
+
|
|
88
|
+
h2 = fits.Header()
|
|
89
|
+
# copy everything first
|
|
90
|
+
for k, v in hdr.items():
|
|
91
|
+
h2[k] = v
|
|
92
|
+
|
|
93
|
+
# set dimensionality to 2
|
|
94
|
+
h2["NAXIS"] = 2
|
|
95
|
+
h2["WCSAXES"] = 2
|
|
96
|
+
|
|
97
|
+
# kill axis-3 style cards
|
|
98
|
+
kill_prefixes = ("CRPIX3", "CRVAL3", "CDELT3", "CTYPE3", "CUNIT3")
|
|
99
|
+
to_del = []
|
|
100
|
+
for k in h2.keys():
|
|
101
|
+
uk = k.upper()
|
|
102
|
+
if uk in kill_prefixes:
|
|
103
|
+
to_del.append(k)
|
|
104
|
+
elif uk.startswith("CD3_") or uk.startswith("PC3_") or uk.startswith("PV3_") or uk.startswith("PS3_"):
|
|
105
|
+
to_del.append(k)
|
|
106
|
+
elif uk.endswith("3") and uk.startswith("A_"):
|
|
107
|
+
# very unlikely, but be safe
|
|
108
|
+
to_del.append(k)
|
|
109
|
+
elif uk.endswith("3") and uk.startswith("B_"):
|
|
110
|
+
to_del.append(k)
|
|
111
|
+
for k in to_del:
|
|
112
|
+
del h2[k]
|
|
113
|
+
|
|
114
|
+
return h2
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def update_wcs_after_crop(metadata: dict, M_src_to_dst: np.ndarray, out_w: int, out_h: int) -> dict:
|
|
118
|
+
"""
|
|
119
|
+
Refit a WCS (TAN or TAN-SIP) after crop given the src->dst homography.
|
|
120
|
+
|
|
121
|
+
This version also handles the “3-D + SIP” FITS case by coercing the header
|
|
122
|
+
down to 2 dimensions *before* calling astropy.wcs.WCS(...). That is exactly
|
|
123
|
+
the situation that produced:
|
|
124
|
+
|
|
125
|
+
FITS WCS distortion paper lookup tables and SIP distortions only work
|
|
126
|
+
in 2 dimensions...
|
|
127
|
+
"""
|
|
128
|
+
debug = True
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
from astropy.io import fits
|
|
132
|
+
from astropy.wcs import WCS
|
|
133
|
+
from astropy.wcs.utils import fit_wcs_from_points
|
|
134
|
+
from astropy.coordinates import SkyCoord
|
|
135
|
+
import astropy.units as u # noqa: F401
|
|
136
|
+
except Exception:
|
|
137
|
+
if debug:
|
|
138
|
+
print("[WCS-CROP] astropy not available; skipping WCS update.")
|
|
139
|
+
return metadata
|
|
140
|
+
|
|
141
|
+
hdr0 = _get_header_from_meta(metadata)
|
|
142
|
+
if hdr0 is None:
|
|
143
|
+
if debug:
|
|
144
|
+
print("[WCS-CROP] No header found in metadata; skipping.")
|
|
145
|
+
return metadata
|
|
146
|
+
|
|
147
|
+
# Normalize to fits.Header
|
|
148
|
+
if not isinstance(hdr0, fits.Header):
|
|
149
|
+
try:
|
|
150
|
+
tmp = fits.Header()
|
|
151
|
+
for k, v in dict(hdr0).items():
|
|
152
|
+
try:
|
|
153
|
+
tmp[k] = v
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
hdr0 = tmp
|
|
157
|
+
except Exception:
|
|
158
|
+
if debug:
|
|
159
|
+
print("[WCS-CROP] Could not coerce header to fits.Header; skipping.")
|
|
160
|
+
return metadata
|
|
161
|
+
|
|
162
|
+
# ------------------------------------------------------------------
|
|
163
|
+
# 1) build the *old* WCS, but handle "3-D + SIP" first
|
|
164
|
+
# ------------------------------------------------------------------
|
|
165
|
+
hdr_for_wcs = hdr0
|
|
166
|
+
coerced = False
|
|
167
|
+
|
|
168
|
+
# If NAXIS>2 or WCSAXES>2, always coerce to a 2-D celestial view
|
|
169
|
+
if _needs_2d_coercion(hdr0):
|
|
170
|
+
hdr_for_wcs = _coerce_header_to_2d(hdr0)
|
|
171
|
+
coerced = True
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
w_old = WCS(hdr_for_wcs, relax=True)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
# If we *didn't* already coerce, try once more with a 2-D header
|
|
177
|
+
if not coerced:
|
|
178
|
+
try:
|
|
179
|
+
hdr_for_wcs = _coerce_header_to_2d(hdr0)
|
|
180
|
+
w_old = WCS(hdr_for_wcs, relax=True)
|
|
181
|
+
coerced = True
|
|
182
|
+
if debug:
|
|
183
|
+
print("[WCS-CROP] WCS() failed on original header; "
|
|
184
|
+
"succeeded after 2-D coercion.")
|
|
185
|
+
except Exception as e2:
|
|
186
|
+
if debug:
|
|
187
|
+
print(f"[WCS-CROP] WCS() failed even after 2-D coercion: {e2}; skipping.")
|
|
188
|
+
return metadata
|
|
189
|
+
else:
|
|
190
|
+
if debug:
|
|
191
|
+
print(f"[WCS-CROP] WCS() failed: {e}; skipping.")
|
|
192
|
+
return metadata
|
|
193
|
+
|
|
194
|
+
# ------------------------------------------------------------------
|
|
195
|
+
# Grab some "before" stats
|
|
196
|
+
# ------------------------------------------------------------------
|
|
197
|
+
try:
|
|
198
|
+
old_crval = (float(w_old.wcs.crval[0]), float(w_old.wcs.crval[1]))
|
|
199
|
+
old_crpix = (float(w_old.wcs.crpix[0]), float(w_old.wcs.crpix[1]))
|
|
200
|
+
except Exception:
|
|
201
|
+
old_crval = (np.nan, np.nan)
|
|
202
|
+
old_crpix = (np.nan, np.nan)
|
|
203
|
+
try:
|
|
204
|
+
old_sx, old_sy, old_rot = _pixscale_rot_from_wcs(w_old)
|
|
205
|
+
except Exception:
|
|
206
|
+
old_sx = old_sy = old_rot = float("nan")
|
|
207
|
+
|
|
208
|
+
# ------------------------------------------------------------------
|
|
209
|
+
# dst->src inverse homography
|
|
210
|
+
# ------------------------------------------------------------------
|
|
211
|
+
try:
|
|
212
|
+
M_dst_to_src = np.linalg.inv(M_src_to_dst)
|
|
213
|
+
except Exception as e:
|
|
214
|
+
if debug:
|
|
215
|
+
print(f"[WCS-CROP] inv(M) failed: {e}")
|
|
216
|
+
return metadata
|
|
217
|
+
|
|
218
|
+
# ------------------------------------------------------------------
|
|
219
|
+
# sample a grid across output
|
|
220
|
+
# ------------------------------------------------------------------
|
|
221
|
+
nx = min(25, max(5, out_w // max(1, out_w // 25)))
|
|
222
|
+
ny = min(25, max(5, out_h // max(1, out_h // 25)))
|
|
223
|
+
xs = np.linspace(0.5, out_w - 0.5, nx)
|
|
224
|
+
ys = np.linspace(0.5, out_h - 0.5, ny)
|
|
225
|
+
Xn, Yn = np.meshgrid(xs, ys) # shapes (ny, nx)
|
|
226
|
+
ones = np.ones_like(Xn)
|
|
227
|
+
|
|
228
|
+
# NEW->OLD via inverse homography
|
|
229
|
+
Xo_h = (M_dst_to_src[0, 0] * Xn + M_dst_to_src[0, 1] * Yn + M_dst_to_src[0, 2] * ones)
|
|
230
|
+
Yo_h = (M_dst_to_src[1, 0] * Xn + M_dst_to_src[1, 1] * Yn + M_dst_to_src[1, 2] * ones)
|
|
231
|
+
Wo_h = (M_dst_to_src[2, 0] * Xn + M_dst_to_src[2, 1] * Yn + M_dst_to_src[2, 2] * ones)
|
|
232
|
+
Xo = Xo_h / Wo_h
|
|
233
|
+
Yo = Yo_h / Wo_h
|
|
234
|
+
|
|
235
|
+
# ------------------------------------------------------------------
|
|
236
|
+
# Old WCS → sky coords
|
|
237
|
+
# ------------------------------------------------------------------
|
|
238
|
+
try:
|
|
239
|
+
sky = w_old.pixel_to_world(Xo, Yo) # SkyCoord
|
|
240
|
+
if not isinstance(sky, SkyCoord):
|
|
241
|
+
sky = SkyCoord(sky.ra, sky.dec)
|
|
242
|
+
except Exception:
|
|
243
|
+
# fall back to older API
|
|
244
|
+
radec = w_old.wcs_pix2world(np.column_stack([Xo.ravel(), Yo.ravel()]), 0)
|
|
245
|
+
sky = SkyCoord(
|
|
246
|
+
radec[:, 0].reshape(Xo.shape),
|
|
247
|
+
radec[:, 1].reshape(Yo.shape),
|
|
248
|
+
unit="deg"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Flatten to 1-D for fitting
|
|
252
|
+
x_new = Xn.ravel()
|
|
253
|
+
y_new = Yn.ravel()
|
|
254
|
+
sky_flat = sky.reshape(x_new.shape)
|
|
255
|
+
|
|
256
|
+
# ------------------------------------------------------------------
|
|
257
|
+
# SIP degree choice
|
|
258
|
+
# ------------------------------------------------------------------
|
|
259
|
+
use_sip = _has_sip(hdr_for_wcs)
|
|
260
|
+
sip_degree = None
|
|
261
|
+
if use_sip:
|
|
262
|
+
try:
|
|
263
|
+
sip_degree = int(hdr_for_wcs.get("A_ORDER", hdr_for_wcs.get("AP_ORDER", 3)))
|
|
264
|
+
except Exception:
|
|
265
|
+
sip_degree = 3
|
|
266
|
+
|
|
267
|
+
# ------------------------------------------------------------------
|
|
268
|
+
# Fit NEW WCS
|
|
269
|
+
# ------------------------------------------------------------------
|
|
270
|
+
try:
|
|
271
|
+
w_new = fit_wcs_from_points(
|
|
272
|
+
(x_new, y_new),
|
|
273
|
+
sky_flat,
|
|
274
|
+
sip_degree=sip_degree,
|
|
275
|
+
projection='TAN',
|
|
276
|
+
proj_point='center'
|
|
277
|
+
)
|
|
278
|
+
w_new.array_shape = (out_h, out_w)
|
|
279
|
+
except Exception as e:
|
|
280
|
+
if debug:
|
|
281
|
+
print(f"[WCS-CROP] fit_wcs_from_points failed: {e}")
|
|
282
|
+
return metadata
|
|
283
|
+
|
|
284
|
+
# ------------------------------------------------------------------
|
|
285
|
+
# Residuals
|
|
286
|
+
# ------------------------------------------------------------------
|
|
287
|
+
try:
|
|
288
|
+
sky_fit = w_new.pixel_to_world(x_new, y_new)
|
|
289
|
+
sep = sky_flat.separation(sky_fit).arcsecond
|
|
290
|
+
rms_arcsec = float(np.sqrt(np.mean(sep ** 2)))
|
|
291
|
+
p50 = float(np.percentile(sep, 50))
|
|
292
|
+
p95 = float(np.percentile(sep, 95))
|
|
293
|
+
except Exception:
|
|
294
|
+
rms_arcsec = p50 = p95 = float("nan")
|
|
295
|
+
|
|
296
|
+
# ------------------------------------------------------------------
|
|
297
|
+
# After stats
|
|
298
|
+
# ------------------------------------------------------------------
|
|
299
|
+
try:
|
|
300
|
+
new_crval = (float(w_new.wcs.crval[0]), float(w_new.wcs.crval[1]))
|
|
301
|
+
new_crpix = (float(w_new.wcs.crpix[0]), float(w_new.wcs.crpix[1]))
|
|
302
|
+
except Exception:
|
|
303
|
+
new_crval = (np.nan, np.nan)
|
|
304
|
+
new_crpix = (np.nan, np.nan)
|
|
305
|
+
try:
|
|
306
|
+
new_sx, new_sy, new_rot = _pixscale_rot_from_wcs(w_new)
|
|
307
|
+
except Exception:
|
|
308
|
+
new_sx = new_sy = new_rot = float("nan")
|
|
309
|
+
|
|
310
|
+
# ------------------------------------------------------------------
|
|
311
|
+
# Build new header
|
|
312
|
+
# ------------------------------------------------------------------
|
|
313
|
+
new_hdr = hdr0.copy() # start from the original metadata header
|
|
314
|
+
_strip_wcs_keys(new_hdr)
|
|
315
|
+
wcards = w_new.to_header(relax=True)
|
|
316
|
+
for k, v in wcards.items():
|
|
317
|
+
try:
|
|
318
|
+
new_hdr[k] = v
|
|
319
|
+
except Exception:
|
|
320
|
+
pass
|
|
321
|
+
new_hdr["NAXIS"] = 2
|
|
322
|
+
new_hdr["NAXIS1"] = int(out_w)
|
|
323
|
+
new_hdr["NAXIS2"] = int(out_h)
|
|
324
|
+
|
|
325
|
+
# ------------------------------------------------------------------
|
|
326
|
+
# Debug print
|
|
327
|
+
# ------------------------------------------------------------------
|
|
328
|
+
if debug:
|
|
329
|
+
print("[WCS] === BEFORE ===")
|
|
330
|
+
print(f" CRVAL (deg): {old_crval}")
|
|
331
|
+
print(f" CRPIX (pix): {old_crpix}")
|
|
332
|
+
print(f" SCALE (as/px): ({old_sx:.3f}, {old_sy:.3f}) ROT(deg): {old_rot:.3f}")
|
|
333
|
+
print("[WCS] === AFTER ===")
|
|
334
|
+
print(f" CRVAL (deg): {new_crval}")
|
|
335
|
+
print(f" CRPIX (pix): {new_crpix} (image is {out_w}x{out_h})")
|
|
336
|
+
print(f" SCALE (as/px): ({new_sx:.3f}, {new_sy:.3f}) ROT(deg): {new_rot:.3f}")
|
|
337
|
+
print(f" SIP degree: {sip_degree if use_sip else 'None (pure TAN)'}")
|
|
338
|
+
print(f" Fit residuals (arcsec): RMS={rms_arcsec:.3f} p50={p50:.3f} p95={p95:.3f}")
|
|
339
|
+
|
|
340
|
+
# ------------------------------------------------------------------
|
|
341
|
+
# Stash a structured summary for the UI
|
|
342
|
+
# ------------------------------------------------------------------
|
|
343
|
+
debug_summary = {
|
|
344
|
+
"before": {
|
|
345
|
+
"crval_deg": old_crval,
|
|
346
|
+
"crpix_pix": old_crpix,
|
|
347
|
+
"scale_as_per_pix": (old_sx, old_sy),
|
|
348
|
+
"rot_deg": old_rot,
|
|
349
|
+
},
|
|
350
|
+
"after": {
|
|
351
|
+
"crval_deg": new_crval,
|
|
352
|
+
"crpix_pix": new_crpix,
|
|
353
|
+
"scale_as_per_pix": (new_sx, new_sy),
|
|
354
|
+
"rot_deg": new_rot,
|
|
355
|
+
"sip_degree": (sip_degree if use_sip else None),
|
|
356
|
+
"size": (int(out_w), int(out_h)),
|
|
357
|
+
},
|
|
358
|
+
"fit": {
|
|
359
|
+
"rms_arcsec": rms_arcsec,
|
|
360
|
+
"p50_arcsec": p50,
|
|
361
|
+
"p95_arcsec": p95,
|
|
362
|
+
"grid": (int(nx), int(ny)),
|
|
363
|
+
},
|
|
364
|
+
"coerced_to_2d": bool(coerced),
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
out_meta = dict(metadata)
|
|
368
|
+
out_meta["original_header"] = new_hdr
|
|
369
|
+
try:
|
|
370
|
+
out_meta["wcs"] = w_new
|
|
371
|
+
except Exception:
|
|
372
|
+
pass
|
|
373
|
+
out_meta["__wcs_debug__"] = debug_summary
|
|
374
|
+
return out_meta
|