setiastrosuitepro 1.6.4__py3-none-any.whl → 1.7.1.post2__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/TextureClarity.svg +56 -0
- 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/narrowbandnormalization.png +0 -0
- setiastro/images/new_moon.png +0 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/images/planetarystacker.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 +364 -33
- setiastro/saspro/aberration_ai_preset.py +29 -3
- setiastro/saspro/acv_exporter.py +379 -0
- setiastro/saspro/add_stars.py +33 -6
- setiastro/saspro/astrospike_python.py +45 -3
- setiastro/saspro/backgroundneutral.py +108 -40
- setiastro/saspro/blemish_blaster.py +4 -1
- setiastro/saspro/blink_comparator_pro.py +150 -55
- 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 +181 -64
- setiastro/saspro/curves_preset.py +249 -47
- 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 +706 -264
- 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 +35 -1
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +499 -24
- 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 +184 -8
- setiastro/saspro/image_combine.py +4 -0
- setiastro/saspro/image_peeker_pro.py +4 -0
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +1345 -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 +68 -48
- setiastro/saspro/legacy/xisf.py +240 -98
- setiastro/saspro/live_stacking.py +203 -82
- 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 +81 -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 +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_green.py +1 -1
- setiastro/saspro/resources.py +73 -0
- setiastro/saspro/rgbalign.py +460 -12
- setiastro/saspro/selective_color.py +4 -1
- setiastro/saspro/ser_stack_config.py +82 -0
- setiastro/saspro/ser_stacker.py +2321 -0
- setiastro/saspro/ser_stacker_dialog.py +1838 -0
- setiastro/saspro/ser_tracking.py +206 -0
- setiastro/saspro/serviewer.py +1625 -0
- setiastro/saspro/sfcc.py +662 -216
- setiastro/saspro/shortcuts.py +171 -33
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1347 -485
- 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 +892 -129
- setiastro/saspro/subwindow.py +787 -363
- setiastro/saspro/supernovaasteroidhunter.py +1 -1
- setiastro/saspro/texture_clarity.py +593 -0
- 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 +209 -111
- 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.7.1.post2.dist-info}/METADATA +4 -2
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/RECORD +132 -87
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.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,92 @@ 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()
|
|
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
|
+
|
|
1298
1347
|
|
|
1299
1348
|
def process_frame(self, path):
|
|
1349
|
+
if self._should_stop():
|
|
1350
|
+
return
|
|
1300
1351
|
if not self._file_ready(path):
|
|
1301
1352
|
# do not mark as processed here; monitor will retry after cool-down
|
|
1302
1353
|
return
|
|
@@ -1376,6 +1427,9 @@ class LiveStackWindow(QDialog):
|
|
|
1376
1427
|
QApplication.processEvents()
|
|
1377
1428
|
return
|
|
1378
1429
|
|
|
1430
|
+
if self._should_stop():
|
|
1431
|
+
return
|
|
1432
|
+
|
|
1379
1433
|
# ——— 2) CALIBRATION (once) ————————————————————————
|
|
1380
1434
|
# ——— 2a) DETECT MONO→COLOR MODE ————————————————————
|
|
1381
1435
|
mono_key = None
|
|
@@ -1384,12 +1438,19 @@ class LiveStackWindow(QDialog):
|
|
|
1384
1438
|
|
|
1385
1439
|
# ——— 2b) CALIBRATION (once) ————————————————————————
|
|
1386
1440
|
if self.master_dark is not None:
|
|
1387
|
-
|
|
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
|
|
1388
1443
|
# prefer per-filter flat if we’re in mono→color and have one
|
|
1389
1444
|
if mono_key and mono_key in self.master_flats:
|
|
1390
|
-
|
|
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)
|
|
1391
1447
|
elif self.master_flat is not None:
|
|
1392
|
-
|
|
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
|
|
1393
1454
|
|
|
1394
1455
|
# ——— 3) DEBAYER if BAYERPAT ——————————————————————
|
|
1395
1456
|
if is_mono and header.get('BAYERPAT'):
|
|
@@ -1397,6 +1458,9 @@ class LiveStackWindow(QDialog):
|
|
|
1397
1458
|
img = debayer_fits_fast(img, pat)
|
|
1398
1459
|
is_mono = False
|
|
1399
1460
|
|
|
1461
|
+
if self._should_stop():
|
|
1462
|
+
return
|
|
1463
|
+
|
|
1400
1464
|
# ——— 5) PROMOTION TO 3-CHANNEL if NOT in mono-mode —————
|
|
1401
1465
|
if mono_key is None and img.ndim == 2:
|
|
1402
1466
|
img = np.stack([img, img, img], axis=2)
|
|
@@ -1418,6 +1482,9 @@ class LiveStackWindow(QDialog):
|
|
|
1418
1482
|
plane if plane.ndim == 2 else plane[:, :, None], delta
|
|
1419
1483
|
).squeeze()
|
|
1420
1484
|
|
|
1485
|
+
if self._should_stop():
|
|
1486
|
+
return
|
|
1487
|
+
|
|
1421
1488
|
# ——— 8) NORMALIZE —————————————————————————————
|
|
1422
1489
|
if mono_key:
|
|
1423
1490
|
norm_plane = stretch_mono_image(plane, target_median=0.3)
|
|
@@ -1426,6 +1493,9 @@ class LiveStackWindow(QDialog):
|
|
|
1426
1493
|
norm_color = stretch_color_image(img, target_median=0.3, linked=False)
|
|
1427
1494
|
norm_plane = np.mean(norm_color, axis=2)
|
|
1428
1495
|
|
|
1496
|
+
if self._should_stop():
|
|
1497
|
+
return
|
|
1498
|
+
|
|
1429
1499
|
# ——— 9) METRICS & SNR —————————————————————————
|
|
1430
1500
|
sc, fwhm, ecc = compute_frame_star_metrics(norm_plane)
|
|
1431
1501
|
# instead, use the cumulative stack (or composite) for SNR:
|
|
@@ -1445,6 +1515,9 @@ class LiveStackWindow(QDialog):
|
|
|
1445
1515
|
stack_img = norm_color
|
|
1446
1516
|
snr_val = estimate_global_snr(stack_img)
|
|
1447
1517
|
|
|
1518
|
+
if self._should_stop():
|
|
1519
|
+
return
|
|
1520
|
+
|
|
1448
1521
|
# ——— 10) CULLING? ————————————————————————————
|
|
1449
1522
|
flagged = (
|
|
1450
1523
|
(fwhm > self.max_fwhm) or
|
|
@@ -1473,6 +1546,9 @@ class LiveStackWindow(QDialog):
|
|
|
1473
1546
|
self.status_label.setText("Started linear stack")
|
|
1474
1547
|
QApplication.processEvents()
|
|
1475
1548
|
|
|
1549
|
+
if self._should_stop():
|
|
1550
|
+
return
|
|
1551
|
+
|
|
1476
1552
|
if mono_key:
|
|
1477
1553
|
# start the filter stack
|
|
1478
1554
|
self.filter_stacks[mono_key] = norm_plane.copy()
|
|
@@ -1512,6 +1588,9 @@ class LiveStackWindow(QDialog):
|
|
|
1512
1588
|
)
|
|
1513
1589
|
self._buffer.append(norm_color.copy())
|
|
1514
1590
|
|
|
1591
|
+
if self._should_stop():
|
|
1592
|
+
return
|
|
1593
|
+
|
|
1515
1594
|
# hit the bootstrap threshold?
|
|
1516
1595
|
if n == self.bootstrap_frames:
|
|
1517
1596
|
# init Welford stats
|
|
@@ -1542,6 +1621,9 @@ class LiveStackWindow(QDialog):
|
|
|
1542
1621
|
+ (1.0 / n) * clipped
|
|
1543
1622
|
)
|
|
1544
1623
|
|
|
1624
|
+
if self._should_stop():
|
|
1625
|
+
return
|
|
1626
|
+
|
|
1545
1627
|
# Welford update
|
|
1546
1628
|
delta_mu = clipped - self._mu
|
|
1547
1629
|
self._mu += delta_mu / n
|
|
@@ -1590,6 +1672,9 @@ class LiveStackWindow(QDialog):
|
|
|
1590
1672
|
buf.append(norm_plane.copy())
|
|
1591
1673
|
self.filter_counts[mono_key] = new_count
|
|
1592
1674
|
|
|
1675
|
+
if self._should_stop():
|
|
1676
|
+
return
|
|
1677
|
+
|
|
1593
1678
|
if new_count == self.bootstrap_frames:
|
|
1594
1679
|
# init Welford
|
|
1595
1680
|
stacked = np.stack(buf, axis=0)
|
|
@@ -1623,7 +1708,8 @@ class LiveStackWindow(QDialog):
|
|
|
1623
1708
|
(count / new_count) * self.filter_stacks[mono_key]
|
|
1624
1709
|
+ (1.0 / new_count) * clipped
|
|
1625
1710
|
)
|
|
1626
|
-
|
|
1711
|
+
if self._should_stop():
|
|
1712
|
+
return
|
|
1627
1713
|
# Welford update on µ and m2
|
|
1628
1714
|
delta = clipped - mu
|
|
1629
1715
|
new_mu = mu + delta / new_count
|
|
@@ -1658,6 +1744,9 @@ class LiveStackWindow(QDialog):
|
|
|
1658
1744
|
pass # Ignore exposure parsing errors
|
|
1659
1745
|
QApplication.processEvents()
|
|
1660
1746
|
|
|
1747
|
+
if self._should_stop():
|
|
1748
|
+
return
|
|
1749
|
+
|
|
1661
1750
|
# ─── 13) Update UI ─────────────────────────────────────────
|
|
1662
1751
|
self.frame_count_label.setText(f"Frames: {self.frame_count}")
|
|
1663
1752
|
QApplication.processEvents()
|
|
@@ -1667,6 +1756,9 @@ class LiveStackWindow(QDialog):
|
|
|
1667
1756
|
self.frame_count, fwhm, ecc, sc, snr_val, False
|
|
1668
1757
|
)
|
|
1669
1758
|
|
|
1759
|
+
if self._should_stop():
|
|
1760
|
+
return
|
|
1761
|
+
|
|
1670
1762
|
# ——— 14) PREVIEW & STATUS LABEL —————————————————————
|
|
1671
1763
|
if mono_key:
|
|
1672
1764
|
preview = self._build_color_composite()
|
|
@@ -1685,6 +1777,8 @@ class LiveStackWindow(QDialog):
|
|
|
1685
1777
|
Load/calibrate a single frame (RAW or FITS/TIFF), debayer if needed,
|
|
1686
1778
|
normalize, then build a max‐value “star trail” in self.current_stack.
|
|
1687
1779
|
"""
|
|
1780
|
+
if self._should_stop():
|
|
1781
|
+
return
|
|
1688
1782
|
# ─── 1) Load (RAW vs FITS) ─────────────────────────────
|
|
1689
1783
|
lower = path.lower()
|
|
1690
1784
|
raw_exts = ('.cr2', '.cr3', '.nef', '.arw', '.dng', '.raf',
|
|
@@ -1737,6 +1831,9 @@ class LiveStackWindow(QDialog):
|
|
|
1737
1831
|
QApplication.processEvents()
|
|
1738
1832
|
return
|
|
1739
1833
|
|
|
1834
|
+
if self._should_stop():
|
|
1835
|
+
return
|
|
1836
|
+
|
|
1740
1837
|
# ─── 2) Calibration ─────────────────────────────────────
|
|
1741
1838
|
mono_key = None
|
|
1742
1839
|
if (self.mono_color_mode
|
|
@@ -1755,6 +1852,9 @@ class LiveStackWindow(QDialog):
|
|
|
1755
1852
|
img = apply_flat_division_numba(img,
|
|
1756
1853
|
self.master_flat)
|
|
1757
1854
|
|
|
1855
|
+
if self._should_stop():
|
|
1856
|
+
return
|
|
1857
|
+
|
|
1758
1858
|
# ─── 3) Debayer ─────────────────────────────────────────
|
|
1759
1859
|
if is_mono and header.get('BAYERPAT'):
|
|
1760
1860
|
pat = (header['BAYERPAT'][0]
|
|
@@ -1767,6 +1867,9 @@ class LiveStackWindow(QDialog):
|
|
|
1767
1867
|
if not mono_key and img.ndim == 2:
|
|
1768
1868
|
img = np.stack([img, img, img], axis=2)
|
|
1769
1869
|
|
|
1870
|
+
if self._should_stop():
|
|
1871
|
+
return
|
|
1872
|
+
|
|
1770
1873
|
# ─── 5) Normalize ───────────────────────────────────────
|
|
1771
1874
|
# for star-trail we want a visible, stretched version:
|
|
1772
1875
|
if img.ndim == 2:
|
|
@@ -1777,6 +1880,9 @@ class LiveStackWindow(QDialog):
|
|
|
1777
1880
|
target_median=0.3,
|
|
1778
1881
|
linked=False)
|
|
1779
1882
|
|
|
1883
|
+
if self._should_stop():
|
|
1884
|
+
return
|
|
1885
|
+
|
|
1780
1886
|
# ─── 6) Build max-value stack ───────────────────────────
|
|
1781
1887
|
if self.frame_count == 0:
|
|
1782
1888
|
self.current_stack = norm_color.copy()
|
|
@@ -1785,6 +1891,9 @@ class LiveStackWindow(QDialog):
|
|
|
1785
1891
|
self.current_stack = np.maximum(self.current_stack,
|
|
1786
1892
|
norm_color)
|
|
1787
1893
|
|
|
1894
|
+
if self._should_stop():
|
|
1895
|
+
return
|
|
1896
|
+
|
|
1788
1897
|
# ─── 7) Update counters and labels ──────────────────────
|
|
1789
1898
|
self.frame_count += 1
|
|
1790
1899
|
self.frame_count_label.setText(f"Frames: {self.frame_count}")
|
|
@@ -1809,6 +1918,18 @@ class LiveStackWindow(QDialog):
|
|
|
1809
1918
|
self.update_preview(self.current_stack)
|
|
1810
1919
|
QApplication.processEvents()
|
|
1811
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()
|
|
1812
1933
|
|
|
1813
1934
|
|
|
1814
1935
|
def update_preview(self, array: np.ndarray):
|