setiastrosuitepro 1.6.2__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/rotatearbitrary.png +0 -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 +114 -37
- setiastro/saspro/blemish_blaster.py +4 -1
- setiastro/saspro/blink_comparator_pro.py +548 -275
- 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 +134 -8
- setiastro/saspro/curve_editor_pro.py +109 -42
- setiastro/saspro/doc_manager.py +246 -16
- setiastro/saspro/exoplanet_detector.py +120 -28
- setiastro/saspro/frequency_separation.py +1158 -204
- setiastro/saspro/function_bundle.py +16 -16
- setiastro/saspro/ghs_dialog_pro.py +81 -16
- setiastro/saspro/graxpert.py +1 -0
- setiastro/saspro/gui/main_window.py +519 -289
- setiastro/saspro/gui/mixins/dock_mixin.py +276 -42
- setiastro/saspro/gui/mixins/geometry_mixin.py +105 -5
- setiastro/saspro/gui/mixins/menu_mixin.py +28 -1
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +416 -27
- 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 +748 -255
- 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/remove_stars_preset.py +55 -13
- setiastro/saspro/resources.py +97 -11
- setiastro/saspro/rgbalign.py +4 -0
- setiastro/saspro/selective_color.py +83 -21
- setiastro/saspro/sfcc.py +364 -152
- setiastro/saspro/shortcuts.py +253 -49
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1610 -574
- setiastro/saspro/star_alignment.py +522 -453
- setiastro/saspro/star_spikes.py +4 -0
- setiastro/saspro/star_stretch.py +38 -3
- setiastro/saspro/stat_stretch.py +743 -128
- setiastro/saspro/status_log_dock.py +1 -1
- setiastro/saspro/subwindow.py +786 -360
- setiastro/saspro/supernovaasteroidhunter.py +1 -1
- setiastro/saspro/swap_manager.py +77 -42
- setiastro/saspro/translations/all_source_strings.json +1588 -516
- setiastro/saspro/translations/ar_translations.py +915 -684
- setiastro/saspro/translations/de_translations.py +442 -463
- setiastro/saspro/translations/es_translations.py +277 -47
- setiastro/saspro/translations/fr_translations.py +279 -47
- setiastro/saspro/translations/hi_translations.py +253 -21
- setiastro/saspro/translations/integrate_translations.py +3 -2
- setiastro/saspro/translations/it_translations.py +1211 -161
- setiastro/saspro/translations/ja_translations.py +3340 -3107
- setiastro/saspro/translations/pt_translations.py +3315 -3337
- setiastro/saspro/translations/ru_translations.py +351 -117
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +15902 -138
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +14428 -133
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +11503 -7821
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +11168 -7812
- setiastro/saspro/translations/saspro_hi.qm +0 -0
- setiastro/saspro/translations/saspro_hi.ts +14733 -135
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +14347 -7821
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +14860 -137
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +14904 -137
- setiastro/saspro/translations/saspro_ru.qm +0 -0
- setiastro/saspro/translations/saspro_ru.ts +11766 -168
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +15115 -135
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +11206 -6729
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +10581 -7812
- setiastro/saspro/translations/sw_translations.py +282 -56
- setiastro/saspro/translations/uk_translations.py +264 -35
- setiastro/saspro/translations/zh_translations.py +282 -47
- setiastro/saspro/view_bundle.py +17 -17
- 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/minigame/game.js +11 -6
- setiastro/saspro/widgets/resource_monitor.py +133 -57
- setiastro/saspro/widgets/spinboxes.py +28 -13
- setiastro/saspro/wimi.py +92 -721
- setiastro/saspro/wims.py +46 -36
- setiastro/saspro/window_shelf.py +2 -2
- setiastro/saspro/xisf.py +101 -11
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/METADATA +8 -7
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/RECORD +162 -128
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/license.txt +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#src/setiastro/saspro/luminancerecombine.py
|
|
1
2
|
from __future__ import annotations
|
|
2
3
|
import numpy as np
|
|
3
4
|
import cv2
|
|
@@ -13,12 +14,184 @@ from setiastro.saspro.widgets.image_utils import (
|
|
|
13
14
|
to_float01_strict as _to_float01_strict,
|
|
14
15
|
)
|
|
15
16
|
|
|
16
|
-
# Linear luma weights
|
|
17
17
|
_LUMA_REC709 = np.array([0.2126, 0.7152, 0.0722], dtype=np.float32)
|
|
18
18
|
_LUMA_REC601 = np.array([0.2990, 0.5870, 0.1140], dtype=np.float32)
|
|
19
19
|
_LUMA_REC2020 = np.array([0.2627, 0.6780, 0.0593], dtype=np.float32)
|
|
20
20
|
|
|
21
|
+
# ---- Luma profiles (UI selectable) ----
|
|
22
|
+
# Key = what the UI stores in self.luma_method / preset["mode"]
|
|
23
|
+
# weights must be length-3 (RGB), assumed linear
|
|
24
|
+
LUMA_PROFILES: dict[str, dict] = {
|
|
25
|
+
# --- Standard ---
|
|
26
|
+
"rec709": {"method": "rec709", "weights": _LUMA_REC709, "category": "Standard", "description": "Broadband RGB (Rec.709)"},
|
|
27
|
+
"rec601": {"method": "rec601", "weights": _LUMA_REC601, "category": "Standard", "description": "Rec.601"},
|
|
28
|
+
"rec2020": {"method": "rec2020", "weights": _LUMA_REC2020, "category": "Standard", "description": "Rec.2020"},
|
|
29
|
+
"equal": {"method": "equal", "weights": None, "category": "Standard", "description": "Equal RGB"},
|
|
30
|
+
"max": {"method": "max", "weights": None, "category": "Standard", "description": "Max (Narrowband mappings)"},
|
|
31
|
+
"median": {"method": "median", "weights": None, "category": "Standard", "description": "Median RGB"},
|
|
32
|
+
"snr": {"method": "snr", "weights": None, "category": "Standard", "description": "Unequal Noise (SNR)"},
|
|
33
|
+
|
|
34
|
+
# --- Sensors (examples — paste your whole list here) ---
|
|
35
|
+
"sensor:Sony IMX571 (ASI2600/QHY268)": {
|
|
36
|
+
"method": "custom",
|
|
37
|
+
"weights": np.array([0.2944, 0.5021, 0.2035], dtype=np.float32),
|
|
38
|
+
"category": "Sensors/Sony Modern BSI",
|
|
39
|
+
"description": "Sony IMX571 26MP APS-C BSI (STARVIS)",
|
|
40
|
+
"info": "Gold standard APS-C. Excellent balance for broadband.",
|
|
41
|
+
},
|
|
42
|
+
"sensor:Sony IMX533 (ASI533)": {
|
|
43
|
+
"method": "custom",
|
|
44
|
+
"weights": np.array([0.2910, 0.5072, 0.2018], dtype=np.float32),
|
|
45
|
+
"category": "Sensors/Sony Modern BSI",
|
|
46
|
+
"description": "Sony IMX533 9MP 1\" Square BSI (STARVIS)",
|
|
47
|
+
"info": "Popular square format. Very low noise.",
|
|
48
|
+
},
|
|
49
|
+
"sensor:Sony IMX455 (ASI6200/QHY600)": {
|
|
50
|
+
"weights": (0.2987, 0.5001, 0.2013),
|
|
51
|
+
"description": "Sony IMX455 61MP Full Frame BSI (STARVIS)",
|
|
52
|
+
"info": "Full frame reference sensor.",
|
|
53
|
+
"category": "Sony / Modern BSI",
|
|
54
|
+
},
|
|
55
|
+
"sensor:Sony IMX294 (ASI294)": {
|
|
56
|
+
"weights": (0.3068, 0.5008, 0.1925),
|
|
57
|
+
"description": "Sony IMX294 11.7MP 4/3\" BSI",
|
|
58
|
+
"info": "High sensitivity 4/3 format.",
|
|
59
|
+
"category": "Sony / Modern BSI",
|
|
60
|
+
},
|
|
61
|
+
"sensor:Sony IMX183 (ASI183)": {
|
|
62
|
+
"weights": (0.2967, 0.4983, 0.2050),
|
|
63
|
+
"description": "Sony IMX183 20MP 1\" BSI",
|
|
64
|
+
"info": "High resolution 1-inch sensor.",
|
|
65
|
+
"category": "Sony / Modern BSI",
|
|
66
|
+
},
|
|
67
|
+
"sensor:Sony IMX178 (ASI178)": {
|
|
68
|
+
"weights": (0.2346, 0.5206, 0.2448),
|
|
69
|
+
"description": "Sony IMX178 6.4MP 1/1.8\" BSI",
|
|
70
|
+
"info": "High resolution entry-level sensor.",
|
|
71
|
+
"category": "Sony / Modern BSI",
|
|
72
|
+
},
|
|
73
|
+
"sensor:Sony IMX224 (ASI224)": {
|
|
74
|
+
"weights": (0.3402, 0.4765, 0.1833),
|
|
75
|
+
"description": "Sony IMX224 1.27MP 1/3\" BSI",
|
|
76
|
+
"info": "Classic planetary sensor. High Red response.",
|
|
77
|
+
"category": "Sony / Modern BSI",
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
# --- SONY STARVIS 2 (NIR Optimized) ---
|
|
81
|
+
"sensor:Sony IMX585 (ASI585) - STARVIS 2": {
|
|
82
|
+
"weights": (0.3431, 0.4822, 0.1747),
|
|
83
|
+
"description": "Sony IMX585 8.3MP 1/1.2\" BSI (STARVIS 2)",
|
|
84
|
+
"info": "NIR optimized. Excellent for H-Alpha/Narrowband.",
|
|
85
|
+
"category": "Sony / STARVIS 2",
|
|
86
|
+
},
|
|
87
|
+
"sensor:Sony IMX662 (ASI662) - STARVIS 2": {
|
|
88
|
+
"weights": (0.3430, 0.4821, 0.1749),
|
|
89
|
+
"description": "Sony IMX662 2.1MP 1/2.8\" BSI (STARVIS 2)",
|
|
90
|
+
"info": "Planetary/Guiding. High Red/NIR sensitivity.",
|
|
91
|
+
"category": "Sony / STARVIS 2",
|
|
92
|
+
},
|
|
93
|
+
"sensor:Sony IMX678/715 - STARVIS 2": {
|
|
94
|
+
"weights": (0.3426, 0.4825, 0.1750),
|
|
95
|
+
"description": "Sony IMX678/715 BSI (STARVIS 2)",
|
|
96
|
+
"info": "High resolution planetary/security sensors.",
|
|
97
|
+
"category": "Sony / STARVIS 2",
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
# --- PANASONIC / OTHERS ---
|
|
101
|
+
"sensor:Panasonic MN34230 (ASI1600/QHY163)": {
|
|
102
|
+
"weights": (0.2650, 0.5250, 0.2100),
|
|
103
|
+
"description": "Panasonic MN34230 4/3\" CMOS",
|
|
104
|
+
"info": "Classic Mono/OSC sensor. Optimized weights.",
|
|
105
|
+
"category": "Panasonic",
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
# --- CANON DSLR (Averaged Profiles) ---
|
|
109
|
+
"sensor:Canon EOS (Modern - 60D/6D/R)": {
|
|
110
|
+
"weights": (0.2550, 0.5250, 0.2200),
|
|
111
|
+
"description": "Canon CMOS Profile (Modern)",
|
|
112
|
+
"info": "Balanced profile for most Canon EOS cameras (60D, 6D, 5D, R-series).",
|
|
113
|
+
"category": "Canon",
|
|
114
|
+
},
|
|
115
|
+
"sensor:Canon EOS (Legacy - 300D/40D)": {
|
|
116
|
+
"weights": (0.2400, 0.5400, 0.2200),
|
|
117
|
+
"description": "Canon CMOS Profile (Legacy)",
|
|
118
|
+
"info": "For older Canon models (Digic 2/3 era).",
|
|
119
|
+
"category": "Canon",
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
# --- NIKON DSLR (Averaged Profiles) ---
|
|
123
|
+
"sensor:Nikon DSLR (Modern - D5300/D850)": {
|
|
124
|
+
"weights": (0.2600, 0.5100, 0.2300),
|
|
125
|
+
"description": "Nikon CMOS Profile (Modern)",
|
|
126
|
+
"info": "Balanced profile for Nikon Expeed 4+ cameras.",
|
|
127
|
+
"category": "Nikon",
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
# --- SMART TELESCOPES ---
|
|
131
|
+
"sensor:ZWO Seestar S50": {
|
|
132
|
+
"weights": (0.3333, 0.4866, 0.1801),
|
|
133
|
+
"description": "ZWO Seestar S50 (IMX462)",
|
|
134
|
+
"info": "Specific profile for Seestar S50 smart telescope.",
|
|
135
|
+
"category": "Smart Telescopes",
|
|
136
|
+
},
|
|
137
|
+
"sensor:ZWO Seestar S30": {
|
|
138
|
+
"weights": (0.2928, 0.5053, 0.2019),
|
|
139
|
+
"description": "ZWO Seestar S30",
|
|
140
|
+
"info": "Specific profile for Seestar S30 smart telescope.",
|
|
141
|
+
"category": "Smart Telescopes",
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
21
146
|
# ---------- helpers ----------
|
|
147
|
+
def resolve_luma_profile_weights(mode: str | None):
|
|
148
|
+
"""
|
|
149
|
+
Returns (resolved_method, weights_or_None, profile_name_or_None)
|
|
150
|
+
|
|
151
|
+
- Standard modes return (mode, None or standard weights, None)
|
|
152
|
+
- Sensor profiles return ("custom", weights, <profile display name>)
|
|
153
|
+
"""
|
|
154
|
+
if mode is None:
|
|
155
|
+
mode = "rec709"
|
|
156
|
+
key = str(mode).strip()
|
|
157
|
+
|
|
158
|
+
# common aliases
|
|
159
|
+
alias = {
|
|
160
|
+
"rec.709": "rec709",
|
|
161
|
+
"rec-709": "rec709",
|
|
162
|
+
"rgb": "rec709",
|
|
163
|
+
"k": "rec709",
|
|
164
|
+
"rec.601": "rec601",
|
|
165
|
+
"rec-601": "rec601",
|
|
166
|
+
"rec.2020": "rec2020",
|
|
167
|
+
"rec-2020": "rec2020",
|
|
168
|
+
"nb_max": "max",
|
|
169
|
+
"narrowband": "max",
|
|
170
|
+
"snr_unequal": "snr",
|
|
171
|
+
"unequal_noise": "snr",
|
|
172
|
+
}
|
|
173
|
+
key = alias.get(key.lower(), key)
|
|
174
|
+
|
|
175
|
+
prof = LUMA_PROFILES.get(key)
|
|
176
|
+
if not prof:
|
|
177
|
+
# fallback
|
|
178
|
+
return ("rec709", _LUMA_REC709, None)
|
|
179
|
+
|
|
180
|
+
method = str(prof.get("method", "rec709")).strip().lower()
|
|
181
|
+
w = prof.get("weights", None)
|
|
182
|
+
if w is not None:
|
|
183
|
+
w = np.asarray(w, dtype=np.float32)
|
|
184
|
+
|
|
185
|
+
if key.startswith("sensor:"):
|
|
186
|
+
# Use "custom" path in compute_luminance by passing weights
|
|
187
|
+
# We'll return resolved_method="rec709" (ignored) and weights=w
|
|
188
|
+
# BUT to keep your API simple: return ("rec709", w, profile_name)
|
|
189
|
+
profile_name = key.split("sensor:", 1)[1].strip()
|
|
190
|
+
return ("rec709", w, profile_name)
|
|
191
|
+
|
|
192
|
+
# Standard modes
|
|
193
|
+
return (key, w, None)
|
|
194
|
+
|
|
22
195
|
|
|
23
196
|
def _estimate_noise_sigma_per_channel(img01: np.ndarray) -> np.ndarray:
|
|
24
197
|
# unchanged (but call with strict input)
|
|
@@ -83,6 +256,10 @@ def compute_luminance(
|
|
|
83
256
|
lum = f.max(axis=2)
|
|
84
257
|
elif method == "median":
|
|
85
258
|
lum = np.median(f, axis=2)
|
|
259
|
+
elif method == "rec601":
|
|
260
|
+
lum = np.tensordot(f[..., :3], _LUMA_REC601, axes=([2],[0]))
|
|
261
|
+
elif method == "rec2020":
|
|
262
|
+
lum = np.tensordot(f[..., :3], _LUMA_REC2020, axes=([2],[0]))
|
|
86
263
|
else: # default rec709
|
|
87
264
|
lum = np.tensordot(f[..., :3], _LUMA_REC709, axes=([2],[0]))
|
|
88
265
|
|
|
@@ -159,42 +336,66 @@ def apply_recombine_to_doc(
|
|
|
159
336
|
"""
|
|
160
337
|
base = _to_float01_strict(np.asarray(target_doc.image))
|
|
161
338
|
|
|
162
|
-
#
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
w = np.asarray(weights, dtype=np.float32)
|
|
339
|
+
# Resolve profile (sensor profiles return weights w)
|
|
340
|
+
resolved_method, w, profile_name = resolve_luma_profile_weights(method)
|
|
341
|
+
|
|
342
|
+
# Caller override for weights wins (useful for custom UI / scripts)
|
|
343
|
+
if weights is not None:
|
|
344
|
+
w = np.asarray(weights, dtype=np.float32).reshape(-1)
|
|
169
345
|
if w.size != 3:
|
|
170
|
-
raise ValueError("
|
|
171
|
-
|
|
172
|
-
w =
|
|
346
|
+
raise ValueError("weights must be a 3-element RGB vector")
|
|
347
|
+
elif w is not None:
|
|
348
|
+
w = np.asarray(w, dtype=np.float32).reshape(-1)
|
|
349
|
+
if w.size != 3:
|
|
350
|
+
w = None # ignore bad profile weights defensively
|
|
173
351
|
|
|
174
352
|
# Build L (mono source passes through; RGB is weighted)
|
|
175
353
|
src = _to_float01_strict(luminance_source_img)
|
|
176
354
|
if src.ndim == 2 or (src.ndim == 3 and src.shape[2] == 1):
|
|
177
355
|
L = src if src.ndim == 2 else src[..., 0]
|
|
356
|
+
# For mono L sources, we still want recombine weights to match the selected method/profile.
|
|
178
357
|
else:
|
|
358
|
+
# Noise sigma: if caller provided, use it; otherwise estimate when needed
|
|
179
359
|
ns = None
|
|
180
|
-
if
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
360
|
+
if resolved_method == "snr":
|
|
361
|
+
if noise_sigma is not None:
|
|
362
|
+
ns = np.asarray(noise_sigma, dtype=np.float32).reshape(-1)
|
|
363
|
+
else:
|
|
364
|
+
ns = _estimate_noise_sigma_per_channel(src)
|
|
365
|
+
|
|
366
|
+
# compute_luminance respects weights override; for sensor/custom profiles w is used
|
|
367
|
+
L = compute_luminance(src, method=resolved_method, weights=w, noise_sigma=ns)
|
|
368
|
+
|
|
369
|
+
# For scaling recombine, we need an actual RGB weight vector.
|
|
370
|
+
# If we don't have one from the chosen mode/profile, fall back sensibly.
|
|
371
|
+
if w is not None and w.size == 3:
|
|
372
|
+
recombine_w = w
|
|
373
|
+
else:
|
|
374
|
+
# If your resolver returns w=None for rec709/rec601/rec2020, fill explicitly here:
|
|
375
|
+
if resolved_method == "rec601":
|
|
376
|
+
recombine_w = _LUMA_REC601
|
|
377
|
+
elif resolved_method == "rec2020":
|
|
378
|
+
recombine_w = _LUMA_REC2020
|
|
379
|
+
else:
|
|
380
|
+
recombine_w = _LUMA_REC709
|
|
381
|
+
|
|
382
|
+
replaced = recombine_luminance_linear_scale(
|
|
383
|
+
base,
|
|
384
|
+
L,
|
|
385
|
+
weights=recombine_w,
|
|
386
|
+
blend=float(blend),
|
|
387
|
+
highlight_soft_knee=float(soft_knee),
|
|
196
388
|
)
|
|
197
389
|
|
|
390
|
+
# Metadata
|
|
391
|
+
md = {"step_name": "Recombine Luminance", "luma_method": resolved_method}
|
|
392
|
+
if profile_name:
|
|
393
|
+
md["luma_profile"] = profile_name
|
|
394
|
+
if w is not None:
|
|
395
|
+
md["luma_weights"] = np.asarray(w, dtype=np.float32).tolist()
|
|
396
|
+
|
|
397
|
+
target_doc.apply_edit(replaced.astype(np.float32, copy=False), metadata=md, step_name="Recombine Luminance")
|
|
398
|
+
|
|
198
399
|
|
|
199
400
|
def run_recombine_luminance_via_preset(main_or_ctx, preset=None, target_doc=None):
|
|
200
401
|
"""
|
|
@@ -17,7 +17,7 @@ except Exception:
|
|
|
17
17
|
from PyQt6.QtCore import Qt, QPointF, QRectF, QTimer, QEvent
|
|
18
18
|
from PyQt6.QtGui import (
|
|
19
19
|
QImage, QPixmap, QPainter, QColor, QPen, QBrush,
|
|
20
|
-
QPainterPath, QWheelEvent, QPolygonF
|
|
20
|
+
QPainterPath, QWheelEvent, QPolygonF, QMouseEvent
|
|
21
21
|
)
|
|
22
22
|
from PyQt6.QtWidgets import (
|
|
23
23
|
QInputDialog, QMessageBox, QFileDialog, # QFileDialog only used if you later add “export”
|
|
@@ -32,7 +32,7 @@ from PyQt6.QtWidgets import (
|
|
|
32
32
|
|
|
33
33
|
from .masks_core import MaskLayer
|
|
34
34
|
from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
|
|
35
|
-
|
|
35
|
+
from setiastro.saspro.imageops.stretch import stretch_color_image
|
|
36
36
|
|
|
37
37
|
# ---------- small utils ----------
|
|
38
38
|
|
|
@@ -48,6 +48,38 @@ def _to_qpixmap01(img01: np.ndarray) -> QPixmap:
|
|
|
48
48
|
qimg = QImage(buf.data, w, h, buf.strides[0], QImage.Format.Format_RGB888)
|
|
49
49
|
return QPixmap.fromImage(qimg)
|
|
50
50
|
|
|
51
|
+
def _display_stretch(img01: np.ndarray) -> np.ndarray:
|
|
52
|
+
"""
|
|
53
|
+
Display-only stretch. Does NOT modify underlying data used for mask creation.
|
|
54
|
+
Returns float32 in [0,1].
|
|
55
|
+
"""
|
|
56
|
+
a = np.asarray(img01, dtype=np.float32)
|
|
57
|
+
a = np.clip(a, 0.0, 1.0)
|
|
58
|
+
|
|
59
|
+
# Color: use your existing stretch if available
|
|
60
|
+
if a.ndim == 3 and a.shape[2] == 3 and stretch_color_image is not None:
|
|
61
|
+
try:
|
|
62
|
+
return np.clip(stretch_color_image(a, 0.25, linked=False, normalize=False), 0.0, 1.0).astype(np.float32)
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
# Mono (or fallback): simple robust stretch around median
|
|
67
|
+
# (keeps it predictable and fast; display-only)
|
|
68
|
+
m = float(np.nanmedian(a))
|
|
69
|
+
if not np.isfinite(m):
|
|
70
|
+
return a.astype(np.float32, copy=False)
|
|
71
|
+
|
|
72
|
+
# Simple gamma-like lift using median anchor
|
|
73
|
+
# If median is tiny, boost; if already bright, minimal change.
|
|
74
|
+
target = 0.25
|
|
75
|
+
eps = 1e-8
|
|
76
|
+
scale = target / max(m, eps)
|
|
77
|
+
out = np.clip(a * scale, 0.0, 1.0)
|
|
78
|
+
|
|
79
|
+
# Gentle midtone curve
|
|
80
|
+
out = np.sqrt(out)
|
|
81
|
+
return np.clip(out, 0.0, 1.0).astype(np.float32, copy=False)
|
|
82
|
+
|
|
51
83
|
|
|
52
84
|
def _find_main_window(w):
|
|
53
85
|
p = w
|
|
@@ -248,10 +280,14 @@ class MaskCanvas(QGraphicsView):
|
|
|
248
280
|
super().__init__(parent)
|
|
249
281
|
self.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
250
282
|
|
|
283
|
+
self._base_image01 = np.asarray(image01, dtype=np.float32)
|
|
284
|
+
self._display_stretch_enabled = False
|
|
285
|
+
|
|
251
286
|
# scene + background image
|
|
252
287
|
self.scene = QGraphicsScene(self)
|
|
253
288
|
self.setScene(self.scene)
|
|
254
|
-
|
|
289
|
+
|
|
290
|
+
self.bg_item = QGraphicsPixmapItem(_to_qpixmap01(self._base_image01))
|
|
255
291
|
self.scene.addItem(self.bg_item)
|
|
256
292
|
|
|
257
293
|
# --- NEW: basic zoom state ---
|
|
@@ -306,6 +342,38 @@ class MaskCanvas(QGraphicsView):
|
|
|
306
342
|
super().wheelEvent(ev)
|
|
307
343
|
# ----------------- END: Zoom API ---------------------
|
|
308
344
|
|
|
345
|
+
def set_display_stretch_enabled(self, enabled: bool):
|
|
346
|
+
enabled = bool(enabled)
|
|
347
|
+
if enabled == self._display_stretch_enabled:
|
|
348
|
+
return
|
|
349
|
+
self._display_stretch_enabled = enabled
|
|
350
|
+
self._refresh_background_pixmap(keep_view=True)
|
|
351
|
+
|
|
352
|
+
def display_stretch_enabled(self) -> bool:
|
|
353
|
+
return bool(self._display_stretch_enabled)
|
|
354
|
+
|
|
355
|
+
def current_display_image01(self) -> np.ndarray:
|
|
356
|
+
"""Returns the image currently used for *display* (not for mask math)."""
|
|
357
|
+
if self._display_stretch_enabled:
|
|
358
|
+
return _display_stretch(self._base_image01)
|
|
359
|
+
return self._base_image01
|
|
360
|
+
|
|
361
|
+
def _refresh_background_pixmap(self, keep_view: bool = True):
|
|
362
|
+
# Preserve current view transform/center so toggling doesn't “jump”
|
|
363
|
+
old_transform = self.transform()
|
|
364
|
+
old_center = self.mapToScene(self.viewport().rect().center())
|
|
365
|
+
|
|
366
|
+
disp = self.current_display_image01()
|
|
367
|
+
self.bg_item.setPixmap(_to_qpixmap01(disp))
|
|
368
|
+
|
|
369
|
+
# Ensure scene rect still matches image pixels
|
|
370
|
+
self.setSceneRect(self.bg_item.boundingRect())
|
|
371
|
+
|
|
372
|
+
if keep_view:
|
|
373
|
+
self.setTransform(old_transform)
|
|
374
|
+
self.centerOn(old_center)
|
|
375
|
+
|
|
376
|
+
|
|
309
377
|
def set_mode(self, mode: str):
|
|
310
378
|
assert mode in ('polygon', 'ellipse', 'select')
|
|
311
379
|
self.mode = mode
|
|
@@ -453,9 +521,10 @@ class LivePreviewDialog(QDialog):
|
|
|
453
521
|
Qt.AspectRatioMode.KeepAspectRatio,
|
|
454
522
|
Qt.TransformationMode.SmoothTransformation))
|
|
455
523
|
|
|
524
|
+
def set_base_image(self, image01: np.ndarray):
|
|
525
|
+
self.base_pixmap = _to_qpixmap01(image01)
|
|
456
526
|
|
|
457
527
|
# ---------- Preview (push-as-doc) ----------
|
|
458
|
-
|
|
459
528
|
class MaskPreviewDialog(QDialog):
|
|
460
529
|
"""Scrollable preview + 'Push as New Document…'."""
|
|
461
530
|
def __init__(self, mask01: np.ndarray, parent=None):
|
|
@@ -463,29 +532,50 @@ class MaskPreviewDialog(QDialog):
|
|
|
463
532
|
self.setWindowTitle(self.tr("Mask Preview"))
|
|
464
533
|
self.mask = np.clip(mask01, 0, 1).astype(np.float32)
|
|
465
534
|
|
|
466
|
-
|
|
535
|
+
# --- drag-pan state ---
|
|
536
|
+
self._dragging = False
|
|
537
|
+
self._drag_start = None
|
|
538
|
+
self._h_start = 0
|
|
539
|
+
self._v_start = 0
|
|
540
|
+
|
|
541
|
+
# Build UI first
|
|
542
|
+
self.scroll = QScrollArea(self)
|
|
543
|
+
self.scroll.setWidgetResizable(False)
|
|
467
544
|
self.scroll.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
545
|
+
|
|
468
546
|
self.label = QLabel(alignment=Qt.AlignmentFlag.AlignCenter)
|
|
469
547
|
self.label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored)
|
|
470
|
-
|
|
548
|
+
|
|
549
|
+
self.pixmap = self._to_pixmap(self.mask)
|
|
550
|
+
self.label.setPixmap(self.pixmap)
|
|
551
|
+
self.label.resize(self.pixmap.size())
|
|
552
|
+
|
|
471
553
|
self.scroll.setWidget(self.label)
|
|
472
554
|
|
|
555
|
+
# Enable mouse drag panning on the label (NOW label exists)
|
|
556
|
+
self.label.setMouseTracking(True)
|
|
557
|
+
self.label.installEventFilter(self)
|
|
558
|
+
|
|
473
559
|
btns = QHBoxLayout()
|
|
474
560
|
b_in = themed_toolbtn("zoom-in", "Zoom In")
|
|
475
561
|
b_out = themed_toolbtn("zoom-out", "Zoom Out")
|
|
476
562
|
b_fit = themed_toolbtn("zoom-fit-best", "Fit to Preview")
|
|
477
|
-
|
|
478
|
-
|
|
479
563
|
b_push = QPushButton(self.tr("Push as New Document…"))
|
|
564
|
+
|
|
480
565
|
b_in.clicked.connect(lambda: self._zoom(1.2))
|
|
481
566
|
b_out.clicked.connect(lambda: self._zoom(1/1.2))
|
|
482
567
|
b_fit.clicked.connect(self._fit)
|
|
483
568
|
b_push.clicked.connect(self.push_as_new_document)
|
|
569
|
+
|
|
484
570
|
for b in (b_in, b_out, b_fit, b_push):
|
|
485
571
|
btns.addWidget(b)
|
|
486
572
|
|
|
487
|
-
lay = QVBoxLayout(self)
|
|
488
|
-
|
|
573
|
+
lay = QVBoxLayout(self)
|
|
574
|
+
lay.addWidget(self.scroll)
|
|
575
|
+
lay.addLayout(btns)
|
|
576
|
+
|
|
577
|
+
self.scale = 1.0
|
|
578
|
+
self.setMinimumSize(600, 400)
|
|
489
579
|
|
|
490
580
|
def _to_pixmap(self, mask01: np.ndarray) -> QPixmap:
|
|
491
581
|
m8 = (np.clip(mask01, 0, 1) * 255).astype(np.uint8)
|
|
@@ -495,17 +585,52 @@ class MaskPreviewDialog(QDialog):
|
|
|
495
585
|
|
|
496
586
|
def _zoom(self, factor: float):
|
|
497
587
|
self.scale *= factor
|
|
498
|
-
scaled = self.pixmap.scaled(
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
588
|
+
scaled = self.pixmap.scaled(
|
|
589
|
+
self.pixmap.size() * self.scale,
|
|
590
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
|
591
|
+
Qt.TransformationMode.SmoothTransformation
|
|
592
|
+
)
|
|
593
|
+
self.label.setPixmap(scaled)
|
|
594
|
+
self.label.resize(scaled.size())
|
|
502
595
|
|
|
503
596
|
def _fit(self):
|
|
504
597
|
vp = self.scroll.viewport().size()
|
|
505
598
|
if self.pixmap.width() and self.pixmap.height():
|
|
506
599
|
s = min(vp.width()/self.pixmap.width(), vp.height()/self.pixmap.height())
|
|
507
600
|
self.scale = max(0.05, s)
|
|
508
|
-
|
|
601
|
+
# re-render at the new scale (don’t multiply again)
|
|
602
|
+
scaled = self.pixmap.scaled(
|
|
603
|
+
self.pixmap.size() * self.scale,
|
|
604
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
|
605
|
+
Qt.TransformationMode.SmoothTransformation
|
|
606
|
+
)
|
|
607
|
+
self.label.setPixmap(scaled)
|
|
608
|
+
self.label.resize(scaled.size())
|
|
609
|
+
|
|
610
|
+
def eventFilter(self, obj, ev):
|
|
611
|
+
if obj is self.label:
|
|
612
|
+
if ev.type() == QEvent.Type.MouseButtonPress and ev.button() == Qt.MouseButton.LeftButton:
|
|
613
|
+
self._dragging = True
|
|
614
|
+
self._drag_start = ev.globalPosition().toPoint()
|
|
615
|
+
self._h_start = self.scroll.horizontalScrollBar().value()
|
|
616
|
+
self._v_start = self.scroll.verticalScrollBar().value()
|
|
617
|
+
self.setCursor(Qt.CursorShape.ClosedHandCursor)
|
|
618
|
+
return True
|
|
619
|
+
|
|
620
|
+
if ev.type() == QEvent.Type.MouseMove and self._dragging:
|
|
621
|
+
p = ev.globalPosition().toPoint()
|
|
622
|
+
d = p - self._drag_start
|
|
623
|
+
self.scroll.horizontalScrollBar().setValue(self._h_start - d.x())
|
|
624
|
+
self.scroll.verticalScrollBar().setValue(self._v_start - d.y())
|
|
625
|
+
return True
|
|
626
|
+
|
|
627
|
+
if ev.type() == QEvent.Type.MouseButtonRelease and ev.button() == Qt.MouseButton.LeftButton:
|
|
628
|
+
self._dragging = False
|
|
629
|
+
self._drag_start = None
|
|
630
|
+
self.unsetCursor()
|
|
631
|
+
return True
|
|
632
|
+
|
|
633
|
+
return super().eventFilter(obj, ev)
|
|
509
634
|
|
|
510
635
|
def push_as_new_document(self):
|
|
511
636
|
if self.mask is None:
|
|
@@ -558,6 +683,10 @@ class MaskCreationDialog(QDialog):
|
|
|
558
683
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
559
684
|
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
560
685
|
self.setModal(False)
|
|
686
|
+
try:
|
|
687
|
+
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
688
|
+
except Exception:
|
|
689
|
+
pass # older PyQt6 versions
|
|
561
690
|
self.image = np.asarray(image01, dtype=np.float32).copy()
|
|
562
691
|
self.mask: np.ndarray | None = None
|
|
563
692
|
self.live_preview = LivePreviewDialog(self.image, parent=self)
|
|
@@ -601,6 +730,18 @@ class MaskCreationDialog(QDialog):
|
|
|
601
730
|
zoom_bar.addWidget(z_out); zoom_bar.addWidget(z_in); zoom_bar.addWidget(z_fit)
|
|
602
731
|
layout.addLayout(zoom_bar)
|
|
603
732
|
|
|
733
|
+
# Display stretch toggle (display-only; never modifies image data)
|
|
734
|
+
self.btn_disp_stretch = QPushButton(self.tr("Toggle Display Stretch"))
|
|
735
|
+
self.btn_disp_stretch.setCheckable(True)
|
|
736
|
+
self.btn_disp_stretch.setToolTip(
|
|
737
|
+
"Display-only stretch for easier masking on linear images.\n"
|
|
738
|
+
"This does NOT change the image data or the generated mask."
|
|
739
|
+
)
|
|
740
|
+
self.btn_disp_stretch.toggled.connect(self._toggle_display_stretch)
|
|
741
|
+
self.btn_disp_stretch.setChecked(False)
|
|
742
|
+
self.btn_disp_stretch.setText("Enable Display Stretch")
|
|
743
|
+
zoom_bar.addWidget(self.btn_disp_stretch)
|
|
744
|
+
|
|
604
745
|
# Canvas
|
|
605
746
|
self.canvas = MaskCanvas(self.image)
|
|
606
747
|
layout.addWidget(self.canvas, 1)
|
|
@@ -707,6 +848,24 @@ class MaskCreationDialog(QDialog):
|
|
|
707
848
|
if self.link_cb.isChecked():
|
|
708
849
|
self.upper_sl.setValue(v)
|
|
709
850
|
|
|
851
|
+
def _toggle_display_stretch(self, enabled: bool):
|
|
852
|
+
try:
|
|
853
|
+
self.canvas.set_display_stretch_enabled(bool(enabled))
|
|
854
|
+
|
|
855
|
+
# keep button label in sync
|
|
856
|
+
self.btn_disp_stretch.setText(
|
|
857
|
+
self.tr("Disable Display Stretch") if enabled else self.tr("Enable Display Stretch")
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
# Keep the live preview background in sync (Range Selection uses it)
|
|
861
|
+
if hasattr(self, "live_preview") and self.live_preview is not None:
|
|
862
|
+
self.live_preview.set_base_image(self.canvas.current_display_image01())
|
|
863
|
+
if self.live_preview.isVisible():
|
|
864
|
+
self._update_live_preview()
|
|
865
|
+
except Exception:
|
|
866
|
+
pass
|
|
867
|
+
|
|
868
|
+
|
|
710
869
|
# ---- generators
|
|
711
870
|
def _component_lightness(self) -> np.ndarray:
|
|
712
871
|
if self.image.ndim == 3:
|