setiastrosuitepro 1.6.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.
Potentially problematic release.
This version of setiastrosuitepro might be problematic. Click here for more details.
- setiastro/__init__.py +2 -0
- setiastro/saspro/__init__.py +20 -0
- setiastro/saspro/__main__.py +784 -0
- setiastro/saspro/_generated/__init__.py +7 -0
- setiastro/saspro/_generated/build_info.py +2 -0
- setiastro/saspro/abe.py +1295 -0
- setiastro/saspro/abe_preset.py +196 -0
- setiastro/saspro/aberration_ai.py +694 -0
- setiastro/saspro/aberration_ai_preset.py +224 -0
- setiastro/saspro/accel_installer.py +218 -0
- setiastro/saspro/accel_workers.py +30 -0
- setiastro/saspro/add_stars.py +621 -0
- setiastro/saspro/astrobin_exporter.py +1007 -0
- setiastro/saspro/astrospike.py +153 -0
- setiastro/saspro/astrospike_python.py +1839 -0
- setiastro/saspro/autostretch.py +196 -0
- setiastro/saspro/backgroundneutral.py +560 -0
- setiastro/saspro/batch_convert.py +325 -0
- setiastro/saspro/batch_renamer.py +519 -0
- setiastro/saspro/blemish_blaster.py +488 -0
- setiastro/saspro/blink_comparator_pro.py +2923 -0
- setiastro/saspro/bundles.py +61 -0
- setiastro/saspro/bundles_dock.py +114 -0
- setiastro/saspro/cheat_sheet.py +168 -0
- setiastro/saspro/clahe.py +342 -0
- setiastro/saspro/comet_stacking.py +1377 -0
- setiastro/saspro/config.py +38 -0
- setiastro/saspro/config_bootstrap.py +40 -0
- setiastro/saspro/config_manager.py +316 -0
- setiastro/saspro/continuum_subtract.py +1617 -0
- setiastro/saspro/convo.py +1397 -0
- setiastro/saspro/convo_preset.py +414 -0
- setiastro/saspro/copyastro.py +187 -0
- setiastro/saspro/cosmicclarity.py +1564 -0
- setiastro/saspro/cosmicclarity_preset.py +407 -0
- setiastro/saspro/crop_dialog_pro.py +948 -0
- setiastro/saspro/crop_preset.py +189 -0
- setiastro/saspro/curve_editor_pro.py +2544 -0
- setiastro/saspro/curves_preset.py +375 -0
- setiastro/saspro/debayer.py +670 -0
- setiastro/saspro/debug_utils.py +29 -0
- setiastro/saspro/dnd_mime.py +35 -0
- setiastro/saspro/doc_manager.py +2634 -0
- setiastro/saspro/exoplanet_detector.py +2166 -0
- setiastro/saspro/file_utils.py +284 -0
- setiastro/saspro/fitsmodifier.py +744 -0
- setiastro/saspro/free_torch_memory.py +48 -0
- setiastro/saspro/frequency_separation.py +1343 -0
- setiastro/saspro/function_bundle.py +1594 -0
- setiastro/saspro/ghs_dialog_pro.py +660 -0
- setiastro/saspro/ghs_preset.py +284 -0
- setiastro/saspro/graxpert.py +634 -0
- setiastro/saspro/graxpert_preset.py +287 -0
- setiastro/saspro/gui/__init__.py +0 -0
- setiastro/saspro/gui/main_window.py +8494 -0
- setiastro/saspro/gui/mixins/__init__.py +33 -0
- setiastro/saspro/gui/mixins/dock_mixin.py +263 -0
- setiastro/saspro/gui/mixins/file_mixin.py +445 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
- setiastro/saspro/gui/mixins/header_mixin.py +441 -0
- setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
- setiastro/saspro/gui/mixins/menu_mixin.py +361 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +1324 -0
- setiastro/saspro/gui/mixins/update_mixin.py +309 -0
- setiastro/saspro/gui/mixins/view_mixin.py +435 -0
- setiastro/saspro/halobgon.py +462 -0
- setiastro/saspro/header_viewer.py +445 -0
- setiastro/saspro/headless_utils.py +88 -0
- setiastro/saspro/histogram.py +753 -0
- setiastro/saspro/history_explorer.py +939 -0
- setiastro/saspro/image_combine.py +414 -0
- setiastro/saspro/image_peeker_pro.py +1596 -0
- setiastro/saspro/imageops/__init__.py +37 -0
- setiastro/saspro/imageops/mdi_snap.py +292 -0
- setiastro/saspro/imageops/scnr.py +36 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
- setiastro/saspro/imageops/stretch.py +244 -0
- setiastro/saspro/isophote.py +1179 -0
- setiastro/saspro/layers.py +208 -0
- setiastro/saspro/layers_dock.py +714 -0
- setiastro/saspro/lazy_imports.py +193 -0
- setiastro/saspro/legacy/__init__.py +2 -0
- setiastro/saspro/legacy/image_manager.py +2226 -0
- setiastro/saspro/legacy/numba_utils.py +3659 -0
- setiastro/saspro/legacy/xisf.py +1071 -0
- setiastro/saspro/linear_fit.py +534 -0
- setiastro/saspro/live_stacking.py +1830 -0
- setiastro/saspro/log_bus.py +5 -0
- setiastro/saspro/logging_config.py +460 -0
- setiastro/saspro/luminancerecombine.py +309 -0
- setiastro/saspro/main_helpers.py +201 -0
- setiastro/saspro/mask_creation.py +928 -0
- setiastro/saspro/masks_core.py +56 -0
- setiastro/saspro/mdi_widgets.py +353 -0
- setiastro/saspro/memory_utils.py +666 -0
- setiastro/saspro/metadata_patcher.py +75 -0
- setiastro/saspro/mfdeconv.py +3826 -0
- setiastro/saspro/mfdeconv_earlystop.py +71 -0
- setiastro/saspro/mfdeconvcudnn.py +3263 -0
- setiastro/saspro/mfdeconvsport.py +2382 -0
- setiastro/saspro/minorbodycatalog.py +567 -0
- setiastro/saspro/morphology.py +382 -0
- setiastro/saspro/multiscale_decomp.py +1290 -0
- setiastro/saspro/nbtorgb_stars.py +531 -0
- setiastro/saspro/numba_utils.py +3044 -0
- setiastro/saspro/numba_warmup.py +141 -0
- setiastro/saspro/ops/__init__.py +9 -0
- setiastro/saspro/ops/command_help_dialog.py +623 -0
- setiastro/saspro/ops/command_runner.py +217 -0
- setiastro/saspro/ops/commands.py +1594 -0
- setiastro/saspro/ops/script_editor.py +1102 -0
- setiastro/saspro/ops/scripts.py +1413 -0
- setiastro/saspro/ops/settings.py +560 -0
- setiastro/saspro/parallel_utils.py +554 -0
- setiastro/saspro/pedestal.py +121 -0
- setiastro/saspro/perfect_palette_picker.py +1053 -0
- setiastro/saspro/pipeline.py +110 -0
- setiastro/saspro/pixelmath.py +1600 -0
- setiastro/saspro/plate_solver.py +2435 -0
- setiastro/saspro/project_io.py +797 -0
- setiastro/saspro/psf_utils.py +136 -0
- setiastro/saspro/psf_viewer.py +549 -0
- setiastro/saspro/pyi_rthook_astroquery.py +95 -0
- setiastro/saspro/remove_green.py +314 -0
- setiastro/saspro/remove_stars.py +1625 -0
- setiastro/saspro/remove_stars_preset.py +404 -0
- setiastro/saspro/resources.py +472 -0
- setiastro/saspro/rgb_combination.py +207 -0
- setiastro/saspro/rgb_extract.py +19 -0
- setiastro/saspro/rgbalign.py +723 -0
- setiastro/saspro/runtime_imports.py +7 -0
- setiastro/saspro/runtime_torch.py +754 -0
- setiastro/saspro/save_options.py +72 -0
- setiastro/saspro/selective_color.py +1552 -0
- setiastro/saspro/sfcc.py +1425 -0
- setiastro/saspro/shortcuts.py +2807 -0
- setiastro/saspro/signature_insert.py +1099 -0
- setiastro/saspro/stacking_suite.py +17712 -0
- setiastro/saspro/star_alignment.py +7420 -0
- setiastro/saspro/star_alignment_preset.py +329 -0
- setiastro/saspro/star_metrics.py +49 -0
- setiastro/saspro/star_spikes.py +681 -0
- setiastro/saspro/star_stretch.py +470 -0
- setiastro/saspro/stat_stretch.py +502 -0
- setiastro/saspro/status_log_dock.py +78 -0
- setiastro/saspro/subwindow.py +3267 -0
- setiastro/saspro/supernovaasteroidhunter.py +1712 -0
- setiastro/saspro/swap_manager.py +99 -0
- setiastro/saspro/torch_backend.py +89 -0
- setiastro/saspro/torch_rejection.py +434 -0
- setiastro/saspro/view_bundle.py +1555 -0
- setiastro/saspro/wavescale_hdr.py +624 -0
- setiastro/saspro/wavescale_hdr_preset.py +100 -0
- setiastro/saspro/wavescalede.py +657 -0
- setiastro/saspro/wavescalede_preset.py +228 -0
- setiastro/saspro/wcs_update.py +374 -0
- setiastro/saspro/whitebalance.py +456 -0
- setiastro/saspro/widgets/__init__.py +48 -0
- setiastro/saspro/widgets/common_utilities.py +305 -0
- setiastro/saspro/widgets/graphics_views.py +122 -0
- setiastro/saspro/widgets/image_utils.py +518 -0
- setiastro/saspro/widgets/preview_dialogs.py +280 -0
- setiastro/saspro/widgets/spinboxes.py +275 -0
- setiastro/saspro/widgets/themed_buttons.py +13 -0
- setiastro/saspro/widgets/wavelet_utils.py +299 -0
- setiastro/saspro/window_shelf.py +185 -0
- setiastro/saspro/xisf.py +1123 -0
- setiastrosuitepro-1.6.0.dist-info/METADATA +266 -0
- setiastrosuitepro-1.6.0.dist-info/RECORD +174 -0
- setiastrosuitepro-1.6.0.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.0.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.0.dist-info/licenses/LICENSE +674 -0
- setiastrosuitepro-1.6.0.dist-info/licenses/license.txt +2580 -0
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
# pro/add_stars.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import os
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
# Qt
|
|
7
|
+
from PyQt6.QtCore import Qt, pyqtSignal, QTimer
|
|
8
|
+
from PyQt6.QtGui import QImage, QPixmap, QWheelEvent
|
|
9
|
+
from PyQt6.QtWidgets import (
|
|
10
|
+
QDialog, QVBoxLayout, QHBoxLayout, QGridLayout,
|
|
11
|
+
QLabel, QPushButton, QScrollArea, QSizePolicy,
|
|
12
|
+
QComboBox, QSlider, QMessageBox, QFileDialog, QFormLayout
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# I/O (use your legacy functions)
|
|
16
|
+
from setiastro.saspro.legacy.image_manager import load_image
|
|
17
|
+
from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import cv2
|
|
22
|
+
except Exception:
|
|
23
|
+
cv2 = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
# Helpers to enumerate docs and masks from the Pro app
|
|
28
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
# REPLACE OLD _iter_open_docs WITH THIS
|
|
30
|
+
def _iter_open_docs(main):
|
|
31
|
+
"""
|
|
32
|
+
Find open views/docs by:
|
|
33
|
+
1) docman.{documents|docs|open_docs|views|iter_docs|all_docs}
|
|
34
|
+
2) Any attribute on main that has subWindowList() (QMdiArea), pulling docs
|
|
35
|
+
from subwindow.widget().{doc,_doc,document} or the widget itself if it
|
|
36
|
+
exposes an image.
|
|
37
|
+
Returns a list of (label, provider) where provider can be a doc or widget.
|
|
38
|
+
"""
|
|
39
|
+
def _label_for(obj, fallback):
|
|
40
|
+
name = ""
|
|
41
|
+
try:
|
|
42
|
+
if hasattr(obj, "display_name") and callable(obj.display_name):
|
|
43
|
+
name = obj.display_name()
|
|
44
|
+
else:
|
|
45
|
+
name = getattr(obj, "name", "") or ""
|
|
46
|
+
except Exception:
|
|
47
|
+
name = ""
|
|
48
|
+
return name or fallback or f"View {len(items)}"
|
|
49
|
+
|
|
50
|
+
def _image_from_any(x):
|
|
51
|
+
"""Robustly get a numpy-ish image from doc/widget."""
|
|
52
|
+
if x is None:
|
|
53
|
+
return None
|
|
54
|
+
chain = [x, getattr(x, "doc", None), getattr(x, "_doc", None), getattr(x, "document", None)]
|
|
55
|
+
for c in chain:
|
|
56
|
+
if c is None:
|
|
57
|
+
continue
|
|
58
|
+
img = getattr(c, "image", None)
|
|
59
|
+
if img is not None:
|
|
60
|
+
try:
|
|
61
|
+
a = np.asarray(img)
|
|
62
|
+
if a is not None and a.size:
|
|
63
|
+
return a
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
# method fallbacks
|
|
67
|
+
for m in ("get_image", "current_image", "image_array"):
|
|
68
|
+
f = getattr(c, m, None)
|
|
69
|
+
if callable(f):
|
|
70
|
+
try:
|
|
71
|
+
a = f()
|
|
72
|
+
a = np.asarray(a) if a is not None else None
|
|
73
|
+
if a is not None and a.size:
|
|
74
|
+
return a
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
def _add_item(obj, label_hint=None):
|
|
80
|
+
img = _image_from_any(obj)
|
|
81
|
+
if img is None:
|
|
82
|
+
return
|
|
83
|
+
key = id(getattr(obj, "image", obj)) # stable-ish identity
|
|
84
|
+
if key in seen:
|
|
85
|
+
return
|
|
86
|
+
seen.add(key)
|
|
87
|
+
items.append((_label_for(obj, label_hint), obj))
|
|
88
|
+
|
|
89
|
+
items, seen = [], set()
|
|
90
|
+
|
|
91
|
+
# 1) docman sources
|
|
92
|
+
dm = getattr(main, "docman", None)
|
|
93
|
+
if dm is not None:
|
|
94
|
+
for attr in ("documents", "docs", "open_docs", "views"):
|
|
95
|
+
coll = getattr(dm, attr, None)
|
|
96
|
+
if isinstance(coll, dict):
|
|
97
|
+
for d in coll.values():
|
|
98
|
+
_add_item(d)
|
|
99
|
+
elif isinstance(coll, (list, tuple, set)):
|
|
100
|
+
for d in coll:
|
|
101
|
+
_add_item(d)
|
|
102
|
+
for meth in ("iter_docs", "all_docs", "iter"):
|
|
103
|
+
fn = getattr(dm, meth, None)
|
|
104
|
+
if callable(fn):
|
|
105
|
+
try:
|
|
106
|
+
for d in fn():
|
|
107
|
+
_add_item(d)
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
# 2) any QMdiArea on main
|
|
112
|
+
for attr in dir(main):
|
|
113
|
+
try:
|
|
114
|
+
val = getattr(main, attr)
|
|
115
|
+
except Exception:
|
|
116
|
+
continue
|
|
117
|
+
if hasattr(val, "subWindowList"):
|
|
118
|
+
try:
|
|
119
|
+
for sw in val.subWindowList():
|
|
120
|
+
title = ""
|
|
121
|
+
try:
|
|
122
|
+
title = sw.windowTitle()
|
|
123
|
+
except Exception:
|
|
124
|
+
pass
|
|
125
|
+
w = None
|
|
126
|
+
try:
|
|
127
|
+
w = sw.widget()
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
130
|
+
# prefer an actual doc if present; fallback to widget
|
|
131
|
+
for candidate in (
|
|
132
|
+
getattr(w, "doc", None),
|
|
133
|
+
getattr(w, "_doc", None),
|
|
134
|
+
getattr(w, "document", None),
|
|
135
|
+
w,
|
|
136
|
+
):
|
|
137
|
+
if candidate is None:
|
|
138
|
+
continue
|
|
139
|
+
if _image_from_any(candidate) is not None:
|
|
140
|
+
_add_item(candidate, label_hint=title)
|
|
141
|
+
break
|
|
142
|
+
except Exception:
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
return items
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _doc_image(doc_like) -> np.ndarray | None:
|
|
150
|
+
"""
|
|
151
|
+
Accepts a doc or a view widget and returns a float32 image array
|
|
152
|
+
(mono 2D or RGB 3D). No boolean ops on arrays to avoid ambiguity.
|
|
153
|
+
"""
|
|
154
|
+
def _grab(x):
|
|
155
|
+
if x is None:
|
|
156
|
+
return None
|
|
157
|
+
# direct attribute
|
|
158
|
+
img = getattr(x, "image", None)
|
|
159
|
+
if img is not None:
|
|
160
|
+
return img
|
|
161
|
+
# method fallbacks
|
|
162
|
+
for m in ("get_image", "current_image", "image_array"):
|
|
163
|
+
fn = getattr(x, m, None)
|
|
164
|
+
if callable(fn):
|
|
165
|
+
try:
|
|
166
|
+
return fn()
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
img = _grab(doc_like)
|
|
172
|
+
if img is None:
|
|
173
|
+
img = _grab(getattr(doc_like, "doc", None))
|
|
174
|
+
if img is None:
|
|
175
|
+
img = _grab(getattr(doc_like, "_doc", None))
|
|
176
|
+
if img is None:
|
|
177
|
+
img = _grab(getattr(doc_like, "document", None))
|
|
178
|
+
if img is None:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
a = np.asarray(img).astype(np.float32, copy=False)
|
|
182
|
+
if a.ndim == 3 and a.shape[2] == 1:
|
|
183
|
+
a = a[..., 0]
|
|
184
|
+
# Defensive normalization for big float ranges
|
|
185
|
+
if a.dtype.kind == "f" and a.size:
|
|
186
|
+
mx = float(a.max())
|
|
187
|
+
if mx > 5.0:
|
|
188
|
+
a = a / mx
|
|
189
|
+
return a
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _active_mask_array_from_doc(doc) -> np.ndarray | None:
|
|
195
|
+
"""
|
|
196
|
+
Return active mask (H,W) float32 in [0,1] from the document, if present.
|
|
197
|
+
"""
|
|
198
|
+
try:
|
|
199
|
+
mid = getattr(doc, "active_mask_id", None)
|
|
200
|
+
if not mid:
|
|
201
|
+
return None
|
|
202
|
+
masks = getattr(doc, "masks", {}) or {}
|
|
203
|
+
layer = masks.get(mid)
|
|
204
|
+
data = getattr(layer, "data", None) if layer is not None else None
|
|
205
|
+
if data is None:
|
|
206
|
+
return None
|
|
207
|
+
a = np.asarray(data)
|
|
208
|
+
if a.ndim == 3:
|
|
209
|
+
if cv2 is not None:
|
|
210
|
+
a = cv2.cvtColor(a, cv2.COLOR_BGR2GRAY)
|
|
211
|
+
else:
|
|
212
|
+
a = a.mean(axis=2)
|
|
213
|
+
a = a.astype(np.float32, copy=False)
|
|
214
|
+
a = np.clip(a, 0.0, 1.0)
|
|
215
|
+
return a
|
|
216
|
+
except Exception:
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
221
|
+
# Dialog
|
|
222
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
223
|
+
class AddStarsDialog(QDialog):
|
|
224
|
+
stars_added = pyqtSignal(object, object)
|
|
225
|
+
def __init__(self, main, parent=None):
|
|
226
|
+
super().__init__(parent)
|
|
227
|
+
self.setWindowTitle("Add Stars to Image")
|
|
228
|
+
|
|
229
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
230
|
+
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
|
231
|
+
self.setModal(False)
|
|
232
|
+
#self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
233
|
+
|
|
234
|
+
self.main = main
|
|
235
|
+
self.starless = None
|
|
236
|
+
self.stars_only = None
|
|
237
|
+
self.blended_image = None
|
|
238
|
+
self.scale_factor = 1.0
|
|
239
|
+
self._fit_once = False
|
|
240
|
+
|
|
241
|
+
self._build_ui()
|
|
242
|
+
self._populate_doc_combos()
|
|
243
|
+
|
|
244
|
+
# UI -----------------------------------------------------------------------
|
|
245
|
+
def _build_ui(self):
|
|
246
|
+
layout = QVBoxLayout(self)
|
|
247
|
+
|
|
248
|
+
# Preview
|
|
249
|
+
self.preview_label = QLabel()
|
|
250
|
+
self.preview_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
251
|
+
self.preview_label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored)
|
|
252
|
+
self.preview_label.setScaledContents(False)
|
|
253
|
+
|
|
254
|
+
self.scroll_area = QScrollArea(self)
|
|
255
|
+
self.scroll_area.setWidgetResizable(False)
|
|
256
|
+
self.scroll_area.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
257
|
+
self.scroll_area.setWidget(self.preview_label)
|
|
258
|
+
layout.addWidget(self.scroll_area)
|
|
259
|
+
|
|
260
|
+
# Zoom row (standardized themed toolbuttons)
|
|
261
|
+
zrow = QHBoxLayout()
|
|
262
|
+
self.btn_zoom_in = themed_toolbtn("zoom-in", "Zoom In")
|
|
263
|
+
self.btn_zoom_out = themed_toolbtn("zoom-out", "Zoom Out")
|
|
264
|
+
self.btn_fit = themed_toolbtn("zoom-fit-best", "Fit to Preview")
|
|
265
|
+
|
|
266
|
+
self.btn_zoom_in.clicked.connect(self.zoom_in)
|
|
267
|
+
self.btn_zoom_out.clicked.connect(self.zoom_out)
|
|
268
|
+
self.btn_fit.clicked.connect(self.fit_to_preview)
|
|
269
|
+
|
|
270
|
+
zrow.addWidget(self.btn_zoom_in)
|
|
271
|
+
zrow.addWidget(self.btn_zoom_out)
|
|
272
|
+
zrow.addWidget(self.btn_fit)
|
|
273
|
+
zrow.addStretch(1)
|
|
274
|
+
layout.addLayout(zrow)
|
|
275
|
+
|
|
276
|
+
# Selection + blend
|
|
277
|
+
grid = QGridLayout()
|
|
278
|
+
|
|
279
|
+
# Blend type
|
|
280
|
+
grid.addWidget(QLabel("Blend Type:"), 0, 0)
|
|
281
|
+
self.cmb_blend = QComboBox(); self.cmb_blend.addItems(["Screen", "Add"])
|
|
282
|
+
self.cmb_blend.currentIndexChanged.connect(self.update_preview)
|
|
283
|
+
grid.addWidget(self.cmb_blend, 0, 1)
|
|
284
|
+
|
|
285
|
+
# Starless source
|
|
286
|
+
grid.addWidget(QLabel("Starless View:"), 1, 0)
|
|
287
|
+
self.cmb_starless = QComboBox(); grid.addWidget(self.cmb_starless, 1, 1)
|
|
288
|
+
btn_sless_file = QPushButton("Load from File"); btn_sless_file.clicked.connect(lambda: self._load_from_file('starless'))
|
|
289
|
+
grid.addWidget(btn_sless_file, 1, 2)
|
|
290
|
+
|
|
291
|
+
# Stars-only source
|
|
292
|
+
grid.addWidget(QLabel("Stars-Only View:"), 2, 0)
|
|
293
|
+
self.cmb_stars = QComboBox(); grid.addWidget(self.cmb_stars, 2, 1)
|
|
294
|
+
btn_stars_file = QPushButton("Load from File"); btn_stars_file.clicked.connect(lambda: self._load_from_file('stars'))
|
|
295
|
+
grid.addWidget(btn_stars_file, 2, 2)
|
|
296
|
+
|
|
297
|
+
layout.addLayout(grid)
|
|
298
|
+
|
|
299
|
+
refresh_row = QHBoxLayout()
|
|
300
|
+
btn_refresh = QPushButton("Refresh Views")
|
|
301
|
+
btn_refresh.clicked.connect(self._populate_doc_combos)
|
|
302
|
+
refresh_row.addStretch(1)
|
|
303
|
+
refresh_row.addWidget(btn_refresh)
|
|
304
|
+
layout.addLayout(refresh_row)
|
|
305
|
+
|
|
306
|
+
# Ratio slider
|
|
307
|
+
row = QHBoxLayout()
|
|
308
|
+
row.addWidget(QLabel("Blend Ratio (Screen/Add Intensity):"))
|
|
309
|
+
self.slider_ratio = QSlider(Qt.Orientation.Horizontal)
|
|
310
|
+
self.slider_ratio.setRange(0, 100); self.slider_ratio.setValue(100)
|
|
311
|
+
self.slider_ratio.setTickInterval(10); self.slider_ratio.setTickPosition(QSlider.TickPosition.TicksBelow)
|
|
312
|
+
self.slider_ratio.valueChanged.connect(self.update_preview)
|
|
313
|
+
row.addWidget(self.slider_ratio)
|
|
314
|
+
layout.addLayout(row)
|
|
315
|
+
|
|
316
|
+
# Buttons
|
|
317
|
+
brow = QHBoxLayout(); brow.addStretch(1)
|
|
318
|
+
btn_apply = QPushButton("Apply"); btn_apply.clicked.connect(self._apply)
|
|
319
|
+
btn_cancel= QPushButton("Cancel"); btn_cancel.clicked.connect(self.reject)
|
|
320
|
+
brow.addWidget(btn_apply); brow.addWidget(btn_cancel)
|
|
321
|
+
layout.addLayout(brow)
|
|
322
|
+
|
|
323
|
+
self.setMinimumSize(900, 650)
|
|
324
|
+
|
|
325
|
+
# signals for combos
|
|
326
|
+
self.cmb_starless.currentIndexChanged.connect(self._pick_starless_from_combo)
|
|
327
|
+
self.cmb_stars.currentIndexChanged.connect(self._pick_stars_from_combo)
|
|
328
|
+
|
|
329
|
+
# Populate combos with open docs (+ sentinel for file)
|
|
330
|
+
def _populate_doc_combos(self):
|
|
331
|
+
items = [("Select View", None)]
|
|
332
|
+
for name, d in _iter_open_docs(self.main):
|
|
333
|
+
items.append((name, d))
|
|
334
|
+
items.append(("Load from File", "file"))
|
|
335
|
+
|
|
336
|
+
self.cmb_starless.clear()
|
|
337
|
+
self.cmb_stars.clear()
|
|
338
|
+
for label, data in items:
|
|
339
|
+
self.cmb_starless.addItem(label, data)
|
|
340
|
+
self.cmb_stars.addItem(label, data)
|
|
341
|
+
|
|
342
|
+
# File load ----------------------------------------------------------------
|
|
343
|
+
def _load_from_file(self, which: str):
|
|
344
|
+
fn, _ = QFileDialog.getOpenFileName(
|
|
345
|
+
self, f"Select {'Starless' if which=='starless' else 'Stars-Only'} Image", "",
|
|
346
|
+
"Image Files (*.png *.tif *.tiff *.fits *.fit *.xisf *.jpg *.jpeg)"
|
|
347
|
+
)
|
|
348
|
+
if not fn:
|
|
349
|
+
return
|
|
350
|
+
img, _, _, _ = load_image(fn)
|
|
351
|
+
if img is None:
|
|
352
|
+
QMessageBox.critical(self, "Load Error", f"Failed to load: {os.path.basename(fn)}")
|
|
353
|
+
return
|
|
354
|
+
if which == 'starless':
|
|
355
|
+
self.starless = self._to_rgb01(img)
|
|
356
|
+
self.cmb_starless.setCurrentIndex(self.cmb_starless.count()-1) # "Load from File"
|
|
357
|
+
else:
|
|
358
|
+
self.stars_only = self._to_rgb01(img)
|
|
359
|
+
self.cmb_stars.setCurrentIndex(self.cmb_stars.count()-1)
|
|
360
|
+
self.update_preview()
|
|
361
|
+
|
|
362
|
+
@staticmethod
|
|
363
|
+
def _resolve_doc_object(doc_like):
|
|
364
|
+
if doc_like is None:
|
|
365
|
+
return None
|
|
366
|
+
for c in (doc_like,
|
|
367
|
+
getattr(doc_like, "doc", None),
|
|
368
|
+
getattr(doc_like, "_doc", None),
|
|
369
|
+
getattr(doc_like, "document", None)):
|
|
370
|
+
if c is None:
|
|
371
|
+
continue
|
|
372
|
+
if hasattr(c, "apply_edit") and any(
|
|
373
|
+
hasattr(c, a) for a in ("image", "get_image", "current_image", "image_array")
|
|
374
|
+
):
|
|
375
|
+
return c
|
|
376
|
+
return None
|
|
377
|
+
|
|
378
|
+
def _target_doc_for_mask(self):
|
|
379
|
+
"""Use the selected Starless View's doc (fallback to active doc)."""
|
|
380
|
+
sel = self.cmb_starless.currentData()
|
|
381
|
+
if sel is None or sel == "file":
|
|
382
|
+
doc = getattr(self.main, "_active_doc", None)
|
|
383
|
+
if callable(doc): doc = doc()
|
|
384
|
+
return self._resolve_doc_object(doc)
|
|
385
|
+
return self._resolve_doc_object(sel)
|
|
386
|
+
|
|
387
|
+
# Combo selects ------------------------------------------------------------
|
|
388
|
+
def _pick_starless_from_combo(self):
|
|
389
|
+
data = self.cmb_starless.currentData()
|
|
390
|
+
if data is None or data == "file":
|
|
391
|
+
# None or "Load from File" (the button sets image)
|
|
392
|
+
self.update_preview()
|
|
393
|
+
return
|
|
394
|
+
img = _doc_image(data)
|
|
395
|
+
if img is None:
|
|
396
|
+
QMessageBox.warning(self, "Empty View", "Selected starless view has no image.")
|
|
397
|
+
return
|
|
398
|
+
self.starless = self._to_rgb01(img)
|
|
399
|
+
self.update_preview()
|
|
400
|
+
|
|
401
|
+
def _pick_stars_from_combo(self):
|
|
402
|
+
data = self.cmb_stars.currentData()
|
|
403
|
+
if data is None or data == "file":
|
|
404
|
+
self.update_preview()
|
|
405
|
+
return
|
|
406
|
+
img = _doc_image(data)
|
|
407
|
+
if img is None:
|
|
408
|
+
QMessageBox.warning(self, "Empty View", "Selected stars-only view has no image.")
|
|
409
|
+
return
|
|
410
|
+
self.stars_only = self._to_rgb01(img)
|
|
411
|
+
self.update_preview()
|
|
412
|
+
|
|
413
|
+
# Math ---------------------------------------------------------------------
|
|
414
|
+
@staticmethod
|
|
415
|
+
def _to_rgb01(a: np.ndarray) -> np.ndarray:
|
|
416
|
+
a = np.asarray(a).astype(np.float32, copy=False)
|
|
417
|
+
if a.ndim == 2:
|
|
418
|
+
a = np.stack([a]*3, axis=-1)
|
|
419
|
+
elif a.ndim == 3 and a.shape[2] == 1:
|
|
420
|
+
a = np.repeat(a, 3, axis=2)
|
|
421
|
+
a = np.clip(a, 0.0, 1.0)
|
|
422
|
+
return a
|
|
423
|
+
|
|
424
|
+
def _blend_images(self) -> np.ndarray | None:
|
|
425
|
+
if self.starless is None or self.stars_only is None:
|
|
426
|
+
return None
|
|
427
|
+
|
|
428
|
+
# same size?
|
|
429
|
+
if self.starless.shape != self.stars_only.shape:
|
|
430
|
+
QMessageBox.critical(self, "Size Mismatch", "Starless and Stars-Only views are different sizes.")
|
|
431
|
+
return None
|
|
432
|
+
|
|
433
|
+
mode = self.cmb_blend.currentText()
|
|
434
|
+
r = self.slider_ratio.value() / 100.0
|
|
435
|
+
|
|
436
|
+
if mode == "Screen":
|
|
437
|
+
base = self.starless + self.stars_only - (self.starless * self.stars_only)
|
|
438
|
+
else:
|
|
439
|
+
base = self.starless + self.stars_only
|
|
440
|
+
|
|
441
|
+
blended = (1.0 - r) * self.starless + r * base
|
|
442
|
+
blended = np.clip(blended, 0.0, 1.0)
|
|
443
|
+
|
|
444
|
+
# mask from the *destination* doc (selected Starless View)
|
|
445
|
+
tgt = self._target_doc_for_mask()
|
|
446
|
+
if tgt is not None:
|
|
447
|
+
m = _active_mask_array_from_doc(tgt)
|
|
448
|
+
if m is not None:
|
|
449
|
+
h, w = blended.shape[:2]
|
|
450
|
+
if m.shape != (h, w):
|
|
451
|
+
if cv2 is not None:
|
|
452
|
+
m = cv2.resize(m, (w, h), interpolation=cv2.INTER_NEAREST)
|
|
453
|
+
else:
|
|
454
|
+
yi = (np.linspace(0, m.shape[0]-1, h)).astype(np.int32)
|
|
455
|
+
xi = (np.linspace(0, m.shape[1]-1, w)).astype(np.int32)
|
|
456
|
+
m = m[yi][:, xi]
|
|
457
|
+
m3 = np.repeat(m[:, :, None], 3, axis=2)
|
|
458
|
+
# only replace where mask==1; keep original starless elsewhere
|
|
459
|
+
blended = np.clip(self.starless * (1.0 - m3) + blended * m3, 0.0, 1.0).astype(np.float32, copy=False)
|
|
460
|
+
|
|
461
|
+
return blended
|
|
462
|
+
|
|
463
|
+
# Preview ------------------------------------------------------------------
|
|
464
|
+
def update_preview(self):
|
|
465
|
+
out = self._blend_images()
|
|
466
|
+
self.blended_image = out
|
|
467
|
+
if out is None:
|
|
468
|
+
self.preview_label.clear()
|
|
469
|
+
return
|
|
470
|
+
|
|
471
|
+
pix = self._to_pixmap(out)
|
|
472
|
+
# keep scroll position
|
|
473
|
+
hs = self.scroll_area.horizontalScrollBar().value()
|
|
474
|
+
vs = self.scroll_area.verticalScrollBar().value()
|
|
475
|
+
|
|
476
|
+
scaled = pix.scaled(
|
|
477
|
+
pix.size() * self.scale_factor,
|
|
478
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
|
479
|
+
Qt.TransformationMode.SmoothTransformation
|
|
480
|
+
)
|
|
481
|
+
self.preview_label.setPixmap(scaled)
|
|
482
|
+
self.preview_label.adjustSize()
|
|
483
|
+
self.scroll_area.horizontalScrollBar().setValue(hs)
|
|
484
|
+
self.scroll_area.verticalScrollBar().setValue(vs)
|
|
485
|
+
|
|
486
|
+
def _to_pixmap(self, img: np.ndarray) -> QPixmap:
|
|
487
|
+
im = np.clip(img, 0.0, 1.0)
|
|
488
|
+
u8 = (im * 255.0 + 0.5).astype(np.uint8)
|
|
489
|
+
if u8.ndim == 2:
|
|
490
|
+
q = QImage(u8.data, u8.shape[1], u8.shape[0], u8.strides[0], QImage.Format.Format_Grayscale8)
|
|
491
|
+
else:
|
|
492
|
+
# RGB888
|
|
493
|
+
q = QImage(u8.data, u8.shape[1], u8.shape[0], u8.strides[0], QImage.Format.Format_RGB888)
|
|
494
|
+
return QPixmap.fromImage(q)
|
|
495
|
+
|
|
496
|
+
# Zoom/fit -----------------------------------------------------------------
|
|
497
|
+
def wheelEvent(self, ev: QWheelEvent):
|
|
498
|
+
if ev.angleDelta().y() > 0:
|
|
499
|
+
self.zoom_in()
|
|
500
|
+
else:
|
|
501
|
+
self.zoom_out()
|
|
502
|
+
ev.accept()
|
|
503
|
+
|
|
504
|
+
def zoom_in(self):
|
|
505
|
+
self.scale_factor *= 1.25
|
|
506
|
+
self._refresh_scaled()
|
|
507
|
+
|
|
508
|
+
def zoom_out(self):
|
|
509
|
+
self.scale_factor /= 1.25
|
|
510
|
+
self._refresh_scaled()
|
|
511
|
+
|
|
512
|
+
def fit_to_preview(self):
|
|
513
|
+
if self.blended_image is None:
|
|
514
|
+
return
|
|
515
|
+
QTimer.singleShot(0, self._do_fit)
|
|
516
|
+
|
|
517
|
+
def _do_fit(self):
|
|
518
|
+
if self.blended_image is None:
|
|
519
|
+
return
|
|
520
|
+
pix = self._to_pixmap(self.blended_image)
|
|
521
|
+
vsz = self.scroll_area.viewport().size()
|
|
522
|
+
if pix.isNull() or pix.width() == 0 or pix.height() == 0:
|
|
523
|
+
return
|
|
524
|
+
sw = vsz.width() / pix.width()
|
|
525
|
+
sh = vsz.height() / pix.height()
|
|
526
|
+
self.scale_factor = min(sw, sh)
|
|
527
|
+
self.update_preview()
|
|
528
|
+
|
|
529
|
+
def _refresh_scaled(self):
|
|
530
|
+
if self.blended_image is None:
|
|
531
|
+
return
|
|
532
|
+
pix = self._to_pixmap(self.blended_image)
|
|
533
|
+
scaled = pix.scaled(
|
|
534
|
+
pix.size() * self.scale_factor,
|
|
535
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
|
536
|
+
Qt.TransformationMode.SmoothTransformation
|
|
537
|
+
)
|
|
538
|
+
self.preview_label.setPixmap(scaled)
|
|
539
|
+
self.preview_label.adjustSize()
|
|
540
|
+
|
|
541
|
+
# Apply --------------------------------------------------------------------
|
|
542
|
+
def _apply(self):
|
|
543
|
+
"""
|
|
544
|
+
Applies the blended image to the selected *Starless View* (or, if the starless
|
|
545
|
+
source is "Load from File", falls back to the active doc).
|
|
546
|
+
"""
|
|
547
|
+
if self.blended_image is None:
|
|
548
|
+
QMessageBox.warning(self, "No Blend", "No blended image to apply.")
|
|
549
|
+
return
|
|
550
|
+
|
|
551
|
+
sel = self.cmb_starless.currentData()
|
|
552
|
+
target_doc = None
|
|
553
|
+
|
|
554
|
+
if sel is None: # "Select View"
|
|
555
|
+
# Fallback: active doc
|
|
556
|
+
doc = getattr(self.main, "_active_doc", None)
|
|
557
|
+
if callable(doc):
|
|
558
|
+
doc = doc()
|
|
559
|
+
target_doc = self._resolve_doc_object(doc)
|
|
560
|
+
elif sel == "file":
|
|
561
|
+
# Starless came from a file; no view to overwrite → fallback to active
|
|
562
|
+
doc = getattr(self.main, "_active_doc", None)
|
|
563
|
+
if callable(doc):
|
|
564
|
+
doc = doc()
|
|
565
|
+
target_doc = self._resolve_doc_object(doc)
|
|
566
|
+
else:
|
|
567
|
+
# A real view/doc was chosen
|
|
568
|
+
target_doc = self._resolve_doc_object(sel)
|
|
569
|
+
|
|
570
|
+
if target_doc is None:
|
|
571
|
+
QMessageBox.warning(self, "No Target",
|
|
572
|
+
"Pick a starless view to overwrite (or activate a destination window).")
|
|
573
|
+
return
|
|
574
|
+
|
|
575
|
+
# Emit (target_doc, blended_image)
|
|
576
|
+
self.stars_added.emit(target_doc, self.blended_image.astype(np.float32, copy=False))
|
|
577
|
+
self.accept()
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
# Ensure initial fit once shown
|
|
581
|
+
def showEvent(self, ev):
|
|
582
|
+
super().showEvent(ev)
|
|
583
|
+
# repopulate in case windows opened after dialog construction
|
|
584
|
+
self._populate_doc_combos()
|
|
585
|
+
if not self._fit_once:
|
|
586
|
+
self._fit_once = True
|
|
587
|
+
QTimer.singleShot(0, self.fit_to_preview)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
591
|
+
# Public entry point: open dialog, then apply to active doc
|
|
592
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
593
|
+
def add_stars(main):
|
|
594
|
+
doc = getattr(main, "_active_doc", None)
|
|
595
|
+
if callable(doc):
|
|
596
|
+
doc = doc()
|
|
597
|
+
if doc is None or getattr(doc, "image", None) is None:
|
|
598
|
+
QMessageBox.warning(main, "No Image", "Please activate a destination image window first.")
|
|
599
|
+
return
|
|
600
|
+
|
|
601
|
+
dlg = AddStarsDialog(main, parent=main)
|
|
602
|
+
dlg.stars_added.connect(lambda target, arr: _apply_to_doc(main, target, arr))
|
|
603
|
+
dlg.exec()
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _apply_to_doc(main, doc, arr: np.ndarray):
|
|
607
|
+
"""Overwrite the given document with the blended (stars added) result."""
|
|
608
|
+
if doc is None:
|
|
609
|
+
QMessageBox.warning(main, "No Target Document", "No document to apply to.")
|
|
610
|
+
return
|
|
611
|
+
try:
|
|
612
|
+
meta = {
|
|
613
|
+
"step_name": "Stars Added",
|
|
614
|
+
"bit_depth": "32-bit floating point",
|
|
615
|
+
"is_mono": (arr.ndim == 2),
|
|
616
|
+
}
|
|
617
|
+
doc.apply_edit(arr.astype(np.float32, copy=False), metadata=meta, step_name="Stars Added")
|
|
618
|
+
if hasattr(main, "_log"):
|
|
619
|
+
main._log("Stars Added")
|
|
620
|
+
except Exception as e:
|
|
621
|
+
QMessageBox.critical(main, "Add Stars", f"Failed to apply result:\n{e}")
|