setiastrosuitepro 1.6.2.post1__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/andromedatry.png +0 -0
- setiastro/images/andromedatry_satellited.png +0 -0
- setiastro/images/annotated.png +0 -0
- setiastro/images/aperture.png +0 -0
- setiastro/images/astrosuite.ico +0 -0
- setiastro/images/astrosuite.png +0 -0
- setiastro/images/astrosuitepro.icns +0 -0
- setiastro/images/astrosuitepro.ico +0 -0
- setiastro/images/astrosuitepro.png +0 -0
- setiastro/images/background.png +0 -0
- setiastro/images/background2.png +0 -0
- setiastro/images/benchmark.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
- setiastro/images/blaster.png +0 -0
- setiastro/images/blink.png +0 -0
- setiastro/images/clahe.png +0 -0
- setiastro/images/collage.png +0 -0
- setiastro/images/colorwheel.png +0 -0
- setiastro/images/contsub.png +0 -0
- setiastro/images/convo.png +0 -0
- setiastro/images/copyslot.png +0 -0
- setiastro/images/cosmic.png +0 -0
- setiastro/images/cosmicsat.png +0 -0
- setiastro/images/crop1.png +0 -0
- setiastro/images/cropicon.png +0 -0
- setiastro/images/curves.png +0 -0
- setiastro/images/cvs.png +0 -0
- setiastro/images/debayer.png +0 -0
- setiastro/images/denoise_cnn_custom.png +0 -0
- setiastro/images/denoise_cnn_graph.png +0 -0
- setiastro/images/disk.png +0 -0
- setiastro/images/dse.png +0 -0
- setiastro/images/exoicon.png +0 -0
- setiastro/images/eye.png +0 -0
- setiastro/images/fliphorizontal.png +0 -0
- setiastro/images/flipvertical.png +0 -0
- setiastro/images/font.png +0 -0
- setiastro/images/freqsep.png +0 -0
- setiastro/images/functionbundle.png +0 -0
- setiastro/images/graxpert.png +0 -0
- setiastro/images/green.png +0 -0
- setiastro/images/gridicon.png +0 -0
- setiastro/images/halo.png +0 -0
- setiastro/images/hdr.png +0 -0
- setiastro/images/histogram.png +0 -0
- setiastro/images/hubble.png +0 -0
- setiastro/images/imagecombine.png +0 -0
- setiastro/images/invert.png +0 -0
- setiastro/images/isophote.png +0 -0
- setiastro/images/isophote_demo_figure.png +0 -0
- setiastro/images/isophote_demo_image.png +0 -0
- setiastro/images/isophote_demo_model.png +0 -0
- setiastro/images/isophote_demo_residual.png +0 -0
- setiastro/images/jwstpupil.png +0 -0
- setiastro/images/linearfit.png +0 -0
- setiastro/images/livestacking.png +0 -0
- setiastro/images/mask.png +0 -0
- setiastro/images/maskapply.png +0 -0
- setiastro/images/maskcreate.png +0 -0
- setiastro/images/maskremove.png +0 -0
- setiastro/images/morpho.png +0 -0
- setiastro/images/mosaic.png +0 -0
- setiastro/images/multiscale_decomp.png +0 -0
- setiastro/images/nbtorgb.png +0 -0
- setiastro/images/neutral.png +0 -0
- setiastro/images/nuke.png +0 -0
- setiastro/images/openfile.png +0 -0
- setiastro/images/pedestal.png +0 -0
- setiastro/images/pen.png +0 -0
- setiastro/images/pixelmath.png +0 -0
- setiastro/images/platesolve.png +0 -0
- setiastro/images/ppp.png +0 -0
- setiastro/images/pro.png +0 -0
- setiastro/images/project.png +0 -0
- setiastro/images/psf.png +0 -0
- setiastro/images/redo.png +0 -0
- setiastro/images/redoicon.png +0 -0
- setiastro/images/rescale.png +0 -0
- setiastro/images/rgbalign.png +0 -0
- setiastro/images/rgbcombo.png +0 -0
- setiastro/images/rgbextract.png +0 -0
- setiastro/images/rotate180.png +0 -0
- setiastro/images/rotateclockwise.png +0 -0
- setiastro/images/rotatecounterclockwise.png +0 -0
- setiastro/images/satellite.png +0 -0
- setiastro/images/script.png +0 -0
- setiastro/images/selectivecolor.png +0 -0
- setiastro/images/simbad.png +0 -0
- setiastro/images/slot0.png +0 -0
- setiastro/images/slot1.png +0 -0
- setiastro/images/slot2.png +0 -0
- setiastro/images/slot3.png +0 -0
- setiastro/images/slot4.png +0 -0
- setiastro/images/slot5.png +0 -0
- setiastro/images/slot6.png +0 -0
- setiastro/images/slot7.png +0 -0
- setiastro/images/slot8.png +0 -0
- setiastro/images/slot9.png +0 -0
- setiastro/images/spcc.png +0 -0
- setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
- setiastro/images/spinner.gif +0 -0
- setiastro/images/stacking.png +0 -0
- setiastro/images/staradd.png +0 -0
- setiastro/images/staralign.png +0 -0
- setiastro/images/starnet.png +0 -0
- setiastro/images/starregistration.png +0 -0
- setiastro/images/starspike.png +0 -0
- setiastro/images/starstretch.png +0 -0
- setiastro/images/statstretch.png +0 -0
- setiastro/images/supernova.png +0 -0
- setiastro/images/uhs.png +0 -0
- setiastro/images/undoicon.png +0 -0
- setiastro/images/upscale.png +0 -0
- setiastro/images/viewbundle.png +0 -0
- setiastro/images/whitebalance.png +0 -0
- setiastro/images/wimi_icon_256x256.png +0 -0
- setiastro/images/wimilogo.png +0 -0
- setiastro/images/wims.png +0 -0
- setiastro/images/wrench_icon.png +0 -0
- setiastro/images/xisfliberator.png +0 -0
- setiastro/qml/ResourceMonitor.qml +126 -0
- setiastro/saspro/__init__.py +20 -0
- setiastro/saspro/__main__.py +945 -0
- setiastro/saspro/_generated/__init__.py +7 -0
- setiastro/saspro/_generated/build_info.py +3 -0
- setiastro/saspro/abe.py +1346 -0
- setiastro/saspro/abe_preset.py +196 -0
- setiastro/saspro/aberration_ai.py +694 -0
- setiastro/saspro/aberration_ai_preset.py +224 -0
- setiastro/saspro/accel_installer.py +218 -0
- setiastro/saspro/accel_workers.py +30 -0
- setiastro/saspro/add_stars.py +624 -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 +602 -0
- setiastro/saspro/batch_convert.py +328 -0
- setiastro/saspro/batch_renamer.py +522 -0
- setiastro/saspro/blemish_blaster.py +491 -0
- setiastro/saspro/blink_comparator_pro.py +2926 -0
- setiastro/saspro/bundles.py +61 -0
- setiastro/saspro/bundles_dock.py +114 -0
- setiastro/saspro/cheat_sheet.py +213 -0
- setiastro/saspro/clahe.py +368 -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 +1617 -0
- setiastro/saspro/convo.py +1400 -0
- setiastro/saspro/convo_preset.py +414 -0
- setiastro/saspro/copyastro.py +190 -0
- setiastro/saspro/cosmicclarity.py +1589 -0
- setiastro/saspro/cosmicclarity_preset.py +407 -0
- setiastro/saspro/crop_dialog_pro.py +973 -0
- setiastro/saspro/crop_preset.py +189 -0
- setiastro/saspro/curve_editor_pro.py +2562 -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 +2664 -0
- setiastro/saspro/exoplanet_detector.py +2166 -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 +1349 -0
- setiastro/saspro/function_bundle.py +1596 -0
- setiastro/saspro/generate_translations.py +3092 -0
- setiastro/saspro/ghs_dialog_pro.py +663 -0
- setiastro/saspro/ghs_preset.py +284 -0
- setiastro/saspro/graxpert.py +637 -0
- setiastro/saspro/graxpert_preset.py +287 -0
- setiastro/saspro/gui/__init__.py +0 -0
- setiastro/saspro/gui/main_window.py +8810 -0
- setiastro/saspro/gui/mixins/__init__.py +33 -0
- setiastro/saspro/gui/mixins/dock_mixin.py +362 -0
- setiastro/saspro/gui/mixins/file_mixin.py +450 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
- setiastro/saspro/gui/mixins/header_mixin.py +441 -0
- setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
- setiastro/saspro/gui/mixins/menu_mixin.py +389 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +1457 -0
- setiastro/saspro/gui/mixins/update_mixin.py +309 -0
- setiastro/saspro/gui/mixins/view_mixin.py +435 -0
- setiastro/saspro/gui/statistics_dialog.py +47 -0
- setiastro/saspro/halobgon.py +488 -0
- setiastro/saspro/header_viewer.py +448 -0
- setiastro/saspro/headless_utils.py +88 -0
- setiastro/saspro/histogram.py +756 -0
- setiastro/saspro/history_explorer.py +941 -0
- setiastro/saspro/i18n.py +168 -0
- setiastro/saspro/image_combine.py +417 -0
- setiastro/saspro/image_peeker_pro.py +1604 -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 +1182 -0
- setiastro/saspro/layers.py +208 -0
- setiastro/saspro/layers_dock.py +714 -0
- setiastro/saspro/lazy_imports.py +193 -0
- setiastro/saspro/legacy/__init__.py +2 -0
- setiastro/saspro/legacy/image_manager.py +2226 -0
- setiastro/saspro/legacy/numba_utils.py +3676 -0
- setiastro/saspro/legacy/xisf.py +1071 -0
- setiastro/saspro/linear_fit.py +537 -0
- setiastro/saspro/live_stacking.py +1841 -0
- setiastro/saspro/log_bus.py +5 -0
- setiastro/saspro/logging_config.py +460 -0
- setiastro/saspro/luminancerecombine.py +309 -0
- setiastro/saspro/main_helpers.py +201 -0
- setiastro/saspro/mask_creation.py +931 -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 +3831 -0
- setiastro/saspro/mfdeconv_earlystop.py +71 -0
- setiastro/saspro/mfdeconvcudnn.py +3263 -0
- setiastro/saspro/mfdeconvsport.py +2382 -0
- setiastro/saspro/minorbodycatalog.py +567 -0
- setiastro/saspro/morphology.py +407 -0
- setiastro/saspro/multiscale_decomp.py +1293 -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 +1102 -0
- setiastro/saspro/ops/scripts.py +1473 -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 +1071 -0
- setiastro/saspro/pipeline.py +110 -0
- setiastro/saspro/pixelmath.py +1604 -0
- setiastro/saspro/plate_solver.py +2445 -0
- setiastro/saspro/project_io.py +797 -0
- setiastro/saspro/psf_utils.py +136 -0
- setiastro/saspro/psf_viewer.py +549 -0
- setiastro/saspro/pyi_rthook_astroquery.py +95 -0
- setiastro/saspro/remove_green.py +331 -0
- setiastro/saspro/remove_stars.py +1599 -0
- setiastro/saspro/remove_stars_preset.py +404 -0
- setiastro/saspro/resources.py +501 -0
- setiastro/saspro/rgb_combination.py +208 -0
- setiastro/saspro/rgb_extract.py +19 -0
- setiastro/saspro/rgbalign.py +723 -0
- setiastro/saspro/runtime_imports.py +7 -0
- setiastro/saspro/runtime_torch.py +754 -0
- setiastro/saspro/save_options.py +73 -0
- setiastro/saspro/selective_color.py +1552 -0
- setiastro/saspro/sfcc.py +1472 -0
- setiastro/saspro/shortcuts.py +3043 -0
- setiastro/saspro/signature_insert.py +1102 -0
- setiastro/saspro/stacking_suite.py +18470 -0
- setiastro/saspro/star_alignment.py +7435 -0
- setiastro/saspro/star_alignment_preset.py +329 -0
- setiastro/saspro/star_metrics.py +49 -0
- setiastro/saspro/star_spikes.py +765 -0
- setiastro/saspro/star_stretch.py +507 -0
- setiastro/saspro/stat_stretch.py +538 -0
- setiastro/saspro/status_log_dock.py +78 -0
- setiastro/saspro/subwindow.py +3328 -0
- setiastro/saspro/supernovaasteroidhunter.py +1719 -0
- setiastro/saspro/swap_manager.py +99 -0
- setiastro/saspro/torch_backend.py +89 -0
- setiastro/saspro/torch_rejection.py +434 -0
- setiastro/saspro/translations/all_source_strings.json +3654 -0
- setiastro/saspro/translations/ar_translations.py +3865 -0
- setiastro/saspro/translations/de_translations.py +3749 -0
- setiastro/saspro/translations/es_translations.py +3939 -0
- setiastro/saspro/translations/fr_translations.py +3858 -0
- setiastro/saspro/translations/hi_translations.py +3571 -0
- setiastro/saspro/translations/integrate_translations.py +270 -0
- setiastro/saspro/translations/it_translations.py +3678 -0
- setiastro/saspro/translations/ja_translations.py +3601 -0
- setiastro/saspro/translations/pt_translations.py +3869 -0
- setiastro/saspro/translations/ru_translations.py +2848 -0
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +255 -0
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +253 -0
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +12520 -0
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +12514 -0
- setiastro/saspro/translations/saspro_hi.qm +0 -0
- setiastro/saspro/translations/saspro_hi.ts +257 -0
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +12520 -0
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +257 -0
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +257 -0
- setiastro/saspro/translations/saspro_ru.qm +0 -0
- setiastro/saspro/translations/saspro_ru.ts +237 -0
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +257 -0
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +10771 -0
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +12520 -0
- setiastro/saspro/translations/sw_translations.py +3671 -0
- setiastro/saspro/translations/uk_translations.py +3700 -0
- setiastro/saspro/translations/zh_translations.py +3675 -0
- setiastro/saspro/versioning.py +77 -0
- setiastro/saspro/view_bundle.py +1558 -0
- setiastro/saspro/wavescale_hdr.py +645 -0
- setiastro/saspro/wavescale_hdr_preset.py +101 -0
- setiastro/saspro/wavescalede.py +680 -0
- setiastro/saspro/wavescalede_preset.py +230 -0
- setiastro/saspro/wcs_update.py +374 -0
- setiastro/saspro/whitebalance.py +492 -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 +986 -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 +237 -0
- setiastro/saspro/widgets/spinboxes.py +275 -0
- setiastro/saspro/widgets/themed_buttons.py +13 -0
- setiastro/saspro/widgets/wavelet_utils.py +331 -0
- setiastro/saspro/wimi.py +7996 -0
- setiastro/saspro/wims.py +578 -0
- setiastro/saspro/window_shelf.py +185 -0
- setiastro/saspro/xisf.py +1123 -0
- setiastrosuitepro-1.6.2.post1.dist-info/METADATA +278 -0
- setiastrosuitepro-1.6.2.post1.dist-info/RECORD +367 -0
- setiastrosuitepro-1.6.2.post1.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.2.post1.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.2.post1.dist-info/licenses/LICENSE +674 -0
- setiastrosuitepro-1.6.2.post1.dist-info/licenses/license.txt +2580 -0
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
# pro/tools/star_spikes.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import numpy as np
|
|
4
|
+
import math
|
|
5
|
+
from PyQt6.QtCore import Qt
|
|
6
|
+
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QSplitter, QSizePolicy, QWidget, QApplication,
|
|
7
|
+
QFormLayout, QGroupBox, QDoubleSpinBox, QSpinBox,
|
|
8
|
+
QMessageBox, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem)
|
|
9
|
+
|
|
10
|
+
from PyQt6.QtGui import QPixmap, QImage, QPainter
|
|
11
|
+
from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
|
|
12
|
+
|
|
13
|
+
# deps
|
|
14
|
+
try:
|
|
15
|
+
import sep
|
|
16
|
+
except Exception as _e_sep:
|
|
17
|
+
sep = None
|
|
18
|
+
try:
|
|
19
|
+
import cv2
|
|
20
|
+
except Exception as _e_cv2:
|
|
21
|
+
cv2 = None
|
|
22
|
+
try:
|
|
23
|
+
from scipy.ndimage import gaussian_filter
|
|
24
|
+
import scipy.ndimage as ndi
|
|
25
|
+
except Exception as _e_scipy:
|
|
26
|
+
gaussian_filter = None
|
|
27
|
+
ndi = None
|
|
28
|
+
|
|
29
|
+
class PreviewView(QGraphicsView):
|
|
30
|
+
def __init__(self, *a, **k):
|
|
31
|
+
super().__init__(*a, **k)
|
|
32
|
+
# drag to pan
|
|
33
|
+
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
|
|
34
|
+
# zoom toward mouse
|
|
35
|
+
self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
|
36
|
+
# when the view resizes, keep the scene centered
|
|
37
|
+
self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter)
|
|
38
|
+
# nicer defaults
|
|
39
|
+
self.setRenderHints(self.renderHints() | QPainter.RenderHint.SmoothPixmapTransform)
|
|
40
|
+
self.setViewportUpdateMode(QGraphicsView.ViewportUpdateMode.SmartViewportUpdate)
|
|
41
|
+
|
|
42
|
+
class StarSpikesDialogPro(QDialog):
|
|
43
|
+
WARN_LIMIT = 1000
|
|
44
|
+
MAX_AUTO_RETRIES = 2
|
|
45
|
+
|
|
46
|
+
def __init__(self, parent=None, doc_manager=None,
|
|
47
|
+
initial_doc=None,
|
|
48
|
+
jwstpupil_path: str | None = None,
|
|
49
|
+
aperture_help_path: str | None = None,
|
|
50
|
+
spinner_path: str | None = None):
|
|
51
|
+
super().__init__(parent)
|
|
52
|
+
self.setWindowTitle(self.tr("Diffraction Spikes"))
|
|
53
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
54
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
55
|
+
self.setModal(False)
|
|
56
|
+
self.docman = doc_manager
|
|
57
|
+
self.doc = initial_doc or (self.docman.get_active_document() if self.docman else None)
|
|
58
|
+
self.jwstpupil_path = jwstpupil_path
|
|
59
|
+
self.aperture_help_path = aperture_help_path
|
|
60
|
+
|
|
61
|
+
self.final_image = None
|
|
62
|
+
self._img_src = None # float32, 2D or 3D, [0..1]
|
|
63
|
+
|
|
64
|
+
# defaults (aligned to your SASv2 tool)
|
|
65
|
+
self.advanced = {
|
|
66
|
+
"flux_max": 300.0, "bscale_min": 10.0, "bscale_max": 30.0,
|
|
67
|
+
"shrink_min": 1.0, "shrink_max": 5.0, "detect_thresh": 5.0,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
self._build_ui()
|
|
71
|
+
self._load_active_image()
|
|
72
|
+
|
|
73
|
+
# ---------- UI ----------
|
|
74
|
+
def _build_ui(self):
|
|
75
|
+
# top-level splitter: controls (left) | preview (right)
|
|
76
|
+
splitter = QSplitter(Qt.Orientation.Horizontal, self)
|
|
77
|
+
splitter.setChildrenCollapsible(False)
|
|
78
|
+
|
|
79
|
+
# ----- LEFT: controls panel (stacked groups) -----
|
|
80
|
+
left = QWidget()
|
|
81
|
+
left_v = QVBoxLayout(left)
|
|
82
|
+
left_v.setContentsMargins(10, 10, 10, 10)
|
|
83
|
+
left_v.setSpacing(10)
|
|
84
|
+
|
|
85
|
+
def dspin(lo, hi, step, val, decimals=2):
|
|
86
|
+
sp = QDoubleSpinBox()
|
|
87
|
+
sp.setRange(lo, hi)
|
|
88
|
+
sp.setSingleStep(step)
|
|
89
|
+
sp.setDecimals(decimals)
|
|
90
|
+
sp.setValue(val)
|
|
91
|
+
sp.setMaximumWidth(140)
|
|
92
|
+
return sp
|
|
93
|
+
|
|
94
|
+
def ispin(lo, hi, step, val):
|
|
95
|
+
sp = QSpinBox()
|
|
96
|
+
sp.setRange(lo, hi)
|
|
97
|
+
sp.setSingleStep(step)
|
|
98
|
+
sp.setValue(val)
|
|
99
|
+
sp.setMaximumWidth(140)
|
|
100
|
+
return sp
|
|
101
|
+
|
|
102
|
+
# --- Group: Star Detection ---
|
|
103
|
+
grp_detect = QGroupBox(self.tr("Star Detection"))
|
|
104
|
+
f_detect = QFormLayout(grp_detect)
|
|
105
|
+
f_detect.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
|
|
106
|
+
f_detect.setFormAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
|
107
|
+
|
|
108
|
+
self.flux_min = dspin(0.0, 999999.0, 10.0, 30.0, decimals=1)
|
|
109
|
+
self.detect_thresh = dspin(0.5, 50.0, 0.5, float(self.advanced.get("detect_thresh", 5.0)), decimals=2)
|
|
110
|
+
self.detect_thresh.setToolTip("σ threshold for SEP detection (higher = fewer stars).")
|
|
111
|
+
# keep self.advanced in sync if user edits
|
|
112
|
+
self.detect_thresh.valueChanged.connect(lambda v: self.advanced.__setitem__("detect_thresh", float(v)))
|
|
113
|
+
|
|
114
|
+
f_detect.addRow(self.tr("Flux Min:"), self.flux_min)
|
|
115
|
+
f_detect.addRow(self.tr("Detection Threshold (σ):"), self.detect_thresh)
|
|
116
|
+
|
|
117
|
+
# --- Group: Aperture (Geometry) ---
|
|
118
|
+
grp_ap = QGroupBox(self.tr("Aperture"))
|
|
119
|
+
f_ap = QFormLayout(grp_ap)
|
|
120
|
+
f_ap.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
|
|
121
|
+
f_ap.setFormAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
|
122
|
+
|
|
123
|
+
self.pupil_jwst = QPushButton("Circular")
|
|
124
|
+
self.pupil_jwst.setCheckable(True)
|
|
125
|
+
self.pupil_jwst.setChecked(False)
|
|
126
|
+
self.pupil_jwst.toggled.connect(lambda on: self._toggle_pupil(on))
|
|
127
|
+
self.pupil_jwst.setToolTip("Toggle between circular aperture and JWST pupil image.")
|
|
128
|
+
self.pupil_jwst.setStyleSheet("""
|
|
129
|
+
QPushButton { min-width: 72px; max-width: 72px; min-height: 28px; max-height: 28px;
|
|
130
|
+
border-radius: 14px; background:#ccc; border:1px solid #999;}
|
|
131
|
+
QPushButton:checked { background:#66bb6a; }
|
|
132
|
+
""")
|
|
133
|
+
f_ap.addRow(self.tr("Aperture Type:"), self.pupil_jwst)
|
|
134
|
+
|
|
135
|
+
self.radius = dspin(1.0, 512.0, 1.0, 128.0, decimals=1)
|
|
136
|
+
self.obstruction = dspin(0.0, 0.99, 0.01, 0.2, decimals=2)
|
|
137
|
+
self.num_vanes = ispin(2, 8, 1, 4)
|
|
138
|
+
self.vane_width = dspin(0.0, 50.0, 0.5, 4.0, decimals=2)
|
|
139
|
+
self.rotation = dspin(0.0, 360.0, 1.0, 0.0, decimals=1)
|
|
140
|
+
|
|
141
|
+
f_ap.addRow(self.tr("Pupil Radius:"), self.radius)
|
|
142
|
+
f_ap.addRow(self.tr("Obstruction:"), self.obstruction)
|
|
143
|
+
f_ap.addRow(self.tr("Number of Vanes:"), self.num_vanes)
|
|
144
|
+
f_ap.addRow(self.tr("Vane Width:"), self.vane_width)
|
|
145
|
+
f_ap.addRow(self.tr("Rotation (deg):"), self.rotation)
|
|
146
|
+
|
|
147
|
+
# --- Group: PSF & Synthesis ---
|
|
148
|
+
grp_psf = QGroupBox(self.tr("PSF & Synthesis"))
|
|
149
|
+
f_psf = QFormLayout(grp_psf)
|
|
150
|
+
f_psf.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
|
|
151
|
+
f_psf.setFormAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
|
152
|
+
|
|
153
|
+
self.color_boost = dspin(0.1, 10.0, 0.1, 1.5, decimals=2)
|
|
154
|
+
self.blur_sigma = dspin(0.1, 10.0, 0.1, 2.0, decimals=2)
|
|
155
|
+
|
|
156
|
+
f_psf.addRow(self.tr("Spike Boost:"), self.color_boost)
|
|
157
|
+
f_psf.addRow(self.tr("PSF Blur Sigma:"), self.blur_sigma)
|
|
158
|
+
|
|
159
|
+
# --- Actions ---
|
|
160
|
+
row_actions = QHBoxLayout()
|
|
161
|
+
row_actions.setSpacing(8)
|
|
162
|
+
self.btn_run = QPushButton(self.tr("Generate Spikes"))
|
|
163
|
+
self.btn_run.clicked.connect(self._run)
|
|
164
|
+
self.btn_apply = QPushButton(self.tr("Apply to Active Document"))
|
|
165
|
+
self.btn_apply.clicked.connect(self._apply_to_doc)
|
|
166
|
+
self.btn_apply.setEnabled(False)
|
|
167
|
+
self.btn_help = QPushButton(self.tr("Aperture Help"))
|
|
168
|
+
self.btn_help.clicked.connect(self._show_help)
|
|
169
|
+
row_actions.addWidget(self.btn_run)
|
|
170
|
+
row_actions.addWidget(self.btn_apply)
|
|
171
|
+
row_actions.addWidget(self.btn_help)
|
|
172
|
+
row_actions.addStretch(1)
|
|
173
|
+
|
|
174
|
+
# --- Status ---
|
|
175
|
+
self.status = QLabel(self.tr("Ready"))
|
|
176
|
+
self.status.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
177
|
+
self.status.setWordWrap(True)
|
|
178
|
+
|
|
179
|
+
# assemble left panel
|
|
180
|
+
left_v.addWidget(grp_detect)
|
|
181
|
+
left_v.addWidget(grp_ap)
|
|
182
|
+
left_v.addWidget(grp_psf)
|
|
183
|
+
left_v.addLayout(row_actions)
|
|
184
|
+
left_v.addWidget(self.status)
|
|
185
|
+
left_v.addStretch(1)
|
|
186
|
+
|
|
187
|
+
splitter.addWidget(left)
|
|
188
|
+
|
|
189
|
+
# ----- RIGHT: preview panel -----
|
|
190
|
+
right = QWidget()
|
|
191
|
+
right_v = QVBoxLayout(right)
|
|
192
|
+
|
|
193
|
+
# zoom toolbar
|
|
194
|
+
zrow = QHBoxLayout()
|
|
195
|
+
self.btn_zoom_in = QPushButton(self.tr("Zoom In"))
|
|
196
|
+
self.btn_zoom_out = QPushButton(self.tr("Zoom Out"))
|
|
197
|
+
self.btn_fit = QPushButton(self.tr("Fit to Preview"))
|
|
198
|
+
self.btn_zoom_in.clicked.connect(self._zoom_in)
|
|
199
|
+
self.btn_zoom_out.clicked.connect(self._zoom_out)
|
|
200
|
+
self.btn_fit.clicked.connect(self._fit_to_preview)
|
|
201
|
+
zrow.addWidget(self.btn_zoom_in)
|
|
202
|
+
zrow.addWidget(self.btn_zoom_out)
|
|
203
|
+
zrow.addWidget(self.btn_fit)
|
|
204
|
+
zrow.addStretch(1)
|
|
205
|
+
right_v.addLayout(zrow)
|
|
206
|
+
|
|
207
|
+
# graphics scene/view
|
|
208
|
+
self.scene = QGraphicsScene()
|
|
209
|
+
self.view = PreviewView()
|
|
210
|
+
self.view.setScene(self.scene)
|
|
211
|
+
self.view.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
212
|
+
self.view.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
|
213
|
+
self.view.setMinimumSize(600, 450)
|
|
214
|
+
self.pix = QGraphicsPixmapItem()
|
|
215
|
+
self.scene.addItem(self.pix)
|
|
216
|
+
right_v.addWidget(self.view, 1)
|
|
217
|
+
|
|
218
|
+
splitter.addWidget(right)
|
|
219
|
+
|
|
220
|
+
# make preview side bigger by default
|
|
221
|
+
splitter.setStretchFactor(0, 0) # left
|
|
222
|
+
splitter.setStretchFactor(1, 1) # right
|
|
223
|
+
splitter.setSizes([360, 900])
|
|
224
|
+
|
|
225
|
+
# top-level layout contains just the splitter
|
|
226
|
+
root = QVBoxLayout(self)
|
|
227
|
+
root.addWidget(splitter)
|
|
228
|
+
|
|
229
|
+
# init pupil visibility
|
|
230
|
+
self._toggle_pupil(False)
|
|
231
|
+
|
|
232
|
+
# zoom state
|
|
233
|
+
self._zoom = 1.0
|
|
234
|
+
self._fit_mode = True # start fitted
|
|
235
|
+
|
|
236
|
+
def _toggle_pupil(self, jwst: bool):
|
|
237
|
+
self.pupil_jwst.setText("JWST" if jwst else "Circular")
|
|
238
|
+
# hide circular-only params when JWST pupil is used
|
|
239
|
+
for w in (self.num_vanes, self.vane_width, self.obstruction, self.radius):
|
|
240
|
+
w.setVisible(not jwst)
|
|
241
|
+
|
|
242
|
+
# ---------- data/preset ----------
|
|
243
|
+
def _load_active_image(self):
|
|
244
|
+
if not self.doc or getattr(self.doc, "image", None) is None:
|
|
245
|
+
self.status.setText("No active image.")
|
|
246
|
+
return
|
|
247
|
+
arr = np.asarray(self.doc.image)
|
|
248
|
+
if arr.dtype != np.float32:
|
|
249
|
+
arr = arr.astype(np.float32, copy=False)
|
|
250
|
+
# strip alpha
|
|
251
|
+
if arr.ndim == 3 and arr.shape[2] == 4:
|
|
252
|
+
arr = arr[..., :3]
|
|
253
|
+
# keep within [0..1] for the math we use
|
|
254
|
+
if np.issubdtype(arr.dtype, np.floating):
|
|
255
|
+
mx = float(arr.max()) if arr.size else 1.0
|
|
256
|
+
if mx > 1.0:
|
|
257
|
+
arr = arr / (65535.0 if mx > 5.0 else mx)
|
|
258
|
+
self._img_src = np.clip(arr, 0.0, 1.0)
|
|
259
|
+
|
|
260
|
+
def apply_preset(self, p: dict):
|
|
261
|
+
if not p:
|
|
262
|
+
return
|
|
263
|
+
self.flux_min.setValue(float(p.get("flux_min", self.flux_min.value())))
|
|
264
|
+
self.advanced["flux_max"] = float(p.get("flux_max", self.advanced["flux_max"]))
|
|
265
|
+
self.advanced["bscale_min"] = float(p.get("bscale_min", self.advanced["bscale_min"]))
|
|
266
|
+
self.advanced["bscale_max"] = float(p.get("bscale_max", self.advanced["bscale_max"]))
|
|
267
|
+
self.advanced["shrink_min"] = float(p.get("shrink_min", self.advanced["shrink_min"]))
|
|
268
|
+
self.advanced["shrink_max"] = float(p.get("shrink_max", self.advanced["shrink_max"]))
|
|
269
|
+
self.advanced["detect_thresh"] = float(p.get("detect_thresh", self.advanced["detect_thresh"]))
|
|
270
|
+
self.detect_thresh.setValue(float(self.advanced["detect_thresh"])) # reflect in UI
|
|
271
|
+
self.radius.setValue(float(p.get("radius", self.radius.value())))
|
|
272
|
+
self.obstruction.setValue(float(p.get("obstruction", self.obstruction.value())))
|
|
273
|
+
self.num_vanes.setValue(int(p.get("num_vanes", self.num_vanes.value())))
|
|
274
|
+
self.vane_width.setValue(float(p.get("vane_width", self.vane_width.value())))
|
|
275
|
+
self.rotation.setValue(float(p.get("rotation", self.rotation.value())))
|
|
276
|
+
self.color_boost.setValue(float(p.get("color_boost", self.color_boost.value())))
|
|
277
|
+
self.blur_sigma.setValue(float(p.get("blur_sigma", self.blur_sigma.value())))
|
|
278
|
+
self.pupil_jwst.setChecked(bool(p.get("jwst", self.pupil_jwst.isChecked())))
|
|
279
|
+
|
|
280
|
+
# ---------- core ----------
|
|
281
|
+
def _run(self):
|
|
282
|
+
if self._img_src is None:
|
|
283
|
+
self._load_active_image()
|
|
284
|
+
if self._img_src is None:
|
|
285
|
+
QMessageBox.information(self, "Diffraction Spikes", "No active image.")
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
# deps check
|
|
289
|
+
if sep is None:
|
|
290
|
+
QMessageBox.critical(self, "Missing Dependency", "python-sep is required for star detection.")
|
|
291
|
+
return
|
|
292
|
+
if gaussian_filter is None or ndi is None:
|
|
293
|
+
QMessageBox.critical(self, "Missing Dependency", "scipy.ndimage is required.")
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
self.status.setText("Detecting stars…")
|
|
297
|
+
QApplication.processEvents()
|
|
298
|
+
img = self._img_src
|
|
299
|
+
# un-stretch via midtones(0.95) for detection
|
|
300
|
+
if img.ndim == 3:
|
|
301
|
+
lin = img.copy()
|
|
302
|
+
for c in range(3):
|
|
303
|
+
lin[..., c] = self._midtones_m(lin[..., c], 0.95)
|
|
304
|
+
base = 0.2126*lin[...,0] + 0.7152*lin[...,1] + 0.0722*lin[...,2]
|
|
305
|
+
else:
|
|
306
|
+
lin = self._midtones_m(img, 0.95)
|
|
307
|
+
base = lin
|
|
308
|
+
|
|
309
|
+
# initial detection
|
|
310
|
+
thresh = float(self.detect_thresh.value())
|
|
311
|
+
stars = self._detect_stars(base,
|
|
312
|
+
threshold=thresh,
|
|
313
|
+
flux_min=self.flux_min.value(),
|
|
314
|
+
size_min=1.0)
|
|
315
|
+
|
|
316
|
+
# interactive guardrail for dense fields
|
|
317
|
+
tries = 0
|
|
318
|
+
while len(stars) > self.WARN_LIMIT and tries < self.MAX_AUTO_RETRIES:
|
|
319
|
+
suggested = min(50.0, max(thresh + 1.0,
|
|
320
|
+
thresh * (len(stars) / float(self.WARN_LIMIT))**0.5))
|
|
321
|
+
msg = QMessageBox(self)
|
|
322
|
+
msg.setWindowTitle("Too Many Stars Detected")
|
|
323
|
+
msg.setIcon(QMessageBox.Icon.Warning)
|
|
324
|
+
msg.setText(f"{len(stars)} stars detected (limit {self.WARN_LIMIT}).\n"
|
|
325
|
+
"Increase the detection threshold to reduce clutter?")
|
|
326
|
+
raise_btn = msg.addButton(f"Raise to σ={suggested:.2f}", QMessageBox.ButtonRole.AcceptRole)
|
|
327
|
+
cont_btn = msg.addButton("Continue Anyway", QMessageBox.ButtonRole.DestructiveRole)
|
|
328
|
+
cancel_btn= msg.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)
|
|
329
|
+
msg.setDefaultButton(raise_btn)
|
|
330
|
+
msg.exec()
|
|
331
|
+
|
|
332
|
+
clicked = msg.clickedButton()
|
|
333
|
+
if clicked is raise_btn:
|
|
334
|
+
thresh = suggested
|
|
335
|
+
self.detect_thresh.setValue(thresh) # reflect in UI
|
|
336
|
+
self.status.setText(f"Re-detecting stars at σ={thresh:.2f}…")
|
|
337
|
+
QApplication.processEvents()
|
|
338
|
+
stars = self._detect_stars(base,
|
|
339
|
+
threshold=thresh,
|
|
340
|
+
flux_min=self.flux_min.value(),
|
|
341
|
+
size_min=1.0)
|
|
342
|
+
tries += 1
|
|
343
|
+
continue
|
|
344
|
+
elif clicked is cont_btn:
|
|
345
|
+
break
|
|
346
|
+
else: # cancel
|
|
347
|
+
self.status.setText("Cancelled.")
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
if len(stars) == 0:
|
|
351
|
+
self.status.setText("No stars found.")
|
|
352
|
+
QMessageBox.information(self, "Diffraction Spikes", "No stars found above flux_min.")
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
self.status.setText(f"Building pupil/PSFs… ({len(stars)} stars)")
|
|
356
|
+
QApplication.processEvents()
|
|
357
|
+
if self.pupil_jwst.isChecked():
|
|
358
|
+
if cv2 is None or not self.jwstpupil_path:
|
|
359
|
+
QMessageBox.critical(self, "Missing JWST Pupil",
|
|
360
|
+
"OpenCV not available or JWST pupil image path missing.")
|
|
361
|
+
return
|
|
362
|
+
pupil = self._load_pupil_from_png(self.jwstpupil_path, size=1024, rotation=self.rotation.value())
|
|
363
|
+
else:
|
|
364
|
+
pupil = self._make_pupil(size=1024,
|
|
365
|
+
radius=self.radius.value(),
|
|
366
|
+
obstruction=self.obstruction.value(),
|
|
367
|
+
vane_width=self.vane_width.value(),
|
|
368
|
+
num_vanes=self.num_vanes.value(),
|
|
369
|
+
rotation=self.rotation.value())
|
|
370
|
+
|
|
371
|
+
psf_r = self._simulate_psf(pupil, wavelength_scale=1.15, blur_sigma=self.blur_sigma.value())
|
|
372
|
+
psf_g = self._simulate_psf(pupil, wavelength_scale=1.00, blur_sigma=self.blur_sigma.value())
|
|
373
|
+
psf_b = self._simulate_psf(pupil, wavelength_scale=0.85, blur_sigma=self.blur_sigma.value())
|
|
374
|
+
|
|
375
|
+
self.status.setText("Synthesizing spikes…")
|
|
376
|
+
QApplication.processEvents()
|
|
377
|
+
H, W = img.shape[:2]
|
|
378
|
+
canvas = np.zeros((H, W, 3), dtype=np.float32)
|
|
379
|
+
|
|
380
|
+
flux_max = self.advanced["flux_max"]
|
|
381
|
+
bscale_min = self.advanced["bscale_min"]
|
|
382
|
+
bscale_max = self.advanced["bscale_max"]
|
|
383
|
+
shrink_min = self.advanced["shrink_min"]
|
|
384
|
+
shrink_max = self.advanced["shrink_max"]
|
|
385
|
+
color_boost = self.color_boost.value()
|
|
386
|
+
|
|
387
|
+
# Try OpenCV for faster zoom/blur
|
|
388
|
+
try:
|
|
389
|
+
import cv2
|
|
390
|
+
_HAS_CV2 = True
|
|
391
|
+
except ImportError:
|
|
392
|
+
_HAS_CV2 = False
|
|
393
|
+
|
|
394
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
395
|
+
def star_runner(x, y, flux, a, b):
|
|
396
|
+
brightness = np.clip(np.log1p(flux)/8.0, 0.1, 3.0)
|
|
397
|
+
tile_size = int(256 + brightness*20)
|
|
398
|
+
tile_size = min(tile_size, 768)
|
|
399
|
+
tile_size += tile_size % 2
|
|
400
|
+
pad = tile_size // 2
|
|
401
|
+
|
|
402
|
+
# Guard against fully out-of-bounds, but allow partial overlaps
|
|
403
|
+
if not (0 <= x < W and 0 <= y < H):
|
|
404
|
+
return None
|
|
405
|
+
|
|
406
|
+
# Measure star color
|
|
407
|
+
r_ratio, g_ratio, b_ratio = self._measure_star_color(img, x, y, sampling_radius=3)
|
|
408
|
+
|
|
409
|
+
# Extract PSF tiles
|
|
410
|
+
tile_r = self._extract_center_tile(psf_r, tile_size) * brightness * r_ratio * color_boost
|
|
411
|
+
tile_g = self._extract_center_tile(psf_g, tile_size) * brightness * g_ratio * color_boost
|
|
412
|
+
tile_b = self._extract_center_tile(psf_b, tile_size) * brightness * b_ratio * color_boost
|
|
413
|
+
|
|
414
|
+
# Boost/Shrink
|
|
415
|
+
b_scale, s_factor = self._boost_shrink_from_flux(flux, self.flux_min.value(), flux_max,
|
|
416
|
+
bscale_min, bscale_max, shrink_min, shrink_max)
|
|
417
|
+
|
|
418
|
+
# --- Fast Resize (Zoom) ---
|
|
419
|
+
def _fast_zoom(arr, z):
|
|
420
|
+
if z == 1.0: return arr
|
|
421
|
+
if _HAS_CV2:
|
|
422
|
+
h, w = arr.shape
|
|
423
|
+
nw, nh = int(round(w * z)), int(round(h * z))
|
|
424
|
+
if nw <= 0 or nh <= 0: return np.zeros((2,2), dtype=np.float32)
|
|
425
|
+
return cv2.resize(arr, (nw, nh), interpolation=cv2.INTER_LINEAR)
|
|
426
|
+
else:
|
|
427
|
+
return ndi.zoom(arr, z, order=1)
|
|
428
|
+
|
|
429
|
+
final_r = np.clip(_fast_zoom(tile_r * b_scale, 1.0/s_factor), 0.0, 1.0)
|
|
430
|
+
final_g = np.clip(_fast_zoom(tile_g * b_scale, 1.0/s_factor), 0.0, 1.0)
|
|
431
|
+
final_b = np.clip(_fast_zoom(tile_b * b_scale, 1.0/s_factor), 0.0, 1.0)
|
|
432
|
+
|
|
433
|
+
# --- Return Patch Data (y, x, patch) ---
|
|
434
|
+
new_h, new_w = final_r.shape
|
|
435
|
+
|
|
436
|
+
# Coords of the *patch top-left* relative to the image
|
|
437
|
+
# The star is at (x,y), and the patch center is approx (new_w//2, new_h//2)
|
|
438
|
+
# We want to center the patch on the star.
|
|
439
|
+
py0 = y - (new_h // 2)
|
|
440
|
+
px0 = x - (new_w // 2)
|
|
441
|
+
|
|
442
|
+
# Combine channels
|
|
443
|
+
patch = np.dstack((final_r, final_g, final_b)).astype(np.float32)
|
|
444
|
+
return (int(py0), int(px0), patch)
|
|
445
|
+
|
|
446
|
+
with ThreadPoolExecutor() as ex:
|
|
447
|
+
futs = [ex.submit(star_runner, *s) for s in stars]
|
|
448
|
+
for f in as_completed(futs):
|
|
449
|
+
res = f.result()
|
|
450
|
+
if res is None:
|
|
451
|
+
continue
|
|
452
|
+
|
|
453
|
+
py0, px0, patch = res
|
|
454
|
+
ph, pw, _ = patch.shape
|
|
455
|
+
|
|
456
|
+
# Calculate intersection with canvas
|
|
457
|
+
y_start = max(0, py0)
|
|
458
|
+
y_end = min(H, py0 + ph)
|
|
459
|
+
x_start = max(0, px0)
|
|
460
|
+
x_end = min(W, px0 + pw)
|
|
461
|
+
|
|
462
|
+
# If no overlap, skip
|
|
463
|
+
if y_start >= y_end or x_start >= x_end:
|
|
464
|
+
continue
|
|
465
|
+
|
|
466
|
+
# Offsets into the patch
|
|
467
|
+
patch_y_start = y_start - py0
|
|
468
|
+
patch_y_end = patch_y_start + (y_end - y_start)
|
|
469
|
+
patch_x_start = x_start - px0
|
|
470
|
+
patch_x_end = patch_x_start + (x_end - x_start)
|
|
471
|
+
|
|
472
|
+
# Add to canvas
|
|
473
|
+
canvas[y_start:y_end, x_start:x_end] += patch[patch_y_start:patch_y_end, patch_x_start:patch_x_end]
|
|
474
|
+
|
|
475
|
+
self.status.setText("Compositing…")
|
|
476
|
+
QApplication.processEvents()
|
|
477
|
+
if lin.ndim == 3:
|
|
478
|
+
spiked_lin = np.clip(lin + canvas, 0, 1)
|
|
479
|
+
else:
|
|
480
|
+
spikes_mono = 0.2126*canvas[...,0] + 0.7152*canvas[...,1] + 0.0722*canvas[...,2]
|
|
481
|
+
spiked_lin = np.clip(lin + spikes_mono, 0, 1)
|
|
482
|
+
|
|
483
|
+
# protect by active mask (document-level)
|
|
484
|
+
if spiked_lin.ndim == 3:
|
|
485
|
+
spiked_final = np.empty_like(spiked_lin)
|
|
486
|
+
for c in range(3):
|
|
487
|
+
spiked_final[..., c] = self._midtones_m(spiked_lin[..., c], 0.05)
|
|
488
|
+
else:
|
|
489
|
+
spiked_final = self._midtones_m(spiked_lin, 0.05)
|
|
490
|
+
|
|
491
|
+
# ---- apply mask AFTER full processing ----
|
|
492
|
+
m = self._active_mask_array(self.doc)
|
|
493
|
+
if m is not None:
|
|
494
|
+
if spiked_final.ndim == 3 and m.ndim == 2:
|
|
495
|
+
m = m[..., None]
|
|
496
|
+
|
|
497
|
+
# white = apply effect, black = protect original
|
|
498
|
+
final = np.clip(spiked_final * m + img * (1.0 - m), 0.0, 1.0)
|
|
499
|
+
else:
|
|
500
|
+
final = spiked_final
|
|
501
|
+
|
|
502
|
+
self.final_image = final
|
|
503
|
+
self._update_preview(final)
|
|
504
|
+
self.btn_apply.setEnabled(True)
|
|
505
|
+
self.status.setText("Done.")
|
|
506
|
+
|
|
507
|
+
def _apply_to_doc(self):
|
|
508
|
+
if self.final_image is None:
|
|
509
|
+
QMessageBox.information(self, "Diffraction Spikes", "Nothing to apply yet.")
|
|
510
|
+
return
|
|
511
|
+
if not self.docman:
|
|
512
|
+
QMessageBox.warning(self, "No DocManager", "DocManager not available.")
|
|
513
|
+
return
|
|
514
|
+
self.docman.apply_edit_to_active(self.final_image, step_name="Diffraction Spikes")
|
|
515
|
+
self.status.setText("Applied to active document.")
|
|
516
|
+
# keep dialog open so user can tweak more if desired
|
|
517
|
+
|
|
518
|
+
# ---------- helpers ----------
|
|
519
|
+
def _update_preview(self, arr):
|
|
520
|
+
arr8 = np.clip(arr, 0, 1)
|
|
521
|
+
arr8 = (arr8 * 255.0).astype(np.uint8)
|
|
522
|
+
if arr8.ndim == 2:
|
|
523
|
+
h, w = arr8.shape
|
|
524
|
+
qimg = QImage(arr8.data, w, h, w, QImage.Format.Format_Grayscale8)
|
|
525
|
+
else:
|
|
526
|
+
h, w, _ = arr8.shape
|
|
527
|
+
qimg = QImage(arr8.data, w, h, 3*w, QImage.Format.Format_RGB888)
|
|
528
|
+
self.pix.setPixmap(QPixmap.fromImage(qimg))
|
|
529
|
+
self.scene.setSceneRect(self.pix.boundingRect())
|
|
530
|
+
# keep current zoom mode
|
|
531
|
+
self._apply_zoom()
|
|
532
|
+
|
|
533
|
+
def _show_help(self):
|
|
534
|
+
if not self.aperture_help_path:
|
|
535
|
+
QMessageBox.information(self, "Aperture Help", "No help image configured.")
|
|
536
|
+
return
|
|
537
|
+
pm = QPixmap(self.aperture_help_path)
|
|
538
|
+
if pm.isNull():
|
|
539
|
+
QMessageBox.critical(self, "Aperture Help", "Failed to load help image.")
|
|
540
|
+
return
|
|
541
|
+
dlg = QDialog(self)
|
|
542
|
+
dlg.setWindowTitle("Aperture Help")
|
|
543
|
+
v = QVBoxLayout(dlg)
|
|
544
|
+
lab = QLabel()
|
|
545
|
+
lab.setPixmap(pm)
|
|
546
|
+
lab.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
547
|
+
v.addWidget(lab)
|
|
548
|
+
dlg.setWindowFlag(Qt.WindowType.Window, True)
|
|
549
|
+
dlg.resize(480, 480)
|
|
550
|
+
dlg.show()
|
|
551
|
+
|
|
552
|
+
# ----- math from SASv2, adapted -----
|
|
553
|
+
@staticmethod
|
|
554
|
+
def _midtones_m(x, m):
|
|
555
|
+
x = np.clip(x, 0.0, 1.0).astype(np.float32)
|
|
556
|
+
out = np.zeros_like(x, dtype=np.float32)
|
|
557
|
+
mask0 = (x == 0); out[mask0] = 0.0
|
|
558
|
+
mask1 = (x == 1); out[mask1] = 1.0
|
|
559
|
+
eps = 1e-7
|
|
560
|
+
maskm = (np.abs(x - m) < eps); out[maskm] = 0.5
|
|
561
|
+
mask_oth = ~(mask0 | mask1 | maskm)
|
|
562
|
+
xm = x[mask_oth]
|
|
563
|
+
num = (m - 1.0)*xm
|
|
564
|
+
den = (2.0*m - 1.0)*xm - m
|
|
565
|
+
out[mask_oth] = np.clip(num/(den+1e-12),0,1)
|
|
566
|
+
return out
|
|
567
|
+
|
|
568
|
+
def _make_pupil(self, size=512, radius=100, obstruction=0.3, vane_width=2, num_vanes=4, rotation=0):
|
|
569
|
+
y, x = np.indices((size, size)) - size // 2
|
|
570
|
+
r = np.sqrt(x**2 + y**2)
|
|
571
|
+
pupil = (r <= radius).astype(np.float32)
|
|
572
|
+
pupil[r < radius * obstruction] = 0.0
|
|
573
|
+
if num_vanes >= 2:
|
|
574
|
+
rot = np.deg2rad(rotation)
|
|
575
|
+
for angle in np.linspace(0, np.pi, num_vanes, endpoint=False) + rot:
|
|
576
|
+
xp = x * np.cos(angle) + y * np.sin(angle)
|
|
577
|
+
vane = np.abs(xp) < vane_width
|
|
578
|
+
pupil[vane] = 0.0
|
|
579
|
+
return pupil
|
|
580
|
+
|
|
581
|
+
def _load_pupil_from_png(self, filepath, size=1024, rotation=0.0):
|
|
582
|
+
img = cv2.imread(filepath, cv2.IMREAD_GRAYSCALE)
|
|
583
|
+
if img is None:
|
|
584
|
+
raise ValueError(f"Failed to load image from {filepath}")
|
|
585
|
+
img = img.astype(np.float32) / 255.0
|
|
586
|
+
if img.shape != (size, size):
|
|
587
|
+
img = cv2.resize(img, (size, size), interpolation=cv2.INTER_AREA)
|
|
588
|
+
if abs(rotation) > 1e-3:
|
|
589
|
+
center = (size // 2, size // 2)
|
|
590
|
+
M = cv2.getRotationMatrix2D(center, rotation, 1.0)
|
|
591
|
+
img = cv2.warpAffine(img, M, (size, size), flags=cv2.INTER_LINEAR,
|
|
592
|
+
borderMode=cv2.BORDER_CONSTANT, borderValue=0)
|
|
593
|
+
return img
|
|
594
|
+
|
|
595
|
+
def _simulate_psf(self, pupil, wavelength_scale=1.0, blur_sigma=1.0):
|
|
596
|
+
# Try to use OpenCV for speed
|
|
597
|
+
if getattr(self, "_cv2_checked", False):
|
|
598
|
+
has_cv2 = True
|
|
599
|
+
import cv2
|
|
600
|
+
else:
|
|
601
|
+
try:
|
|
602
|
+
import cv2
|
|
603
|
+
has_cv2 = True
|
|
604
|
+
except ImportError:
|
|
605
|
+
has_cv2 = False
|
|
606
|
+
self._cv2_checked = has_cv2
|
|
607
|
+
|
|
608
|
+
if has_cv2:
|
|
609
|
+
# Gaussian blur on pupil
|
|
610
|
+
# kernel size usually ~6*sigma, must be odd
|
|
611
|
+
k_pupil = int(math.ceil(6 * (0.1 * wavelength_scale))) | 1
|
|
612
|
+
sp = cv2.GaussianBlur(pupil, (k_pupil, k_pupil), 0.1 * wavelength_scale)
|
|
613
|
+
else:
|
|
614
|
+
sp = gaussian_filter(pupil, sigma=0.1 * wavelength_scale)
|
|
615
|
+
|
|
616
|
+
fft = np.fft.fftshift(np.fft.fft2(np.fft.ifftshift(sp)))
|
|
617
|
+
intensity = np.abs(fft)**2
|
|
618
|
+
intensity /= (intensity.max() + 1e-8)
|
|
619
|
+
|
|
620
|
+
if has_cv2 and blur_sigma > 0:
|
|
621
|
+
k_blur = int(math.ceil(6 * blur_sigma)) | 1
|
|
622
|
+
blurred = cv2.GaussianBlur(intensity, (k_blur, k_blur), blur_sigma)
|
|
623
|
+
else:
|
|
624
|
+
blurred = gaussian_filter(intensity, sigma=blur_sigma)
|
|
625
|
+
|
|
626
|
+
psf = blurred / max(blurred.max(), 1e-8)
|
|
627
|
+
|
|
628
|
+
if wavelength_scale != 1.0:
|
|
629
|
+
if has_cv2:
|
|
630
|
+
h, w = psf.shape
|
|
631
|
+
# Zoom uses size, NOT scale factor in resize(..., dsize=(w,h))
|
|
632
|
+
# wavelength_scale > 1 => zoom in => crop middle? or simply scale?
|
|
633
|
+
# The original used ndi.zoom(psf, zoom=wavelength_scale).
|
|
634
|
+
# New size:
|
|
635
|
+
nw, nh = int(round(w * wavelength_scale)), int(round(h * wavelength_scale))
|
|
636
|
+
if nw > 0 and nh > 0:
|
|
637
|
+
scaled = cv2.resize(psf, (nw, nh), interpolation=cv2.INTER_LINEAR)
|
|
638
|
+
# We might need to crop back to original size or pad?
|
|
639
|
+
# ndi.zoom changes the array size.
|
|
640
|
+
# The simulator seems to assume we handle whatever size comes out?
|
|
641
|
+
# Let's check _extract_center_tile usage.
|
|
642
|
+
psf = scaled
|
|
643
|
+
else:
|
|
644
|
+
psf = ndi.zoom(psf, zoom=wavelength_scale, order=1)
|
|
645
|
+
|
|
646
|
+
psf /= psf.max() + 1e-12
|
|
647
|
+
return psf
|
|
648
|
+
|
|
649
|
+
@staticmethod
|
|
650
|
+
def _extract_center_tile(psf, tile_size):
|
|
651
|
+
c = psf.shape[0]//2
|
|
652
|
+
h = tile_size//2
|
|
653
|
+
y0 = max(0, c-h); x0 = max(0, c-h)
|
|
654
|
+
y1 = y0 + tile_size; x1 = x0 + tile_size
|
|
655
|
+
cropped = psf[y0:y1, x0:x1]
|
|
656
|
+
if cropped.shape != (tile_size, tile_size):
|
|
657
|
+
out = np.zeros((tile_size, tile_size), dtype=np.float32)
|
|
658
|
+
ph, pw = cropped.shape
|
|
659
|
+
out[:ph, :pw] = cropped
|
|
660
|
+
return out
|
|
661
|
+
return cropped
|
|
662
|
+
|
|
663
|
+
@staticmethod
|
|
664
|
+
def _detect_stars(image, threshold=5.0, flux_min=30.0, size_min=1.0):
|
|
665
|
+
data = image.astype(np.float32)
|
|
666
|
+
bkg = sep.Background(data)
|
|
667
|
+
data_sub = data - bkg.back()
|
|
668
|
+
err_val = bkg.globalrms
|
|
669
|
+
try:
|
|
670
|
+
objects = sep.extract(data_sub, threshold, err=err_val)
|
|
671
|
+
except Exception as e:
|
|
672
|
+
if "internal pixel buffer full" in str(e):
|
|
673
|
+
QMessageBox.warning(None, "Star Detection Failed",
|
|
674
|
+
"Star detection failed: internal pixel buffer full.\n"
|
|
675
|
+
"Increase detection threshold or minimum flux.")
|
|
676
|
+
else:
|
|
677
|
+
QMessageBox.critical(None, "Star Detection Failed", str(e))
|
|
678
|
+
return []
|
|
679
|
+
stars = []
|
|
680
|
+
for obj in objects:
|
|
681
|
+
flux = obj['flux']; a = obj['a']; b = obj['b']
|
|
682
|
+
if flux >= flux_min and max(a,b) >= size_min:
|
|
683
|
+
stars.append((int(obj['x']), int(obj['y']), float(flux), float(a), float(b)))
|
|
684
|
+
return stars
|
|
685
|
+
|
|
686
|
+
# _shrink_and_boost removed (replaced by inline _fast_zoom for performance)
|
|
687
|
+
|
|
688
|
+
@staticmethod
|
|
689
|
+
def _boost_shrink_from_flux(flux, flux_min, flux_max, bmin, bmax, smin, smax):
|
|
690
|
+
f = np.clip(flux, flux_min, flux_max)
|
|
691
|
+
alpha = 0.0 if flux_max <= flux_min else (f - flux_min) / (flux_max - flux_min)
|
|
692
|
+
bscale = bmin + alpha * (bmax - bmin)
|
|
693
|
+
shrink = smax - alpha * (smax - smin)
|
|
694
|
+
return float(bscale), float(shrink)
|
|
695
|
+
|
|
696
|
+
@staticmethod
|
|
697
|
+
def _measure_star_color(img_color, x, y, sampling_radius=20):
|
|
698
|
+
if img_color.ndim == 2:
|
|
699
|
+
return (1.0, 1.0, 1.0)
|
|
700
|
+
H, W, C = img_color.shape
|
|
701
|
+
if C != 3:
|
|
702
|
+
return (1.0, 1.0, 1.0)
|
|
703
|
+
x0 = max(0, int(x - sampling_radius)); x1 = min(W, int(x + sampling_radius + 1))
|
|
704
|
+
y0 = max(0, int(y - sampling_radius)); y1 = min(H, int(y + sampling_radius + 1))
|
|
705
|
+
if x1 <= x0 or y1 <= y0:
|
|
706
|
+
return (1.0, 1.0, 1.0)
|
|
707
|
+
patch = img_color[y0:y1, x0:x1, :]
|
|
708
|
+
mean_col = np.mean(patch, axis=(0, 1))
|
|
709
|
+
mx = float(np.max(mean_col))
|
|
710
|
+
if mx < 1e-9:
|
|
711
|
+
return (1.0, 1.0, 1.0)
|
|
712
|
+
return (float(mean_col[0]/mx), float(mean_col[1]/mx), float(mean_col[2]/mx))
|
|
713
|
+
|
|
714
|
+
@staticmethod
|
|
715
|
+
def _active_mask_array(doc) -> np.ndarray | None:
|
|
716
|
+
if not doc:
|
|
717
|
+
return None
|
|
718
|
+
mid = getattr(doc, "active_mask_id", None)
|
|
719
|
+
if not mid:
|
|
720
|
+
return None
|
|
721
|
+
masks = getattr(doc, "masks", {}) or {}
|
|
722
|
+
layer = masks.get(mid)
|
|
723
|
+
data = getattr(layer, "data", None) if layer is not None else None
|
|
724
|
+
if data is None:
|
|
725
|
+
return None
|
|
726
|
+
a = np.asarray(data)
|
|
727
|
+
if a.ndim == 3 and a.shape[2] == 1:
|
|
728
|
+
a = a[..., 0]
|
|
729
|
+
if a.ndim != 2:
|
|
730
|
+
return None
|
|
731
|
+
a = a.astype(np.float32, copy=False)
|
|
732
|
+
a = np.clip(a, 0.0, 1.0)
|
|
733
|
+
# keep original where mask == 1.0 (protection mask semantics)
|
|
734
|
+
return a
|
|
735
|
+
|
|
736
|
+
def _apply_zoom(self):
|
|
737
|
+
if self._fit_mode:
|
|
738
|
+
self.view.fitInView(self.pix, Qt.AspectRatioMode.KeepAspectRatio)
|
|
739
|
+
return
|
|
740
|
+
self.view.resetTransform()
|
|
741
|
+
self.view.scale(self._zoom, self._zoom)
|
|
742
|
+
|
|
743
|
+
def _zoom_in(self):
|
|
744
|
+
if self.pix.pixmap().isNull():
|
|
745
|
+
return
|
|
746
|
+
if self._fit_mode:
|
|
747
|
+
self._fit_mode = False
|
|
748
|
+
self._zoom = 1.0
|
|
749
|
+
self._zoom = min(self._zoom * 1.25, 20.0)
|
|
750
|
+
self._apply_zoom()
|
|
751
|
+
|
|
752
|
+
def _zoom_out(self):
|
|
753
|
+
if self.pix.pixmap().isNull():
|
|
754
|
+
return
|
|
755
|
+
if self._fit_mode:
|
|
756
|
+
self._fit_mode = False
|
|
757
|
+
self._zoom = 1.0
|
|
758
|
+
self._zoom = max(self._zoom / 1.25, 0.05)
|
|
759
|
+
self._apply_zoom()
|
|
760
|
+
|
|
761
|
+
def _fit_to_preview(self):
|
|
762
|
+
if self.pix.pixmap().isNull():
|
|
763
|
+
return
|
|
764
|
+
self._fit_mode = True
|
|
765
|
+
self._apply_zoom()
|