setiastrosuitepro 1.8.0__py3-none-any.whl → 1.8.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/saspro/__main__.py +12 -1
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/cosmicclarity_engines/benchmark_engine.py +33 -16
- setiastro/saspro/cosmicclarity_engines/darkstar_engine.py +22 -2
- setiastro/saspro/cosmicclarity_engines/denoise_engine.py +68 -15
- setiastro/saspro/cosmicclarity_engines/satellite_engine.py +7 -3
- setiastro/saspro/cosmicclarity_engines/sharpen_engine.py +371 -98
- setiastro/saspro/cosmicclarity_engines/superres_engine.py +1 -0
- setiastro/saspro/model_manager.py +83 -0
- setiastro/saspro/model_workers.py +95 -24
- setiastro/saspro/ops/settings.py +142 -7
- setiastro/saspro/planetprojection.py +68 -36
- setiastro/saspro/resources.py +18 -14
- setiastro/saspro/runtime_torch.py +571 -127
- setiastro/saspro/star_alignment.py +262 -210
- setiastro/saspro/widgets/spinboxes.py +5 -7
- {setiastrosuitepro-1.8.0.dist-info → setiastrosuitepro-1.8.1.post2.dist-info}/METADATA +1 -1
- {setiastrosuitepro-1.8.0.dist-info → setiastrosuitepro-1.8.1.post2.dist-info}/RECORD +22 -22
- {setiastrosuitepro-1.8.0.dist-info → setiastrosuitepro-1.8.1.post2.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.8.0.dist-info → setiastrosuitepro-1.8.1.post2.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.8.0.dist-info → setiastrosuitepro-1.8.1.post2.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.8.0.dist-info → setiastrosuitepro-1.8.1.post2.dist-info}/licenses/license.txt +0 -0
|
@@ -17,6 +17,24 @@ APP_FOLDER_NAME = "SetiAstroSuitePro" # keep stable
|
|
|
17
17
|
ProgressCB = Optional[Callable[[str], None]]
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
def model_path(filename: str) -> Path:
|
|
21
|
+
p = Path(models_root()) / filename
|
|
22
|
+
return p
|
|
23
|
+
|
|
24
|
+
def require_model(filename: str) -> Path:
|
|
25
|
+
"""
|
|
26
|
+
Return full path to a runtime-managed model file.
|
|
27
|
+
Raises FileNotFoundError with a helpful message if missing.
|
|
28
|
+
"""
|
|
29
|
+
p = model_path(filename)
|
|
30
|
+
if not p.exists():
|
|
31
|
+
raise FileNotFoundError(
|
|
32
|
+
f"Model not found: {p}\n"
|
|
33
|
+
f"Expected models in: {models_root()}\n"
|
|
34
|
+
f"Please install/download the Cosmic Clarity models."
|
|
35
|
+
)
|
|
36
|
+
return p
|
|
37
|
+
|
|
20
38
|
def app_data_root() -> str:
|
|
21
39
|
"""
|
|
22
40
|
Frozen-safe persistent data root.
|
|
@@ -113,6 +131,71 @@ def _parse_gdrive_download_form(html: str) -> tuple[Optional[str], Optional[dict
|
|
|
113
131
|
|
|
114
132
|
return action, params
|
|
115
133
|
|
|
134
|
+
def download_http_file(
|
|
135
|
+
url: str,
|
|
136
|
+
dst_path: str | os.PathLike,
|
|
137
|
+
*,
|
|
138
|
+
progress_cb: ProgressCB = None,
|
|
139
|
+
should_cancel=None,
|
|
140
|
+
timeout: int = 60,
|
|
141
|
+
chunk_size: int = 1024 * 1024,
|
|
142
|
+
) -> Path:
|
|
143
|
+
import requests
|
|
144
|
+
|
|
145
|
+
dst = Path(dst_path)
|
|
146
|
+
tmp = dst.with_suffix(dst.suffix + ".part")
|
|
147
|
+
tmp.parent.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
|
|
149
|
+
def log(msg: str):
|
|
150
|
+
if progress_cb:
|
|
151
|
+
progress_cb(msg)
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
tmp.unlink(missing_ok=True)
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
with requests.Session() as s:
|
|
159
|
+
log(f"Connecting… {url}")
|
|
160
|
+
r = s.get(url, stream=True, timeout=timeout, allow_redirects=True)
|
|
161
|
+
r.raise_for_status()
|
|
162
|
+
|
|
163
|
+
total = int(r.headers.get("Content-Length") or 0)
|
|
164
|
+
done = 0
|
|
165
|
+
t_last = time.time()
|
|
166
|
+
done_last = 0
|
|
167
|
+
|
|
168
|
+
with open(tmp, "wb") as f:
|
|
169
|
+
for chunk in r.iter_content(chunk_size=chunk_size):
|
|
170
|
+
if should_cancel and should_cancel():
|
|
171
|
+
try:
|
|
172
|
+
f.close()
|
|
173
|
+
tmp.unlink(missing_ok=True)
|
|
174
|
+
except Exception:
|
|
175
|
+
pass
|
|
176
|
+
raise RuntimeError("Download canceled.")
|
|
177
|
+
|
|
178
|
+
if not chunk:
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
f.write(chunk)
|
|
182
|
+
done += len(chunk)
|
|
183
|
+
|
|
184
|
+
now = time.time()
|
|
185
|
+
if now - t_last >= 0.5:
|
|
186
|
+
if total > 0:
|
|
187
|
+
pct = (done * 100.0) / total
|
|
188
|
+
log(f"Downloading… {pct:5.1f}% ({done}/{total} bytes)")
|
|
189
|
+
else:
|
|
190
|
+
bps = (done - done_last) / max(now - t_last, 1e-9)
|
|
191
|
+
log(f"Downloading… {done} bytes ({bps/1024/1024:.1f} MB/s)")
|
|
192
|
+
t_last = now
|
|
193
|
+
done_last = done
|
|
194
|
+
|
|
195
|
+
os.replace(str(tmp), str(dst))
|
|
196
|
+
log(f"Download complete: {dst}")
|
|
197
|
+
return dst
|
|
198
|
+
|
|
116
199
|
|
|
117
200
|
def download_google_drive_file(
|
|
118
201
|
file_id: str,
|
|
@@ -9,57 +9,128 @@ import zipfile
|
|
|
9
9
|
from setiastro.saspro.model_manager import (
|
|
10
10
|
extract_drive_file_id,
|
|
11
11
|
download_google_drive_file,
|
|
12
|
+
download_http_file,
|
|
12
13
|
install_models_zip,
|
|
13
14
|
sha256_file,
|
|
14
15
|
)
|
|
15
16
|
|
|
17
|
+
class ModelsInstallZipWorker(QObject):
|
|
18
|
+
progress = pyqtSignal(str)
|
|
19
|
+
finished = pyqtSignal(bool, str)
|
|
20
|
+
|
|
21
|
+
def __init__(self, zip_path: str, should_cancel=None):
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.zip_path = zip_path
|
|
24
|
+
self.should_cancel = should_cancel # optional callable
|
|
25
|
+
|
|
26
|
+
def run(self):
|
|
27
|
+
try:
|
|
28
|
+
from setiastro.saspro.model_manager import install_models_zip, sha256_file
|
|
29
|
+
|
|
30
|
+
if not self.zip_path or not os.path.exists(self.zip_path):
|
|
31
|
+
raise RuntimeError("ZIP file not found.")
|
|
32
|
+
|
|
33
|
+
self.progress.emit("Verifying ZIP…")
|
|
34
|
+
# quick hash (optional but helpful for support logs)
|
|
35
|
+
zhash = sha256_file(self.zip_path)
|
|
36
|
+
|
|
37
|
+
manifest = {
|
|
38
|
+
"source": "manual_zip",
|
|
39
|
+
"file": os.path.basename(self.zip_path),
|
|
40
|
+
"sha256": zhash,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
install_models_zip(
|
|
44
|
+
self.zip_path,
|
|
45
|
+
progress_cb=lambda s: self.progress.emit(s),
|
|
46
|
+
manifest=manifest,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
self.finished.emit(True, "Models installed successfully from ZIP.")
|
|
50
|
+
except Exception as e:
|
|
51
|
+
self.finished.emit(False, str(e))
|
|
52
|
+
|
|
53
|
+
|
|
16
54
|
class ModelsDownloadWorker(QObject):
|
|
17
55
|
progress = pyqtSignal(str)
|
|
18
56
|
finished = pyqtSignal(bool, str)
|
|
19
57
|
|
|
20
|
-
def __init__(self, primary: str, backup: str,
|
|
58
|
+
def __init__(self, primary: str, backup: str, tertiary: str | None = None,
|
|
59
|
+
expected_sha256: str | None = None, should_cancel=None):
|
|
21
60
|
super().__init__()
|
|
22
61
|
self.primary = primary
|
|
23
62
|
self.backup = backup
|
|
63
|
+
self.tertiary = (tertiary or "").strip() or None
|
|
24
64
|
self.expected_sha256 = (expected_sha256 or "").strip() or None
|
|
25
65
|
self.should_cancel = should_cancel # callable -> bool
|
|
26
66
|
|
|
27
67
|
def run(self):
|
|
28
68
|
try:
|
|
29
|
-
# The inputs should be FILE links (or IDs), not folder links.
|
|
30
|
-
fid = extract_drive_file_id(self.primary) or extract_drive_file_id(self.backup)
|
|
31
|
-
if not fid:
|
|
32
|
-
raise RuntimeError(
|
|
33
|
-
"Models URL is not a Google Drive *file* link or id.\n"
|
|
34
|
-
"Please provide a shared file link (…/file/d/<ID>/view) to the models zip."
|
|
35
|
-
)
|
|
36
|
-
|
|
37
69
|
tmp = os.path.join(tempfile.gettempdir(), "saspro_models_latest.zip")
|
|
38
|
-
try:
|
|
39
|
-
self.progress.emit("Downloading from primary…")
|
|
40
|
-
download_google_drive_file(fid, tmp, progress_cb=lambda s: self.progress.emit(s), should_cancel=self.should_cancel)
|
|
41
|
-
except Exception as e:
|
|
42
|
-
# Try backup if primary fails AND backup has a different file id
|
|
43
|
-
fid2 = extract_drive_file_id(self.backup)
|
|
44
|
-
if fid2 and fid2 != fid:
|
|
45
|
-
self.progress.emit("Primary failed. Trying backup…")
|
|
46
|
-
download_google_drive_file(fid2, tmp, progress_cb=lambda s: self.progress.emit(s), should_cancel=self.should_cancel)
|
|
47
|
-
else:
|
|
48
|
-
raise
|
|
49
70
|
|
|
71
|
+
# 1) Try Google Drive primary/backup (by file id)
|
|
72
|
+
fid_primary = extract_drive_file_id(self.primary)
|
|
73
|
+
fid_backup = extract_drive_file_id(self.backup)
|
|
74
|
+
|
|
75
|
+
drive_ok = False
|
|
76
|
+
used_source = None
|
|
77
|
+
|
|
78
|
+
if fid_primary or fid_backup:
|
|
79
|
+
try:
|
|
80
|
+
if fid_primary:
|
|
81
|
+
self.progress.emit("Downloading from primary (Google Drive)…")
|
|
82
|
+
download_google_drive_file(
|
|
83
|
+
fid_primary, tmp,
|
|
84
|
+
progress_cb=lambda s: self.progress.emit(s),
|
|
85
|
+
should_cancel=self.should_cancel
|
|
86
|
+
)
|
|
87
|
+
drive_ok = True
|
|
88
|
+
used_source = ("google_drive", fid_primary)
|
|
89
|
+
else:
|
|
90
|
+
raise RuntimeError("Primary is not a valid Drive file link/id.")
|
|
91
|
+
except Exception:
|
|
92
|
+
# Try backup if different id
|
|
93
|
+
if fid_backup and fid_backup != fid_primary:
|
|
94
|
+
self.progress.emit("Primary failed. Trying backup (Google Drive)…")
|
|
95
|
+
download_google_drive_file(
|
|
96
|
+
fid_backup, tmp,
|
|
97
|
+
progress_cb=lambda s: self.progress.emit(s),
|
|
98
|
+
should_cancel=self.should_cancel
|
|
99
|
+
)
|
|
100
|
+
drive_ok = True
|
|
101
|
+
used_source = ("google_drive", fid_backup)
|
|
102
|
+
|
|
103
|
+
# 2) If Drive failed (or links weren’t Drive), try GitHub / HTTP tertiary
|
|
104
|
+
if not drive_ok:
|
|
105
|
+
if not self.tertiary:
|
|
106
|
+
raise RuntimeError(
|
|
107
|
+
"Google Drive download failed and no tertiary mirror URL was provided."
|
|
108
|
+
)
|
|
109
|
+
self.progress.emit("Google Drive failed. Trying GitHub mirror…")
|
|
110
|
+
download_http_file(
|
|
111
|
+
self.tertiary, tmp,
|
|
112
|
+
progress_cb=lambda s: self.progress.emit(s),
|
|
113
|
+
should_cancel=self.should_cancel
|
|
114
|
+
)
|
|
115
|
+
used_source = ("http", self.tertiary)
|
|
116
|
+
|
|
117
|
+
# 3) Optional checksum
|
|
50
118
|
if self.expected_sha256:
|
|
51
119
|
self.progress.emit("Verifying checksum…")
|
|
52
120
|
got = sha256_file(tmp)
|
|
53
121
|
if got.lower() != self.expected_sha256.lower():
|
|
54
122
|
raise RuntimeError(f"SHA256 mismatch.\nExpected: {self.expected_sha256}\nGot: {got}")
|
|
55
123
|
|
|
124
|
+
# 4) Install + manifest
|
|
125
|
+
src_kind, src_val = used_source if used_source else ("unknown", "")
|
|
56
126
|
manifest = {
|
|
57
|
-
"source":
|
|
58
|
-
"
|
|
59
|
-
"sha256": self.expected_sha256,
|
|
127
|
+
"source": src_kind,
|
|
128
|
+
"source_ref": src_val,
|
|
129
|
+
"sha256": self.expected_sha256 or "",
|
|
60
130
|
}
|
|
61
|
-
install_models_zip(tmp, progress_cb=lambda s: self.progress.emit(s), manifest=manifest)
|
|
62
131
|
|
|
132
|
+
install_models_zip(tmp, progress_cb=lambda s: self.progress.emit(s), manifest=manifest)
|
|
63
133
|
self.finished.emit(True, "Models updated successfully.")
|
|
64
134
|
except Exception as e:
|
|
65
135
|
self.finished.emit(False, str(e))
|
|
136
|
+
|
setiastro/saspro/ops/settings.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# ops.settings.py
|
|
2
2
|
from PyQt6.QtWidgets import (
|
|
3
|
-
QLineEdit, QDialogButtonBox, QFileDialog, QDialog, QPushButton, QFormLayout,QApplication,
|
|
3
|
+
QLineEdit, QDialogButtonBox, QFileDialog, QDialog, QPushButton, QFormLayout,QApplication, QMenu,
|
|
4
4
|
QHBoxLayout, QVBoxLayout, QWidget, QCheckBox, QComboBox, QSpinBox, QDoubleSpinBox, QLabel, QColorDialog, QFontDialog, QSlider)
|
|
5
5
|
from PyQt6.QtCore import QSettings, Qt
|
|
6
|
+
from PyQt6.QtGui import QAction
|
|
6
7
|
import pytz # for timezone list
|
|
7
8
|
from setiastro.saspro.accel_installer import current_backend
|
|
8
9
|
import sys, platform
|
|
@@ -12,6 +13,7 @@ from PyQt6.QtCore import QThread
|
|
|
12
13
|
from setiastro.saspro.i18n import get_available_languages, get_saved_language, save_language
|
|
13
14
|
import importlib.util
|
|
14
15
|
import importlib.metadata
|
|
16
|
+
import webbrowser
|
|
15
17
|
|
|
16
18
|
class SettingsDialog(QDialog):
|
|
17
19
|
"""
|
|
@@ -240,7 +242,24 @@ class SettingsDialog(QDialog):
|
|
|
240
242
|
|
|
241
243
|
self.btn_models_update = QPushButton(self.tr("Download/Update Models…"))
|
|
242
244
|
self.btn_models_update.clicked.connect(self._models_update_clicked)
|
|
243
|
-
|
|
245
|
+
|
|
246
|
+
self.btn_models_install_zip = QPushButton(self.tr("Install from ZIP…"))
|
|
247
|
+
self.btn_models_install_zip.setToolTip(self.tr("Use a manually downloaded models .zip file"))
|
|
248
|
+
self.btn_models_install_zip.clicked.connect(self._models_install_from_zip_clicked)
|
|
249
|
+
|
|
250
|
+
self.btn_models_open_drive = QPushButton(self.tr("Open Drive…"))
|
|
251
|
+
self.btn_models_open_drive.setToolTip(self.tr("Download models (Primary/Backup/GitHub mirror)"))
|
|
252
|
+
|
|
253
|
+
self.btn_models_open_drive.clicked.connect(self._models_open_drive_clicked)
|
|
254
|
+
|
|
255
|
+
row_models = QHBoxLayout()
|
|
256
|
+
row_models.addWidget(self.btn_models_update, 1)
|
|
257
|
+
row_models.addWidget(self.btn_models_install_zip)
|
|
258
|
+
row_models.addWidget(self.btn_models_open_drive)
|
|
259
|
+
|
|
260
|
+
w_models = QWidget()
|
|
261
|
+
w_models.setLayout(row_models)
|
|
262
|
+
right_col.addRow(w_models)
|
|
244
263
|
|
|
245
264
|
# ---- Right column: WIMS + RA/Dec + Updates + Display ----
|
|
246
265
|
right_col.addRow(QLabel(self.tr("<b>What's In My Sky — Defaults</b>")))
|
|
@@ -298,6 +317,92 @@ class SettingsDialog(QDialog):
|
|
|
298
317
|
# Initial Load:
|
|
299
318
|
self.refresh_ui()
|
|
300
319
|
|
|
320
|
+
def _models_open_drive_clicked(self):
|
|
321
|
+
PRIMARY_FOLDER = "https://drive.google.com/drive/folders/1-fktZb3I9l-mQimJX2fZAmJCBj_t0yAF?usp=drive_link"
|
|
322
|
+
BACKUP_FOLDER = "https://drive.google.com/drive/folders/1j46RV6touQtOmtxkhdFWGm_LQKwEpTl9?usp=drive_link"
|
|
323
|
+
GITHUB_ZIP = "https://github.com/setiastro/setiastrosuitepro/releases/download/benchmarkFIT/SASPro_Models_Latest.zip"
|
|
324
|
+
|
|
325
|
+
menu = QMenu(self)
|
|
326
|
+
act_primary = menu.addAction(self.tr("Primary (Google Drive)"))
|
|
327
|
+
act_backup = menu.addAction(self.tr("Backup (Google Drive)"))
|
|
328
|
+
menu.addSeparator()
|
|
329
|
+
act_gh = menu.addAction(self.tr("GitHub (no quota limit)"))
|
|
330
|
+
|
|
331
|
+
chosen = menu.exec(self.btn_models_open_drive.mapToGlobal(self.btn_models_open_drive.rect().bottomLeft()))
|
|
332
|
+
if chosen == act_primary:
|
|
333
|
+
webbrowser.open(PRIMARY_FOLDER)
|
|
334
|
+
elif chosen == act_backup:
|
|
335
|
+
webbrowser.open(BACKUP_FOLDER)
|
|
336
|
+
elif chosen == act_gh:
|
|
337
|
+
webbrowser.open(GITHUB_ZIP)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _models_install_from_zip_clicked(self):
|
|
342
|
+
from PyQt6.QtWidgets import QFileDialog, QMessageBox, QProgressDialog
|
|
343
|
+
from PyQt6.QtCore import Qt, QThread
|
|
344
|
+
import os
|
|
345
|
+
|
|
346
|
+
zip_path, _ = QFileDialog.getOpenFileName(
|
|
347
|
+
self,
|
|
348
|
+
self.tr("Select models ZIP"),
|
|
349
|
+
"",
|
|
350
|
+
self.tr("ZIP files (*.zip);;All files (*)")
|
|
351
|
+
)
|
|
352
|
+
if not zip_path:
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
if not os.path.exists(zip_path):
|
|
356
|
+
QMessageBox.warning(self, self.tr("Models"), self.tr("File not found."))
|
|
357
|
+
return
|
|
358
|
+
|
|
359
|
+
self.btn_models_update.setEnabled(False)
|
|
360
|
+
self.btn_models_install_zip.setEnabled(False)
|
|
361
|
+
|
|
362
|
+
pd = QProgressDialog(self.tr("Preparing…"), self.tr("Cancel"), 0, 0, self)
|
|
363
|
+
pd.setWindowTitle(self.tr("Installing Models"))
|
|
364
|
+
pd.setWindowModality(Qt.WindowModality.ApplicationModal)
|
|
365
|
+
pd.setAutoClose(True)
|
|
366
|
+
pd.setMinimumDuration(0)
|
|
367
|
+
pd.show()
|
|
368
|
+
|
|
369
|
+
from setiastro.saspro.model_workers import ModelsInstallZipWorker
|
|
370
|
+
|
|
371
|
+
self._models_thread = QThread(self)
|
|
372
|
+
self._models_worker = ModelsInstallZipWorker(zip_path)
|
|
373
|
+
self._models_worker.moveToThread(self._models_thread)
|
|
374
|
+
|
|
375
|
+
self._models_thread.started.connect(self._models_worker.run, Qt.ConnectionType.QueuedConnection)
|
|
376
|
+
self._models_worker.progress.connect(pd.setLabelText, Qt.ConnectionType.QueuedConnection)
|
|
377
|
+
|
|
378
|
+
def _cancel():
|
|
379
|
+
if self._models_thread.isRunning():
|
|
380
|
+
self._models_thread.requestInterruption()
|
|
381
|
+
pd.canceled.connect(_cancel, Qt.ConnectionType.QueuedConnection)
|
|
382
|
+
|
|
383
|
+
def _done(ok: bool, msg: str):
|
|
384
|
+
pd.reset()
|
|
385
|
+
pd.deleteLater()
|
|
386
|
+
|
|
387
|
+
self._models_thread.quit()
|
|
388
|
+
self._models_thread.wait()
|
|
389
|
+
|
|
390
|
+
self.btn_models_update.setEnabled(True)
|
|
391
|
+
self.btn_models_install_zip.setEnabled(True)
|
|
392
|
+
self._refresh_models_status()
|
|
393
|
+
|
|
394
|
+
if ok:
|
|
395
|
+
QMessageBox.information(self, self.tr("Models"), self.tr("✅ {0}").format(msg))
|
|
396
|
+
else:
|
|
397
|
+
QMessageBox.warning(self, self.tr("Models"), self.tr("❌ {0}").format(msg))
|
|
398
|
+
|
|
399
|
+
self._models_worker.finished.connect(_done, Qt.ConnectionType.QueuedConnection)
|
|
400
|
+
self._models_thread.finished.connect(self._models_worker.deleteLater, Qt.ConnectionType.QueuedConnection)
|
|
401
|
+
self._models_thread.finished.connect(self._models_thread.deleteLater, Qt.ConnectionType.QueuedConnection)
|
|
402
|
+
|
|
403
|
+
self._models_thread.start()
|
|
404
|
+
|
|
405
|
+
|
|
301
406
|
def _models_update_clicked(self):
|
|
302
407
|
from PyQt6.QtWidgets import QMessageBox, QProgressDialog
|
|
303
408
|
from PyQt6.QtCore import Qt, QThread
|
|
@@ -306,6 +411,7 @@ class SettingsDialog(QDialog):
|
|
|
306
411
|
# Put your actual *zip file* share links here once you create them.
|
|
307
412
|
PRIMARY = "https://drive.google.com/file/d/1n4p0grtNpfllalMqtgaEmsTYaFhT5u7Y/view?usp=drive_link"
|
|
308
413
|
BACKUP = "https://drive.google.com/file/d/1uRGJCITlfMMN89ZkOO5ICWEKMH24KGit/view?usp=drive_link"
|
|
414
|
+
TERTIARY = "https://github.com/setiastro/setiastrosuitepro/releases/download/benchmarkFIT/SASPro_Models_Latest.zip"
|
|
309
415
|
|
|
310
416
|
|
|
311
417
|
self.btn_models_update.setEnabled(False)
|
|
@@ -320,7 +426,7 @@ class SettingsDialog(QDialog):
|
|
|
320
426
|
from setiastro.saspro.model_workers import ModelsDownloadWorker
|
|
321
427
|
|
|
322
428
|
self._models_thread = QThread(self)
|
|
323
|
-
self._models_worker = ModelsDownloadWorker(PRIMARY, BACKUP, expected_sha256=None)
|
|
429
|
+
self._models_worker = ModelsDownloadWorker(PRIMARY, BACKUP, TERTIARY, expected_sha256=None)
|
|
324
430
|
self._models_worker.moveToThread(self._models_thread)
|
|
325
431
|
|
|
326
432
|
self._models_thread.started.connect(self._models_worker.run, Qt.ConnectionType.QueuedConnection)
|
|
@@ -359,10 +465,39 @@ class SettingsDialog(QDialog):
|
|
|
359
465
|
if not m:
|
|
360
466
|
self.lbl_models_status.setText(self.tr("Status: not installed"))
|
|
361
467
|
return
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
)
|
|
468
|
+
|
|
469
|
+
# New fields (preferred)
|
|
470
|
+
src = (m.get("source") or "").strip()
|
|
471
|
+
ref = (m.get("source_ref") or "").strip()
|
|
472
|
+
sha = (m.get("sha256") or "").strip()
|
|
473
|
+
|
|
474
|
+
# Back-compat with older manifests
|
|
475
|
+
if not src:
|
|
476
|
+
# old versions used google_drive + file_id
|
|
477
|
+
src = "google_drive" if m.get("file_id") else (m.get("source") or "unknown")
|
|
478
|
+
if not ref:
|
|
479
|
+
ref = (m.get("file_id") or m.get("file") or "").strip()
|
|
480
|
+
|
|
481
|
+
# Keep it short + readable
|
|
482
|
+
src_label = {
|
|
483
|
+
"google_drive": "Google Drive",
|
|
484
|
+
"http": "GitHub/HTTP",
|
|
485
|
+
"manual_zip": "Manual ZIP",
|
|
486
|
+
}.get(src, src or "Unknown")
|
|
487
|
+
|
|
488
|
+
lines = [self.tr("Status: installed"),
|
|
489
|
+
self.tr("Location: {0}").format(models_root())]
|
|
490
|
+
|
|
491
|
+
if ref:
|
|
492
|
+
# show just a compact hint (id or url or filename)
|
|
493
|
+
lines.append(self.tr("Source: {0}").format(src_label))
|
|
494
|
+
lines.append(self.tr("Ref: {0}").format(ref))
|
|
495
|
+
|
|
496
|
+
if sha:
|
|
497
|
+
lines.append(self.tr("SHA256: {0}").format(sha[:12] + "…"))
|
|
498
|
+
|
|
499
|
+
self.lbl_models_status.setText("\n".join(lines))
|
|
500
|
+
self.lbl_models_status.setStyleSheet("color:#888;")
|
|
366
501
|
|
|
367
502
|
|
|
368
503
|
def _accel_pref_changed(self, idx: int):
|
|
@@ -2175,6 +2175,52 @@ class PlanetProjectionDialog(QDialog):
|
|
|
2175
2175
|
self._show_stereo_pair(cross_eye=cross_eye)
|
|
2176
2176
|
return
|
|
2177
2177
|
|
|
2178
|
+
# ---- GALAXY TOP-DOWN (early exit) ----
|
|
2179
|
+
# IMPORTANT: do this BEFORE any ROI crop, otherwise huge galaxies get clipped by ROI sizing.
|
|
2180
|
+
is_galaxy = (ptype == 3) or (mode == 5) # planet_type==Galaxy OR output==Galaxy Polar View
|
|
2181
|
+
if is_galaxy:
|
|
2182
|
+
def to01(x):
|
|
2183
|
+
if x.dtype == np.uint8:
|
|
2184
|
+
return x.astype(np.float32) / 255.0
|
|
2185
|
+
if x.dtype == np.uint16:
|
|
2186
|
+
return x.astype(np.float32) / 65535.0
|
|
2187
|
+
return x.astype(np.float32, copy=False)
|
|
2188
|
+
|
|
2189
|
+
roi = img[..., :3] # FULL IMAGE
|
|
2190
|
+
cx0 = float(cx) # FULL-IMAGE coords
|
|
2191
|
+
cy0 = float(cy)
|
|
2192
|
+
roi01 = to01(roi)
|
|
2193
|
+
|
|
2194
|
+
pa = float(self.spin_ring_pa.value()) # reuse ring PA widget as galaxy PA
|
|
2195
|
+
tilt = float(self.spin_ring_tilt.value()) # reuse ring tilt widget as galaxy b/a
|
|
2196
|
+
|
|
2197
|
+
# output size: tied to full image (clamped)
|
|
2198
|
+
out_size = int(max(256, min(2400, max(roi.shape[0], roi.shape[1]))))
|
|
2199
|
+
|
|
2200
|
+
try:
|
|
2201
|
+
top8 = deproject_galaxy_topdown_u8(
|
|
2202
|
+
roi01,
|
|
2203
|
+
cx0=cx0, cy0=cy0,
|
|
2204
|
+
rpx=float(r),
|
|
2205
|
+
pa_deg=pa,
|
|
2206
|
+
tilt=tilt,
|
|
2207
|
+
out_size=out_size,
|
|
2208
|
+
)
|
|
2209
|
+
except Exception as e:
|
|
2210
|
+
QMessageBox.warning(self, "Galaxy Polar View", f"Failed to deproject galaxy:\n{e}")
|
|
2211
|
+
return
|
|
2212
|
+
|
|
2213
|
+
# push single-frame output
|
|
2214
|
+
self._left = None
|
|
2215
|
+
self._right = None
|
|
2216
|
+
self._wiggle_frames = None
|
|
2217
|
+
self._wiggle_state = False
|
|
2218
|
+
|
|
2219
|
+
self._last_preview_u8 = top8
|
|
2220
|
+
self._push_preview_u8(top8)
|
|
2221
|
+
return
|
|
2222
|
+
|
|
2223
|
+
|
|
2178
2224
|
# ---- Saturn rings ROI expansion (only increases s) ----
|
|
2179
2225
|
is_saturn = (self.cmb_planet_type.currentIndex() == 1)
|
|
2180
2226
|
rings_on = bool(is_saturn and getattr(self, "chk_rings", None) is not None and self.chk_rings.isChecked())
|
|
@@ -2209,6 +2255,28 @@ class PlanetProjectionDialog(QDialog):
|
|
|
2209
2255
|
y1 = min(Hfull, y0 + s)
|
|
2210
2256
|
|
|
2211
2257
|
roi = img[y0:y1, x0:x1, :3]
|
|
2258
|
+
# ---- GALAXY ROI expansion (ensure full projected ellipse fits) ----
|
|
2259
|
+
if ptype == 3:
|
|
2260
|
+
tilt = float(self.spin_ring_tilt.value())
|
|
2261
|
+
pa = float(self.spin_ring_pa.value())
|
|
2262
|
+
|
|
2263
|
+
tilt = float(np.clip(tilt, 0.02, 1.0))
|
|
2264
|
+
|
|
2265
|
+
# ellipse in SOURCE pixels
|
|
2266
|
+
a = float(r) # semi-major
|
|
2267
|
+
b = max(1.0, a * tilt) # semi-minor
|
|
2268
|
+
|
|
2269
|
+
th = np.deg2rad(pa)
|
|
2270
|
+
cth, sth = np.cos(th), np.sin(th)
|
|
2271
|
+
|
|
2272
|
+
# bounding half-extents of rotated ellipse
|
|
2273
|
+
dx = np.sqrt((a * cth) ** 2 + (b * sth) ** 2)
|
|
2274
|
+
dy = np.sqrt((a * sth) ** 2 + (b * cth) ** 2)
|
|
2275
|
+
need_half = float(max(dx, dy))
|
|
2276
|
+
|
|
2277
|
+
margin = 12.0
|
|
2278
|
+
s_need = int(np.ceil(2.0 * (need_half + margin)))
|
|
2279
|
+
s = max(s, s_need)
|
|
2212
2280
|
|
|
2213
2281
|
# ---- disk mask (ROI coords) ----
|
|
2214
2282
|
H0, W0 = roi.shape[:2]
|
|
@@ -2226,42 +2294,6 @@ class PlanetProjectionDialog(QDialog):
|
|
|
2226
2294
|
|
|
2227
2295
|
theta = float(self.spin_theta.value())
|
|
2228
2296
|
|
|
2229
|
-
# ---- GALAXY TOP-DOWN (early exit) ----
|
|
2230
|
-
is_galaxy = (ptype == 3) or (mode == 5) # planet_type==Galaxy OR output==Galaxy Polar View
|
|
2231
|
-
|
|
2232
|
-
if is_galaxy:
|
|
2233
|
-
# Galaxy wants the ROI disk params (cx0, cy0, r) + PA/tilt
|
|
2234
|
-
roi01 = to01(roi)
|
|
2235
|
-
|
|
2236
|
-
pa = float(self.spin_ring_pa.value()) # reuse ring PA widget as galaxy PA
|
|
2237
|
-
tilt = float(self.spin_ring_tilt.value()) # reuse ring tilt widget as galaxy b/a
|
|
2238
|
-
|
|
2239
|
-
# choose output size: use ROI size or clamp to something reasonable
|
|
2240
|
-
out_size = int(max(256, min(2000, max(roi.shape[0], roi.shape[1]))))
|
|
2241
|
-
|
|
2242
|
-
try:
|
|
2243
|
-
top8 = deproject_galaxy_topdown_u8(
|
|
2244
|
-
roi01,
|
|
2245
|
-
cx0=float(cx0), cy0=float(cy0),
|
|
2246
|
-
rpx=float(r),
|
|
2247
|
-
pa_deg=pa,
|
|
2248
|
-
tilt=tilt,
|
|
2249
|
-
out_size=out_size,
|
|
2250
|
-
)
|
|
2251
|
-
except Exception as e:
|
|
2252
|
-
QMessageBox.warning(self, "Galaxy Polar View", f"Failed to deproject galaxy:\n{e}")
|
|
2253
|
-
return
|
|
2254
|
-
|
|
2255
|
-
# push single-frame output
|
|
2256
|
-
self._left = None
|
|
2257
|
-
self._right = None
|
|
2258
|
-
self._wiggle_frames = None
|
|
2259
|
-
self._wiggle_state = False
|
|
2260
|
-
|
|
2261
|
-
self._last_preview_u8 = top8
|
|
2262
|
-
self._push_preview_u8(top8)
|
|
2263
|
-
return
|
|
2264
|
-
|
|
2265
2297
|
# ---- BODY (sphere reprojection) ----
|
|
2266
2298
|
interp = cv2.INTER_LANCZOS4
|
|
2267
2299
|
left_w, right_w, maskL, maskR = make_stereo_pair(
|
setiastro/saspro/resources.py
CHANGED
|
@@ -603,23 +603,27 @@ class Resources:
|
|
|
603
603
|
|
|
604
604
|
@lru_cache(maxsize=8)
|
|
605
605
|
def get_models_dir() -> str:
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
606
|
+
"""
|
|
607
|
+
Models are NOT packaged resources. They must be installed via the model manager.
|
|
608
|
+
This returns the user models root and never falls back to _internal/data/models.
|
|
609
|
+
"""
|
|
610
|
+
from setiastro.saspro.model_manager import models_root
|
|
611
|
+
p = Path(models_root())
|
|
612
|
+
# Ensure dir exists (models_root should already do this, but harmless)
|
|
613
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
614
|
+
return str(p)
|
|
615
|
+
|
|
615
616
|
|
|
616
|
-
|
|
617
|
-
|
|
617
|
+
def _assert_not_internal_models_path(p: str):
|
|
618
|
+
s = str(p).lower().replace("/", "\\")
|
|
619
|
+
if "\\_internal\\" in s and "\\data\\models\\" in s:
|
|
620
|
+
raise RuntimeError(f"Legacy internal model path detected: {p}")
|
|
618
621
|
|
|
619
622
|
def model_path(filename: str) -> str:
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
+
from setiastro.saspro.model_manager import require_model
|
|
624
|
+
p = str(require_model(filename))
|
|
625
|
+
_assert_not_internal_models_path(p)
|
|
626
|
+
return p
|
|
623
627
|
|
|
624
628
|
# Export all legacy paths as module-level variables
|
|
625
629
|
_legacy = _init_legacy_paths()
|