setiastrosuitepro 1.8.1.post2__py3-none-any.whl → 1.8.3__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.

Files changed (40) hide show
  1. setiastro/images/finderchart.png +0 -0
  2. setiastro/images/magnitude.png +0 -0
  3. setiastro/saspro/__main__.py +29 -38
  4. setiastro/saspro/_generated/build_info.py +2 -2
  5. setiastro/saspro/abe.py +1 -1
  6. setiastro/saspro/backgroundneutral.py +54 -16
  7. setiastro/saspro/blink_comparator_pro.py +3 -1
  8. setiastro/saspro/bright_stars.py +305 -0
  9. setiastro/saspro/continuum_subtract.py +2 -1
  10. setiastro/saspro/cosmicclarity_preset.py +2 -1
  11. setiastro/saspro/doc_manager.py +8 -0
  12. setiastro/saspro/exoplanet_detector.py +22 -17
  13. setiastro/saspro/finder_chart.py +1650 -0
  14. setiastro/saspro/gui/main_window.py +131 -17
  15. setiastro/saspro/gui/mixins/header_mixin.py +40 -15
  16. setiastro/saspro/gui/mixins/menu_mixin.py +3 -0
  17. setiastro/saspro/gui/mixins/toolbar_mixin.py +16 -1
  18. setiastro/saspro/imageops/stretch.py +1 -1
  19. setiastro/saspro/legacy/image_manager.py +18 -4
  20. setiastro/saspro/legacy/xisf.py +3 -3
  21. setiastro/saspro/magnitude_tool.py +1724 -0
  22. setiastro/saspro/main_helpers.py +18 -0
  23. setiastro/saspro/memory_utils.py +18 -14
  24. setiastro/saspro/remove_stars.py +13 -30
  25. setiastro/saspro/resources.py +177 -161
  26. setiastro/saspro/runtime_torch.py +71 -10
  27. setiastro/saspro/sfcc.py +86 -77
  28. setiastro/saspro/stacking_suite.py +4 -3
  29. setiastro/saspro/star_alignment.py +4 -2
  30. setiastro/saspro/texture_clarity.py +1 -1
  31. setiastro/saspro/torch_rejection.py +59 -28
  32. setiastro/saspro/widgets/image_utils.py +12 -4
  33. setiastro/saspro/wimi.py +2 -1
  34. setiastro/saspro/xisf.py +3 -3
  35. {setiastrosuitepro-1.8.1.post2.dist-info → setiastrosuitepro-1.8.3.dist-info}/METADATA +4 -4
  36. {setiastrosuitepro-1.8.1.post2.dist-info → setiastrosuitepro-1.8.3.dist-info}/RECORD +40 -35
  37. {setiastrosuitepro-1.8.1.post2.dist-info → setiastrosuitepro-1.8.3.dist-info}/WHEEL +0 -0
  38. {setiastrosuitepro-1.8.1.post2.dist-info → setiastrosuitepro-1.8.3.dist-info}/entry_points.txt +0 -0
  39. {setiastrosuitepro-1.8.1.post2.dist-info → setiastrosuitepro-1.8.3.dist-info}/licenses/LICENSE +0 -0
  40. {setiastrosuitepro-1.8.1.post2.dist-info → setiastrosuitepro-1.8.3.dist-info}/licenses/license.txt +0 -0
@@ -33,8 +33,8 @@ from urllib.parse import quote, quote_plus
33
33
  # ============================================================================
34
34
  import numpy as np
35
35
  import matplotlib
36
- from tifffile import imwrite
37
- from setiastro.saspro.xisf import XISF
36
+ # tifffile and XISF imports removed (unused in this file)
37
+
38
38
 
39
39
  # ============================================================================
40
40
  # Bootstrap Configuration (must run early)
@@ -93,18 +93,8 @@ from setiastro.saspro.widgets.common_utilities import (
93
93
  )
94
94
 
95
95
 
96
- # Reproject for WCS-based alignment
97
- try:
98
- from reproject import reproject_interp
99
- except ImportError:
100
- reproject_interp = None # fallback if not installed
96
+ # Reproject and OpenCV imports removed (unused or available via lazy_imports)
101
97
 
102
- # OpenCV for transform estimation & warping
103
- try:
104
- import cv2
105
- OPENCV_AVAILABLE = True
106
- except ImportError:
107
- OPENCV_AVAILABLE = False
108
98
 
109
99
 
110
100
 
@@ -196,7 +186,7 @@ from setiastro.saspro.resources import (
196
186
  colorwheel_path, font_path, csv_icon_path, spinner_path, wims_path, narrowbandnormalization_path,
197
187
  wimi_path, linearfit_path, debayer_path, aberration_path, acv_icon_path,
198
188
  functionbundles_path, viewbundles_path, selectivecolor_path, rgbalign_path, planetarystacker_path,
199
- background_path, script_icon_path, planetprojection_path,clonestampicon_path,
189
+ background_path, script_icon_path, planetprojection_path,clonestampicon_path, finderchart_path,magnitude_path,
200
190
  )
201
191
 
202
192
  import faulthandler
@@ -3195,6 +3185,45 @@ class AstroSuiteProMainWindow(
3195
3185
  pass
3196
3186
  self.SFCC_window.show()
3197
3187
 
3188
+ def _open_magnitude_tool(self):
3189
+ import os
3190
+ from PyQt6.QtGui import QIcon
3191
+ from PyQt6.QtWidgets import QMessageBox
3192
+
3193
+ # Keep same window-singleton behavior as SFCC
3194
+ if getattr(self, "MAG_window", None) and self.MAG_window.isVisible():
3195
+ self.MAG_window.raise_()
3196
+ self.MAG_window.activateWindow()
3197
+ return
3198
+
3199
+ # ensure we have a DocManager (mirror SFCC pattern)
3200
+ from setiastro.saspro.doc_manager import DocManager
3201
+ if not hasattr(self, "doc_manager") or self.doc_manager is None:
3202
+ self.doc_manager = DocManager(image_manager=getattr(self, "image_manager", None), parent=self)
3203
+
3204
+ # import tool
3205
+ from setiastro.saspro.magnitude_tool import MagnitudeToolDialog
3206
+
3207
+ self.MAG_window = MagnitudeToolDialog(
3208
+ doc_manager=self.doc_manager,
3209
+ parent=self
3210
+ )
3211
+
3212
+ # optional icon
3213
+ try:
3214
+ self.MAG_window.setWindowIcon(QIcon(magnitude_path))
3215
+ except Exception:
3216
+ pass
3217
+
3218
+ # cleanup
3219
+ try:
3220
+ self.MAG_window.destroyed.connect(lambda _=None: setattr(self, "MAG_window", None))
3221
+ except Exception:
3222
+ pass
3223
+
3224
+ self.MAG_window.show()
3225
+
3226
+
3198
3227
  def show_convo_deconvo(self, doc=None):
3199
3228
  # Reuse existing dialog if it's already open
3200
3229
  sw = self.mdi.activeSubWindow()
@@ -3506,13 +3535,53 @@ class AstroSuiteProMainWindow(
3506
3535
  base = base_title or (getattr(doc, "display_name", lambda: None)() or "RGB")
3507
3536
 
3508
3537
  def _open(arr, suffix):
3509
- meta = {
3538
+ from astropy.io import fits
3539
+
3540
+ # ---- 1) start from source metadata (preserve headers/WCS) ----
3541
+ src_meta = getattr(doc, "metadata", {}) or {}
3542
+ meta = dict(src_meta) # shallow copy of top-level dict
3543
+
3544
+ # Preserve header objects safely
3545
+ fits_hdr = src_meta.get("fits_header")
3546
+ wcs_hdr = src_meta.get("wcs_header")
3547
+ orig_hdr = src_meta.get("original_header")
3548
+
3549
+ # Keep them if they are astropy Headers; otherwise fall back gracefully
3550
+ meta["fits_header"] = fits_hdr.copy() if isinstance(fits_hdr, fits.Header) else fits_hdr
3551
+ meta["wcs_header"] = wcs_hdr.copy() if isinstance(wcs_hdr, fits.Header) else wcs_hdr
3552
+ meta["original_header"] = orig_hdr.copy() if isinstance(orig_hdr, fits.Header) else orig_hdr
3553
+
3554
+ # Preserve WCS object + flags (ok if None)
3555
+ if "wcs" in src_meta:
3556
+ meta["wcs"] = src_meta.get("wcs")
3557
+ if "HasAstrometricSolution" in src_meta:
3558
+ meta["HasAstrometricSolution"] = src_meta.get("HasAstrometricSolution")
3559
+
3560
+ # Preserve image_meta mirror, but make it a copy so we don't mutate parent
3561
+ im = src_meta.get("image_meta")
3562
+ if isinstance(im, dict):
3563
+ meta["image_meta"] = dict(im)
3564
+
3565
+ # ---- 2) overwrite/extend with RGB-extract specific fields ----
3566
+ base = base_title or (getattr(doc, "display_name", lambda: None)() or "RGB")
3567
+ title = f"{base}_{suffix}"
3568
+ try:
3569
+ fh = meta.get("fits_header")
3570
+ if isinstance(fh, fits.Header):
3571
+ fh["NAXIS"] = 2
3572
+ if "NAXIS3" in fh:
3573
+ del fh["NAXIS3"]
3574
+ except Exception:
3575
+ pass
3576
+ meta.update({
3510
3577
  "source": "RGB Extract",
3511
3578
  "is_mono": True,
3512
3579
  "bit_depth": "32-bit floating point",
3513
3580
  "parent_title": base,
3514
- }
3515
- title = f"{base}_{suffix}"
3581
+ "channel": suffix, # handy for later
3582
+ })
3583
+
3584
+ # ---- 3) create doc ----
3516
3585
  try:
3517
3586
  if hasattr(dm, "open_array"):
3518
3587
  newdoc = dm.open_array(arr, metadata=meta, title=title)
@@ -3520,10 +3589,23 @@ class AstroSuiteProMainWindow(
3520
3589
  newdoc = dm.open_numpy(arr, metadata=meta, title=title)
3521
3590
  else:
3522
3591
  newdoc = dm.create_document(image=arr, metadata=meta, name=title)
3592
+
3593
+ # ---- 4) ensure WCS is internally consistent on the new doc ----
3594
+ # If source had a WCS solution, rebuild original_header+wcs using your canonical path.
3595
+ try:
3596
+ if meta.get("HasAstrometricSolution") or (meta.get("wcs_header") is not None):
3597
+ wcs_dict = self._extract_wcs_dict(doc) # from SOURCE doc
3598
+ if wcs_dict:
3599
+ self._apply_wcs_dict_to_doc(newdoc, dict(wcs_dict))
3600
+ except Exception:
3601
+ pass
3602
+
3523
3603
  self._spawn_subwindow_for(newdoc)
3604
+
3524
3605
  except Exception as ex:
3525
3606
  QMessageBox.critical(self, "RGB Extract", f"Failed to open '{title}':\n{ex}")
3526
3607
 
3608
+
3527
3609
  _open(r, "R")
3528
3610
  _open(g, "G")
3529
3611
  _open(b, "B")
@@ -4238,6 +4320,38 @@ class AstroSuiteProMainWindow(
4238
4320
  except Exception:
4239
4321
  pass
4240
4322
 
4323
+ def _doc_has_wcs(self, doc) -> bool:
4324
+ if doc is None:
4325
+ return False
4326
+ meta = getattr(doc, "metadata", None) or {}
4327
+ if meta.get("wcs") is not None:
4328
+ return True
4329
+
4330
+ hdr = meta.get("original_header") or meta.get("fits_header") or meta.get("header")
4331
+ if hdr is None:
4332
+ return False
4333
+
4334
+ try:
4335
+ keys = {str(k).upper() for k in hdr.keys()}
4336
+ except Exception:
4337
+ try:
4338
+ keys = {str(k).upper() for k in dict(hdr).keys()}
4339
+ except Exception:
4340
+ return False
4341
+
4342
+ return {"CTYPE1","CTYPE2","CRVAL1","CRVAL2"}.issubset(keys)
4343
+
4344
+
4345
+ def _open_finder_chart(self):
4346
+ doc = self._active_doc()
4347
+ if not self._doc_has_wcs(doc):
4348
+ QMessageBox.information(self, self.tr("Finder Chart"), self.tr("Active image has no astrometric solution (WCS). Plate solve first."))
4349
+ return
4350
+
4351
+ from setiastro.saspro.finder_chart import FinderChartDialog
4352
+ dlg = FinderChartDialog(doc=doc, settings=self.settings, parent=self)
4353
+ dlg.setWindowIcon(QIcon(finderchart_path))
4354
+ dlg.show()
4241
4355
 
4242
4356
  def _open_stellar_alignment(self):
4243
4357
  from setiastro.saspro.star_alignment import StellarAlignmentDialog
@@ -36,16 +36,31 @@ class HeaderMixin:
36
36
  }
37
37
 
38
38
  def _ensure_header_map(self, doc):
39
- """Ensure doc has a header dictionary in metadata, return it."""
39
+ """Ensure doc has an original_header container; preserve fits.Header if present."""
40
40
  meta = getattr(doc, "metadata", None)
41
41
  if meta is None:
42
42
  return None
43
+
43
44
  hdr = meta.get("original_header")
44
- if not isinstance(hdr, dict):
45
- hdr = {}
46
- meta["original_header"] = hdr
45
+
46
+ # Preserve astropy Header
47
+ try:
48
+ from astropy.io.fits import Header
49
+ if isinstance(hdr, Header):
50
+ return hdr
51
+ except Exception:
52
+ pass
53
+
54
+ # Preserve dict
55
+ if isinstance(hdr, dict):
56
+ return hdr
57
+
58
+ # If missing/unknown, create dict (but do NOT overwrite a valid Header above)
59
+ hdr = {}
60
+ meta["original_header"] = hdr
47
61
  return hdr
48
62
 
63
+
49
64
  def _coerce_wcs_numbers(self, d: dict) -> dict:
50
65
  """Convert common WCS/SIP values to int/float where appropriate."""
51
66
  numeric = {
@@ -72,23 +87,33 @@ class HeaderMixin:
72
87
  return out
73
88
 
74
89
  def _extract_wcs_dict(self, doc) -> dict:
75
- """Collect a complete WCS/SIP dict from the doc's header/meta."""
76
90
  if doc is None:
77
91
  return {}
78
- src = (getattr(doc, "metadata", {}) or {}).get("original_header")
79
92
 
80
- wcs = {}
81
- if src is None:
93
+ meta = getattr(doc, "metadata", {}) or {}
94
+
95
+ # 0) Preferred: astropy WCS object in metadata
96
+ w = meta.get("wcs", None)
97
+ try:
98
+ from astropy.wcs import WCS as _AstroWCS
99
+ if isinstance(w, _AstroWCS):
100
+ h = w.to_header(relax=True)
101
+ return self._coerce_wcs_numbers({k.upper(): h[k] for k in h.keys()})
102
+ except Exception:
82
103
  pass
83
- else:
104
+
105
+ src = meta.get("original_header")
106
+
107
+ wcs = {}
108
+ src = meta.get("original_header")
109
+ if src is not None:
84
110
  try:
85
- for k, v in dict(src).items():
111
+ # fits.Header has .keys()
112
+ items = src.items() if hasattr(src, "items") else dict(src).items()
113
+ for k, v in items:
86
114
  K = str(k).upper()
87
- if (K.startswith(("CRPIX", "CRVAL", "CDELT", "CD", "PC", "CROTA", "CTYPE", "CUNIT",
88
- "WCSAXES", "LONPOLE", "LATPOLE", "EQUINOX", "PV")) or
89
- K in {"RADECSYS", "RADESYS", "NAXIS1", "NAXIS2"} or
90
- K.startswith(("A_", "B_", "AP_", "BP_"))):
91
- wcs[K] = v
115
+ ...
116
+ wcs[K] = v
92
117
  except Exception:
93
118
  pass
94
119
 
@@ -181,6 +181,7 @@ class MenuMixin:
181
181
  m_tools.addAction(self.act_ppp)
182
182
 
183
183
  m_tools.addAction(self.act_selective_color)
184
+ m_tools.addAction(self.act_magnitude)
184
185
  m_tools.addSeparator()
185
186
  m_tools.addAction(self.act_view_bundles)
186
187
  m_tools.addAction(self.act_function_bundles)
@@ -204,6 +205,7 @@ class MenuMixin:
204
205
 
205
206
  m_star.addAction(self.act_astrospike)
206
207
  m_star.addAction(self.act_exo_detector)
208
+
207
209
  m_star.addAction(self.act_image_peeker)
208
210
  m_star.addAction(self.act_isophote)
209
211
  m_star.addAction(self.act_live_stacking)
@@ -233,6 +235,7 @@ class MenuMixin:
233
235
  m_wim = mb.addMenu(self.tr("&What's In My..."))
234
236
  m_wim.addAction(self.act_whats_in_my_sky)
235
237
  m_wim.addAction(self.act_wimi)
238
+ m_wim.addAction(self.act_finder_chart)
236
239
 
237
240
  m_scripts = mb.addMenu(self.tr("&Scripts"))
238
241
  self.menu_scripts = m_scripts
@@ -36,7 +36,7 @@ from setiastro.saspro.resources import (
36
36
  nbtorgb_path, freqsep_path, multiscale_decomp_path, contsub_path, halo_path, cosmic_path,
37
37
  satellite_path, imagecombine_path, wims_path, wimi_path, linearfit_path,
38
38
  debayer_path, aberration_path, functionbundles_path, viewbundles_path, planetarystacker_path,
39
- selectivecolor_path, rgbalign_path, planetprojection_path, clonestampicon_path,
39
+ selectivecolor_path, rgbalign_path, planetprojection_path, clonestampicon_path, finderchart_path,magnitude_path,
40
40
  )
41
41
 
42
42
  # Import shortcuts module
@@ -254,6 +254,7 @@ class ToolbarMixin:
254
254
  tb_tl.addAction(self.act_multiscale_decomp)
255
255
  tb_tl.addAction(self.act_contsub)
256
256
  tb_tl.addAction(self.act_image_combine)
257
+ tb_tl.addAction(self.act_magnitude)
257
258
 
258
259
  self._restore_toolbar_order(tb_tl, "Toolbar/Tools")
259
260
  try:
@@ -298,6 +299,7 @@ class ToolbarMixin:
298
299
  tb_star.addAction(self.act_planetary_stacker)
299
300
  tb_star.addAction(self.act_planet_projection)
300
301
  tb_star.addAction(self.act_plate_solve)
302
+
301
303
  tb_star.addAction(self.act_star_align)
302
304
  tb_star.addAction(self.act_star_register)
303
305
  tb_star.addAction(self.act_rgb_align)
@@ -332,6 +334,7 @@ class ToolbarMixin:
332
334
  tb_wim = DraggableToolBar(self.tr("What's In My..."), self)
333
335
  tb_wim.setObjectName("What's In My...")
334
336
  tb_wim.setSettingsKey("Toolbar/WhatsInMy")
337
+ tb_wim.addAction(self.act_finder_chart)
335
338
  self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_wim)
336
339
 
337
340
  tb_wim.addAction(self.act_whats_in_my_sky)
@@ -1147,6 +1150,11 @@ class ToolbarMixin:
1147
1150
  self.act_selective_color.setStatusTip(self.tr("Adjust specific hue ranges with CMY/RGB controls"))
1148
1151
  self.act_selective_color.triggered.connect(self._open_selective_color_tool)
1149
1152
 
1153
+ self.act_magnitude = QAction(QIcon(magnitude_path), self.tr("Magnitude / Surface Brightness..."), self)
1154
+ self.act_magnitude.setStatusTip(self.tr("Measure magnitude and mag/arcsec² from the active linear view"))
1155
+ self.act_magnitude.setIconVisibleInMenu(True)
1156
+ self.act_magnitude.triggered.connect(self._open_magnitude_tool)
1157
+
1150
1158
  # NEW: Frequency Separation
1151
1159
  self.act_freqsep = QAction(QIcon(freqsep_path), self.tr("Frequency Separation..."), self)
1152
1160
  self.act_freqsep.setStatusTip(self.tr("Split into LF/HF and enhance HF (scale, wavelet, denoise)"))
@@ -1245,6 +1253,11 @@ class ToolbarMixin:
1245
1253
  self.act_rgb_align.setStatusTip(self.tr("Align R and B channels to G using astroalign (affine/homography/poly)"))
1246
1254
  self.act_rgb_align.triggered.connect(self._open_rgb_align)
1247
1255
 
1256
+ self.act_finder_chart = QAction(QIcon(finderchart_path), self.tr("Finder Chart..."), self)
1257
+ self.act_finder_chart.setIconVisibleInMenu(True)
1258
+ self.act_finder_chart.setStatusTip(self.tr("Show a finder chart for the active plate-solved image"))
1259
+ self.act_finder_chart.triggered.connect(self._open_finder_chart)
1260
+
1248
1261
  self.act_whats_in_my_sky = QAction(QIcon(wims_path), self.tr("What's In My Sky..."), self)
1249
1262
  self.act_whats_in_my_sky.setIconVisibleInMenu(True)
1250
1263
  self.act_whats_in_my_sky.setStatusTip(self.tr("Plan targets by altitude, transit time, and lunar separation"))
@@ -1434,6 +1447,7 @@ class ToolbarMixin:
1434
1447
  reg("image_combine", self.act_image_combine)
1435
1448
  reg("psf_viewer", self.act_psf_viewer)
1436
1449
  reg("plate_solve", self.act_plate_solve)
1450
+ reg("magnitude_tool", self.act_magnitude)
1437
1451
  reg("star_align", self.act_star_align)
1438
1452
  reg("star_register", self.act_star_register)
1439
1453
  reg("mosaic_master", self.act_mosaic_master)
@@ -1446,6 +1460,7 @@ class ToolbarMixin:
1446
1460
  reg("astrospike", self.act_astrospike)
1447
1461
  reg("exo_detector", self.act_exo_detector)
1448
1462
  reg("isophote", self.act_isophote)
1463
+ reg("finder_chart", self.act_finder_chart)
1449
1464
  reg("rgb_align", self.act_rgb_align)
1450
1465
  reg("whats_in_my_sky", self.act_whats_in_my_sky)
1451
1466
  reg("whats_in_my_image", self.act_wimi)
@@ -1,4 +1,4 @@
1
- # imageops/stretch.py
1
+ # src/setiastro/saspro/imageops/stretch.py
2
2
  from __future__ import annotations
3
3
  import numpy as np
4
4
 
@@ -242,8 +242,16 @@ class ImageManager(QObject):
242
242
  def set_image(self, new_image, metadata, step_name=None):
243
243
  slot = self.current_slot
244
244
  if self._images[slot] is not None:
245
+ # OPTIMIZATION: If we are setting the EXACT SAME image object (e.g. metadata update),
246
+ # do not deep-copy the image data to undo stack.
247
+ # This saves massive memory when just renaming a slot or changing WCS.
248
+ if new_image is self._images[slot]:
249
+ stored_img = self._images[slot] # Shallow copy / reference
250
+ else:
251
+ stored_img = self._images[slot].copy() # Deep copy for safety
252
+
245
253
  self._undo_stacks[slot].append(
246
- (self._images[slot].copy(), self._metadata[slot].copy(), step_name or "Unnamed Step")
254
+ (stored_img, self._metadata[slot].copy(), step_name or "Unnamed Step")
247
255
  )
248
256
  self._redo_stacks[slot].clear()
249
257
  print(f"ImageManager: Previous image in slot {slot} pushed to undo stack.")
@@ -1526,8 +1534,12 @@ def load_image(filename, max_retries=3, wait_seconds=3, return_metadata: bool =
1526
1534
  # 1) PixInsight astrometric solution (fallback only)
1527
1535
  try:
1528
1536
  if not all(k in hdr for k in ("CRPIX1","CRPIX2","CRVAL1","CRVAL2")):
1529
- ref_img = props['PCL:AstrometricSolution:ReferenceImageCoordinates']['value']
1530
- ref_sky = props['PCL:AstrometricSolution:ReferenceCelestialCoordinates']['value']
1537
+ p_img = props['PCL:AstrometricSolution:ReferenceImageCoordinates']
1538
+ p_sky = props['PCL:AstrometricSolution:ReferenceCelestialCoordinates']
1539
+
1540
+ # Resolve lazy properties (decode base64/binary)
1541
+ ref_img = xisf.resolve_property(p_img)
1542
+ ref_sky = xisf.resolve_property(p_sky)
1531
1543
 
1532
1544
  # Some files store extra values; only first two are CRPIX/CRVAL
1533
1545
  im0, im1 = float(ref_img[0]), float(ref_img[1])
@@ -1547,7 +1559,9 @@ def load_image(filename, max_retries=3, wait_seconds=3, return_metadata: bool =
1547
1559
  # 2) CD matrix (fallback only)
1548
1560
  try:
1549
1561
  if not all(k in hdr for k in ("CD1_1","CD1_2","CD2_1","CD2_2")):
1550
- lin = np.asarray(props['PCL:AstrometricSolution:LinearTransformationMatrix']['value'], float)
1562
+ p_mat = props['PCL:AstrometricSolution:LinearTransformationMatrix']
1563
+ lin = np.asarray(xisf.resolve_property(p_mat), float)
1564
+
1551
1565
  hdr['CD1_1'], hdr['CD1_2'] = float(lin[0,0]), float(lin[0,1])
1552
1566
  hdr['CD2_1'], hdr['CD2_2'] = float(lin[1,0]), float(lin[1,1])
1553
1567
  _filled |= {'CD1_1','CD1_2','CD2_1','CD2_2'}
@@ -1072,7 +1072,7 @@ class XISF:
1072
1072
  }
1073
1073
  try:
1074
1074
  return _dtypes[s]
1075
- except:
1075
+ except KeyError:
1076
1076
  raise NotImplementedError(f"sampleFormat {s} not implemented")
1077
1077
 
1078
1078
  # Return XISF data type from numpy dtype
@@ -1087,7 +1087,7 @@ class XISF:
1087
1087
  }
1088
1088
  try:
1089
1089
  return _sampleFormats[str(dtype)]
1090
- except:
1090
+ except KeyError:
1091
1091
  raise NotImplementedError(f"sampleFormat for {dtype} not implemented")
1092
1092
 
1093
1093
  @staticmethod
@@ -1121,7 +1121,7 @@ class XISF:
1121
1121
  }
1122
1122
  try:
1123
1123
  return _dtypes[type_prefix]
1124
- except:
1124
+ except KeyError:
1125
1125
  raise NotImplementedError(f"data type {type_name} not implemented")
1126
1126
 
1127
1127
  # __/ Auxiliary functions for compression/shuffling \________