setiastrosuitepro 1.6.10__py3-none-any.whl → 1.7.0.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.
- setiastro/images/colorwheel.svg +97 -0
- setiastro/images/narrowbandnormalization.png +0 -0
- setiastro/images/planetarystacker.png +0 -0
- setiastro/saspro/__main__.py +1 -1
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/aberration_ai.py +49 -11
- setiastro/saspro/aberration_ai_preset.py +29 -3
- setiastro/saspro/backgroundneutral.py +73 -33
- setiastro/saspro/blink_comparator_pro.py +116 -71
- setiastro/saspro/convo.py +9 -6
- setiastro/saspro/curve_editor_pro.py +72 -22
- setiastro/saspro/curves_preset.py +249 -47
- setiastro/saspro/doc_manager.py +178 -11
- setiastro/saspro/gui/main_window.py +305 -66
- 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 +32 -1
- setiastro/saspro/gui/mixins/toolbar_mixin.py +135 -11
- setiastro/saspro/histogram.py +179 -7
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +972 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
- setiastro/saspro/imageops/stretch.py +66 -15
- setiastro/saspro/legacy/numba_utils.py +25 -48
- setiastro/saspro/live_stacking.py +24 -4
- setiastro/saspro/multiscale_decomp.py +30 -17
- setiastro/saspro/narrowband_normalization.py +1618 -0
- setiastro/saspro/numba_utils.py +0 -55
- setiastro/saspro/ops/script_editor.py +5 -0
- setiastro/saspro/ops/scripts.py +119 -0
- setiastro/saspro/remove_green.py +1 -1
- setiastro/saspro/resources.py +4 -0
- setiastro/saspro/ser_stack_config.py +74 -0
- setiastro/saspro/ser_stacker.py +2310 -0
- setiastro/saspro/ser_stacker_dialog.py +1500 -0
- setiastro/saspro/ser_tracking.py +206 -0
- setiastro/saspro/serviewer.py +1258 -0
- setiastro/saspro/sfcc.py +602 -214
- setiastro/saspro/shortcuts.py +35 -16
- setiastro/saspro/stacking_suite.py +332 -87
- setiastro/saspro/star_alignment.py +243 -122
- setiastro/saspro/stat_stretch.py +220 -31
- setiastro/saspro/subwindow.py +2 -4
- setiastro/saspro/whitebalance.py +24 -0
- setiastro/saspro/widgets/resource_monitor.py +122 -74
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/METADATA +2 -2
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/RECORD +51 -40
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/licenses/license.txt +0 -0
setiastro/saspro/stat_stretch.py
CHANGED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from PyQt6.QtCore import Qt, QSize, QEvent
|
|
4
4
|
from PyQt6.QtWidgets import (
|
|
5
5
|
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLabel, QDoubleSpinBox,
|
|
6
|
-
QCheckBox, QPushButton, QScrollArea, QWidget, QMessageBox, QSlider, QToolBar, QToolButton, QComboBox
|
|
6
|
+
QCheckBox, QPushButton, QScrollArea, QWidget, QMessageBox, QSlider, QToolBar, QToolButton, QComboBox,QProgressBar, QApplication
|
|
7
7
|
)
|
|
8
8
|
from PyQt6.QtGui import QImage, QPixmap, QMouseEvent, QCursor
|
|
9
9
|
import numpy as np
|
|
@@ -243,6 +243,32 @@ class StatisticalStretchDialog(QDialog):
|
|
|
243
243
|
lr.addWidget(QLabel(self.tr("Mode:")))
|
|
244
244
|
lr.addWidget(self.cmb_luma, 1)
|
|
245
245
|
|
|
246
|
+
# --- Luma blend row (only meaningful when Luma-only is enabled) ---
|
|
247
|
+
self.luma_blend_row = QWidget()
|
|
248
|
+
lbr = QHBoxLayout(self.luma_blend_row)
|
|
249
|
+
lbr.setContentsMargins(0, 0, 0, 0)
|
|
250
|
+
lbr.setSpacing(8)
|
|
251
|
+
|
|
252
|
+
lbr.addWidget(QLabel(self.tr("Luma blend:")))
|
|
253
|
+
|
|
254
|
+
self.sld_luma_blend = QSlider(Qt.Orientation.Horizontal)
|
|
255
|
+
self.sld_luma_blend.setRange(0, 100) # 0=normal linked, 100=luma-only
|
|
256
|
+
self.sld_luma_blend.setValue(60) # nice default: “mostly luma” but tame
|
|
257
|
+
lbr.addWidget(self.sld_luma_blend, 1)
|
|
258
|
+
|
|
259
|
+
self.lbl_luma_blend = QLabel(f"{self.sld_luma_blend.value()/100:.2f}")
|
|
260
|
+
lbr.addWidget(self.lbl_luma_blend)
|
|
261
|
+
|
|
262
|
+
tip = self.tr(
|
|
263
|
+
"Blend between a normal linked RGB stretch (0.00) and a luminance-only stretch (1.00).\n"
|
|
264
|
+
"Use this to tame the saturation punch of luma-only."
|
|
265
|
+
)
|
|
266
|
+
self.luma_blend_row.setToolTip(tip)
|
|
267
|
+
self.sld_luma_blend.setToolTip(tip)
|
|
268
|
+
self.lbl_luma_blend.setToolTip(tip)
|
|
269
|
+
|
|
270
|
+
self.luma_blend_row.setEnabled(False)
|
|
271
|
+
|
|
246
272
|
# --- Curves boost ---
|
|
247
273
|
self.chk_curves = QCheckBox(self.tr("Curves boost"))
|
|
248
274
|
self.chk_curves.setChecked(False)
|
|
@@ -294,6 +320,7 @@ class StatisticalStretchDialog(QDialog):
|
|
|
294
320
|
self.btn_preview = QPushButton(self.tr("Preview"))
|
|
295
321
|
self.btn_apply = QPushButton(self.tr("Apply"))
|
|
296
322
|
self.btn_close = QPushButton(self.tr("Close"))
|
|
323
|
+
self.btn_reset = QPushButton(self.tr("Reset ⟳"))
|
|
297
324
|
|
|
298
325
|
self.btn_clipstats = QPushButton(self.tr("Clip stats"))
|
|
299
326
|
self.lbl_clipstats = QLabel("")
|
|
@@ -304,6 +331,24 @@ class StatisticalStretchDialog(QDialog):
|
|
|
304
331
|
self.lbl_clipstats.setFrameShadow(QLabel.Shadow.Sunken)
|
|
305
332
|
self.lbl_clipstats.setContentsMargins(6, 4, 6, 4)
|
|
306
333
|
|
|
334
|
+
# --- In-UI busy indicator (Wayland-friendly) ---
|
|
335
|
+
self.busy_row = QWidget()
|
|
336
|
+
br = QHBoxLayout(self.busy_row)
|
|
337
|
+
br.setContentsMargins(0, 0, 0, 0)
|
|
338
|
+
br.setSpacing(8)
|
|
339
|
+
|
|
340
|
+
self.lbl_busy = QLabel(self.tr("Processing…"))
|
|
341
|
+
self.lbl_busy.setStyleSheet("color:#888;")
|
|
342
|
+
self.pbar_busy = QProgressBar()
|
|
343
|
+
self.pbar_busy.setRange(0, 0) # indeterminate
|
|
344
|
+
self.pbar_busy.setTextVisible(False)
|
|
345
|
+
self.pbar_busy.setFixedHeight(10)
|
|
346
|
+
|
|
347
|
+
br.addWidget(self.lbl_busy)
|
|
348
|
+
br.addWidget(self.pbar_busy, 1)
|
|
349
|
+
|
|
350
|
+
self.busy_row.setVisible(False) # hidden until needed
|
|
351
|
+
|
|
307
352
|
# ------------------------------------------------------------------
|
|
308
353
|
# Layout
|
|
309
354
|
# ------------------------------------------------------------------
|
|
@@ -315,6 +360,7 @@ class StatisticalStretchDialog(QDialog):
|
|
|
315
360
|
form.addRow("", self.chk_hdr)
|
|
316
361
|
form.addRow("", self.hdr_row)
|
|
317
362
|
form.addRow("", self.luma_row)
|
|
363
|
+
form.addRow("", self.luma_blend_row)
|
|
318
364
|
form.addRow("", self.chk_normalize)
|
|
319
365
|
form.addRow("", self.chk_curves)
|
|
320
366
|
form.addRow("", self.curves_row)
|
|
@@ -326,10 +372,13 @@ class StatisticalStretchDialog(QDialog):
|
|
|
326
372
|
btn_row.addWidget(self.btn_preview)
|
|
327
373
|
btn_row.addWidget(self.btn_apply)
|
|
328
374
|
btn_row.addWidget(self.btn_clipstats)
|
|
375
|
+
btn_row.addStretch(1)
|
|
376
|
+
btn_row.addWidget(self.btn_reset)
|
|
329
377
|
btn_row.addStretch(1)
|
|
330
378
|
left.addLayout(btn_row)
|
|
331
379
|
|
|
332
380
|
left.addWidget(self.lbl_clipstats)
|
|
381
|
+
left.addWidget(self.busy_row)
|
|
333
382
|
left.addStretch(1)
|
|
334
383
|
|
|
335
384
|
right = QVBoxLayout()
|
|
@@ -382,14 +431,6 @@ class StatisticalStretchDialog(QDialog):
|
|
|
382
431
|
|
|
383
432
|
self.spin_target.valueChanged.connect(_suggest_hdr_knee_from_target)
|
|
384
433
|
|
|
385
|
-
# Luma-only: enables dropdown, disables "linked channels"
|
|
386
|
-
self.chk_luma_only.toggled.connect(self.cmb_luma.setEnabled)
|
|
387
|
-
|
|
388
|
-
def _on_luma_only_toggled(on: bool):
|
|
389
|
-
self.chk_linked.setEnabled(not on)
|
|
390
|
-
|
|
391
|
-
self.chk_luma_only.toggled.connect(_on_luma_only_toggled)
|
|
392
|
-
|
|
393
434
|
# Zoom buttons
|
|
394
435
|
self.btn_zoom_in.clicked.connect(lambda: self._zoom_by(1.25))
|
|
395
436
|
self.btn_zoom_out.clicked.connect(lambda: self._zoom_by(1/1.25))
|
|
@@ -399,6 +440,7 @@ class StatisticalStretchDialog(QDialog):
|
|
|
399
440
|
# Main buttons
|
|
400
441
|
self.btn_preview.clicked.connect(self._do_preview)
|
|
401
442
|
self.btn_apply.clicked.connect(self._do_apply)
|
|
443
|
+
self.btn_reset.clicked.connect(self._reset_defaults)
|
|
402
444
|
self.btn_close.clicked.connect(self.close)
|
|
403
445
|
self.btn_clipstats.clicked.connect(self._do_clip_stats)
|
|
404
446
|
|
|
@@ -410,50 +452,191 @@ class StatisticalStretchDialog(QDialog):
|
|
|
410
452
|
|
|
411
453
|
# Initialize UI state
|
|
412
454
|
_suggest_hdr_knee_from_target()
|
|
455
|
+
self.sld_luma_blend.valueChanged.connect(
|
|
456
|
+
lambda v: self.lbl_luma_blend.setText(f"{v/100:.2f}")
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
# Luma-only: one unified handler for all dependent UI state
|
|
460
|
+
def _on_luma_only_toggled(on: bool):
|
|
461
|
+
# enable luma mode dropdown only when luma-only is on
|
|
462
|
+
self.cmb_luma.setEnabled(on)
|
|
463
|
+
|
|
464
|
+
# linked channels doesn't make sense in luma-only mode
|
|
465
|
+
self.chk_linked.setEnabled(not on)
|
|
466
|
+
|
|
467
|
+
# luma blend row only meaningful when luma-only is enabled
|
|
468
|
+
self.luma_blend_row.setEnabled(on)
|
|
469
|
+
|
|
470
|
+
# mode-affecting => refresh clip stats
|
|
471
|
+
self._schedule_clip_stats()
|
|
472
|
+
|
|
473
|
+
self.chk_luma_only.toggled.connect(_on_luma_only_toggled)
|
|
413
474
|
_on_luma_only_toggled(self.chk_luma_only.isChecked())
|
|
414
475
|
|
|
476
|
+
|
|
415
477
|
# Initial preview + clip stats
|
|
416
478
|
self._populate_initial_preview()
|
|
417
479
|
|
|
418
480
|
|
|
419
481
|
# ----- helpers -----
|
|
420
482
|
def _show_busy(self, title: str, text: str):
|
|
421
|
-
#
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
dlg.setCancelButton(None) # no cancel button (keeps it simple)
|
|
430
|
-
dlg.setAutoClose(False)
|
|
431
|
-
dlg.setAutoReset(False)
|
|
432
|
-
dlg.setFixedWidth(320)
|
|
433
|
-
dlg.show()
|
|
434
|
-
|
|
435
|
-
# Ensure it paints before heavy work starts
|
|
436
|
-
QApplication.processEvents()
|
|
437
|
-
self._busy = dlg
|
|
483
|
+
# title kept for signature compatibility; not shown
|
|
484
|
+
try:
|
|
485
|
+
self.lbl_busy.setText(text or self.tr("Processing…"))
|
|
486
|
+
self.busy_row.setVisible(True)
|
|
487
|
+
# make sure UI repaints before thread work starts
|
|
488
|
+
QApplication.processEvents()
|
|
489
|
+
except Exception:
|
|
490
|
+
pass
|
|
438
491
|
|
|
439
492
|
def _hide_busy(self):
|
|
440
493
|
try:
|
|
441
|
-
if getattr(self, "
|
|
442
|
-
self.
|
|
443
|
-
self._busy.deleteLater()
|
|
494
|
+
if getattr(self, "busy_row", None) is not None:
|
|
495
|
+
self.busy_row.setVisible(False)
|
|
444
496
|
except Exception:
|
|
445
497
|
pass
|
|
446
|
-
|
|
498
|
+
|
|
447
499
|
|
|
448
500
|
def _set_controls_enabled(self, enabled: bool):
|
|
449
501
|
try:
|
|
450
502
|
self.btn_preview.setEnabled(enabled)
|
|
451
503
|
self.btn_apply.setEnabled(enabled)
|
|
504
|
+
if getattr(self, "btn_reset", None) is not None:
|
|
505
|
+
self.btn_reset.setEnabled(enabled) # <-- NEW
|
|
452
506
|
if getattr(self, "btn_clipstats", None) is not None:
|
|
453
507
|
self.btn_clipstats.setEnabled(enabled)
|
|
454
508
|
except Exception:
|
|
455
509
|
pass
|
|
456
510
|
|
|
511
|
+
def _reset_defaults(self):
|
|
512
|
+
"""Reset all controls back to factory defaults."""
|
|
513
|
+
if getattr(self, "_job_running", False):
|
|
514
|
+
return
|
|
515
|
+
|
|
516
|
+
# Defaults (must match your __init__ setValue/setChecked calls)
|
|
517
|
+
DEFAULT_TARGET = 0.25
|
|
518
|
+
DEFAULT_LINKED = False
|
|
519
|
+
DEFAULT_NORMALIZE = False
|
|
520
|
+
DEFAULT_BP_SLIDER = 500 # 5.00
|
|
521
|
+
DEFAULT_NO_BLACK_CLIP = False
|
|
522
|
+
|
|
523
|
+
DEFAULT_HDR_ON = False
|
|
524
|
+
DEFAULT_HDR_AMT = 15 # 0.15
|
|
525
|
+
DEFAULT_HDR_KNEE = 75 # 0.75
|
|
526
|
+
|
|
527
|
+
DEFAULT_LUMA_ONLY = False
|
|
528
|
+
DEFAULT_LUMA_MODE = "rec709"
|
|
529
|
+
DEFAULT_LUMA_BLEND = 60 # 0.60
|
|
530
|
+
|
|
531
|
+
DEFAULT_CURVES_ON = False
|
|
532
|
+
DEFAULT_CURVES_STRENGTH = 20 # 0.20
|
|
533
|
+
|
|
534
|
+
# Avoid cascading signal storms while we set everything
|
|
535
|
+
widgets = [
|
|
536
|
+
self.spin_target,
|
|
537
|
+
self.chk_linked,
|
|
538
|
+
self.chk_normalize,
|
|
539
|
+
self.sld_bp,
|
|
540
|
+
self.chk_no_black_clip,
|
|
541
|
+
self.chk_hdr,
|
|
542
|
+
self.sld_hdr_amt,
|
|
543
|
+
self.sld_hdr_knee,
|
|
544
|
+
self.chk_luma_only,
|
|
545
|
+
self.cmb_luma,
|
|
546
|
+
self.sld_luma_blend,
|
|
547
|
+
self.chk_curves,
|
|
548
|
+
self.sld_curves,
|
|
549
|
+
]
|
|
550
|
+
|
|
551
|
+
old_blocks = []
|
|
552
|
+
for w in widgets:
|
|
553
|
+
try:
|
|
554
|
+
old_blocks.append((w, w.blockSignals(True)))
|
|
555
|
+
except Exception:
|
|
556
|
+
pass
|
|
557
|
+
|
|
558
|
+
try:
|
|
559
|
+
# Reset “user locked” HDR knee behavior
|
|
560
|
+
self._hdr_knee_user_locked = False
|
|
561
|
+
|
|
562
|
+
# Core controls
|
|
563
|
+
self.spin_target.setValue(DEFAULT_TARGET)
|
|
564
|
+
self.chk_linked.setChecked(DEFAULT_LINKED)
|
|
565
|
+
self.chk_normalize.setChecked(DEFAULT_NORMALIZE)
|
|
566
|
+
|
|
567
|
+
# Black point
|
|
568
|
+
self.chk_no_black_clip.setChecked(DEFAULT_NO_BLACK_CLIP)
|
|
569
|
+
self.sld_bp.setValue(DEFAULT_BP_SLIDER)
|
|
570
|
+
self.lbl_bp.setText(f"{DEFAULT_BP_SLIDER/100:.2f}")
|
|
571
|
+
|
|
572
|
+
# HDR
|
|
573
|
+
self.chk_hdr.setChecked(DEFAULT_HDR_ON)
|
|
574
|
+
self.sld_hdr_amt.setValue(DEFAULT_HDR_AMT)
|
|
575
|
+
self.lbl_hdr_amt.setText(f"{DEFAULT_HDR_AMT/100:.2f}")
|
|
576
|
+
self.sld_hdr_knee.setValue(DEFAULT_HDR_KNEE)
|
|
577
|
+
self.lbl_hdr_knee.setText(f"{DEFAULT_HDR_KNEE/100:.2f}")
|
|
578
|
+
|
|
579
|
+
# Luma-only + mode + blend
|
|
580
|
+
self.chk_luma_only.setChecked(DEFAULT_LUMA_ONLY)
|
|
581
|
+
if DEFAULT_LUMA_MODE:
|
|
582
|
+
self.cmb_luma.setCurrentText(DEFAULT_LUMA_MODE)
|
|
583
|
+
self.sld_luma_blend.setValue(DEFAULT_LUMA_BLEND)
|
|
584
|
+
self.lbl_luma_blend.setText(f"{DEFAULT_LUMA_BLEND/100:.2f}")
|
|
585
|
+
|
|
586
|
+
# Curves
|
|
587
|
+
self.chk_curves.setChecked(DEFAULT_CURVES_ON)
|
|
588
|
+
self.sld_curves.setValue(DEFAULT_CURVES_STRENGTH)
|
|
589
|
+
self.lbl_curves_val.setText(f"{DEFAULT_CURVES_STRENGTH/100:.2f}")
|
|
590
|
+
|
|
591
|
+
finally:
|
|
592
|
+
# Restore signal states
|
|
593
|
+
for w, _prev in old_blocks:
|
|
594
|
+
try:
|
|
595
|
+
w.blockSignals(False)
|
|
596
|
+
except Exception:
|
|
597
|
+
pass
|
|
598
|
+
|
|
599
|
+
# Re-apply dependent enable/disable states exactly like normal interactions
|
|
600
|
+
try:
|
|
601
|
+
# no-black-clip disables BP row
|
|
602
|
+
self.row_bp.setEnabled(not self.chk_no_black_clip.isChecked())
|
|
603
|
+
except Exception:
|
|
604
|
+
pass
|
|
605
|
+
|
|
606
|
+
try:
|
|
607
|
+
# HDR enables HDR row
|
|
608
|
+
self.hdr_row.setEnabled(self.chk_hdr.isChecked())
|
|
609
|
+
except Exception:
|
|
610
|
+
pass
|
|
611
|
+
|
|
612
|
+
try:
|
|
613
|
+
# Curves enables curves row
|
|
614
|
+
self.curves_row.setEnabled(self.chk_curves.isChecked())
|
|
615
|
+
except Exception:
|
|
616
|
+
pass
|
|
617
|
+
|
|
618
|
+
try:
|
|
619
|
+
# Luma-only enables dropdown + blend row, disables linked
|
|
620
|
+
luma_on = self.chk_luma_only.isChecked()
|
|
621
|
+
self.cmb_luma.setEnabled(luma_on)
|
|
622
|
+
self.luma_blend_row.setEnabled(luma_on)
|
|
623
|
+
self.chk_linked.setEnabled(not luma_on)
|
|
624
|
+
except Exception:
|
|
625
|
+
pass
|
|
626
|
+
|
|
627
|
+
# Auto-suggest HDR knee from target (since we cleared lock)
|
|
628
|
+
try:
|
|
629
|
+
t = float(self.spin_target.value())
|
|
630
|
+
knee = float(np.clip(t + 0.10, 0.10, 0.95))
|
|
631
|
+
self.sld_hdr_knee.setValue(int(round(knee * 100)))
|
|
632
|
+
self.lbl_hdr_knee.setText(f"{knee:.2f}")
|
|
633
|
+
except Exception:
|
|
634
|
+
pass
|
|
635
|
+
|
|
636
|
+
# Refresh baseline preview + clip stats
|
|
637
|
+
self._populate_initial_preview()
|
|
638
|
+
|
|
639
|
+
|
|
457
640
|
def _clip_mode_label(self, imgf: np.ndarray) -> str:
|
|
458
641
|
# Mono image
|
|
459
642
|
if imgf.ndim == 2 or (imgf.ndim == 3 and imgf.shape[2] == 1):
|
|
@@ -590,7 +773,8 @@ class StatisticalStretchDialog(QDialog):
|
|
|
590
773
|
self._job_mode = mode
|
|
591
774
|
|
|
592
775
|
self._set_controls_enabled(False)
|
|
593
|
-
self._show_busy("Statistical Stretch", "Processing…")
|
|
776
|
+
self._show_busy("Statistical Stretch", "Processing preview…" if mode == "preview" else "Applying stretch…")
|
|
777
|
+
|
|
594
778
|
|
|
595
779
|
self._thread = QThread(self._main)
|
|
596
780
|
self._worker = _StretchWorker(self)
|
|
@@ -761,6 +945,7 @@ class StatisticalStretchDialog(QDialog):
|
|
|
761
945
|
luma_only = bool(getattr(self, "chk_luma_only", None) and self.chk_luma_only.isChecked())
|
|
762
946
|
luma_mode = str(self.cmb_luma.currentText()) if getattr(self, "cmb_luma", None) else "rec709"
|
|
763
947
|
no_black_clip = bool(self.chk_no_black_clip.isChecked())
|
|
948
|
+
luma_blend = float(self.sld_luma_blend.value()) / 100.0 if getattr(self, "sld_luma_blend", None) else 1.0
|
|
764
949
|
|
|
765
950
|
target = float(self.spin_target.value())
|
|
766
951
|
linked = bool(self.chk_linked.isChecked())
|
|
@@ -796,6 +981,7 @@ class StatisticalStretchDialog(QDialog):
|
|
|
796
981
|
hdr_knee=hdr_knee,
|
|
797
982
|
luma_only=luma_only,
|
|
798
983
|
luma_mode=luma_mode,
|
|
984
|
+
luma_blend=luma_blend, # <-- NEW
|
|
799
985
|
)
|
|
800
986
|
|
|
801
987
|
# ✅ If a mask is active, blend stretched result with original
|
|
@@ -895,6 +1081,7 @@ class StatisticalStretchDialog(QDialog):
|
|
|
895
1081
|
luma_only = bool(getattr(self, "chk_luma_only", None) and self.chk_luma_only.isChecked())
|
|
896
1082
|
luma_mode = str(self.cmb_luma.currentText()) if getattr(self, "cmb_luma", None) else "rec709"
|
|
897
1083
|
no_black_clip = bool(self.chk_no_black_clip.isChecked())
|
|
1084
|
+
luma_blend = float(self.sld_luma_blend.value()) / 100.0 if getattr(self, "sld_luma_blend", None) else 1.0
|
|
898
1085
|
|
|
899
1086
|
parts = [f"target={target:.2f}", "linked" if linked else "unlinked"]
|
|
900
1087
|
if normalize:
|
|
@@ -908,6 +1095,7 @@ class StatisticalStretchDialog(QDialog):
|
|
|
908
1095
|
parts.append(f"hdr={hdr_amount:.2f}@{hdr_knee:.2f}")
|
|
909
1096
|
if luma_only:
|
|
910
1097
|
parts.append(f"luma={luma_mode}")
|
|
1098
|
+
parts.append(f"blend={luma_blend:.2f}")
|
|
911
1099
|
if no_black_clip:
|
|
912
1100
|
parts.append("no_black_clip")
|
|
913
1101
|
|
|
@@ -952,6 +1140,7 @@ class StatisticalStretchDialog(QDialog):
|
|
|
952
1140
|
"hdr_knee": hdr_knee,
|
|
953
1141
|
"luma_only": luma_only,
|
|
954
1142
|
"luma_mode": luma_mode,
|
|
1143
|
+
"luma_blend": luma_blend,
|
|
955
1144
|
}
|
|
956
1145
|
|
|
957
1146
|
# ✅ Remember this as the last headless-style command
|
setiastro/saspro/subwindow.py
CHANGED
|
@@ -590,6 +590,7 @@ class ImageSubWindow(QWidget):
|
|
|
590
590
|
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
591
591
|
self.customContextMenuRequested.connect(self._show_ctx_menu)
|
|
592
592
|
QShortcut(QKeySequence("F2"), self, activated=self._rename_view)
|
|
593
|
+
QShortcut(QKeySequence("F3"), self, activated=self._rename_document)
|
|
593
594
|
#QShortcut(QKeySequence("A"), self, activated=self.toggle_autostretch)
|
|
594
595
|
QShortcut(QKeySequence("Ctrl+Space"), self, activated=self.toggle_autostretch)
|
|
595
596
|
QShortcut(QKeySequence("Alt+Shift+A"), self, activated=self.toggle_autostretch)
|
|
@@ -1726,7 +1727,7 @@ class ImageSubWindow(QWidget):
|
|
|
1726
1727
|
def _show_ctx_menu(self, pos):
|
|
1727
1728
|
menu = QMenu(self)
|
|
1728
1729
|
a_view = menu.addAction(self.tr("Rename View… (F2)"))
|
|
1729
|
-
a_doc = menu.addAction(self.tr("Rename Document…"))
|
|
1730
|
+
a_doc = menu.addAction(self.tr("Rename Document… (F3)"))
|
|
1730
1731
|
menu.addSeparator()
|
|
1731
1732
|
a_min = menu.addAction(self.tr("Send to Shelf"))
|
|
1732
1733
|
a_clear = menu.addAction(self.tr("Clear View Name (use doc name)"))
|
|
@@ -3081,7 +3082,6 @@ class ImageSubWindow(QWidget):
|
|
|
3081
3082
|
yi = int(round(py / s))
|
|
3082
3083
|
return xi, yi
|
|
3083
3084
|
|
|
3084
|
-
|
|
3085
3085
|
def _finish_preview_rect(self, vp_rect: QRect):
|
|
3086
3086
|
if vp_rect.width() < 4 or vp_rect.height() < 4:
|
|
3087
3087
|
self._cancel_rubber()
|
|
@@ -3187,8 +3187,6 @@ class ImageSubWindow(QWidget):
|
|
|
3187
3187
|
|
|
3188
3188
|
super().mousePressEvent(e)
|
|
3189
3189
|
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
3190
|
def _show_readout(self, xi, yi, sample):
|
|
3193
3191
|
mw = self._find_main_window()
|
|
3194
3192
|
if mw is None:
|
setiastro/saspro/whitebalance.py
CHANGED
|
@@ -184,6 +184,30 @@ def apply_white_balance_to_doc(doc, preset: Optional[Dict] = None):
|
|
|
184
184
|
step_name="White Balance",
|
|
185
185
|
)
|
|
186
186
|
|
|
187
|
+
def apply_pivot_gain(img: np.ndarray, med: np.ndarray, gains: np.ndarray) -> np.ndarray:
|
|
188
|
+
# img: HxWx3 float32 in [0,1]
|
|
189
|
+
med3 = med.reshape(1, 1, 3).astype(np.float32)
|
|
190
|
+
g3 = gains.reshape(1, 1, 3).astype(np.float32)
|
|
191
|
+
|
|
192
|
+
# pivot around median; do not scale negative deltas
|
|
193
|
+
d = img - med3
|
|
194
|
+
d = np.maximum(d, 0.0)
|
|
195
|
+
out = d * g3 + med3
|
|
196
|
+
return np.clip(out, 0.0, 1.0).astype(np.float32, copy=False)
|
|
197
|
+
|
|
198
|
+
def smoothstep(edge0, edge1, x):
|
|
199
|
+
t = np.clip((x - edge0) / (edge1 - edge0 + 1e-12), 0.0, 1.0)
|
|
200
|
+
return t * t * (3.0 - 2.0 * t)
|
|
201
|
+
|
|
202
|
+
def apply_soft_protect(img: np.ndarray, out_pivot: np.ndarray, k: float = 0.02) -> np.ndarray:
|
|
203
|
+
# luminance-based fade-in above median luminance
|
|
204
|
+
L = 0.2126*img[...,0] + 0.7152*img[...,1] + 0.0722*img[...,2]
|
|
205
|
+
Lm = float(np.median(L))
|
|
206
|
+
w = smoothstep(Lm, Lm + k, L).astype(np.float32)
|
|
207
|
+
w3 = w[..., None]
|
|
208
|
+
out = img * (1.0 - w3) + out_pivot * w3
|
|
209
|
+
return np.clip(out, 0.0, 1.0).astype(np.float32, copy=False)
|
|
210
|
+
|
|
187
211
|
|
|
188
212
|
# -------------------------
|
|
189
213
|
# Interactive dialog (UI)
|