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
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from PyQt6.QtCore import Qt, QSize, QEvent
4
4
  from PyQt6.QtWidgets import (
5
5
  QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLabel, QDoubleSpinBox,
6
- QCheckBox, QPushButton, QScrollArea, QWidget, QMessageBox, QSlider, QToolBar, QToolButton, QComboBox
6
+ QCheckBox, QPushButton, QScrollArea, QWidget, QMessageBox, QSlider, QToolBar, QToolButton, QComboBox,QProgressBar, QApplication
7
7
  )
8
8
  from PyQt6.QtGui import QImage, QPixmap, QMouseEvent, QCursor
9
9
  import numpy as np
@@ -320,6 +320,7 @@ class StatisticalStretchDialog(QDialog):
320
320
  self.btn_preview = QPushButton(self.tr("Preview"))
321
321
  self.btn_apply = QPushButton(self.tr("Apply"))
322
322
  self.btn_close = QPushButton(self.tr("Close"))
323
+ self.btn_reset = QPushButton(self.tr("Reset ⟳"))
323
324
 
324
325
  self.btn_clipstats = QPushButton(self.tr("Clip stats"))
325
326
  self.lbl_clipstats = QLabel("")
@@ -330,6 +331,24 @@ class StatisticalStretchDialog(QDialog):
330
331
  self.lbl_clipstats.setFrameShadow(QLabel.Shadow.Sunken)
331
332
  self.lbl_clipstats.setContentsMargins(6, 4, 6, 4)
332
333
 
334
+ # --- In-UI busy indicator (Wayland-friendly) ---
335
+ self.busy_row = QWidget()
336
+ br = QHBoxLayout(self.busy_row)
337
+ br.setContentsMargins(0, 0, 0, 0)
338
+ br.setSpacing(8)
339
+
340
+ self.lbl_busy = QLabel(self.tr("Processing…"))
341
+ self.lbl_busy.setStyleSheet("color:#888;")
342
+ self.pbar_busy = QProgressBar()
343
+ self.pbar_busy.setRange(0, 0) # indeterminate
344
+ self.pbar_busy.setTextVisible(False)
345
+ self.pbar_busy.setFixedHeight(10)
346
+
347
+ br.addWidget(self.lbl_busy)
348
+ br.addWidget(self.pbar_busy, 1)
349
+
350
+ self.busy_row.setVisible(False) # hidden until needed
351
+
333
352
  # ------------------------------------------------------------------
334
353
  # Layout
335
354
  # ------------------------------------------------------------------
@@ -353,10 +372,13 @@ class StatisticalStretchDialog(QDialog):
353
372
  btn_row.addWidget(self.btn_preview)
354
373
  btn_row.addWidget(self.btn_apply)
355
374
  btn_row.addWidget(self.btn_clipstats)
375
+ btn_row.addStretch(1)
376
+ btn_row.addWidget(self.btn_reset)
356
377
  btn_row.addStretch(1)
357
378
  left.addLayout(btn_row)
358
379
 
359
380
  left.addWidget(self.lbl_clipstats)
381
+ left.addWidget(self.busy_row)
360
382
  left.addStretch(1)
361
383
 
362
384
  right = QVBoxLayout()
@@ -409,14 +431,6 @@ class StatisticalStretchDialog(QDialog):
409
431
 
410
432
  self.spin_target.valueChanged.connect(_suggest_hdr_knee_from_target)
411
433
 
412
- # Luma-only: enables dropdown, disables "linked channels"
413
- self.chk_luma_only.toggled.connect(self.cmb_luma.setEnabled)
414
-
415
- def _on_luma_only_toggled(on: bool):
416
- self.chk_linked.setEnabled(not on)
417
-
418
- self.chk_luma_only.toggled.connect(_on_luma_only_toggled)
419
-
420
434
  # Zoom buttons
421
435
  self.btn_zoom_in.clicked.connect(lambda: self._zoom_by(1.25))
422
436
  self.btn_zoom_out.clicked.connect(lambda: self._zoom_by(1/1.25))
@@ -426,6 +440,7 @@ class StatisticalStretchDialog(QDialog):
426
440
  # Main buttons
427
441
  self.btn_preview.clicked.connect(self._do_preview)
428
442
  self.btn_apply.clicked.connect(self._do_apply)
443
+ self.btn_reset.clicked.connect(self._reset_defaults)
429
444
  self.btn_close.clicked.connect(self.close)
430
445
  self.btn_clipstats.clicked.connect(self._do_clip_stats)
431
446
 
@@ -441,55 +456,187 @@ class StatisticalStretchDialog(QDialog):
441
456
  lambda v: self.lbl_luma_blend.setText(f"{v/100:.2f}")
442
457
  )
443
458
 
459
+ # Luma-only: one unified handler for all dependent UI state
444
460
  def _on_luma_only_toggled(on: bool):
461
+ # enable luma mode dropdown only when luma-only is on
462
+ self.cmb_luma.setEnabled(on)
463
+
464
+ # linked channels doesn't make sense in luma-only mode
445
465
  self.chk_linked.setEnabled(not on)
466
+
467
+ # luma blend row only meaningful when luma-only is enabled
446
468
  self.luma_blend_row.setEnabled(on)
447
469
 
470
+ # mode-affecting => refresh clip stats
471
+ self._schedule_clip_stats()
472
+
448
473
  self.chk_luma_only.toggled.connect(_on_luma_only_toggled)
449
474
  _on_luma_only_toggled(self.chk_luma_only.isChecked())
450
475
 
476
+
451
477
  # Initial preview + clip stats
452
478
  self._populate_initial_preview()
453
479
 
454
480
 
455
481
  # ----- helpers -----
456
482
  def _show_busy(self, title: str, text: str):
457
- # Avoid stacking dialogs
458
- self._hide_busy()
459
-
460
- dlg = QProgressDialog(text, None, 0, 0, self)
461
- dlg.setWindowTitle(title)
462
- dlg.setWindowModality(Qt.WindowModality.WindowModal) # blocks only this tool window
463
- dlg.setMinimumDuration(0)
464
- dlg.setValue(0)
465
- dlg.setCancelButton(None) # no cancel button (keeps it simple)
466
- dlg.setAutoClose(False)
467
- dlg.setAutoReset(False)
468
- dlg.setFixedWidth(320)
469
- dlg.show()
470
-
471
- # Ensure it paints before heavy work starts
472
- QApplication.processEvents()
473
- self._busy = dlg
483
+ # title kept for signature compatibility; not shown
484
+ try:
485
+ self.lbl_busy.setText(text or self.tr("Processing…"))
486
+ self.busy_row.setVisible(True)
487
+ # make sure UI repaints before thread work starts
488
+ QApplication.processEvents()
489
+ except Exception:
490
+ pass
474
491
 
475
492
  def _hide_busy(self):
476
493
  try:
477
- if getattr(self, "_busy", None) is not None:
478
- self._busy.close()
479
- self._busy.deleteLater()
494
+ if getattr(self, "busy_row", None) is not None:
495
+ self.busy_row.setVisible(False)
480
496
  except Exception:
481
497
  pass
482
- self._busy = None
498
+
483
499
 
484
500
  def _set_controls_enabled(self, enabled: bool):
485
501
  try:
486
502
  self.btn_preview.setEnabled(enabled)
487
503
  self.btn_apply.setEnabled(enabled)
504
+ if getattr(self, "btn_reset", None) is not None:
505
+ self.btn_reset.setEnabled(enabled) # <-- NEW
488
506
  if getattr(self, "btn_clipstats", None) is not None:
489
507
  self.btn_clipstats.setEnabled(enabled)
490
508
  except Exception:
491
509
  pass
492
510
 
511
+ def _reset_defaults(self):
512
+ """Reset all controls back to factory defaults."""
513
+ if getattr(self, "_job_running", False):
514
+ return
515
+
516
+ # Defaults (must match your __init__ setValue/setChecked calls)
517
+ DEFAULT_TARGET = 0.25
518
+ DEFAULT_LINKED = False
519
+ DEFAULT_NORMALIZE = False
520
+ DEFAULT_BP_SLIDER = 500 # 5.00
521
+ DEFAULT_NO_BLACK_CLIP = False
522
+
523
+ DEFAULT_HDR_ON = False
524
+ DEFAULT_HDR_AMT = 15 # 0.15
525
+ DEFAULT_HDR_KNEE = 75 # 0.75
526
+
527
+ DEFAULT_LUMA_ONLY = False
528
+ DEFAULT_LUMA_MODE = "rec709"
529
+ DEFAULT_LUMA_BLEND = 60 # 0.60
530
+
531
+ DEFAULT_CURVES_ON = False
532
+ DEFAULT_CURVES_STRENGTH = 20 # 0.20
533
+
534
+ # Avoid cascading signal storms while we set everything
535
+ widgets = [
536
+ self.spin_target,
537
+ self.chk_linked,
538
+ self.chk_normalize,
539
+ self.sld_bp,
540
+ self.chk_no_black_clip,
541
+ self.chk_hdr,
542
+ self.sld_hdr_amt,
543
+ self.sld_hdr_knee,
544
+ self.chk_luma_only,
545
+ self.cmb_luma,
546
+ self.sld_luma_blend,
547
+ self.chk_curves,
548
+ self.sld_curves,
549
+ ]
550
+
551
+ old_blocks = []
552
+ for w in widgets:
553
+ try:
554
+ old_blocks.append((w, w.blockSignals(True)))
555
+ except Exception:
556
+ pass
557
+
558
+ try:
559
+ # Reset “user locked” HDR knee behavior
560
+ self._hdr_knee_user_locked = False
561
+
562
+ # Core controls
563
+ self.spin_target.setValue(DEFAULT_TARGET)
564
+ self.chk_linked.setChecked(DEFAULT_LINKED)
565
+ self.chk_normalize.setChecked(DEFAULT_NORMALIZE)
566
+
567
+ # Black point
568
+ self.chk_no_black_clip.setChecked(DEFAULT_NO_BLACK_CLIP)
569
+ self.sld_bp.setValue(DEFAULT_BP_SLIDER)
570
+ self.lbl_bp.setText(f"{DEFAULT_BP_SLIDER/100:.2f}")
571
+
572
+ # HDR
573
+ self.chk_hdr.setChecked(DEFAULT_HDR_ON)
574
+ self.sld_hdr_amt.setValue(DEFAULT_HDR_AMT)
575
+ self.lbl_hdr_amt.setText(f"{DEFAULT_HDR_AMT/100:.2f}")
576
+ self.sld_hdr_knee.setValue(DEFAULT_HDR_KNEE)
577
+ self.lbl_hdr_knee.setText(f"{DEFAULT_HDR_KNEE/100:.2f}")
578
+
579
+ # Luma-only + mode + blend
580
+ self.chk_luma_only.setChecked(DEFAULT_LUMA_ONLY)
581
+ if DEFAULT_LUMA_MODE:
582
+ self.cmb_luma.setCurrentText(DEFAULT_LUMA_MODE)
583
+ self.sld_luma_blend.setValue(DEFAULT_LUMA_BLEND)
584
+ self.lbl_luma_blend.setText(f"{DEFAULT_LUMA_BLEND/100:.2f}")
585
+
586
+ # Curves
587
+ self.chk_curves.setChecked(DEFAULT_CURVES_ON)
588
+ self.sld_curves.setValue(DEFAULT_CURVES_STRENGTH)
589
+ self.lbl_curves_val.setText(f"{DEFAULT_CURVES_STRENGTH/100:.2f}")
590
+
591
+ finally:
592
+ # Restore signal states
593
+ for w, _prev in old_blocks:
594
+ try:
595
+ w.blockSignals(False)
596
+ except Exception:
597
+ pass
598
+
599
+ # Re-apply dependent enable/disable states exactly like normal interactions
600
+ try:
601
+ # no-black-clip disables BP row
602
+ self.row_bp.setEnabled(not self.chk_no_black_clip.isChecked())
603
+ except Exception:
604
+ pass
605
+
606
+ try:
607
+ # HDR enables HDR row
608
+ self.hdr_row.setEnabled(self.chk_hdr.isChecked())
609
+ except Exception:
610
+ pass
611
+
612
+ try:
613
+ # Curves enables curves row
614
+ self.curves_row.setEnabled(self.chk_curves.isChecked())
615
+ except Exception:
616
+ pass
617
+
618
+ try:
619
+ # Luma-only enables dropdown + blend row, disables linked
620
+ luma_on = self.chk_luma_only.isChecked()
621
+ self.cmb_luma.setEnabled(luma_on)
622
+ self.luma_blend_row.setEnabled(luma_on)
623
+ self.chk_linked.setEnabled(not luma_on)
624
+ except Exception:
625
+ pass
626
+
627
+ # Auto-suggest HDR knee from target (since we cleared lock)
628
+ try:
629
+ t = float(self.spin_target.value())
630
+ knee = float(np.clip(t + 0.10, 0.10, 0.95))
631
+ self.sld_hdr_knee.setValue(int(round(knee * 100)))
632
+ self.lbl_hdr_knee.setText(f"{knee:.2f}")
633
+ except Exception:
634
+ pass
635
+
636
+ # Refresh baseline preview + clip stats
637
+ self._populate_initial_preview()
638
+
639
+
493
640
  def _clip_mode_label(self, imgf: np.ndarray) -> str:
494
641
  # Mono image
495
642
  if imgf.ndim == 2 or (imgf.ndim == 3 and imgf.shape[2] == 1):
@@ -626,7 +773,8 @@ class StatisticalStretchDialog(QDialog):
626
773
  self._job_mode = mode
627
774
 
628
775
  self._set_controls_enabled(False)
629
- self._show_busy("Statistical Stretch", "Processing…")
776
+ self._show_busy("Statistical Stretch", "Processing preview…" if mode == "preview" else "Applying stretch…")
777
+
630
778
 
631
779
  self._thread = QThread(self._main)
632
780
  self._worker = _StretchWorker(self)
@@ -590,6 +590,7 @@ class ImageSubWindow(QWidget):
590
590
  self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
591
591
  self.customContextMenuRequested.connect(self._show_ctx_menu)
592
592
  QShortcut(QKeySequence("F2"), self, activated=self._rename_view)
593
+ QShortcut(QKeySequence("F3"), self, activated=self._rename_document)
593
594
  #QShortcut(QKeySequence("A"), self, activated=self.toggle_autostretch)
594
595
  QShortcut(QKeySequence("Ctrl+Space"), self, activated=self.toggle_autostretch)
595
596
  QShortcut(QKeySequence("Alt+Shift+A"), self, activated=self.toggle_autostretch)
@@ -1726,7 +1727,7 @@ class ImageSubWindow(QWidget):
1726
1727
  def _show_ctx_menu(self, pos):
1727
1728
  menu = QMenu(self)
1728
1729
  a_view = menu.addAction(self.tr("Rename View… (F2)"))
1729
- a_doc = menu.addAction(self.tr("Rename Document…"))
1730
+ a_doc = menu.addAction(self.tr("Rename Document… (F3)"))
1730
1731
  menu.addSeparator()
1731
1732
  a_min = menu.addAction(self.tr("Send to Shelf"))
1732
1733
  a_clear = menu.addAction(self.tr("Clear View Name (use doc name)"))
@@ -1736,6 +1737,29 @@ class ImageSubWindow(QWidget):
1736
1737
  a_help = menu.addAction(self.tr("Show pixel/WCS readout hint"))
1737
1738
  menu.addSeparator()
1738
1739
  a_prev = menu.addAction(self.tr("Create Preview (drag rectangle)"))
1740
+ # --- Mask actions (requested in zoom/context menu) ---
1741
+ mw = self._find_main_window()
1742
+ vw = self # this ImageSubWindow is the view
1743
+ doc = getattr(vw, "document", None)
1744
+
1745
+ has_mask = bool(doc and getattr(doc, "active_mask_id", None))
1746
+
1747
+ menu.addSeparator()
1748
+ menu.addSection(self.tr("Mask"))
1749
+
1750
+ # 1) Toggle overlay (single item: Show/Hide)
1751
+ a_mask_overlay = menu.addAction(self.tr("Show Mask Overlay"))
1752
+ a_mask_overlay.setCheckable(True)
1753
+ a_mask_overlay.setChecked(bool(getattr(vw, "show_mask_overlay", False)))
1754
+ a_mask_overlay.setEnabled(has_mask)
1755
+
1756
+ # 2) Invert mask
1757
+ a_invert = menu.addAction(self.tr("Invert Mask"))
1758
+ a_invert.setEnabled(has_mask)
1759
+
1760
+ # 3) Remove mask
1761
+ a_remove = menu.addAction(self.tr("Remove Mask"))
1762
+ a_remove.setEnabled(has_mask)
1739
1763
 
1740
1764
  act = menu.exec(self.mapToGlobal(pos))
1741
1765
 
@@ -1755,7 +1779,19 @@ class ImageSubWindow(QWidget):
1755
1779
  elif act == a_prev:
1756
1780
  self._preview_btn.setChecked(True)
1757
1781
  self._toggle_preview_select_mode(True)
1758
-
1782
+ # --- Mask dispatch ---
1783
+ elif act == a_mask_overlay:
1784
+ if mw:
1785
+ if a_mask_overlay.isChecked():
1786
+ mw._show_mask_overlay()
1787
+ else:
1788
+ mw._hide_mask_overlay()
1789
+ elif act == a_invert:
1790
+ if mw:
1791
+ mw._invert_mask()
1792
+ elif act == a_remove:
1793
+ if mw:
1794
+ mw._remove_mask_menu()
1759
1795
 
1760
1796
 
1761
1797
  def _send_to_shelf(self):
@@ -3081,7 +3117,6 @@ class ImageSubWindow(QWidget):
3081
3117
  yi = int(round(py / s))
3082
3118
  return xi, yi
3083
3119
 
3084
-
3085
3120
  def _finish_preview_rect(self, vp_rect: QRect):
3086
3121
  if vp_rect.width() < 4 or vp_rect.height() < 4:
3087
3122
  self._cancel_rubber()
@@ -3187,8 +3222,6 @@ class ImageSubWindow(QWidget):
3187
3222
 
3188
3223
  super().mousePressEvent(e)
3189
3224
 
3190
-
3191
-
3192
3225
  def _show_readout(self, xi, yi, sample):
3193
3226
  mw = self._find_main_window()
3194
3227
  if mw is None: