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.
- setiastro/images/colorwheel.svg +97 -0
- setiastro/images/narrowbandnormalization.png +0 -0
- setiastro/images/planetarystacker.png +0 -0
- setiastro/saspro/__main__.py +1 -1
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/aberration_ai.py +49 -11
- setiastro/saspro/aberration_ai_preset.py +29 -3
- setiastro/saspro/backgroundneutral.py +73 -33
- setiastro/saspro/blink_comparator_pro.py +116 -71
- setiastro/saspro/convo.py +9 -6
- setiastro/saspro/curve_editor_pro.py +72 -22
- setiastro/saspro/curves_preset.py +249 -47
- setiastro/saspro/doc_manager.py +178 -11
- setiastro/saspro/gui/main_window.py +218 -66
- setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
- setiastro/saspro/gui/mixins/file_mixin.py +35 -16
- setiastro/saspro/gui/mixins/menu_mixin.py +31 -1
- setiastro/saspro/gui/mixins/toolbar_mixin.py +132 -10
- setiastro/saspro/histogram.py +179 -7
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +769 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
- setiastro/saspro/imageops/stretch.py +66 -15
- setiastro/saspro/legacy/numba_utils.py +25 -48
- setiastro/saspro/live_stacking.py +24 -4
- setiastro/saspro/multiscale_decomp.py +30 -17
- setiastro/saspro/narrowband_normalization.py +1618 -0
- setiastro/saspro/numba_utils.py +0 -55
- setiastro/saspro/ops/script_editor.py +5 -0
- setiastro/saspro/ops/scripts.py +119 -0
- setiastro/saspro/remove_green.py +1 -1
- setiastro/saspro/resources.py +4 -0
- setiastro/saspro/ser_stack_config.py +68 -0
- setiastro/saspro/ser_stacker.py +2245 -0
- setiastro/saspro/ser_stacker_dialog.py +1481 -0
- setiastro/saspro/ser_tracking.py +206 -0
- setiastro/saspro/serviewer.py +1242 -0
- setiastro/saspro/sfcc.py +602 -214
- setiastro/saspro/shortcuts.py +35 -16
- setiastro/saspro/stacking_suite.py +332 -87
- setiastro/saspro/star_alignment.py +243 -122
- setiastro/saspro/stat_stretch.py +220 -31
- setiastro/saspro/subwindow.py +2 -4
- setiastro/saspro/whitebalance.py +24 -0
- setiastro/saspro/widgets/resource_monitor.py +122 -74
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/METADATA +2 -2
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/RECORD +51 -40
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/licenses/license.txt +0 -0
setiastro/saspro/numba_utils.py
CHANGED
|
@@ -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
|
# -----------------------------------------------------------------------------
|
setiastro/saspro/ops/scripts.py
CHANGED
|
@@ -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)
|
setiastro/saspro/remove_green.py
CHANGED
|
@@ -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
|
|
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)
|
setiastro/saspro/resources.py
CHANGED
|
@@ -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())}")
|