setiastrosuitepro 1.6.12__py3-none-any.whl → 1.7.3__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/3dplanet.png +0 -0
- setiastro/images/TextureClarity.svg +56 -0
- setiastro/images/narrowbandnormalization.png +0 -0
- setiastro/images/planetarystacker.png +0 -0
- setiastro/saspro/__init__.py +9 -8
- setiastro/saspro/__main__.py +326 -285
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/aberration_ai.py +128 -13
- setiastro/saspro/aberration_ai_preset.py +29 -3
- setiastro/saspro/astrospike_python.py +45 -3
- setiastro/saspro/blink_comparator_pro.py +116 -71
- setiastro/saspro/curve_editor_pro.py +72 -22
- setiastro/saspro/curves_preset.py +249 -47
- setiastro/saspro/doc_manager.py +4 -1
- setiastro/saspro/gui/main_window.py +326 -46
- setiastro/saspro/gui/mixins/file_mixin.py +41 -18
- setiastro/saspro/gui/mixins/menu_mixin.py +9 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +123 -7
- setiastro/saspro/histogram.py +179 -7
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +1429 -0
- setiastro/saspro/layers.py +186 -10
- setiastro/saspro/layers_dock.py +198 -5
- setiastro/saspro/legacy/image_manager.py +10 -4
- setiastro/saspro/legacy/numba_utils.py +1 -1
- setiastro/saspro/live_stacking.py +24 -4
- setiastro/saspro/multiscale_decomp.py +30 -17
- setiastro/saspro/narrowband_normalization.py +1618 -0
- setiastro/saspro/planetprojection.py +3854 -0
- setiastro/saspro/remove_green.py +1 -1
- setiastro/saspro/resources.py +8 -0
- setiastro/saspro/rgbalign.py +456 -12
- setiastro/saspro/save_options.py +45 -13
- setiastro/saspro/ser_stack_config.py +102 -0
- setiastro/saspro/ser_stacker.py +2327 -0
- setiastro/saspro/ser_stacker_dialog.py +1865 -0
- setiastro/saspro/ser_tracking.py +228 -0
- setiastro/saspro/serviewer.py +1773 -0
- setiastro/saspro/sfcc.py +298 -64
- setiastro/saspro/shortcuts.py +14 -7
- setiastro/saspro/stacking_suite.py +21 -6
- setiastro/saspro/stat_stretch.py +179 -31
- setiastro/saspro/subwindow.py +38 -5
- setiastro/saspro/texture_clarity.py +593 -0
- setiastro/saspro/widgets/resource_monitor.py +122 -74
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/METADATA +3 -2
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/RECORD +51 -37
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/licenses/license.txt +0 -0
|
@@ -13,13 +13,14 @@ from PyQt6.QtWidgets import QMenu, QToolButton
|
|
|
13
13
|
from PyQt6.QtCore import QElapsedTimer
|
|
14
14
|
|
|
15
15
|
import sys
|
|
16
|
+
import os
|
|
16
17
|
|
|
17
18
|
if TYPE_CHECKING:
|
|
18
19
|
pass
|
|
19
20
|
|
|
20
21
|
# Import icon paths - these are needed at runtime
|
|
21
22
|
from setiastro.saspro.resources import (
|
|
22
|
-
icon_path, green_path, neutral_path, whitebalance_path,
|
|
23
|
+
icon_path, green_path, neutral_path, whitebalance_path, texture_clarity_path,
|
|
23
24
|
morpho_path, clahe_path, starnet_path, staradd_path, LExtract_path,
|
|
24
25
|
LInsert_path, rgbcombo_path, rgbextract_path, graxperticon_path,
|
|
25
26
|
cropicon_path, openfile_path, abeicon_path, undoicon_path, redoicon_path,
|
|
@@ -31,11 +32,11 @@ from setiastro.saspro.resources import (
|
|
|
31
32
|
stacking_path, pedestal_icon_path, starspike_path, astrospike_path,
|
|
32
33
|
signature_icon_path, livestacking_path, convoicon_path, spcc_icon_path,
|
|
33
34
|
exoicon_path, peeker_icon, dse_icon_path, isophote_path, statstretch_path,
|
|
34
|
-
starstretch_path, curves_path, disk_path, uhs_path, blink_path, ppp_path,
|
|
35
|
+
starstretch_path, curves_path, disk_path, uhs_path, blink_path, ppp_path, narrowbandnormalization_path,
|
|
35
36
|
nbtorgb_path, freqsep_path, multiscale_decomp_path, contsub_path, halo_path, cosmic_path,
|
|
36
37
|
satellite_path, imagecombine_path, wims_path, wimi_path, linearfit_path,
|
|
37
|
-
debayer_path, aberration_path, functionbundles_path, viewbundles_path,
|
|
38
|
-
selectivecolor_path, rgbalign_path,
|
|
38
|
+
debayer_path, aberration_path, functionbundles_path, viewbundles_path, planetarystacker_path,
|
|
39
|
+
selectivecolor_path, rgbalign_path, planetprojection_path,
|
|
39
40
|
)
|
|
40
41
|
|
|
41
42
|
# Import shortcuts module
|
|
@@ -222,6 +223,7 @@ class ToolbarMixin:
|
|
|
222
223
|
tb_fn.addAction(self.act_wavescale_hdr)
|
|
223
224
|
tb_fn.addAction(self.act_wavescale_de)
|
|
224
225
|
tb_fn.addAction(self.act_clahe)
|
|
226
|
+
tb_fn.addAction(self.act_texture_clarity)
|
|
225
227
|
tb_fn.addAction(self.act_morphology)
|
|
226
228
|
tb_fn.addAction(self.act_pixelmath)
|
|
227
229
|
tb_fn.addAction(self.act_signature)
|
|
@@ -256,6 +258,8 @@ class ToolbarMixin:
|
|
|
256
258
|
tb_tl.addAction(self.act_blink) # Tools start here; Blink shows with QIcon(blink_path)
|
|
257
259
|
tb_tl.addAction(self.act_ppp) # Perfect Palette Picker
|
|
258
260
|
tb_tl.addAction(self.act_nbtorgb)
|
|
261
|
+
tb_tl.addAction(self.act_narrowband_normalization)
|
|
262
|
+
|
|
259
263
|
tb_tl.addAction(self.act_selective_color)
|
|
260
264
|
tb_tl.addAction(self.act_freqsep)
|
|
261
265
|
tb_tl.addAction(self.act_multiscale_decomp)
|
|
@@ -302,6 +306,8 @@ class ToolbarMixin:
|
|
|
302
306
|
tb_star.addAction(self.act_psf_viewer)
|
|
303
307
|
tb_star.addAction(self.act_stacking_suite)
|
|
304
308
|
tb_star.addAction(self.act_live_stacking)
|
|
309
|
+
tb_star.addAction(self.act_planetary_stacker)
|
|
310
|
+
tb_star.addAction(self.act_planet_projection)
|
|
305
311
|
tb_star.addAction(self.act_plate_solve)
|
|
306
312
|
tb_star.addAction(self.act_star_align)
|
|
307
313
|
tb_star.addAction(self.act_star_register)
|
|
@@ -367,6 +373,7 @@ class ToolbarMixin:
|
|
|
367
373
|
tb_hidden.setObjectName("Hidden")
|
|
368
374
|
tb_hidden.setSettingsKey("Toolbar/Hidden")
|
|
369
375
|
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_hidden)
|
|
376
|
+
#tb_hidden.addAction(self.act_narrowband_normalization)
|
|
370
377
|
tb_hidden.setVisible(False) # <- always hidden
|
|
371
378
|
|
|
372
379
|
# This can move actions between toolbars, so do it after each toolbar has its base order restored.
|
|
@@ -829,7 +836,13 @@ class ToolbarMixin:
|
|
|
829
836
|
self.act_paste_view.setShortcut("Ctrl+Shift+V")
|
|
830
837
|
self.act_copy_view.triggered.connect(self._copy_active_view)
|
|
831
838
|
self.act_paste_view.triggered.connect(self._paste_active_view)
|
|
832
|
-
|
|
839
|
+
# --- Edit: Mono -> RGB (triplicate channels) ---
|
|
840
|
+
self.act_mono_to_rgb = QAction(self.tr("Convert Mono to RGB"), self)
|
|
841
|
+
self.act_mono_to_rgb.setStatusTip(self.tr("Convert a mono image to RGB by duplicating the channel"))
|
|
842
|
+
self.act_mono_to_rgb.triggered.connect(self._convert_mono_to_rgb_active)
|
|
843
|
+
self.act_swap_rb = QAction(self.tr("Swap R and B Channels"), self)
|
|
844
|
+
self.act_swap_rb.setStatusTip(self.tr("Swap red and blue channels on the active RGB image"))
|
|
845
|
+
self.act_swap_rb.triggered.connect(self._swap_rb_active)
|
|
833
846
|
# Functions
|
|
834
847
|
self.act_crop = QAction(QIcon(cropicon_path), self.tr("Crop..."), self)
|
|
835
848
|
self.act_crop.setStatusTip(self.tr("Crop / rotate with handles"))
|
|
@@ -893,11 +906,17 @@ class ToolbarMixin:
|
|
|
893
906
|
self.act_linear_fit.setShortcut("Ctrl+L")
|
|
894
907
|
self.act_linear_fit.triggered.connect(self._open_linear_fit)
|
|
895
908
|
|
|
896
|
-
self.act_remove_green = QAction(QIcon(green_path), self.tr("Remove Green..."), self)
|
|
909
|
+
self.act_remove_green = QAction(QIcon(green_path), self.tr("Remove Green (SCNR)..."), self)
|
|
897
910
|
self.act_remove_green.setToolTip(self.tr("SCNR-style green channel removal."))
|
|
898
911
|
self.act_remove_green.setIconVisibleInMenu(True)
|
|
899
912
|
self.act_remove_green.triggered.connect(self._open_remove_green)
|
|
900
913
|
|
|
914
|
+
# Texture and Clarity
|
|
915
|
+
self.act_texture_clarity = QAction(QIcon(texture_clarity_path), self.tr("Texture and Clarity..."), self)
|
|
916
|
+
self.act_texture_clarity.setToolTip(self.tr("Enhance texture and clarity using Unsharp Masking"))
|
|
917
|
+
self.act_texture_clarity.setIconVisibleInMenu(True)
|
|
918
|
+
self.act_texture_clarity.triggered.connect(self._open_texture_clarity)
|
|
919
|
+
|
|
901
920
|
self.act_background_neutral = QAction(QIcon(neutral_path), self.tr("Background Neutralization..."), self)
|
|
902
921
|
self.act_background_neutral.setStatusTip(self.tr("Neutralize background color balance using a sampled region"))
|
|
903
922
|
self.act_background_neutral.setIconVisibleInMenu(True)
|
|
@@ -1118,6 +1137,13 @@ class ToolbarMixin:
|
|
|
1118
1137
|
self.act_ppp.setStatusTip(self.tr("Pick the perfect palette for your image"))
|
|
1119
1138
|
self.act_ppp.triggered.connect(self._open_ppp_tool)
|
|
1120
1139
|
|
|
1140
|
+
self.act_narrowband_normalization = QAction(QIcon(narrowbandnormalization_path), self.tr("Narrowband Normalization..."), self)
|
|
1141
|
+
self.act_narrowband_normalization.setStatusTip(
|
|
1142
|
+
self.tr("Normalize HOO/SHO/HSO/HOS (PixelMath port by Bill Blanshan and Mike Cranfield)")
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1145
|
+
self.act_narrowband_normalization.triggered.connect(self._open_narrowband_normalization_tool)
|
|
1146
|
+
|
|
1121
1147
|
self.act_nbtorgb = QAction(QIcon(nbtorgb_path), self.tr("NB->RGB Stars..."), self)
|
|
1122
1148
|
self.act_nbtorgb.setStatusTip(self.tr("Combine narrowband to RGB with optional OSC stars"))
|
|
1123
1149
|
self.act_nbtorgb.setIconVisibleInMenu(True)
|
|
@@ -1165,6 +1191,16 @@ class ToolbarMixin:
|
|
|
1165
1191
|
self.act_live_stacking.setStatusTip(self.tr("Live monitor and stack incoming frames"))
|
|
1166
1192
|
self.act_live_stacking.triggered.connect(self._open_live_stacking)
|
|
1167
1193
|
|
|
1194
|
+
self.act_planetary_stacker = QAction(QIcon(planetarystacker_path), self.tr("Planetary Stacker..."), self)
|
|
1195
|
+
self.act_planetary_stacker.setIconVisibleInMenu(True)
|
|
1196
|
+
self.act_planetary_stacker.setStatusTip(self.tr("Stack SER videos (planetary/solar/lunar)"))
|
|
1197
|
+
self.act_planetary_stacker.triggered.connect(self._open_planetary_stacker)
|
|
1198
|
+
|
|
1199
|
+
self.act_planet_projection = QAction(QIcon(planetprojection_path), self.tr("Planetary Projection..."), self)
|
|
1200
|
+
self.act_planet_projection.setIconVisibleInMenu(True)
|
|
1201
|
+
self.act_planet_projection.setStatusTip(self.tr("View your planets with stereographic projection"))
|
|
1202
|
+
self.act_planet_projection.triggered.connect(self._open_planet_projection)
|
|
1203
|
+
|
|
1168
1204
|
self.act_plate_solve = QAction(QIcon(platesolve_path), self.tr("Plate Solver..."), self)
|
|
1169
1205
|
self.act_plate_solve.setIconVisibleInMenu(True)
|
|
1170
1206
|
self.act_plate_solve.setStatusTip(self.tr("Solve WCS/SIP for the active image or a file"))
|
|
@@ -1361,6 +1397,7 @@ class ToolbarMixin:
|
|
|
1361
1397
|
reg("nbtorgb", self.act_nbtorgb)
|
|
1362
1398
|
reg("freqsep", self.act_freqsep)
|
|
1363
1399
|
reg("selective_color", self.act_selective_color)
|
|
1400
|
+
reg("narrowband_normalization", self.act_narrowband_normalization)
|
|
1364
1401
|
reg("contsub", self.act_contsub)
|
|
1365
1402
|
reg("abe", self.act_abe)
|
|
1366
1403
|
reg("create_mask", self.act_create_mask)
|
|
@@ -1369,6 +1406,7 @@ class ToolbarMixin:
|
|
|
1369
1406
|
reg("add_stars", self.act_add_stars)
|
|
1370
1407
|
reg("pedestal", self.act_pedestal)
|
|
1371
1408
|
reg("remove_green", self.act_remove_green)
|
|
1409
|
+
reg("texture_clarity", self.act_texture_clarity)
|
|
1372
1410
|
reg("background_neutral", self.act_background_neutral)
|
|
1373
1411
|
reg("white_balance", self.act_white_balance)
|
|
1374
1412
|
reg("sfcc", self.act_sfcc)
|
|
@@ -1385,7 +1423,7 @@ class ToolbarMixin:
|
|
|
1385
1423
|
reg("pixel_math", self.act_pixelmath)
|
|
1386
1424
|
reg("signature_insert", self.act_signature)
|
|
1387
1425
|
reg("halo_b_gon", self.act_halobgon)
|
|
1388
|
-
|
|
1426
|
+
reg("planetary_stacker", self.act_planetary_stacker)
|
|
1389
1427
|
reg("multiscale_decomp", self.act_multiscale_decomp)
|
|
1390
1428
|
reg("geom_invert", self.act_geom_invert)
|
|
1391
1429
|
reg("geom_flip_horizontal", self.act_geom_flip_h)
|
|
@@ -1407,6 +1445,7 @@ class ToolbarMixin:
|
|
|
1407
1445
|
reg("image_peeker", self.act_image_peeker)
|
|
1408
1446
|
reg("live_stacking", self.act_live_stacking)
|
|
1409
1447
|
reg("stacking_suite", self.act_stacking_suite)
|
|
1448
|
+
reg("planet_projection", self.act_planet_projection)
|
|
1410
1449
|
reg("supernova_hunter", self.act_supernova_hunter)
|
|
1411
1450
|
reg("star_spikes", self.act_star_spikes)
|
|
1412
1451
|
reg("astrospike", self.act_astrospike)
|
|
@@ -1423,6 +1462,83 @@ class ToolbarMixin:
|
|
|
1423
1462
|
reg("view_bundles", self.act_view_bundles)
|
|
1424
1463
|
reg("function_bundles", self.act_function_bundles)
|
|
1425
1464
|
|
|
1465
|
+
def _reset_all_toolbars_to_factory(self):
|
|
1466
|
+
"""
|
|
1467
|
+
Clears all persisted toolbar membership/order/hidden state so the UI
|
|
1468
|
+
returns to the factory layout defined in code.
|
|
1469
|
+
"""
|
|
1470
|
+
if not hasattr(self, "settings"):
|
|
1471
|
+
return
|
|
1472
|
+
|
|
1473
|
+
# 1) Clear global toolbar persistence
|
|
1474
|
+
keys_to_clear = [
|
|
1475
|
+
"Toolbar/Assignments",
|
|
1476
|
+
"Toolbar/HiddenPrev",
|
|
1477
|
+
"Toolbar/HiddenMigrationDone",
|
|
1478
|
+
|
|
1479
|
+
# Per-toolbar order lists
|
|
1480
|
+
"Toolbar/View",
|
|
1481
|
+
"Toolbar/Functions",
|
|
1482
|
+
"Toolbar/Cosmic",
|
|
1483
|
+
"Toolbar/Tools",
|
|
1484
|
+
"Toolbar/Geometry",
|
|
1485
|
+
"Toolbar/StarStuff",
|
|
1486
|
+
"Toolbar/Masks",
|
|
1487
|
+
"Toolbar/WhatsInMy",
|
|
1488
|
+
"Toolbar/Bundles",
|
|
1489
|
+
"Toolbar/Hidden",
|
|
1490
|
+
]
|
|
1491
|
+
|
|
1492
|
+
for k in keys_to_clear:
|
|
1493
|
+
try:
|
|
1494
|
+
self.settings.remove(k)
|
|
1495
|
+
except Exception:
|
|
1496
|
+
pass
|
|
1497
|
+
|
|
1498
|
+
# 2) Clear legacy hidden lists (old system) if they exist
|
|
1499
|
+
# (These are the ones like "Toolbar/Tools/Hidden")
|
|
1500
|
+
try:
|
|
1501
|
+
# brute-force remove known ones
|
|
1502
|
+
legacy_hidden = [
|
|
1503
|
+
"Toolbar/View/Hidden",
|
|
1504
|
+
"Toolbar/Functions/Hidden",
|
|
1505
|
+
"Toolbar/Cosmic/Hidden",
|
|
1506
|
+
"Toolbar/Tools/Hidden",
|
|
1507
|
+
"Toolbar/Geometry/Hidden",
|
|
1508
|
+
"Toolbar/StarStuff/Hidden",
|
|
1509
|
+
"Toolbar/Masks/Hidden",
|
|
1510
|
+
"Toolbar/WhatsInMy/Hidden",
|
|
1511
|
+
"Toolbar/Bundles/Hidden",
|
|
1512
|
+
]
|
|
1513
|
+
for k in legacy_hidden:
|
|
1514
|
+
self.settings.remove(k)
|
|
1515
|
+
except Exception:
|
|
1516
|
+
pass
|
|
1517
|
+
|
|
1518
|
+
try:
|
|
1519
|
+
self.settings.sync()
|
|
1520
|
+
except Exception:
|
|
1521
|
+
pass
|
|
1522
|
+
|
|
1523
|
+
# 3) Rebuild toolbars from code defaults
|
|
1524
|
+
# Safest approach: remove existing toolbars and rebuild.
|
|
1525
|
+
from setiastro.saspro.shortcuts import DraggableToolBar
|
|
1526
|
+
for tb in self.findChildren(DraggableToolBar):
|
|
1527
|
+
try:
|
|
1528
|
+
self.removeToolBar(tb)
|
|
1529
|
+
tb.deleteLater()
|
|
1530
|
+
except Exception:
|
|
1531
|
+
pass
|
|
1532
|
+
|
|
1533
|
+
# Build fresh
|
|
1534
|
+
self._init_toolbar()
|
|
1535
|
+
|
|
1536
|
+
# Optional: ensure Hidden stays hidden
|
|
1537
|
+
tb_hidden = self._hidden_toolbar()
|
|
1538
|
+
if tb_hidden:
|
|
1539
|
+
tb_hidden.setVisible(False)
|
|
1540
|
+
|
|
1541
|
+
|
|
1426
1542
|
def _restore_toolbar_order(self, tb, settings_key: str):
|
|
1427
1543
|
"""
|
|
1428
1544
|
Restore toolbar action order from QSettings, using command_id/objectName.
|
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()
|