setiastrosuitepro 1.6.4__py3-none-any.whl → 1.7.1.post2__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/images/TextureClarity.svg +56 -0
- setiastro/images/abeicon.svg +16 -0
- setiastro/images/acv_icon.png +0 -0
- setiastro/images/colorwheel.svg +97 -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/narrowbandnormalization.png +0 -0
- setiastro/images/new_moon.png +0 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/images/planetarystacker.png +0 -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 +20 -1
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/abe.py +37 -4
- setiastro/saspro/aberration_ai.py +364 -33
- setiastro/saspro/aberration_ai_preset.py +29 -3
- setiastro/saspro/acv_exporter.py +379 -0
- setiastro/saspro/add_stars.py +33 -6
- setiastro/saspro/astrospike_python.py +45 -3
- setiastro/saspro/backgroundneutral.py +108 -40
- setiastro/saspro/blemish_blaster.py +4 -1
- setiastro/saspro/blink_comparator_pro.py +150 -55
- setiastro/saspro/clahe.py +4 -1
- setiastro/saspro/continuum_subtract.py +4 -1
- setiastro/saspro/convo.py +13 -7
- setiastro/saspro/cosmicclarity.py +129 -18
- setiastro/saspro/crop_dialog_pro.py +123 -7
- setiastro/saspro/curve_editor_pro.py +181 -64
- setiastro/saspro/curves_preset.py +249 -47
- setiastro/saspro/doc_manager.py +245 -15
- 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 +706 -264
- 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 +35 -1
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +499 -24
- 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 +184 -8
- setiastro/saspro/image_combine.py +4 -0
- setiastro/saspro/image_peeker_pro.py +4 -0
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +1345 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
- setiastro/saspro/imageops/stretch.py +582 -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 +68 -48
- setiastro/saspro/legacy/xisf.py +240 -98
- setiastro/saspro/live_stacking.py +203 -82
- 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 +81 -29
- setiastro/saspro/narrowband_normalization.py +1618 -0
- setiastro/saspro/numba_utils.py +72 -57
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/ops/script_editor.py +10 -2
- setiastro/saspro/ops/scripts.py +122 -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/remove_green.py +1 -1
- setiastro/saspro/resources.py +73 -0
- setiastro/saspro/rgbalign.py +460 -12
- setiastro/saspro/selective_color.py +4 -1
- setiastro/saspro/ser_stack_config.py +82 -0
- setiastro/saspro/ser_stacker.py +2321 -0
- setiastro/saspro/ser_stacker_dialog.py +1838 -0
- setiastro/saspro/ser_tracking.py +206 -0
- setiastro/saspro/serviewer.py +1625 -0
- setiastro/saspro/sfcc.py +662 -216
- setiastro/saspro/shortcuts.py +171 -33
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1347 -485
- setiastro/saspro/star_alignment.py +247 -123
- setiastro/saspro/star_spikes.py +4 -0
- setiastro/saspro/star_stretch.py +38 -3
- setiastro/saspro/stat_stretch.py +892 -129
- setiastro/saspro/subwindow.py +787 -363
- setiastro/saspro/supernovaasteroidhunter.py +1 -1
- setiastro/saspro/texture_clarity.py +593 -0
- setiastro/saspro/wavescale_hdr.py +4 -1
- setiastro/saspro/wavescalede.py +4 -1
- setiastro/saspro/whitebalance.py +84 -12
- setiastro/saspro/widgets/common_utilities.py +28 -21
- setiastro/saspro/widgets/resource_monitor.py +209 -111
- 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.7.1.post2.dist-info}/METADATA +4 -2
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/RECORD +132 -87
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.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):
|
|
@@ -685,9 +685,51 @@ def render_spikes(output: np.ndarray, stars: List[Star], config: SpikeConfig, ct
|
|
|
685
685
|
|
|
686
686
|
# Main spikes
|
|
687
687
|
if config.intensity > 0:
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
688
|
+
rainbow_str = config.rainbow_spike_intensity if (config.enable_rainbow and config.rainbow_spikes) else 0
|
|
689
|
+
for i in range(int(config.quantity)):
|
|
690
|
+
theta = main_angle_rad + (i * (math.pi * 2) / float(config.quantity))
|
|
691
|
+
cos_t = math.cos(theta)
|
|
692
|
+
sin_t = math.sin(theta)
|
|
693
|
+
|
|
694
|
+
start_x = star.x + cos_t * 0.5
|
|
695
|
+
start_y = star.y + sin_t * 0.5
|
|
696
|
+
end_x = star.x + cos_t * base_length
|
|
697
|
+
end_y = star.y + sin_t * base_length
|
|
698
|
+
|
|
699
|
+
# Standard Spike
|
|
700
|
+
# Base star color, fading to zero alpha
|
|
701
|
+
c_end = (star.color.r/255.0, star.color.g/255.0, star.color.b/255.0, 0.0)
|
|
702
|
+
|
|
703
|
+
# If rainbow enabled, standard spike is dimmed (matches preview logic)
|
|
704
|
+
opacity_mult = 0.4 if rainbow_str > 0 else 1.0
|
|
705
|
+
c_start = (color[0], color[1], color[2], color[3] * opacity_mult)
|
|
706
|
+
|
|
707
|
+
draw_line_gradient(output, start_x, start_y, end_x, end_y,
|
|
708
|
+
c_start, c_end, thickness, config.sharpness)
|
|
709
|
+
|
|
710
|
+
# Rainbow Overlay
|
|
711
|
+
if rainbow_str > 0:
|
|
712
|
+
stops = 10
|
|
713
|
+
for s in range(stops):
|
|
714
|
+
p1 = s / stops
|
|
715
|
+
p2 = (s + 1) / stops
|
|
716
|
+
if p1 > config.rainbow_spike_length:
|
|
717
|
+
break
|
|
718
|
+
|
|
719
|
+
hue = (p1 * 360.0 * config.rainbow_spike_frequency) % 360.0
|
|
720
|
+
a_rainbow = min(1.0, config.intensity * rainbow_str * 2.0) * (1.0 - p1)
|
|
721
|
+
r_seg, g_seg, b_seg = hsl_to_rgb(hue / 360.0, 0.8, 0.6)
|
|
722
|
+
c_seg = (r_seg, g_seg, b_seg, a_rainbow)
|
|
723
|
+
|
|
724
|
+
# Calculate segment positions
|
|
725
|
+
sx = start_x + (end_x - start_x) * p1
|
|
726
|
+
sy = start_y + (end_y - start_y) * p1
|
|
727
|
+
ex = start_x + (end_x - start_x) * p2
|
|
728
|
+
ey = start_y + (end_y - start_y) * p2
|
|
729
|
+
|
|
730
|
+
# Draw rainbow segment with constant color
|
|
731
|
+
draw_line_gradient(output, sx, sy, ex, ey,
|
|
732
|
+
c_seg, c_seg, thickness, 1.0)
|
|
691
733
|
|
|
692
734
|
# Secondary spikes
|
|
693
735
|
if config.secondary_intensity > 0:
|