setiastrosuitepro 1.6.10__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.
Files changed (51) hide show
  1. setiastro/images/colorwheel.svg +97 -0
  2. setiastro/images/narrowbandnormalization.png +0 -0
  3. setiastro/images/planetarystacker.png +0 -0
  4. setiastro/saspro/__main__.py +1 -1
  5. setiastro/saspro/_generated/build_info.py +2 -2
  6. setiastro/saspro/aberration_ai.py +49 -11
  7. setiastro/saspro/aberration_ai_preset.py +29 -3
  8. setiastro/saspro/backgroundneutral.py +73 -33
  9. setiastro/saspro/blink_comparator_pro.py +116 -71
  10. setiastro/saspro/convo.py +9 -6
  11. setiastro/saspro/curve_editor_pro.py +72 -22
  12. setiastro/saspro/curves_preset.py +249 -47
  13. setiastro/saspro/doc_manager.py +178 -11
  14. setiastro/saspro/gui/main_window.py +218 -66
  15. setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
  16. setiastro/saspro/gui/mixins/file_mixin.py +35 -16
  17. setiastro/saspro/gui/mixins/menu_mixin.py +31 -1
  18. setiastro/saspro/gui/mixins/toolbar_mixin.py +132 -10
  19. setiastro/saspro/histogram.py +179 -7
  20. setiastro/saspro/imageops/narrowband_normalization.py +816 -0
  21. setiastro/saspro/imageops/serloader.py +769 -0
  22. setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
  23. setiastro/saspro/imageops/stretch.py +66 -15
  24. setiastro/saspro/legacy/numba_utils.py +25 -48
  25. setiastro/saspro/live_stacking.py +24 -4
  26. setiastro/saspro/multiscale_decomp.py +30 -17
  27. setiastro/saspro/narrowband_normalization.py +1618 -0
  28. setiastro/saspro/numba_utils.py +0 -55
  29. setiastro/saspro/ops/script_editor.py +5 -0
  30. setiastro/saspro/ops/scripts.py +119 -0
  31. setiastro/saspro/remove_green.py +1 -1
  32. setiastro/saspro/resources.py +4 -0
  33. setiastro/saspro/ser_stack_config.py +68 -0
  34. setiastro/saspro/ser_stacker.py +2245 -0
  35. setiastro/saspro/ser_stacker_dialog.py +1481 -0
  36. setiastro/saspro/ser_tracking.py +206 -0
  37. setiastro/saspro/serviewer.py +1242 -0
  38. setiastro/saspro/sfcc.py +602 -214
  39. setiastro/saspro/shortcuts.py +35 -16
  40. setiastro/saspro/stacking_suite.py +332 -87
  41. setiastro/saspro/star_alignment.py +243 -122
  42. setiastro/saspro/stat_stretch.py +220 -31
  43. setiastro/saspro/subwindow.py +2 -4
  44. setiastro/saspro/whitebalance.py +24 -0
  45. setiastro/saspro/widgets/resource_monitor.py +122 -74
  46. {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/METADATA +2 -2
  47. {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/RECORD +51 -40
  48. {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/WHEEL +0 -0
  49. {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/entry_points.txt +0 -0
  50. {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/licenses/LICENSE +0 -0
  51. {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/licenses/license.txt +0 -0
@@ -317,61 +317,6 @@ def invert_image_numba(image):
317
317
  output[y, x, c] = 1.0 - image[y, x, c]
318
318
  return output
319
319
 
320
-
321
-
322
- @njit(parallel=True, fastmath=True, cache=True)
323
- def apply_flat_division_numba_2d(image, master_flat, master_bias=None):
324
- """
325
- Mono version: image.shape == (H,W)
326
- """
327
- if master_bias is not None:
328
- master_flat = master_flat - master_bias
329
- image = image - master_bias
330
-
331
- median_flat = np.mean(master_flat)
332
- height, width = image.shape
333
-
334
- for y in prange(height):
335
- for x in range(width):
336
- image[y, x] /= (master_flat[y, x] / median_flat)
337
-
338
- return image
339
-
340
-
341
- @njit(parallel=True, fastmath=True, cache=True)
342
- def apply_flat_division_numba_3d(image, master_flat, master_bias=None):
343
- """
344
- Color version: image.shape == (H,W,C)
345
- """
346
- if master_bias is not None:
347
- master_flat = master_flat - master_bias
348
- image = image - master_bias
349
-
350
- median_flat = np.mean(master_flat)
351
- height, width, channels = image.shape
352
-
353
- for y in prange(height):
354
- for x in range(width):
355
- for c in range(channels):
356
- image[y, x, c] /= (master_flat[y, x, c] / median_flat)
357
-
358
- return image
359
-
360
- def apply_flat_division_numba(image, master_flat, master_bias=None):
361
- """
362
- Dispatcher that calls the correct Numba function
363
- depending on whether 'image' is 2D or 3D.
364
- """
365
- if image.ndim == 2:
366
- # Mono
367
- return apply_flat_division_numba_2d(image, master_flat, master_bias)
368
- elif image.ndim == 3:
369
- # Color
370
- return apply_flat_division_numba_3d(image, master_flat, master_bias)
371
- else:
372
- raise ValueError(f"apply_flat_division_numba: expected 2D or 3D, got shape {image.shape}")
373
-
374
-
375
320
  @njit(parallel=True, cache=True)
376
321
  def subtract_dark_3d(frames, dark_frame):
377
322
  """
@@ -15,6 +15,11 @@ from PyQt6.QtWidgets import (
15
15
  QLineEdit, QToolButton, QCheckBox, QTextEdit
16
16
  )
17
17
 
18
+ from typing import TYPE_CHECKING
19
+
20
+ if TYPE_CHECKING:
21
+ from setiastro.saspro.ops.scripts import ScriptEntry
22
+
18
23
  # -----------------------------------------------------------------------------
19
24
  # Code editor with line numbers (QPlainTextEdit subclass)
20
25
  # -----------------------------------------------------------------------------
@@ -294,6 +294,125 @@ class ScriptContext:
294
294
  # ✅ Normal run: let DocManager decide (ROI preview vs full)
295
295
  dm.update_active_document(img, metadata={}, step_name=step_name)
296
296
 
297
+ def _find_subwindow_for_doc(self, base_doc):
298
+ """Return (sw, widget) for the first subwindow showing base_doc."""
299
+ for sw, w in self._iter_open_subwindows():
300
+ d = self._base_doc_for_widget(w)
301
+ if d is base_doc:
302
+ return sw, w
303
+ return None, None
304
+
305
+ def rename_active_view(self, new_title: str) -> bool:
306
+ """Rename only the active MDI view title (this window)."""
307
+ w = self.active_view()
308
+ if w is None:
309
+ return False
310
+
311
+ t = (new_title or "").strip()
312
+ if not t:
313
+ return False
314
+
315
+ try:
316
+ # ImageSubWindow convention
317
+ setattr(w, "_view_title_override", t)
318
+ if hasattr(w, "_sync_host_title"):
319
+ w._sync_host_title()
320
+ return True
321
+ except Exception:
322
+ return False
323
+
324
+ def rename_active_document(self, new_name: str) -> bool:
325
+ """Rename the underlying document display name (affects explorer + other views)."""
326
+ doc = self.active_document()
327
+ if doc is None:
328
+ return False
329
+
330
+ n = (new_name or "").strip()
331
+ if not n:
332
+ return False
333
+
334
+ try:
335
+ old = ""
336
+ try:
337
+ old = str(doc.display_name() or "")
338
+ except Exception:
339
+ old = str(getattr(doc, "metadata", {}).get("display_name", "") or "")
340
+
341
+ doc.metadata["display_name"] = n
342
+ try:
343
+ doc.changed.emit()
344
+ except Exception:
345
+ pass
346
+
347
+ # If the active view had an override equal to the old doc name, drop it (matches your UI behavior)
348
+ w = self.active_view()
349
+ if w is not None:
350
+ try:
351
+ if getattr(w, "_view_title_override", None) == old:
352
+ setattr(w, "_view_title_override", None)
353
+ except Exception:
354
+ pass
355
+ try:
356
+ if hasattr(w, "_sync_host_title"):
357
+ w._sync_host_title()
358
+ except Exception:
359
+ pass
360
+
361
+ return True
362
+ except Exception:
363
+ return False
364
+
365
+ def rename_view(self, view_name_or_uid: str, new_title: str) -> bool:
366
+ """Rename a specific *view/window* by name/title/uid (first match)."""
367
+ doc = self.get_document(view_name_or_uid)
368
+ if doc is None:
369
+ return False
370
+
371
+ sw, w = self._find_subwindow_for_doc(doc)
372
+ if w is None:
373
+ return False
374
+
375
+ t = (new_title or "").strip()
376
+ if not t:
377
+ return False
378
+
379
+ try:
380
+ setattr(w, "_view_title_override", t)
381
+ if hasattr(w, "_sync_host_title"):
382
+ w._sync_host_title()
383
+ return True
384
+ except Exception:
385
+ return False
386
+
387
+ def rename_document(self, view_name_or_uid: str, new_name: str) -> bool:
388
+ """Rename a specific *document* by view name/title/uid."""
389
+ doc = self.get_document(view_name_or_uid)
390
+ if doc is None:
391
+ return False
392
+ n = (new_name or "").strip()
393
+ if not n:
394
+ return False
395
+
396
+ try:
397
+ doc.metadata["display_name"] = n
398
+ try:
399
+ doc.changed.emit()
400
+ except Exception:
401
+ pass
402
+
403
+ # resync any window currently showing it
404
+ sw, w = self._find_subwindow_for_doc(doc)
405
+ if w is not None and hasattr(w, "_sync_host_title"):
406
+ try:
407
+ w._sync_host_title()
408
+ except Exception:
409
+ pass
410
+
411
+ return True
412
+ except Exception:
413
+ return False
414
+
415
+
297
416
  # ---- convenience wrappers into main window ----
298
417
  def run_command(self, command_id: str, preset=None, **kwargs):
299
418
  return _run_command(self, command_id, preset, **kwargs)
@@ -164,7 +164,7 @@ class RemoveGreenDialog(QDialog):
164
164
 
165
165
  def _build_ui(self):
166
166
  lay = QVBoxLayout(self)
167
- lay.addWidget(QLabel(self.tr("Select the amount to remove green noise:")))
167
+ lay.addWidget(QLabel(self.tr("Select the amount to remove green:")))
168
168
 
169
169
  # amount
170
170
  self.slider = QSlider(Qt.Orientation.Horizontal)
@@ -244,6 +244,7 @@ class Icons:
244
244
  STACKING = property(lambda self: _resource_path('stacking.png'))
245
245
  LIVE_STACKING = property(lambda self: _resource_path('livestacking.png'))
246
246
  IMAGE_COMBINE = property(lambda self: _resource_path('imagecombine.png'))
247
+ PLANETARY_STACKER = property(lambda self: _resource_path('planetarystacker.png'))
247
248
 
248
249
  # Moon phase (WIMS)
249
250
  MOON_NEW = property(lambda self: _resource_path('new_moon.png'))
@@ -301,6 +302,7 @@ class Icons:
301
302
  COLOR_WHEEL = property(lambda self: _resource_path('colorwheel.png'))
302
303
  SELECTIVE_COLOR = property(lambda self: _resource_path('selectivecolor.png'))
303
304
  NB_TO_RGB = property(lambda self: _resource_path('nbtorgb.png'))
305
+ NARROWBANDNORMALIZATION = property(lambda self: _resource_path('narrowbandnormalization.png'))
304
306
 
305
307
  # Stretching
306
308
  STAT_STRETCH = property(lambda self: _resource_path('statstretch.png'))
@@ -531,6 +533,7 @@ def _init_legacy_paths():
531
533
  'collage_path': get_icon_path('collage.png'),
532
534
  'annotated_path': get_icon_path('annotated.png'),
533
535
  'colorwheel_path': get_icon_path('colorwheel.png'),
536
+ 'narrowbandnormalization_path': get_icon_path('narrowbandnormalization.png'),
534
537
  'font_path': get_icon_path('font.png'),
535
538
  'csv_icon_path': get_icon_path('cvs.png'),
536
539
  'spinner_path': get_data_path('spinner.gif'),
@@ -540,6 +543,7 @@ def _init_legacy_paths():
540
543
  'debayer_path': get_icon_path('debayer.png'),
541
544
  'aberration_path': get_icon_path('aberration.png'),
542
545
  'functionbundles_path': get_icon_path('functionbundle.png'),
546
+ 'planetarystacker_path': get_icon_path('planetarystacker.png'),
543
547
  'viewbundles_path': get_icon_path('viewbundle.png'),
544
548
  'selectivecolor_path': get_icon_path('selectivecolor.png'),
545
549
  'rgbalign_path': get_icon_path('rgbalign.png'),
@@ -0,0 +1,68 @@
1
+ # src/setiastro/saspro/ser_stack_config.py
2
+ from __future__ import annotations
3
+ from dataclasses import dataclass
4
+ from typing import Optional, Tuple, Literal, Union, Sequence
5
+
6
+ from setiastro.saspro.imageops.serloader import PlanetaryFrameSource
7
+
8
+ TrackMode = Literal["off", "planetary", "surface"]
9
+ PlanetarySource = Union[str, Sequence[str], PlanetaryFrameSource]
10
+
11
+ @dataclass
12
+ class SERStackConfig:
13
+ source: PlanetarySource
14
+ roi: Optional[Tuple[int, int, int, int]] = None
15
+ track_mode: TrackMode = "planetary"
16
+ surface_anchor: Optional[Tuple[int, int, int, int]] = None
17
+ keep_percent: float = 20.0
18
+
19
+ # AP / alignment
20
+ ap_size: int = 64
21
+ ap_spacing: int = 48
22
+ ap_min_mean: float = 0.03
23
+ ap_multiscale: bool = False
24
+ ssd_refine_bruteforce: bool = False
25
+
26
+ # ✅ Drizzle
27
+ drizzle_scale: float = 1.0 # 1.0 = off, 1.5, 2.0
28
+ drizzle_pixfrac: float = 0.80 # "drop shrink" in output pixels (roughly)
29
+ drizzle_kernel: str = "gaussian" # "square" | "circle" | "gaussian"
30
+ drizzle_sigma: float = 0.0 # only used for gaussian; 0 => auto from pixfrac
31
+
32
+ def __init__(self, source: PlanetarySource, **kwargs):
33
+ # Allow deprecated/ignored kwargs without crashing
34
+ kwargs.pop("multipoint", None) # accept but ignore
35
+
36
+ self.source = source
37
+ self.roi = kwargs.pop("roi", None)
38
+ self.track_mode = kwargs.pop("track_mode", "planetary")
39
+ self.surface_anchor = kwargs.pop("surface_anchor", None)
40
+ self.keep_percent = float(kwargs.pop("keep_percent", 20.0))
41
+
42
+ self.ap_size = int(kwargs.pop("ap_size", 64))
43
+ self.ap_spacing = int(kwargs.pop("ap_spacing", 48))
44
+ self.ap_min_mean = float(kwargs.pop("ap_min_mean", 0.03))
45
+ self.ap_multiscale = bool(kwargs.pop("ap_multiscale", False))
46
+ self.ssd_refine_bruteforce = bool(kwargs.pop("ssd_refine_bruteforce", False))
47
+
48
+ # ✅ NEW: Drizzle params
49
+ self.drizzle_scale = float(kwargs.pop("drizzle_scale", 1.0))
50
+ if self.drizzle_scale not in (1.0, 1.5, 2.0):
51
+ self.drizzle_scale = 1.0
52
+
53
+ self.drizzle_pixfrac = float(kwargs.pop("drizzle_pixfrac", 0.80))
54
+ self.drizzle_kernel = str(kwargs.pop("drizzle_kernel", "gaussian")).strip().lower()
55
+ self.drizzle_sigma = float(kwargs.pop("drizzle_sigma", 0.0))
56
+
57
+ # sanitize a bit
58
+ if self.drizzle_scale < 1.0:
59
+ self.drizzle_scale = 1.0
60
+ if self.drizzle_pixfrac <= 0.0:
61
+ self.drizzle_pixfrac = 0.01
62
+ if self.drizzle_kernel not in ("square", "circle", "gaussian"):
63
+ self.drizzle_kernel = "gaussian"
64
+ if self.drizzle_sigma < 0.0:
65
+ self.drizzle_sigma = 0.0
66
+
67
+ if kwargs:
68
+ raise TypeError(f"Unexpected config keys: {sorted(kwargs.keys())}")