setiastrosuitepro 1.7.1.post2__py3-none-any.whl → 1.7.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 (62) hide show
  1. setiastro/images/3dplanet.png +0 -0
  2. setiastro/saspro/__init__.py +20 -8
  3. setiastro/saspro/__main__.py +349 -290
  4. setiastro/saspro/_generated/build_info.py +2 -2
  5. setiastro/saspro/abe.py +4 -4
  6. setiastro/saspro/autostretch.py +29 -18
  7. setiastro/saspro/doc_manager.py +4 -1
  8. setiastro/saspro/gui/main_window.py +46 -7
  9. setiastro/saspro/gui/mixins/file_mixin.py +6 -2
  10. setiastro/saspro/gui/mixins/menu_mixin.py +1 -0
  11. setiastro/saspro/gui/mixins/toolbar_mixin.py +9 -2
  12. setiastro/saspro/imageops/serloader.py +101 -17
  13. setiastro/saspro/layers.py +186 -10
  14. setiastro/saspro/layers_dock.py +198 -5
  15. setiastro/saspro/legacy/image_manager.py +10 -4
  16. setiastro/saspro/legacy/numba_utils.py +301 -119
  17. setiastro/saspro/numba_utils.py +998 -270
  18. setiastro/saspro/ops/settings.py +6 -6
  19. setiastro/saspro/pixelmath.py +1 -1
  20. setiastro/saspro/planetprojection.py +4059 -0
  21. setiastro/saspro/resources.py +2 -0
  22. setiastro/saspro/save_options.py +45 -13
  23. setiastro/saspro/ser_stack_config.py +21 -1
  24. setiastro/saspro/ser_stacker.py +8 -2
  25. setiastro/saspro/ser_stacker_dialog.py +37 -10
  26. setiastro/saspro/ser_tracking.py +57 -35
  27. setiastro/saspro/serviewer.py +164 -16
  28. setiastro/saspro/sfcc.py +14 -8
  29. setiastro/saspro/stacking_suite.py +292 -111
  30. setiastro/saspro/subwindow.py +64 -36
  31. setiastro/saspro/translations/all_source_strings.json +2 -2
  32. setiastro/saspro/translations/ar_translations.py +3 -3
  33. setiastro/saspro/translations/de_translations.py +2 -2
  34. setiastro/saspro/translations/es_translations.py +2 -2
  35. setiastro/saspro/translations/fr_translations.py +2 -2
  36. setiastro/saspro/translations/hi_translations.py +2 -2
  37. setiastro/saspro/translations/it_translations.py +2 -2
  38. setiastro/saspro/translations/ja_translations.py +2 -2
  39. setiastro/saspro/translations/pt_translations.py +2 -2
  40. setiastro/saspro/translations/ru_translations.py +2 -2
  41. setiastro/saspro/translations/saspro_ar.ts +2 -2
  42. setiastro/saspro/translations/saspro_de.ts +4 -4
  43. setiastro/saspro/translations/saspro_es.ts +2 -2
  44. setiastro/saspro/translations/saspro_fr.ts +2 -2
  45. setiastro/saspro/translations/saspro_hi.ts +2 -2
  46. setiastro/saspro/translations/saspro_it.ts +4 -4
  47. setiastro/saspro/translations/saspro_ja.ts +2 -2
  48. setiastro/saspro/translations/saspro_pt.ts +2 -2
  49. setiastro/saspro/translations/saspro_ru.ts +2 -2
  50. setiastro/saspro/translations/saspro_sw.ts +2 -2
  51. setiastro/saspro/translations/saspro_uk.ts +2 -2
  52. setiastro/saspro/translations/saspro_zh.ts +2 -2
  53. setiastro/saspro/translations/sw_translations.py +2 -2
  54. setiastro/saspro/translations/uk_translations.py +2 -2
  55. setiastro/saspro/translations/zh_translations.py +2 -2
  56. setiastro/saspro/window_shelf.py +62 -1
  57. {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.4.dist-info}/METADATA +1 -1
  58. {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.4.dist-info}/RECORD +62 -60
  59. {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.4.dist-info}/entry_points.txt +1 -1
  60. {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.4.dist-info}/WHEEL +0 -0
  61. {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.4.dist-info}/licenses/LICENSE +0 -0
  62. {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.4.dist-info}/licenses/license.txt +0 -0
@@ -9,7 +9,7 @@ from PyQt6.QtGui import QImage, QPixmap, QPainter, QPen, QColor
9
9
  from PyQt6.QtWidgets import (
10
10
  QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFileDialog,
11
11
  QScrollArea, QSlider, QCheckBox, QGroupBox, QFormLayout, QSpinBox,
12
- QMessageBox, QRubberBand, QComboBox, QDoubleSpinBox
12
+ QMessageBox, QRubberBand, QComboBox, QDoubleSpinBox, QWidget
13
13
  )
14
14
 
15
15
  from setiastro.saspro.imageops.serloader import open_planetary_source, PlanetaryFrameSource
@@ -323,9 +323,80 @@ class SERViewer(QDialog):
323
323
 
324
324
  self.btn_stack = QPushButton("Open Stacker…", self)
325
325
  self.btn_stack.setEnabled(False) # enabled once SER loaded
326
+ self.chk_planet_norm = QCheckBox("Normalize for planetary centroid detect")
327
+ self.chk_planet_norm.setChecked(True)
328
+ self.chk_planet_norm.setToolTip("Detection-only normalization (does not change stacking pixels). Helps dim planets.")
329
+
330
+ self.spin_planet_thresh = QDoubleSpinBox()
331
+ self.spin_planet_thresh.setRange(50.0, 99.9)
332
+ self.spin_planet_thresh.setDecimals(1)
333
+ self.spin_planet_thresh.setSingleStep(0.5)
334
+ self.spin_planet_thresh.setValue(92.0)
335
+ self.spin_planet_thresh.setToolTip("Percentile used to threshold the blob for centroid detection.")
336
+ self.spin_planet_min = QDoubleSpinBox()
337
+ self.spin_planet_min.setRange(0.0, 0.5) # abs floor in [0..1]
338
+ self.spin_planet_min.setDecimals(3)
339
+ self.spin_planet_min.setSingleStep(0.005)
340
+ self.spin_planet_min.setValue(0.02)
341
+ self.spin_planet_min.setToolTip(
342
+ "Minimum normalized intensity (0..1) allowed for detection thresholding.\n"
343
+ "If percentile threshold is too low on dim planets, this prevents the mask from vanishing.\n"
344
+ "Typical: 0.01–0.05."
345
+ )
346
+
347
+ self.spin_planet_smooth = QDoubleSpinBox()
348
+ self.spin_planet_smooth.setRange(0.0, 10.0)
349
+ self.spin_planet_smooth.setDecimals(2)
350
+ self.spin_planet_smooth.setSingleStep(0.25)
351
+ self.spin_planet_smooth.setValue(1.5)
352
+ self.spin_planet_smooth.setToolTip("Gaussian blur sigma used before thresholding. 1.0–2.0 is typical.")
353
+
354
+ self.spin_norm_lo = QDoubleSpinBox(); self.spin_norm_lo.setRange(0.0, 20.0); self.spin_norm_lo.setValue(1.0)
355
+ self.spin_norm_hi = QDoubleSpinBox(); self.spin_norm_hi.setRange(80.0, 100.0); self.spin_norm_hi.setValue(99.5)
356
+
357
+ # -----------------------------
358
+ # Advanced detection settings
359
+ # -----------------------------
360
+ adv = QGroupBox("Advanced detection settings", self)
361
+ adv.setCheckable(True)
362
+ adv.setChecked(False)
363
+
364
+ adv_body = QWidget(adv) # <- content container
365
+ adv_form = QFormLayout(adv_body)
366
+ adv_form.setContentsMargins(8, 8, 8, 8)
367
+ adv_form.setVerticalSpacing(6)
368
+ adv_form.setHorizontalSpacing(10)
369
+
370
+ adv_form.addRow("", self.chk_planet_norm)
371
+ adv_form.addRow("Planet detect thresh (%)", self.spin_planet_thresh)
372
+ adv_form.addRow("Norm low pct", self.spin_norm_lo)
373
+ adv_form.addRow("Norm high pct", self.spin_norm_hi)
374
+ adv_form.addRow("Planet min val", self.spin_planet_min)
375
+ adv_form.addRow("Planet smooth σ", self.spin_planet_smooth)
376
+
377
+ # Put the body into the groupbox layout
378
+ adv_layout = QVBoxLayout(adv)
379
+ adv_layout.setContentsMargins(8, 8, 8, 8)
380
+ adv_layout.addWidget(adv_body)
381
+
382
+ # show/hide only the body
383
+ adv_body.setVisible(False)
384
+ adv.toggled.connect(adv_body.setVisible)
326
385
 
327
386
  sform.addRow("Tracking", self.cmb_track)
328
387
  sform.addRow("Keep %", self.spin_keep)
388
+
389
+ # instead of adding the detection rows directly:
390
+ # sform.addRow("", self.chk_planet_norm)
391
+ # sform.addRow("Planet detect thresh (%)", self.spin_planet_thresh)
392
+ # sform.addRow("Norm low pct", self.spin_norm_lo)
393
+ # sform.addRow("Norm high pct", self.spin_norm_hi)
394
+ # sform.addRow("Planet min val", self.spin_planet_min)
395
+ # sform.addRow("Planet smooth σ", self.spin_planet_smooth)
396
+
397
+ # add the advanced groupbox as a single row spanning the form
398
+ sform.addRow(adv)
399
+
329
400
  sform.addRow("", self.lbl_anchor)
330
401
  sform.addRow("", self.btn_stack)
331
402
 
@@ -354,7 +425,8 @@ class SERViewer(QDialog):
354
425
  w.toggled.connect(self._refresh)
355
426
  if hasattr(w, "valueChanged"):
356
427
  w.valueChanged.connect(self._refresh)
357
-
428
+ for s in (self.spin_x, self.spin_y, self.spin_w, self.spin_h):
429
+ s.valueChanged.connect(self._sanitize_roi_controls)
358
430
  self.cmb_track.currentIndexChanged.connect(self._on_track_mode_changed)
359
431
  self.btn_stack.clicked.connect(self._open_stacker_clicked)
360
432
  self.cmb_bayer.currentIndexChanged.connect(self._refresh)
@@ -746,16 +818,23 @@ class SERViewer(QDialog):
746
818
  x, y, w, h = rect_disp
747
819
  x_full = int(rx + x)
748
820
  y_full = int(ry + y)
821
+ w = int(w)
822
+ h = int(h)
823
+
824
+ x_full, y_full, w, h = self._even_roi(x_full, y_full, w, h)
825
+
749
826
  self.spin_x.setValue(x_full)
750
827
  self.spin_y.setValue(y_full)
751
- self.spin_w.setValue(int(w))
752
- self.spin_h.setValue(int(h))
828
+ self.spin_w.setValue(w)
829
+ self.spin_h.setValue(h)
753
830
  else:
754
831
  x, y, w, h = rect_disp
755
- self.spin_x.setValue(int(x))
756
- self.spin_y.setValue(int(y))
757
- self.spin_w.setValue(int(w))
758
- self.spin_h.setValue(int(h))
832
+ x, y, w, h = self._even_roi(int(x), int(y), int(w), int(h))
833
+
834
+ self.spin_x.setValue(x)
835
+ self.spin_y.setValue(y)
836
+ self.spin_w.setValue(w)
837
+ self.spin_h.setValue(h)
759
838
 
760
839
  self.chk_roi.setChecked(True)
761
840
  self._refresh()
@@ -844,10 +923,16 @@ class SERViewer(QDialog):
844
923
  track_mode=self._track_mode_value(),
845
924
  surface_anchor=anchor,
846
925
  debayer=debayer,
847
- bayer_pattern=bp, # ✅ THIS IS THE FIX
926
+ bayer_pattern=bp,
848
927
  keep_percent=float(self.spin_keep.value()),
849
- )
850
928
 
929
+ # ✅ planetary detect knobs
930
+ planet_min_val=float(self.spin_planet_min.value()),
931
+ planet_use_norm=bool(self.chk_planet_norm.isChecked()),
932
+ planet_norm_hi_pct=float(self.spin_norm_hi.value()), # <-- you already have this
933
+ planet_thresh_pct=float(self.spin_planet_thresh.value()),
934
+ planet_smooth_sigma=float(self.spin_planet_smooth.value()),
935
+ )
851
936
 
852
937
  dlg.stackProduced.connect(self._on_stacker_produced)
853
938
  dlg.show()
@@ -1090,10 +1175,10 @@ class SERViewer(QDialog):
1090
1175
  dlg.setDirectory(start_dir)
1091
1176
  dlg.setFileMode(QFileDialog.FileMode.ExistingFiles)
1092
1177
  dlg.setNameFilters([
1093
- "Planetary Sources (*.ser *.avi *.mp4 *.mov *.mkv *.png *.tif *.tiff *.jpg *.jpeg *.bmp *.webp)",
1178
+ "Planetary Sources (*.ser *.avi *.mp4 *.mov *.mkv *.png *.tif *.tiff *.jpg *.jpeg *.bmp *.webp *.fit *.fits)",
1094
1179
  "SER Videos (*.ser)",
1095
1180
  "AVI/Video (*.avi *.mp4 *.mov *.mkv)",
1096
- "Images (*.png *.tif *.tiff *.jpg *.jpeg *.bmp *.webp)",
1181
+ "Images (*.png *.tif *.tiff *.jpg *.jpeg *.bmp *.webp *.fit *.fits)",
1097
1182
  "All Files (*)",
1098
1183
  ])
1099
1184
 
@@ -1217,12 +1302,65 @@ class SERViewer(QDialog):
1217
1302
  self._refresh()
1218
1303
 
1219
1304
  # ---------------- rendering ----------------
1305
+ def _even_roi(self, x: int, y: int, w: int, h: int):
1306
+ """Force ROI x,y,w,h to even numbers (preserves Bayer phase)."""
1307
+ if self.reader is None:
1308
+ return x, y, w, h
1309
+
1310
+ m = self.reader.meta
1311
+ W = int(m.width)
1312
+ H = int(m.height)
1313
+
1314
+ # Clamp first
1315
+ x = max(0, min(W - 1, int(x)))
1316
+ y = max(0, min(H - 1, int(y)))
1317
+ w = max(1, int(w))
1318
+ h = max(1, int(h))
1319
+
1320
+ # Make origin even (keep Bayer phase)
1321
+ x &= ~1
1322
+ y &= ~1
1323
+
1324
+ # Make size even
1325
+ w &= ~1
1326
+ h &= ~1
1327
+ if w < 2: w = 2
1328
+ if h < 2: h = 2
1329
+
1330
+ # Fit inside image (keep evenness)
1331
+ if x + w > W:
1332
+ w = (W - x) & ~1
1333
+ if w < 2:
1334
+ x = max(0, (W - 2) & ~1)
1335
+ w = 2
1336
+ if y + h > H:
1337
+ h = (H - y) & ~1
1338
+ if h < 2:
1339
+ y = max(0, (H - 2) & ~1)
1340
+ h = 2
1341
+
1342
+ return int(x), int(y), int(w), int(h)
1343
+
1344
+ def _sanitize_roi_controls(self):
1345
+ if self.reader is None:
1346
+ return
1347
+ x = int(self.spin_x.value()); y = int(self.spin_y.value())
1348
+ w = int(self.spin_w.value()); h = int(self.spin_h.value())
1349
+ ex, ey, ew, eh = self._even_roi(x, y, w, h)
1350
+ if (ex, ey, ew, eh) != (x, y, w, h):
1351
+ self.spin_x.blockSignals(True); self.spin_y.blockSignals(True)
1352
+ self.spin_w.blockSignals(True); self.spin_h.blockSignals(True)
1353
+ self.spin_x.setValue(ex); self.spin_y.setValue(ey)
1354
+ self.spin_w.setValue(ew); self.spin_h.setValue(eh)
1355
+ self.spin_x.blockSignals(False); self.spin_y.blockSignals(False)
1356
+ self.spin_w.blockSignals(False); self.spin_h.blockSignals(False)
1220
1357
 
1221
1358
  def _roi_tuple(self):
1222
1359
  if not self.chk_roi.isChecked():
1223
1360
  return None
1224
- return (int(self.spin_x.value()), int(self.spin_y.value()),
1225
- int(self.spin_w.value()), int(self.spin_h.value()))
1361
+ x, y, w, h = (int(self.spin_x.value()), int(self.spin_y.value()),
1362
+ int(self.spin_w.value()), int(self.spin_h.value()))
1363
+ return self._even_roi(x, y, w, h)
1226
1364
 
1227
1365
  def _on_trim_changed(self):
1228
1366
  if self.reader is None:
@@ -1542,18 +1680,28 @@ class SERViewer(QDialog):
1542
1680
 
1543
1681
  def _to_qimage(self, arr: np.ndarray) -> QImage:
1544
1682
  a = np.clip(arr, 0.0, 1.0)
1683
+
1545
1684
  if a.ndim == 2:
1546
- u = (a * 255.0).astype(np.uint8)
1685
+ u = (a * 255.0).astype(np.uint8, copy=False)
1686
+ # ✅ FITS/memmap correction: ensure C-contiguous rows
1687
+ if not u.flags["C_CONTIGUOUS"]:
1688
+ u = np.ascontiguousarray(u)
1689
+
1547
1690
  h, w = u.shape
1548
1691
  return QImage(u.data, w, h, w, QImage.Format.Format_Grayscale8).copy()
1549
1692
 
1550
1693
  if a.ndim == 3 and a.shape[2] >= 3:
1551
- u = (a[..., :3] * 255.0).astype(np.uint8)
1694
+ u = (a[..., :3] * 255.0).astype(np.uint8, copy=False)
1695
+ # ✅ FITS/memmap correction: ensure C-contiguous packed RGB
1696
+ if not u.flags["C_CONTIGUOUS"]:
1697
+ u = np.ascontiguousarray(u)
1698
+
1552
1699
  h, w, _ = u.shape
1553
1700
  return QImage(u.data, w, h, w * 3, QImage.Format.Format_RGB888).copy()
1554
1701
 
1555
1702
  raise ValueError(f"Unexpected image shape: {a.shape}")
1556
1703
 
1704
+
1557
1705
  def _roi_bounds(self):
1558
1706
  """
1559
1707
  Returns (rx, ry, rw, rh) in full-frame coords if ROI enabled,
setiastro/saspro/sfcc.py CHANGED
@@ -20,6 +20,12 @@ from datetime import datetime
20
20
  from typing import List, Tuple, Optional
21
21
 
22
22
  import numpy as np
23
+
24
+ try:
25
+ _trapz = np.trapezoid
26
+ except AttributeError:
27
+ _trapz = np.trapz
28
+
23
29
  import numpy.ma as ma
24
30
  import pandas as pd
25
31
 
@@ -313,8 +319,8 @@ class SaspViewer(QMainWindow):
313
319
  for color in ("red","green","blue"):
314
320
  data = rgb_data[color]
315
321
  if data is not None:
316
- S_star = np.trapezoid(data["response"], x=common_wl)
317
- S_veg = np.trapezoid(fl_veg_c * data["T_sys"], x=common_wl)
322
+ S_star = _trapz(data["response"], x=common_wl)
323
+ S_veg = _trapz(fl_veg_c * data["T_sys"], x=common_wl)
318
324
  if S_veg>0 and S_star>0:
319
325
  mag = -2.5 * np.log10(S_star / S_veg)
320
326
  mag_texts.append(f"{color[0].upper()}→{data['filter_name']}: {mag:.2f}")
@@ -1485,9 +1491,9 @@ class SFCCDialog(QDialog):
1485
1491
  wl_ref, fl_ref = load_sed(ref_sed_name)
1486
1492
  fr_i = np.interp(wl_grid, wl_ref, fl_ref, left=0.0, right=0.0)
1487
1493
 
1488
- S_ref_R = np.trapezoid(fr_i * T_sys_R, x=wl_grid)
1489
- S_ref_G = np.trapezoid(fr_i * T_sys_G, x=wl_grid)
1490
- S_ref_B = np.trapezoid(fr_i * T_sys_B, x=wl_grid)
1494
+ S_ref_R = _trapz(fr_i * T_sys_R, x=wl_grid)
1495
+ S_ref_G = _trapz(fr_i * T_sys_G, x=wl_grid)
1496
+ S_ref_B = _trapz(fr_i * T_sys_B, x=wl_grid)
1491
1497
 
1492
1498
  diag_meas_RG, diag_exp_RG = [], []
1493
1499
  diag_meas_BG, diag_exp_BG = [], []
@@ -1510,9 +1516,9 @@ class SFCCDialog(QDialog):
1510
1516
  try:
1511
1517
  wl_s, fl_s = load_sed(pname)
1512
1518
  fs_i = np.interp(wl_grid, wl_s, fl_s, left=0.0, right=0.0)
1513
- S_sr = np.trapezoid(fs_i * T_sys_R, x=wl_grid)
1514
- S_sg = np.trapezoid(fs_i * T_sys_G, x=wl_grid)
1515
- S_sb = np.trapezoid(fs_i * T_sys_B, x=wl_grid)
1519
+ S_sr = _trapz(fs_i * T_sys_R, x=wl_grid)
1520
+ S_sg = _trapz(fs_i * T_sys_G, x=wl_grid)
1521
+ S_sb = _trapz(fs_i * T_sys_B, x=wl_grid)
1516
1522
  template_integrals[pname] = (S_sr, S_sg, S_sb)
1517
1523
  except Exception as e:
1518
1524
  print(f"[SFCC] Warning: failed to load/integrate template {pname}: {e}")