setiastrosuitepro 1.6.7__py3-none-any.whl → 1.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of setiastrosuitepro might be problematic. Click here for more details.

Files changed (68) hide show
  1. setiastro/images/abeicon.svg +16 -0
  2. setiastro/images/colorwheel.svg +97 -0
  3. setiastro/images/cosmic.svg +40 -0
  4. setiastro/images/cosmicsat.svg +24 -0
  5. setiastro/images/graxpert.svg +19 -0
  6. setiastro/images/linearfit.svg +32 -0
  7. setiastro/images/narrowbandnormalization.png +0 -0
  8. setiastro/images/pixelmath.svg +42 -0
  9. setiastro/images/planetarystacker.png +0 -0
  10. setiastro/saspro/__main__.py +1 -1
  11. setiastro/saspro/_generated/build_info.py +2 -2
  12. setiastro/saspro/aberration_ai.py +49 -11
  13. setiastro/saspro/aberration_ai_preset.py +29 -3
  14. setiastro/saspro/add_stars.py +29 -5
  15. setiastro/saspro/backgroundneutral.py +73 -33
  16. setiastro/saspro/blink_comparator_pro.py +150 -55
  17. setiastro/saspro/convo.py +9 -6
  18. setiastro/saspro/cosmicclarity.py +125 -18
  19. setiastro/saspro/crop_dialog_pro.py +96 -2
  20. setiastro/saspro/curve_editor_pro.py +132 -61
  21. setiastro/saspro/curves_preset.py +249 -47
  22. setiastro/saspro/doc_manager.py +178 -11
  23. setiastro/saspro/frequency_separation.py +1159 -208
  24. setiastro/saspro/gui/main_window.py +340 -88
  25. setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
  26. setiastro/saspro/gui/mixins/file_mixin.py +35 -16
  27. setiastro/saspro/gui/mixins/menu_mixin.py +31 -1
  28. setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
  29. setiastro/saspro/gui/mixins/toolbar_mixin.py +132 -10
  30. setiastro/saspro/gui/mixins/update_mixin.py +121 -33
  31. setiastro/saspro/histogram.py +179 -7
  32. setiastro/saspro/imageops/narrowband_normalization.py +816 -0
  33. setiastro/saspro/imageops/serloader.py +769 -0
  34. setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
  35. setiastro/saspro/imageops/stretch.py +582 -62
  36. setiastro/saspro/layers.py +13 -9
  37. setiastro/saspro/layers_dock.py +183 -3
  38. setiastro/saspro/legacy/numba_utils.py +68 -48
  39. setiastro/saspro/live_stacking.py +181 -73
  40. setiastro/saspro/multiscale_decomp.py +77 -29
  41. setiastro/saspro/narrowband_normalization.py +1618 -0
  42. setiastro/saspro/numba_utils.py +72 -57
  43. setiastro/saspro/ops/commands.py +18 -18
  44. setiastro/saspro/ops/script_editor.py +5 -0
  45. setiastro/saspro/ops/scripts.py +119 -0
  46. setiastro/saspro/remove_green.py +1 -1
  47. setiastro/saspro/resources.py +4 -0
  48. setiastro/saspro/ser_stack_config.py +68 -0
  49. setiastro/saspro/ser_stacker.py +2245 -0
  50. setiastro/saspro/ser_stacker_dialog.py +1481 -0
  51. setiastro/saspro/ser_tracking.py +206 -0
  52. setiastro/saspro/serviewer.py +1242 -0
  53. setiastro/saspro/sfcc.py +602 -214
  54. setiastro/saspro/shortcuts.py +154 -25
  55. setiastro/saspro/signature_insert.py +688 -33
  56. setiastro/saspro/stacking_suite.py +853 -401
  57. setiastro/saspro/star_alignment.py +243 -122
  58. setiastro/saspro/stat_stretch.py +878 -131
  59. setiastro/saspro/subwindow.py +303 -74
  60. setiastro/saspro/whitebalance.py +24 -0
  61. setiastro/saspro/widgets/common_utilities.py +28 -21
  62. setiastro/saspro/widgets/resource_monitor.py +128 -80
  63. {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/METADATA +2 -2
  64. {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/RECORD +68 -51
  65. {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/WHEEL +0 -0
  66. {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/entry_points.txt +0 -0
  67. {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/licenses/LICENSE +0 -0
  68. {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/licenses/license.txt +0 -0
@@ -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()