setiastrosuitepro 1.6.4__py3-none-any.whl → 1.6.10__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/images/abeicon.svg +16 -0
- setiastro/images/acv_icon.png +0 -0
- setiastro/images/cosmic.svg +40 -0
- setiastro/images/cosmicsat.svg +24 -0
- setiastro/images/first_quarter.png +0 -0
- setiastro/images/full_moon.png +0 -0
- setiastro/images/graxpert.svg +19 -0
- setiastro/images/last_quarter.png +0 -0
- setiastro/images/linearfit.svg +32 -0
- setiastro/images/new_moon.png +0 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/images/waning_crescent_1.png +0 -0
- setiastro/images/waning_crescent_2.png +0 -0
- setiastro/images/waning_crescent_3.png +0 -0
- setiastro/images/waning_crescent_4.png +0 -0
- setiastro/images/waning_crescent_5.png +0 -0
- setiastro/images/waning_gibbous_1.png +0 -0
- setiastro/images/waning_gibbous_2.png +0 -0
- setiastro/images/waning_gibbous_3.png +0 -0
- setiastro/images/waning_gibbous_4.png +0 -0
- setiastro/images/waning_gibbous_5.png +0 -0
- setiastro/images/waxing_crescent_1.png +0 -0
- setiastro/images/waxing_crescent_2.png +0 -0
- setiastro/images/waxing_crescent_3.png +0 -0
- setiastro/images/waxing_crescent_4.png +0 -0
- setiastro/images/waxing_crescent_5.png +0 -0
- setiastro/images/waxing_gibbous_1.png +0 -0
- setiastro/images/waxing_gibbous_2.png +0 -0
- setiastro/images/waxing_gibbous_3.png +0 -0
- setiastro/images/waxing_gibbous_4.png +0 -0
- setiastro/images/waxing_gibbous_5.png +0 -0
- setiastro/qml/ResourceMonitor.qml +84 -82
- setiastro/saspro/__main__.py +19 -0
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/abe.py +37 -4
- setiastro/saspro/aberration_ai.py +237 -21
- setiastro/saspro/acv_exporter.py +379 -0
- setiastro/saspro/add_stars.py +33 -6
- setiastro/saspro/backgroundneutral.py +35 -7
- setiastro/saspro/blemish_blaster.py +4 -1
- setiastro/saspro/blink_comparator_pro.py +74 -24
- setiastro/saspro/clahe.py +4 -1
- setiastro/saspro/continuum_subtract.py +4 -1
- setiastro/saspro/convo.py +4 -1
- setiastro/saspro/cosmicclarity.py +129 -18
- setiastro/saspro/crop_dialog_pro.py +123 -7
- setiastro/saspro/curve_editor_pro.py +109 -42
- setiastro/saspro/doc_manager.py +67 -4
- setiastro/saspro/exoplanet_detector.py +120 -28
- setiastro/saspro/frequency_separation.py +1158 -204
- setiastro/saspro/ghs_dialog_pro.py +81 -16
- setiastro/saspro/graxpert.py +1 -0
- setiastro/saspro/gui/main_window.py +393 -204
- setiastro/saspro/gui/mixins/menu_mixin.py +1 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +356 -12
- setiastro/saspro/gui/mixins/update_mixin.py +138 -36
- setiastro/saspro/gui/mixins/view_mixin.py +42 -0
- setiastro/saspro/halobgon.py +4 -0
- setiastro/saspro/histogram.py +5 -1
- setiastro/saspro/image_combine.py +4 -0
- setiastro/saspro/image_peeker_pro.py +4 -0
- setiastro/saspro/imageops/stretch.py +531 -62
- setiastro/saspro/isophote.py +4 -0
- setiastro/saspro/layers.py +13 -9
- setiastro/saspro/layers_dock.py +183 -3
- setiastro/saspro/legacy/image_manager.py +154 -20
- setiastro/saspro/legacy/numba_utils.py +43 -0
- setiastro/saspro/legacy/xisf.py +240 -98
- setiastro/saspro/live_stacking.py +180 -79
- setiastro/saspro/luminancerecombine.py +228 -27
- setiastro/saspro/mask_creation.py +174 -15
- setiastro/saspro/mfdeconv.py +113 -35
- setiastro/saspro/mfdeconvcudnn.py +119 -70
- setiastro/saspro/mfdeconvsport.py +112 -35
- setiastro/saspro/morphology.py +4 -0
- setiastro/saspro/multiscale_decomp.py +51 -12
- setiastro/saspro/numba_utils.py +72 -2
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/ops/script_editor.py +5 -2
- setiastro/saspro/ops/scripts.py +3 -0
- setiastro/saspro/perfect_palette_picker.py +37 -3
- setiastro/saspro/plate_solver.py +84 -49
- setiastro/saspro/psf_viewer.py +119 -37
- setiastro/saspro/resources.py +67 -0
- setiastro/saspro/rgbalign.py +4 -0
- setiastro/saspro/selective_color.py +4 -1
- setiastro/saspro/sfcc.py +60 -2
- setiastro/saspro/shortcuts.py +142 -23
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1017 -400
- setiastro/saspro/star_alignment.py +4 -1
- setiastro/saspro/star_spikes.py +4 -0
- setiastro/saspro/star_stretch.py +38 -3
- setiastro/saspro/stat_stretch.py +702 -128
- setiastro/saspro/subwindow.py +786 -360
- setiastro/saspro/supernovaasteroidhunter.py +1 -1
- setiastro/saspro/wavescale_hdr.py +4 -1
- setiastro/saspro/wavescalede.py +4 -1
- setiastro/saspro/whitebalance.py +60 -12
- setiastro/saspro/widgets/common_utilities.py +28 -21
- setiastro/saspro/widgets/resource_monitor.py +109 -59
- setiastro/saspro/widgets/spinboxes.py +10 -13
- setiastro/saspro/wimi.py +27 -656
- setiastro/saspro/wims.py +13 -3
- setiastro/saspro/xisf.py +101 -11
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/METADATA +2 -1
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/RECORD +112 -80
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/license.txt +0 -0
|
@@ -219,6 +219,10 @@ class MultiscaleDecompDialog(QDialog):
|
|
|
219
219
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
220
220
|
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
221
221
|
self.setModal(False)
|
|
222
|
+
try:
|
|
223
|
+
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
224
|
+
except Exception:
|
|
225
|
+
pass # older PyQt6 versions
|
|
222
226
|
self.setMinimumSize(1050, 700)
|
|
223
227
|
self.residual_enabled = True
|
|
224
228
|
self._layer_noise = None # list[float] per detail layer
|
|
@@ -575,19 +579,29 @@ class MultiscaleDecompDialog(QDialog):
|
|
|
575
579
|
|
|
576
580
|
# ---------- Preview plumbing ----------
|
|
577
581
|
def _spinner_on(self):
|
|
578
|
-
if getattr(self, "
|
|
582
|
+
if getattr(self, "_closing", False):
|
|
583
|
+
return
|
|
584
|
+
try:
|
|
585
|
+
sp = getattr(self, "busy_spinner", None)
|
|
586
|
+
if sp is None:
|
|
587
|
+
return
|
|
588
|
+
sp.setVisible(True)
|
|
589
|
+
mv = getattr(self, "_busy_movie", None)
|
|
590
|
+
if mv is not None and mv.state() != QMovie.MovieState.Running:
|
|
591
|
+
mv.start()
|
|
592
|
+
except RuntimeError:
|
|
579
593
|
return
|
|
580
|
-
self.busy_spinner.setVisible(True)
|
|
581
|
-
if getattr(self, "_busy_movie", None) is not None:
|
|
582
|
-
if self._busy_movie.state() != QMovie.MovieState.Running:
|
|
583
|
-
self._busy_movie.start()
|
|
584
594
|
|
|
585
595
|
def _spinner_off(self):
|
|
586
|
-
|
|
596
|
+
try:
|
|
597
|
+
sp = getattr(self, "busy_spinner", None)
|
|
598
|
+
mv = getattr(self, "_busy_movie", None)
|
|
599
|
+
if mv is not None:
|
|
600
|
+
mv.stop()
|
|
601
|
+
if sp is not None:
|
|
602
|
+
sp.setVisible(False)
|
|
603
|
+
except RuntimeError:
|
|
587
604
|
return
|
|
588
|
-
if getattr(self, "_busy_movie", None) is not None:
|
|
589
|
-
self._busy_movie.stop()
|
|
590
|
-
self.busy_spinner.setVisible(False)
|
|
591
605
|
|
|
592
606
|
|
|
593
607
|
def _show_busy_overlay(self):
|
|
@@ -619,11 +633,13 @@ class MultiscaleDecompDialog(QDialog):
|
|
|
619
633
|
self._schedule_preview()
|
|
620
634
|
|
|
621
635
|
def _schedule_preview(self):
|
|
622
|
-
|
|
636
|
+
if getattr(self, "_closing", False):
|
|
637
|
+
return
|
|
623
638
|
self._preview_timer.start(60)
|
|
624
639
|
|
|
625
640
|
def _schedule_roi_preview(self):
|
|
626
|
-
|
|
641
|
+
if getattr(self, "_closing", False):
|
|
642
|
+
return
|
|
627
643
|
self._preview_timer.start(60)
|
|
628
644
|
|
|
629
645
|
def _connect_viewport_signals(self):
|
|
@@ -760,8 +776,15 @@ class MultiscaleDecompDialog(QDialog):
|
|
|
760
776
|
return tuned, residual
|
|
761
777
|
|
|
762
778
|
def _rebuild_preview(self):
|
|
779
|
+
if getattr(self, "_closing", False):
|
|
780
|
+
return
|
|
763
781
|
self._spinner_on()
|
|
764
|
-
|
|
782
|
+
QTimer.singleShot(0, self._rebuild_preview_impl)
|
|
783
|
+
|
|
784
|
+
def _rebuild_preview_impl(self):
|
|
785
|
+
if getattr(self, "_closing", False):
|
|
786
|
+
return
|
|
787
|
+
|
|
765
788
|
#self._begin_busy()
|
|
766
789
|
try:
|
|
767
790
|
# ROI preview can't work until we have *some* pixmap in the scene to derive visible rects from.
|
|
@@ -1745,3 +1768,19 @@ class _MultiScaleDecompPresetDialog(QDialog):
|
|
|
1745
1768
|
"linked_rgb": bool(self.cb_linked.isChecked()),
|
|
1746
1769
|
"layers_cfg": out_layers,
|
|
1747
1770
|
}
|
|
1771
|
+
def closeEvent(self, ev):
|
|
1772
|
+
self._closing = True
|
|
1773
|
+
try:
|
|
1774
|
+
if hasattr(self, "_preview_timer"):
|
|
1775
|
+
self._preview_timer.stop()
|
|
1776
|
+
if hasattr(self, "_busy_show_timer"):
|
|
1777
|
+
self._busy_show_timer.stop()
|
|
1778
|
+
# Optional: disconnect scrollbars to stop ROI scheduling
|
|
1779
|
+
try:
|
|
1780
|
+
self.view.horizontalScrollBar().valueChanged.disconnect(self._schedule_roi_preview)
|
|
1781
|
+
self.view.verticalScrollBar().valueChanged.disconnect(self._schedule_roi_preview)
|
|
1782
|
+
except Exception:
|
|
1783
|
+
pass
|
|
1784
|
+
except Exception:
|
|
1785
|
+
pass
|
|
1786
|
+
super().closeEvent(ev)
|
setiastro/saspro/numba_utils.py
CHANGED
|
@@ -2495,7 +2495,77 @@ def drizzle_deposit_color_naive(image_data, affine_2x3, drizzle_buffer, coverage
|
|
|
2495
2495
|
|
|
2496
2496
|
return drizzle_buffer, coverage_buffer
|
|
2497
2497
|
|
|
2498
|
-
@njit(parallel=True, fastmath=True
|
|
2498
|
+
@njit(parallel=True, fastmath=True)
|
|
2499
|
+
def numba_mono_from_img(img, bp, denom, median_rescaled, target_median):
|
|
2500
|
+
H, W = img.shape
|
|
2501
|
+
out = np.empty_like(img)
|
|
2502
|
+
for y in prange(H):
|
|
2503
|
+
for x in range(W):
|
|
2504
|
+
r = (img[y, x] - bp) / denom
|
|
2505
|
+
numer = (median_rescaled - 1.0) * target_median * r
|
|
2506
|
+
denom2 = median_rescaled * (target_median + r - 1.0) - target_median * r
|
|
2507
|
+
if abs(denom2) < 1e-12:
|
|
2508
|
+
denom2 = 1e-12
|
|
2509
|
+
out[y, x] = numer / denom2
|
|
2510
|
+
return out
|
|
2511
|
+
|
|
2512
|
+
@njit(parallel=True, fastmath=True)
|
|
2513
|
+
def numba_color_linked_from_img(img, bp, denom, median_rescaled, target_median):
|
|
2514
|
+
H, W, C = img.shape
|
|
2515
|
+
out = np.empty_like(img)
|
|
2516
|
+
for y in prange(H):
|
|
2517
|
+
for x in range(W):
|
|
2518
|
+
for c in range(C):
|
|
2519
|
+
r = (img[y, x, c] - bp) / denom
|
|
2520
|
+
numer = (median_rescaled - 1.0) * target_median * r
|
|
2521
|
+
denom2 = median_rescaled * (target_median + r - 1.0) - target_median * r
|
|
2522
|
+
if abs(denom2) < 1e-12:
|
|
2523
|
+
denom2 = 1e-12
|
|
2524
|
+
out[y, x, c] = numer / denom2
|
|
2525
|
+
return out
|
|
2526
|
+
|
|
2527
|
+
@njit(parallel=True, fastmath=True)
|
|
2528
|
+
def numba_color_unlinked_from_img(img, bp3, denom3, meds_rescaled3, target_median):
|
|
2529
|
+
H, W, C = img.shape
|
|
2530
|
+
out = np.empty_like(img)
|
|
2531
|
+
for y in prange(H):
|
|
2532
|
+
for x in range(W):
|
|
2533
|
+
for c in range(C):
|
|
2534
|
+
r = (img[y, x, c] - bp3[c]) / denom3[c]
|
|
2535
|
+
med = meds_rescaled3[c]
|
|
2536
|
+
numer = (med - 1.0) * target_median * r
|
|
2537
|
+
denom2 = med * (target_median + r - 1.0) - target_median * r
|
|
2538
|
+
if abs(denom2) < 1e-12:
|
|
2539
|
+
denom2 = 1e-12
|
|
2540
|
+
out[y, x, c] = numer / denom2
|
|
2541
|
+
return out
|
|
2542
|
+
|
|
2543
|
+
@njit(parallel=True, fastmath=True)
|
|
2544
|
+
def numba_mono_final_formula(rescaled, median_rescaled, target_median):
|
|
2545
|
+
"""
|
|
2546
|
+
Applies the final formula *after* we already have the rescaled values.
|
|
2547
|
+
|
|
2548
|
+
rescaled[y,x] = (original[y,x] - black_point) / (1 - black_point)
|
|
2549
|
+
median_rescaled = median(rescaled)
|
|
2550
|
+
|
|
2551
|
+
out_val = ((median_rescaled - 1) * target_median * r) /
|
|
2552
|
+
( median_rescaled*(target_median + r -1) - target_median*r )
|
|
2553
|
+
"""
|
|
2554
|
+
H, W = rescaled.shape
|
|
2555
|
+
out = np.empty_like(rescaled)
|
|
2556
|
+
|
|
2557
|
+
for y in prange(H):
|
|
2558
|
+
for x in range(W):
|
|
2559
|
+
r = rescaled[y, x]
|
|
2560
|
+
numer = (median_rescaled - 1.0) * target_median * r
|
|
2561
|
+
denom = median_rescaled * (target_median + r - 1.0) - target_median * r
|
|
2562
|
+
if np.abs(denom) < 1e-12:
|
|
2563
|
+
denom = 1e-12
|
|
2564
|
+
out[y, x] = numer / denom
|
|
2565
|
+
|
|
2566
|
+
return out
|
|
2567
|
+
|
|
2568
|
+
@njit(parallel=True, fastmath=True)
|
|
2499
2569
|
def numba_color_final_formula_linked(rescaled, median_rescaled, target_median):
|
|
2500
2570
|
"""
|
|
2501
2571
|
Linked color transform: we use one median_rescaled for all channels.
|
|
@@ -2517,7 +2587,7 @@ def numba_color_final_formula_linked(rescaled, median_rescaled, target_median):
|
|
|
2517
2587
|
|
|
2518
2588
|
return out
|
|
2519
2589
|
|
|
2520
|
-
@njit(parallel=True, fastmath=True
|
|
2590
|
+
@njit(parallel=True, fastmath=True)
|
|
2521
2591
|
def numba_color_final_formula_unlinked(rescaled, medians_rescaled, target_median):
|
|
2522
2592
|
"""
|
|
2523
2593
|
Unlinked color transform: a separate median_rescaled per channel.
|
setiastro/saspro/ops/commands.py
CHANGED
|
@@ -202,7 +202,7 @@ register(CommandSpec(
|
|
|
202
202
|
"optional targets, inherit_target."
|
|
203
203
|
),
|
|
204
204
|
call_style="ctx.run_command",
|
|
205
|
-
import_path="
|
|
205
|
+
import_path="setiastro.saspro.function_bundle", # <── important
|
|
206
206
|
callable_name="run_function_bundle_command",# <── important
|
|
207
207
|
notes=(
|
|
208
208
|
"Use this command from scripts to run a saved Function Bundle or an "
|
|
@@ -274,7 +274,7 @@ register(CommandSpec(
|
|
|
274
274
|
group="Bundles",
|
|
275
275
|
summary="Internal bundle runner. steps=[...], targets='all_open'|[doc_ptrs], stop_on_error.",
|
|
276
276
|
call_style="ctx.run_command",
|
|
277
|
-
import_path="
|
|
277
|
+
import_path="setiastro.saspro.function_bundle",
|
|
278
278
|
callable_name="run_function_bundle_command",
|
|
279
279
|
))
|
|
280
280
|
|
|
@@ -388,7 +388,7 @@ register(CommandSpec(
|
|
|
388
388
|
id="ghs",
|
|
389
389
|
name="Generalized Hyperbolic Stretch",
|
|
390
390
|
group="Stretch",
|
|
391
|
-
import_path="
|
|
391
|
+
import_path="setiastro.saspro.ghs_preset",
|
|
392
392
|
callable_name="apply_ghs_via_preset",
|
|
393
393
|
ui_method="open_ghs_with_preset",
|
|
394
394
|
summary=(
|
|
@@ -499,7 +499,7 @@ register(CommandSpec(
|
|
|
499
499
|
id="curves",
|
|
500
500
|
title="Curves",
|
|
501
501
|
group="Stretch",
|
|
502
|
-
import_path="
|
|
502
|
+
import_path="setiastro.saspro.curves_preset",
|
|
503
503
|
callable_name="apply_curves_via_preset",
|
|
504
504
|
ui_method="open_curves_with_preset",
|
|
505
505
|
summary=(
|
|
@@ -597,7 +597,7 @@ register(CommandSpec(
|
|
|
597
597
|
id="abe",
|
|
598
598
|
title="Automatic Background Extraction",
|
|
599
599
|
group="Background",
|
|
600
|
-
import_path="
|
|
600
|
+
import_path="setiastro.saspro.abe_preset",
|
|
601
601
|
callable_name="apply_abe_via_preset",
|
|
602
602
|
ui_method="open_abe_with_preset", # ✅ matches your pro/abe_preset.py
|
|
603
603
|
summary=(
|
|
@@ -683,7 +683,7 @@ register(CommandSpec(
|
|
|
683
683
|
id="graxpert",
|
|
684
684
|
title="GraXpert Gradient / Denoise",
|
|
685
685
|
group="Background",
|
|
686
|
-
import_path="
|
|
686
|
+
import_path="setiastro.saspro.graxpert_preset",
|
|
687
687
|
callable_name="run_graxpert_via_preset",
|
|
688
688
|
# no ui_method here unless you want to open your optional preset dialog from drops
|
|
689
689
|
# ui_method="open_graxpert_with_preset", # (only if/when you add one)
|
|
@@ -807,7 +807,7 @@ register(CommandSpec(
|
|
|
807
807
|
id="background_neutral",
|
|
808
808
|
name="Background Neutralization",
|
|
809
809
|
group="Background",
|
|
810
|
-
import_path="
|
|
810
|
+
import_path="setiastro.saspro.backgroundneutral",
|
|
811
811
|
callable_name="run_background_neutral_via_preset",
|
|
812
812
|
summary=(
|
|
813
813
|
"Neutralizes RGB background either automatically or using a user-specified "
|
|
@@ -865,7 +865,7 @@ register(CommandSpec(
|
|
|
865
865
|
id="remove_green",
|
|
866
866
|
name="Remove Green (SCNR)",
|
|
867
867
|
group="Color",
|
|
868
|
-
import_path="
|
|
868
|
+
import_path="setiastro.saspro.remove_green",
|
|
869
869
|
callable_name="apply_remove_green_preset_to_doc",
|
|
870
870
|
ui_method="open_remove_green_dialog",
|
|
871
871
|
summary=(
|
|
@@ -1044,7 +1044,7 @@ register(CommandSpec(
|
|
|
1044
1044
|
id="recombine_luminance",
|
|
1045
1045
|
name="Recombine Luminance",
|
|
1046
1046
|
group="Luminance",
|
|
1047
|
-
import_path="
|
|
1047
|
+
import_path="setiastro.saspro.luminancerecombine",
|
|
1048
1048
|
callable_name="run_recombine_luminance_via_preset",
|
|
1049
1049
|
ui_method="_recombine_luminance_ui",
|
|
1050
1050
|
notes=(
|
|
@@ -1144,7 +1144,7 @@ register(CommandSpec(
|
|
|
1144
1144
|
id="wavescale_hdr",
|
|
1145
1145
|
name="WaveScale HDR",
|
|
1146
1146
|
group="Contrast",
|
|
1147
|
-
import_path="
|
|
1147
|
+
import_path="setiastro.saspro.wavescale_hdr_preset",
|
|
1148
1148
|
callable_name="run_wavescale_hdr_via_preset",
|
|
1149
1149
|
ui_method="_open_wavescale_hdr", # or whatever your main window uses
|
|
1150
1150
|
summary=(
|
|
@@ -1188,7 +1188,7 @@ register(CommandSpec(
|
|
|
1188
1188
|
id="wavescale_dark_enhance",
|
|
1189
1189
|
name="WaveScale Dark Enhance",
|
|
1190
1190
|
group="Contrast",
|
|
1191
|
-
import_path="
|
|
1191
|
+
import_path="setiastro.saspro.wavescalede_preset",
|
|
1192
1192
|
callable_name="run_wavescalede_via_preset",
|
|
1193
1193
|
ui_method="_open_wavescale_dark_enhance", # adjust if your main window uses a different name
|
|
1194
1194
|
summary=(
|
|
@@ -1295,7 +1295,7 @@ register(CommandSpec(
|
|
|
1295
1295
|
id="aberration_ai",
|
|
1296
1296
|
title="Aberration AI",
|
|
1297
1297
|
group="Optics",
|
|
1298
|
-
import_path="
|
|
1298
|
+
import_path="setiastro.saspro.aberration_ai_preset",
|
|
1299
1299
|
callable_name="run_aberration_ai_via_preset",
|
|
1300
1300
|
# ui_method="open_aberration_ai_dialog", # if you have one; otherwise omit
|
|
1301
1301
|
presets=[
|
|
@@ -1338,7 +1338,7 @@ register(CommandSpec(
|
|
|
1338
1338
|
id="convo",
|
|
1339
1339
|
title="Convolution / Deconvolution",
|
|
1340
1340
|
group="Blur & Sharpen",
|
|
1341
|
-
import_path="
|
|
1341
|
+
import_path="setiastro.saspro.convo_preset",
|
|
1342
1342
|
callable_name="run_convo_via_preset",
|
|
1343
1343
|
aliases=[
|
|
1344
1344
|
"convolution",
|
|
@@ -1433,7 +1433,7 @@ register(CommandSpec(
|
|
|
1433
1433
|
id="cosmic_clarity",
|
|
1434
1434
|
title="Cosmic Clarity",
|
|
1435
1435
|
group="AI",
|
|
1436
|
-
import_path="
|
|
1436
|
+
import_path="setiastro.saspro.cosmicclarity_preset",
|
|
1437
1437
|
callable_name="run_cosmicclarity_via_preset",
|
|
1438
1438
|
presets=[
|
|
1439
1439
|
PresetSpec("mode", "enum", default="sharpen",
|
|
@@ -1484,7 +1484,7 @@ register(CommandSpec(
|
|
|
1484
1484
|
id="debayer",
|
|
1485
1485
|
title="Debayer",
|
|
1486
1486
|
group="Color / CFA",
|
|
1487
|
-
import_path="
|
|
1487
|
+
import_path="setiastro.saspro.debayer",
|
|
1488
1488
|
callable_name="run_debayer_via_preset",
|
|
1489
1489
|
presets=[
|
|
1490
1490
|
PresetSpec(
|
|
@@ -1506,7 +1506,7 @@ register(CommandSpec(
|
|
|
1506
1506
|
id="linear_fit",
|
|
1507
1507
|
title="Linear Fit",
|
|
1508
1508
|
group="Calibration",
|
|
1509
|
-
import_path="
|
|
1509
|
+
import_path="setiastro.saspro.linear_fit",
|
|
1510
1510
|
callable_name="run_linear_fit_via_preset",
|
|
1511
1511
|
presets=[
|
|
1512
1512
|
PresetSpec(
|
|
@@ -1527,7 +1527,7 @@ register(CommandSpec(
|
|
|
1527
1527
|
id="morphology",
|
|
1528
1528
|
title="Morphology",
|
|
1529
1529
|
group="Masks & Morphology",
|
|
1530
|
-
import_path="
|
|
1530
|
+
import_path="setiastro.saspro.morphology",
|
|
1531
1531
|
callable_name="apply_morphology_to_doc",
|
|
1532
1532
|
presets=[
|
|
1533
1533
|
PresetSpec(
|
|
@@ -1556,7 +1556,7 @@ register(CommandSpec(
|
|
|
1556
1556
|
id="remove_stars",
|
|
1557
1557
|
title="Remove Stars",
|
|
1558
1558
|
group="Star Tools",
|
|
1559
|
-
import_path="
|
|
1559
|
+
import_path="setiastro.saspro.remove_stars_preset",
|
|
1560
1560
|
callable_name="run_remove_stars_via_preset",
|
|
1561
1561
|
replay_apply_name="apply_remove_stars_to_doc",
|
|
1562
1562
|
presets=[
|
|
@@ -1058,7 +1058,8 @@ class ScriptEditorDock(QDockWidget):
|
|
|
1058
1058
|
if man is None:
|
|
1059
1059
|
raise RuntimeError("ScriptManager not initialized on main window.")
|
|
1060
1060
|
|
|
1061
|
-
entry = man.
|
|
1061
|
+
entry = man.load_script_from_path(self._current_path)
|
|
1062
|
+
|
|
1062
1063
|
if entry is None or entry.run is None:
|
|
1063
1064
|
raise RuntimeError("Script has no run(ctx).")
|
|
1064
1065
|
|
|
@@ -1074,7 +1075,9 @@ class ScriptEditorDock(QDockWidget):
|
|
|
1074
1075
|
self.output.appendPlainText(tb)
|
|
1075
1076
|
self._log("Script ERROR:\n" + tb)
|
|
1076
1077
|
|
|
1077
|
-
|
|
1078
|
+
def load_script_from_path(self, path: Path) -> ScriptEntry | None:
|
|
1079
|
+
scripts_root = get_scripts_dir()
|
|
1080
|
+
return self._load_one_script(path, scripts_root)
|
|
1078
1081
|
|
|
1079
1082
|
# ------------------------------------------------------------------
|
|
1080
1083
|
# ui helpers
|
setiastro/saspro/ops/scripts.py
CHANGED
|
@@ -678,6 +678,9 @@ class ScriptManager(QObject):
|
|
|
678
678
|
|
|
679
679
|
self._log(f"[Scripts] Loaded {len(self.registry)} script(s) from {scripts_dir}")
|
|
680
680
|
|
|
681
|
+
def load_script_from_path(self, path: Path) -> ScriptEntry | None:
|
|
682
|
+
scripts_root = get_scripts_dir()
|
|
683
|
+
return self._load_one_script(path, scripts_root)
|
|
681
684
|
|
|
682
685
|
def _load_one_script(self, path: Path, scripts_root: Path) -> ScriptEntry | None:
|
|
683
686
|
"""
|
|
@@ -227,6 +227,7 @@ class PerfectPalettePicker(QWidget):
|
|
|
227
227
|
self.sii = None
|
|
228
228
|
self.osc1 = None
|
|
229
229
|
self.osc2 = None
|
|
230
|
+
self._dim_mismatch_accepted = False
|
|
230
231
|
|
|
231
232
|
# stretched cache (per input name → stretched array)
|
|
232
233
|
self._stretched: dict[str, np.ndarray] = {}
|
|
@@ -675,10 +676,42 @@ class PerfectPalettePicker(QWidget):
|
|
|
675
676
|
oo = g2b2 if oo is None else 0.5*oo + 0.5*g2b2
|
|
676
677
|
|
|
677
678
|
# shapes must match for full-size
|
|
678
|
-
shapes
|
|
679
|
+
# shapes must match for full-size
|
|
680
|
+
shapes = [x.shape[:2] for x in (ha, oo, si) if x is not None]
|
|
679
681
|
if len(shapes) and len(set(shapes)) > 1 and not for_thumbs:
|
|
680
|
-
|
|
681
|
-
|
|
682
|
+
# pick a reference size (prefer Ha, then OIII, then SII)
|
|
683
|
+
ref = ha if ha is not None else (oo if oo is not None else si)
|
|
684
|
+
ref_name = "Ha" if ha is not None else ("OIII" if oo is not None else "SII")
|
|
685
|
+
ref_h, ref_w = ref.shape[:2]
|
|
686
|
+
|
|
687
|
+
# Only prompt once per session unless you want every time
|
|
688
|
+
if not self._dim_mismatch_accepted:
|
|
689
|
+
msg = (
|
|
690
|
+
"The loaded channels have different image dimensions.\n\n"
|
|
691
|
+
f"• Ha: {None if ha is None else ha.shape}\n"
|
|
692
|
+
f"• OIII: {None if oo is None else oo.shape}\n"
|
|
693
|
+
f"• SII: {None if si is None else si.shape}\n\n"
|
|
694
|
+
f"SASpro can resize (warp) the channels to match the reference frame:\n"
|
|
695
|
+
f"• Reference: {ref_name}\n"
|
|
696
|
+
f"• Target size: ({ref_w} × {ref_h})\n\n"
|
|
697
|
+
"Proceed and resize mismatched channels?"
|
|
698
|
+
)
|
|
699
|
+
ret = QMessageBox.question(
|
|
700
|
+
self,
|
|
701
|
+
"Channel Size Mismatch",
|
|
702
|
+
msg,
|
|
703
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
704
|
+
QMessageBox.StandardButton.Yes
|
|
705
|
+
)
|
|
706
|
+
if ret != QMessageBox.StandardButton.Yes:
|
|
707
|
+
return None, None, None
|
|
708
|
+
|
|
709
|
+
self._dim_mismatch_accepted = True
|
|
710
|
+
|
|
711
|
+
# resize to reference
|
|
712
|
+
ha = self._resize_to(ha, (ref_w, ref_h)) if ha is not None else None
|
|
713
|
+
oo = self._resize_to(oo, (ref_w, ref_h)) if oo is not None else None
|
|
714
|
+
si = self._resize_to(si, (ref_w, ref_h)) if si is not None else None
|
|
682
715
|
|
|
683
716
|
# thumbnails: crop AFTER stretch/synth
|
|
684
717
|
if for_thumbs:
|
|
@@ -953,6 +986,7 @@ class PerfectPalettePicker(QWidget):
|
|
|
953
986
|
def _clear_channels(self):
|
|
954
987
|
self.ha = self.oiii = self.sii = self.osc1 = self.osc2 = None
|
|
955
988
|
self._stretched.clear()
|
|
989
|
+
self._dim_mismatch_accepted = False
|
|
956
990
|
self.final = None
|
|
957
991
|
self.preview.clear()
|
|
958
992
|
for which in ("Ha","OIII","SII","OSC1","OSC2"):
|
setiastro/saspro/plate_solver.py
CHANGED
|
@@ -116,16 +116,15 @@ def _status_popup_update(text: str):
|
|
|
116
116
|
_STATUS_POPUP.update_text(text)
|
|
117
117
|
|
|
118
118
|
def _status_popup_close():
|
|
119
|
-
"""Hide (but do not destroy) the singleton status popup if it exists."""
|
|
120
119
|
global _STATUS_POPUP
|
|
120
|
+
if _STATUS_POPUP is None:
|
|
121
|
+
return
|
|
121
122
|
try:
|
|
122
|
-
|
|
123
|
-
_STATUS_POPUP.hide()
|
|
124
|
-
# keep instance for reuse (fast re-open)
|
|
123
|
+
_STATUS_POPUP.hide()
|
|
125
124
|
except Exception:
|
|
126
|
-
# Completely safe to ignore; worst case the popup was already gone.
|
|
127
125
|
pass
|
|
128
126
|
|
|
127
|
+
|
|
129
128
|
def _sleep_ui(ms: int):
|
|
130
129
|
"""Non-blocking sleep that keeps the UI responsive."""
|
|
131
130
|
loop = QEventLoop()
|
|
@@ -137,54 +136,38 @@ def _with_events():
|
|
|
137
136
|
QApplication.processEvents()
|
|
138
137
|
|
|
139
138
|
def _set_status_ui(parent, text: str):
|
|
140
|
-
"""
|
|
141
|
-
Update dialog/main-window status or batch log; if neither exists (headless),
|
|
142
|
-
show/update a small modeless popup. Always pumps events for responsiveness.
|
|
143
|
-
"""
|
|
144
139
|
try:
|
|
145
140
|
updated_any = False
|
|
146
141
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
target =
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
142
|
+
target = None
|
|
143
|
+
if hasattr(parent, "status") and isinstance(getattr(parent, "status"), QLabel):
|
|
144
|
+
target = parent.status
|
|
145
|
+
if target is None and hasattr(parent, "findChild"):
|
|
146
|
+
target = parent.findChild(QLabel, "status_label")
|
|
147
|
+
if target is not None:
|
|
148
|
+
target.setText(text)
|
|
149
|
+
updated_any = True
|
|
150
|
+
|
|
151
|
+
logw = getattr(parent, "log", None)
|
|
152
|
+
if logw and hasattr(logw, "append"):
|
|
153
|
+
tr_status = QCoreApplication.translate("PlateSolver", "Status:")
|
|
154
|
+
if text and (text.startswith("Status:") or text.startswith(tr_status) or text.startswith("▶") or text.startswith("✔") or text.startswith("❌")):
|
|
155
|
+
logw.append(text)
|
|
158
156
|
updated_any = True
|
|
159
157
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if logw and hasattr(logw, "append"):
|
|
163
|
-
tr_status = QCoreApplication.translate("PlateSolver", "Status:")
|
|
164
|
-
if text and (text.startswith("Status:") or text.startswith(tr_status) or text.startswith("▶") or text.startswith("✔") or text.startswith("❌")):
|
|
165
|
-
logw.append(text)
|
|
166
|
-
updated_any = True
|
|
167
|
-
|
|
168
|
-
# If we couldn't update any inline widget, use the headless popup.
|
|
169
|
-
if not updated_any:
|
|
170
|
-
_status_popup_open(parent, text)
|
|
171
|
-
else:
|
|
172
|
-
# If inline widgets exist and popup is visible, keep it quiet.
|
|
173
|
-
_status_popup_update(text)
|
|
174
|
-
|
|
175
|
-
QApplication.processEvents()
|
|
176
|
-
|
|
177
|
-
if isinstance(parent, QWidget):
|
|
178
|
-
QTimer.singleShot(0, _do)
|
|
158
|
+
if not updated_any:
|
|
159
|
+
_status_popup_open(parent, text)
|
|
179
160
|
else:
|
|
180
|
-
|
|
161
|
+
_status_popup_update(text)
|
|
162
|
+
|
|
163
|
+
QApplication.processEvents()
|
|
181
164
|
except Exception:
|
|
182
|
-
# Last-resort popup if even the above failed
|
|
183
165
|
try:
|
|
184
166
|
_status_popup_open(parent, text)
|
|
185
167
|
except Exception:
|
|
186
168
|
pass
|
|
187
169
|
|
|
170
|
+
|
|
188
171
|
def _wait_process(proc: QProcess, timeout_ms: int, parent=None) -> bool:
|
|
189
172
|
"""
|
|
190
173
|
Incrementally wait for a QProcess while pumping UI events so the dialog stays responsive.
|
|
@@ -234,10 +217,53 @@ def _get_solvefield_exe(settings) -> str:
|
|
|
234
217
|
return cand[0] # may be empty (used to decide web vs. local)
|
|
235
218
|
|
|
236
219
|
def _get_astrometry_api_key(settings) -> str:
|
|
237
|
-
|
|
220
|
+
"""
|
|
221
|
+
Canonical key: 'api/astrometry_key' (matches SettingsDialog).
|
|
222
|
+
Also check older legacy keys for backward compatibility.
|
|
223
|
+
"""
|
|
224
|
+
if settings is None:
|
|
225
|
+
return ""
|
|
226
|
+
|
|
227
|
+
# ✅ canonical
|
|
228
|
+
key = settings.value("api/astrometry_key", "", type=str) or ""
|
|
229
|
+
key = key.strip()
|
|
230
|
+
if key:
|
|
231
|
+
return key
|
|
232
|
+
|
|
233
|
+
# 🔁 legacy fallbacks (if you ever stored them differently)
|
|
234
|
+
for k in (
|
|
235
|
+
"api/astrometry", # old guess
|
|
236
|
+
"astrometry/api_key",
|
|
237
|
+
"astrometry/key",
|
|
238
|
+
"astrometry_key",
|
|
239
|
+
"plate_solver/astrometry_key",
|
|
240
|
+
):
|
|
241
|
+
v = settings.value(k, "", type=str) or ""
|
|
242
|
+
v = v.strip()
|
|
243
|
+
if v:
|
|
244
|
+
# migrate forward so it works next time
|
|
245
|
+
settings.setValue("api/astrometry_key", v)
|
|
246
|
+
try:
|
|
247
|
+
settings.remove(k)
|
|
248
|
+
except Exception:
|
|
249
|
+
pass
|
|
250
|
+
try:
|
|
251
|
+
settings.sync()
|
|
252
|
+
except Exception:
|
|
253
|
+
pass
|
|
254
|
+
return v
|
|
255
|
+
|
|
256
|
+
return ""
|
|
238
257
|
|
|
239
|
-
|
|
240
|
-
|
|
258
|
+
|
|
259
|
+
def _set_astrometry_api_key(settings, key: str) -> None:
|
|
260
|
+
if settings is None:
|
|
261
|
+
return
|
|
262
|
+
settings.setValue("api/astrometry_key", (key or "").strip())
|
|
263
|
+
try:
|
|
264
|
+
settings.sync()
|
|
265
|
+
except Exception:
|
|
266
|
+
pass
|
|
241
267
|
|
|
242
268
|
def _wcs_header_from_astrometry_calib(calib: dict, image_shape: tuple[int, ...]) -> Header:
|
|
243
269
|
"""
|
|
@@ -1774,7 +1800,8 @@ def _debug_dump_meta(label: str, meta: dict):
|
|
|
1774
1800
|
print(f" {k}: {type(v).__name__}")
|
|
1775
1801
|
print("================================\n")
|
|
1776
1802
|
|
|
1777
|
-
|
|
1803
|
+
def tr(s: str) -> str:
|
|
1804
|
+
return QCoreApplication.translate("PlateSolver", s)
|
|
1778
1805
|
|
|
1779
1806
|
def plate_solve_doc_inplace(parent, doc, settings) -> Tuple[bool, Header | str]:
|
|
1780
1807
|
img = getattr(doc, "image", None)
|
|
@@ -1831,8 +1858,9 @@ def plate_solve_doc_inplace(parent, doc, settings) -> Tuple[bool, Header | str]:
|
|
|
1831
1858
|
(hasattr(parent, "findChild") and parent.findChild(QLabel, "status_label") is not None)
|
|
1832
1859
|
)
|
|
1833
1860
|
if headless:
|
|
1834
|
-
_status_popup_open(parent,
|
|
1861
|
+
_status_popup_open(parent, tr("Status: Preparing plate solve…"))
|
|
1835
1862
|
|
|
1863
|
+
ok_solve = False
|
|
1836
1864
|
try:
|
|
1837
1865
|
ok, res = _solve_numpy_with_fallback(parent, settings, img, seed_h)
|
|
1838
1866
|
if not ok:
|
|
@@ -1886,11 +1914,18 @@ def plate_solve_doc_inplace(parent, doc, settings) -> Tuple[bool, Header | str]:
|
|
|
1886
1914
|
if hasattr(parent, "currentDocumentChanged"):
|
|
1887
1915
|
QTimer.singleShot(0, lambda: parent.currentDocumentChanged.emit(doc))
|
|
1888
1916
|
|
|
1889
|
-
_set_status_ui(parent,
|
|
1890
|
-
|
|
1917
|
+
_set_status_ui(parent, tr("Status: Plate solve completed."))
|
|
1918
|
+
|
|
1919
|
+
|
|
1920
|
+
ok_solve = True
|
|
1921
|
+
if headless:
|
|
1922
|
+
QTimer.singleShot(1200, _status_popup_close)
|
|
1923
|
+
else:
|
|
1924
|
+
_status_popup_close()
|
|
1891
1925
|
return True, hdr
|
|
1892
1926
|
finally:
|
|
1893
|
-
|
|
1927
|
+
if not ok_solve:
|
|
1928
|
+
_status_popup_close()
|
|
1894
1929
|
|
|
1895
1930
|
|
|
1896
1931
|
|