setiastrosuitepro 1.6.12__py3-none-any.whl → 1.7.1.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.
- setiastro/images/TextureClarity.svg +56 -0
- setiastro/images/narrowbandnormalization.png +0 -0
- setiastro/images/planetarystacker.png +0 -0
- 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/gui/main_window.py +285 -44
- setiastro/saspro/gui/mixins/file_mixin.py +35 -16
- setiastro/saspro/gui/mixins/menu_mixin.py +8 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +115 -6
- setiastro/saspro/histogram.py +179 -7
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +1345 -0
- 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/remove_green.py +1 -1
- setiastro/saspro/resources.py +6 -0
- setiastro/saspro/rgbalign.py +456 -12
- setiastro/saspro/ser_stack_config.py +82 -0
- setiastro/saspro/ser_stacker.py +2321 -0
- setiastro/saspro/ser_stacker_dialog.py +1838 -0
- setiastro/saspro/ser_tracking.py +206 -0
- setiastro/saspro/serviewer.py +1625 -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 +2 -4
- setiastro/saspro/texture_clarity.py +593 -0
- setiastro/saspro/widgets/resource_monitor.py +122 -74
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/METADATA +3 -2
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/RECORD +42 -30
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.1.post2.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,10 +32,10 @@ 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
|
+
debayer_path, aberration_path, functionbundles_path, viewbundles_path, planetarystacker_path,
|
|
38
39
|
selectivecolor_path, rgbalign_path,
|
|
39
40
|
)
|
|
40
41
|
|
|
@@ -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,7 @@ 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)
|
|
305
310
|
tb_star.addAction(self.act_plate_solve)
|
|
306
311
|
tb_star.addAction(self.act_star_align)
|
|
307
312
|
tb_star.addAction(self.act_star_register)
|
|
@@ -367,6 +372,7 @@ class ToolbarMixin:
|
|
|
367
372
|
tb_hidden.setObjectName("Hidden")
|
|
368
373
|
tb_hidden.setSettingsKey("Toolbar/Hidden")
|
|
369
374
|
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_hidden)
|
|
375
|
+
#tb_hidden.addAction(self.act_narrowband_normalization)
|
|
370
376
|
tb_hidden.setVisible(False) # <- always hidden
|
|
371
377
|
|
|
372
378
|
# This can move actions between toolbars, so do it after each toolbar has its base order restored.
|
|
@@ -829,7 +835,13 @@ class ToolbarMixin:
|
|
|
829
835
|
self.act_paste_view.setShortcut("Ctrl+Shift+V")
|
|
830
836
|
self.act_copy_view.triggered.connect(self._copy_active_view)
|
|
831
837
|
self.act_paste_view.triggered.connect(self._paste_active_view)
|
|
832
|
-
|
|
838
|
+
# --- Edit: Mono -> RGB (triplicate channels) ---
|
|
839
|
+
self.act_mono_to_rgb = QAction(self.tr("Convert Mono to RGB"), self)
|
|
840
|
+
self.act_mono_to_rgb.setStatusTip(self.tr("Convert a mono image to RGB by duplicating the channel"))
|
|
841
|
+
self.act_mono_to_rgb.triggered.connect(self._convert_mono_to_rgb_active)
|
|
842
|
+
self.act_swap_rb = QAction(self.tr("Swap R and B Channels"), self)
|
|
843
|
+
self.act_swap_rb.setStatusTip(self.tr("Swap red and blue channels on the active RGB image"))
|
|
844
|
+
self.act_swap_rb.triggered.connect(self._swap_rb_active)
|
|
833
845
|
# Functions
|
|
834
846
|
self.act_crop = QAction(QIcon(cropicon_path), self.tr("Crop..."), self)
|
|
835
847
|
self.act_crop.setStatusTip(self.tr("Crop / rotate with handles"))
|
|
@@ -893,11 +905,17 @@ class ToolbarMixin:
|
|
|
893
905
|
self.act_linear_fit.setShortcut("Ctrl+L")
|
|
894
906
|
self.act_linear_fit.triggered.connect(self._open_linear_fit)
|
|
895
907
|
|
|
896
|
-
self.act_remove_green = QAction(QIcon(green_path), self.tr("Remove Green..."), self)
|
|
908
|
+
self.act_remove_green = QAction(QIcon(green_path), self.tr("Remove Green (SCNR)..."), self)
|
|
897
909
|
self.act_remove_green.setToolTip(self.tr("SCNR-style green channel removal."))
|
|
898
910
|
self.act_remove_green.setIconVisibleInMenu(True)
|
|
899
911
|
self.act_remove_green.triggered.connect(self._open_remove_green)
|
|
900
912
|
|
|
913
|
+
# Texture and Clarity
|
|
914
|
+
self.act_texture_clarity = QAction(QIcon(texture_clarity_path), self.tr("Texture and Clarity..."), self)
|
|
915
|
+
self.act_texture_clarity.setToolTip(self.tr("Enhance texture and clarity using Unsharp Masking"))
|
|
916
|
+
self.act_texture_clarity.setIconVisibleInMenu(True)
|
|
917
|
+
self.act_texture_clarity.triggered.connect(self._open_texture_clarity)
|
|
918
|
+
|
|
901
919
|
self.act_background_neutral = QAction(QIcon(neutral_path), self.tr("Background Neutralization..."), self)
|
|
902
920
|
self.act_background_neutral.setStatusTip(self.tr("Neutralize background color balance using a sampled region"))
|
|
903
921
|
self.act_background_neutral.setIconVisibleInMenu(True)
|
|
@@ -1118,6 +1136,13 @@ class ToolbarMixin:
|
|
|
1118
1136
|
self.act_ppp.setStatusTip(self.tr("Pick the perfect palette for your image"))
|
|
1119
1137
|
self.act_ppp.triggered.connect(self._open_ppp_tool)
|
|
1120
1138
|
|
|
1139
|
+
self.act_narrowband_normalization = QAction(QIcon(narrowbandnormalization_path), self.tr("Narrowband Normalization..."), self)
|
|
1140
|
+
self.act_narrowband_normalization.setStatusTip(
|
|
1141
|
+
self.tr("Normalize HOO/SHO/HSO/HOS (PixelMath port by Bill Blanshan and Mike Cranfield)")
|
|
1142
|
+
)
|
|
1143
|
+
|
|
1144
|
+
self.act_narrowband_normalization.triggered.connect(self._open_narrowband_normalization_tool)
|
|
1145
|
+
|
|
1121
1146
|
self.act_nbtorgb = QAction(QIcon(nbtorgb_path), self.tr("NB->RGB Stars..."), self)
|
|
1122
1147
|
self.act_nbtorgb.setStatusTip(self.tr("Combine narrowband to RGB with optional OSC stars"))
|
|
1123
1148
|
self.act_nbtorgb.setIconVisibleInMenu(True)
|
|
@@ -1165,6 +1190,11 @@ class ToolbarMixin:
|
|
|
1165
1190
|
self.act_live_stacking.setStatusTip(self.tr("Live monitor and stack incoming frames"))
|
|
1166
1191
|
self.act_live_stacking.triggered.connect(self._open_live_stacking)
|
|
1167
1192
|
|
|
1193
|
+
self.act_planetary_stacker = QAction(QIcon(planetarystacker_path), self.tr("Planetary Stacker..."), self)
|
|
1194
|
+
self.act_planetary_stacker.setIconVisibleInMenu(True)
|
|
1195
|
+
self.act_planetary_stacker.setStatusTip(self.tr("Stack SER videos (planetary/solar/lunar)"))
|
|
1196
|
+
self.act_planetary_stacker.triggered.connect(self._open_planetary_stacker)
|
|
1197
|
+
|
|
1168
1198
|
self.act_plate_solve = QAction(QIcon(platesolve_path), self.tr("Plate Solver..."), self)
|
|
1169
1199
|
self.act_plate_solve.setIconVisibleInMenu(True)
|
|
1170
1200
|
self.act_plate_solve.setStatusTip(self.tr("Solve WCS/SIP for the active image or a file"))
|
|
@@ -1361,6 +1391,7 @@ class ToolbarMixin:
|
|
|
1361
1391
|
reg("nbtorgb", self.act_nbtorgb)
|
|
1362
1392
|
reg("freqsep", self.act_freqsep)
|
|
1363
1393
|
reg("selective_color", self.act_selective_color)
|
|
1394
|
+
reg("narrowband_normalization", self.act_narrowband_normalization)
|
|
1364
1395
|
reg("contsub", self.act_contsub)
|
|
1365
1396
|
reg("abe", self.act_abe)
|
|
1366
1397
|
reg("create_mask", self.act_create_mask)
|
|
@@ -1369,6 +1400,7 @@ class ToolbarMixin:
|
|
|
1369
1400
|
reg("add_stars", self.act_add_stars)
|
|
1370
1401
|
reg("pedestal", self.act_pedestal)
|
|
1371
1402
|
reg("remove_green", self.act_remove_green)
|
|
1403
|
+
reg("texture_clarity", self.act_texture_clarity)
|
|
1372
1404
|
reg("background_neutral", self.act_background_neutral)
|
|
1373
1405
|
reg("white_balance", self.act_white_balance)
|
|
1374
1406
|
reg("sfcc", self.act_sfcc)
|
|
@@ -1385,7 +1417,7 @@ class ToolbarMixin:
|
|
|
1385
1417
|
reg("pixel_math", self.act_pixelmath)
|
|
1386
1418
|
reg("signature_insert", self.act_signature)
|
|
1387
1419
|
reg("halo_b_gon", self.act_halobgon)
|
|
1388
|
-
|
|
1420
|
+
reg("planetary_stacker", self.act_planetary_stacker)
|
|
1389
1421
|
reg("multiscale_decomp", self.act_multiscale_decomp)
|
|
1390
1422
|
reg("geom_invert", self.act_geom_invert)
|
|
1391
1423
|
reg("geom_flip_horizontal", self.act_geom_flip_h)
|
|
@@ -1423,6 +1455,83 @@ class ToolbarMixin:
|
|
|
1423
1455
|
reg("view_bundles", self.act_view_bundles)
|
|
1424
1456
|
reg("function_bundles", self.act_function_bundles)
|
|
1425
1457
|
|
|
1458
|
+
def _reset_all_toolbars_to_factory(self):
|
|
1459
|
+
"""
|
|
1460
|
+
Clears all persisted toolbar membership/order/hidden state so the UI
|
|
1461
|
+
returns to the factory layout defined in code.
|
|
1462
|
+
"""
|
|
1463
|
+
if not hasattr(self, "settings"):
|
|
1464
|
+
return
|
|
1465
|
+
|
|
1466
|
+
# 1) Clear global toolbar persistence
|
|
1467
|
+
keys_to_clear = [
|
|
1468
|
+
"Toolbar/Assignments",
|
|
1469
|
+
"Toolbar/HiddenPrev",
|
|
1470
|
+
"Toolbar/HiddenMigrationDone",
|
|
1471
|
+
|
|
1472
|
+
# Per-toolbar order lists
|
|
1473
|
+
"Toolbar/View",
|
|
1474
|
+
"Toolbar/Functions",
|
|
1475
|
+
"Toolbar/Cosmic",
|
|
1476
|
+
"Toolbar/Tools",
|
|
1477
|
+
"Toolbar/Geometry",
|
|
1478
|
+
"Toolbar/StarStuff",
|
|
1479
|
+
"Toolbar/Masks",
|
|
1480
|
+
"Toolbar/WhatsInMy",
|
|
1481
|
+
"Toolbar/Bundles",
|
|
1482
|
+
"Toolbar/Hidden",
|
|
1483
|
+
]
|
|
1484
|
+
|
|
1485
|
+
for k in keys_to_clear:
|
|
1486
|
+
try:
|
|
1487
|
+
self.settings.remove(k)
|
|
1488
|
+
except Exception:
|
|
1489
|
+
pass
|
|
1490
|
+
|
|
1491
|
+
# 2) Clear legacy hidden lists (old system) if they exist
|
|
1492
|
+
# (These are the ones like "Toolbar/Tools/Hidden")
|
|
1493
|
+
try:
|
|
1494
|
+
# brute-force remove known ones
|
|
1495
|
+
legacy_hidden = [
|
|
1496
|
+
"Toolbar/View/Hidden",
|
|
1497
|
+
"Toolbar/Functions/Hidden",
|
|
1498
|
+
"Toolbar/Cosmic/Hidden",
|
|
1499
|
+
"Toolbar/Tools/Hidden",
|
|
1500
|
+
"Toolbar/Geometry/Hidden",
|
|
1501
|
+
"Toolbar/StarStuff/Hidden",
|
|
1502
|
+
"Toolbar/Masks/Hidden",
|
|
1503
|
+
"Toolbar/WhatsInMy/Hidden",
|
|
1504
|
+
"Toolbar/Bundles/Hidden",
|
|
1505
|
+
]
|
|
1506
|
+
for k in legacy_hidden:
|
|
1507
|
+
self.settings.remove(k)
|
|
1508
|
+
except Exception:
|
|
1509
|
+
pass
|
|
1510
|
+
|
|
1511
|
+
try:
|
|
1512
|
+
self.settings.sync()
|
|
1513
|
+
except Exception:
|
|
1514
|
+
pass
|
|
1515
|
+
|
|
1516
|
+
# 3) Rebuild toolbars from code defaults
|
|
1517
|
+
# Safest approach: remove existing toolbars and rebuild.
|
|
1518
|
+
from setiastro.saspro.shortcuts import DraggableToolBar
|
|
1519
|
+
for tb in self.findChildren(DraggableToolBar):
|
|
1520
|
+
try:
|
|
1521
|
+
self.removeToolBar(tb)
|
|
1522
|
+
tb.deleteLater()
|
|
1523
|
+
except Exception:
|
|
1524
|
+
pass
|
|
1525
|
+
|
|
1526
|
+
# Build fresh
|
|
1527
|
+
self._init_toolbar()
|
|
1528
|
+
|
|
1529
|
+
# Optional: ensure Hidden stays hidden
|
|
1530
|
+
tb_hidden = self._hidden_toolbar()
|
|
1531
|
+
if tb_hidden:
|
|
1532
|
+
tb_hidden.setVisible(False)
|
|
1533
|
+
|
|
1534
|
+
|
|
1426
1535
|
def _restore_toolbar_order(self, tb, settings_key: str):
|
|
1427
1536
|
"""
|
|
1428
1537
|
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()
|