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,541 @@
|
|
|
1
|
+
# pro/nbtorgb_stars.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import os
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from PyQt6.QtCore import (
|
|
7
|
+
Qt, QSize, QEvent, QTimer, QPoint, QThread, pyqtSignal
|
|
8
|
+
)
|
|
9
|
+
from PyQt6.QtWidgets import (
|
|
10
|
+
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QScrollArea,
|
|
11
|
+
QFileDialog, QInputDialog, QMessageBox, QGridLayout, QCheckBox, QSizePolicy,
|
|
12
|
+
QSlider
|
|
13
|
+
)
|
|
14
|
+
from PyQt6.QtGui import (
|
|
15
|
+
QPixmap, QImage, QIcon, QPainter, QPen, QColor, QFont, QFontMetrics,
|
|
16
|
+
QCursor, QMovie
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Legacy I/O (same used elsewhere in SASpro)
|
|
20
|
+
from setiastro.saspro.legacy.image_manager import load_image as legacy_load_image
|
|
21
|
+
|
|
22
|
+
from setiastro.saspro.legacy.numba_utils import applySCNR_numba, adjust_saturation_numba
|
|
23
|
+
from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Optional: your stretch helpers (only used if you’d like to pre-stretch inputs)
|
|
27
|
+
# from imageops.stretch import stretch_mono_image, stretch_color_image
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NBtoRGBStars(QWidget):
|
|
31
|
+
"""
|
|
32
|
+
SASpro version of NB→RGB Stars:
|
|
33
|
+
- Ha/OIII/SII mono (any subset) and/or OSC stars image
|
|
34
|
+
- Ha↔OIII ratio
|
|
35
|
+
- Optional "star stretch"
|
|
36
|
+
- Live preview on the right (with PPP-style zoom/pan/fit)
|
|
37
|
+
- Push final to a new view via DocManager
|
|
38
|
+
"""
|
|
39
|
+
THUMB_ICON_SIZE = QSize(22, 22) # just for button decoration if icon_path provided
|
|
40
|
+
|
|
41
|
+
def __init__(self, doc_manager=None, parent=None, icon_path: str | None = None):
|
|
42
|
+
super().__init__(parent)
|
|
43
|
+
self.doc_manager = doc_manager
|
|
44
|
+
self.setWindowTitle("NB→RGB Stars")
|
|
45
|
+
|
|
46
|
+
if icon_path:
|
|
47
|
+
try:
|
|
48
|
+
self.setWindowIcon(QIcon(icon_path))
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
# raw inputs (float32 ~[0..1])
|
|
53
|
+
self.ha : np.ndarray | None = None
|
|
54
|
+
self.oiii : np.ndarray | None = None
|
|
55
|
+
self.sii : np.ndarray | None = None
|
|
56
|
+
self.osc : np.ndarray | None = None # 3-channel stars-only (optional)
|
|
57
|
+
|
|
58
|
+
# filenames / metadata (best-effort)
|
|
59
|
+
self._file_ha = None
|
|
60
|
+
self._file_oiii = None
|
|
61
|
+
self._file_sii = None
|
|
62
|
+
self._file_osc = None
|
|
63
|
+
|
|
64
|
+
# output
|
|
65
|
+
self.final: np.ndarray | None = None
|
|
66
|
+
|
|
67
|
+
# preview pixmap/zoom state
|
|
68
|
+
self._base_pm: QPixmap | None = None
|
|
69
|
+
self._zoom = 1.0
|
|
70
|
+
self._min_zoom = 0.05
|
|
71
|
+
self._max_zoom = 20.0
|
|
72
|
+
self._panning = False
|
|
73
|
+
self._pan_last: QPoint | None = None
|
|
74
|
+
|
|
75
|
+
self._build_ui()
|
|
76
|
+
|
|
77
|
+
# ---------------- UI ----------------
|
|
78
|
+
def _build_ui(self):
|
|
79
|
+
root = QHBoxLayout(self)
|
|
80
|
+
|
|
81
|
+
# -------- left controls
|
|
82
|
+
left = QVBoxLayout()
|
|
83
|
+
left_host = QWidget(self); left_host.setLayout(left); left_host.setFixedWidth(320)
|
|
84
|
+
|
|
85
|
+
left.addWidget(QLabel(
|
|
86
|
+
"<b>NB→RGB Stars</b><br>"
|
|
87
|
+
"Load Ha / OIII / (optional SII) and/or OSC stars.<br>"
|
|
88
|
+
"Tune ratio and preview; push to a new view."
|
|
89
|
+
))
|
|
90
|
+
|
|
91
|
+
# Load buttons + status labels
|
|
92
|
+
self.btn_ha = QPushButton("Load Ha…"); self.btn_ha.clicked.connect(lambda: self._load_channel("Ha"))
|
|
93
|
+
self.btn_oiii = QPushButton("Load OIII…"); self.btn_oiii.clicked.connect(lambda: self._load_channel("OIII"))
|
|
94
|
+
self.btn_sii = QPushButton("Load SII (optional)…"); self.btn_sii.clicked.connect(lambda: self._load_channel("SII"))
|
|
95
|
+
self.btn_osc = QPushButton("Load OSC stars (optional)…"); self.btn_osc.clicked.connect(lambda: self._load_channel("OSC"))
|
|
96
|
+
|
|
97
|
+
self.lbl_ha = QLabel("No Ha loaded.")
|
|
98
|
+
self.lbl_oiii = QLabel("No OIII loaded.")
|
|
99
|
+
self.lbl_sii = QLabel("No SII loaded.")
|
|
100
|
+
self.lbl_osc = QLabel("No OSC stars loaded.")
|
|
101
|
+
|
|
102
|
+
for lab in (self.lbl_ha, self.lbl_oiii, self.lbl_sii, self.lbl_osc):
|
|
103
|
+
lab.setWordWrap(True); lab.setStyleSheet("color:#888; margin-left:8px;")
|
|
104
|
+
|
|
105
|
+
for btn, lab in ((self.btn_ha, self.lbl_ha),
|
|
106
|
+
(self.btn_oiii, self.lbl_oiii),
|
|
107
|
+
(self.btn_sii, self.lbl_sii),
|
|
108
|
+
(self.btn_osc, self.lbl_osc)):
|
|
109
|
+
left.addWidget(btn); left.addWidget(lab)
|
|
110
|
+
|
|
111
|
+
# Ratio (Ha to OIII)
|
|
112
|
+
row = QHBoxLayout()
|
|
113
|
+
self.lbl_ratio = QLabel("Ha:OIII ratio = 0.30")
|
|
114
|
+
self.sld_ratio = QSlider(Qt.Orientation.Horizontal); self.sld_ratio.setRange(0, 100); self.sld_ratio.setValue(30)
|
|
115
|
+
self.sld_ratio.valueChanged.connect(lambda v: self.lbl_ratio.setText(f"Ha:OIII ratio = {v/100:.2f}"))
|
|
116
|
+
row.addWidget(self.lbl_ratio); left.addLayout(row)
|
|
117
|
+
left.addWidget(self.sld_ratio)
|
|
118
|
+
|
|
119
|
+
# Star Stretch
|
|
120
|
+
self.chk_star_stretch = QCheckBox("Enable star stretch"); self.chk_star_stretch.setChecked(True)
|
|
121
|
+
left.addWidget(self.chk_star_stretch)
|
|
122
|
+
|
|
123
|
+
row2 = QHBoxLayout()
|
|
124
|
+
self.lbl_stretch = QLabel("Stretch factor = 5.00")
|
|
125
|
+
self.sld_stretch = QSlider(Qt.Orientation.Horizontal); self.sld_stretch.setRange(0, 800); self.sld_stretch.setValue(500)
|
|
126
|
+
self.sld_stretch.valueChanged.connect(lambda v: self.lbl_stretch.setText(f"Stretch factor = {v/100:.2f}"))
|
|
127
|
+
row2.addWidget(self.lbl_stretch); left.addLayout(row2)
|
|
128
|
+
left.addWidget(self.sld_stretch)
|
|
129
|
+
|
|
130
|
+
row3 = QHBoxLayout()
|
|
131
|
+
self.lbl_sat = QLabel("Saturation = 1.00×")
|
|
132
|
+
self.sld_sat = QSlider(Qt.Orientation.Horizontal)
|
|
133
|
+
self.sld_sat.setRange(0, 300) # 0.00× … 3.00×
|
|
134
|
+
self.sld_sat.setValue(100) # 1.00× by default
|
|
135
|
+
self.sld_sat.valueChanged.connect(lambda v: self.lbl_sat.setText(f"Saturation = {v/100:.2f}×"))
|
|
136
|
+
row3.addWidget(self.lbl_sat)
|
|
137
|
+
left.addLayout(row3)
|
|
138
|
+
left.addWidget(self.sld_sat)
|
|
139
|
+
|
|
140
|
+
# Actions
|
|
141
|
+
act = QHBoxLayout()
|
|
142
|
+
self.btn_preview = QPushButton("Preview Combine"); self.btn_preview.clicked.connect(self._preview_combine)
|
|
143
|
+
self.btn_push = QPushButton("Push Final to New View"); self.btn_push.clicked.connect(self._push_final)
|
|
144
|
+
act.addWidget(self.btn_preview); act.addWidget(self.btn_push)
|
|
145
|
+
left.addLayout(act)
|
|
146
|
+
|
|
147
|
+
self.btn_clear = QPushButton("Clear Inputs"); self.btn_clear.clicked.connect(self._clear_inputs)
|
|
148
|
+
left.addWidget(self.btn_clear)
|
|
149
|
+
|
|
150
|
+
# Spinner (optional)
|
|
151
|
+
self.spinner = QLabel(alignment=Qt.AlignmentFlag.AlignCenter)
|
|
152
|
+
self.spinner_movie = QMovie(os.path.join(os.path.dirname(__file__), "spinner.gif"))
|
|
153
|
+
self.spinner.setMovie(self.spinner_movie); self.spinner.hide()
|
|
154
|
+
left.addWidget(self.spinner)
|
|
155
|
+
|
|
156
|
+
left.addStretch(1)
|
|
157
|
+
root.addWidget(left_host, 0)
|
|
158
|
+
|
|
159
|
+
# -------- right: preview (zoom/pan like PPP)
|
|
160
|
+
right = QVBoxLayout()
|
|
161
|
+
|
|
162
|
+
tools = QHBoxLayout()
|
|
163
|
+
|
|
164
|
+
self.btn_zoom_in = themed_toolbtn("zoom-in", "Zoom In")
|
|
165
|
+
self.btn_zoom_out = themed_toolbtn("zoom-out", "Zoom Out")
|
|
166
|
+
self.btn_fit = themed_toolbtn("zoom-fit-best", "Fit to Preview")
|
|
167
|
+
|
|
168
|
+
self.btn_zoom_in.clicked.connect(lambda: self._zoom_at(1.25))
|
|
169
|
+
self.btn_zoom_out.clicked.connect(lambda: self._zoom_at(0.8))
|
|
170
|
+
self.btn_fit.clicked.connect(self._fit_to_preview)
|
|
171
|
+
|
|
172
|
+
tools.addWidget(self.btn_zoom_in)
|
|
173
|
+
tools.addWidget(self.btn_zoom_out)
|
|
174
|
+
tools.addWidget(self.btn_fit)
|
|
175
|
+
right.addLayout(tools)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
self.scroll = QScrollArea(self); self.scroll.setWidgetResizable(True)
|
|
179
|
+
self.scroll.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
180
|
+
self.preview = QLabel(alignment=Qt.AlignmentFlag.AlignCenter)
|
|
181
|
+
self.scroll.setWidget(self.preview)
|
|
182
|
+
self.preview.setMouseTracking(True)
|
|
183
|
+
|
|
184
|
+
# Intercept wheel everywhere to prevent scroll while zooming; pan via drag
|
|
185
|
+
for obj in (self.preview, self.scroll, self.scroll.viewport(),
|
|
186
|
+
self.scroll.horizontalScrollBar(), self.scroll.verticalScrollBar()):
|
|
187
|
+
obj.installEventFilter(self)
|
|
188
|
+
|
|
189
|
+
right.addWidget(self.scroll, 1)
|
|
190
|
+
self.status = QLabel(""); right.addWidget(self.status, 0)
|
|
191
|
+
|
|
192
|
+
right_host = QWidget(self); right_host.setLayout(right)
|
|
193
|
+
root.addWidget(right_host, 1)
|
|
194
|
+
|
|
195
|
+
self.setLayout(root)
|
|
196
|
+
self.setMinimumSize(980, 640)
|
|
197
|
+
|
|
198
|
+
def showEvent(self, e):
|
|
199
|
+
super().showEvent(e)
|
|
200
|
+
QTimer.singleShot(0, self._center_scrollbars)
|
|
201
|
+
|
|
202
|
+
# ---------- file/view loading ----------
|
|
203
|
+
def _set_status_label(self, which: str, text: str | None):
|
|
204
|
+
lab = getattr(self, f"lbl_{which.lower()}")
|
|
205
|
+
if text:
|
|
206
|
+
lab.setText(text); lab.setStyleSheet("color:#2a7; font-weight:600; margin-left:8px;")
|
|
207
|
+
else:
|
|
208
|
+
lab.setText(f"No {which} loaded."); lab.setStyleSheet("color:#888; margin-left:8px;")
|
|
209
|
+
|
|
210
|
+
def _load_channel(self, which: str):
|
|
211
|
+
src, ok = QInputDialog.getItem(self, f"Load {which}", "Source:", ["From View", "From File"], 0, False)
|
|
212
|
+
if not ok: return
|
|
213
|
+
|
|
214
|
+
out = self._load_from_view(which) if src == "From View" else self._load_from_file(which)
|
|
215
|
+
if out is None: return
|
|
216
|
+
img, header, bit_depth, is_mono, path, label = out
|
|
217
|
+
|
|
218
|
+
# Normalize to floats in [0,1]; collapse mono to 2D; ensure OSC is RGB
|
|
219
|
+
if which in ("Ha","OIII","SII"):
|
|
220
|
+
if img.ndim == 3: img = img[...,0]
|
|
221
|
+
setattr(self, which.lower(), self._as_float01(img))
|
|
222
|
+
else: # OSC
|
|
223
|
+
# Optimization: Store mono OSC as-is (2D) to save memory
|
|
224
|
+
# The combine step will handle expansion.
|
|
225
|
+
if img.ndim == 3 and img.shape[2] == 1:
|
|
226
|
+
img = img[..., 0]
|
|
227
|
+
setattr(self, which.lower(), self._as_float01(img))
|
|
228
|
+
|
|
229
|
+
setattr(self, f"_file_{which.lower()}", path)
|
|
230
|
+
self._set_status_label(which, label)
|
|
231
|
+
self.status.setText(f"{which} loaded ({'mono' if img.ndim==2 else 'RGB'}) shape={img.shape}")
|
|
232
|
+
|
|
233
|
+
def _load_from_view(self, which):
|
|
234
|
+
views = self._list_open_views()
|
|
235
|
+
if not views:
|
|
236
|
+
QMessageBox.warning(self, "No Views", "No open image views found."); return None
|
|
237
|
+
labels = [lab for lab, _ in views]
|
|
238
|
+
choice, ok = QInputDialog.getItem(self, f"Select View for {which}", "Choose a view:", labels, 0, False)
|
|
239
|
+
if not ok or not choice: return None
|
|
240
|
+
sw = dict(views)[choice]
|
|
241
|
+
doc = getattr(sw, "document", None)
|
|
242
|
+
if doc is None or getattr(doc, "image", None) is None:
|
|
243
|
+
QMessageBox.warning(self, "Empty View", "Selected view has no image."); return None
|
|
244
|
+
img = doc.image
|
|
245
|
+
meta = getattr(doc, "metadata", {}) or {}
|
|
246
|
+
header = meta.get("original_header", None)
|
|
247
|
+
bit_depth = meta.get("bit_depth", "Unknown")
|
|
248
|
+
is_mono = (img.ndim == 2) or (img.ndim == 3 and img.shape[2] == 1)
|
|
249
|
+
path = meta.get("file_path", None)
|
|
250
|
+
return img, header, bit_depth, is_mono, path, f"From View: {choice}"
|
|
251
|
+
|
|
252
|
+
def _load_from_file(self, which):
|
|
253
|
+
filt = "Images (*.png *.tif *.tiff *.fits *.fit *.xisf)"
|
|
254
|
+
path, _ = QFileDialog.getOpenFileName(self, f"Select {which} File", "", filt)
|
|
255
|
+
if not path: return None
|
|
256
|
+
img, header, bit_depth, is_mono = legacy_load_image(path)
|
|
257
|
+
if img is None:
|
|
258
|
+
QMessageBox.critical(self, "Load Error", f"Could not load {os.path.basename(path)}"); return None
|
|
259
|
+
return img, header, bit_depth, is_mono, path, f"From File: {os.path.basename(path)}"
|
|
260
|
+
|
|
261
|
+
# ---------- combine / preview ----------
|
|
262
|
+
def _preview_combine(self):
|
|
263
|
+
if (self.osc is None) and not (self.ha is not None and self.oiii is not None):
|
|
264
|
+
QMessageBox.warning(self, "Missing Images", "Load OSC, or Ha+OIII (SII optional).")
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
self.spinner.show(); self.spinner_movie.start()
|
|
268
|
+
|
|
269
|
+
ratio = self.sld_ratio.value() / 100.0
|
|
270
|
+
stretch_enabled = self.chk_star_stretch.isChecked()
|
|
271
|
+
stretch_factor = self.sld_stretch.value() / 100.0
|
|
272
|
+
sat_factor = self.sld_sat.value() / 100.0
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
# 1) combine (no SCNR here)
|
|
276
|
+
rgb = self._combine_nb_rgb(ratio, stretch_enabled, stretch_factor)
|
|
277
|
+
|
|
278
|
+
# 2) ensure float32 & contiguous for numba
|
|
279
|
+
rgb = np.ascontiguousarray(rgb.astype(np.float32))
|
|
280
|
+
|
|
281
|
+
# 3) SCNR (numba)
|
|
282
|
+
rgb = applySCNR_numba(rgb)
|
|
283
|
+
|
|
284
|
+
# 4) Saturation (numba)
|
|
285
|
+
if abs(sat_factor - 1.0) > 1e-3:
|
|
286
|
+
rgb = adjust_saturation_numba(rgb, sat_factor)
|
|
287
|
+
|
|
288
|
+
self.final = np.clip(rgb, 0.0, 1.0)
|
|
289
|
+
|
|
290
|
+
except Exception as e:
|
|
291
|
+
self.spinner.hide(); self.spinner_movie.stop()
|
|
292
|
+
QMessageBox.critical(self, "Combine Error", str(e))
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
self._set_preview_image(self._to_qimage(self.final))
|
|
296
|
+
self.status.setText("Preview updated.")
|
|
297
|
+
self.spinner.hide(); self.spinner_movie.stop()
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _combine_nb_rgb(self, ratio: float, star_stretch: bool, stretch_k: float) -> np.ndarray:
|
|
301
|
+
"""
|
|
302
|
+
Combine to RGB:
|
|
303
|
+
- If OSC present: use channels from OSC, optionally blend Ha/SII/OO into them.
|
|
304
|
+
- Else NB-only: R ~ 0.5*(Ha+SII), G ~ mix(Ha, OIII) via ratio, B ~ OIII.
|
|
305
|
+
Shapes must match.
|
|
306
|
+
"""
|
|
307
|
+
# Ensure shapes
|
|
308
|
+
shapes = [x.shape[:2] for x in (self.ha, self.oiii, self.sii) if x is not None]
|
|
309
|
+
if self.osc is not None:
|
|
310
|
+
shapes.append(self.osc.shape[:2])
|
|
311
|
+
if shapes and len(set(shapes)) != 1:
|
|
312
|
+
raise ValueError(f"Channel sizes differ: {set(shapes)}")
|
|
313
|
+
|
|
314
|
+
if self.osc is not None:
|
|
315
|
+
if self.osc.ndim == 2:
|
|
316
|
+
r = self.osc; g = self.osc; b = self.osc
|
|
317
|
+
elif self.osc.ndim == 3 and self.osc.shape[2] >= 3:
|
|
318
|
+
r = self.osc[...,0]; g = self.osc[...,1]; b = self.osc[...,2]
|
|
319
|
+
else:
|
|
320
|
+
# fallback for unexpected shapes (e.g. 3D but 1-channel)
|
|
321
|
+
r = self.osc.squeeze(); g = r; b = r
|
|
322
|
+
|
|
323
|
+
sii = self.sii if self.sii is not None else r
|
|
324
|
+
ha = self.ha if self.ha is not None else r
|
|
325
|
+
oiii= self.oiii if self.oiii is not None else b
|
|
326
|
+
|
|
327
|
+
r_out = 0.5*r + 0.5*sii
|
|
328
|
+
g_out = ratio*ha + (1.0 - ratio)*g
|
|
329
|
+
b_out = oiii
|
|
330
|
+
else:
|
|
331
|
+
if self.ha is None or self.oiii is None:
|
|
332
|
+
raise ValueError("Need Ha and OIII if no OSC image is provided.")
|
|
333
|
+
ha = self.ha
|
|
334
|
+
sii = self.sii if self.sii is not None else ha
|
|
335
|
+
oiii = self.oiii
|
|
336
|
+
r_out = 0.5*ha + 0.5*sii
|
|
337
|
+
g_out = ratio*ha + (1.0 - ratio)*oiii
|
|
338
|
+
b_out = oiii
|
|
339
|
+
|
|
340
|
+
rgb = np.stack([r_out, g_out, b_out], axis=2).astype(np.float32)
|
|
341
|
+
rgb = np.clip(rgb, 0, 1)
|
|
342
|
+
|
|
343
|
+
if star_stretch:
|
|
344
|
+
# Simple non-linear boost; bounded and monotonic
|
|
345
|
+
# ((3^k)*x) / ((3^k - 1)*x + 1)
|
|
346
|
+
t = 3.0 ** float(stretch_k)
|
|
347
|
+
rgb = (t*rgb) / ((t - 1.0)*rgb + 1.0)
|
|
348
|
+
rgb = np.clip(rgb, 0, 1)
|
|
349
|
+
|
|
350
|
+
return rgb
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# ---------- preview helpers + zoom/pan ----------
|
|
354
|
+
def _set_preview_image(self, qimg: QImage):
|
|
355
|
+
self._base_pm = QPixmap.fromImage(qimg)
|
|
356
|
+
self._zoom = 1.0
|
|
357
|
+
self._update_preview_pixmap()
|
|
358
|
+
QTimer.singleShot(0, self._center_scrollbars)
|
|
359
|
+
|
|
360
|
+
def _update_preview_pixmap(self):
|
|
361
|
+
if self._base_pm is None: return
|
|
362
|
+
scaled = self._base_pm.scaled(
|
|
363
|
+
self._base_pm.size() * self._zoom,
|
|
364
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
|
365
|
+
Qt.TransformationMode.SmoothTransformation
|
|
366
|
+
)
|
|
367
|
+
self.preview.setPixmap(scaled)
|
|
368
|
+
self.preview.resize(scaled.size())
|
|
369
|
+
|
|
370
|
+
def _set_zoom(self, new_zoom: float):
|
|
371
|
+
self._zoom = max(self._min_zoom, min(self._max_zoom, new_zoom))
|
|
372
|
+
self._update_preview_pixmap()
|
|
373
|
+
|
|
374
|
+
def _zoom_at(self, factor: float = 1.25, anchor_vp: QPoint | None = None):
|
|
375
|
+
if self._base_pm is None: return
|
|
376
|
+
|
|
377
|
+
old_zoom = self._zoom
|
|
378
|
+
new_zoom = max(self._min_zoom, min(self._max_zoom, old_zoom * factor))
|
|
379
|
+
ratio = new_zoom / max(1e-6, old_zoom)
|
|
380
|
+
|
|
381
|
+
vp = self.scroll.viewport()
|
|
382
|
+
if anchor_vp is None:
|
|
383
|
+
anchor_vp = QPoint(vp.width() // 2, vp.height() // 2) # center of view
|
|
384
|
+
|
|
385
|
+
hbar = self.scroll.horizontalScrollBar()
|
|
386
|
+
vbar = self.scroll.verticalScrollBar()
|
|
387
|
+
|
|
388
|
+
content_x = hbar.value() + anchor_vp.x()
|
|
389
|
+
content_y = vbar.value() + anchor_vp.y()
|
|
390
|
+
|
|
391
|
+
self._set_zoom(new_zoom)
|
|
392
|
+
|
|
393
|
+
if self.preview.width() <= vp.width():
|
|
394
|
+
hbar.setValue((hbar.maximum() + hbar.minimum()) // 2)
|
|
395
|
+
else:
|
|
396
|
+
new_h = int(content_x * ratio - anchor_vp.x())
|
|
397
|
+
hbar.setValue(max(hbar.minimum(), min(hbar.maximum(), new_h)))
|
|
398
|
+
|
|
399
|
+
if self.preview.height() <= vp.height():
|
|
400
|
+
vbar.setValue((vbar.maximum() + vbar.minimum()) // 2)
|
|
401
|
+
else:
|
|
402
|
+
new_v = int(content_y * ratio - anchor_vp.y())
|
|
403
|
+
vbar.setValue(max(vbar.minimum(), min(vbar.maximum(), new_v)))
|
|
404
|
+
|
|
405
|
+
def _fit_to_preview(self):
|
|
406
|
+
if self._base_pm is None: return
|
|
407
|
+
vp = self.scroll.viewport().size()
|
|
408
|
+
pm = self._base_pm.size()
|
|
409
|
+
if pm.width() == 0 or pm.height() == 0: return
|
|
410
|
+
k = min(vp.width() / pm.width(), vp.height() / pm.height())
|
|
411
|
+
self._set_zoom(max(self._min_zoom, min(self._max_zoom, k)))
|
|
412
|
+
self._center_scrollbars()
|
|
413
|
+
|
|
414
|
+
def _center_scrollbars(self):
|
|
415
|
+
h = self.scroll.horizontalScrollBar()
|
|
416
|
+
v = self.scroll.verticalScrollBar()
|
|
417
|
+
h.setValue((h.maximum() + h.minimum()) // 2)
|
|
418
|
+
v.setValue((v.maximum() + v.minimum()) // 2)
|
|
419
|
+
|
|
420
|
+
# ---------- utilities ----------
|
|
421
|
+
def _clear_inputs(self):
|
|
422
|
+
self.ha = self.oiii = self.sii = self.osc = None
|
|
423
|
+
self._file_ha = self._file_oiii = self._file_sii = self._file_osc = None
|
|
424
|
+
self.final = None
|
|
425
|
+
self.preview.clear(); self._base_pm = None
|
|
426
|
+
for which in ("Ha","OIII","SII","OSC"):
|
|
427
|
+
self._set_status_label(which, None)
|
|
428
|
+
self.status.setText("Cleared inputs.")
|
|
429
|
+
|
|
430
|
+
@staticmethod
|
|
431
|
+
def _as_float01(arr):
|
|
432
|
+
a = np.asarray(arr)
|
|
433
|
+
if a.dtype == np.uint8: return a.astype(np.float32)/255.0
|
|
434
|
+
if a.dtype == np.uint16: return a.astype(np.float32)/65535.0
|
|
435
|
+
return np.clip(a.astype(np.float32), 0.0, 1.0)
|
|
436
|
+
|
|
437
|
+
@staticmethod
|
|
438
|
+
def _to_qimage(arr):
|
|
439
|
+
a = np.clip(arr, 0, 1)
|
|
440
|
+
if a.ndim == 2:
|
|
441
|
+
u = (a * 255).astype(np.uint8); h, w = u.shape
|
|
442
|
+
return QImage(u.data, w, h, w, QImage.Format.Format_Grayscale8).copy()
|
|
443
|
+
if a.ndim == 3 and a.shape[2] == 3:
|
|
444
|
+
u = (a * 255).astype(np.uint8); h, w, _ = u.shape
|
|
445
|
+
return QImage(u.data, w, h, w*3, QImage.Format.Format_RGB888).copy()
|
|
446
|
+
raise ValueError(f"Unexpected image shape: {a.shape}")
|
|
447
|
+
|
|
448
|
+
def _find_main_window(self):
|
|
449
|
+
w = self
|
|
450
|
+
from PyQt6.QtWidgets import QMainWindow, QApplication
|
|
451
|
+
while w is not None and not isinstance(w, QMainWindow):
|
|
452
|
+
w = w.parentWidget()
|
|
453
|
+
if w: return w
|
|
454
|
+
for tlw in QApplication.topLevelWidgets():
|
|
455
|
+
if isinstance(tlw, QMainWindow):
|
|
456
|
+
return tlw
|
|
457
|
+
return None
|
|
458
|
+
|
|
459
|
+
def _list_open_views(self):
|
|
460
|
+
mw = self._find_main_window()
|
|
461
|
+
if not mw: return []
|
|
462
|
+
try:
|
|
463
|
+
from setiastro.saspro.subwindow import ImageSubWindow
|
|
464
|
+
subs = mw.findChildren(ImageSubWindow)
|
|
465
|
+
except Exception:
|
|
466
|
+
subs = []
|
|
467
|
+
out = []
|
|
468
|
+
for sw in subs:
|
|
469
|
+
title = getattr(sw, "view_title", None) or sw.windowTitle() or getattr(sw.document, "display_name", lambda: "Untitled")()
|
|
470
|
+
out.append((str(title), sw))
|
|
471
|
+
return out
|
|
472
|
+
|
|
473
|
+
def _push_final(self):
|
|
474
|
+
if self.final is None:
|
|
475
|
+
QMessageBox.warning(self, "No Image", "Preview first, then push."); return
|
|
476
|
+
mw = self._find_main_window()
|
|
477
|
+
dm = getattr(mw, "docman", None)
|
|
478
|
+
if not mw or not dm:
|
|
479
|
+
QMessageBox.critical(self, "UI", "Main window or DocManager not available."); return
|
|
480
|
+
title = "NB→RGB Stars"
|
|
481
|
+
try:
|
|
482
|
+
if hasattr(dm, "open_array"):
|
|
483
|
+
doc = dm.open_array(self.final, metadata={"is_mono": False}, title=title)
|
|
484
|
+
elif hasattr(dm, "create_document"):
|
|
485
|
+
doc = dm.create_document(image=self.final, metadata={"is_mono": False}, name=title)
|
|
486
|
+
else:
|
|
487
|
+
raise RuntimeError("DocManager lacks open_array/create_document")
|
|
488
|
+
if hasattr(mw, "_spawn_subwindow_for"):
|
|
489
|
+
mw._spawn_subwindow_for(doc)
|
|
490
|
+
else:
|
|
491
|
+
from setiastro.saspro.subwindow import ImageSubWindow
|
|
492
|
+
sw = ImageSubWindow(doc, parent=mw); sw.setWindowTitle(title); sw.show()
|
|
493
|
+
self.status.setText("Opened final composite in a new view.")
|
|
494
|
+
except Exception as e:
|
|
495
|
+
QMessageBox.critical(self, "Error", f"Failed to open new view:\n{e}")
|
|
496
|
+
|
|
497
|
+
# ---------- event filter (zoom/pan like PPP) ----------
|
|
498
|
+
def eventFilter(self, obj, ev):
|
|
499
|
+
# Ctrl+wheel zoom at mouse (prevent scrolling); wheel without Ctrl: eat it (no scroll)
|
|
500
|
+
if ev.type() == QEvent.Type.Wheel and (
|
|
501
|
+
obj is self.preview
|
|
502
|
+
or obj is self.scroll
|
|
503
|
+
or obj is self.scroll.viewport()
|
|
504
|
+
or obj is self.scroll.horizontalScrollBar()
|
|
505
|
+
or obj is self.scroll.verticalScrollBar()
|
|
506
|
+
):
|
|
507
|
+
if ev.modifiers() & Qt.KeyboardModifier.ControlModifier:
|
|
508
|
+
factor = 1.25 if ev.angleDelta().y() > 0 else 0.8
|
|
509
|
+
# safer: compute anchor in viewport coords via global position
|
|
510
|
+
try:
|
|
511
|
+
anchor_vp = self.scroll.viewport().mapFromGlobal(ev.globalPosition().toPoint())
|
|
512
|
+
except Exception:
|
|
513
|
+
vp = self.scroll.viewport()
|
|
514
|
+
anchor_vp = QPoint(vp.width()//2, vp.height()//2)
|
|
515
|
+
self._zoom_at(factor, anchor_vp)
|
|
516
|
+
ev.accept()
|
|
517
|
+
return True
|
|
518
|
+
|
|
519
|
+
# click-drag pan on viewport
|
|
520
|
+
if obj is self.scroll.viewport():
|
|
521
|
+
if ev.type() == QEvent.Type.MouseButtonPress and ev.button() == Qt.MouseButton.LeftButton:
|
|
522
|
+
self._panning = True
|
|
523
|
+
self._pan_last = ev.position().toPoint()
|
|
524
|
+
self.scroll.viewport().setCursor(QCursor(Qt.CursorShape.ClosedHandCursor))
|
|
525
|
+
return True
|
|
526
|
+
if ev.type() == QEvent.Type.MouseMove and self._panning:
|
|
527
|
+
cur = ev.position().toPoint()
|
|
528
|
+
delta = cur - (self._pan_last or cur)
|
|
529
|
+
self._pan_last = cur
|
|
530
|
+
h = self.scroll.horizontalScrollBar()
|
|
531
|
+
v = self.scroll.verticalScrollBar()
|
|
532
|
+
h.setValue(h.value() - delta.x())
|
|
533
|
+
v.setValue(v.value() - delta.y())
|
|
534
|
+
return True
|
|
535
|
+
if ev.type() == QEvent.Type.MouseButtonRelease and ev.button() == Qt.MouseButton.LeftButton:
|
|
536
|
+
self._panning = False
|
|
537
|
+
self._pan_last = None
|
|
538
|
+
self.scroll.viewport().setCursor(QCursor(Qt.CursorShape.ArrowCursor))
|
|
539
|
+
return True
|
|
540
|
+
|
|
541
|
+
return super().eventFilter(obj, ev)
|