setiastrosuitepro 1.6.1.post1__py3-none-any.whl → 1.6.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of setiastrosuitepro might be problematic. Click here for more details.
- setiastro/images/Background_startup.jpg +0 -0
- setiastro/images/rotatearbitrary.png +0 -0
- setiastro/qml/ResourceMonitor.qml +126 -0
- setiastro/saspro/__main__.py +162 -25
- 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 +60 -9
- setiastro/saspro/batch_convert.py +3 -0
- setiastro/saspro/batch_renamer.py +3 -0
- setiastro/saspro/blemish_blaster.py +3 -0
- setiastro/saspro/blink_comparator_pro.py +474 -251
- 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 +28 -1
- setiastro/saspro/curve_editor_pro.py +18 -0
- setiastro/saspro/debayer.py +3 -0
- setiastro/saspro/doc_manager.py +40 -17
- setiastro/saspro/fitsmodifier.py +3 -0
- setiastro/saspro/frequency_separation.py +8 -2
- setiastro/saspro/function_bundle.py +18 -16
- 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 +364 -92
- setiastro/saspro/gui/mixins/dock_mixin.py +119 -7
- setiastro/saspro/gui/mixins/file_mixin.py +7 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +105 -5
- setiastro/saspro/gui/mixins/menu_mixin.py +29 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +33 -10
- 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 +713 -256
- 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 +1 -0
- setiastro/saspro/remove_green.py +18 -1
- setiastro/saspro/remove_stars.py +136 -162
- setiastro/saspro/remove_stars_preset.py +55 -13
- setiastro/saspro/resources.py +36 -10
- setiastro/saspro/rgb_combination.py +1 -0
- setiastro/saspro/rgbalign.py +4 -4
- setiastro/saspro/save_options.py +1 -0
- setiastro/saspro/selective_color.py +79 -20
- setiastro/saspro/sfcc.py +50 -8
- setiastro/saspro/shortcuts.py +94 -21
- setiastro/saspro/signature_insert.py +3 -0
- setiastro/saspro/stacking_suite.py +924 -446
- setiastro/saspro/star_alignment.py +291 -331
- setiastro/saspro/star_spikes.py +116 -32
- setiastro/saspro/star_stretch.py +38 -1
- setiastro/saspro/stat_stretch.py +35 -3
- setiastro/saspro/status_log_dock.py +1 -1
- setiastro/saspro/subwindow.py +63 -2
- setiastro/saspro/supernovaasteroidhunter.py +3 -0
- setiastro/saspro/swap_manager.py +77 -42
- setiastro/saspro/translations/all_source_strings.json +4726 -0
- setiastro/saspro/translations/ar_translations.py +4096 -0
- setiastro/saspro/translations/de_translations.py +441 -446
- setiastro/saspro/translations/es_translations.py +278 -32
- setiastro/saspro/translations/fr_translations.py +280 -32
- setiastro/saspro/translations/hi_translations.py +3803 -0
- setiastro/saspro/translations/integrate_translations.py +38 -1
- setiastro/saspro/translations/it_translations.py +1211 -145
- setiastro/saspro/translations/ja_translations.py +556 -307
- setiastro/saspro/translations/pt_translations.py +3316 -3322
- setiastro/saspro/translations/ru_translations.py +3082 -0
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +16019 -0
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +14428 -133
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +11503 -7821
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +11168 -7812
- setiastro/saspro/translations/saspro_hi.qm +0 -0
- setiastro/saspro/translations/saspro_hi.ts +14855 -0
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +14347 -7821
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +14860 -137
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +14904 -137
- setiastro/saspro/translations/saspro_ru.qm +0 -0
- setiastro/saspro/translations/saspro_ru.ts +11835 -0
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +15237 -0
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +15248 -0
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +10581 -7812
- setiastro/saspro/translations/sw_translations.py +3897 -0
- setiastro/saspro/translations/uk_translations.py +3929 -0
- setiastro/saspro/translations/zh_translations.py +283 -32
- setiastro/saspro/versioning.py +36 -5
- setiastro/saspro/view_bundle.py +20 -17
- 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 +991 -0
- setiastro/saspro/widgets/minigame/index.html +53 -0
- setiastro/saspro/widgets/minigame/style.css +241 -0
- setiastro/saspro/widgets/resource_monitor.py +263 -0
- setiastro/saspro/widgets/spinboxes.py +18 -0
- setiastro/saspro/widgets/wavelet_utils.py +52 -20
- setiastro/saspro/wimi.py +100 -80
- setiastro/saspro/wims.py +33 -33
- setiastro/saspro/window_shelf.py +2 -2
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/METADATA +15 -4
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/RECORD +139 -115
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/license.txt +0 -0
|
@@ -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
|
setiastro/saspro/numba_utils.py
CHANGED
|
@@ -584,39 +584,55 @@ def kappa_sigma_clip_weighted_3d(stack, weights, kappa=2.5, iterations=3):
|
|
|
584
584
|
pixel_weights = weights[:]
|
|
585
585
|
else:
|
|
586
586
|
pixel_weights = weights[:, i, j].copy()
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
current_vals = pixel_values
|
|
592
|
-
current_w = pixel_weights
|
|
593
|
-
current_indices = current_idx
|
|
587
|
+
|
|
588
|
+
# Use boolean mask instead of tracking indices
|
|
589
|
+
valid_mask = pixel_values != 0
|
|
590
|
+
|
|
594
591
|
med = 0.0
|
|
595
592
|
for _ in range(iterations):
|
|
596
|
-
|
|
593
|
+
# Count valid pixels
|
|
594
|
+
count = 0
|
|
595
|
+
for k in range(num_frames):
|
|
596
|
+
if valid_mask[k]:
|
|
597
|
+
count += 1
|
|
598
|
+
|
|
599
|
+
if count == 0:
|
|
597
600
|
break
|
|
601
|
+
|
|
602
|
+
# Extract valid values for stats (this allocation is unavoidable but smaller/temp)
|
|
603
|
+
# In numba this usually lowers to efficient code if we avoid 'np.empty' overhead inside loops
|
|
604
|
+
# but pure mask operations are often faster.
|
|
605
|
+
# However, for median/std we need the compacted array.
|
|
606
|
+
current_vals = pixel_values[valid_mask]
|
|
607
|
+
|
|
598
608
|
med = np.median(current_vals)
|
|
599
609
|
std = np.std(current_vals)
|
|
600
610
|
lower_bound = med - kappa * std
|
|
601
611
|
upper_bound = med + kappa * std
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
612
|
+
|
|
613
|
+
# Update mask: must be valid AND within bounds
|
|
614
|
+
for k in range(num_frames):
|
|
615
|
+
if valid_mask[k]:
|
|
616
|
+
val = pixel_values[k]
|
|
617
|
+
if val < lower_bound or val > upper_bound:
|
|
618
|
+
valid_mask[k] = False
|
|
619
|
+
|
|
620
|
+
# Fill rejection mask
|
|
607
621
|
for f in range(num_frames):
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
if
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
622
|
+
rej_mask[f, i, j] = not valid_mask[f]
|
|
623
|
+
|
|
624
|
+
# Compute weighted mean of final valid pixels
|
|
625
|
+
wsum = 0.0
|
|
626
|
+
vsum = 0.0
|
|
627
|
+
for k in range(num_frames):
|
|
628
|
+
if valid_mask[k]:
|
|
629
|
+
w = pixel_weights[k]
|
|
630
|
+
v = pixel_values[k]
|
|
631
|
+
wsum += w
|
|
632
|
+
vsum += v * w
|
|
633
|
+
|
|
634
|
+
if wsum > 0:
|
|
635
|
+
clipped[i, j] = vsum / wsum
|
|
620
636
|
else:
|
|
621
637
|
clipped[i, j] = med
|
|
622
638
|
return clipped, rej_mask
|
|
@@ -641,36 +657,46 @@ def kappa_sigma_clip_weighted_4d(stack, weights, kappa=2.5, iterations=3):
|
|
|
641
657
|
pixel_weights = weights[:]
|
|
642
658
|
else:
|
|
643
659
|
pixel_weights = weights[:, i, j, c].copy()
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
current_vals = pixel_values
|
|
648
|
-
current_w = pixel_weights
|
|
649
|
-
current_indices = current_idx
|
|
660
|
+
|
|
661
|
+
valid_mask = pixel_values != 0
|
|
662
|
+
|
|
650
663
|
med = 0.0
|
|
651
664
|
for _ in range(iterations):
|
|
652
|
-
|
|
665
|
+
count = 0
|
|
666
|
+
for k in range(num_frames):
|
|
667
|
+
if valid_mask[k]:
|
|
668
|
+
count += 1
|
|
669
|
+
|
|
670
|
+
if count == 0:
|
|
653
671
|
break
|
|
672
|
+
|
|
673
|
+
current_vals = pixel_values[valid_mask]
|
|
674
|
+
|
|
654
675
|
med = np.median(current_vals)
|
|
655
676
|
std = np.std(current_vals)
|
|
656
677
|
lower_bound = med - kappa * std
|
|
657
678
|
upper_bound = med + kappa * std
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
679
|
+
|
|
680
|
+
for k in range(num_frames):
|
|
681
|
+
if valid_mask[k]:
|
|
682
|
+
val = pixel_values[k]
|
|
683
|
+
if val < lower_bound or val > upper_bound:
|
|
684
|
+
valid_mask[k] = False
|
|
685
|
+
|
|
662
686
|
for f in range(num_frames):
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
if
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
687
|
+
rej_mask[f, i, j, c] = not valid_mask[f]
|
|
688
|
+
|
|
689
|
+
wsum = 0.0
|
|
690
|
+
vsum = 0.0
|
|
691
|
+
for k in range(num_frames):
|
|
692
|
+
if valid_mask[k]:
|
|
693
|
+
w = pixel_weights[k]
|
|
694
|
+
v = pixel_values[k]
|
|
695
|
+
wsum += w
|
|
696
|
+
vsum += v * w
|
|
697
|
+
|
|
698
|
+
if wsum > 0:
|
|
699
|
+
clipped[i, j, c] = vsum / wsum
|
|
674
700
|
else:
|
|
675
701
|
clipped[i, j, c] = med
|
|
676
702
|
return clipped, rej_mask
|
|
@@ -3042,3 +3068,78 @@ def fast_star_detect(image,
|
|
|
3042
3068
|
return np.empty((0,2), dtype=np.float32)
|
|
3043
3069
|
else:
|
|
3044
3070
|
return np.array(star_positions, dtype=np.float32)
|
|
3071
|
+
|
|
3072
|
+
|
|
3073
|
+
@njit(fastmath=True, cache=True)
|
|
3074
|
+
def gradient_descent_to_dim_spot_numba(gray_small, start_x, start_y, patch_size):
|
|
3075
|
+
"""
|
|
3076
|
+
Numba implementation of _gradient_descent_to_dim_spot.
|
|
3077
|
+
Walks to the local minimum (median-of-patch) around (start_x, start_y).
|
|
3078
|
+
gray_small: 2D float32 array
|
|
3079
|
+
"""
|
|
3080
|
+
H, W = gray_small.shape
|
|
3081
|
+
half = patch_size // 2
|
|
3082
|
+
|
|
3083
|
+
cx = int(min(max(start_x, 0), W - 1))
|
|
3084
|
+
cy = int(min(max(start_y, 0), H - 1))
|
|
3085
|
+
|
|
3086
|
+
# Helper to compute patch median manually or efficiently
|
|
3087
|
+
# Numba supports np.median on arrays, but slicing inside a loop can be costly.
|
|
3088
|
+
# However, for small patches (e.g. 15x15), it should be okay.
|
|
3089
|
+
|
|
3090
|
+
for _ in range(60):
|
|
3091
|
+
# Current value
|
|
3092
|
+
x0 = max(0, cx - half)
|
|
3093
|
+
x1 = min(W, cx + half + 1)
|
|
3094
|
+
y0 = max(0, cy - half)
|
|
3095
|
+
y1 = min(H, cy + half + 1)
|
|
3096
|
+
sub = gray_small[y0:y1, x0:x1].flatten()
|
|
3097
|
+
if sub.size == 0:
|
|
3098
|
+
cur_val = 1e9 # Should not happen
|
|
3099
|
+
else:
|
|
3100
|
+
cur_val = np.median(sub)
|
|
3101
|
+
|
|
3102
|
+
best_x, best_y = cx, cy
|
|
3103
|
+
best_val = cur_val
|
|
3104
|
+
|
|
3105
|
+
# 3x3 search
|
|
3106
|
+
changed = False
|
|
3107
|
+
|
|
3108
|
+
# Unroll for strict 3x3 neighborhood
|
|
3109
|
+
for dy in range(-1, 2):
|
|
3110
|
+
for dx in range(-1, 2):
|
|
3111
|
+
if dx == 0 and dy == 0:
|
|
3112
|
+
continue
|
|
3113
|
+
|
|
3114
|
+
nx = cx + dx
|
|
3115
|
+
ny = cy + dy
|
|
3116
|
+
|
|
3117
|
+
if nx < 0 or ny < 0 or nx >= W or ny >= H:
|
|
3118
|
+
continue
|
|
3119
|
+
|
|
3120
|
+
# Compute median for neighbor
|
|
3121
|
+
nx0 = max(0, nx - half)
|
|
3122
|
+
nx1 = min(W, nx + half + 1)
|
|
3123
|
+
ny0 = max(0, ny - half)
|
|
3124
|
+
ny1 = min(H, ny + half + 1)
|
|
3125
|
+
|
|
3126
|
+
# In Numba, median on a slice creates a copy.
|
|
3127
|
+
# For small patches this is acceptable given the huge speedup vs Python interpreter overhead.
|
|
3128
|
+
n_sub = gray_small[ny0:ny1, nx0:nx1].flatten()
|
|
3129
|
+
if n_sub.size == 0:
|
|
3130
|
+
val = 1e9
|
|
3131
|
+
else:
|
|
3132
|
+
val = np.median(n_sub)
|
|
3133
|
+
|
|
3134
|
+
if val < best_val:
|
|
3135
|
+
best_val = val
|
|
3136
|
+
best_x = nx
|
|
3137
|
+
best_y = ny
|
|
3138
|
+
changed = True
|
|
3139
|
+
|
|
3140
|
+
if not changed:
|
|
3141
|
+
break
|
|
3142
|
+
|
|
3143
|
+
cx, cy = best_x, best_y
|
|
3144
|
+
|
|
3145
|
+
return cx, cy
|
setiastro/saspro/ops/scripts.py
CHANGED
|
@@ -644,13 +644,33 @@ class ScriptManager(QObject):
|
|
|
644
644
|
|
|
645
645
|
# ---- loading ----
|
|
646
646
|
def load_registry(self):
|
|
647
|
-
|
|
647
|
+
"""
|
|
648
|
+
Discover scripts recursively under SASpro/scripts, load them, and build registry.
|
|
649
|
+
Skips __pycache__, hidden/underscore-prefixed files, and __init__.py.
|
|
650
|
+
"""
|
|
651
|
+
migrate_old_scripts_if_needed()
|
|
648
652
|
scripts_dir = get_scripts_dir()
|
|
649
653
|
self.registry = []
|
|
650
654
|
|
|
651
|
-
|
|
655
|
+
try:
|
|
656
|
+
candidates = sorted(scripts_dir.rglob("*.py"))
|
|
657
|
+
except Exception:
|
|
658
|
+
candidates = []
|
|
659
|
+
|
|
660
|
+
for path in candidates:
|
|
661
|
+
# Skip pycache anywhere in path
|
|
662
|
+
parts_l = {p.lower() for p in path.parts}
|
|
663
|
+
if "__pycache__" in parts_l:
|
|
664
|
+
continue
|
|
665
|
+
|
|
666
|
+
# Skip hidden/private python files and package init
|
|
667
|
+
if path.name == "__init__.py":
|
|
668
|
+
continue
|
|
669
|
+
if path.name.startswith((".", "_")):
|
|
670
|
+
continue
|
|
671
|
+
|
|
652
672
|
try:
|
|
653
|
-
entry = self._load_one_script(path)
|
|
673
|
+
entry = self._load_one_script(path, scripts_dir)
|
|
654
674
|
if entry:
|
|
655
675
|
self.registry.append(entry)
|
|
656
676
|
except Exception:
|
|
@@ -658,8 +678,18 @@ class ScriptManager(QObject):
|
|
|
658
678
|
|
|
659
679
|
self._log(f"[Scripts] Loaded {len(self.registry)} script(s) from {scripts_dir}")
|
|
660
680
|
|
|
661
|
-
|
|
662
|
-
|
|
681
|
+
|
|
682
|
+
def _load_one_script(self, path: Path, scripts_root: Path) -> ScriptEntry | None:
|
|
683
|
+
"""
|
|
684
|
+
Load a single user script from disk.
|
|
685
|
+
|
|
686
|
+
- Creates a unique module name based on mtime so reload picks up changes.
|
|
687
|
+
- Imports module.
|
|
688
|
+
- Determines stable script_id (prefer SCRIPT_ID in module, else persisted id).
|
|
689
|
+
- Pulls metadata: SCRIPT_NAME/GROUP/SHORTCUT.
|
|
690
|
+
Group defaults to relative folder under scripts_root.
|
|
691
|
+
"""
|
|
692
|
+
# Unique module name so reloading actually re-imports
|
|
663
693
|
try:
|
|
664
694
|
mtime_ns = path.stat().st_mtime_ns
|
|
665
695
|
except Exception:
|
|
@@ -671,7 +701,8 @@ class ScriptManager(QObject):
|
|
|
671
701
|
return None
|
|
672
702
|
|
|
673
703
|
mod = importlib.util.module_from_spec(spec)
|
|
674
|
-
|
|
704
|
+
|
|
705
|
+
# Import module first (so SCRIPT_ID / metadata exists)
|
|
675
706
|
try:
|
|
676
707
|
spec.loader.exec_module(mod) # type: ignore
|
|
677
708
|
except Exception:
|
|
@@ -687,7 +718,7 @@ class ScriptManager(QObject):
|
|
|
687
718
|
self._log(f"[Scripts] {path.name} has no run(ctx) or main(ctx); skipping.")
|
|
688
719
|
return None
|
|
689
720
|
|
|
690
|
-
# ----
|
|
721
|
+
# ---- helper: allow CAPS or lowercase ----
|
|
691
722
|
def _pick(*names, default=None):
|
|
692
723
|
for n in names:
|
|
693
724
|
if hasattr(mod, n):
|
|
@@ -695,11 +726,23 @@ class ScriptManager(QObject):
|
|
|
695
726
|
return default
|
|
696
727
|
|
|
697
728
|
name = _pick("SCRIPT_NAME", "script_name", default=path.stem)
|
|
698
|
-
|
|
729
|
+
|
|
730
|
+
# Prefer explicit group; else derive group from relative folder
|
|
731
|
+
group = _pick("SCRIPT_GROUP", "script_group", default=None)
|
|
732
|
+
if group is None or not str(group).strip():
|
|
733
|
+
try:
|
|
734
|
+
rel_parent = path.parent.relative_to(scripts_root)
|
|
735
|
+
group = "" if str(rel_parent) in ("", ".") else rel_parent.as_posix()
|
|
736
|
+
except Exception:
|
|
737
|
+
group = ""
|
|
738
|
+
|
|
699
739
|
shortcut = _pick("SCRIPT_SHORTCUT", "script_shortcut", default=None)
|
|
700
740
|
|
|
741
|
+
# Stable script id (prefer explicit SCRIPT_ID; else persisted by rel-path)
|
|
742
|
+
script_id = self._script_id_for_path(path, scripts_root, mod)
|
|
743
|
+
|
|
701
744
|
entry = ScriptEntry(
|
|
702
|
-
script_id=script_id,
|
|
745
|
+
script_id=str(script_id),
|
|
703
746
|
path=path,
|
|
704
747
|
name=str(name),
|
|
705
748
|
group=str(group or ""),
|
|
@@ -710,6 +753,7 @@ class ScriptManager(QObject):
|
|
|
710
753
|
return entry
|
|
711
754
|
|
|
712
755
|
|
|
756
|
+
|
|
713
757
|
# ---- menu wiring ----
|
|
714
758
|
def rebuild_menu(self, menu_scripts):
|
|
715
759
|
"""
|
|
@@ -1394,15 +1438,31 @@ def run(ctx):
|
|
|
1394
1438
|
self._log(f"[Scripts] Failed to write sample script:\n{traceback.format_exc()}")
|
|
1395
1439
|
|
|
1396
1440
|
|
|
1397
|
-
def _script_id_for_path(self, path: Path, mod) -> str:
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1441
|
+
def _script_id_for_path(self, path: Path, scripts_root: Path, mod=None) -> str:
|
|
1442
|
+
"""
|
|
1443
|
+
Determine a stable script_id.
|
|
1444
|
+
|
|
1445
|
+
Priority:
|
|
1446
|
+
1) SCRIPT_ID / script_id defined in the script (best; survives renames/moves)
|
|
1447
|
+
2) Persisted id in QSettings keyed by *relative path inside scripts_root*
|
|
1448
|
+
(stable across machines if folder structure is same)
|
|
1449
|
+
|
|
1450
|
+
NOTE: We intentionally DO NOT key by absolute path.
|
|
1451
|
+
"""
|
|
1452
|
+
# 1) Prefer explicit SCRIPT_ID in the script file (best)
|
|
1453
|
+
if mod is not None:
|
|
1454
|
+
sid = getattr(mod, "SCRIPT_ID", None) or getattr(mod, "script_id", None)
|
|
1455
|
+
if isinstance(sid, str) and sid.strip():
|
|
1456
|
+
return sid.strip()
|
|
1457
|
+
|
|
1458
|
+
# 2) Persist per-relative-path (not absolute)
|
|
1459
|
+
try:
|
|
1460
|
+
rel = path.relative_to(scripts_root).as_posix()
|
|
1461
|
+
except Exception:
|
|
1462
|
+
rel = path.as_posix()
|
|
1402
1463
|
|
|
1403
|
-
# 2) Fallback: persist per-path in QSettings (ok for existing scripts)
|
|
1404
1464
|
s = QSettings()
|
|
1405
|
-
key = f"Scripts/
|
|
1465
|
+
key = f"Scripts/ids_rel/{rel}"
|
|
1406
1466
|
sid = s.value(key, "", type=str) or ""
|
|
1407
1467
|
if sid:
|
|
1408
1468
|
return sid
|
|
@@ -1410,4 +1470,4 @@ def run(ctx):
|
|
|
1410
1470
|
sid = uuid.uuid4().hex
|
|
1411
1471
|
s.setValue(key, sid)
|
|
1412
1472
|
s.sync()
|
|
1413
|
-
return sid
|
|
1473
|
+
return sid
|
setiastro/saspro/ops/settings.py
CHANGED
|
@@ -500,54 +500,12 @@ class SettingsDialog(QDialog):
|
|
|
500
500
|
# Apply language change immediately if changed
|
|
501
501
|
if new_lang != self._initial_language:
|
|
502
502
|
from PyQt6.QtWidgets import QMessageBox
|
|
503
|
-
import sys
|
|
504
|
-
import os
|
|
505
|
-
|
|
506
|
-
# Save UI state before restart to avoid losing toolbar/window changes
|
|
507
|
-
p = self.parent()
|
|
508
|
-
if p and hasattr(p, "save_ui_state"):
|
|
509
|
-
try:
|
|
510
|
-
p.save_ui_state()
|
|
511
|
-
except Exception:
|
|
512
|
-
pass
|
|
513
|
-
|
|
514
|
-
# Set restart flag on parent to bypass exit confirmation
|
|
515
|
-
if p:
|
|
516
|
-
p._is_restarting = True
|
|
517
|
-
|
|
518
|
-
self.settings.sync()
|
|
519
503
|
|
|
520
504
|
QMessageBox.information(
|
|
521
505
|
self,
|
|
522
506
|
self.tr("Restart required"),
|
|
523
|
-
self.tr("
|
|
507
|
+
self.tr("Language changed. Please manually restart the application to apply the new language.")
|
|
524
508
|
)
|
|
525
|
-
|
|
526
|
-
# Restart logic
|
|
527
|
-
import subprocess
|
|
528
|
-
try:
|
|
529
|
-
# Prepare arguments for restart
|
|
530
|
-
args = sys.argv[:]
|
|
531
|
-
if getattr(sys, 'frozen', False):
|
|
532
|
-
# If frozen, sys.executable is the app, and args[0] is also the app
|
|
533
|
-
cmd = [sys.executable] + args[1:]
|
|
534
|
-
else:
|
|
535
|
-
# If running from source, sys.executable is python, args[0] is the script
|
|
536
|
-
cmd = [sys.executable] + args
|
|
537
|
-
|
|
538
|
-
# Start new process and exit current one
|
|
539
|
-
if os.name == 'nt':
|
|
540
|
-
# On Windows, use DETACHED_PROCESS to break the process tree connection to the terminal
|
|
541
|
-
# and close_fds to ensure no handles are inherited.
|
|
542
|
-
subprocess.Popen(cmd, creationflags=subprocess.DETACHED_PROCESS, close_fds=True)
|
|
543
|
-
else:
|
|
544
|
-
subprocess.Popen(cmd)
|
|
545
|
-
|
|
546
|
-
QApplication.instance().quit()
|
|
547
|
-
sys.exit(0)
|
|
548
|
-
except Exception:
|
|
549
|
-
# Fallback to execl if Popen fails
|
|
550
|
-
os.execl(sys.executable, sys.executable, *sys.argv)
|
|
551
509
|
|
|
552
510
|
self.settings.sync()
|
|
553
511
|
|
|
@@ -28,6 +28,7 @@ class PaletteAdjustDialog(QDialog):
|
|
|
28
28
|
super().__init__(owner)
|
|
29
29
|
self.setWindowTitle("Adjust Palette Intensities")
|
|
30
30
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
31
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
31
32
|
self.setModal(False)
|
|
32
33
|
#self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
33
34
|
|
setiastro/saspro/pixelmath.py
CHANGED
|
@@ -59,9 +59,10 @@ class PixelImage:
|
|
|
59
59
|
a = np.asarray(a, dtype=np.float32)
|
|
60
60
|
b = np.asarray(b, dtype=np.float32)
|
|
61
61
|
if a.ndim == 3 and b.ndim == 2:
|
|
62
|
-
b
|
|
62
|
+
# Broadcast b to (H,W,1) virtual view; numpy ufuncs handle (H,W,3) vs (H,W,1) automatically
|
|
63
|
+
b = b[..., None]
|
|
63
64
|
elif a.ndim == 2 and b.ndim == 3:
|
|
64
|
-
a =
|
|
65
|
+
a = a[..., None]
|
|
65
66
|
return a, b
|
|
66
67
|
|
|
67
68
|
# ---- binary arithmetic helpers ----
|
|
@@ -756,6 +757,9 @@ class PixelMathDialogPro(QDialog):
|
|
|
756
757
|
def __init__(self, parent, doc, icon: QIcon | None = None):
|
|
757
758
|
super().__init__(parent)
|
|
758
759
|
self.setWindowTitle(self.tr("Pixel Math"))
|
|
760
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
761
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
762
|
+
self.setModal(False)
|
|
759
763
|
if icon:
|
|
760
764
|
try:
|
|
761
765
|
self.setWindowIcon(icon)
|
setiastro/saspro/plate_solver.py
CHANGED
|
@@ -2059,6 +2059,7 @@ class PlateSolverDialog(QDialog):
|
|
|
2059
2059
|
self.setWindowTitle(self.tr("Plate Solver"))
|
|
2060
2060
|
self.setMinimumWidth(560)
|
|
2061
2061
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
2062
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
2062
2063
|
self.setModal(False)
|
|
2063
2064
|
#self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
2064
2065
|
|
setiastro/saspro/remove_green.py
CHANGED
|
@@ -157,6 +157,9 @@ class RemoveGreenDialog(QDialog):
|
|
|
157
157
|
self.main = main
|
|
158
158
|
self.doc = doc
|
|
159
159
|
self.setWindowTitle(self.tr("Remove Green (SCNR)"))
|
|
160
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
161
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
162
|
+
self.setModal(False)
|
|
160
163
|
self._build_ui()
|
|
161
164
|
|
|
162
165
|
def _build_ui(self):
|
|
@@ -263,8 +266,22 @@ class RemoveGreenDialog(QDialog):
|
|
|
263
266
|
# Never let replay bookkeeping kill the dialog
|
|
264
267
|
pass
|
|
265
268
|
|
|
266
|
-
|
|
269
|
+
# Dialog stays open so user can apply to other images
|
|
270
|
+
# Refresh document reference for next operation
|
|
271
|
+
self._refresh_document_from_active()
|
|
267
272
|
|
|
273
|
+
def _refresh_document_from_active(self):
|
|
274
|
+
"""
|
|
275
|
+
Refresh the dialog's document reference to the currently active document.
|
|
276
|
+
This allows reusing the same dialog on different images.
|
|
277
|
+
"""
|
|
278
|
+
try:
|
|
279
|
+
if self.main and hasattr(self.main, "_active_doc"):
|
|
280
|
+
new_doc = self.main._active_doc()
|
|
281
|
+
if new_doc is not None and new_doc is not self.doc:
|
|
282
|
+
self.doc = new_doc
|
|
283
|
+
except Exception:
|
|
284
|
+
pass
|
|
268
285
|
|
|
269
286
|
|
|
270
287
|
# ---------- entry points used by main ----------
|