setiastrosuitepro 1.6.4__py3-none-any.whl → 1.6.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of setiastrosuitepro might be problematic. Click here for more details.
- setiastro/images/abeicon.svg +16 -0
- setiastro/images/acv_icon.png +0 -0
- setiastro/images/colorwheel.svg +97 -0
- setiastro/images/cosmic.svg +40 -0
- setiastro/images/cosmicsat.svg +24 -0
- setiastro/images/first_quarter.png +0 -0
- setiastro/images/full_moon.png +0 -0
- setiastro/images/graxpert.svg +19 -0
- setiastro/images/last_quarter.png +0 -0
- setiastro/images/linearfit.svg +32 -0
- setiastro/images/new_moon.png +0 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/images/waning_crescent_1.png +0 -0
- setiastro/images/waning_crescent_2.png +0 -0
- setiastro/images/waning_crescent_3.png +0 -0
- setiastro/images/waning_crescent_4.png +0 -0
- setiastro/images/waning_crescent_5.png +0 -0
- setiastro/images/waning_gibbous_1.png +0 -0
- setiastro/images/waning_gibbous_2.png +0 -0
- setiastro/images/waning_gibbous_3.png +0 -0
- setiastro/images/waning_gibbous_4.png +0 -0
- setiastro/images/waning_gibbous_5.png +0 -0
- setiastro/images/waxing_crescent_1.png +0 -0
- setiastro/images/waxing_crescent_2.png +0 -0
- setiastro/images/waxing_crescent_3.png +0 -0
- setiastro/images/waxing_crescent_4.png +0 -0
- setiastro/images/waxing_crescent_5.png +0 -0
- setiastro/images/waxing_gibbous_1.png +0 -0
- setiastro/images/waxing_gibbous_2.png +0 -0
- setiastro/images/waxing_gibbous_3.png +0 -0
- setiastro/images/waxing_gibbous_4.png +0 -0
- setiastro/images/waxing_gibbous_5.png +0 -0
- setiastro/qml/ResourceMonitor.qml +84 -82
- setiastro/saspro/__main__.py +20 -1
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/abe.py +37 -4
- setiastro/saspro/aberration_ai.py +237 -21
- setiastro/saspro/acv_exporter.py +379 -0
- setiastro/saspro/add_stars.py +33 -6
- setiastro/saspro/backgroundneutral.py +108 -40
- 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 +13 -7
- 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 +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 +429 -228
- setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
- setiastro/saspro/gui/mixins/menu_mixin.py +27 -1
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +384 -18
- setiastro/saspro/gui/mixins/update_mixin.py +138 -36
- setiastro/saspro/gui/mixins/view_mixin.py +42 -0
- setiastro/saspro/halobgon.py +4 -0
- setiastro/saspro/histogram.py +5 -1
- setiastro/saspro/image_combine.py +4 -0
- setiastro/saspro/image_peeker_pro.py +4 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
- setiastro/saspro/imageops/stretch.py +582 -62
- setiastro/saspro/isophote.py +4 -0
- setiastro/saspro/layers.py +13 -9
- setiastro/saspro/layers_dock.py +183 -3
- setiastro/saspro/legacy/image_manager.py +154 -20
- setiastro/saspro/legacy/numba_utils.py +67 -47
- setiastro/saspro/legacy/xisf.py +240 -98
- setiastro/saspro/live_stacking.py +180 -79
- setiastro/saspro/luminancerecombine.py +228 -27
- setiastro/saspro/mask_creation.py +174 -15
- setiastro/saspro/mfdeconv.py +113 -35
- setiastro/saspro/mfdeconvcudnn.py +119 -70
- setiastro/saspro/mfdeconvsport.py +112 -35
- setiastro/saspro/morphology.py +4 -0
- setiastro/saspro/multiscale_decomp.py +51 -12
- 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/resources.py +67 -0
- setiastro/saspro/rgbalign.py +4 -0
- setiastro/saspro/selective_color.py +4 -1
- setiastro/saspro/sfcc.py +364 -152
- setiastro/saspro/shortcuts.py +160 -29
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1331 -484
- 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 +743 -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 +84 -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.12.dist-info}/METADATA +2 -1
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/RECORD +115 -82
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/license.txt +0 -0
|
@@ -521,32 +521,6 @@ class CurveEditor(QGraphicsView):
|
|
|
521
521
|
if ln is not None:
|
|
522
522
|
ln.setVisible(False)
|
|
523
523
|
|
|
524
|
-
def _scene_to_norm_points(self, pts_scene: list[tuple[float,float]]) -> list[tuple[float,float]]:
|
|
525
|
-
"""(x:[0..360], y:[0..360] down) → (x,y in [0..1] up). Ensures endpoints present & strictly increasing x."""
|
|
526
|
-
out = []
|
|
527
|
-
lastx = -1e9
|
|
528
|
-
for (x, y) in sorted(pts_scene, key=lambda t: t[0]):
|
|
529
|
-
x = float(np.clip(x, 0.0, 360.0))
|
|
530
|
-
y = float(np.clip(y, 0.0, 360.0))
|
|
531
|
-
# strictly increasing X
|
|
532
|
-
if x <= lastx:
|
|
533
|
-
x = lastx + 1e-3
|
|
534
|
-
lastx = x
|
|
535
|
-
out.append((x / 360.0, 1.0 - (y / 360.0)))
|
|
536
|
-
# ensure endpoints
|
|
537
|
-
if not any(abs(px - 0.0) < 1e-6 for px, _ in out): out.insert(0, (0.0, 0.0))
|
|
538
|
-
if not any(abs(px - 1.0) < 1e-6 for px, _ in out): out.append((1.0, 1.0))
|
|
539
|
-
# clamp
|
|
540
|
-
return [(float(np.clip(x,0,1)), float(np.clip(y,0,1))) for (x,y) in out]
|
|
541
|
-
|
|
542
|
-
def _collect_points_norm_from_editor(self) -> list[tuple[float,float]]:
|
|
543
|
-
"""Take endpoints+handles from editor => normalized points."""
|
|
544
|
-
pts_scene = []
|
|
545
|
-
for p in (self.editor.end_points + self.editor.control_points):
|
|
546
|
-
pos = p.scenePos()
|
|
547
|
-
pts_scene.append((float(pos.x()), float(pos.y())))
|
|
548
|
-
return self._scene_to_norm_points(pts_scene)
|
|
549
|
-
|
|
550
524
|
|
|
551
525
|
def redistributeHandlesByPivot(self, u: float):
|
|
552
526
|
"""
|
|
@@ -1022,10 +996,18 @@ class CurvesDialogPro(QDialog):
|
|
|
1022
996
|
self._main = parent
|
|
1023
997
|
self.doc = document
|
|
1024
998
|
|
|
1025
|
-
|
|
999
|
+
self._follow_conn = False
|
|
1026
1000
|
if hasattr(self._main, "currentDocumentChanged"):
|
|
1027
|
-
|
|
1028
|
-
|
|
1001
|
+
try:
|
|
1002
|
+
self._main.currentDocumentChanged.connect(self._on_active_doc_changed)
|
|
1003
|
+
self._follow_conn = True
|
|
1004
|
+
except Exception:
|
|
1005
|
+
self._follow_conn = False
|
|
1006
|
+
try:
|
|
1007
|
+
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
1008
|
+
except Exception:
|
|
1009
|
+
pass # older PyQt6 versions
|
|
1010
|
+
self.finished.connect(self._cleanup_connections)
|
|
1029
1011
|
self._preview_img = None # downsampled float01
|
|
1030
1012
|
self._full_img = None # full-res float01
|
|
1031
1013
|
self._pix = None
|
|
@@ -1040,7 +1022,14 @@ class CurvesDialogPro(QDialog):
|
|
|
1040
1022
|
self._cdf = None
|
|
1041
1023
|
self._cdf_bins = 1024
|
|
1042
1024
|
self._cdf_total = 0
|
|
1043
|
-
|
|
1025
|
+
# Debounce: coalesce rapid curve edits into one rebuild
|
|
1026
|
+
self._curve_debounce_ms = 120 # tweak: 80–200ms feels good
|
|
1027
|
+
self._curve_debounce = QTimer(self)
|
|
1028
|
+
self._curve_debounce.setSingleShot(True)
|
|
1029
|
+
self._curve_debounce.timeout.connect(self._rebuild_preview_from_curve_debounced)
|
|
1030
|
+
|
|
1031
|
+
# Optional: generation counter so stale results can't “win”
|
|
1032
|
+
self._curve_gen = 0
|
|
1044
1033
|
self._clip_scale = 1.0 # preview→full multiplier
|
|
1045
1034
|
self._cdf_total_full = 0 # total pixels in full image (H*W)
|
|
1046
1035
|
self._cdf_total_preview = 0 # total pixels in preview (H*W)
|
|
@@ -1212,18 +1201,34 @@ class CurvesDialogPro(QDialog):
|
|
|
1212
1201
|
|
|
1213
1202
|
def _on_editor_curve_changed(self, _lut8=None):
|
|
1214
1203
|
"""
|
|
1215
|
-
Called on every editor redraw/drag. Persist
|
|
1216
|
-
|
|
1204
|
+
Called on every editor redraw/drag. Persist points and refresh overlays.
|
|
1205
|
+
Preview rebuild is DEBOUNCED to avoid spamming.
|
|
1217
1206
|
"""
|
|
1218
1207
|
try:
|
|
1219
1208
|
self._curves_store[self._current_mode_key] = self._editor_points_norm()
|
|
1220
1209
|
except Exception:
|
|
1221
1210
|
pass
|
|
1222
|
-
|
|
1211
|
+
|
|
1212
|
+
# cheap: overlay redraw is fine every move (or you can debounce this too)
|
|
1223
1213
|
self._refresh_overlays()
|
|
1224
|
-
# now build from *all* current curves (including the just-edited one)
|
|
1225
|
-
self._quick_preview()
|
|
1226
1214
|
|
|
1215
|
+
# expensive: debounce the preview rebuild
|
|
1216
|
+
self._curve_gen += 1
|
|
1217
|
+
self._curve_debounce.start(self._curve_debounce_ms)
|
|
1218
|
+
|
|
1219
|
+
def _rebuild_preview_from_curve_debounced(self):
|
|
1220
|
+
"""
|
|
1221
|
+
Runs after the user pauses dragging for _curve_debounce_ms.
|
|
1222
|
+
Only rebuild if we have images loaded.
|
|
1223
|
+
"""
|
|
1224
|
+
if self._preview_orig is None and self._preview_img is None:
|
|
1225
|
+
return
|
|
1226
|
+
# If your preview toggle is off, you may want to skip:
|
|
1227
|
+
if not getattr(self, "btn_preview", None) or not self.btn_preview.isChecked():
|
|
1228
|
+
return
|
|
1229
|
+
|
|
1230
|
+
# Do the real work (what you were doing before)
|
|
1231
|
+
self._quick_preview()
|
|
1227
1232
|
|
|
1228
1233
|
def _active_mode_key(self) -> str:
|
|
1229
1234
|
for b in self.mode_group.buttons():
|
|
@@ -1670,29 +1675,53 @@ class CurvesDialogPro(QDialog):
|
|
|
1670
1675
|
|
|
1671
1676
|
# 1) Put this helper inside CurvesDialogPro (near other helpers)
|
|
1672
1677
|
def _map_label_xy_to_image_ij(self, x: float, y: float):
|
|
1673
|
-
"""
|
|
1678
|
+
"""
|
|
1679
|
+
Map label-local coords (x,y) to _preview_img pixel (ix, iy).
|
|
1680
|
+
Correct even when the pixmap is centered inside a larger label.
|
|
1681
|
+
Returns None if cursor is outside the displayed pixmap area.
|
|
1682
|
+
"""
|
|
1674
1683
|
if self._pix is None:
|
|
1675
1684
|
return None
|
|
1685
|
+
|
|
1676
1686
|
pm_disp = self.label.pixmap()
|
|
1677
1687
|
if pm_disp is None or pm_disp.isNull():
|
|
1678
1688
|
return None
|
|
1679
1689
|
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
disp_w = pm_disp.width() # size of the *displayed* pixmap on the label
|
|
1690
|
+
# Displayed pixmap size (after zoom)
|
|
1691
|
+
disp_w = pm_disp.width()
|
|
1683
1692
|
disp_h = pm_disp.height()
|
|
1684
|
-
|
|
1693
|
+
|
|
1694
|
+
# Label may be bigger -> pixmap is centered with margins
|
|
1695
|
+
lbl_w = self.label.width()
|
|
1696
|
+
lbl_h = self.label.height()
|
|
1697
|
+
|
|
1698
|
+
off_x = max(0, (lbl_w - disp_w) // 2)
|
|
1699
|
+
off_y = max(0, (lbl_h - disp_h) // 2)
|
|
1700
|
+
|
|
1701
|
+
# Remove margins: label-local -> pixmap-local
|
|
1702
|
+
px = float(x) - float(off_x)
|
|
1703
|
+
py = float(y) - float(off_y)
|
|
1704
|
+
|
|
1705
|
+
if px < 0 or py < 0 or px >= disp_w or py >= disp_h:
|
|
1706
|
+
return None # outside actual image area
|
|
1707
|
+
|
|
1708
|
+
# Now convert displayed pixmap pixel -> source preview pixel
|
|
1709
|
+
src_w = self._pix.width()
|
|
1710
|
+
src_h = self._pix.height()
|
|
1711
|
+
if src_w <= 0 or src_h <= 0:
|
|
1685
1712
|
return None
|
|
1686
1713
|
|
|
1687
1714
|
sx = disp_w / float(src_w)
|
|
1688
1715
|
sy = disp_h / float(src_h)
|
|
1689
1716
|
|
|
1690
|
-
ix = int(
|
|
1691
|
-
iy = int(
|
|
1717
|
+
ix = int(px / sx)
|
|
1718
|
+
iy = int(py / sy)
|
|
1719
|
+
|
|
1692
1720
|
if ix < 0 or iy < 0 or ix >= src_w or iy >= src_h:
|
|
1693
1721
|
return None
|
|
1694
1722
|
return ix, iy
|
|
1695
1723
|
|
|
1724
|
+
|
|
1696
1725
|
def _scene_to_norm_points(self, pts_scene: list[tuple[float,float]]) -> list[tuple[float,float]]:
|
|
1697
1726
|
"""(x:[0..360], y:[0..360] down) → (x,y in [0..1] up). Ensures endpoints present & strictly increasing x."""
|
|
1698
1727
|
out = []
|
|
@@ -2178,6 +2207,44 @@ class CurvesDialogPro(QDialog):
|
|
|
2178
2207
|
|
|
2179
2208
|
return (m * out + (1.0 - m) * src).astype(np.float32, copy=False)
|
|
2180
2209
|
|
|
2210
|
+
def closeEvent(self, ev):
|
|
2211
|
+
self._cleanup_connections()
|
|
2212
|
+
super().closeEvent(ev)
|
|
2213
|
+
|
|
2214
|
+
def _cleanup_connections(self):
|
|
2215
|
+
# disconnect the "follow active doc" hook
|
|
2216
|
+
try:
|
|
2217
|
+
if self._follow_conn and hasattr(self._main, "currentDocumentChanged"):
|
|
2218
|
+
self._main.currentDocumentChanged.disconnect(self._on_active_doc_changed)
|
|
2219
|
+
except Exception:
|
|
2220
|
+
pass
|
|
2221
|
+
self._follow_conn = False
|
|
2222
|
+
|
|
2223
|
+
# stop/kill any running worker thread(s)
|
|
2224
|
+
try:
|
|
2225
|
+
thr = getattr(self, "_thr", None)
|
|
2226
|
+
if thr is not None:
|
|
2227
|
+
try:
|
|
2228
|
+
thr.requestInterruption()
|
|
2229
|
+
except Exception:
|
|
2230
|
+
pass
|
|
2231
|
+
try:
|
|
2232
|
+
thr.quit()
|
|
2233
|
+
except Exception:
|
|
2234
|
+
pass
|
|
2235
|
+
try:
|
|
2236
|
+
thr.wait(250)
|
|
2237
|
+
except Exception:
|
|
2238
|
+
pass
|
|
2239
|
+
except Exception:
|
|
2240
|
+
pass
|
|
2241
|
+
|
|
2242
|
+
# optional: drop refs that can keep things alive
|
|
2243
|
+
try:
|
|
2244
|
+
self._thr = None
|
|
2245
|
+
except Exception:
|
|
2246
|
+
pass
|
|
2247
|
+
|
|
2181
2248
|
|
|
2182
2249
|
# zoom/pan
|
|
2183
2250
|
def _apply_zoom(self):
|
setiastro/saspro/doc_manager.py
CHANGED
|
@@ -310,18 +310,133 @@ class ImageDocument(QObject):
|
|
|
310
310
|
|
|
311
311
|
def close(self):
|
|
312
312
|
"""
|
|
313
|
-
|
|
313
|
+
Free all resources held by this document:
|
|
314
|
+
- delete swap states for undo/redo
|
|
315
|
+
- clear undo/redo stacks
|
|
316
|
+
- drop in-memory image array and any in-memory history
|
|
317
|
+
- clear heavy metadata (headers/WCS)
|
|
318
|
+
- clear any cached previews/pixmaps
|
|
319
|
+
- disconnect signals to help break reference cycles
|
|
314
320
|
"""
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
#
|
|
322
|
-
|
|
323
|
-
sm
|
|
324
|
-
|
|
321
|
+
# --- 0) Stop emitting while we tear down (best-effort) --------------
|
|
322
|
+
try:
|
|
323
|
+
self.blockSignals(True)
|
|
324
|
+
except Exception:
|
|
325
|
+
pass
|
|
326
|
+
|
|
327
|
+
# --- 1) Swap cleanup (your existing logic) --------------------------
|
|
328
|
+
try:
|
|
329
|
+
sm = get_swap_manager()
|
|
330
|
+
except Exception:
|
|
331
|
+
sm = None
|
|
332
|
+
|
|
333
|
+
# Undo stack
|
|
334
|
+
try:
|
|
335
|
+
for item in list(getattr(self, "_undo", [])):
|
|
336
|
+
try:
|
|
337
|
+
swap_id = item[0] # (swap_id, ..., ...)
|
|
338
|
+
except Exception:
|
|
339
|
+
swap_id = None
|
|
340
|
+
if sm is not None and swap_id is not None:
|
|
341
|
+
try:
|
|
342
|
+
sm.delete_state(swap_id)
|
|
343
|
+
except Exception:
|
|
344
|
+
pass
|
|
345
|
+
getattr(self, "_undo", []).clear()
|
|
346
|
+
except Exception:
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
# Redo stack
|
|
350
|
+
try:
|
|
351
|
+
for item in list(getattr(self, "_redo", [])):
|
|
352
|
+
try:
|
|
353
|
+
swap_id = item[0]
|
|
354
|
+
except Exception:
|
|
355
|
+
swap_id = None
|
|
356
|
+
if sm is not None and swap_id is not None:
|
|
357
|
+
try:
|
|
358
|
+
sm.delete_state(swap_id)
|
|
359
|
+
except Exception:
|
|
360
|
+
pass
|
|
361
|
+
getattr(self, "_redo", []).clear()
|
|
362
|
+
except Exception:
|
|
363
|
+
pass
|
|
364
|
+
|
|
365
|
+
# ROI preview stacks if you have them (your code uses _pundo/_predo on ROI docs)
|
|
366
|
+
for attr in ("_pundo", "_predo"):
|
|
367
|
+
try:
|
|
368
|
+
lst = getattr(self, attr, None)
|
|
369
|
+
if isinstance(lst, list):
|
|
370
|
+
# If these also store swap states, delete them too (safe even if not)
|
|
371
|
+
if sm is not None:
|
|
372
|
+
for item in list(lst):
|
|
373
|
+
try:
|
|
374
|
+
swap_id = item[0]
|
|
375
|
+
except Exception:
|
|
376
|
+
swap_id = None
|
|
377
|
+
if swap_id is not None:
|
|
378
|
+
try:
|
|
379
|
+
sm.delete_state(swap_id)
|
|
380
|
+
except Exception:
|
|
381
|
+
pass
|
|
382
|
+
lst.clear()
|
|
383
|
+
except Exception:
|
|
384
|
+
pass
|
|
385
|
+
|
|
386
|
+
# --- 2) Drop the big in-memory image --------------------------------
|
|
387
|
+
# This is what actually frees the 2–10GB allocations (assuming no other refs).
|
|
388
|
+
try:
|
|
389
|
+
self.image = None
|
|
390
|
+
except Exception:
|
|
391
|
+
pass
|
|
392
|
+
|
|
393
|
+
# --- 3) Clear metadata that can keep large objects alive -------------
|
|
394
|
+
# fits.Header/WCS objects aren't huge like the image, but they can keep references
|
|
395
|
+
# and add up; also helps break cycles.
|
|
396
|
+
try:
|
|
397
|
+
md = getattr(self, "metadata", None)
|
|
398
|
+
if isinstance(md, dict):
|
|
399
|
+
for k in ("wcs", "original_header", "fits_header", "wcs_header", "header"):
|
|
400
|
+
md.pop(k, None)
|
|
401
|
+
# If you keep derived/cached headers anywhere:
|
|
402
|
+
for k in ("_header_snapshot", "_wcs_snapshot", "roi_wcs_header"):
|
|
403
|
+
md.pop(k, None)
|
|
404
|
+
except Exception:
|
|
405
|
+
pass
|
|
406
|
+
|
|
407
|
+
# --- 4) Clear any preview/pixmap/qimage caches -----------------------
|
|
408
|
+
# Adjust these attr names to match what your view/doc uses.
|
|
409
|
+
for attr in ("_qimage_cache", "_pixmap_cache", "_preview_cache", "_render_cache"):
|
|
410
|
+
try:
|
|
411
|
+
v = getattr(self, attr, None)
|
|
412
|
+
if isinstance(v, dict):
|
|
413
|
+
v.clear()
|
|
414
|
+
setattr(self, attr, None)
|
|
415
|
+
except Exception:
|
|
416
|
+
pass
|
|
417
|
+
|
|
418
|
+
# --- 5) Disconnect signals (helps Qt reference cycles) ---------------
|
|
419
|
+
# If you connect doc.changed to closures (like ROI docs do), this helps.
|
|
420
|
+
try:
|
|
421
|
+
self.changed.disconnect()
|
|
422
|
+
except Exception:
|
|
423
|
+
pass
|
|
424
|
+
|
|
425
|
+
# If you have other signals, disconnect them similarly:
|
|
426
|
+
for sig_name in ("imageChanged", "metadataChanged"):
|
|
427
|
+
try:
|
|
428
|
+
sig = getattr(self, sig_name, None)
|
|
429
|
+
if sig is not None:
|
|
430
|
+
sig.disconnect()
|
|
431
|
+
except Exception:
|
|
432
|
+
pass
|
|
433
|
+
|
|
434
|
+
# --- 6) Allow signals again (optional) -------------------------------
|
|
435
|
+
try:
|
|
436
|
+
self.blockSignals(False)
|
|
437
|
+
except Exception:
|
|
438
|
+
pass
|
|
439
|
+
|
|
325
440
|
|
|
326
441
|
def __del__(self):
|
|
327
442
|
# Fallback cleanup if close() wasn't called (though explicit close is better)
|
|
@@ -1555,6 +1670,9 @@ def debug_dump_metadata_print(meta: dict, context: str = ""):
|
|
|
1555
1670
|
|
|
1556
1671
|
print("===== END METADATA DUMP ({}) =====".format(context))
|
|
1557
1672
|
|
|
1673
|
+
import time
|
|
1674
|
+
_DEBUG_DND_DUP = False
|
|
1675
|
+
|
|
1558
1676
|
class DocManager(QObject):
|
|
1559
1677
|
documentAdded = pyqtSignal(object) # ImageDocument
|
|
1560
1678
|
documentRemoved = pyqtSignal(object) # ImageDocument
|
|
@@ -2266,6 +2384,46 @@ class DocManager(QObject):
|
|
|
2266
2384
|
if hasattr(doc, "changed"):
|
|
2267
2385
|
doc.changed.emit()
|
|
2268
2386
|
|
|
2387
|
+
def _current_view_title_for_doc(self, source_doc: ImageDocument) -> str | None:
|
|
2388
|
+
"""
|
|
2389
|
+
If the active MDI subwindow is showing 'source_doc' (or its parent/base),
|
|
2390
|
+
return the current view's title (windowTitle), otherwise None.
|
|
2391
|
+
"""
|
|
2392
|
+
sw = self._active_subwindow()
|
|
2393
|
+
if sw is None:
|
|
2394
|
+
return None
|
|
2395
|
+
|
|
2396
|
+
try:
|
|
2397
|
+
w = sw.widget()
|
|
2398
|
+
except Exception:
|
|
2399
|
+
w = None
|
|
2400
|
+
|
|
2401
|
+
# Resolve what doc the active view corresponds to (base doc)
|
|
2402
|
+
try:
|
|
2403
|
+
base = (
|
|
2404
|
+
getattr(w, "base_document", None)
|
|
2405
|
+
or getattr(w, "_base_document", None)
|
|
2406
|
+
or getattr(w, "document", None)
|
|
2407
|
+
or getattr(sw, "document", None)
|
|
2408
|
+
)
|
|
2409
|
+
parent = getattr(base, "_parent_doc", None)
|
|
2410
|
+
if isinstance(parent, ImageDocument):
|
|
2411
|
+
base = parent
|
|
2412
|
+
except Exception:
|
|
2413
|
+
base = None
|
|
2414
|
+
|
|
2415
|
+
if base is not source_doc:
|
|
2416
|
+
return None
|
|
2417
|
+
|
|
2418
|
+
# Prefer the actual subwindow title (includes [View N], etc.)
|
|
2419
|
+
try:
|
|
2420
|
+
title = sw.windowTitle()
|
|
2421
|
+
title = title.strip() if isinstance(title, str) else ""
|
|
2422
|
+
return title or None
|
|
2423
|
+
except Exception:
|
|
2424
|
+
return None
|
|
2425
|
+
|
|
2426
|
+
|
|
2269
2427
|
def duplicate_document(self, source_doc: ImageDocument, new_name: str | None = None) -> ImageDocument:
|
|
2270
2428
|
# DEBUG: log the source doc WCS before we touch anything
|
|
2271
2429
|
if _DEBUG_WCS:
|
|
@@ -2275,19 +2433,33 @@ class DocManager(QObject):
|
|
|
2275
2433
|
name = "<src>"
|
|
2276
2434
|
|
|
2277
2435
|
_debug_log_wcs_context(" source.metadata", getattr(source_doc, "metadata", {}))
|
|
2278
|
-
|
|
2436
|
+
if _DEBUG_DND_DUP:
|
|
2437
|
+
try:
|
|
2438
|
+
src_dn = source_doc.display_name() if hasattr(source_doc, "display_name") else None
|
|
2439
|
+
except Exception:
|
|
2440
|
+
src_dn = None
|
|
2441
|
+
print("\n[DNDDBG:DUPLICATE_DOCUMENT]")
|
|
2442
|
+
print(" source_doc:", source_doc, "id:", id(source_doc), "uid:", getattr(source_doc,"uid",None))
|
|
2443
|
+
print(" source_doc.display_name():", src_dn)
|
|
2444
|
+
print(" new_name arg:", new_name)
|
|
2279
2445
|
# COPY-ON-WRITE: Share the source image instead of copying immediately.
|
|
2280
2446
|
# The duplicate's apply_edit will copy when it first modifies the image.
|
|
2281
2447
|
# This saves memory when duplicates are created but not modified.
|
|
2282
2448
|
img_ref = source_doc.image # Shared reference, no copy
|
|
2283
2449
|
|
|
2284
2450
|
meta = dict(source_doc.metadata or {})
|
|
2285
|
-
|
|
2451
|
+
|
|
2452
|
+
# ✅ Use CURRENT VIEW NAME if this doc is what's active; else fall back to doc display_name()
|
|
2453
|
+
base = self._current_view_title_for_doc(source_doc) or source_doc.display_name()
|
|
2454
|
+
|
|
2286
2455
|
dup_title = (new_name or f"{base}_duplicate")
|
|
2456
|
+
|
|
2287
2457
|
# 🚫 strip any lingering emojis / link markers
|
|
2288
2458
|
dup_title = dup_title.replace("🔗", "").strip()
|
|
2289
|
-
meta["display_name"] = dup_title
|
|
2290
2459
|
|
|
2460
|
+
meta["display_name"] = dup_title
|
|
2461
|
+
if _DEBUG_DND_DUP:
|
|
2462
|
+
print(" dup_title computed:", dup_title)
|
|
2291
2463
|
# Remove anything that makes the view look "linked/preview"
|
|
2292
2464
|
imi = dict(meta.get("image_meta") or {})
|
|
2293
2465
|
for k in ("readonly", "view_kind", "derived_from", "layer", "layer_index", "linked"):
|
|
@@ -2311,7 +2483,13 @@ class DocManager(QObject):
|
|
|
2311
2483
|
# Mark this duplicate as sharing image data with source
|
|
2312
2484
|
dup._cow_source = source_doc
|
|
2313
2485
|
self._register_doc(dup)
|
|
2314
|
-
|
|
2486
|
+
if _DEBUG_DND_DUP:
|
|
2487
|
+
try:
|
|
2488
|
+
dn = dup.display_name() if hasattr(dup, "display_name") else None
|
|
2489
|
+
except Exception:
|
|
2490
|
+
dn = None
|
|
2491
|
+
print(" dup.metadata.display_name:", (dup.metadata or {}).get("display_name"))
|
|
2492
|
+
print(" dup.display_name():", dn)
|
|
2315
2493
|
# DEBUG: log the duplicate doc WCS
|
|
2316
2494
|
if _DEBUG_WCS:
|
|
2317
2495
|
try:
|
|
@@ -2370,7 +2548,58 @@ class DocManager(QObject):
|
|
|
2370
2548
|
def create_document(self, image, metadata: dict | None = None, name: str | None = None) -> ImageDocument:
|
|
2371
2549
|
return self.open_array(image, metadata=metadata, title=name)
|
|
2372
2550
|
|
|
2551
|
+
def _drop_all_roi_for_parent(self, parent_doc):
|
|
2552
|
+
dead = [k for k in list(self._roi_doc_cache.keys()) if k[0] == id(parent_doc)]
|
|
2553
|
+
for k in dead:
|
|
2554
|
+
roi_doc = self._roi_doc_cache.pop(k, None)
|
|
2555
|
+
if roi_doc is not None:
|
|
2556
|
+
try:
|
|
2557
|
+
roi_doc.close() # you’ll implement doc.close() to release arrays
|
|
2558
|
+
except Exception:
|
|
2559
|
+
pass
|
|
2560
|
+
|
|
2561
|
+
def _hard_memory_cleanup(self):
|
|
2562
|
+
# 1) Drop Qt pixmap cache (can hold big chunks)
|
|
2563
|
+
try:
|
|
2564
|
+
from PyQt6.QtGui import QPixmapCache
|
|
2565
|
+
QPixmapCache.clear()
|
|
2566
|
+
except Exception:
|
|
2567
|
+
pass
|
|
2568
|
+
|
|
2569
|
+
# 2) Let pending deleteLater() actually execute
|
|
2570
|
+
try:
|
|
2571
|
+
from PyQt6.QtWidgets import QApplication
|
|
2572
|
+
QApplication.processEvents()
|
|
2573
|
+
except Exception:
|
|
2574
|
+
pass
|
|
2575
|
+
|
|
2576
|
+
# 3) Force Python GC to collect cycles (common with Qt signal/closure cycles)
|
|
2577
|
+
try:
|
|
2578
|
+
import gc
|
|
2579
|
+
gc.collect()
|
|
2580
|
+
except Exception:
|
|
2581
|
+
pass
|
|
2582
|
+
|
|
2583
|
+
# 4) Optional: Linux heap trim (only helps on Linux/glibc)
|
|
2584
|
+
try:
|
|
2585
|
+
import sys
|
|
2586
|
+
if sys.platform.startswith("linux"):
|
|
2587
|
+
import ctypes
|
|
2588
|
+
libc = ctypes.CDLL("libc.so.6")
|
|
2589
|
+
libc.malloc_trim(0)
|
|
2590
|
+
except Exception:
|
|
2591
|
+
pass
|
|
2592
|
+
|
|
2373
2593
|
def close_document(self, doc):
|
|
2594
|
+
# If ROI wrapper, close parent; if parent, purge ROI cache
|
|
2595
|
+
try:
|
|
2596
|
+
parent = getattr(doc, "_parent_doc", None)
|
|
2597
|
+
if parent is not None:
|
|
2598
|
+
doc = parent
|
|
2599
|
+
except Exception:
|
|
2600
|
+
pass
|
|
2601
|
+
|
|
2602
|
+
self._drop_all_roi_for_parent(doc)
|
|
2374
2603
|
if doc in self._docs:
|
|
2375
2604
|
self._docs.remove(doc)
|
|
2376
2605
|
try:
|
|
@@ -2387,6 +2616,7 @@ class DocManager(QObject):
|
|
|
2387
2616
|
print(f"[DocManager] Failed to close document {doc}: {e}")
|
|
2388
2617
|
|
|
2389
2618
|
self.documentRemoved.emit(doc)
|
|
2619
|
+
self._hard_memory_cleanup()
|
|
2390
2620
|
|
|
2391
2621
|
# --- Active-document helpers (NEW) ---------------------------------
|
|
2392
2622
|
def all_documents(self):
|