setiastrosuitepro 1.6.1.post1__py3-none-any.whl → 1.6.4__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/Background_startup.jpg +0 -0
- setiastro/images/rotatearbitrary.png +0 -0
- setiastro/qml/ResourceMonitor.qml +126 -0
- setiastro/saspro/__main__.py +162 -25
- setiastro/saspro/_generated/build_info.py +2 -1
- setiastro/saspro/abe.py +62 -11
- setiastro/saspro/aberration_ai.py +3 -3
- setiastro/saspro/add_stars.py +5 -2
- setiastro/saspro/astrobin_exporter.py +3 -0
- setiastro/saspro/astrospike_python.py +3 -1
- setiastro/saspro/autostretch.py +4 -2
- setiastro/saspro/backgroundneutral.py +60 -9
- setiastro/saspro/batch_convert.py +3 -0
- setiastro/saspro/batch_renamer.py +3 -0
- setiastro/saspro/blemish_blaster.py +3 -0
- setiastro/saspro/blink_comparator_pro.py +474 -251
- setiastro/saspro/cheat_sheet.py +50 -15
- setiastro/saspro/clahe.py +27 -1
- setiastro/saspro/comet_stacking.py +103 -38
- setiastro/saspro/convo.py +3 -0
- setiastro/saspro/copyastro.py +3 -0
- setiastro/saspro/cosmicclarity.py +70 -45
- setiastro/saspro/crop_dialog_pro.py +28 -1
- setiastro/saspro/curve_editor_pro.py +18 -0
- setiastro/saspro/debayer.py +3 -0
- setiastro/saspro/doc_manager.py +40 -17
- setiastro/saspro/fitsmodifier.py +3 -0
- setiastro/saspro/frequency_separation.py +8 -2
- setiastro/saspro/function_bundle.py +18 -16
- setiastro/saspro/generate_translations.py +715 -1
- setiastro/saspro/ghs_dialog_pro.py +3 -0
- setiastro/saspro/graxpert.py +3 -0
- setiastro/saspro/gui/main_window.py +364 -92
- setiastro/saspro/gui/mixins/dock_mixin.py +119 -7
- setiastro/saspro/gui/mixins/file_mixin.py +7 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +105 -5
- setiastro/saspro/gui/mixins/menu_mixin.py +29 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +33 -10
- setiastro/saspro/gui/statistics_dialog.py +47 -0
- setiastro/saspro/halobgon.py +29 -3
- setiastro/saspro/histogram.py +3 -0
- setiastro/saspro/history_explorer.py +2 -0
- setiastro/saspro/i18n.py +22 -10
- setiastro/saspro/image_combine.py +3 -0
- setiastro/saspro/image_peeker_pro.py +3 -0
- setiastro/saspro/imageops/stretch.py +5 -13
- setiastro/saspro/isophote.py +3 -0
- setiastro/saspro/legacy/numba_utils.py +64 -47
- setiastro/saspro/linear_fit.py +3 -0
- setiastro/saspro/live_stacking.py +13 -2
- setiastro/saspro/mask_creation.py +3 -0
- setiastro/saspro/mfdeconv.py +5 -0
- setiastro/saspro/morphology.py +30 -5
- setiastro/saspro/multiscale_decomp.py +713 -256
- setiastro/saspro/nbtorgb_stars.py +12 -2
- setiastro/saspro/numba_utils.py +148 -47
- setiastro/saspro/ops/scripts.py +77 -17
- setiastro/saspro/ops/settings.py +1 -43
- setiastro/saspro/perfect_palette_picker.py +1 -0
- setiastro/saspro/pixelmath.py +6 -2
- setiastro/saspro/plate_solver.py +1 -0
- setiastro/saspro/remove_green.py +18 -1
- setiastro/saspro/remove_stars.py +136 -162
- setiastro/saspro/remove_stars_preset.py +55 -13
- setiastro/saspro/resources.py +36 -10
- setiastro/saspro/rgb_combination.py +1 -0
- setiastro/saspro/rgbalign.py +4 -4
- setiastro/saspro/save_options.py +1 -0
- setiastro/saspro/selective_color.py +79 -20
- setiastro/saspro/sfcc.py +50 -8
- setiastro/saspro/shortcuts.py +94 -21
- setiastro/saspro/signature_insert.py +3 -0
- setiastro/saspro/stacking_suite.py +924 -446
- setiastro/saspro/star_alignment.py +291 -331
- setiastro/saspro/star_spikes.py +116 -32
- setiastro/saspro/star_stretch.py +38 -1
- setiastro/saspro/stat_stretch.py +35 -3
- setiastro/saspro/status_log_dock.py +1 -1
- setiastro/saspro/subwindow.py +63 -2
- setiastro/saspro/supernovaasteroidhunter.py +3 -0
- setiastro/saspro/swap_manager.py +77 -42
- setiastro/saspro/translations/all_source_strings.json +4726 -0
- setiastro/saspro/translations/ar_translations.py +4096 -0
- setiastro/saspro/translations/de_translations.py +441 -446
- setiastro/saspro/translations/es_translations.py +278 -32
- setiastro/saspro/translations/fr_translations.py +280 -32
- setiastro/saspro/translations/hi_translations.py +3803 -0
- setiastro/saspro/translations/integrate_translations.py +38 -1
- setiastro/saspro/translations/it_translations.py +1211 -145
- setiastro/saspro/translations/ja_translations.py +556 -307
- setiastro/saspro/translations/pt_translations.py +3316 -3322
- setiastro/saspro/translations/ru_translations.py +3082 -0
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +16019 -0
- 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 +14855 -0
- 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 +11835 -0
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +15237 -0
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +15248 -0
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +10581 -7812
- setiastro/saspro/translations/sw_translations.py +3897 -0
- setiastro/saspro/translations/uk_translations.py +3929 -0
- setiastro/saspro/translations/zh_translations.py +283 -32
- setiastro/saspro/versioning.py +36 -5
- setiastro/saspro/view_bundle.py +20 -17
- setiastro/saspro/wavescale_hdr.py +22 -1
- setiastro/saspro/wavescalede.py +23 -1
- setiastro/saspro/whitebalance.py +39 -3
- setiastro/saspro/widgets/minigame/game.js +991 -0
- setiastro/saspro/widgets/minigame/index.html +53 -0
- setiastro/saspro/widgets/minigame/style.css +241 -0
- setiastro/saspro/widgets/resource_monitor.py +263 -0
- setiastro/saspro/widgets/spinboxes.py +18 -0
- setiastro/saspro/widgets/wavelet_utils.py +52 -20
- setiastro/saspro/wimi.py +100 -80
- setiastro/saspro/wims.py +33 -33
- setiastro/saspro/window_shelf.py +2 -2
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/METADATA +15 -4
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/RECORD +139 -115
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/license.txt +0 -0
setiastro/saspro/remove_stars.py
CHANGED
|
@@ -136,154 +136,82 @@ def _mtf_params_unlinked(img_rgb01: np.ndarray,
|
|
|
136
136
|
Works on float32 data assumed in [0,1].
|
|
137
137
|
Returns dict with arrays: {'s': (C,), 'm': (C,), 'h': (C,)}.
|
|
138
138
|
"""
|
|
139
|
-
x = np.asarray(img_rgb01, dtype=np.float32)
|
|
140
|
-
# Force 3 channels internally (Siril expects 1 or 3; we always give it 3 here)
|
|
141
|
-
if x.ndim == 2:
|
|
142
|
-
x = np.stack([x] * 3, axis=-1)
|
|
143
|
-
elif x.ndim == 3 and x.shape[2] == 1:
|
|
144
|
-
x = np.repeat(x, 3, axis=2)
|
|
145
|
-
|
|
146
|
-
C = x.shape[2]
|
|
147
|
-
s = np.zeros(C, dtype=np.float32)
|
|
148
|
-
m = np.zeros(C, dtype=np.float32)
|
|
149
|
-
h = np.zeros(C, dtype=np.float32)
|
|
150
|
-
|
|
151
|
-
med = np.zeros(C, dtype=np.float32)
|
|
152
|
-
mad = np.zeros(C, dtype=np.float32)
|
|
153
|
-
inverted_flags = np.zeros(C, dtype=bool)
|
|
154
|
-
|
|
155
|
-
# --- stats per channel (Siril: median / normValue, mad / normValue * MAD_NORM) ---
|
|
156
|
-
# Here normValue == 1.0 because we're already in [0,1]
|
|
157
|
-
for c in range(C):
|
|
158
|
-
ch = x[..., c].astype(np.float32, copy=False)
|
|
159
|
-
med_c = float(np.median(ch))
|
|
160
|
-
mad_raw = float(np.median(np.abs(ch - med_c)))
|
|
161
|
-
mad_c = mad_raw * _MAD_NORM
|
|
162
|
-
if mad_c == 0.0:
|
|
163
|
-
mad_c = 0.001
|
|
164
|
-
|
|
165
|
-
med[c] = med_c
|
|
166
|
-
mad[c] = mad_c
|
|
167
|
-
if med_c > 0.5:
|
|
168
|
-
inverted_flags[c] = True
|
|
169
|
-
|
|
170
|
-
inverted_channels = int(inverted_flags.sum())
|
|
171
|
-
|
|
172
|
-
# --- Main branch (non-inverted dominant) ---
|
|
173
|
-
if inverted_channels < C:
|
|
174
|
-
for c in range(C):
|
|
175
|
-
median = float(med[c])
|
|
176
|
-
mad_c = float(mad[c])
|
|
177
|
-
|
|
178
|
-
c0 = median + shadows_clipping * mad_c
|
|
179
|
-
if c0 < 0.0:
|
|
180
|
-
c0 = 0.0
|
|
181
|
-
# Siril: m2 = median - c0; midtones = MTF(m2, target_bg, 0,1)
|
|
182
|
-
m2 = median - c0
|
|
183
|
-
mid = float(_mtf_scalar(m2, targetbg, 0.0, 1.0))
|
|
184
|
-
|
|
185
|
-
s[c] = c0
|
|
186
|
-
m[c] = mid
|
|
187
|
-
h[c] = 1.0
|
|
188
|
-
|
|
189
|
-
# --- Inverted channel branch ---
|
|
190
|
-
else:
|
|
191
|
-
for c in range(C):
|
|
192
|
-
median = float(med[c])
|
|
193
|
-
mad_c = float(mad[c])
|
|
194
|
-
|
|
195
|
-
c1 = median - shadows_clipping * mad_c
|
|
196
|
-
if c1 > 1.0:
|
|
197
|
-
c1 = 1.0
|
|
198
|
-
m2 = c1 - median
|
|
199
|
-
mid = 1.0 - float(_mtf_scalar(m2, targetbg, 0.0, 1.0))
|
|
200
|
-
|
|
201
|
-
s[c] = 0.0
|
|
202
|
-
m[c] = mid
|
|
203
|
-
h[c] = c1
|
|
204
|
-
|
|
205
|
-
return {"s": s, "m": m, "h": h}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def _apply_mtf_unlinked_rgb(img_rgb01: np.ndarray, p: dict) -> np.ndarray:
|
|
209
139
|
"""
|
|
210
|
-
|
|
140
|
+
Siril-style per-channel MTF parameter estimation, matching
|
|
141
|
+
find_unlinked_midtones_balance_default() / find_unlinked_midtones_balance().
|
|
142
|
+
|
|
143
|
+
Works on float32 data assumed in [0,1].
|
|
144
|
+
Returns dict with arrays: {'s': (C,), 'm': (C,), 'h': (C,)}.
|
|
211
145
|
"""
|
|
212
146
|
x = np.asarray(img_rgb01, dtype=np.float32)
|
|
147
|
+
|
|
148
|
+
# Analyze input shape to handle mono efficiently
|
|
213
149
|
if x.ndim == 2:
|
|
214
|
-
|
|
150
|
+
# (H, W) -> treat as single channel
|
|
151
|
+
x_in = x[..., None] # Virtual 3D (H,W,1)
|
|
152
|
+
C_in = 1
|
|
215
153
|
elif x.ndim == 3 and x.shape[2] == 1:
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
154
|
+
x_in = x
|
|
155
|
+
C_in = 1
|
|
156
|
+
else:
|
|
157
|
+
x_in = x
|
|
158
|
+
C_in = x.shape[2]
|
|
159
|
+
|
|
160
|
+
# Vectorized stats calculation on actual data only
|
|
161
|
+
med = np.median(x_in, axis=(0, 1)).astype(np.float32) # shape (C_in,)
|
|
162
|
+
|
|
163
|
+
# MAD requires centered abs diff
|
|
164
|
+
diff = np.abs(x_in - med.reshape(1, 1, C_in))
|
|
165
|
+
mad_raw = np.median(diff, axis=(0, 1)).astype(np.float32) # shape (C_in,)
|
|
166
|
+
|
|
167
|
+
mad = mad_raw * _MAD_NORM
|
|
168
|
+
mad[mad == 0] = 0.001
|
|
169
|
+
|
|
170
|
+
inverted_flags = (med > 0.5)
|
|
171
|
+
# If mono, we just check the one channel. If RGB, we check all.
|
|
172
|
+
# Logic below assumes we return 3-channel params s,m,h even for mono input (broadcasted).
|
|
173
|
+
|
|
174
|
+
# To match original behavior which always returned 3-element arrays for s,m,h:
|
|
175
|
+
# We will compute s_in, m_in, h_in for the input channels, then broadcast to 3.
|
|
176
|
+
|
|
177
|
+
s_in = np.zeros(C_in, dtype=np.float32)
|
|
178
|
+
m_in = np.zeros(C_in, dtype=np.float32)
|
|
179
|
+
h_in = np.zeros(C_in, dtype=np.float32)
|
|
180
|
+
|
|
181
|
+
# We iterate C_in times (1 or 3)
|
|
182
|
+
for c in range(C_in):
|
|
183
|
+
is_inv = inverted_flags[c]
|
|
184
|
+
md = med[c]
|
|
185
|
+
md_dev = mad[c]
|
|
186
|
+
|
|
187
|
+
if not is_inv:
|
|
188
|
+
# Normal
|
|
189
|
+
c0 = max(md + shadows_clipping * md_dev, 0.0)
|
|
190
|
+
m2 = md - c0
|
|
191
|
+
|
|
192
|
+
s_in[c] = c0
|
|
193
|
+
m_in[c] = float(_mtf_scalar(m2, targetbg, 0.0, 1.0))
|
|
194
|
+
h_in[c] = 1.0
|
|
195
|
+
else:
|
|
196
|
+
# Inverted
|
|
197
|
+
c1 = min(md - shadows_clipping * md_dev, 1.0)
|
|
198
|
+
m2 = c1 - md
|
|
199
|
+
|
|
200
|
+
s_in[c] = 0.0
|
|
201
|
+
m_in[c] = 1.0 - float(_mtf_scalar(m2, targetbg, 0.0, 1.0))
|
|
202
|
+
h_in[c] = c1
|
|
203
|
+
|
|
204
|
+
# Broadcast to 3 channels if needed
|
|
205
|
+
if C_in == 1:
|
|
206
|
+
s = np.repeat(s_in, 3)
|
|
207
|
+
m = np.repeat(m_in, 3)
|
|
208
|
+
h = np.repeat(h_in, 3)
|
|
209
|
+
else:
|
|
210
|
+
s = s_in
|
|
211
|
+
m = m_in
|
|
212
|
+
h = h_in
|
|
254
213
|
|
|
255
|
-
|
|
256
|
-
ch = x[..., c]
|
|
257
|
-
lo = float(np.percentile(ch, lo_pct))
|
|
258
|
-
hi = float(np.percentile(ch, hi_pct))
|
|
259
|
-
if not np.isfinite(lo): lo = 0.0
|
|
260
|
-
if not np.isfinite(hi): hi = 1.0
|
|
261
|
-
if hi - lo < 1e-6:
|
|
262
|
-
hi = lo + 1e-6
|
|
263
|
-
lo_vals.append(lo); hi_vals.append(hi)
|
|
264
|
-
out[..., c] = (ch - lo) / (hi - lo)
|
|
265
|
-
|
|
266
|
-
out = np.clip(out, 0.0, 1.0)
|
|
267
|
-
params = {"lo": lo_vals, "hi": hi_vals, "was_single": was_single}
|
|
268
|
-
return out, params
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
def _stat_unstretch_rgb(img: np.ndarray, params: dict) -> np.ndarray:
|
|
272
|
-
"""
|
|
273
|
-
Inverse of _stat_stretch_rgb. Expects img RGB float32 [0,1].
|
|
274
|
-
"""
|
|
275
|
-
lo = np.asarray(params["lo"], dtype=np.float32)
|
|
276
|
-
hi = np.asarray(params["hi"], dtype=np.float32)
|
|
277
|
-
out = img.astype(np.float32, copy=True)
|
|
278
|
-
for c in range(3):
|
|
279
|
-
out[..., c] = out[..., c] * (hi[c] - lo[c]) + lo[c]
|
|
280
|
-
out = np.clip(out, 0.0, 1.0)
|
|
281
|
-
if params.get("was_single", False):
|
|
282
|
-
out = out.mean(axis=2, keepdims=False) # back to single channel if needed
|
|
283
|
-
# StarNet needs RGB during processing; we keep RGB after removal for consistency.
|
|
284
|
-
# If you want to return mono to the doc when the source was mono, do it at the very end.
|
|
285
|
-
out = np.stack([out] * 3, axis=-1)
|
|
286
|
-
return out
|
|
214
|
+
return {"s": s, "m": m, "h": h}
|
|
287
215
|
|
|
288
216
|
def _mtf_scalar(x: float, m: float, lo: float = 0.0, hi: float = 1.0) -> float:
|
|
289
217
|
"""
|
|
@@ -324,6 +252,36 @@ def _mtf_scalar(x: float, m: float, lo: float = 0.0, hi: float = 1.0) -> float:
|
|
|
324
252
|
return float(y)
|
|
325
253
|
|
|
326
254
|
|
|
255
|
+
def _apply_mtf_unlinked_rgb(img_rgb01: np.ndarray, p: dict) -> np.ndarray:
|
|
256
|
+
"""
|
|
257
|
+
Apply per-channel MTF exactly. p from _mtf_params_unlinked.
|
|
258
|
+
"""
|
|
259
|
+
x = np.asarray(img_rgb01, dtype=np.float32)
|
|
260
|
+
if x.ndim == 2:
|
|
261
|
+
x = np.stack([x]*3, axis=-1)
|
|
262
|
+
elif x.ndim == 3 and x.shape[2] == 1:
|
|
263
|
+
x = np.repeat(x, 3, axis=2)
|
|
264
|
+
|
|
265
|
+
out = np.empty_like(x, dtype=np.float32)
|
|
266
|
+
for c in range(x.shape[2]):
|
|
267
|
+
out[..., c] = _mtf_apply(x[..., c], float(p["s"][c]), float(p["m"][c]), float(p["h"][c]))
|
|
268
|
+
return np.clip(out, 0.0, 1.0)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _invert_mtf_unlinked_rgb(img_rgb01: np.ndarray, p: dict) -> np.ndarray:
|
|
272
|
+
"""
|
|
273
|
+
Exact analytic inverse per channel (uses same s/m/h arrays).
|
|
274
|
+
"""
|
|
275
|
+
y = np.asarray(img_rgb01, dtype=np.float32)
|
|
276
|
+
if y.ndim == 2:
|
|
277
|
+
y = np.stack([y]*3, axis=-1)
|
|
278
|
+
elif y.ndim == 3 and y.shape[2] == 1:
|
|
279
|
+
y = np.repeat(y, 3, axis=2)
|
|
280
|
+
|
|
281
|
+
out = np.empty_like(y, dtype=np.float32)
|
|
282
|
+
for c in range(y.shape[2]):
|
|
283
|
+
out[..., c] = _mtf_inverse(y[..., c], float(p["s"][c]), float(p["m"][c]), float(p["h"][c]))
|
|
284
|
+
return np.clip(out, 0.0, 1.0)
|
|
327
285
|
# ------------------------------------------------------------
|
|
328
286
|
# Settings helper
|
|
329
287
|
# ------------------------------------------------------------
|
|
@@ -370,24 +328,28 @@ def starnet_starless_from_array(arr_rgb01: np.ndarray, settings, *, tmp_prefix="
|
|
|
370
328
|
in_path = os.path.join(workdir, f"{tmp_prefix}_in.tif")
|
|
371
329
|
out_path = os.path.join(workdir, f"{tmp_prefix}_out.tif")
|
|
372
330
|
|
|
373
|
-
# --- Normalize input shape and safe values ---
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
331
|
+
# --- Normalize input shape (virtual) and safe values ---
|
|
332
|
+
x_in = np.asarray(arr, dtype=np.float32)
|
|
333
|
+
|
|
334
|
+
# If (H,W,1), collapse to (H,W) so mono flows cleanly
|
|
335
|
+
if x_in.ndim == 3 and x_in.shape[2] == 1:
|
|
336
|
+
x_in = x_in[..., 0]
|
|
337
|
+
|
|
338
|
+
# sanitize
|
|
339
|
+
x_in = np.nan_to_num(x_in, nan=0.0, posinf=0.0, neginf=0.0).astype(np.float32, copy=False)
|
|
380
340
|
|
|
381
341
|
# Preserve original numeric scale if users pass >1.0
|
|
382
|
-
xmax = float(np.max(
|
|
342
|
+
xmax = float(np.max(x_in)) if x_in.size else 1.0
|
|
383
343
|
scale_factor = xmax if xmax > 1.01 else 1.0
|
|
384
|
-
|
|
344
|
+
|
|
345
|
+
xin = (x_in / scale_factor) if scale_factor > 1.0 else x_in
|
|
385
346
|
xin = np.clip(xin, 0.0, 1.0)
|
|
386
347
|
|
|
387
348
|
# --- Siril-style unlinked MTF params + pre-stretch ---
|
|
388
349
|
mtf_params = _mtf_params_unlinked(xin, shadows_clipping=-2.8, targetbg=0.25)
|
|
389
350
|
x_for_starnet = _apply_mtf_unlinked_rgb(xin, mtf_params).astype(np.float32, copy=False)
|
|
390
351
|
|
|
352
|
+
|
|
391
353
|
# --- Write 16-bit TIFF for StarNet ---
|
|
392
354
|
save_image(
|
|
393
355
|
x_for_starnet, in_path,
|
|
@@ -662,18 +624,19 @@ def _run_starnet(main, doc):
|
|
|
662
624
|
)
|
|
663
625
|
except Exception:
|
|
664
626
|
pass
|
|
665
|
-
# --- Ensure RGB float32 in safe range
|
|
627
|
+
# --- Ensure RGB float32 in safe range (without expanding yet)
|
|
628
|
+
# Starnet needs RGB eventually, but we can compute stats/normalization on mono
|
|
666
629
|
src = np.asarray(doc.image)
|
|
667
|
-
if src.ndim == 2:
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
processing_image = np.repeat(src, 3, axis=2)
|
|
630
|
+
if src.ndim == 3 and src.shape[2] == 1:
|
|
631
|
+
# standardizing shape is cheap
|
|
632
|
+
processing_image = src[..., 0]
|
|
671
633
|
else:
|
|
672
634
|
processing_image = src
|
|
635
|
+
|
|
673
636
|
processing_image = np.nan_to_num(processing_image.astype(np.float32, copy=False),
|
|
674
637
|
nan=0.0, posinf=0.0, neginf=0.0)
|
|
675
638
|
|
|
676
|
-
# --- Scale normalization if >1.0
|
|
639
|
+
# --- Scale normalization if >1.0
|
|
677
640
|
scale_factor = float(np.max(processing_image))
|
|
678
641
|
if scale_factor > 1.0:
|
|
679
642
|
processing_norm = processing_image / scale_factor
|
|
@@ -1027,11 +990,10 @@ def _run_darkstar(main, doc):
|
|
|
1027
990
|
pass
|
|
1028
991
|
|
|
1029
992
|
# --- Build processing image (RGB float32, normalized) ---
|
|
993
|
+
# DarkStar needs RGB, but we can delay expansion until save
|
|
1030
994
|
src = np.asarray(doc.image)
|
|
1031
|
-
if src.ndim == 2:
|
|
1032
|
-
processing_image =
|
|
1033
|
-
elif src.ndim == 3 and src.shape[2] == 1:
|
|
1034
|
-
processing_image = np.repeat(src, 3, axis=2)
|
|
995
|
+
if src.ndim == 3 and src.shape[2] == 1:
|
|
996
|
+
processing_image = src[..., 0]
|
|
1035
997
|
else:
|
|
1036
998
|
processing_image = src
|
|
1037
999
|
|
|
@@ -1088,8 +1050,20 @@ def _run_darkstar(main, doc):
|
|
|
1088
1050
|
# --- Save pre-stretched image as 32-bit float TIFF for DarkStar ---
|
|
1089
1051
|
in_path = os.path.join(input_dir, "imagetoremovestars.tif")
|
|
1090
1052
|
try:
|
|
1053
|
+
# Check if we need to expand on-the-fly for DarkStar (it expects RGB input)
|
|
1054
|
+
# If img_for_darkstar is mono, save_image might save mono.
|
|
1055
|
+
# "is_mono=False" flag to save_image hints we want RGB.
|
|
1056
|
+
# If the array is 2D, save_image might still save mono unless we feed it 3D.
|
|
1057
|
+
# For safety with DarkStar, we create the 3D view now if needed.
|
|
1058
|
+
|
|
1059
|
+
to_save = img_for_darkstar
|
|
1060
|
+
if to_save.ndim == 2:
|
|
1061
|
+
to_save = np.stack([to_save]*3, axis=-1)
|
|
1062
|
+
elif to_save.ndim == 3 and to_save.shape[2] == 1:
|
|
1063
|
+
to_save = np.repeat(to_save, 3, axis=2)
|
|
1064
|
+
|
|
1091
1065
|
save_image(
|
|
1092
|
-
|
|
1066
|
+
to_save,
|
|
1093
1067
|
in_path,
|
|
1094
1068
|
original_format="tif",
|
|
1095
1069
|
bit_depth="32-bit floating point",
|
|
@@ -12,7 +12,7 @@ from setiastro.saspro.legacy.image_manager import save_image, load_image
|
|
|
12
12
|
# Reuse helpers & plumbing from the interactive module
|
|
13
13
|
from .remove_stars import (
|
|
14
14
|
_ProcThread, _ProcDialog,
|
|
15
|
-
|
|
15
|
+
_mtf_params_unlinked, _apply_mtf_unlinked_rgb, _invert_mtf_unlinked_rgb,
|
|
16
16
|
_active_mask3_from_doc, _mask_blend_with_doc_mask, _push_as_new_doc,
|
|
17
17
|
_ensure_exec_bit,
|
|
18
18
|
)
|
|
@@ -125,24 +125,48 @@ def _run_starnet_headless(main, doc, p):
|
|
|
125
125
|
processing_image = processing_image.astype(np.float32, copy=False)
|
|
126
126
|
|
|
127
127
|
is_linear = bool(p.get("linear", True))
|
|
128
|
-
did_stretch =
|
|
129
|
-
|
|
128
|
+
did_stretch = is_linear
|
|
129
|
+
|
|
130
|
+
# sanitize + normalize if needed (keep exactly like interactive)
|
|
131
|
+
processing_image = np.nan_to_num(processing_image, nan=0.0, posinf=0.0, neginf=0.0).astype(np.float32, copy=False)
|
|
132
|
+
|
|
133
|
+
scale_factor = float(np.max(processing_image)) if processing_image.size else 1.0
|
|
134
|
+
processing_norm = (processing_image / scale_factor) if scale_factor > 1.0 else processing_image
|
|
135
|
+
processing_norm = np.clip(processing_norm, 0.0, 1.0)
|
|
136
|
+
|
|
137
|
+
img_for_starnet = processing_norm
|
|
138
|
+
|
|
130
139
|
if is_linear:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
140
|
+
mtf_params = _mtf_params_unlinked(processing_norm, shadows_clipping=-2.8, targetbg=0.25)
|
|
141
|
+
img_for_starnet = _apply_mtf_unlinked_rgb(processing_norm, mtf_params)
|
|
142
|
+
|
|
143
|
+
# stash for inverse step (same keys as interactive)
|
|
144
|
+
try:
|
|
145
|
+
setattr(main, "_starnet_stat_meta", {
|
|
146
|
+
"scheme": "siril_mtf",
|
|
147
|
+
"s": np.asarray(mtf_params["s"], dtype=np.float32),
|
|
148
|
+
"m": np.asarray(mtf_params["m"], dtype=np.float32),
|
|
149
|
+
"h": np.asarray(mtf_params["h"], dtype=np.float32),
|
|
150
|
+
"scale": float(scale_factor),
|
|
151
|
+
})
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
134
154
|
else:
|
|
135
|
-
|
|
136
|
-
|
|
155
|
+
try:
|
|
156
|
+
if hasattr(main, "_starnet_stat_meta"):
|
|
157
|
+
delattr(main, "_starnet_stat_meta")
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
|
|
137
161
|
|
|
138
162
|
starnet_dir = os.path.dirname(exe) or os.getcwd()
|
|
139
163
|
in_path = os.path.join(starnet_dir, "imagetoremovestars.tif")
|
|
140
164
|
out_path = os.path.join(starnet_dir, "starless.tif")
|
|
141
165
|
|
|
142
166
|
try:
|
|
143
|
-
save_image(
|
|
144
|
-
|
|
145
|
-
|
|
167
|
+
save_image(img_for_starnet, in_path, original_format="tif",
|
|
168
|
+
bit_depth="16-bit", original_header=None, is_mono=False,
|
|
169
|
+
image_meta=None, file_meta=None)
|
|
146
170
|
except Exception as e:
|
|
147
171
|
QMessageBox.critical(main, "StarNet", f"Failed to write input TIFF:\n{e}")
|
|
148
172
|
return
|
|
@@ -179,12 +203,30 @@ def _finish_starnet(main, doc, rc, dlg, in_path, out_path, did_stretch):
|
|
|
179
203
|
starless_rgb = starless_rgb.astype(np.float32, copy=False)
|
|
180
204
|
|
|
181
205
|
if did_stretch:
|
|
206
|
+
meta = getattr(main, "_starnet_stat_meta", None)
|
|
207
|
+
if isinstance(meta, dict) and meta.get("scheme") == "siril_mtf":
|
|
208
|
+
try:
|
|
209
|
+
p = {
|
|
210
|
+
"s": np.asarray(meta.get("s"), dtype=np.float32),
|
|
211
|
+
"m": np.asarray(meta.get("m"), dtype=np.float32),
|
|
212
|
+
"h": np.asarray(meta.get("h"), dtype=np.float32),
|
|
213
|
+
}
|
|
214
|
+
inv = _invert_mtf_unlinked_rgb(starless_rgb, p)
|
|
215
|
+
sc = float(meta.get("scale", 1.0))
|
|
216
|
+
if sc > 1.0:
|
|
217
|
+
inv *= sc
|
|
218
|
+
starless_rgb = np.clip(inv, 0.0, 1.0).astype(np.float32, copy=False)
|
|
219
|
+
except Exception:
|
|
220
|
+
pass
|
|
221
|
+
|
|
222
|
+
# cleanup so it can't leak
|
|
182
223
|
try:
|
|
183
|
-
|
|
184
|
-
|
|
224
|
+
if hasattr(main, "_starnet_stat_meta"):
|
|
225
|
+
delattr(main, "_starnet_stat_meta")
|
|
185
226
|
except Exception:
|
|
186
227
|
pass
|
|
187
228
|
|
|
229
|
+
|
|
188
230
|
# original as RGB
|
|
189
231
|
orig = np.asarray(doc.image)
|
|
190
232
|
if orig.ndim == 2: original_rgb = np.stack([orig]*3, axis=-1)
|
setiastro/saspro/resources.py
CHANGED
|
@@ -123,17 +123,31 @@ def _get_base_path() -> str:
|
|
|
123
123
|
|
|
124
124
|
|
|
125
125
|
def _resource_path(filename: str) -> str:
|
|
126
|
-
"""Get full path to a resource file."""
|
|
127
126
|
base = _get_base_path()
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
127
|
+
fn = filename
|
|
128
|
+
|
|
129
|
+
is_img = fn.lower().endswith(('.png','.ico','.gif','.icns','.svg','.jpg','.jpeg','.bmp'))
|
|
130
|
+
if is_img:
|
|
131
|
+
candidates = [
|
|
132
|
+
os.path.join(base, 'images', fn),
|
|
133
|
+
os.path.join(base, 'setiastro', 'images', fn),
|
|
134
|
+
os.path.join(base, 'setiastro', 'saspro', 'images', fn),
|
|
135
|
+
]
|
|
136
|
+
for p in candidates:
|
|
137
|
+
if os.path.exists(p):
|
|
138
|
+
return p
|
|
139
|
+
|
|
140
|
+
# data / other files
|
|
141
|
+
candidates = [
|
|
142
|
+
os.path.join(base, fn),
|
|
143
|
+
os.path.join(base, 'setiastro', fn),
|
|
144
|
+
os.path.join(base, 'setiastro', 'saspro', fn),
|
|
145
|
+
]
|
|
146
|
+
for p in candidates:
|
|
147
|
+
if os.path.exists(p):
|
|
148
|
+
return p
|
|
149
|
+
|
|
150
|
+
return os.path.join(base, fn)
|
|
137
151
|
|
|
138
152
|
|
|
139
153
|
class Icons:
|
|
@@ -210,6 +224,7 @@ class Icons:
|
|
|
210
224
|
ROTATE_CW = property(lambda self: _resource_path('rotateclockwise.png'))
|
|
211
225
|
ROTATE_CCW = property(lambda self: _resource_path('rotatecounterclockwise.png'))
|
|
212
226
|
ROTATE_180 = property(lambda self: _resource_path('rotate180.png'))
|
|
227
|
+
ROTATE_ANY = property(lambda self: _resource_path('rotatearbitrary.png'))
|
|
213
228
|
RESCALE = property(lambda self: _resource_path('rescale.png'))
|
|
214
229
|
|
|
215
230
|
# Masks
|
|
@@ -395,6 +410,7 @@ def _init_legacy_paths():
|
|
|
395
410
|
'rotateclockwise_path': get_icon_path('rotateclockwise.png'),
|
|
396
411
|
'rotatecounterclockwise_path': get_icon_path('rotatecounterclockwise.png'),
|
|
397
412
|
'rotate180_path': get_icon_path('rotate180.png'),
|
|
413
|
+
'rotatearbitrary_path': get_icon_path('rotatearbitrary.png'),
|
|
398
414
|
'maskcreate_path': get_icon_path('maskcreate.png'),
|
|
399
415
|
'maskapply_path': get_icon_path('maskapply.png'),
|
|
400
416
|
'maskremove_path': get_icon_path('maskremove.png'),
|
|
@@ -469,9 +485,19 @@ def _init_legacy_paths():
|
|
|
469
485
|
_legacy = _init_legacy_paths()
|
|
470
486
|
globals().update(_legacy)
|
|
471
487
|
|
|
488
|
+
|
|
489
|
+
# Background for startup
|
|
490
|
+
background_startup_path = _resource_path('Background_startup.jpg')
|
|
491
|
+
_legacy['background_startup_path'] = background_startup_path
|
|
492
|
+
|
|
493
|
+
# QML helper
|
|
494
|
+
resource_monitor_qml = _resource_path(os.path.join("qml", "ResourceMonitor.qml"))
|
|
495
|
+
|
|
472
496
|
# Export list for `from setiastro.saspro.resources import *`
|
|
473
497
|
__all__ = [
|
|
474
498
|
'Icons', 'Resources',
|
|
475
499
|
'get_icons', 'get_resources',
|
|
476
500
|
'get_icon_path', 'get_data_path',
|
|
501
|
+
'background_startup_path',
|
|
477
502
|
] + list(_legacy.keys())
|
|
503
|
+
|
|
@@ -44,6 +44,7 @@ class RGBCombinationDialogPro(QDialog):
|
|
|
44
44
|
super().__init__(parent)
|
|
45
45
|
self.setWindowTitle(self.tr("RGB Combination"))
|
|
46
46
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
47
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
47
48
|
self.setModal(False)
|
|
48
49
|
#self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
49
50
|
self._list_open_docs = list_open_docs_fn or (lambda: [])
|
setiastro/saspro/rgbalign.py
CHANGED
|
@@ -324,15 +324,12 @@ class RGBAlignWorker(QThread):
|
|
|
324
324
|
def _warp_channel(self, ch: np.ndarray, kind: str, X, ref_shape):
|
|
325
325
|
H, W = ref_shape[:2]
|
|
326
326
|
if kind == "affine":
|
|
327
|
-
|
|
328
|
-
return ch
|
|
327
|
+
# Just assume cv2 is available (standard dependency) for perf
|
|
329
328
|
A = np.asarray(X, dtype=np.float32).reshape(2, 3)
|
|
330
329
|
return cv2.warpAffine(ch, A, (W, H), flags=cv2.INTER_LANCZOS4,
|
|
331
330
|
borderMode=cv2.BORDER_CONSTANT, borderValue=0)
|
|
332
331
|
|
|
333
332
|
if kind == "homography":
|
|
334
|
-
if cv2 is None:
|
|
335
|
-
return ch
|
|
336
333
|
Hm = np.asarray(X, dtype=np.float32).reshape(3, 3)
|
|
337
334
|
return cv2.warpPerspective(ch, Hm, (W, H), flags=cv2.INTER_LANCZOS4,
|
|
338
335
|
borderMode=cv2.BORDER_CONSTANT, borderValue=0)
|
|
@@ -350,6 +347,9 @@ class RGBAlignDialog(QDialog):
|
|
|
350
347
|
def __init__(self, parent=None, document=None):
|
|
351
348
|
super().__init__(parent)
|
|
352
349
|
self.setWindowTitle(self.tr("RGB Align"))
|
|
350
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
351
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
352
|
+
self.setModal(False)
|
|
353
353
|
self.parent = parent
|
|
354
354
|
# document could be a view; try to unwrap
|
|
355
355
|
self.doc_view = document
|
setiastro/saspro/save_options.py
CHANGED
|
@@ -20,6 +20,7 @@ class SaveOptionsDialog(QDialog):
|
|
|
20
20
|
super().__init__(parent)
|
|
21
21
|
self.setWindowTitle(self.tr("Save Options"))
|
|
22
22
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
23
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
23
24
|
self.setModal(False)
|
|
24
25
|
#self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
25
26
|
|