setiastrosuitepro 1.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of setiastrosuitepro might be problematic. Click here for more details.
- setiastro/__init__.py +2 -0
- setiastro/saspro/__init__.py +20 -0
- setiastro/saspro/__main__.py +784 -0
- setiastro/saspro/_generated/__init__.py +7 -0
- setiastro/saspro/_generated/build_info.py +2 -0
- setiastro/saspro/abe.py +1295 -0
- setiastro/saspro/abe_preset.py +196 -0
- setiastro/saspro/aberration_ai.py +694 -0
- setiastro/saspro/aberration_ai_preset.py +224 -0
- setiastro/saspro/accel_installer.py +218 -0
- setiastro/saspro/accel_workers.py +30 -0
- setiastro/saspro/add_stars.py +621 -0
- setiastro/saspro/astrobin_exporter.py +1007 -0
- setiastro/saspro/astrospike.py +153 -0
- setiastro/saspro/astrospike_python.py +1839 -0
- setiastro/saspro/autostretch.py +196 -0
- setiastro/saspro/backgroundneutral.py +560 -0
- setiastro/saspro/batch_convert.py +325 -0
- setiastro/saspro/batch_renamer.py +519 -0
- setiastro/saspro/blemish_blaster.py +488 -0
- setiastro/saspro/blink_comparator_pro.py +2923 -0
- setiastro/saspro/bundles.py +61 -0
- setiastro/saspro/bundles_dock.py +114 -0
- setiastro/saspro/cheat_sheet.py +168 -0
- setiastro/saspro/clahe.py +342 -0
- setiastro/saspro/comet_stacking.py +1377 -0
- setiastro/saspro/config.py +38 -0
- setiastro/saspro/config_bootstrap.py +40 -0
- setiastro/saspro/config_manager.py +316 -0
- setiastro/saspro/continuum_subtract.py +1617 -0
- setiastro/saspro/convo.py +1397 -0
- setiastro/saspro/convo_preset.py +414 -0
- setiastro/saspro/copyastro.py +187 -0
- setiastro/saspro/cosmicclarity.py +1564 -0
- setiastro/saspro/cosmicclarity_preset.py +407 -0
- setiastro/saspro/crop_dialog_pro.py +948 -0
- setiastro/saspro/crop_preset.py +189 -0
- setiastro/saspro/curve_editor_pro.py +2544 -0
- setiastro/saspro/curves_preset.py +375 -0
- setiastro/saspro/debayer.py +670 -0
- setiastro/saspro/debug_utils.py +29 -0
- setiastro/saspro/dnd_mime.py +35 -0
- setiastro/saspro/doc_manager.py +2634 -0
- setiastro/saspro/exoplanet_detector.py +2166 -0
- setiastro/saspro/file_utils.py +284 -0
- setiastro/saspro/fitsmodifier.py +744 -0
- setiastro/saspro/free_torch_memory.py +48 -0
- setiastro/saspro/frequency_separation.py +1343 -0
- setiastro/saspro/function_bundle.py +1594 -0
- setiastro/saspro/ghs_dialog_pro.py +660 -0
- setiastro/saspro/ghs_preset.py +284 -0
- setiastro/saspro/graxpert.py +634 -0
- setiastro/saspro/graxpert_preset.py +287 -0
- setiastro/saspro/gui/__init__.py +0 -0
- setiastro/saspro/gui/main_window.py +8494 -0
- setiastro/saspro/gui/mixins/__init__.py +33 -0
- setiastro/saspro/gui/mixins/dock_mixin.py +263 -0
- setiastro/saspro/gui/mixins/file_mixin.py +445 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
- setiastro/saspro/gui/mixins/header_mixin.py +441 -0
- setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
- setiastro/saspro/gui/mixins/menu_mixin.py +361 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +1324 -0
- setiastro/saspro/gui/mixins/update_mixin.py +309 -0
- setiastro/saspro/gui/mixins/view_mixin.py +435 -0
- setiastro/saspro/halobgon.py +462 -0
- setiastro/saspro/header_viewer.py +445 -0
- setiastro/saspro/headless_utils.py +88 -0
- setiastro/saspro/histogram.py +753 -0
- setiastro/saspro/history_explorer.py +939 -0
- setiastro/saspro/image_combine.py +414 -0
- setiastro/saspro/image_peeker_pro.py +1596 -0
- setiastro/saspro/imageops/__init__.py +37 -0
- setiastro/saspro/imageops/mdi_snap.py +292 -0
- setiastro/saspro/imageops/scnr.py +36 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
- setiastro/saspro/imageops/stretch.py +244 -0
- setiastro/saspro/isophote.py +1179 -0
- setiastro/saspro/layers.py +208 -0
- setiastro/saspro/layers_dock.py +714 -0
- setiastro/saspro/lazy_imports.py +193 -0
- setiastro/saspro/legacy/__init__.py +2 -0
- setiastro/saspro/legacy/image_manager.py +2226 -0
- setiastro/saspro/legacy/numba_utils.py +3659 -0
- setiastro/saspro/legacy/xisf.py +1071 -0
- setiastro/saspro/linear_fit.py +534 -0
- setiastro/saspro/live_stacking.py +1830 -0
- setiastro/saspro/log_bus.py +5 -0
- setiastro/saspro/logging_config.py +460 -0
- setiastro/saspro/luminancerecombine.py +309 -0
- setiastro/saspro/main_helpers.py +201 -0
- setiastro/saspro/mask_creation.py +928 -0
- setiastro/saspro/masks_core.py +56 -0
- setiastro/saspro/mdi_widgets.py +353 -0
- setiastro/saspro/memory_utils.py +666 -0
- setiastro/saspro/metadata_patcher.py +75 -0
- setiastro/saspro/mfdeconv.py +3826 -0
- setiastro/saspro/mfdeconv_earlystop.py +71 -0
- setiastro/saspro/mfdeconvcudnn.py +3263 -0
- setiastro/saspro/mfdeconvsport.py +2382 -0
- setiastro/saspro/minorbodycatalog.py +567 -0
- setiastro/saspro/morphology.py +382 -0
- setiastro/saspro/multiscale_decomp.py +1290 -0
- setiastro/saspro/nbtorgb_stars.py +531 -0
- setiastro/saspro/numba_utils.py +3044 -0
- setiastro/saspro/numba_warmup.py +141 -0
- setiastro/saspro/ops/__init__.py +9 -0
- setiastro/saspro/ops/command_help_dialog.py +623 -0
- setiastro/saspro/ops/command_runner.py +217 -0
- setiastro/saspro/ops/commands.py +1594 -0
- setiastro/saspro/ops/script_editor.py +1102 -0
- setiastro/saspro/ops/scripts.py +1413 -0
- setiastro/saspro/ops/settings.py +560 -0
- setiastro/saspro/parallel_utils.py +554 -0
- setiastro/saspro/pedestal.py +121 -0
- setiastro/saspro/perfect_palette_picker.py +1053 -0
- setiastro/saspro/pipeline.py +110 -0
- setiastro/saspro/pixelmath.py +1600 -0
- setiastro/saspro/plate_solver.py +2435 -0
- setiastro/saspro/project_io.py +797 -0
- setiastro/saspro/psf_utils.py +136 -0
- setiastro/saspro/psf_viewer.py +549 -0
- setiastro/saspro/pyi_rthook_astroquery.py +95 -0
- setiastro/saspro/remove_green.py +314 -0
- setiastro/saspro/remove_stars.py +1625 -0
- setiastro/saspro/remove_stars_preset.py +404 -0
- setiastro/saspro/resources.py +472 -0
- setiastro/saspro/rgb_combination.py +207 -0
- setiastro/saspro/rgb_extract.py +19 -0
- setiastro/saspro/rgbalign.py +723 -0
- setiastro/saspro/runtime_imports.py +7 -0
- setiastro/saspro/runtime_torch.py +754 -0
- setiastro/saspro/save_options.py +72 -0
- setiastro/saspro/selective_color.py +1552 -0
- setiastro/saspro/sfcc.py +1425 -0
- setiastro/saspro/shortcuts.py +2807 -0
- setiastro/saspro/signature_insert.py +1099 -0
- setiastro/saspro/stacking_suite.py +17712 -0
- setiastro/saspro/star_alignment.py +7420 -0
- setiastro/saspro/star_alignment_preset.py +329 -0
- setiastro/saspro/star_metrics.py +49 -0
- setiastro/saspro/star_spikes.py +681 -0
- setiastro/saspro/star_stretch.py +470 -0
- setiastro/saspro/stat_stretch.py +502 -0
- setiastro/saspro/status_log_dock.py +78 -0
- setiastro/saspro/subwindow.py +3267 -0
- setiastro/saspro/supernovaasteroidhunter.py +1712 -0
- setiastro/saspro/swap_manager.py +99 -0
- setiastro/saspro/torch_backend.py +89 -0
- setiastro/saspro/torch_rejection.py +434 -0
- setiastro/saspro/view_bundle.py +1555 -0
- setiastro/saspro/wavescale_hdr.py +624 -0
- setiastro/saspro/wavescale_hdr_preset.py +100 -0
- setiastro/saspro/wavescalede.py +657 -0
- setiastro/saspro/wavescalede_preset.py +228 -0
- setiastro/saspro/wcs_update.py +374 -0
- setiastro/saspro/whitebalance.py +456 -0
- setiastro/saspro/widgets/__init__.py +48 -0
- setiastro/saspro/widgets/common_utilities.py +305 -0
- setiastro/saspro/widgets/graphics_views.py +122 -0
- setiastro/saspro/widgets/image_utils.py +518 -0
- setiastro/saspro/widgets/preview_dialogs.py +280 -0
- setiastro/saspro/widgets/spinboxes.py +275 -0
- setiastro/saspro/widgets/themed_buttons.py +13 -0
- setiastro/saspro/widgets/wavelet_utils.py +299 -0
- setiastro/saspro/window_shelf.py +185 -0
- setiastro/saspro/xisf.py +1123 -0
- setiastrosuitepro-1.6.0.dist-info/METADATA +266 -0
- setiastrosuitepro-1.6.0.dist-info/RECORD +174 -0
- setiastrosuitepro-1.6.0.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.0.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.0.dist-info/licenses/LICENSE +674 -0
- setiastrosuitepro-1.6.0.dist-info/licenses/license.txt +2580 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# pro/morphology.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import numpy as np
|
|
4
|
+
import cv2
|
|
5
|
+
|
|
6
|
+
from PyQt6.QtCore import Qt, QTimer
|
|
7
|
+
from PyQt6.QtGui import QImage, QPixmap, QIcon
|
|
8
|
+
from PyQt6.QtWidgets import (
|
|
9
|
+
QDialog, QVBoxLayout, QHBoxLayout, QGroupBox, QGridLayout, QFormLayout,
|
|
10
|
+
QLabel, QPushButton, QSlider, QComboBox,
|
|
11
|
+
QGraphicsScene, QGraphicsPixmapItem, QMessageBox, QSpinBox, QDialogButtonBox
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Import centralized widgets
|
|
15
|
+
from setiastro.saspro.widgets.graphics_views import ZoomableGraphicsView
|
|
16
|
+
from setiastro.saspro.widgets.image_utils import (
|
|
17
|
+
extract_mask_resized as _get_active_mask_resized,
|
|
18
|
+
blend_with_mask as _blend_with_mask
|
|
19
|
+
)
|
|
20
|
+
from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ---------------- Core (unchanged) ----------------
|
|
24
|
+
def apply_morphology(image: np.ndarray, *, operation: str = "erosion",
|
|
25
|
+
kernel_size: int = 3, iterations: int = 1) -> np.ndarray:
|
|
26
|
+
# ... (existing body unchanged)
|
|
27
|
+
if image is None:
|
|
28
|
+
raise ValueError("image is None")
|
|
29
|
+
img = np.clip(np.asarray(image, dtype=np.float32), 0.0, 1.0)
|
|
30
|
+
was_hw1 = (img.ndim == 3 and img.shape[2] == 1)
|
|
31
|
+
if kernel_size % 2 == 0: kernel_size += 1
|
|
32
|
+
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size))
|
|
33
|
+
|
|
34
|
+
def _do(u8):
|
|
35
|
+
if operation == "erosion": return cv2.erode(u8, kernel, iterations=iterations)
|
|
36
|
+
if operation == "dilation": return cv2.dilate(u8, kernel, iterations=iterations)
|
|
37
|
+
if operation == "opening": return cv2.morphologyEx(u8, cv2.MORPH_OPEN, kernel, iterations=iterations)
|
|
38
|
+
if operation == "closing": return cv2.morphologyEx(u8, cv2.MORPH_CLOSE, kernel, iterations=iterations)
|
|
39
|
+
raise ValueError(f"Unsupported morphological operation: {operation}")
|
|
40
|
+
|
|
41
|
+
if img.ndim == 2 or was_hw1:
|
|
42
|
+
mono = (img.squeeze() * 255.0).astype(np.uint8)
|
|
43
|
+
out = _do(mono).astype(np.float32) / 255.0
|
|
44
|
+
out = np.clip(out, 0.0, 1.0)
|
|
45
|
+
return out[..., None] if was_hw1 else out
|
|
46
|
+
|
|
47
|
+
if img.ndim == 3 and img.shape[2] == 3:
|
|
48
|
+
u8 = (img * 255.0).astype(np.uint8)
|
|
49
|
+
ch = cv2.split(u8)
|
|
50
|
+
ch = [_do(c) for c in ch]
|
|
51
|
+
out = cv2.merge(ch).astype(np.float32) / 255.0
|
|
52
|
+
return np.clip(out, 0.0, 1.0)
|
|
53
|
+
|
|
54
|
+
raise ValueError("Input image must be mono (H,W)/(H,W,1) or RGB (H,W,3).")
|
|
55
|
+
|
|
56
|
+
def apply_morphology_to_doc(doc, preset: dict | None):
|
|
57
|
+
if doc is None or getattr(doc, "image", None) is None:
|
|
58
|
+
raise RuntimeError("Document has no image.")
|
|
59
|
+
|
|
60
|
+
img = np.asarray(doc.image)
|
|
61
|
+
op = (preset or {}).get("operation", "erosion")
|
|
62
|
+
ker = int((preset or {}).get("kernel", 3))
|
|
63
|
+
it = int((preset or {}).get("iterations", 1))
|
|
64
|
+
|
|
65
|
+
out = apply_morphology(img, operation=str(op), kernel_size=ker, iterations=it)
|
|
66
|
+
out = np.clip(out, 0.0, 1.0).astype(np.float32, copy=False)
|
|
67
|
+
|
|
68
|
+
# Blend with active mask if present
|
|
69
|
+
H, W = out.shape[:2]
|
|
70
|
+
m = _get_active_mask_resized(doc, H, W)
|
|
71
|
+
if m is not None:
|
|
72
|
+
base = np.asarray(doc.image, dtype=np.float32)
|
|
73
|
+
if base.dtype.kind in "ui":
|
|
74
|
+
maxv = float(np.iinfo(base.dtype).max)
|
|
75
|
+
base = base / max(1.0, maxv)
|
|
76
|
+
else:
|
|
77
|
+
base = np.clip(base, 0.0, 1.0)
|
|
78
|
+
out = _blend_with_mask(base, out, m).astype(np.float32, copy=False)
|
|
79
|
+
|
|
80
|
+
if hasattr(doc, "set_image"): doc.set_image(out, step_name="Morphology")
|
|
81
|
+
elif hasattr(doc, "apply_numpy"): doc.apply_numpy(out, step_name="Morphology")
|
|
82
|
+
else: doc.image = out
|
|
83
|
+
|
|
84
|
+
# Note: _get_active_mask_resized and _blend_with_mask imported from setiastro.saspro.widgets.image_utils
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ---------------- Dialog ----------------
|
|
88
|
+
class MorphologyDialogPro(QDialog):
|
|
89
|
+
OPS = ["Erosion", "Dilation", "Opening", "Closing"]
|
|
90
|
+
OP_MAP = {"Erosion":"erosion","Dilation":"dilation","Opening":"opening","Closing":"closing"}
|
|
91
|
+
|
|
92
|
+
def __init__(self, parent, doc, icon: QIcon | None = None, initial: dict | None = None):
|
|
93
|
+
super().__init__(parent)
|
|
94
|
+
self.setWindowTitle("Morphological Operations")
|
|
95
|
+
if icon:
|
|
96
|
+
try: self.setWindowIcon(icon)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
import logging
|
|
99
|
+
logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
|
|
100
|
+
|
|
101
|
+
self.doc = doc
|
|
102
|
+
self.orig = np.clip(np.asarray(doc.image, dtype=np.float32), 0.0, 1.0)
|
|
103
|
+
|
|
104
|
+
disp = self.orig
|
|
105
|
+
if disp.ndim == 2: disp = disp[..., None].repeat(3, axis=2)
|
|
106
|
+
elif disp.ndim == 3 and disp.shape[2] == 1: disp = disp.repeat(3, axis=2)
|
|
107
|
+
self._disp_base = disp
|
|
108
|
+
|
|
109
|
+
v = QVBoxLayout(self)
|
|
110
|
+
|
|
111
|
+
# ---- Params (unchanged) ----
|
|
112
|
+
grp = QGroupBox("Morphological Parameters")
|
|
113
|
+
grid = QGridLayout(grp)
|
|
114
|
+
self.cb_op = QComboBox(); self.cb_op.addItems(self.OPS)
|
|
115
|
+
self.sp_kernel = QSpinBox(); self.sp_kernel.setRange(1, 31); self.sp_kernel.setSingleStep(2)
|
|
116
|
+
self.sp_iter = QSpinBox(); self.sp_iter.setRange(1, 10)
|
|
117
|
+
|
|
118
|
+
init = dict(initial or {})
|
|
119
|
+
op_text = {v:k for k,v in self.OP_MAP.items()}.get(str(init.get("operation","erosion")).lower(), "Erosion")
|
|
120
|
+
self.cb_op.setCurrentText(op_text)
|
|
121
|
+
k = int(init.get("kernel", 3)); self.sp_kernel.setValue(k if k % 2 == 1 else k + 1)
|
|
122
|
+
self.sp_iter.setValue(int(init.get("iterations", 1)))
|
|
123
|
+
|
|
124
|
+
self.cb_op.currentTextChanged.connect(self._debounce)
|
|
125
|
+
self.sp_kernel.valueChanged.connect(self._debounce)
|
|
126
|
+
self.sp_iter.valueChanged.connect(self._debounce)
|
|
127
|
+
|
|
128
|
+
grid.addWidget(QLabel("Operation:"), 0, 0); grid.addWidget(self.cb_op, 0, 1, 1, 2)
|
|
129
|
+
grid.addWidget(QLabel("Kernel size:"), 1, 0); grid.addWidget(self.sp_kernel, 1, 1)
|
|
130
|
+
grid.addWidget(QLabel("Iterations:"), 2, 0); grid.addWidget(self.sp_iter, 2, 1)
|
|
131
|
+
v.addWidget(grp)
|
|
132
|
+
|
|
133
|
+
# ---- Preview with zoom/pan ----
|
|
134
|
+
self.scene = QGraphicsScene(self)
|
|
135
|
+
self.view = ZoomableGraphicsView(self.scene)
|
|
136
|
+
self.view.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
137
|
+
self.pix = QGraphicsPixmapItem()
|
|
138
|
+
self.scene.addItem(self.pix)
|
|
139
|
+
v.addWidget(self.view, 1)
|
|
140
|
+
|
|
141
|
+
# ---- Zoom bar (themed) ----
|
|
142
|
+
z = QHBoxLayout()
|
|
143
|
+
z.addStretch(1)
|
|
144
|
+
|
|
145
|
+
btn_in = themed_toolbtn("zoom-in", "Zoom In")
|
|
146
|
+
btn_out = themed_toolbtn("zoom-out", "Zoom Out")
|
|
147
|
+
btn_fit = themed_toolbtn("zoom-fit-best", "Fit to Preview")
|
|
148
|
+
|
|
149
|
+
btn_in.clicked.connect(self.view.zoom_in)
|
|
150
|
+
btn_out.clicked.connect(self.view.zoom_out)
|
|
151
|
+
btn_fit.clicked.connect(lambda: self.view.fit_to_item(self.pix))
|
|
152
|
+
|
|
153
|
+
z.addWidget(btn_in)
|
|
154
|
+
z.addWidget(btn_out)
|
|
155
|
+
z.addWidget(btn_fit)
|
|
156
|
+
v.addLayout(z)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---- Buttons (unchanged) ----
|
|
160
|
+
row = QHBoxLayout()
|
|
161
|
+
btn_apply = QPushButton("Apply"); btn_apply.clicked.connect(self._apply)
|
|
162
|
+
btn_reset = QPushButton("Reset"); btn_reset.clicked.connect(self._reset)
|
|
163
|
+
btn_cancel= QPushButton("Cancel"); btn_cancel.clicked.connect(self.reject)
|
|
164
|
+
row.addStretch(1); row.addWidget(btn_apply); row.addWidget(btn_reset); row.addWidget(btn_cancel)
|
|
165
|
+
v.addLayout(row)
|
|
166
|
+
|
|
167
|
+
self._timer = QTimer(self); self._timer.setSingleShot(True); self._timer.timeout.connect(self._update_preview)
|
|
168
|
+
|
|
169
|
+
self._set_pix(self._disp_base)
|
|
170
|
+
self._update_preview()
|
|
171
|
+
# initial fit
|
|
172
|
+
self.view.fit_to_item(self.pix)
|
|
173
|
+
|
|
174
|
+
def _debounce(self): self._timer.start(200)
|
|
175
|
+
|
|
176
|
+
def _set_pix(self, rgb):
|
|
177
|
+
arr = (np.clip(rgb, 0, 1) * 255).astype(np.uint8)
|
|
178
|
+
h, w, _ = arr.shape
|
|
179
|
+
q = QImage(arr.data, w, h, 3*w, QImage.Format.Format_RGB888)
|
|
180
|
+
self.pix.setPixmap(QPixmap.fromImage(q))
|
|
181
|
+
self.scene.setSceneRect(self.pix.boundingRect())
|
|
182
|
+
|
|
183
|
+
def _params(self):
|
|
184
|
+
op = self.OP_MAP[self.cb_op.currentText()]
|
|
185
|
+
k = int(self.sp_kernel.value())
|
|
186
|
+
it = int(self.sp_iter.value())
|
|
187
|
+
if k % 2 == 0: k += 1
|
|
188
|
+
return op, k, it
|
|
189
|
+
|
|
190
|
+
def _update_preview(self):
|
|
191
|
+
op, k, it = self._params()
|
|
192
|
+
try:
|
|
193
|
+
out = apply_morphology(self._disp_base, operation=op, kernel_size=k, iterations=it)
|
|
194
|
+
|
|
195
|
+
# Blend preview with active mask (preview is on _disp_base size)
|
|
196
|
+
H, W = out.shape[:2]
|
|
197
|
+
m = _get_active_mask_resized(self.doc, H, W)
|
|
198
|
+
if m is not None:
|
|
199
|
+
base = self._disp_base.astype(np.float32)
|
|
200
|
+
out = _blend_with_mask(base, out.astype(np.float32), m)
|
|
201
|
+
|
|
202
|
+
self._set_pix(out)
|
|
203
|
+
except Exception as e:
|
|
204
|
+
QMessageBox.warning(self, "Morphology", f"Preview failed:\n{e}")
|
|
205
|
+
|
|
206
|
+
def _apply(self):
|
|
207
|
+
op, k, it = self._params()
|
|
208
|
+
try:
|
|
209
|
+
out = apply_morphology(self.orig, operation=op, kernel_size=k, iterations=it)
|
|
210
|
+
out = np.clip(out, 0.0, 1.0).astype(np.float32, copy=False)
|
|
211
|
+
|
|
212
|
+
# Blend with active mask at full resolution
|
|
213
|
+
H, W = out.shape[:2]
|
|
214
|
+
m = _get_active_mask_resized(self.doc, H, W)
|
|
215
|
+
if m is not None:
|
|
216
|
+
base = np.asarray(self.doc.image, dtype=np.float32)
|
|
217
|
+
if base.dtype.kind in "ui":
|
|
218
|
+
maxv = float(np.iinfo(base.dtype).max)
|
|
219
|
+
base = base / max(1.0, maxv)
|
|
220
|
+
else:
|
|
221
|
+
base = np.clip(base, 0.0, 1.0)
|
|
222
|
+
out = _blend_with_mask(base, out, m).astype(np.float32, copy=False)
|
|
223
|
+
|
|
224
|
+
# Commit to document
|
|
225
|
+
if hasattr(self.doc, "set_image"):
|
|
226
|
+
self.doc.set_image(out, step_name="Morphology")
|
|
227
|
+
elif hasattr(self.doc, "apply_numpy"):
|
|
228
|
+
self.doc.apply_numpy(out, step_name="Morphology")
|
|
229
|
+
else:
|
|
230
|
+
self.doc.image = out
|
|
231
|
+
|
|
232
|
+
# ── Register as last_headless_command for replay ───────────
|
|
233
|
+
try:
|
|
234
|
+
main = self.parent()
|
|
235
|
+
if main is not None:
|
|
236
|
+
preset = {
|
|
237
|
+
"operation": op,
|
|
238
|
+
"kernel": int(k),
|
|
239
|
+
"iterations": int(it),
|
|
240
|
+
}
|
|
241
|
+
payload = {
|
|
242
|
+
"command_id": "morphology",
|
|
243
|
+
"preset": dict(preset),
|
|
244
|
+
}
|
|
245
|
+
setattr(main, "_last_headless_command", payload)
|
|
246
|
+
|
|
247
|
+
# optional log
|
|
248
|
+
try:
|
|
249
|
+
if hasattr(main, "_log"):
|
|
250
|
+
main._log(
|
|
251
|
+
f"[Replay] Registered Morphology as last action "
|
|
252
|
+
f"(op={op}, kernel={k}, iter={it})"
|
|
253
|
+
)
|
|
254
|
+
except Exception:
|
|
255
|
+
pass
|
|
256
|
+
except Exception:
|
|
257
|
+
# never break apply if replay wiring fails
|
|
258
|
+
pass
|
|
259
|
+
# ────────────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
self.accept()
|
|
262
|
+
except Exception as e:
|
|
263
|
+
QMessageBox.critical(self, "Morphology", f"Failed to apply:\n{e}")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _reset(self):
|
|
268
|
+
self.cb_op.setCurrentText("Erosion")
|
|
269
|
+
self.sp_kernel.setValue(3)
|
|
270
|
+
self.sp_iter.setValue(1)
|
|
271
|
+
self._set_pix(self._disp_base)
|
|
272
|
+
self.view.fit_to_item(self.pix)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# ---------------------- Preset editor (Shortcuts) ----------------------
|
|
276
|
+
|
|
277
|
+
class _MorphologyPresetDialog(QDialog):
|
|
278
|
+
"""
|
|
279
|
+
Preset editor for Morphology shortcuts.
|
|
280
|
+
Stores JSON-safe dict:
|
|
281
|
+
{ "operation": "erosion|dilation|opening|closing",
|
|
282
|
+
"kernel": int odd,
|
|
283
|
+
"iterations": int }
|
|
284
|
+
"""
|
|
285
|
+
OPS = ["erosion", "dilation", "opening", "closing"]
|
|
286
|
+
|
|
287
|
+
def __init__(self, parent=None, initial: dict | None = None):
|
|
288
|
+
super().__init__(parent)
|
|
289
|
+
self.setWindowTitle("Morphology — Preset")
|
|
290
|
+
p = dict(initial or {})
|
|
291
|
+
f = QFormLayout(self)
|
|
292
|
+
|
|
293
|
+
self.cb_op = QComboBox()
|
|
294
|
+
self.cb_op.addItems([op.title() for op in self.OPS])
|
|
295
|
+
op0 = str(p.get("operation", "erosion")).lower()
|
|
296
|
+
if op0 not in self.OPS:
|
|
297
|
+
op0 = "erosion"
|
|
298
|
+
self.cb_op.setCurrentText(op0.title())
|
|
299
|
+
|
|
300
|
+
self.sp_kernel = QSpinBox()
|
|
301
|
+
self.sp_kernel.setRange(1, 31)
|
|
302
|
+
self.sp_kernel.setSingleStep(2)
|
|
303
|
+
k = int(p.get("kernel", 3))
|
|
304
|
+
if k % 2 == 0:
|
|
305
|
+
k += 1
|
|
306
|
+
self.sp_kernel.setValue(k)
|
|
307
|
+
|
|
308
|
+
self.sp_iter = QSpinBox()
|
|
309
|
+
self.sp_iter.setRange(1, 10)
|
|
310
|
+
self.sp_iter.setValue(int(p.get("iterations", 1)))
|
|
311
|
+
|
|
312
|
+
f.addRow("Operation:", self.cb_op)
|
|
313
|
+
f.addRow("Kernel size:", self.sp_kernel)
|
|
314
|
+
f.addRow("Iterations:", self.sp_iter)
|
|
315
|
+
|
|
316
|
+
btns = QDialogButtonBox(
|
|
317
|
+
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel,
|
|
318
|
+
parent=self
|
|
319
|
+
)
|
|
320
|
+
btns.accepted.connect(self.accept)
|
|
321
|
+
btns.rejected.connect(self.reject)
|
|
322
|
+
f.addRow(btns)
|
|
323
|
+
|
|
324
|
+
def result_dict(self) -> dict:
|
|
325
|
+
k = int(self.sp_kernel.value())
|
|
326
|
+
if k % 2 == 0:
|
|
327
|
+
k += 1
|
|
328
|
+
return {
|
|
329
|
+
"operation": self.cb_op.currentText().lower(),
|
|
330
|
+
"kernel": int(k),
|
|
331
|
+
"iterations": int(self.sp_iter.value()),
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# ---------------------- Headless runner (Scripts / Presets / Replay) ----------------------
|
|
336
|
+
|
|
337
|
+
def run_morphology_via_preset(main, preset: dict | None = None, *, target_doc=None):
|
|
338
|
+
"""
|
|
339
|
+
Headless Morphology runner.
|
|
340
|
+
|
|
341
|
+
preset keys:
|
|
342
|
+
- operation: "erosion" | "dilation" | "opening" | "closing"
|
|
343
|
+
- kernel: odd int (default 3)
|
|
344
|
+
- iterations: int >= 1 (default 1)
|
|
345
|
+
"""
|
|
346
|
+
p = dict(preset or {})
|
|
347
|
+
|
|
348
|
+
# ---- Remember for Replay ----
|
|
349
|
+
try:
|
|
350
|
+
remember = getattr(main, "_remember_last_headless_command", None) \
|
|
351
|
+
or getattr(main, "remember_last_headless_command", None)
|
|
352
|
+
if callable(remember):
|
|
353
|
+
remember("morphology", p, description="Morphology")
|
|
354
|
+
else:
|
|
355
|
+
setattr(main, "_last_headless_command", {
|
|
356
|
+
"command_id": "morphology",
|
|
357
|
+
"preset": dict(p),
|
|
358
|
+
})
|
|
359
|
+
except Exception:
|
|
360
|
+
pass
|
|
361
|
+
# ----------------------------
|
|
362
|
+
|
|
363
|
+
dm = getattr(main, "doc_manager", None) or getattr(main, "dm", None)
|
|
364
|
+
|
|
365
|
+
# Resolve doc
|
|
366
|
+
doc = target_doc
|
|
367
|
+
if doc is None:
|
|
368
|
+
d = getattr(main, "_active_doc", None)
|
|
369
|
+
doc = d() if callable(d) else d
|
|
370
|
+
|
|
371
|
+
if doc is None or getattr(doc, "image", None) is None:
|
|
372
|
+
QMessageBox.warning(main, "Morphology", "Load an image first.")
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
apply_morphology_to_doc(doc, p)
|
|
377
|
+
if hasattr(main, "_log"):
|
|
378
|
+
main._log(f"✅ Morphology (headless) preset={p}")
|
|
379
|
+
except Exception as e:
|
|
380
|
+
QMessageBox.critical(main, "Morphology", str(e))
|
|
381
|
+
if hasattr(main, "_log"):
|
|
382
|
+
main._log(f"❌ Morphology failed: {e}")
|