setiastrosuitepro 1.6.5.post3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- setiastro/__init__.py +2 -0
- 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/__init__.py +20 -0
- setiastro/saspro/__main__.py +958 -0
- setiastro/saspro/_generated/__init__.py +7 -0
- setiastro/saspro/_generated/build_info.py +3 -0
- setiastro/saspro/abe.py +1346 -0
- setiastro/saspro/abe_preset.py +196 -0
- setiastro/saspro/aberration_ai.py +698 -0
- setiastro/saspro/aberration_ai_preset.py +224 -0
- setiastro/saspro/accel_installer.py +218 -0
- setiastro/saspro/accel_workers.py +30 -0
- setiastro/saspro/add_stars.py +624 -0
- setiastro/saspro/astrobin_exporter.py +1010 -0
- setiastro/saspro/astrospike.py +153 -0
- setiastro/saspro/astrospike_python.py +1841 -0
- setiastro/saspro/autostretch.py +198 -0
- setiastro/saspro/backgroundneutral.py +611 -0
- setiastro/saspro/batch_convert.py +328 -0
- setiastro/saspro/batch_renamer.py +522 -0
- setiastro/saspro/blemish_blaster.py +491 -0
- setiastro/saspro/blink_comparator_pro.py +3149 -0
- setiastro/saspro/bundles.py +61 -0
- setiastro/saspro/bundles_dock.py +114 -0
- setiastro/saspro/cheat_sheet.py +213 -0
- setiastro/saspro/clahe.py +368 -0
- setiastro/saspro/comet_stacking.py +1442 -0
- setiastro/saspro/common_tr.py +107 -0
- setiastro/saspro/config.py +38 -0
- setiastro/saspro/config_bootstrap.py +40 -0
- setiastro/saspro/config_manager.py +316 -0
- setiastro/saspro/continuum_subtract.py +1617 -0
- setiastro/saspro/convo.py +1400 -0
- setiastro/saspro/convo_preset.py +414 -0
- setiastro/saspro/copyastro.py +190 -0
- setiastro/saspro/cosmicclarity.py +1589 -0
- setiastro/saspro/cosmicclarity_preset.py +407 -0
- setiastro/saspro/crop_dialog_pro.py +983 -0
- setiastro/saspro/crop_preset.py +189 -0
- setiastro/saspro/curve_editor_pro.py +2562 -0
- setiastro/saspro/curves_preset.py +375 -0
- setiastro/saspro/debayer.py +673 -0
- setiastro/saspro/debug_utils.py +29 -0
- setiastro/saspro/dnd_mime.py +35 -0
- setiastro/saspro/doc_manager.py +2664 -0
- setiastro/saspro/exoplanet_detector.py +2166 -0
- setiastro/saspro/file_utils.py +284 -0
- setiastro/saspro/fitsmodifier.py +748 -0
- setiastro/saspro/fix_bom.py +32 -0
- setiastro/saspro/free_torch_memory.py +48 -0
- setiastro/saspro/frequency_separation.py +1349 -0
- setiastro/saspro/function_bundle.py +1596 -0
- setiastro/saspro/generate_translations.py +3092 -0
- setiastro/saspro/ghs_dialog_pro.py +663 -0
- setiastro/saspro/ghs_preset.py +284 -0
- setiastro/saspro/graxpert.py +637 -0
- setiastro/saspro/graxpert_preset.py +287 -0
- setiastro/saspro/gui/__init__.py +0 -0
- setiastro/saspro/gui/main_window.py +8792 -0
- setiastro/saspro/gui/mixins/__init__.py +33 -0
- setiastro/saspro/gui/mixins/dock_mixin.py +375 -0
- setiastro/saspro/gui/mixins/file_mixin.py +450 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +503 -0
- setiastro/saspro/gui/mixins/header_mixin.py +441 -0
- setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
- setiastro/saspro/gui/mixins/menu_mixin.py +390 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +1619 -0
- setiastro/saspro/gui/mixins/update_mixin.py +323 -0
- setiastro/saspro/gui/mixins/view_mixin.py +435 -0
- setiastro/saspro/gui/statistics_dialog.py +47 -0
- setiastro/saspro/halobgon.py +488 -0
- setiastro/saspro/header_viewer.py +448 -0
- setiastro/saspro/headless_utils.py +88 -0
- setiastro/saspro/histogram.py +756 -0
- setiastro/saspro/history_explorer.py +941 -0
- setiastro/saspro/i18n.py +168 -0
- setiastro/saspro/image_combine.py +417 -0
- setiastro/saspro/image_peeker_pro.py +1604 -0
- setiastro/saspro/imageops/__init__.py +37 -0
- setiastro/saspro/imageops/mdi_snap.py +292 -0
- setiastro/saspro/imageops/scnr.py +36 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
- setiastro/saspro/imageops/stretch.py +236 -0
- setiastro/saspro/isophote.py +1182 -0
- setiastro/saspro/layers.py +208 -0
- setiastro/saspro/layers_dock.py +714 -0
- setiastro/saspro/lazy_imports.py +193 -0
- setiastro/saspro/legacy/__init__.py +2 -0
- setiastro/saspro/legacy/image_manager.py +2360 -0
- setiastro/saspro/legacy/numba_utils.py +3676 -0
- setiastro/saspro/legacy/xisf.py +1213 -0
- setiastro/saspro/linear_fit.py +537 -0
- setiastro/saspro/live_stacking.py +1854 -0
- setiastro/saspro/log_bus.py +5 -0
- setiastro/saspro/logging_config.py +460 -0
- setiastro/saspro/luminancerecombine.py +510 -0
- setiastro/saspro/main_helpers.py +201 -0
- setiastro/saspro/mask_creation.py +1086 -0
- setiastro/saspro/masks_core.py +56 -0
- setiastro/saspro/mdi_widgets.py +353 -0
- setiastro/saspro/memory_utils.py +666 -0
- setiastro/saspro/metadata_patcher.py +75 -0
- setiastro/saspro/mfdeconv.py +3909 -0
- setiastro/saspro/mfdeconv_earlystop.py +71 -0
- setiastro/saspro/mfdeconvcudnn.py +3312 -0
- setiastro/saspro/mfdeconvsport.py +2459 -0
- setiastro/saspro/minorbodycatalog.py +567 -0
- setiastro/saspro/morphology.py +407 -0
- setiastro/saspro/multiscale_decomp.py +1747 -0
- setiastro/saspro/nbtorgb_stars.py +541 -0
- setiastro/saspro/numba_utils.py +3145 -0
- setiastro/saspro/numba_warmup.py +141 -0
- setiastro/saspro/ops/__init__.py +9 -0
- setiastro/saspro/ops/command_help_dialog.py +623 -0
- setiastro/saspro/ops/command_runner.py +217 -0
- setiastro/saspro/ops/commands.py +1594 -0
- setiastro/saspro/ops/script_editor.py +1105 -0
- setiastro/saspro/ops/scripts.py +1476 -0
- setiastro/saspro/ops/settings.py +637 -0
- setiastro/saspro/parallel_utils.py +554 -0
- setiastro/saspro/pedestal.py +121 -0
- setiastro/saspro/perfect_palette_picker.py +1105 -0
- setiastro/saspro/pipeline.py +110 -0
- setiastro/saspro/pixelmath.py +1604 -0
- setiastro/saspro/plate_solver.py +2445 -0
- setiastro/saspro/project_io.py +797 -0
- setiastro/saspro/psf_utils.py +136 -0
- setiastro/saspro/psf_viewer.py +549 -0
- setiastro/saspro/pyi_rthook_astroquery.py +95 -0
- setiastro/saspro/remove_green.py +331 -0
- setiastro/saspro/remove_stars.py +1599 -0
- setiastro/saspro/remove_stars_preset.py +446 -0
- setiastro/saspro/resources.py +503 -0
- setiastro/saspro/rgb_combination.py +208 -0
- setiastro/saspro/rgb_extract.py +19 -0
- setiastro/saspro/rgbalign.py +723 -0
- setiastro/saspro/runtime_imports.py +7 -0
- setiastro/saspro/runtime_torch.py +754 -0
- setiastro/saspro/save_options.py +73 -0
- setiastro/saspro/selective_color.py +1611 -0
- setiastro/saspro/sfcc.py +1472 -0
- setiastro/saspro/shortcuts.py +3116 -0
- setiastro/saspro/signature_insert.py +1102 -0
- setiastro/saspro/stacking_suite.py +19066 -0
- setiastro/saspro/star_alignment.py +7380 -0
- setiastro/saspro/star_alignment_preset.py +329 -0
- setiastro/saspro/star_metrics.py +49 -0
- setiastro/saspro/star_spikes.py +765 -0
- setiastro/saspro/star_stretch.py +507 -0
- setiastro/saspro/stat_stretch.py +538 -0
- setiastro/saspro/status_log_dock.py +78 -0
- setiastro/saspro/subwindow.py +3407 -0
- setiastro/saspro/supernovaasteroidhunter.py +1719 -0
- setiastro/saspro/swap_manager.py +134 -0
- setiastro/saspro/torch_backend.py +89 -0
- setiastro/saspro/torch_rejection.py +434 -0
- 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 +1558 -0
- setiastro/saspro/wavescale_hdr.py +645 -0
- setiastro/saspro/wavescale_hdr_preset.py +101 -0
- setiastro/saspro/wavescalede.py +680 -0
- setiastro/saspro/wavescalede_preset.py +230 -0
- setiastro/saspro/wcs_update.py +374 -0
- setiastro/saspro/whitebalance.py +513 -0
- setiastro/saspro/widgets/__init__.py +48 -0
- setiastro/saspro/widgets/common_utilities.py +306 -0
- setiastro/saspro/widgets/graphics_views.py +122 -0
- setiastro/saspro/widgets/image_utils.py +518 -0
- 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 +280 -0
- setiastro/saspro/widgets/resource_monitor.py +263 -0
- setiastro/saspro/widgets/spinboxes.py +290 -0
- setiastro/saspro/widgets/themed_buttons.py +13 -0
- setiastro/saspro/widgets/wavelet_utils.py +331 -0
- setiastro/saspro/wimi.py +7996 -0
- setiastro/saspro/wims.py +578 -0
- setiastro/saspro/window_shelf.py +185 -0
- setiastro/saspro/xisf.py +1213 -0
- setiastrosuitepro-1.6.5.post3.dist-info/METADATA +278 -0
- setiastrosuitepro-1.6.5.post3.dist-info/RECORD +368 -0
- setiastrosuitepro-1.6.5.post3.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.5.post3.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.5.post3.dist-info/licenses/LICENSE +674 -0
- setiastrosuitepro-1.6.5.post3.dist-info/licenses/license.txt +2580 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
# pro/star_stretch.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import os
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QEvent, QPointF
|
|
7
|
+
from PyQt6.QtWidgets import (
|
|
8
|
+
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QSlider, QCheckBox,
|
|
9
|
+
QPushButton, QScrollArea, QWidget, QMessageBox
|
|
10
|
+
)
|
|
11
|
+
from PyQt6.QtGui import QPixmap, QImage, QMovie
|
|
12
|
+
from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
|
|
13
|
+
|
|
14
|
+
# Shared utilities
|
|
15
|
+
from setiastro.saspro.widgets.image_utils import to_float01 as _to_float01
|
|
16
|
+
|
|
17
|
+
# --- use your Numba kernels; fall back to pure numpy SCNR if needed ----
|
|
18
|
+
try:
|
|
19
|
+
from setiastro.saspro.legacy.numba_utils import applyPixelMath_numba, applySCNR_numba
|
|
20
|
+
_HAS_NUMBA = True
|
|
21
|
+
except Exception:
|
|
22
|
+
_HAS_NUMBA = False
|
|
23
|
+
# Fallback SCNR (Average Neutral) if legacy.numba_utils is unavailable
|
|
24
|
+
def applySCNR_numba(image_array: np.ndarray) -> np.ndarray:
|
|
25
|
+
img = image_array.astype(np.float32, copy=False)
|
|
26
|
+
if img.ndim != 3 or img.shape[2] != 3:
|
|
27
|
+
return img
|
|
28
|
+
r = img[..., 0]; g = img[..., 1]; b = img[..., 2]
|
|
29
|
+
g2 = np.minimum(g, 0.5 * (r + b))
|
|
30
|
+
out = img.copy()
|
|
31
|
+
out[..., 1] = g2
|
|
32
|
+
return np.clip(out, 0.0, 1.0)
|
|
33
|
+
|
|
34
|
+
# ---- small helpers --------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
def _as_qimage_rgb8(float01: np.ndarray) -> QImage:
|
|
37
|
+
f = np.asarray(float01, dtype=np.float32)
|
|
38
|
+
|
|
39
|
+
# Ensure 3-channel RGB for preview
|
|
40
|
+
if f.ndim == 2:
|
|
41
|
+
f = np.stack([f]*3, axis=-1)
|
|
42
|
+
elif f.ndim == 3 and f.shape[2] == 1:
|
|
43
|
+
f = np.repeat(f, 3, axis=2)
|
|
44
|
+
|
|
45
|
+
# [0,1] -> uint8 and force C-contiguous
|
|
46
|
+
buf8 = (np.clip(f, 0.0, 1.0) * 255.0).astype(np.uint8, copy=False)
|
|
47
|
+
buf8 = np.ascontiguousarray(buf8)
|
|
48
|
+
h, w, _ = buf8.shape
|
|
49
|
+
bpl = int(buf8.strides[0])
|
|
50
|
+
|
|
51
|
+
# Prefer zero-copy via sip pointer if available; fall back to bytes
|
|
52
|
+
try:
|
|
53
|
+
from PyQt6 import sip
|
|
54
|
+
qimg = QImage(sip.voidptr(buf8.ctypes.data), w, h, bpl, QImage.Format.Format_RGB888)
|
|
55
|
+
qimg._keepalive = buf8 # keep numpy alive while qimg exists
|
|
56
|
+
return qimg.copy() # detach so Qt owns the pixels (safe for QPixmap.fromImage)
|
|
57
|
+
except Exception:
|
|
58
|
+
data = buf8.tobytes()
|
|
59
|
+
qimg = QImage(data, w, h, bpl, QImage.Format.Format_RGB888)
|
|
60
|
+
return qimg.copy() # detach to avoid lifetime issues
|
|
61
|
+
|
|
62
|
+
def _saturation_boost(rgb01: np.ndarray, amount: float) -> np.ndarray:
|
|
63
|
+
"""
|
|
64
|
+
Fast saturation-like boost without HSV dependency:
|
|
65
|
+
C' = mean + (C - mean) * amount
|
|
66
|
+
"""
|
|
67
|
+
if rgb01.ndim != 3 or rgb01.shape[2] != 3:
|
|
68
|
+
return rgb01
|
|
69
|
+
mean = rgb01.mean(axis=2, keepdims=True)
|
|
70
|
+
out = mean + (rgb01 - mean) * float(amount)
|
|
71
|
+
return np.clip(out, 0.0, 1.0)
|
|
72
|
+
|
|
73
|
+
# ---- background thread ----------------------------------------------------
|
|
74
|
+
|
|
75
|
+
class _StarStretchWorker(QThread):
|
|
76
|
+
preview_ready = pyqtSignal(object) # np.ndarray float32 0..1
|
|
77
|
+
|
|
78
|
+
def __init__(self, image: np.ndarray, stretch_factor: float, sat_amount: float, do_scnr: bool):
|
|
79
|
+
super().__init__()
|
|
80
|
+
self.image = image
|
|
81
|
+
self.stretch_factor = float(stretch_factor) # this is the "amount" for your pixel math
|
|
82
|
+
self.sat_amount = float(sat_amount)
|
|
83
|
+
self.do_scnr = bool(do_scnr)
|
|
84
|
+
|
|
85
|
+
def run(self):
|
|
86
|
+
imgf = _to_float01(self.image)
|
|
87
|
+
if imgf is None:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# If grayscale, make it 3-channel to keep the kernels happy, then restore shape
|
|
91
|
+
orig_ndim = imgf.ndim
|
|
92
|
+
need_collapse = False
|
|
93
|
+
if imgf.ndim == 2:
|
|
94
|
+
imgf = np.stack([imgf]*3, axis=-1)
|
|
95
|
+
need_collapse = True
|
|
96
|
+
elif imgf.ndim == 3 and imgf.shape[2] == 1:
|
|
97
|
+
imgf = np.repeat(imgf, 3, axis=2)
|
|
98
|
+
need_collapse = True
|
|
99
|
+
|
|
100
|
+
# --- Star Stretch: your Numba pixel math ---
|
|
101
|
+
# amount maps to the SASv2 slider (0..8); kernel uses: f=3**amount
|
|
102
|
+
out = applyPixelMath_numba(imgf.astype(np.float32, copy=False), self.stretch_factor)
|
|
103
|
+
|
|
104
|
+
# --- Optional saturation (RGB only) ---
|
|
105
|
+
if out.ndim == 3 and out.shape[2] == 3 and abs(self.sat_amount - 1.0) > 1e-6:
|
|
106
|
+
out = _saturation_boost(out, self.sat_amount)
|
|
107
|
+
|
|
108
|
+
# --- Optional SCNR (Average Neutral via your Numba kernel) ---
|
|
109
|
+
if self.do_scnr and out.ndim == 3 and out.shape[2] == 3:
|
|
110
|
+
out = applySCNR_numba(out.astype(np.float32, copy=False))
|
|
111
|
+
|
|
112
|
+
# collapse back to mono if we expanded earlier
|
|
113
|
+
if need_collapse:
|
|
114
|
+
out = out[..., 0]
|
|
115
|
+
|
|
116
|
+
self.preview_ready.emit(out.astype(np.float32, copy=False))
|
|
117
|
+
|
|
118
|
+
# ---- dialog ---------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
class StarStretchDialog(QDialog):
|
|
121
|
+
"""
|
|
122
|
+
Star Stretch for SASpro.
|
|
123
|
+
- Works on active ImageDocument (passed in).
|
|
124
|
+
- Preview is computed in background thread.
|
|
125
|
+
- 'Apply to Document' records history via doc.apply_edit(..., step_name="Star Stretch").
|
|
126
|
+
"""
|
|
127
|
+
def __init__(self, parent, document):
|
|
128
|
+
super().__init__(parent)
|
|
129
|
+
self.setWindowTitle(self.tr("Star Stretch"))
|
|
130
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
131
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
132
|
+
self.setModal(False)
|
|
133
|
+
self._main = parent
|
|
134
|
+
self.doc = document
|
|
135
|
+
self._preview: np.ndarray | None = None
|
|
136
|
+
|
|
137
|
+
# Connect to active document change signal
|
|
138
|
+
if hasattr(self._main, "currentDocumentChanged"):
|
|
139
|
+
self._main.currentDocumentChanged.connect(self._on_active_doc_changed)
|
|
140
|
+
self._pix: QPixmap | None = None
|
|
141
|
+
self._zoom = 0.25
|
|
142
|
+
self._panning = False
|
|
143
|
+
self._pan_start = QPointF()
|
|
144
|
+
self._apply_when_ready = False
|
|
145
|
+
|
|
146
|
+
# UI
|
|
147
|
+
main = QHBoxLayout(self)
|
|
148
|
+
|
|
149
|
+
# Left column (controls)
|
|
150
|
+
left = QVBoxLayout()
|
|
151
|
+
info = QLabel(
|
|
152
|
+
"Instructions:\n"
|
|
153
|
+
"1) Adjust stretch and options.\n"
|
|
154
|
+
"2) Preview the result.\n"
|
|
155
|
+
"3) Apply to the current document."
|
|
156
|
+
)
|
|
157
|
+
info.setWordWrap(True)
|
|
158
|
+
left.addWidget(info)
|
|
159
|
+
|
|
160
|
+
# Stretch slider (0..8.00)
|
|
161
|
+
self.lbl_st = QLabel(self.tr("Stretch Amount:") + " 5.00")
|
|
162
|
+
self.sld_st = QSlider(Qt.Orientation.Horizontal)
|
|
163
|
+
self.sld_st.setRange(0, 800)
|
|
164
|
+
self.sld_st.setValue(500)
|
|
165
|
+
self.sld_st.valueChanged.connect(self._on_stretch_changed)
|
|
166
|
+
left.addWidget(self.lbl_st)
|
|
167
|
+
left.addWidget(self.sld_st)
|
|
168
|
+
|
|
169
|
+
# Saturation slider (0..2.00)
|
|
170
|
+
self.lbl_sat = QLabel(self.tr("Color Boost:") + " 1.00")
|
|
171
|
+
self.sld_sat = QSlider(Qt.Orientation.Horizontal)
|
|
172
|
+
self.sld_sat.setRange(0, 200)
|
|
173
|
+
self.sld_sat.setValue(100)
|
|
174
|
+
self.sld_sat.valueChanged.connect(self._on_sat_changed)
|
|
175
|
+
left.addWidget(self.lbl_sat)
|
|
176
|
+
left.addWidget(self.sld_sat)
|
|
177
|
+
|
|
178
|
+
# SCNR checkbox
|
|
179
|
+
self.chk_scnr = QCheckBox(self.tr("Remove Green via SCNR (Optional)"))
|
|
180
|
+
left.addWidget(self.chk_scnr)
|
|
181
|
+
|
|
182
|
+
# Buttons row
|
|
183
|
+
rowb = QHBoxLayout()
|
|
184
|
+
self.btn_preview = QPushButton(self.tr("Preview"))
|
|
185
|
+
self.btn_apply = QPushButton(self.tr("Apply to Document"))
|
|
186
|
+
rowb.addWidget(self.btn_preview)
|
|
187
|
+
rowb.addWidget(self.btn_apply)
|
|
188
|
+
left.addLayout(rowb)
|
|
189
|
+
|
|
190
|
+
# Spinner
|
|
191
|
+
self.lbl_spin = QLabel()
|
|
192
|
+
self.lbl_spin.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
193
|
+
self.lbl_spin.hide()
|
|
194
|
+
spinner_gif = _guess_spinner_path()
|
|
195
|
+
if spinner_gif and os.path.exists(spinner_gif):
|
|
196
|
+
mv = QMovie(spinner_gif)
|
|
197
|
+
self.lbl_spin.setMovie(mv)
|
|
198
|
+
self._spinner = mv
|
|
199
|
+
else:
|
|
200
|
+
self._spinner = None
|
|
201
|
+
left.addWidget(self.lbl_spin)
|
|
202
|
+
|
|
203
|
+
left.addStretch(1)
|
|
204
|
+
main.addLayout(left, 0)
|
|
205
|
+
|
|
206
|
+
# Right column (preview with zoom/pan)
|
|
207
|
+
right = QVBoxLayout()
|
|
208
|
+
zoombar = QHBoxLayout()
|
|
209
|
+
b_out = QPushButton(self.tr("Zoom Out"))
|
|
210
|
+
b_in = QPushButton(self.tr("Zoom In"))
|
|
211
|
+
b_fit = QPushButton(self.tr("Fit to Preview"))
|
|
212
|
+
b_out.clicked.connect(self._zoom_out)
|
|
213
|
+
b_in.clicked.connect(self._zoom_in)
|
|
214
|
+
b_fit.clicked.connect(self._fit)
|
|
215
|
+
zoombar.addWidget(b_out); zoombar.addWidget(b_in); zoombar.addWidget(b_fit)
|
|
216
|
+
right.addLayout(zoombar)
|
|
217
|
+
|
|
218
|
+
self.scroll = QScrollArea()
|
|
219
|
+
self.scroll.setWidgetResizable(True)
|
|
220
|
+
self.scroll.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
221
|
+
self.scroll.viewport().installEventFilter(self)
|
|
222
|
+
|
|
223
|
+
self.label = QLabel(alignment=Qt.AlignmentFlag.AlignCenter)
|
|
224
|
+
self.scroll.setWidget(self.label)
|
|
225
|
+
|
|
226
|
+
right.addWidget(self.scroll, 1)
|
|
227
|
+
main.addLayout(right, 1)
|
|
228
|
+
|
|
229
|
+
# signals
|
|
230
|
+
self.btn_preview.clicked.connect(self._run_preview)
|
|
231
|
+
self.btn_apply.clicked.connect(self._apply_to_doc)
|
|
232
|
+
|
|
233
|
+
# initialize preview with current doc image
|
|
234
|
+
self._update_preview_pix(self.doc.image)
|
|
235
|
+
|
|
236
|
+
# --- active document change ---
|
|
237
|
+
def _on_active_doc_changed(self, doc):
|
|
238
|
+
"""Called when user clicks a different image window."""
|
|
239
|
+
if doc is None or getattr(doc, "image", None) is None:
|
|
240
|
+
return
|
|
241
|
+
self.doc = doc
|
|
242
|
+
self._preview = None
|
|
243
|
+
self._update_preview_pix(self.doc.image)
|
|
244
|
+
|
|
245
|
+
# --- UI change handlers ---
|
|
246
|
+
def _on_stretch_changed(self, v: int):
|
|
247
|
+
self.lbl_st.setText(f"Stretch Amount: {v/100.0:.2f}")
|
|
248
|
+
|
|
249
|
+
def _on_sat_changed(self, v: int):
|
|
250
|
+
self.lbl_sat.setText(f"Color Boost: {v/100.0:.2f}")
|
|
251
|
+
|
|
252
|
+
# --- preview / processing ---
|
|
253
|
+
def _run_preview(self):
|
|
254
|
+
img = self.doc.image
|
|
255
|
+
if img is None:
|
|
256
|
+
QMessageBox.information(self, "No image", "Open an image first.")
|
|
257
|
+
return
|
|
258
|
+
self._show_spinner(True)
|
|
259
|
+
self.btn_preview.setEnabled(False)
|
|
260
|
+
self.btn_apply.setEnabled(False)
|
|
261
|
+
|
|
262
|
+
self._thr = _StarStretchWorker(
|
|
263
|
+
image=img,
|
|
264
|
+
stretch_factor=self.sld_st.value()/100.0,
|
|
265
|
+
sat_amount=self.sld_sat.value()/100.0,
|
|
266
|
+
do_scnr=self.chk_scnr.isChecked()
|
|
267
|
+
)
|
|
268
|
+
self._thr.preview_ready.connect(self._on_preview_ready)
|
|
269
|
+
self._thr.finished.connect(lambda: self._show_spinner(False))
|
|
270
|
+
self._thr.start()
|
|
271
|
+
|
|
272
|
+
def _on_preview_ready(self, out: np.ndarray):
|
|
273
|
+
out_masked = self._blend_with_mask(out)
|
|
274
|
+
self._preview = out_masked
|
|
275
|
+
self.btn_preview.setEnabled(True)
|
|
276
|
+
self.btn_apply.setEnabled(True)
|
|
277
|
+
self._update_preview_pix(out_masked)
|
|
278
|
+
|
|
279
|
+
mw = self._find_main_window()
|
|
280
|
+
if mw and hasattr(mw, "_log"):
|
|
281
|
+
mw._log("Star Stretch: preview generated.")
|
|
282
|
+
|
|
283
|
+
# NEW: if Apply was pressed before preview completed, finish now.
|
|
284
|
+
if self._apply_when_ready:
|
|
285
|
+
self._apply_when_ready = False
|
|
286
|
+
self._finish_apply()
|
|
287
|
+
|
|
288
|
+
def _apply_to_doc(self):
|
|
289
|
+
# If we don't have a preview yet, compute it and auto-apply when ready.
|
|
290
|
+
if self._preview is None:
|
|
291
|
+
if getattr(self, "_thr", None) and self._thr.isRunning():
|
|
292
|
+
# already computing; just mark to apply when it lands
|
|
293
|
+
self._apply_when_ready = True
|
|
294
|
+
return
|
|
295
|
+
self._apply_when_ready = True
|
|
296
|
+
self._run_preview()
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
# We do have a preview → finish immediately
|
|
300
|
+
self._finish_apply()
|
|
301
|
+
|
|
302
|
+
def _finish_apply(self):
|
|
303
|
+
try:
|
|
304
|
+
_marr, mid, mname = self._active_mask_layer()
|
|
305
|
+
meta = {
|
|
306
|
+
"step_name": "Star Stretch",
|
|
307
|
+
"star_stretch": {
|
|
308
|
+
"stretch_factor": self.sld_st.value()/100.0,
|
|
309
|
+
"color_boost": self.sld_sat.value()/100.0,
|
|
310
|
+
"scnr_green": self.chk_scnr.isChecked(),
|
|
311
|
+
"numba": _HAS_NUMBA,
|
|
312
|
+
},
|
|
313
|
+
# ✅ mask bookkeeping
|
|
314
|
+
"masked": bool(mid),
|
|
315
|
+
"mask_id": mid,
|
|
316
|
+
"mask_name": mname,
|
|
317
|
+
"mask_blend": "m*out + (1-m)*src",
|
|
318
|
+
}
|
|
319
|
+
self.doc.apply_edit(self._preview.copy(), metadata=meta, step_name="Star Stretch")
|
|
320
|
+
|
|
321
|
+
mw = self._find_main_window()
|
|
322
|
+
if mw and hasattr(mw, "_log"):
|
|
323
|
+
mw._log("Star Stretch: applied to document.")
|
|
324
|
+
|
|
325
|
+
# 🔁 Record as last headless-style command for Replay
|
|
326
|
+
try:
|
|
327
|
+
if mw and hasattr(mw, "_remember_last_headless_command"):
|
|
328
|
+
preset = {
|
|
329
|
+
"stretch_factor": self.sld_st.value()/100.0,
|
|
330
|
+
"color_boost": self.sld_sat.value()/100.0,
|
|
331
|
+
"scnr_green": self.chk_scnr.isChecked(),
|
|
332
|
+
}
|
|
333
|
+
mw._remember_last_headless_command(
|
|
334
|
+
"star_stretch",
|
|
335
|
+
preset,
|
|
336
|
+
description="Star Stretch",
|
|
337
|
+
)
|
|
338
|
+
except Exception:
|
|
339
|
+
# Don't let replay bookkeeping break the dialog
|
|
340
|
+
pass
|
|
341
|
+
|
|
342
|
+
except Exception as e:
|
|
343
|
+
QMessageBox.critical(self, "Apply failed", str(e))
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
# Dialog stays open so user can apply to other images
|
|
347
|
+
# Refresh document reference for next operation
|
|
348
|
+
self._refresh_document_from_active()
|
|
349
|
+
|
|
350
|
+
def _refresh_document_from_active(self):
|
|
351
|
+
"""
|
|
352
|
+
Refresh the dialog's document reference to the currently active document.
|
|
353
|
+
This allows reusing the same dialog on different images.
|
|
354
|
+
"""
|
|
355
|
+
try:
|
|
356
|
+
main = self._find_main_window()
|
|
357
|
+
if main and hasattr(main, "_active_doc"):
|
|
358
|
+
new_doc = main._active_doc()
|
|
359
|
+
if new_doc is not None and new_doc is not self.doc:
|
|
360
|
+
self.doc = new_doc
|
|
361
|
+
# Reset preview for new document
|
|
362
|
+
self._preview = None
|
|
363
|
+
self._compute_and_show_preview()
|
|
364
|
+
except Exception:
|
|
365
|
+
pass
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# --- preview rendering ---
|
|
369
|
+
def _update_preview_pix(self, img: np.ndarray | None):
|
|
370
|
+
if img is None:
|
|
371
|
+
self.label.clear(); self._pix = None; return
|
|
372
|
+
qimg = _as_qimage_rgb8(_to_float01(img))
|
|
373
|
+
pm = QPixmap.fromImage(qimg)
|
|
374
|
+
self._pix = pm
|
|
375
|
+
self._apply_zoom()
|
|
376
|
+
|
|
377
|
+
def _apply_zoom(self):
|
|
378
|
+
if self._pix is None:
|
|
379
|
+
return
|
|
380
|
+
scaled = self._pix.scaled(self._pix.size()*self._zoom,
|
|
381
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
|
382
|
+
Qt.TransformationMode.SmoothTransformation)
|
|
383
|
+
self.label.setPixmap(scaled)
|
|
384
|
+
self.label.resize(scaled.size())
|
|
385
|
+
|
|
386
|
+
# --- zoom/pan ---
|
|
387
|
+
def _zoom_in(self): self._set_zoom(self._zoom * 1.25)
|
|
388
|
+
def _zoom_out(self): self._set_zoom(self._zoom / 1.25)
|
|
389
|
+
def _fit(self):
|
|
390
|
+
if self._pix is None: return
|
|
391
|
+
vp = self.scroll.viewport().size()
|
|
392
|
+
if self._pix.width()==0 or self._pix.height()==0: return
|
|
393
|
+
s = min(vp.width()/self._pix.width(), vp.height()/self._pix.height())
|
|
394
|
+
self._set_zoom(max(0.05, s))
|
|
395
|
+
|
|
396
|
+
def _set_zoom(self, z: float):
|
|
397
|
+
self._zoom = float(max(0.05, min(z, 8.0)))
|
|
398
|
+
self._apply_zoom()
|
|
399
|
+
|
|
400
|
+
# --- spinner ---
|
|
401
|
+
def _show_spinner(self, on: bool):
|
|
402
|
+
if self._spinner is None:
|
|
403
|
+
self.lbl_spin.setVisible(on)
|
|
404
|
+
return
|
|
405
|
+
if on:
|
|
406
|
+
self.lbl_spin.show(); self._spinner.start()
|
|
407
|
+
else:
|
|
408
|
+
self._spinner.stop(); self.lbl_spin.hide()
|
|
409
|
+
|
|
410
|
+
# --- event filter (wheel zoom + panning) ---
|
|
411
|
+
def eventFilter(self, obj, ev):
|
|
412
|
+
if obj is self.scroll.viewport():
|
|
413
|
+
if ev.type() == QEvent.Type.Wheel and (ev.modifiers() & Qt.KeyboardModifier.ControlModifier):
|
|
414
|
+
self._set_zoom(self._zoom * (1.25 if ev.angleDelta().y() > 0 else 0.8))
|
|
415
|
+
ev.accept(); return True
|
|
416
|
+
if ev.type() == QEvent.Type.MouseButtonPress and ev.button() == Qt.MouseButton.LeftButton:
|
|
417
|
+
self._panning = True; self._pan_start = ev.position()
|
|
418
|
+
self.scroll.viewport().setCursor(Qt.CursorShape.ClosedHandCursor)
|
|
419
|
+
ev.accept(); return True
|
|
420
|
+
if ev.type() == QEvent.Type.MouseMove and self._panning:
|
|
421
|
+
d = ev.position() - self._pan_start
|
|
422
|
+
h = self.scroll.horizontalScrollBar(); v = self.scroll.verticalScrollBar()
|
|
423
|
+
h.setValue(h.value() - int(d.x())); v.setValue(v.value() - int(d.y()))
|
|
424
|
+
self._pan_start = ev.position()
|
|
425
|
+
ev.accept(); return True
|
|
426
|
+
if ev.type() == QEvent.Type.MouseButtonRelease and ev.button() == Qt.MouseButton.LeftButton:
|
|
427
|
+
self._panning = False
|
|
428
|
+
self.scroll.viewport().setCursor(Qt.CursorShape.ArrowCursor)
|
|
429
|
+
ev.accept(); return True
|
|
430
|
+
return super().eventFilter(obj, ev)
|
|
431
|
+
|
|
432
|
+
# --- helper ---
|
|
433
|
+
def _find_main_window(self):
|
|
434
|
+
p = self.parent()
|
|
435
|
+
while p is not None and not hasattr(p, "docman"):
|
|
436
|
+
p = p.parent()
|
|
437
|
+
return p
|
|
438
|
+
|
|
439
|
+
# --- mask helpers ---------------------------------------------------
|
|
440
|
+
def _active_mask_layer(self):
|
|
441
|
+
"""Return (mask_array_float01, mask_id, mask_name) or (None, None, None)."""
|
|
442
|
+
doc = self.doc
|
|
443
|
+
mid = getattr(doc, "active_mask_id", None)
|
|
444
|
+
if not mid:
|
|
445
|
+
return None, None, None
|
|
446
|
+
layer = getattr(doc, "masks", {}).get(mid)
|
|
447
|
+
if layer is None:
|
|
448
|
+
return None, None, None
|
|
449
|
+
m = np.asarray(getattr(layer, "data", None), dtype=np.float32)
|
|
450
|
+
if m is None or m.size == 0:
|
|
451
|
+
return None, None, None
|
|
452
|
+
# ensure [0..1]
|
|
453
|
+
if m.dtype.kind in "ui":
|
|
454
|
+
m = m / float(np.iinfo(m.dtype).max)
|
|
455
|
+
else:
|
|
456
|
+
mx = float(m.max()) if m.size else 1.0
|
|
457
|
+
if mx > 1.0:
|
|
458
|
+
m = m / mx
|
|
459
|
+
m = np.clip(m, 0.0, 1.0)
|
|
460
|
+
return m, mid, getattr(layer, "name", "Mask")
|
|
461
|
+
|
|
462
|
+
def _resample_mask_if_needed(self, mask: np.ndarray, out_hw: tuple[int,int]) -> np.ndarray:
|
|
463
|
+
"""Nearest-neighbor resize using integer indexing (fast, dependency-free)."""
|
|
464
|
+
mh, mw = mask.shape[:2]
|
|
465
|
+
th, tw = out_hw
|
|
466
|
+
if (mh, mw) == (th, tw):
|
|
467
|
+
return mask
|
|
468
|
+
yi = np.linspace(0, mh - 1, th).astype(np.int32)
|
|
469
|
+
xi = np.linspace(0, mw - 1, tw).astype(np.int32)
|
|
470
|
+
return mask[yi][:, xi]
|
|
471
|
+
|
|
472
|
+
def _blend_with_mask(self, stretched: np.ndarray) -> np.ndarray:
|
|
473
|
+
"""Blend preview/apply with original using active mask if present."""
|
|
474
|
+
mask, _mid, _name = self._active_mask_layer()
|
|
475
|
+
if mask is None:
|
|
476
|
+
return stretched
|
|
477
|
+
src = _to_float01(self.doc.image)
|
|
478
|
+
out = stretched.astype(np.float32, copy=False)
|
|
479
|
+
|
|
480
|
+
# Make sure spatial size matches mask
|
|
481
|
+
th, tw = out.shape[:2]
|
|
482
|
+
m = self._resample_mask_if_needed(mask, (th, tw))
|
|
483
|
+
|
|
484
|
+
# Broadcast mask to 3ch when needed
|
|
485
|
+
if out.ndim == 3 and out.shape[2] == 3:
|
|
486
|
+
m = m[..., None]
|
|
487
|
+
|
|
488
|
+
# If preview changed mono↔RGB shape, match src first
|
|
489
|
+
if src.ndim == 2 and out.ndim == 3 and out.shape[2] == 3:
|
|
490
|
+
src = np.stack([src]*3, axis=-1)
|
|
491
|
+
elif src.ndim == 3 and src.shape[2] == 3 and out.ndim == 2:
|
|
492
|
+
src = src[..., 0] # collapse to mono
|
|
493
|
+
|
|
494
|
+
return (m * out + (1.0 - m) * src).astype(np.float32, copy=False)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _guess_spinner_path() -> str | None:
|
|
498
|
+
here = os.path.dirname(__file__)
|
|
499
|
+
cands = [
|
|
500
|
+
os.path.join(here, "spinner.gif"),
|
|
501
|
+
os.path.join(os.path.dirname(here), "spinner.gif"),
|
|
502
|
+
os.path.join(os.getcwd(), "spinner.gif"),
|
|
503
|
+
]
|
|
504
|
+
for c in cands:
|
|
505
|
+
if os.path.exists(c):
|
|
506
|
+
return c
|
|
507
|
+
return None
|