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.

Files changed (51) hide show
  1. setiastro/images/3dplanet.png +0 -0
  2. setiastro/images/TextureClarity.svg +56 -0
  3. setiastro/images/narrowbandnormalization.png +0 -0
  4. setiastro/images/planetarystacker.png +0 -0
  5. setiastro/saspro/__init__.py +9 -8
  6. setiastro/saspro/__main__.py +326 -285
  7. setiastro/saspro/_generated/build_info.py +2 -2
  8. setiastro/saspro/aberration_ai.py +128 -13
  9. setiastro/saspro/aberration_ai_preset.py +29 -3
  10. setiastro/saspro/astrospike_python.py +45 -3
  11. setiastro/saspro/blink_comparator_pro.py +116 -71
  12. setiastro/saspro/curve_editor_pro.py +72 -22
  13. setiastro/saspro/curves_preset.py +249 -47
  14. setiastro/saspro/doc_manager.py +4 -1
  15. setiastro/saspro/gui/main_window.py +326 -46
  16. setiastro/saspro/gui/mixins/file_mixin.py +41 -18
  17. setiastro/saspro/gui/mixins/menu_mixin.py +9 -0
  18. setiastro/saspro/gui/mixins/toolbar_mixin.py +123 -7
  19. setiastro/saspro/histogram.py +179 -7
  20. setiastro/saspro/imageops/narrowband_normalization.py +816 -0
  21. setiastro/saspro/imageops/serloader.py +1429 -0
  22. setiastro/saspro/layers.py +186 -10
  23. setiastro/saspro/layers_dock.py +198 -5
  24. setiastro/saspro/legacy/image_manager.py +10 -4
  25. setiastro/saspro/legacy/numba_utils.py +1 -1
  26. setiastro/saspro/live_stacking.py +24 -4
  27. setiastro/saspro/multiscale_decomp.py +30 -17
  28. setiastro/saspro/narrowband_normalization.py +1618 -0
  29. setiastro/saspro/planetprojection.py +3854 -0
  30. setiastro/saspro/remove_green.py +1 -1
  31. setiastro/saspro/resources.py +8 -0
  32. setiastro/saspro/rgbalign.py +456 -12
  33. setiastro/saspro/save_options.py +45 -13
  34. setiastro/saspro/ser_stack_config.py +102 -0
  35. setiastro/saspro/ser_stacker.py +2327 -0
  36. setiastro/saspro/ser_stacker_dialog.py +1865 -0
  37. setiastro/saspro/ser_tracking.py +228 -0
  38. setiastro/saspro/serviewer.py +1773 -0
  39. setiastro/saspro/sfcc.py +298 -64
  40. setiastro/saspro/shortcuts.py +14 -7
  41. setiastro/saspro/stacking_suite.py +21 -6
  42. setiastro/saspro/stat_stretch.py +179 -31
  43. setiastro/saspro/subwindow.py +38 -5
  44. setiastro/saspro/texture_clarity.py +593 -0
  45. setiastro/saspro/widgets/resource_monitor.py +122 -74
  46. {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/METADATA +3 -2
  47. {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/RECORD +51 -37
  48. {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/WHEEL +0 -0
  49. {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/entry_points.txt +0 -0
  50. {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/licenses/LICENSE +0 -0
  51. {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.
@@ -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, # <- was Fixed
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
- # hdr.setStretchLastSection(True)
148
- splitter.addWidget(self.stats_table)
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()