setiastrosuitepro 1.6.10__py3-none-any.whl → 1.7.0__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.
- setiastro/images/colorwheel.svg +97 -0
- setiastro/images/narrowbandnormalization.png +0 -0
- setiastro/images/planetarystacker.png +0 -0
- setiastro/saspro/__main__.py +1 -1
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/aberration_ai.py +49 -11
- setiastro/saspro/aberration_ai_preset.py +29 -3
- setiastro/saspro/backgroundneutral.py +73 -33
- setiastro/saspro/blink_comparator_pro.py +116 -71
- setiastro/saspro/convo.py +9 -6
- setiastro/saspro/curve_editor_pro.py +72 -22
- setiastro/saspro/curves_preset.py +249 -47
- setiastro/saspro/doc_manager.py +178 -11
- setiastro/saspro/gui/main_window.py +218 -66
- setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
- setiastro/saspro/gui/mixins/file_mixin.py +35 -16
- setiastro/saspro/gui/mixins/menu_mixin.py +31 -1
- setiastro/saspro/gui/mixins/toolbar_mixin.py +132 -10
- setiastro/saspro/histogram.py +179 -7
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +769 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
- setiastro/saspro/imageops/stretch.py +66 -15
- setiastro/saspro/legacy/numba_utils.py +25 -48
- setiastro/saspro/live_stacking.py +24 -4
- setiastro/saspro/multiscale_decomp.py +30 -17
- setiastro/saspro/narrowband_normalization.py +1618 -0
- setiastro/saspro/numba_utils.py +0 -55
- setiastro/saspro/ops/script_editor.py +5 -0
- setiastro/saspro/ops/scripts.py +119 -0
- setiastro/saspro/remove_green.py +1 -1
- setiastro/saspro/resources.py +4 -0
- setiastro/saspro/ser_stack_config.py +68 -0
- setiastro/saspro/ser_stacker.py +2245 -0
- setiastro/saspro/ser_stacker_dialog.py +1481 -0
- setiastro/saspro/ser_tracking.py +206 -0
- setiastro/saspro/serviewer.py +1242 -0
- setiastro/saspro/sfcc.py +602 -214
- setiastro/saspro/shortcuts.py +35 -16
- setiastro/saspro/stacking_suite.py +332 -87
- setiastro/saspro/star_alignment.py +243 -122
- setiastro/saspro/stat_stretch.py +220 -31
- setiastro/saspro/subwindow.py +2 -4
- setiastro/saspro/whitebalance.py +24 -0
- setiastro/saspro/widgets/resource_monitor.py +122 -74
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/METADATA +2 -2
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/RECORD +51 -40
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/licenses/license.txt +0 -0
|
@@ -7,7 +7,7 @@ import os
|
|
|
7
7
|
from typing import TYPE_CHECKING
|
|
8
8
|
|
|
9
9
|
from PyQt6.QtCore import Qt
|
|
10
|
-
from PyQt6.QtWidgets import QFileDialog, QMessageBox, QProgressDialog
|
|
10
|
+
from PyQt6.QtWidgets import QFileDialog, QMessageBox, QProgressDialog, QApplication
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
pass
|
|
@@ -85,7 +85,6 @@ class FileMixin:
|
|
|
85
85
|
self._save_recent_lists()
|
|
86
86
|
if hasattr(self, "_rebuild_recent_menus"):
|
|
87
87
|
self._rebuild_recent_menus()
|
|
88
|
-
# Extracted FILE methods
|
|
89
88
|
|
|
90
89
|
def open_files(self):
|
|
91
90
|
# One-stop "All Supported" plus focused groups the user can switch to
|
|
@@ -114,21 +113,41 @@ class FileMixin:
|
|
|
114
113
|
except Exception:
|
|
115
114
|
pass
|
|
116
115
|
|
|
117
|
-
#
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
116
|
+
# ---- BEGIN batch open (stable placement) ----
|
|
117
|
+
try:
|
|
118
|
+
self._mdi_begin_open_batch(mode="cascade")
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
|
|
123
|
+
try:
|
|
124
|
+
# open each path (doc_manager should emit documentAdded; no manual spawn)
|
|
125
|
+
for p in paths:
|
|
125
126
|
try:
|
|
126
|
-
|
|
127
|
-
self.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
_ = self.docman.open_path(p) # emits documentAdded; spawn will happen
|
|
128
|
+
self._log(f"Opened: {p}")
|
|
129
|
+
self._add_recent_image(p) # track MRU
|
|
130
|
+
|
|
131
|
+
# Increment statistics
|
|
132
|
+
try:
|
|
133
|
+
count = self.settings.value("stats/opened_images_count", 0, type=int)
|
|
134
|
+
self.settings.setValue("stats/opened_images_count", count + 1)
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
# Let Qt paint newly spawned subwindows as we go
|
|
139
|
+
QApplication.processEvents()
|
|
140
|
+
|
|
141
|
+
except Exception as e:
|
|
142
|
+
QMessageBox.warning(self, self.tr("Open failed"), f"{p}\n\n{e}")
|
|
143
|
+
QApplication.processEvents()
|
|
144
|
+
finally:
|
|
145
|
+
QApplication.restoreOverrideCursor()
|
|
146
|
+
try:
|
|
147
|
+
self._mdi_end_open_batch()
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
|
|
132
151
|
|
|
133
152
|
def save_active(self):
|
|
134
153
|
from setiastro.saspro.main_helpers import (
|
|
@@ -6,8 +6,9 @@ from __future__ import annotations
|
|
|
6
6
|
import os
|
|
7
7
|
from typing import TYPE_CHECKING
|
|
8
8
|
|
|
9
|
-
from PyQt6.QtGui import QAction
|
|
9
|
+
from PyQt6.QtGui import QAction, QKeySequence
|
|
10
10
|
from PyQt6.QtWidgets import QMenu, QToolButton, QWidgetAction
|
|
11
|
+
from PyQt6.QtCore import Qt
|
|
11
12
|
|
|
12
13
|
if TYPE_CHECKING:
|
|
13
14
|
pass
|
|
@@ -120,6 +121,9 @@ class MenuMixin:
|
|
|
120
121
|
m_edit = mb.addMenu(self.tr("&Edit"))
|
|
121
122
|
m_edit.addAction(self.act_undo)
|
|
122
123
|
m_edit.addAction(self.act_redo)
|
|
124
|
+
m_edit.addSeparator()
|
|
125
|
+
m_edit.addAction(self.act_mono_to_rgb)
|
|
126
|
+
|
|
123
127
|
|
|
124
128
|
# Functions
|
|
125
129
|
m_fn = mb.addMenu(self.tr("&Functions"))
|
|
@@ -169,6 +173,7 @@ class MenuMixin:
|
|
|
169
173
|
m_tools.addAction(self.act_freqsep)
|
|
170
174
|
m_tools.addAction(self.act_image_combine)
|
|
171
175
|
m_tools.addAction(self.act_multiscale_decomp)
|
|
176
|
+
m_tools.addAction(self.act_narrowband_normalization)
|
|
172
177
|
m_tools.addAction(self.act_nbtorgb)
|
|
173
178
|
m_tools.addAction(self.act_ppp)
|
|
174
179
|
m_tools.addAction(self.act_selective_color)
|
|
@@ -199,6 +204,7 @@ class MenuMixin:
|
|
|
199
204
|
m_star.addAction(self.act_isophote)
|
|
200
205
|
m_star.addAction(self.act_live_stacking)
|
|
201
206
|
m_star.addAction(self.act_mosaic_master)
|
|
207
|
+
m_star.addAction(self.act_planetary_stacker)
|
|
202
208
|
m_star.addAction(self.act_plate_solve)
|
|
203
209
|
m_star.addAction(self.act_psf_viewer)
|
|
204
210
|
m_star.addAction(self.act_rgb_align)
|
|
@@ -267,6 +273,14 @@ class MenuMixin:
|
|
|
267
273
|
m_view.addAction(self.act_tile_grid)
|
|
268
274
|
m_view.addSeparator()
|
|
269
275
|
|
|
276
|
+
# NEW: Minimize All Views
|
|
277
|
+
self.act_minimize_all_views = QAction(self.tr("Minimize All Views"), self)
|
|
278
|
+
self.act_minimize_all_views.setShortcut(QKeySequence("Ctrl+Shift+M"))
|
|
279
|
+
self.act_minimize_all_views.setShortcutContext(Qt.ShortcutContext.ApplicationShortcut)
|
|
280
|
+
self.act_minimize_all_views.triggered.connect(self._minimize_all_views)
|
|
281
|
+
m_view.addAction(self.act_minimize_all_views)
|
|
282
|
+
|
|
283
|
+
m_view.addSeparator()
|
|
270
284
|
|
|
271
285
|
# a button that shows current group & opens a drop-down
|
|
272
286
|
self._link_btn = QToolButton(self)
|
|
@@ -389,3 +403,19 @@ class MenuMixin:
|
|
|
389
403
|
if sub is not None:
|
|
390
404
|
yield from self._iter_menu_actions(sub)
|
|
391
405
|
|
|
406
|
+
def _minimize_all_views(self):
|
|
407
|
+
mdi = getattr(self, "mdi", None)
|
|
408
|
+
if mdi is None:
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
for sw in mdi.subWindowList():
|
|
413
|
+
try:
|
|
414
|
+
if not sw.isVisible():
|
|
415
|
+
continue
|
|
416
|
+
# Minimize each MDI child
|
|
417
|
+
sw.showMinimized()
|
|
418
|
+
except Exception:
|
|
419
|
+
pass
|
|
420
|
+
except Exception:
|
|
421
|
+
pass
|
|
@@ -12,6 +12,7 @@ from PyQt6.QtWidgets import QMenu, QToolButton
|
|
|
12
12
|
|
|
13
13
|
from PyQt6.QtCore import QElapsedTimer
|
|
14
14
|
|
|
15
|
+
import sys
|
|
15
16
|
|
|
16
17
|
if TYPE_CHECKING:
|
|
17
18
|
pass
|
|
@@ -30,10 +31,10 @@ from setiastro.saspro.resources import (
|
|
|
30
31
|
stacking_path, pedestal_icon_path, starspike_path, astrospike_path,
|
|
31
32
|
signature_icon_path, livestacking_path, convoicon_path, spcc_icon_path,
|
|
32
33
|
exoicon_path, peeker_icon, dse_icon_path, isophote_path, statstretch_path,
|
|
33
|
-
starstretch_path, curves_path, disk_path, uhs_path, blink_path, ppp_path,
|
|
34
|
+
starstretch_path, curves_path, disk_path, uhs_path, blink_path, ppp_path, narrowbandnormalization_path,
|
|
34
35
|
nbtorgb_path, freqsep_path, multiscale_decomp_path, contsub_path, halo_path, cosmic_path,
|
|
35
36
|
satellite_path, imagecombine_path, wims_path, wimi_path, linearfit_path,
|
|
36
|
-
debayer_path, aberration_path, functionbundles_path, viewbundles_path,
|
|
37
|
+
debayer_path, aberration_path, functionbundles_path, viewbundles_path, planetarystacker_path,
|
|
37
38
|
selectivecolor_path, rgbalign_path,
|
|
38
39
|
)
|
|
39
40
|
|
|
@@ -255,6 +256,7 @@ class ToolbarMixin:
|
|
|
255
256
|
tb_tl.addAction(self.act_blink) # Tools start here; Blink shows with QIcon(blink_path)
|
|
256
257
|
tb_tl.addAction(self.act_ppp) # Perfect Palette Picker
|
|
257
258
|
tb_tl.addAction(self.act_nbtorgb)
|
|
259
|
+
tb_tl.addAction(self.act_narrowband_normalization)
|
|
258
260
|
tb_tl.addAction(self.act_selective_color)
|
|
259
261
|
tb_tl.addAction(self.act_freqsep)
|
|
260
262
|
tb_tl.addAction(self.act_multiscale_decomp)
|
|
@@ -301,6 +303,7 @@ class ToolbarMixin:
|
|
|
301
303
|
tb_star.addAction(self.act_psf_viewer)
|
|
302
304
|
tb_star.addAction(self.act_stacking_suite)
|
|
303
305
|
tb_star.addAction(self.act_live_stacking)
|
|
306
|
+
tb_star.addAction(self.act_planetary_stacker)
|
|
304
307
|
tb_star.addAction(self.act_plate_solve)
|
|
305
308
|
tb_star.addAction(self.act_star_align)
|
|
306
309
|
tb_star.addAction(self.act_star_register)
|
|
@@ -366,6 +369,7 @@ class ToolbarMixin:
|
|
|
366
369
|
tb_hidden.setObjectName("Hidden")
|
|
367
370
|
tb_hidden.setSettingsKey("Toolbar/Hidden")
|
|
368
371
|
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_hidden)
|
|
372
|
+
#tb_hidden.addAction(self.act_narrowband_normalization)
|
|
369
373
|
tb_hidden.setVisible(False) # <- always hidden
|
|
370
374
|
|
|
371
375
|
# This can move actions between toolbars, so do it after each toolbar has its base order restored.
|
|
@@ -594,7 +598,16 @@ class ToolbarMixin:
|
|
|
594
598
|
fit_menu.addAction(self.act_auto_fit_resize) # use the real action
|
|
595
599
|
btn_fit.setMenu(fit_menu)
|
|
596
600
|
btn_fit.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
|
|
601
|
+
tb = self._toolbar_containing_action(self.act_autostretch)
|
|
602
|
+
if tb:
|
|
603
|
+
btn = tb.widgetForAction(self.act_autostretch)
|
|
604
|
+
if isinstance(btn, QToolButton):
|
|
605
|
+
# ... build menu ...
|
|
606
|
+
btn.setMenu(menu)
|
|
607
|
+
btn.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
|
|
597
608
|
|
|
609
|
+
# IMPORTANT: re-apply style after action moves / rebind
|
|
610
|
+
self._style_toggle_toolbutton(btn)
|
|
598
611
|
|
|
599
612
|
def _bind_view_toolbar_menus(self, tb: DraggableToolBar):
|
|
600
613
|
# --- Display-Stretch menu ---
|
|
@@ -650,6 +663,12 @@ class ToolbarMixin:
|
|
|
650
663
|
btn_fit.setMenu(fit_menu)
|
|
651
664
|
btn_fit.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
|
|
652
665
|
|
|
666
|
+
def _linux_force_text_action(self, act: QAction, text: str) -> None:
|
|
667
|
+
"""On Linux, show text-only for this action (no theme icon)."""
|
|
668
|
+
if not sys.platform.startswith("linux"):
|
|
669
|
+
return
|
|
670
|
+
act.setIcon(QIcon()) # remove whatever theme icon got assigned
|
|
671
|
+
act.setText(text) # show the glyph/text
|
|
653
672
|
|
|
654
673
|
def _create_actions(self):
|
|
655
674
|
# File actions
|
|
@@ -771,30 +790,30 @@ class ToolbarMixin:
|
|
|
771
790
|
self.act_bake_display_stretch.triggered.connect(self._bake_display_stretch)
|
|
772
791
|
|
|
773
792
|
# --- Zoom controls ---
|
|
774
|
-
# --- Zoom controls (themed icons) ---
|
|
775
793
|
self.act_zoom_out = QAction(QIcon.fromTheme("zoom-out"), self.tr("Zoom Out"), self)
|
|
776
794
|
self.act_zoom_out.setStatusTip(self.tr("Zoom out"))
|
|
777
795
|
self.act_zoom_out.setShortcuts([QKeySequence("Ctrl+-")])
|
|
778
796
|
self.act_zoom_out.triggered.connect(lambda: self._zoom_step_active(-1))
|
|
797
|
+
self._linux_force_text_action(self.act_zoom_out, "−") # true minus
|
|
779
798
|
|
|
780
799
|
self.act_zoom_in = QAction(QIcon.fromTheme("zoom-in"), self.tr("Zoom In"), self)
|
|
781
800
|
self.act_zoom_in.setStatusTip(self.tr("Zoom in"))
|
|
782
|
-
self.act_zoom_in.setShortcuts([
|
|
783
|
-
QKeySequence("Ctrl++"), # Ctrl + (Shift + = on many keyboards)
|
|
784
|
-
QKeySequence("Ctrl+="), # fallback
|
|
785
|
-
])
|
|
801
|
+
self.act_zoom_in.setShortcuts([QKeySequence("Ctrl++"), QKeySequence("Ctrl+=")])
|
|
786
802
|
self.act_zoom_in.triggered.connect(lambda: self._zoom_step_active(+1))
|
|
803
|
+
self._linux_force_text_action(self.act_zoom_in, "+")
|
|
787
804
|
|
|
788
805
|
self.act_zoom_1_1 = QAction(QIcon.fromTheme("zoom-original"), self.tr("1:1"), self)
|
|
789
806
|
self.act_zoom_1_1.setStatusTip(self.tr("Zoom to 100% (pixel-for-pixel)"))
|
|
790
807
|
self.act_zoom_1_1.setShortcut(QKeySequence("Ctrl+1"))
|
|
791
808
|
self.act_zoom_1_1.triggered.connect(self._zoom_active_1_1)
|
|
809
|
+
self._linux_force_text_action(self.act_zoom_1_1, "1:1")
|
|
792
810
|
|
|
793
811
|
self.act_zoom_fit = QAction(QIcon.fromTheme("zoom-fit-best"), self.tr("Fit"), self)
|
|
794
812
|
self.act_zoom_fit.setStatusTip(self.tr("Fit image to current window"))
|
|
795
813
|
self.act_zoom_fit.setShortcut(QKeySequence("Ctrl+0"))
|
|
796
814
|
self.act_zoom_fit.triggered.connect(self._zoom_active_fit)
|
|
797
815
|
self.act_zoom_fit.setCheckable(True)
|
|
816
|
+
self._linux_force_text_action(self.act_zoom_fit, "Fit")
|
|
798
817
|
|
|
799
818
|
self.act_auto_fit_resize = QAction(self.tr("Auto-fit on Resize"), self)
|
|
800
819
|
self.act_auto_fit_resize.setCheckable(True)
|
|
@@ -813,6 +832,10 @@ class ToolbarMixin:
|
|
|
813
832
|
self.act_paste_view.setShortcut("Ctrl+Shift+V")
|
|
814
833
|
self.act_copy_view.triggered.connect(self._copy_active_view)
|
|
815
834
|
self.act_paste_view.triggered.connect(self._paste_active_view)
|
|
835
|
+
# --- Edit: Mono -> RGB (triplicate channels) ---
|
|
836
|
+
self.act_mono_to_rgb = QAction(self.tr("Convert Mono to RGB"), self)
|
|
837
|
+
self.act_mono_to_rgb.setStatusTip(self.tr("Convert a mono image to RGB by duplicating the channel"))
|
|
838
|
+
self.act_mono_to_rgb.triggered.connect(self._convert_mono_to_rgb_active)
|
|
816
839
|
|
|
817
840
|
# Functions
|
|
818
841
|
self.act_crop = QAction(QIcon(cropicon_path), self.tr("Crop..."), self)
|
|
@@ -877,7 +900,7 @@ class ToolbarMixin:
|
|
|
877
900
|
self.act_linear_fit.setShortcut("Ctrl+L")
|
|
878
901
|
self.act_linear_fit.triggered.connect(self._open_linear_fit)
|
|
879
902
|
|
|
880
|
-
self.act_remove_green = QAction(QIcon(green_path), self.tr("Remove Green..."), self)
|
|
903
|
+
self.act_remove_green = QAction(QIcon(green_path), self.tr("Remove Green (SCNR)..."), self)
|
|
881
904
|
self.act_remove_green.setToolTip(self.tr("SCNR-style green channel removal."))
|
|
882
905
|
self.act_remove_green.setIconVisibleInMenu(True)
|
|
883
906
|
self.act_remove_green.triggered.connect(self._open_remove_green)
|
|
@@ -1102,6 +1125,16 @@ class ToolbarMixin:
|
|
|
1102
1125
|
self.act_ppp.setStatusTip(self.tr("Pick the perfect palette for your image"))
|
|
1103
1126
|
self.act_ppp.triggered.connect(self._open_ppp_tool)
|
|
1104
1127
|
|
|
1128
|
+
self.act_narrowband_normalization = QAction(QIcon(narrowbandnormalization_path), self.tr("Narrowband Normalization..."), self)
|
|
1129
|
+
self.act_narrowband_normalization.setStatusTip(
|
|
1130
|
+
self.tr("Normalize HOO/SHO/HSO/HOS (PixelMath port by Bill Blanshan and Mike Cranfield)")
|
|
1131
|
+
)
|
|
1132
|
+
self.act_narrowband_normalization.setIconVisibleInMenu(False)
|
|
1133
|
+
self.act_narrowband_normalization.setShortcut(QKeySequence("Ctrl+Alt+Shift+N"))
|
|
1134
|
+
self.act_narrowband_normalization.setShortcutContext(Qt.ShortcutContext.ApplicationShortcut)
|
|
1135
|
+
self.addAction(self.act_narrowband_normalization)
|
|
1136
|
+
self.act_narrowband_normalization.triggered.connect(self._open_narrowband_normalization_tool)
|
|
1137
|
+
|
|
1105
1138
|
self.act_nbtorgb = QAction(QIcon(nbtorgb_path), self.tr("NB->RGB Stars..."), self)
|
|
1106
1139
|
self.act_nbtorgb.setStatusTip(self.tr("Combine narrowband to RGB with optional OSC stars"))
|
|
1107
1140
|
self.act_nbtorgb.setIconVisibleInMenu(True)
|
|
@@ -1149,6 +1182,11 @@ class ToolbarMixin:
|
|
|
1149
1182
|
self.act_live_stacking.setStatusTip(self.tr("Live monitor and stack incoming frames"))
|
|
1150
1183
|
self.act_live_stacking.triggered.connect(self._open_live_stacking)
|
|
1151
1184
|
|
|
1185
|
+
self.act_planetary_stacker = QAction(QIcon(planetarystacker_path), self.tr("Planetary Stacker..."), self)
|
|
1186
|
+
self.act_planetary_stacker.setIconVisibleInMenu(True)
|
|
1187
|
+
self.act_planetary_stacker.setStatusTip(self.tr("Stack SER videos (planetary/solar/lunar)"))
|
|
1188
|
+
self.act_planetary_stacker.triggered.connect(self._open_planetary_stacker)
|
|
1189
|
+
|
|
1152
1190
|
self.act_plate_solve = QAction(QIcon(platesolve_path), self.tr("Plate Solver..."), self)
|
|
1153
1191
|
self.act_plate_solve.setIconVisibleInMenu(True)
|
|
1154
1192
|
self.act_plate_solve.setStatusTip(self.tr("Solve WCS/SIP for the active image or a file"))
|
|
@@ -1345,6 +1383,7 @@ class ToolbarMixin:
|
|
|
1345
1383
|
reg("nbtorgb", self.act_nbtorgb)
|
|
1346
1384
|
reg("freqsep", self.act_freqsep)
|
|
1347
1385
|
reg("selective_color", self.act_selective_color)
|
|
1386
|
+
reg("narrowband_normalization", self.act_narrowband_normalization)
|
|
1348
1387
|
reg("contsub", self.act_contsub)
|
|
1349
1388
|
reg("abe", self.act_abe)
|
|
1350
1389
|
reg("create_mask", self.act_create_mask)
|
|
@@ -1369,7 +1408,7 @@ class ToolbarMixin:
|
|
|
1369
1408
|
reg("pixel_math", self.act_pixelmath)
|
|
1370
1409
|
reg("signature_insert", self.act_signature)
|
|
1371
1410
|
reg("halo_b_gon", self.act_halobgon)
|
|
1372
|
-
|
|
1411
|
+
reg("planetary_stacker", self.act_planetary_stacker)
|
|
1373
1412
|
reg("multiscale_decomp", self.act_multiscale_decomp)
|
|
1374
1413
|
reg("geom_invert", self.act_geom_invert)
|
|
1375
1414
|
reg("geom_flip_horizontal", self.act_geom_flip_h)
|
|
@@ -1407,6 +1446,83 @@ class ToolbarMixin:
|
|
|
1407
1446
|
reg("view_bundles", self.act_view_bundles)
|
|
1408
1447
|
reg("function_bundles", self.act_function_bundles)
|
|
1409
1448
|
|
|
1449
|
+
def _reset_all_toolbars_to_factory(self):
|
|
1450
|
+
"""
|
|
1451
|
+
Clears all persisted toolbar membership/order/hidden state so the UI
|
|
1452
|
+
returns to the factory layout defined in code.
|
|
1453
|
+
"""
|
|
1454
|
+
if not hasattr(self, "settings"):
|
|
1455
|
+
return
|
|
1456
|
+
|
|
1457
|
+
# 1) Clear global toolbar persistence
|
|
1458
|
+
keys_to_clear = [
|
|
1459
|
+
"Toolbar/Assignments",
|
|
1460
|
+
"Toolbar/HiddenPrev",
|
|
1461
|
+
"Toolbar/HiddenMigrationDone",
|
|
1462
|
+
|
|
1463
|
+
# Per-toolbar order lists
|
|
1464
|
+
"Toolbar/View",
|
|
1465
|
+
"Toolbar/Functions",
|
|
1466
|
+
"Toolbar/Cosmic",
|
|
1467
|
+
"Toolbar/Tools",
|
|
1468
|
+
"Toolbar/Geometry",
|
|
1469
|
+
"Toolbar/StarStuff",
|
|
1470
|
+
"Toolbar/Masks",
|
|
1471
|
+
"Toolbar/WhatsInMy",
|
|
1472
|
+
"Toolbar/Bundles",
|
|
1473
|
+
"Toolbar/Hidden",
|
|
1474
|
+
]
|
|
1475
|
+
|
|
1476
|
+
for k in keys_to_clear:
|
|
1477
|
+
try:
|
|
1478
|
+
self.settings.remove(k)
|
|
1479
|
+
except Exception:
|
|
1480
|
+
pass
|
|
1481
|
+
|
|
1482
|
+
# 2) Clear legacy hidden lists (old system) if they exist
|
|
1483
|
+
# (These are the ones like "Toolbar/Tools/Hidden")
|
|
1484
|
+
try:
|
|
1485
|
+
# brute-force remove known ones
|
|
1486
|
+
legacy_hidden = [
|
|
1487
|
+
"Toolbar/View/Hidden",
|
|
1488
|
+
"Toolbar/Functions/Hidden",
|
|
1489
|
+
"Toolbar/Cosmic/Hidden",
|
|
1490
|
+
"Toolbar/Tools/Hidden",
|
|
1491
|
+
"Toolbar/Geometry/Hidden",
|
|
1492
|
+
"Toolbar/StarStuff/Hidden",
|
|
1493
|
+
"Toolbar/Masks/Hidden",
|
|
1494
|
+
"Toolbar/WhatsInMy/Hidden",
|
|
1495
|
+
"Toolbar/Bundles/Hidden",
|
|
1496
|
+
]
|
|
1497
|
+
for k in legacy_hidden:
|
|
1498
|
+
self.settings.remove(k)
|
|
1499
|
+
except Exception:
|
|
1500
|
+
pass
|
|
1501
|
+
|
|
1502
|
+
try:
|
|
1503
|
+
self.settings.sync()
|
|
1504
|
+
except Exception:
|
|
1505
|
+
pass
|
|
1506
|
+
|
|
1507
|
+
# 3) Rebuild toolbars from code defaults
|
|
1508
|
+
# Safest approach: remove existing toolbars and rebuild.
|
|
1509
|
+
from setiastro.saspro.shortcuts import DraggableToolBar
|
|
1510
|
+
for tb in self.findChildren(DraggableToolBar):
|
|
1511
|
+
try:
|
|
1512
|
+
self.removeToolBar(tb)
|
|
1513
|
+
tb.deleteLater()
|
|
1514
|
+
except Exception:
|
|
1515
|
+
pass
|
|
1516
|
+
|
|
1517
|
+
# Build fresh
|
|
1518
|
+
self._init_toolbar()
|
|
1519
|
+
|
|
1520
|
+
# Optional: ensure Hidden stays hidden
|
|
1521
|
+
tb_hidden = self._hidden_toolbar()
|
|
1522
|
+
if tb_hidden:
|
|
1523
|
+
tb_hidden.setVisible(False)
|
|
1524
|
+
|
|
1525
|
+
|
|
1410
1526
|
def _restore_toolbar_order(self, tb, settings_key: str):
|
|
1411
1527
|
"""
|
|
1412
1528
|
Restore toolbar action order from QSettings, using command_id/objectName.
|
|
@@ -1821,4 +1937,10 @@ class ToolbarMixin:
|
|
|
1821
1937
|
if hasattr(self, "act_hide_mask"):
|
|
1822
1938
|
self.act_hide_mask.setEnabled(has_mask and overlay_on)
|
|
1823
1939
|
|
|
1824
|
-
|
|
1940
|
+
def _style_toggle_toolbutton(self, btn: QToolButton):
|
|
1941
|
+
# Make sure the action visually shows "on" state
|
|
1942
|
+
btn.setCheckable(True) # safe even if already
|
|
1943
|
+
btn.setStyleSheet("""
|
|
1944
|
+
QToolButton { color: #dcdcdc; }
|
|
1945
|
+
QToolButton:checked { color: #DAA520; font-weight: 600; }
|
|
1946
|
+
""")
|
setiastro/saspro/histogram.py
CHANGED
|
@@ -4,8 +4,8 @@ import numpy as np
|
|
|
4
4
|
|
|
5
5
|
from PyQt6.QtCore import Qt, QSettings, QTimer, QEvent, pyqtSignal
|
|
6
6
|
from PyQt6.QtWidgets import (
|
|
7
|
-
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QSlider, QPushButton, QScrollArea,
|
|
8
|
-
QTableWidget, QTableWidgetItem, QMessageBox, QToolButton, QInputDialog, QSplitter, QSizePolicy, QHeaderView
|
|
7
|
+
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QSlider, QPushButton, QScrollArea,QWidget,
|
|
8
|
+
QTableWidget, QTableWidgetItem, QMessageBox, QToolButton, QInputDialog, QSplitter, QSizePolicy, QHeaderView, QApplication
|
|
9
9
|
)
|
|
10
10
|
from PyQt6.QtGui import QPixmap, QPainter, QPen, QColor, QFont, QPalette
|
|
11
11
|
|
|
@@ -125,7 +125,12 @@ class HistogramDialog(QDialog):
|
|
|
125
125
|
|
|
126
126
|
splitter.addWidget(self.scroll_area)
|
|
127
127
|
|
|
128
|
-
# right: stats table
|
|
128
|
+
# right: stats panel (table + button under it)
|
|
129
|
+
stats_panel = QWidget(self)
|
|
130
|
+
stats_v = QVBoxLayout(stats_panel)
|
|
131
|
+
stats_v.setContentsMargins(0, 0, 0, 0)
|
|
132
|
+
stats_v.setSpacing(6)
|
|
133
|
+
|
|
129
134
|
self.stats_table = QTableWidget(self)
|
|
130
135
|
self.stats_table.setRowCount(7)
|
|
131
136
|
self.stats_table.setColumnCount(1)
|
|
@@ -137,15 +142,21 @@ class HistogramDialog(QDialog):
|
|
|
137
142
|
# Let it grow/shrink with the splitter
|
|
138
143
|
self.stats_table.setMinimumWidth(320)
|
|
139
144
|
self.stats_table.setSizePolicy(
|
|
140
|
-
QSizePolicy.Policy.Preferred,
|
|
145
|
+
QSizePolicy.Policy.Preferred,
|
|
141
146
|
QSizePolicy.Policy.Expanding,
|
|
142
147
|
)
|
|
143
148
|
|
|
144
|
-
# Make the columns use available width nicely
|
|
145
149
|
hdr = self.stats_table.horizontalHeader()
|
|
146
150
|
hdr.setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
|
|
152
|
+
stats_v.addWidget(self.stats_table, 1)
|
|
153
|
+
|
|
154
|
+
# NEW: button directly under the table
|
|
155
|
+
self.btn_more_stats = QPushButton(self.tr("More Stats…"), self)
|
|
156
|
+
self.btn_more_stats.clicked.connect(self._show_more_stats)
|
|
157
|
+
stats_v.addWidget(self.btn_more_stats, 0)
|
|
158
|
+
|
|
159
|
+
splitter.addWidget(stats_panel)
|
|
149
160
|
|
|
150
161
|
# Give more space to histogram side by default
|
|
151
162
|
splitter.setStretchFactor(0, 3)
|
|
@@ -631,6 +642,167 @@ class HistogramDialog(QDialog):
|
|
|
631
642
|
|
|
632
643
|
self._adjust_stats_width()
|
|
633
644
|
|
|
645
|
+
def _show_more_stats(self):
|
|
646
|
+
if self.image is None:
|
|
647
|
+
return
|
|
648
|
+
|
|
649
|
+
dlg = QDialog(self)
|
|
650
|
+
dlg.setWindowTitle(self.tr("Image Statistics"))
|
|
651
|
+
dlg.setWindowModality(Qt.WindowModality.NonModal)
|
|
652
|
+
dlg.setModal(False)
|
|
653
|
+
|
|
654
|
+
root = QVBoxLayout(dlg)
|
|
655
|
+
|
|
656
|
+
info = QLabel(self.tr(
|
|
657
|
+
"Detailed robust statistics and percentiles.\n"
|
|
658
|
+
"Computed on normalized float image values in [0,1]."
|
|
659
|
+
))
|
|
660
|
+
info.setWordWrap(True)
|
|
661
|
+
root.addWidget(info)
|
|
662
|
+
|
|
663
|
+
tbl = QTableWidget(dlg)
|
|
664
|
+
tbl.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
|
665
|
+
root.addWidget(tbl, 1)
|
|
666
|
+
|
|
667
|
+
btn_row = QHBoxLayout()
|
|
668
|
+
btn_copy = QPushButton(self.tr("Copy as Text"), dlg)
|
|
669
|
+
btn_close = QPushButton(self.tr("Close"), dlg)
|
|
670
|
+
btn_row.addStretch(1)
|
|
671
|
+
btn_row.addWidget(btn_copy)
|
|
672
|
+
btn_row.addWidget(btn_close)
|
|
673
|
+
root.addLayout(btn_row)
|
|
674
|
+
|
|
675
|
+
btn_close.clicked.connect(dlg.accept)
|
|
676
|
+
|
|
677
|
+
# ---- compute stats ----
|
|
678
|
+
img = self.image
|
|
679
|
+
if img.ndim == 3 and img.shape[2] == 3:
|
|
680
|
+
chans = [img[..., 0], img[..., 1], img[..., 2]]
|
|
681
|
+
col_names = ["R", "G", "B"]
|
|
682
|
+
else:
|
|
683
|
+
chan = img if img.ndim == 2 else img[..., 0]
|
|
684
|
+
chans = [chan]
|
|
685
|
+
col_names = ["Gray"]
|
|
686
|
+
|
|
687
|
+
row_defs = [
|
|
688
|
+
("Min", "min"),
|
|
689
|
+
("Max", "max"),
|
|
690
|
+
("Mean", "mean"),
|
|
691
|
+
("Median", "median"),
|
|
692
|
+
("StdDev", "std"),
|
|
693
|
+
("Variance", "var"),
|
|
694
|
+
("MAD", "mad"),
|
|
695
|
+
("IQR (p75-p25)", "iqr"),
|
|
696
|
+
("p0.1", "p0.1"),
|
|
697
|
+
("p1", "p1"),
|
|
698
|
+
("p5", "p5"),
|
|
699
|
+
("p25", "p25"),
|
|
700
|
+
("p50", "p50"),
|
|
701
|
+
("p75", "p75"),
|
|
702
|
+
("p95", "p95"),
|
|
703
|
+
("p99", "p99"),
|
|
704
|
+
("p99.9", "p99.9"),
|
|
705
|
+
("Low Clipped (<=0)", "lowclip"),
|
|
706
|
+
("High Clipped (>=TrueMax)", "highclip"),
|
|
707
|
+
]
|
|
708
|
+
|
|
709
|
+
tbl.setRowCount(len(row_defs))
|
|
710
|
+
tbl.setColumnCount(len(chans))
|
|
711
|
+
tbl.setHorizontalHeaderLabels(col_names)
|
|
712
|
+
tbl.setVerticalHeaderLabels([r[0] for r in row_defs])
|
|
713
|
+
|
|
714
|
+
# Precompute thresholds for clipping
|
|
715
|
+
eps = 1e-6
|
|
716
|
+
hi_thr = max(eps, float(self.sensor_max01) - eps)
|
|
717
|
+
|
|
718
|
+
def _fmt(x):
|
|
719
|
+
return f"{float(x):.6f}"
|
|
720
|
+
|
|
721
|
+
def _fmt_clip(k, n):
|
|
722
|
+
pct = 100.0 * float(k) / float(max(1, n))
|
|
723
|
+
return f"{int(k)} ({pct:.3f}%)"
|
|
724
|
+
|
|
725
|
+
# Percentiles we want
|
|
726
|
+
pct_list = [0.1, 1, 5, 25, 50, 75, 95, 99, 99.9]
|
|
727
|
+
|
|
728
|
+
# Fill table
|
|
729
|
+
for c_idx, c_arr in enumerate(chans):
|
|
730
|
+
flat = np.asarray(c_arr, dtype=np.float32).ravel()
|
|
731
|
+
if flat.size > 20_000_000:
|
|
732
|
+
idx = np.random.default_rng(0).choice(flat.size, size=5_000_000, replace=False)
|
|
733
|
+
flat = flat[idx]
|
|
734
|
+
|
|
735
|
+
n = int(flat.size) if flat.size else 1
|
|
736
|
+
|
|
737
|
+
# basic moments
|
|
738
|
+
cmin = float(np.min(flat))
|
|
739
|
+
cmax = float(np.max(flat))
|
|
740
|
+
cmean = float(np.mean(flat))
|
|
741
|
+
cmed = float(np.median(flat))
|
|
742
|
+
cstd = float(np.std(flat))
|
|
743
|
+
cvar = float(np.var(flat))
|
|
744
|
+
|
|
745
|
+
# robust
|
|
746
|
+
mad = float(np.median(np.abs(flat - cmed)))
|
|
747
|
+
pcts = np.percentile(flat, pct_list) if flat.size else np.zeros(len(pct_list), dtype=np.float32)
|
|
748
|
+
p25, p75 = float(pcts[3]), float(pcts[5])
|
|
749
|
+
iqr = float(p75 - p25)
|
|
750
|
+
|
|
751
|
+
# clipping
|
|
752
|
+
low_k = int(np.count_nonzero(flat <= eps))
|
|
753
|
+
high_k = int(np.count_nonzero(flat >= hi_thr))
|
|
754
|
+
|
|
755
|
+
values = {
|
|
756
|
+
"min": cmin,
|
|
757
|
+
"max": cmax,
|
|
758
|
+
"mean": cmean,
|
|
759
|
+
"median": cmed,
|
|
760
|
+
"std": cstd,
|
|
761
|
+
"var": cvar,
|
|
762
|
+
"mad": mad,
|
|
763
|
+
"iqr": iqr,
|
|
764
|
+
"p0.1": float(pcts[0]),
|
|
765
|
+
"p1": float(pcts[1]),
|
|
766
|
+
"p5": float(pcts[2]),
|
|
767
|
+
"p25": float(pcts[3]),
|
|
768
|
+
"p50": float(pcts[4]),
|
|
769
|
+
"p75": float(pcts[5]),
|
|
770
|
+
"p95": float(pcts[6]),
|
|
771
|
+
"p99": float(pcts[7]),
|
|
772
|
+
"p99.9": float(pcts[8]),
|
|
773
|
+
"lowclip": _fmt_clip(low_k, n),
|
|
774
|
+
"highclip": _fmt_clip(high_k, n),
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
for r, (_, key) in enumerate(row_defs):
|
|
778
|
+
v = values[key]
|
|
779
|
+
text = v if isinstance(v, str) else _fmt(v)
|
|
780
|
+
it = QTableWidgetItem(text)
|
|
781
|
+
it.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
782
|
+
tbl.setItem(r, c_idx, it)
|
|
783
|
+
|
|
784
|
+
hdr = tbl.horizontalHeader()
|
|
785
|
+
hdr.setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
|
786
|
+
|
|
787
|
+
def _copy_as_text():
|
|
788
|
+
# TSV with rows
|
|
789
|
+
lines = []
|
|
790
|
+
lines.append("\t" + "\t".join(col_names))
|
|
791
|
+
for r, (lab, _) in enumerate(row_defs):
|
|
792
|
+
row = [lab]
|
|
793
|
+
for c in range(len(col_names)):
|
|
794
|
+
item = tbl.item(r, c)
|
|
795
|
+
row.append(item.text() if item else "")
|
|
796
|
+
lines.append("\t".join(row))
|
|
797
|
+
QApplication.clipboard().setText("\n".join(lines))
|
|
798
|
+
QMessageBox.information(dlg, self.tr("Copied"), self.tr("Statistics copied to clipboard."))
|
|
799
|
+
|
|
800
|
+
btn_copy.clicked.connect(_copy_as_text)
|
|
801
|
+
|
|
802
|
+
dlg.resize(720, 520)
|
|
803
|
+
dlg.show()
|
|
804
|
+
|
|
805
|
+
|
|
634
806
|
def _theoretical_native_max_from_meta(self):
|
|
635
807
|
meta = getattr(self.doc, "metadata", None) or {}
|
|
636
808
|
bd = str(meta.get("bit_depth", "")).lower()
|