setiastrosuitepro 1.7.5.post1__py3-none-any.whl → 1.8.0.post3__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/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/accel_installer.py +21 -8
- setiastro/saspro/accel_workers.py +11 -12
- setiastro/saspro/comet_stacking.py +113 -85
- setiastro/saspro/cosmicclarity.py +604 -826
- setiastro/saspro/cosmicclarity_engines/benchmark_engine.py +732 -0
- setiastro/saspro/cosmicclarity_engines/darkstar_engine.py +576 -0
- setiastro/saspro/cosmicclarity_engines/denoise_engine.py +567 -0
- setiastro/saspro/cosmicclarity_engines/satellite_engine.py +620 -0
- setiastro/saspro/cosmicclarity_engines/sharpen_engine.py +587 -0
- setiastro/saspro/cosmicclarity_engines/superres_engine.py +412 -0
- setiastro/saspro/gui/main_window.py +14 -0
- setiastro/saspro/gui/mixins/menu_mixin.py +2 -0
- setiastro/saspro/model_manager.py +324 -0
- setiastro/saspro/model_workers.py +102 -0
- setiastro/saspro/ops/benchmark.py +320 -0
- setiastro/saspro/ops/settings.py +407 -10
- setiastro/saspro/remove_stars.py +424 -442
- setiastro/saspro/resources.py +73 -10
- setiastro/saspro/runtime_torch.py +107 -22
- setiastro/saspro/signature_insert.py +14 -3
- {setiastrosuitepro-1.7.5.post1.dist-info → setiastrosuitepro-1.8.0.post3.dist-info}/METADATA +2 -1
- {setiastrosuitepro-1.7.5.post1.dist-info → setiastrosuitepro-1.8.0.post3.dist-info}/RECORD +27 -18
- {setiastrosuitepro-1.7.5.post1.dist-info → setiastrosuitepro-1.8.0.post3.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.7.5.post1.dist-info → setiastrosuitepro-1.8.0.post3.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.7.5.post1.dist-info → setiastrosuitepro-1.8.0.post3.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.7.5.post1.dist-info → setiastrosuitepro-1.8.0.post3.dist-info}/licenses/license.txt +0 -0
setiastro/saspro/remove_stars.py
CHANGED
|
@@ -7,13 +7,18 @@ import stat
|
|
|
7
7
|
import tempfile
|
|
8
8
|
import numpy as np
|
|
9
9
|
|
|
10
|
-
from PyQt6.QtCore import QThread, pyqtSignal
|
|
10
|
+
from PyQt6.QtCore import QThread, pyqtSignal, Qt
|
|
11
11
|
from PyQt6.QtWidgets import (
|
|
12
12
|
QInputDialog, QMessageBox, QFileDialog,
|
|
13
|
-
QDialog, QVBoxLayout, QTextEdit, QPushButton,
|
|
13
|
+
QDialog, QVBoxLayout, QTextEdit, QPushButton, QProgressBar,
|
|
14
14
|
QLabel, QComboBox, QCheckBox, QSpinBox, QFormLayout, QDialogButtonBox, QWidget, QHBoxLayout
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
+
from setiastro.saspro.cosmicclarity_engines.darkstar_engine import (
|
|
18
|
+
darkstar_starremoval_rgb01,
|
|
19
|
+
DarkStarParams,
|
|
20
|
+
)
|
|
21
|
+
|
|
17
22
|
# use your legacy I/O functions (as requested)
|
|
18
23
|
from setiastro.saspro.legacy.image_manager import save_image, load_image
|
|
19
24
|
import glob
|
|
@@ -25,6 +30,7 @@ except Exception:
|
|
|
25
30
|
# Shared utilities
|
|
26
31
|
from setiastro.saspro.widgets.image_utils import extract_mask_from_document as _active_mask_array_from_doc
|
|
27
32
|
|
|
33
|
+
|
|
28
34
|
_MAD_NORM = 1.4826
|
|
29
35
|
|
|
30
36
|
# --------- deterministic, invertible stretch used for StarNet ----------
|
|
@@ -393,55 +399,6 @@ def starnet_starless_from_array(arr_rgb01: np.ndarray, settings, *, tmp_prefix="
|
|
|
393
399
|
|
|
394
400
|
return result
|
|
395
401
|
|
|
396
|
-
|
|
397
|
-
def darkstar_starless_from_array(arr_rgb01: np.ndarray, settings, *, tmp_prefix="comet",
|
|
398
|
-
disable_gpu=False, mode="unscreen", stride=512) -> np.ndarray:
|
|
399
|
-
"""
|
|
400
|
-
Save arr -> run DarkStar -> load starless -> return starless RGB float32 [0..1].
|
|
401
|
-
"""
|
|
402
|
-
exe, base = _resolve_darkstar_exe(type("dummy", (), {"settings": settings}) )
|
|
403
|
-
if not exe or not base:
|
|
404
|
-
raise RuntimeError("Cosmic Clarity DarkStar executable not configured.")
|
|
405
|
-
arr = np.asarray(arr_rgb01, dtype=np.float32)
|
|
406
|
-
was_single = (arr.ndim == 2) or (arr.ndim == 3 and arr.shape[2] == 1)
|
|
407
|
-
input_dir = os.path.join(base, "input")
|
|
408
|
-
output_dir = os.path.join(base, "output")
|
|
409
|
-
os.makedirs(input_dir, exist_ok=True)
|
|
410
|
-
os.makedirs(output_dir, exist_ok=True)
|
|
411
|
-
_purge_darkstar_io(base, prefix=None, clear_input=True, clear_output=True)
|
|
412
|
-
|
|
413
|
-
in_path = os.path.join(input_dir, f"{tmp_prefix}_in.tif")
|
|
414
|
-
save_image(
|
|
415
|
-
arr, in_path,
|
|
416
|
-
original_format="tif", bit_depth="32-bit floating point",
|
|
417
|
-
original_header=None, is_mono=was_single, image_meta=None, file_meta=None
|
|
418
|
-
)
|
|
419
|
-
|
|
420
|
-
args = []
|
|
421
|
-
if disable_gpu: args.append("--disable_gpu")
|
|
422
|
-
args += ["--star_removal_mode", mode, "--chunk_size", str(int(stride))]
|
|
423
|
-
import subprocess
|
|
424
|
-
rc = subprocess.call([exe] + args, cwd=output_dir)
|
|
425
|
-
if rc != 0:
|
|
426
|
-
_safe_rm(in_path); raise RuntimeError(f"DarkStar failed rc={rc}")
|
|
427
|
-
|
|
428
|
-
starless_path = os.path.join(output_dir, "imagetoremovestars_starless.tif")
|
|
429
|
-
starless, _, _, _ = load_image(starless_path)
|
|
430
|
-
if starless is None:
|
|
431
|
-
_safe_rm(in_path); raise RuntimeError("DarkStar produced no starless image.")
|
|
432
|
-
if starless.ndim == 2 or (starless.ndim == 3 and starless.shape[2] == 1):
|
|
433
|
-
starless = np.stack([starless] * 3, axis=-1)
|
|
434
|
-
starless = np.clip(starless.astype(np.float32, copy=False), 0.0, 1.0)
|
|
435
|
-
|
|
436
|
-
# If the source was mono, collapse back to single channel
|
|
437
|
-
if was_single and starless.ndim == 3:
|
|
438
|
-
starless = starless.mean(axis=2)
|
|
439
|
-
|
|
440
|
-
# cleanup typical outputs
|
|
441
|
-
_purge_darkstar_io(base, prefix="imagetoremovestars", clear_input=True, clear_output=True)
|
|
442
|
-
return starless
|
|
443
|
-
|
|
444
|
-
|
|
445
402
|
# ------------------------------------------------------------
|
|
446
403
|
# Public entry
|
|
447
404
|
# ------------------------------------------------------------
|
|
@@ -559,21 +516,20 @@ def _inverse_statstretch_from_starless(starless_s01: np.ndarray, meta: dict) ->
|
|
|
559
516
|
out = out + bp
|
|
560
517
|
return np.clip(out, 0.0, 1.0).astype(np.float32, copy=False)
|
|
561
518
|
|
|
562
|
-
|
|
563
|
-
# ------------------------------------------------------------
|
|
564
|
-
# StarNet (SASv2-like: 16-bit TIFF in StarNet folder)
|
|
565
|
-
# ------------------------------------------------------------
|
|
566
519
|
def _run_starnet(main, doc):
|
|
567
520
|
import os
|
|
568
521
|
import platform
|
|
569
522
|
import numpy as np
|
|
523
|
+
import re
|
|
570
524
|
from PyQt6.QtWidgets import QFileDialog, QMessageBox
|
|
571
525
|
|
|
572
526
|
# --- Resolve StarNet exe, persist in settings
|
|
573
527
|
exe = _get_setting_any(getattr(main, "settings", None),
|
|
574
528
|
("starnet/exe_path", "paths/starnet"), "")
|
|
575
529
|
if not exe or not os.path.exists(exe):
|
|
576
|
-
exe_path, _ = QFileDialog.getOpenFileName(
|
|
530
|
+
exe_path, _ = QFileDialog.getOpenFileName(
|
|
531
|
+
main, "Select StarNet Executable", "", "Executable Files (*)"
|
|
532
|
+
)
|
|
577
533
|
if not exe_path:
|
|
578
534
|
return
|
|
579
535
|
exe = exe_path
|
|
@@ -587,8 +543,10 @@ def _run_starnet(main, doc):
|
|
|
587
543
|
|
|
588
544
|
sysname = platform.system()
|
|
589
545
|
if sysname not in ("Windows", "Darwin", "Linux"):
|
|
590
|
-
QMessageBox.critical(
|
|
591
|
-
|
|
546
|
+
QMessageBox.critical(
|
|
547
|
+
main, "Unsupported OS",
|
|
548
|
+
f"The current operating system '{sysname}' is not supported."
|
|
549
|
+
)
|
|
592
550
|
return
|
|
593
551
|
|
|
594
552
|
# --- Ask linearity (SASv2 behavior)
|
|
@@ -598,7 +556,9 @@ def _run_starnet(main, doc):
|
|
|
598
556
|
QMessageBox.StandardButton.No
|
|
599
557
|
)
|
|
600
558
|
is_linear = (reply == QMessageBox.StandardButton.Yes)
|
|
601
|
-
did_stretch = is_linear
|
|
559
|
+
did_stretch = is_linear
|
|
560
|
+
|
|
561
|
+
# stash params for replay-last
|
|
602
562
|
try:
|
|
603
563
|
main._last_remove_stars_params = {
|
|
604
564
|
"engine": "StarNet",
|
|
@@ -608,6 +568,7 @@ def _run_starnet(main, doc):
|
|
|
608
568
|
}
|
|
609
569
|
except Exception:
|
|
610
570
|
pass
|
|
571
|
+
|
|
611
572
|
# 🔁 Record headless command for Replay Last
|
|
612
573
|
try:
|
|
613
574
|
main._last_headless_command = {
|
|
@@ -623,35 +584,35 @@ def _run_starnet(main, doc):
|
|
|
623
584
|
f"{'yes' if is_linear else 'no'})"
|
|
624
585
|
)
|
|
625
586
|
except Exception:
|
|
626
|
-
pass
|
|
627
|
-
|
|
628
|
-
#
|
|
587
|
+
pass
|
|
588
|
+
|
|
589
|
+
# --- Ensure float32 and sane values (no forced RGB expansion yet)
|
|
629
590
|
src = np.asarray(doc.image)
|
|
630
591
|
if src.ndim == 3 and src.shape[2] == 1:
|
|
631
|
-
# standardizing shape is cheap
|
|
632
592
|
processing_image = src[..., 0]
|
|
633
593
|
else:
|
|
634
594
|
processing_image = src
|
|
635
|
-
|
|
636
|
-
processing_image = np.nan_to_num(
|
|
637
|
-
|
|
595
|
+
|
|
596
|
+
processing_image = np.nan_to_num(
|
|
597
|
+
processing_image.astype(np.float32, copy=False),
|
|
598
|
+
nan=0.0, posinf=0.0, neginf=0.0
|
|
599
|
+
)
|
|
638
600
|
|
|
639
601
|
# --- Scale normalization if >1.0
|
|
640
|
-
scale_factor = float(np.max(processing_image))
|
|
602
|
+
scale_factor = float(np.max(processing_image)) if processing_image.size else 1.0
|
|
641
603
|
if scale_factor > 1.0:
|
|
642
604
|
processing_norm = processing_image / scale_factor
|
|
643
605
|
else:
|
|
644
606
|
processing_norm = processing_image
|
|
645
607
|
|
|
646
|
-
# --- Build input/output paths
|
|
608
|
+
# --- Build input/output paths (StarNet folder)
|
|
647
609
|
starnet_dir = os.path.dirname(exe) or os.getcwd()
|
|
648
|
-
input_image_path
|
|
610
|
+
input_image_path = os.path.join(starnet_dir, "imagetoremovestars.tif")
|
|
649
611
|
output_image_path = os.path.join(starnet_dir, "starless.tif")
|
|
650
612
|
|
|
651
|
-
# --- Prepare input for StarNet (Siril-style MTF pre-stretch for linear data) ---
|
|
613
|
+
# --- Prepare input for StarNet (Siril-style unlinked MTF pre-stretch for linear data) ---
|
|
652
614
|
img_for_starnet = processing_norm
|
|
653
615
|
if is_linear:
|
|
654
|
-
# Siril-style unlinked MTF params from linear normalized image
|
|
655
616
|
mtf_params = _mtf_params_unlinked(processing_norm, shadows_clipping=-2.8, targetbg=0.25)
|
|
656
617
|
img_for_starnet = _apply_mtf_unlinked_rgb(processing_norm, mtf_params)
|
|
657
618
|
|
|
@@ -668,20 +629,24 @@ def _run_starnet(main, doc):
|
|
|
668
629
|
pass
|
|
669
630
|
else:
|
|
670
631
|
# non-linear: do not try to invert any pre-stretch later
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
632
|
+
try:
|
|
633
|
+
if hasattr(main, "_starnet_stat_meta"):
|
|
634
|
+
delattr(main, "_starnet_stat_meta")
|
|
635
|
+
except Exception:
|
|
636
|
+
pass
|
|
674
637
|
|
|
675
|
-
# --- Write TIFF for StarNet
|
|
638
|
+
# --- Write TIFF for StarNet (16-bit)
|
|
676
639
|
try:
|
|
677
|
-
save_image(
|
|
678
|
-
|
|
679
|
-
|
|
640
|
+
save_image(
|
|
641
|
+
img_for_starnet, input_image_path,
|
|
642
|
+
original_format="tif", bit_depth="16-bit",
|
|
643
|
+
original_header=None, is_mono=False, image_meta=None, file_meta=None
|
|
644
|
+
)
|
|
680
645
|
except Exception as e:
|
|
681
646
|
QMessageBox.critical(main, "StarNet", f"Failed to write input TIFF:\n{e}")
|
|
682
647
|
return
|
|
683
648
|
|
|
684
|
-
# ---
|
|
649
|
+
# --- Build command
|
|
685
650
|
exe_name = os.path.basename(exe).lower()
|
|
686
651
|
if sysname in ("Windows", "Linux"):
|
|
687
652
|
command = [exe, input_image_path, output_image_path, "256"]
|
|
@@ -691,29 +656,122 @@ def _run_starnet(main, doc):
|
|
|
691
656
|
else:
|
|
692
657
|
command = [exe, input_image_path, output_image_path]
|
|
693
658
|
|
|
659
|
+
# --- Progress dialog + worker
|
|
694
660
|
dlg = _ProcDialog(main, title="StarNet Progress")
|
|
661
|
+
dlg.reset_progress("Starting StarNet…")
|
|
662
|
+
dlg.pbar.setRange(0, 100)
|
|
663
|
+
dlg.pbar.setValue(0)
|
|
664
|
+
dlg.pbar.setFormat("0%")
|
|
665
|
+
dlg.append_text("Launching StarNet...\n")
|
|
666
|
+
|
|
695
667
|
thr = _ProcThread(command, cwd=starnet_dir)
|
|
696
|
-
thr.output_signal.connect(dlg.append_text)
|
|
697
668
|
|
|
698
|
-
#
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
669
|
+
# ---- Output parsing (stages + percent finished + tile count) ----
|
|
670
|
+
_re_pct = re.compile(r"^\s*(\d{1,3})%\s+finished\s*$")
|
|
671
|
+
_re_tiles = re.compile(r"Total number of tiles:\s*(\d+)\s*$")
|
|
672
|
+
|
|
673
|
+
tile_total = {"n": 0}
|
|
674
|
+
last_pct = {"v": -1}
|
|
675
|
+
|
|
676
|
+
def _stage_from_line(low: str) -> str | None:
|
|
677
|
+
if "reading input image" in low:
|
|
678
|
+
return "Reading input image…"
|
|
679
|
+
if ("bits per sample" in low) or ("samples per pixel" in low) or ("height:" in low) or ("width:" in low):
|
|
680
|
+
return "Inspecting input…"
|
|
681
|
+
if "restoring neural network checkpoint" in low:
|
|
682
|
+
return "Loading model checkpoint…"
|
|
683
|
+
if "created device" in low and "gpu" in low:
|
|
684
|
+
return "Initializing GPU…"
|
|
685
|
+
if "loaded cudnn version" in low:
|
|
686
|
+
return "Initializing cuDNN…"
|
|
687
|
+
if "total number of tiles" in low:
|
|
688
|
+
return "Tiling image…"
|
|
689
|
+
if "% finished" in low:
|
|
690
|
+
return "Processing tiles…"
|
|
691
|
+
if "writing" in low or "saving" in low:
|
|
692
|
+
return "Writing output…"
|
|
693
|
+
return None
|
|
694
|
+
|
|
695
|
+
def _is_noise(low: str) -> bool:
|
|
696
|
+
# Keep progress + stage parsing, but optionally suppress spam in the log box.
|
|
697
|
+
# You can loosen/tighten this anytime.
|
|
698
|
+
return (
|
|
699
|
+
(low.startswith("202") and "tensorflow/" in low) or
|
|
700
|
+
("cpu_feature_guard" in low) or
|
|
701
|
+
("mlir" in low) or
|
|
702
|
+
("ptxas" in low) or
|
|
703
|
+
("bfc_allocator" in low) or
|
|
704
|
+
("garbage collection" in low)
|
|
702
705
|
)
|
|
703
|
-
|
|
706
|
+
|
|
707
|
+
def _on_out(line: str):
|
|
708
|
+
low = line.lower()
|
|
709
|
+
|
|
710
|
+
# stage updates
|
|
711
|
+
st = _stage_from_line(low)
|
|
712
|
+
if st:
|
|
713
|
+
try:
|
|
714
|
+
dlg.lbl_stage.setText(st)
|
|
715
|
+
except Exception:
|
|
716
|
+
pass
|
|
717
|
+
|
|
718
|
+
# tile total
|
|
719
|
+
m = _re_tiles.search(line)
|
|
720
|
+
if m:
|
|
721
|
+
try:
|
|
722
|
+
tile_total["n"] = int(m.group(1))
|
|
723
|
+
except Exception:
|
|
724
|
+
tile_total["n"] = 0
|
|
725
|
+
|
|
726
|
+
# percent updates (throttled)
|
|
727
|
+
m = _re_pct.match(line)
|
|
728
|
+
if m:
|
|
729
|
+
try:
|
|
730
|
+
pct = int(m.group(1))
|
|
731
|
+
pct = max(0, min(100, pct))
|
|
732
|
+
if pct != last_pct["v"]:
|
|
733
|
+
last_pct["v"] = pct
|
|
734
|
+
if tile_total["n"] > 0:
|
|
735
|
+
done = int(round(tile_total["n"] * (pct / 100.0)))
|
|
736
|
+
dlg.set_progress(done, tile_total["n"], "Processing tiles…")
|
|
737
|
+
else:
|
|
738
|
+
dlg.set_progress(pct, 100, "Processing…")
|
|
739
|
+
except Exception:
|
|
740
|
+
pass
|
|
741
|
+
|
|
742
|
+
# append (optionally suppress TF spam)
|
|
743
|
+
if not _is_noise(low):
|
|
744
|
+
dlg.append_text(line)
|
|
745
|
+
|
|
746
|
+
thr.output_signal.connect(_on_out)
|
|
747
|
+
|
|
748
|
+
# finished -> apply + cleanup
|
|
749
|
+
def _on_finish(rc: int):
|
|
750
|
+
try:
|
|
751
|
+
# snap to 100% for UX (even if StarNet ended abruptly, it will be overwritten by error handling)
|
|
752
|
+
dlg.set_progress(100, 100, "StarNet finished. Loading output…")
|
|
753
|
+
except Exception:
|
|
754
|
+
pass
|
|
755
|
+
_on_starnet_finished(main, doc, rc, dlg, input_image_path, output_image_path, did_stretch)
|
|
756
|
+
|
|
757
|
+
thr.finished_signal.connect(_on_finish)
|
|
758
|
+
|
|
759
|
+
# cancel kills subprocess
|
|
704
760
|
dlg.cancel_button.clicked.connect(thr.cancel)
|
|
705
761
|
|
|
706
|
-
dlg.show()
|
|
707
762
|
thr.start()
|
|
708
763
|
dlg.exec()
|
|
709
764
|
|
|
710
|
-
|
|
711
765
|
def _on_starnet_finished(main, doc, return_code, dialog, input_path, output_path, did_stretch):
|
|
712
766
|
import os
|
|
713
767
|
import numpy as np
|
|
714
768
|
from PyQt6.QtWidgets import QMessageBox
|
|
715
769
|
from setiastro.saspro.imageops.stretch import stretch_mono_image # used for statistical inverse
|
|
716
|
-
|
|
770
|
+
try:
|
|
771
|
+
dialog.pbar.setRange(0, 100)
|
|
772
|
+
dialog.set_progress(100, 100, "StarNet finished. Loading output…")
|
|
773
|
+
except Exception:
|
|
774
|
+
pass
|
|
717
775
|
def _first_nonzero_bp_per_channel(img3: np.ndarray) -> np.ndarray:
|
|
718
776
|
bps = np.zeros(3, dtype=np.float32)
|
|
719
777
|
for c in range(3):
|
|
@@ -917,48 +975,28 @@ def _on_starnet_finished(main, doc, return_code, dialog, input_path, output_path
|
|
|
917
975
|
# CosmicClarityDarkStar
|
|
918
976
|
# ------------------------------------------------------------
|
|
919
977
|
def _run_darkstar(main, doc):
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
QMessageBox.critical(main, "Cosmic Clarity Folder Error",
|
|
923
|
-
"Cosmic Clarity Dark Star executable not set.")
|
|
924
|
-
return
|
|
925
|
-
|
|
926
|
-
# --- Input/output folders per SASv2 ---
|
|
927
|
-
input_dir = os.path.join(base, "input")
|
|
928
|
-
output_dir = os.path.join(base, "output")
|
|
929
|
-
os.makedirs(input_dir, exist_ok=True)
|
|
930
|
-
os.makedirs(output_dir, exist_ok=True)
|
|
931
|
-
_purge_darkstar_io(base, prefix=None, clear_input=True, clear_output=True)
|
|
978
|
+
import numpy as np
|
|
979
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
932
980
|
|
|
933
|
-
# --- Config dialog (
|
|
981
|
+
# --- Config dialog (keep as-is) ---
|
|
934
982
|
cfg = DarkStarConfigDialog(main)
|
|
935
983
|
if not cfg.exec():
|
|
936
984
|
return
|
|
937
|
-
|
|
938
|
-
disable_gpu = params["disable_gpu"]
|
|
939
|
-
mode = params["mode"] # "unscreen" or "additive"
|
|
940
|
-
show_extracted_stars = params["show_extracted_stars"]
|
|
941
|
-
stride = params["stride"] # 64..1024, default 512
|
|
985
|
+
v = cfg.get_values()
|
|
942
986
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
QMessageBox.StandardButton.Yes
|
|
948
|
-
)
|
|
949
|
-
is_linear = (reply == QMessageBox.StandardButton.Yes)
|
|
950
|
-
did_prestretch = is_linear
|
|
987
|
+
disable_gpu = bool(v["disable_gpu"])
|
|
988
|
+
mode = str(v["mode"]) # "unscreen" | "additive"
|
|
989
|
+
show_extracted_stars = bool(v["show_extracted_stars"])
|
|
990
|
+
stride = int(v["stride"]) # chunk size
|
|
951
991
|
|
|
952
|
-
# 🔹 Stash parameters for replay-last
|
|
992
|
+
# 🔹 Stash parameters for replay-last (same structure as you had)
|
|
953
993
|
try:
|
|
954
994
|
main._last_remove_stars_params = {
|
|
955
|
-
"engine": "
|
|
995
|
+
"engine": "DarkStar",
|
|
956
996
|
"disable_gpu": bool(disable_gpu),
|
|
957
997
|
"mode": mode,
|
|
958
998
|
"show_extracted_stars": bool(show_extracted_stars),
|
|
959
999
|
"stride": int(stride),
|
|
960
|
-
"is_linear": bool(is_linear),
|
|
961
|
-
"did_prestretch": bool(did_prestretch),
|
|
962
1000
|
"label": "Remove Stars (DarkStar)",
|
|
963
1001
|
}
|
|
964
1002
|
except Exception:
|
|
@@ -974,344 +1012,155 @@ def _run_darkstar(main, doc):
|
|
|
974
1012
|
"mode": mode,
|
|
975
1013
|
"show_extracted_stars": bool(show_extracted_stars),
|
|
976
1014
|
"stride": int(stride),
|
|
977
|
-
"is_linear": bool(is_linear),
|
|
978
|
-
"did_prestretch": bool(did_prestretch),
|
|
979
1015
|
},
|
|
980
1016
|
}
|
|
981
1017
|
if hasattr(main, "_log"):
|
|
982
1018
|
main._log(
|
|
983
1019
|
"[Replay] Recorded remove_stars (DarkStar, "
|
|
984
|
-
f"mode={mode},
|
|
1020
|
+
f"mode={mode}, chunk={int(stride)}, "
|
|
985
1021
|
f"gpu={'off' if disable_gpu else 'on'}, "
|
|
986
|
-
f"stars={'on' if show_extracted_stars else 'off'}
|
|
987
|
-
f"linear={'yes' if is_linear else 'no'})"
|
|
1022
|
+
f"stars={'on' if show_extracted_stars else 'off'})"
|
|
988
1023
|
)
|
|
989
1024
|
except Exception:
|
|
990
1025
|
pass
|
|
991
1026
|
|
|
992
|
-
# --- Build
|
|
993
|
-
# DarkStar needs RGB, but we can delay expansion until save
|
|
1027
|
+
# --- Build input image for engine: float32 [0..1], HxWx3/1/mono ok ---
|
|
994
1028
|
src = np.asarray(doc.image)
|
|
995
|
-
|
|
996
|
-
processing_image = src[..., 0]
|
|
997
|
-
else:
|
|
998
|
-
processing_image = src
|
|
1029
|
+
orig_was_mono = (src.ndim == 2) or (src.ndim == 3 and src.shape[2] == 1)
|
|
999
1030
|
|
|
1000
|
-
|
|
1001
|
-
processing_image.astype(np.float32, copy=False),
|
|
1002
|
-
nan=0.0, posinf=0.0, neginf=0.0
|
|
1003
|
-
)
|
|
1031
|
+
x = np.nan_to_num(src.astype(np.float32, copy=False), nan=0.0, posinf=0.0, neginf=0.0)
|
|
1004
1032
|
|
|
1005
|
-
|
|
1033
|
+
# If your doc domain can exceed 1.0, normalize to [0..1] for the engine.
|
|
1034
|
+
# (Matches what you were already doing for external DarkStar.)
|
|
1035
|
+
scale_factor = float(np.max(x)) if x.size else 1.0
|
|
1006
1036
|
if scale_factor > 1.0:
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
mtf_params = _mtf_params_unlinked(
|
|
1017
|
-
processing_norm,
|
|
1018
|
-
shadows_clipping=-2.8,
|
|
1019
|
-
targetbg=0.25
|
|
1020
|
-
)
|
|
1021
|
-
img_for_darkstar = _apply_mtf_unlinked_rgb(processing_norm, mtf_params)
|
|
1022
|
-
|
|
1023
|
-
# 🔐 Stash EXACT params for inverse step later
|
|
1024
|
-
setattr(main, "_darkstar_mtf_meta", {
|
|
1025
|
-
"s": np.asarray(mtf_params["s"], dtype=np.float32),
|
|
1026
|
-
"m": np.asarray(mtf_params["m"], dtype=np.float32),
|
|
1027
|
-
"h": np.asarray(mtf_params["h"], dtype=np.float32),
|
|
1028
|
-
"scale": float(scale_factor),
|
|
1029
|
-
})
|
|
1030
|
-
if hasattr(main, "_log"):
|
|
1031
|
-
main._log("[DarkStar] Applying Siril-style MTF pre-stretch for linear image.")
|
|
1032
|
-
except Exception as e:
|
|
1033
|
-
# If anything goes wrong, fall back to un-stretched normalized image
|
|
1034
|
-
img_for_darkstar = processing_norm
|
|
1035
|
-
try:
|
|
1036
|
-
if hasattr(main, "_darkstar_mtf_meta"):
|
|
1037
|
-
delattr(main, "_darkstar_mtf_meta")
|
|
1038
|
-
except Exception:
|
|
1039
|
-
pass
|
|
1040
|
-
if hasattr(main, "_log"):
|
|
1041
|
-
main._log(f"[DarkStar] MTF pre-stretch failed, using normalized image only: {e}")
|
|
1042
|
-
else:
|
|
1043
|
-
# Non-linear: don't store any pre-stretch meta
|
|
1044
|
-
try:
|
|
1045
|
-
if hasattr(main, "_darkstar_mtf_meta"):
|
|
1046
|
-
delattr(main, "_darkstar_mtf_meta")
|
|
1047
|
-
except Exception:
|
|
1048
|
-
pass
|
|
1049
|
-
|
|
1050
|
-
# --- Save pre-stretched image as 32-bit float TIFF for DarkStar ---
|
|
1051
|
-
in_path = os.path.join(input_dir, "imagetoremovestars.tif")
|
|
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
|
-
|
|
1065
|
-
save_image(
|
|
1066
|
-
to_save,
|
|
1067
|
-
in_path,
|
|
1068
|
-
original_format="tif",
|
|
1069
|
-
bit_depth="32-bit floating point",
|
|
1070
|
-
original_header=None,
|
|
1071
|
-
is_mono=False, # we always send RGB to DarkStar
|
|
1072
|
-
image_meta=None,
|
|
1073
|
-
file_meta=None
|
|
1074
|
-
)
|
|
1075
|
-
except Exception as e:
|
|
1076
|
-
QMessageBox.critical(main, "Cosmic Clarity", f"Failed to write input TIFF:\n{e}")
|
|
1077
|
-
return
|
|
1078
|
-
|
|
1079
|
-
# --- Build CLI exactly like SASv2 (using --chunk_size, not chunk_size) ---
|
|
1080
|
-
args = []
|
|
1081
|
-
if disable_gpu:
|
|
1082
|
-
args.append("--disable_gpu")
|
|
1083
|
-
args += ["--star_removal_mode", mode]
|
|
1084
|
-
if show_extracted_stars:
|
|
1085
|
-
args.append("--show_extracted_stars")
|
|
1086
|
-
args += ["--chunk_size", str(stride)]
|
|
1087
|
-
|
|
1088
|
-
command = [exe] + args
|
|
1089
|
-
|
|
1090
|
-
dlg = _ProcDialog(main, title="CosmicClarityDarkStar Progress")
|
|
1091
|
-
thr = _ProcThread(command, cwd=output_dir)
|
|
1092
|
-
thr.output_signal.connect(dlg.append_text)
|
|
1093
|
-
thr.finished_signal.connect(
|
|
1094
|
-
lambda rc, base=base, ds=did_prestretch: _on_darkstar_finished(
|
|
1095
|
-
main, doc, rc, dlg, in_path, output_dir, base, ds
|
|
1096
|
-
)
|
|
1037
|
+
x = x / scale_factor
|
|
1038
|
+
x = np.clip(x, 0.0, 1.0).astype(np.float32, copy=False)
|
|
1039
|
+
|
|
1040
|
+
params = DarkStarParams(
|
|
1041
|
+
use_gpu=(not disable_gpu),
|
|
1042
|
+
chunk_size=int(stride),
|
|
1043
|
+
overlap_frac=0.125,
|
|
1044
|
+
mode=mode,
|
|
1045
|
+
output_stars_only=show_extracted_stars,
|
|
1097
1046
|
)
|
|
1098
|
-
dlg.cancel_button.clicked.connect(thr.cancel)
|
|
1099
|
-
|
|
1100
|
-
dlg.show()
|
|
1101
|
-
thr.start()
|
|
1102
|
-
dlg.exec()
|
|
1103
1047
|
|
|
1048
|
+
dlg = _ProcDialog(main, title="Dark Star Progress")
|
|
1049
|
+
dlg.append_text("Starting Dark Star (engine)…\n")
|
|
1104
1050
|
|
|
1051
|
+
thr = _DarkStarThread(x, params, parent=dlg)
|
|
1105
1052
|
|
|
1053
|
+
def _on_prog(done, total, stage):
|
|
1054
|
+
dlg.set_progress(done, total, stage)
|
|
1106
1055
|
|
|
1107
|
-
def
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
"""
|
|
1113
|
-
settings = getattr(main, "settings", None)
|
|
1114
|
-
raw = _get_setting_any(settings, ("paths/cosmic_clarity", "cosmic_clarity_folder"), "")
|
|
1115
|
-
|
|
1116
|
-
def _platform_exe_name():
|
|
1117
|
-
return "setiastrocosmicclarity_darkstar.exe" if platform.system() == "Windows" \
|
|
1118
|
-
else "setiastrocosmicclarity_darkstar"
|
|
1119
|
-
|
|
1120
|
-
exe_name = _platform_exe_name()
|
|
1121
|
-
|
|
1122
|
-
exe_path = None
|
|
1123
|
-
base_folder = None
|
|
1124
|
-
|
|
1125
|
-
if raw:
|
|
1126
|
-
if os.path.isfile(raw):
|
|
1127
|
-
# user stored the executable path directly
|
|
1128
|
-
exe_path = raw
|
|
1129
|
-
base_folder = os.path.dirname(raw)
|
|
1130
|
-
elif os.path.isdir(raw):
|
|
1131
|
-
# user stored the parent folder
|
|
1132
|
-
base_folder = raw
|
|
1133
|
-
exe_path = os.path.join(base_folder, exe_name)
|
|
1134
|
-
|
|
1135
|
-
# if missing or invalid, let user pick the executable directly
|
|
1136
|
-
if not exe_path or not os.path.exists(exe_path):
|
|
1137
|
-
picked, _ = QFileDialog.getOpenFileName(main, "Select CosmicClarityDarkStar Executable", "", "Executable Files (*)")
|
|
1138
|
-
if not picked:
|
|
1139
|
-
return None, None
|
|
1140
|
-
exe_path = picked
|
|
1141
|
-
base_folder = os.path.dirname(picked)
|
|
1142
|
-
|
|
1143
|
-
# ensure exec bit on POSIX
|
|
1144
|
-
if platform.system() in ("Darwin", "Linux"):
|
|
1145
|
-
_ensure_exec_bit(exe_path)
|
|
1146
|
-
|
|
1147
|
-
# persist folder (not the exe) to the canonical key
|
|
1148
|
-
if settings:
|
|
1149
|
-
settings.setValue("paths/cosmic_clarity", base_folder)
|
|
1150
|
-
settings.sync()
|
|
1151
|
-
|
|
1152
|
-
return exe_path, base_folder
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
def _on_darkstar_finished(main, doc, return_code, dialog, in_path, output_dir, base_folder, did_prestretch):
|
|
1156
|
-
dialog.append_text(f"\nProcess finished with return code {return_code}.\n")
|
|
1157
|
-
if return_code != 0:
|
|
1158
|
-
QMessageBox.critical(main, "CosmicClarityDarkStar Error",
|
|
1159
|
-
f"CosmicClarityDarkStar failed with return code {return_code}.")
|
|
1160
|
-
_safe_rm(in_path); dialog.close(); return
|
|
1161
|
-
|
|
1162
|
-
starless_path = os.path.join(output_dir, "imagetoremovestars_starless.tif")
|
|
1163
|
-
if not os.path.exists(starless_path):
|
|
1164
|
-
QMessageBox.critical(main, "CosmicClarityDarkStar Error", "Starless image was not created.")
|
|
1165
|
-
_safe_rm(in_path); dialog.close(); return
|
|
1166
|
-
|
|
1167
|
-
dialog.append_text(f"Loading starless image from {starless_path}...\n")
|
|
1168
|
-
starless, _, _, _ = load_image(starless_path)
|
|
1169
|
-
if starless is None:
|
|
1170
|
-
QMessageBox.critical(main, "CosmicClarityDarkStar Error", "Failed to load starless image.")
|
|
1171
|
-
_safe_rm(in_path); dialog.close(); return
|
|
1172
|
-
|
|
1173
|
-
if starless.ndim == 2 or (starless.ndim == 3 and starless.shape[2] == 1):
|
|
1174
|
-
starless_rgb = np.stack([starless] * 3, axis=-1)
|
|
1175
|
-
else:
|
|
1176
|
-
starless_rgb = starless
|
|
1177
|
-
starless_rgb = starless_rgb.astype(np.float32, copy=False)
|
|
1178
|
-
|
|
1179
|
-
src = np.asarray(doc.image)
|
|
1180
|
-
if src.ndim == 2:
|
|
1181
|
-
original_rgb = np.stack([src] * 3, axis=-1)
|
|
1182
|
-
orig_was_mono = True
|
|
1183
|
-
elif src.ndim == 3 and src.shape[2] == 1:
|
|
1184
|
-
original_rgb = np.repeat(src, 3, axis=2)
|
|
1185
|
-
orig_was_mono = True
|
|
1186
|
-
else:
|
|
1187
|
-
original_rgb = src
|
|
1188
|
-
orig_was_mono = False
|
|
1189
|
-
original_rgb = original_rgb.astype(np.float32, copy=False)
|
|
1056
|
+
def _on_done(starless, stars_only, was_mono, err):
|
|
1057
|
+
if err:
|
|
1058
|
+
QMessageBox.critical(main, "Dark Star Error", f"Dark Star failed:\n{err}")
|
|
1059
|
+
dlg.close()
|
|
1060
|
+
return
|
|
1190
1061
|
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
if isinstance(meta, dict):
|
|
1195
|
-
dialog.append_text("Unstretching starless result (DarkStar MTF inverse)...\n")
|
|
1062
|
+
# Engine returns float32 [0..1]; if we normalized >1, restore scale if you want.
|
|
1063
|
+
# BUT you later clip to [0..1] for doc anyway, so this is mostly for consistency.
|
|
1064
|
+
if scale_factor > 1.0:
|
|
1196
1065
|
try:
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1066
|
+
starless = starless * scale_factor
|
|
1067
|
+
if stars_only is not None:
|
|
1068
|
+
stars_only = stars_only * scale_factor
|
|
1069
|
+
except Exception:
|
|
1070
|
+
pass
|
|
1201
1071
|
|
|
1202
|
-
|
|
1203
|
-
|
|
1072
|
+
# Original as RGB for blending math
|
|
1073
|
+
orig = np.asarray(doc.image).astype(np.float32, copy=False)
|
|
1074
|
+
if orig.ndim == 2:
|
|
1075
|
+
original_rgb = np.stack([orig]*3, axis=-1)
|
|
1076
|
+
elif orig.ndim == 3 and orig.shape[2] == 1:
|
|
1077
|
+
original_rgb = np.repeat(orig, 3, axis=2)
|
|
1078
|
+
else:
|
|
1079
|
+
original_rgb = orig
|
|
1204
1080
|
|
|
1205
|
-
|
|
1206
|
-
|
|
1081
|
+
# Starless as RGB for blending math
|
|
1082
|
+
if starless.ndim == 2:
|
|
1083
|
+
starless_rgb = np.stack([starless]*3, axis=-1)
|
|
1084
|
+
elif starless.ndim == 3 and starless.shape[2] == 1:
|
|
1085
|
+
starless_rgb = np.repeat(starless, 3, axis=2)
|
|
1086
|
+
else:
|
|
1087
|
+
starless_rgb = starless
|
|
1207
1088
|
|
|
1208
|
-
|
|
1209
|
-
except Exception as e:
|
|
1210
|
-
dialog.append_text(f"⚠️ DarkStar MTF inverse failed: {e}\n")
|
|
1089
|
+
starless_rgb = starless_rgb.astype(np.float32, copy=False)
|
|
1211
1090
|
|
|
1212
|
-
#
|
|
1213
|
-
|
|
1214
|
-
if
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1091
|
+
# ---- Optional stars-only push ----
|
|
1092
|
+
if show_extracted_stars:
|
|
1093
|
+
if stars_only is None:
|
|
1094
|
+
# Safety fallback if someone changes engine params later
|
|
1095
|
+
stars_only_rgb = np.clip(original_rgb - starless_rgb, 0.0, 1.0).astype(np.float32, copy=False)
|
|
1096
|
+
else:
|
|
1097
|
+
if stars_only.ndim == 2:
|
|
1098
|
+
stars_only_rgb = np.stack([stars_only]*3, axis=-1)
|
|
1099
|
+
elif stars_only.ndim == 3 and stars_only.shape[2] == 1:
|
|
1100
|
+
stars_only_rgb = np.repeat(stars_only, 3, axis=2)
|
|
1101
|
+
else:
|
|
1102
|
+
stars_only_rgb = stars_only.astype(np.float32, copy=False)
|
|
1218
1103
|
|
|
1219
|
-
|
|
1220
|
-
stars_path = os.path.join(output_dir, "imagetoremovestars_stars_only.tif")
|
|
1221
|
-
if os.path.exists(stars_path):
|
|
1222
|
-
dialog.append_text(f"Loading stars-only image from {stars_path}...\n")
|
|
1223
|
-
stars_only, _, _, _ = load_image(stars_path)
|
|
1224
|
-
if stars_only is not None:
|
|
1225
|
-
if stars_only.ndim == 2 or (stars_only.ndim == 3 and stars_only.shape[2] == 1):
|
|
1226
|
-
stars_only = np.stack([stars_only] * 3, axis=-1)
|
|
1227
|
-
stars_only = stars_only.astype(np.float32, copy=False)
|
|
1228
|
-
m3 = _active_mask3_from_doc(doc, stars_only.shape[1], stars_only.shape[0])
|
|
1104
|
+
m3 = _active_mask3_from_doc(doc, stars_only_rgb.shape[1], stars_only_rgb.shape[0])
|
|
1229
1105
|
if m3 is not None:
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
else:
|
|
1233
|
-
dialog.append_text("ℹ️ Mask not active for stars-only; skipping.\n")
|
|
1106
|
+
stars_only_rgb *= m3
|
|
1107
|
+
dlg.append_text("✅ Applied active mask to stars-only image.\n")
|
|
1234
1108
|
|
|
1235
|
-
# If the original doc was mono, collapse stars-only back to single channel
|
|
1236
1109
|
if orig_was_mono:
|
|
1237
|
-
stars_to_push =
|
|
1110
|
+
stars_to_push = stars_only_rgb.mean(axis=2).astype(np.float32, copy=False)
|
|
1238
1111
|
else:
|
|
1239
|
-
stars_to_push =
|
|
1112
|
+
stars_to_push = stars_only_rgb
|
|
1240
1113
|
|
|
1241
1114
|
_push_as_new_doc(main, doc, stars_to_push, title_suffix="_stars", source="Stars-Only (DarkStar)")
|
|
1242
|
-
|
|
1243
|
-
dialog.append_text("Failed to load stars-only image.\n")
|
|
1244
|
-
else:
|
|
1245
|
-
dialog.append_text("No stars-only image generated.\n")
|
|
1246
|
-
|
|
1247
|
-
# --- Mask-blend starless → overwrite current doc (in original domain) ---
|
|
1248
|
-
dialog.append_text("Mask-blending starless image before update...\n")
|
|
1249
|
-
final_starless = _mask_blend_with_doc_mask(doc, starless_rgb, original_rgb)
|
|
1250
|
-
|
|
1251
|
-
# If the original doc was mono, collapse back to single-channel
|
|
1252
|
-
if orig_was_mono:
|
|
1253
|
-
final_to_apply = final_starless.mean(axis=2).astype(np.float32, copy=False)
|
|
1254
|
-
else:
|
|
1255
|
-
final_to_apply = final_starless.astype(np.float32, copy=False)
|
|
1115
|
+
dlg.append_text("Stars-only image pushed.\n")
|
|
1256
1116
|
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
"step_name": "Stars Removed",
|
|
1260
|
-
"bit_depth": "32-bit floating point",
|
|
1261
|
-
"is_mono": bool(orig_was_mono),
|
|
1262
|
-
}
|
|
1117
|
+
# ---- Mask-blend starless into current doc ----
|
|
1118
|
+
final_starless = _mask_blend_with_doc_mask(doc, starless_rgb, original_rgb)
|
|
1263
1119
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
if isinstance(rp, dict):
|
|
1267
|
-
replay_params = dict(rp)
|
|
1120
|
+
if orig_was_mono:
|
|
1121
|
+
final_to_apply = final_starless.mean(axis=2).astype(np.float32, copy=False)
|
|
1268
1122
|
else:
|
|
1269
|
-
|
|
1270
|
-
"engine": "CosmicClarityDarkStar",
|
|
1271
|
-
"label": "Remove Stars (DarkStar)",
|
|
1272
|
-
}
|
|
1123
|
+
final_to_apply = final_starless.astype(np.float32, copy=False)
|
|
1273
1124
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1125
|
+
# Clip to [0..1] because that’s what your pipeline expects almost everywhere
|
|
1126
|
+
final_to_apply = np.clip(final_to_apply, 0.0, 1.0).astype(np.float32, copy=False)
|
|
1276
1127
|
|
|
1277
|
-
meta["replay_last"] = {
|
|
1278
|
-
"op": "remove_stars",
|
|
1279
|
-
"params": replay_params,
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
# Clean up stash
|
|
1283
1128
|
try:
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1129
|
+
meta = {
|
|
1130
|
+
"step_name": "Stars Removed",
|
|
1131
|
+
"bit_depth": "32-bit floating point",
|
|
1132
|
+
"is_mono": bool(orig_was_mono),
|
|
1133
|
+
}
|
|
1288
1134
|
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
)
|
|
1294
|
-
if hasattr(main, "_log"):
|
|
1295
|
-
main._log("Stars Removed (DarkStar)")
|
|
1296
|
-
except Exception as e:
|
|
1297
|
-
QMessageBox.critical(main, "CosmicClarityDarkStar", f"Failed to apply result:\n{e}")
|
|
1135
|
+
rp = getattr(main, "_last_remove_stars_params", None)
|
|
1136
|
+
replay_params = dict(rp) if isinstance(rp, dict) else {"engine": "DarkStar", "label": "Remove Stars (DarkStar)"}
|
|
1137
|
+
replay_params.setdefault("engine", "DarkStar")
|
|
1138
|
+
replay_params.setdefault("label", "Remove Stars (DarkStar)")
|
|
1298
1139
|
|
|
1299
|
-
|
|
1300
|
-
try:
|
|
1301
|
-
_safe_rm(in_path)
|
|
1302
|
-
_safe_rm(starless_path)
|
|
1303
|
-
_safe_rm(os.path.join(output_dir, "imagetoremovestars_stars_only.tif"))
|
|
1140
|
+
meta["replay_last"] = {"op": "remove_stars", "params": replay_params}
|
|
1304
1141
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1142
|
+
try:
|
|
1143
|
+
if hasattr(main, "_last_remove_stars_params"):
|
|
1144
|
+
delattr(main, "_last_remove_stars_params")
|
|
1145
|
+
except Exception:
|
|
1146
|
+
pass
|
|
1308
1147
|
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1148
|
+
doc.apply_edit(final_to_apply, metadata=meta, step_name="Stars Removed")
|
|
1149
|
+
if hasattr(main, "_log"):
|
|
1150
|
+
main._log("Stars Removed (DarkStar)")
|
|
1151
|
+
except Exception as e:
|
|
1152
|
+
QMessageBox.critical(main, "Dark Star Error", f"Failed to apply result:\n{e}")
|
|
1312
1153
|
|
|
1313
|
-
|
|
1154
|
+
dlg.append_text("Done.\n")
|
|
1155
|
+
dlg.close()
|
|
1156
|
+
|
|
1157
|
+
thr.progress_signal.connect(_on_prog)
|
|
1158
|
+
thr.finished_signal.connect(_on_done)
|
|
1314
1159
|
|
|
1160
|
+
dlg.cancel_button.clicked.connect(lambda: dlg.append_text("Cancel not supported for in-process engine.\n"))
|
|
1161
|
+
dlg.show()
|
|
1162
|
+
thr.start()
|
|
1163
|
+
dlg.exec()
|
|
1315
1164
|
|
|
1316
1165
|
# ------------------------------------------------------------
|
|
1317
1166
|
# Mask helpers (doc-centric)
|
|
@@ -1453,32 +1302,6 @@ def _safe_rm(p):
|
|
|
1453
1302
|
except Exception:
|
|
1454
1303
|
pass
|
|
1455
1304
|
|
|
1456
|
-
def _safe_rm_globs(patterns: list[str]):
|
|
1457
|
-
for pat in patterns:
|
|
1458
|
-
try:
|
|
1459
|
-
for fp in glob.glob(pat):
|
|
1460
|
-
_safe_rm(fp)
|
|
1461
|
-
except Exception:
|
|
1462
|
-
pass
|
|
1463
|
-
|
|
1464
|
-
def _purge_darkstar_io(base_folder: str, *, prefix: str | None = None, clear_input=True, clear_output=True):
|
|
1465
|
-
"""Delete old image-like files from CC DarkStar input/output."""
|
|
1466
|
-
try:
|
|
1467
|
-
inp = os.path.join(base_folder, "input")
|
|
1468
|
-
out = os.path.join(base_folder, "output")
|
|
1469
|
-
if clear_input and os.path.isdir(inp):
|
|
1470
|
-
for fn in os.listdir(inp):
|
|
1471
|
-
fp = os.path.join(inp, fn)
|
|
1472
|
-
if os.path.isfile(fp) and (prefix is None or fn.startswith(prefix)):
|
|
1473
|
-
_safe_rm(fp)
|
|
1474
|
-
if clear_output and os.path.isdir(out):
|
|
1475
|
-
for fn in os.listdir(out):
|
|
1476
|
-
fp = os.path.join(out, fn)
|
|
1477
|
-
if os.path.isfile(fp) and (prefix is None or fn.startswith(prefix)):
|
|
1478
|
-
_safe_rm(fp)
|
|
1479
|
-
except Exception:
|
|
1480
|
-
pass
|
|
1481
|
-
|
|
1482
1305
|
|
|
1483
1306
|
# ------------------------------------------------------------
|
|
1484
1307
|
# Proc runner & dialog (merged stdout/stderr)
|
|
@@ -1530,15 +1353,34 @@ class _ProcThread(QThread):
|
|
|
1530
1353
|
self.process = None
|
|
1531
1354
|
self.finished_signal.emit(rc)
|
|
1532
1355
|
|
|
1533
|
-
|
|
1534
1356
|
class _ProcDialog(QDialog):
|
|
1535
1357
|
def __init__(self, parent, title="Process"):
|
|
1536
1358
|
super().__init__(parent)
|
|
1537
1359
|
self.setWindowTitle(title)
|
|
1538
|
-
self.setMinimumSize(600,
|
|
1360
|
+
self.setMinimumSize(600, 460)
|
|
1361
|
+
|
|
1362
|
+
self._last_pct = -1 # for throttling UI updates
|
|
1363
|
+
|
|
1539
1364
|
lay = QVBoxLayout(self)
|
|
1540
|
-
|
|
1541
|
-
|
|
1365
|
+
|
|
1366
|
+
# --- status line + progress bar ---
|
|
1367
|
+
self.lbl_stage = QLabel("", self)
|
|
1368
|
+
self.lbl_stage.setWordWrap(True)
|
|
1369
|
+
self.lbl_stage.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
|
1370
|
+
lay.addWidget(self.lbl_stage)
|
|
1371
|
+
|
|
1372
|
+
self.pbar = QProgressBar(self)
|
|
1373
|
+
self.pbar.setRange(0, 100)
|
|
1374
|
+
self.pbar.setValue(0)
|
|
1375
|
+
self.pbar.setTextVisible(True)
|
|
1376
|
+
lay.addWidget(self.pbar)
|
|
1377
|
+
|
|
1378
|
+
# --- log output ---
|
|
1379
|
+
self.text = QTextEdit(self)
|
|
1380
|
+
self.text.setReadOnly(True)
|
|
1381
|
+
lay.addWidget(self.text, 1)
|
|
1382
|
+
|
|
1383
|
+
# --- cancel ---
|
|
1542
1384
|
self.cancel_button = QPushButton("Cancel", self)
|
|
1543
1385
|
lay.addWidget(self.cancel_button)
|
|
1544
1386
|
|
|
@@ -1548,6 +1390,73 @@ class _ProcDialog(QDialog):
|
|
|
1548
1390
|
except Exception:
|
|
1549
1391
|
pass
|
|
1550
1392
|
|
|
1393
|
+
def set_progress(self, done: int, total: int, stage: str = ""):
|
|
1394
|
+
"""
|
|
1395
|
+
Update stage label + progress bar.
|
|
1396
|
+
`total<=0` puts the bar into an indeterminate-ish state (0%).
|
|
1397
|
+
Throttles updates when percent hasn't changed.
|
|
1398
|
+
"""
|
|
1399
|
+
try:
|
|
1400
|
+
if stage:
|
|
1401
|
+
self.lbl_stage.setText(stage)
|
|
1402
|
+
|
|
1403
|
+
if total and total > 0:
|
|
1404
|
+
pct = int(round(100.0 * float(done) / float(total)))
|
|
1405
|
+
pct = max(0, min(100, pct))
|
|
1406
|
+
else:
|
|
1407
|
+
pct = 0
|
|
1408
|
+
|
|
1409
|
+
# throttle: only repaint if percent changed or we're at end
|
|
1410
|
+
if pct != self._last_pct or done == total:
|
|
1411
|
+
self._last_pct = pct
|
|
1412
|
+
self.pbar.setValue(pct)
|
|
1413
|
+
|
|
1414
|
+
# keep the text helpful
|
|
1415
|
+
if total and total > 0:
|
|
1416
|
+
self.pbar.setFormat(f"{pct}% ({done}/{total})")
|
|
1417
|
+
else:
|
|
1418
|
+
self.pbar.setFormat(f"{pct}%")
|
|
1419
|
+
except Exception:
|
|
1420
|
+
pass
|
|
1421
|
+
|
|
1422
|
+
def reset_progress(self, stage: str = ""):
|
|
1423
|
+
self._last_pct = -1
|
|
1424
|
+
if stage:
|
|
1425
|
+
try:
|
|
1426
|
+
self.lbl_stage.setText(stage)
|
|
1427
|
+
except Exception:
|
|
1428
|
+
pass
|
|
1429
|
+
try:
|
|
1430
|
+
self.pbar.setValue(0)
|
|
1431
|
+
self.pbar.setFormat("0%")
|
|
1432
|
+
except Exception:
|
|
1433
|
+
pass
|
|
1434
|
+
|
|
1435
|
+
class _DarkStarThread(QThread):
|
|
1436
|
+
progress_signal = pyqtSignal(int, int, str) # done, total, stage
|
|
1437
|
+
finished_signal = pyqtSignal(object, object, bool, str) # starless, stars_only, was_mono, errstr
|
|
1438
|
+
|
|
1439
|
+
def __init__(self, img_rgb01: np.ndarray, params: DarkStarParams, parent=None):
|
|
1440
|
+
super().__init__(parent)
|
|
1441
|
+
self._img = img_rgb01
|
|
1442
|
+
self._params = params
|
|
1443
|
+
|
|
1444
|
+
def run(self):
|
|
1445
|
+
try:
|
|
1446
|
+
def prog(done, total, stage):
|
|
1447
|
+
self.progress_signal.emit(int(done), int(total), str(stage))
|
|
1448
|
+
|
|
1449
|
+
# status_cb is optional; keep quiet or route to progress text if you want
|
|
1450
|
+
starless, stars_only, was_mono = darkstar_starremoval_rgb01(
|
|
1451
|
+
self._img,
|
|
1452
|
+
params=self._params,
|
|
1453
|
+
progress_cb=prog,
|
|
1454
|
+
status_cb=lambda s: None,
|
|
1455
|
+
)
|
|
1456
|
+
self.finished_signal.emit(starless, stars_only, bool(was_mono), "")
|
|
1457
|
+
except Exception as e:
|
|
1458
|
+
self.finished_signal.emit(None, None, False, str(e))
|
|
1459
|
+
|
|
1551
1460
|
|
|
1552
1461
|
class DarkStarConfigDialog(QDialog):
|
|
1553
1462
|
"""
|
|
@@ -1597,3 +1506,76 @@ class DarkStarConfigDialog(QDialog):
|
|
|
1597
1506
|
"show_extracted_stars": self.chk_show_stars.isChecked(),
|
|
1598
1507
|
"stride": int(self.cmb_stride.currentData()),
|
|
1599
1508
|
}
|
|
1509
|
+
|
|
1510
|
+
|
|
1511
|
+
def darkstar_starless_from_array(
|
|
1512
|
+
arr_rgb01: np.ndarray,
|
|
1513
|
+
*,
|
|
1514
|
+
use_gpu: bool = True,
|
|
1515
|
+
chunk_size: int = 512,
|
|
1516
|
+
overlap_frac: float = 0.125,
|
|
1517
|
+
mode: str = "unscreen", # "unscreen" | "additive"
|
|
1518
|
+
output_stars_only: bool = False,
|
|
1519
|
+
progress_cb=None, # (done:int, total:int, stage:str) -> None
|
|
1520
|
+
status_cb=None # (msg:str) -> None
|
|
1521
|
+
) -> tuple[np.ndarray, np.ndarray | None, bool]:
|
|
1522
|
+
"""
|
|
1523
|
+
Headless DarkStar:
|
|
1524
|
+
input: float32 [0..1], mono or RGB
|
|
1525
|
+
output: (starless, stars_only_or_None, was_mono)
|
|
1526
|
+
"""
|
|
1527
|
+
x = np.asarray(arr_rgb01, dtype=np.float32)
|
|
1528
|
+
|
|
1529
|
+
was_mono = (x.ndim == 2) or (x.ndim == 3 and x.shape[2] == 1)
|
|
1530
|
+
|
|
1531
|
+
# Normalize shape to what engine expects (it can accept mono or rgb, but keep it consistent)
|
|
1532
|
+
if x.ndim == 3 and x.shape[2] == 1:
|
|
1533
|
+
x = x[..., 0] # collapse to (H,W) for mono
|
|
1534
|
+
|
|
1535
|
+
x = np.nan_to_num(x, nan=0.0, posinf=0.0, neginf=0.0).astype(np.float32, copy=False)
|
|
1536
|
+
x = np.clip(x, 0.0, 1.0)
|
|
1537
|
+
|
|
1538
|
+
params = DarkStarParams(
|
|
1539
|
+
use_gpu=bool(use_gpu),
|
|
1540
|
+
chunk_size=int(chunk_size),
|
|
1541
|
+
overlap_frac=float(overlap_frac),
|
|
1542
|
+
mode=str(mode),
|
|
1543
|
+
output_stars_only=bool(output_stars_only),
|
|
1544
|
+
)
|
|
1545
|
+
|
|
1546
|
+
def _prog(done, total, stage):
|
|
1547
|
+
if callable(progress_cb):
|
|
1548
|
+
try:
|
|
1549
|
+
progress_cb(int(done), int(total), str(stage))
|
|
1550
|
+
except Exception:
|
|
1551
|
+
pass
|
|
1552
|
+
|
|
1553
|
+
def _status(msg: str):
|
|
1554
|
+
if callable(status_cb):
|
|
1555
|
+
try:
|
|
1556
|
+
status_cb(str(msg))
|
|
1557
|
+
except Exception:
|
|
1558
|
+
pass
|
|
1559
|
+
|
|
1560
|
+
starless, stars_only, engine_was_mono = darkstar_starremoval_rgb01(
|
|
1561
|
+
x,
|
|
1562
|
+
params=params,
|
|
1563
|
+
progress_cb=_prog if callable(progress_cb) else None,
|
|
1564
|
+
status_cb=_status if callable(status_cb) else None,
|
|
1565
|
+
)
|
|
1566
|
+
|
|
1567
|
+
# Engine should return [0..1] float32, but enforce it anyway
|
|
1568
|
+
if starless is not None:
|
|
1569
|
+
starless = np.clip(np.asarray(starless, dtype=np.float32), 0.0, 1.0)
|
|
1570
|
+
|
|
1571
|
+
if stars_only is not None:
|
|
1572
|
+
stars_only = np.clip(np.asarray(stars_only, dtype=np.float32), 0.0, 1.0)
|
|
1573
|
+
|
|
1574
|
+
# If input was mono, guarantee mono out (some paths may hand back rgb)
|
|
1575
|
+
if was_mono and starless is not None and starless.ndim == 3:
|
|
1576
|
+
starless = starless.mean(axis=2).astype(np.float32, copy=False)
|
|
1577
|
+
|
|
1578
|
+
if was_mono and stars_only is not None and stars_only.ndim == 3:
|
|
1579
|
+
stars_only = stars_only.mean(axis=2).astype(np.float32, copy=False)
|
|
1580
|
+
|
|
1581
|
+
return starless, (stars_only if output_stars_only else None), bool(was_mono)
|