setiastrosuitepro 1.6.4__py3-none-any.whl → 1.7.1.post2__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/TextureClarity.svg +56 -0
- 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/narrowbandnormalization.png +0 -0
- setiastro/images/new_moon.png +0 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/images/planetarystacker.png +0 -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 +364 -33
- setiastro/saspro/aberration_ai_preset.py +29 -3
- setiastro/saspro/acv_exporter.py +379 -0
- setiastro/saspro/add_stars.py +33 -6
- setiastro/saspro/astrospike_python.py +45 -3
- setiastro/saspro/backgroundneutral.py +108 -40
- setiastro/saspro/blemish_blaster.py +4 -1
- setiastro/saspro/blink_comparator_pro.py +150 -55
- 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 +181 -64
- setiastro/saspro/curves_preset.py +249 -47
- 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 +706 -264
- setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
- setiastro/saspro/gui/mixins/file_mixin.py +35 -16
- setiastro/saspro/gui/mixins/menu_mixin.py +35 -1
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +499 -24
- 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 +184 -8
- setiastro/saspro/image_combine.py +4 -0
- setiastro/saspro/image_peeker_pro.py +4 -0
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +1345 -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 +68 -48
- setiastro/saspro/legacy/xisf.py +240 -98
- setiastro/saspro/live_stacking.py +203 -82
- 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 +81 -29
- setiastro/saspro/narrowband_normalization.py +1618 -0
- 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/remove_green.py +1 -1
- setiastro/saspro/resources.py +73 -0
- setiastro/saspro/rgbalign.py +460 -12
- setiastro/saspro/selective_color.py +4 -1
- setiastro/saspro/ser_stack_config.py +82 -0
- setiastro/saspro/ser_stacker.py +2321 -0
- setiastro/saspro/ser_stacker_dialog.py +1838 -0
- setiastro/saspro/ser_tracking.py +206 -0
- setiastro/saspro/serviewer.py +1625 -0
- setiastro/saspro/sfcc.py +662 -216
- setiastro/saspro/shortcuts.py +171 -33
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1347 -485
- 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 +892 -129
- setiastro/saspro/subwindow.py +787 -363
- setiastro/saspro/supernovaasteroidhunter.py +1 -1
- setiastro/saspro/texture_clarity.py +593 -0
- 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 +209 -111
- 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.7.1.post2.dist-info}/METADATA +4 -2
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/RECORD +132 -87
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/license.txt +0 -0
setiastro/saspro/rgbalign.py
CHANGED
|
@@ -4,11 +4,13 @@ from __future__ import annotations
|
|
|
4
4
|
import os
|
|
5
5
|
import numpy as np
|
|
6
6
|
|
|
7
|
-
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
|
7
|
+
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QEvent, QTimer
|
|
8
8
|
from PyQt6.QtWidgets import (
|
|
9
9
|
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QApplication,
|
|
10
|
-
QComboBox, QCheckBox, QMessageBox, QProgressBar, QPlainTextEdit,
|
|
10
|
+
QComboBox, QCheckBox, QMessageBox, QProgressBar, QPlainTextEdit,
|
|
11
|
+
QSpinBox, QGridLayout, QWidget
|
|
11
12
|
)
|
|
13
|
+
from PyQt6.QtGui import QImage, QPixmap, QMouseEvent, QCursor
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
import astroalign
|
|
@@ -24,6 +26,54 @@ except Exception:
|
|
|
24
26
|
PolynomialTransform = None
|
|
25
27
|
|
|
26
28
|
|
|
29
|
+
def _translate_channel(ch: np.ndarray, dx: float, dy: float) -> np.ndarray:
|
|
30
|
+
if cv2 is None:
|
|
31
|
+
return ch
|
|
32
|
+
H, W = ch.shape[:2]
|
|
33
|
+
A = np.array([[1, 0, dx],
|
|
34
|
+
[0, 1, dy]], dtype=np.float32)
|
|
35
|
+
return cv2.warpAffine(
|
|
36
|
+
ch, A, (W, H),
|
|
37
|
+
flags=cv2.INTER_LANCZOS4,
|
|
38
|
+
borderMode=cv2.BORDER_CONSTANT,
|
|
39
|
+
borderValue=0,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def _planet_centroid(ch: np.ndarray):
|
|
43
|
+
"""Return (cx, cy, area) of dominant planet blob, or None."""
|
|
44
|
+
if cv2 is None:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
img = ch.astype(np.float32, copy=False)
|
|
48
|
+
p1 = float(np.percentile(img, 1.0))
|
|
49
|
+
p99 = float(np.percentile(img, 99.5))
|
|
50
|
+
if p99 <= p1:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
scaled = (img - p1) * (255.0 / (p99 - p1))
|
|
54
|
+
scaled = np.clip(scaled, 0, 255).astype(np.uint8)
|
|
55
|
+
scaled = cv2.GaussianBlur(scaled, (0, 0), 1.2)
|
|
56
|
+
|
|
57
|
+
_, bw = cv2.threshold(scaled, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
|
58
|
+
|
|
59
|
+
k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
|
|
60
|
+
bw = cv2.morphologyEx(bw, cv2.MORPH_OPEN, k, iterations=1)
|
|
61
|
+
bw = cv2.morphologyEx(bw, cv2.MORPH_CLOSE, k, iterations=2)
|
|
62
|
+
|
|
63
|
+
num, labels, stats, cents = cv2.connectedComponentsWithStats(bw, connectivity=8)
|
|
64
|
+
if num <= 1:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
areas = stats[1:, cv2.CC_STAT_AREA]
|
|
68
|
+
j = int(np.argmax(areas)) + 1
|
|
69
|
+
area = float(stats[j, cv2.CC_STAT_AREA])
|
|
70
|
+
if area < 200:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
cx, cy = cents[j]
|
|
74
|
+
return (float(cx), float(cy), area)
|
|
75
|
+
|
|
76
|
+
|
|
27
77
|
# ─────────────────────────────────────────────────────────────────────
|
|
28
78
|
# Worker
|
|
29
79
|
# ─────────────────────────────────────────────────────────────────────
|
|
@@ -160,11 +210,83 @@ class RGBAlignWorker(QThread):
|
|
|
160
210
|
except Exception as e:
|
|
161
211
|
self.failed.emit(str(e))
|
|
162
212
|
|
|
213
|
+
def _planet_centroid(self, ch: np.ndarray):
|
|
214
|
+
"""
|
|
215
|
+
Return (cx, cy, area) in pixel coordinates for the dominant planet blob.
|
|
216
|
+
Robust for uint8/uint16/float images.
|
|
217
|
+
"""
|
|
218
|
+
if cv2 is None:
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
img = ch.astype(np.float32, copy=False)
|
|
222
|
+
|
|
223
|
+
# Normalize to 0..255 for thresholding (robust percentile scaling)
|
|
224
|
+
p1 = float(np.percentile(img, 1.0))
|
|
225
|
+
p99 = float(np.percentile(img, 99.5))
|
|
226
|
+
if p99 <= p1:
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
scaled = (img - p1) * (255.0 / (p99 - p1))
|
|
230
|
+
scaled = np.clip(scaled, 0, 255).astype(np.uint8)
|
|
231
|
+
|
|
232
|
+
# Blur to suppress banding/noise
|
|
233
|
+
scaled = cv2.GaussianBlur(scaled, (0, 0), 1.2)
|
|
234
|
+
|
|
235
|
+
# Otsu threshold to separate planet from background
|
|
236
|
+
_, bw = cv2.threshold(scaled, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
|
237
|
+
|
|
238
|
+
# Clean up small junk / fill holes a bit
|
|
239
|
+
k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
|
|
240
|
+
bw = cv2.morphologyEx(bw, cv2.MORPH_OPEN, k, iterations=1)
|
|
241
|
+
bw = cv2.morphologyEx(bw, cv2.MORPH_CLOSE, k, iterations=2)
|
|
242
|
+
|
|
243
|
+
# Largest connected component = planet
|
|
244
|
+
num, labels, stats, cents = cv2.connectedComponentsWithStats(bw, connectivity=8)
|
|
245
|
+
if num <= 1:
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
# stats: [label, x, y, w, h, area] but area is stats[:, cv2.CC_STAT_AREA]
|
|
249
|
+
areas = stats[1:, cv2.CC_STAT_AREA]
|
|
250
|
+
j = int(np.argmax(areas)) + 1
|
|
251
|
+
area = float(stats[j, cv2.CC_STAT_AREA])
|
|
252
|
+
if area < 200: # too tiny = probably noise
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
cx, cy = cents[j]
|
|
256
|
+
return (float(cx), float(cy), area)
|
|
257
|
+
|
|
258
|
+
def _estimate_translation_by_centroid(self, src: np.ndarray, ref: np.ndarray):
|
|
259
|
+
c_src = self._planet_centroid(src)
|
|
260
|
+
c_ref = self._planet_centroid(ref)
|
|
261
|
+
if c_src is None or c_ref is None:
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
sx, sy, sa = c_src
|
|
265
|
+
rx, ry, ra = c_ref
|
|
266
|
+
|
|
267
|
+
dx = rx - sx
|
|
268
|
+
dy = ry - sy
|
|
269
|
+
return (dx, dy, (sx, sy, sa), (rx, ry, ra))
|
|
270
|
+
|
|
163
271
|
|
|
164
272
|
# ───── helpers (basically mini versions of your big star alignment logic) ─────
|
|
165
273
|
def _estimate_transform(self, src: np.ndarray, ref: np.ndarray, model: str):
|
|
166
274
|
H, W = ref.shape[:2]
|
|
167
275
|
|
|
276
|
+
# ── Planet centroid ADC mode ─────────────────────────────
|
|
277
|
+
if model == "planet-centroid":
|
|
278
|
+
t = self._estimate_translation_by_centroid(src, ref)
|
|
279
|
+
if t is None:
|
|
280
|
+
# fall back to affine/astroalign if centroid fails
|
|
281
|
+
# (or you can hard-fail here if you prefer)
|
|
282
|
+
pass
|
|
283
|
+
else:
|
|
284
|
+
dx, dy, src_info, ref_info = t
|
|
285
|
+
# store "pairs" as tiny diagnostic info
|
|
286
|
+
src_xy = np.array([[src_info[0], src_info[1]]], dtype=np.float32)
|
|
287
|
+
dst_xy = np.array([[ref_info[0], ref_info[1]]], dtype=np.float32)
|
|
288
|
+
X = (float(dx), float(dy))
|
|
289
|
+
return ("translate", X, (src_xy, dst_xy))
|
|
168
290
|
# ── 0) edge-only, SEP-based path ─────────────────────────────
|
|
169
291
|
if model == "edge-sep":
|
|
170
292
|
src_xy, dst_xy = self._pair_edge_points(src, ref, (H, W))
|
|
@@ -323,6 +445,17 @@ class RGBAlignWorker(QThread):
|
|
|
323
445
|
|
|
324
446
|
def _warp_channel(self, ch: np.ndarray, kind: str, X, ref_shape):
|
|
325
447
|
H, W = ref_shape[:2]
|
|
448
|
+
|
|
449
|
+
if kind == "translate":
|
|
450
|
+
dx, dy = X
|
|
451
|
+
A = np.array([[1, 0, dx],
|
|
452
|
+
[0, 1, dy]], dtype=np.float32)
|
|
453
|
+
return cv2.warpAffine(
|
|
454
|
+
ch, A, (W, H),
|
|
455
|
+
flags=cv2.INTER_LANCZOS4,
|
|
456
|
+
borderMode=cv2.BORDER_CONSTANT,
|
|
457
|
+
borderValue=0
|
|
458
|
+
)
|
|
326
459
|
if kind == "affine":
|
|
327
460
|
# Just assume cv2 is available (standard dependency) for perf
|
|
328
461
|
A = np.asarray(X, dtype=np.float32).reshape(2, 3)
|
|
@@ -350,6 +483,10 @@ class RGBAlignDialog(QDialog):
|
|
|
350
483
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
351
484
|
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
352
485
|
self.setModal(False)
|
|
486
|
+
try:
|
|
487
|
+
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
488
|
+
except Exception:
|
|
489
|
+
pass # older PyQt6 versions
|
|
353
490
|
self.parent = parent
|
|
354
491
|
# document could be a view; try to unwrap
|
|
355
492
|
self.doc_view = document
|
|
@@ -364,17 +501,28 @@ class RGBAlignDialog(QDialog):
|
|
|
364
501
|
hl.addWidget(QLabel(self.tr("Alignment model:")))
|
|
365
502
|
self.model_combo = QComboBox()
|
|
366
503
|
self.model_combo.addItems([
|
|
504
|
+
"Planet Centroid (ADC)", # NEW
|
|
367
505
|
"EDGE", # ← first, new default
|
|
368
506
|
"Homography",
|
|
369
507
|
"Affine",
|
|
370
508
|
"Poly 3",
|
|
371
509
|
"Poly 4",
|
|
372
510
|
])
|
|
373
|
-
self.model_combo.setCurrentIndex(
|
|
374
|
-
|
|
375
|
-
# tooltips for each mode
|
|
511
|
+
self.model_combo.setCurrentIndex(1)
|
|
376
512
|
self.model_combo.setItemData(
|
|
377
513
|
0,
|
|
514
|
+
(
|
|
515
|
+
"Planet Centroid (Atmospheric Dispersion)\n"
|
|
516
|
+
"• Find planet disk in each channel\n"
|
|
517
|
+
"• Compute centroid (moments)\n"
|
|
518
|
+
"• Shift R and B so their centroids match G\n"
|
|
519
|
+
"• Translation only (no warp), ideal for planets"
|
|
520
|
+
),
|
|
521
|
+
Qt.ItemDataRole.ToolTipRole,
|
|
522
|
+
)
|
|
523
|
+
# tooltips for each mode
|
|
524
|
+
self.model_combo.setItemData(
|
|
525
|
+
1,
|
|
378
526
|
(
|
|
379
527
|
"EDGE (Edge-Detected Guided Estimator)\n"
|
|
380
528
|
"• Detect stars in both channels with SEP\n"
|
|
@@ -386,22 +534,22 @@ class RGBAlignDialog(QDialog):
|
|
|
386
534
|
Qt.ItemDataRole.ToolTipRole,
|
|
387
535
|
)
|
|
388
536
|
self.model_combo.setItemData(
|
|
389
|
-
|
|
537
|
+
2,
|
|
390
538
|
"Standard homography using astroalign matches (good general-purpose choice).",
|
|
391
539
|
Qt.ItemDataRole.ToolTipRole,
|
|
392
540
|
)
|
|
393
541
|
self.model_combo.setItemData(
|
|
394
|
-
|
|
542
|
+
3,
|
|
395
543
|
"Affine (shift + scale + rotate + shear). Good when channels are mostly parallel.",
|
|
396
544
|
Qt.ItemDataRole.ToolTipRole,
|
|
397
545
|
)
|
|
398
546
|
self.model_combo.setItemData(
|
|
399
|
-
|
|
547
|
+
4,
|
|
400
548
|
"Polynomial (order 3). Use when you have mild field distortion.",
|
|
401
549
|
Qt.ItemDataRole.ToolTipRole,
|
|
402
550
|
)
|
|
403
551
|
self.model_combo.setItemData(
|
|
404
|
-
|
|
552
|
+
5,
|
|
405
553
|
"Polynomial (order 4). Use for stronger distortion, but needs more/better matches.",
|
|
406
554
|
Qt.ItemDataRole.ToolTipRole,
|
|
407
555
|
)
|
|
@@ -450,8 +598,15 @@ class RGBAlignDialog(QDialog):
|
|
|
450
598
|
|
|
451
599
|
btns = QHBoxLayout()
|
|
452
600
|
self.btn_run = QPushButton(self.tr("Align"))
|
|
601
|
+
|
|
602
|
+
self.btn_manual = QPushButton(self.tr("Manual…"))
|
|
603
|
+
self.btn_manual.setToolTip("Manual planetary RGB alignment (nudge channels by 1 px with preview).")
|
|
604
|
+
|
|
605
|
+
self.btn_manual.clicked.connect(self._open_manual)
|
|
606
|
+
|
|
453
607
|
self.btn_close = QPushButton(self.tr("Close"))
|
|
454
608
|
btns.addWidget(self.btn_run)
|
|
609
|
+
btns.addWidget(self.btn_manual)
|
|
455
610
|
btns.addWidget(self.btn_close)
|
|
456
611
|
lay.addLayout(btns)
|
|
457
612
|
|
|
@@ -460,6 +615,56 @@ class RGBAlignDialog(QDialog):
|
|
|
460
615
|
|
|
461
616
|
self.worker: RGBAlignWorker | None = None
|
|
462
617
|
|
|
618
|
+
def _open_manual(self):
|
|
619
|
+
if self.image is None or self.image.ndim != 3 or self.image.shape[2] < 3:
|
|
620
|
+
QMessageBox.warning(self, "RGB Align", "Image must be RGB (3 channels).")
|
|
621
|
+
return
|
|
622
|
+
|
|
623
|
+
dlg = RGBAlignManualDialog(self, self.image, planet_roi=("planet" in self.model_combo.currentText().lower()))
|
|
624
|
+
|
|
625
|
+
if dlg.exec() != QDialog.DialogCode.Accepted:
|
|
626
|
+
return
|
|
627
|
+
|
|
628
|
+
rdx, rdy, bdx, bdy = dlg.result_offsets()
|
|
629
|
+
self.summary_box.setPlainText(
|
|
630
|
+
"[Manual Planetary]\n"
|
|
631
|
+
f"R shift: dx={rdx}, dy={rdy}\n"
|
|
632
|
+
f"B shift: dx={bdx}, dy={bdy}\n"
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
# apply full-res
|
|
636
|
+
img = self.image
|
|
637
|
+
R = img[..., 0].astype(np.float32, copy=False)
|
|
638
|
+
G = img[..., 1].astype(np.float32, copy=False)
|
|
639
|
+
B = img[..., 2].astype(np.float32, copy=False)
|
|
640
|
+
|
|
641
|
+
R2 = _translate_channel(R, rdx, rdy)
|
|
642
|
+
B2 = _translate_channel(B, bdx, bdy)
|
|
643
|
+
|
|
644
|
+
out = np.stack([R2, G, B2], axis=2)
|
|
645
|
+
if out.dtype != img.dtype:
|
|
646
|
+
out = out.astype(img.dtype, copy=False)
|
|
647
|
+
|
|
648
|
+
# same create-new-doc logic you already use
|
|
649
|
+
try:
|
|
650
|
+
if self.chk_new_doc.isChecked():
|
|
651
|
+
dm = getattr(self.parent, "docman", None)
|
|
652
|
+
if dm is not None:
|
|
653
|
+
dm.open_array(out, {"display_name": "RGB Aligned (Manual)"}, title="RGB Aligned (Manual)")
|
|
654
|
+
else:
|
|
655
|
+
if hasattr(self.doc, "apply_edit"):
|
|
656
|
+
self.doc.apply_edit(out, {"step_name": "RGB Align (Manual)"}, step_name="RGB Align (Manual)")
|
|
657
|
+
else:
|
|
658
|
+
self.doc.image = out
|
|
659
|
+
else:
|
|
660
|
+
if hasattr(self.doc, "apply_edit"):
|
|
661
|
+
self.doc.apply_edit(out, {"step_name": "RGB Align (Manual)"}, step_name="RGB Align (Manual)")
|
|
662
|
+
else:
|
|
663
|
+
self.doc.image = out
|
|
664
|
+
except Exception as e:
|
|
665
|
+
QMessageBox.warning(self, "RGB Align", f"Manual aligned image created, but applying failed:\n{e}")
|
|
666
|
+
|
|
667
|
+
|
|
463
668
|
def _trial_sep_detect(self):
|
|
464
669
|
if self.image is None:
|
|
465
670
|
QMessageBox.warning(self, "RGB Align", "No image loaded.")
|
|
@@ -525,6 +730,8 @@ class RGBAlignDialog(QDialog):
|
|
|
525
730
|
|
|
526
731
|
def _selected_model(self) -> str:
|
|
527
732
|
txt = self.model_combo.currentText().lower()
|
|
733
|
+
if "planet" in txt or "centroid" in txt or "adc" in txt:
|
|
734
|
+
return "planet-centroid"
|
|
528
735
|
if "edge" in txt:
|
|
529
736
|
return "edge-sep"
|
|
530
737
|
if "affine" in txt:
|
|
@@ -535,7 +742,7 @@ class RGBAlignDialog(QDialog):
|
|
|
535
742
|
return "poly4"
|
|
536
743
|
if "homography" in txt:
|
|
537
744
|
return "homography"
|
|
538
|
-
return "edge-sep"
|
|
745
|
+
return "edge-sep"
|
|
539
746
|
|
|
540
747
|
# slots
|
|
541
748
|
def _on_worker_progress(self, pct: int, msg: str):
|
|
@@ -589,7 +796,10 @@ class RGBAlignDialog(QDialog):
|
|
|
589
796
|
if w.r_pairs is not None and len(w.r_pairs) == 2:
|
|
590
797
|
summary_lines.append(_spread_stats(w.r_pairs[1], (h_img, w_img)))
|
|
591
798
|
summary_lines.append(f" model: {kind}")
|
|
592
|
-
if kind == "
|
|
799
|
+
if kind == "translate":
|
|
800
|
+
dx, dy = X
|
|
801
|
+
summary_lines.append(f" shift: dx={dx:.3f}, dy={dy:.3f}")
|
|
802
|
+
elif kind == "affine":
|
|
593
803
|
A = np.asarray(X, dtype=float).reshape(2, 3)
|
|
594
804
|
M = np.vstack([A, [0, 0, 1]])
|
|
595
805
|
summary_lines.append(_fmt_mat(M))
|
|
@@ -607,7 +817,10 @@ class RGBAlignDialog(QDialog):
|
|
|
607
817
|
if w.b_pairs is not None and len(w.b_pairs) == 2:
|
|
608
818
|
summary_lines.append(_spread_stats(w.b_pairs[1], (h_img, w_img)))
|
|
609
819
|
summary_lines.append(f" model: {kind}")
|
|
610
|
-
if kind == "
|
|
820
|
+
if kind == "translate":
|
|
821
|
+
dx, dy = X
|
|
822
|
+
summary_lines.append(f" shift: dx={dx:.3f}, dy={dy:.3f}")
|
|
823
|
+
elif kind == "affine":
|
|
611
824
|
A = np.asarray(X, dtype=float).reshape(2, 3)
|
|
612
825
|
M = np.vstack([A, [0, 0, 1]])
|
|
613
826
|
summary_lines.append(_fmt_mat(M))
|
|
@@ -721,3 +934,238 @@ def run_rgb_align_headless(main_window, document, preset: dict | None = None):
|
|
|
721
934
|
|
|
722
935
|
if callable(sb):
|
|
723
936
|
sb().showMessage("RGB Align done.", 3000)
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
class RGBAlignManualDialog(QDialog):
|
|
940
|
+
"""
|
|
941
|
+
Manual channel translation for ADC / dispersion.
|
|
942
|
+
Adjust R and B offsets relative to G with 1px steps and live preview.
|
|
943
|
+
"""
|
|
944
|
+
def __init__(self, parent=None, image: np.ndarray | None = None, planet_roi: bool = False):
|
|
945
|
+
super().__init__(parent)
|
|
946
|
+
self.setWindowTitle("RGB Align — Manual (Planetary)")
|
|
947
|
+
self.setModal(True)
|
|
948
|
+
|
|
949
|
+
self.image = np.asarray(image) if image is not None else None
|
|
950
|
+
self._timer = None
|
|
951
|
+
self._result = None # (r_dx, r_dy, b_dx, b_dy)
|
|
952
|
+
|
|
953
|
+
# offsets
|
|
954
|
+
self.r_dx = 0
|
|
955
|
+
self.r_dy = 0
|
|
956
|
+
self.b_dx = 0
|
|
957
|
+
self.b_dy = 0
|
|
958
|
+
|
|
959
|
+
lay = QVBoxLayout(self)
|
|
960
|
+
|
|
961
|
+
self.lbl = QLabel("Nudge Red and Blue so they line up with Green (1 px steps).")
|
|
962
|
+
lay.addWidget(self.lbl)
|
|
963
|
+
|
|
964
|
+
# preview label
|
|
965
|
+
self.preview = QLabel()
|
|
966
|
+
self.preview.setMinimumSize(420, 320)
|
|
967
|
+
self.preview.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
968
|
+
self.preview.setStyleSheet("background:#111; border:1px solid #333;")
|
|
969
|
+
lay.addWidget(self.preview)
|
|
970
|
+
|
|
971
|
+
# controls grid
|
|
972
|
+
row = QHBoxLayout()
|
|
973
|
+
lay.addLayout(row)
|
|
974
|
+
|
|
975
|
+
# controls
|
|
976
|
+
row = QHBoxLayout()
|
|
977
|
+
lay.addLayout(row)
|
|
978
|
+
|
|
979
|
+
self.red_pad = OffsetPad("Red offset (relative to Green):")
|
|
980
|
+
self.blue_pad = OffsetPad("Blue offset (relative to Green):")
|
|
981
|
+
|
|
982
|
+
self.red_pad.changed.connect(self._on_change)
|
|
983
|
+
self.blue_pad.changed.connect(self._on_change)
|
|
984
|
+
|
|
985
|
+
row.addWidget(self.red_pad)
|
|
986
|
+
row.addWidget(self.blue_pad)
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
# buttons
|
|
990
|
+
btns = QHBoxLayout()
|
|
991
|
+
self.btn_reset = QPushButton("Reset")
|
|
992
|
+
self.btn_apply = QPushButton("Apply")
|
|
993
|
+
self.btn_close = QPushButton("Close")
|
|
994
|
+
btns.addWidget(self.btn_reset)
|
|
995
|
+
btns.addStretch(1)
|
|
996
|
+
btns.addWidget(self.btn_apply)
|
|
997
|
+
btns.addWidget(self.btn_close)
|
|
998
|
+
lay.addLayout(btns)
|
|
999
|
+
|
|
1000
|
+
self.btn_reset.clicked.connect(self._reset)
|
|
1001
|
+
self.btn_apply.clicked.connect(self._apply)
|
|
1002
|
+
self.btn_close.clicked.connect(self.reject)
|
|
1003
|
+
|
|
1004
|
+
# build a reasonable ROI for preview (center crop)
|
|
1005
|
+
self._roi = None
|
|
1006
|
+
if self.image is not None and self.image.ndim == 3 and self.image.shape[2] >= 3:
|
|
1007
|
+
H, W = self.image.shape[:2]
|
|
1008
|
+
|
|
1009
|
+
if planet_roi:
|
|
1010
|
+
# Use green channel to find the planet (most stable)
|
|
1011
|
+
c = _planet_centroid(self.image[..., 1])
|
|
1012
|
+
if c is not None:
|
|
1013
|
+
cx, cy, area = c
|
|
1014
|
+
# Estimate radius from area, then choose a padded ROI
|
|
1015
|
+
r = max(32.0, np.sqrt(area / np.pi))
|
|
1016
|
+
s = int(np.clip(r * 3.2, 160, 900)) # ROI size = ~3.2x radius
|
|
1017
|
+
cx_i, cy_i = int(round(cx)), int(round(cy))
|
|
1018
|
+
else:
|
|
1019
|
+
cx_i, cy_i = W // 2, H // 2
|
|
1020
|
+
s = int(np.clip(min(H, W) * 0.45, 160, 900))
|
|
1021
|
+
else:
|
|
1022
|
+
cx_i, cy_i = W // 2, H // 2
|
|
1023
|
+
s = int(np.clip(min(H, W) * 0.45, 160, 900))
|
|
1024
|
+
|
|
1025
|
+
x0 = max(0, cx_i - s // 2)
|
|
1026
|
+
y0 = max(0, cy_i - s // 2)
|
|
1027
|
+
x1 = min(W, x0 + s)
|
|
1028
|
+
y1 = min(H, y0 + s)
|
|
1029
|
+
self._roi = (x0, y0, x1, y1)
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
# debounce timer
|
|
1033
|
+
from PyQt6.QtCore import QTimer
|
|
1034
|
+
self._timer = QTimer(self)
|
|
1035
|
+
self._timer.setSingleShot(True)
|
|
1036
|
+
self._timer.timeout.connect(self._render_preview)
|
|
1037
|
+
if self._roi is not None:
|
|
1038
|
+
x0,y0,x1,y1 = self._roi
|
|
1039
|
+
self.lbl.setText(self.lbl.text() + f"\nPreview ROI: x={x0}:{x1}, y={y0}:{y1}")
|
|
1040
|
+
self._render_preview()
|
|
1041
|
+
|
|
1042
|
+
def result_offsets(self):
|
|
1043
|
+
return self._result
|
|
1044
|
+
|
|
1045
|
+
def _on_change(self, *_):
|
|
1046
|
+
self._timer.start(60)
|
|
1047
|
+
|
|
1048
|
+
def _reset(self):
|
|
1049
|
+
self.red_pad.reset()
|
|
1050
|
+
self.blue_pad.reset()
|
|
1051
|
+
|
|
1052
|
+
def _apply(self):
|
|
1053
|
+
rdx, rdy = self.red_pad.offsets()
|
|
1054
|
+
bdx, bdy = self.blue_pad.offsets()
|
|
1055
|
+
self._result = (int(rdx), int(rdy), int(bdx), int(bdy))
|
|
1056
|
+
self.accept()
|
|
1057
|
+
|
|
1058
|
+
def _render_preview(self):
|
|
1059
|
+
if self.image is None or self.image.ndim != 3 or self.image.shape[2] < 3:
|
|
1060
|
+
self.preview.setText("No RGB image.")
|
|
1061
|
+
return
|
|
1062
|
+
|
|
1063
|
+
x0, y0, x1, y1 = self._roi if self._roi is not None else (0, 0, self.image.shape[1], self.image.shape[0])
|
|
1064
|
+
crop = self.image[y0:y1, x0:x1, :3]
|
|
1065
|
+
|
|
1066
|
+
R = crop[..., 0].astype(np.float32, copy=False)
|
|
1067
|
+
G = crop[..., 1].astype(np.float32, copy=False)
|
|
1068
|
+
B = crop[..., 2].astype(np.float32, copy=False)
|
|
1069
|
+
|
|
1070
|
+
rdx, rdy = self.red_pad.offsets()
|
|
1071
|
+
bdx, bdy = self.blue_pad.offsets()
|
|
1072
|
+
|
|
1073
|
+
R2 = _translate_channel(R, rdx, rdy)
|
|
1074
|
+
B2 = _translate_channel(B, bdx, bdy)
|
|
1075
|
+
|
|
1076
|
+
out = np.stack([R2, G, B2], axis=2)
|
|
1077
|
+
|
|
1078
|
+
# display: robust scale to uint8
|
|
1079
|
+
out8 = self._to_u8_preview(out)
|
|
1080
|
+
qimg = QImage(out8.data, out8.shape[1], out8.shape[0], out8.strides[0], QImage.Format.Format_RGB888)
|
|
1081
|
+
pix = QPixmap.fromImage(qimg).scaled(
|
|
1082
|
+
self.preview.size(),
|
|
1083
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
|
1084
|
+
Qt.TransformationMode.SmoothTransformation,
|
|
1085
|
+
)
|
|
1086
|
+
self.preview.setPixmap(pix)
|
|
1087
|
+
|
|
1088
|
+
@staticmethod
|
|
1089
|
+
def _to_u8_preview(rgb: np.ndarray) -> np.ndarray:
|
|
1090
|
+
x = rgb.astype(np.float32, copy=False)
|
|
1091
|
+
# percentile stretch per-channel, simple + robust
|
|
1092
|
+
out = np.empty_like(x, dtype=np.uint8)
|
|
1093
|
+
for c in range(3):
|
|
1094
|
+
ch = x[..., c]
|
|
1095
|
+
p1 = float(np.percentile(ch, 1.0))
|
|
1096
|
+
p99 = float(np.percentile(ch, 99.5))
|
|
1097
|
+
if p99 <= p1:
|
|
1098
|
+
out[..., c] = 0
|
|
1099
|
+
else:
|
|
1100
|
+
y = (ch - p1) * (255.0 / (p99 - p1))
|
|
1101
|
+
out[..., c] = np.clip(y, 0, 255).astype(np.uint8)
|
|
1102
|
+
return out
|
|
1103
|
+
|
|
1104
|
+
class OffsetPad(QWidget):
|
|
1105
|
+
changed = pyqtSignal(int, int) # dx, dy
|
|
1106
|
+
|
|
1107
|
+
def __init__(self, title: str, parent=None):
|
|
1108
|
+
super().__init__(parent)
|
|
1109
|
+
self._dx = 0
|
|
1110
|
+
self._dy = 0
|
|
1111
|
+
|
|
1112
|
+
outer = QVBoxLayout(self)
|
|
1113
|
+
outer.setContentsMargins(0, 0, 0, 0)
|
|
1114
|
+
outer.setSpacing(4)
|
|
1115
|
+
|
|
1116
|
+
hdr = QLabel(title)
|
|
1117
|
+
hdr.setStyleSheet("font-weight:600;")
|
|
1118
|
+
outer.addWidget(hdr)
|
|
1119
|
+
|
|
1120
|
+
self.lbl = QLabel("dx=0 dy=0")
|
|
1121
|
+
self.lbl.setStyleSheet("color:#bbb;")
|
|
1122
|
+
outer.addWidget(self.lbl)
|
|
1123
|
+
|
|
1124
|
+
grid = QGridLayout()
|
|
1125
|
+
grid.setSpacing(2)
|
|
1126
|
+
|
|
1127
|
+
self.btn_up = QPushButton("↑")
|
|
1128
|
+
self.btn_dn = QPushButton("↓")
|
|
1129
|
+
self.btn_lt = QPushButton("←")
|
|
1130
|
+
self.btn_rt = QPushButton("→")
|
|
1131
|
+
self.btn_mid = QPushButton("⟲") # reset
|
|
1132
|
+
|
|
1133
|
+
for b in (self.btn_up, self.btn_dn, self.btn_lt, self.btn_rt, self.btn_mid):
|
|
1134
|
+
b.setFixedSize(34, 34)
|
|
1135
|
+
b.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
|
1136
|
+
|
|
1137
|
+
grid.addWidget(self.btn_up, 0, 1)
|
|
1138
|
+
grid.addWidget(self.btn_lt, 1, 0)
|
|
1139
|
+
grid.addWidget(self.btn_mid, 1, 1)
|
|
1140
|
+
grid.addWidget(self.btn_rt, 1, 2)
|
|
1141
|
+
grid.addWidget(self.btn_dn, 2, 1)
|
|
1142
|
+
|
|
1143
|
+
outer.addLayout(grid)
|
|
1144
|
+
|
|
1145
|
+
# connections
|
|
1146
|
+
self.btn_up.clicked.connect(lambda: self.nudge(0, -1))
|
|
1147
|
+
self.btn_dn.clicked.connect(lambda: self.nudge(0, +1))
|
|
1148
|
+
self.btn_lt.clicked.connect(lambda: self.nudge(-1, 0))
|
|
1149
|
+
self.btn_rt.clicked.connect(lambda: self.nudge(+1, 0))
|
|
1150
|
+
self.btn_mid.clicked.connect(self.reset)
|
|
1151
|
+
|
|
1152
|
+
def set_offsets(self, dx: int, dy: int):
|
|
1153
|
+
self._dx = int(dx)
|
|
1154
|
+
self._dy = int(dy)
|
|
1155
|
+
self._update_label()
|
|
1156
|
+
self.changed.emit(self._dx, self._dy)
|
|
1157
|
+
|
|
1158
|
+
def offsets(self):
|
|
1159
|
+
return (self._dx, self._dy)
|
|
1160
|
+
|
|
1161
|
+
def reset(self):
|
|
1162
|
+
self.set_offsets(0, 0)
|
|
1163
|
+
|
|
1164
|
+
def nudge(self, ddx: int, ddy: int, step: int = 1):
|
|
1165
|
+
self._dx += int(ddx) * int(step)
|
|
1166
|
+
self._dy += int(ddy) * int(step)
|
|
1167
|
+
self._update_label()
|
|
1168
|
+
self.changed.emit(self._dx, self._dy)
|
|
1169
|
+
|
|
1170
|
+
def _update_label(self):
|
|
1171
|
+
self.lbl.setText(f"dx={self._dx:+d} dy={self._dy:+d}")
|
|
@@ -458,7 +458,10 @@ class SelectiveColorCorrection(QDialog):
|
|
|
458
458
|
self.setWindowTitle(self.tr("Selective Color Correction"))
|
|
459
459
|
if window_icon:
|
|
460
460
|
self.setWindowIcon(window_icon)
|
|
461
|
-
|
|
461
|
+
try:
|
|
462
|
+
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
463
|
+
except Exception:
|
|
464
|
+
pass # older PyQt6 versions
|
|
462
465
|
self.docman = doc_manager
|
|
463
466
|
self.document = document
|
|
464
467
|
if self.document is None or getattr(self.document, "image", None) is None:
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# src/setiastro/saspro/ser_stack_config.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional, Tuple, Literal, Union, Sequence, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
import numpy as np
|
|
8
|
+
KeepMask = "np.ndarray"
|
|
9
|
+
else:
|
|
10
|
+
KeepMask = object
|
|
11
|
+
|
|
12
|
+
from setiastro.saspro.imageops.serloader import PlanetaryFrameSource
|
|
13
|
+
|
|
14
|
+
TrackMode = Literal["off", "planetary", "surface"]
|
|
15
|
+
PlanetarySource = Union[str, Sequence[str], PlanetaryFrameSource]
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class SERStackConfig:
|
|
19
|
+
source: PlanetarySource
|
|
20
|
+
roi: Optional[Tuple[int, int, int, int]] = None
|
|
21
|
+
track_mode: TrackMode = "planetary"
|
|
22
|
+
surface_anchor: Optional[Tuple[int, int, int, int]] = None
|
|
23
|
+
keep_percent: float = 20.0
|
|
24
|
+
bayer_pattern: Optional[str] = None
|
|
25
|
+
|
|
26
|
+
# AP / alignment
|
|
27
|
+
ap_size: int = 64
|
|
28
|
+
ap_spacing: int = 48
|
|
29
|
+
ap_min_mean: float = 0.03
|
|
30
|
+
ap_multiscale: bool = False
|
|
31
|
+
ssd_refine_bruteforce: bool = False
|
|
32
|
+
keep_mask: Optional[KeepMask] = None
|
|
33
|
+
|
|
34
|
+
# ✅ Drizzle
|
|
35
|
+
drizzle_scale: float = 1.0 # 1.0 = off, 1.5, 2.0
|
|
36
|
+
drizzle_pixfrac: float = 0.80 # "drop shrink" in output pixels (roughly)
|
|
37
|
+
drizzle_kernel: str = "gaussian" # "square" | "circle" | "gaussian"
|
|
38
|
+
drizzle_sigma: float = 0.0 # only used for gaussian; 0 => auto from pixfrac
|
|
39
|
+
|
|
40
|
+
def __init__(self, source: PlanetarySource, **kwargs):
|
|
41
|
+
# Allow deprecated/ignored kwargs without crashing
|
|
42
|
+
kwargs.pop("multipoint", None) # accept but ignore
|
|
43
|
+
|
|
44
|
+
self.source = source
|
|
45
|
+
self.roi = kwargs.pop("roi", None)
|
|
46
|
+
self.track_mode = kwargs.pop("track_mode", "planetary")
|
|
47
|
+
self.surface_anchor = kwargs.pop("surface_anchor", None)
|
|
48
|
+
self.keep_percent = float(kwargs.pop("keep_percent", 20.0))
|
|
49
|
+
self.bayer_pattern = kwargs.pop("bayer_pattern", None)
|
|
50
|
+
if isinstance(self.bayer_pattern, str):
|
|
51
|
+
s = self.bayer_pattern.strip().upper()
|
|
52
|
+
self.bayer_pattern = s if s in ("RGGB", "BGGR", "GRBG", "GBRG") else None
|
|
53
|
+
else:
|
|
54
|
+
self.bayer_pattern = None
|
|
55
|
+
self.ap_size = int(kwargs.pop("ap_size", 64))
|
|
56
|
+
self.ap_spacing = int(kwargs.pop("ap_spacing", 48))
|
|
57
|
+
self.ap_min_mean = float(kwargs.pop("ap_min_mean", 0.03))
|
|
58
|
+
self.ap_multiscale = bool(kwargs.pop("ap_multiscale", False))
|
|
59
|
+
self.ssd_refine_bruteforce = bool(kwargs.pop("ssd_refine_bruteforce", False))
|
|
60
|
+
self.keep_mask = kwargs.pop("keep_mask", None)
|
|
61
|
+
|
|
62
|
+
# ✅ NEW: Drizzle params
|
|
63
|
+
self.drizzle_scale = float(kwargs.pop("drizzle_scale", 1.0))
|
|
64
|
+
if self.drizzle_scale not in (1.0, 1.5, 2.0):
|
|
65
|
+
self.drizzle_scale = 1.0
|
|
66
|
+
|
|
67
|
+
self.drizzle_pixfrac = float(kwargs.pop("drizzle_pixfrac", 0.80))
|
|
68
|
+
self.drizzle_kernel = str(kwargs.pop("drizzle_kernel", "gaussian")).strip().lower()
|
|
69
|
+
self.drizzle_sigma = float(kwargs.pop("drizzle_sigma", 0.0))
|
|
70
|
+
|
|
71
|
+
# sanitize a bit
|
|
72
|
+
if self.drizzle_scale < 1.0:
|
|
73
|
+
self.drizzle_scale = 1.0
|
|
74
|
+
if self.drizzle_pixfrac <= 0.0:
|
|
75
|
+
self.drizzle_pixfrac = 0.01
|
|
76
|
+
if self.drizzle_kernel not in ("square", "circle", "gaussian"):
|
|
77
|
+
self.drizzle_kernel = "gaussian"
|
|
78
|
+
if self.drizzle_sigma < 0.0:
|
|
79
|
+
self.drizzle_sigma = 0.0
|
|
80
|
+
|
|
81
|
+
if kwargs:
|
|
82
|
+
raise TypeError(f"Unexpected config keys: {sorted(kwargs.keys())}")
|