setiastrosuitepro 1.6.10__py3-none-any.whl → 1.7.0.post2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- setiastro/images/colorwheel.svg +97 -0
- setiastro/images/narrowbandnormalization.png +0 -0
- setiastro/images/planetarystacker.png +0 -0
- setiastro/saspro/__main__.py +1 -1
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/aberration_ai.py +49 -11
- setiastro/saspro/aberration_ai_preset.py +29 -3
- setiastro/saspro/backgroundneutral.py +73 -33
- setiastro/saspro/blink_comparator_pro.py +116 -71
- setiastro/saspro/convo.py +9 -6
- setiastro/saspro/curve_editor_pro.py +72 -22
- setiastro/saspro/curves_preset.py +249 -47
- setiastro/saspro/doc_manager.py +178 -11
- setiastro/saspro/gui/main_window.py +305 -66
- setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
- setiastro/saspro/gui/mixins/file_mixin.py +35 -16
- setiastro/saspro/gui/mixins/menu_mixin.py +32 -1
- setiastro/saspro/gui/mixins/toolbar_mixin.py +135 -11
- setiastro/saspro/histogram.py +179 -7
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +972 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
- setiastro/saspro/imageops/stretch.py +66 -15
- setiastro/saspro/legacy/numba_utils.py +25 -48
- setiastro/saspro/live_stacking.py +24 -4
- setiastro/saspro/multiscale_decomp.py +30 -17
- setiastro/saspro/narrowband_normalization.py +1618 -0
- setiastro/saspro/numba_utils.py +0 -55
- setiastro/saspro/ops/script_editor.py +5 -0
- setiastro/saspro/ops/scripts.py +119 -0
- setiastro/saspro/remove_green.py +1 -1
- setiastro/saspro/resources.py +4 -0
- setiastro/saspro/ser_stack_config.py +74 -0
- setiastro/saspro/ser_stacker.py +2310 -0
- setiastro/saspro/ser_stacker_dialog.py +1500 -0
- setiastro/saspro/ser_tracking.py +206 -0
- setiastro/saspro/serviewer.py +1258 -0
- setiastro/saspro/sfcc.py +602 -214
- setiastro/saspro/shortcuts.py +35 -16
- setiastro/saspro/stacking_suite.py +332 -87
- setiastro/saspro/star_alignment.py +243 -122
- setiastro/saspro/stat_stretch.py +220 -31
- setiastro/saspro/subwindow.py +2 -4
- setiastro/saspro/whitebalance.py +24 -0
- setiastro/saspro/widgets/resource_monitor.py +122 -74
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/METADATA +2 -2
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/RECORD +51 -40
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/licenses/license.txt +0 -0
|
@@ -93,13 +93,34 @@ class MetricsPanel(QWidget):
|
|
|
93
93
|
|
|
94
94
|
@staticmethod
|
|
95
95
|
def _compute_one(i_entry):
|
|
96
|
+
"""
|
|
97
|
+
Compute (FWHM, eccentricity, star_count) using SEP on a *2x downsampled*
|
|
98
|
+
mono float32 frame.
|
|
99
|
+
|
|
100
|
+
- Downsample is fixed at 2x (linear), using AREA.
|
|
101
|
+
- FWHM is converted back to full-res pixel units by multiplying by 2.
|
|
102
|
+
Optionally multiply by sqrt(2) if you want to compensate for the
|
|
103
|
+
AREA downsample's effective smoothing (see fwhm_factor below).
|
|
104
|
+
- Eccentricity is scale-invariant.
|
|
105
|
+
- Star count should be closer to full-res if we also scale minarea
|
|
106
|
+
from 16 -> 4 (area scales by 1/4).
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
import cv2
|
|
110
|
+
import sep
|
|
111
|
+
|
|
96
112
|
idx, entry = i_entry
|
|
97
|
-
img = entry[
|
|
113
|
+
img = entry["image_data"]
|
|
98
114
|
|
|
99
|
-
# normalize to float32 mono [0..1] exactly like live
|
|
100
115
|
data = np.asarray(img)
|
|
116
|
+
h0, w0 = data.shape[:2]
|
|
117
|
+
|
|
118
|
+
# ----------------------------
|
|
119
|
+
# 1) Normalize to float32 mono [0..1]
|
|
120
|
+
# ----------------------------
|
|
101
121
|
if data.ndim == 3:
|
|
102
122
|
data = data.mean(axis=2)
|
|
123
|
+
|
|
103
124
|
if data.dtype == np.uint8:
|
|
104
125
|
data = data.astype(np.float32) / 255.0
|
|
105
126
|
elif data.dtype == np.uint16:
|
|
@@ -107,35 +128,63 @@ class MetricsPanel(QWidget):
|
|
|
107
128
|
else:
|
|
108
129
|
data = data.astype(np.float32, copy=False)
|
|
109
130
|
|
|
131
|
+
# Guard: SEP expects finite values
|
|
132
|
+
if not np.isfinite(data).all():
|
|
133
|
+
data = np.nan_to_num(data, nan=0.0, posinf=1.0, neginf=0.0).astype(np.float32, copy=False)
|
|
134
|
+
|
|
135
|
+
# ----------------------------
|
|
136
|
+
# 2) Fixed 2x downsample (linear /2)
|
|
137
|
+
# ----------------------------
|
|
138
|
+
# Use integer decimation by resize to preserve speed and consistency.
|
|
139
|
+
new_w = max(16, w0 // 2)
|
|
140
|
+
new_h = max(16, h0 // 2)
|
|
141
|
+
ds = cv2.resize(data, (new_w, new_h), interpolation=cv2.INTER_AREA)
|
|
142
|
+
|
|
143
|
+
# ----------------------------
|
|
144
|
+
# 3) SEP pipeline (same as before, but minarea scaled)
|
|
145
|
+
# ----------------------------
|
|
110
146
|
try:
|
|
111
|
-
|
|
112
|
-
bkg = sep.Background(data)
|
|
147
|
+
bkg = sep.Background(ds)
|
|
113
148
|
back = bkg.back()
|
|
114
149
|
try:
|
|
115
150
|
gr = float(bkg.globalrms)
|
|
116
151
|
except Exception:
|
|
117
|
-
# some SEP builds only expose per-cell rms map
|
|
118
152
|
gr = float(np.median(np.asarray(bkg.rms(), dtype=np.float32)))
|
|
119
153
|
|
|
154
|
+
# minarea: 16 at full-res ~= 4 at 2x downsample (area /4)
|
|
155
|
+
minarea = 4
|
|
156
|
+
|
|
120
157
|
cat = sep.extract(
|
|
121
|
-
|
|
158
|
+
ds - back,
|
|
122
159
|
thresh=7.0,
|
|
123
160
|
err=gr,
|
|
124
|
-
minarea=
|
|
161
|
+
minarea=minarea,
|
|
125
162
|
clean=True,
|
|
126
163
|
deblend_nthresh=32,
|
|
127
164
|
)
|
|
128
165
|
|
|
129
166
|
if len(cat) > 0:
|
|
130
167
|
# FWHM via geometric-mean sigma (old Blink)
|
|
131
|
-
sig = np.sqrt(cat[
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
168
|
+
sig = np.sqrt(cat["a"] * cat["b"]).astype(np.float32, copy=False)
|
|
169
|
+
fwhm_ds = float(np.nanmedian(2.3548 * sig))
|
|
170
|
+
|
|
171
|
+
# ----------------------------
|
|
172
|
+
# 4) Convert FWHM back to full-res
|
|
173
|
+
# ----------------------------
|
|
174
|
+
# Pure geometric reconversion: *2
|
|
175
|
+
# If you want the "noise reduction" compensation you mentioned:
|
|
176
|
+
# multiply by sqrt(2) instead of 2, or 2*sqrt(2) depending on intent.
|
|
177
|
+
#
|
|
178
|
+
# Most consistent with "true full-res pixels" is factor = 2.
|
|
179
|
+
# If you insist on smoothing-compensation, set factor = 2*np.sqrt(2)
|
|
180
|
+
# (because you still have to undo scale, and then add smoothing term).
|
|
181
|
+
fwhm_factor = 2.0 # change to (2.0 * np.sqrt(2.0)) if you really want it
|
|
182
|
+
fwhm = fwhm_ds * fwhm_factor
|
|
183
|
+
|
|
184
|
+
# TRUE eccentricity
|
|
185
|
+
a = np.maximum(cat["a"].astype(np.float32, copy=False), 1e-12)
|
|
186
|
+
b = np.clip(cat["b"].astype(np.float32, copy=False), 0.0, None)
|
|
187
|
+
q = np.clip(b / a, 0.0, 1.0)
|
|
139
188
|
e_true = np.sqrt(np.maximum(0.0, 1.0 - q * q))
|
|
140
189
|
ecc = float(np.nanmedian(e_true))
|
|
141
190
|
|
|
@@ -144,16 +193,26 @@ class MetricsPanel(QWidget):
|
|
|
144
193
|
fwhm, ecc, star_cnt = np.nan, np.nan, 0
|
|
145
194
|
|
|
146
195
|
except Exception:
|
|
147
|
-
# same sentinel behavior as before
|
|
148
196
|
fwhm, ecc, star_cnt = 10.0, 1.0, 0
|
|
149
197
|
|
|
150
|
-
orig_back = entry.get(
|
|
198
|
+
orig_back = entry.get("orig_background", np.nan)
|
|
151
199
|
return idx, fwhm, ecc, orig_back, star_cnt
|
|
152
200
|
|
|
201
|
+
|
|
202
|
+
|
|
153
203
|
def compute_all_metrics(self, loaded_images) -> bool:
|
|
154
|
-
"""Run SEP over the full list in parallel using threads and cache results.
|
|
155
|
-
Returns True if metrics were computed, False if user declined/canceled.
|
|
156
204
|
"""
|
|
205
|
+
Run SEP over the full list in parallel using threads and cache results.
|
|
206
|
+
Uses *downsampled* SEP for speed + lower RAM.
|
|
207
|
+
Returns True if metrics were computed, False if user canceled.
|
|
208
|
+
"""
|
|
209
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
210
|
+
import os
|
|
211
|
+
import numpy as np
|
|
212
|
+
import psutil
|
|
213
|
+
from PyQt6.QtCore import Qt
|
|
214
|
+
from PyQt6.QtWidgets import QProgressDialog, QApplication
|
|
215
|
+
|
|
157
216
|
n = len(loaded_images)
|
|
158
217
|
if n == 0:
|
|
159
218
|
self._orig_images = []
|
|
@@ -162,62 +221,17 @@ class MetricsPanel(QWidget):
|
|
|
162
221
|
self._threshold_initialized = [False] * 4
|
|
163
222
|
return True
|
|
164
223
|
|
|
165
|
-
def _has_metrics(md):
|
|
166
|
-
try:
|
|
167
|
-
return md is not None and len(md) == 4 and md[0] is not None and len(md[0]) > 0
|
|
168
|
-
except Exception:
|
|
169
|
-
return False
|
|
170
|
-
|
|
171
|
-
settings = QSettings()
|
|
172
|
-
show_warning = settings.value("metrics/showWarning", True, type=bool)
|
|
173
|
-
|
|
174
|
-
if (not show_warning) and (not _has_metrics(getattr(self, "metrics_data", None))):
|
|
175
|
-
settings.setValue("metrics/showWarning", True)
|
|
176
|
-
show_warning = True
|
|
177
|
-
|
|
178
|
-
# ----------------------------
|
|
179
|
-
# 1) Optional warning gate
|
|
180
|
-
# ----------------------------
|
|
181
|
-
if show_warning:
|
|
182
|
-
msg = QMessageBox(self)
|
|
183
|
-
msg.setWindowTitle(self.tr("Heads-up"))
|
|
184
|
-
msg.setText(self.tr(
|
|
185
|
-
"This is going to use ALL your CPU cores and the UI may lock up until it finishes.\n\n"
|
|
186
|
-
"Continue?"
|
|
187
|
-
))
|
|
188
|
-
msg.setStandardButtons(
|
|
189
|
-
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
190
|
-
)
|
|
191
|
-
cb = QCheckBox(self.tr("Don't show again"), msg)
|
|
192
|
-
msg.setCheckBox(cb)
|
|
193
|
-
|
|
194
|
-
clicked = msg.exec()
|
|
195
|
-
clicked_yes = (clicked == QMessageBox.StandardButton.Yes)
|
|
196
|
-
|
|
197
|
-
if not clicked_yes:
|
|
198
|
-
# If they said NO, never allow "Don't show again" to lock them out.
|
|
199
|
-
# Keep the warning enabled so they can opt-in later.
|
|
200
|
-
if cb.isChecked():
|
|
201
|
-
settings.setValue("metrics/showWarning", True)
|
|
202
|
-
return False
|
|
203
|
-
|
|
204
|
-
# They said YES: now it's safe to honor "Don't show again"
|
|
205
|
-
if cb.isChecked():
|
|
206
|
-
settings.setValue("metrics/showWarning", False)
|
|
207
|
-
|
|
208
|
-
# If show_warning is False, we compute with no prompt.
|
|
209
|
-
|
|
210
224
|
# ----------------------------
|
|
211
|
-
#
|
|
225
|
+
# 1) Allocate result arrays
|
|
212
226
|
# ----------------------------
|
|
213
|
-
m0 = np.full(n, np.nan, dtype=np.float32) # FWHM
|
|
227
|
+
m0 = np.full(n, np.nan, dtype=np.float32) # FWHM (full-res px units)
|
|
214
228
|
m1 = np.full(n, np.nan, dtype=np.float32) # Eccentricity
|
|
215
229
|
m2 = np.full(n, np.nan, dtype=np.float32) # Background (cached)
|
|
216
230
|
m3 = np.full(n, np.nan, dtype=np.float32) # Star count
|
|
217
|
-
flags = [e.get(
|
|
231
|
+
flags = [e.get("flagged", False) for e in loaded_images]
|
|
218
232
|
|
|
219
233
|
# ----------------------------
|
|
220
|
-
#
|
|
234
|
+
# 2) Progress dialog (Cancel)
|
|
221
235
|
# ----------------------------
|
|
222
236
|
prog = QProgressDialog(self.tr("Computing frame metrics…"), self.tr("Cancel"), 0, n, self)
|
|
223
237
|
prog.setWindowModality(Qt.WindowModality.WindowModal)
|
|
@@ -226,7 +240,34 @@ class MetricsPanel(QWidget):
|
|
|
226
240
|
prog.show()
|
|
227
241
|
QApplication.processEvents()
|
|
228
242
|
|
|
229
|
-
|
|
243
|
+
cpu = os.cpu_count() or 1
|
|
244
|
+
|
|
245
|
+
# ----------------------------
|
|
246
|
+
# 3) Worker sizing by RAM (downsample-aware)
|
|
247
|
+
# ----------------------------
|
|
248
|
+
# Estimate using the same max_dim as _compute_one (default 1024).
|
|
249
|
+
# Use first frame to estimate scale.
|
|
250
|
+
max_dim = int(loaded_images[0].get("metrics_max_dim", 1024))
|
|
251
|
+
h0, w0 = loaded_images[0]["image_data"].shape[:2]
|
|
252
|
+
scale = 1.0
|
|
253
|
+
if max(h0, w0) > max_dim:
|
|
254
|
+
scale = max_dim / float(max(h0, w0))
|
|
255
|
+
|
|
256
|
+
hd = max(16, int(round(h0 * scale)))
|
|
257
|
+
wd = max(16, int(round(w0 * scale)))
|
|
258
|
+
|
|
259
|
+
# float32 mono downsample buffer
|
|
260
|
+
bytes_per = hd * wd * 4
|
|
261
|
+
|
|
262
|
+
# SEP allocates extra maps; budget ~3x to be safe.
|
|
263
|
+
budget_per_worker = int(bytes_per * 3.0)
|
|
264
|
+
|
|
265
|
+
avail = psutil.virtual_memory().available
|
|
266
|
+
max_by_mem = max(1, int(avail / max(budget_per_worker, 1)))
|
|
267
|
+
|
|
268
|
+
# Don’t exceed CPU, and don’t go crazy high even if RAM is huge
|
|
269
|
+
workers = max(1, min(cpu, max_by_mem, 24))
|
|
270
|
+
|
|
230
271
|
tasks = [(i, loaded_images[i]) for i in range(n)]
|
|
231
272
|
done = 0
|
|
232
273
|
canceled = False
|
|
@@ -238,6 +279,7 @@ class MetricsPanel(QWidget):
|
|
|
238
279
|
if prog.wasCanceled():
|
|
239
280
|
canceled = True
|
|
240
281
|
break
|
|
282
|
+
|
|
241
283
|
try:
|
|
242
284
|
idx, fwhm, ecc, orig_back, star_cnt = fut.result()
|
|
243
285
|
except Exception:
|
|
@@ -245,7 +287,10 @@ class MetricsPanel(QWidget):
|
|
|
245
287
|
fwhm, ecc, orig_back, star_cnt = np.nan, np.nan, np.nan, 0
|
|
246
288
|
|
|
247
289
|
if 0 <= idx < n:
|
|
248
|
-
m0[idx]
|
|
290
|
+
m0[idx] = fwhm
|
|
291
|
+
m1[idx] = ecc
|
|
292
|
+
m2[idx] = orig_back
|
|
293
|
+
m3[idx] = float(star_cnt)
|
|
249
294
|
|
|
250
295
|
done += 1
|
|
251
296
|
prog.setValue(done)
|
|
@@ -254,7 +299,7 @@ class MetricsPanel(QWidget):
|
|
|
254
299
|
prog.close()
|
|
255
300
|
|
|
256
301
|
if canceled:
|
|
257
|
-
# IMPORTANT: leave caches alone; caller
|
|
302
|
+
# IMPORTANT: leave caches alone; caller handles clear/return
|
|
258
303
|
return False
|
|
259
304
|
|
|
260
305
|
# ----------------------------
|
setiastro/saspro/convo.py
CHANGED
|
@@ -1118,13 +1118,14 @@ class ConvoDeconvoDialog(QDialog):
|
|
|
1118
1118
|
if img is None:
|
|
1119
1119
|
QMessageBox.warning(self, "No Image", "Please select an image before estimating PSF.")
|
|
1120
1120
|
return
|
|
1121
|
+
|
|
1121
1122
|
img_gray = img.mean(axis=2).astype(np.float32) if (img.ndim == 3) else img.astype(np.float32)
|
|
1122
1123
|
|
|
1123
|
-
sigma
|
|
1124
|
-
minarea
|
|
1125
|
-
sat
|
|
1126
|
-
maxstars= self.sep_maxstars_spin.value
|
|
1127
|
-
half_w
|
|
1124
|
+
sigma = float(self.sep_threshold_slider.value())
|
|
1125
|
+
minarea = int(self.sep_minarea_spin.value()) # ✅
|
|
1126
|
+
sat = float(self.sep_sat_slider.value())
|
|
1127
|
+
maxstars = int(self.sep_maxstars_spin.value()) # ✅
|
|
1128
|
+
half_w = int(self.sep_stamp_spin.value()) # ✅
|
|
1128
1129
|
|
|
1129
1130
|
try:
|
|
1130
1131
|
psf_kernel = estimate_psf_from_image(
|
|
@@ -1136,11 +1137,13 @@ class ConvoDeconvoDialog(QDialog):
|
|
|
1136
1137
|
stamp_half_width=half_w
|
|
1137
1138
|
)
|
|
1138
1139
|
except RuntimeError as e:
|
|
1139
|
-
QMessageBox.critical(self, "PSF Error", str(e))
|
|
1140
|
+
QMessageBox.critical(self, "PSF Error", str(e))
|
|
1141
|
+
return
|
|
1140
1142
|
|
|
1141
1143
|
self._last_stellar_psf = psf_kernel
|
|
1142
1144
|
self._show_stellar_psf_preview(psf_kernel)
|
|
1143
1145
|
|
|
1146
|
+
|
|
1144
1147
|
def _show_stellar_psf_preview(self, psf_kernel: np.ndarray):
|
|
1145
1148
|
h, w = psf_kernel.shape
|
|
1146
1149
|
img8 = ((psf_kernel / max(psf_kernel.max(), 1e-12)) * 255.0).astype(np.uint8)
|
|
@@ -1769,14 +1769,38 @@ class CurvesDialogPro(QDialog):
|
|
|
1769
1769
|
name, ok = QInputDialog.getText(self, self.tr("Save Curves Preset"), self.tr("Preset name:"))
|
|
1770
1770
|
if not ok or not name.strip():
|
|
1771
1771
|
return
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1772
|
+
name = name.strip()
|
|
1773
|
+
|
|
1774
|
+
# 0) flush active editor -> store (CRITICAL)
|
|
1775
|
+
try:
|
|
1776
|
+
self._curves_store[self._current_mode_key] = self._editor_points_norm()
|
|
1777
|
+
except Exception:
|
|
1778
|
+
pass
|
|
1779
|
+
|
|
1780
|
+
# 1) build a full multi-curve payload
|
|
1781
|
+
modes = {}
|
|
1782
|
+
for k, pts in self._curves_store.items():
|
|
1783
|
+
if not isinstance(pts, (list, tuple)) or len(pts) < 2:
|
|
1784
|
+
continue
|
|
1785
|
+
# ensure floats (QSettings/JSON safety)
|
|
1786
|
+
modes[k] = [(float(x), float(y)) for (x, y) in pts]
|
|
1787
|
+
|
|
1788
|
+
preset = {
|
|
1789
|
+
"name": name,
|
|
1790
|
+
"version": 2,
|
|
1791
|
+
"kind": "curves_multi",
|
|
1792
|
+
"active": self._current_mode_key, # "K", "R", ...
|
|
1793
|
+
"modes": modes,
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
# 2) save via your existing persistence
|
|
1797
|
+
if save_custom_preset(name, preset): # <-- change signature (see section 3)
|
|
1798
|
+
self._set_status(self.tr("Saved preset “{0}”.").format(name))
|
|
1776
1799
|
self._rebuild_presets_menu()
|
|
1777
1800
|
else:
|
|
1778
1801
|
QMessageBox.warning(self, self.tr("Save failed"), self.tr("Could not save preset."))
|
|
1779
1802
|
|
|
1803
|
+
|
|
1780
1804
|
def _rebuild_presets_menu(self):
|
|
1781
1805
|
m = QMenu(self)
|
|
1782
1806
|
# Built-in shapes under K (Brightness)
|
|
@@ -2400,43 +2424,69 @@ class CurvesDialogPro(QDialog):
|
|
|
2400
2424
|
def _apply_preset_dict(self, preset: dict):
|
|
2401
2425
|
preset = preset or {}
|
|
2402
2426
|
|
|
2403
|
-
#
|
|
2427
|
+
# -------- MULTI-CURVE (v2) --------
|
|
2428
|
+
if preset.get("kind") == "curves_multi" or ("modes" in preset and isinstance(preset.get("modes"), dict)):
|
|
2429
|
+
modes = preset.get("modes", {}) or {}
|
|
2430
|
+
|
|
2431
|
+
# 0) load all curves into store (fill missing keys with linear)
|
|
2432
|
+
for k in self._curves_store.keys():
|
|
2433
|
+
pts = modes.get(k)
|
|
2434
|
+
if isinstance(pts, (list, tuple)) and len(pts) >= 2:
|
|
2435
|
+
self._curves_store[k] = [(float(x), float(y)) for (x, y) in pts]
|
|
2436
|
+
else:
|
|
2437
|
+
self._curves_store[k] = [(0.0, 0.0), (1.0, 1.0)]
|
|
2438
|
+
|
|
2439
|
+
# 1) choose active key (default to K)
|
|
2440
|
+
active = str(preset.get("active") or "K")
|
|
2441
|
+
if active not in self._curves_store:
|
|
2442
|
+
active = "K"
|
|
2443
|
+
self._current_mode_key = active
|
|
2444
|
+
|
|
2445
|
+
# 2) set radio button that corresponds to active key
|
|
2446
|
+
# map internal key -> radio label
|
|
2447
|
+
key_to_label = {v: k for (k, v) in self._mode_key_map.items()} # "K"->"K (Brightness)"
|
|
2448
|
+
want_label = key_to_label.get(active, "K (Brightness)")
|
|
2449
|
+
for b in self.mode_group.buttons():
|
|
2450
|
+
if b.text() == want_label:
|
|
2451
|
+
b.setChecked(True)
|
|
2452
|
+
break
|
|
2453
|
+
|
|
2454
|
+
# 3) push active curve into editor
|
|
2455
|
+
self._editor_set_from_norm(self._curves_store[active])
|
|
2456
|
+
|
|
2457
|
+
# 4) refresh overlays + preview
|
|
2458
|
+
self._refresh_overlays()
|
|
2459
|
+
self._quick_preview()
|
|
2460
|
+
|
|
2461
|
+
self._set_status(self.tr("Preset: {0} [multi]").format(preset.get("name", self.tr("(built-in)"))))
|
|
2462
|
+
return
|
|
2463
|
+
|
|
2464
|
+
# -------- LEGACY SINGLE-CURVE --------
|
|
2465
|
+
# your existing single-curve behavior (slightly adjusted: store it too)
|
|
2404
2466
|
want = _norm_mode(preset.get("mode"))
|
|
2405
2467
|
for b in self.mode_group.buttons():
|
|
2406
2468
|
if b.text().lower() == want.lower():
|
|
2407
2469
|
b.setChecked(True)
|
|
2408
2470
|
break
|
|
2409
2471
|
|
|
2410
|
-
# 2) get points_norm — if absent, build from shape/amount (built-ins)
|
|
2411
2472
|
ptsN = preset.get("points_norm")
|
|
2412
|
-
shape = preset.get("shape")
|
|
2473
|
+
shape = preset.get("shape")
|
|
2413
2474
|
amount = float(preset.get("amount", 1.0))
|
|
2414
2475
|
|
|
2415
2476
|
if not (isinstance(ptsN, (list, tuple)) and len(ptsN) >= 2):
|
|
2416
2477
|
try:
|
|
2417
|
-
# build from a named shape (built-ins); default to linear
|
|
2418
2478
|
ptsN = _shape_points_norm(str(shape or "linear"), amount)
|
|
2419
2479
|
except Exception:
|
|
2420
|
-
ptsN = [(0.0, 0.0), (1.0, 1.0)]
|
|
2421
|
-
|
|
2422
|
-
# 3) apply handles to the editor (strip exact endpoints)
|
|
2423
|
-
pts_scene = _points_norm_to_scene(ptsN)
|
|
2424
|
-
filt = [(x, y) for (x, y) in pts_scene if 1e-6 < x < 360.0 - 1e-6]
|
|
2425
|
-
|
|
2426
|
-
if hasattr(self.editor, "clearSymmetryLine"):
|
|
2427
|
-
self.editor.clearSymmetryLine()
|
|
2480
|
+
ptsN = [(0.0, 0.0), (1.0, 1.0)]
|
|
2428
2481
|
|
|
2429
|
-
self.
|
|
2430
|
-
self.editor.updateCurve() # ensure redraw
|
|
2431
|
-
|
|
2432
|
-
# persist into store & refresh
|
|
2482
|
+
self._editor_set_from_norm(ptsN)
|
|
2433
2483
|
self._curves_store[self._current_mode_key] = self._editor_points_norm()
|
|
2434
2484
|
self._refresh_overlays()
|
|
2435
2485
|
self._quick_preview()
|
|
2436
2486
|
|
|
2437
|
-
# 4) status: don’t assume shape exists
|
|
2438
2487
|
shape_tag = f"[{shape}]" if shape else "[custom]"
|
|
2439
|
-
self._set_status(self.tr("Preset: {0} {1}").format(preset.get(
|
|
2488
|
+
self._set_status(self.tr("Preset: {0} {1}").format(preset.get("name", self.tr("(built-in)")), shape_tag))
|
|
2489
|
+
|
|
2440
2490
|
|
|
2441
2491
|
|
|
2442
2492
|
def apply_curves_ops(doc, op: dict):
|