setiastrosuitepro 1.6.10__py3-none-any.whl → 1.7.0.post2__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/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 +305 -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 +32 -1
- setiastro/saspro/gui/mixins/toolbar_mixin.py +135 -11
- setiastro/saspro/histogram.py +179 -7
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +972 -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 +74 -0
- setiastro/saspro/ser_stacker.py +2310 -0
- setiastro/saspro/ser_stacker_dialog.py +1500 -0
- setiastro/saspro/ser_tracking.py +206 -0
- setiastro/saspro/serviewer.py +1258 -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.post2.dist-info}/METADATA +2 -2
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/RECORD +51 -40
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/licenses/license.txt +0 -0
|
@@ -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,7 +832,13 @@ 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)
|
|
816
|
-
|
|
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)
|
|
839
|
+
self.act_swap_rb = QAction(self.tr("Swap R and B Channels"), self)
|
|
840
|
+
self.act_swap_rb.setStatusTip(self.tr("Swap red and blue channels on the active RGB image"))
|
|
841
|
+
self.act_swap_rb.triggered.connect(self._swap_rb_active)
|
|
817
842
|
# Functions
|
|
818
843
|
self.act_crop = QAction(QIcon(cropicon_path), self.tr("Crop..."), self)
|
|
819
844
|
self.act_crop.setStatusTip(self.tr("Crop / rotate with handles"))
|
|
@@ -877,7 +902,7 @@ class ToolbarMixin:
|
|
|
877
902
|
self.act_linear_fit.setShortcut("Ctrl+L")
|
|
878
903
|
self.act_linear_fit.triggered.connect(self._open_linear_fit)
|
|
879
904
|
|
|
880
|
-
self.act_remove_green = QAction(QIcon(green_path), self.tr("Remove Green..."), self)
|
|
905
|
+
self.act_remove_green = QAction(QIcon(green_path), self.tr("Remove Green (SCNR)..."), self)
|
|
881
906
|
self.act_remove_green.setToolTip(self.tr("SCNR-style green channel removal."))
|
|
882
907
|
self.act_remove_green.setIconVisibleInMenu(True)
|
|
883
908
|
self.act_remove_green.triggered.connect(self._open_remove_green)
|
|
@@ -1102,6 +1127,16 @@ class ToolbarMixin:
|
|
|
1102
1127
|
self.act_ppp.setStatusTip(self.tr("Pick the perfect palette for your image"))
|
|
1103
1128
|
self.act_ppp.triggered.connect(self._open_ppp_tool)
|
|
1104
1129
|
|
|
1130
|
+
self.act_narrowband_normalization = QAction(QIcon(narrowbandnormalization_path), self.tr("Narrowband Normalization..."), self)
|
|
1131
|
+
self.act_narrowband_normalization.setStatusTip(
|
|
1132
|
+
self.tr("Normalize HOO/SHO/HSO/HOS (PixelMath port by Bill Blanshan and Mike Cranfield)")
|
|
1133
|
+
)
|
|
1134
|
+
self.act_narrowband_normalization.setIconVisibleInMenu(False)
|
|
1135
|
+
self.act_narrowband_normalization.setShortcut(QKeySequence("Ctrl+Alt+Shift+N"))
|
|
1136
|
+
self.act_narrowband_normalization.setShortcutContext(Qt.ShortcutContext.ApplicationShortcut)
|
|
1137
|
+
self.addAction(self.act_narrowband_normalization)
|
|
1138
|
+
self.act_narrowband_normalization.triggered.connect(self._open_narrowband_normalization_tool)
|
|
1139
|
+
|
|
1105
1140
|
self.act_nbtorgb = QAction(QIcon(nbtorgb_path), self.tr("NB->RGB Stars..."), self)
|
|
1106
1141
|
self.act_nbtorgb.setStatusTip(self.tr("Combine narrowband to RGB with optional OSC stars"))
|
|
1107
1142
|
self.act_nbtorgb.setIconVisibleInMenu(True)
|
|
@@ -1149,6 +1184,11 @@ class ToolbarMixin:
|
|
|
1149
1184
|
self.act_live_stacking.setStatusTip(self.tr("Live monitor and stack incoming frames"))
|
|
1150
1185
|
self.act_live_stacking.triggered.connect(self._open_live_stacking)
|
|
1151
1186
|
|
|
1187
|
+
self.act_planetary_stacker = QAction(QIcon(planetarystacker_path), self.tr("Planetary Stacker..."), self)
|
|
1188
|
+
self.act_planetary_stacker.setIconVisibleInMenu(True)
|
|
1189
|
+
self.act_planetary_stacker.setStatusTip(self.tr("Stack SER videos (planetary/solar/lunar)"))
|
|
1190
|
+
self.act_planetary_stacker.triggered.connect(self._open_planetary_stacker)
|
|
1191
|
+
|
|
1152
1192
|
self.act_plate_solve = QAction(QIcon(platesolve_path), self.tr("Plate Solver..."), self)
|
|
1153
1193
|
self.act_plate_solve.setIconVisibleInMenu(True)
|
|
1154
1194
|
self.act_plate_solve.setStatusTip(self.tr("Solve WCS/SIP for the active image or a file"))
|
|
@@ -1345,6 +1385,7 @@ class ToolbarMixin:
|
|
|
1345
1385
|
reg("nbtorgb", self.act_nbtorgb)
|
|
1346
1386
|
reg("freqsep", self.act_freqsep)
|
|
1347
1387
|
reg("selective_color", self.act_selective_color)
|
|
1388
|
+
reg("narrowband_normalization", self.act_narrowband_normalization)
|
|
1348
1389
|
reg("contsub", self.act_contsub)
|
|
1349
1390
|
reg("abe", self.act_abe)
|
|
1350
1391
|
reg("create_mask", self.act_create_mask)
|
|
@@ -1369,7 +1410,7 @@ class ToolbarMixin:
|
|
|
1369
1410
|
reg("pixel_math", self.act_pixelmath)
|
|
1370
1411
|
reg("signature_insert", self.act_signature)
|
|
1371
1412
|
reg("halo_b_gon", self.act_halobgon)
|
|
1372
|
-
|
|
1413
|
+
reg("planetary_stacker", self.act_planetary_stacker)
|
|
1373
1414
|
reg("multiscale_decomp", self.act_multiscale_decomp)
|
|
1374
1415
|
reg("geom_invert", self.act_geom_invert)
|
|
1375
1416
|
reg("geom_flip_horizontal", self.act_geom_flip_h)
|
|
@@ -1407,6 +1448,83 @@ class ToolbarMixin:
|
|
|
1407
1448
|
reg("view_bundles", self.act_view_bundles)
|
|
1408
1449
|
reg("function_bundles", self.act_function_bundles)
|
|
1409
1450
|
|
|
1451
|
+
def _reset_all_toolbars_to_factory(self):
|
|
1452
|
+
"""
|
|
1453
|
+
Clears all persisted toolbar membership/order/hidden state so the UI
|
|
1454
|
+
returns to the factory layout defined in code.
|
|
1455
|
+
"""
|
|
1456
|
+
if not hasattr(self, "settings"):
|
|
1457
|
+
return
|
|
1458
|
+
|
|
1459
|
+
# 1) Clear global toolbar persistence
|
|
1460
|
+
keys_to_clear = [
|
|
1461
|
+
"Toolbar/Assignments",
|
|
1462
|
+
"Toolbar/HiddenPrev",
|
|
1463
|
+
"Toolbar/HiddenMigrationDone",
|
|
1464
|
+
|
|
1465
|
+
# Per-toolbar order lists
|
|
1466
|
+
"Toolbar/View",
|
|
1467
|
+
"Toolbar/Functions",
|
|
1468
|
+
"Toolbar/Cosmic",
|
|
1469
|
+
"Toolbar/Tools",
|
|
1470
|
+
"Toolbar/Geometry",
|
|
1471
|
+
"Toolbar/StarStuff",
|
|
1472
|
+
"Toolbar/Masks",
|
|
1473
|
+
"Toolbar/WhatsInMy",
|
|
1474
|
+
"Toolbar/Bundles",
|
|
1475
|
+
"Toolbar/Hidden",
|
|
1476
|
+
]
|
|
1477
|
+
|
|
1478
|
+
for k in keys_to_clear:
|
|
1479
|
+
try:
|
|
1480
|
+
self.settings.remove(k)
|
|
1481
|
+
except Exception:
|
|
1482
|
+
pass
|
|
1483
|
+
|
|
1484
|
+
# 2) Clear legacy hidden lists (old system) if they exist
|
|
1485
|
+
# (These are the ones like "Toolbar/Tools/Hidden")
|
|
1486
|
+
try:
|
|
1487
|
+
# brute-force remove known ones
|
|
1488
|
+
legacy_hidden = [
|
|
1489
|
+
"Toolbar/View/Hidden",
|
|
1490
|
+
"Toolbar/Functions/Hidden",
|
|
1491
|
+
"Toolbar/Cosmic/Hidden",
|
|
1492
|
+
"Toolbar/Tools/Hidden",
|
|
1493
|
+
"Toolbar/Geometry/Hidden",
|
|
1494
|
+
"Toolbar/StarStuff/Hidden",
|
|
1495
|
+
"Toolbar/Masks/Hidden",
|
|
1496
|
+
"Toolbar/WhatsInMy/Hidden",
|
|
1497
|
+
"Toolbar/Bundles/Hidden",
|
|
1498
|
+
]
|
|
1499
|
+
for k in legacy_hidden:
|
|
1500
|
+
self.settings.remove(k)
|
|
1501
|
+
except Exception:
|
|
1502
|
+
pass
|
|
1503
|
+
|
|
1504
|
+
try:
|
|
1505
|
+
self.settings.sync()
|
|
1506
|
+
except Exception:
|
|
1507
|
+
pass
|
|
1508
|
+
|
|
1509
|
+
# 3) Rebuild toolbars from code defaults
|
|
1510
|
+
# Safest approach: remove existing toolbars and rebuild.
|
|
1511
|
+
from setiastro.saspro.shortcuts import DraggableToolBar
|
|
1512
|
+
for tb in self.findChildren(DraggableToolBar):
|
|
1513
|
+
try:
|
|
1514
|
+
self.removeToolBar(tb)
|
|
1515
|
+
tb.deleteLater()
|
|
1516
|
+
except Exception:
|
|
1517
|
+
pass
|
|
1518
|
+
|
|
1519
|
+
# Build fresh
|
|
1520
|
+
self._init_toolbar()
|
|
1521
|
+
|
|
1522
|
+
# Optional: ensure Hidden stays hidden
|
|
1523
|
+
tb_hidden = self._hidden_toolbar()
|
|
1524
|
+
if tb_hidden:
|
|
1525
|
+
tb_hidden.setVisible(False)
|
|
1526
|
+
|
|
1527
|
+
|
|
1410
1528
|
def _restore_toolbar_order(self, tb, settings_key: str):
|
|
1411
1529
|
"""
|
|
1412
1530
|
Restore toolbar action order from QSettings, using command_id/objectName.
|
|
@@ -1821,4 +1939,10 @@ class ToolbarMixin:
|
|
|
1821
1939
|
if hasattr(self, "act_hide_mask"):
|
|
1822
1940
|
self.act_hide_mask.setEnabled(has_mask and overlay_on)
|
|
1823
1941
|
|
|
1824
|
-
|
|
1942
|
+
def _style_toggle_toolbutton(self, btn: QToolButton):
|
|
1943
|
+
# Make sure the action visually shows "on" state
|
|
1944
|
+
btn.setCheckable(True) # safe even if already
|
|
1945
|
+
btn.setStyleSheet("""
|
|
1946
|
+
QToolButton { color: #dcdcdc; }
|
|
1947
|
+
QToolButton:checked { color: #DAA520; font-weight: 600; }
|
|
1948
|
+
""")
|
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()
|