setiastrosuitepro 1.6.0__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.

Files changed (174) hide show
  1. setiastro/__init__.py +2 -0
  2. setiastro/saspro/__init__.py +20 -0
  3. setiastro/saspro/__main__.py +784 -0
  4. setiastro/saspro/_generated/__init__.py +7 -0
  5. setiastro/saspro/_generated/build_info.py +2 -0
  6. setiastro/saspro/abe.py +1295 -0
  7. setiastro/saspro/abe_preset.py +196 -0
  8. setiastro/saspro/aberration_ai.py +694 -0
  9. setiastro/saspro/aberration_ai_preset.py +224 -0
  10. setiastro/saspro/accel_installer.py +218 -0
  11. setiastro/saspro/accel_workers.py +30 -0
  12. setiastro/saspro/add_stars.py +621 -0
  13. setiastro/saspro/astrobin_exporter.py +1007 -0
  14. setiastro/saspro/astrospike.py +153 -0
  15. setiastro/saspro/astrospike_python.py +1839 -0
  16. setiastro/saspro/autostretch.py +196 -0
  17. setiastro/saspro/backgroundneutral.py +560 -0
  18. setiastro/saspro/batch_convert.py +325 -0
  19. setiastro/saspro/batch_renamer.py +519 -0
  20. setiastro/saspro/blemish_blaster.py +488 -0
  21. setiastro/saspro/blink_comparator_pro.py +2923 -0
  22. setiastro/saspro/bundles.py +61 -0
  23. setiastro/saspro/bundles_dock.py +114 -0
  24. setiastro/saspro/cheat_sheet.py +168 -0
  25. setiastro/saspro/clahe.py +342 -0
  26. setiastro/saspro/comet_stacking.py +1377 -0
  27. setiastro/saspro/config.py +38 -0
  28. setiastro/saspro/config_bootstrap.py +40 -0
  29. setiastro/saspro/config_manager.py +316 -0
  30. setiastro/saspro/continuum_subtract.py +1617 -0
  31. setiastro/saspro/convo.py +1397 -0
  32. setiastro/saspro/convo_preset.py +414 -0
  33. setiastro/saspro/copyastro.py +187 -0
  34. setiastro/saspro/cosmicclarity.py +1564 -0
  35. setiastro/saspro/cosmicclarity_preset.py +407 -0
  36. setiastro/saspro/crop_dialog_pro.py +948 -0
  37. setiastro/saspro/crop_preset.py +189 -0
  38. setiastro/saspro/curve_editor_pro.py +2544 -0
  39. setiastro/saspro/curves_preset.py +375 -0
  40. setiastro/saspro/debayer.py +670 -0
  41. setiastro/saspro/debug_utils.py +29 -0
  42. setiastro/saspro/dnd_mime.py +35 -0
  43. setiastro/saspro/doc_manager.py +2634 -0
  44. setiastro/saspro/exoplanet_detector.py +2166 -0
  45. setiastro/saspro/file_utils.py +284 -0
  46. setiastro/saspro/fitsmodifier.py +744 -0
  47. setiastro/saspro/free_torch_memory.py +48 -0
  48. setiastro/saspro/frequency_separation.py +1343 -0
  49. setiastro/saspro/function_bundle.py +1594 -0
  50. setiastro/saspro/ghs_dialog_pro.py +660 -0
  51. setiastro/saspro/ghs_preset.py +284 -0
  52. setiastro/saspro/graxpert.py +634 -0
  53. setiastro/saspro/graxpert_preset.py +287 -0
  54. setiastro/saspro/gui/__init__.py +0 -0
  55. setiastro/saspro/gui/main_window.py +8494 -0
  56. setiastro/saspro/gui/mixins/__init__.py +33 -0
  57. setiastro/saspro/gui/mixins/dock_mixin.py +263 -0
  58. setiastro/saspro/gui/mixins/file_mixin.py +445 -0
  59. setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
  60. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  61. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  62. setiastro/saspro/gui/mixins/menu_mixin.py +361 -0
  63. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  64. setiastro/saspro/gui/mixins/toolbar_mixin.py +1324 -0
  65. setiastro/saspro/gui/mixins/update_mixin.py +309 -0
  66. setiastro/saspro/gui/mixins/view_mixin.py +435 -0
  67. setiastro/saspro/halobgon.py +462 -0
  68. setiastro/saspro/header_viewer.py +445 -0
  69. setiastro/saspro/headless_utils.py +88 -0
  70. setiastro/saspro/histogram.py +753 -0
  71. setiastro/saspro/history_explorer.py +939 -0
  72. setiastro/saspro/image_combine.py +414 -0
  73. setiastro/saspro/image_peeker_pro.py +1596 -0
  74. setiastro/saspro/imageops/__init__.py +37 -0
  75. setiastro/saspro/imageops/mdi_snap.py +292 -0
  76. setiastro/saspro/imageops/scnr.py +36 -0
  77. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  78. setiastro/saspro/imageops/stretch.py +244 -0
  79. setiastro/saspro/isophote.py +1179 -0
  80. setiastro/saspro/layers.py +208 -0
  81. setiastro/saspro/layers_dock.py +714 -0
  82. setiastro/saspro/lazy_imports.py +193 -0
  83. setiastro/saspro/legacy/__init__.py +2 -0
  84. setiastro/saspro/legacy/image_manager.py +2226 -0
  85. setiastro/saspro/legacy/numba_utils.py +3659 -0
  86. setiastro/saspro/legacy/xisf.py +1071 -0
  87. setiastro/saspro/linear_fit.py +534 -0
  88. setiastro/saspro/live_stacking.py +1830 -0
  89. setiastro/saspro/log_bus.py +5 -0
  90. setiastro/saspro/logging_config.py +460 -0
  91. setiastro/saspro/luminancerecombine.py +309 -0
  92. setiastro/saspro/main_helpers.py +201 -0
  93. setiastro/saspro/mask_creation.py +928 -0
  94. setiastro/saspro/masks_core.py +56 -0
  95. setiastro/saspro/mdi_widgets.py +353 -0
  96. setiastro/saspro/memory_utils.py +666 -0
  97. setiastro/saspro/metadata_patcher.py +75 -0
  98. setiastro/saspro/mfdeconv.py +3826 -0
  99. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  100. setiastro/saspro/mfdeconvcudnn.py +3263 -0
  101. setiastro/saspro/mfdeconvsport.py +2382 -0
  102. setiastro/saspro/minorbodycatalog.py +567 -0
  103. setiastro/saspro/morphology.py +382 -0
  104. setiastro/saspro/multiscale_decomp.py +1290 -0
  105. setiastro/saspro/nbtorgb_stars.py +531 -0
  106. setiastro/saspro/numba_utils.py +3044 -0
  107. setiastro/saspro/numba_warmup.py +141 -0
  108. setiastro/saspro/ops/__init__.py +9 -0
  109. setiastro/saspro/ops/command_help_dialog.py +623 -0
  110. setiastro/saspro/ops/command_runner.py +217 -0
  111. setiastro/saspro/ops/commands.py +1594 -0
  112. setiastro/saspro/ops/script_editor.py +1102 -0
  113. setiastro/saspro/ops/scripts.py +1413 -0
  114. setiastro/saspro/ops/settings.py +560 -0
  115. setiastro/saspro/parallel_utils.py +554 -0
  116. setiastro/saspro/pedestal.py +121 -0
  117. setiastro/saspro/perfect_palette_picker.py +1053 -0
  118. setiastro/saspro/pipeline.py +110 -0
  119. setiastro/saspro/pixelmath.py +1600 -0
  120. setiastro/saspro/plate_solver.py +2435 -0
  121. setiastro/saspro/project_io.py +797 -0
  122. setiastro/saspro/psf_utils.py +136 -0
  123. setiastro/saspro/psf_viewer.py +549 -0
  124. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  125. setiastro/saspro/remove_green.py +314 -0
  126. setiastro/saspro/remove_stars.py +1625 -0
  127. setiastro/saspro/remove_stars_preset.py +404 -0
  128. setiastro/saspro/resources.py +472 -0
  129. setiastro/saspro/rgb_combination.py +207 -0
  130. setiastro/saspro/rgb_extract.py +19 -0
  131. setiastro/saspro/rgbalign.py +723 -0
  132. setiastro/saspro/runtime_imports.py +7 -0
  133. setiastro/saspro/runtime_torch.py +754 -0
  134. setiastro/saspro/save_options.py +72 -0
  135. setiastro/saspro/selective_color.py +1552 -0
  136. setiastro/saspro/sfcc.py +1425 -0
  137. setiastro/saspro/shortcuts.py +2807 -0
  138. setiastro/saspro/signature_insert.py +1099 -0
  139. setiastro/saspro/stacking_suite.py +17712 -0
  140. setiastro/saspro/star_alignment.py +7420 -0
  141. setiastro/saspro/star_alignment_preset.py +329 -0
  142. setiastro/saspro/star_metrics.py +49 -0
  143. setiastro/saspro/star_spikes.py +681 -0
  144. setiastro/saspro/star_stretch.py +470 -0
  145. setiastro/saspro/stat_stretch.py +502 -0
  146. setiastro/saspro/status_log_dock.py +78 -0
  147. setiastro/saspro/subwindow.py +3267 -0
  148. setiastro/saspro/supernovaasteroidhunter.py +1712 -0
  149. setiastro/saspro/swap_manager.py +99 -0
  150. setiastro/saspro/torch_backend.py +89 -0
  151. setiastro/saspro/torch_rejection.py +434 -0
  152. setiastro/saspro/view_bundle.py +1555 -0
  153. setiastro/saspro/wavescale_hdr.py +624 -0
  154. setiastro/saspro/wavescale_hdr_preset.py +100 -0
  155. setiastro/saspro/wavescalede.py +657 -0
  156. setiastro/saspro/wavescalede_preset.py +228 -0
  157. setiastro/saspro/wcs_update.py +374 -0
  158. setiastro/saspro/whitebalance.py +456 -0
  159. setiastro/saspro/widgets/__init__.py +48 -0
  160. setiastro/saspro/widgets/common_utilities.py +305 -0
  161. setiastro/saspro/widgets/graphics_views.py +122 -0
  162. setiastro/saspro/widgets/image_utils.py +518 -0
  163. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  164. setiastro/saspro/widgets/spinboxes.py +275 -0
  165. setiastro/saspro/widgets/themed_buttons.py +13 -0
  166. setiastro/saspro/widgets/wavelet_utils.py +299 -0
  167. setiastro/saspro/window_shelf.py +185 -0
  168. setiastro/saspro/xisf.py +1123 -0
  169. setiastrosuitepro-1.6.0.dist-info/METADATA +266 -0
  170. setiastrosuitepro-1.6.0.dist-info/RECORD +174 -0
  171. setiastrosuitepro-1.6.0.dist-info/WHEEL +4 -0
  172. setiastrosuitepro-1.6.0.dist-info/entry_points.txt +6 -0
  173. setiastrosuitepro-1.6.0.dist-info/licenses/LICENSE +674 -0
  174. setiastrosuitepro-1.6.0.dist-info/licenses/license.txt +2580 -0
@@ -0,0 +1,404 @@
1
+ # pro/remove_stars_preset.py
2
+ from __future__ import annotations
3
+ import os
4
+ import platform
5
+ import shutil
6
+ import numpy as np
7
+ from PyQt6.QtCore import QTimer
8
+ from PyQt6.QtWidgets import QMessageBox, QDialog, QFormLayout, QDialogButtonBox, QComboBox, QCheckBox, QSpinBox, QLabel
9
+
10
+ from setiastro.saspro.legacy.image_manager import save_image, load_image
11
+
12
+ # Reuse helpers & plumbing from the interactive module
13
+ from .remove_stars import (
14
+ _ProcThread, _ProcDialog,
15
+ _stat_stretch_rgb, _stat_unstretch_rgb,
16
+ _active_mask3_from_doc, _mask_blend_with_doc_mask, _push_as_new_doc,
17
+ _ensure_exec_bit,
18
+ )
19
+
20
+ # ---------- Headless public entry ----------
21
+ def run_remove_stars_via_preset(main, doc_or_preset=None, preset: dict | None = None, target_doc=None):
22
+ """
23
+ Headless star removal from a shortcut preset.
24
+
25
+ Supports BOTH call shapes:
26
+ 1) New CommandRunner shape:
27
+ run_remove_stars_via_preset(main, target_doc, preset)
28
+ 2) Legacy shape:
29
+ run_remove_stars_via_preset(main, preset_dict, target_doc=doc)
30
+ run_remove_stars_via_preset(main, preset_dict)
31
+ """
32
+ from PyQt6.QtWidgets import QMessageBox
33
+ from PyQt6.QtCore import QTimer
34
+ import os
35
+ import platform
36
+
37
+ # ---- Interpret arguments for backward compat / new executor ----
38
+ if preset is None and isinstance(doc_or_preset, dict):
39
+ # Legacy: (main, preset_dict, target_doc=?)
40
+ p = dict(doc_or_preset or {})
41
+ doc = target_doc
42
+ else:
43
+ # New executor: (main, doc, preset_dict)
44
+ p = dict(preset or {})
45
+ doc = target_doc if target_doc is not None else doc_or_preset
46
+
47
+ # Resolve active doc if still None
48
+ if doc is None:
49
+ d = getattr(main, "_active_doc", None)
50
+ doc = d() if callable(d) else d
51
+
52
+ if doc is None or getattr(doc, "image", None) is None:
53
+ QMessageBox.warning(main, "Remove Stars", "Load an image first.")
54
+ return
55
+
56
+ tool = str(p.get("tool", "starnet")).lower()
57
+
58
+ # mark headless + short cool-down to block the interactive path
59
+ setattr(main, "_remove_stars_headless_running", True)
60
+ setattr(main, "_remove_stars_guard", True)
61
+
62
+ def _clear_flags():
63
+ for name in ("_remove_stars_headless_running", "_remove_stars_guard"):
64
+ try:
65
+ delattr(main, name)
66
+ except Exception:
67
+ setattr(main, name, False)
68
+
69
+ try:
70
+ if tool in ("starnet", "star_net", "sn"):
71
+ _run_starnet_headless(main, doc, p)
72
+ elif tool in ("darkstar", "cosmicclarity", "cosmic_clarity"):
73
+ _run_darkstar_headless(main, doc, p)
74
+ else:
75
+ QMessageBox.critical(main, "Remove Stars", f"Unknown tool '{tool}'.")
76
+ finally:
77
+ QTimer.singleShot(1200, _clear_flags)
78
+
79
+
80
+ def apply_remove_stars_to_doc(parent, target_doc, preset: dict | None):
81
+ """
82
+ Replay helper: apply Remove Stars to a specific doc (base/ROI).
83
+ """
84
+ if parent is None:
85
+ return
86
+ main = parent
87
+ # walk up to main window if needed
88
+ while main is not None and not hasattr(main, "doc_manager"):
89
+ main = main.parent() if hasattr(main, "parent") else None
90
+ if main is None:
91
+ main = parent
92
+
93
+ run_remove_stars_via_preset(main, preset, target_doc=target_doc)
94
+
95
+
96
+ # ---------- StarNet headless ----------
97
+ def _resolve_starnet_exe_headless(main, override: str | None) -> str | None:
98
+ if override and os.path.exists(override):
99
+ if platform.system() in ("Darwin", "Linux"): _ensure_exec_bit(override)
100
+ return override
101
+ s = getattr(main, "settings", None)
102
+ exe = None
103
+ if s:
104
+ for key in ("starnet/exe_path", "paths/starnet"):
105
+ try:
106
+ val = s.value(key, "", type=str)
107
+ except Exception:
108
+ val = s.value(key, "")
109
+ if isinstance(val, str) and val.strip() and os.path.exists(val):
110
+ exe = val.strip(); break
111
+ if exe and platform.system() in ("Darwin", "Linux"): _ensure_exec_bit(exe)
112
+ return exe
113
+
114
+ def _run_starnet_headless(main, doc, p):
115
+ exe = _resolve_starnet_exe_headless(main, p.get("starnet_exe") or p.get("exe"))
116
+ if not exe:
117
+ QMessageBox.warning(main, "StarNet", "StarNet path not set. Open the interactive tool once to set it.")
118
+ return
119
+
120
+ # RGB float32 [0..1]
121
+ src = np.asarray(doc.image)
122
+ if src.ndim == 2: processing_image = np.stack([src]*3, axis=-1)
123
+ elif src.ndim == 3 and src.shape[2] == 1: processing_image = np.repeat(src, 3, axis=2)
124
+ else: processing_image = src
125
+ processing_image = processing_image.astype(np.float32, copy=False)
126
+
127
+ is_linear = bool(p.get("linear", True))
128
+ did_stretch = False
129
+ stretch_params = None
130
+ if is_linear:
131
+ processing_image, stretch_params = _stat_stretch_rgb(processing_image)
132
+ did_stretch = True
133
+ setattr(main, "_starnet_last_stretch_params", stretch_params)
134
+ else:
135
+ if hasattr(main, "_starnet_last_stretch_params"):
136
+ delattr(main, "_starnet_last_stretch_params")
137
+
138
+ starnet_dir = os.path.dirname(exe) or os.getcwd()
139
+ in_path = os.path.join(starnet_dir, "imagetoremovestars.tif")
140
+ out_path = os.path.join(starnet_dir, "starless.tif")
141
+
142
+ try:
143
+ save_image(processing_image, in_path, original_format="tif",
144
+ bit_depth="16-bit", original_header=None, is_mono=False,
145
+ image_meta=None, file_meta=None)
146
+ except Exception as e:
147
+ QMessageBox.critical(main, "StarNet", f"Failed to write input TIFF:\n{e}")
148
+ return
149
+
150
+ exe_name = os.path.basename(exe).lower()
151
+ sysname = platform.system()
152
+ if sysname in ("Windows", "Linux"):
153
+ command = [exe, in_path, out_path, "256"]
154
+ else:
155
+ if "starnet2" in exe_name:
156
+ command = [exe, "--input", in_path, "--output", out_path]
157
+ else:
158
+ command = [exe, in_path, out_path]
159
+
160
+ dlg = _ProcDialog(main, title="StarNet Progress")
161
+ thr = _ProcThread(command, cwd=starnet_dir)
162
+ thr.output_signal.connect(dlg.append_text)
163
+ thr.finished_signal.connect(lambda rc: _finish_starnet(main, doc, rc, dlg, in_path, out_path, did_stretch))
164
+ dlg.cancel_button.clicked.connect(thr.terminate)
165
+ dlg.show(); thr.start(); dlg.exec()
166
+
167
+ def _finish_starnet(main, doc, rc, dlg, in_path, out_path, did_stretch):
168
+ if rc != 0 or not os.path.exists(out_path):
169
+ QMessageBox.critical(main, "StarNet", "StarNet failed or no output image produced.")
170
+ _safe_rm(in_path); _safe_rm(out_path); dlg.close(); return
171
+
172
+ starless_rgb, _, _, _ = load_image(out_path)
173
+ if starless_rgb is None:
174
+ QMessageBox.critical(main, "StarNet", "Failed to load starless image.")
175
+ _safe_rm(in_path); _safe_rm(out_path); dlg.close(); return
176
+
177
+ if starless_rgb.ndim == 2 or (starless_rgb.ndim == 3 and starless_rgb.shape[2] == 1):
178
+ starless_rgb = np.stack([starless_rgb]*3, axis=-1)
179
+ starless_rgb = starless_rgb.astype(np.float32, copy=False)
180
+
181
+ if did_stretch:
182
+ try:
183
+ params = getattr(main, "_starnet_last_stretch_params", None)
184
+ if params: starless_rgb = _stat_unstretch_rgb(starless_rgb, params)
185
+ except Exception:
186
+ pass
187
+
188
+ # original as RGB
189
+ orig = np.asarray(doc.image)
190
+ if orig.ndim == 2: original_rgb = np.stack([orig]*3, axis=-1)
191
+ elif orig.ndim == 3 and orig.shape[2] == 1: original_rgb = np.repeat(orig, 3, axis=2)
192
+ else: original_rgb = orig
193
+ original_rgb = original_rgb.astype(np.float32, copy=False)
194
+
195
+ # Stars-Only (same as interactive)
196
+ with np.errstate(divide='ignore', invalid='ignore'):
197
+ stars_only = (original_rgb - starless_rgb) / np.clip(1.0 - starless_rgb, 1e-6, None)
198
+ stars_only = np.nan_to_num(stars_only, nan=0.0, posinf=0.0, neginf=0.0)
199
+ stars_only = np.clip(stars_only, 0.0, 1.0)
200
+ m3 = _active_mask3_from_doc(doc, stars_only.shape[1], stars_only.shape[0])
201
+ if m3 is not None: stars_only *= m3
202
+ _push_as_new_doc(main, doc, stars_only, title_suffix="_stars", source="Stars-Only (StarNet)")
203
+
204
+ # mask-blend starless, then commit
205
+ final_starless = _mask_blend_with_doc_mask(doc, starless_rgb, original_rgb)
206
+ try:
207
+ meta = {"step_name": "Stars Removed", "bit_depth": "32-bit floating point", "is_mono": False}
208
+ doc.apply_edit(final_starless.astype(np.float32, copy=False), metadata=meta, step_name="Stars Removed")
209
+ if hasattr(main, "_log"): main._log("Stars Removed (StarNet, headless)")
210
+ except Exception as e:
211
+ QMessageBox.critical(main, "StarNet", f"Failed to apply starless result:\n{e}")
212
+
213
+ _safe_rm(in_path); _safe_rm(out_path); dlg.close()
214
+
215
+
216
+ # ---------- DarkStar headless ----------
217
+ def _resolve_darkstar_exe_headless(main, override: str | None) -> tuple[str | None, str | None]:
218
+ if override and os.path.exists(override):
219
+ base = os.path.dirname(override)
220
+ if platform.system() in ("Darwin","Linux"): _ensure_exec_bit(override)
221
+ return override, base
222
+ s = getattr(main, "settings", None)
223
+ base = None
224
+ if s:
225
+ for key in ("paths/cosmic_clarity", "cosmic_clarity_folder"):
226
+ try:
227
+ v = s.value(key, "", type=str)
228
+ except Exception:
229
+ v = s.value(key, "")
230
+ if isinstance(v, str) and v.strip() and os.path.isdir(v):
231
+ base = v.strip(); break
232
+ if not base: return None, None
233
+ exe = os.path.join(base, "setiastrocosmicclarity_darkstar.exe" if platform.system()=="Windows"
234
+ else "setiastrocosmicclarity_darkstar")
235
+ if not os.path.exists(exe): return None, None
236
+ if platform.system() in ("Darwin","Linux"): _ensure_exec_bit(exe)
237
+ return exe, base
238
+
239
+ def _run_darkstar_headless(main, doc, p):
240
+ exe, base = _resolve_darkstar_exe_headless(main, p.get("darkstar_exe") or p.get("exe"))
241
+ if not exe or not base:
242
+ QMessageBox.warning(main, "CosmicClarity DarkStar", "DarkStar path not set. Open the interactive tool once to set it.")
243
+ return
244
+
245
+ input_dir = os.path.join(base, "input")
246
+ output_dir = os.path.join(base, "output")
247
+ os.makedirs(input_dir, exist_ok=True)
248
+ os.makedirs(output_dir, exist_ok=True)
249
+
250
+ in_path = os.path.join(input_dir, "imagetoremovestars.tif")
251
+ try:
252
+ save_image(doc.image, in_path, original_format="tif",
253
+ bit_depth="32-bit floating point",
254
+ original_header=None, is_mono=False, image_meta=None, file_meta=None)
255
+ except Exception as e:
256
+ QMessageBox.critical(main, "CosmicClarity DarkStar", f"Failed to write input TIFF:\n{e}")
257
+ return
258
+
259
+ disable_gpu = bool(p.get("disable_gpu", False))
260
+ mode = str(p.get("mode", "unscreen"))
261
+ show = bool(p.get("show_extracted_stars", True))
262
+ stride = int(p.get("stride", 512))
263
+
264
+ args = []
265
+ if disable_gpu: args.append("--disable_gpu")
266
+ args += ["--star_removal_mode", mode]
267
+ if show: args.append("--show_extracted_stars")
268
+ args += ["--chunk_size", str(stride)]
269
+
270
+ command = [exe] + args
271
+
272
+ dlg = _ProcDialog(main, title="CosmicClarityDarkStar Progress")
273
+ thr = _ProcThread(command, cwd=base)
274
+ thr.output_signal.connect(dlg.append_text)
275
+ thr.finished_signal.connect(lambda rc: _finish_darkstar(main, doc, rc, dlg, in_path, output_dir))
276
+ dlg.cancel_button.clicked.connect(thr.terminate)
277
+ dlg.show(); thr.start(); dlg.exec()
278
+
279
+ def _finish_darkstar(main, doc, rc, dlg, in_path, output_dir):
280
+ if rc != 0:
281
+ QMessageBox.critical(main, "CosmicClarity DarkStar", "Process failed.")
282
+ _safe_rm(in_path); dlg.close(); return
283
+
284
+ starless_path = os.path.join(output_dir, "imagetoremovestars_starless.tif")
285
+ if not os.path.exists(starless_path):
286
+ QMessageBox.critical(main, "CosmicClarity DarkStar", "Starless image not found.")
287
+ _safe_rm(in_path); dlg.close(); return
288
+
289
+ starless, _, _, _ = load_image(starless_path)
290
+ if starless is None:
291
+ QMessageBox.critical(main, "CosmicClarity DarkStar", "Failed to load starless image.")
292
+ _safe_rm(in_path); dlg.close(); return
293
+
294
+ if starless.ndim == 2 or (starless.ndim == 3 and starless.shape[2] == 1):
295
+ starless_rgb = np.stack([starless]*3, axis=-1)
296
+ else:
297
+ starless_rgb = starless
298
+ starless_rgb = starless_rgb.astype(np.float32, copy=False)
299
+
300
+ src = np.asarray(doc.image)
301
+ if src.ndim == 2: original_rgb = np.stack([src]*3, axis=-1)
302
+ elif src.ndim == 3 and src.shape[2] == 1: original_rgb = np.repeat(src, 3, axis=2)
303
+ else: original_rgb = src
304
+ original_rgb = original_rgb.astype(np.float32, copy=False)
305
+
306
+ # stars-only (if DarkStar produced it)
307
+ stars_path = os.path.join(output_dir, "imagetoremovestars_stars_only.tif")
308
+ if os.path.exists(stars_path):
309
+ stars_only, _, _, _ = load_image(stars_path)
310
+ if stars_only is not None:
311
+ if stars_only.ndim == 2 or (stars_only.ndim == 3 and stars_only.shape[2] == 1):
312
+ stars_only = np.stack([stars_only]*3, axis=-1)
313
+ stars_only = stars_only.astype(np.float32, copy=False)
314
+ m3 = _active_mask3_from_doc(doc, stars_only.shape[1], stars_only.shape[0])
315
+ if m3 is not None: stars_only *= m3
316
+ _push_as_new_doc(main, doc, stars_only, title_suffix="_stars", source="Stars-Only (DarkStar)")
317
+
318
+ final_starless = _mask_blend_with_doc_mask(doc, starless_rgb, original_rgb)
319
+ try:
320
+ meta = {"step_name": "Stars Removed", "bit_depth": "32-bit floating point", "is_mono": False}
321
+ doc.apply_edit(final_starless.astype(np.float32, copy=False), metadata=meta, step_name="Stars Removed")
322
+ if hasattr(main, "_log"): main._log("Stars Removed (DarkStar, headless)")
323
+ except Exception as e:
324
+ QMessageBox.critical(main, "CosmicClarity DarkStar", f"Failed to apply starless result:\n{e}")
325
+
326
+ # cleanup
327
+ try:
328
+ _safe_rm(in_path)
329
+ for fn in ("imagetoremovestars_starless.tif","imagetoremovestars_stars_only.tif"):
330
+ p = os.path.join(output_dir, fn)
331
+ if os.path.exists(p): _safe_rm(p)
332
+ except Exception:
333
+ pass
334
+ dlg.close()
335
+
336
+
337
+ # ---------- Simple preset editor (for the shortcut button) ----------
338
+ class RemoveStarsPresetDialog(QDialog):
339
+ def __init__(self, parent=None, initial: dict | None = None):
340
+ super().__init__(parent)
341
+ self.setWindowTitle("Remove Stars — Preset")
342
+ p = dict(initial or {})
343
+
344
+ self.cmb_tool = QComboBox()
345
+ self.cmb_tool.addItems(["StarNet", "CosmicClarity DarkStar"])
346
+ self.cmb_tool.setCurrentIndex(0 if str(p.get("tool","starnet")).lower().startswith("star") else 1)
347
+
348
+ # StarNet options
349
+ self.chk_linear = QCheckBox("Image is linear (apply temporary stretch)")
350
+ self.chk_linear.setChecked(bool(p.get("linear", True)))
351
+
352
+ # DarkStar options
353
+ self.chk_disable_gpu = QCheckBox("Disable GPU")
354
+ self.chk_disable_gpu.setChecked(bool(p.get("disable_gpu", False)))
355
+ self.cmb_mode = QComboBox(); self.cmb_mode.addItems(["unscreen","additive"])
356
+ self.cmb_mode.setCurrentText(str(p.get("mode","unscreen")))
357
+ self.chk_show = QCheckBox("Show extracted stars")
358
+ self.chk_show.setChecked(bool(p.get("show_extracted_stars", True)))
359
+ self.cmb_stride = QComboBox()
360
+ for v in (64,128,256,512,1024): self.cmb_stride.addItem(str(v), v)
361
+ self.cmb_stride.setCurrentText(str(int(p.get("stride", 512))))
362
+
363
+ form = QFormLayout(self)
364
+ form.addRow("Tool:", self.cmb_tool)
365
+ form.addRow(QLabel("— StarNet —"))
366
+ form.addRow("", self.chk_linear)
367
+ form.addRow(QLabel("— DarkStar —"))
368
+ form.addRow("Mode:", self.cmb_mode)
369
+ form.addRow("Stride:", self.cmb_stride)
370
+ form.addRow("", self.chk_disable_gpu)
371
+ form.addRow("", self.chk_show)
372
+
373
+ btns = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, parent=self)
374
+ btns.accepted.connect(self.accept); btns.rejected.connect(self.reject)
375
+ form.addRow(btns)
376
+
377
+ # show/hide sections depending on tool
378
+ def _toggle():
379
+ star = self.cmb_tool.currentIndex() == 0
380
+ self.chk_linear.setEnabled(star)
381
+ for w in (self.cmb_mode, self.cmb_stride, self.chk_disable_gpu, self.chk_show):
382
+ w.setEnabled(not star)
383
+ self.cmb_tool.currentIndexChanged.connect(lambda _: _toggle())
384
+ _toggle()
385
+
386
+ def result_dict(self) -> dict:
387
+ if self.cmb_tool.currentIndex() == 0:
388
+ return {"tool": "starnet", "linear": bool(self.chk_linear.isChecked())}
389
+ return {
390
+ "tool": "darkstar",
391
+ "disable_gpu": bool(self.chk_disable_gpu.isChecked()),
392
+ "mode": self.cmb_mode.currentText(),
393
+ "show_extracted_stars": bool(self.chk_show.isChecked()),
394
+ "stride": int(self.cmb_stride.currentData()),
395
+ }
396
+
397
+
398
+ # ---------- local util ----------
399
+ def _safe_rm(p):
400
+ try:
401
+ if p and os.path.exists(p): os.remove(p)
402
+ except Exception as e:
403
+ import logging
404
+ logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")