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
setiastro/saspro/halobgon.py
CHANGED
|
@@ -249,6 +249,9 @@ class HaloBGonDialogPro(QDialog):
|
|
|
249
249
|
def __init__(self, parent, doc, icon: Optional[QIcon] = None):
|
|
250
250
|
super().__init__(parent)
|
|
251
251
|
self.setWindowTitle("Halo-B-Gon")
|
|
252
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
253
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
254
|
+
self.setModal(False)
|
|
252
255
|
if icon:
|
|
253
256
|
try: self.setWindowIcon(icon)
|
|
254
257
|
except Exception as e:
|
|
@@ -427,7 +430,8 @@ class HaloBGonDialogPro(QDialog):
|
|
|
427
430
|
except Exception:
|
|
428
431
|
pass
|
|
429
432
|
|
|
430
|
-
|
|
433
|
+
# Dialog stays open - refresh document for next operation
|
|
434
|
+
self._refresh_document_from_active()
|
|
431
435
|
return
|
|
432
436
|
else:
|
|
433
437
|
# Fallback: try legacy spawner if present; else warn and overwrite.
|
|
@@ -437,7 +441,8 @@ class HaloBGonDialogPro(QDialog):
|
|
|
437
441
|
if callable(spawner):
|
|
438
442
|
title = self.doc.display_name() if hasattr(self.doc, "display_name") else "Image"
|
|
439
443
|
spawner(out, f"{title} [Halo-B-Gon]")
|
|
440
|
-
|
|
444
|
+
# Dialog stays open - refresh document for next operation
|
|
445
|
+
self._refresh_document_from_active()
|
|
441
446
|
return
|
|
442
447
|
else:
|
|
443
448
|
QMessageBox.warning(
|
|
@@ -448,11 +453,32 @@ class HaloBGonDialogPro(QDialog):
|
|
|
448
453
|
|
|
449
454
|
# Overwrite current (original behavior)
|
|
450
455
|
self._apply_overwrite(out)
|
|
451
|
-
|
|
456
|
+
# Dialog stays open - refresh document for next operation
|
|
457
|
+
self._refresh_document_from_active()
|
|
452
458
|
|
|
453
459
|
except Exception as e:
|
|
454
460
|
QMessageBox.critical(self, "Halo-B-Gon", f"Failed to apply:\n{e}")
|
|
455
461
|
|
|
462
|
+
def _refresh_document_from_active(self):
|
|
463
|
+
"""
|
|
464
|
+
Refresh the dialog's document reference to the currently active document.
|
|
465
|
+
This allows reusing the same dialog on different images.
|
|
466
|
+
"""
|
|
467
|
+
try:
|
|
468
|
+
main = self.parent()
|
|
469
|
+
if main and hasattr(main, "_active_doc"):
|
|
470
|
+
new_doc = main._active_doc()
|
|
471
|
+
if new_doc is not None and new_doc is not self.doc:
|
|
472
|
+
self.doc = new_doc
|
|
473
|
+
# Refresh preview for new document
|
|
474
|
+
self.orig = np.clip(np.asarray(new_doc.image, dtype=np.float32), 0.0, 1.0)
|
|
475
|
+
disp = self.orig
|
|
476
|
+
if disp.ndim == 2: disp = disp[..., None].repeat(3, axis=2)
|
|
477
|
+
elif disp.ndim == 3 and disp.shape[2] == 1: disp = disp.repeat(3, axis=2)
|
|
478
|
+
self._disp_base = disp
|
|
479
|
+
self._update_preview()
|
|
480
|
+
except Exception:
|
|
481
|
+
pass
|
|
456
482
|
|
|
457
483
|
|
|
458
484
|
def _reset(self):
|
setiastro/saspro/histogram.py
CHANGED
|
@@ -30,6 +30,9 @@ class HistogramDialog(QDialog):
|
|
|
30
30
|
def __init__(self, parent, document):
|
|
31
31
|
super().__init__(parent)
|
|
32
32
|
self.setWindowTitle(self.tr("Histogram"))
|
|
33
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
34
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
35
|
+
self.setModal(False)
|
|
33
36
|
self.doc = document
|
|
34
37
|
self.image = _to_float_preserve(document.image)
|
|
35
38
|
|
|
@@ -436,6 +436,8 @@ class HistoryExplorerDialog(QDialog):
|
|
|
436
436
|
def __init__(self, document, parent=None):
|
|
437
437
|
super().__init__(parent)
|
|
438
438
|
self.setWindowTitle("History Explorer")
|
|
439
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
440
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
439
441
|
self.setModal(False)
|
|
440
442
|
self.doc = document
|
|
441
443
|
|
setiastro/saspro/i18n.py
CHANGED
|
@@ -25,22 +25,34 @@ AVAILABLE_LANGUAGES: Dict[str, str] = {
|
|
|
25
25
|
"de": "Deutsch",
|
|
26
26
|
"pt": "Português",
|
|
27
27
|
"ja": "日本語",
|
|
28
|
+
"hi": "हिन्दी",
|
|
29
|
+
"sw": "Kiswahili",
|
|
30
|
+
"uk": "Українська",
|
|
31
|
+
"ru": "Русский",
|
|
32
|
+
"ar": "العربية",
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
|
|
31
36
|
def get_translations_dir() -> str:
|
|
32
37
|
"""Get the path to the translations directory."""
|
|
33
|
-
#
|
|
38
|
+
# Source / installed package location
|
|
34
39
|
module_dir = os.path.dirname(os.path.abspath(__file__))
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
#
|
|
38
|
-
if hasattr(os.sys,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
pkg_dir = os.path.join(module_dir, "translations")
|
|
41
|
+
|
|
42
|
+
# PyInstaller frozen builds
|
|
43
|
+
if hasattr(os.sys, "_MEIPASS"):
|
|
44
|
+
# New bundle layout (preferred)
|
|
45
|
+
frozen_internal = os.path.join(os.sys._MEIPASS, "_internal", "translations")
|
|
46
|
+
if os.path.exists(frozen_internal):
|
|
47
|
+
return frozen_internal
|
|
48
|
+
|
|
49
|
+
# Legacy bundle layout fallback
|
|
50
|
+
frozen_legacy = os.path.join(os.sys._MEIPASS, "translations")
|
|
51
|
+
if os.path.exists(frozen_legacy):
|
|
52
|
+
return frozen_legacy
|
|
53
|
+
|
|
54
|
+
return pkg_dir
|
|
55
|
+
|
|
44
56
|
|
|
45
57
|
|
|
46
58
|
def get_available_languages() -> Dict[str, str]:
|
|
@@ -91,6 +91,9 @@ class ImageCombineDialog(QDialog):
|
|
|
91
91
|
def __init__(self, main_window):
|
|
92
92
|
super().__init__(main_window)
|
|
93
93
|
self.setWindowTitle("Image Combine")
|
|
94
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
95
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
96
|
+
self.setModal(False)
|
|
94
97
|
self.mw = main_window
|
|
95
98
|
self.dm = getattr(main_window, "doc_manager", None) or getattr(main_window, "dm", None)
|
|
96
99
|
self.zoom = 1.0
|
|
@@ -1314,6 +1314,9 @@ class ImagePeekerDialogPro(QDialog):
|
|
|
1314
1314
|
def __init__(self, parent, document, settings):
|
|
1315
1315
|
super().__init__(parent)
|
|
1316
1316
|
self.setWindowTitle(self.tr("Image Peeker"))
|
|
1317
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
1318
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
1319
|
+
self.setModal(False)
|
|
1317
1320
|
self.document = self._coerce_doc(document) # <- ensure we hold a real doc
|
|
1318
1321
|
self.settings = settings
|
|
1319
1322
|
# status / progress line
|
|
@@ -95,19 +95,11 @@ def apply_curves_adjustment(image: np.ndarray,
|
|
|
95
95
|
xvals, yvals = _calculate_curve_points(target_median, curves_boost)
|
|
96
96
|
|
|
97
97
|
# Apply the 1D LUT per channel using np.interp (piecewise linear)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
out = np.empty_like(img, dtype=np.float32)
|
|
104
|
-
# Apply same curve to each color channel
|
|
105
|
-
for ch in range(c):
|
|
106
|
-
flat = img[..., ch].ravel()
|
|
107
|
-
out[..., ch] = np.interp(flat, xvals, yvals).reshape(h, w)
|
|
108
|
-
else:
|
|
109
|
-
# Fallback: just return clamped image
|
|
110
|
-
out = img
|
|
98
|
+
# Apply the 1D LUT per channel using np.interp (piecewise linear)
|
|
99
|
+
# Optimization: np.interp can handle N-D 'x' array directly.
|
|
100
|
+
# No need to loop over channels or flatten/reshape if we pass the whole array.
|
|
101
|
+
|
|
102
|
+
out = np.interp(img, xvals, yvals).astype(np.float32, copy=False)
|
|
111
103
|
|
|
112
104
|
return np.clip(out, 0.0, 1.0)
|
|
113
105
|
|
setiastro/saspro/isophote.py
CHANGED
|
@@ -360,6 +360,9 @@ class IsophoteModelerDialog(QDialog):
|
|
|
360
360
|
def __init__(self, mono_image: np.ndarray, parent: Optional[QWidget] = None,
|
|
361
361
|
title_hint: Optional[str] = None, image_manager=None, doc_manager=None):
|
|
362
362
|
super().__init__(parent)
|
|
363
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
364
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
365
|
+
self.setModal(False)
|
|
363
366
|
self.image_manager = image_manager
|
|
364
367
|
self.doc_manager = doc_manager
|
|
365
368
|
|
|
@@ -802,39 +802,46 @@ def kappa_sigma_clip_weighted_3d(stack, weights, kappa=2.5, iterations=3):
|
|
|
802
802
|
pixel_weights = weights[:]
|
|
803
803
|
else:
|
|
804
804
|
pixel_weights = weights[:, i, j].copy()
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
current_idx[f] = f
|
|
809
|
-
current_vals = pixel_values
|
|
810
|
-
current_w = pixel_weights
|
|
811
|
-
current_indices = current_idx
|
|
805
|
+
|
|
806
|
+
valid_mask = pixel_values != 0
|
|
807
|
+
|
|
812
808
|
med = 0.0
|
|
813
809
|
for _ in range(iterations):
|
|
814
|
-
|
|
810
|
+
count = 0
|
|
811
|
+
for k in range(num_frames):
|
|
812
|
+
if valid_mask[k]:
|
|
813
|
+
count += 1
|
|
814
|
+
|
|
815
|
+
if count == 0:
|
|
815
816
|
break
|
|
817
|
+
|
|
818
|
+
current_vals = pixel_values[valid_mask]
|
|
819
|
+
|
|
816
820
|
med = np.median(current_vals)
|
|
817
821
|
std = np.std(current_vals)
|
|
818
822
|
lower_bound = med - kappa * std
|
|
819
823
|
upper_bound = med + kappa * std
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
824
|
+
|
|
825
|
+
for k in range(num_frames):
|
|
826
|
+
if valid_mask[k]:
|
|
827
|
+
val = pixel_values[k]
|
|
828
|
+
if val < lower_bound or val > upper_bound:
|
|
829
|
+
valid_mask[k] = False
|
|
830
|
+
|
|
825
831
|
for f in range(num_frames):
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
832
|
+
rej_mask[f, i, j] = not valid_mask[f]
|
|
833
|
+
|
|
834
|
+
wsum = 0.0
|
|
835
|
+
vsum = 0.0
|
|
836
|
+
for k in range(num_frames):
|
|
837
|
+
if valid_mask[k]:
|
|
838
|
+
w = pixel_weights[k]
|
|
839
|
+
v = pixel_values[k]
|
|
840
|
+
wsum += w
|
|
841
|
+
vsum += v * w
|
|
842
|
+
|
|
843
|
+
if wsum > 0:
|
|
844
|
+
clipped[i, j] = vsum / wsum
|
|
838
845
|
else:
|
|
839
846
|
clipped[i, j] = med
|
|
840
847
|
return clipped, rej_mask
|
|
@@ -859,36 +866,46 @@ def kappa_sigma_clip_weighted_4d(stack, weights, kappa=2.5, iterations=3):
|
|
|
859
866
|
pixel_weights = weights[:]
|
|
860
867
|
else:
|
|
861
868
|
pixel_weights = weights[:, i, j, c].copy()
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
current_vals = pixel_values
|
|
866
|
-
current_w = pixel_weights
|
|
867
|
-
current_indices = current_idx
|
|
869
|
+
|
|
870
|
+
valid_mask = pixel_values != 0
|
|
871
|
+
|
|
868
872
|
med = 0.0
|
|
869
873
|
for _ in range(iterations):
|
|
870
|
-
|
|
874
|
+
count = 0
|
|
875
|
+
for k in range(num_frames):
|
|
876
|
+
if valid_mask[k]:
|
|
877
|
+
count += 1
|
|
878
|
+
|
|
879
|
+
if count == 0:
|
|
871
880
|
break
|
|
881
|
+
|
|
882
|
+
current_vals = pixel_values[valid_mask]
|
|
883
|
+
|
|
872
884
|
med = np.median(current_vals)
|
|
873
885
|
std = np.std(current_vals)
|
|
874
886
|
lower_bound = med - kappa * std
|
|
875
887
|
upper_bound = med + kappa * std
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
888
|
+
|
|
889
|
+
for k in range(num_frames):
|
|
890
|
+
if valid_mask[k]:
|
|
891
|
+
val = pixel_values[k]
|
|
892
|
+
if val < lower_bound or val > upper_bound:
|
|
893
|
+
valid_mask[k] = False
|
|
894
|
+
|
|
880
895
|
for f in range(num_frames):
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
if
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
896
|
+
rej_mask[f, i, j, c] = not valid_mask[f]
|
|
897
|
+
|
|
898
|
+
wsum = 0.0
|
|
899
|
+
vsum = 0.0
|
|
900
|
+
for k in range(num_frames):
|
|
901
|
+
if valid_mask[k]:
|
|
902
|
+
w = pixel_weights[k]
|
|
903
|
+
v = pixel_values[k]
|
|
904
|
+
wsum += w
|
|
905
|
+
vsum += v * w
|
|
906
|
+
|
|
907
|
+
if wsum > 0:
|
|
908
|
+
clipped[i, j, c] = vsum / wsum
|
|
892
909
|
else:
|
|
893
910
|
clipped[i, j, c] = med
|
|
894
911
|
return clipped, rej_mask
|
setiastro/saspro/linear_fit.py
CHANGED
|
@@ -224,6 +224,9 @@ class LinearFitDialog(QDialog):
|
|
|
224
224
|
def __init__(self, parent, doc_manager, active_doc):
|
|
225
225
|
super().__init__(parent)
|
|
226
226
|
self.setWindowTitle("Linear Fit")
|
|
227
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
228
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
229
|
+
self.setModal(False)
|
|
227
230
|
self.dm = doc_manager
|
|
228
231
|
self.doc = active_doc
|
|
229
232
|
self.worker: Optional[_LinearFitWorker] = None
|
|
@@ -48,6 +48,9 @@ class LiveStackSettingsDialog(QDialog):
|
|
|
48
48
|
def __init__(self, parent):
|
|
49
49
|
super().__init__(parent)
|
|
50
50
|
self.setWindowTitle("Live Stack & Culling Settings")
|
|
51
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
52
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
53
|
+
self.setModal(False)
|
|
51
54
|
|
|
52
55
|
# — Live Stack Settings —
|
|
53
56
|
# Bootstrap frames (int)
|
|
@@ -290,8 +293,16 @@ def estimate_global_snr(
|
|
|
290
293
|
|
|
291
294
|
# 1) Collapse to simple 2D float array (grayscale)
|
|
292
295
|
if stack_image.ndim == 3 and stack_image.shape[2] == 3:
|
|
293
|
-
|
|
294
|
-
|
|
296
|
+
try:
|
|
297
|
+
import cv2
|
|
298
|
+
# cv2.cvtColor is significantly faster than mean(axis=2)
|
|
299
|
+
# Assuming RGB input, but even if BGR, for SNR estimation luma difference is negligible
|
|
300
|
+
gray = cv2.cvtColor(stack_image, cv2.COLOR_RGB2GRAY)
|
|
301
|
+
if gray.dtype != np.float32:
|
|
302
|
+
gray = gray.astype(np.float32)
|
|
303
|
+
except ImportError:
|
|
304
|
+
# Fallback
|
|
305
|
+
gray = stack_image.mean(axis=2).astype(np.float32)
|
|
295
306
|
else:
|
|
296
307
|
# Already mono: just cast to float32
|
|
297
308
|
gray = stack_image.astype(np.float32)
|
|
@@ -555,6 +555,9 @@ class MaskCreationDialog(QDialog):
|
|
|
555
555
|
def __init__(self, image01: np.ndarray, parent=None, auto_push_on_ok: bool = True):
|
|
556
556
|
super().__init__(parent)
|
|
557
557
|
self.setWindowTitle(self.tr("Mask Creation"))
|
|
558
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
559
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
560
|
+
self.setModal(False)
|
|
558
561
|
self.image = np.asarray(image01, dtype=np.float32).copy()
|
|
559
562
|
self.mask: np.ndarray | None = None
|
|
560
563
|
self.live_preview = LivePreviewDialog(self.image, parent=self)
|
setiastro/saspro/mfdeconv.py
CHANGED
|
@@ -822,6 +822,11 @@ def _to_luma_local(a: np.ndarray) -> np.ndarray:
|
|
|
822
822
|
return a
|
|
823
823
|
# (H,W,3) or (3,H,W)
|
|
824
824
|
if a.ndim == 3 and a.shape[-1] == 3:
|
|
825
|
+
try:
|
|
826
|
+
import cv2
|
|
827
|
+
return cv2.cvtColor(a, cv2.COLOR_RGB2GRAY).astype(np.float32, copy=False)
|
|
828
|
+
except Exception:
|
|
829
|
+
pass
|
|
825
830
|
r, g, b = a[..., 0], a[..., 1], a[..., 2]
|
|
826
831
|
return (0.2126*r + 0.7152*g + 0.0722*b).astype(np.float32, copy=False)
|
|
827
832
|
if a.ndim == 3 and a.shape[0] == 3:
|
setiastro/saspro/morphology.py
CHANGED
|
@@ -46,10 +46,9 @@ def apply_morphology(image: np.ndarray, *, operation: str = "erosion",
|
|
|
46
46
|
|
|
47
47
|
if img.ndim == 3 and img.shape[2] == 3:
|
|
48
48
|
u8 = (img * 255.0).astype(np.uint8)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return np.clip(out, 0.0, 1.0)
|
|
49
|
+
# OpenCV morphology functions handle multi-channel images natively (independent channels)
|
|
50
|
+
out_u8 = _do(u8)
|
|
51
|
+
return (out_u8.astype(np.float32) / 255.0).clip(0.0, 1.0)
|
|
53
52
|
|
|
54
53
|
raise ValueError("Input image must be mono (H,W)/(H,W,1) or RGB (H,W,3).")
|
|
55
54
|
|
|
@@ -92,6 +91,9 @@ class MorphologyDialogPro(QDialog):
|
|
|
92
91
|
def __init__(self, parent, doc, icon: QIcon | None = None, initial: dict | None = None):
|
|
93
92
|
super().__init__(parent)
|
|
94
93
|
self.setWindowTitle(self.tr("Morphological Operations"))
|
|
94
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
95
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
96
|
+
self.setModal(False)
|
|
95
97
|
if icon:
|
|
96
98
|
try: self.setWindowIcon(icon)
|
|
97
99
|
except Exception as e:
|
|
@@ -258,10 +260,33 @@ class MorphologyDialogPro(QDialog):
|
|
|
258
260
|
pass
|
|
259
261
|
# ────────────────────────────────────────────────────────────
|
|
260
262
|
|
|
261
|
-
|
|
263
|
+
# Dialog stays open so user can apply to other images
|
|
264
|
+
# Refresh document reference for next operation
|
|
265
|
+
self._refresh_document_from_active()
|
|
262
266
|
except Exception as e:
|
|
263
267
|
QMessageBox.critical(self, "Morphology", f"Failed to apply:\n{e}")
|
|
264
268
|
|
|
269
|
+
def _refresh_document_from_active(self):
|
|
270
|
+
"""
|
|
271
|
+
Refresh the dialog's document reference to the currently active document.
|
|
272
|
+
This allows reusing the same dialog on different images.
|
|
273
|
+
"""
|
|
274
|
+
try:
|
|
275
|
+
main = self.parent()
|
|
276
|
+
if main and hasattr(main, "_active_doc"):
|
|
277
|
+
new_doc = main._active_doc()
|
|
278
|
+
if new_doc is not None and new_doc is not self.doc:
|
|
279
|
+
self.doc = new_doc
|
|
280
|
+
# Refresh preview for new document
|
|
281
|
+
self.orig = np.clip(np.asarray(new_doc.image, dtype=np.float32), 0.0, 1.0)
|
|
282
|
+
disp = self.orig
|
|
283
|
+
if disp.ndim == 2: disp = disp[..., None].repeat(3, axis=2)
|
|
284
|
+
elif disp.ndim == 3 and disp.shape[2] == 1: disp = disp.repeat(3, axis=2)
|
|
285
|
+
self._disp_base = disp
|
|
286
|
+
self._update_preview()
|
|
287
|
+
except Exception:
|
|
288
|
+
pass
|
|
289
|
+
|
|
265
290
|
|
|
266
291
|
|
|
267
292
|
def _reset(self):
|
|
@@ -196,6 +196,9 @@ class MultiscaleDecompDialog(QDialog):
|
|
|
196
196
|
def __init__(self, parent, doc):
|
|
197
197
|
super().__init__(parent)
|
|
198
198
|
self.setWindowTitle("Multiscale Decomposition")
|
|
199
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
200
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
201
|
+
self.setModal(False)
|
|
199
202
|
self.setMinimumSize(1050, 700)
|
|
200
203
|
self.residual_enabled = True
|
|
201
204
|
self._layer_noise = None # list[float] per detail layer
|
|
@@ -220,7 +220,10 @@ class NBtoRGBStars(QWidget):
|
|
|
220
220
|
if img.ndim == 3: img = img[...,0]
|
|
221
221
|
setattr(self, which.lower(), self._as_float01(img))
|
|
222
222
|
else: # OSC
|
|
223
|
-
|
|
223
|
+
# Optimization: Store mono OSC as-is (2D) to save memory
|
|
224
|
+
# The combine step will handle expansion.
|
|
225
|
+
if img.ndim == 3 and img.shape[2] == 1:
|
|
226
|
+
img = img[..., 0]
|
|
224
227
|
setattr(self, which.lower(), self._as_float01(img))
|
|
225
228
|
|
|
226
229
|
setattr(self, f"_file_{which.lower()}", path)
|
|
@@ -309,7 +312,14 @@ class NBtoRGBStars(QWidget):
|
|
|
309
312
|
raise ValueError(f"Channel sizes differ: {set(shapes)}")
|
|
310
313
|
|
|
311
314
|
if self.osc is not None:
|
|
312
|
-
|
|
315
|
+
if self.osc.ndim == 2:
|
|
316
|
+
r = self.osc; g = self.osc; b = self.osc
|
|
317
|
+
elif self.osc.ndim == 3 and self.osc.shape[2] >= 3:
|
|
318
|
+
r = self.osc[...,0]; g = self.osc[...,1]; b = self.osc[...,2]
|
|
319
|
+
else:
|
|
320
|
+
# fallback for unexpected shapes (e.g. 3D but 1-channel)
|
|
321
|
+
r = self.osc.squeeze(); g = r; b = r
|
|
322
|
+
|
|
313
323
|
sii = self.sii if self.sii is not None else r
|
|
314
324
|
ha = self.ha if self.ha is not None else r
|
|
315
325
|
oiii= self.oiii if self.oiii is not None else b
|