setiastrosuitepro 1.6.4__py3-none-any.whl → 1.6.10__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.
- setiastro/images/abeicon.svg +16 -0
- setiastro/images/acv_icon.png +0 -0
- setiastro/images/cosmic.svg +40 -0
- setiastro/images/cosmicsat.svg +24 -0
- setiastro/images/first_quarter.png +0 -0
- setiastro/images/full_moon.png +0 -0
- setiastro/images/graxpert.svg +19 -0
- setiastro/images/last_quarter.png +0 -0
- setiastro/images/linearfit.svg +32 -0
- setiastro/images/new_moon.png +0 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/images/waning_crescent_1.png +0 -0
- setiastro/images/waning_crescent_2.png +0 -0
- setiastro/images/waning_crescent_3.png +0 -0
- setiastro/images/waning_crescent_4.png +0 -0
- setiastro/images/waning_crescent_5.png +0 -0
- setiastro/images/waning_gibbous_1.png +0 -0
- setiastro/images/waning_gibbous_2.png +0 -0
- setiastro/images/waning_gibbous_3.png +0 -0
- setiastro/images/waning_gibbous_4.png +0 -0
- setiastro/images/waning_gibbous_5.png +0 -0
- setiastro/images/waxing_crescent_1.png +0 -0
- setiastro/images/waxing_crescent_2.png +0 -0
- setiastro/images/waxing_crescent_3.png +0 -0
- setiastro/images/waxing_crescent_4.png +0 -0
- setiastro/images/waxing_crescent_5.png +0 -0
- setiastro/images/waxing_gibbous_1.png +0 -0
- setiastro/images/waxing_gibbous_2.png +0 -0
- setiastro/images/waxing_gibbous_3.png +0 -0
- setiastro/images/waxing_gibbous_4.png +0 -0
- setiastro/images/waxing_gibbous_5.png +0 -0
- setiastro/qml/ResourceMonitor.qml +84 -82
- setiastro/saspro/__main__.py +19 -0
- 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 +35 -7
- setiastro/saspro/blemish_blaster.py +4 -1
- setiastro/saspro/blink_comparator_pro.py +74 -24
- setiastro/saspro/clahe.py +4 -1
- setiastro/saspro/continuum_subtract.py +4 -1
- setiastro/saspro/convo.py +4 -1
- setiastro/saspro/cosmicclarity.py +129 -18
- setiastro/saspro/crop_dialog_pro.py +123 -7
- setiastro/saspro/curve_editor_pro.py +109 -42
- setiastro/saspro/doc_manager.py +67 -4
- 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 +393 -204
- setiastro/saspro/gui/mixins/menu_mixin.py +1 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +356 -12
- 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/stretch.py +531 -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 +43 -0
- setiastro/saspro/legacy/xisf.py +240 -98
- setiastro/saspro/live_stacking.py +180 -79
- setiastro/saspro/luminancerecombine.py +228 -27
- setiastro/saspro/mask_creation.py +174 -15
- setiastro/saspro/mfdeconv.py +113 -35
- setiastro/saspro/mfdeconvcudnn.py +119 -70
- setiastro/saspro/mfdeconvsport.py +112 -35
- setiastro/saspro/morphology.py +4 -0
- setiastro/saspro/multiscale_decomp.py +51 -12
- setiastro/saspro/numba_utils.py +72 -2
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/ops/script_editor.py +5 -2
- setiastro/saspro/ops/scripts.py +3 -0
- setiastro/saspro/perfect_palette_picker.py +37 -3
- setiastro/saspro/plate_solver.py +84 -49
- setiastro/saspro/psf_viewer.py +119 -37
- setiastro/saspro/resources.py +67 -0
- setiastro/saspro/rgbalign.py +4 -0
- setiastro/saspro/selective_color.py +4 -1
- setiastro/saspro/sfcc.py +60 -2
- setiastro/saspro/shortcuts.py +142 -23
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1017 -400
- setiastro/saspro/star_alignment.py +4 -1
- setiastro/saspro/star_spikes.py +4 -0
- setiastro/saspro/star_stretch.py +38 -3
- setiastro/saspro/stat_stretch.py +702 -128
- setiastro/saspro/subwindow.py +786 -360
- setiastro/saspro/supernovaasteroidhunter.py +1 -1
- setiastro/saspro/wavescale_hdr.py +4 -1
- setiastro/saspro/wavescalede.py +4 -1
- setiastro/saspro/whitebalance.py +60 -12
- setiastro/saspro/widgets/common_utilities.py +28 -21
- setiastro/saspro/widgets/resource_monitor.py +109 -59
- setiastro/saspro/widgets/spinboxes.py +10 -13
- setiastro/saspro/wimi.py +27 -656
- setiastro/saspro/wims.py +13 -3
- setiastro/saspro/xisf.py +101 -11
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/METADATA +2 -1
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/RECORD +112 -80
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/license.txt +0 -0
setiastro/saspro/doc_manager.py
CHANGED
|
@@ -1555,6 +1555,9 @@ def debug_dump_metadata_print(meta: dict, context: str = ""):
|
|
|
1555
1555
|
|
|
1556
1556
|
print("===== END METADATA DUMP ({}) =====".format(context))
|
|
1557
1557
|
|
|
1558
|
+
import time
|
|
1559
|
+
_DEBUG_DND_DUP = False
|
|
1560
|
+
|
|
1558
1561
|
class DocManager(QObject):
|
|
1559
1562
|
documentAdded = pyqtSignal(object) # ImageDocument
|
|
1560
1563
|
documentRemoved = pyqtSignal(object) # ImageDocument
|
|
@@ -2266,6 +2269,46 @@ class DocManager(QObject):
|
|
|
2266
2269
|
if hasattr(doc, "changed"):
|
|
2267
2270
|
doc.changed.emit()
|
|
2268
2271
|
|
|
2272
|
+
def _current_view_title_for_doc(self, source_doc: ImageDocument) -> str | None:
|
|
2273
|
+
"""
|
|
2274
|
+
If the active MDI subwindow is showing 'source_doc' (or its parent/base),
|
|
2275
|
+
return the current view's title (windowTitle), otherwise None.
|
|
2276
|
+
"""
|
|
2277
|
+
sw = self._active_subwindow()
|
|
2278
|
+
if sw is None:
|
|
2279
|
+
return None
|
|
2280
|
+
|
|
2281
|
+
try:
|
|
2282
|
+
w = sw.widget()
|
|
2283
|
+
except Exception:
|
|
2284
|
+
w = None
|
|
2285
|
+
|
|
2286
|
+
# Resolve what doc the active view corresponds to (base doc)
|
|
2287
|
+
try:
|
|
2288
|
+
base = (
|
|
2289
|
+
getattr(w, "base_document", None)
|
|
2290
|
+
or getattr(w, "_base_document", None)
|
|
2291
|
+
or getattr(w, "document", None)
|
|
2292
|
+
or getattr(sw, "document", None)
|
|
2293
|
+
)
|
|
2294
|
+
parent = getattr(base, "_parent_doc", None)
|
|
2295
|
+
if isinstance(parent, ImageDocument):
|
|
2296
|
+
base = parent
|
|
2297
|
+
except Exception:
|
|
2298
|
+
base = None
|
|
2299
|
+
|
|
2300
|
+
if base is not source_doc:
|
|
2301
|
+
return None
|
|
2302
|
+
|
|
2303
|
+
# Prefer the actual subwindow title (includes [View N], etc.)
|
|
2304
|
+
try:
|
|
2305
|
+
title = sw.windowTitle()
|
|
2306
|
+
title = title.strip() if isinstance(title, str) else ""
|
|
2307
|
+
return title or None
|
|
2308
|
+
except Exception:
|
|
2309
|
+
return None
|
|
2310
|
+
|
|
2311
|
+
|
|
2269
2312
|
def duplicate_document(self, source_doc: ImageDocument, new_name: str | None = None) -> ImageDocument:
|
|
2270
2313
|
# DEBUG: log the source doc WCS before we touch anything
|
|
2271
2314
|
if _DEBUG_WCS:
|
|
@@ -2275,19 +2318,33 @@ class DocManager(QObject):
|
|
|
2275
2318
|
name = "<src>"
|
|
2276
2319
|
|
|
2277
2320
|
_debug_log_wcs_context(" source.metadata", getattr(source_doc, "metadata", {}))
|
|
2278
|
-
|
|
2321
|
+
if _DEBUG_DND_DUP:
|
|
2322
|
+
try:
|
|
2323
|
+
src_dn = source_doc.display_name() if hasattr(source_doc, "display_name") else None
|
|
2324
|
+
except Exception:
|
|
2325
|
+
src_dn = None
|
|
2326
|
+
print("\n[DNDDBG:DUPLICATE_DOCUMENT]")
|
|
2327
|
+
print(" source_doc:", source_doc, "id:", id(source_doc), "uid:", getattr(source_doc,"uid",None))
|
|
2328
|
+
print(" source_doc.display_name():", src_dn)
|
|
2329
|
+
print(" new_name arg:", new_name)
|
|
2279
2330
|
# COPY-ON-WRITE: Share the source image instead of copying immediately.
|
|
2280
2331
|
# The duplicate's apply_edit will copy when it first modifies the image.
|
|
2281
2332
|
# This saves memory when duplicates are created but not modified.
|
|
2282
2333
|
img_ref = source_doc.image # Shared reference, no copy
|
|
2283
2334
|
|
|
2284
2335
|
meta = dict(source_doc.metadata or {})
|
|
2285
|
-
|
|
2336
|
+
|
|
2337
|
+
# ✅ Use CURRENT VIEW NAME if this doc is what's active; else fall back to doc display_name()
|
|
2338
|
+
base = self._current_view_title_for_doc(source_doc) or source_doc.display_name()
|
|
2339
|
+
|
|
2286
2340
|
dup_title = (new_name or f"{base}_duplicate")
|
|
2341
|
+
|
|
2287
2342
|
# 🚫 strip any lingering emojis / link markers
|
|
2288
2343
|
dup_title = dup_title.replace("🔗", "").strip()
|
|
2289
|
-
meta["display_name"] = dup_title
|
|
2290
2344
|
|
|
2345
|
+
meta["display_name"] = dup_title
|
|
2346
|
+
if _DEBUG_DND_DUP:
|
|
2347
|
+
print(" dup_title computed:", dup_title)
|
|
2291
2348
|
# Remove anything that makes the view look "linked/preview"
|
|
2292
2349
|
imi = dict(meta.get("image_meta") or {})
|
|
2293
2350
|
for k in ("readonly", "view_kind", "derived_from", "layer", "layer_index", "linked"):
|
|
@@ -2311,7 +2368,13 @@ class DocManager(QObject):
|
|
|
2311
2368
|
# Mark this duplicate as sharing image data with source
|
|
2312
2369
|
dup._cow_source = source_doc
|
|
2313
2370
|
self._register_doc(dup)
|
|
2314
|
-
|
|
2371
|
+
if _DEBUG_DND_DUP:
|
|
2372
|
+
try:
|
|
2373
|
+
dn = dup.display_name() if hasattr(dup, "display_name") else None
|
|
2374
|
+
except Exception:
|
|
2375
|
+
dn = None
|
|
2376
|
+
print(" dup.metadata.display_name:", (dup.metadata or {}).get("display_name"))
|
|
2377
|
+
print(" dup.display_name():", dn)
|
|
2315
2378
|
# DEBUG: log the duplicate doc WCS
|
|
2316
2379
|
if _DEBUG_WCS:
|
|
2317
2380
|
try:
|
|
@@ -27,6 +27,7 @@ from astropy.coordinates import SkyCoord
|
|
|
27
27
|
import astropy.units as u
|
|
28
28
|
from astropy.wcs import WCS
|
|
29
29
|
from astropy.timeseries import LombScargle, BoxLeastSquares
|
|
30
|
+
import re
|
|
30
31
|
|
|
31
32
|
from astroquery.simbad import Simbad
|
|
32
33
|
from astroquery.vizier import Vizier
|
|
@@ -152,6 +153,41 @@ def _estimate_scale_arcsec_per_pix(h: fits.Header):
|
|
|
152
153
|
|
|
153
154
|
return None
|
|
154
155
|
|
|
156
|
+
_TZ_RE = re.compile(r'([+-])(\d{2})(\d{2})$') # -0700 -> -07:00
|
|
157
|
+
|
|
158
|
+
def _fix_iso_tz(s: str) -> str:
|
|
159
|
+
s = s.strip()
|
|
160
|
+
m = _TZ_RE.search(s)
|
|
161
|
+
if m:
|
|
162
|
+
s = s[:m.start()] + f"{m.group(1)}{m.group(2)}:{m.group(3)}"
|
|
163
|
+
return s
|
|
164
|
+
|
|
165
|
+
def _parse_obs_time_from_header(hdr) -> Time | None:
|
|
166
|
+
# hdr can be fits.Header or your dict-ish header
|
|
167
|
+
def _get(key):
|
|
168
|
+
try:
|
|
169
|
+
return hdr.get(key)
|
|
170
|
+
except Exception:
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
# 1) Prefer UT-OBS if present (already “UTC-ish”)
|
|
174
|
+
for key in ("UT-OBS", "DATE-OBS", "DATE-END"):
|
|
175
|
+
v = _get(key)
|
|
176
|
+
if isinstance(v, str) and v.strip():
|
|
177
|
+
try:
|
|
178
|
+
return Time(_fix_iso_tz(v), format="isot", scale="utc")
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
# 2) MJD-OBS is super reliable
|
|
183
|
+
v = _get("MJD-OBS")
|
|
184
|
+
if v is not None:
|
|
185
|
+
try:
|
|
186
|
+
return Time(float(v), format="mjd", scale="utc")
|
|
187
|
+
except Exception:
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
return None
|
|
155
191
|
|
|
156
192
|
def _estimate_fov_deg(img_shape, scale_arcsec):
|
|
157
193
|
"""Rough FOV (deg) from image size and scale (max of X/Y)."""
|
|
@@ -646,9 +682,12 @@ class ExoPlanetWindow(QDialog):
|
|
|
646
682
|
self.measure_btn.setVisible(is_raw)
|
|
647
683
|
|
|
648
684
|
def load_and_measure_subs(self):
|
|
685
|
+
before = len(getattr(self, "image_paths", []))
|
|
649
686
|
self.load_aligned_subs()
|
|
687
|
+
after = len(getattr(self, "image_paths", []))
|
|
688
|
+
if after == 0 or after == before and not self._cached_images:
|
|
689
|
+
return
|
|
650
690
|
self.detect_stars()
|
|
651
|
-
|
|
652
691
|
# --------------- I/O + Calibration ----------------
|
|
653
692
|
|
|
654
693
|
def load_raw_subs(self):
|
|
@@ -682,13 +721,38 @@ class ExoPlanetWindow(QDialog):
|
|
|
682
721
|
ds = hdr0.get('DATE-OBS')
|
|
683
722
|
except:
|
|
684
723
|
ds = None
|
|
724
|
+
|
|
725
|
+
# Use robust header time parsing (UT-OBS -> DATE-OBS -> MJD-OBS fallback)
|
|
685
726
|
t = None
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
727
|
+
try:
|
|
728
|
+
if ext == ".xisf":
|
|
729
|
+
# Build a tiny dict-like header for the helper
|
|
730
|
+
hdr_like = {}
|
|
731
|
+
try:
|
|
732
|
+
xisf = XISF(p)
|
|
733
|
+
img_meta = xisf.get_images_metadata()[0]
|
|
734
|
+
kw = img_meta.get("FITSKeywords", {}) or {}
|
|
735
|
+
# XISF FITSKeywords layout: key -> [ {value: ...}, ... ]
|
|
736
|
+
for k in ("UT-OBS", "DATE-OBS", "DATE-END", "MJD-OBS"):
|
|
737
|
+
if k in kw and kw[k]:
|
|
738
|
+
hdr_like[k] = kw[k][0].get("value")
|
|
739
|
+
except Exception:
|
|
740
|
+
hdr_like = {}
|
|
741
|
+
t = _parse_obs_time_from_header(hdr_like)
|
|
742
|
+
|
|
743
|
+
elif ext in (".fit", ".fits", ".fz"):
|
|
744
|
+
hdr0, _ = get_valid_header(p)
|
|
745
|
+
t = _parse_obs_time_from_header(hdr0)
|
|
746
|
+
|
|
747
|
+
else:
|
|
748
|
+
# TIFF etc. may not have FITS-like time headers; leave None
|
|
749
|
+
t = None
|
|
750
|
+
|
|
751
|
+
except Exception as e:
|
|
752
|
+
print(f"[DEBUG] Failed to parse obs time for {p}: {e}")
|
|
753
|
+
|
|
691
754
|
datelist.append((p, t))
|
|
755
|
+
|
|
692
756
|
self.progress_bar.setValue(i)
|
|
693
757
|
QApplication.processEvents()
|
|
694
758
|
|
|
@@ -768,24 +832,28 @@ class ExoPlanetWindow(QDialog):
|
|
|
768
832
|
self.progress_bar.setValue(i)
|
|
769
833
|
QApplication.processEvents()
|
|
770
834
|
|
|
771
|
-
|
|
772
|
-
for _, t in datelist
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
ma_strs = np.ma.MaskedArray(iso_strs, mask=mask_arr)
|
|
778
|
-
self.times = Time(ma_strs, format='isot', scale='utc', out_subfmt='date')
|
|
835
|
+
# Keep full timestamps (DO NOT truncate to date-only)
|
|
836
|
+
tlist = [t for _, t in datelist if t is not None]
|
|
837
|
+
if tlist:
|
|
838
|
+
self.times = Time(tlist) # already utc scale from helper
|
|
839
|
+
else:
|
|
840
|
+
self.times = None
|
|
779
841
|
|
|
780
842
|
self.progress_bar.setVisible(False)
|
|
781
843
|
loaded = sum(1 for im in self._cached_images if im is not None)
|
|
782
844
|
self.status_label.setText(f"Loaded {loaded}/{len(sorted_paths)} raw frames")
|
|
783
845
|
|
|
784
|
-
def load_aligned_subs(self):
|
|
846
|
+
def load_aligned_subs(self) -> bool:
|
|
785
847
|
settings = QSettings()
|
|
786
848
|
start_dir = settings.value("ExoPlanet/lastAlignedFolder", os.path.expanduser("~"), type=str)
|
|
787
|
-
paths, _ = QFileDialog.getOpenFileNames(
|
|
788
|
-
|
|
849
|
+
paths, _ = QFileDialog.getOpenFileNames(
|
|
850
|
+
self, "Select Aligned Frames", start_dir,
|
|
851
|
+
"FITS or TIFF (*.fit *.fits *.tif *.tiff *.xisf)"
|
|
852
|
+
)
|
|
853
|
+
if not paths:
|
|
854
|
+
self.status_label.setText("Load canceled.")
|
|
855
|
+
return False
|
|
856
|
+
|
|
789
857
|
settings.setValue("ExoPlanet/lastAlignedFolder", os.path.dirname(paths[0]))
|
|
790
858
|
|
|
791
859
|
self.status_label.setText("Reading metadata from aligned frames…")
|
|
@@ -809,10 +877,35 @@ class ExoPlanetWindow(QDialog):
|
|
|
809
877
|
hdr0, _ = get_valid_header(p)
|
|
810
878
|
ds = hdr0.get('DATE-OBS')
|
|
811
879
|
except: ds = None
|
|
880
|
+
# Use robust header time parsing (UT-OBS -> DATE-OBS -> MJD-OBS fallback)
|
|
812
881
|
t = None
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
882
|
+
try:
|
|
883
|
+
if ext == ".xisf":
|
|
884
|
+
# Build a tiny dict-like header for the helper
|
|
885
|
+
hdr_like = {}
|
|
886
|
+
try:
|
|
887
|
+
xisf = XISF(p)
|
|
888
|
+
img_meta = xisf.get_images_metadata()[0]
|
|
889
|
+
kw = img_meta.get("FITSKeywords", {}) or {}
|
|
890
|
+
# XISF FITSKeywords layout: key -> [ {value: ...}, ... ]
|
|
891
|
+
for k in ("UT-OBS", "DATE-OBS", "DATE-END", "MJD-OBS"):
|
|
892
|
+
if k in kw and kw[k]:
|
|
893
|
+
hdr_like[k] = kw[k][0].get("value")
|
|
894
|
+
except Exception:
|
|
895
|
+
hdr_like = {}
|
|
896
|
+
t = _parse_obs_time_from_header(hdr_like)
|
|
897
|
+
|
|
898
|
+
elif ext in (".fit", ".fits", ".fz"):
|
|
899
|
+
hdr0, _ = get_valid_header(p)
|
|
900
|
+
t = _parse_obs_time_from_header(hdr0)
|
|
901
|
+
|
|
902
|
+
else:
|
|
903
|
+
# TIFF etc. may not have FITS-like time headers; leave None
|
|
904
|
+
t = None
|
|
905
|
+
|
|
906
|
+
except Exception as e:
|
|
907
|
+
print(f"[DEBUG] Failed to parse obs time for {p}: {e}")
|
|
908
|
+
|
|
816
909
|
datelist.append((p, t))
|
|
817
910
|
self.progress_bar.setValue(i)
|
|
818
911
|
QApplication.processEvents()
|
|
@@ -889,18 +982,17 @@ class ExoPlanetWindow(QDialog):
|
|
|
889
982
|
self.progress_bar.setValue(i)
|
|
890
983
|
QApplication.processEvents()
|
|
891
984
|
|
|
892
|
-
|
|
893
|
-
for _, t in datelist
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
ma_strs = np.ma.MaskedArray(iso_strs, mask=mask_arr)
|
|
899
|
-
self.times = Time(ma_strs, format='isot', scale='utc', out_subfmt='date')
|
|
985
|
+
# Keep full timestamps (DO NOT truncate to date-only)
|
|
986
|
+
tlist = [t for _, t in datelist if t is not None]
|
|
987
|
+
if tlist:
|
|
988
|
+
self.times = Time(tlist) # already utc scale from helper
|
|
989
|
+
else:
|
|
990
|
+
self.times = None
|
|
900
991
|
|
|
901
992
|
self.progress_bar.setVisible(False)
|
|
902
993
|
loaded = sum(1 for im in self._cached_images if im is not None)
|
|
903
994
|
self.status_label.setText(f"Loaded {loaded}/{len(sorted_paths)} aligned frames")
|
|
995
|
+
return loaded > 0
|
|
904
996
|
|
|
905
997
|
def load_masters(self):
|
|
906
998
|
settings = QSettings()
|