setiastrosuitepro 1.7.1.post2__py3-none-any.whl → 1.7.4__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/3dplanet.png +0 -0
- setiastro/saspro/__init__.py +20 -8
- setiastro/saspro/__main__.py +349 -290
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/abe.py +4 -4
- setiastro/saspro/autostretch.py +29 -18
- setiastro/saspro/doc_manager.py +4 -1
- setiastro/saspro/gui/main_window.py +46 -7
- setiastro/saspro/gui/mixins/file_mixin.py +6 -2
- setiastro/saspro/gui/mixins/menu_mixin.py +1 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +9 -2
- setiastro/saspro/imageops/serloader.py +101 -17
- setiastro/saspro/layers.py +186 -10
- setiastro/saspro/layers_dock.py +198 -5
- setiastro/saspro/legacy/image_manager.py +10 -4
- setiastro/saspro/legacy/numba_utils.py +301 -119
- setiastro/saspro/numba_utils.py +998 -270
- setiastro/saspro/ops/settings.py +6 -6
- setiastro/saspro/pixelmath.py +1 -1
- setiastro/saspro/planetprojection.py +4059 -0
- setiastro/saspro/resources.py +2 -0
- setiastro/saspro/save_options.py +45 -13
- setiastro/saspro/ser_stack_config.py +21 -1
- setiastro/saspro/ser_stacker.py +8 -2
- setiastro/saspro/ser_stacker_dialog.py +37 -10
- setiastro/saspro/ser_tracking.py +57 -35
- setiastro/saspro/serviewer.py +164 -16
- setiastro/saspro/sfcc.py +14 -8
- setiastro/saspro/stacking_suite.py +292 -111
- setiastro/saspro/subwindow.py +64 -36
- setiastro/saspro/translations/all_source_strings.json +2 -2
- setiastro/saspro/translations/ar_translations.py +3 -3
- setiastro/saspro/translations/de_translations.py +2 -2
- setiastro/saspro/translations/es_translations.py +2 -2
- setiastro/saspro/translations/fr_translations.py +2 -2
- setiastro/saspro/translations/hi_translations.py +2 -2
- setiastro/saspro/translations/it_translations.py +2 -2
- setiastro/saspro/translations/ja_translations.py +2 -2
- setiastro/saspro/translations/pt_translations.py +2 -2
- setiastro/saspro/translations/ru_translations.py +2 -2
- setiastro/saspro/translations/saspro_ar.ts +2 -2
- setiastro/saspro/translations/saspro_de.ts +4 -4
- setiastro/saspro/translations/saspro_es.ts +2 -2
- setiastro/saspro/translations/saspro_fr.ts +2 -2
- setiastro/saspro/translations/saspro_hi.ts +2 -2
- setiastro/saspro/translations/saspro_it.ts +4 -4
- setiastro/saspro/translations/saspro_ja.ts +2 -2
- setiastro/saspro/translations/saspro_pt.ts +2 -2
- setiastro/saspro/translations/saspro_ru.ts +2 -2
- setiastro/saspro/translations/saspro_sw.ts +2 -2
- setiastro/saspro/translations/saspro_uk.ts +2 -2
- setiastro/saspro/translations/saspro_zh.ts +2 -2
- setiastro/saspro/translations/sw_translations.py +2 -2
- setiastro/saspro/translations/uk_translations.py +2 -2
- setiastro/saspro/translations/zh_translations.py +2 -2
- setiastro/saspro/window_shelf.py +62 -1
- {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.4.dist-info}/METADATA +1 -1
- {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.4.dist-info}/RECORD +62 -60
- {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.4.dist-info}/entry_points.txt +1 -1
- {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.4.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.4.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.4.dist-info}/licenses/license.txt +0 -0
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
# Auto-generated at build time. Do not edit.
|
|
2
|
-
BUILD_TIMESTAMP = "2026-01-
|
|
3
|
-
APP_VERSION = "1.7.
|
|
2
|
+
BUILD_TIMESTAMP = "2026-01-21T15:42:11Z"
|
|
3
|
+
APP_VERSION = "1.7.4"
|
setiastro/saspro/abe.py
CHANGED
|
@@ -951,7 +951,7 @@ class ABEDialog(QDialog):
|
|
|
951
951
|
|
|
952
952
|
# show autostretched or raw; siril_style_autostretch() already clips its result
|
|
953
953
|
src_to_show = (hard_autostretch(self._preview_source_f01, target_median=0.5, sigma=2,
|
|
954
|
-
linked=False,
|
|
954
|
+
linked=False, use_24bit=True)
|
|
955
955
|
if getattr(self, "_autostretch_on", False) else self._preview_source_f01)
|
|
956
956
|
|
|
957
957
|
if src_to_show.ndim == 2 or (src_to_show.ndim == 3 and src_to_show.shape[2] == 1):
|
|
@@ -1229,7 +1229,7 @@ class ABEDialog(QDialog):
|
|
|
1229
1229
|
self._preview_source_f01 = a # ← no np.clip
|
|
1230
1230
|
|
|
1231
1231
|
src_to_show = (hard_autostretch(self._preview_source_f01, target_median=0.5, sigma=2,
|
|
1232
|
-
linked=False,
|
|
1232
|
+
linked=False, use_24bit=True)
|
|
1233
1233
|
if getattr(self, "_autostretch_on", False) else self._preview_source_f01)
|
|
1234
1234
|
|
|
1235
1235
|
if src_to_show.ndim == 2 or (src_to_show.ndim == 3 and src_to_show.shape[2] == 1):
|
|
@@ -1349,7 +1349,7 @@ class ABEDialog(QDialog):
|
|
|
1349
1349
|
# Prefer float source (avoids 8-bit clipping); fall back to decoding _last_preview if needed
|
|
1350
1350
|
arr = self._preview_source_f01 if self._preview_source_f01 is not None else (self._last_preview.astype(np.float32)/255.0)
|
|
1351
1351
|
|
|
1352
|
-
stretched = hard_autostretch(arr, target_median=0.5, sigma=2, linked=False,
|
|
1352
|
+
stretched = hard_autostretch(arr, target_median=0.5, sigma=2, linked=False, use_24bit=True)
|
|
1353
1353
|
|
|
1354
1354
|
buf8 = (np.clip(stretched, 0.0, 1.0) * 255.0).astype(np.uint8)
|
|
1355
1355
|
if buf8.ndim == 2:
|
|
@@ -1367,7 +1367,7 @@ class ABEDialog(QDialog):
|
|
|
1367
1367
|
if self._preview_source_f01 is None:
|
|
1368
1368
|
return
|
|
1369
1369
|
stretched = hard_autostretch(self._preview_source_f01, target_median=0.5, sigma=2,
|
|
1370
|
-
linked=False,
|
|
1370
|
+
linked=False, use_24bit=True)
|
|
1371
1371
|
buf8 = (np.clip(stretched, 0.0, 1.0) * 255.0).astype(np.uint8)
|
|
1372
1372
|
if buf8.ndim == 2:
|
|
1373
1373
|
buf8 = np.stack([buf8] * 3, axis=-1)
|
setiastro/saspro/autostretch.py
CHANGED
|
@@ -4,12 +4,18 @@ import numpy as np
|
|
|
4
4
|
_MAX_STATS_PIXELS = 1_000_000
|
|
5
5
|
_DEFAULT_SIGMA = 3
|
|
6
6
|
_U8_MAX = 4095 # 12-bit output for better gradations than 255
|
|
7
|
-
|
|
7
|
+
_U24_MAX = 16777215 # 24-bit output for better gradations
|
|
8
8
|
|
|
9
9
|
# ---------- helpers (generic N-level pipeline) ----------
|
|
10
10
|
def _to_uN(a: np.ndarray, maxv: int) -> np.ndarray:
|
|
11
|
-
"""Convert to uint8/uint16 [0..maxv] for cheap hist/LUT work."""
|
|
12
|
-
|
|
11
|
+
"""Convert to uint8/uint16/uint32 [0..maxv] for cheap hist/LUT work."""
|
|
12
|
+
# uint8 for maxv <= 255, uint16 for 256..65535, uint32 for larger (24-bit)
|
|
13
|
+
if maxv > 65535:
|
|
14
|
+
tgt_dtype = np.uint32
|
|
15
|
+
elif maxv > 255:
|
|
16
|
+
tgt_dtype = np.uint16
|
|
17
|
+
else:
|
|
18
|
+
tgt_dtype = np.uint8
|
|
13
19
|
if a.dtype == tgt_dtype:
|
|
14
20
|
return a
|
|
15
21
|
if np.issubdtype(a.dtype, np.integer):
|
|
@@ -17,10 +23,12 @@ def _to_uN(a: np.ndarray, maxv: int) -> np.ndarray:
|
|
|
17
23
|
if info.max <= 0:
|
|
18
24
|
return np.zeros_like(a, dtype=tgt_dtype)
|
|
19
25
|
scaled = np.clip(a.astype(np.float32), 0, info.max) * (maxv / float(info.max))
|
|
20
|
-
|
|
26
|
+
# Clamp to maxv to avoid index-out-of-bounds when used as LUT indices
|
|
27
|
+
return np.minimum((scaled + 0.5).astype(tgt_dtype), maxv)
|
|
21
28
|
# float-ish
|
|
22
29
|
af = np.clip(a.astype(np.float32), 0.0, 1.0)
|
|
23
|
-
|
|
30
|
+
# Clamp to maxv: when af=1.0, af*maxv+0.5 can exceed maxv
|
|
31
|
+
return np.minimum((af * maxv + 0.5).astype(tgt_dtype), maxv)
|
|
24
32
|
|
|
25
33
|
def _choose_stride(h: int, w: int, max_pixels: int) -> tuple[int, int]:
|
|
26
34
|
n = h * w
|
|
@@ -132,29 +140,32 @@ def autostretch(
|
|
|
132
140
|
linked: bool = False,
|
|
133
141
|
sigma: float = _DEFAULT_SIGMA,
|
|
134
142
|
*,
|
|
135
|
-
|
|
143
|
+
use_24bit: bool | None = None,
|
|
144
|
+
use_16bit: bool | None = None, # <-- legacy compat (ignored / mapped)
|
|
145
|
+
**_ignored_kwargs, # <-- swallow any other legacy flags safely
|
|
136
146
|
) -> np.ndarray:
|
|
137
|
-
"""
|
|
138
|
-
High-quality autostretch that can operate in 16-bit (HQ, default) or 8-bit (fast) mode.
|
|
139
|
-
|
|
140
|
-
• 16-bit mode: smooth gradients, minimal posterization (recommended).
|
|
141
|
-
• 8-bit mode: slightly faster on very large images, lower fidelity.
|
|
142
147
|
|
|
143
|
-
If use_16bit is None, we try to read QSettings("display/autostretch_16bit") and
|
|
144
|
-
default to True on failure (no Qt in context).
|
|
145
|
-
"""
|
|
146
148
|
if img is None:
|
|
147
149
|
return None
|
|
148
150
|
|
|
151
|
+
# ---- legacy compat -------------------------------------------------
|
|
152
|
+
# Old callers may pass use_16bit. We no longer support 16-bit preview output.
|
|
153
|
+
# If they pass it, we just treat it as "use higher precision display", i.e. 24-bit.
|
|
154
|
+
if use_16bit is not None:
|
|
155
|
+
# If caller explicitly asked for 16-bit, we interpret that as "high precision".
|
|
156
|
+
# Only override if caller didn't explicitly pass use_24bit.
|
|
157
|
+
if use_24bit is None:
|
|
158
|
+
use_24bit = True
|
|
159
|
+
|
|
149
160
|
# Optional auto-read from QSettings if caller didn’t pass a flag.
|
|
150
|
-
if
|
|
161
|
+
if use_24bit is None:
|
|
151
162
|
try:
|
|
152
163
|
from PyQt6.QtCore import QSettings
|
|
153
|
-
|
|
164
|
+
use_24bit = QSettings().value("display/autostretch_24bit", True, type=bool)
|
|
154
165
|
except Exception:
|
|
155
|
-
|
|
166
|
+
use_24bit = True
|
|
156
167
|
|
|
157
|
-
maxv =
|
|
168
|
+
maxv = _U24_MAX if use_24bit else _U8_MAX
|
|
158
169
|
a = np.asarray(img)
|
|
159
170
|
|
|
160
171
|
# MONO (or pseudo-mono)
|
setiastro/saspro/doc_manager.py
CHANGED
|
@@ -2276,6 +2276,7 @@ class DocManager(QObject):
|
|
|
2276
2276
|
bit_depth: str | None = None,
|
|
2277
2277
|
*,
|
|
2278
2278
|
bit_depth_override: str | None = None,
|
|
2279
|
+
jpeg_quality: int | None = None, # <-- NEW
|
|
2279
2280
|
):
|
|
2280
2281
|
"""
|
|
2281
2282
|
Save the given ImageDocument to 'path'.
|
|
@@ -2289,7 +2290,8 @@ class DocManager(QObject):
|
|
|
2289
2290
|
ext = _normalize_ext(os.path.splitext(path)[1])
|
|
2290
2291
|
img = doc.image
|
|
2291
2292
|
meta = doc.metadata or {}
|
|
2292
|
-
|
|
2293
|
+
if jpeg_quality is not None:
|
|
2294
|
+
meta["jpeg_quality"] = int(jpeg_quality)
|
|
2293
2295
|
# ── MASSIVE DEBUG: show everything we know coming in ───────────────
|
|
2294
2296
|
debug_dump_metadata_print(meta, context="save_document: BEFORE HEADER PICK")
|
|
2295
2297
|
|
|
@@ -2359,6 +2361,7 @@ class DocManager(QObject):
|
|
|
2359
2361
|
image_meta=meta.get("image_meta"),
|
|
2360
2362
|
file_meta=meta.get("file_meta"),
|
|
2361
2363
|
wcs_header=meta.get("wcs_header"),
|
|
2364
|
+
jpeg_quality=jpeg_quality,
|
|
2362
2365
|
)
|
|
2363
2366
|
|
|
2364
2367
|
# ── Update metadata in memory to match what we just wrote ─────────
|
|
@@ -196,7 +196,7 @@ from setiastro.saspro.resources import (
|
|
|
196
196
|
colorwheel_path, font_path, csv_icon_path, spinner_path, wims_path, narrowbandnormalization_path,
|
|
197
197
|
wimi_path, linearfit_path, debayer_path, aberration_path, acv_icon_path,
|
|
198
198
|
functionbundles_path, viewbundles_path, selectivecolor_path, rgbalign_path, planetarystacker_path,
|
|
199
|
-
background_path, script_icon_path
|
|
199
|
+
background_path, script_icon_path, planetprojection_path,
|
|
200
200
|
)
|
|
201
201
|
|
|
202
202
|
import faulthandler
|
|
@@ -435,7 +435,23 @@ class AstroSuiteProMainWindow(
|
|
|
435
435
|
version: str = "dev", build_timestamp: str = "dev"):
|
|
436
436
|
super().__init__(parent)
|
|
437
437
|
# Prevent white flash: start strictly transparent and force dark bg
|
|
438
|
-
|
|
438
|
+
from PyQt6.QtGui import QGuiApplication
|
|
439
|
+
|
|
440
|
+
def _is_wayland() -> bool:
|
|
441
|
+
try:
|
|
442
|
+
plat = (QGuiApplication.platformName() or "").lower()
|
|
443
|
+
if "wayland" in plat:
|
|
444
|
+
return True
|
|
445
|
+
except Exception:
|
|
446
|
+
pass
|
|
447
|
+
# fallback env checks
|
|
448
|
+
return bool(os.environ.get("WAYLAND_DISPLAY")) and not bool(os.environ.get("DISPLAY"))
|
|
449
|
+
|
|
450
|
+
# Prevent white flash: start strictly transparent and force dark bg
|
|
451
|
+
if not _is_wayland():
|
|
452
|
+
self.setWindowOpacity(0.0)
|
|
453
|
+
self.setStyleSheet("QMainWindow { background-color: #0F0F19; }")
|
|
454
|
+
|
|
439
455
|
self.setStyleSheet("QMainWindow { background-color: #0F0F19; }")
|
|
440
456
|
#self._stall = UiStallDetector(self, interval_ms=50, threshold_ms=250)
|
|
441
457
|
#self._stall.start()
|
|
@@ -675,8 +691,8 @@ class AstroSuiteProMainWindow(
|
|
|
675
691
|
self.apply_theme_from_settings()
|
|
676
692
|
self._populate_view_panels_menu()
|
|
677
693
|
# Startup check (no lambdas)
|
|
678
|
-
if self.settings.value("updates/check_on_startup", True, type=bool):
|
|
679
|
-
|
|
694
|
+
#if self.settings.value("updates/check_on_startup", True, type=bool):
|
|
695
|
+
# QTimer.singleShot(1500, self.check_for_updates_startup)
|
|
680
696
|
|
|
681
697
|
self._hdr_refresh_timer = QTimer(self)
|
|
682
698
|
self._hdr_refresh_timer.setSingleShot(True)
|
|
@@ -1457,7 +1473,7 @@ class AstroSuiteProMainWindow(
|
|
|
1457
1473
|
|
|
1458
1474
|
def _open_user_scripts_github(self):
|
|
1459
1475
|
# User script examples on GitHub
|
|
1460
|
-
url = QUrl("https://
|
|
1476
|
+
url = QUrl("https://drive.google.com/drive/folders/1TSxKZey4R_t7F2RsB53Hd1SBIGXv3-Nl?usp=drive_link")
|
|
1461
1477
|
QDesktopServices.openUrl(url)
|
|
1462
1478
|
|
|
1463
1479
|
def _open_scripts_discord_forum(self):
|
|
@@ -2782,7 +2798,7 @@ class AstroSuiteProMainWindow(
|
|
|
2782
2798
|
target = float(self.settings.value("display/target", 0.30, type=float))
|
|
2783
2799
|
sigma = float(self.settings.value("display/sigma", 5.0, type=float))
|
|
2784
2800
|
linked = bool(self.settings.value("display/stretch_linked", False, type=bool))
|
|
2785
|
-
|
|
2801
|
+
use_24 = self.settings.value("display/autostretch_24bit", True, type=bool)
|
|
2786
2802
|
|
|
2787
2803
|
# if your view exposes per-view overrides, prefer those
|
|
2788
2804
|
if hasattr(view, "autostretch_target"):
|
|
@@ -2808,7 +2824,7 @@ class AstroSuiteProMainWindow(
|
|
|
2808
2824
|
target_median=target,
|
|
2809
2825
|
linked=linked,
|
|
2810
2826
|
sigma=sigma,
|
|
2811
|
-
|
|
2827
|
+
use_24bit=use_24,
|
|
2812
2828
|
)
|
|
2813
2829
|
except Exception as e:
|
|
2814
2830
|
QMessageBox.warning(self, "Display-Stretch", f"Failed to apply autostretch:\n{e}")
|
|
@@ -4280,6 +4296,29 @@ class AstroSuiteProMainWindow(
|
|
|
4280
4296
|
dlg.setWindowIcon(QIcon(planetarystacker_path))
|
|
4281
4297
|
dlg.show()
|
|
4282
4298
|
|
|
4299
|
+
def _open_planet_projection(self):
|
|
4300
|
+
from setiastro.saspro.planetprojection import PlanetProjectionDialog
|
|
4301
|
+
sw = self.mdi.activeSubWindow()
|
|
4302
|
+
if not sw:
|
|
4303
|
+
QMessageBox.information(self, "No image", "Open an image first.")
|
|
4304
|
+
return
|
|
4305
|
+
|
|
4306
|
+
view = sw.widget()
|
|
4307
|
+
doc = self.doc_manager.get_document_for_view(view)
|
|
4308
|
+
|
|
4309
|
+
dlg = PlanetProjectionDialog(self, doc)
|
|
4310
|
+
try:
|
|
4311
|
+
# dlg.setWindowIcon(QIcon(planetprojection_path))
|
|
4312
|
+
pass
|
|
4313
|
+
except Exception:
|
|
4314
|
+
pass
|
|
4315
|
+
dlg.resize(980, 720)
|
|
4316
|
+
dlg.setWindowFlag(Qt.WindowType.Window, True)
|
|
4317
|
+
dlg.setWindowIcon(QIcon(planetprojection_path))
|
|
4318
|
+
dlg.show()
|
|
4319
|
+
self._log("Functions: opened Planet Projection.")
|
|
4320
|
+
|
|
4321
|
+
|
|
4283
4322
|
def _open_stacking_suite(self):
|
|
4284
4323
|
# Reuse if we already have one
|
|
4285
4324
|
dlg = getattr(self, "_stacking_suite", None)
|
|
@@ -216,14 +216,18 @@ class FileMixin:
|
|
|
216
216
|
# --- Bit depth selection ----------------------------------------
|
|
217
217
|
from setiastro.saspro.save_options import SaveOptionsDialog
|
|
218
218
|
current_bd = doc.metadata.get("bit_depth")
|
|
219
|
-
|
|
219
|
+
current_jq = (doc.metadata or {}).get("jpeg_quality", None)
|
|
220
|
+
|
|
221
|
+
dlg = SaveOptionsDialog(self, ext_norm, current_bd, current_jpeg_quality=current_jq)
|
|
220
222
|
if dlg.exec() != dlg.DialogCode.Accepted:
|
|
221
223
|
return
|
|
224
|
+
|
|
222
225
|
chosen_bd = dlg.selected_bit_depth()
|
|
226
|
+
chosen_jq = dlg.selected_jpeg_quality()
|
|
223
227
|
|
|
224
228
|
# --- Save & remember folder ----------------------------------------
|
|
225
229
|
try:
|
|
226
|
-
self.docman.save_document(doc, path, bit_depth_override=chosen_bd)
|
|
230
|
+
self.docman.save_document(doc, path, bit_depth_override=chosen_bd, jpeg_quality=chosen_jq)
|
|
227
231
|
self._log(f"Saved: {path} ({chosen_bd})")
|
|
228
232
|
self.settings.setValue("paths/last_save_dir", os.path.dirname(path))
|
|
229
233
|
except Exception as e:
|
|
@@ -207,6 +207,7 @@ class MenuMixin:
|
|
|
207
207
|
m_star.addAction(self.act_isophote)
|
|
208
208
|
m_star.addAction(self.act_live_stacking)
|
|
209
209
|
m_star.addAction(self.act_mosaic_master)
|
|
210
|
+
m_star.addAction(self.act_planet_projection)
|
|
210
211
|
m_star.addAction(self.act_planetary_stacker)
|
|
211
212
|
m_star.addAction(self.act_plate_solve)
|
|
212
213
|
m_star.addAction(self.act_psf_viewer)
|
|
@@ -36,7 +36,7 @@ from setiastro.saspro.resources import (
|
|
|
36
36
|
nbtorgb_path, freqsep_path, multiscale_decomp_path, contsub_path, halo_path, cosmic_path,
|
|
37
37
|
satellite_path, imagecombine_path, wims_path, wimi_path, linearfit_path,
|
|
38
38
|
debayer_path, aberration_path, functionbundles_path, viewbundles_path, planetarystacker_path,
|
|
39
|
-
selectivecolor_path, rgbalign_path,
|
|
39
|
+
selectivecolor_path, rgbalign_path, planetprojection_path,
|
|
40
40
|
)
|
|
41
41
|
|
|
42
42
|
# Import shortcuts module
|
|
@@ -307,6 +307,7 @@ class ToolbarMixin:
|
|
|
307
307
|
tb_star.addAction(self.act_stacking_suite)
|
|
308
308
|
tb_star.addAction(self.act_live_stacking)
|
|
309
309
|
tb_star.addAction(self.act_planetary_stacker)
|
|
310
|
+
tb_star.addAction(self.act_planet_projection)
|
|
310
311
|
tb_star.addAction(self.act_plate_solve)
|
|
311
312
|
tb_star.addAction(self.act_star_align)
|
|
312
313
|
tb_star.addAction(self.act_star_register)
|
|
@@ -1195,6 +1196,11 @@ class ToolbarMixin:
|
|
|
1195
1196
|
self.act_planetary_stacker.setStatusTip(self.tr("Stack SER videos (planetary/solar/lunar)"))
|
|
1196
1197
|
self.act_planetary_stacker.triggered.connect(self._open_planetary_stacker)
|
|
1197
1198
|
|
|
1199
|
+
self.act_planet_projection = QAction(QIcon(planetprojection_path), self.tr("3D Projection..."), self)
|
|
1200
|
+
self.act_planet_projection.setIconVisibleInMenu(True)
|
|
1201
|
+
self.act_planet_projection.setStatusTip(self.tr("View your planets with stereographic projection"))
|
|
1202
|
+
self.act_planet_projection.triggered.connect(self._open_planet_projection)
|
|
1203
|
+
|
|
1198
1204
|
self.act_plate_solve = QAction(QIcon(platesolve_path), self.tr("Plate Solver..."), self)
|
|
1199
1205
|
self.act_plate_solve.setIconVisibleInMenu(True)
|
|
1200
1206
|
self.act_plate_solve.setStatusTip(self.tr("Solve WCS/SIP for the active image or a file"))
|
|
@@ -1272,7 +1278,7 @@ class ToolbarMixin:
|
|
|
1272
1278
|
self.act_script_editor.setStatusTip(self.tr("Open the built-in script editor"))
|
|
1273
1279
|
self.act_script_editor.triggered.connect(self._show_script_editor)
|
|
1274
1280
|
|
|
1275
|
-
self.act_open_user_scripts_github = QAction(self.tr("Open User Scripts (
|
|
1281
|
+
self.act_open_user_scripts_github = QAction(self.tr("Open User Scripts (GoogleDrive)..."), self)
|
|
1276
1282
|
self.act_open_user_scripts_github.triggered.connect(self._open_user_scripts_github)
|
|
1277
1283
|
|
|
1278
1284
|
self.act_open_scripts_discord = QAction(self.tr("Open Scripts Forum (Discord)..."), self)
|
|
@@ -1439,6 +1445,7 @@ class ToolbarMixin:
|
|
|
1439
1445
|
reg("image_peeker", self.act_image_peeker)
|
|
1440
1446
|
reg("live_stacking", self.act_live_stacking)
|
|
1441
1447
|
reg("stacking_suite", self.act_stacking_suite)
|
|
1448
|
+
reg("planet_projection", self.act_planet_projection)
|
|
1442
1449
|
reg("supernova_hunter", self.act_supernova_hunter)
|
|
1443
1450
|
reg("star_spikes", self.act_star_spikes)
|
|
1444
1451
|
reg("astrospike", self.act_astrospike)
|
|
@@ -10,6 +10,7 @@ from typing import Optional, Tuple, Dict, List, Sequence, Union, Callable
|
|
|
10
10
|
from collections import OrderedDict
|
|
11
11
|
import numpy as np
|
|
12
12
|
import time
|
|
13
|
+
from PyQt6 import sip
|
|
13
14
|
|
|
14
15
|
import cv2
|
|
15
16
|
|
|
@@ -115,6 +116,20 @@ def _bytes_per_sample(pixel_depth_bits: int) -> int:
|
|
|
115
116
|
def _is_bayer(color_name: str) -> bool:
|
|
116
117
|
return color_name in BAYER_NAMES
|
|
117
118
|
|
|
119
|
+
def _roi_unrotate_180(roi: Tuple[int, int, int, int], W: int, H: int) -> Tuple[int, int, int, int]:
|
|
120
|
+
"""
|
|
121
|
+
Convert an ROI specified in *post-rot180/display coords* into *raw/pre-rot coords*.
|
|
122
|
+
"""
|
|
123
|
+
x, y, w, h = [int(v) for v in roi]
|
|
124
|
+
x2 = x + w
|
|
125
|
+
y2 = y + h
|
|
126
|
+
|
|
127
|
+
x_raw = int(W - x2)
|
|
128
|
+
y_raw = int(H - y2)
|
|
129
|
+
|
|
130
|
+
return (x_raw, y_raw, int(w), int(h))
|
|
131
|
+
|
|
132
|
+
|
|
118
133
|
def _rot180(img: np.ndarray) -> np.ndarray:
|
|
119
134
|
# Works for mono (H,W) and RGB(A) (H,W,C)
|
|
120
135
|
return img[::-1, ::-1].copy()
|
|
@@ -847,7 +862,9 @@ class SERReader:
|
|
|
847
862
|
|
|
848
863
|
# ROI (apply before debayer; for Bayer enforce even-even origin)
|
|
849
864
|
if roi is not None:
|
|
850
|
-
|
|
865
|
+
# ✅ roi is specified in DISPLAY coords (post-rot180)
|
|
866
|
+
x, y, w, h = _roi_unrotate_180(roi, meta.width, meta.height)
|
|
867
|
+
|
|
851
868
|
x = max(0, min(meta.width - 1, x))
|
|
852
869
|
y = max(0, min(meta.height - 1, y))
|
|
853
870
|
w = max(1, min(meta.width - x, w))
|
|
@@ -862,7 +879,6 @@ class SERReader:
|
|
|
862
879
|
|
|
863
880
|
# --- SER global orientation fix (rotate 180) ---
|
|
864
881
|
img = _rot180(img)
|
|
865
|
-
|
|
866
882
|
# Convert BGR->RGB if needed
|
|
867
883
|
if color_name == "BGR" and img.ndim == 3 and img.shape[2] >= 3:
|
|
868
884
|
img = img[..., ::-1].copy()
|
|
@@ -1075,15 +1091,16 @@ class AVIReader(PlanetaryFrameSource):
|
|
|
1075
1091
|
|
|
1076
1092
|
# ROI first (but if we are going to debayer mosaic, ROI origin must be even-even)
|
|
1077
1093
|
if roi is not None:
|
|
1078
|
-
x, y, w, h = [int(v) for v in roi]
|
|
1079
1094
|
H, W = frame.shape[:2]
|
|
1095
|
+
|
|
1096
|
+
# ✅ roi is specified in DISPLAY coords (post-rot180)
|
|
1097
|
+
x, y, w, h = _roi_unrotate_180(roi, W, H)
|
|
1098
|
+
|
|
1080
1099
|
x = max(0, min(W - 1, x))
|
|
1081
1100
|
y = max(0, min(H - 1, y))
|
|
1082
1101
|
w = max(1, min(W - x, w))
|
|
1083
1102
|
h = max(1, min(H - y, h))
|
|
1084
1103
|
|
|
1085
|
-
# If user explicitly requests debayering, preserve Bayer phase
|
|
1086
|
-
# (even-even origin) exactly like SER
|
|
1087
1104
|
if debayer and user_pat is not None:
|
|
1088
1105
|
x, y = _roi_evenize_for_bayer(x, y)
|
|
1089
1106
|
w = max(1, min(W - x, w))
|
|
@@ -1160,13 +1177,45 @@ class AVIReader(PlanetaryFrameSource):
|
|
|
1160
1177
|
|
|
1161
1178
|
def _imread_any(path: str) -> np.ndarray:
|
|
1162
1179
|
"""
|
|
1163
|
-
Read PNG/JPG/TIF/etc into numpy.
|
|
1164
|
-
Tries cv2 first (fast), falls back to PIL.
|
|
1180
|
+
Read PNG/JPG/TIF/FITS/etc into numpy.
|
|
1165
1181
|
Returns:
|
|
1166
|
-
- grayscale: (H,W)
|
|
1167
|
-
- color: (H,W,3)
|
|
1182
|
+
- grayscale: (H,W)
|
|
1183
|
+
- color: (H,W,3) in RGB when applicable
|
|
1168
1184
|
"""
|
|
1169
1185
|
p = os.fspath(path)
|
|
1186
|
+
ext = os.path.splitext(p)[1].lower()
|
|
1187
|
+
|
|
1188
|
+
# ---- FITS / FIT ----
|
|
1189
|
+
if ext in (".fit", ".fits"):
|
|
1190
|
+
try:
|
|
1191
|
+
from astropy.io import fits
|
|
1192
|
+
|
|
1193
|
+
data = fits.getdata(p, memmap=False)
|
|
1194
|
+
if data is None:
|
|
1195
|
+
raise ValueError("Empty FITS data.")
|
|
1196
|
+
|
|
1197
|
+
arr = np.asarray(data)
|
|
1198
|
+
|
|
1199
|
+
# Common shapes:
|
|
1200
|
+
# (H,W) -> mono
|
|
1201
|
+
# (C,H,W) -> convert to (H,W,C)
|
|
1202
|
+
# (H,W,C) -> already fine
|
|
1203
|
+
if arr.ndim == 3:
|
|
1204
|
+
# If first axis is small (1/3/4), assume CHW
|
|
1205
|
+
if arr.shape[0] in (1, 3, 4) and arr.shape[1] > 8 and arr.shape[2] > 8:
|
|
1206
|
+
arr = np.moveaxis(arr, 0, -1) # CHW -> HWC
|
|
1207
|
+
|
|
1208
|
+
# If now HWC and has alpha, drop it
|
|
1209
|
+
if arr.shape[-1] >= 4:
|
|
1210
|
+
arr = arr[..., :3]
|
|
1211
|
+
|
|
1212
|
+
# If single channel, squeeze
|
|
1213
|
+
if arr.shape[-1] == 1:
|
|
1214
|
+
arr = arr[..., 0]
|
|
1215
|
+
|
|
1216
|
+
return arr
|
|
1217
|
+
except Exception as e:
|
|
1218
|
+
raise RuntimeError(f"Failed to read FITS: {p}\n{e}")
|
|
1170
1219
|
|
|
1171
1220
|
# Prefer cv2 if available
|
|
1172
1221
|
if cv2 is not None:
|
|
@@ -1194,6 +1243,47 @@ def _imread_any(path: str) -> np.ndarray:
|
|
|
1194
1243
|
im = im.convert("RGB")
|
|
1195
1244
|
return np.array(im)
|
|
1196
1245
|
|
|
1246
|
+
def _to_float01_robust(img: np.ndarray) -> np.ndarray:
|
|
1247
|
+
"""
|
|
1248
|
+
Robust float01 conversion for preview:
|
|
1249
|
+
- uint8/uint16/int types scale by dtype max
|
|
1250
|
+
- float types:
|
|
1251
|
+
* if already ~[0,1], keep
|
|
1252
|
+
* else percentile-scale (0.1..99.9) to [0,1]
|
|
1253
|
+
Works for mono or RGB arrays.
|
|
1254
|
+
"""
|
|
1255
|
+
a = np.asarray(img)
|
|
1256
|
+
|
|
1257
|
+
if a.dtype == np.uint8:
|
|
1258
|
+
return a.astype(np.float32) / 255.0
|
|
1259
|
+
if a.dtype == np.uint16:
|
|
1260
|
+
return a.astype(np.float32) / 65535.0
|
|
1261
|
+
if np.issubdtype(a.dtype, np.integer):
|
|
1262
|
+
info = np.iinfo(a.dtype)
|
|
1263
|
+
denom = float(info.max) if info.max > 0 else 1.0
|
|
1264
|
+
return (a.astype(np.float32) / denom).clip(0.0, 1.0)
|
|
1265
|
+
|
|
1266
|
+
# float path
|
|
1267
|
+
f = a.astype(np.float32, copy=False)
|
|
1268
|
+
|
|
1269
|
+
# If it already looks like [0,1], don’t mess with it
|
|
1270
|
+
mn = float(np.nanmin(f))
|
|
1271
|
+
mx = float(np.nanmax(f))
|
|
1272
|
+
if (mn >= -1e-3) and (mx <= 1.0 + 1e-3):
|
|
1273
|
+
return np.clip(f, 0.0, 1.0)
|
|
1274
|
+
|
|
1275
|
+
# Percentile scale (planetary-friendly)
|
|
1276
|
+
lo = float(np.nanpercentile(f, 0.1))
|
|
1277
|
+
hi = float(np.nanpercentile(f, 99.9))
|
|
1278
|
+
if not np.isfinite(lo) or not np.isfinite(hi) or hi <= lo + 1e-12:
|
|
1279
|
+
# fallback to min/max
|
|
1280
|
+
lo, hi = mn, mx
|
|
1281
|
+
if not np.isfinite(lo) or not np.isfinite(hi) or hi <= lo + 1e-12:
|
|
1282
|
+
return np.zeros_like(f, dtype=np.float32)
|
|
1283
|
+
|
|
1284
|
+
out = (f - lo) / (hi - lo)
|
|
1285
|
+
return np.clip(out, 0.0, 1.0)
|
|
1286
|
+
|
|
1197
1287
|
|
|
1198
1288
|
def _infer_bit_depth(arr: np.ndarray) -> int:
|
|
1199
1289
|
if arr.dtype == np.uint16:
|
|
@@ -1295,13 +1385,7 @@ class ImageSequenceReader(PlanetaryFrameSource):
|
|
|
1295
1385
|
|
|
1296
1386
|
# Normalize to float01
|
|
1297
1387
|
if to_float01:
|
|
1298
|
-
|
|
1299
|
-
img = img.astype(np.float32) / 255.0
|
|
1300
|
-
elif img.dtype == np.uint16:
|
|
1301
|
-
img = img.astype(np.float32) / 65535.0
|
|
1302
|
-
else:
|
|
1303
|
-
img = img.astype(np.float32)
|
|
1304
|
-
img = np.clip(img, 0.0, 1.0)
|
|
1388
|
+
img = _to_float01_robust(img)
|
|
1305
1389
|
|
|
1306
1390
|
self._cache.put(key, img)
|
|
1307
1391
|
return img
|
|
@@ -1339,7 +1423,7 @@ def open_planetary_source(
|
|
|
1339
1423
|
return AVIReader(path, cache_items=cache_items)
|
|
1340
1424
|
|
|
1341
1425
|
# If user passes a single image, treat it as a 1-frame sequence
|
|
1342
|
-
if ext in (".png", ".tif", ".tiff", ".jpg", ".jpeg", ".bmp", ".webp"):
|
|
1426
|
+
if ext in (".png", ".tif", ".tiff", ".jpg", ".jpeg", ".bmp", ".webp", ".fit", ".fits"):
|
|
1343
1427
|
return ImageSequenceReader([path], cache_items=cache_items)
|
|
1344
1428
|
|
|
1345
1429
|
raise ValueError(f"Unsupported input: {path}")
|