setiastrosuitepro 1.6.1__py3-none-any.whl → 1.6.2__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/Background_startup.jpg +0 -0
- setiastro/qml/ResourceMonitor.qml +126 -0
- setiastro/saspro/__main__.py +159 -23
- setiastro/saspro/_generated/build_info.py +2 -1
- setiastro/saspro/abe.py +62 -11
- setiastro/saspro/aberration_ai.py +3 -3
- setiastro/saspro/add_stars.py +5 -2
- setiastro/saspro/astrobin_exporter.py +3 -0
- setiastro/saspro/astrospike_python.py +3 -1
- setiastro/saspro/autostretch.py +4 -2
- setiastro/saspro/backgroundneutral.py +52 -10
- setiastro/saspro/batch_convert.py +3 -0
- setiastro/saspro/batch_renamer.py +3 -0
- setiastro/saspro/blemish_blaster.py +3 -0
- setiastro/saspro/cheat_sheet.py +50 -15
- setiastro/saspro/clahe.py +27 -1
- setiastro/saspro/comet_stacking.py +103 -38
- setiastro/saspro/convo.py +3 -0
- setiastro/saspro/copyastro.py +3 -0
- setiastro/saspro/cosmicclarity.py +70 -45
- setiastro/saspro/crop_dialog_pro.py +17 -0
- setiastro/saspro/curve_editor_pro.py +18 -0
- setiastro/saspro/debayer.py +3 -0
- setiastro/saspro/doc_manager.py +39 -16
- setiastro/saspro/fitsmodifier.py +3 -0
- setiastro/saspro/frequency_separation.py +8 -2
- setiastro/saspro/function_bundle.py +2 -0
- setiastro/saspro/generate_translations.py +715 -1
- setiastro/saspro/ghs_dialog_pro.py +3 -0
- setiastro/saspro/graxpert.py +3 -0
- setiastro/saspro/gui/main_window.py +275 -32
- setiastro/saspro/gui/mixins/dock_mixin.py +100 -1
- setiastro/saspro/gui/mixins/file_mixin.py +7 -0
- setiastro/saspro/gui/mixins/menu_mixin.py +28 -0
- setiastro/saspro/gui/statistics_dialog.py +47 -0
- setiastro/saspro/halobgon.py +29 -3
- setiastro/saspro/histogram.py +3 -0
- setiastro/saspro/history_explorer.py +2 -0
- setiastro/saspro/i18n.py +22 -10
- setiastro/saspro/image_combine.py +3 -0
- setiastro/saspro/image_peeker_pro.py +3 -0
- setiastro/saspro/imageops/stretch.py +5 -13
- setiastro/saspro/isophote.py +3 -0
- setiastro/saspro/legacy/numba_utils.py +64 -47
- setiastro/saspro/linear_fit.py +3 -0
- setiastro/saspro/live_stacking.py +13 -2
- setiastro/saspro/mask_creation.py +3 -0
- setiastro/saspro/mfdeconv.py +5 -0
- setiastro/saspro/morphology.py +30 -5
- setiastro/saspro/multiscale_decomp.py +3 -0
- setiastro/saspro/nbtorgb_stars.py +12 -2
- setiastro/saspro/numba_utils.py +148 -47
- setiastro/saspro/ops/scripts.py +77 -17
- setiastro/saspro/ops/settings.py +1 -43
- setiastro/saspro/perfect_palette_picker.py +1 -0
- setiastro/saspro/pixelmath.py +6 -2
- setiastro/saspro/plate_solver.py +2 -1
- setiastro/saspro/remove_green.py +18 -1
- setiastro/saspro/remove_stars.py +136 -162
- setiastro/saspro/resources.py +7 -0
- setiastro/saspro/rgb_combination.py +1 -0
- setiastro/saspro/rgbalign.py +4 -4
- setiastro/saspro/save_options.py +1 -0
- setiastro/saspro/sfcc.py +50 -8
- setiastro/saspro/signature_insert.py +3 -0
- setiastro/saspro/stacking_suite.py +630 -341
- setiastro/saspro/star_alignment.py +16 -1
- setiastro/saspro/star_spikes.py +116 -32
- setiastro/saspro/star_stretch.py +38 -1
- setiastro/saspro/stat_stretch.py +35 -3
- setiastro/saspro/subwindow.py +63 -2
- setiastro/saspro/supernovaasteroidhunter.py +3 -0
- setiastro/saspro/translations/all_source_strings.json +3654 -0
- setiastro/saspro/translations/ar_translations.py +3865 -0
- setiastro/saspro/translations/de_translations.py +16 -0
- setiastro/saspro/translations/es_translations.py +16 -0
- setiastro/saspro/translations/fr_translations.py +16 -0
- setiastro/saspro/translations/hi_translations.py +3571 -0
- setiastro/saspro/translations/integrate_translations.py +36 -0
- setiastro/saspro/translations/it_translations.py +16 -0
- setiastro/saspro/translations/ja_translations.py +16 -0
- setiastro/saspro/translations/pt_translations.py +16 -0
- setiastro/saspro/translations/ru_translations.py +2848 -0
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +255 -0
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +3 -3
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +3 -3
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +3 -3
- setiastro/saspro/translations/saspro_hi.qm +0 -0
- setiastro/saspro/translations/saspro_hi.ts +257 -0
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +3 -3
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +4 -4
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +3 -3
- setiastro/saspro/translations/saspro_ru.qm +0 -0
- setiastro/saspro/translations/saspro_ru.ts +237 -0
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +257 -0
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +10771 -0
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +3 -3
- setiastro/saspro/translations/sw_translations.py +3671 -0
- setiastro/saspro/translations/uk_translations.py +3700 -0
- setiastro/saspro/translations/zh_translations.py +16 -0
- setiastro/saspro/versioning.py +12 -6
- setiastro/saspro/view_bundle.py +3 -0
- setiastro/saspro/wavescale_hdr.py +22 -1
- setiastro/saspro/wavescalede.py +23 -1
- setiastro/saspro/whitebalance.py +39 -3
- setiastro/saspro/widgets/minigame/game.js +986 -0
- setiastro/saspro/widgets/minigame/index.html +53 -0
- setiastro/saspro/widgets/minigame/style.css +241 -0
- setiastro/saspro/widgets/resource_monitor.py +237 -0
- setiastro/saspro/widgets/wavelet_utils.py +52 -20
- setiastro/saspro/wimi.py +7996 -0
- setiastro/saspro/wims.py +578 -0
- {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/METADATA +15 -4
- {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/RECORD +128 -103
- {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/licenses/license.txt +0 -0
|
@@ -42,12 +42,23 @@ def background_neutralize_rgb(img: np.ndarray, rect_xywh: tuple[int, int, int, i
|
|
|
42
42
|
|
|
43
43
|
out = img.copy()
|
|
44
44
|
eps = 1e-8
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
|
|
46
|
+
# Vectorized neutralization
|
|
47
|
+
# diff shape: (3,) -> (1, 1, 3)
|
|
48
|
+
diffs = (medians - avg_med).reshape(1, 1, 3)
|
|
49
|
+
|
|
50
|
+
# denom shape: (1, 1, 3)
|
|
51
|
+
denoms = 1.0 - diffs
|
|
52
|
+
|
|
53
|
+
# Avoid div-by-zero (vectorized)
|
|
54
|
+
# logic: if abs(denom) < eps, set to eps (sign matched)
|
|
55
|
+
# We can do this efficiently:
|
|
56
|
+
small_mask = np.abs(denoms) < eps
|
|
57
|
+
denoms[small_mask] = np.where(denoms[small_mask] >= 0, eps, -eps)
|
|
58
|
+
|
|
59
|
+
# Apply formula: (pixel - diff) / denom
|
|
60
|
+
out = (out - diffs) / denoms
|
|
61
|
+
out = np.clip(out, 0.0, 1.0)
|
|
51
62
|
|
|
52
63
|
return out.astype(np.float32, copy=False)
|
|
53
64
|
|
|
@@ -237,14 +248,21 @@ def apply_background_neutral_to_doc(doc, preset: dict | None = None):
|
|
|
237
248
|
class BackgroundNeutralizationDialog(QDialog):
|
|
238
249
|
def __init__(self, parent, doc, icon: QIcon | None = None):
|
|
239
250
|
super().__init__(parent)
|
|
251
|
+
self._main = parent
|
|
240
252
|
self.doc = doc
|
|
253
|
+
|
|
254
|
+
# Connect to active document change signal
|
|
255
|
+
if hasattr(self._main, "currentDocumentChanged"):
|
|
256
|
+
self._main.currentDocumentChanged.connect(self._on_active_doc_changed)
|
|
257
|
+
|
|
241
258
|
if icon:
|
|
242
259
|
self.setWindowIcon(icon)
|
|
243
260
|
self.setWindowTitle(self.tr("Background Neutralization"))
|
|
244
261
|
self.resize(900, 600)
|
|
245
262
|
|
|
246
263
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
247
|
-
|
|
264
|
+
# Non-modal: allow user to switch between images while dialog is open
|
|
265
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
248
266
|
self.setModal(False)
|
|
249
267
|
#self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
250
268
|
|
|
@@ -313,7 +331,14 @@ class BackgroundNeutralizationDialog(QDialog):
|
|
|
313
331
|
|
|
314
332
|
self._load_image()
|
|
315
333
|
|
|
316
|
-
|
|
334
|
+
# ---- active document change ------------------------------------
|
|
335
|
+
def _on_active_doc_changed(self, doc):
|
|
336
|
+
"""Called when user clicks a different image window."""
|
|
337
|
+
if doc is None or getattr(doc, "image", None) is None:
|
|
338
|
+
return
|
|
339
|
+
self.doc = doc
|
|
340
|
+
self.selection_item = None
|
|
341
|
+
self._load_image()
|
|
317
342
|
|
|
318
343
|
# ---------- image display ----------
|
|
319
344
|
def _doc_image_normalized(self) -> np.ndarray:
|
|
@@ -509,8 +534,25 @@ class BackgroundNeutralizationDialog(QDialog):
|
|
|
509
534
|
metadata=meta,
|
|
510
535
|
step_name="Background Neutralization",
|
|
511
536
|
)
|
|
512
|
-
|
|
513
|
-
|
|
537
|
+
# Dialog stays open so user can apply to other images
|
|
538
|
+
# Refresh to use the now-active document for next operation
|
|
539
|
+
self._refresh_document_from_active()
|
|
540
|
+
|
|
541
|
+
def _refresh_document_from_active(self):
|
|
542
|
+
"""
|
|
543
|
+
Refresh the dialog's document reference to the currently active document.
|
|
544
|
+
This allows reusing the same dialog on different images.
|
|
545
|
+
"""
|
|
546
|
+
try:
|
|
547
|
+
main = self.parent()
|
|
548
|
+
if main and hasattr(main, "_active_doc"):
|
|
549
|
+
new_doc = main._active_doc()
|
|
550
|
+
if new_doc is not None and new_doc is not self.doc:
|
|
551
|
+
self.doc = new_doc
|
|
552
|
+
# Refresh the preview image
|
|
553
|
+
self._load_preview()
|
|
554
|
+
except Exception:
|
|
555
|
+
pass
|
|
514
556
|
|
|
515
557
|
def _zoom(self, factor: float):
|
|
516
558
|
self._user_zoomed = True
|
|
@@ -169,6 +169,9 @@ class BatchConvertDialog(QDialog):
|
|
|
169
169
|
def __init__(self, parent=None):
|
|
170
170
|
super().__init__(parent)
|
|
171
171
|
self.setWindowTitle(self.tr("Batch Convert"))
|
|
172
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
173
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
174
|
+
self.setModal(False)
|
|
172
175
|
self.setMinimumWidth(560)
|
|
173
176
|
self.worker: _BatchWorker | None = None
|
|
174
177
|
|
|
@@ -46,6 +46,9 @@ class BatchRenamerDialog(QDialog):
|
|
|
46
46
|
self.setWindowFlag(Qt.WindowType.WindowTitleHint, True)
|
|
47
47
|
self.setWindowFlag(Qt.WindowType.WindowMinMaxButtonsHint, True)
|
|
48
48
|
self.setWindowFlag(Qt.WindowType.WindowContextHelpButtonHint, False)
|
|
49
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
50
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
51
|
+
self.setModal(False)
|
|
49
52
|
self.setSizeGripEnabled(True)
|
|
50
53
|
|
|
51
54
|
self._build_ui()
|
|
@@ -155,6 +155,9 @@ class BlemishBlasterDialogPro(QDialog):
|
|
|
155
155
|
def __init__(self, parent, doc):
|
|
156
156
|
super().__init__(parent)
|
|
157
157
|
self.setWindowTitle(self.tr("Blemish Blaster"))
|
|
158
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
159
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
160
|
+
self.setModal(False)
|
|
158
161
|
self.setMinimumSize(900, 650)
|
|
159
162
|
|
|
160
163
|
self._doc = doc
|
setiastro/saspro/cheat_sheet.py
CHANGED
|
@@ -11,6 +11,7 @@ from PyQt6.QtWidgets import (
|
|
|
11
11
|
QApplication, QMessageBox
|
|
12
12
|
)
|
|
13
13
|
from PyQt6.QtGui import QAction, QShortcut, QKeySequence
|
|
14
|
+
from PyQt6.QtCore import Qt, QCoreApplication
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def _qs_to_str(seq: QKeySequence) -> str:
|
|
@@ -46,37 +47,68 @@ def _seqs_for_action(act: QAction):
|
|
|
46
47
|
|
|
47
48
|
def _where_for_action(act: QAction) -> str:
|
|
48
49
|
"""Determine where an action is available (Menus/Toolbar or Window)."""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
try:
|
|
51
|
+
parent = act.parent()
|
|
52
|
+
if parent is not None:
|
|
53
|
+
pn = parent.__class__.__name__
|
|
54
|
+
if pn.startswith("QMenu") or pn.startswith("QToolBar"):
|
|
55
|
+
return QCoreApplication.translate("CheatSheet", "Menus/Toolbar")
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
54
58
|
return QCoreApplication.translate("CheatSheet", "Window")
|
|
55
59
|
|
|
56
60
|
|
|
57
61
|
def _describe_action(act: QAction) -> str:
|
|
58
62
|
"""Get a human-readable description for an action."""
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
try:
|
|
64
|
+
desc = _clean_text(
|
|
65
|
+
act.statusTip() or act.toolTip() or act.text() or act.objectName() or ""
|
|
66
|
+
)
|
|
67
|
+
except Exception:
|
|
68
|
+
desc = ""
|
|
69
|
+
|
|
70
|
+
if not desc:
|
|
62
71
|
desc = QCoreApplication.translate("CheatSheet", "Action")
|
|
63
72
|
return desc
|
|
64
73
|
|
|
65
74
|
|
|
66
75
|
def _describe_shortcut(sc: QShortcut) -> str:
|
|
67
76
|
"""Get a human-readable description for a shortcut."""
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
try:
|
|
78
|
+
hint = sc.property("hint")
|
|
79
|
+
desc = _clean_text(hint or sc.whatsThis() or sc.objectName() or "")
|
|
80
|
+
except Exception:
|
|
81
|
+
desc = ""
|
|
82
|
+
|
|
83
|
+
if not desc:
|
|
71
84
|
desc = QCoreApplication.translate("CheatSheet", "Shortcut")
|
|
72
85
|
return desc
|
|
73
86
|
|
|
74
|
-
|
|
87
|
+
def add_extra_shortcuts(rows):
|
|
88
|
+
"""
|
|
89
|
+
Add app-level shortcuts that aren't represented by QActions/QShortcuts.
|
|
90
|
+
rows: list of (shortcut_str, action_str, where_str)
|
|
91
|
+
"""
|
|
92
|
+
rows.append((
|
|
93
|
+
_clean_text("Ctrl+K"),
|
|
94
|
+
_clean_text("Toggle Show/Hide Mask"),
|
|
95
|
+
QCoreApplication.translate("CheatSheet", "Window"),
|
|
96
|
+
))
|
|
97
|
+
rows.append((
|
|
98
|
+
_clean_text("Ctrl+Alt+M"),
|
|
99
|
+
_clean_text("SASpro Neon Invaders (Easter Egg)"),
|
|
100
|
+
QCoreApplication.translate("CheatSheet", "Window"),
|
|
101
|
+
))
|
|
102
|
+
|
|
75
103
|
def _where_for_shortcut(sc: QShortcut) -> str:
|
|
76
104
|
"""Determine where a shortcut is available."""
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
105
|
+
try:
|
|
106
|
+
par = sc.parent()
|
|
107
|
+
if par is not None:
|
|
108
|
+
return par.__class__.__name__
|
|
109
|
+
except Exception:
|
|
110
|
+
pass
|
|
111
|
+
return QCoreApplication.translate("CheatSheet", "Window")
|
|
80
112
|
|
|
81
113
|
|
|
82
114
|
class CheatSheetDialog(QDialog):
|
|
@@ -91,6 +123,9 @@ class CheatSheetDialog(QDialog):
|
|
|
91
123
|
def __init__(self, parent, keyboard_rows, gesture_rows):
|
|
92
124
|
super().__init__(parent)
|
|
93
125
|
self.setWindowTitle(self.tr("Keyboard Shortcut Cheat Sheet"))
|
|
126
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
127
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
128
|
+
self.setModal(False)
|
|
94
129
|
self.resize(780, 520)
|
|
95
130
|
|
|
96
131
|
self._keyboard_rows = keyboard_rows
|
setiastro/saspro/clahe.py
CHANGED
|
@@ -114,6 +114,9 @@ class CLAHEDialogPro(QDialog):
|
|
|
114
114
|
def __init__(self, parent, doc, icon: QIcon | None = None):
|
|
115
115
|
super().__init__(parent)
|
|
116
116
|
self.setWindowTitle(self.tr("CLAHE"))
|
|
117
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
118
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
119
|
+
self.setModal(False)
|
|
117
120
|
if icon:
|
|
118
121
|
try: self.setWindowIcon(icon)
|
|
119
122
|
except Exception as e:
|
|
@@ -319,11 +322,34 @@ class CLAHEDialogPro(QDialog):
|
|
|
319
322
|
pass
|
|
320
323
|
# ─────────────────────────────────────────────────────────────
|
|
321
324
|
|
|
322
|
-
|
|
325
|
+
# Dialog stays open so user can apply to other images
|
|
326
|
+
# Refresh document reference for next operation
|
|
327
|
+
self._refresh_document_from_active()
|
|
323
328
|
|
|
324
329
|
except Exception as e:
|
|
325
330
|
QMessageBox.critical(self, "CLAHE", f"Failed to apply:\n{e}")
|
|
326
331
|
|
|
332
|
+
def _refresh_document_from_active(self):
|
|
333
|
+
"""
|
|
334
|
+
Refresh the dialog's document reference to the currently active document.
|
|
335
|
+
This allows reusing the same dialog on different images.
|
|
336
|
+
"""
|
|
337
|
+
try:
|
|
338
|
+
main = self.parent()
|
|
339
|
+
if main and hasattr(main, "_active_doc"):
|
|
340
|
+
new_doc = main._active_doc()
|
|
341
|
+
if new_doc is not None and new_doc is not self.doc:
|
|
342
|
+
self.doc = new_doc
|
|
343
|
+
# Refresh original image and preview for new document
|
|
344
|
+
self.orig = np.clip(np.asarray(new_doc.image, dtype=np.float32), 0.0, 1.0)
|
|
345
|
+
disp = self.orig
|
|
346
|
+
if disp.ndim == 2: disp = disp[..., None].repeat(3, axis=2)
|
|
347
|
+
elif disp.ndim == 3 and disp.shape[2] == 1: disp = disp.repeat(3, axis=2)
|
|
348
|
+
self._disp_base = disp
|
|
349
|
+
self._update_preview()
|
|
350
|
+
except Exception:
|
|
351
|
+
pass
|
|
352
|
+
|
|
327
353
|
def _tile_grid_from_px(self, tile_px: int, hw: tuple[int, int]) -> tuple[int, int]:
|
|
328
354
|
"""
|
|
329
355
|
Convert desired tile size (pixels) into OpenCV tileGridSize=(n,n)
|
|
@@ -90,28 +90,48 @@ def starnet_starless_pair_from_array(
|
|
|
90
90
|
# -------- normalize & shape: float32 [0..1], keep note if mono ----------
|
|
91
91
|
x = np.asarray(src_rgb01, dtype=np.float32)
|
|
92
92
|
was_mono = (x.ndim == 2) or (x.ndim == 3 and x.shape[2] == 1)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
#
|
|
93
|
+
|
|
94
|
+
# DELAY expansion: work with 'x' (mono or rgb) directly where possible
|
|
95
|
+
x_input = x
|
|
96
|
+
if x_input.ndim == 3 and x_input.shape[2] == 1:
|
|
97
|
+
x_input = x_input[..., 0] # collapse to 2D for processing if needed, or keep 2D
|
|
98
|
+
|
|
99
|
+
# For StarNet save, we need 3 channels usually, but check if we can save mono?
|
|
100
|
+
# Actually StarNet usually expects RGB Tiff. So we might need to expand just for saving.
|
|
101
|
+
# But let's avoid `x3 = np.repeat` globally.
|
|
102
|
+
|
|
103
|
+
# Optimization: Create x3 ON DEMAND or virtually using broadcasting only when needed.
|
|
104
|
+
# But `save_image` might handle mono TIFs. If StarNet accepts Mono TIF, we save huge RAM.
|
|
105
|
+
# Standard StarNet typically wants RGB. We will enable "is_mono" flag in `save_image` if it is mono,
|
|
106
|
+
# but StarNet is finicky. Let's stick to RGB for StarNet input but avoid `np.repeat` for the WHOLE array
|
|
107
|
+
# if we can just broadcast or slice.
|
|
108
|
+
# Actually, `stretch_color_image` handles broadcasting? No.
|
|
109
|
+
# Let's simple optimize:
|
|
110
|
+
|
|
102
111
|
if is_linear:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
# stretch; if mono use mono stretch
|
|
113
|
+
if was_mono:
|
|
114
|
+
if x.ndim == 3: x = x[..., 0]
|
|
115
|
+
pre = stretch_mono_image(x, 0.25, False, False)
|
|
116
|
+
# expand ONLY for save
|
|
117
|
+
pre_to_save = np.dstack([pre, pre, pre])
|
|
118
|
+
else:
|
|
119
|
+
pre = stretch_color_image(x, 0.25, False, False, False)
|
|
120
|
+
pre_to_save = pre
|
|
106
121
|
else:
|
|
107
|
-
pre =
|
|
122
|
+
pre = x # floating point 0..1
|
|
123
|
+
if was_mono:
|
|
124
|
+
if pre.ndim == 3: pre = pre[..., 0]
|
|
125
|
+
pre_to_save = np.dstack([pre, pre, pre])
|
|
126
|
+
else:
|
|
127
|
+
pre_to_save = pre
|
|
108
128
|
|
|
109
129
|
# -------- StarNet I/O (write float->16b TIFF; read back float) ----------
|
|
110
130
|
starnet_dir = os.path.dirname(exe) or os.getcwd()
|
|
111
131
|
in_path = os.path.join(starnet_dir, "imagetoremovestars.tif")
|
|
112
132
|
out_path = os.path.join(starnet_dir, "starless.tif")
|
|
113
133
|
|
|
114
|
-
save_image(
|
|
134
|
+
save_image(pre_to_save, in_path, original_format="tif", bit_depth="16-bit",
|
|
115
135
|
original_header=None, is_mono=False, image_meta=None, file_meta=None)
|
|
116
136
|
|
|
117
137
|
exe_name = os.path.basename(exe).lower()
|
|
@@ -134,32 +154,61 @@ def starnet_starless_pair_from_array(
|
|
|
134
154
|
except Exception:
|
|
135
155
|
pass
|
|
136
156
|
|
|
137
|
-
|
|
138
|
-
starless_pre = np.stack([starless_pre]*3, axis=-1)
|
|
139
|
-
elif starless_pre.ndim == 3 and starless_pre.shape[2] == 1:
|
|
140
|
-
starless_pre = np.repeat(starless_pre, 3, axis=2)
|
|
157
|
+
# Don't expand starless_pre yet if we don't need to.
|
|
141
158
|
starless_pre = starless_pre.astype(np.float32, copy=False)
|
|
142
|
-
|
|
159
|
+
if was_mono and starless_pre.ndim == 3:
|
|
160
|
+
# StarNet output is usually RGB even for mono input. Convert back to mono?
|
|
161
|
+
# Or just use one channel.
|
|
162
|
+
starless_pre = starless_pre[..., 0]
|
|
163
|
+
|
|
164
|
+
# Maintain `pre` as the stretched input (mono or rgb)
|
|
165
|
+
|
|
143
166
|
# ---- mask-protect in the SAME (stretched) domain as pre/starless_pre ----
|
|
144
167
|
if core_mask is not None:
|
|
145
168
|
m = np.clip(core_mask.astype(np.float32), 0.0, 1.0)
|
|
146
|
-
|
|
147
|
-
|
|
169
|
+
# broadcast mask
|
|
170
|
+
if not was_mono:
|
|
171
|
+
if m.ndim == 2: m = m[..., None]
|
|
172
|
+
|
|
173
|
+
protected_stretched = starless_pre * (1.0 - m) + pre * m
|
|
148
174
|
else:
|
|
149
175
|
protected_stretched = starless_pre
|
|
176
|
+
|
|
177
|
+
# Return to 3-channel ONLY if requested by the caller's context?
|
|
178
|
+
# The signature `starnet_starless_pair_from_array` implies it might return what it got.
|
|
179
|
+
# The original returned `protected_unstretch`.
|
|
180
|
+
pass # logic flow continues below...
|
|
150
181
|
|
|
151
182
|
# -------- “unstretch” → shared pseudo-linear space (once, after blend) ----------
|
|
152
183
|
if is_linear:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
184
|
+
# choose stretcher based on channels
|
|
185
|
+
if was_mono:
|
|
186
|
+
# ensure 2d
|
|
187
|
+
if protected_stretched.ndim == 3 and protected_stretched.shape[2] == 1:
|
|
188
|
+
protected_stretched = protected_stretched[..., 0]
|
|
189
|
+
elif protected_stretched.ndim == 3:
|
|
190
|
+
# collapse rgb to mono if needed? likely StarNet gave RGB.
|
|
191
|
+
# Keep RGB if StarNet created color artifacts we want to keep?
|
|
192
|
+
# Usually for mono data we want to kill color.
|
|
193
|
+
protected_stretched = protected_stretched.mean(axis=2)
|
|
194
|
+
|
|
195
|
+
protected_unstretch = stretch_mono_image(
|
|
196
|
+
protected_stretched, 0.05, False, False
|
|
197
|
+
)
|
|
198
|
+
# Expand finally for return constraint?
|
|
199
|
+
# The older function returned RGB-like.
|
|
200
|
+
# Let's expand here at the VERY END.
|
|
201
|
+
protected_unstretch = np.dstack([protected_unstretch]*3)
|
|
202
|
+
else:
|
|
203
|
+
protected_unstretch = stretch_color_image(
|
|
204
|
+
protected_stretched, 0.05, False, False, False
|
|
205
|
+
)
|
|
156
206
|
else:
|
|
157
207
|
protected_unstretch = protected_stretched
|
|
208
|
+
if was_mono and protected_unstretch.ndim == 2:
|
|
209
|
+
protected_unstretch = np.dstack([protected_unstretch]*3)
|
|
158
210
|
|
|
159
|
-
protected_unstretch
|
|
160
|
-
protected_unstretch.astype(np.float32, copy=False), 0.0, 1.0
|
|
161
|
-
)
|
|
162
|
-
return protected_unstretch, protected_unstretch
|
|
211
|
+
return np.clip(protected_unstretch, 0.0, 1.0), np.clip(protected_unstretch, 0.0, 1.0)
|
|
163
212
|
|
|
164
213
|
|
|
165
214
|
|
|
@@ -170,8 +219,17 @@ def darkstar_starless_from_array(src_rgb01: np.ndarray, settings, **_ignored) ->
|
|
|
170
219
|
"""
|
|
171
220
|
# normalize channels
|
|
172
221
|
img = src_rgb01.astype(np.float32, copy=False)
|
|
173
|
-
if
|
|
174
|
-
|
|
222
|
+
# Delay expansion: if it's 2D/Mono, send it as-is if DarkStar supports it,
|
|
223
|
+
# but DarkStar expects 3-channel TIF usually.
|
|
224
|
+
# We'll just expand for the save call, not "in place" if possible.
|
|
225
|
+
# Actually DarkStar runner saves `img` directly.
|
|
226
|
+
# So we'll expand just for that save to avoid holding 2 copies in memory.
|
|
227
|
+
if img.ndim == 2:
|
|
228
|
+
img_to_save = np.stack([img]*3, axis=-1)
|
|
229
|
+
elif img.ndim == 3 and img.shape[2] == 1:
|
|
230
|
+
img_to_save = np.repeat(img, 3, axis=2)
|
|
231
|
+
else:
|
|
232
|
+
img_to_save = img
|
|
175
233
|
|
|
176
234
|
# resolve exe and base folder
|
|
177
235
|
exe, base = _resolve_darkstar_exe(type("Dummy", (), {"settings": settings})())
|
|
@@ -192,7 +250,8 @@ def darkstar_starless_from_array(src_rgb01: np.ndarray, settings, **_ignored) ->
|
|
|
192
250
|
out_path = os.path.join(output_dir, "imagetoremovestars_starless.tif")
|
|
193
251
|
|
|
194
252
|
# save input as float32 TIFF
|
|
195
|
-
|
|
253
|
+
# save input as float32 TIFF
|
|
254
|
+
save_image(img_to_save, in_path, original_format="tif", bit_depth="32-bit floating point",
|
|
196
255
|
original_header=None, is_mono=False, image_meta=None, file_meta=None)
|
|
197
256
|
|
|
198
257
|
# build command (SASv2 parity): default unscreen, show extracted stars off, stride 512
|
|
@@ -218,8 +277,12 @@ def darkstar_starless_from_array(src_rgb01: np.ndarray, settings, **_ignored) ->
|
|
|
218
277
|
if starless is None:
|
|
219
278
|
raise RuntimeError("DarkStar produced no output.")
|
|
220
279
|
|
|
221
|
-
|
|
222
|
-
if starless.
|
|
280
|
+
# Delayed expansion
|
|
281
|
+
if starless.ndim == 2:
|
|
282
|
+
starless = np.stack([starless]*3, axis=-1)
|
|
283
|
+
elif starless.ndim == 3 and starless.shape[2] == 1:
|
|
284
|
+
starless = np.repeat(starless, 3, axis=2)
|
|
285
|
+
|
|
223
286
|
return np.clip(starless.astype(np.float32, copy=False), 0.0, 1.0)
|
|
224
287
|
|
|
225
288
|
# ---------- small helpers ----------
|
|
@@ -239,6 +302,10 @@ def _inv_affine_2x3(M: np.ndarray) -> np.ndarray:
|
|
|
239
302
|
def _to_luma(img: np.ndarray) -> np.ndarray:
|
|
240
303
|
if img.ndim == 2: return img.astype(np.float32, copy=False)
|
|
241
304
|
if img.ndim == 3 and img.shape[-1] == 3:
|
|
305
|
+
try:
|
|
306
|
+
return cv2.cvtColor(img, cv2.COLOR_RGB2GRAY).astype(np.float32, copy=False)
|
|
307
|
+
except Exception:
|
|
308
|
+
pass
|
|
242
309
|
r,g,b = img[...,0], img[...,1], img[...,2]
|
|
243
310
|
return (0.2126*r + 0.7152*g + 0.0722*b).astype(np.float32, copy=False)
|
|
244
311
|
if img.ndim == 3 and img.shape[-1] == 1:
|
|
@@ -748,11 +815,9 @@ def _shift_to_comet(img: np.ndarray, xy: Tuple[float,float], ref_xy: Tuple[float
|
|
|
748
815
|
M = np.array([[1.0, 0.0, dx], [0.0, 1.0, dy]], dtype=np.float32)
|
|
749
816
|
H, W = img.shape[:2]
|
|
750
817
|
interp = cv2.INTER_LANCZOS4
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
ch = [cv2.warpAffine(img[...,c], M, (W, H), flags=interp, borderMode=cv2.BORDER_REFLECT) for c in range(img.shape[-1])]
|
|
755
|
-
return np.stack(ch, axis=-1)
|
|
818
|
+
|
|
819
|
+
# Vectorized warp for both 2D (mono) and 3D (RGB)
|
|
820
|
+
return cv2.warpAffine(img, M, (W, H), flags=interp, borderMode=cv2.BORDER_REFLECT)
|
|
756
821
|
|
|
757
822
|
def stack_comet_aligned(file_list: List[str],
|
|
758
823
|
comet_xy: Dict[str, Tuple[float,float]],
|
setiastro/saspro/convo.py
CHANGED
|
@@ -153,6 +153,9 @@ class ConvoDeconvoDialog(QDialog):
|
|
|
153
153
|
self._main.currentDocumentChanged.connect(self._on_active_doc_changed)
|
|
154
154
|
|
|
155
155
|
self.setWindowTitle(self.tr("Convolution / Deconvolution"))
|
|
156
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
157
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
158
|
+
self.setModal(False)
|
|
156
159
|
self.resize(1000, 650)
|
|
157
160
|
self._use_custom_psf = False
|
|
158
161
|
self._custom_psf: Optional[np.ndarray] = None
|
setiastro/saspro/copyastro.py
CHANGED
|
@@ -15,6 +15,9 @@ class CopyAstrometryDialog(QDialog):
|
|
|
15
15
|
def __init__(self, parent=None, target=None):
|
|
16
16
|
super().__init__(parent)
|
|
17
17
|
self.setWindowTitle("Copy Astrometric Solution")
|
|
18
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
19
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
20
|
+
self.setModal(False)
|
|
18
21
|
self.setMinimumWidth(420)
|
|
19
22
|
|
|
20
23
|
self._mw = parent
|
|
@@ -271,6 +271,9 @@ class CosmicClarityDialogPro(QDialog):
|
|
|
271
271
|
QTimer.singleShot(0, self.reject)
|
|
272
272
|
return
|
|
273
273
|
self.setWindowTitle(self.tr("Cosmic Clarity"))
|
|
274
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
275
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
276
|
+
self.setModal(False)
|
|
274
277
|
if icon:
|
|
275
278
|
try: self.setWindowIcon(icon)
|
|
276
279
|
except Exception as e:
|
|
@@ -632,7 +635,53 @@ class CosmicClarityDialogPro(QDialog):
|
|
|
632
635
|
if self._wait: self._wait.close(); self._wait = None
|
|
633
636
|
if self._wait_thread: self._wait_thread.stop(); self._wait_thread = None
|
|
634
637
|
|
|
635
|
-
|
|
638
|
+
has_more = bool(self._op_queue)
|
|
639
|
+
|
|
640
|
+
# --- Optimization: Chained Execution Fast Path ---
|
|
641
|
+
# If we have more steps, skip the expensive load/display/save cycle.
|
|
642
|
+
# Just move the output file to be the input for the next step.
|
|
643
|
+
if has_more:
|
|
644
|
+
if not out_path or not os.path.exists(out_path):
|
|
645
|
+
QMessageBox.critical(self, "Cosmic Clarity", "Output file missing during chain execution.")
|
|
646
|
+
self._op_queue.clear()
|
|
647
|
+
return
|
|
648
|
+
|
|
649
|
+
base = self._base_name()
|
|
650
|
+
next_in = os.path.join(self.cosmic_root, "input", f"{base}.tif")
|
|
651
|
+
prev_in = getattr(self, "_current_input", None)
|
|
652
|
+
|
|
653
|
+
try:
|
|
654
|
+
# Direct move/copy instead of decode+encode
|
|
655
|
+
if os.path.abspath(out_path) != os.path.abspath(next_in):
|
|
656
|
+
# Windows cannot atomic replace if target exists via os.rename usually,
|
|
657
|
+
# but shutil.move is generally robust.
|
|
658
|
+
# We remove target first to be sure.
|
|
659
|
+
if os.path.exists(next_in):
|
|
660
|
+
os.remove(next_in)
|
|
661
|
+
shutil.move(out_path, next_in)
|
|
662
|
+
|
|
663
|
+
# Ensure stability of the *new* input
|
|
664
|
+
if not _wait_stable_file(next_in):
|
|
665
|
+
QMessageBox.critical(self, "Cosmic Clarity", "Staged input for next step is unstable.")
|
|
666
|
+
self._op_queue.clear()
|
|
667
|
+
return
|
|
668
|
+
|
|
669
|
+
self._current_input = next_in
|
|
670
|
+
|
|
671
|
+
# Cleanup previous input if distinct
|
|
672
|
+
if prev_in and prev_in != next_in and os.path.exists(prev_in):
|
|
673
|
+
os.remove(prev_in)
|
|
674
|
+
|
|
675
|
+
except Exception as e:
|
|
676
|
+
QMessageBox.critical(self, "Cosmic Clarity", f"Failed to stage next step:\n{e}")
|
|
677
|
+
self._op_queue.clear()
|
|
678
|
+
return
|
|
679
|
+
|
|
680
|
+
# Trigger next step immediately
|
|
681
|
+
QTimer.singleShot(50, self._run_next)
|
|
682
|
+
return
|
|
683
|
+
|
|
684
|
+
# --- Final Step (or Single Step): Load and Display ---
|
|
636
685
|
try:
|
|
637
686
|
img, hdr, bd, mono = load_image(out_path)
|
|
638
687
|
if img is None:
|
|
@@ -643,61 +692,34 @@ class CosmicClarityDialogPro(QDialog):
|
|
|
643
692
|
|
|
644
693
|
dest = img.astype(np.float32, copy=False)
|
|
645
694
|
|
|
646
|
-
# Apply to document
|
|
695
|
+
# Apply to document
|
|
647
696
|
step_title = f"Cosmic Clarity – {mode.title()}"
|
|
648
697
|
create_new = (self.cmb_target.currentIndex() == 1)
|
|
649
698
|
|
|
650
699
|
if create_new:
|
|
651
700
|
ok = self._spawn_new_doc_from_numpy(dest, step_title)
|
|
652
701
|
if not ok:
|
|
653
|
-
# fall back to overwriting if we couldn’t spawn a new doc
|
|
654
702
|
self._apply_to_active(dest, step_title)
|
|
655
703
|
else:
|
|
656
704
|
self._apply_to_active(dest, step_title)
|
|
657
705
|
|
|
658
|
-
#
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
706
|
+
# Cleanup final output
|
|
707
|
+
if out_path and os.path.exists(out_path):
|
|
708
|
+
try: os.remove(out_path)
|
|
709
|
+
except OSError: pass
|
|
710
|
+
|
|
711
|
+
# Cleanup final input
|
|
712
|
+
prev_in = getattr(self, "_current_input", None)
|
|
713
|
+
if prev_in and os.path.exists(prev_in):
|
|
714
|
+
try: os.remove(prev_in)
|
|
715
|
+
except OSError: pass
|
|
716
|
+
|
|
717
|
+
# Final purge
|
|
664
718
|
try:
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
getattr(self.doc, "is_mono", False))
|
|
670
|
-
_atomic_fsync_replace(_writer, next_in)
|
|
671
|
-
if not _wait_stable_file(next_in):
|
|
672
|
-
QMessageBox.critical(self, "Cosmic Clarity", "Staging for next step failed (not stable).")
|
|
673
|
-
self._op_queue.clear()
|
|
674
|
-
return
|
|
675
|
-
self._current_input = next_in
|
|
676
|
-
|
|
677
|
-
# Now it’s safe to clean up the produced output
|
|
678
|
-
if out_path and os.path.exists(out_path):
|
|
679
|
-
os.remove(out_path)
|
|
680
|
-
|
|
681
|
-
# Remove the previous input file if it’s different from the new one
|
|
682
|
-
if prev_in and prev_in != next_in and os.path.exists(prev_in):
|
|
683
|
-
os.remove(prev_in)
|
|
684
|
-
|
|
685
|
-
except Exception as e:
|
|
686
|
-
QMessageBox.critical(self, "Cosmic Clarity", f"Failed while staging next step:\n{e}")
|
|
687
|
-
self._op_queue.clear()
|
|
688
|
-
return
|
|
689
|
-
|
|
690
|
-
# Continue or finish
|
|
691
|
-
if has_more:
|
|
692
|
-
QTimer.singleShot(100, self._run_next)
|
|
693
|
-
else:
|
|
694
|
-
# Nothing else queued — we're done
|
|
695
|
-
try:
|
|
696
|
-
# 🔸 Final cleanup: clear both input & output
|
|
697
|
-
_purge_cc_io(self.cosmic_root, clear_input=True, clear_output=True)
|
|
698
|
-
except Exception:
|
|
699
|
-
pass
|
|
700
|
-
self.accept()
|
|
719
|
+
_purge_cc_io(self.cosmic_root, clear_input=True, clear_output=True)
|
|
720
|
+
except Exception:
|
|
721
|
+
pass
|
|
722
|
+
self.accept()
|
|
701
723
|
|
|
702
724
|
|
|
703
725
|
def _on_wait_error(self, msg: str):
|
|
@@ -1009,6 +1031,9 @@ class CosmicClaritySatelliteDialogPro(QDialog):
|
|
|
1009
1031
|
def __init__(self, parent, doc=None, icon: QIcon | None = None):
|
|
1010
1032
|
super().__init__(parent)
|
|
1011
1033
|
self.setWindowTitle("Cosmic Clarity – Satellite Removal")
|
|
1034
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
1035
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
1036
|
+
self.setModal(False)
|
|
1012
1037
|
if icon:
|
|
1013
1038
|
try: self.setWindowIcon(icon)
|
|
1014
1039
|
except Exception as e:
|