setiastrosuitepro 1.6.4__py3-none-any.whl → 1.6.12__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/colorwheel.svg +97 -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 +20 -1
- 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 +108 -40
- 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 +13 -7
- 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 +245 -15
- 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 +429 -228
- setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
- setiastro/saspro/gui/mixins/menu_mixin.py +27 -1
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +384 -18
- 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/starbasedwhitebalance.py +23 -52
- setiastro/saspro/imageops/stretch.py +582 -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 +67 -47
- 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 -57
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/ops/script_editor.py +10 -2
- setiastro/saspro/ops/scripts.py +122 -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 +364 -152
- setiastro/saspro/shortcuts.py +160 -29
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1331 -484
- setiastro/saspro/star_alignment.py +247 -123
- setiastro/saspro/star_spikes.py +4 -0
- setiastro/saspro/star_stretch.py +38 -3
- setiastro/saspro/stat_stretch.py +743 -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 +84 -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.12.dist-info}/METADATA +2 -1
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/RECORD +115 -82
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.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
|
@@ -317,61 +317,6 @@ def invert_image_numba(image):
|
|
|
317
317
|
output[y, x, c] = 1.0 - image[y, x, c]
|
|
318
318
|
return output
|
|
319
319
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
@njit(parallel=True, fastmath=True, cache=True)
|
|
323
|
-
def apply_flat_division_numba_2d(image, master_flat, master_bias=None):
|
|
324
|
-
"""
|
|
325
|
-
Mono version: image.shape == (H,W)
|
|
326
|
-
"""
|
|
327
|
-
if master_bias is not None:
|
|
328
|
-
master_flat = master_flat - master_bias
|
|
329
|
-
image = image - master_bias
|
|
330
|
-
|
|
331
|
-
median_flat = np.mean(master_flat)
|
|
332
|
-
height, width = image.shape
|
|
333
|
-
|
|
334
|
-
for y in prange(height):
|
|
335
|
-
for x in range(width):
|
|
336
|
-
image[y, x] /= (master_flat[y, x] / median_flat)
|
|
337
|
-
|
|
338
|
-
return image
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
@njit(parallel=True, fastmath=True, cache=True)
|
|
342
|
-
def apply_flat_division_numba_3d(image, master_flat, master_bias=None):
|
|
343
|
-
"""
|
|
344
|
-
Color version: image.shape == (H,W,C)
|
|
345
|
-
"""
|
|
346
|
-
if master_bias is not None:
|
|
347
|
-
master_flat = master_flat - master_bias
|
|
348
|
-
image = image - master_bias
|
|
349
|
-
|
|
350
|
-
median_flat = np.mean(master_flat)
|
|
351
|
-
height, width, channels = image.shape
|
|
352
|
-
|
|
353
|
-
for y in prange(height):
|
|
354
|
-
for x in range(width):
|
|
355
|
-
for c in range(channels):
|
|
356
|
-
image[y, x, c] /= (master_flat[y, x, c] / median_flat)
|
|
357
|
-
|
|
358
|
-
return image
|
|
359
|
-
|
|
360
|
-
def apply_flat_division_numba(image, master_flat, master_bias=None):
|
|
361
|
-
"""
|
|
362
|
-
Dispatcher that calls the correct Numba function
|
|
363
|
-
depending on whether 'image' is 2D or 3D.
|
|
364
|
-
"""
|
|
365
|
-
if image.ndim == 2:
|
|
366
|
-
# Mono
|
|
367
|
-
return apply_flat_division_numba_2d(image, master_flat, master_bias)
|
|
368
|
-
elif image.ndim == 3:
|
|
369
|
-
# Color
|
|
370
|
-
return apply_flat_division_numba_3d(image, master_flat, master_bias)
|
|
371
|
-
else:
|
|
372
|
-
raise ValueError(f"apply_flat_division_numba: expected 2D or 3D, got shape {image.shape}")
|
|
373
|
-
|
|
374
|
-
|
|
375
320
|
@njit(parallel=True, cache=True)
|
|
376
321
|
def subtract_dark_3d(frames, dark_frame):
|
|
377
322
|
"""
|
|
@@ -2495,7 +2440,77 @@ def drizzle_deposit_color_naive(image_data, affine_2x3, drizzle_buffer, coverage
|
|
|
2495
2440
|
|
|
2496
2441
|
return drizzle_buffer, coverage_buffer
|
|
2497
2442
|
|
|
2498
|
-
@njit(parallel=True, fastmath=True
|
|
2443
|
+
@njit(parallel=True, fastmath=True)
|
|
2444
|
+
def numba_mono_from_img(img, bp, denom, median_rescaled, target_median):
|
|
2445
|
+
H, W = img.shape
|
|
2446
|
+
out = np.empty_like(img)
|
|
2447
|
+
for y in prange(H):
|
|
2448
|
+
for x in range(W):
|
|
2449
|
+
r = (img[y, x] - bp) / denom
|
|
2450
|
+
numer = (median_rescaled - 1.0) * target_median * r
|
|
2451
|
+
denom2 = median_rescaled * (target_median + r - 1.0) - target_median * r
|
|
2452
|
+
if abs(denom2) < 1e-12:
|
|
2453
|
+
denom2 = 1e-12
|
|
2454
|
+
out[y, x] = numer / denom2
|
|
2455
|
+
return out
|
|
2456
|
+
|
|
2457
|
+
@njit(parallel=True, fastmath=True)
|
|
2458
|
+
def numba_color_linked_from_img(img, bp, denom, median_rescaled, target_median):
|
|
2459
|
+
H, W, C = img.shape
|
|
2460
|
+
out = np.empty_like(img)
|
|
2461
|
+
for y in prange(H):
|
|
2462
|
+
for x in range(W):
|
|
2463
|
+
for c in range(C):
|
|
2464
|
+
r = (img[y, x, c] - bp) / denom
|
|
2465
|
+
numer = (median_rescaled - 1.0) * target_median * r
|
|
2466
|
+
denom2 = median_rescaled * (target_median + r - 1.0) - target_median * r
|
|
2467
|
+
if abs(denom2) < 1e-12:
|
|
2468
|
+
denom2 = 1e-12
|
|
2469
|
+
out[y, x, c] = numer / denom2
|
|
2470
|
+
return out
|
|
2471
|
+
|
|
2472
|
+
@njit(parallel=True, fastmath=True)
|
|
2473
|
+
def numba_color_unlinked_from_img(img, bp3, denom3, meds_rescaled3, target_median):
|
|
2474
|
+
H, W, C = img.shape
|
|
2475
|
+
out = np.empty_like(img)
|
|
2476
|
+
for y in prange(H):
|
|
2477
|
+
for x in range(W):
|
|
2478
|
+
for c in range(C):
|
|
2479
|
+
r = (img[y, x, c] - bp3[c]) / denom3[c]
|
|
2480
|
+
med = meds_rescaled3[c]
|
|
2481
|
+
numer = (med - 1.0) * target_median * r
|
|
2482
|
+
denom2 = med * (target_median + r - 1.0) - target_median * r
|
|
2483
|
+
if abs(denom2) < 1e-12:
|
|
2484
|
+
denom2 = 1e-12
|
|
2485
|
+
out[y, x, c] = numer / denom2
|
|
2486
|
+
return out
|
|
2487
|
+
|
|
2488
|
+
@njit(parallel=True, fastmath=True)
|
|
2489
|
+
def numba_mono_final_formula(rescaled, median_rescaled, target_median):
|
|
2490
|
+
"""
|
|
2491
|
+
Applies the final formula *after* we already have the rescaled values.
|
|
2492
|
+
|
|
2493
|
+
rescaled[y,x] = (original[y,x] - black_point) / (1 - black_point)
|
|
2494
|
+
median_rescaled = median(rescaled)
|
|
2495
|
+
|
|
2496
|
+
out_val = ((median_rescaled - 1) * target_median * r) /
|
|
2497
|
+
( median_rescaled*(target_median + r -1) - target_median*r )
|
|
2498
|
+
"""
|
|
2499
|
+
H, W = rescaled.shape
|
|
2500
|
+
out = np.empty_like(rescaled)
|
|
2501
|
+
|
|
2502
|
+
for y in prange(H):
|
|
2503
|
+
for x in range(W):
|
|
2504
|
+
r = rescaled[y, x]
|
|
2505
|
+
numer = (median_rescaled - 1.0) * target_median * r
|
|
2506
|
+
denom = median_rescaled * (target_median + r - 1.0) - target_median * r
|
|
2507
|
+
if np.abs(denom) < 1e-12:
|
|
2508
|
+
denom = 1e-12
|
|
2509
|
+
out[y, x] = numer / denom
|
|
2510
|
+
|
|
2511
|
+
return out
|
|
2512
|
+
|
|
2513
|
+
@njit(parallel=True, fastmath=True)
|
|
2499
2514
|
def numba_color_final_formula_linked(rescaled, median_rescaled, target_median):
|
|
2500
2515
|
"""
|
|
2501
2516
|
Linked color transform: we use one median_rescaled for all channels.
|
|
@@ -2517,7 +2532,7 @@ def numba_color_final_formula_linked(rescaled, median_rescaled, target_median):
|
|
|
2517
2532
|
|
|
2518
2533
|
return out
|
|
2519
2534
|
|
|
2520
|
-
@njit(parallel=True, fastmath=True
|
|
2535
|
+
@njit(parallel=True, fastmath=True)
|
|
2521
2536
|
def numba_color_final_formula_unlinked(rescaled, medians_rescaled, target_median):
|
|
2522
2537
|
"""
|
|
2523
2538
|
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=[
|
|
@@ -15,6 +15,11 @@ from PyQt6.QtWidgets import (
|
|
|
15
15
|
QLineEdit, QToolButton, QCheckBox, QTextEdit
|
|
16
16
|
)
|
|
17
17
|
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from setiastro.saspro.ops.scripts import ScriptEntry
|
|
22
|
+
|
|
18
23
|
# -----------------------------------------------------------------------------
|
|
19
24
|
# Code editor with line numbers (QPlainTextEdit subclass)
|
|
20
25
|
# -----------------------------------------------------------------------------
|
|
@@ -1058,7 +1063,8 @@ class ScriptEditorDock(QDockWidget):
|
|
|
1058
1063
|
if man is None:
|
|
1059
1064
|
raise RuntimeError("ScriptManager not initialized on main window.")
|
|
1060
1065
|
|
|
1061
|
-
entry = man.
|
|
1066
|
+
entry = man.load_script_from_path(self._current_path)
|
|
1067
|
+
|
|
1062
1068
|
if entry is None or entry.run is None:
|
|
1063
1069
|
raise RuntimeError("Script has no run(ctx).")
|
|
1064
1070
|
|
|
@@ -1074,7 +1080,9 @@ class ScriptEditorDock(QDockWidget):
|
|
|
1074
1080
|
self.output.appendPlainText(tb)
|
|
1075
1081
|
self._log("Script ERROR:\n" + tb)
|
|
1076
1082
|
|
|
1077
|
-
|
|
1083
|
+
def load_script_from_path(self, path: Path) -> ScriptEntry | None:
|
|
1084
|
+
scripts_root = get_scripts_dir()
|
|
1085
|
+
return self._load_one_script(path, scripts_root)
|
|
1078
1086
|
|
|
1079
1087
|
# ------------------------------------------------------------------
|
|
1080
1088
|
# ui helpers
|
setiastro/saspro/ops/scripts.py
CHANGED
|
@@ -294,6 +294,125 @@ class ScriptContext:
|
|
|
294
294
|
# ✅ Normal run: let DocManager decide (ROI preview vs full)
|
|
295
295
|
dm.update_active_document(img, metadata={}, step_name=step_name)
|
|
296
296
|
|
|
297
|
+
def _find_subwindow_for_doc(self, base_doc):
|
|
298
|
+
"""Return (sw, widget) for the first subwindow showing base_doc."""
|
|
299
|
+
for sw, w in self._iter_open_subwindows():
|
|
300
|
+
d = self._base_doc_for_widget(w)
|
|
301
|
+
if d is base_doc:
|
|
302
|
+
return sw, w
|
|
303
|
+
return None, None
|
|
304
|
+
|
|
305
|
+
def rename_active_view(self, new_title: str) -> bool:
|
|
306
|
+
"""Rename only the active MDI view title (this window)."""
|
|
307
|
+
w = self.active_view()
|
|
308
|
+
if w is None:
|
|
309
|
+
return False
|
|
310
|
+
|
|
311
|
+
t = (new_title or "").strip()
|
|
312
|
+
if not t:
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
# ImageSubWindow convention
|
|
317
|
+
setattr(w, "_view_title_override", t)
|
|
318
|
+
if hasattr(w, "_sync_host_title"):
|
|
319
|
+
w._sync_host_title()
|
|
320
|
+
return True
|
|
321
|
+
except Exception:
|
|
322
|
+
return False
|
|
323
|
+
|
|
324
|
+
def rename_active_document(self, new_name: str) -> bool:
|
|
325
|
+
"""Rename the underlying document display name (affects explorer + other views)."""
|
|
326
|
+
doc = self.active_document()
|
|
327
|
+
if doc is None:
|
|
328
|
+
return False
|
|
329
|
+
|
|
330
|
+
n = (new_name or "").strip()
|
|
331
|
+
if not n:
|
|
332
|
+
return False
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
old = ""
|
|
336
|
+
try:
|
|
337
|
+
old = str(doc.display_name() or "")
|
|
338
|
+
except Exception:
|
|
339
|
+
old = str(getattr(doc, "metadata", {}).get("display_name", "") or "")
|
|
340
|
+
|
|
341
|
+
doc.metadata["display_name"] = n
|
|
342
|
+
try:
|
|
343
|
+
doc.changed.emit()
|
|
344
|
+
except Exception:
|
|
345
|
+
pass
|
|
346
|
+
|
|
347
|
+
# If the active view had an override equal to the old doc name, drop it (matches your UI behavior)
|
|
348
|
+
w = self.active_view()
|
|
349
|
+
if w is not None:
|
|
350
|
+
try:
|
|
351
|
+
if getattr(w, "_view_title_override", None) == old:
|
|
352
|
+
setattr(w, "_view_title_override", None)
|
|
353
|
+
except Exception:
|
|
354
|
+
pass
|
|
355
|
+
try:
|
|
356
|
+
if hasattr(w, "_sync_host_title"):
|
|
357
|
+
w._sync_host_title()
|
|
358
|
+
except Exception:
|
|
359
|
+
pass
|
|
360
|
+
|
|
361
|
+
return True
|
|
362
|
+
except Exception:
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
def rename_view(self, view_name_or_uid: str, new_title: str) -> bool:
|
|
366
|
+
"""Rename a specific *view/window* by name/title/uid (first match)."""
|
|
367
|
+
doc = self.get_document(view_name_or_uid)
|
|
368
|
+
if doc is None:
|
|
369
|
+
return False
|
|
370
|
+
|
|
371
|
+
sw, w = self._find_subwindow_for_doc(doc)
|
|
372
|
+
if w is None:
|
|
373
|
+
return False
|
|
374
|
+
|
|
375
|
+
t = (new_title or "").strip()
|
|
376
|
+
if not t:
|
|
377
|
+
return False
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
setattr(w, "_view_title_override", t)
|
|
381
|
+
if hasattr(w, "_sync_host_title"):
|
|
382
|
+
w._sync_host_title()
|
|
383
|
+
return True
|
|
384
|
+
except Exception:
|
|
385
|
+
return False
|
|
386
|
+
|
|
387
|
+
def rename_document(self, view_name_or_uid: str, new_name: str) -> bool:
|
|
388
|
+
"""Rename a specific *document* by view name/title/uid."""
|
|
389
|
+
doc = self.get_document(view_name_or_uid)
|
|
390
|
+
if doc is None:
|
|
391
|
+
return False
|
|
392
|
+
n = (new_name or "").strip()
|
|
393
|
+
if not n:
|
|
394
|
+
return False
|
|
395
|
+
|
|
396
|
+
try:
|
|
397
|
+
doc.metadata["display_name"] = n
|
|
398
|
+
try:
|
|
399
|
+
doc.changed.emit()
|
|
400
|
+
except Exception:
|
|
401
|
+
pass
|
|
402
|
+
|
|
403
|
+
# resync any window currently showing it
|
|
404
|
+
sw, w = self._find_subwindow_for_doc(doc)
|
|
405
|
+
if w is not None and hasattr(w, "_sync_host_title"):
|
|
406
|
+
try:
|
|
407
|
+
w._sync_host_title()
|
|
408
|
+
except Exception:
|
|
409
|
+
pass
|
|
410
|
+
|
|
411
|
+
return True
|
|
412
|
+
except Exception:
|
|
413
|
+
return False
|
|
414
|
+
|
|
415
|
+
|
|
297
416
|
# ---- convenience wrappers into main window ----
|
|
298
417
|
def run_command(self, command_id: str, preset=None, **kwargs):
|
|
299
418
|
return _run_command(self, command_id, preset, **kwargs)
|
|
@@ -678,6 +797,9 @@ class ScriptManager(QObject):
|
|
|
678
797
|
|
|
679
798
|
self._log(f"[Scripts] Loaded {len(self.registry)} script(s) from {scripts_dir}")
|
|
680
799
|
|
|
800
|
+
def load_script_from_path(self, path: Path) -> ScriptEntry | None:
|
|
801
|
+
scripts_root = get_scripts_dir()
|
|
802
|
+
return self._load_one_script(path, scripts_root)
|
|
681
803
|
|
|
682
804
|
def _load_one_script(self, path: Path, scripts_root: Path) -> ScriptEntry | None:
|
|
683
805
|
"""
|
|
@@ -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"):
|