setiastrosuitepro 1.6.2__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/rotatearbitrary.png +0 -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 +114 -37
- setiastro/saspro/blemish_blaster.py +4 -1
- setiastro/saspro/blink_comparator_pro.py +548 -275
- 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 +134 -8
- setiastro/saspro/curve_editor_pro.py +109 -42
- setiastro/saspro/doc_manager.py +246 -16
- setiastro/saspro/exoplanet_detector.py +120 -28
- setiastro/saspro/frequency_separation.py +1158 -204
- setiastro/saspro/function_bundle.py +16 -16
- setiastro/saspro/ghs_dialog_pro.py +81 -16
- setiastro/saspro/graxpert.py +1 -0
- setiastro/saspro/gui/main_window.py +519 -289
- setiastro/saspro/gui/mixins/dock_mixin.py +276 -42
- setiastro/saspro/gui/mixins/geometry_mixin.py +105 -5
- setiastro/saspro/gui/mixins/menu_mixin.py +28 -1
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +416 -27
- 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 +748 -255
- 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/remove_stars_preset.py +55 -13
- setiastro/saspro/resources.py +97 -11
- setiastro/saspro/rgbalign.py +4 -0
- setiastro/saspro/selective_color.py +83 -21
- setiastro/saspro/sfcc.py +364 -152
- setiastro/saspro/shortcuts.py +253 -49
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1610 -574
- setiastro/saspro/star_alignment.py +522 -453
- setiastro/saspro/star_spikes.py +4 -0
- setiastro/saspro/star_stretch.py +38 -3
- setiastro/saspro/stat_stretch.py +743 -128
- setiastro/saspro/status_log_dock.py +1 -1
- setiastro/saspro/subwindow.py +786 -360
- setiastro/saspro/supernovaasteroidhunter.py +1 -1
- setiastro/saspro/swap_manager.py +77 -42
- setiastro/saspro/translations/all_source_strings.json +1588 -516
- setiastro/saspro/translations/ar_translations.py +915 -684
- setiastro/saspro/translations/de_translations.py +442 -463
- setiastro/saspro/translations/es_translations.py +277 -47
- setiastro/saspro/translations/fr_translations.py +279 -47
- setiastro/saspro/translations/hi_translations.py +253 -21
- setiastro/saspro/translations/integrate_translations.py +3 -2
- setiastro/saspro/translations/it_translations.py +1211 -161
- setiastro/saspro/translations/ja_translations.py +3340 -3107
- setiastro/saspro/translations/pt_translations.py +3315 -3337
- setiastro/saspro/translations/ru_translations.py +351 -117
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +15902 -138
- 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 +14733 -135
- 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 +11766 -168
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +15115 -135
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +11206 -6729
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +10581 -7812
- setiastro/saspro/translations/sw_translations.py +282 -56
- setiastro/saspro/translations/uk_translations.py +264 -35
- setiastro/saspro/translations/zh_translations.py +282 -47
- setiastro/saspro/view_bundle.py +17 -17
- 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/minigame/game.js +11 -6
- setiastro/saspro/widgets/resource_monitor.py +133 -57
- setiastro/saspro/widgets/spinboxes.py +28 -13
- setiastro/saspro/wimi.py +92 -721
- setiastro/saspro/wims.py +46 -36
- setiastro/saspro/window_shelf.py +2 -2
- setiastro/saspro/xisf.py +101 -11
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/METADATA +8 -7
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/RECORD +162 -128
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/license.txt +0 -0
|
@@ -6,7 +6,7 @@ import tempfile
|
|
|
6
6
|
import datetime as _dt
|
|
7
7
|
import numpy as np
|
|
8
8
|
import time
|
|
9
|
-
|
|
9
|
+
import threading
|
|
10
10
|
from PyQt6.QtCore import Qt, QTimer, QSettings, pyqtSignal
|
|
11
11
|
from PyQt6.QtGui import QIcon, QImage, QPixmap, QAction, QIntValidator, QDoubleValidator
|
|
12
12
|
from PyQt6.QtWidgets import (QDialog, QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QLineEdit,
|
|
@@ -19,14 +19,10 @@ from astropy.io import fits
|
|
|
19
19
|
from astropy.stats import sigma_clipped_stats
|
|
20
20
|
|
|
21
21
|
# optional deps used in your code; guard if not installed
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
try:
|
|
27
|
-
import exifread
|
|
28
|
-
except Exception:
|
|
29
|
-
exifread = None
|
|
22
|
+
import rawpy
|
|
23
|
+
|
|
24
|
+
import exifread
|
|
25
|
+
|
|
30
26
|
|
|
31
27
|
import sep
|
|
32
28
|
import exifread
|
|
@@ -147,15 +143,27 @@ class LiveStackSettingsDialog(QDialog):
|
|
|
147
143
|
(bootstrap_frames, clip_threshold,
|
|
148
144
|
max_fwhm, max_ecc, min_star_count, delay)
|
|
149
145
|
"""
|
|
150
|
-
bs = self.bs_spin.value
|
|
146
|
+
bs = int(self.bs_spin.value())
|
|
151
147
|
sigma = self.sigma_spin.value()
|
|
152
148
|
fwhm = self.fwhm_spin.value()
|
|
153
149
|
ecc = self.ecc_spin.value()
|
|
154
|
-
stars = self.star_spin.value
|
|
150
|
+
stars = int(self.star_spin.value())
|
|
155
151
|
mapping = self.mapping_combo.currentText()
|
|
156
152
|
delay = self.delay_spin.value()
|
|
157
153
|
return bs, sigma, fwhm, ecc, stars, mapping, delay
|
|
158
154
|
|
|
155
|
+
def _qget(settings: QSettings, key: str, default, typ):
|
|
156
|
+
try:
|
|
157
|
+
return settings.value(key, default, type=typ)
|
|
158
|
+
except TypeError:
|
|
159
|
+
# Key contains junk (likely from earlier method-object save). Reset it.
|
|
160
|
+
try:
|
|
161
|
+
settings.remove(key)
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
settings.setValue(key, default)
|
|
165
|
+
return default
|
|
166
|
+
|
|
159
167
|
|
|
160
168
|
|
|
161
169
|
class LiveMetricsPanel(QWidget):
|
|
@@ -342,7 +350,7 @@ def estimate_global_snr(
|
|
|
342
350
|
if sigma_central <= 0.0:
|
|
343
351
|
return 0.0
|
|
344
352
|
|
|
345
|
-
nu = med_patch - 3.0 * sigma_central
|
|
353
|
+
nu = med_patch - 3.0 * sigma_central
|
|
346
354
|
|
|
347
355
|
# 6) Return (mean − nu) / σ
|
|
348
356
|
return (mu_patch - nu) / sigma_central
|
|
@@ -383,15 +391,18 @@ class LiveStackWindow(QDialog):
|
|
|
383
391
|
self.OPEN_RETRY_PENALTY_SECS = 10.0 # cool-down after a read/permission failure
|
|
384
392
|
self.MAX_FILE_WAIT_SECS = 600.0 # optional safety cap (unused by default logic)
|
|
385
393
|
|
|
394
|
+
self._stop_event = threading.Event()
|
|
395
|
+
|
|
386
396
|
# ── Load persisted settings ───────────────────────────────
|
|
387
397
|
s = QSettings()
|
|
388
|
-
self.bootstrap_frames
|
|
389
|
-
self.clip_threshold
|
|
390
|
-
self.max_fwhm
|
|
391
|
-
self.max_ecc
|
|
392
|
-
self.min_star_count
|
|
393
|
-
self.narrowband_mapping
|
|
394
|
-
self.star_trail_mode
|
|
398
|
+
self.bootstrap_frames = _qget(s, "LiveStack/bootstrap_frames", 24, int)
|
|
399
|
+
self.clip_threshold = _qget(s, "LiveStack/clip_threshold", 3.5, float)
|
|
400
|
+
self.max_fwhm = _qget(s, "LiveStack/max_fwhm", 15.0, float)
|
|
401
|
+
self.max_ecc = _qget(s, "LiveStack/max_ecc", 0.9, float)
|
|
402
|
+
self.min_star_count = _qget(s, "LiveStack/min_star_count", 5, int)
|
|
403
|
+
self.narrowband_mapping = _qget(s, "LiveStack/narrowband_mapping", "Natural", str)
|
|
404
|
+
self.star_trail_mode = _qget(s, "LiveStack/star_trail_mode", False, bool)
|
|
405
|
+
self.FILE_STABLE_SECS = _qget(s, "LiveStack/file_stable_secs", 3.0, float)
|
|
395
406
|
|
|
396
407
|
|
|
397
408
|
self.total_exposure = 0.0 # seconds
|
|
@@ -598,6 +609,17 @@ class LiveStackWindow(QDialog):
|
|
|
598
609
|
self.poll_timer.timeout.connect(self.check_for_new_frames)
|
|
599
610
|
self._on_mono_color_toggled(self.mono_color_checkbox.isChecked())
|
|
600
611
|
|
|
612
|
+
app = QApplication.instance()
|
|
613
|
+
if app is not None:
|
|
614
|
+
try:
|
|
615
|
+
app.aboutToQuit.connect(self.stop_live)
|
|
616
|
+
except Exception:
|
|
617
|
+
pass
|
|
618
|
+
|
|
619
|
+
def _should_stop(self) -> bool:
|
|
620
|
+
# stop requested or not running anymore
|
|
621
|
+
return self._stop_event.is_set() or (not self.is_running)
|
|
622
|
+
|
|
601
623
|
|
|
602
624
|
# ─────────────────────────────────────────────────────────────────────────
|
|
603
625
|
def _on_star_trail_toggled(self, checked: bool):
|
|
@@ -925,8 +947,9 @@ class LiveStackWindow(QDialog):
|
|
|
925
947
|
# direct mapping: e.g. "SHO" → R=S, G=H, B=O
|
|
926
948
|
letters = list(mode)
|
|
927
949
|
if len(letters) != 3 or any(l not in ("S","H","O") for l in letters):
|
|
928
|
-
#
|
|
929
|
-
|
|
950
|
+
# fallback to natural
|
|
951
|
+
self.narrowband_mapping = "Natural"
|
|
952
|
+
return self._build_color_composite()
|
|
930
953
|
|
|
931
954
|
R = getf(letters[0])
|
|
932
955
|
G = getf(letters[1])
|
|
@@ -1076,12 +1099,10 @@ class LiveStackWindow(QDialog):
|
|
|
1076
1099
|
if not self.watch_folder:
|
|
1077
1100
|
self.status_label.setText("❗ No folder selected")
|
|
1078
1101
|
return
|
|
1079
|
-
|
|
1102
|
+
self._stop_event.clear()
|
|
1103
|
+
self.is_running = True
|
|
1080
1104
|
self.processed_files.clear()
|
|
1081
|
-
# Process all current files once
|
|
1082
1105
|
self.check_for_new_frames()
|
|
1083
|
-
# Now start monitoring
|
|
1084
|
-
self.is_running = True
|
|
1085
1106
|
self.poll_timer.start()
|
|
1086
1107
|
self.status_label.setText(f"▶ Processing & Monitoring: {os.path.basename(self.watch_folder)}")
|
|
1087
1108
|
|
|
@@ -1090,6 +1111,7 @@ class LiveStackWindow(QDialog):
|
|
|
1090
1111
|
if not self.watch_folder:
|
|
1091
1112
|
self.status_label.setText("❗ No folder selected")
|
|
1092
1113
|
return
|
|
1114
|
+
self._stop_event.clear()
|
|
1093
1115
|
# Populate processed_files with all existing files so they won't be re-processed
|
|
1094
1116
|
exts = (
|
|
1095
1117
|
"*.fit", "*.fits", "*.tif", "*.tiff",
|
|
@@ -1110,21 +1132,23 @@ class LiveStackWindow(QDialog):
|
|
|
1110
1132
|
if not self.watch_folder:
|
|
1111
1133
|
self.status_label.setText("❗ No folder selected")
|
|
1112
1134
|
return
|
|
1135
|
+
self._stop_event.clear()
|
|
1113
1136
|
self.is_running = True
|
|
1114
1137
|
self.poll_timer.start()
|
|
1115
1138
|
self.status_label.setText(f"▶ Monitoring: {os.path.basename(self.watch_folder)}")
|
|
1116
1139
|
self.mode_label.setText("Mode: Linear Average")
|
|
1117
1140
|
|
|
1118
1141
|
def stop_live(self):
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1142
|
+
self._stop_event.set() # <-- NEW: request cancellation immediately
|
|
1143
|
+
self.is_running = False
|
|
1144
|
+
self.poll_timer.stop()
|
|
1145
|
+
self.status_label.setText("■ Stopped")
|
|
1146
|
+
QApplication.processEvents()
|
|
1147
|
+
|
|
1125
1148
|
|
|
1126
1149
|
def reset_live(self):
|
|
1127
1150
|
if self.is_running:
|
|
1151
|
+
self._stop_event.set()
|
|
1128
1152
|
self.is_running = False
|
|
1129
1153
|
self.poll_timer.stop()
|
|
1130
1154
|
self.status_label.setText("■ Stopped")
|
|
@@ -1238,65 +1262,76 @@ class LiveStackWindow(QDialog):
|
|
|
1238
1262
|
|
|
1239
1263
|
|
|
1240
1264
|
def check_for_new_frames(self):
|
|
1241
|
-
if
|
|
1265
|
+
if self._should_stop() or not self.watch_folder:
|
|
1266
|
+
return
|
|
1267
|
+
if getattr(self, "_poll_busy", False):
|
|
1242
1268
|
return
|
|
1269
|
+
self._poll_busy = True
|
|
1270
|
+
try:
|
|
1271
|
+
# Gather candidates
|
|
1272
|
+
exts = (
|
|
1273
|
+
"*.fit", "*.fits", "*.tif", "*.tiff",
|
|
1274
|
+
"*.cr2", "*.cr3", "*.nef", "*.arw",
|
|
1275
|
+
"*.dng", "*.raf", "*.orf", "*.rw2", "*.pef", "*.xisf",
|
|
1276
|
+
"*.png", "*.jpg", "*.jpeg"
|
|
1277
|
+
)
|
|
1278
|
+
all_paths = []
|
|
1279
|
+
for ext in exts:
|
|
1280
|
+
all_paths += glob.glob(os.path.join(self.watch_folder, '**', ext), recursive=True)
|
|
1243
1281
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
"*.dng", "*.raf", "*.orf", "*.rw2", "*.pef", "*.xisf",
|
|
1249
|
-
"*.png", "*.jpg", "*.jpeg"
|
|
1250
|
-
)
|
|
1251
|
-
all_paths = []
|
|
1252
|
-
for ext in exts:
|
|
1253
|
-
all_paths += glob.glob(os.path.join(self.watch_folder, '**', ext), recursive=True)
|
|
1282
|
+
# Only consider paths not yet processed
|
|
1283
|
+
candidates = [p for p in sorted(all_paths) if p not in self.processed_files]
|
|
1284
|
+
if not candidates:
|
|
1285
|
+
return
|
|
1254
1286
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
if not candidates:
|
|
1258
|
-
return
|
|
1287
|
+
self.status_label.setText(f"➜ New/updated files: {len(candidates)}")
|
|
1288
|
+
QApplication.processEvents()
|
|
1259
1289
|
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1290
|
+
processed_now = 0
|
|
1291
|
+
for path in candidates:
|
|
1292
|
+
if self._should_stop(): # <-- NEW
|
|
1293
|
+
self.status_label.setText("■ Stopped")
|
|
1294
|
+
QApplication.processEvents()
|
|
1295
|
+
break
|
|
1263
1296
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
# Skip if we recently penalized this path
|
|
1268
|
-
info = self._probe.get(path)
|
|
1269
|
-
if info and time.time() < info.get("penalty_until", 0.0):
|
|
1270
|
-
continue
|
|
1297
|
+
info = self._probe.get(path)
|
|
1298
|
+
if info and time.time() < info.get("penalty_until", 0.0):
|
|
1299
|
+
continue
|
|
1271
1300
|
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
continue # not yet ready; we'll see it again on the next tick
|
|
1301
|
+
if not self._file_ready(path):
|
|
1302
|
+
continue
|
|
1275
1303
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
self.status_label.setText(f"→ Processing: {base}")
|
|
1280
|
-
QApplication.processEvents()
|
|
1304
|
+
# IMPORTANT: still allow stop before committing the path as processed
|
|
1305
|
+
if self._should_stop(): # <-- NEW
|
|
1306
|
+
break
|
|
1281
1307
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
except Exception as e:
|
|
1286
|
-
# If anything unexpected happens, clear 'processed' so we can retry later
|
|
1287
|
-
# but add a penalty to avoid tight loops.
|
|
1288
|
-
self.processed_files.discard(path)
|
|
1289
|
-
info = self._probe.get(path) or self._update_probe(path)
|
|
1290
|
-
if info:
|
|
1291
|
-
info["penalty_until"] = time.time() + self.OPEN_RETRY_PENALTY_SECS
|
|
1292
|
-
self.status_label.setText(f"⚠ Error on {base}: {e}")
|
|
1308
|
+
self.processed_files.add(path)
|
|
1309
|
+
base = os.path.basename(path)
|
|
1310
|
+
self.status_label.setText(f"→ Processing: {base}")
|
|
1293
1311
|
QApplication.processEvents()
|
|
1294
1312
|
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1313
|
+
try:
|
|
1314
|
+
self.process_frame(path)
|
|
1315
|
+
processed_now += 1
|
|
1316
|
+
except Exception as e:
|
|
1317
|
+
# If anything unexpected happens, clear 'processed' so we can retry later
|
|
1318
|
+
# but add a penalty to avoid tight loops.
|
|
1319
|
+
self.processed_files.discard(path)
|
|
1320
|
+
info = self._probe.get(path) or self._update_probe(path)
|
|
1321
|
+
if info:
|
|
1322
|
+
info["penalty_until"] = time.time() + self.OPEN_RETRY_PENALTY_SECS
|
|
1323
|
+
self.status_label.setText(f"⚠ Error on {base}: {e}")
|
|
1324
|
+
QApplication.processEvents()
|
|
1298
1325
|
|
|
1326
|
+
if processed_now > 0:
|
|
1327
|
+
self.status_label.setText(f"✔ Processed {processed_now} file(s)")
|
|
1328
|
+
QApplication.processEvents()
|
|
1329
|
+
finally:
|
|
1330
|
+
self._poll_busy = False
|
|
1331
|
+
|
|
1299
1332
|
def process_frame(self, path):
|
|
1333
|
+
if self._should_stop():
|
|
1334
|
+
return
|
|
1300
1335
|
if not self._file_ready(path):
|
|
1301
1336
|
# do not mark as processed here; monitor will retry after cool-down
|
|
1302
1337
|
return
|
|
@@ -1376,6 +1411,9 @@ class LiveStackWindow(QDialog):
|
|
|
1376
1411
|
QApplication.processEvents()
|
|
1377
1412
|
return
|
|
1378
1413
|
|
|
1414
|
+
if self._should_stop():
|
|
1415
|
+
return
|
|
1416
|
+
|
|
1379
1417
|
# ——— 2) CALIBRATION (once) ————————————————————————
|
|
1380
1418
|
# ——— 2a) DETECT MONO→COLOR MODE ————————————————————
|
|
1381
1419
|
mono_key = None
|
|
@@ -1391,12 +1429,18 @@ class LiveStackWindow(QDialog):
|
|
|
1391
1429
|
elif self.master_flat is not None:
|
|
1392
1430
|
img = apply_flat_division_numba(img, self.master_flat)
|
|
1393
1431
|
|
|
1432
|
+
if self._should_stop():
|
|
1433
|
+
return
|
|
1434
|
+
|
|
1394
1435
|
# ——— 3) DEBAYER if BAYERPAT ——————————————————————
|
|
1395
1436
|
if is_mono and header.get('BAYERPAT'):
|
|
1396
1437
|
pat = header['BAYERPAT'][0] if isinstance(header['BAYERPAT'], tuple) else header['BAYERPAT']
|
|
1397
1438
|
img = debayer_fits_fast(img, pat)
|
|
1398
1439
|
is_mono = False
|
|
1399
1440
|
|
|
1441
|
+
if self._should_stop():
|
|
1442
|
+
return
|
|
1443
|
+
|
|
1400
1444
|
# ——— 5) PROMOTION TO 3-CHANNEL if NOT in mono-mode —————
|
|
1401
1445
|
if mono_key is None and img.ndim == 2:
|
|
1402
1446
|
img = np.stack([img, img, img], axis=2)
|
|
@@ -1418,6 +1462,9 @@ class LiveStackWindow(QDialog):
|
|
|
1418
1462
|
plane if plane.ndim == 2 else plane[:, :, None], delta
|
|
1419
1463
|
).squeeze()
|
|
1420
1464
|
|
|
1465
|
+
if self._should_stop():
|
|
1466
|
+
return
|
|
1467
|
+
|
|
1421
1468
|
# ——— 8) NORMALIZE —————————————————————————————
|
|
1422
1469
|
if mono_key:
|
|
1423
1470
|
norm_plane = stretch_mono_image(plane, target_median=0.3)
|
|
@@ -1426,6 +1473,9 @@ class LiveStackWindow(QDialog):
|
|
|
1426
1473
|
norm_color = stretch_color_image(img, target_median=0.3, linked=False)
|
|
1427
1474
|
norm_plane = np.mean(norm_color, axis=2)
|
|
1428
1475
|
|
|
1476
|
+
if self._should_stop():
|
|
1477
|
+
return
|
|
1478
|
+
|
|
1429
1479
|
# ——— 9) METRICS & SNR —————————————————————————
|
|
1430
1480
|
sc, fwhm, ecc = compute_frame_star_metrics(norm_plane)
|
|
1431
1481
|
# instead, use the cumulative stack (or composite) for SNR:
|
|
@@ -1445,6 +1495,9 @@ class LiveStackWindow(QDialog):
|
|
|
1445
1495
|
stack_img = norm_color
|
|
1446
1496
|
snr_val = estimate_global_snr(stack_img)
|
|
1447
1497
|
|
|
1498
|
+
if self._should_stop():
|
|
1499
|
+
return
|
|
1500
|
+
|
|
1448
1501
|
# ——— 10) CULLING? ————————————————————————————
|
|
1449
1502
|
flagged = (
|
|
1450
1503
|
(fwhm > self.max_fwhm) or
|
|
@@ -1473,6 +1526,9 @@ class LiveStackWindow(QDialog):
|
|
|
1473
1526
|
self.status_label.setText("Started linear stack")
|
|
1474
1527
|
QApplication.processEvents()
|
|
1475
1528
|
|
|
1529
|
+
if self._should_stop():
|
|
1530
|
+
return
|
|
1531
|
+
|
|
1476
1532
|
if mono_key:
|
|
1477
1533
|
# start the filter stack
|
|
1478
1534
|
self.filter_stacks[mono_key] = norm_plane.copy()
|
|
@@ -1512,6 +1568,9 @@ class LiveStackWindow(QDialog):
|
|
|
1512
1568
|
)
|
|
1513
1569
|
self._buffer.append(norm_color.copy())
|
|
1514
1570
|
|
|
1571
|
+
if self._should_stop():
|
|
1572
|
+
return
|
|
1573
|
+
|
|
1515
1574
|
# hit the bootstrap threshold?
|
|
1516
1575
|
if n == self.bootstrap_frames:
|
|
1517
1576
|
# init Welford stats
|
|
@@ -1542,6 +1601,9 @@ class LiveStackWindow(QDialog):
|
|
|
1542
1601
|
+ (1.0 / n) * clipped
|
|
1543
1602
|
)
|
|
1544
1603
|
|
|
1604
|
+
if self._should_stop():
|
|
1605
|
+
return
|
|
1606
|
+
|
|
1545
1607
|
# Welford update
|
|
1546
1608
|
delta_mu = clipped - self._mu
|
|
1547
1609
|
self._mu += delta_mu / n
|
|
@@ -1590,6 +1652,9 @@ class LiveStackWindow(QDialog):
|
|
|
1590
1652
|
buf.append(norm_plane.copy())
|
|
1591
1653
|
self.filter_counts[mono_key] = new_count
|
|
1592
1654
|
|
|
1655
|
+
if self._should_stop():
|
|
1656
|
+
return
|
|
1657
|
+
|
|
1593
1658
|
if new_count == self.bootstrap_frames:
|
|
1594
1659
|
# init Welford
|
|
1595
1660
|
stacked = np.stack(buf, axis=0)
|
|
@@ -1623,7 +1688,8 @@ class LiveStackWindow(QDialog):
|
|
|
1623
1688
|
(count / new_count) * self.filter_stacks[mono_key]
|
|
1624
1689
|
+ (1.0 / new_count) * clipped
|
|
1625
1690
|
)
|
|
1626
|
-
|
|
1691
|
+
if self._should_stop():
|
|
1692
|
+
return
|
|
1627
1693
|
# Welford update on µ and m2
|
|
1628
1694
|
delta = clipped - mu
|
|
1629
1695
|
new_mu = mu + delta / new_count
|
|
@@ -1658,6 +1724,9 @@ class LiveStackWindow(QDialog):
|
|
|
1658
1724
|
pass # Ignore exposure parsing errors
|
|
1659
1725
|
QApplication.processEvents()
|
|
1660
1726
|
|
|
1727
|
+
if self._should_stop():
|
|
1728
|
+
return
|
|
1729
|
+
|
|
1661
1730
|
# ─── 13) Update UI ─────────────────────────────────────────
|
|
1662
1731
|
self.frame_count_label.setText(f"Frames: {self.frame_count}")
|
|
1663
1732
|
QApplication.processEvents()
|
|
@@ -1667,6 +1736,9 @@ class LiveStackWindow(QDialog):
|
|
|
1667
1736
|
self.frame_count, fwhm, ecc, sc, snr_val, False
|
|
1668
1737
|
)
|
|
1669
1738
|
|
|
1739
|
+
if self._should_stop():
|
|
1740
|
+
return
|
|
1741
|
+
|
|
1670
1742
|
# ——— 14) PREVIEW & STATUS LABEL —————————————————————
|
|
1671
1743
|
if mono_key:
|
|
1672
1744
|
preview = self._build_color_composite()
|
|
@@ -1685,6 +1757,8 @@ class LiveStackWindow(QDialog):
|
|
|
1685
1757
|
Load/calibrate a single frame (RAW or FITS/TIFF), debayer if needed,
|
|
1686
1758
|
normalize, then build a max‐value “star trail” in self.current_stack.
|
|
1687
1759
|
"""
|
|
1760
|
+
if self._should_stop():
|
|
1761
|
+
return
|
|
1688
1762
|
# ─── 1) Load (RAW vs FITS) ─────────────────────────────
|
|
1689
1763
|
lower = path.lower()
|
|
1690
1764
|
raw_exts = ('.cr2', '.cr3', '.nef', '.arw', '.dng', '.raf',
|
|
@@ -1737,6 +1811,9 @@ class LiveStackWindow(QDialog):
|
|
|
1737
1811
|
QApplication.processEvents()
|
|
1738
1812
|
return
|
|
1739
1813
|
|
|
1814
|
+
if self._should_stop():
|
|
1815
|
+
return
|
|
1816
|
+
|
|
1740
1817
|
# ─── 2) Calibration ─────────────────────────────────────
|
|
1741
1818
|
mono_key = None
|
|
1742
1819
|
if (self.mono_color_mode
|
|
@@ -1755,6 +1832,9 @@ class LiveStackWindow(QDialog):
|
|
|
1755
1832
|
img = apply_flat_division_numba(img,
|
|
1756
1833
|
self.master_flat)
|
|
1757
1834
|
|
|
1835
|
+
if self._should_stop():
|
|
1836
|
+
return
|
|
1837
|
+
|
|
1758
1838
|
# ─── 3) Debayer ─────────────────────────────────────────
|
|
1759
1839
|
if is_mono and header.get('BAYERPAT'):
|
|
1760
1840
|
pat = (header['BAYERPAT'][0]
|
|
@@ -1767,6 +1847,9 @@ class LiveStackWindow(QDialog):
|
|
|
1767
1847
|
if not mono_key and img.ndim == 2:
|
|
1768
1848
|
img = np.stack([img, img, img], axis=2)
|
|
1769
1849
|
|
|
1850
|
+
if self._should_stop():
|
|
1851
|
+
return
|
|
1852
|
+
|
|
1770
1853
|
# ─── 5) Normalize ───────────────────────────────────────
|
|
1771
1854
|
# for star-trail we want a visible, stretched version:
|
|
1772
1855
|
if img.ndim == 2:
|
|
@@ -1777,6 +1860,9 @@ class LiveStackWindow(QDialog):
|
|
|
1777
1860
|
target_median=0.3,
|
|
1778
1861
|
linked=False)
|
|
1779
1862
|
|
|
1863
|
+
if self._should_stop():
|
|
1864
|
+
return
|
|
1865
|
+
|
|
1780
1866
|
# ─── 6) Build max-value stack ───────────────────────────
|
|
1781
1867
|
if self.frame_count == 0:
|
|
1782
1868
|
self.current_stack = norm_color.copy()
|
|
@@ -1785,6 +1871,9 @@ class LiveStackWindow(QDialog):
|
|
|
1785
1871
|
self.current_stack = np.maximum(self.current_stack,
|
|
1786
1872
|
norm_color)
|
|
1787
1873
|
|
|
1874
|
+
if self._should_stop():
|
|
1875
|
+
return
|
|
1876
|
+
|
|
1788
1877
|
# ─── 7) Update counters and labels ──────────────────────
|
|
1789
1878
|
self.frame_count += 1
|
|
1790
1879
|
self.frame_count_label.setText(f"Frames: {self.frame_count}")
|
|
@@ -1809,6 +1898,18 @@ class LiveStackWindow(QDialog):
|
|
|
1809
1898
|
self.update_preview(self.current_stack)
|
|
1810
1899
|
QApplication.processEvents()
|
|
1811
1900
|
|
|
1901
|
+
def closeEvent(self, event):
|
|
1902
|
+
# request stop + stop timer
|
|
1903
|
+
self.stop_live()
|
|
1904
|
+
|
|
1905
|
+
# also close the metrics window so it doesn't keep stuff alive
|
|
1906
|
+
try:
|
|
1907
|
+
if self.metrics_window is not None:
|
|
1908
|
+
self.metrics_window.close()
|
|
1909
|
+
except Exception:
|
|
1910
|
+
pass
|
|
1911
|
+
|
|
1912
|
+
event.accept()
|
|
1812
1913
|
|
|
1813
1914
|
|
|
1814
1915
|
def update_preview(self, array: np.ndarray):
|