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
|
@@ -5,6 +5,104 @@ import os
|
|
|
5
5
|
import math
|
|
6
6
|
import random
|
|
7
7
|
import sys
|
|
8
|
+
from PyQt6.QtCore import QByteArray
|
|
9
|
+
|
|
10
|
+
def _qs_raw(settings, key, default=None):
|
|
11
|
+
try:
|
|
12
|
+
return settings.value(key, default)
|
|
13
|
+
except Exception:
|
|
14
|
+
return default
|
|
15
|
+
|
|
16
|
+
def _qs_text(v):
|
|
17
|
+
"""Best-effort: convert QByteArray/bytes -> str, preserve strings, else None."""
|
|
18
|
+
try:
|
|
19
|
+
if isinstance(v, QByteArray):
|
|
20
|
+
# PyQt6-safe
|
|
21
|
+
v = bytes(v.data()).decode("utf-8", "ignore")
|
|
22
|
+
elif isinstance(v, (bytes, bytearray)):
|
|
23
|
+
v = bytes(v).decode("utf-8", "ignore")
|
|
24
|
+
if isinstance(v, str):
|
|
25
|
+
return v.strip()
|
|
26
|
+
except Exception:
|
|
27
|
+
pass
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
def _purge(settings, key):
|
|
31
|
+
try:
|
|
32
|
+
settings.remove(key)
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
def qs_int(settings, key, default=0, *, purge_bad=True):
|
|
37
|
+
v = _qs_raw(settings, key, default)
|
|
38
|
+
try:
|
|
39
|
+
if isinstance(v, bool):
|
|
40
|
+
return int(v)
|
|
41
|
+
if isinstance(v, int):
|
|
42
|
+
return v
|
|
43
|
+
if isinstance(v, float):
|
|
44
|
+
return int(v)
|
|
45
|
+
|
|
46
|
+
s = _qs_text(v)
|
|
47
|
+
if s is not None:
|
|
48
|
+
if s == "":
|
|
49
|
+
return default
|
|
50
|
+
s = s.replace(",", ".") # locale fix
|
|
51
|
+
return int(float(s)) # handles "3.0"
|
|
52
|
+
|
|
53
|
+
# Last resort: try numeric conversion, but don't guess on random objects
|
|
54
|
+
return int(v)
|
|
55
|
+
except Exception:
|
|
56
|
+
if purge_bad:
|
|
57
|
+
_purge(settings, key)
|
|
58
|
+
return default
|
|
59
|
+
|
|
60
|
+
def qs_float(settings, key, default=0.0, *, purge_bad=True):
|
|
61
|
+
v = _qs_raw(settings, key, default)
|
|
62
|
+
try:
|
|
63
|
+
if isinstance(v, bool):
|
|
64
|
+
return float(int(v))
|
|
65
|
+
if isinstance(v, (int, float)):
|
|
66
|
+
return float(v)
|
|
67
|
+
|
|
68
|
+
s = _qs_text(v)
|
|
69
|
+
if s is not None:
|
|
70
|
+
if s == "":
|
|
71
|
+
return default
|
|
72
|
+
s = s.replace(",", ".") # locale fix
|
|
73
|
+
return float(s)
|
|
74
|
+
|
|
75
|
+
return float(v)
|
|
76
|
+
except Exception:
|
|
77
|
+
if purge_bad:
|
|
78
|
+
_purge(settings, key)
|
|
79
|
+
return default
|
|
80
|
+
|
|
81
|
+
def qs_bool(settings, key, default=False, *, purge_bad=True):
|
|
82
|
+
v = _qs_raw(settings, key, default)
|
|
83
|
+
try:
|
|
84
|
+
if isinstance(v, bool):
|
|
85
|
+
return v
|
|
86
|
+
if isinstance(v, (int, float)):
|
|
87
|
+
return bool(int(v))
|
|
88
|
+
|
|
89
|
+
s = _qs_text(v)
|
|
90
|
+
if s is not None:
|
|
91
|
+
s = s.lower()
|
|
92
|
+
if s in ("1", "true", "yes", "on"):
|
|
93
|
+
return True
|
|
94
|
+
if s in ("0", "false", "no", "off"):
|
|
95
|
+
return False
|
|
96
|
+
return default
|
|
97
|
+
|
|
98
|
+
# Do NOT guess truthiness of random objects; that's how weirdness spreads
|
|
99
|
+
return default
|
|
100
|
+
except Exception:
|
|
101
|
+
if purge_bad:
|
|
102
|
+
_purge(settings, key)
|
|
103
|
+
return default
|
|
104
|
+
|
|
105
|
+
|
|
8
106
|
# ---------------------------------------------------------------------
|
|
9
107
|
# Executor helper: avoid ProcessPool in frozen (PyInstaller) builds
|
|
10
108
|
# ---------------------------------------------------------------------
|
|
@@ -637,6 +735,7 @@ class StellarAlignmentDialog(QDialog):
|
|
|
637
735
|
pass # older PyQt6 versions
|
|
638
736
|
|
|
639
737
|
self.settings = settings
|
|
738
|
+
|
|
640
739
|
self.parent_window = parent
|
|
641
740
|
self._docman = doc_manager or getattr(parent, "doc_manager", None)
|
|
642
741
|
|
|
@@ -4169,65 +4268,98 @@ class MosaicSettingsDialog(QDialog):
|
|
|
4169
4268
|
self.setWindowTitle("Mosaic Master Settings")
|
|
4170
4269
|
self.initUI()
|
|
4171
4270
|
|
|
4271
|
+
def _set_2dec(self, spin):
|
|
4272
|
+
"""Safely force 2-dec display regardless of CustomDoubleSpinBox implementation."""
|
|
4273
|
+
try:
|
|
4274
|
+
# Standard QAbstractSpinBox API
|
|
4275
|
+
le = spin.lineEdit()
|
|
4276
|
+
if le is not None:
|
|
4277
|
+
le.setText(f"{spin.value():.2f}")
|
|
4278
|
+
return
|
|
4279
|
+
except Exception:
|
|
4280
|
+
pass
|
|
4281
|
+
|
|
4282
|
+
# Fallback: your custom class might expose .lineEdit as an attribute
|
|
4283
|
+
try:
|
|
4284
|
+
le = getattr(spin, "lineEdit", None)
|
|
4285
|
+
if le is not None:
|
|
4286
|
+
le.setText(f"{spin.value():.2f}")
|
|
4287
|
+
except Exception:
|
|
4288
|
+
pass
|
|
4289
|
+
|
|
4172
4290
|
def initUI(self):
|
|
4173
4291
|
layout = QFormLayout(self)
|
|
4174
4292
|
|
|
4175
4293
|
# Number of Stars to Attempt to Use
|
|
4176
|
-
self.starCountSpin = CustomSpinBox(
|
|
4177
|
-
|
|
4178
|
-
|
|
4294
|
+
self.starCountSpin = CustomSpinBox(
|
|
4295
|
+
minimum=1, maximum=1000,
|
|
4296
|
+
initial=qs_int(self.settings, "mosaic/num_stars", 150),
|
|
4297
|
+
step=1
|
|
4298
|
+
)
|
|
4179
4299
|
layout.addRow("Number of Stars:", self.starCountSpin)
|
|
4180
4300
|
|
|
4181
4301
|
# Translation Max Tolerance
|
|
4182
|
-
self.transTolSpin = CustomDoubleSpinBox(
|
|
4183
|
-
|
|
4184
|
-
|
|
4302
|
+
self.transTolSpin = CustomDoubleSpinBox(
|
|
4303
|
+
minimum=0.0, maximum=10.0,
|
|
4304
|
+
initial=qs_float(self.settings, "mosaic/translation_max_tolerance", 3.0),
|
|
4305
|
+
step=0.1
|
|
4306
|
+
)
|
|
4185
4307
|
layout.addRow("Translation Max Tolerance:", self.transTolSpin)
|
|
4186
4308
|
|
|
4187
4309
|
# Scale Min Tolerance
|
|
4188
|
-
self.scaleMinSpin = CustomDoubleSpinBox(
|
|
4189
|
-
|
|
4190
|
-
|
|
4310
|
+
self.scaleMinSpin = CustomDoubleSpinBox(
|
|
4311
|
+
minimum=0.0, maximum=10.0,
|
|
4312
|
+
initial=qs_float(self.settings, "mosaic/scale_min_tolerance", 0.8),
|
|
4313
|
+
step=0.1
|
|
4314
|
+
)
|
|
4191
4315
|
layout.addRow("Scale Min Tolerance:", self.scaleMinSpin)
|
|
4192
4316
|
|
|
4193
4317
|
# Scale Max Tolerance
|
|
4194
|
-
self.scaleMaxSpin = CustomDoubleSpinBox(
|
|
4195
|
-
|
|
4196
|
-
|
|
4318
|
+
self.scaleMaxSpin = CustomDoubleSpinBox(
|
|
4319
|
+
minimum=0.0, maximum=10.0,
|
|
4320
|
+
initial=qs_float(self.settings, "mosaic/scale_max_tolerance", 1.25),
|
|
4321
|
+
step=0.1
|
|
4322
|
+
)
|
|
4197
4323
|
layout.addRow("Scale Max Tolerance:", self.scaleMaxSpin)
|
|
4198
4324
|
|
|
4199
4325
|
# Rotation Max Tolerance
|
|
4200
|
-
self.rotationMaxSpin = CustomDoubleSpinBox(
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4326
|
+
self.rotationMaxSpin = CustomDoubleSpinBox(
|
|
4327
|
+
minimum=0.0, maximum=180.0,
|
|
4328
|
+
initial=qs_float(self.settings, "mosaic/rotation_max_tolerance", 45.0),
|
|
4329
|
+
step=0.1, decimals=2
|
|
4330
|
+
)
|
|
4205
4331
|
layout.addRow("Rotation Max Tolerance (°):", self.rotationMaxSpin)
|
|
4206
4332
|
|
|
4207
4333
|
# Skew Max Tolerance
|
|
4208
|
-
self.skewMaxSpin = CustomDoubleSpinBox(
|
|
4209
|
-
|
|
4210
|
-
|
|
4334
|
+
self.skewMaxSpin = CustomDoubleSpinBox(
|
|
4335
|
+
minimum=0.0, maximum=1.0,
|
|
4336
|
+
initial=qs_float(self.settings, "mosaic/skew_max_tolerance", 0.1),
|
|
4337
|
+
step=0.01
|
|
4338
|
+
)
|
|
4211
4339
|
layout.addRow("Skew Max Tolerance:", self.skewMaxSpin)
|
|
4212
4340
|
|
|
4213
4341
|
# FWHM for Star Detection
|
|
4214
|
-
self.fwhmSpin = CustomDoubleSpinBox(
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4342
|
+
self.fwhmSpin = CustomDoubleSpinBox(
|
|
4343
|
+
minimum=0.0, maximum=20.0,
|
|
4344
|
+
initial=qs_float(self.settings, "mosaic/star_fwhm", 3.0),
|
|
4345
|
+
step=0.1, decimals=2
|
|
4346
|
+
)
|
|
4218
4347
|
layout.addRow("FWHM for Star Detection:", self.fwhmSpin)
|
|
4219
4348
|
|
|
4220
4349
|
# Sigma for Star Detection
|
|
4221
|
-
self.sigmaSpin = CustomDoubleSpinBox(
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4350
|
+
self.sigmaSpin = CustomDoubleSpinBox(
|
|
4351
|
+
minimum=0.0, maximum=10.0,
|
|
4352
|
+
initial=qs_float(self.settings, "mosaic/star_sigma", 3.0),
|
|
4353
|
+
step=0.1, decimals=2
|
|
4354
|
+
)
|
|
4225
4355
|
layout.addRow("Sigma for Star Detection:", self.sigmaSpin)
|
|
4226
4356
|
|
|
4227
4357
|
# Polynomial Degree
|
|
4228
|
-
self.polyDegreeSpin = CustomSpinBox(
|
|
4229
|
-
|
|
4230
|
-
|
|
4358
|
+
self.polyDegreeSpin = CustomSpinBox(
|
|
4359
|
+
minimum=1, maximum=6,
|
|
4360
|
+
initial=qs_int(self.settings, "mosaic/poly_degree", 3),
|
|
4361
|
+
step=1
|
|
4362
|
+
)
|
|
4231
4363
|
layout.addRow("Polynomial Degree:", self.polyDegreeSpin)
|
|
4232
4364
|
|
|
4233
4365
|
buttons = QDialogButtonBox(
|
|
@@ -4239,16 +4371,18 @@ class MosaicSettingsDialog(QDialog):
|
|
|
4239
4371
|
layout.addRow(buttons)
|
|
4240
4372
|
|
|
4241
4373
|
def accept(self):
|
|
4242
|
-
# Save the values to QSettings
|
|
4243
|
-
self.settings.setValue("mosaic/num_stars", self.starCountSpin.value)
|
|
4244
|
-
self.settings.setValue("mosaic/translation_max_tolerance", self.transTolSpin.value())
|
|
4245
|
-
self.settings.setValue("mosaic/scale_min_tolerance", self.scaleMinSpin.value())
|
|
4246
|
-
self.settings.setValue("mosaic/scale_max_tolerance", self.scaleMaxSpin.value())
|
|
4247
|
-
self.settings.setValue("mosaic/rotation_max_tolerance", self.rotationMaxSpin.value())
|
|
4248
|
-
self.settings.setValue("mosaic/skew_max_tolerance", self.skewMaxSpin.value())
|
|
4249
|
-
self.settings.setValue("mosaic/star_fwhm", self.fwhmSpin.value())
|
|
4250
|
-
self.settings.setValue("mosaic/star_sigma", self.sigmaSpin.value())
|
|
4251
|
-
self.settings.setValue("mosaic/poly_degree", self.polyDegreeSpin.value)
|
|
4374
|
+
# Save the values to QSettings (IMPORTANT: call value() not value)
|
|
4375
|
+
self.settings.setValue("mosaic/num_stars", int(self.starCountSpin.value()))
|
|
4376
|
+
self.settings.setValue("mosaic/translation_max_tolerance", float(self.transTolSpin.value()))
|
|
4377
|
+
self.settings.setValue("mosaic/scale_min_tolerance", float(self.scaleMinSpin.value()))
|
|
4378
|
+
self.settings.setValue("mosaic/scale_max_tolerance", float(self.scaleMaxSpin.value()))
|
|
4379
|
+
self.settings.setValue("mosaic/rotation_max_tolerance", float(self.rotationMaxSpin.value()))
|
|
4380
|
+
self.settings.setValue("mosaic/skew_max_tolerance", float(self.skewMaxSpin.value()))
|
|
4381
|
+
self.settings.setValue("mosaic/star_fwhm", float(self.fwhmSpin.value()))
|
|
4382
|
+
self.settings.setValue("mosaic/star_sigma", float(self.sigmaSpin.value()))
|
|
4383
|
+
self.settings.setValue("mosaic/poly_degree", int(self.polyDegreeSpin.value()))
|
|
4384
|
+
|
|
4385
|
+
self.settings.sync()
|
|
4252
4386
|
super().accept()
|
|
4253
4387
|
|
|
4254
4388
|
class PolyGradientRemoval:
|
|
@@ -4618,6 +4752,7 @@ class MosaicMasterDialog(QDialog):
|
|
|
4618
4752
|
self.stretch_original_mins = []
|
|
4619
4753
|
self.stretch_original_medians = []
|
|
4620
4754
|
self.was_single_channel = False
|
|
4755
|
+
self._migrate_mosaic_settings()
|
|
4621
4756
|
|
|
4622
4757
|
self.initUI()
|
|
4623
4758
|
|
|
@@ -4699,8 +4834,9 @@ class MosaicMasterDialog(QDialog):
|
|
|
4699
4834
|
layout.addLayout(checkbox_layout)
|
|
4700
4835
|
|
|
4701
4836
|
# Persisted WCS-only
|
|
4702
|
-
|
|
4703
|
-
|
|
4837
|
+
self.wcsOnlyCheckBox.setChecked(qs_bool(self.settings, "mosaic/wcs_only", False))
|
|
4838
|
+
|
|
4839
|
+
|
|
4704
4840
|
|
|
4705
4841
|
# Helpers ----------------------------------------------------------
|
|
4706
4842
|
def _set_checked(cb: QCheckBox, checked: bool):
|
|
@@ -4725,7 +4861,8 @@ class MosaicMasterDialog(QDialog):
|
|
|
4725
4861
|
|
|
4726
4862
|
def _on_wcs_only_changed(state: int):
|
|
4727
4863
|
# Persist setting first (this one is user-visible preference)
|
|
4728
|
-
|
|
4864
|
+
self.settings.setValue("mosaic/wcs_only", self.wcsOnlyCheckBox.isChecked())
|
|
4865
|
+
self.settings.sync()
|
|
4729
4866
|
|
|
4730
4867
|
# If WCS-only is turned ON, force Seestar OFF
|
|
4731
4868
|
if self.wcsOnlyCheckBox.isChecked():
|
|
@@ -4762,14 +4899,15 @@ class MosaicMasterDialog(QDialog):
|
|
|
4762
4899
|
"Precise — Full WCS: astropy.reproject per channel; slowest, most exact."
|
|
4763
4900
|
)
|
|
4764
4901
|
|
|
4765
|
-
_default_mode =
|
|
4902
|
+
_default_mode = self.settings.value("mosaic/reproject_mode", "Fast — SIP-aware (Exact Remap)", type=str)
|
|
4903
|
+
|
|
4766
4904
|
valid_modes = [self.reprojectModeCombo.itemText(i) for i in range(self.reprojectModeCombo.count())]
|
|
4767
4905
|
if _default_mode not in valid_modes:
|
|
4768
4906
|
_default_mode = "Fast — SIP-aware (Exact Remap)"
|
|
4769
4907
|
self.reprojectModeCombo.setCurrentText(_default_mode)
|
|
4770
|
-
self.reprojectModeCombo.currentTextChanged.connect(
|
|
4771
|
-
|
|
4772
|
-
)
|
|
4908
|
+
self.reprojectModeCombo.currentTextChanged.connect(self._on_reproject_mode_changed)
|
|
4909
|
+
|
|
4910
|
+
self.settings.sync()
|
|
4773
4911
|
|
|
4774
4912
|
row = QHBoxLayout()
|
|
4775
4913
|
row.addWidget(self.reprojectModeLabel)
|
|
@@ -4809,6 +4947,18 @@ class MosaicMasterDialog(QDialog):
|
|
|
4809
4947
|
|
|
4810
4948
|
self.setLayout(layout)
|
|
4811
4949
|
|
|
4950
|
+
def _on_reproject_mode_changed(self, t: str):
|
|
4951
|
+
self.settings.setValue("mosaic/reproject_mode", t)
|
|
4952
|
+
self.settings.sync()
|
|
4953
|
+
|
|
4954
|
+
|
|
4955
|
+
def _migrate_mosaic_settings(self):
|
|
4956
|
+
s = self.settings
|
|
4957
|
+
_ = qs_int(s, "mosaic/poly_degree", 3)
|
|
4958
|
+
_ = qs_float(s, "mosaic/star_sigma", 3.0)
|
|
4959
|
+
_ = qs_float(s, "mosaic/star_fwhm", 3.0)
|
|
4960
|
+
# etc...
|
|
4961
|
+
s.sync()
|
|
4812
4962
|
|
|
4813
4963
|
def _target_median_from_first(self, items):
|
|
4814
4964
|
# Pick a stable target (median of first image after safe clipping)
|
|
@@ -5043,82 +5193,6 @@ class MosaicMasterDialog(QDialog):
|
|
|
5043
5193
|
return [(self._title_for_doc(d), d) for d in docs]
|
|
5044
5194
|
|
|
5045
5195
|
|
|
5046
|
-
# --- title helpers -------------------------------------------------
|
|
5047
|
-
def _callmaybe(self, obj, attr):
|
|
5048
|
-
"""Return obj.attr() if callable, else obj.attr; else None."""
|
|
5049
|
-
try:
|
|
5050
|
-
v = getattr(obj, attr, None)
|
|
5051
|
-
return v() if callable(v) else v
|
|
5052
|
-
except Exception:
|
|
5053
|
-
return None
|
|
5054
|
-
|
|
5055
|
-
def _best_title_from_obj(self, o):
|
|
5056
|
-
"""Try common title/name providers on a QWidget/ImageDocument."""
|
|
5057
|
-
if o is None:
|
|
5058
|
-
return None
|
|
5059
|
-
# 1) display_name (method or attr)
|
|
5060
|
-
t = self._callmaybe(o, "display_name")
|
|
5061
|
-
if isinstance(t, str) and t.strip():
|
|
5062
|
-
return t
|
|
5063
|
-
# 2) windowTitle / objectName (Qt)
|
|
5064
|
-
for a in ("windowTitle", "objectName", "title", "name"):
|
|
5065
|
-
t = self._callmaybe(o, a)
|
|
5066
|
-
if isinstance(t, str) and t.strip():
|
|
5067
|
-
return t
|
|
5068
|
-
# 3) metadata.display_name
|
|
5069
|
-
try:
|
|
5070
|
-
md = getattr(o, "metadata", {}) or {}
|
|
5071
|
-
t = md.get("display_name")
|
|
5072
|
-
if isinstance(t, str) and t.strip():
|
|
5073
|
-
return t
|
|
5074
|
-
except Exception:
|
|
5075
|
-
pass
|
|
5076
|
-
return None
|
|
5077
|
-
|
|
5078
|
-
def _resolve_view_title(self, view, doc, title_hint=None):
|
|
5079
|
-
"""Prefer the host-provided title, then view, then doc, then a safe default."""
|
|
5080
|
-
# host-provided title from _list_open_docs()
|
|
5081
|
-
if isinstance(title_hint, str) and title_hint.strip():
|
|
5082
|
-
return title_hint.strip()
|
|
5083
|
-
# view wins over doc
|
|
5084
|
-
t = self._best_title_from_obj(view) or self._best_title_from_obj(doc)
|
|
5085
|
-
return t if t else "Untitled View"
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
def _pick_view_dialog(self):
|
|
5089
|
-
items = self._iter_docs()
|
|
5090
|
-
if not items:
|
|
5091
|
-
QMessageBox.information(self, "Add from View", "No open views found.")
|
|
5092
|
-
return None
|
|
5093
|
-
|
|
5094
|
-
dlg = QDialog(self)
|
|
5095
|
-
dlg.setWindowTitle("Select View")
|
|
5096
|
-
v = QVBoxLayout(dlg)
|
|
5097
|
-
v.addWidget(QLabel("Choose a view to add:"))
|
|
5098
|
-
|
|
5099
|
-
combo = QComboBox()
|
|
5100
|
-
for (_, doc) in items:
|
|
5101
|
-
title = self._fmt_doc_title(doc)
|
|
5102
|
-
# append dims if available
|
|
5103
|
-
try:
|
|
5104
|
-
img = _doc_image(doc) # same utility used by Stellar Alignment
|
|
5105
|
-
if img is not None:
|
|
5106
|
-
h, w = img.shape[:2]
|
|
5107
|
-
title = f"{title} — {w}×{h}"
|
|
5108
|
-
except Exception:
|
|
5109
|
-
pass
|
|
5110
|
-
combo.addItem(title, userData=doc)
|
|
5111
|
-
v.addWidget(combo)
|
|
5112
|
-
|
|
5113
|
-
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
|
|
5114
|
-
QDialogButtonBox.StandardButton.Cancel)
|
|
5115
|
-
bb.accepted.connect(dlg.accept)
|
|
5116
|
-
bb.rejected.connect(dlg.reject)
|
|
5117
|
-
v.addWidget(bb)
|
|
5118
|
-
|
|
5119
|
-
return combo.currentData() if dlg.exec() else None
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
5196
|
def openSettings(self):
|
|
5123
5197
|
dlg = MosaicSettingsDialog(self.settings, self)
|
|
5124
5198
|
if dlg.exec():
|
|
@@ -5130,31 +5204,37 @@ class MosaicMasterDialog(QDialog):
|
|
|
5130
5204
|
self,
|
|
5131
5205
|
"Add Image(s)",
|
|
5132
5206
|
"",
|
|
5133
|
-
"Images (*.png *.jpg *.jpeg *.tif *.tiff *.fits *.fit *.fz *.
|
|
5207
|
+
"Images (*.png *.jpg *.jpeg *.tif *.tiff *.fits *.fit *.fz *.xisf *.cr2 *.nef *.arw *.dng *.raf *.orf *.rw2 *.pef)"
|
|
5134
5208
|
)
|
|
5135
|
-
if paths:
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5209
|
+
if not paths:
|
|
5210
|
+
return
|
|
5211
|
+
|
|
5212
|
+
for path in paths:
|
|
5213
|
+
arr, header, bitdepth, ismono = load_image(path)
|
|
5214
|
+
header = sanitize_wcs_header(header) if header else None
|
|
5215
|
+
wcs_obj = get_wcs_from_header(header) if header else None
|
|
5216
|
+
|
|
5217
|
+
base_name = os.path.basename(path) # <- what you want in status strings
|
|
5218
|
+
|
|
5219
|
+
d = {
|
|
5220
|
+
"path": path, # internal identity
|
|
5221
|
+
"display": base_name, # user-facing label
|
|
5222
|
+
"image": arr,
|
|
5223
|
+
"header": header,
|
|
5224
|
+
"wcs": wcs_obj,
|
|
5225
|
+
"bit_depth": bitdepth,
|
|
5226
|
+
"is_mono": ismono,
|
|
5227
|
+
"transform": None
|
|
5228
|
+
}
|
|
5229
|
+
self.loaded_images.append(d)
|
|
5230
|
+
|
|
5231
|
+
# list text can include decorations like [WCS]
|
|
5232
|
+
list_text = base_name + (" [WCS]" if wcs_obj is not None else "")
|
|
5233
|
+
item = QListWidgetItem(list_text)
|
|
5234
|
+
item.setToolTip(path) # still used for removal
|
|
5235
|
+
self.images_list.addItem(item)
|
|
5236
|
+
|
|
5237
|
+
self.update_status()
|
|
5158
5238
|
|
|
5159
5239
|
def add_image_from_view(self):
|
|
5160
5240
|
items = self._iter_docs()
|
|
@@ -5165,38 +5245,31 @@ class MosaicMasterDialog(QDialog):
|
|
|
5165
5245
|
candidates = []
|
|
5166
5246
|
for title, doc in items:
|
|
5167
5247
|
# image
|
|
5168
|
-
img = None
|
|
5169
5248
|
try:
|
|
5170
|
-
if hasattr(doc, "get_image") and callable(doc.get_image)
|
|
5171
|
-
img = doc.get_image()
|
|
5172
|
-
else:
|
|
5173
|
-
img = getattr(doc, "image", None)
|
|
5249
|
+
img = doc.get_image() if hasattr(doc, "get_image") and callable(doc.get_image) else getattr(doc, "image", None)
|
|
5174
5250
|
except Exception:
|
|
5175
5251
|
img = None
|
|
5176
5252
|
if not isinstance(img, np.ndarray):
|
|
5177
5253
|
continue
|
|
5178
5254
|
|
|
5179
5255
|
# metadata/header
|
|
5180
|
-
meta = {}
|
|
5181
5256
|
try:
|
|
5182
|
-
if hasattr(doc, "get_metadata") and callable(doc.get_metadata)
|
|
5183
|
-
meta = doc.get_metadata() or {}
|
|
5184
|
-
else:
|
|
5185
|
-
meta = getattr(doc, "metadata", {}) or {}
|
|
5257
|
+
meta = doc.get_metadata() if hasattr(doc, "get_metadata") and callable(doc.get_metadata) else getattr(doc, "metadata", {}) or {}
|
|
5186
5258
|
except Exception:
|
|
5187
5259
|
meta = {}
|
|
5188
5260
|
|
|
5189
|
-
header = (meta.get("original_header")
|
|
5190
|
-
or meta.get("header")
|
|
5191
|
-
or meta.get("fits_header"))
|
|
5261
|
+
header = (meta.get("original_header") or meta.get("header") or meta.get("fits_header"))
|
|
5192
5262
|
header = sanitize_wcs_header(header) if header else None
|
|
5193
5263
|
wcs_obj = get_wcs_from_header(header) if header else None
|
|
5194
5264
|
|
|
5195
5265
|
h, w = img.shape[:2]
|
|
5196
|
-
label = f"{title} — {w}×{h}"
|
|
5197
|
-
key = f"view://{id(doc)}" # stable pseudo-path for removal
|
|
5198
5266
|
|
|
5199
|
-
|
|
5267
|
+
display_name = str(title).strip() if title else "View" # <- clean
|
|
5268
|
+
list_label = f"{display_name} — {w}×{h}" # <- fancy UI
|
|
5269
|
+
|
|
5270
|
+
key = f"view://{id(doc)}"
|
|
5271
|
+
|
|
5272
|
+
candidates.append((list_label, display_name, img, meta, key, header, wcs_obj))
|
|
5200
5273
|
|
|
5201
5274
|
if not candidates:
|
|
5202
5275
|
QMessageBox.information(self, "Add from View", "No views with image data are open.")
|
|
@@ -5207,10 +5280,11 @@ class MosaicMasterDialog(QDialog):
|
|
|
5207
5280
|
if not ok or not choice:
|
|
5208
5281
|
return
|
|
5209
5282
|
|
|
5210
|
-
|
|
5283
|
+
list_label, display_name, image, metadata, path_key, header, wcs_obj = candidates[labels.index(choice)]
|
|
5211
5284
|
|
|
5212
5285
|
d = {
|
|
5213
|
-
"path": path_key,
|
|
5286
|
+
"path": path_key, # internal id for removal
|
|
5287
|
+
"display": display_name, # <- used by status strings / logs
|
|
5214
5288
|
"image": image,
|
|
5215
5289
|
"header": header,
|
|
5216
5290
|
"wcs": wcs_obj,
|
|
@@ -5220,13 +5294,25 @@ class MosaicMasterDialog(QDialog):
|
|
|
5220
5294
|
}
|
|
5221
5295
|
self.loaded_images.append(d)
|
|
5222
5296
|
|
|
5223
|
-
txt =
|
|
5297
|
+
txt = list_label + (" [WCS]" if wcs_obj is not None else "")
|
|
5224
5298
|
item = QListWidgetItem(txt)
|
|
5225
5299
|
item.setToolTip(path_key)
|
|
5226
5300
|
self.images_list.addItem(item)
|
|
5227
5301
|
self.update_status()
|
|
5228
5302
|
|
|
5229
5303
|
|
|
5304
|
+
def _item_label(self, item: dict) -> str:
|
|
5305
|
+
# 1) explicit display label wins
|
|
5306
|
+
d = item.get("display")
|
|
5307
|
+
if isinstance(d, str) and d.strip():
|
|
5308
|
+
return d.strip()
|
|
5309
|
+
|
|
5310
|
+
# 2) fallback from path (disk or view://)
|
|
5311
|
+
p = item.get("path", "")
|
|
5312
|
+
if isinstance(p, str) and p.startswith("view://"):
|
|
5313
|
+
return "View"
|
|
5314
|
+
return os.path.basename(str(p)) if p else "Item"
|
|
5315
|
+
|
|
5230
5316
|
def remove_selected(self):
|
|
5231
5317
|
s = self.images_list.selectedItems()
|
|
5232
5318
|
if not s:
|
|
@@ -5258,42 +5344,6 @@ class MosaicMasterDialog(QDialog):
|
|
|
5258
5344
|
push_cb=self._push_mosaic_to_new_doc).show()
|
|
5259
5345
|
break
|
|
5260
5346
|
|
|
5261
|
-
def _resolve_view_title(self, *objs) -> str:
|
|
5262
|
-
"""
|
|
5263
|
-
Try hard to get a meaningful title from any of the passed objects
|
|
5264
|
-
(view, subwindow, doc, etc.). Returns a non-empty string or 'Untitled View'.
|
|
5265
|
-
"""
|
|
5266
|
-
def first_str(x):
|
|
5267
|
-
return str(x).strip() if x is not None and str(x).strip() else None
|
|
5268
|
-
|
|
5269
|
-
for o in objs:
|
|
5270
|
-
if o is None:
|
|
5271
|
-
continue
|
|
5272
|
-
# Methods that return a title
|
|
5273
|
-
for meth in ("windowTitle", "title", "displayTitle", "text"):
|
|
5274
|
-
fn = getattr(o, meth, None)
|
|
5275
|
-
if callable(fn):
|
|
5276
|
-
s = first_str(fn())
|
|
5277
|
-
if s: return s
|
|
5278
|
-
# Properties/attributes that might hold a title
|
|
5279
|
-
for attr in ("title", "display_title", "name", "objectName", "display_name"):
|
|
5280
|
-
s = first_str(getattr(o, attr, None))
|
|
5281
|
-
if s: return s
|
|
5282
|
-
# From metadata
|
|
5283
|
-
meta = getattr(o, "metadata", None)
|
|
5284
|
-
if isinstance(meta, dict):
|
|
5285
|
-
s = first_str(meta.get("display_name") or meta.get("title") or meta.get("name"))
|
|
5286
|
-
if s: return s
|
|
5287
|
-
# From file path-ish things
|
|
5288
|
-
for attr in ("file_path", "filepath", "path", "filename"):
|
|
5289
|
-
p = getattr(o, attr, None)
|
|
5290
|
-
if p:
|
|
5291
|
-
base = os.path.basename(str(p))
|
|
5292
|
-
s = first_str(base)
|
|
5293
|
-
if s: return s
|
|
5294
|
-
|
|
5295
|
-
return "Untitled View"
|
|
5296
|
-
|
|
5297
5347
|
# --- WCS/SIP helpers ---------------------------------------------------------
|
|
5298
5348
|
def _ensure_image_naxis(self, hdr: dict | fits.Header, shape):
|
|
5299
5349
|
try:
|
|
@@ -5390,7 +5440,7 @@ class MosaicMasterDialog(QDialog):
|
|
|
5390
5440
|
self.astap_exe = new_path
|
|
5391
5441
|
QMessageBox.information(self, "Mosaic Master", "ASTAP path updated successfully.")
|
|
5392
5442
|
else:
|
|
5393
|
-
self.status_label.setText(f"No ASTAP path; blind solving {item
|
|
5443
|
+
self.status_label.setText(f"No ASTAP path; blind solving {self._item_label(item)}...")
|
|
5394
5444
|
QApplication.processEvents()
|
|
5395
5445
|
solved_header = self.perform_blind_solve(item)
|
|
5396
5446
|
if solved_header:
|
|
@@ -5399,12 +5449,13 @@ class MosaicMasterDialog(QDialog):
|
|
|
5399
5449
|
continue
|
|
5400
5450
|
|
|
5401
5451
|
# Attempt ASTAP solve.
|
|
5402
|
-
self.status_label.setText(f"Attempting ASTAP solve for {item
|
|
5452
|
+
self.status_label.setText(f"Attempting ASTAP solve for {self._item_label(item)}...")
|
|
5453
|
+
|
|
5403
5454
|
QApplication.processEvents()
|
|
5404
5455
|
solved_header = self.attempt_astap_solve(item)
|
|
5405
5456
|
|
|
5406
5457
|
if solved_header is None:
|
|
5407
|
-
self.status_label.setText(f"ASTAP failed for {item
|
|
5458
|
+
self.status_label.setText(f"ASTAP failed for {self._item_label(item)}. Falling back to blind solve...")
|
|
5408
5459
|
QApplication.processEvents()
|
|
5409
5460
|
solved_header = self.perform_blind_solve(item)
|
|
5410
5461
|
|
|
@@ -5412,7 +5463,8 @@ class MosaicMasterDialog(QDialog):
|
|
|
5412
5463
|
solved_header = self._normalize_wcs_header(solved_header, item["image"].shape)
|
|
5413
5464
|
item["wcs"] = self._build_wcs(solved_header, item["image"].shape)
|
|
5414
5465
|
else:
|
|
5415
|
-
print(f"[Mosaic] Plate solving failed for {item
|
|
5466
|
+
print(f"[Mosaic] Plate solving failed for {self._item_label(item)} ({item.get('path','')}).")
|
|
5467
|
+
|
|
5416
5468
|
|
|
5417
5469
|
# ------------------------------------------------------------
|
|
5418
5470
|
# 2) Gather WCS-valid panels
|
|
@@ -5502,7 +5554,7 @@ class MosaicMasterDialog(QDialog):
|
|
|
5502
5554
|
for idx, itm in enumerate(wcs_items):
|
|
5503
5555
|
arr = itm["image"]
|
|
5504
5556
|
|
|
5505
|
-
self.status_label.setText(f"Mapping {itm
|
|
5557
|
+
self.status_label.setText(f"Mapping {self._item_label(itm)} into mosaic frame...")
|
|
5506
5558
|
QApplication.processEvents()
|
|
5507
5559
|
|
|
5508
5560
|
img_lin = arr.astype(np.float32, copy=False)
|
|
@@ -5607,7 +5659,7 @@ class MosaicMasterDialog(QDialog):
|
|
|
5607
5659
|
self.final_mosaic += gray_aligned * smooth_mask
|
|
5608
5660
|
self.weight_mosaic += smooth_mask
|
|
5609
5661
|
|
|
5610
|
-
self.status_label.setText(f"Processed: {itm
|
|
5662
|
+
self.status_label.setText(f"Processed: {self._item_label(itm)}")
|
|
5611
5663
|
QApplication.processEvents()
|
|
5612
5664
|
|
|
5613
5665
|
# ------------------------------------------------------------
|
|
@@ -6497,7 +6549,7 @@ class MosaicMasterDialog(QDialog):
|
|
|
6497
6549
|
- If refinement fails, returns None.
|
|
6498
6550
|
"""
|
|
6499
6551
|
print("\n--- Starting Refined Alignment ---")
|
|
6500
|
-
poly_degree = self.settings
|
|
6552
|
+
poly_degree = qs_int(self.settings, "mosaic/poly_degree", 3)
|
|
6501
6553
|
self.status_label.setText("Refinement: Converting images to grayscale...")
|
|
6502
6554
|
QApplication.processEvents()
|
|
6503
6555
|
|