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,554 @@
|
|
|
1
|
+
# pro/stat_stretch.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from PyQt6.QtCore import Qt, QSize, QEvent
|
|
4
|
+
from PyQt6.QtWidgets import (
|
|
5
|
+
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLabel, QDoubleSpinBox,
|
|
6
|
+
QCheckBox, QPushButton, QScrollArea, QWidget, QMessageBox, QSlider, QToolBar, QToolButton
|
|
7
|
+
)
|
|
8
|
+
from PyQt6.QtGui import QImage, QPixmap, QMouseEvent, QCursor
|
|
9
|
+
import numpy as np
|
|
10
|
+
from PyQt6 import sip
|
|
11
|
+
|
|
12
|
+
from .doc_manager import ImageDocument
|
|
13
|
+
# use your existing stretch code
|
|
14
|
+
from setiastro.saspro.imageops.stretch import stretch_mono_image, stretch_color_image
|
|
15
|
+
from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
|
|
16
|
+
|
|
17
|
+
class StatisticalStretchDialog(QDialog):
|
|
18
|
+
"""
|
|
19
|
+
Non-destructive preview; Apply commits to the document image.
|
|
20
|
+
"""
|
|
21
|
+
def __init__(self, parent, document: ImageDocument):
|
|
22
|
+
super().__init__(parent)
|
|
23
|
+
self.setWindowTitle(self.tr("Statistical Stretch"))
|
|
24
|
+
|
|
25
|
+
# --- IMPORTANT: avoid “attached modal” behavior on some Linux WMs ---
|
|
26
|
+
# Make this a proper top-level window (tool-style) rather than an attached sheet.
|
|
27
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
28
|
+
# Non-modal: allow user to switch between images while dialog is open
|
|
29
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
30
|
+
# Don’t let the generic modal flag override the explicit modality
|
|
31
|
+
self.setModal(False)
|
|
32
|
+
try:
|
|
33
|
+
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
34
|
+
except Exception:
|
|
35
|
+
pass # older PyQt6 versions
|
|
36
|
+
self._main = parent
|
|
37
|
+
self.doc = document
|
|
38
|
+
self._last_preview = None
|
|
39
|
+
|
|
40
|
+
self._follow_conn = None
|
|
41
|
+
if hasattr(self._main, "currentDocumentChanged"):
|
|
42
|
+
try:
|
|
43
|
+
# store connection so we can cleanly disconnect
|
|
44
|
+
self._main.currentDocumentChanged.connect(self._on_active_doc_changed)
|
|
45
|
+
self._follow_conn = True
|
|
46
|
+
except Exception:
|
|
47
|
+
self._follow_conn = None
|
|
48
|
+
self._panning = False
|
|
49
|
+
self._pan_last = None # QPoint
|
|
50
|
+
self._preview_scale = 1.0 # NEW: zoom factor for preview
|
|
51
|
+
self._preview_qimg = None # NEW: store unscaled QImage for clean scaling
|
|
52
|
+
self._suppress_replay_record = False
|
|
53
|
+
|
|
54
|
+
# --- Controls ---
|
|
55
|
+
self.spin_target = QDoubleSpinBox()
|
|
56
|
+
self.spin_target.setRange(0.01, 0.99)
|
|
57
|
+
self.spin_target.setSingleStep(0.01)
|
|
58
|
+
self.spin_target.setValue(0.25)
|
|
59
|
+
self.spin_target.setDecimals(3)
|
|
60
|
+
|
|
61
|
+
self.chk_linked = QCheckBox(self.tr("Linked channels"))
|
|
62
|
+
self.chk_linked.setChecked(False)
|
|
63
|
+
|
|
64
|
+
self.chk_normalize = QCheckBox(self.tr("Normalize to [0..1]"))
|
|
65
|
+
self.chk_normalize.setChecked(False)
|
|
66
|
+
|
|
67
|
+
# NEW: Curves boost
|
|
68
|
+
self.chk_curves = QCheckBox(self.tr("Curves boost"))
|
|
69
|
+
self.chk_curves.setChecked(False)
|
|
70
|
+
|
|
71
|
+
self.curves_row = QWidget()
|
|
72
|
+
cr_lay = QHBoxLayout(self.curves_row); cr_lay.setContentsMargins(0,0,0,0)
|
|
73
|
+
cr_lay.setSpacing(8)
|
|
74
|
+
cr_lay.addWidget(QLabel(self.tr("Strength:")))
|
|
75
|
+
self.sld_curves = QSlider(Qt.Orientation.Horizontal)
|
|
76
|
+
self.sld_curves.setRange(0, 100) # 0.00 … 1.00 mapped to 0…100
|
|
77
|
+
self.sld_curves.setSingleStep(1)
|
|
78
|
+
self.sld_curves.setPageStep(5)
|
|
79
|
+
self.sld_curves.setValue(20) # default 0.20
|
|
80
|
+
self.lbl_curves_val = QLabel("0.20")
|
|
81
|
+
self.sld_curves.valueChanged.connect(lambda v: self.lbl_curves_val.setText(f"{v/100:.2f}"))
|
|
82
|
+
cr_lay.addWidget(self.sld_curves, 1)
|
|
83
|
+
cr_lay.addWidget(self.lbl_curves_val)
|
|
84
|
+
self.curves_row.setEnabled(False) # disabled until checkbox is ticked
|
|
85
|
+
self.chk_curves.toggled.connect(self.curves_row.setEnabled)
|
|
86
|
+
|
|
87
|
+
# Preview area
|
|
88
|
+
self.preview_label = QLabel(alignment=Qt.AlignmentFlag.AlignCenter)
|
|
89
|
+
self.preview_label.setMinimumSize(QSize(320, 240))
|
|
90
|
+
self.preview_label.setScaledContents(False)
|
|
91
|
+
self.preview_scroll = QScrollArea()
|
|
92
|
+
self.preview_scroll.setWidgetResizable(False) # <- was True; we manage size
|
|
93
|
+
self.preview_scroll.setWidget(self.preview_label)
|
|
94
|
+
self.preview_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
|
95
|
+
self.preview_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
|
96
|
+
|
|
97
|
+
self._fit_mode = True # NEW: start in Fit mode
|
|
98
|
+
|
|
99
|
+
# --- Zoom buttons row (place before the main layout or right above preview) ---
|
|
100
|
+
# --- Zoom buttons row ---
|
|
101
|
+
zoom_row = QHBoxLayout()
|
|
102
|
+
|
|
103
|
+
# Use themed tool buttons (consistent with the rest of SASpro)
|
|
104
|
+
self.btn_zoom_out = themed_toolbtn("zoom-out", "Zoom Out")
|
|
105
|
+
self.btn_zoom_in = themed_toolbtn("zoom-in", "Zoom In")
|
|
106
|
+
self.btn_zoom_100 = themed_toolbtn("zoom-original", "1:1")
|
|
107
|
+
self.btn_zoom_fit = themed_toolbtn("zoom-fit-best", "Fit")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
zoom_row.addStretch(1)
|
|
111
|
+
for b in (self.btn_zoom_out, self.btn_zoom_in, self.btn_zoom_100, self.btn_zoom_fit):
|
|
112
|
+
zoom_row.addWidget(b)
|
|
113
|
+
zoom_row.addStretch(1)
|
|
114
|
+
|
|
115
|
+
# Buttons
|
|
116
|
+
self.btn_preview = QPushButton(self.tr("Preview"))
|
|
117
|
+
self.btn_apply = QPushButton(self.tr("Apply"))
|
|
118
|
+
self.btn_close = QPushButton(self.tr("Close"))
|
|
119
|
+
|
|
120
|
+
self.btn_preview.clicked.connect(self._do_preview)
|
|
121
|
+
self.btn_apply.clicked.connect(self._do_apply)
|
|
122
|
+
self.btn_close.clicked.connect(self.close)
|
|
123
|
+
|
|
124
|
+
# --- Layout ---
|
|
125
|
+
form = QFormLayout()
|
|
126
|
+
form.addRow(self.tr("Target median:"), self.spin_target)
|
|
127
|
+
form.addRow("", self.chk_linked)
|
|
128
|
+
form.addRow("", self.chk_normalize)
|
|
129
|
+
form.addRow("", self.chk_curves)
|
|
130
|
+
form.addRow("", self.curves_row)
|
|
131
|
+
|
|
132
|
+
left = QVBoxLayout()
|
|
133
|
+
left.addLayout(form)
|
|
134
|
+
row = QHBoxLayout()
|
|
135
|
+
row.addWidget(self.btn_preview)
|
|
136
|
+
row.addWidget(self.btn_apply)
|
|
137
|
+
row.addStretch(1)
|
|
138
|
+
left.addLayout(row)
|
|
139
|
+
left.addStretch(1)
|
|
140
|
+
|
|
141
|
+
main = QHBoxLayout(self)
|
|
142
|
+
main.addLayout(left, 0)
|
|
143
|
+
|
|
144
|
+
# NEW: right column with zoom row + preview
|
|
145
|
+
right = QVBoxLayout()
|
|
146
|
+
right.addLayout(zoom_row) # ← actually add the zoom controls
|
|
147
|
+
right.addWidget(self.preview_scroll, 1) # preview below the buttons
|
|
148
|
+
main.addLayout(right, 1)
|
|
149
|
+
|
|
150
|
+
self.btn_zoom_in.clicked.connect(lambda: self._zoom_by(1.25))
|
|
151
|
+
self.btn_zoom_out.clicked.connect(lambda: self._zoom_by(1/1.25))
|
|
152
|
+
self.btn_zoom_100.clicked.connect(self._zoom_reset_100)
|
|
153
|
+
self.btn_zoom_fit.clicked.connect(self._fit_preview)
|
|
154
|
+
|
|
155
|
+
self.preview_scroll.viewport().installEventFilter(self)
|
|
156
|
+
self.preview_label.installEventFilter(self)
|
|
157
|
+
|
|
158
|
+
self._populate_initial_preview()
|
|
159
|
+
|
|
160
|
+
# ----- helpers -----
|
|
161
|
+
def _get_source_float(self) -> np.ndarray:
|
|
162
|
+
"""
|
|
163
|
+
Return a float32 array scaled into ~[0..1] for stretching.
|
|
164
|
+
"""
|
|
165
|
+
src = np.asarray(self.doc.image)
|
|
166
|
+
if src is None or src.size == 0:
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
if np.issubdtype(src.dtype, np.integer):
|
|
170
|
+
# Assume 16-bit astro sources by default; adjust if you prefer
|
|
171
|
+
scale = 65535.0 if src.dtype.itemsize >= 2 else 255.0
|
|
172
|
+
return (src.astype(np.float32) / scale).clip(0, 1)
|
|
173
|
+
else:
|
|
174
|
+
f = src.astype(np.float32)
|
|
175
|
+
# If values are way above 1 (linear calibrated data), compress softly
|
|
176
|
+
mx = float(f.max()) if f.size else 1.0
|
|
177
|
+
if mx > 5.0:
|
|
178
|
+
f = f / mx
|
|
179
|
+
return f
|
|
180
|
+
|
|
181
|
+
def _apply_current_zoom(self):
|
|
182
|
+
"""Apply the current zoom mode (fit or manual) to the preview image."""
|
|
183
|
+
if self._preview_qimg is None:
|
|
184
|
+
return
|
|
185
|
+
if self._fit_mode:
|
|
186
|
+
self._fit_preview()
|
|
187
|
+
else:
|
|
188
|
+
self._update_preview_scaled()
|
|
189
|
+
|
|
190
|
+
def _fit_preview(self):
|
|
191
|
+
"""Fit the image into the visible scroll viewport."""
|
|
192
|
+
if self._preview_qimg is None:
|
|
193
|
+
return
|
|
194
|
+
vp = self.preview_scroll.viewport().size()
|
|
195
|
+
if vp.width() <= 1 or vp.height() <= 1:
|
|
196
|
+
return
|
|
197
|
+
iw, ih = self._preview_qimg.width(), self._preview_qimg.height()
|
|
198
|
+
if iw <= 0 or ih <= 0:
|
|
199
|
+
return
|
|
200
|
+
# compute scale to fit
|
|
201
|
+
sx = vp.width() / iw
|
|
202
|
+
sy = vp.height() / ih
|
|
203
|
+
self._preview_scale = max(0.05, min(sx, sy))
|
|
204
|
+
self._fit_mode = True
|
|
205
|
+
self._update_preview_scaled()
|
|
206
|
+
|
|
207
|
+
def _zoom_reset_100(self):
|
|
208
|
+
"""Set zoom to 100% (1:1)."""
|
|
209
|
+
self._fit_mode = False
|
|
210
|
+
self._preview_scale = 1.0
|
|
211
|
+
self._update_preview_scaled()
|
|
212
|
+
|
|
213
|
+
def _zoom_by(self, factor: float):
|
|
214
|
+
"""Incremental zoom around the current center; exits Fit mode."""
|
|
215
|
+
self._fit_mode = False
|
|
216
|
+
new_scale = self._preview_scale * float(factor)
|
|
217
|
+
self._preview_scale = max(0.05, min(new_scale, 8.0))
|
|
218
|
+
self._update_preview_scaled()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# --- MASK helpers ----------------------------------------------------
|
|
222
|
+
def _active_mask_array(self) -> np.ndarray | None:
|
|
223
|
+
"""Return active mask as float32 [H,W] in 0..1, resized to doc image."""
|
|
224
|
+
try:
|
|
225
|
+
mid = getattr(self.doc, "active_mask_id", None)
|
|
226
|
+
if not mid:
|
|
227
|
+
return None
|
|
228
|
+
layer = getattr(self.doc, "masks", {}).get(mid)
|
|
229
|
+
if layer is None:
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
m = np.asarray(getattr(layer, "data", None))
|
|
233
|
+
if m is None or m.size == 0:
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
# squeeze to 2D
|
|
237
|
+
if m.ndim == 3 and m.shape[2] == 1:
|
|
238
|
+
m = m[..., 0]
|
|
239
|
+
elif m.ndim == 3: # RGB/whatever → luminance
|
|
240
|
+
m = (0.2126*m[...,0] + 0.7152*m[...,1] + 0.0722*m[...,2])
|
|
241
|
+
|
|
242
|
+
m = m.astype(np.float32, copy=False)
|
|
243
|
+
# normalize if integer / out-of-range
|
|
244
|
+
if m.dtype.kind in "ui":
|
|
245
|
+
m /= float(np.iinfo(m.dtype).max)
|
|
246
|
+
m = np.clip(m, 0.0, 1.0)
|
|
247
|
+
|
|
248
|
+
th, tw = self.doc.image.shape[:2]
|
|
249
|
+
sh, sw = m.shape[:2]
|
|
250
|
+
if (sh, sw) != (th, tw):
|
|
251
|
+
yi = (np.linspace(0, sh-1, th)).astype(np.int32)
|
|
252
|
+
xi = (np.linspace(0, sw-1, tw)).astype(np.int32)
|
|
253
|
+
m = m[yi][:, xi]
|
|
254
|
+
|
|
255
|
+
# honor opacity if present
|
|
256
|
+
opacity = float(getattr(layer, "opacity", 1.0) or 1.0)
|
|
257
|
+
if opacity < 1.0:
|
|
258
|
+
m *= opacity
|
|
259
|
+
return m
|
|
260
|
+
except Exception:
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
def _blend_with_mask(self, base: np.ndarray, out: np.ndarray, mask: np.ndarray) -> np.ndarray:
|
|
264
|
+
"""base/out can be mono or 3ch; mask is [H,W] in 0..1."""
|
|
265
|
+
if out.ndim == 3 and out.shape[2] == 3:
|
|
266
|
+
m = mask[..., None]
|
|
267
|
+
else:
|
|
268
|
+
m = mask
|
|
269
|
+
return base * (1.0 - m) + out * m
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _run_stretch(self) -> np.ndarray | None:
|
|
273
|
+
imgf = self._get_source_float()
|
|
274
|
+
if imgf is None:
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
target = float(self.spin_target.value())
|
|
278
|
+
linked = bool(self.chk_linked.isChecked())
|
|
279
|
+
normalize = bool(self.chk_normalize.isChecked())
|
|
280
|
+
apply_curves = bool(self.chk_curves.isChecked())
|
|
281
|
+
curves_boost = float(self.sld_curves.value()) / 100.0
|
|
282
|
+
|
|
283
|
+
if imgf.ndim == 2 or (imgf.ndim == 3 and imgf.shape[2] == 1):
|
|
284
|
+
out = stretch_mono_image(
|
|
285
|
+
imgf.squeeze(),
|
|
286
|
+
target_median=target,
|
|
287
|
+
normalize=normalize,
|
|
288
|
+
apply_curves=apply_curves,
|
|
289
|
+
curves_boost=curves_boost,
|
|
290
|
+
)
|
|
291
|
+
else:
|
|
292
|
+
out = stretch_color_image(
|
|
293
|
+
imgf,
|
|
294
|
+
target_median=target,
|
|
295
|
+
linked=linked,
|
|
296
|
+
normalize=normalize,
|
|
297
|
+
apply_curves=apply_curves,
|
|
298
|
+
curves_boost=curves_boost,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# ✅ If a mask is active, blend stretched result with original
|
|
302
|
+
m = self._active_mask_array()
|
|
303
|
+
if m is not None:
|
|
304
|
+
base = imgf.astype(np.float32, copy=False)
|
|
305
|
+
out = self._blend_with_mask(base, out, m)
|
|
306
|
+
|
|
307
|
+
return out
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _set_preview_pixmap(self, arr: np.ndarray):
|
|
311
|
+
vis = arr
|
|
312
|
+
if vis is None or vis.size == 0:
|
|
313
|
+
self.preview_label.clear()
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
# Ensure 3 channels for display
|
|
317
|
+
if vis.ndim == 2:
|
|
318
|
+
vis3 = np.stack([vis] * 3, axis=-1)
|
|
319
|
+
elif vis.ndim == 3 and vis.shape[2] == 1:
|
|
320
|
+
vis3 = np.repeat(vis, 3, axis=2)
|
|
321
|
+
else:
|
|
322
|
+
vis3 = vis
|
|
323
|
+
|
|
324
|
+
# Convert to 8-bit RGB
|
|
325
|
+
if vis3.dtype == np.uint8:
|
|
326
|
+
buf8 = vis3
|
|
327
|
+
elif vis3.dtype == np.uint16:
|
|
328
|
+
buf8 = (vis3.astype(np.float32) / 65535.0 * 255.0).clip(0, 255).astype(np.uint8)
|
|
329
|
+
else:
|
|
330
|
+
buf8 = (np.clip(vis3, 0.0, 1.0) * 255.0).astype(np.uint8)
|
|
331
|
+
|
|
332
|
+
# Must be C-contiguous for QImage
|
|
333
|
+
buf8 = np.ascontiguousarray(buf8)
|
|
334
|
+
h, w, _ = buf8.shape
|
|
335
|
+
bytes_per_line = buf8.strides[0]
|
|
336
|
+
|
|
337
|
+
# Build QImage from raw pointer; keep references alive
|
|
338
|
+
self._last_preview = buf8 # keep backing store alive
|
|
339
|
+
ptr = sip.voidptr(self._last_preview.ctypes.data)
|
|
340
|
+
qimg = QImage(ptr, w, h, bytes_per_line, QImage.Format.Format_RGB888)
|
|
341
|
+
|
|
342
|
+
self._preview_qimg = qimg
|
|
343
|
+
self._apply_current_zoom()
|
|
344
|
+
|
|
345
|
+
# ----- active document change -----
|
|
346
|
+
def _on_active_doc_changed(self, doc):
|
|
347
|
+
"""Called when user clicks a different image window."""
|
|
348
|
+
if doc is None or getattr(doc, "image", None) is None:
|
|
349
|
+
return
|
|
350
|
+
self.doc = doc
|
|
351
|
+
self._populate_initial_preview()
|
|
352
|
+
|
|
353
|
+
# ----- slots -----
|
|
354
|
+
def _populate_initial_preview(self):
|
|
355
|
+
# show the current (unstretched) image as baseline
|
|
356
|
+
src = self._get_source_float()
|
|
357
|
+
if src is not None:
|
|
358
|
+
self._set_preview_pixmap(np.clip(src, 0, 1))
|
|
359
|
+
|
|
360
|
+
def _do_preview(self):
|
|
361
|
+
try:
|
|
362
|
+
out = self._run_stretch()
|
|
363
|
+
if out is None:
|
|
364
|
+
QMessageBox.information(self, "No image", "No image is loaded in the active document.")
|
|
365
|
+
return
|
|
366
|
+
self._set_preview_pixmap(out)
|
|
367
|
+
except Exception as e:
|
|
368
|
+
QMessageBox.warning(self, "Preview failed", str(e))
|
|
369
|
+
|
|
370
|
+
def _do_apply(self):
|
|
371
|
+
try:
|
|
372
|
+
out = self._run_stretch()
|
|
373
|
+
if out is None:
|
|
374
|
+
QMessageBox.information(self, "No image", "No image is loaded in the active document.")
|
|
375
|
+
return
|
|
376
|
+
|
|
377
|
+
# Preserve mono vs color shape
|
|
378
|
+
if out.ndim == 3 and out.shape[2] == 3 and (self.doc.image.ndim == 2 or self.doc.image.shape[-1] == 1):
|
|
379
|
+
out = out[..., 0]
|
|
380
|
+
|
|
381
|
+
# --- Gather current UI state ------------------------------------
|
|
382
|
+
target = float(self.spin_target.value())
|
|
383
|
+
linked = bool(self.chk_linked.isChecked())
|
|
384
|
+
normalize = bool(self.chk_normalize.isChecked())
|
|
385
|
+
apply_curves = bool(getattr(self, "chk_curves", None) and self.chk_curves.isChecked())
|
|
386
|
+
curves_boost = 0.0
|
|
387
|
+
if getattr(self, "sld_curves", None) is not None:
|
|
388
|
+
curves_boost = float(self.sld_curves.value()) / 100.0
|
|
389
|
+
|
|
390
|
+
# Build human-readable step name
|
|
391
|
+
parts = [f"target={target:.2f}", "linked" if linked else "unlinked"]
|
|
392
|
+
if normalize:
|
|
393
|
+
parts.append("norm")
|
|
394
|
+
if apply_curves:
|
|
395
|
+
parts.append(f"curves={curves_boost:.2f}")
|
|
396
|
+
if self._active_mask_array() is not None:
|
|
397
|
+
parts.append("masked")
|
|
398
|
+
step_name = f"Statistical Stretch ({', '.join(parts)})"
|
|
399
|
+
|
|
400
|
+
# Apply to document
|
|
401
|
+
self.doc.apply_edit(out.astype(np.float32, copy=False), step_name=step_name)
|
|
402
|
+
|
|
403
|
+
# Turn off display stretch on the active view, if any
|
|
404
|
+
mw = self.parent()
|
|
405
|
+
if hasattr(mw, "mdi") and mw.mdi.activeSubWindow():
|
|
406
|
+
view = mw.mdi.activeSubWindow().widget()
|
|
407
|
+
if getattr(view, "autostretch_enabled", False):
|
|
408
|
+
view.set_autostretch(False)
|
|
409
|
+
|
|
410
|
+
# Existing logging, now using the same values as above
|
|
411
|
+
if hasattr(mw, "_log"):
|
|
412
|
+
curves_on = apply_curves
|
|
413
|
+
boost_val = curves_boost if curves_on else 0.0
|
|
414
|
+
mw._log(
|
|
415
|
+
"Applied Statistical Stretch "
|
|
416
|
+
f"(target={target:.3f}, linked={linked}, normalize={normalize}, "
|
|
417
|
+
f"curves={'ON' if curves_on else 'OFF'}"
|
|
418
|
+
f"{', boost='+str(round(boost_val,2)) if curves_on else ''}, "
|
|
419
|
+
f"mask={'ON' if self._active_mask_array() is not None else 'OFF'})"
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
# --- Build preset for headless replay ---------------------------
|
|
423
|
+
# --- Build preset for headless replay ---------------------------
|
|
424
|
+
preset = {
|
|
425
|
+
"target_median": target,
|
|
426
|
+
"linked": linked,
|
|
427
|
+
"normalize": normalize,
|
|
428
|
+
"apply_curves": apply_curves,
|
|
429
|
+
"curves_boost": curves_boost,
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
# ✅ Remember this as the last headless-style command
|
|
433
|
+
# (unless we are in a headless/suppressed call)
|
|
434
|
+
suppress = bool(getattr(self, "_suppress_replay_record", False))
|
|
435
|
+
if not suppress:
|
|
436
|
+
from PyQt6.QtWidgets import QMainWindow
|
|
437
|
+
try:
|
|
438
|
+
mw2 = self.parent()
|
|
439
|
+
while mw2 is not None and not isinstance(mw2, QMainWindow):
|
|
440
|
+
mw2 = mw2.parent()
|
|
441
|
+
|
|
442
|
+
if mw2 is not None and hasattr(mw2, "remember_last_headless_command"):
|
|
443
|
+
mw2.remember_last_headless_command(
|
|
444
|
+
command_id="stat_stretch",
|
|
445
|
+
preset=preset,
|
|
446
|
+
description="Statistical Stretch",
|
|
447
|
+
)
|
|
448
|
+
print(f"Remembered Statistical Stretch last headless command: {preset}")
|
|
449
|
+
else:
|
|
450
|
+
print("No main window with remember_last_headless_command; cannot store stat_stretch preset")
|
|
451
|
+
except Exception as e:
|
|
452
|
+
print(f"Failed to remember Statistical Stretch last headless command: {e}")
|
|
453
|
+
else:
|
|
454
|
+
# optional debug
|
|
455
|
+
print("Statistical Stretch: replay recording suppressed for this apply()")
|
|
456
|
+
|
|
457
|
+
self.close()
|
|
458
|
+
return
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
except Exception as e:
|
|
462
|
+
QMessageBox.critical(self, "Apply failed", str(e))
|
|
463
|
+
|
|
464
|
+
def _refresh_document_from_active(self):
|
|
465
|
+
"""
|
|
466
|
+
Refresh the dialog's document reference to the currently active document.
|
|
467
|
+
This allows reusing the same dialog on different images.
|
|
468
|
+
"""
|
|
469
|
+
try:
|
|
470
|
+
main = self.parent()
|
|
471
|
+
if main and hasattr(main, "_active_doc"):
|
|
472
|
+
new_doc = main._active_doc()
|
|
473
|
+
if new_doc is not None and new_doc is not self.doc:
|
|
474
|
+
self.doc = new_doc
|
|
475
|
+
# Reset preview state for new document
|
|
476
|
+
self._last_preview = None
|
|
477
|
+
self._preview_qimg = None
|
|
478
|
+
except Exception:
|
|
479
|
+
pass
|
|
480
|
+
|
|
481
|
+
def closeEvent(self, ev):
|
|
482
|
+
# disconnect the “follow active document” hook
|
|
483
|
+
try:
|
|
484
|
+
if self._follow_conn and hasattr(self._main, "currentDocumentChanged"):
|
|
485
|
+
self._main.currentDocumentChanged.disconnect(self._on_active_doc_changed)
|
|
486
|
+
except Exception:
|
|
487
|
+
pass
|
|
488
|
+
super().closeEvent(ev)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _update_preview_scaled(self):
|
|
492
|
+
if self._preview_qimg is None:
|
|
493
|
+
self.preview_label.clear()
|
|
494
|
+
return
|
|
495
|
+
sw = max(1, int(self._preview_qimg.width() * self._preview_scale))
|
|
496
|
+
sh = max(1, int(self._preview_qimg.height() * self._preview_scale))
|
|
497
|
+
scaled = self._preview_qimg.scaled(
|
|
498
|
+
sw, sh,
|
|
499
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
|
500
|
+
Qt.TransformationMode.SmoothTransformation
|
|
501
|
+
)
|
|
502
|
+
self.preview_label.setPixmap(QPixmap.fromImage(scaled))
|
|
503
|
+
self.preview_label.resize(scaled.size()) # <- crucial for scrollbars
|
|
504
|
+
|
|
505
|
+
def resizeEvent(self, ev):
|
|
506
|
+
super().resizeEvent(ev)
|
|
507
|
+
if self._fit_mode:
|
|
508
|
+
self._fit_preview()
|
|
509
|
+
|
|
510
|
+
def eventFilter(self, obj, ev):
|
|
511
|
+
# Ctrl+wheel zoom
|
|
512
|
+
if ev.type() == QEvent.Type.Wheel and (obj is self.preview_scroll.viewport() or obj is self.preview_label):
|
|
513
|
+
if ev.modifiers() & Qt.KeyboardModifier.ControlModifier:
|
|
514
|
+
factor = 1.25 if ev.angleDelta().y() > 0 else 1/1.25
|
|
515
|
+
self._fit_mode = False # ← ensure we exit Fit mode
|
|
516
|
+
self._preview_scale = max(0.05, min(self._preview_scale * factor, 8.0))
|
|
517
|
+
self._update_preview_scaled()
|
|
518
|
+
return True
|
|
519
|
+
return False
|
|
520
|
+
|
|
521
|
+
# Click+drag pan (left or middle mouse)
|
|
522
|
+
if obj is self.preview_scroll.viewport() or obj is self.preview_label:
|
|
523
|
+
if ev.type() == QEvent.Type.MouseButtonPress:
|
|
524
|
+
if ev.buttons() & (Qt.MouseButton.LeftButton | Qt.MouseButton.MiddleButton):
|
|
525
|
+
self._panning = True
|
|
526
|
+
self._pan_last = ev.position().toPoint()
|
|
527
|
+
# show a "grab" cursor where the drag begins
|
|
528
|
+
if obj is self.preview_label:
|
|
529
|
+
self.preview_label.setCursor(Qt.CursorShape.ClosedHandCursor)
|
|
530
|
+
else:
|
|
531
|
+
self.preview_scroll.viewport().setCursor(Qt.CursorShape.ClosedHandCursor)
|
|
532
|
+
return True
|
|
533
|
+
|
|
534
|
+
elif ev.type() == QEvent.Type.MouseMove and self._panning:
|
|
535
|
+
pos = ev.position().toPoint()
|
|
536
|
+
delta = pos - self._pan_last
|
|
537
|
+
self._pan_last = pos
|
|
538
|
+
|
|
539
|
+
hsb = self.preview_scroll.horizontalScrollBar()
|
|
540
|
+
vsb = self.preview_scroll.verticalScrollBar()
|
|
541
|
+
hsb.setValue(hsb.value() - delta.x())
|
|
542
|
+
vsb.setValue(vsb.value() - delta.y())
|
|
543
|
+
return True
|
|
544
|
+
|
|
545
|
+
elif ev.type() == QEvent.Type.MouseButtonRelease and self._panning:
|
|
546
|
+
self._panning = False
|
|
547
|
+
self._pan_last = None
|
|
548
|
+
# restore cursor
|
|
549
|
+
self.preview_label.unsetCursor()
|
|
550
|
+
self.preview_scroll.viewport().unsetCursor()
|
|
551
|
+
return True
|
|
552
|
+
|
|
553
|
+
return super().eventFilter(obj, ev)
|
|
554
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# pro/status_log_dock.py
|
|
2
|
+
from PyQt6.QtCore import Qt, pyqtSlot
|
|
3
|
+
from PyQt6.QtGui import QTextCursor
|
|
4
|
+
from PyQt6.QtWidgets import (
|
|
5
|
+
QDockWidget, QWidget, QVBoxLayout, QPlainTextEdit, QPushButton, QHBoxLayout
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
class StatusLogDock(QDockWidget):
|
|
9
|
+
MAX_BLOCKS = 2000
|
|
10
|
+
|
|
11
|
+
def __init__(self, parent=None):
|
|
12
|
+
super().__init__(self.tr("Stacking Log"), parent)
|
|
13
|
+
self.setObjectName("StackingLogDock")
|
|
14
|
+
self.setAllowedAreas(
|
|
15
|
+
Qt.DockWidgetArea.BottomDockWidgetArea
|
|
16
|
+
| Qt.DockWidgetArea.LeftDockWidgetArea
|
|
17
|
+
| Qt.DockWidgetArea.RightDockWidgetArea
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
w = QWidget(self)
|
|
21
|
+
lay = QVBoxLayout(w); lay.setContentsMargins(6,6,6,6)
|
|
22
|
+
|
|
23
|
+
self.view = QPlainTextEdit(w)
|
|
24
|
+
self.view.setReadOnly(True)
|
|
25
|
+
self.view.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
|
|
26
|
+
self.view.setStyleSheet(
|
|
27
|
+
"background-color: black; color: white; font-family: Monospace; padding: 6px;"
|
|
28
|
+
)
|
|
29
|
+
lay.addWidget(self.view, 1)
|
|
30
|
+
|
|
31
|
+
row = QHBoxLayout()
|
|
32
|
+
btn_clear = QPushButton("Clear", w)
|
|
33
|
+
btn_clear.clicked.connect(self.view.clear)
|
|
34
|
+
row.addWidget(btn_clear)
|
|
35
|
+
row.addStretch(1)
|
|
36
|
+
lay.addLayout(row)
|
|
37
|
+
|
|
38
|
+
self.setWidget(w)
|
|
39
|
+
|
|
40
|
+
@pyqtSlot(str)
|
|
41
|
+
def append_line(self, message: str):
|
|
42
|
+
doc = self.view.document()
|
|
43
|
+
|
|
44
|
+
# coalesce “Normalizing …” lines (replace last if same prefix)
|
|
45
|
+
if message.startswith("🔄 Normalizing") and doc.blockCount() > 0:
|
|
46
|
+
last = doc.findBlockByNumber(doc.blockCount() - 1)
|
|
47
|
+
if last.isValid() and last.text().startswith("🔄 Normalizing"):
|
|
48
|
+
cur = self.view.textCursor()
|
|
49
|
+
cur.movePosition(QTextCursor.MoveOperation.End)
|
|
50
|
+
cur.movePosition(QTextCursor.MoveOperation.StartOfBlock,
|
|
51
|
+
QTextCursor.MoveMode.KeepAnchor)
|
|
52
|
+
cur.removeSelectedText()
|
|
53
|
+
cur.insertText(message)
|
|
54
|
+
self.view.setTextCursor(cur)
|
|
55
|
+
else:
|
|
56
|
+
self.view.appendPlainText(message)
|
|
57
|
+
else:
|
|
58
|
+
self.view.appendPlainText(message)
|
|
59
|
+
|
|
60
|
+
# trim earliest lines
|
|
61
|
+
if doc.blockCount() > self.MAX_BLOCKS:
|
|
62
|
+
extra = doc.blockCount() - self.MAX_BLOCKS
|
|
63
|
+
cur = self.view.textCursor()
|
|
64
|
+
cur.movePosition(QTextCursor.MoveOperation.Start)
|
|
65
|
+
cur.movePosition(QTextCursor.MoveOperation.Down,
|
|
66
|
+
QTextCursor.MoveMode.KeepAnchor, extra)
|
|
67
|
+
cur.removeSelectedText()
|
|
68
|
+
self.view.setTextCursor(self.view.textCursor())
|
|
69
|
+
|
|
70
|
+
# autoscroll
|
|
71
|
+
sb = self.view.verticalScrollBar()
|
|
72
|
+
sb.setValue(sb.maximum())
|
|
73
|
+
|
|
74
|
+
def show_raise(self):
|
|
75
|
+
self.setVisible(True)
|
|
76
|
+
self.raise_()
|
|
77
|
+
if self.widget():
|
|
78
|
+
self.widget().setFocus()
|