setiastrosuitepro 1.6.12__py3-none-any.whl → 1.7.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.
- setiastro/images/3dplanet.png +0 -0
- setiastro/images/TextureClarity.svg +56 -0
- setiastro/images/narrowbandnormalization.png +0 -0
- setiastro/images/planetarystacker.png +0 -0
- setiastro/saspro/__init__.py +9 -8
- setiastro/saspro/__main__.py +326 -285
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/aberration_ai.py +128 -13
- setiastro/saspro/aberration_ai_preset.py +29 -3
- setiastro/saspro/astrospike_python.py +45 -3
- setiastro/saspro/blink_comparator_pro.py +116 -71
- setiastro/saspro/curve_editor_pro.py +72 -22
- setiastro/saspro/curves_preset.py +249 -47
- setiastro/saspro/doc_manager.py +4 -1
- setiastro/saspro/gui/main_window.py +326 -46
- setiastro/saspro/gui/mixins/file_mixin.py +41 -18
- setiastro/saspro/gui/mixins/menu_mixin.py +9 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +123 -7
- setiastro/saspro/histogram.py +179 -7
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +1429 -0
- setiastro/saspro/layers.py +186 -10
- setiastro/saspro/layers_dock.py +198 -5
- setiastro/saspro/legacy/image_manager.py +10 -4
- setiastro/saspro/legacy/numba_utils.py +1 -1
- setiastro/saspro/live_stacking.py +24 -4
- setiastro/saspro/multiscale_decomp.py +30 -17
- setiastro/saspro/narrowband_normalization.py +1618 -0
- setiastro/saspro/planetprojection.py +3854 -0
- setiastro/saspro/remove_green.py +1 -1
- setiastro/saspro/resources.py +8 -0
- setiastro/saspro/rgbalign.py +456 -12
- setiastro/saspro/save_options.py +45 -13
- setiastro/saspro/ser_stack_config.py +102 -0
- setiastro/saspro/ser_stacker.py +2327 -0
- setiastro/saspro/ser_stacker_dialog.py +1865 -0
- setiastro/saspro/ser_tracking.py +228 -0
- setiastro/saspro/serviewer.py +1773 -0
- setiastro/saspro/sfcc.py +298 -64
- setiastro/saspro/shortcuts.py +14 -7
- setiastro/saspro/stacking_suite.py +21 -6
- setiastro/saspro/stat_stretch.py +179 -31
- setiastro/saspro/subwindow.py +38 -5
- setiastro/saspro/texture_clarity.py +593 -0
- setiastro/saspro/widgets/resource_monitor.py +122 -74
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/METADATA +3 -2
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/RECORD +51 -37
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/licenses/license.txt +0 -0
|
@@ -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):
|
|
@@ -12,18 +12,96 @@ except Exception:
|
|
|
12
12
|
_HAS_PCHIP = False
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
|
|
16
15
|
# ---------------------- preset schema ----------------------
|
|
16
|
+
# Legacy (v1 single-curve) preset:
|
|
17
17
|
# {
|
|
18
|
+
# "name": "MyPreset",
|
|
18
19
|
# "mode": "K (Brightness)" | "R" | "G" | "B" | "L*" | "a*" | "b*" | "Chroma" | "Saturation" | aliases ("rgb","k","lum"…),
|
|
19
|
-
# "shape": "linear" | "s_mild" |
|
|
20
|
-
#
|
|
21
|
-
# "
|
|
22
|
-
# "points_norm": [[x,y], ...] # optional when shape="custom" (normalized 0..1 domain/range)
|
|
20
|
+
# "shape": "linear" | "s_mild" | ... | "custom",
|
|
21
|
+
# "amount": 0..1,
|
|
22
|
+
# "points_norm": [[x,y], ...] # normalized 0..1 domain/range
|
|
23
23
|
# }
|
|
24
24
|
#
|
|
25
|
-
#
|
|
25
|
+
# New (v2 multi-curve) preset:
|
|
26
|
+
# {
|
|
27
|
+
# "name": "MyPreset",
|
|
28
|
+
# "kind": "curves_multi",
|
|
29
|
+
# "version": 2,
|
|
30
|
+
# "active": "K" | "R" | "G" | "B" | "L*" | "a*" | "b*" | "Chroma" | "Saturation",
|
|
31
|
+
# "modes": {
|
|
32
|
+
# "K": [[x,y], ...],
|
|
33
|
+
# "R": [[x,y], ...],
|
|
34
|
+
# ...
|
|
35
|
+
# }
|
|
36
|
+
# }
|
|
37
|
+
|
|
38
|
+
_MODE_KEY_TO_LABEL = {
|
|
39
|
+
"K": "K (Brightness)",
|
|
40
|
+
"R": "R",
|
|
41
|
+
"G": "G",
|
|
42
|
+
"B": "B",
|
|
43
|
+
"L*": "L*",
|
|
44
|
+
"a*": "a*",
|
|
45
|
+
"b*": "b*",
|
|
46
|
+
"Chroma": "Chroma",
|
|
47
|
+
"Saturation": "Saturation",
|
|
48
|
+
}
|
|
26
49
|
|
|
50
|
+
|
|
51
|
+
_LABEL_TO_MODE_KEY = {v: k for k, v in _MODE_KEY_TO_LABEL.items()}
|
|
52
|
+
|
|
53
|
+
_APPLY_ORDER_KEYS = ["K", "R", "G", "B", "L*", "a*", "b*", "Chroma", "Saturation"]
|
|
54
|
+
|
|
55
|
+
def _is_linear_points_norm(pts) -> bool:
|
|
56
|
+
try:
|
|
57
|
+
return (
|
|
58
|
+
isinstance(pts, (list, tuple)) and len(pts) == 2
|
|
59
|
+
and tuple(pts[0]) == (0.0, 0.0)
|
|
60
|
+
and tuple(pts[1]) == (1.0, 1.0)
|
|
61
|
+
)
|
|
62
|
+
except Exception:
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def _coerce_points_norm(raw) -> List[Tuple[float, float]] | None:
|
|
66
|
+
if not isinstance(raw, (list, tuple)) or len(raw) < 2:
|
|
67
|
+
return None
|
|
68
|
+
out: List[Tuple[float, float]] = []
|
|
69
|
+
for e in raw:
|
|
70
|
+
if isinstance(e, (list, tuple)) and len(e) >= 2:
|
|
71
|
+
x, y = e[0], e[1]
|
|
72
|
+
elif isinstance(e, dict):
|
|
73
|
+
x, y = e.get("x"), e.get("y")
|
|
74
|
+
else:
|
|
75
|
+
continue
|
|
76
|
+
if x is None or y is None:
|
|
77
|
+
continue
|
|
78
|
+
out.append((float(x), float(y)))
|
|
79
|
+
return out if len(out) >= 2 else None
|
|
80
|
+
|
|
81
|
+
def _coerce_modes_dict(raw) -> Dict[str, List[Tuple[float, float]]]:
|
|
82
|
+
"""
|
|
83
|
+
Return {mode_key: points_norm}, filtering junk but keeping backward-compat.
|
|
84
|
+
Accepts keys as internal mode keys ("K") or labels ("K (Brightness)").
|
|
85
|
+
"""
|
|
86
|
+
modes: Dict[str, List[Tuple[float, float]]] = {}
|
|
87
|
+
if not isinstance(raw, dict):
|
|
88
|
+
return modes
|
|
89
|
+
|
|
90
|
+
for k, v in raw.items():
|
|
91
|
+
key = str(k)
|
|
92
|
+
# allow labels or keys
|
|
93
|
+
if key in _MODE_KEY_TO_LABEL:
|
|
94
|
+
mode_key = key
|
|
95
|
+
else:
|
|
96
|
+
mode_label = _norm_mode(key) # converts aliases -> proper label
|
|
97
|
+
mode_key = _LABEL_TO_MODE_KEY.get(mode_label, "K")
|
|
98
|
+
|
|
99
|
+
pts = _coerce_points_norm(v)
|
|
100
|
+
if pts is None:
|
|
101
|
+
continue
|
|
102
|
+
modes[mode_key] = pts
|
|
103
|
+
|
|
104
|
+
return modes
|
|
27
105
|
# ---------------------- shape library (normalized) ----------------------
|
|
28
106
|
def _shape_points_norm(shape: str, amount: float) -> List[Tuple[float, float]]:
|
|
29
107
|
a = float(max(0.0, min(1.0, amount)))
|
|
@@ -141,32 +219,38 @@ def _unwrap_preset_dict(preset: Dict) -> Dict:
|
|
|
141
219
|
"""
|
|
142
220
|
Accept a variety of containers and peel down to the actual curve data.
|
|
143
221
|
|
|
144
|
-
|
|
145
|
-
{"
|
|
146
|
-
{"curves": {...}}
|
|
147
|
-
{"state": {...}}
|
|
222
|
+
Handles:
|
|
223
|
+
- {"preset": {...}}
|
|
224
|
+
- {"curves": {...}}
|
|
225
|
+
- {"state": {...}}
|
|
226
|
+
- multi presets with {"modes": {...}}
|
|
148
227
|
"""
|
|
149
228
|
p = dict(preset or {})
|
|
150
229
|
|
|
151
|
-
|
|
230
|
+
def _looks_like_curve_dict(d: dict) -> bool:
|
|
231
|
+
return (
|
|
232
|
+
isinstance(d, dict)
|
|
233
|
+
and (
|
|
234
|
+
"points_norm" in d
|
|
235
|
+
or "handles" in d
|
|
236
|
+
or "points_scene" in d
|
|
237
|
+
or "scene_points" in d
|
|
238
|
+
or "modes" in d # <-- multi
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
|
|
152
242
|
inner = p.get("preset")
|
|
153
|
-
if
|
|
154
|
-
or "points_scene" in inner or "scene_points" in inner):
|
|
243
|
+
if _looks_like_curve_dict(inner):
|
|
155
244
|
return inner
|
|
156
245
|
|
|
157
|
-
# Case 2: payloads like {"curves": {...}}
|
|
158
246
|
inner = p.get("curves")
|
|
159
|
-
if
|
|
160
|
-
or "points_scene" in inner or "scene_points" in inner):
|
|
247
|
+
if _looks_like_curve_dict(inner):
|
|
161
248
|
return inner
|
|
162
249
|
|
|
163
|
-
# Case 3: {"state": {...}} (if you stored the curve state under that key)
|
|
164
250
|
inner = p.get("state")
|
|
165
|
-
if
|
|
166
|
-
or "points_scene" in inner or "scene_points" in inner):
|
|
251
|
+
if _looks_like_curve_dict(inner):
|
|
167
252
|
return inner
|
|
168
253
|
|
|
169
|
-
# Otherwise assume p already *is* the preset
|
|
170
254
|
return p
|
|
171
255
|
|
|
172
256
|
|
|
@@ -179,7 +263,9 @@ def _settings() -> QSettings | None:
|
|
|
179
263
|
return None
|
|
180
264
|
|
|
181
265
|
def list_custom_presets() -> list[dict]:
|
|
182
|
-
"""
|
|
266
|
+
"""
|
|
267
|
+
Returns a list of preset dicts (v1 single-curve or v2 multi-curve).
|
|
268
|
+
"""
|
|
183
269
|
s = _settings()
|
|
184
270
|
if not s:
|
|
185
271
|
return []
|
|
@@ -187,29 +273,82 @@ def list_custom_presets() -> list[dict]:
|
|
|
187
273
|
try:
|
|
188
274
|
lst = json.loads(raw)
|
|
189
275
|
if isinstance(lst, list):
|
|
190
|
-
|
|
276
|
+
out = [p for p in lst if isinstance(p, dict)]
|
|
277
|
+
# normalize missing name fields (defensive)
|
|
278
|
+
for p in out:
|
|
279
|
+
if "name" not in p:
|
|
280
|
+
p["name"] = "(unnamed)"
|
|
281
|
+
return out
|
|
191
282
|
except Exception:
|
|
192
283
|
pass
|
|
193
284
|
return []
|
|
194
285
|
|
|
195
|
-
def save_custom_preset(name: str,
|
|
196
|
-
"""
|
|
286
|
+
def save_custom_preset(name: str, mode_or_preset, points_norm: list[tuple[float, float]] | None = None) -> bool:
|
|
287
|
+
"""
|
|
288
|
+
Save a custom preset. Supports:
|
|
289
|
+
- legacy: save_custom_preset(name, mode, points_norm)
|
|
290
|
+
- new: save_custom_preset(name, preset_dict)
|
|
291
|
+
"""
|
|
197
292
|
s = _settings()
|
|
198
293
|
if not s:
|
|
199
294
|
return False
|
|
295
|
+
|
|
200
296
|
name = (name or "").strip()
|
|
201
297
|
if not name:
|
|
202
298
|
return False
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
299
|
+
|
|
300
|
+
# --- build preset dict ---
|
|
301
|
+
preset: dict
|
|
302
|
+
|
|
303
|
+
if isinstance(mode_or_preset, dict):
|
|
304
|
+
# v2 (or v1 dict passed directly)
|
|
305
|
+
preset = dict(mode_or_preset)
|
|
306
|
+
preset["name"] = name # enforce
|
|
307
|
+
|
|
308
|
+
# If it's a multi preset, sanitize modes
|
|
309
|
+
if preset.get("kind") == "curves_multi" or isinstance(preset.get("modes"), dict):
|
|
310
|
+
modes = _coerce_modes_dict(preset.get("modes", {}))
|
|
311
|
+
# fill missing keys with linear so UI always has a full set
|
|
312
|
+
full_modes = {}
|
|
313
|
+
for k in _MODE_KEY_TO_LABEL.keys():
|
|
314
|
+
full_modes[k] = modes.get(k, [(0.0, 0.0), (1.0, 1.0)])
|
|
315
|
+
preset = {
|
|
316
|
+
"name": name,
|
|
317
|
+
"kind": "curves_multi",
|
|
318
|
+
"version": 2,
|
|
319
|
+
"active": str(preset.get("active") or "K"),
|
|
320
|
+
"modes": {k: [(float(x), float(y)) for (x, y) in v] for k, v in full_modes.items()},
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
else:
|
|
324
|
+
# treat as v1 single-curve dict
|
|
325
|
+
mode_label = _norm_mode(preset.get("mode"))
|
|
326
|
+
pts = _coerce_points_norm(preset.get("points_norm")) or [(0.0, 0.0), (1.0, 1.0)]
|
|
327
|
+
preset = {
|
|
328
|
+
"name": name,
|
|
329
|
+
"mode": mode_label,
|
|
330
|
+
"shape": str(preset.get("shape", "custom")),
|
|
331
|
+
"amount": float(preset.get("amount", 1.0)),
|
|
332
|
+
"points_norm": [(float(x), float(y)) for (x, y) in pts],
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
else:
|
|
336
|
+
# legacy signature
|
|
337
|
+
mode = _norm_mode(str(mode_or_preset))
|
|
338
|
+
pts = points_norm or [(0.0, 0.0), (1.0, 1.0)]
|
|
339
|
+
preset = {
|
|
340
|
+
"name": name,
|
|
341
|
+
"mode": mode,
|
|
342
|
+
"shape": "custom",
|
|
343
|
+
"amount": 1.0,
|
|
344
|
+
"points_norm": [(float(x), float(y)) for (x, y) in pts],
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
# --- upsert by name (case-insensitive) ---
|
|
210
348
|
lst = list_custom_presets()
|
|
211
|
-
lst = [p for p in lst if (p.get("name","").lower() != name.lower())]
|
|
349
|
+
lst = [p for p in lst if (p.get("name", "").lower() != name.lower())]
|
|
212
350
|
lst.append(preset)
|
|
351
|
+
|
|
213
352
|
s.setValue(_SETTINGS_KEY, json.dumps(lst))
|
|
214
353
|
s.sync()
|
|
215
354
|
return True
|
|
@@ -218,8 +357,11 @@ def delete_custom_preset(name: str) -> bool:
|
|
|
218
357
|
s = _settings()
|
|
219
358
|
if not s:
|
|
220
359
|
return False
|
|
360
|
+
nm = (name or "").strip().lower()
|
|
361
|
+
if not nm:
|
|
362
|
+
return False
|
|
221
363
|
lst = list_custom_presets()
|
|
222
|
-
lst = [p for p in lst if (p.get("name",
|
|
364
|
+
lst = [p for p in lst if (p.get("name", "").strip().lower() != nm)]
|
|
223
365
|
s.setValue(_SETTINGS_KEY, json.dumps(lst))
|
|
224
366
|
s.sync()
|
|
225
367
|
return True
|
|
@@ -228,7 +370,7 @@ def delete_custom_preset(name: str) -> bool:
|
|
|
228
370
|
# ---------------------- headless apply ----------------------
|
|
229
371
|
def apply_curves_via_preset(main_window, doc, preset: Dict):
|
|
230
372
|
import numpy as _np
|
|
231
|
-
from setiastro.saspro.curves_preset import
|
|
373
|
+
from setiastro.saspro.curves_preset import _unwrap_preset_dict # self
|
|
232
374
|
# lazy import to avoid cycle
|
|
233
375
|
from setiastro.saspro.curve_editor_pro import _apply_mode_any
|
|
234
376
|
|
|
@@ -236,8 +378,7 @@ def apply_curves_via_preset(main_window, doc, preset: Dict):
|
|
|
236
378
|
if img is None:
|
|
237
379
|
return
|
|
238
380
|
|
|
239
|
-
|
|
240
|
-
core_preset = _unwrap_preset_dict(preset or {})
|
|
381
|
+
core = _unwrap_preset_dict(preset or {})
|
|
241
382
|
|
|
242
383
|
arr = _np.asarray(img)
|
|
243
384
|
if arr.dtype.kind in "ui":
|
|
@@ -248,18 +389,52 @@ def apply_curves_via_preset(main_window, doc, preset: Dict):
|
|
|
248
389
|
else:
|
|
249
390
|
arr01 = arr.astype(_np.float32)
|
|
250
391
|
|
|
251
|
-
|
|
392
|
+
# -------- MULTI --------
|
|
393
|
+
if core.get("kind") == "curves_multi" or isinstance(core.get("modes"), dict):
|
|
394
|
+
modes = _coerce_modes_dict(core.get("modes", {}))
|
|
395
|
+
|
|
396
|
+
out01 = arr01
|
|
397
|
+
used_any = False
|
|
398
|
+
|
|
399
|
+
for mode_key in _APPLY_ORDER_KEYS:
|
|
400
|
+
pts = modes.get(mode_key)
|
|
401
|
+
if not pts:
|
|
402
|
+
continue
|
|
403
|
+
if _is_linear_points_norm(pts):
|
|
404
|
+
continue
|
|
405
|
+
|
|
406
|
+
pts_scene = _points_norm_to_scene(pts)
|
|
407
|
+
fn = _interpolator_from_scene_points(pts_scene)
|
|
408
|
+
lut01 = build_curve_lut(fn, size=65536)
|
|
409
|
+
|
|
410
|
+
mode_label = _MODE_KEY_TO_LABEL.get(mode_key, "K (Brightness)")
|
|
411
|
+
out01 = _apply_mode_any(out01, mode_label, lut01)
|
|
412
|
+
used_any = True
|
|
413
|
+
|
|
414
|
+
if not used_any:
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
meta = {
|
|
418
|
+
"step_name": "Curves",
|
|
419
|
+
"mode": _MODE_KEY_TO_LABEL.get(str(core.get("active") or "K"), "K (Brightness)"),
|
|
420
|
+
"preset": dict(core),
|
|
421
|
+
}
|
|
422
|
+
doc.apply_edit(out01, metadata=meta, step_name="Curves")
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
# -------- LEGACY SINGLE --------
|
|
426
|
+
lut01, mode = _lut_from_preset(core)
|
|
252
427
|
out01 = _apply_mode_any(arr01, mode, lut01)
|
|
253
428
|
|
|
254
429
|
meta = {
|
|
255
430
|
"step_name": "Curves",
|
|
256
431
|
"mode": mode,
|
|
257
|
-
"preset": dict(
|
|
432
|
+
"preset": dict(core),
|
|
258
433
|
}
|
|
259
434
|
doc.apply_edit(out01, metadata=meta, step_name="Curves")
|
|
260
435
|
|
|
261
436
|
|
|
262
|
-
|
|
437
|
+
|
|
263
438
|
# ---------------------- open UI with preset ----------------------
|
|
264
439
|
def open_curves_with_preset(main_window, preset: Dict | None = None):
|
|
265
440
|
# lazy import UI to avoid cycle
|
|
@@ -274,20 +449,47 @@ def open_curves_with_preset(main_window, preset: Dict | None = None):
|
|
|
274
449
|
|
|
275
450
|
dlg = CurvesDialogPro(main_window, doc)
|
|
276
451
|
|
|
277
|
-
|
|
278
|
-
|
|
452
|
+
core = _unwrap_preset_dict(preset or {})
|
|
453
|
+
|
|
454
|
+
# -------- MULTI --------
|
|
455
|
+
if core.get("kind") == "curves_multi" or isinstance(core.get("modes"), dict):
|
|
456
|
+
modes = _coerce_modes_dict(core.get("modes", {}))
|
|
457
|
+
|
|
458
|
+
# Fill dialog store for every key
|
|
459
|
+
for k in getattr(dlg, "_curves_store", {}).keys():
|
|
460
|
+
dlg._curves_store[k] = modes.get(k, [(0.0, 0.0), (1.0, 1.0)])
|
|
461
|
+
|
|
462
|
+
# Choose active key
|
|
463
|
+
active_key = str(core.get("active") or "K")
|
|
464
|
+
if active_key not in dlg._curves_store:
|
|
465
|
+
active_key = "K"
|
|
466
|
+
dlg._current_mode_key = active_key
|
|
467
|
+
|
|
468
|
+
# Set the radio to match active
|
|
469
|
+
want_label = _MODE_KEY_TO_LABEL.get(active_key, "K (Brightness)")
|
|
470
|
+
for b in dlg.mode_group.buttons():
|
|
471
|
+
if b.text().lower() == want_label.lower():
|
|
472
|
+
b.setChecked(True)
|
|
473
|
+
break
|
|
474
|
+
|
|
475
|
+
# Push active curve into editor
|
|
476
|
+
dlg._editor_set_from_norm(dlg._curves_store.get(active_key, [(0.0, 0.0), (1.0, 1.0)]))
|
|
477
|
+
dlg._refresh_overlays()
|
|
478
|
+
dlg._quick_preview()
|
|
479
|
+
|
|
480
|
+
dlg.show()
|
|
481
|
+
dlg.raise_()
|
|
482
|
+
dlg.activateWindow()
|
|
483
|
+
return
|
|
279
484
|
|
|
280
|
-
#
|
|
281
|
-
want = _norm_mode(
|
|
485
|
+
# -------- LEGACY SINGLE --------
|
|
486
|
+
want = _norm_mode(core.get("mode"))
|
|
282
487
|
for b in dlg.mode_group.buttons():
|
|
283
488
|
if b.text().lower() == want.lower():
|
|
284
489
|
b.setChecked(True)
|
|
285
490
|
break
|
|
286
491
|
|
|
287
|
-
|
|
288
|
-
pts_scene = _scene_points_from_preset(core_preset)
|
|
289
|
-
|
|
290
|
-
# remove exact endpoints if present; editor expects control handles only
|
|
492
|
+
pts_scene = _scene_points_from_preset(core)
|
|
291
493
|
filt = [(x, y) for (x, y) in pts_scene if x > 0.0 + 1e-6 and x < 360.0 - 1e-6]
|
|
292
494
|
dlg.editor.setControlHandles(filt)
|
|
293
495
|
|
setiastro/saspro/doc_manager.py
CHANGED
|
@@ -2276,6 +2276,7 @@ class DocManager(QObject):
|
|
|
2276
2276
|
bit_depth: str | None = None,
|
|
2277
2277
|
*,
|
|
2278
2278
|
bit_depth_override: str | None = None,
|
|
2279
|
+
jpeg_quality: int | None = None, # <-- NEW
|
|
2279
2280
|
):
|
|
2280
2281
|
"""
|
|
2281
2282
|
Save the given ImageDocument to 'path'.
|
|
@@ -2289,7 +2290,8 @@ class DocManager(QObject):
|
|
|
2289
2290
|
ext = _normalize_ext(os.path.splitext(path)[1])
|
|
2290
2291
|
img = doc.image
|
|
2291
2292
|
meta = doc.metadata or {}
|
|
2292
|
-
|
|
2293
|
+
if jpeg_quality is not None:
|
|
2294
|
+
meta["jpeg_quality"] = int(jpeg_quality)
|
|
2293
2295
|
# ── MASSIVE DEBUG: show everything we know coming in ───────────────
|
|
2294
2296
|
debug_dump_metadata_print(meta, context="save_document: BEFORE HEADER PICK")
|
|
2295
2297
|
|
|
@@ -2359,6 +2361,7 @@ class DocManager(QObject):
|
|
|
2359
2361
|
image_meta=meta.get("image_meta"),
|
|
2360
2362
|
file_meta=meta.get("file_meta"),
|
|
2361
2363
|
wcs_header=meta.get("wcs_header"),
|
|
2364
|
+
jpeg_quality=jpeg_quality,
|
|
2362
2365
|
)
|
|
2363
2366
|
|
|
2364
2367
|
# ── Update metadata in memory to match what we just wrote ─────────
|