setiastrosuitepro 1.6.4__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/acv_icon.png +0 -0
- setiastro/images/cosmic.svg +40 -0
- setiastro/images/cosmicsat.svg +24 -0
- setiastro/images/first_quarter.png +0 -0
- setiastro/images/full_moon.png +0 -0
- setiastro/images/graxpert.svg +19 -0
- setiastro/images/last_quarter.png +0 -0
- setiastro/images/linearfit.svg +32 -0
- setiastro/images/new_moon.png +0 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/images/waning_crescent_1.png +0 -0
- setiastro/images/waning_crescent_2.png +0 -0
- setiastro/images/waning_crescent_3.png +0 -0
- setiastro/images/waning_crescent_4.png +0 -0
- setiastro/images/waning_crescent_5.png +0 -0
- setiastro/images/waning_gibbous_1.png +0 -0
- setiastro/images/waning_gibbous_2.png +0 -0
- setiastro/images/waning_gibbous_3.png +0 -0
- setiastro/images/waning_gibbous_4.png +0 -0
- setiastro/images/waning_gibbous_5.png +0 -0
- setiastro/images/waxing_crescent_1.png +0 -0
- setiastro/images/waxing_crescent_2.png +0 -0
- setiastro/images/waxing_crescent_3.png +0 -0
- setiastro/images/waxing_crescent_4.png +0 -0
- setiastro/images/waxing_crescent_5.png +0 -0
- setiastro/images/waxing_gibbous_1.png +0 -0
- setiastro/images/waxing_gibbous_2.png +0 -0
- setiastro/images/waxing_gibbous_3.png +0 -0
- setiastro/images/waxing_gibbous_4.png +0 -0
- setiastro/images/waxing_gibbous_5.png +0 -0
- setiastro/qml/ResourceMonitor.qml +84 -82
- setiastro/saspro/__main__.py +19 -0
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/abe.py +37 -4
- setiastro/saspro/aberration_ai.py +237 -21
- setiastro/saspro/acv_exporter.py +379 -0
- setiastro/saspro/add_stars.py +33 -6
- setiastro/saspro/backgroundneutral.py +35 -7
- setiastro/saspro/blemish_blaster.py +4 -1
- setiastro/saspro/blink_comparator_pro.py +74 -24
- setiastro/saspro/clahe.py +4 -1
- setiastro/saspro/continuum_subtract.py +4 -1
- setiastro/saspro/convo.py +4 -1
- setiastro/saspro/cosmicclarity.py +129 -18
- setiastro/saspro/crop_dialog_pro.py +123 -7
- setiastro/saspro/curve_editor_pro.py +109 -42
- setiastro/saspro/doc_manager.py +67 -4
- setiastro/saspro/exoplanet_detector.py +120 -28
- setiastro/saspro/frequency_separation.py +1158 -204
- setiastro/saspro/ghs_dialog_pro.py +81 -16
- setiastro/saspro/graxpert.py +1 -0
- setiastro/saspro/gui/main_window.py +393 -204
- setiastro/saspro/gui/mixins/menu_mixin.py +1 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +356 -12
- setiastro/saspro/gui/mixins/update_mixin.py +138 -36
- setiastro/saspro/gui/mixins/view_mixin.py +42 -0
- setiastro/saspro/halobgon.py +4 -0
- setiastro/saspro/histogram.py +5 -1
- setiastro/saspro/image_combine.py +4 -0
- setiastro/saspro/image_peeker_pro.py +4 -0
- setiastro/saspro/imageops/stretch.py +531 -62
- setiastro/saspro/isophote.py +4 -0
- setiastro/saspro/layers.py +13 -9
- setiastro/saspro/layers_dock.py +183 -3
- setiastro/saspro/legacy/image_manager.py +154 -20
- setiastro/saspro/legacy/numba_utils.py +43 -0
- setiastro/saspro/legacy/xisf.py +240 -98
- setiastro/saspro/live_stacking.py +180 -79
- setiastro/saspro/luminancerecombine.py +228 -27
- setiastro/saspro/mask_creation.py +174 -15
- setiastro/saspro/mfdeconv.py +113 -35
- setiastro/saspro/mfdeconvcudnn.py +119 -70
- setiastro/saspro/mfdeconvsport.py +112 -35
- setiastro/saspro/morphology.py +4 -0
- setiastro/saspro/multiscale_decomp.py +51 -12
- setiastro/saspro/numba_utils.py +72 -2
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/ops/script_editor.py +5 -2
- setiastro/saspro/ops/scripts.py +3 -0
- setiastro/saspro/perfect_palette_picker.py +37 -3
- setiastro/saspro/plate_solver.py +84 -49
- setiastro/saspro/psf_viewer.py +119 -37
- setiastro/saspro/resources.py +67 -0
- setiastro/saspro/rgbalign.py +4 -0
- setiastro/saspro/selective_color.py +4 -1
- setiastro/saspro/sfcc.py +60 -2
- setiastro/saspro/shortcuts.py +142 -23
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1017 -400
- setiastro/saspro/star_alignment.py +4 -1
- setiastro/saspro/star_spikes.py +4 -0
- setiastro/saspro/star_stretch.py +38 -3
- setiastro/saspro/stat_stretch.py +702 -128
- setiastro/saspro/subwindow.py +786 -360
- setiastro/saspro/supernovaasteroidhunter.py +1 -1
- setiastro/saspro/wavescale_hdr.py +4 -1
- setiastro/saspro/wavescalede.py +4 -1
- setiastro/saspro/whitebalance.py +60 -12
- setiastro/saspro/widgets/common_utilities.py +28 -21
- setiastro/saspro/widgets/resource_monitor.py +109 -59
- setiastro/saspro/widgets/spinboxes.py +10 -13
- setiastro/saspro/wimi.py +27 -656
- setiastro/saspro/wims.py +13 -3
- setiastro/saspro/xisf.py +101 -11
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/METADATA +2 -1
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/RECORD +112 -80
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/license.txt +0 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# src/setiastro/saspro/acv_exporter.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from PyQt6.QtCore import Qt, QCoreApplication
|
|
9
|
+
from PyQt6.QtWidgets import (
|
|
10
|
+
QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QGroupBox,
|
|
11
|
+
QLabel, QLineEdit, QPushButton, QFileDialog, QComboBox,
|
|
12
|
+
QMessageBox, QWidget
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# If you have a canonical place for "global save_as", wire it here.
|
|
16
|
+
# Example placeholders (change to your actual import):
|
|
17
|
+
# from setiastro.saspro.actions.save_as import save_as
|
|
18
|
+
# from setiastro.saspro.fileio.save_as import save_as
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _tr(s: str) -> str:
|
|
22
|
+
return QCoreApplication.translate("AstroCatalogueViewerExporter", s)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AstroCatalogueViewerExporterDialog(QDialog):
|
|
26
|
+
"""
|
|
27
|
+
Astro Catalogue Viewer Exporter
|
|
28
|
+
- Persist folders in QSettings
|
|
29
|
+
- Export current active document/view via your global save_as
|
|
30
|
+
- Route by name prefix: M -> Messier, NGC -> NGC, IC -> IC, C -> Caldwell, else Master
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
# QSettings keys
|
|
34
|
+
K_MASTER = "acv_export/master_folder"
|
|
35
|
+
K_M = "acv_export/messier_folder"
|
|
36
|
+
K_NGC = "acv_export/ngc_folder"
|
|
37
|
+
K_IC = "acv_export/ic_folder"
|
|
38
|
+
K_C = "acv_export/caldwell_folder"
|
|
39
|
+
K_FMT = "acv_export/format"
|
|
40
|
+
|
|
41
|
+
def __init__(self, parent, dm, doc):
|
|
42
|
+
super().__init__(parent)
|
|
43
|
+
self.dm = dm
|
|
44
|
+
self.doc = doc
|
|
45
|
+
self.setObjectName("acv_exporter_dialog")
|
|
46
|
+
self.setWindowTitle(_tr("Astro Catalogue Viewer Exporter"))
|
|
47
|
+
|
|
48
|
+
# Prefer not using WA_DeleteOnClose if that’s a Linux landmine for you.
|
|
49
|
+
# (You can still set it from caller if you want.)
|
|
50
|
+
self._main = parent
|
|
51
|
+
self._settings = getattr(parent, "settings", None) # typical SASpro pattern
|
|
52
|
+
|
|
53
|
+
root = QVBoxLayout(self)
|
|
54
|
+
root.setContentsMargins(12, 12, 12, 12)
|
|
55
|
+
root.setSpacing(10)
|
|
56
|
+
|
|
57
|
+
# -------------------------
|
|
58
|
+
# Folder section
|
|
59
|
+
# -------------------------
|
|
60
|
+
gb = QGroupBox(_tr("Image folders"), self)
|
|
61
|
+
g = QGridLayout(gb)
|
|
62
|
+
g.setContentsMargins(10, 10, 10, 10)
|
|
63
|
+
g.setHorizontalSpacing(10)
|
|
64
|
+
g.setVerticalSpacing(8)
|
|
65
|
+
|
|
66
|
+
self.ed_master = QLineEdit(self)
|
|
67
|
+
self.btn_master = QPushButton(_tr("Browse…"), self)
|
|
68
|
+
self.btn_master.clicked.connect(lambda: self._browse_folder(self.ed_master))
|
|
69
|
+
|
|
70
|
+
g.addWidget(QLabel(_tr("Master Image Folder")), 0, 0)
|
|
71
|
+
g.addWidget(self.ed_master, 0, 1)
|
|
72
|
+
g.addWidget(self.btn_master, 0, 2)
|
|
73
|
+
|
|
74
|
+
gb2 = QGroupBox(_tr("Image folder per catalog"), self)
|
|
75
|
+
gg = QGridLayout(gb2)
|
|
76
|
+
gg.setContentsMargins(10, 10, 10, 10)
|
|
77
|
+
gg.setHorizontalSpacing(10)
|
|
78
|
+
gg.setVerticalSpacing(8)
|
|
79
|
+
|
|
80
|
+
self.ed_m = QLineEdit(self)
|
|
81
|
+
self.ed_ngc = QLineEdit(self)
|
|
82
|
+
self.ed_ic = QLineEdit(self)
|
|
83
|
+
self.ed_c = QLineEdit(self)
|
|
84
|
+
|
|
85
|
+
bm = QPushButton(_tr("Browse…"), self); bm.clicked.connect(lambda: self._browse_folder(self.ed_m))
|
|
86
|
+
bn = QPushButton(_tr("Browse…"), self); bn.clicked.connect(lambda: self._browse_folder(self.ed_ngc))
|
|
87
|
+
bi = QPushButton(_tr("Browse…"), self); bi.clicked.connect(lambda: self._browse_folder(self.ed_ic))
|
|
88
|
+
bc = QPushButton(_tr("Browse…"), self); bc.clicked.connect(lambda: self._browse_folder(self.ed_c))
|
|
89
|
+
|
|
90
|
+
gg.addWidget(QLabel(_tr("Messier")), 0, 0); gg.addWidget(self.ed_m, 0, 1); gg.addWidget(bm, 0, 2)
|
|
91
|
+
gg.addWidget(QLabel(_tr("NGC")), 1, 0); gg.addWidget(self.ed_ngc, 1, 1); gg.addWidget(bn, 1, 2)
|
|
92
|
+
gg.addWidget(QLabel(_tr("IC")), 2, 0); gg.addWidget(self.ed_ic, 2, 1); gg.addWidget(bi, 2, 2)
|
|
93
|
+
gg.addWidget(QLabel(_tr("Caldwell")),3, 0); gg.addWidget(self.ed_c, 3, 1); gg.addWidget(bc, 3, 2)
|
|
94
|
+
|
|
95
|
+
root.addWidget(gb)
|
|
96
|
+
root.addWidget(gb2)
|
|
97
|
+
|
|
98
|
+
# -------------------------
|
|
99
|
+
# Export controls
|
|
100
|
+
# -------------------------
|
|
101
|
+
row = QHBoxLayout()
|
|
102
|
+
row.setSpacing(10)
|
|
103
|
+
|
|
104
|
+
self.ed_name = QLineEdit(self)
|
|
105
|
+
self.ed_name.setPlaceholderText(_tr("e.g. M31, NGC5060, M31_HaOnly…"))
|
|
106
|
+
|
|
107
|
+
self.cmb_fmt = QComboBox(self)
|
|
108
|
+
# keep these lower-case as “extensions”
|
|
109
|
+
self.cmb_fmt.addItems(["jpg", "png", "tif"])
|
|
110
|
+
|
|
111
|
+
self.btn_export = QPushButton(_tr("Export"), self)
|
|
112
|
+
self.btn_export.clicked.connect(self._on_export)
|
|
113
|
+
|
|
114
|
+
row.addWidget(QLabel(_tr("Image Name")), 0)
|
|
115
|
+
row.addWidget(self.ed_name, 2)
|
|
116
|
+
row.addWidget(QLabel(_tr("Type")), 0)
|
|
117
|
+
row.addWidget(self.cmb_fmt, 0)
|
|
118
|
+
row.addWidget(self.btn_export, 0)
|
|
119
|
+
|
|
120
|
+
root.addLayout(row)
|
|
121
|
+
|
|
122
|
+
# Load persisted settings
|
|
123
|
+
self._load_settings()
|
|
124
|
+
|
|
125
|
+
# Save on edits (lightweight)
|
|
126
|
+
self.ed_master.textChanged.connect(self._save_settings)
|
|
127
|
+
self.ed_m.textChanged.connect(self._save_settings)
|
|
128
|
+
self.ed_ngc.textChanged.connect(self._save_settings)
|
|
129
|
+
self.ed_ic.textChanged.connect(self._save_settings)
|
|
130
|
+
self.ed_c.textChanged.connect(self._save_settings)
|
|
131
|
+
self.cmb_fmt.currentTextChanged.connect(self._save_settings)
|
|
132
|
+
|
|
133
|
+
# -------------------------
|
|
134
|
+
# Settings
|
|
135
|
+
# -------------------------
|
|
136
|
+
def _load_settings(self):
|
|
137
|
+
s = self._settings
|
|
138
|
+
if s is None:
|
|
139
|
+
return
|
|
140
|
+
self.ed_master.setText(s.value(self.K_MASTER, "", type=str) or "")
|
|
141
|
+
self.ed_m.setText(s.value(self.K_M, "", type=str) or "")
|
|
142
|
+
self.ed_ngc.setText(s.value(self.K_NGC, "", type=str) or "")
|
|
143
|
+
self.ed_ic.setText(s.value(self.K_IC, "", type=str) or "")
|
|
144
|
+
self.ed_c.setText(s.value(self.K_C, "", type=str) or "")
|
|
145
|
+
fmt = (s.value(self.K_FMT, "png", type=str) or "png").lower()
|
|
146
|
+
idx = self.cmb_fmt.findText(fmt)
|
|
147
|
+
if idx >= 0:
|
|
148
|
+
self.cmb_fmt.setCurrentIndex(idx)
|
|
149
|
+
|
|
150
|
+
def _save_settings(self):
|
|
151
|
+
s = self._settings
|
|
152
|
+
if s is None:
|
|
153
|
+
return
|
|
154
|
+
s.setValue(self.K_MASTER, self.ed_master.text().strip())
|
|
155
|
+
s.setValue(self.K_M, self.ed_m.text().strip())
|
|
156
|
+
s.setValue(self.K_NGC, self.ed_ngc.text().strip())
|
|
157
|
+
s.setValue(self.K_IC, self.ed_ic.text().strip())
|
|
158
|
+
s.setValue(self.K_C, self.ed_c.text().strip())
|
|
159
|
+
s.setValue(self.K_FMT, self.cmb_fmt.currentText().strip().lower())
|
|
160
|
+
try:
|
|
161
|
+
s.sync()
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
# -------------------------
|
|
166
|
+
# Folder helpers
|
|
167
|
+
# -------------------------
|
|
168
|
+
def _browse_folder(self, target_edit: QLineEdit):
|
|
169
|
+
start = target_edit.text().strip() or os.path.expanduser("~")
|
|
170
|
+
path = QFileDialog.getExistingDirectory(self, _tr("Select folder"), start)
|
|
171
|
+
if path:
|
|
172
|
+
target_edit.setText(path)
|
|
173
|
+
|
|
174
|
+
# -------------------------
|
|
175
|
+
# Active doc resolution (ROI aware)
|
|
176
|
+
# -------------------------
|
|
177
|
+
def _get_active_doc(self):
|
|
178
|
+
mw = self._main
|
|
179
|
+
mdi = getattr(mw, "mdi", None)
|
|
180
|
+
dm = getattr(mw, "doc_manager", None) or getattr(mw, "docman", None)
|
|
181
|
+
|
|
182
|
+
if mdi is None or not hasattr(mdi, "activeSubWindow"):
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
sw = mdi.activeSubWindow()
|
|
186
|
+
if not sw:
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
view = sw.widget()
|
|
190
|
+
|
|
191
|
+
# Prefer ROI-aware doc lookup via DocManager
|
|
192
|
+
if dm is not None and hasattr(dm, "get_document_for_view"):
|
|
193
|
+
try:
|
|
194
|
+
doc = dm.get_document_for_view(view)
|
|
195
|
+
if doc is not None:
|
|
196
|
+
return doc
|
|
197
|
+
except Exception:
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
# Fallback: view.document
|
|
201
|
+
return getattr(view, "document", None)
|
|
202
|
+
|
|
203
|
+
# -------------------------
|
|
204
|
+
# Routing logic
|
|
205
|
+
# -------------------------
|
|
206
|
+
@staticmethod
|
|
207
|
+
def _pick_target_folder(name: str, master: str, m: str, ngc: str, ic: str, c: str) -> str:
|
|
208
|
+
raw = (name or "").strip().upper()
|
|
209
|
+
|
|
210
|
+
# routing normalization: remove spaces (optionally also "_" and "-" if you want)
|
|
211
|
+
u = re.sub(r"\s+", "", raw)
|
|
212
|
+
|
|
213
|
+
if re.match(r"^M\d+", u):
|
|
214
|
+
return m or master
|
|
215
|
+
if re.match(r"^NGC\d+", u): # optional tighten
|
|
216
|
+
return ngc or master
|
|
217
|
+
if re.match(r"^IC\d+", u): # optional tighten
|
|
218
|
+
return ic or master
|
|
219
|
+
if re.match(r"^C\d+", u):
|
|
220
|
+
return c or master
|
|
221
|
+
return master
|
|
222
|
+
|
|
223
|
+
def _normalize_for_routing(self, name: str) -> str:
|
|
224
|
+
# Uppercase + remove spaces only (routing only)
|
|
225
|
+
u = (name or "").strip().upper()
|
|
226
|
+
u = re.sub(r"\s+", "", u)
|
|
227
|
+
return u
|
|
228
|
+
|
|
229
|
+
def _sanitize_filename(self, s: str) -> str:
|
|
230
|
+
# Keep it simple + safe across OSes
|
|
231
|
+
s = (s or "").strip()
|
|
232
|
+
s = s.replace("\\", "_").replace("/", "_").replace(":", "_")
|
|
233
|
+
s = re.sub(r"\s+", " ", s) # keep single spaces
|
|
234
|
+
s = re.sub(r"[^\w\-\.\(\) ]+", "", s) # drop odd chars
|
|
235
|
+
return s.strip() or "export"
|
|
236
|
+
|
|
237
|
+
# -------------------------
|
|
238
|
+
# Export action
|
|
239
|
+
# -------------------------
|
|
240
|
+
def _on_export(self):
|
|
241
|
+
fmt = self.cmb_fmt.currentText().strip().lower()
|
|
242
|
+
|
|
243
|
+
# -------------------------
|
|
244
|
+
# Name (raw + routing-normalized)
|
|
245
|
+
# -------------------------
|
|
246
|
+
raw_name = self.ed_name.text().strip()
|
|
247
|
+
if not raw_name:
|
|
248
|
+
QMessageBox.information(
|
|
249
|
+
self, _tr("Export"),
|
|
250
|
+
_tr("Please enter an Image Name (e.g. M31, NGC5060…).")
|
|
251
|
+
)
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
route_name = self._normalize_for_routing(raw_name)
|
|
255
|
+
|
|
256
|
+
# -------------------------
|
|
257
|
+
# Active doc (passed in)
|
|
258
|
+
# -------------------------
|
|
259
|
+
doc = self.doc
|
|
260
|
+
if doc is None or getattr(doc, "image", None) is None:
|
|
261
|
+
QMessageBox.information(self, _tr("Export"), _tr("No active image. Open an image first."))
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
# -------------------------
|
|
265
|
+
# Folder config
|
|
266
|
+
# -------------------------
|
|
267
|
+
master_dir = self.ed_master.text().strip()
|
|
268
|
+
if not master_dir:
|
|
269
|
+
QMessageBox.information(self, _tr("Export"), _tr("Please set a Master Image Folder."))
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
messier_dir = self.ed_m.text().strip() or master_dir
|
|
273
|
+
ngc_dir = self.ed_ngc.text().strip() or master_dir
|
|
274
|
+
ic_dir = self.ed_ic.text().strip() or master_dir
|
|
275
|
+
caldwell_dir = self.ed_c.text().strip() or master_dir
|
|
276
|
+
|
|
277
|
+
# -------------------------
|
|
278
|
+
# Route by prefix (digit-guard)
|
|
279
|
+
# -------------------------
|
|
280
|
+
if route_name.startswith("M") and route_name[1:2].isdigit():
|
|
281
|
+
base_dir = messier_dir
|
|
282
|
+
elif route_name.startswith("NGC") and route_name[3:4].isdigit():
|
|
283
|
+
base_dir = ngc_dir
|
|
284
|
+
elif route_name.startswith("IC") and route_name[2:3].isdigit():
|
|
285
|
+
base_dir = ic_dir
|
|
286
|
+
elif route_name.startswith("C") and route_name[1:2].isdigit():
|
|
287
|
+
base_dir = caldwell_dir
|
|
288
|
+
else:
|
|
289
|
+
base_dir = master_dir
|
|
290
|
+
|
|
291
|
+
# Ensure folder exists
|
|
292
|
+
try:
|
|
293
|
+
Path(base_dir).mkdir(parents=True, exist_ok=True)
|
|
294
|
+
except Exception as e:
|
|
295
|
+
QMessageBox.critical(
|
|
296
|
+
self, _tr("Export"),
|
|
297
|
+
_tr("Could not create folder:\n{0}\n\n{1}").format(base_dir, str(e))
|
|
298
|
+
)
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
# Filename uses user's raw name (sanitized for filesystem)
|
|
302
|
+
file_stem = self._sanitize_filename(raw_name)
|
|
303
|
+
out_path = str(Path(base_dir) / f"{file_stem}.{fmt}")
|
|
304
|
+
|
|
305
|
+
# -------------------------
|
|
306
|
+
# Save
|
|
307
|
+
# -------------------------
|
|
308
|
+
ok, err = self._save_current_doc_to_path(doc, out_path, fmt)
|
|
309
|
+
if ok:
|
|
310
|
+
QMessageBox.information(self, _tr("Export"), _tr("Exported:\n{0}").format(out_path))
|
|
311
|
+
else:
|
|
312
|
+
QMessageBox.critical(self, _tr("Export"), _tr("Export failed:\n{0}").format(err or "Unknown error"))
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _save_current_doc_to_path(self, doc, out_path: str, fmt: str) -> tuple[bool, str]:
|
|
316
|
+
try:
|
|
317
|
+
import numpy as np
|
|
318
|
+
from astropy.io import fits
|
|
319
|
+
from astropy.wcs import WCS
|
|
320
|
+
|
|
321
|
+
from setiastro.saspro.legacy.image_manager import save_image as legacy_save_image
|
|
322
|
+
|
|
323
|
+
img = getattr(doc, "image", None)
|
|
324
|
+
if img is None:
|
|
325
|
+
return False, "Active document has no image data."
|
|
326
|
+
|
|
327
|
+
# Ensure numpy array
|
|
328
|
+
img_array = np.asarray(img)
|
|
329
|
+
|
|
330
|
+
# Mono detection (your saver uses this for RAW/XISF paths; safe to provide anyway)
|
|
331
|
+
is_mono = (img_array.ndim == 2) or (img_array.ndim == 3 and img_array.shape[2] == 1)
|
|
332
|
+
|
|
333
|
+
# Preserve bit depth for tif/fit if the doc has it; PNG/JPG ignore bit depth anyway.
|
|
334
|
+
bit_depth = (
|
|
335
|
+
getattr(doc, "bit_depth", None)
|
|
336
|
+
or getattr(doc, "bitdepth", None)
|
|
337
|
+
or (getattr(doc, "metadata", None) or {}).get("bit_depth")
|
|
338
|
+
or (getattr(doc, "metadata", None) or {}).get("bitDepth")
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# Pull headers/WCS from metadata (matches your SASv2->SASpro WCS preference)
|
|
342
|
+
md = getattr(doc, "metadata", None) or {}
|
|
343
|
+
|
|
344
|
+
original_header = md.get("original_header", None)
|
|
345
|
+
wcs_header = None
|
|
346
|
+
|
|
347
|
+
# If wcs is stored as astropy.wcs.WCS, convert to Header
|
|
348
|
+
wcs_obj = md.get("wcs", None)
|
|
349
|
+
if wcs_obj is not None:
|
|
350
|
+
try:
|
|
351
|
+
if isinstance(wcs_obj, WCS):
|
|
352
|
+
wcs_header = wcs_obj.to_header(relax=True)
|
|
353
|
+
elif isinstance(wcs_obj, fits.Header):
|
|
354
|
+
wcs_header = wcs_obj
|
|
355
|
+
except Exception:
|
|
356
|
+
wcs_header = None
|
|
357
|
+
|
|
358
|
+
# Optional passthroughs (safe if missing)
|
|
359
|
+
image_meta = md.get("image_meta", None) or md.get("image_metadata", None)
|
|
360
|
+
file_meta = md.get("file_meta", None) or md.get("xisf_metadata", None)
|
|
361
|
+
|
|
362
|
+
# IMPORTANT:
|
|
363
|
+
# legacy_save_image expects original_format to be the desired output format,
|
|
364
|
+
# and will normalize extension itself.
|
|
365
|
+
legacy_save_image(
|
|
366
|
+
img_array=img_array,
|
|
367
|
+
filename=out_path,
|
|
368
|
+
original_format=fmt, # "jpg","png","tif","fit"
|
|
369
|
+
bit_depth=bit_depth, # keep same depth for tif/fit when possible
|
|
370
|
+
original_header=original_header,
|
|
371
|
+
is_mono=is_mono,
|
|
372
|
+
image_meta=image_meta,
|
|
373
|
+
file_meta=file_meta,
|
|
374
|
+
wcs_header=wcs_header, # merged into FITS header (your saver supports this)
|
|
375
|
+
)
|
|
376
|
+
return True, ""
|
|
377
|
+
|
|
378
|
+
except Exception as e:
|
|
379
|
+
return False, f"{type(e).__name__}: {e}"
|
setiastro/saspro/add_stars.py
CHANGED
|
@@ -231,7 +231,10 @@ class AddStarsDialog(QDialog):
|
|
|
231
231
|
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
232
232
|
self.setModal(False)
|
|
233
233
|
#self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
234
|
-
|
|
234
|
+
try:
|
|
235
|
+
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
236
|
+
except Exception:
|
|
237
|
+
pass # older PyQt6 versions
|
|
235
238
|
self.main = main
|
|
236
239
|
self.starless = None
|
|
237
240
|
self.stars_only = None
|
|
@@ -575,10 +578,9 @@ class AddStarsDialog(QDialog):
|
|
|
575
578
|
|
|
576
579
|
# Emit (target_doc, blended_image)
|
|
577
580
|
self.stars_added.emit(target_doc, self.blended_image.astype(np.float32, copy=False))
|
|
578
|
-
#
|
|
579
|
-
#
|
|
580
|
-
|
|
581
|
-
|
|
581
|
+
# Close UI after apply
|
|
582
|
+
self.accept() # or: self.close()
|
|
583
|
+
return
|
|
582
584
|
|
|
583
585
|
# Ensure initial fit once shown
|
|
584
586
|
def showEvent(self, ev):
|
|
@@ -603,7 +605,32 @@ def add_stars(main):
|
|
|
603
605
|
|
|
604
606
|
dlg = AddStarsDialog(main, parent=main)
|
|
605
607
|
dlg.stars_added.connect(lambda target, arr: _apply_to_doc(main, target, arr))
|
|
606
|
-
|
|
608
|
+
|
|
609
|
+
# IMPORTANT: keep a strong reference (non-modal show)
|
|
610
|
+
if not hasattr(main, "_tool_dialogs"):
|
|
611
|
+
main._tool_dialogs = []
|
|
612
|
+
main._tool_dialogs.append(dlg)
|
|
613
|
+
|
|
614
|
+
# When the dialog closes, drop the reference
|
|
615
|
+
def _cleanup(_=None, d=dlg):
|
|
616
|
+
try:
|
|
617
|
+
if hasattr(main, "_tool_dialogs") and d in main._tool_dialogs:
|
|
618
|
+
main._tool_dialogs.remove(d)
|
|
619
|
+
except Exception:
|
|
620
|
+
pass
|
|
621
|
+
|
|
622
|
+
try:
|
|
623
|
+
dlg.finished.connect(_cleanup) # QDialog signal
|
|
624
|
+
except Exception:
|
|
625
|
+
pass
|
|
626
|
+
try:
|
|
627
|
+
dlg.destroyed.connect(_cleanup) # QObject signal (extra safety)
|
|
628
|
+
except Exception:
|
|
629
|
+
pass
|
|
630
|
+
|
|
631
|
+
dlg.show()
|
|
632
|
+
dlg.raise_()
|
|
633
|
+
dlg.activateWindow()
|
|
607
634
|
|
|
608
635
|
|
|
609
636
|
def _apply_to_doc(main, doc, arr: np.ndarray):
|
|
@@ -251,9 +251,19 @@ class BackgroundNeutralizationDialog(QDialog):
|
|
|
251
251
|
self._main = parent
|
|
252
252
|
self.doc = doc
|
|
253
253
|
|
|
254
|
-
|
|
254
|
+
self._connected_current_doc_changed = False
|
|
255
255
|
if hasattr(self._main, "currentDocumentChanged"):
|
|
256
|
-
|
|
256
|
+
try:
|
|
257
|
+
self._main.currentDocumentChanged.connect(self._on_active_doc_changed)
|
|
258
|
+
self._connected_current_doc_changed = True
|
|
259
|
+
except Exception:
|
|
260
|
+
self._connected_current_doc_changed = False
|
|
261
|
+
|
|
262
|
+
self.finished.connect(self._cleanup_connections)
|
|
263
|
+
try:
|
|
264
|
+
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
265
|
+
except Exception:
|
|
266
|
+
pass # older PyQt6 versions
|
|
257
267
|
|
|
258
268
|
if icon:
|
|
259
269
|
self.setWindowIcon(icon)
|
|
@@ -316,7 +326,7 @@ class BackgroundNeutralizationDialog(QDialog):
|
|
|
316
326
|
|
|
317
327
|
# Events
|
|
318
328
|
self.btn_apply.clicked.connect(self._on_apply)
|
|
319
|
-
self.btn_cancel.clicked.connect(self.
|
|
329
|
+
self.btn_cancel.clicked.connect(self.close)
|
|
320
330
|
self.btn_toggle_stretch.clicked.connect(self._toggle_auto_stretch)
|
|
321
331
|
self.btn_find_bg.clicked.connect(self._on_find_background)
|
|
322
332
|
self.btn_zoom_out.clicked.connect(self.zoom_out)
|
|
@@ -536,15 +546,33 @@ class BackgroundNeutralizationDialog(QDialog):
|
|
|
536
546
|
)
|
|
537
547
|
# Dialog stays open so user can apply to other images
|
|
538
548
|
# Refresh to use the now-active document for next operation
|
|
539
|
-
self.
|
|
549
|
+
self.close()
|
|
540
550
|
|
|
541
|
-
def closeEvent(self,
|
|
551
|
+
def closeEvent(self, ev):
|
|
552
|
+
self._cleanup_connections()
|
|
553
|
+
super().closeEvent(ev)
|
|
554
|
+
|
|
555
|
+
def _cleanup_connections(self):
|
|
556
|
+
# Disconnect active-doc tracking (Fabio hook)
|
|
542
557
|
try:
|
|
543
|
-
if hasattr(self._main, "currentDocumentChanged"):
|
|
558
|
+
if self._connected_current_doc_changed and hasattr(self._main, "currentDocumentChanged"):
|
|
544
559
|
self._main.currentDocumentChanged.disconnect(self._on_active_doc_changed)
|
|
545
560
|
except Exception:
|
|
546
561
|
pass
|
|
547
|
-
|
|
562
|
+
self._connected_current_doc_changed = False
|
|
563
|
+
|
|
564
|
+
# If you ever add threads/workers later, stop them here too (safe no-ops now)
|
|
565
|
+
try:
|
|
566
|
+
if getattr(self, "_worker", None) is not None:
|
|
567
|
+
try:
|
|
568
|
+
self._worker.requestInterruption()
|
|
569
|
+
except Exception:
|
|
570
|
+
pass
|
|
571
|
+
if getattr(self, "_thread", None) is not None:
|
|
572
|
+
self._thread.quit()
|
|
573
|
+
self._thread.wait(500)
|
|
574
|
+
except Exception:
|
|
575
|
+
pass
|
|
548
576
|
|
|
549
577
|
|
|
550
578
|
def _refresh_document_from_active(self):
|
|
@@ -43,7 +43,10 @@ class _BlemishWorker(QRunnable):
|
|
|
43
43
|
self.opacity = float(opacity)
|
|
44
44
|
self.channels_to_process = channels_to_process
|
|
45
45
|
self.signals = _BBWorkerSignals()
|
|
46
|
-
|
|
46
|
+
try:
|
|
47
|
+
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
48
|
+
except Exception:
|
|
49
|
+
pass # older PyQt6 versions
|
|
47
50
|
@pyqtSlot()
|
|
48
51
|
def run(self):
|
|
49
52
|
out = self._remove_blemish(
|
|
@@ -150,45 +150,75 @@ class MetricsPanel(QWidget):
|
|
|
150
150
|
orig_back = entry.get('orig_background', np.nan)
|
|
151
151
|
return idx, fwhm, ecc, orig_back, star_cnt
|
|
152
152
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
def compute_all_metrics(self, loaded_images) -> bool:
|
|
154
|
+
"""Run SEP over the full list in parallel using threads and cache results.
|
|
155
|
+
Returns True if metrics were computed, False if user declined/canceled.
|
|
156
|
+
"""
|
|
156
157
|
n = len(loaded_images)
|
|
157
158
|
if n == 0:
|
|
158
|
-
# Clear any previous state and bail
|
|
159
159
|
self._orig_images = []
|
|
160
|
-
self.metrics_data = [np.array([])]*4
|
|
160
|
+
self.metrics_data = [np.array([])] * 4
|
|
161
161
|
self.flags = []
|
|
162
|
-
self._threshold_initialized = [False]*4
|
|
163
|
-
return
|
|
162
|
+
self._threshold_initialized = [False] * 4
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
def _has_metrics(md):
|
|
166
|
+
try:
|
|
167
|
+
return md is not None and len(md) == 4 and md[0] is not None and len(md[0]) > 0
|
|
168
|
+
except Exception:
|
|
169
|
+
return False
|
|
164
170
|
|
|
165
|
-
# Heads-up dialog (as you already had)
|
|
166
171
|
settings = QSettings()
|
|
167
|
-
|
|
168
|
-
|
|
172
|
+
show_warning = settings.value("metrics/showWarning", True, type=bool)
|
|
173
|
+
|
|
174
|
+
if (not show_warning) and (not _has_metrics(getattr(self, "metrics_data", None))):
|
|
175
|
+
settings.setValue("metrics/showWarning", True)
|
|
176
|
+
show_warning = True
|
|
177
|
+
|
|
178
|
+
# ----------------------------
|
|
179
|
+
# 1) Optional warning gate
|
|
180
|
+
# ----------------------------
|
|
181
|
+
if show_warning:
|
|
169
182
|
msg = QMessageBox(self)
|
|
170
183
|
msg.setWindowTitle(self.tr("Heads-up"))
|
|
171
184
|
msg.setText(self.tr(
|
|
172
185
|
"This is going to use ALL your CPU cores and the UI may lock up until it finishes.\n\n"
|
|
173
186
|
"Continue?"
|
|
174
187
|
))
|
|
175
|
-
msg.setStandardButtons(
|
|
176
|
-
|
|
188
|
+
msg.setStandardButtons(
|
|
189
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
190
|
+
)
|
|
177
191
|
cb = QCheckBox(self.tr("Don't show again"), msg)
|
|
178
192
|
msg.setCheckBox(cb)
|
|
179
|
-
|
|
180
|
-
|
|
193
|
+
|
|
194
|
+
clicked = msg.exec()
|
|
195
|
+
clicked_yes = (clicked == QMessageBox.StandardButton.Yes)
|
|
196
|
+
|
|
197
|
+
if not clicked_yes:
|
|
198
|
+
# If they said NO, never allow "Don't show again" to lock them out.
|
|
199
|
+
# Keep the warning enabled so they can opt-in later.
|
|
200
|
+
if cb.isChecked():
|
|
201
|
+
settings.setValue("metrics/showWarning", True)
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
# They said YES: now it's safe to honor "Don't show again"
|
|
181
205
|
if cb.isChecked():
|
|
182
206
|
settings.setValue("metrics/showWarning", False)
|
|
183
207
|
|
|
184
|
-
#
|
|
208
|
+
# If show_warning is False, we compute with no prompt.
|
|
209
|
+
|
|
210
|
+
# ----------------------------
|
|
211
|
+
# 2) Allocate result arrays
|
|
212
|
+
# ----------------------------
|
|
185
213
|
m0 = np.full(n, np.nan, dtype=np.float32) # FWHM
|
|
186
214
|
m1 = np.full(n, np.nan, dtype=np.float32) # Eccentricity
|
|
187
215
|
m2 = np.full(n, np.nan, dtype=np.float32) # Background (cached)
|
|
188
216
|
m3 = np.full(n, np.nan, dtype=np.float32) # Star count
|
|
189
217
|
flags = [e.get('flagged', False) for e in loaded_images]
|
|
190
218
|
|
|
191
|
-
#
|
|
219
|
+
# ----------------------------
|
|
220
|
+
# 3) Progress dialog
|
|
221
|
+
# ----------------------------
|
|
192
222
|
prog = QProgressDialog(self.tr("Computing frame metrics…"), self.tr("Cancel"), 0, n, self)
|
|
193
223
|
prog.setWindowModality(Qt.WindowModality.WindowModal)
|
|
194
224
|
prog.setMinimumDuration(0)
|
|
@@ -198,32 +228,43 @@ class MetricsPanel(QWidget):
|
|
|
198
228
|
|
|
199
229
|
workers = min(os.cpu_count() or 1, 60)
|
|
200
230
|
tasks = [(i, loaded_images[i]) for i in range(n)]
|
|
201
|
-
done = 0
|
|
231
|
+
done = 0
|
|
232
|
+
canceled = False
|
|
202
233
|
|
|
203
234
|
try:
|
|
204
235
|
with ThreadPoolExecutor(max_workers=workers) as exe:
|
|
205
236
|
futures = {exe.submit(self._compute_one, t): t[0] for t in tasks}
|
|
206
237
|
for fut in as_completed(futures):
|
|
207
238
|
if prog.wasCanceled():
|
|
239
|
+
canceled = True
|
|
208
240
|
break
|
|
209
241
|
try:
|
|
210
242
|
idx, fwhm, ecc, orig_back, star_cnt = fut.result()
|
|
211
243
|
except Exception:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
244
|
+
idx = futures.get(fut, 0)
|
|
245
|
+
fwhm, ecc, orig_back, star_cnt = np.nan, np.nan, np.nan, 0
|
|
246
|
+
|
|
247
|
+
if 0 <= idx < n:
|
|
248
|
+
m0[idx], m1[idx], m2[idx], m3[idx] = fwhm, ecc, orig_back, float(star_cnt)
|
|
249
|
+
|
|
215
250
|
done += 1
|
|
216
251
|
prog.setValue(done)
|
|
217
252
|
QApplication.processEvents()
|
|
218
253
|
finally:
|
|
219
254
|
prog.close()
|
|
220
255
|
|
|
221
|
-
|
|
256
|
+
if canceled:
|
|
257
|
+
# IMPORTANT: leave caches alone; caller will clear/return
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
# ----------------------------
|
|
261
|
+
# 4) Stash results
|
|
262
|
+
# ----------------------------
|
|
222
263
|
self._orig_images = loaded_images
|
|
223
264
|
self.metrics_data = [m0, m1, m2, m3]
|
|
224
265
|
self.flags = flags
|
|
225
|
-
self._threshold_initialized = [False]*4
|
|
226
|
-
|
|
266
|
+
self._threshold_initialized = [False] * 4
|
|
267
|
+
return True
|
|
227
268
|
|
|
228
269
|
def plot(self, loaded_images, indices=None):
|
|
229
270
|
"""
|
|
@@ -242,7 +283,16 @@ class MetricsPanel(QWidget):
|
|
|
242
283
|
|
|
243
284
|
# compute & cache on first call or new image list
|
|
244
285
|
if self._orig_images is not loaded_images or self.metrics_data is None:
|
|
245
|
-
self.compute_all_metrics(loaded_images)
|
|
286
|
+
ok = self.compute_all_metrics(loaded_images)
|
|
287
|
+
if not ok or self.metrics_data is None:
|
|
288
|
+
# user declined/canceled -> clear plots and exit cleanly
|
|
289
|
+
for pw, scat, line in zip(self.plots, self.scats, self.lines):
|
|
290
|
+
scat.setData(x=[], y=[])
|
|
291
|
+
line.setPos(0)
|
|
292
|
+
pw.getPlotItem().getViewBox().update()
|
|
293
|
+
pw.repaint()
|
|
294
|
+
return
|
|
295
|
+
|
|
246
296
|
|
|
247
297
|
# default to all indices
|
|
248
298
|
if indices is None:
|