setiastrosuitepro 1.6.7__py3-none-any.whl → 1.6.10__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/abeicon.svg +16 -0
- setiastro/images/cosmic.svg +40 -0
- setiastro/images/cosmicsat.svg +24 -0
- setiastro/images/graxpert.svg +19 -0
- setiastro/images/linearfit.svg +32 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/add_stars.py +29 -5
- setiastro/saspro/blink_comparator_pro.py +74 -24
- setiastro/saspro/cosmicclarity.py +125 -18
- setiastro/saspro/crop_dialog_pro.py +96 -2
- setiastro/saspro/curve_editor_pro.py +60 -39
- setiastro/saspro/frequency_separation.py +1159 -208
- setiastro/saspro/gui/main_window.py +131 -31
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/update_mixin.py +121 -33
- setiastro/saspro/imageops/stretch.py +531 -62
- setiastro/saspro/layers.py +13 -9
- setiastro/saspro/layers_dock.py +183 -3
- setiastro/saspro/legacy/numba_utils.py +43 -0
- setiastro/saspro/live_stacking.py +158 -70
- setiastro/saspro/multiscale_decomp.py +47 -12
- setiastro/saspro/numba_utils.py +72 -2
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/shortcuts.py +122 -12
- setiastro/saspro/signature_insert.py +688 -33
- setiastro/saspro/stacking_suite.py +523 -316
- setiastro/saspro/stat_stretch.py +688 -130
- setiastro/saspro/subwindow.py +302 -71
- setiastro/saspro/widgets/common_utilities.py +28 -21
- setiastro/saspro/widgets/resource_monitor.py +7 -7
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/METADATA +1 -1
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/RECORD +37 -31
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/license.txt +0 -0
setiastro/saspro/layers.py
CHANGED
|
@@ -23,18 +23,19 @@ BLEND_MODES = [
|
|
|
23
23
|
@dataclass
|
|
24
24
|
class ImageLayer:
|
|
25
25
|
name: str
|
|
26
|
-
src_doc: object
|
|
26
|
+
src_doc: Optional[object] = None # ImageDocument (can be None for baked raster)
|
|
27
|
+
pixels: Optional[np.ndarray] = None # NEW: baked raster pixels in float32 or any dtype
|
|
28
|
+
|
|
27
29
|
visible: bool = True
|
|
28
|
-
opacity: float = 1.0
|
|
29
|
-
mode: str = "Normal"
|
|
30
|
-
mask_doc: Optional[object] = None
|
|
30
|
+
opacity: float = 1.0
|
|
31
|
+
mode: str = "Normal"
|
|
32
|
+
mask_doc: Optional[object] = None
|
|
31
33
|
mask_invert: bool = False
|
|
32
|
-
mask_feather: float = 0.0
|
|
34
|
+
mask_feather: float = 0.0
|
|
33
35
|
mask_use_luma: bool = False
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
sigmoid_strength: float = 10.0 # steepness of the curve
|
|
37
|
+
sigmoid_center: float = 0.5
|
|
38
|
+
sigmoid_strength: float = 10.0
|
|
38
39
|
|
|
39
40
|
def _float01(arr: np.ndarray) -> np.ndarray:
|
|
40
41
|
a = np.asarray(arr)
|
|
@@ -182,7 +183,10 @@ def composite_stack(base_img: np.ndarray, layers: List[ImageLayer]) -> np.ndarra
|
|
|
182
183
|
for L in reversed(layers or []):
|
|
183
184
|
if not L.visible:
|
|
184
185
|
continue
|
|
185
|
-
src = getattr(L
|
|
186
|
+
src = getattr(L, "pixels", None)
|
|
187
|
+
if src is None:
|
|
188
|
+
src_doc = getattr(L, "src_doc", None)
|
|
189
|
+
src = getattr(src_doc, "image", None) if src_doc is not None else None
|
|
186
190
|
if src is None:
|
|
187
191
|
continue
|
|
188
192
|
s = _ensure_3c(_float01(src))
|
setiastro/saspro/layers_dock.py
CHANGED
|
@@ -212,19 +212,29 @@ class LayersDock(QDockWidget):
|
|
|
212
212
|
top.addWidget(self.view_combo, 1)
|
|
213
213
|
|
|
214
214
|
self.list = QListWidget()
|
|
215
|
-
self.list.setSelectionMode(QAbstractItemView.SelectionMode.
|
|
215
|
+
self.list.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
|
216
216
|
self.list.setAlternatingRowColors(True)
|
|
217
217
|
v.addWidget(self.list, 1)
|
|
218
218
|
|
|
219
219
|
# buttons
|
|
220
220
|
row = QHBoxLayout(); v.addLayout(row)
|
|
221
|
+
|
|
221
222
|
self.btn_clear = QPushButton("Clear All Layers")
|
|
222
|
-
|
|
223
|
+
|
|
224
|
+
self.btn_merge = QPushButton("Merge → Push to View")
|
|
223
225
|
self.btn_merge.setToolTip("Flatten the visible layers into the current view and add an undo step.")
|
|
226
|
+
|
|
227
|
+
self.btn_merge_new = QPushButton("Merge → New Document")
|
|
228
|
+
self.btn_merge_new.setToolTip("Flatten the visible layers into a new document (does not modify the base view).")
|
|
229
|
+
|
|
230
|
+
self.btn_merge_sel = QPushButton("Merge Selected → Single Layer")
|
|
231
|
+
self.btn_merge_sel.setToolTip("Merge the selected layers into one raster layer (Photoshop-style).")
|
|
232
|
+
|
|
224
233
|
row.addWidget(self.btn_merge)
|
|
234
|
+
row.addWidget(self.btn_merge_new)
|
|
235
|
+
row.addWidget(self.btn_merge_sel)
|
|
225
236
|
row.addStretch(1)
|
|
226
237
|
row.addWidget(self.btn_clear)
|
|
227
|
-
|
|
228
238
|
self.setWidget(w)
|
|
229
239
|
|
|
230
240
|
# dnd (accept drops from views)
|
|
@@ -240,6 +250,8 @@ class LayersDock(QDockWidget):
|
|
|
240
250
|
self.docman.documentRemoved.connect(lambda _d: self._refresh_views())
|
|
241
251
|
|
|
242
252
|
self.btn_merge.clicked.connect(self._merge_and_push)
|
|
253
|
+
self.btn_merge_new.clicked.connect(self._merge_to_new_doc)
|
|
254
|
+
self.btn_merge_sel.clicked.connect(self._merge_selected_to_single_layer)
|
|
243
255
|
|
|
244
256
|
# initial
|
|
245
257
|
self._refresh_views()
|
|
@@ -420,8 +432,107 @@ class LayersDock(QDockWidget):
|
|
|
420
432
|
has_layers = bool(getattr(vw, "_layers", []))
|
|
421
433
|
self.btn_merge.setEnabled(has_layers)
|
|
422
434
|
self.btn_clear.setEnabled(has_layers)
|
|
435
|
+
if hasattr(self, "btn_merge_new"):
|
|
436
|
+
self.btn_merge_new.setEnabled(has_layers)
|
|
437
|
+
has_layers = bool(getattr(vw, "_layers", []))
|
|
438
|
+
self.btn_merge_sel.setEnabled(has_layers)
|
|
423
439
|
self._refresh_row_heights()
|
|
424
440
|
|
|
441
|
+
def _selected_layer_indices(self) -> list[int]:
|
|
442
|
+
vw = self.current_view()
|
|
443
|
+
if not vw:
|
|
444
|
+
return []
|
|
445
|
+
n = len(getattr(vw, "_layers", []) or [])
|
|
446
|
+
idxs = []
|
|
447
|
+
for it in self.list.selectedItems():
|
|
448
|
+
r = self.list.row(it)
|
|
449
|
+
if 0 <= r < n:
|
|
450
|
+
idxs.append(r)
|
|
451
|
+
idxs = sorted(set(idxs))
|
|
452
|
+
return idxs
|
|
453
|
+
|
|
454
|
+
def _render_stack(self, base_img: np.ndarray, layers: list[ImageLayer]) -> np.ndarray:
|
|
455
|
+
# composite_stack already respects visibility/opacity/modes/masks
|
|
456
|
+
out = composite_stack(base_img, layers)
|
|
457
|
+
return out if out is not None else base_img
|
|
458
|
+
|
|
459
|
+
def _open_baked_layer_doc(self, base_doc, arr: np.ndarray, title: str):
|
|
460
|
+
dm = getattr(self.mw, "docman", None)
|
|
461
|
+
if not dm or not hasattr(dm, "open_array"):
|
|
462
|
+
return None
|
|
463
|
+
meta = dict(getattr(base_doc, "metadata", {}) or {})
|
|
464
|
+
meta.update({
|
|
465
|
+
"bit_depth": "32-bit floating point",
|
|
466
|
+
"is_mono": (arr.ndim == 2 or (arr.ndim == 3 and arr.shape[-1] == 1)),
|
|
467
|
+
"source": "Layers Merge Selected",
|
|
468
|
+
})
|
|
469
|
+
return dm.open_array(arr.astype(np.float32, copy=False), metadata=meta, title=title)
|
|
470
|
+
|
|
471
|
+
def _merge_selected_to_single_layer(self):
|
|
472
|
+
vw = self.current_view()
|
|
473
|
+
if not vw:
|
|
474
|
+
return
|
|
475
|
+
|
|
476
|
+
layers = list(getattr(vw, "_layers", []) or [])
|
|
477
|
+
if not layers:
|
|
478
|
+
QMessageBox.information(self, "Layers", "There are no layers to merge.")
|
|
479
|
+
return
|
|
480
|
+
|
|
481
|
+
sel = self._selected_layer_indices()
|
|
482
|
+
if len(sel) < 2:
|
|
483
|
+
QMessageBox.information(self, "Layers", "Select two or more layers to merge.")
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
try:
|
|
487
|
+
base_doc = getattr(vw, "document", None)
|
|
488
|
+
if base_doc is None or getattr(base_doc, "image", None) is None:
|
|
489
|
+
QMessageBox.warning(self, "Layers", "No base image available for this view.")
|
|
490
|
+
return
|
|
491
|
+
base_img = base_doc.image
|
|
492
|
+
|
|
493
|
+
i0, i1 = sel[0], sel[-1]
|
|
494
|
+
|
|
495
|
+
# IMPORTANT ASSUMPTION (matches your UI order):
|
|
496
|
+
# vw._layers is top-to-bottom in the list, and "below" means larger index.
|
|
497
|
+
layers_above = layers[:i0]
|
|
498
|
+
layers_sel = layers[i0:i1+1]
|
|
499
|
+
layers_below = layers[i1+1:]
|
|
500
|
+
|
|
501
|
+
# 1) Render what exists directly under the selected range
|
|
502
|
+
under = self._render_stack(base_img, layers_below)
|
|
503
|
+
|
|
504
|
+
# 2) Render selected layers on top of that "under" image
|
|
505
|
+
baked = self._render_stack(under, layers_sel)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
# 3) Create a baked raster layer (NO new document)
|
|
509
|
+
merged_layer = ImageLayer(
|
|
510
|
+
name=f"Merged ({len(layers_sel)})",
|
|
511
|
+
src_doc=None,
|
|
512
|
+
pixels=baked.astype(np.float32, copy=False),
|
|
513
|
+
visible=True,
|
|
514
|
+
opacity=1.0,
|
|
515
|
+
mode="Normal",
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
# Keep masks off by default; you can also decide to inherit the topmost mask
|
|
519
|
+
merged_layer.mask_doc = None
|
|
520
|
+
merged_layer.mask_use_luma = True
|
|
521
|
+
merged_layer.mask_invert = False
|
|
522
|
+
|
|
523
|
+
new_layers = layers_above + [merged_layer] + layers_below
|
|
524
|
+
vw._layers = new_layers
|
|
525
|
+
|
|
526
|
+
vw._reinstall_layer_watchers()
|
|
527
|
+
self._rebuild_list()
|
|
528
|
+
vw.apply_layer_stack(vw._layers)
|
|
529
|
+
|
|
530
|
+
QMessageBox.information(self, "Layers", f"Merged {len(layers_sel)} layers into a single layer.")
|
|
531
|
+
except Exception as ex:
|
|
532
|
+
print("[LayersDock] merge_selected error:", ex)
|
|
533
|
+
QMessageBox.critical(self, "Layers", f"Merge Selected failed:\n{ex}")
|
|
534
|
+
|
|
535
|
+
|
|
425
536
|
def _layer_count(self) -> int:
|
|
426
537
|
vw = self.current_view()
|
|
427
538
|
return len(getattr(vw, "_layers", [])) if vw else 0
|
|
@@ -712,3 +823,72 @@ class LayersDock(QDockWidget):
|
|
|
712
823
|
print("[LayersDock] merge error:", ex)
|
|
713
824
|
QMessageBox.critical(self, "Layers", f"Merge failed:\n{ex}")
|
|
714
825
|
|
|
826
|
+
def _merge_to_new_doc(self):
|
|
827
|
+
vw = self.current_view()
|
|
828
|
+
if not vw:
|
|
829
|
+
return
|
|
830
|
+
|
|
831
|
+
layers = list(getattr(vw, "_layers", []) or [])
|
|
832
|
+
if not layers:
|
|
833
|
+
QMessageBox.information(self, "Layers", "There are no layers to merge.")
|
|
834
|
+
return
|
|
835
|
+
|
|
836
|
+
try:
|
|
837
|
+
base_doc = getattr(vw, "document", None)
|
|
838
|
+
if base_doc is None or getattr(base_doc, "image", None) is None:
|
|
839
|
+
QMessageBox.warning(self, "Layers", "No base image available for this view.")
|
|
840
|
+
return
|
|
841
|
+
|
|
842
|
+
base_img = base_doc.image
|
|
843
|
+
merged = composite_stack(base_img, layers)
|
|
844
|
+
if merged is None:
|
|
845
|
+
QMessageBox.warning(self, "Layers", "Composite failed (empty result).")
|
|
846
|
+
return
|
|
847
|
+
|
|
848
|
+
# Push as a new document (same pattern as stars-only)
|
|
849
|
+
self._push_merged_as_new_doc(base_doc, merged)
|
|
850
|
+
|
|
851
|
+
QMessageBox.information(self, "Layers",
|
|
852
|
+
"Merged visible layers and created a new document.")
|
|
853
|
+
except Exception as ex:
|
|
854
|
+
print("[LayersDock] merge_to_new_doc error:", ex)
|
|
855
|
+
QMessageBox.critical(self, "Layers", f"Merge failed:\n{ex}")
|
|
856
|
+
|
|
857
|
+
def _push_merged_as_new_doc(self, base_doc, arr: np.ndarray):
|
|
858
|
+
dm = getattr(self.mw, "docman", None)
|
|
859
|
+
if not dm or not hasattr(dm, "open_array"):
|
|
860
|
+
return
|
|
861
|
+
|
|
862
|
+
# Derive a friendly title based on the *view title* if possible
|
|
863
|
+
title = None
|
|
864
|
+
try:
|
|
865
|
+
# Use current view title (respects per-view rename)
|
|
866
|
+
vw = self.current_view()
|
|
867
|
+
if vw and hasattr(vw, "_effective_title"):
|
|
868
|
+
base = (vw._effective_title() or "").strip()
|
|
869
|
+
else:
|
|
870
|
+
base = ""
|
|
871
|
+
|
|
872
|
+
if not base:
|
|
873
|
+
dn = getattr(base_doc, "display_name", None)
|
|
874
|
+
base = dn() if callable(dn) else (dn or "Untitled")
|
|
875
|
+
|
|
876
|
+
suffix = "_merged"
|
|
877
|
+
title = base if base.endswith(suffix) else f"{base}{suffix}"
|
|
878
|
+
except Exception:
|
|
879
|
+
title = "Merged Layers"
|
|
880
|
+
|
|
881
|
+
try:
|
|
882
|
+
meta = dict(getattr(base_doc, "metadata", {}) or {})
|
|
883
|
+
meta.update({
|
|
884
|
+
"bit_depth": "32-bit floating point",
|
|
885
|
+
"is_mono": (arr.ndim == 2 or (arr.ndim == 3 and arr.shape[-1] == 1)),
|
|
886
|
+
"source": "Layers Merge",
|
|
887
|
+
"step_name": "Layers Merge",
|
|
888
|
+
})
|
|
889
|
+
|
|
890
|
+
newdoc = dm.open_array(arr.astype(np.float32, copy=False), metadata=meta, title=title)
|
|
891
|
+
if hasattr(self.mw, "_spawn_subwindow_for"):
|
|
892
|
+
self.mw._spawn_subwindow_for(newdoc)
|
|
893
|
+
except Exception as ex:
|
|
894
|
+
print("[LayersDock] _push_merged_as_new_doc error:", ex)
|
|
@@ -2790,7 +2790,50 @@ def evaluate_polynomial(H: int, W: int, coeffs: np.ndarray, degree: int) -> np.n
|
|
|
2790
2790
|
A_full = build_poly_terms(xx.ravel(), yy.ravel(), degree)
|
|
2791
2791
|
return (A_full @ coeffs).reshape(H, W)
|
|
2792
2792
|
|
|
2793
|
+
@njit(parallel=True, fastmath=True)
|
|
2794
|
+
def numba_mono_from_img(img, bp, denom, median_rescaled, target_median):
|
|
2795
|
+
H, W = img.shape
|
|
2796
|
+
out = np.empty_like(img)
|
|
2797
|
+
for y in prange(H):
|
|
2798
|
+
for x in range(W):
|
|
2799
|
+
r = (img[y, x] - bp) / denom
|
|
2800
|
+
numer = (median_rescaled - 1.0) * target_median * r
|
|
2801
|
+
denom2 = median_rescaled * (target_median + r - 1.0) - target_median * r
|
|
2802
|
+
if abs(denom2) < 1e-12:
|
|
2803
|
+
denom2 = 1e-12
|
|
2804
|
+
out[y, x] = numer / denom2
|
|
2805
|
+
return out
|
|
2793
2806
|
|
|
2807
|
+
@njit(parallel=True, fastmath=True)
|
|
2808
|
+
def numba_color_linked_from_img(img, bp, denom, median_rescaled, target_median):
|
|
2809
|
+
H, W, C = img.shape
|
|
2810
|
+
out = np.empty_like(img)
|
|
2811
|
+
for y in prange(H):
|
|
2812
|
+
for x in range(W):
|
|
2813
|
+
for c in range(C):
|
|
2814
|
+
r = (img[y, x, c] - bp) / denom
|
|
2815
|
+
numer = (median_rescaled - 1.0) * target_median * r
|
|
2816
|
+
denom2 = median_rescaled * (target_median + r - 1.0) - target_median * r
|
|
2817
|
+
if abs(denom2) < 1e-12:
|
|
2818
|
+
denom2 = 1e-12
|
|
2819
|
+
out[y, x, c] = numer / denom2
|
|
2820
|
+
return out
|
|
2821
|
+
|
|
2822
|
+
@njit(parallel=True, fastmath=True)
|
|
2823
|
+
def numba_color_unlinked_from_img(img, bp3, denom3, meds_rescaled3, target_median):
|
|
2824
|
+
H, W, C = img.shape
|
|
2825
|
+
out = np.empty_like(img)
|
|
2826
|
+
for y in prange(H):
|
|
2827
|
+
for x in range(W):
|
|
2828
|
+
for c in range(C):
|
|
2829
|
+
r = (img[y, x, c] - bp3[c]) / denom3[c]
|
|
2830
|
+
med = meds_rescaled3[c]
|
|
2831
|
+
numer = (med - 1.0) * target_median * r
|
|
2832
|
+
denom2 = med * (target_median + r - 1.0) - target_median * r
|
|
2833
|
+
if abs(denom2) < 1e-12:
|
|
2834
|
+
denom2 = 1e-12
|
|
2835
|
+
out[y, x, c] = numer / denom2
|
|
2836
|
+
return out
|
|
2794
2837
|
|
|
2795
2838
|
@njit(parallel=True, fastmath=True)
|
|
2796
2839
|
def numba_mono_final_formula(rescaled, median_rescaled, target_median):
|