setiastrosuitepro 1.7.3__py3-none-any.whl → 1.7.5__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/clonestamp.png +0 -0
- setiastro/saspro/__init__.py +15 -4
- setiastro/saspro/__main__.py +23 -5
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/abe.py +4 -4
- setiastro/saspro/autostretch.py +29 -18
- setiastro/saspro/blemish_blaster.py +54 -14
- setiastro/saspro/clone_stamp.py +753 -0
- setiastro/saspro/gui/main_window.py +27 -6
- setiastro/saspro/gui/mixins/menu_mixin.py +1 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +10 -15
- setiastro/saspro/legacy/numba_utils.py +301 -119
- setiastro/saspro/numba_utils.py +998 -270
- setiastro/saspro/ops/settings.py +6 -6
- setiastro/saspro/pixelmath.py +1 -1
- setiastro/saspro/planetprojection.py +310 -105
- setiastro/saspro/resources.py +2 -0
- setiastro/saspro/sfcc.py +14 -8
- setiastro/saspro/stacking_suite.py +413 -174
- setiastro/saspro/subwindow.py +28 -35
- setiastro/saspro/translations/all_source_strings.json +2 -2
- setiastro/saspro/translations/ar_translations.py +3 -3
- setiastro/saspro/translations/de_translations.py +2 -2
- setiastro/saspro/translations/es_translations.py +2 -2
- setiastro/saspro/translations/fr_translations.py +2 -2
- setiastro/saspro/translations/hi_translations.py +2 -2
- setiastro/saspro/translations/it_translations.py +2 -2
- setiastro/saspro/translations/ja_translations.py +2 -2
- setiastro/saspro/translations/pt_translations.py +2 -2
- setiastro/saspro/translations/ru_translations.py +2 -2
- setiastro/saspro/translations/saspro_ar.ts +2 -2
- setiastro/saspro/translations/saspro_de.ts +4 -4
- setiastro/saspro/translations/saspro_es.ts +2 -2
- setiastro/saspro/translations/saspro_fr.ts +2 -2
- setiastro/saspro/translations/saspro_hi.ts +2 -2
- setiastro/saspro/translations/saspro_it.ts +4 -4
- setiastro/saspro/translations/saspro_ja.ts +2 -2
- setiastro/saspro/translations/saspro_pt.ts +2 -2
- setiastro/saspro/translations/saspro_ru.ts +2 -2
- setiastro/saspro/translations/saspro_sw.ts +2 -2
- setiastro/saspro/translations/saspro_uk.ts +2 -2
- setiastro/saspro/translations/saspro_zh.ts +2 -2
- setiastro/saspro/translations/sw_translations.py +2 -2
- setiastro/saspro/translations/uk_translations.py +2 -2
- setiastro/saspro/translations/zh_translations.py +2 -2
- setiastro/saspro/window_shelf.py +62 -1
- {setiastrosuitepro-1.7.3.dist-info → setiastrosuitepro-1.7.5.dist-info}/METADATA +1 -1
- {setiastrosuitepro-1.7.3.dist-info → setiastrosuitepro-1.7.5.dist-info}/RECORD +52 -50
- {setiastrosuitepro-1.7.3.dist-info → setiastrosuitepro-1.7.5.dist-info}/entry_points.txt +1 -1
- {setiastrosuitepro-1.7.3.dist-info → setiastrosuitepro-1.7.5.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.7.3.dist-info → setiastrosuitepro-1.7.5.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.7.3.dist-info → setiastrosuitepro-1.7.5.dist-info}/licenses/license.txt +0 -0
setiastro/saspro/ops/settings.py
CHANGED
|
@@ -96,8 +96,8 @@ class SettingsDialog(QDialog):
|
|
|
96
96
|
self.sp_min_alt = QDoubleSpinBox(); self.sp_min_alt.setRange(0.0, 90.0); self.sp_min_alt.setDecimals(1)
|
|
97
97
|
self.sp_obj_limit = QSpinBox(); self.sp_obj_limit.setRange(1, 1000)
|
|
98
98
|
|
|
99
|
-
self.
|
|
100
|
-
self.
|
|
99
|
+
self.chk_autostretch_24bit = QCheckBox(self.tr("High-quality autostretch (24-bit; better gradients)"))
|
|
100
|
+
self.chk_autostretch_24bit.setToolTip(self.tr("Compute autostretch on a 24-bit histogram (smoother gradients)."))
|
|
101
101
|
|
|
102
102
|
self.slider_bg_opacity = QSlider(Qt.Orientation.Horizontal)
|
|
103
103
|
self.slider_bg_opacity.setRange(0, 100)
|
|
@@ -178,7 +178,7 @@ class SettingsDialog(QDialog):
|
|
|
178
178
|
|
|
179
179
|
# ---- Display (moved under Theme) ----
|
|
180
180
|
left_col.addRow(QLabel(self.tr("<b>Display</b>")))
|
|
181
|
-
left_col.addRow(self.
|
|
181
|
+
left_col.addRow(self.chk_autostretch_24bit)
|
|
182
182
|
left_col.addRow(self.tr("Background Opacity:"), w_bg_opacity)
|
|
183
183
|
left_col.addRow(self.tr("Background Image:"), w_bg_image)
|
|
184
184
|
|
|
@@ -296,8 +296,8 @@ class SettingsDialog(QDialog):
|
|
|
296
296
|
self.sp_obj_limit.setValue(self.settings.value("object_limit", 100, type=int))
|
|
297
297
|
|
|
298
298
|
# Display
|
|
299
|
-
self.
|
|
300
|
-
self.settings.value("display/
|
|
299
|
+
self.chk_autostretch_24bit.setChecked(
|
|
300
|
+
self.settings.value("display/autostretch_24bit", True, type=bool)
|
|
301
301
|
)
|
|
302
302
|
|
|
303
303
|
current_opacity = self.settings.value("display/bg_opacity", 50, type=int)
|
|
@@ -464,7 +464,7 @@ class SettingsDialog(QDialog):
|
|
|
464
464
|
# Updates + Display
|
|
465
465
|
self.settings.setValue("updates/check_on_startup", self.chk_updates_startup.isChecked())
|
|
466
466
|
self.settings.setValue("updates/url", self.le_updates_url.text().strip())
|
|
467
|
-
self.settings.setValue("display/
|
|
467
|
+
self.settings.setValue("display/autostretch_24bit", self.chk_autostretch_24bit.isChecked())
|
|
468
468
|
|
|
469
469
|
# Custom background: persist the chosen path (empty -> remove)
|
|
470
470
|
bg_path = (self.le_bg_path.text() or "").strip()
|
setiastro/saspro/pixelmath.py
CHANGED
|
@@ -1179,7 +1179,7 @@ class PixelMathDialogPro(QDialog):
|
|
|
1179
1179
|
target_median=float(getattr(self, "_as_target", 0.25)),
|
|
1180
1180
|
linked=bool(getattr(self, "_as_linked", True)),
|
|
1181
1181
|
sigma=float(getattr(self, "_as_sigma", 3.0)),
|
|
1182
|
-
|
|
1182
|
+
use_24bit=bool(getattr(self, "_as_24bit", True)),
|
|
1183
1183
|
)
|
|
1184
1184
|
|
|
1185
1185
|
self._set_preview_image(out)
|
|
@@ -1022,152 +1022,261 @@ def export_planet_sphere_html(
|
|
|
1022
1022
|
|
|
1023
1023
|
return html, out_path
|
|
1024
1024
|
|
|
1025
|
+
|
|
1025
1026
|
def export_pseudo_surface_html(
|
|
1026
1027
|
rgb: np.ndarray,
|
|
1027
1028
|
out_path: str | None = None,
|
|
1028
1029
|
*,
|
|
1029
|
-
title: str = "Pseudo Surface (
|
|
1030
|
-
# mesh size control (trade quality vs HTML size)
|
|
1030
|
+
title: str = "Pseudo Surface (Point Cloud)",
|
|
1031
1031
|
max_dim: int = 420,
|
|
1032
|
-
|
|
1033
|
-
z_scale: float = 0.35, # relative to min(W,H) after downsample
|
|
1032
|
+
z_scale: float = 0.35,
|
|
1034
1033
|
depth_gamma: float = 1.15,
|
|
1035
1034
|
blur_sigma: float = 1.2,
|
|
1036
1035
|
invert: bool = False,
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1036
|
+
block: int = 10,
|
|
1037
|
+
block_blur_sigma: float = 0.6,
|
|
1038
|
+
max_vertices: int = 250_000,
|
|
1039
|
+
point_size: float = 1.6,
|
|
1040
|
+
|
|
1041
|
+
# height source(s)
|
|
1042
|
+
height_from: str = "brightness", # "brightness" | "color" | "dual"
|
|
1043
|
+
|
|
1044
|
+
# how points are COLORED (from dropdown)
|
|
1045
|
+
color_mode: str = "brightness", # "brightness" | "depth" | "dual"
|
|
1046
|
+
depth_colorscale: str = "Turbo",
|
|
1047
|
+
depth_opacity: float = 0.55,
|
|
1048
|
+
depth_point_size: float | None = None,
|
|
1049
|
+
|
|
1050
|
+
# Dual height controls (saturation-height layer)
|
|
1051
|
+
sat_opacity: float = 0.45,
|
|
1052
|
+
sat_point_size: float | None = None,
|
|
1053
|
+
|
|
1054
|
+
# Optional: reduce saturation-height noise in dark background
|
|
1055
|
+
sat_luma_gate: float = 0.02,
|
|
1056
|
+
sat_luma_soft: float = 0.18,
|
|
1042
1057
|
):
|
|
1043
1058
|
"""
|
|
1044
|
-
Interactive Plotly
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1059
|
+
Interactive Plotly Scatter3d point cloud.
|
|
1060
|
+
|
|
1061
|
+
Z (height_from):
|
|
1062
|
+
• "brightness": luminance-derived height
|
|
1063
|
+
• "color": saturation-derived height
|
|
1064
|
+
• "dual": overlays TWO clouds (brightness-height + saturation-height)
|
|
1048
1065
|
|
|
1049
|
-
|
|
1066
|
+
Coloring (color_mode) is ALWAYS honored, even for dual heights:
|
|
1067
|
+
• "brightness": RGB image colors
|
|
1068
|
+
• "depth": colormap by height
|
|
1069
|
+
• "dual": RGB + depth overlay (per height cloud)
|
|
1050
1070
|
"""
|
|
1051
1071
|
import os
|
|
1052
1072
|
import numpy as np
|
|
1073
|
+
import cv2
|
|
1053
1074
|
import plotly.graph_objects as go
|
|
1054
1075
|
|
|
1055
1076
|
x = np.asarray(rgb)
|
|
1056
1077
|
if x.ndim != 3 or x.shape[2] < 3:
|
|
1057
1078
|
raise ValueError("export_pseudo_surface_html expects RGB image (H,W,3).")
|
|
1058
1079
|
|
|
1080
|
+
# normalize mode strings
|
|
1081
|
+
cmode = (color_mode or "brightness").strip().lower()
|
|
1082
|
+
if cmode not in ("brightness", "depth", "dual"):
|
|
1083
|
+
cmode = "brightness"
|
|
1084
|
+
|
|
1085
|
+
hmode = (height_from or "brightness").strip().lower()
|
|
1086
|
+
if hmode not in ("brightness", "color", "dual"):
|
|
1087
|
+
hmode = "brightness"
|
|
1088
|
+
|
|
1059
1089
|
# ---- float01 ----
|
|
1060
1090
|
if x.dtype == np.uint8:
|
|
1061
1091
|
img01 = x[..., :3].astype(np.float32) / 255.0
|
|
1062
1092
|
elif x.dtype == np.uint16:
|
|
1063
1093
|
img01 = x[..., :3].astype(np.float32) / 65535.0
|
|
1064
1094
|
else:
|
|
1065
|
-
img01 = x[..., :3].astype(np.float32, copy=False)
|
|
1066
|
-
img01 = np.clip(img01, 0.0, 1.0)
|
|
1095
|
+
img01 = np.clip(x[..., :3].astype(np.float32, copy=False), 0.0, 1.0)
|
|
1067
1096
|
|
|
1068
1097
|
H, W = img01.shape[:2]
|
|
1069
1098
|
|
|
1070
|
-
# ---- downsample
|
|
1071
|
-
|
|
1072
|
-
max_dim = int(np.clip(max_dim, 128, 900))
|
|
1073
|
-
|
|
1099
|
+
# ---- downsample ----
|
|
1100
|
+
max_dim = int(np.clip(max_dim, 128, 2048))
|
|
1074
1101
|
s = float(max_dim) / float(max(H, W))
|
|
1075
1102
|
if s < 1.0:
|
|
1076
1103
|
newW = max(64, int(round(W * s)))
|
|
1077
1104
|
newH = max(64, int(round(H * s)))
|
|
1078
|
-
|
|
1079
|
-
else:
|
|
1080
|
-
img01_ds = img01
|
|
1105
|
+
img01 = cv2.resize(img01, (newW, newH), interpolation=cv2.INTER_AREA)
|
|
1081
1106
|
|
|
1082
|
-
hH, hW =
|
|
1107
|
+
hH, hW = img01.shape[:2]
|
|
1083
1108
|
|
|
1084
|
-
# ----
|
|
1109
|
+
# ---- vertex cap ----
|
|
1110
|
+
max_vertices = int(max(10_000, max_vertices))
|
|
1085
1111
|
if hH * hW > max_vertices:
|
|
1086
1112
|
scale = np.sqrt(float(max_vertices) / float(hH * hW))
|
|
1087
1113
|
newW = max(64, int(round(hW * scale)))
|
|
1088
1114
|
newH = max(64, int(round(hH * scale)))
|
|
1089
|
-
|
|
1090
|
-
hH, hW =
|
|
1091
|
-
|
|
1092
|
-
# ----
|
|
1093
|
-
|
|
1115
|
+
img01 = cv2.resize(img01, (newW, newH), interpolation=cv2.INTER_AREA)
|
|
1116
|
+
hH, hW = img01.shape[:2]
|
|
1117
|
+
|
|
1118
|
+
# ---- helpers ----
|
|
1119
|
+
def _robust01(base: np.ndarray) -> np.ndarray:
|
|
1120
|
+
base = base.astype(np.float32, copy=False)
|
|
1121
|
+
p_lo = float(np.percentile(base, 1.0))
|
|
1122
|
+
p_hi = float(np.percentile(base, 99.5))
|
|
1123
|
+
h = np.clip((base - p_lo) / max(p_hi - p_lo, 1e-9), 0.0, 1.0)
|
|
1124
|
+
|
|
1125
|
+
if invert:
|
|
1126
|
+
h = 1.0 - h
|
|
1127
|
+
|
|
1128
|
+
# coherence smoothing
|
|
1129
|
+
b = max(1, int(block))
|
|
1130
|
+
if b > 1:
|
|
1131
|
+
h = cv2.blur(h, (b, b), borderType=cv2.BORDER_REFLECT101)
|
|
1132
|
+
|
|
1133
|
+
if block_blur_sigma and block_blur_sigma > 0:
|
|
1134
|
+
h = cv2.GaussianBlur(h, (0, 0), float(block_blur_sigma))
|
|
1135
|
+
|
|
1136
|
+
if blur_sigma and blur_sigma > 0:
|
|
1137
|
+
h = cv2.GaussianBlur(h, (0, 0), float(blur_sigma))
|
|
1138
|
+
|
|
1139
|
+
h = np.clip(h, 0.0, 1.0) ** max(1e-3, float(depth_gamma))
|
|
1140
|
+
return h.astype(np.float32)
|
|
1141
|
+
|
|
1142
|
+
# luminance (also used for saturation gating)
|
|
1143
|
+
lum = (0.299 * img01[..., 0] + 0.587 * img01[..., 1] + 0.114 * img01[..., 2]).astype(np.float32)
|
|
1144
|
+
h01_lum = _robust01(lum)
|
|
1145
|
+
|
|
1146
|
+
# saturation
|
|
1147
|
+
hsv = cv2.cvtColor(img01.astype(np.float32), cv2.COLOR_RGB2HSV)
|
|
1148
|
+
sat = hsv[..., 1].astype(np.float32)
|
|
1149
|
+
h01_sat = _robust01(sat)
|
|
1150
|
+
|
|
1151
|
+
# Optional: suppress sat-height in very dark background
|
|
1152
|
+
if sat_luma_soft and sat_luma_soft > 0:
|
|
1153
|
+
gate = float(sat_luma_gate)
|
|
1154
|
+
soft = float(sat_luma_soft)
|
|
1155
|
+
wgt = np.clip((lum - gate) / max(soft, 1e-6), 0.0, 1.0).astype(np.float32)
|
|
1156
|
+
h01_sat = h01_sat * wgt
|
|
1157
|
+
|
|
1158
|
+
# ---- depth scaling ----
|
|
1159
|
+
zmax = 0.5 * float(min(hH, hW)) * float(z_scale)
|
|
1160
|
+
Z_lum = ((h01_lum * 2.0) - 1.0) * zmax
|
|
1161
|
+
Z_sat = ((h01_sat * 2.0) - 1.0) * zmax
|
|
1162
|
+
|
|
1163
|
+
# ---- XY grid ----
|
|
1164
|
+
yy, xx = np.mgrid[0:hH, 0:hW]
|
|
1165
|
+
X = (xx - (hW - 1) * 0.5).reshape(-1)
|
|
1166
|
+
Y = (yy - (hH - 1) * 0.5).reshape(-1)
|
|
1094
1167
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
p_hi = float(np.percentile(lum, 99.5))
|
|
1098
|
-
if p_hi <= p_lo + 1e-9:
|
|
1099
|
-
h01 = np.clip(lum, 0.0, 1.0)
|
|
1100
|
-
else:
|
|
1101
|
-
h01 = (lum - p_lo) / (p_hi - p_lo)
|
|
1102
|
-
h01 = np.clip(h01, 0.0, 1.0)
|
|
1168
|
+
Zlum = Z_lum.reshape(-1)
|
|
1169
|
+
Zsat = Z_sat.reshape(-1)
|
|
1103
1170
|
|
|
1104
|
-
|
|
1105
|
-
|
|
1171
|
+
# ---- RGB strings ----
|
|
1172
|
+
rgb_u8 = np.clip(img01.reshape(-1, 3) * 255.0, 0, 255).astype(np.uint8)
|
|
1173
|
+
rgb_strings = [f"rgb({r},{g},{b})" for r, g, b in rgb_u8]
|
|
1106
1174
|
|
|
1107
|
-
|
|
1108
|
-
# each block shares one height value (10x10 average by default)
|
|
1109
|
-
b = int(max(1, block))
|
|
1110
|
-
if b > 1:
|
|
1111
|
-
# cv2.blur is a fast box filter (mean filter)
|
|
1112
|
-
h01 = cv2.blur(h01, (b, b), borderType=cv2.BORDER_REFLECT101)
|
|
1175
|
+
traces: list = []
|
|
1113
1176
|
|
|
1114
|
-
#
|
|
1115
|
-
|
|
1116
|
-
|
|
1177
|
+
# sizes / opacities per cloud
|
|
1178
|
+
ps_lum = float(point_size)
|
|
1179
|
+
ps_sat = float(sat_point_size) if sat_point_size is not None else float(point_size) * 1.05
|
|
1180
|
+
op_lum = 0.95
|
|
1181
|
+
op_sat = float(np.clip(sat_opacity, 0.05, 1.0))
|
|
1117
1182
|
|
|
1118
|
-
|
|
1119
|
-
if blur_sigma and blur_sigma > 0:
|
|
1120
|
-
h01 = cv2.GaussianBlur(h01, (0, 0), float(blur_sigma))
|
|
1183
|
+
dps = float(depth_point_size) if depth_point_size is not None else float(point_size)
|
|
1121
1184
|
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1185
|
+
def _add_cloud(
|
|
1186
|
+
*,
|
|
1187
|
+
name_prefix: str,
|
|
1188
|
+
Z: np.ndarray,
|
|
1189
|
+
size_rgb: float,
|
|
1190
|
+
size_depth: float,
|
|
1191
|
+
op_rgb: float,
|
|
1192
|
+
op_depth: float,
|
|
1193
|
+
):
|
|
1194
|
+
"""
|
|
1195
|
+
Add traces for a single height cloud, honoring cmode.
|
|
1196
|
+
"""
|
|
1197
|
+
if cmode in ("brightness", "dual"):
|
|
1198
|
+
traces.append(
|
|
1199
|
+
go.Scatter3d(
|
|
1200
|
+
x=X, y=Y, z=Z,
|
|
1201
|
+
mode="markers",
|
|
1202
|
+
marker=dict(
|
|
1203
|
+
size=float(size_rgb),
|
|
1204
|
+
color=rgb_strings,
|
|
1205
|
+
opacity=1.0 if cmode == "brightness" else float(op_rgb),
|
|
1206
|
+
),
|
|
1207
|
+
hoverinfo="skip",
|
|
1208
|
+
name=f"{name_prefix} (RGB)" if cmode != "brightness" else f"{name_prefix}",
|
|
1209
|
+
)
|
|
1210
|
+
)
|
|
1125
1211
|
|
|
1126
|
-
|
|
1127
|
-
|
|
1212
|
+
if cmode in ("depth", "dual"):
|
|
1213
|
+
traces.append(
|
|
1214
|
+
go.Scatter3d(
|
|
1215
|
+
x=X, y=Y, z=Z,
|
|
1216
|
+
mode="markers",
|
|
1217
|
+
marker=dict(
|
|
1218
|
+
size=float(size_depth),
|
|
1219
|
+
color=Z, # numeric -> colorscale
|
|
1220
|
+
colorscale=depth_colorscale,
|
|
1221
|
+
cmin=float(Z.min()),
|
|
1222
|
+
cmax=float(Z.max()),
|
|
1223
|
+
opacity=1.0 if cmode == "depth" else float(op_depth),
|
|
1224
|
+
showscale=(cmode == "depth"),
|
|
1225
|
+
colorbar=dict(
|
|
1226
|
+
title="Depth",
|
|
1227
|
+
thickness=14,
|
|
1228
|
+
len=0.6,
|
|
1229
|
+
) if cmode == "depth" else None,
|
|
1230
|
+
),
|
|
1231
|
+
hoverinfo="skip",
|
|
1232
|
+
name=f"{name_prefix} (Depth)" if cmode != "depth" else f"{name_prefix}",
|
|
1233
|
+
)
|
|
1234
|
+
)
|
|
1128
1235
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1236
|
+
if hmode == "brightness":
|
|
1237
|
+
_add_cloud(
|
|
1238
|
+
name_prefix="Brightness Height",
|
|
1239
|
+
Z=Zlum,
|
|
1240
|
+
size_rgb=ps_lum,
|
|
1241
|
+
size_depth=dps,
|
|
1242
|
+
op_rgb=0.95,
|
|
1243
|
+
op_depth=depth_opacity,
|
|
1244
|
+
)
|
|
1245
|
+
show_legend = (cmode == "dual")
|
|
1246
|
+
|
|
1247
|
+
elif hmode == "color":
|
|
1248
|
+
_add_cloud(
|
|
1249
|
+
name_prefix="Color Height",
|
|
1250
|
+
Z=Zsat,
|
|
1251
|
+
size_rgb=ps_lum, # reuse point_size for single-mode
|
|
1252
|
+
size_depth=dps,
|
|
1253
|
+
op_rgb=0.95,
|
|
1254
|
+
op_depth=depth_opacity,
|
|
1255
|
+
)
|
|
1256
|
+
show_legend = (cmode == "dual")
|
|
1132
1257
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
J = np.concatenate([p10, p11]).astype(np.int32)
|
|
1153
|
-
K = np.concatenate([p11, p01]).astype(np.int32)
|
|
1154
|
-
|
|
1155
|
-
mesh = go.Mesh3d(
|
|
1156
|
-
x=X, y=Y, z=Z,
|
|
1157
|
-
i=I, j=J, k=K,
|
|
1158
|
-
vertexcolor=vcol,
|
|
1159
|
-
flatshading=False,
|
|
1160
|
-
lighting=dict(
|
|
1161
|
-
ambient=0.55, diffuse=0.85, specular=0.20,
|
|
1162
|
-
roughness=0.95, fresnel=0.10
|
|
1163
|
-
),
|
|
1164
|
-
lightposition=dict(x=2, y=1, z=3),
|
|
1165
|
-
hoverinfo="skip",
|
|
1166
|
-
name="PseudoSurface",
|
|
1167
|
-
showscale=False,
|
|
1168
|
-
)
|
|
1258
|
+
else:
|
|
1259
|
+
# dual heights: add BOTH clouds, each honoring cmode
|
|
1260
|
+
_add_cloud(
|
|
1261
|
+
name_prefix="Brightness Height",
|
|
1262
|
+
Z=Zlum,
|
|
1263
|
+
size_rgb=ps_lum,
|
|
1264
|
+
size_depth=dps,
|
|
1265
|
+
op_rgb=op_lum,
|
|
1266
|
+
op_depth=depth_opacity,
|
|
1267
|
+
)
|
|
1268
|
+
_add_cloud(
|
|
1269
|
+
name_prefix="Color Height",
|
|
1270
|
+
Z=Zsat,
|
|
1271
|
+
size_rgb=ps_sat,
|
|
1272
|
+
size_depth=dps,
|
|
1273
|
+
op_rgb=op_sat,
|
|
1274
|
+
op_depth=min(1.0, depth_opacity * 0.85), # slightly softer overlay feels nicer
|
|
1275
|
+
)
|
|
1276
|
+
show_legend = True # dual heights should show what’s what
|
|
1169
1277
|
|
|
1170
|
-
|
|
1278
|
+
# ---- figure ----
|
|
1279
|
+
fig = go.Figure(data=traces)
|
|
1171
1280
|
fig.update_layout(
|
|
1172
1281
|
title=title,
|
|
1173
1282
|
margin=dict(l=0, r=0, b=0, t=40),
|
|
@@ -1178,20 +1287,26 @@ def export_pseudo_surface_html(
|
|
|
1178
1287
|
zaxis=dict(visible=False),
|
|
1179
1288
|
bgcolor="black",
|
|
1180
1289
|
camera=dict(
|
|
1181
|
-
eye=dict(x=0.0, y=-1.6, z=1.
|
|
1290
|
+
eye=dict(x=0.0, y=-1.6, z=1.0),
|
|
1182
1291
|
up=dict(x=0.0, y=-1.0, z=0.0),
|
|
1183
1292
|
),
|
|
1184
1293
|
),
|
|
1185
1294
|
paper_bgcolor="black",
|
|
1186
1295
|
plot_bgcolor="black",
|
|
1187
|
-
showlegend=
|
|
1296
|
+
showlegend=bool(show_legend),
|
|
1297
|
+
legend=dict(
|
|
1298
|
+
bgcolor="rgba(0,0,0,0)",
|
|
1299
|
+
font=dict(color="white"),
|
|
1300
|
+
),
|
|
1188
1301
|
)
|
|
1189
1302
|
|
|
1190
1303
|
html = fig.to_html(include_plotlyjs="cdn", full_html=True)
|
|
1191
1304
|
if out_path is None:
|
|
1192
|
-
out_path = os.path.expanduser("~/
|
|
1305
|
+
out_path = os.path.expanduser("~/pseudo_surface_pointcloud.html")
|
|
1306
|
+
|
|
1193
1307
|
return html, out_path
|
|
1194
1308
|
|
|
1309
|
+
|
|
1195
1310
|
def deproject_galaxy_topdown_u8(
|
|
1196
1311
|
roi01: np.ndarray, # float32 [0..1], (H,W,3)
|
|
1197
1312
|
cx0: float, cy0: float,
|
|
@@ -1251,7 +1366,7 @@ class PlanetProjectionDialog(QDialog):
|
|
|
1251
1366
|
self.setMinimumSize(520, 520)
|
|
1252
1367
|
|
|
1253
1368
|
self.resize(560, 640)
|
|
1254
|
-
self.setWindowTitle("
|
|
1369
|
+
self.setWindowTitle("3D Projection")
|
|
1255
1370
|
self.setModal(False)
|
|
1256
1371
|
self.parent = parent
|
|
1257
1372
|
self.doc = document
|
|
@@ -1317,7 +1432,7 @@ class PlanetProjectionDialog(QDialog):
|
|
|
1317
1432
|
"Stereo (Cross-eye) R | L",
|
|
1318
1433
|
"Wiggle stereo (toggle L/R)",
|
|
1319
1434
|
"Anaglyph (Red/Cyan 3D Glasses)",
|
|
1320
|
-
"Interactive 3D
|
|
1435
|
+
"Interactive 3D (HTML)",
|
|
1321
1436
|
"Galaxy Polar View (Top-Down)",
|
|
1322
1437
|
])
|
|
1323
1438
|
form.addRow("Output:", self.cmb_mode)
|
|
@@ -1398,6 +1513,61 @@ class PlanetProjectionDialog(QDialog):
|
|
|
1398
1513
|
self.chk_ps_invert.setChecked(True)
|
|
1399
1514
|
ps_form.addRow("", self.chk_ps_invert)
|
|
1400
1515
|
|
|
1516
|
+
|
|
1517
|
+
# Pseudo-surface 3D coloring mode (how points are COLORED)
|
|
1518
|
+
self.cmb_ps_3d_mode = QComboBox(self)
|
|
1519
|
+
self.cmb_ps_3d_mode.addItems([
|
|
1520
|
+
"Brightness (RGB)",
|
|
1521
|
+
"Depth (Height Colormap)",
|
|
1522
|
+
"Dual (RGB + Depth)",
|
|
1523
|
+
])
|
|
1524
|
+
self.cmb_ps_3d_mode.setToolTip(
|
|
1525
|
+
"3D point cloud coloring:\n"
|
|
1526
|
+
"• Brightness: points colored from the image (RGB)\n"
|
|
1527
|
+
"• Depth: points colored by height (colormap)\n"
|
|
1528
|
+
"• Dual: overlays RGB + depth coloring"
|
|
1529
|
+
)
|
|
1530
|
+
ps_form.addRow("3D Color Mode:", self.cmb_ps_3d_mode)
|
|
1531
|
+
|
|
1532
|
+
# Height-from (what drives HEIGHT / Z)
|
|
1533
|
+
self.cmb_ps_height_from = QComboBox(self)
|
|
1534
|
+
self.cmb_ps_height_from.addItems([
|
|
1535
|
+
"Brightness (Luminance)",
|
|
1536
|
+
"Color Intensity (Saturation)",
|
|
1537
|
+
"Dual (Brightness + Color)",
|
|
1538
|
+
])
|
|
1539
|
+
self.cmb_ps_height_from.setToolTip(
|
|
1540
|
+
"What drives the HEIGHT (Z) of the 3D point cloud:\n"
|
|
1541
|
+
"• Brightness: luminance-derived height\n"
|
|
1542
|
+
"• Color Intensity: saturation/chroma-derived height\n"
|
|
1543
|
+
"• Dual: overlays TWO clouds (brightness height + saturation height)\n\n"
|
|
1544
|
+
"Tip: Dual gives nebulae \"bulk\" even where brightness is flatter."
|
|
1545
|
+
)
|
|
1546
|
+
ps_form.addRow("Height From:", self.cmb_ps_height_from)
|
|
1547
|
+
|
|
1548
|
+
# Max points (vertex cap)
|
|
1549
|
+
self.spin_ps_max_points = QSpinBox(self)
|
|
1550
|
+
self.spin_ps_max_points.setRange(50_000, 900_000) # tune if you want
|
|
1551
|
+
self.spin_ps_max_points.setSingleStep(50_000)
|
|
1552
|
+
self.spin_ps_max_points.setValue(250_000) # good default
|
|
1553
|
+
self.spin_ps_max_points.setToolTip(
|
|
1554
|
+
"Maximum number of points used in the 3D plot.\n"
|
|
1555
|
+
"Higher = more detail but heavier in the browser."
|
|
1556
|
+
)
|
|
1557
|
+
ps_form.addRow("Max Points:", self.spin_ps_max_points)
|
|
1558
|
+
|
|
1559
|
+
# Dual saturation cloud opacity (only used when Height From == Dual)
|
|
1560
|
+
self.spin_ps_sat_opacity = QDoubleSpinBox(self)
|
|
1561
|
+
self.spin_ps_sat_opacity.setRange(0.05, 1.0)
|
|
1562
|
+
self.spin_ps_sat_opacity.setSingleStep(0.05)
|
|
1563
|
+
self.spin_ps_sat_opacity.setValue(0.45)
|
|
1564
|
+
self.spin_ps_sat_opacity.setToolTip(
|
|
1565
|
+
"Opacity of the saturation-height cloud when Height From is Dual.\n"
|
|
1566
|
+
"Lower = subtle bulk; higher = more pronounced volume."
|
|
1567
|
+
)
|
|
1568
|
+
ps_form.addRow("Dual Sat Opacity:", self.spin_ps_sat_opacity)
|
|
1569
|
+
|
|
1570
|
+
# (Keep your existing)
|
|
1401
1571
|
form.addRow(ps_box)
|
|
1402
1572
|
|
|
1403
1573
|
self.chk_auto_roi = QCheckBox("Auto ROI from planet centroid (green channel)")
|
|
@@ -1921,15 +2091,50 @@ class PlanetProjectionDialog(QDialog):
|
|
|
1921
2091
|
|
|
1922
2092
|
if mode == 4:
|
|
1923
2093
|
try:
|
|
2094
|
+
# color mode (how points are colored)
|
|
2095
|
+
idx = int(self.cmb_ps_3d_mode.currentIndex()) if hasattr(self, "cmb_ps_3d_mode") else 0
|
|
2096
|
+
color_mode = ("brightness", "depth", "dual")[max(0, min(2, idx))]
|
|
2097
|
+
|
|
2098
|
+
# height source (what drives Z)
|
|
2099
|
+
hidx = int(self.cmb_ps_height_from.currentIndex()) if hasattr(self, "cmb_ps_height_from") else 0
|
|
2100
|
+
height_from = ("brightness", "color", "dual")[max(0, min(2, hidx))]
|
|
2101
|
+
|
|
2102
|
+
# cap
|
|
2103
|
+
max_pts = int(self.spin_ps_max_points.value()) if hasattr(self, "spin_ps_max_points") else 250_000
|
|
2104
|
+
|
|
2105
|
+
# dual saturation opacity (only used in height_from="dual")
|
|
2106
|
+
sat_opacity = float(self.spin_ps_sat_opacity.value()) if hasattr(self, "spin_ps_sat_opacity") else 0.45
|
|
2107
|
+
|
|
2108
|
+
# title
|
|
2109
|
+
if height_from == "brightness":
|
|
2110
|
+
ht = "Brightness Height"
|
|
2111
|
+
elif height_from == "color":
|
|
2112
|
+
ht = "Color Intensity Height"
|
|
2113
|
+
else:
|
|
2114
|
+
ht = "Dual Height (Brightness + Color)"
|
|
2115
|
+
|
|
1924
2116
|
html, default_path = export_pseudo_surface_html(
|
|
1925
2117
|
roi,
|
|
1926
2118
|
out_path=None,
|
|
1927
|
-
title="Pseudo Surface (
|
|
2119
|
+
title=f"Pseudo Surface ({ht})",
|
|
1928
2120
|
max_dim=2048,
|
|
1929
2121
|
z_scale=0.35,
|
|
1930
2122
|
depth_gamma=float(self.spin_ps_gamma.value()),
|
|
1931
2123
|
blur_sigma=float(self.spin_ps_blur.value()),
|
|
1932
|
-
invert=
|
|
2124
|
+
invert=bool(self.chk_ps_invert.isChecked()),
|
|
2125
|
+
block=10,
|
|
2126
|
+
block_blur_sigma=0.6,
|
|
2127
|
+
max_vertices=max_pts,
|
|
2128
|
+
point_size=1.6,
|
|
2129
|
+
height_from=height_from, # "brightness" | "color" | "dual"
|
|
2130
|
+
color_mode=color_mode, # "brightness" | "depth" | "dual"
|
|
2131
|
+
depth_colorscale="Turbo",
|
|
2132
|
+
depth_opacity=0.55,
|
|
2133
|
+
depth_point_size=1.9,
|
|
2134
|
+
sat_opacity=sat_opacity, # used in height_from="dual"
|
|
2135
|
+
sat_point_size=1.75, # slightly different size looks great
|
|
2136
|
+
sat_luma_gate=0.02, # suppress color-noise in very dark background
|
|
2137
|
+
sat_luma_soft=0.18, # soft knee range
|
|
1933
2138
|
)
|
|
1934
2139
|
|
|
1935
2140
|
fn, _ = QFileDialog.getSaveFileName(
|
|
@@ -3180,7 +3385,7 @@ class PlanetProjectionPreviewDialog(QDialog):
|
|
|
3180
3385
|
"""
|
|
3181
3386
|
def __init__(self, parent=None):
|
|
3182
3387
|
super().__init__(parent)
|
|
3183
|
-
self.setWindowTitle("
|
|
3388
|
+
self.setWindowTitle("3D Projection — Preview")
|
|
3184
3389
|
self.setModal(False)
|
|
3185
3390
|
self._img_zoom = 1.0 # content zoom (1.0 = full view)
|
|
3186
3391
|
self._img_pan_x = 0.0 # in source pixels, relative to center
|
setiastro/saspro/resources.py
CHANGED
|
@@ -214,6 +214,7 @@ class Icons:
|
|
|
214
214
|
OPEN_FILE = property(lambda self: _resource_path('openfile.png'))
|
|
215
215
|
ABE = property(lambda self: _resource_path('abeicon.png'))
|
|
216
216
|
BLASTER = property(lambda self: _resource_path('blaster.png'))
|
|
217
|
+
CLONESTAMP = property(lambda self: _resource_path('clonestamp.png'))
|
|
217
218
|
|
|
218
219
|
# Undo/Redo
|
|
219
220
|
UNDO = property(lambda self: _resource_path('undoicon.png'))
|
|
@@ -475,6 +476,7 @@ def _init_legacy_paths():
|
|
|
475
476
|
'undoicon_path': get_icon_path('undoicon.png'),
|
|
476
477
|
'redoicon_path': get_icon_path('redoicon.png'),
|
|
477
478
|
'blastericon_path': get_icon_path('blaster.png'),
|
|
479
|
+
'clonestampicon_path': get_icon_path('clonestamp.png'),
|
|
478
480
|
'hdr_path': get_icon_path('hdr.png'),
|
|
479
481
|
'invert_path': get_icon_path('invert.png'),
|
|
480
482
|
'fliphorizontal_path': get_icon_path('fliphorizontal.png'),
|
setiastro/saspro/sfcc.py
CHANGED
|
@@ -20,6 +20,12 @@ from datetime import datetime
|
|
|
20
20
|
from typing import List, Tuple, Optional
|
|
21
21
|
|
|
22
22
|
import numpy as np
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
_trapz = np.trapezoid
|
|
26
|
+
except AttributeError:
|
|
27
|
+
_trapz = np.trapz
|
|
28
|
+
|
|
23
29
|
import numpy.ma as ma
|
|
24
30
|
import pandas as pd
|
|
25
31
|
|
|
@@ -313,8 +319,8 @@ class SaspViewer(QMainWindow):
|
|
|
313
319
|
for color in ("red","green","blue"):
|
|
314
320
|
data = rgb_data[color]
|
|
315
321
|
if data is not None:
|
|
316
|
-
S_star =
|
|
317
|
-
S_veg =
|
|
322
|
+
S_star = _trapz(data["response"], x=common_wl)
|
|
323
|
+
S_veg = _trapz(fl_veg_c * data["T_sys"], x=common_wl)
|
|
318
324
|
if S_veg>0 and S_star>0:
|
|
319
325
|
mag = -2.5 * np.log10(S_star / S_veg)
|
|
320
326
|
mag_texts.append(f"{color[0].upper()}→{data['filter_name']}: {mag:.2f}")
|
|
@@ -1485,9 +1491,9 @@ class SFCCDialog(QDialog):
|
|
|
1485
1491
|
wl_ref, fl_ref = load_sed(ref_sed_name)
|
|
1486
1492
|
fr_i = np.interp(wl_grid, wl_ref, fl_ref, left=0.0, right=0.0)
|
|
1487
1493
|
|
|
1488
|
-
S_ref_R =
|
|
1489
|
-
S_ref_G =
|
|
1490
|
-
S_ref_B =
|
|
1494
|
+
S_ref_R = _trapz(fr_i * T_sys_R, x=wl_grid)
|
|
1495
|
+
S_ref_G = _trapz(fr_i * T_sys_G, x=wl_grid)
|
|
1496
|
+
S_ref_B = _trapz(fr_i * T_sys_B, x=wl_grid)
|
|
1491
1497
|
|
|
1492
1498
|
diag_meas_RG, diag_exp_RG = [], []
|
|
1493
1499
|
diag_meas_BG, diag_exp_BG = [], []
|
|
@@ -1510,9 +1516,9 @@ class SFCCDialog(QDialog):
|
|
|
1510
1516
|
try:
|
|
1511
1517
|
wl_s, fl_s = load_sed(pname)
|
|
1512
1518
|
fs_i = np.interp(wl_grid, wl_s, fl_s, left=0.0, right=0.0)
|
|
1513
|
-
S_sr =
|
|
1514
|
-
S_sg =
|
|
1515
|
-
S_sb =
|
|
1519
|
+
S_sr = _trapz(fs_i * T_sys_R, x=wl_grid)
|
|
1520
|
+
S_sg = _trapz(fs_i * T_sys_G, x=wl_grid)
|
|
1521
|
+
S_sb = _trapz(fs_i * T_sys_B, x=wl_grid)
|
|
1516
1522
|
template_integrals[pname] = (S_sr, S_sg, S_sb)
|
|
1517
1523
|
except Exception as e:
|
|
1518
1524
|
print(f"[SFCC] Warning: failed to load/integrate template {pname}: {e}")
|