setiastrosuitepro 1.6.4__py3-none-any.whl → 1.6.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of setiastrosuitepro might be problematic. Click here for more details.
- setiastro/images/abeicon.svg +16 -0
- setiastro/images/acv_icon.png +0 -0
- setiastro/images/colorwheel.svg +97 -0
- setiastro/images/cosmic.svg +40 -0
- setiastro/images/cosmicsat.svg +24 -0
- setiastro/images/first_quarter.png +0 -0
- setiastro/images/full_moon.png +0 -0
- setiastro/images/graxpert.svg +19 -0
- setiastro/images/last_quarter.png +0 -0
- setiastro/images/linearfit.svg +32 -0
- setiastro/images/new_moon.png +0 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/images/waning_crescent_1.png +0 -0
- setiastro/images/waning_crescent_2.png +0 -0
- setiastro/images/waning_crescent_3.png +0 -0
- setiastro/images/waning_crescent_4.png +0 -0
- setiastro/images/waning_crescent_5.png +0 -0
- setiastro/images/waning_gibbous_1.png +0 -0
- setiastro/images/waning_gibbous_2.png +0 -0
- setiastro/images/waning_gibbous_3.png +0 -0
- setiastro/images/waning_gibbous_4.png +0 -0
- setiastro/images/waning_gibbous_5.png +0 -0
- setiastro/images/waxing_crescent_1.png +0 -0
- setiastro/images/waxing_crescent_2.png +0 -0
- setiastro/images/waxing_crescent_3.png +0 -0
- setiastro/images/waxing_crescent_4.png +0 -0
- setiastro/images/waxing_crescent_5.png +0 -0
- setiastro/images/waxing_gibbous_1.png +0 -0
- setiastro/images/waxing_gibbous_2.png +0 -0
- setiastro/images/waxing_gibbous_3.png +0 -0
- setiastro/images/waxing_gibbous_4.png +0 -0
- setiastro/images/waxing_gibbous_5.png +0 -0
- setiastro/qml/ResourceMonitor.qml +84 -82
- setiastro/saspro/__main__.py +20 -1
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/abe.py +37 -4
- setiastro/saspro/aberration_ai.py +237 -21
- setiastro/saspro/acv_exporter.py +379 -0
- setiastro/saspro/add_stars.py +33 -6
- setiastro/saspro/backgroundneutral.py +108 -40
- setiastro/saspro/blemish_blaster.py +4 -1
- setiastro/saspro/blink_comparator_pro.py +74 -24
- setiastro/saspro/clahe.py +4 -1
- setiastro/saspro/continuum_subtract.py +4 -1
- setiastro/saspro/convo.py +13 -7
- setiastro/saspro/cosmicclarity.py +129 -18
- setiastro/saspro/crop_dialog_pro.py +123 -7
- setiastro/saspro/curve_editor_pro.py +109 -42
- setiastro/saspro/doc_manager.py +245 -15
- setiastro/saspro/exoplanet_detector.py +120 -28
- setiastro/saspro/frequency_separation.py +1158 -204
- setiastro/saspro/ghs_dialog_pro.py +81 -16
- setiastro/saspro/graxpert.py +1 -0
- setiastro/saspro/gui/main_window.py +429 -228
- setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
- setiastro/saspro/gui/mixins/menu_mixin.py +27 -1
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +384 -18
- setiastro/saspro/gui/mixins/update_mixin.py +138 -36
- setiastro/saspro/gui/mixins/view_mixin.py +42 -0
- setiastro/saspro/halobgon.py +4 -0
- setiastro/saspro/histogram.py +5 -1
- setiastro/saspro/image_combine.py +4 -0
- setiastro/saspro/image_peeker_pro.py +4 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
- setiastro/saspro/imageops/stretch.py +582 -62
- setiastro/saspro/isophote.py +4 -0
- setiastro/saspro/layers.py +13 -9
- setiastro/saspro/layers_dock.py +183 -3
- setiastro/saspro/legacy/image_manager.py +154 -20
- setiastro/saspro/legacy/numba_utils.py +67 -47
- setiastro/saspro/legacy/xisf.py +240 -98
- setiastro/saspro/live_stacking.py +180 -79
- setiastro/saspro/luminancerecombine.py +228 -27
- setiastro/saspro/mask_creation.py +174 -15
- setiastro/saspro/mfdeconv.py +113 -35
- setiastro/saspro/mfdeconvcudnn.py +119 -70
- setiastro/saspro/mfdeconvsport.py +112 -35
- setiastro/saspro/morphology.py +4 -0
- setiastro/saspro/multiscale_decomp.py +51 -12
- setiastro/saspro/numba_utils.py +72 -57
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/ops/script_editor.py +10 -2
- setiastro/saspro/ops/scripts.py +122 -0
- setiastro/saspro/perfect_palette_picker.py +37 -3
- setiastro/saspro/plate_solver.py +84 -49
- setiastro/saspro/psf_viewer.py +119 -37
- setiastro/saspro/resources.py +67 -0
- setiastro/saspro/rgbalign.py +4 -0
- setiastro/saspro/selective_color.py +4 -1
- setiastro/saspro/sfcc.py +364 -152
- setiastro/saspro/shortcuts.py +160 -29
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1331 -484
- setiastro/saspro/star_alignment.py +247 -123
- setiastro/saspro/star_spikes.py +4 -0
- setiastro/saspro/star_stretch.py +38 -3
- setiastro/saspro/stat_stretch.py +743 -128
- setiastro/saspro/subwindow.py +786 -360
- setiastro/saspro/supernovaasteroidhunter.py +1 -1
- setiastro/saspro/wavescale_hdr.py +4 -1
- setiastro/saspro/wavescalede.py +4 -1
- setiastro/saspro/whitebalance.py +84 -12
- setiastro/saspro/widgets/common_utilities.py +28 -21
- setiastro/saspro/widgets/resource_monitor.py +109 -59
- setiastro/saspro/widgets/spinboxes.py +10 -13
- setiastro/saspro/wimi.py +27 -656
- setiastro/saspro/wims.py +13 -3
- setiastro/saspro/xisf.py +101 -11
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/METADATA +2 -1
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/RECORD +115 -82
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/license.txt +0 -0
setiastro/saspro/sfcc.py
CHANGED
|
@@ -48,6 +48,9 @@ from PyQt6.QtWidgets import (QToolBar, QWidget, QToolButton, QMenu, QApplication
|
|
|
48
48
|
QInputDialog, QMessageBox, QDialog, QFileDialog,
|
|
49
49
|
QFormLayout, QDialogButtonBox, QDoubleSpinBox, QCheckBox, QLabel, QRubberBand, QRadioButton, QMainWindow, QPushButton)
|
|
50
50
|
|
|
51
|
+
from setiastro.saspro.backgroundneutral import run_background_neutral_via_preset
|
|
52
|
+
from setiastro.saspro.backgroundneutral import background_neutralize_rgb, auto_rect_50x50
|
|
53
|
+
|
|
51
54
|
|
|
52
55
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
53
56
|
# Utilities
|
|
@@ -188,6 +191,12 @@ def compute_gradient_map(sources, delta_flux, shape, method="poly2"):
|
|
|
188
191
|
else:
|
|
189
192
|
raise ValueError("method must be one of 'poly2','poly3','rbf'")
|
|
190
193
|
|
|
194
|
+
def _pivot_scale_channel(ch: np.ndarray, gain: np.ndarray | float, pivot: float) -> np.ndarray:
|
|
195
|
+
"""
|
|
196
|
+
Apply gain around a pivot: pivot + (x - pivot)*gain.
|
|
197
|
+
gain can be scalar or per-pixel array.
|
|
198
|
+
"""
|
|
199
|
+
return pivot + (ch - pivot) * gain
|
|
191
200
|
|
|
192
201
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
193
202
|
# Simple responses viewer (unchanged core logic; useful for diagnostics)
|
|
@@ -349,6 +358,10 @@ class SFCCDialog(QDialog):
|
|
|
349
358
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
350
359
|
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
351
360
|
self.setModal(False)
|
|
361
|
+
try:
|
|
362
|
+
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
363
|
+
except Exception:
|
|
364
|
+
pass # older PyQt6 versions
|
|
352
365
|
self.setMinimumSize(800, 600)
|
|
353
366
|
|
|
354
367
|
self.doc_manager = doc_manager
|
|
@@ -375,6 +388,7 @@ class SFCCDialog(QDialog):
|
|
|
375
388
|
self.lp_filter_combo2.currentIndexChanged.connect(self.save_lp2_setting)
|
|
376
389
|
self.sens_combo.currentIndexChanged.connect(self.save_sensor_setting)
|
|
377
390
|
self.star_combo.currentIndexChanged.connect(self.save_star_setting)
|
|
391
|
+
self.finished.connect(lambda *_: self._cleanup())
|
|
378
392
|
|
|
379
393
|
self.grad_method = "poly3"
|
|
380
394
|
self.grad_method_combo.currentTextChanged.connect(lambda m: setattr(self, "grad_method", m))
|
|
@@ -528,12 +542,12 @@ class SFCCDialog(QDialog):
|
|
|
528
542
|
self.remove_curve_btn = QPushButton(self.tr("Remove Filter/Sensor Curve…"))
|
|
529
543
|
self.remove_curve_btn.clicked.connect(self.remove_custom_curve); row4.addWidget(self.remove_curve_btn)
|
|
530
544
|
row4.addStretch()
|
|
531
|
-
self.close_btn = QPushButton(self.tr("Close")); self.close_btn.clicked.connect(self.
|
|
545
|
+
self.close_btn = QPushButton(self.tr("Close")); self.close_btn.clicked.connect(self.reject); row4.addWidget(self.close_btn)
|
|
532
546
|
|
|
533
547
|
self.count_label = QLabel(""); layout.addWidget(self.count_label)
|
|
534
548
|
|
|
535
549
|
self.figure = Figure(figsize=(6, 4)); self.canvas = FigureCanvas(self.figure); self.canvas.setVisible(False); layout.addWidget(self.canvas, stretch=1)
|
|
536
|
-
self.reset_btn = QPushButton(self.tr("Reset View/Close")); self.reset_btn.clicked.connect(self.
|
|
550
|
+
self.reset_btn = QPushButton(self.tr("Reset View/Close")); self.reset_btn.clicked.connect(self.reject); layout.addWidget(self.reset_btn)
|
|
537
551
|
|
|
538
552
|
# hide gradient controls by default (enable if you like)
|
|
539
553
|
self.run_grad_btn.hide(); self.grad_method_combo.hide()
|
|
@@ -870,28 +884,89 @@ class SFCCDialog(QDialog):
|
|
|
870
884
|
return self.wcs.all_pix2world(x, y, 0)
|
|
871
885
|
|
|
872
886
|
# ── Background neutralization ───────────────────────────────────────
|
|
887
|
+
def _neutralize_background(self, rgb_f: np.ndarray, *, remove_pedestal: bool = False) -> np.ndarray:
|
|
888
|
+
img = np.asarray(rgb_f, dtype=np.float32)
|
|
889
|
+
|
|
890
|
+
if img.ndim != 3 or img.shape[2] != 3:
|
|
891
|
+
raise ValueError("Expected RGB image (H,W,3)")
|
|
873
892
|
|
|
874
|
-
|
|
875
|
-
|
|
893
|
+
img = np.clip(img, 0.0, 1.0)
|
|
894
|
+
|
|
895
|
+
try:
|
|
896
|
+
rect = auto_rect_50x50(img) # same SASv2-ish auto finder
|
|
897
|
+
out = background_neutralize_rgb(
|
|
898
|
+
img,
|
|
899
|
+
rect,
|
|
900
|
+
mode="pivot1", # or "offset" if you prefer
|
|
901
|
+
remove_pedestal=remove_pedestal,
|
|
902
|
+
)
|
|
903
|
+
return np.clip(out, 0.0, 1.0).astype(np.float32, copy=False)
|
|
904
|
+
|
|
905
|
+
except Exception as e:
|
|
906
|
+
print(f"[SFCC] BN preset failed, falling back to simple neutralization: {e}")
|
|
907
|
+
return self._neutralize_background_simple(img, patch_size=10)
|
|
908
|
+
|
|
909
|
+
def _neutralize_background_simple(self, rgb_f: np.ndarray, patch_size: int = 50) -> np.ndarray:
|
|
910
|
+
"""
|
|
911
|
+
Simple neutralization: find darkest patch by summed medians,
|
|
912
|
+
then equalize channel medians around the mean.
|
|
913
|
+
Assumes rgb_f is float in [0,1] with no negatives.
|
|
914
|
+
"""
|
|
915
|
+
img = np.asarray(rgb_f, dtype=np.float32).copy()
|
|
876
916
|
h, w = img.shape[:2]
|
|
877
|
-
ph, pw = h // patch_size, w // patch_size
|
|
917
|
+
ph, pw = max(1, h // patch_size), max(1, w // patch_size)
|
|
918
|
+
|
|
878
919
|
min_sum, best_med = np.inf, None
|
|
879
920
|
for i in range(patch_size):
|
|
880
921
|
for j in range(patch_size):
|
|
881
922
|
y0, x0 = i * ph, j * pw
|
|
882
|
-
patch = img[y0:min(y0+ph, h), x0:min(x0+pw, w), :]
|
|
883
|
-
|
|
884
|
-
|
|
923
|
+
patch = img[y0:min(y0 + ph, h), x0:min(x0 + pw, w), :]
|
|
924
|
+
if patch.size == 0:
|
|
925
|
+
continue
|
|
926
|
+
med = np.median(patch, axis=(0, 1))
|
|
927
|
+
s = float(med.sum())
|
|
885
928
|
if s < min_sum:
|
|
886
929
|
min_sum, best_med = s, med
|
|
930
|
+
|
|
887
931
|
if best_med is None:
|
|
888
|
-
return img
|
|
889
|
-
|
|
932
|
+
return np.clip(img, 0.0, 1.0)
|
|
933
|
+
|
|
934
|
+
target = float(best_med.mean())
|
|
935
|
+
eps = 1e-8
|
|
890
936
|
for c in range(3):
|
|
891
937
|
diff = float(best_med[c] - target)
|
|
892
|
-
if abs(diff) < eps:
|
|
938
|
+
if abs(diff) < eps:
|
|
939
|
+
continue
|
|
940
|
+
# Preserve [0,1] scale; keep the same form you were using.
|
|
893
941
|
img[..., c] = np.clip((img[..., c] - diff) / (1.0 - diff), 0.0, 1.0)
|
|
894
|
-
|
|
942
|
+
|
|
943
|
+
return np.clip(img, 0.0, 1.0)
|
|
944
|
+
|
|
945
|
+
def _make_working_base_for_sep(self, img_float: np.ndarray) -> np.ndarray:
|
|
946
|
+
"""
|
|
947
|
+
Build a working copy for SEP + calibration.
|
|
948
|
+
|
|
949
|
+
Pedestal removal (per channel):
|
|
950
|
+
ch <- ch - min(ch)
|
|
951
|
+
|
|
952
|
+
Then clamp to [0,1] for stability.
|
|
953
|
+
"""
|
|
954
|
+
base = np.asarray(img_float, dtype=np.float32).copy()
|
|
955
|
+
|
|
956
|
+
if base.ndim != 3 or base.shape[2] != 3:
|
|
957
|
+
raise ValueError("Expected RGB image (H,W,3)")
|
|
958
|
+
|
|
959
|
+
# --- Per-channel pedestal removal: ch -= min(ch) ---
|
|
960
|
+
mins = base.reshape(-1, 3).min(axis=0) # (3,)
|
|
961
|
+
base[..., 0] -= float(mins[0])
|
|
962
|
+
base[..., 1] -= float(mins[1])
|
|
963
|
+
base[..., 2] -= float(mins[2])
|
|
964
|
+
|
|
965
|
+
# Stability clamp (SEP likes non-negative; your pipeline assumes [0,1])
|
|
966
|
+
base = np.clip(base, 0.0, 1.0)
|
|
967
|
+
|
|
968
|
+
return base
|
|
969
|
+
|
|
895
970
|
|
|
896
971
|
# ── SIMBAD/Star fetch ──────────────────────────────────────────────
|
|
897
972
|
|
|
@@ -1030,7 +1105,6 @@ class SFCCDialog(QDialog):
|
|
|
1030
1105
|
self.canvas.setVisible(False); self.canvas.draw()
|
|
1031
1106
|
|
|
1032
1107
|
# ── Core SFCC ───────────────────────────────────────────────────────
|
|
1033
|
-
|
|
1034
1108
|
def run_spcc(self):
|
|
1035
1109
|
ref_sed_name = self.star_combo.currentData()
|
|
1036
1110
|
r_filt = self.r_filter_combo.currentText()
|
|
@@ -1041,13 +1115,15 @@ class SFCCDialog(QDialog):
|
|
|
1041
1115
|
lp_filt2 = self.lp_filter_combo2.currentText()
|
|
1042
1116
|
|
|
1043
1117
|
if not ref_sed_name:
|
|
1044
|
-
QMessageBox.warning(self, "Error", "Select a reference spectral type (e.g. A0V).")
|
|
1118
|
+
QMessageBox.warning(self, "Error", "Select a reference spectral type (e.g. A0V).")
|
|
1119
|
+
return
|
|
1045
1120
|
if r_filt == "(None)" and g_filt == "(None)" and b_filt == "(None)":
|
|
1046
|
-
QMessageBox.warning(self, "Error", "Pick at least one of R, G or B filters.")
|
|
1121
|
+
QMessageBox.warning(self, "Error", "Pick at least one of R, G or B filters.")
|
|
1122
|
+
return
|
|
1047
1123
|
if sens_name == "(None)":
|
|
1048
|
-
QMessageBox.warning(self, "Error", "Select a sensor QE curve.")
|
|
1124
|
+
QMessageBox.warning(self, "Error", "Select a sensor QE curve.")
|
|
1125
|
+
return
|
|
1049
1126
|
|
|
1050
|
-
# -- Step 1A: get active image as float32 in [0..1]
|
|
1051
1127
|
doc = self.doc_manager.get_active_document()
|
|
1052
1128
|
if doc is None or doc.image is None:
|
|
1053
1129
|
QMessageBox.critical(self, "Error", "No active document.")
|
|
@@ -1059,29 +1135,32 @@ class SFCCDialog(QDialog):
|
|
|
1059
1135
|
QMessageBox.critical(self, "Error", "Active document must be RGB (3 channels).")
|
|
1060
1136
|
return
|
|
1061
1137
|
|
|
1138
|
+
# ---- Convert to float working space ----
|
|
1062
1139
|
if img.dtype == np.uint8:
|
|
1063
|
-
|
|
1140
|
+
img_float = img.astype(np.float32) / 255.0
|
|
1064
1141
|
else:
|
|
1065
|
-
|
|
1142
|
+
img_float = img.astype(np.float32, copy=False)
|
|
1066
1143
|
|
|
1067
|
-
# pedestal
|
|
1068
|
-
base =
|
|
1069
|
-
|
|
1070
|
-
|
|
1144
|
+
# ---- Build SEP working copy (ONE pedestal handling only) ----
|
|
1145
|
+
base = self._make_working_base_for_sep(img_float)
|
|
1146
|
+
|
|
1147
|
+
# Optional BN after calibration:
|
|
1148
|
+
# IMPORTANT: do NOT remove pedestal here either (avoid double pedestal removal).
|
|
1149
|
+
if self.neutralize_chk.isChecked():
|
|
1150
|
+
base = self._neutralize_background(base, remove_pedestal=False)
|
|
1071
1151
|
|
|
1072
1152
|
# SEP on grayscale
|
|
1073
|
-
gray = np.mean(base, axis=2)
|
|
1074
|
-
|
|
1153
|
+
gray = np.mean(base, axis=2).astype(np.float32)
|
|
1154
|
+
|
|
1075
1155
|
bkg = sep.Background(gray)
|
|
1076
1156
|
data_sub = gray - bkg.back()
|
|
1077
|
-
err = bkg.globalrms
|
|
1157
|
+
err = float(bkg.globalrms)
|
|
1158
|
+
|
|
1159
|
+
# User threshold
|
|
1160
|
+
sep_sigma = float(self.sep_thr_spin.value()) if hasattr(self, "sep_thr_spin") else 5.0
|
|
1161
|
+
self.count_label.setText(f"Detecting stars (SEP σ={sep_sigma:.1f})…")
|
|
1162
|
+
QApplication.processEvents()
|
|
1078
1163
|
|
|
1079
|
-
# 👇 get user threshold (default 5.0)
|
|
1080
|
-
if hasattr(self, "sep_thr_spin"):
|
|
1081
|
-
sep_sigma = float(self.sep_thr_spin.value())
|
|
1082
|
-
else:
|
|
1083
|
-
sep_sigma = 5.0
|
|
1084
|
-
self.count_label.setText(f"Detecting stars (SEP σ={sep_sigma:.1f})…"); QApplication.processEvents()
|
|
1085
1164
|
sources = sep.extract(data_sub, sep_sigma, err=err)
|
|
1086
1165
|
|
|
1087
1166
|
MAX_SOURCES = 300_000
|
|
@@ -1095,32 +1174,51 @@ class SFCCDialog(QDialog):
|
|
|
1095
1174
|
return
|
|
1096
1175
|
|
|
1097
1176
|
if sources.size == 0:
|
|
1098
|
-
QMessageBox.critical(self, "SEP Error", "SEP found no sources.")
|
|
1099
|
-
|
|
1100
|
-
|
|
1177
|
+
QMessageBox.critical(self, "SEP Error", "SEP found no sources.")
|
|
1178
|
+
return
|
|
1179
|
+
|
|
1180
|
+
# Radius filtering (unchanged)
|
|
1181
|
+
r_fluxrad, _ = sep.flux_radius(
|
|
1182
|
+
gray, sources["x"], sources["y"],
|
|
1183
|
+
2.0 * sources["a"], 0.5,
|
|
1184
|
+
normflux=sources["flux"], subpix=5
|
|
1185
|
+
)
|
|
1186
|
+
mask = (r_fluxrad > 0.2) & (r_fluxrad <= 10)
|
|
1187
|
+
sources = sources[mask]
|
|
1101
1188
|
if sources.size == 0:
|
|
1102
|
-
QMessageBox.critical(self, "SEP Error", "All SEP detections rejected by radius filter.")
|
|
1189
|
+
QMessageBox.critical(self, "SEP Error", "All SEP detections rejected by radius filter.")
|
|
1190
|
+
return
|
|
1103
1191
|
|
|
1104
1192
|
if not getattr(self, "star_list", None):
|
|
1105
|
-
QMessageBox.warning(self, "Error", "Fetch Stars (with WCS) before running SFCC.")
|
|
1193
|
+
QMessageBox.warning(self, "Error", "Fetch Stars (with WCS) before running SFCC.")
|
|
1194
|
+
return
|
|
1106
1195
|
|
|
1196
|
+
# ---- Match SIMBAD stars to SEP detections ----
|
|
1107
1197
|
raw_matches = []
|
|
1108
1198
|
for i, star in enumerate(self.star_list):
|
|
1109
|
-
dx = sources["x"] - star["x"]
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1199
|
+
dx = sources["x"] - star["x"]
|
|
1200
|
+
dy = sources["y"] - star["y"]
|
|
1201
|
+
j = int(np.argmin(dx * dx + dy * dy))
|
|
1202
|
+
if (dx[j] * dx[j] + dy[j] * dy[j]) < (3.0 ** 2):
|
|
1203
|
+
xi, yi = int(round(float(sources["x"][j]))), int(round(float(sources["y"][j])))
|
|
1113
1204
|
if 0 <= xi < W and 0 <= yi < H:
|
|
1114
|
-
raw_matches.append({
|
|
1205
|
+
raw_matches.append({
|
|
1206
|
+
"sim_index": i,
|
|
1207
|
+
"template": star.get("pickles_match") or star["sp_clean"],
|
|
1208
|
+
"x_pix": xi,
|
|
1209
|
+
"y_pix": yi
|
|
1210
|
+
})
|
|
1211
|
+
|
|
1115
1212
|
if not raw_matches:
|
|
1116
|
-
QMessageBox.warning(self, "No Matches", "No SIMBAD star matched to SEP detections.")
|
|
1213
|
+
QMessageBox.warning(self, "No Matches", "No SIMBAD star matched to SEP detections.")
|
|
1214
|
+
return
|
|
1117
1215
|
|
|
1118
1216
|
wl_min, wl_max = 3000, 11000
|
|
1119
|
-
wl_grid = np.arange(wl_min, wl_max+1)
|
|
1217
|
+
wl_grid = np.arange(wl_min, wl_max + 1)
|
|
1120
1218
|
|
|
1121
1219
|
def load_curve(ext):
|
|
1122
1220
|
for p in (self.user_custom_path, self.sasp_data_path):
|
|
1123
|
-
with fits.open(p) as hd:
|
|
1221
|
+
with fits.open(p, memmap=False) as hd:
|
|
1124
1222
|
if ext in hd:
|
|
1125
1223
|
d = hd[ext].data
|
|
1126
1224
|
wl = _ensure_angstrom(d["WAVELENGTH"].astype(float))
|
|
@@ -1130,7 +1228,7 @@ class SFCCDialog(QDialog):
|
|
|
1130
1228
|
|
|
1131
1229
|
def load_sed(ext):
|
|
1132
1230
|
for p in (self.user_custom_path, self.sasp_data_path):
|
|
1133
|
-
with fits.open(p) as hd:
|
|
1231
|
+
with fits.open(p, memmap=False) as hd:
|
|
1134
1232
|
if ext in hd:
|
|
1135
1233
|
d = hd[ext].data
|
|
1136
1234
|
wl = _ensure_angstrom(d["WAVELENGTH"].astype(float))
|
|
@@ -1138,18 +1236,21 @@ class SFCCDialog(QDialog):
|
|
|
1138
1236
|
return wl, fl
|
|
1139
1237
|
raise KeyError(f"SED '{ext}' not found")
|
|
1140
1238
|
|
|
1141
|
-
interp = lambda wl_o, tp_o: np.interp(wl_grid, wl_o, tp_o, left=0
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1239
|
+
interp = lambda wl_o, tp_o: np.interp(wl_grid, wl_o, tp_o, left=0.0, right=0.0)
|
|
1240
|
+
|
|
1241
|
+
T_R = interp(*load_curve(r_filt)) if r_filt != "(None)" else np.ones_like(wl_grid)
|
|
1242
|
+
T_G = interp(*load_curve(g_filt)) if g_filt != "(None)" else np.ones_like(wl_grid)
|
|
1243
|
+
T_B = interp(*load_curve(b_filt)) if b_filt != "(None)" else np.ones_like(wl_grid)
|
|
1244
|
+
QE = interp(*load_curve(sens_name)) if sens_name != "(None)" else np.ones_like(wl_grid)
|
|
1245
|
+
LP1 = interp(*load_curve(lp_filt)) if lp_filt != "(None)" else np.ones_like(wl_grid)
|
|
1246
|
+
LP2 = interp(*load_curve(lp_filt2)) if lp_filt2 != "(None)" else np.ones_like(wl_grid)
|
|
1148
1247
|
LP = LP1 * LP2
|
|
1149
|
-
|
|
1248
|
+
|
|
1249
|
+
T_sys_R, T_sys_G, T_sys_B = T_R * QE * LP, T_G * QE * LP, T_B * QE * LP
|
|
1150
1250
|
|
|
1151
1251
|
wl_ref, fl_ref = load_sed(ref_sed_name)
|
|
1152
|
-
fr_i = np.interp(wl_grid, wl_ref, fl_ref, left=0
|
|
1252
|
+
fr_i = np.interp(wl_grid, wl_ref, fl_ref, left=0.0, right=0.0)
|
|
1253
|
+
|
|
1153
1254
|
S_ref_R = np.trapezoid(fr_i * T_sys_R, x=wl_grid)
|
|
1154
1255
|
S_ref_G = np.trapezoid(fr_i * T_sys_G, x=wl_grid)
|
|
1155
1256
|
S_ref_B = np.trapezoid(fr_i * T_sys_B, x=wl_grid)
|
|
@@ -1158,155 +1259,207 @@ class SFCCDialog(QDialog):
|
|
|
1158
1259
|
diag_meas_BG, diag_exp_BG = [], []
|
|
1159
1260
|
enriched = []
|
|
1160
1261
|
|
|
1161
|
-
#
|
|
1262
|
+
# ---- Pre-calc integrals for unique templates ----
|
|
1162
1263
|
unique_simbad_types = set(m["template"] for m in raw_matches)
|
|
1163
|
-
|
|
1164
|
-
# Map simbad_type -> pickles_template_name
|
|
1264
|
+
|
|
1165
1265
|
simbad_to_pickles = {}
|
|
1166
1266
|
pickles_templates_needed = set()
|
|
1167
|
-
|
|
1168
1267
|
for sp in unique_simbad_types:
|
|
1169
1268
|
cands = pickles_match_for_simbad(sp, getattr(self, "pickles_templates", []))
|
|
1170
1269
|
if cands:
|
|
1171
|
-
|
|
1172
|
-
simbad_to_pickles[sp] =
|
|
1173
|
-
pickles_templates_needed.add(
|
|
1270
|
+
pname = cands[0]
|
|
1271
|
+
simbad_to_pickles[sp] = pname
|
|
1272
|
+
pickles_templates_needed.add(pname)
|
|
1174
1273
|
|
|
1175
|
-
# Pre-calc integrals for each unique Pickles template
|
|
1176
|
-
# Cache structure: template_name -> (S_sr, S_sg, S_sb)
|
|
1177
1274
|
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
1275
|
for pname in pickles_templates_needed:
|
|
1183
1276
|
try:
|
|
1184
1277
|
wl_s, fl_s = load_sed(pname)
|
|
1185
|
-
fs_i = np.interp(wl_grid, wl_s, fl_s, left=0
|
|
1186
|
-
|
|
1278
|
+
fs_i = np.interp(wl_grid, wl_s, fl_s, left=0.0, right=0.0)
|
|
1187
1279
|
S_sr = np.trapezoid(fs_i * T_sys_R, x=wl_grid)
|
|
1188
1280
|
S_sg = np.trapezoid(fs_i * T_sys_G, x=wl_grid)
|
|
1189
1281
|
S_sb = np.trapezoid(fs_i * T_sys_B, x=wl_grid)
|
|
1190
|
-
|
|
1191
1282
|
template_integrals[pname] = (S_sr, S_sg, S_sb)
|
|
1192
1283
|
except Exception as e:
|
|
1193
1284
|
print(f"[SFCC] Warning: failed to load/integrate template {pname}: {e}")
|
|
1194
1285
|
|
|
1195
|
-
#
|
|
1286
|
+
# ---- Main match loop (measure from 'base' only) ----
|
|
1196
1287
|
for m in raw_matches:
|
|
1197
1288
|
xi, yi, sp = m["x_pix"], m["y_pix"], m["template"]
|
|
1198
|
-
Rm = float(base[yi, xi, 0]); Gm = float(base[yi, xi, 1]); Bm = float(base[yi, xi, 2])
|
|
1199
|
-
if Gm <= 0: continue
|
|
1200
1289
|
|
|
1201
|
-
#
|
|
1290
|
+
# measure on the SEP working copy (already BN’d, only one pedestal handling)
|
|
1291
|
+
Rm = float(base[yi, xi, 0])
|
|
1292
|
+
Gm = float(base[yi, xi, 1])
|
|
1293
|
+
Bm = float(base[yi, xi, 2])
|
|
1294
|
+
if Gm <= 0:
|
|
1295
|
+
continue
|
|
1296
|
+
|
|
1202
1297
|
pname = simbad_to_pickles.get(sp)
|
|
1203
|
-
if not pname:
|
|
1204
|
-
|
|
1205
|
-
|
|
1298
|
+
if not pname:
|
|
1299
|
+
continue
|
|
1300
|
+
|
|
1206
1301
|
integrals = template_integrals.get(pname)
|
|
1207
|
-
if not integrals:
|
|
1208
|
-
|
|
1302
|
+
if not integrals:
|
|
1303
|
+
continue
|
|
1304
|
+
|
|
1209
1305
|
S_sr, S_sg, S_sb = integrals
|
|
1210
|
-
|
|
1211
|
-
|
|
1306
|
+
if S_sg <= 0:
|
|
1307
|
+
continue
|
|
1212
1308
|
|
|
1213
|
-
exp_RG = S_sr / S_sg
|
|
1214
|
-
|
|
1309
|
+
exp_RG = S_sr / S_sg
|
|
1310
|
+
exp_BG = S_sb / S_sg
|
|
1311
|
+
meas_RG = Rm / Gm
|
|
1312
|
+
meas_BG = Bm / Gm
|
|
1215
1313
|
|
|
1216
1314
|
diag_meas_RG.append(meas_RG); diag_exp_RG.append(exp_RG)
|
|
1217
1315
|
diag_meas_BG.append(meas_BG); diag_exp_BG.append(exp_BG)
|
|
1218
1316
|
|
|
1219
1317
|
enriched.append({
|
|
1220
|
-
**m,
|
|
1318
|
+
**m,
|
|
1319
|
+
"R_meas": Rm, "G_meas": Gm, "B_meas": Bm,
|
|
1221
1320
|
"S_star_R": S_sr, "S_star_G": S_sg, "S_star_B": S_sb,
|
|
1222
1321
|
"exp_RG": exp_RG, "exp_BG": exp_BG
|
|
1223
1322
|
})
|
|
1224
|
-
|
|
1323
|
+
|
|
1225
1324
|
self._last_matched = enriched
|
|
1226
|
-
diag_meas_RG = np.
|
|
1227
|
-
|
|
1325
|
+
diag_meas_RG = np.asarray(diag_meas_RG, dtype=np.float64)
|
|
1326
|
+
diag_exp_RG = np.asarray(diag_exp_RG, dtype=np.float64)
|
|
1327
|
+
diag_meas_BG = np.asarray(diag_meas_BG, dtype=np.float64)
|
|
1328
|
+
diag_exp_BG = np.asarray(diag_exp_BG, dtype=np.float64)
|
|
1329
|
+
|
|
1228
1330
|
if diag_meas_RG.size == 0 or diag_meas_BG.size == 0:
|
|
1229
|
-
QMessageBox.information(self, "No Valid Stars", "No stars with valid measured vs expected ratios.")
|
|
1230
|
-
|
|
1331
|
+
QMessageBox.information(self, "No Valid Stars", "No stars with valid measured vs expected ratios.")
|
|
1332
|
+
return
|
|
1333
|
+
|
|
1334
|
+
n_stars = int(diag_meas_RG.size)
|
|
1231
1335
|
|
|
1232
|
-
def rms_frac(pred, exp):
|
|
1233
|
-
|
|
1234
|
-
affine = lambda x, m, b: m*x + b
|
|
1235
|
-
quad = lambda x, a, b, c: a*x**2 + b*x + c
|
|
1336
|
+
def rms_frac(pred, exp):
|
|
1337
|
+
return float(np.sqrt(np.mean(((pred / exp) - 1.0) ** 2)))
|
|
1236
1338
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1339
|
+
slope_only = lambda x, m: m * x
|
|
1340
|
+
affine = lambda x, m, b: m * x + b
|
|
1341
|
+
quad = lambda x, a, b, c: a * x**2 + b * x + c
|
|
1342
|
+
|
|
1343
|
+
denR = float(np.sum(diag_meas_RG**2))
|
|
1344
|
+
denB = float(np.sum(diag_meas_BG**2))
|
|
1345
|
+
mR_s = (float(np.sum(diag_meas_RG * diag_exp_RG)) / denR) if denR > 0 else 1.0
|
|
1346
|
+
mB_s = (float(np.sum(diag_meas_BG * diag_exp_BG)) / denB) if denB > 0 else 1.0
|
|
1240
1347
|
rms_s = rms_frac(slope_only(diag_meas_RG, mR_s), diag_exp_RG) + rms_frac(slope_only(diag_meas_BG, mB_s), diag_exp_BG)
|
|
1241
1348
|
|
|
1242
|
-
mR_a, bR_a = np.linalg.lstsq(
|
|
1243
|
-
|
|
1349
|
+
mR_a, bR_a = np.linalg.lstsq(
|
|
1350
|
+
np.vstack([diag_meas_RG, np.ones_like(diag_meas_RG)]).T, diag_exp_RG, rcond=None
|
|
1351
|
+
)[0]
|
|
1352
|
+
mB_a, bB_a = np.linalg.lstsq(
|
|
1353
|
+
np.vstack([diag_meas_BG, np.ones_like(diag_meas_BG)]).T, diag_exp_BG, rcond=None
|
|
1354
|
+
)[0]
|
|
1244
1355
|
rms_a = rms_frac(affine(diag_meas_RG, mR_a, bR_a), diag_exp_RG) + rms_frac(affine(diag_meas_BG, mB_a, bB_a), diag_exp_BG)
|
|
1245
1356
|
|
|
1246
1357
|
aR_q, bR_q, cR_q = np.polyfit(diag_meas_RG, diag_exp_RG, 2)
|
|
1247
1358
|
aB_q, bB_q, cB_q = np.polyfit(diag_meas_BG, diag_exp_BG, 2)
|
|
1248
1359
|
rms_q = rms_frac(quad(diag_meas_RG, aR_q, bR_q, cR_q), diag_exp_RG) + rms_frac(quad(diag_meas_BG, aB_q, bB_q, cB_q), diag_exp_BG)
|
|
1249
1360
|
|
|
1250
|
-
idx = np.argmin([rms_s, rms_a, rms_q])
|
|
1251
|
-
if idx == 0:
|
|
1252
|
-
|
|
1253
|
-
|
|
1361
|
+
idx = int(np.argmin([rms_s, rms_a, rms_q]))
|
|
1362
|
+
if idx == 0:
|
|
1363
|
+
coeff_R, coeff_B, model_choice = (0.0, float(mR_s), 0.0), (0.0, float(mB_s), 0.0), "slope-only"
|
|
1364
|
+
elif idx == 1:
|
|
1365
|
+
coeff_R, coeff_B, model_choice = (0.0, float(mR_a), float(bR_a)), (0.0, float(mB_a), float(bB_a)), "affine"
|
|
1366
|
+
else:
|
|
1367
|
+
coeff_R, coeff_B, model_choice = (float(aR_q), float(bR_q), float(cR_q)), (float(aB_q), float(bB_q), float(cB_q)), "quadratic"
|
|
1368
|
+
|
|
1369
|
+
poly = lambda c, x: c[0] * x**2 + c[1] * x + c[2]
|
|
1254
1370
|
|
|
1255
|
-
|
|
1371
|
+
# ---- Diagnostics plot (unchanged) ----
|
|
1256
1372
|
self.figure.clf()
|
|
1257
|
-
#ax1 = self.figure.add_subplot(1, 3, 1); bins=20
|
|
1258
|
-
#ax1.hist(diag_meas_RG, bins=bins, alpha=.65, label="meas R/G", color="firebrick", edgecolor="black")
|
|
1259
|
-
#ax1.hist(diag_exp_RG, bins=bins, alpha=.55, label="exp R/G", color="salmon", edgecolor="black")
|
|
1260
|
-
#ax1.hist(diag_meas_BG, bins=bins, alpha=.65, label="meas B/G", color="royalblue", edgecolor="black")
|
|
1261
|
-
#ax1.hist(diag_exp_BG, bins=bins, alpha=.55, label="exp B/G", color="lightskyblue", edgecolor="black")
|
|
1262
|
-
#ax1.set_xlabel("Ratio (band / G)"); ax1.set_ylabel("Count"); ax1.set_title("Measured vs expected"); ax1.legend(fontsize=7, frameon=False)
|
|
1263
|
-
|
|
1264
1373
|
res0_RG = (diag_meas_RG / diag_exp_RG) - 1.0
|
|
1265
1374
|
res0_BG = (diag_meas_BG / diag_exp_BG) - 1.0
|
|
1266
1375
|
res1_RG = (poly(coeff_R, diag_meas_RG) / diag_exp_RG) - 1.0
|
|
1267
1376
|
res1_BG = (poly(coeff_B, diag_meas_BG) / diag_exp_BG) - 1.0
|
|
1268
1377
|
|
|
1269
|
-
ymin = np.min(np.concatenate([res0_RG, res0_BG]))
|
|
1270
|
-
|
|
1378
|
+
ymin = float(np.min(np.concatenate([res0_RG, res0_BG])))
|
|
1379
|
+
ymax = float(np.max(np.concatenate([res0_RG, res0_BG])))
|
|
1380
|
+
pad = 0.05 * (ymax - ymin) if ymax > ymin else 0.02
|
|
1381
|
+
y_lim = (ymin - pad, ymax + pad)
|
|
1382
|
+
|
|
1271
1383
|
def shade(ax, yvals, color):
|
|
1272
|
-
q1, q3 = np.percentile(yvals, [25,75])
|
|
1384
|
+
q1, q3 = np.percentile(yvals, [25, 75])
|
|
1385
|
+
ax.axhspan(q1, q3, color=color, alpha=0.10, zorder=0)
|
|
1273
1386
|
|
|
1274
1387
|
ax2 = self.figure.add_subplot(1, 2, 1)
|
|
1275
|
-
ax2.axhline(0, color="0.65", ls="--", lw=1)
|
|
1276
|
-
ax2
|
|
1277
|
-
ax2.scatter(
|
|
1278
|
-
ax2.
|
|
1279
|
-
ax2.
|
|
1388
|
+
ax2.axhline(0, color="0.65", ls="--", lw=1)
|
|
1389
|
+
shade(ax2, res0_RG, "firebrick"); shade(ax2, res0_BG, "royalblue")
|
|
1390
|
+
ax2.scatter(diag_exp_RG, res0_RG, c="firebrick", marker="o", alpha=0.7, label="R/G residual")
|
|
1391
|
+
ax2.scatter(diag_exp_BG, res0_BG, c="royalblue", marker="s", alpha=0.7, label="B/G residual")
|
|
1392
|
+
ax2.set_ylim(*y_lim)
|
|
1393
|
+
ax2.set_xlabel("Expected (band/G)")
|
|
1394
|
+
ax2.set_ylabel("Frac residual (meas/exp − 1)")
|
|
1395
|
+
ax2.set_title("Residuals • BEFORE")
|
|
1396
|
+
ax2.legend(frameon=False, fontsize=7, loc="lower right")
|
|
1280
1397
|
|
|
1281
1398
|
ax3 = self.figure.add_subplot(1, 2, 2)
|
|
1282
|
-
ax3.axhline(0, color="0.65", ls="--", lw=1)
|
|
1283
|
-
ax3
|
|
1284
|
-
ax3.scatter(
|
|
1285
|
-
ax3.
|
|
1399
|
+
ax3.axhline(0, color="0.65", ls="--", lw=1)
|
|
1400
|
+
shade(ax3, res1_RG, "firebrick"); shade(ax3, res1_BG, "royalblue")
|
|
1401
|
+
ax3.scatter(diag_exp_RG, res1_RG, c="firebrick", marker="o", alpha=0.7)
|
|
1402
|
+
ax3.scatter(diag_exp_BG, res1_BG, c="royalblue", marker="s", alpha=0.7)
|
|
1403
|
+
ax3.set_ylim(*y_lim)
|
|
1404
|
+
ax3.set_xlabel("Expected (band/G)")
|
|
1405
|
+
ax3.set_ylabel("Frac residual (corrected/exp − 1)")
|
|
1286
1406
|
ax3.set_title("Residuals • AFTER")
|
|
1287
|
-
self.canvas.setVisible(True); self.figure.tight_layout(w_pad=2.); self.canvas.draw()
|
|
1288
|
-
|
|
1289
|
-
self.count_label.setText("Applying SFCC color scales to image…"); QApplication.processEvents()
|
|
1290
|
-
if img.dtype == np.uint8: img_float = img.astype(np.float32) / 255.0
|
|
1291
|
-
else: img_float = img.astype(np.float32)
|
|
1292
|
-
|
|
1293
|
-
RG = img_float[..., 0] / np.maximum(img_float[..., 1], 1e-8)
|
|
1294
|
-
BG = img_float[..., 2] / np.maximum(img_float[..., 1], 1e-8)
|
|
1295
|
-
aR, bR, cR = coeff_R; aB, bB, cB = coeff_B
|
|
1296
|
-
RG_corr = aR*RG**2 + bR*RG + cR
|
|
1297
|
-
BG_corr = aB*BG**2 + bB*BG + cB
|
|
1298
|
-
calibrated = img_float.copy()
|
|
1299
|
-
calibrated[..., 0] = RG_corr * img_float[..., 1]
|
|
1300
|
-
calibrated[..., 2] = BG_corr * img_float[..., 1]
|
|
1301
|
-
calibrated = np.clip(calibrated, 0, 1)
|
|
1302
1407
|
|
|
1408
|
+
self.canvas.setVisible(True)
|
|
1409
|
+
self.figure.tight_layout(w_pad=2.0)
|
|
1410
|
+
self.canvas.draw()
|
|
1411
|
+
|
|
1412
|
+
# ---- Apply SFCC correction to ORIGINAL floats (not the SEP base) ----
|
|
1413
|
+
self.count_label.setText("Applying SFCC color scales to image…")
|
|
1414
|
+
QApplication.processEvents()
|
|
1415
|
+
|
|
1416
|
+
eps = 1e-8
|
|
1417
|
+
calibrated = base.copy()
|
|
1418
|
+
|
|
1419
|
+
R = calibrated[..., 0]
|
|
1420
|
+
G = calibrated[..., 1]
|
|
1421
|
+
B = calibrated[..., 2]
|
|
1422
|
+
|
|
1423
|
+
RG = R / np.maximum(G, eps)
|
|
1424
|
+
BG = B / np.maximum(G, eps)
|
|
1425
|
+
|
|
1426
|
+
aR, bR, cR = coeff_R
|
|
1427
|
+
aB, bB, cB = coeff_B
|
|
1428
|
+
|
|
1429
|
+
mR = aR * RG**2 + bR * RG + cR
|
|
1430
|
+
mB = aB * BG**2 + bB * BG + cB
|
|
1431
|
+
|
|
1432
|
+
mR = np.clip(mR, 0.25, 4.0)
|
|
1433
|
+
mB = np.clip(mB, 0.25, 4.0)
|
|
1434
|
+
|
|
1435
|
+
pR = float(np.median(R))
|
|
1436
|
+
pB = float(np.median(B))
|
|
1437
|
+
|
|
1438
|
+
calibrated[..., 0] = _pivot_scale_channel(R, mR, pR)
|
|
1439
|
+
calibrated[..., 2] = _pivot_scale_channel(B, mB, pB)
|
|
1440
|
+
|
|
1441
|
+
calibrated = np.clip(calibrated, 0.0, 1.0)
|
|
1442
|
+
|
|
1443
|
+
# --- OPTIONAL: apply BN/pedestal to the FINAL calibrated image, not just SEP base ---
|
|
1303
1444
|
if self.neutralize_chk.isChecked():
|
|
1304
|
-
|
|
1445
|
+
try:
|
|
1446
|
+
print("[SFCC] Applying background neutralization to final calibrated image...")
|
|
1447
|
+
_debug_probe_channels(calibrated, "final_before_BN")
|
|
1448
|
+
|
|
1449
|
+
# If you want pedestal removal as part of BN, set remove_pedestal=True here
|
|
1450
|
+
# (and/or make this a checkbox)
|
|
1451
|
+
calibrated = self._neutralize_background(calibrated, remove_pedestal=True)
|
|
1305
1452
|
|
|
1453
|
+
_debug_probe_channels(calibrated, "final_after_BN")
|
|
1454
|
+
except Exception as e:
|
|
1455
|
+
print(f"[SFCC] Final BN failed: {e}")
|
|
1456
|
+
|
|
1457
|
+
|
|
1458
|
+
# Convert back to original dtype
|
|
1306
1459
|
if img.dtype == np.uint8:
|
|
1307
|
-
|
|
1460
|
+
out_img = (np.clip(calibrated, 0.0, 1.0) * 255.0).astype(np.uint8)
|
|
1308
1461
|
else:
|
|
1309
|
-
|
|
1462
|
+
out_img = np.clip(calibrated, 0.0, 1.0).astype(np.float32)
|
|
1310
1463
|
|
|
1311
1464
|
new_meta = dict(doc.metadata or {})
|
|
1312
1465
|
new_meta.update({
|
|
@@ -1318,25 +1471,31 @@ class SFCCDialog(QDialog):
|
|
|
1318
1471
|
})
|
|
1319
1472
|
|
|
1320
1473
|
self.doc_manager.update_active_document(
|
|
1321
|
-
|
|
1474
|
+
out_img,
|
|
1322
1475
|
metadata=new_meta,
|
|
1323
1476
|
step_name="SFCC Calibrated",
|
|
1324
|
-
doc=doc,
|
|
1477
|
+
doc=doc,
|
|
1325
1478
|
)
|
|
1326
1479
|
|
|
1327
1480
|
self.count_label.setText(f"Applied SFCC color calibration using {n_stars} stars")
|
|
1328
1481
|
QApplication.processEvents()
|
|
1329
1482
|
|
|
1330
|
-
def pretty(coeff):
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1483
|
+
def pretty(coeff):
|
|
1484
|
+
# coefficient sum gives you f(1) for quadratic form a*x^2+b*x+c at x=1
|
|
1485
|
+
return float(coeff[0] + coeff[1] + coeff[2])
|
|
1486
|
+
|
|
1487
|
+
QMessageBox.information(
|
|
1488
|
+
self,
|
|
1489
|
+
"SFCC Complete",
|
|
1490
|
+
f"Applied SFCC using {n_stars} stars\n"
|
|
1491
|
+
f"Model: {model_choice}\n"
|
|
1492
|
+
f"R ratio @ x=1: {pretty(coeff_R):.4f}\n"
|
|
1493
|
+
f"B ratio @ x=1: {pretty(coeff_B):.4f}\n"
|
|
1494
|
+
f"Background neutralisation: {'ON' if self.neutralize_chk.isChecked() else 'OFF'}"
|
|
1495
|
+
)
|
|
1496
|
+
|
|
1497
|
+
self.current_image = out_img # keep for gradient step
|
|
1338
1498
|
|
|
1339
|
-
self.current_image = calibrated # keep for gradient step
|
|
1340
1499
|
|
|
1341
1500
|
# ── Chromatic gradient (optional) ──────────────────────────────────
|
|
1342
1501
|
|
|
@@ -1454,11 +1613,64 @@ class SFCCDialog(QDialog):
|
|
|
1454
1613
|
self.sasp_viewer_window.show()
|
|
1455
1614
|
self.sasp_viewer_window.destroyed.connect(self._on_sasp_closed)
|
|
1456
1615
|
|
|
1616
|
+
def _cleanup(self):
|
|
1617
|
+
# 1) Close/cleanup child window (SaspViewer)
|
|
1618
|
+
try:
|
|
1619
|
+
if getattr(self, "sasp_viewer_window", None) is not None:
|
|
1620
|
+
try:
|
|
1621
|
+
self.sasp_viewer_window.destroyed.disconnect(self._on_sasp_closed)
|
|
1622
|
+
except Exception:
|
|
1623
|
+
pass
|
|
1624
|
+
try:
|
|
1625
|
+
self.sasp_viewer_window.close()
|
|
1626
|
+
except Exception:
|
|
1627
|
+
pass
|
|
1628
|
+
self.sasp_viewer_window = None
|
|
1629
|
+
except Exception:
|
|
1630
|
+
pass
|
|
1631
|
+
|
|
1632
|
+
# 2) Disconnect any long-lived external signals (add these if/when used)
|
|
1633
|
+
# Example patterns:
|
|
1634
|
+
try:
|
|
1635
|
+
self.doc_manager.activeDocumentChanged.disconnect(self._on_active_doc_changed)
|
|
1636
|
+
except Exception:
|
|
1637
|
+
pass
|
|
1638
|
+
try:
|
|
1639
|
+
self.main_win.currentDocumentChanged.disconnect(self._on_active_doc_changed)
|
|
1640
|
+
except Exception:
|
|
1641
|
+
pass
|
|
1642
|
+
|
|
1643
|
+
# 3) Release large caches/refs (important since dialog may not be deleted)
|
|
1644
|
+
try:
|
|
1645
|
+
self.current_image = None
|
|
1646
|
+
self.current_header = None
|
|
1647
|
+
self.star_list = []
|
|
1648
|
+
self._last_matched = []
|
|
1649
|
+
if hasattr(self, "wcs"):
|
|
1650
|
+
self.wcs = None
|
|
1651
|
+
if hasattr(self, "wcs_header"):
|
|
1652
|
+
self.wcs_header = None
|
|
1653
|
+
except Exception:
|
|
1654
|
+
pass
|
|
1655
|
+
|
|
1656
|
+
# 4) Matplotlib cleanup
|
|
1657
|
+
try:
|
|
1658
|
+
if getattr(self, "figure", None) is not None:
|
|
1659
|
+
self.figure.clf()
|
|
1660
|
+
if getattr(self, "canvas", None) is not None:
|
|
1661
|
+
self.canvas.setVisible(False)
|
|
1662
|
+
self.canvas.draw_idle()
|
|
1663
|
+
except Exception:
|
|
1664
|
+
pass
|
|
1665
|
+
|
|
1666
|
+
|
|
1457
1667
|
def _on_sasp_closed(self, _=None):
|
|
1458
1668
|
# Called when the SaspViewer window is destroyed
|
|
1459
1669
|
self.sasp_viewer_window = None
|
|
1670
|
+
self._cleanup()
|
|
1460
1671
|
|
|
1461
1672
|
def closeEvent(self, event):
|
|
1673
|
+
self._cleanup()
|
|
1462
1674
|
super().closeEvent(event)
|
|
1463
1675
|
|
|
1464
1676
|
|