setiastrosuitepro 1.6.1__py3-none-any.whl → 1.6.2__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.
Files changed (128) hide show
  1. setiastro/images/Background_startup.jpg +0 -0
  2. setiastro/qml/ResourceMonitor.qml +126 -0
  3. setiastro/saspro/__main__.py +159 -23
  4. setiastro/saspro/_generated/build_info.py +2 -1
  5. setiastro/saspro/abe.py +62 -11
  6. setiastro/saspro/aberration_ai.py +3 -3
  7. setiastro/saspro/add_stars.py +5 -2
  8. setiastro/saspro/astrobin_exporter.py +3 -0
  9. setiastro/saspro/astrospike_python.py +3 -1
  10. setiastro/saspro/autostretch.py +4 -2
  11. setiastro/saspro/backgroundneutral.py +52 -10
  12. setiastro/saspro/batch_convert.py +3 -0
  13. setiastro/saspro/batch_renamer.py +3 -0
  14. setiastro/saspro/blemish_blaster.py +3 -0
  15. setiastro/saspro/cheat_sheet.py +50 -15
  16. setiastro/saspro/clahe.py +27 -1
  17. setiastro/saspro/comet_stacking.py +103 -38
  18. setiastro/saspro/convo.py +3 -0
  19. setiastro/saspro/copyastro.py +3 -0
  20. setiastro/saspro/cosmicclarity.py +70 -45
  21. setiastro/saspro/crop_dialog_pro.py +17 -0
  22. setiastro/saspro/curve_editor_pro.py +18 -0
  23. setiastro/saspro/debayer.py +3 -0
  24. setiastro/saspro/doc_manager.py +39 -16
  25. setiastro/saspro/fitsmodifier.py +3 -0
  26. setiastro/saspro/frequency_separation.py +8 -2
  27. setiastro/saspro/function_bundle.py +2 -0
  28. setiastro/saspro/generate_translations.py +715 -1
  29. setiastro/saspro/ghs_dialog_pro.py +3 -0
  30. setiastro/saspro/graxpert.py +3 -0
  31. setiastro/saspro/gui/main_window.py +275 -32
  32. setiastro/saspro/gui/mixins/dock_mixin.py +100 -1
  33. setiastro/saspro/gui/mixins/file_mixin.py +7 -0
  34. setiastro/saspro/gui/mixins/menu_mixin.py +28 -0
  35. setiastro/saspro/gui/statistics_dialog.py +47 -0
  36. setiastro/saspro/halobgon.py +29 -3
  37. setiastro/saspro/histogram.py +3 -0
  38. setiastro/saspro/history_explorer.py +2 -0
  39. setiastro/saspro/i18n.py +22 -10
  40. setiastro/saspro/image_combine.py +3 -0
  41. setiastro/saspro/image_peeker_pro.py +3 -0
  42. setiastro/saspro/imageops/stretch.py +5 -13
  43. setiastro/saspro/isophote.py +3 -0
  44. setiastro/saspro/legacy/numba_utils.py +64 -47
  45. setiastro/saspro/linear_fit.py +3 -0
  46. setiastro/saspro/live_stacking.py +13 -2
  47. setiastro/saspro/mask_creation.py +3 -0
  48. setiastro/saspro/mfdeconv.py +5 -0
  49. setiastro/saspro/morphology.py +30 -5
  50. setiastro/saspro/multiscale_decomp.py +3 -0
  51. setiastro/saspro/nbtorgb_stars.py +12 -2
  52. setiastro/saspro/numba_utils.py +148 -47
  53. setiastro/saspro/ops/scripts.py +77 -17
  54. setiastro/saspro/ops/settings.py +1 -43
  55. setiastro/saspro/perfect_palette_picker.py +1 -0
  56. setiastro/saspro/pixelmath.py +6 -2
  57. setiastro/saspro/plate_solver.py +2 -1
  58. setiastro/saspro/remove_green.py +18 -1
  59. setiastro/saspro/remove_stars.py +136 -162
  60. setiastro/saspro/resources.py +7 -0
  61. setiastro/saspro/rgb_combination.py +1 -0
  62. setiastro/saspro/rgbalign.py +4 -4
  63. setiastro/saspro/save_options.py +1 -0
  64. setiastro/saspro/sfcc.py +50 -8
  65. setiastro/saspro/signature_insert.py +3 -0
  66. setiastro/saspro/stacking_suite.py +630 -341
  67. setiastro/saspro/star_alignment.py +16 -1
  68. setiastro/saspro/star_spikes.py +116 -32
  69. setiastro/saspro/star_stretch.py +38 -1
  70. setiastro/saspro/stat_stretch.py +35 -3
  71. setiastro/saspro/subwindow.py +63 -2
  72. setiastro/saspro/supernovaasteroidhunter.py +3 -0
  73. setiastro/saspro/translations/all_source_strings.json +3654 -0
  74. setiastro/saspro/translations/ar_translations.py +3865 -0
  75. setiastro/saspro/translations/de_translations.py +16 -0
  76. setiastro/saspro/translations/es_translations.py +16 -0
  77. setiastro/saspro/translations/fr_translations.py +16 -0
  78. setiastro/saspro/translations/hi_translations.py +3571 -0
  79. setiastro/saspro/translations/integrate_translations.py +36 -0
  80. setiastro/saspro/translations/it_translations.py +16 -0
  81. setiastro/saspro/translations/ja_translations.py +16 -0
  82. setiastro/saspro/translations/pt_translations.py +16 -0
  83. setiastro/saspro/translations/ru_translations.py +2848 -0
  84. setiastro/saspro/translations/saspro_ar.qm +0 -0
  85. setiastro/saspro/translations/saspro_ar.ts +255 -0
  86. setiastro/saspro/translations/saspro_de.qm +0 -0
  87. setiastro/saspro/translations/saspro_de.ts +3 -3
  88. setiastro/saspro/translations/saspro_es.qm +0 -0
  89. setiastro/saspro/translations/saspro_es.ts +3 -3
  90. setiastro/saspro/translations/saspro_fr.qm +0 -0
  91. setiastro/saspro/translations/saspro_fr.ts +3 -3
  92. setiastro/saspro/translations/saspro_hi.qm +0 -0
  93. setiastro/saspro/translations/saspro_hi.ts +257 -0
  94. setiastro/saspro/translations/saspro_it.qm +0 -0
  95. setiastro/saspro/translations/saspro_it.ts +3 -3
  96. setiastro/saspro/translations/saspro_ja.qm +0 -0
  97. setiastro/saspro/translations/saspro_ja.ts +4 -4
  98. setiastro/saspro/translations/saspro_pt.qm +0 -0
  99. setiastro/saspro/translations/saspro_pt.ts +3 -3
  100. setiastro/saspro/translations/saspro_ru.qm +0 -0
  101. setiastro/saspro/translations/saspro_ru.ts +237 -0
  102. setiastro/saspro/translations/saspro_sw.qm +0 -0
  103. setiastro/saspro/translations/saspro_sw.ts +257 -0
  104. setiastro/saspro/translations/saspro_uk.qm +0 -0
  105. setiastro/saspro/translations/saspro_uk.ts +10771 -0
  106. setiastro/saspro/translations/saspro_zh.qm +0 -0
  107. setiastro/saspro/translations/saspro_zh.ts +3 -3
  108. setiastro/saspro/translations/sw_translations.py +3671 -0
  109. setiastro/saspro/translations/uk_translations.py +3700 -0
  110. setiastro/saspro/translations/zh_translations.py +16 -0
  111. setiastro/saspro/versioning.py +12 -6
  112. setiastro/saspro/view_bundle.py +3 -0
  113. setiastro/saspro/wavescale_hdr.py +22 -1
  114. setiastro/saspro/wavescalede.py +23 -1
  115. setiastro/saspro/whitebalance.py +39 -3
  116. setiastro/saspro/widgets/minigame/game.js +986 -0
  117. setiastro/saspro/widgets/minigame/index.html +53 -0
  118. setiastro/saspro/widgets/minigame/style.css +241 -0
  119. setiastro/saspro/widgets/resource_monitor.py +237 -0
  120. setiastro/saspro/widgets/wavelet_utils.py +52 -20
  121. setiastro/saspro/wimi.py +7996 -0
  122. setiastro/saspro/wims.py +578 -0
  123. {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/METADATA +15 -4
  124. {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/RECORD +128 -103
  125. {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/WHEEL +0 -0
  126. {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/entry_points.txt +0 -0
  127. {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/licenses/LICENSE +0 -0
  128. {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/licenses/license.txt +0 -0
@@ -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
- Apply per-channel MTF exactly. p from _mtf_params_unlinked.
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
- x = np.stack([x]*3, axis=-1)
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
- x = np.repeat(x, 3, axis=2)
217
-
218
- out = np.empty_like(x, dtype=np.float32)
219
- for c in range(x.shape[2]):
220
- out[..., c] = _mtf_apply(x[..., c], float(p["s"][c]), float(p["m"][c]), float(p["h"][c]))
221
- return np.clip(out, 0.0, 1.0)
222
-
223
-
224
- def _invert_mtf_unlinked_rgb(img_rgb01: np.ndarray, p: dict) -> np.ndarray:
225
- """
226
- Exact analytic inverse per channel (uses same s/m/h arrays).
227
- """
228
- y = np.asarray(img_rgb01, dtype=np.float32)
229
- if y.ndim == 2:
230
- y = np.stack([y]*3, axis=-1)
231
- elif y.ndim == 3 and y.shape[2] == 1:
232
- y = np.repeat(y, 3, axis=2)
233
-
234
- out = np.empty_like(y, dtype=np.float32)
235
- for c in range(y.shape[2]):
236
- out[..., c] = _mtf_inverse(y[..., c], float(p["s"][c]), float(p["m"][c]), float(p["h"][c]))
237
- return np.clip(out, 0.0, 1.0)
238
-
239
- def _stat_stretch_rgb(img: np.ndarray,
240
- lo_pct: float = 0.25,
241
- hi_pct: float = 99.75) -> tuple[np.ndarray, dict]:
242
- """
243
- Make sure img is RGB float32 in [0,1], stretch each channel to [0,1]
244
- using percentiles. Returns (stretched_img, params) where params can be
245
- fed to _stat_unstretch_rgb() to invert exactly.
246
- """
247
- was_single = (img.ndim == 2) or (img.ndim == 3 and img.shape[2] == 1)
248
- if was_single:
249
- img = np.stack([img] * 3, axis=-1)
250
-
251
- x = img.astype(np.float32, copy=False)
252
- out = np.empty_like(x, dtype=np.float32)
253
- lo_vals, hi_vals = [], []
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
- for c in range(3):
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
- x = arr
375
- if x.ndim == 2:
376
- x = np.stack([x] * 3, axis=-1)
377
- elif x.ndim == 3 and x.shape[2] == 1:
378
- x = np.repeat(x, 3, axis=2)
379
- x = np.nan_to_num(x, nan=0.0, posinf=0.0, neginf=0.0).astype(np.float32, copy=False)
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(x)) if x.size else 1.0
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
- xin = (x / scale_factor) if scale_factor > 1.0 else x
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
- processing_image = np.stack([src]*3, axis=-1)
669
- elif src.ndim == 3 and src.shape[2] == 1:
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 (same reason as before: 16-bit export safety)
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 = np.stack([src] * 3, axis=-1)
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
- img_for_darkstar,
1066
+ to_save,
1093
1067
  in_path,
1094
1068
  original_format="tif",
1095
1069
  bit_depth="32-bit floating point",
@@ -469,9 +469,16 @@ def _init_legacy_paths():
469
469
  _legacy = _init_legacy_paths()
470
470
  globals().update(_legacy)
471
471
 
472
+
473
+ # Background for startup
474
+ background_startup_path = os.path.join(_get_base_path(), 'images', 'Background_startup.jpg')
475
+ _legacy['background_startup_path'] = background_startup_path
476
+
472
477
  # Export list for `from setiastro.saspro.resources import *`
473
478
  __all__ = [
474
479
  'Icons', 'Resources',
475
480
  'get_icons', 'get_resources',
476
481
  'get_icon_path', 'get_data_path',
482
+ 'background_startup_path',
477
483
  ] + list(_legacy.keys())
484
+
@@ -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: [])
@@ -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
- if cv2 is None:
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
@@ -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
 
setiastro/saspro/sfcc.py CHANGED
@@ -346,6 +346,9 @@ class SFCCDialog(QDialog):
346
346
  def __init__(self, doc_manager, sasp_data_path, parent=None):
347
347
  super().__init__(parent)
348
348
  self.setWindowTitle(self.tr("Spectral Flux Color Calibration"))
349
+ self.setWindowFlag(Qt.WindowType.Window, True)
350
+ self.setWindowModality(Qt.WindowModality.NonModal)
351
+ self.setModal(False)
349
352
  self.setMinimumSize(800, 600)
350
353
 
351
354
  self.doc_manager = doc_manager
@@ -1155,18 +1158,56 @@ class SFCCDialog(QDialog):
1155
1158
  diag_meas_BG, diag_exp_BG = [], []
1156
1159
  enriched = []
1157
1160
 
1161
+ # --- Optimization: Pre-calculate integrals for unique templates ---
1162
+ unique_simbad_types = set(m["template"] for m in raw_matches)
1163
+
1164
+ # Map simbad_type -> pickles_template_name
1165
+ simbad_to_pickles = {}
1166
+ pickles_templates_needed = set()
1167
+
1168
+ for sp in unique_simbad_types:
1169
+ cands = pickles_match_for_simbad(sp, getattr(self, "pickles_templates", []))
1170
+ if cands:
1171
+ pickles_name = cands[0]
1172
+ simbad_to_pickles[sp] = pickles_name
1173
+ pickles_templates_needed.add(pickles_name)
1174
+
1175
+ # Pre-calc integrals for each unique Pickles template
1176
+ # Cache structure: template_name -> (S_sr, S_sg, S_sb)
1177
+ template_integrals = {}
1178
+
1179
+ # Cache for load_sed to avoid re-reading even across different calls if desired,
1180
+ # but here we just optimize the loop.
1181
+
1182
+ for pname in pickles_templates_needed:
1183
+ try:
1184
+ wl_s, fl_s = load_sed(pname)
1185
+ fs_i = np.interp(wl_grid, wl_s, fl_s, left=0., right=0.)
1186
+
1187
+ S_sr = np.trapezoid(fs_i * T_sys_R, x=wl_grid)
1188
+ S_sg = np.trapezoid(fs_i * T_sys_G, x=wl_grid)
1189
+ S_sb = np.trapezoid(fs_i * T_sys_B, x=wl_grid)
1190
+
1191
+ template_integrals[pname] = (S_sr, S_sg, S_sb)
1192
+ except Exception as e:
1193
+ print(f"[SFCC] Warning: failed to load/integrate template {pname}: {e}")
1194
+
1195
+ # --- Main Match Loop ---
1158
1196
  for m in raw_matches:
1159
1197
  xi, yi, sp = m["x_pix"], m["y_pix"], m["template"]
1160
1198
  Rm = float(base[yi, xi, 0]); Gm = float(base[yi, xi, 1]); Bm = float(base[yi, xi, 2])
1161
1199
  if Gm <= 0: continue
1162
1200
 
1163
- cands = pickles_match_for_simbad(sp, getattr(self, "pickles_templates", []))
1164
- if not cands: continue
1165
- wl_s, fl_s = load_sed(cands[0])
1166
- fs_i = np.interp(wl_grid, wl_s, fl_s, left=0., right=0.)
1167
- S_sr = np.trapezoid(fs_i * T_sys_R, x=wl_grid)
1168
- S_sg = np.trapezoid(fs_i * T_sys_G, x=wl_grid)
1169
- S_sb = np.trapezoid(fs_i * T_sys_B, x=wl_grid)
1201
+ # 1. Resolve Simbad -> Pickles
1202
+ pname = simbad_to_pickles.get(sp)
1203
+ if not pname: continue
1204
+
1205
+ # 2. Retrieve pre-calced integrals
1206
+ integrals = template_integrals.get(pname)
1207
+ if not integrals: continue
1208
+
1209
+ S_sr, S_sg, S_sb = integrals
1210
+
1170
1211
  if S_sg <= 0: continue
1171
1212
 
1172
1213
  exp_RG = S_sr / S_sg; exp_BG = S_sb / S_sg
@@ -1180,7 +1221,8 @@ class SFCCDialog(QDialog):
1180
1221
  "S_star_R": S_sr, "S_star_G": S_sg, "S_star_B": S_sb,
1181
1222
  "exp_RG": exp_RG, "exp_BG": exp_BG
1182
1223
  })
1183
- self._last_matched = enriched # <-- missing in SASpro
1224
+
1225
+ self._last_matched = enriched
1184
1226
  diag_meas_RG = np.array(diag_meas_RG); diag_exp_RG = np.array(diag_exp_RG)
1185
1227
  diag_meas_BG = np.array(diag_meas_BG); diag_exp_BG = np.array(diag_exp_BG)
1186
1228
  if diag_meas_RG.size == 0 or diag_meas_BG.size == 0:
@@ -363,6 +363,9 @@ class SignatureInsertDialogPro(QDialog):
363
363
  def __init__(self, parent, doc, icon: QIcon | None = None):
364
364
  super().__init__(parent)
365
365
  self.setWindowTitle(self.tr("Signature / Insert"))
366
+ self.setWindowFlag(Qt.WindowType.Window, True)
367
+ self.setWindowModality(Qt.WindowModality.NonModal)
368
+ self.setModal(False)
366
369
  if icon:
367
370
  try: self.setWindowIcon(icon)
368
371
  except Exception as e: