setiastrosuitepro 1.6.0__py3-none-any.whl → 1.6.4.post1__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/data/SASP_data.fits +0 -0
- setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
- setiastro/data/catalogs/astrobin_filters.csv +890 -0
- setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
- setiastro/data/catalogs/cali2.csv +63 -0
- setiastro/data/catalogs/cali2color.csv +65 -0
- setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
- setiastro/data/catalogs/celestial_catalog.csv +24031 -0
- setiastro/data/catalogs/detected_stars.csv +24784 -0
- setiastro/data/catalogs/fits_header_data.csv +46 -0
- setiastro/data/catalogs/test.csv +8 -0
- setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
- setiastro/images/Astro_Spikes.png +0 -0
- setiastro/images/Background_startup.jpg +0 -0
- setiastro/images/HRDiagram.png +0 -0
- setiastro/images/LExtract.png +0 -0
- setiastro/images/LInsert.png +0 -0
- setiastro/images/Oxygenation-atm-2.svg.png +0 -0
- setiastro/images/RGB080604.png +0 -0
- setiastro/images/abeicon.png +0 -0
- setiastro/images/aberration.png +0 -0
- setiastro/images/andromedatry.png +0 -0
- setiastro/images/andromedatry_satellited.png +0 -0
- setiastro/images/annotated.png +0 -0
- setiastro/images/aperture.png +0 -0
- setiastro/images/astrosuite.ico +0 -0
- setiastro/images/astrosuite.png +0 -0
- setiastro/images/astrosuitepro.icns +0 -0
- setiastro/images/astrosuitepro.ico +0 -0
- setiastro/images/astrosuitepro.png +0 -0
- setiastro/images/background.png +0 -0
- setiastro/images/background2.png +0 -0
- setiastro/images/benchmark.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
- setiastro/images/blaster.png +0 -0
- setiastro/images/blink.png +0 -0
- setiastro/images/clahe.png +0 -0
- setiastro/images/collage.png +0 -0
- setiastro/images/colorwheel.png +0 -0
- setiastro/images/contsub.png +0 -0
- setiastro/images/convo.png +0 -0
- setiastro/images/copyslot.png +0 -0
- setiastro/images/cosmic.png +0 -0
- setiastro/images/cosmicsat.png +0 -0
- setiastro/images/crop1.png +0 -0
- setiastro/images/cropicon.png +0 -0
- setiastro/images/curves.png +0 -0
- setiastro/images/cvs.png +0 -0
- setiastro/images/debayer.png +0 -0
- setiastro/images/denoise_cnn_custom.png +0 -0
- setiastro/images/denoise_cnn_graph.png +0 -0
- setiastro/images/disk.png +0 -0
- setiastro/images/dse.png +0 -0
- setiastro/images/exoicon.png +0 -0
- setiastro/images/eye.png +0 -0
- setiastro/images/fliphorizontal.png +0 -0
- setiastro/images/flipvertical.png +0 -0
- setiastro/images/font.png +0 -0
- setiastro/images/freqsep.png +0 -0
- setiastro/images/functionbundle.png +0 -0
- setiastro/images/graxpert.png +0 -0
- setiastro/images/green.png +0 -0
- setiastro/images/gridicon.png +0 -0
- setiastro/images/halo.png +0 -0
- setiastro/images/hdr.png +0 -0
- setiastro/images/histogram.png +0 -0
- setiastro/images/hubble.png +0 -0
- setiastro/images/imagecombine.png +0 -0
- setiastro/images/invert.png +0 -0
- setiastro/images/isophote.png +0 -0
- setiastro/images/isophote_demo_figure.png +0 -0
- setiastro/images/isophote_demo_image.png +0 -0
- setiastro/images/isophote_demo_model.png +0 -0
- setiastro/images/isophote_demo_residual.png +0 -0
- setiastro/images/jwstpupil.png +0 -0
- setiastro/images/linearfit.png +0 -0
- setiastro/images/livestacking.png +0 -0
- setiastro/images/mask.png +0 -0
- setiastro/images/maskapply.png +0 -0
- setiastro/images/maskcreate.png +0 -0
- setiastro/images/maskremove.png +0 -0
- setiastro/images/morpho.png +0 -0
- setiastro/images/mosaic.png +0 -0
- setiastro/images/multiscale_decomp.png +0 -0
- setiastro/images/nbtorgb.png +0 -0
- setiastro/images/neutral.png +0 -0
- setiastro/images/nuke.png +0 -0
- setiastro/images/openfile.png +0 -0
- setiastro/images/pedestal.png +0 -0
- setiastro/images/pen.png +0 -0
- setiastro/images/pixelmath.png +0 -0
- setiastro/images/platesolve.png +0 -0
- setiastro/images/ppp.png +0 -0
- setiastro/images/pro.png +0 -0
- setiastro/images/project.png +0 -0
- setiastro/images/psf.png +0 -0
- setiastro/images/redo.png +0 -0
- setiastro/images/redoicon.png +0 -0
- setiastro/images/rescale.png +0 -0
- setiastro/images/rgbalign.png +0 -0
- setiastro/images/rgbcombo.png +0 -0
- setiastro/images/rgbextract.png +0 -0
- setiastro/images/rotate180.png +0 -0
- setiastro/images/rotatearbitrary.png +0 -0
- setiastro/images/rotateclockwise.png +0 -0
- setiastro/images/rotatecounterclockwise.png +0 -0
- setiastro/images/satellite.png +0 -0
- setiastro/images/script.png +0 -0
- setiastro/images/selectivecolor.png +0 -0
- setiastro/images/simbad.png +0 -0
- setiastro/images/slot0.png +0 -0
- setiastro/images/slot1.png +0 -0
- setiastro/images/slot2.png +0 -0
- setiastro/images/slot3.png +0 -0
- setiastro/images/slot4.png +0 -0
- setiastro/images/slot5.png +0 -0
- setiastro/images/slot6.png +0 -0
- setiastro/images/slot7.png +0 -0
- setiastro/images/slot8.png +0 -0
- setiastro/images/slot9.png +0 -0
- setiastro/images/spcc.png +0 -0
- setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
- setiastro/images/spinner.gif +0 -0
- setiastro/images/stacking.png +0 -0
- setiastro/images/staradd.png +0 -0
- setiastro/images/staralign.png +0 -0
- setiastro/images/starnet.png +0 -0
- setiastro/images/starregistration.png +0 -0
- setiastro/images/starspike.png +0 -0
- setiastro/images/starstretch.png +0 -0
- setiastro/images/statstretch.png +0 -0
- setiastro/images/supernova.png +0 -0
- setiastro/images/uhs.png +0 -0
- setiastro/images/undoicon.png +0 -0
- setiastro/images/upscale.png +0 -0
- setiastro/images/viewbundle.png +0 -0
- setiastro/images/whitebalance.png +0 -0
- setiastro/images/wimi_icon_256x256.png +0 -0
- setiastro/images/wimilogo.png +0 -0
- setiastro/images/wims.png +0 -0
- setiastro/images/wrench_icon.png +0 -0
- setiastro/images/xisfliberator.png +0 -0
- setiastro/qml/ResourceMonitor.qml +126 -0
- setiastro/saspro/__main__.py +228 -67
- setiastro/saspro/_generated/build_info.py +2 -1
- setiastro/saspro/abe.py +76 -25
- setiastro/saspro/aberration_ai.py +14 -14
- setiastro/saspro/add_stars.py +15 -12
- setiastro/saspro/astrobin_exporter.py +61 -58
- setiastro/saspro/astrospike_python.py +3 -1
- setiastro/saspro/autostretch.py +4 -2
- setiastro/saspro/backgroundneutral.py +65 -14
- setiastro/saspro/batch_convert.py +8 -5
- setiastro/saspro/batch_renamer.py +39 -36
- setiastro/saspro/blemish_blaster.py +15 -12
- setiastro/saspro/blink_comparator_pro.py +605 -379
- setiastro/saspro/cheat_sheet.py +62 -17
- setiastro/saspro/clahe.py +34 -8
- setiastro/saspro/comet_stacking.py +103 -38
- setiastro/saspro/common_tr.py +107 -0
- setiastro/saspro/continuum_subtract.py +7 -7
- setiastro/saspro/convo.py +12 -9
- setiastro/saspro/copyastro.py +3 -0
- setiastro/saspro/cosmicclarity.py +77 -52
- setiastro/saspro/crop_dialog_pro.py +80 -45
- setiastro/saspro/curve_editor_pro.py +51 -33
- setiastro/saspro/debayer.py +6 -3
- setiastro/saspro/doc_manager.py +49 -19
- setiastro/saspro/exoplanet_detector.py +11 -11
- setiastro/saspro/fitsmodifier.py +48 -44
- setiastro/saspro/fix_bom.py +32 -0
- setiastro/saspro/frequency_separation.py +18 -12
- setiastro/saspro/function_bundle.py +18 -16
- setiastro/saspro/generate_translations.py +3092 -0
- setiastro/saspro/ghs_dialog_pro.py +19 -16
- setiastro/saspro/graxpert.py +3 -0
- setiastro/saspro/gui/main_window.py +471 -126
- setiastro/saspro/gui/mixins/dock_mixin.py +123 -11
- setiastro/saspro/gui/mixins/file_mixin.py +25 -20
- setiastro/saspro/gui/mixins/geometry_mixin.py +115 -15
- setiastro/saspro/gui/mixins/header_mixin.py +6 -6
- setiastro/saspro/gui/mixins/mask_mixin.py +8 -8
- setiastro/saspro/gui/mixins/menu_mixin.py +62 -33
- setiastro/saspro/gui/mixins/toolbar_mixin.py +382 -226
- setiastro/saspro/gui/mixins/update_mixin.py +26 -26
- setiastro/saspro/gui/statistics_dialog.py +47 -0
- setiastro/saspro/halobgon.py +29 -3
- setiastro/saspro/header_viewer.py +21 -18
- setiastro/saspro/histogram.py +29 -26
- setiastro/saspro/history_explorer.py +2 -0
- setiastro/saspro/i18n.py +168 -0
- setiastro/saspro/image_combine.py +3 -0
- setiastro/saspro/image_peeker_pro.py +52 -44
- setiastro/saspro/imageops/stretch.py +5 -13
- setiastro/saspro/isophote.py +3 -0
- setiastro/saspro/legacy/numba_utils.py +64 -47
- setiastro/saspro/linear_fit.py +3 -0
- setiastro/saspro/live_stacking.py +13 -2
- setiastro/saspro/mask_creation.py +180 -22
- setiastro/saspro/mfdeconv.py +5 -0
- setiastro/saspro/morphology.py +38 -13
- setiastro/saspro/multiscale_decomp.py +713 -256
- setiastro/saspro/nbtorgb_stars.py +12 -2
- setiastro/saspro/numba_utils.py +149 -48
- setiastro/saspro/ops/scripts.py +77 -17
- setiastro/saspro/ops/settings.py +177 -100
- setiastro/saspro/perfect_palette_picker.py +25 -7
- setiastro/saspro/pixelmath.py +114 -110
- setiastro/saspro/plate_solver.py +118 -108
- setiastro/saspro/remove_green.py +24 -7
- setiastro/saspro/remove_stars.py +136 -162
- setiastro/saspro/remove_stars_preset.py +55 -13
- setiastro/saspro/resources.py +46 -15
- setiastro/saspro/rgb_combination.py +19 -18
- setiastro/saspro/rgbalign.py +11 -11
- setiastro/saspro/save_options.py +5 -4
- setiastro/saspro/selective_color.py +84 -25
- setiastro/saspro/sfcc.py +119 -72
- setiastro/saspro/shortcuts.py +345 -36
- setiastro/saspro/signature_insert.py +4 -1
- setiastro/saspro/stacking_suite.py +2066 -1119
- setiastro/saspro/star_alignment.py +291 -331
- setiastro/saspro/star_spikes.py +137 -53
- setiastro/saspro/star_stretch.py +47 -10
- setiastro/saspro/stat_stretch.py +52 -16
- setiastro/saspro/status_log_dock.py +1 -1
- setiastro/saspro/subwindow.py +97 -36
- setiastro/saspro/supernovaasteroidhunter.py +68 -61
- setiastro/saspro/swap_manager.py +77 -42
- setiastro/saspro/translations/all_source_strings.json +4726 -0
- setiastro/saspro/translations/ar_translations.py +4096 -0
- setiastro/saspro/translations/de_translations.py +3728 -0
- setiastro/saspro/translations/es_translations.py +4169 -0
- setiastro/saspro/translations/fr_translations.py +4090 -0
- setiastro/saspro/translations/hi_translations.py +3803 -0
- setiastro/saspro/translations/integrate_translations.py +271 -0
- setiastro/saspro/translations/it_translations.py +4728 -0
- setiastro/saspro/translations/ja_translations.py +3834 -0
- setiastro/saspro/translations/pt_translations.py +3847 -0
- setiastro/saspro/translations/ru_translations.py +3082 -0
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +16019 -0
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +14548 -0
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +16202 -0
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +15870 -0
- setiastro/saspro/translations/saspro_hi.qm +0 -0
- setiastro/saspro/translations/saspro_hi.ts +14855 -0
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +19046 -0
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +14980 -0
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +15024 -0
- setiastro/saspro/translations/saspro_ru.qm +0 -0
- setiastro/saspro/translations/saspro_ru.ts +11835 -0
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +15237 -0
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +15248 -0
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +15289 -0
- setiastro/saspro/translations/sw_translations.py +3897 -0
- setiastro/saspro/translations/uk_translations.py +3929 -0
- setiastro/saspro/translations/zh_translations.py +3910 -0
- setiastro/saspro/versioning.py +77 -0
- setiastro/saspro/view_bundle.py +20 -17
- setiastro/saspro/wavescale_hdr.py +54 -33
- setiastro/saspro/wavescale_hdr_preset.py +6 -5
- setiastro/saspro/wavescalede.py +54 -31
- setiastro/saspro/wavescalede_preset.py +9 -7
- setiastro/saspro/whitebalance.py +58 -22
- setiastro/saspro/widgets/common_utilities.py +12 -11
- setiastro/saspro/widgets/minigame/game.js +991 -0
- setiastro/saspro/widgets/minigame/index.html +53 -0
- setiastro/saspro/widgets/minigame/style.css +241 -0
- setiastro/saspro/widgets/preview_dialogs.py +8 -8
- setiastro/saspro/widgets/resource_monitor.py +263 -0
- setiastro/saspro/widgets/spinboxes.py +18 -0
- setiastro/saspro/widgets/wavelet_utils.py +52 -20
- setiastro/saspro/wimi.py +7996 -0
- setiastro/saspro/wims.py +578 -0
- setiastro/saspro/window_shelf.py +2 -2
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/METADATA +15 -3
- setiastrosuitepro-1.6.4.post1.dist-info/RECORD +368 -0
- setiastrosuitepro-1.6.0.dist-info/RECORD +0 -174
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/licenses/license.txt +0 -0
|
@@ -48,6 +48,9 @@ class LiveStackSettingsDialog(QDialog):
|
|
|
48
48
|
def __init__(self, parent):
|
|
49
49
|
super().__init__(parent)
|
|
50
50
|
self.setWindowTitle("Live Stack & Culling Settings")
|
|
51
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
52
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
53
|
+
self.setModal(False)
|
|
51
54
|
|
|
52
55
|
# — Live Stack Settings —
|
|
53
56
|
# Bootstrap frames (int)
|
|
@@ -290,8 +293,16 @@ def estimate_global_snr(
|
|
|
290
293
|
|
|
291
294
|
# 1) Collapse to simple 2D float array (grayscale)
|
|
292
295
|
if stack_image.ndim == 3 and stack_image.shape[2] == 3:
|
|
293
|
-
|
|
294
|
-
|
|
296
|
+
try:
|
|
297
|
+
import cv2
|
|
298
|
+
# cv2.cvtColor is significantly faster than mean(axis=2)
|
|
299
|
+
# Assuming RGB input, but even if BGR, for SNR estimation luma difference is negligible
|
|
300
|
+
gray = cv2.cvtColor(stack_image, cv2.COLOR_RGB2GRAY)
|
|
301
|
+
if gray.dtype != np.float32:
|
|
302
|
+
gray = gray.astype(np.float32)
|
|
303
|
+
except ImportError:
|
|
304
|
+
# Fallback
|
|
305
|
+
gray = stack_image.mean(axis=2).astype(np.float32)
|
|
295
306
|
else:
|
|
296
307
|
# Already mono: just cast to float32
|
|
297
308
|
gray = stack_image.astype(np.float32)
|
|
@@ -17,7 +17,7 @@ except Exception:
|
|
|
17
17
|
from PyQt6.QtCore import Qt, QPointF, QRectF, QTimer, QEvent
|
|
18
18
|
from PyQt6.QtGui import (
|
|
19
19
|
QImage, QPixmap, QPainter, QColor, QPen, QBrush,
|
|
20
|
-
QPainterPath, QWheelEvent, QPolygonF
|
|
20
|
+
QPainterPath, QWheelEvent, QPolygonF, QMouseEvent
|
|
21
21
|
)
|
|
22
22
|
from PyQt6.QtWidgets import (
|
|
23
23
|
QInputDialog, QMessageBox, QFileDialog, # QFileDialog only used if you later add “export”
|
|
@@ -32,7 +32,7 @@ from PyQt6.QtWidgets import (
|
|
|
32
32
|
|
|
33
33
|
from .masks_core import MaskLayer
|
|
34
34
|
from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
|
|
35
|
-
|
|
35
|
+
from setiastro.saspro.imageops.stretch import stretch_color_image
|
|
36
36
|
|
|
37
37
|
# ---------- small utils ----------
|
|
38
38
|
|
|
@@ -48,6 +48,38 @@ def _to_qpixmap01(img01: np.ndarray) -> QPixmap:
|
|
|
48
48
|
qimg = QImage(buf.data, w, h, buf.strides[0], QImage.Format.Format_RGB888)
|
|
49
49
|
return QPixmap.fromImage(qimg)
|
|
50
50
|
|
|
51
|
+
def _display_stretch(img01: np.ndarray) -> np.ndarray:
|
|
52
|
+
"""
|
|
53
|
+
Display-only stretch. Does NOT modify underlying data used for mask creation.
|
|
54
|
+
Returns float32 in [0,1].
|
|
55
|
+
"""
|
|
56
|
+
a = np.asarray(img01, dtype=np.float32)
|
|
57
|
+
a = np.clip(a, 0.0, 1.0)
|
|
58
|
+
|
|
59
|
+
# Color: use your existing stretch if available
|
|
60
|
+
if a.ndim == 3 and a.shape[2] == 3 and stretch_color_image is not None:
|
|
61
|
+
try:
|
|
62
|
+
return np.clip(stretch_color_image(a, 0.25, linked=False, normalize=False), 0.0, 1.0).astype(np.float32)
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
# Mono (or fallback): simple robust stretch around median
|
|
67
|
+
# (keeps it predictable and fast; display-only)
|
|
68
|
+
m = float(np.nanmedian(a))
|
|
69
|
+
if not np.isfinite(m):
|
|
70
|
+
return a.astype(np.float32, copy=False)
|
|
71
|
+
|
|
72
|
+
# Simple gamma-like lift using median anchor
|
|
73
|
+
# If median is tiny, boost; if already bright, minimal change.
|
|
74
|
+
target = 0.25
|
|
75
|
+
eps = 1e-8
|
|
76
|
+
scale = target / max(m, eps)
|
|
77
|
+
out = np.clip(a * scale, 0.0, 1.0)
|
|
78
|
+
|
|
79
|
+
# Gentle midtone curve
|
|
80
|
+
out = np.sqrt(out)
|
|
81
|
+
return np.clip(out, 0.0, 1.0).astype(np.float32, copy=False)
|
|
82
|
+
|
|
51
83
|
|
|
52
84
|
def _find_main_window(w):
|
|
53
85
|
p = w
|
|
@@ -248,10 +280,14 @@ class MaskCanvas(QGraphicsView):
|
|
|
248
280
|
super().__init__(parent)
|
|
249
281
|
self.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
250
282
|
|
|
283
|
+
self._base_image01 = np.asarray(image01, dtype=np.float32)
|
|
284
|
+
self._display_stretch_enabled = False
|
|
285
|
+
|
|
251
286
|
# scene + background image
|
|
252
287
|
self.scene = QGraphicsScene(self)
|
|
253
288
|
self.setScene(self.scene)
|
|
254
|
-
|
|
289
|
+
|
|
290
|
+
self.bg_item = QGraphicsPixmapItem(_to_qpixmap01(self._base_image01))
|
|
255
291
|
self.scene.addItem(self.bg_item)
|
|
256
292
|
|
|
257
293
|
# --- NEW: basic zoom state ---
|
|
@@ -306,6 +342,38 @@ class MaskCanvas(QGraphicsView):
|
|
|
306
342
|
super().wheelEvent(ev)
|
|
307
343
|
# ----------------- END: Zoom API ---------------------
|
|
308
344
|
|
|
345
|
+
def set_display_stretch_enabled(self, enabled: bool):
|
|
346
|
+
enabled = bool(enabled)
|
|
347
|
+
if enabled == self._display_stretch_enabled:
|
|
348
|
+
return
|
|
349
|
+
self._display_stretch_enabled = enabled
|
|
350
|
+
self._refresh_background_pixmap(keep_view=True)
|
|
351
|
+
|
|
352
|
+
def display_stretch_enabled(self) -> bool:
|
|
353
|
+
return bool(self._display_stretch_enabled)
|
|
354
|
+
|
|
355
|
+
def current_display_image01(self) -> np.ndarray:
|
|
356
|
+
"""Returns the image currently used for *display* (not for mask math)."""
|
|
357
|
+
if self._display_stretch_enabled:
|
|
358
|
+
return _display_stretch(self._base_image01)
|
|
359
|
+
return self._base_image01
|
|
360
|
+
|
|
361
|
+
def _refresh_background_pixmap(self, keep_view: bool = True):
|
|
362
|
+
# Preserve current view transform/center so toggling doesn't “jump”
|
|
363
|
+
old_transform = self.transform()
|
|
364
|
+
old_center = self.mapToScene(self.viewport().rect().center())
|
|
365
|
+
|
|
366
|
+
disp = self.current_display_image01()
|
|
367
|
+
self.bg_item.setPixmap(_to_qpixmap01(disp))
|
|
368
|
+
|
|
369
|
+
# Ensure scene rect still matches image pixels
|
|
370
|
+
self.setSceneRect(self.bg_item.boundingRect())
|
|
371
|
+
|
|
372
|
+
if keep_view:
|
|
373
|
+
self.setTransform(old_transform)
|
|
374
|
+
self.centerOn(old_center)
|
|
375
|
+
|
|
376
|
+
|
|
309
377
|
def set_mode(self, mode: str):
|
|
310
378
|
assert mode in ('polygon', 'ellipse', 'select')
|
|
311
379
|
self.mode = mode
|
|
@@ -432,7 +500,7 @@ class MaskCanvas(QGraphicsView):
|
|
|
432
500
|
class LivePreviewDialog(QDialog):
|
|
433
501
|
def __init__(self, original_image01: np.ndarray, parent=None):
|
|
434
502
|
super().__init__(parent)
|
|
435
|
-
self.setWindowTitle("Live Mask Preview")
|
|
503
|
+
self.setWindowTitle(self.tr("Live Mask Preview"))
|
|
436
504
|
self.label = QLabel(alignment=Qt.AlignmentFlag.AlignCenter)
|
|
437
505
|
lay = QVBoxLayout(self); lay.addWidget(self.label)
|
|
438
506
|
self.resize(300, 300)
|
|
@@ -453,39 +521,61 @@ class LivePreviewDialog(QDialog):
|
|
|
453
521
|
Qt.AspectRatioMode.KeepAspectRatio,
|
|
454
522
|
Qt.TransformationMode.SmoothTransformation))
|
|
455
523
|
|
|
524
|
+
def set_base_image(self, image01: np.ndarray):
|
|
525
|
+
self.base_pixmap = _to_qpixmap01(image01)
|
|
456
526
|
|
|
457
527
|
# ---------- Preview (push-as-doc) ----------
|
|
458
|
-
|
|
459
528
|
class MaskPreviewDialog(QDialog):
|
|
460
529
|
"""Scrollable preview + 'Push as New Document…'."""
|
|
461
530
|
def __init__(self, mask01: np.ndarray, parent=None):
|
|
462
531
|
super().__init__(parent)
|
|
463
|
-
self.setWindowTitle("Mask Preview")
|
|
532
|
+
self.setWindowTitle(self.tr("Mask Preview"))
|
|
464
533
|
self.mask = np.clip(mask01, 0, 1).astype(np.float32)
|
|
465
534
|
|
|
466
|
-
|
|
535
|
+
# --- drag-pan state ---
|
|
536
|
+
self._dragging = False
|
|
537
|
+
self._drag_start = None
|
|
538
|
+
self._h_start = 0
|
|
539
|
+
self._v_start = 0
|
|
540
|
+
|
|
541
|
+
# Build UI first
|
|
542
|
+
self.scroll = QScrollArea(self)
|
|
543
|
+
self.scroll.setWidgetResizable(False)
|
|
467
544
|
self.scroll.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
545
|
+
|
|
468
546
|
self.label = QLabel(alignment=Qt.AlignmentFlag.AlignCenter)
|
|
469
547
|
self.label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored)
|
|
470
|
-
|
|
548
|
+
|
|
549
|
+
self.pixmap = self._to_pixmap(self.mask)
|
|
550
|
+
self.label.setPixmap(self.pixmap)
|
|
551
|
+
self.label.resize(self.pixmap.size())
|
|
552
|
+
|
|
471
553
|
self.scroll.setWidget(self.label)
|
|
472
554
|
|
|
555
|
+
# Enable mouse drag panning on the label (NOW label exists)
|
|
556
|
+
self.label.setMouseTracking(True)
|
|
557
|
+
self.label.installEventFilter(self)
|
|
558
|
+
|
|
473
559
|
btns = QHBoxLayout()
|
|
474
560
|
b_in = themed_toolbtn("zoom-in", "Zoom In")
|
|
475
561
|
b_out = themed_toolbtn("zoom-out", "Zoom Out")
|
|
476
562
|
b_fit = themed_toolbtn("zoom-fit-best", "Fit to Preview")
|
|
563
|
+
b_push = QPushButton(self.tr("Push as New Document…"))
|
|
477
564
|
|
|
478
|
-
|
|
479
|
-
b_push = QPushButton("Push as New Document…")
|
|
480
565
|
b_in.clicked.connect(lambda: self._zoom(1.2))
|
|
481
566
|
b_out.clicked.connect(lambda: self._zoom(1/1.2))
|
|
482
567
|
b_fit.clicked.connect(self._fit)
|
|
483
568
|
b_push.clicked.connect(self.push_as_new_document)
|
|
569
|
+
|
|
484
570
|
for b in (b_in, b_out, b_fit, b_push):
|
|
485
571
|
btns.addWidget(b)
|
|
486
572
|
|
|
487
|
-
lay = QVBoxLayout(self)
|
|
488
|
-
|
|
573
|
+
lay = QVBoxLayout(self)
|
|
574
|
+
lay.addWidget(self.scroll)
|
|
575
|
+
lay.addLayout(btns)
|
|
576
|
+
|
|
577
|
+
self.scale = 1.0
|
|
578
|
+
self.setMinimumSize(600, 400)
|
|
489
579
|
|
|
490
580
|
def _to_pixmap(self, mask01: np.ndarray) -> QPixmap:
|
|
491
581
|
m8 = (np.clip(mask01, 0, 1) * 255).astype(np.uint8)
|
|
@@ -495,17 +585,52 @@ class MaskPreviewDialog(QDialog):
|
|
|
495
585
|
|
|
496
586
|
def _zoom(self, factor: float):
|
|
497
587
|
self.scale *= factor
|
|
498
|
-
scaled = self.pixmap.scaled(
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
588
|
+
scaled = self.pixmap.scaled(
|
|
589
|
+
self.pixmap.size() * self.scale,
|
|
590
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
|
591
|
+
Qt.TransformationMode.SmoothTransformation
|
|
592
|
+
)
|
|
593
|
+
self.label.setPixmap(scaled)
|
|
594
|
+
self.label.resize(scaled.size())
|
|
502
595
|
|
|
503
596
|
def _fit(self):
|
|
504
597
|
vp = self.scroll.viewport().size()
|
|
505
598
|
if self.pixmap.width() and self.pixmap.height():
|
|
506
599
|
s = min(vp.width()/self.pixmap.width(), vp.height()/self.pixmap.height())
|
|
507
600
|
self.scale = max(0.05, s)
|
|
508
|
-
|
|
601
|
+
# re-render at the new scale (don’t multiply again)
|
|
602
|
+
scaled = self.pixmap.scaled(
|
|
603
|
+
self.pixmap.size() * self.scale,
|
|
604
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
|
605
|
+
Qt.TransformationMode.SmoothTransformation
|
|
606
|
+
)
|
|
607
|
+
self.label.setPixmap(scaled)
|
|
608
|
+
self.label.resize(scaled.size())
|
|
609
|
+
|
|
610
|
+
def eventFilter(self, obj, ev):
|
|
611
|
+
if obj is self.label:
|
|
612
|
+
if ev.type() == QEvent.Type.MouseButtonPress and ev.button() == Qt.MouseButton.LeftButton:
|
|
613
|
+
self._dragging = True
|
|
614
|
+
self._drag_start = ev.globalPosition().toPoint()
|
|
615
|
+
self._h_start = self.scroll.horizontalScrollBar().value()
|
|
616
|
+
self._v_start = self.scroll.verticalScrollBar().value()
|
|
617
|
+
self.setCursor(Qt.CursorShape.ClosedHandCursor)
|
|
618
|
+
return True
|
|
619
|
+
|
|
620
|
+
if ev.type() == QEvent.Type.MouseMove and self._dragging:
|
|
621
|
+
p = ev.globalPosition().toPoint()
|
|
622
|
+
d = p - self._drag_start
|
|
623
|
+
self.scroll.horizontalScrollBar().setValue(self._h_start - d.x())
|
|
624
|
+
self.scroll.verticalScrollBar().setValue(self._v_start - d.y())
|
|
625
|
+
return True
|
|
626
|
+
|
|
627
|
+
if ev.type() == QEvent.Type.MouseButtonRelease and ev.button() == Qt.MouseButton.LeftButton:
|
|
628
|
+
self._dragging = False
|
|
629
|
+
self._drag_start = None
|
|
630
|
+
self.unsetCursor()
|
|
631
|
+
return True
|
|
632
|
+
|
|
633
|
+
return super().eventFilter(obj, ev)
|
|
509
634
|
|
|
510
635
|
def push_as_new_document(self):
|
|
511
636
|
if self.mask is None:
|
|
@@ -554,7 +679,10 @@ class MaskCreationDialog(QDialog):
|
|
|
554
679
|
"""Mask creation UI for SASpro documents (returns a np mask on OK)."""
|
|
555
680
|
def __init__(self, image01: np.ndarray, parent=None, auto_push_on_ok: bool = True):
|
|
556
681
|
super().__init__(parent)
|
|
557
|
-
self.setWindowTitle("Mask Creation")
|
|
682
|
+
self.setWindowTitle(self.tr("Mask Creation"))
|
|
683
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
684
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
685
|
+
self.setModal(False)
|
|
558
686
|
self.image = np.asarray(image01, dtype=np.float32).copy()
|
|
559
687
|
self.mask: np.ndarray | None = None
|
|
560
688
|
self.live_preview = LivePreviewDialog(self.image, parent=self)
|
|
@@ -572,9 +700,9 @@ class MaskCreationDialog(QDialog):
|
|
|
572
700
|
|
|
573
701
|
# Mode toolbar
|
|
574
702
|
mode_bar = QHBoxLayout()
|
|
575
|
-
self.free_btn = QPushButton("Freehand"); self.free_btn.setCheckable(True)
|
|
576
|
-
self.ellipse_btn = QPushButton("Ellipse"); self.ellipse_btn.setCheckable(True)
|
|
577
|
-
self.select_btn = QPushButton("Select Entire Image"); self.select_btn.setCheckable(True)
|
|
703
|
+
self.free_btn = QPushButton(self.tr("Freehand")); self.free_btn.setCheckable(True)
|
|
704
|
+
self.ellipse_btn = QPushButton(self.tr("Ellipse")); self.ellipse_btn.setCheckable(True)
|
|
705
|
+
self.select_btn = QPushButton(self.tr("Select Entire Image")); self.select_btn.setCheckable(True)
|
|
578
706
|
group = QButtonGroup(self); group.setExclusive(True)
|
|
579
707
|
for b in (self.free_btn, self.ellipse_btn, self.select_btn):
|
|
580
708
|
b.setAutoExclusive(True); group.addButton(b)
|
|
@@ -598,6 +726,18 @@ class MaskCreationDialog(QDialog):
|
|
|
598
726
|
zoom_bar.addWidget(z_out); zoom_bar.addWidget(z_in); zoom_bar.addWidget(z_fit)
|
|
599
727
|
layout.addLayout(zoom_bar)
|
|
600
728
|
|
|
729
|
+
# Display stretch toggle (display-only; never modifies image data)
|
|
730
|
+
self.btn_disp_stretch = QPushButton(self.tr("Toggle Display Stretch"))
|
|
731
|
+
self.btn_disp_stretch.setCheckable(True)
|
|
732
|
+
self.btn_disp_stretch.setToolTip(
|
|
733
|
+
"Display-only stretch for easier masking on linear images.\n"
|
|
734
|
+
"This does NOT change the image data or the generated mask."
|
|
735
|
+
)
|
|
736
|
+
self.btn_disp_stretch.toggled.connect(self._toggle_display_stretch)
|
|
737
|
+
self.btn_disp_stretch.setChecked(False)
|
|
738
|
+
self.btn_disp_stretch.setText("Enable Display Stretch")
|
|
739
|
+
zoom_bar.addWidget(self.btn_disp_stretch)
|
|
740
|
+
|
|
601
741
|
# Canvas
|
|
602
742
|
self.canvas = MaskCanvas(self.image)
|
|
603
743
|
layout.addWidget(self.canvas, 1)
|
|
@@ -614,7 +754,7 @@ class MaskCreationDialog(QDialog):
|
|
|
614
754
|
self.type_dd.currentTextChanged.connect(lambda t: setattr(self, 'mask_type', t))
|
|
615
755
|
controls.addWidget(self.type_dd)
|
|
616
756
|
|
|
617
|
-
controls.addWidget(QLabel("Edge Blur (px):"))
|
|
757
|
+
controls.addWidget(QLabel(self.tr("Edge Blur (px):")))
|
|
618
758
|
self.blur_slider = QSlider(Qt.Orientation.Horizontal); self.blur_slider.setRange(0, 300)
|
|
619
759
|
self.blur_slider.valueChanged.connect(lambda v: setattr(self, 'blur_amount', int(v)))
|
|
620
760
|
controls.addWidget(self.blur_slider)
|
|
@@ -704,6 +844,24 @@ class MaskCreationDialog(QDialog):
|
|
|
704
844
|
if self.link_cb.isChecked():
|
|
705
845
|
self.upper_sl.setValue(v)
|
|
706
846
|
|
|
847
|
+
def _toggle_display_stretch(self, enabled: bool):
|
|
848
|
+
try:
|
|
849
|
+
self.canvas.set_display_stretch_enabled(bool(enabled))
|
|
850
|
+
|
|
851
|
+
# keep button label in sync
|
|
852
|
+
self.btn_disp_stretch.setText(
|
|
853
|
+
self.tr("Disable Display Stretch") if enabled else self.tr("Enable Display Stretch")
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
# Keep the live preview background in sync (Range Selection uses it)
|
|
857
|
+
if hasattr(self, "live_preview") and self.live_preview is not None:
|
|
858
|
+
self.live_preview.set_base_image(self.canvas.current_display_image01())
|
|
859
|
+
if self.live_preview.isVisible():
|
|
860
|
+
self._update_live_preview()
|
|
861
|
+
except Exception:
|
|
862
|
+
pass
|
|
863
|
+
|
|
864
|
+
|
|
707
865
|
# ---- generators
|
|
708
866
|
def _component_lightness(self) -> np.ndarray:
|
|
709
867
|
if self.image.ndim == 3:
|
setiastro/saspro/mfdeconv.py
CHANGED
|
@@ -822,6 +822,11 @@ def _to_luma_local(a: np.ndarray) -> np.ndarray:
|
|
|
822
822
|
return a
|
|
823
823
|
# (H,W,3) or (3,H,W)
|
|
824
824
|
if a.ndim == 3 and a.shape[-1] == 3:
|
|
825
|
+
try:
|
|
826
|
+
import cv2
|
|
827
|
+
return cv2.cvtColor(a, cv2.COLOR_RGB2GRAY).astype(np.float32, copy=False)
|
|
828
|
+
except Exception:
|
|
829
|
+
pass
|
|
825
830
|
r, g, b = a[..., 0], a[..., 1], a[..., 2]
|
|
826
831
|
return (0.2126*r + 0.7152*g + 0.0722*b).astype(np.float32, copy=False)
|
|
827
832
|
if a.ndim == 3 and a.shape[0] == 3:
|
setiastro/saspro/morphology.py
CHANGED
|
@@ -46,10 +46,9 @@ def apply_morphology(image: np.ndarray, *, operation: str = "erosion",
|
|
|
46
46
|
|
|
47
47
|
if img.ndim == 3 and img.shape[2] == 3:
|
|
48
48
|
u8 = (img * 255.0).astype(np.uint8)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return np.clip(out, 0.0, 1.0)
|
|
49
|
+
# OpenCV morphology functions handle multi-channel images natively (independent channels)
|
|
50
|
+
out_u8 = _do(u8)
|
|
51
|
+
return (out_u8.astype(np.float32) / 255.0).clip(0.0, 1.0)
|
|
53
52
|
|
|
54
53
|
raise ValueError("Input image must be mono (H,W)/(H,W,1) or RGB (H,W,3).")
|
|
55
54
|
|
|
@@ -91,7 +90,10 @@ class MorphologyDialogPro(QDialog):
|
|
|
91
90
|
|
|
92
91
|
def __init__(self, parent, doc, icon: QIcon | None = None, initial: dict | None = None):
|
|
93
92
|
super().__init__(parent)
|
|
94
|
-
self.setWindowTitle("Morphological Operations")
|
|
93
|
+
self.setWindowTitle(self.tr("Morphological Operations"))
|
|
94
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
95
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
96
|
+
self.setModal(False)
|
|
95
97
|
if icon:
|
|
96
98
|
try: self.setWindowIcon(icon)
|
|
97
99
|
except Exception as e:
|
|
@@ -109,7 +111,7 @@ class MorphologyDialogPro(QDialog):
|
|
|
109
111
|
v = QVBoxLayout(self)
|
|
110
112
|
|
|
111
113
|
# ---- Params (unchanged) ----
|
|
112
|
-
grp = QGroupBox("Morphological Parameters")
|
|
114
|
+
grp = QGroupBox(self.tr("Morphological Parameters"))
|
|
113
115
|
grid = QGridLayout(grp)
|
|
114
116
|
self.cb_op = QComboBox(); self.cb_op.addItems(self.OPS)
|
|
115
117
|
self.sp_kernel = QSpinBox(); self.sp_kernel.setRange(1, 31); self.sp_kernel.setSingleStep(2)
|
|
@@ -125,9 +127,9 @@ class MorphologyDialogPro(QDialog):
|
|
|
125
127
|
self.sp_kernel.valueChanged.connect(self._debounce)
|
|
126
128
|
self.sp_iter.valueChanged.connect(self._debounce)
|
|
127
129
|
|
|
128
|
-
grid.addWidget(QLabel("Operation:"), 0, 0); grid.addWidget(self.cb_op, 0, 1, 1, 2)
|
|
129
|
-
grid.addWidget(QLabel("Kernel size:"), 1, 0); grid.addWidget(self.sp_kernel, 1, 1)
|
|
130
|
-
grid.addWidget(QLabel("Iterations:"), 2, 0); grid.addWidget(self.sp_iter, 2, 1)
|
|
130
|
+
grid.addWidget(QLabel(self.tr("Operation:")), 0, 0); grid.addWidget(self.cb_op, 0, 1, 1, 2)
|
|
131
|
+
grid.addWidget(QLabel(self.tr("Kernel size:")), 1, 0); grid.addWidget(self.sp_kernel, 1, 1)
|
|
132
|
+
grid.addWidget(QLabel(self.tr("Iterations:")), 2, 0); grid.addWidget(self.sp_iter, 2, 1)
|
|
131
133
|
v.addWidget(grp)
|
|
132
134
|
|
|
133
135
|
# ---- Preview with zoom/pan ----
|
|
@@ -158,9 +160,9 @@ class MorphologyDialogPro(QDialog):
|
|
|
158
160
|
|
|
159
161
|
# ---- Buttons (unchanged) ----
|
|
160
162
|
row = QHBoxLayout()
|
|
161
|
-
btn_apply = QPushButton("Apply"); btn_apply.clicked.connect(self._apply)
|
|
162
|
-
btn_reset = QPushButton("Reset"); btn_reset.clicked.connect(self._reset)
|
|
163
|
-
btn_cancel= QPushButton("Cancel"); btn_cancel.clicked.connect(self.reject)
|
|
163
|
+
btn_apply = QPushButton(self.tr("Apply")); btn_apply.clicked.connect(self._apply)
|
|
164
|
+
btn_reset = QPushButton(self.tr("Reset")); btn_reset.clicked.connect(self._reset)
|
|
165
|
+
btn_cancel= QPushButton(self.tr("Cancel")); btn_cancel.clicked.connect(self.reject)
|
|
164
166
|
row.addStretch(1); row.addWidget(btn_apply); row.addWidget(btn_reset); row.addWidget(btn_cancel)
|
|
165
167
|
v.addLayout(row)
|
|
166
168
|
|
|
@@ -258,10 +260,33 @@ class MorphologyDialogPro(QDialog):
|
|
|
258
260
|
pass
|
|
259
261
|
# ────────────────────────────────────────────────────────────
|
|
260
262
|
|
|
261
|
-
|
|
263
|
+
# Dialog stays open so user can apply to other images
|
|
264
|
+
# Refresh document reference for next operation
|
|
265
|
+
self._refresh_document_from_active()
|
|
262
266
|
except Exception as e:
|
|
263
267
|
QMessageBox.critical(self, "Morphology", f"Failed to apply:\n{e}")
|
|
264
268
|
|
|
269
|
+
def _refresh_document_from_active(self):
|
|
270
|
+
"""
|
|
271
|
+
Refresh the dialog's document reference to the currently active document.
|
|
272
|
+
This allows reusing the same dialog on different images.
|
|
273
|
+
"""
|
|
274
|
+
try:
|
|
275
|
+
main = self.parent()
|
|
276
|
+
if main and hasattr(main, "_active_doc"):
|
|
277
|
+
new_doc = main._active_doc()
|
|
278
|
+
if new_doc is not None and new_doc is not self.doc:
|
|
279
|
+
self.doc = new_doc
|
|
280
|
+
# Refresh preview for new document
|
|
281
|
+
self.orig = np.clip(np.asarray(new_doc.image, dtype=np.float32), 0.0, 1.0)
|
|
282
|
+
disp = self.orig
|
|
283
|
+
if disp.ndim == 2: disp = disp[..., None].repeat(3, axis=2)
|
|
284
|
+
elif disp.ndim == 3 and disp.shape[2] == 1: disp = disp.repeat(3, axis=2)
|
|
285
|
+
self._disp_base = disp
|
|
286
|
+
self._update_preview()
|
|
287
|
+
except Exception:
|
|
288
|
+
pass
|
|
289
|
+
|
|
265
290
|
|
|
266
291
|
|
|
267
292
|
def _reset(self):
|