setiastrosuitepro 1.6.7__py3-none-any.whl → 1.7.0__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/colorwheel.svg +97 -0
- setiastro/images/cosmic.svg +40 -0
- setiastro/images/cosmicsat.svg +24 -0
- setiastro/images/graxpert.svg +19 -0
- setiastro/images/linearfit.svg +32 -0
- setiastro/images/narrowbandnormalization.png +0 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/images/planetarystacker.png +0 -0
- setiastro/saspro/__main__.py +1 -1
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/aberration_ai.py +49 -11
- setiastro/saspro/aberration_ai_preset.py +29 -3
- setiastro/saspro/add_stars.py +29 -5
- setiastro/saspro/backgroundneutral.py +73 -33
- setiastro/saspro/blink_comparator_pro.py +150 -55
- setiastro/saspro/convo.py +9 -6
- setiastro/saspro/cosmicclarity.py +125 -18
- setiastro/saspro/crop_dialog_pro.py +96 -2
- setiastro/saspro/curve_editor_pro.py +132 -61
- setiastro/saspro/curves_preset.py +249 -47
- setiastro/saspro/doc_manager.py +178 -11
- setiastro/saspro/frequency_separation.py +1159 -208
- setiastro/saspro/gui/main_window.py +340 -88
- setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
- setiastro/saspro/gui/mixins/file_mixin.py +35 -16
- setiastro/saspro/gui/mixins/menu_mixin.py +31 -1
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +132 -10
- setiastro/saspro/gui/mixins/update_mixin.py +121 -33
- setiastro/saspro/histogram.py +179 -7
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +769 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
- setiastro/saspro/imageops/stretch.py +582 -62
- setiastro/saspro/layers.py +13 -9
- setiastro/saspro/layers_dock.py +183 -3
- setiastro/saspro/legacy/numba_utils.py +68 -48
- setiastro/saspro/live_stacking.py +181 -73
- setiastro/saspro/multiscale_decomp.py +77 -29
- setiastro/saspro/narrowband_normalization.py +1618 -0
- setiastro/saspro/numba_utils.py +72 -57
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/ops/script_editor.py +5 -0
- setiastro/saspro/ops/scripts.py +119 -0
- setiastro/saspro/remove_green.py +1 -1
- setiastro/saspro/resources.py +4 -0
- setiastro/saspro/ser_stack_config.py +68 -0
- setiastro/saspro/ser_stacker.py +2245 -0
- setiastro/saspro/ser_stacker_dialog.py +1481 -0
- setiastro/saspro/ser_tracking.py +206 -0
- setiastro/saspro/serviewer.py +1242 -0
- setiastro/saspro/sfcc.py +602 -214
- setiastro/saspro/shortcuts.py +154 -25
- setiastro/saspro/signature_insert.py +688 -33
- setiastro/saspro/stacking_suite.py +853 -401
- setiastro/saspro/star_alignment.py +243 -122
- setiastro/saspro/stat_stretch.py +878 -131
- setiastro/saspro/subwindow.py +303 -74
- setiastro/saspro/whitebalance.py +24 -0
- setiastro/saspro/widgets/common_utilities.py +28 -21
- setiastro/saspro/widgets/resource_monitor.py +128 -80
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/METADATA +2 -2
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/RECORD +68 -51
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.7.0.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
|
|
@@ -354,7 +350,7 @@ def estimate_global_snr(
|
|
|
354
350
|
if sigma_central <= 0.0:
|
|
355
351
|
return 0.0
|
|
356
352
|
|
|
357
|
-
nu = med_patch - 3.0 * sigma_central
|
|
353
|
+
nu = med_patch - 3.0 * sigma_central
|
|
358
354
|
|
|
359
355
|
# 6) Return (mean − nu) / σ
|
|
360
356
|
return (mu_patch - nu) / sigma_central
|
|
@@ -395,6 +391,8 @@ class LiveStackWindow(QDialog):
|
|
|
395
391
|
self.OPEN_RETRY_PENALTY_SECS = 10.0 # cool-down after a read/permission failure
|
|
396
392
|
self.MAX_FILE_WAIT_SECS = 600.0 # optional safety cap (unused by default logic)
|
|
397
393
|
|
|
394
|
+
self._stop_event = threading.Event()
|
|
395
|
+
|
|
398
396
|
# ── Load persisted settings ───────────────────────────────
|
|
399
397
|
s = QSettings()
|
|
400
398
|
self.bootstrap_frames = _qget(s, "LiveStack/bootstrap_frames", 24, int)
|
|
@@ -611,6 +609,17 @@ class LiveStackWindow(QDialog):
|
|
|
611
609
|
self.poll_timer.timeout.connect(self.check_for_new_frames)
|
|
612
610
|
self._on_mono_color_toggled(self.mono_color_checkbox.isChecked())
|
|
613
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
|
+
|
|
614
623
|
|
|
615
624
|
# ─────────────────────────────────────────────────────────────────────────
|
|
616
625
|
def _on_star_trail_toggled(self, checked: bool):
|
|
@@ -938,8 +947,9 @@ class LiveStackWindow(QDialog):
|
|
|
938
947
|
# direct mapping: e.g. "SHO" → R=S, G=H, B=O
|
|
939
948
|
letters = list(mode)
|
|
940
949
|
if len(letters) != 3 or any(l not in ("S","H","O") for l in letters):
|
|
941
|
-
#
|
|
942
|
-
|
|
950
|
+
# fallback to natural
|
|
951
|
+
self.narrowband_mapping = "Natural"
|
|
952
|
+
return self._build_color_composite()
|
|
943
953
|
|
|
944
954
|
R = getf(letters[0])
|
|
945
955
|
G = getf(letters[1])
|
|
@@ -1089,12 +1099,10 @@ class LiveStackWindow(QDialog):
|
|
|
1089
1099
|
if not self.watch_folder:
|
|
1090
1100
|
self.status_label.setText("❗ No folder selected")
|
|
1091
1101
|
return
|
|
1092
|
-
|
|
1102
|
+
self._stop_event.clear()
|
|
1103
|
+
self.is_running = True
|
|
1093
1104
|
self.processed_files.clear()
|
|
1094
|
-
# Process all current files once
|
|
1095
1105
|
self.check_for_new_frames()
|
|
1096
|
-
# Now start monitoring
|
|
1097
|
-
self.is_running = True
|
|
1098
1106
|
self.poll_timer.start()
|
|
1099
1107
|
self.status_label.setText(f"▶ Processing & Monitoring: {os.path.basename(self.watch_folder)}")
|
|
1100
1108
|
|
|
@@ -1103,6 +1111,7 @@ class LiveStackWindow(QDialog):
|
|
|
1103
1111
|
if not self.watch_folder:
|
|
1104
1112
|
self.status_label.setText("❗ No folder selected")
|
|
1105
1113
|
return
|
|
1114
|
+
self._stop_event.clear()
|
|
1106
1115
|
# Populate processed_files with all existing files so they won't be re-processed
|
|
1107
1116
|
exts = (
|
|
1108
1117
|
"*.fit", "*.fits", "*.tif", "*.tiff",
|
|
@@ -1123,21 +1132,23 @@ class LiveStackWindow(QDialog):
|
|
|
1123
1132
|
if not self.watch_folder:
|
|
1124
1133
|
self.status_label.setText("❗ No folder selected")
|
|
1125
1134
|
return
|
|
1135
|
+
self._stop_event.clear()
|
|
1126
1136
|
self.is_running = True
|
|
1127
1137
|
self.poll_timer.start()
|
|
1128
1138
|
self.status_label.setText(f"▶ Monitoring: {os.path.basename(self.watch_folder)}")
|
|
1129
1139
|
self.mode_label.setText("Mode: Linear Average")
|
|
1130
1140
|
|
|
1131
1141
|
def stop_live(self):
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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
|
+
|
|
1138
1148
|
|
|
1139
1149
|
def reset_live(self):
|
|
1140
1150
|
if self.is_running:
|
|
1151
|
+
self._stop_event.set()
|
|
1141
1152
|
self.is_running = False
|
|
1142
1153
|
self.poll_timer.stop()
|
|
1143
1154
|
self.status_label.setText("■ Stopped")
|
|
@@ -1251,65 +1262,92 @@ class LiveStackWindow(QDialog):
|
|
|
1251
1262
|
|
|
1252
1263
|
|
|
1253
1264
|
def check_for_new_frames(self):
|
|
1254
|
-
if
|
|
1265
|
+
if self._should_stop() or not self.watch_folder:
|
|
1266
|
+
return
|
|
1267
|
+
if getattr(self, "_poll_busy", False):
|
|
1255
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)
|
|
1256
1281
|
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
"*.dng", "*.raf", "*.orf", "*.rw2", "*.pef", "*.xisf",
|
|
1262
|
-
"*.png", "*.jpg", "*.jpeg"
|
|
1263
|
-
)
|
|
1264
|
-
all_paths = []
|
|
1265
|
-
for ext in exts:
|
|
1266
|
-
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
|
|
1267
1286
|
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
if not candidates:
|
|
1271
|
-
return
|
|
1287
|
+
self.status_label.setText(f"➜ New/updated files: {len(candidates)}")
|
|
1288
|
+
QApplication.processEvents()
|
|
1272
1289
|
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
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
|
|
1276
1296
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
# Skip if we recently penalized this path
|
|
1281
|
-
info = self._probe.get(path)
|
|
1282
|
-
if info and time.time() < info.get("penalty_until", 0.0):
|
|
1283
|
-
continue
|
|
1297
|
+
info = self._probe.get(path)
|
|
1298
|
+
if info and time.time() < info.get("penalty_until", 0.0):
|
|
1299
|
+
continue
|
|
1284
1300
|
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
continue # not yet ready; we'll see it again on the next tick
|
|
1301
|
+
if not self._file_ready(path):
|
|
1302
|
+
continue
|
|
1288
1303
|
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
self.status_label.setText(f"→ Processing: {base}")
|
|
1293
|
-
QApplication.processEvents()
|
|
1304
|
+
# IMPORTANT: still allow stop before committing the path as processed
|
|
1305
|
+
if self._should_stop(): # <-- NEW
|
|
1306
|
+
break
|
|
1294
1307
|
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
except Exception as e:
|
|
1299
|
-
# If anything unexpected happens, clear 'processed' so we can retry later
|
|
1300
|
-
# but add a penalty to avoid tight loops.
|
|
1301
|
-
self.processed_files.discard(path)
|
|
1302
|
-
info = self._probe.get(path) or self._update_probe(path)
|
|
1303
|
-
if info:
|
|
1304
|
-
info["penalty_until"] = time.time() + self.OPEN_RETRY_PENALTY_SECS
|
|
1305
|
-
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}")
|
|
1306
1311
|
QApplication.processEvents()
|
|
1307
1312
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
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()
|
|
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
|
+
|
|
1332
|
+
def _match_master_to_image(self, master: np.ndarray, img: np.ndarray) -> np.ndarray:
|
|
1333
|
+
"""
|
|
1334
|
+
Coerce master (dark/flat) to match img dimensionality.
|
|
1335
|
+
- If img is RGB (H,W,3) and master is mono (H,W), expand to (H,W,1).
|
|
1336
|
+
- If img is mono (H,W) and master is RGB (H,W,3), collapse to mono via mean.
|
|
1337
|
+
"""
|
|
1338
|
+
if master is None:
|
|
1339
|
+
return None
|
|
1340
|
+
|
|
1341
|
+
if img.ndim == 3 and master.ndim == 2:
|
|
1342
|
+
return master[..., None] # (H,W,1) broadcasts to (H,W,3)
|
|
1343
|
+
if img.ndim == 2 and master.ndim == 3:
|
|
1344
|
+
return master.mean(axis=2) # (H,W)
|
|
1345
|
+
return master
|
|
1346
|
+
|
|
1311
1347
|
|
|
1312
1348
|
def process_frame(self, path):
|
|
1349
|
+
if self._should_stop():
|
|
1350
|
+
return
|
|
1313
1351
|
if not self._file_ready(path):
|
|
1314
1352
|
# do not mark as processed here; monitor will retry after cool-down
|
|
1315
1353
|
return
|
|
@@ -1389,6 +1427,9 @@ class LiveStackWindow(QDialog):
|
|
|
1389
1427
|
QApplication.processEvents()
|
|
1390
1428
|
return
|
|
1391
1429
|
|
|
1430
|
+
if self._should_stop():
|
|
1431
|
+
return
|
|
1432
|
+
|
|
1392
1433
|
# ——— 2) CALIBRATION (once) ————————————————————————
|
|
1393
1434
|
# ——— 2a) DETECT MONO→COLOR MODE ————————————————————
|
|
1394
1435
|
mono_key = None
|
|
@@ -1397,12 +1438,19 @@ class LiveStackWindow(QDialog):
|
|
|
1397
1438
|
|
|
1398
1439
|
# ——— 2b) CALIBRATION (once) ————————————————————————
|
|
1399
1440
|
if self.master_dark is not None:
|
|
1400
|
-
|
|
1441
|
+
md = self._match_master_to_image(self.master_dark, img).astype(np.float32, copy=False)
|
|
1442
|
+
img = img.astype(np.float32, copy=False) - md
|
|
1401
1443
|
# prefer per-filter flat if we’re in mono→color and have one
|
|
1402
1444
|
if mono_key and mono_key in self.master_flats:
|
|
1403
|
-
|
|
1445
|
+
mf = self._match_master_to_image(self.master_flats[mono_key], img).astype(np.float32, copy=False)
|
|
1446
|
+
img = apply_flat_division_numba(img, mf)
|
|
1404
1447
|
elif self.master_flat is not None:
|
|
1405
|
-
|
|
1448
|
+
mf = self._match_master_to_image(self.master_flat, img).astype(np.float32, copy=False)
|
|
1449
|
+
img = apply_flat_division_numba(img, mf)
|
|
1450
|
+
|
|
1451
|
+
|
|
1452
|
+
if self._should_stop():
|
|
1453
|
+
return
|
|
1406
1454
|
|
|
1407
1455
|
# ——— 3) DEBAYER if BAYERPAT ——————————————————————
|
|
1408
1456
|
if is_mono and header.get('BAYERPAT'):
|
|
@@ -1410,6 +1458,9 @@ class LiveStackWindow(QDialog):
|
|
|
1410
1458
|
img = debayer_fits_fast(img, pat)
|
|
1411
1459
|
is_mono = False
|
|
1412
1460
|
|
|
1461
|
+
if self._should_stop():
|
|
1462
|
+
return
|
|
1463
|
+
|
|
1413
1464
|
# ——— 5) PROMOTION TO 3-CHANNEL if NOT in mono-mode —————
|
|
1414
1465
|
if mono_key is None and img.ndim == 2:
|
|
1415
1466
|
img = np.stack([img, img, img], axis=2)
|
|
@@ -1431,6 +1482,9 @@ class LiveStackWindow(QDialog):
|
|
|
1431
1482
|
plane if plane.ndim == 2 else plane[:, :, None], delta
|
|
1432
1483
|
).squeeze()
|
|
1433
1484
|
|
|
1485
|
+
if self._should_stop():
|
|
1486
|
+
return
|
|
1487
|
+
|
|
1434
1488
|
# ——— 8) NORMALIZE —————————————————————————————
|
|
1435
1489
|
if mono_key:
|
|
1436
1490
|
norm_plane = stretch_mono_image(plane, target_median=0.3)
|
|
@@ -1439,6 +1493,9 @@ class LiveStackWindow(QDialog):
|
|
|
1439
1493
|
norm_color = stretch_color_image(img, target_median=0.3, linked=False)
|
|
1440
1494
|
norm_plane = np.mean(norm_color, axis=2)
|
|
1441
1495
|
|
|
1496
|
+
if self._should_stop():
|
|
1497
|
+
return
|
|
1498
|
+
|
|
1442
1499
|
# ——— 9) METRICS & SNR —————————————————————————
|
|
1443
1500
|
sc, fwhm, ecc = compute_frame_star_metrics(norm_plane)
|
|
1444
1501
|
# instead, use the cumulative stack (or composite) for SNR:
|
|
@@ -1458,6 +1515,9 @@ class LiveStackWindow(QDialog):
|
|
|
1458
1515
|
stack_img = norm_color
|
|
1459
1516
|
snr_val = estimate_global_snr(stack_img)
|
|
1460
1517
|
|
|
1518
|
+
if self._should_stop():
|
|
1519
|
+
return
|
|
1520
|
+
|
|
1461
1521
|
# ——— 10) CULLING? ————————————————————————————
|
|
1462
1522
|
flagged = (
|
|
1463
1523
|
(fwhm > self.max_fwhm) or
|
|
@@ -1486,6 +1546,9 @@ class LiveStackWindow(QDialog):
|
|
|
1486
1546
|
self.status_label.setText("Started linear stack")
|
|
1487
1547
|
QApplication.processEvents()
|
|
1488
1548
|
|
|
1549
|
+
if self._should_stop():
|
|
1550
|
+
return
|
|
1551
|
+
|
|
1489
1552
|
if mono_key:
|
|
1490
1553
|
# start the filter stack
|
|
1491
1554
|
self.filter_stacks[mono_key] = norm_plane.copy()
|
|
@@ -1525,6 +1588,9 @@ class LiveStackWindow(QDialog):
|
|
|
1525
1588
|
)
|
|
1526
1589
|
self._buffer.append(norm_color.copy())
|
|
1527
1590
|
|
|
1591
|
+
if self._should_stop():
|
|
1592
|
+
return
|
|
1593
|
+
|
|
1528
1594
|
# hit the bootstrap threshold?
|
|
1529
1595
|
if n == self.bootstrap_frames:
|
|
1530
1596
|
# init Welford stats
|
|
@@ -1555,6 +1621,9 @@ class LiveStackWindow(QDialog):
|
|
|
1555
1621
|
+ (1.0 / n) * clipped
|
|
1556
1622
|
)
|
|
1557
1623
|
|
|
1624
|
+
if self._should_stop():
|
|
1625
|
+
return
|
|
1626
|
+
|
|
1558
1627
|
# Welford update
|
|
1559
1628
|
delta_mu = clipped - self._mu
|
|
1560
1629
|
self._mu += delta_mu / n
|
|
@@ -1603,6 +1672,9 @@ class LiveStackWindow(QDialog):
|
|
|
1603
1672
|
buf.append(norm_plane.copy())
|
|
1604
1673
|
self.filter_counts[mono_key] = new_count
|
|
1605
1674
|
|
|
1675
|
+
if self._should_stop():
|
|
1676
|
+
return
|
|
1677
|
+
|
|
1606
1678
|
if new_count == self.bootstrap_frames:
|
|
1607
1679
|
# init Welford
|
|
1608
1680
|
stacked = np.stack(buf, axis=0)
|
|
@@ -1636,7 +1708,8 @@ class LiveStackWindow(QDialog):
|
|
|
1636
1708
|
(count / new_count) * self.filter_stacks[mono_key]
|
|
1637
1709
|
+ (1.0 / new_count) * clipped
|
|
1638
1710
|
)
|
|
1639
|
-
|
|
1711
|
+
if self._should_stop():
|
|
1712
|
+
return
|
|
1640
1713
|
# Welford update on µ and m2
|
|
1641
1714
|
delta = clipped - mu
|
|
1642
1715
|
new_mu = mu + delta / new_count
|
|
@@ -1671,6 +1744,9 @@ class LiveStackWindow(QDialog):
|
|
|
1671
1744
|
pass # Ignore exposure parsing errors
|
|
1672
1745
|
QApplication.processEvents()
|
|
1673
1746
|
|
|
1747
|
+
if self._should_stop():
|
|
1748
|
+
return
|
|
1749
|
+
|
|
1674
1750
|
# ─── 13) Update UI ─────────────────────────────────────────
|
|
1675
1751
|
self.frame_count_label.setText(f"Frames: {self.frame_count}")
|
|
1676
1752
|
QApplication.processEvents()
|
|
@@ -1680,6 +1756,9 @@ class LiveStackWindow(QDialog):
|
|
|
1680
1756
|
self.frame_count, fwhm, ecc, sc, snr_val, False
|
|
1681
1757
|
)
|
|
1682
1758
|
|
|
1759
|
+
if self._should_stop():
|
|
1760
|
+
return
|
|
1761
|
+
|
|
1683
1762
|
# ——— 14) PREVIEW & STATUS LABEL —————————————————————
|
|
1684
1763
|
if mono_key:
|
|
1685
1764
|
preview = self._build_color_composite()
|
|
@@ -1698,6 +1777,8 @@ class LiveStackWindow(QDialog):
|
|
|
1698
1777
|
Load/calibrate a single frame (RAW or FITS/TIFF), debayer if needed,
|
|
1699
1778
|
normalize, then build a max‐value “star trail” in self.current_stack.
|
|
1700
1779
|
"""
|
|
1780
|
+
if self._should_stop():
|
|
1781
|
+
return
|
|
1701
1782
|
# ─── 1) Load (RAW vs FITS) ─────────────────────────────
|
|
1702
1783
|
lower = path.lower()
|
|
1703
1784
|
raw_exts = ('.cr2', '.cr3', '.nef', '.arw', '.dng', '.raf',
|
|
@@ -1750,6 +1831,9 @@ class LiveStackWindow(QDialog):
|
|
|
1750
1831
|
QApplication.processEvents()
|
|
1751
1832
|
return
|
|
1752
1833
|
|
|
1834
|
+
if self._should_stop():
|
|
1835
|
+
return
|
|
1836
|
+
|
|
1753
1837
|
# ─── 2) Calibration ─────────────────────────────────────
|
|
1754
1838
|
mono_key = None
|
|
1755
1839
|
if (self.mono_color_mode
|
|
@@ -1768,6 +1852,9 @@ class LiveStackWindow(QDialog):
|
|
|
1768
1852
|
img = apply_flat_division_numba(img,
|
|
1769
1853
|
self.master_flat)
|
|
1770
1854
|
|
|
1855
|
+
if self._should_stop():
|
|
1856
|
+
return
|
|
1857
|
+
|
|
1771
1858
|
# ─── 3) Debayer ─────────────────────────────────────────
|
|
1772
1859
|
if is_mono and header.get('BAYERPAT'):
|
|
1773
1860
|
pat = (header['BAYERPAT'][0]
|
|
@@ -1780,6 +1867,9 @@ class LiveStackWindow(QDialog):
|
|
|
1780
1867
|
if not mono_key and img.ndim == 2:
|
|
1781
1868
|
img = np.stack([img, img, img], axis=2)
|
|
1782
1869
|
|
|
1870
|
+
if self._should_stop():
|
|
1871
|
+
return
|
|
1872
|
+
|
|
1783
1873
|
# ─── 5) Normalize ───────────────────────────────────────
|
|
1784
1874
|
# for star-trail we want a visible, stretched version:
|
|
1785
1875
|
if img.ndim == 2:
|
|
@@ -1790,6 +1880,9 @@ class LiveStackWindow(QDialog):
|
|
|
1790
1880
|
target_median=0.3,
|
|
1791
1881
|
linked=False)
|
|
1792
1882
|
|
|
1883
|
+
if self._should_stop():
|
|
1884
|
+
return
|
|
1885
|
+
|
|
1793
1886
|
# ─── 6) Build max-value stack ───────────────────────────
|
|
1794
1887
|
if self.frame_count == 0:
|
|
1795
1888
|
self.current_stack = norm_color.copy()
|
|
@@ -1798,6 +1891,9 @@ class LiveStackWindow(QDialog):
|
|
|
1798
1891
|
self.current_stack = np.maximum(self.current_stack,
|
|
1799
1892
|
norm_color)
|
|
1800
1893
|
|
|
1894
|
+
if self._should_stop():
|
|
1895
|
+
return
|
|
1896
|
+
|
|
1801
1897
|
# ─── 7) Update counters and labels ──────────────────────
|
|
1802
1898
|
self.frame_count += 1
|
|
1803
1899
|
self.frame_count_label.setText(f"Frames: {self.frame_count}")
|
|
@@ -1822,6 +1918,18 @@ class LiveStackWindow(QDialog):
|
|
|
1822
1918
|
self.update_preview(self.current_stack)
|
|
1823
1919
|
QApplication.processEvents()
|
|
1824
1920
|
|
|
1921
|
+
def closeEvent(self, event):
|
|
1922
|
+
# request stop + stop timer
|
|
1923
|
+
self.stop_live()
|
|
1924
|
+
|
|
1925
|
+
# also close the metrics window so it doesn't keep stuff alive
|
|
1926
|
+
try:
|
|
1927
|
+
if self.metrics_window is not None:
|
|
1928
|
+
self.metrics_window.close()
|
|
1929
|
+
except Exception:
|
|
1930
|
+
pass
|
|
1931
|
+
|
|
1932
|
+
event.accept()
|
|
1825
1933
|
|
|
1826
1934
|
|
|
1827
1935
|
def update_preview(self, array: np.ndarray):
|