setiastrosuitepro 1.6.1.post1__py3-none-any.whl → 1.6.4__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 (139) hide show
  1. setiastro/images/Background_startup.jpg +0 -0
  2. setiastro/images/rotatearbitrary.png +0 -0
  3. setiastro/qml/ResourceMonitor.qml +126 -0
  4. setiastro/saspro/__main__.py +162 -25
  5. setiastro/saspro/_generated/build_info.py +2 -1
  6. setiastro/saspro/abe.py +62 -11
  7. setiastro/saspro/aberration_ai.py +3 -3
  8. setiastro/saspro/add_stars.py +5 -2
  9. setiastro/saspro/astrobin_exporter.py +3 -0
  10. setiastro/saspro/astrospike_python.py +3 -1
  11. setiastro/saspro/autostretch.py +4 -2
  12. setiastro/saspro/backgroundneutral.py +60 -9
  13. setiastro/saspro/batch_convert.py +3 -0
  14. setiastro/saspro/batch_renamer.py +3 -0
  15. setiastro/saspro/blemish_blaster.py +3 -0
  16. setiastro/saspro/blink_comparator_pro.py +474 -251
  17. setiastro/saspro/cheat_sheet.py +50 -15
  18. setiastro/saspro/clahe.py +27 -1
  19. setiastro/saspro/comet_stacking.py +103 -38
  20. setiastro/saspro/convo.py +3 -0
  21. setiastro/saspro/copyastro.py +3 -0
  22. setiastro/saspro/cosmicclarity.py +70 -45
  23. setiastro/saspro/crop_dialog_pro.py +28 -1
  24. setiastro/saspro/curve_editor_pro.py +18 -0
  25. setiastro/saspro/debayer.py +3 -0
  26. setiastro/saspro/doc_manager.py +40 -17
  27. setiastro/saspro/fitsmodifier.py +3 -0
  28. setiastro/saspro/frequency_separation.py +8 -2
  29. setiastro/saspro/function_bundle.py +18 -16
  30. setiastro/saspro/generate_translations.py +715 -1
  31. setiastro/saspro/ghs_dialog_pro.py +3 -0
  32. setiastro/saspro/graxpert.py +3 -0
  33. setiastro/saspro/gui/main_window.py +364 -92
  34. setiastro/saspro/gui/mixins/dock_mixin.py +119 -7
  35. setiastro/saspro/gui/mixins/file_mixin.py +7 -0
  36. setiastro/saspro/gui/mixins/geometry_mixin.py +105 -5
  37. setiastro/saspro/gui/mixins/menu_mixin.py +29 -0
  38. setiastro/saspro/gui/mixins/toolbar_mixin.py +33 -10
  39. setiastro/saspro/gui/statistics_dialog.py +47 -0
  40. setiastro/saspro/halobgon.py +29 -3
  41. setiastro/saspro/histogram.py +3 -0
  42. setiastro/saspro/history_explorer.py +2 -0
  43. setiastro/saspro/i18n.py +22 -10
  44. setiastro/saspro/image_combine.py +3 -0
  45. setiastro/saspro/image_peeker_pro.py +3 -0
  46. setiastro/saspro/imageops/stretch.py +5 -13
  47. setiastro/saspro/isophote.py +3 -0
  48. setiastro/saspro/legacy/numba_utils.py +64 -47
  49. setiastro/saspro/linear_fit.py +3 -0
  50. setiastro/saspro/live_stacking.py +13 -2
  51. setiastro/saspro/mask_creation.py +3 -0
  52. setiastro/saspro/mfdeconv.py +5 -0
  53. setiastro/saspro/morphology.py +30 -5
  54. setiastro/saspro/multiscale_decomp.py +713 -256
  55. setiastro/saspro/nbtorgb_stars.py +12 -2
  56. setiastro/saspro/numba_utils.py +148 -47
  57. setiastro/saspro/ops/scripts.py +77 -17
  58. setiastro/saspro/ops/settings.py +1 -43
  59. setiastro/saspro/perfect_palette_picker.py +1 -0
  60. setiastro/saspro/pixelmath.py +6 -2
  61. setiastro/saspro/plate_solver.py +1 -0
  62. setiastro/saspro/remove_green.py +18 -1
  63. setiastro/saspro/remove_stars.py +136 -162
  64. setiastro/saspro/remove_stars_preset.py +55 -13
  65. setiastro/saspro/resources.py +36 -10
  66. setiastro/saspro/rgb_combination.py +1 -0
  67. setiastro/saspro/rgbalign.py +4 -4
  68. setiastro/saspro/save_options.py +1 -0
  69. setiastro/saspro/selective_color.py +79 -20
  70. setiastro/saspro/sfcc.py +50 -8
  71. setiastro/saspro/shortcuts.py +94 -21
  72. setiastro/saspro/signature_insert.py +3 -0
  73. setiastro/saspro/stacking_suite.py +924 -446
  74. setiastro/saspro/star_alignment.py +291 -331
  75. setiastro/saspro/star_spikes.py +116 -32
  76. setiastro/saspro/star_stretch.py +38 -1
  77. setiastro/saspro/stat_stretch.py +35 -3
  78. setiastro/saspro/status_log_dock.py +1 -1
  79. setiastro/saspro/subwindow.py +63 -2
  80. setiastro/saspro/supernovaasteroidhunter.py +3 -0
  81. setiastro/saspro/swap_manager.py +77 -42
  82. setiastro/saspro/translations/all_source_strings.json +4726 -0
  83. setiastro/saspro/translations/ar_translations.py +4096 -0
  84. setiastro/saspro/translations/de_translations.py +441 -446
  85. setiastro/saspro/translations/es_translations.py +278 -32
  86. setiastro/saspro/translations/fr_translations.py +280 -32
  87. setiastro/saspro/translations/hi_translations.py +3803 -0
  88. setiastro/saspro/translations/integrate_translations.py +38 -1
  89. setiastro/saspro/translations/it_translations.py +1211 -145
  90. setiastro/saspro/translations/ja_translations.py +556 -307
  91. setiastro/saspro/translations/pt_translations.py +3316 -3322
  92. setiastro/saspro/translations/ru_translations.py +3082 -0
  93. setiastro/saspro/translations/saspro_ar.qm +0 -0
  94. setiastro/saspro/translations/saspro_ar.ts +16019 -0
  95. setiastro/saspro/translations/saspro_de.qm +0 -0
  96. setiastro/saspro/translations/saspro_de.ts +14428 -133
  97. setiastro/saspro/translations/saspro_es.qm +0 -0
  98. setiastro/saspro/translations/saspro_es.ts +11503 -7821
  99. setiastro/saspro/translations/saspro_fr.qm +0 -0
  100. setiastro/saspro/translations/saspro_fr.ts +11168 -7812
  101. setiastro/saspro/translations/saspro_hi.qm +0 -0
  102. setiastro/saspro/translations/saspro_hi.ts +14855 -0
  103. setiastro/saspro/translations/saspro_it.qm +0 -0
  104. setiastro/saspro/translations/saspro_it.ts +14347 -7821
  105. setiastro/saspro/translations/saspro_ja.qm +0 -0
  106. setiastro/saspro/translations/saspro_ja.ts +14860 -137
  107. setiastro/saspro/translations/saspro_pt.qm +0 -0
  108. setiastro/saspro/translations/saspro_pt.ts +14904 -137
  109. setiastro/saspro/translations/saspro_ru.qm +0 -0
  110. setiastro/saspro/translations/saspro_ru.ts +11835 -0
  111. setiastro/saspro/translations/saspro_sw.qm +0 -0
  112. setiastro/saspro/translations/saspro_sw.ts +15237 -0
  113. setiastro/saspro/translations/saspro_uk.qm +0 -0
  114. setiastro/saspro/translations/saspro_uk.ts +15248 -0
  115. setiastro/saspro/translations/saspro_zh.qm +0 -0
  116. setiastro/saspro/translations/saspro_zh.ts +10581 -7812
  117. setiastro/saspro/translations/sw_translations.py +3897 -0
  118. setiastro/saspro/translations/uk_translations.py +3929 -0
  119. setiastro/saspro/translations/zh_translations.py +283 -32
  120. setiastro/saspro/versioning.py +36 -5
  121. setiastro/saspro/view_bundle.py +20 -17
  122. setiastro/saspro/wavescale_hdr.py +22 -1
  123. setiastro/saspro/wavescalede.py +23 -1
  124. setiastro/saspro/whitebalance.py +39 -3
  125. setiastro/saspro/widgets/minigame/game.js +991 -0
  126. setiastro/saspro/widgets/minigame/index.html +53 -0
  127. setiastro/saspro/widgets/minigame/style.css +241 -0
  128. setiastro/saspro/widgets/resource_monitor.py +263 -0
  129. setiastro/saspro/widgets/spinboxes.py +18 -0
  130. setiastro/saspro/widgets/wavelet_utils.py +52 -20
  131. setiastro/saspro/wimi.py +100 -80
  132. setiastro/saspro/wims.py +33 -33
  133. setiastro/saspro/window_shelf.py +2 -2
  134. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/METADATA +15 -4
  135. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/RECORD +139 -115
  136. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/WHEEL +0 -0
  137. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/entry_points.txt +0 -0
  138. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/LICENSE +0 -0
  139. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/license.txt +0 -0
@@ -91,6 +91,9 @@ class ImageCombineDialog(QDialog):
91
91
  def __init__(self, main_window):
92
92
  super().__init__(main_window)
93
93
  self.setWindowTitle("Image Combine")
94
+ self.setWindowFlag(Qt.WindowType.Window, True)
95
+ self.setWindowModality(Qt.WindowModality.NonModal)
96
+ self.setModal(False)
94
97
  self.mw = main_window
95
98
  self.dm = getattr(main_window, "doc_manager", None) or getattr(main_window, "dm", None)
96
99
  self.zoom = 1.0
@@ -1314,6 +1314,9 @@ class ImagePeekerDialogPro(QDialog):
1314
1314
  def __init__(self, parent, document, settings):
1315
1315
  super().__init__(parent)
1316
1316
  self.setWindowTitle(self.tr("Image Peeker"))
1317
+ self.setWindowFlag(Qt.WindowType.Window, True)
1318
+ self.setWindowModality(Qt.WindowModality.NonModal)
1319
+ self.setModal(False)
1317
1320
  self.document = self._coerce_doc(document) # <- ensure we hold a real doc
1318
1321
  self.settings = settings
1319
1322
  # status / progress line
@@ -95,19 +95,11 @@ def apply_curves_adjustment(image: np.ndarray,
95
95
  xvals, yvals = _calculate_curve_points(target_median, curves_boost)
96
96
 
97
97
  # Apply the 1D LUT per channel using np.interp (piecewise linear)
98
- if img.ndim == 2:
99
- flat = img.ravel()
100
- out = np.interp(flat, xvals, yvals).reshape(img.shape).astype(np.float32)
101
- elif img.ndim == 3 and img.shape[2] in (3, 4):
102
- h, w, c = img.shape
103
- out = np.empty_like(img, dtype=np.float32)
104
- # Apply same curve to each color channel
105
- for ch in range(c):
106
- flat = img[..., ch].ravel()
107
- out[..., ch] = np.interp(flat, xvals, yvals).reshape(h, w)
108
- else:
109
- # Fallback: just return clamped image
110
- out = img
98
+ # Apply the 1D LUT per channel using np.interp (piecewise linear)
99
+ # Optimization: np.interp can handle N-D 'x' array directly.
100
+ # No need to loop over channels or flatten/reshape if we pass the whole array.
101
+
102
+ out = np.interp(img, xvals, yvals).astype(np.float32, copy=False)
111
103
 
112
104
  return np.clip(out, 0.0, 1.0)
113
105
 
@@ -360,6 +360,9 @@ class IsophoteModelerDialog(QDialog):
360
360
  def __init__(self, mono_image: np.ndarray, parent: Optional[QWidget] = None,
361
361
  title_hint: Optional[str] = None, image_manager=None, doc_manager=None):
362
362
  super().__init__(parent)
363
+ self.setWindowFlag(Qt.WindowType.Window, True)
364
+ self.setWindowModality(Qt.WindowModality.NonModal)
365
+ self.setModal(False)
363
366
  self.image_manager = image_manager
364
367
  self.doc_manager = doc_manager
365
368
 
@@ -802,39 +802,46 @@ def kappa_sigma_clip_weighted_3d(stack, weights, kappa=2.5, iterations=3):
802
802
  pixel_weights = weights[:]
803
803
  else:
804
804
  pixel_weights = weights[:, i, j].copy()
805
- # Initialize tracking of indices
806
- current_idx = np.empty(num_frames, dtype=np.int64)
807
- for f in range(num_frames):
808
- current_idx[f] = f
809
- current_vals = pixel_values
810
- current_w = pixel_weights
811
- current_indices = current_idx
805
+
806
+ valid_mask = pixel_values != 0
807
+
812
808
  med = 0.0
813
809
  for _ in range(iterations):
814
- if current_vals.size == 0:
810
+ count = 0
811
+ for k in range(num_frames):
812
+ if valid_mask[k]:
813
+ count += 1
814
+
815
+ if count == 0:
815
816
  break
817
+
818
+ current_vals = pixel_values[valid_mask]
819
+
816
820
  med = np.median(current_vals)
817
821
  std = np.std(current_vals)
818
822
  lower_bound = med - kappa * std
819
823
  upper_bound = med + kappa * std
820
- valid = (current_vals != 0) & (current_vals >= lower_bound) & (current_vals <= upper_bound)
821
- current_vals = current_vals[valid]
822
- current_w = current_w[valid]
823
- current_indices = current_indices[valid]
824
- # Mark rejected: frames not in current_indices are rejected.
824
+
825
+ for k in range(num_frames):
826
+ if valid_mask[k]:
827
+ val = pixel_values[k]
828
+ if val < lower_bound or val > upper_bound:
829
+ valid_mask[k] = False
830
+
825
831
  for f in range(num_frames):
826
- # Check if f is in current_indices
827
- found = False
828
- for k in range(current_indices.size):
829
- if current_indices[k] == f:
830
- found = True
831
- break
832
- if not found:
833
- rej_mask[f, i, j] = True
834
- else:
835
- rej_mask[f, i, j] = False
836
- if current_w.size > 0 and current_w.sum() > 0:
837
- clipped[i, j] = np.sum(current_vals * current_w) / current_w.sum()
832
+ rej_mask[f, i, j] = not valid_mask[f]
833
+
834
+ wsum = 0.0
835
+ vsum = 0.0
836
+ for k in range(num_frames):
837
+ if valid_mask[k]:
838
+ w = pixel_weights[k]
839
+ v = pixel_values[k]
840
+ wsum += w
841
+ vsum += v * w
842
+
843
+ if wsum > 0:
844
+ clipped[i, j] = vsum / wsum
838
845
  else:
839
846
  clipped[i, j] = med
840
847
  return clipped, rej_mask
@@ -859,36 +866,46 @@ def kappa_sigma_clip_weighted_4d(stack, weights, kappa=2.5, iterations=3):
859
866
  pixel_weights = weights[:]
860
867
  else:
861
868
  pixel_weights = weights[:, i, j, c].copy()
862
- current_idx = np.empty(num_frames, dtype=np.int64)
863
- for f in range(num_frames):
864
- current_idx[f] = f
865
- current_vals = pixel_values
866
- current_w = pixel_weights
867
- current_indices = current_idx
869
+
870
+ valid_mask = pixel_values != 0
871
+
868
872
  med = 0.0
869
873
  for _ in range(iterations):
870
- if current_vals.size == 0:
874
+ count = 0
875
+ for k in range(num_frames):
876
+ if valid_mask[k]:
877
+ count += 1
878
+
879
+ if count == 0:
871
880
  break
881
+
882
+ current_vals = pixel_values[valid_mask]
883
+
872
884
  med = np.median(current_vals)
873
885
  std = np.std(current_vals)
874
886
  lower_bound = med - kappa * std
875
887
  upper_bound = med + kappa * std
876
- valid = (current_vals != 0) & (current_vals >= lower_bound) & (current_vals <= upper_bound)
877
- current_vals = current_vals[valid]
878
- current_w = current_w[valid]
879
- current_indices = current_indices[valid]
888
+
889
+ for k in range(num_frames):
890
+ if valid_mask[k]:
891
+ val = pixel_values[k]
892
+ if val < lower_bound or val > upper_bound:
893
+ valid_mask[k] = False
894
+
880
895
  for f in range(num_frames):
881
- found = False
882
- for k in range(current_indices.size):
883
- if current_indices[k] == f:
884
- found = True
885
- break
886
- if not found:
887
- rej_mask[f, i, j, c] = True
888
- else:
889
- rej_mask[f, i, j, c] = False
890
- if current_w.size > 0 and current_w.sum() > 0:
891
- clipped[i, j, c] = np.sum(current_vals * current_w) / current_w.sum()
896
+ rej_mask[f, i, j, c] = not valid_mask[f]
897
+
898
+ wsum = 0.0
899
+ vsum = 0.0
900
+ for k in range(num_frames):
901
+ if valid_mask[k]:
902
+ w = pixel_weights[k]
903
+ v = pixel_values[k]
904
+ wsum += w
905
+ vsum += v * w
906
+
907
+ if wsum > 0:
908
+ clipped[i, j, c] = vsum / wsum
892
909
  else:
893
910
  clipped[i, j, c] = med
894
911
  return clipped, rej_mask
@@ -224,6 +224,9 @@ class LinearFitDialog(QDialog):
224
224
  def __init__(self, parent, doc_manager, active_doc):
225
225
  super().__init__(parent)
226
226
  self.setWindowTitle("Linear Fit")
227
+ self.setWindowFlag(Qt.WindowType.Window, True)
228
+ self.setWindowModality(Qt.WindowModality.NonModal)
229
+ self.setModal(False)
227
230
  self.dm = doc_manager
228
231
  self.doc = active_doc
229
232
  self.worker: Optional[_LinearFitWorker] = None
@@ -48,6 +48,9 @@ class LiveStackSettingsDialog(QDialog):
48
48
  def __init__(self, parent):
49
49
  super().__init__(parent)
50
50
  self.setWindowTitle("Live Stack & Culling Settings")
51
+ self.setWindowFlag(Qt.WindowType.Window, True)
52
+ self.setWindowModality(Qt.WindowModality.NonModal)
53
+ self.setModal(False)
51
54
 
52
55
  # — Live Stack Settings —
53
56
  # Bootstrap frames (int)
@@ -290,8 +293,16 @@ def estimate_global_snr(
290
293
 
291
294
  # 1) Collapse to simple 2D float array (grayscale)
292
295
  if stack_image.ndim == 3 and stack_image.shape[2] == 3:
293
- # RGB → grayscale by averaging channels
294
- gray = stack_image.mean(axis=2).astype(np.float32)
296
+ try:
297
+ import cv2
298
+ # cv2.cvtColor is significantly faster than mean(axis=2)
299
+ # Assuming RGB input, but even if BGR, for SNR estimation luma difference is negligible
300
+ gray = cv2.cvtColor(stack_image, cv2.COLOR_RGB2GRAY)
301
+ if gray.dtype != np.float32:
302
+ gray = gray.astype(np.float32)
303
+ except ImportError:
304
+ # Fallback
305
+ gray = stack_image.mean(axis=2).astype(np.float32)
295
306
  else:
296
307
  # Already mono: just cast to float32
297
308
  gray = stack_image.astype(np.float32)
@@ -555,6 +555,9 @@ class MaskCreationDialog(QDialog):
555
555
  def __init__(self, image01: np.ndarray, parent=None, auto_push_on_ok: bool = True):
556
556
  super().__init__(parent)
557
557
  self.setWindowTitle(self.tr("Mask Creation"))
558
+ self.setWindowFlag(Qt.WindowType.Window, True)
559
+ self.setWindowModality(Qt.WindowModality.NonModal)
560
+ self.setModal(False)
558
561
  self.image = np.asarray(image01, dtype=np.float32).copy()
559
562
  self.mask: np.ndarray | None = None
560
563
  self.live_preview = LivePreviewDialog(self.image, parent=self)
@@ -822,6 +822,11 @@ def _to_luma_local(a: np.ndarray) -> np.ndarray:
822
822
  return a
823
823
  # (H,W,3) or (3,H,W)
824
824
  if a.ndim == 3 and a.shape[-1] == 3:
825
+ try:
826
+ import cv2
827
+ return cv2.cvtColor(a, cv2.COLOR_RGB2GRAY).astype(np.float32, copy=False)
828
+ except Exception:
829
+ pass
825
830
  r, g, b = a[..., 0], a[..., 1], a[..., 2]
826
831
  return (0.2126*r + 0.7152*g + 0.0722*b).astype(np.float32, copy=False)
827
832
  if a.ndim == 3 and a.shape[0] == 3:
@@ -46,10 +46,9 @@ def apply_morphology(image: np.ndarray, *, operation: str = "erosion",
46
46
 
47
47
  if img.ndim == 3 and img.shape[2] == 3:
48
48
  u8 = (img * 255.0).astype(np.uint8)
49
- ch = cv2.split(u8)
50
- ch = [_do(c) for c in ch]
51
- out = cv2.merge(ch).astype(np.float32) / 255.0
52
- return np.clip(out, 0.0, 1.0)
49
+ # OpenCV morphology functions handle multi-channel images natively (independent channels)
50
+ out_u8 = _do(u8)
51
+ return (out_u8.astype(np.float32) / 255.0).clip(0.0, 1.0)
53
52
 
54
53
  raise ValueError("Input image must be mono (H,W)/(H,W,1) or RGB (H,W,3).")
55
54
 
@@ -92,6 +91,9 @@ class MorphologyDialogPro(QDialog):
92
91
  def __init__(self, parent, doc, icon: QIcon | None = None, initial: dict | None = None):
93
92
  super().__init__(parent)
94
93
  self.setWindowTitle(self.tr("Morphological Operations"))
94
+ self.setWindowFlag(Qt.WindowType.Window, True)
95
+ self.setWindowModality(Qt.WindowModality.NonModal)
96
+ self.setModal(False)
95
97
  if icon:
96
98
  try: self.setWindowIcon(icon)
97
99
  except Exception as e:
@@ -258,10 +260,33 @@ class MorphologyDialogPro(QDialog):
258
260
  pass
259
261
  # ────────────────────────────────────────────────────────────
260
262
 
261
- self.accept()
263
+ # Dialog stays open so user can apply to other images
264
+ # Refresh document reference for next operation
265
+ self._refresh_document_from_active()
262
266
  except Exception as e:
263
267
  QMessageBox.critical(self, "Morphology", f"Failed to apply:\n{e}")
264
268
 
269
+ def _refresh_document_from_active(self):
270
+ """
271
+ Refresh the dialog's document reference to the currently active document.
272
+ This allows reusing the same dialog on different images.
273
+ """
274
+ try:
275
+ main = self.parent()
276
+ if main and hasattr(main, "_active_doc"):
277
+ new_doc = main._active_doc()
278
+ if new_doc is not None and new_doc is not self.doc:
279
+ self.doc = new_doc
280
+ # Refresh preview for new document
281
+ self.orig = np.clip(np.asarray(new_doc.image, dtype=np.float32), 0.0, 1.0)
282
+ disp = self.orig
283
+ if disp.ndim == 2: disp = disp[..., None].repeat(3, axis=2)
284
+ elif disp.ndim == 3 and disp.shape[2] == 1: disp = disp.repeat(3, axis=2)
285
+ self._disp_base = disp
286
+ self._update_preview()
287
+ except Exception:
288
+ pass
289
+
265
290
 
266
291
 
267
292
  def _reset(self):