setiastrosuitepro 1.6.2.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/__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/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 +945 -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 +694 -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 +602 -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 +2926 -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 +973 -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 +8810 -0
- setiastro/saspro/gui/mixins/__init__.py +33 -0
- setiastro/saspro/gui/mixins/dock_mixin.py +362 -0
- setiastro/saspro/gui/mixins/file_mixin.py +450 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +403 -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 +389 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +1457 -0
- setiastro/saspro/gui/mixins/update_mixin.py +309 -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 +2226 -0
- setiastro/saspro/legacy/numba_utils.py +3676 -0
- setiastro/saspro/legacy/xisf.py +1071 -0
- setiastro/saspro/linear_fit.py +537 -0
- setiastro/saspro/live_stacking.py +1841 -0
- setiastro/saspro/log_bus.py +5 -0
- setiastro/saspro/logging_config.py +460 -0
- setiastro/saspro/luminancerecombine.py +309 -0
- setiastro/saspro/main_helpers.py +201 -0
- setiastro/saspro/mask_creation.py +931 -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 +3831 -0
- setiastro/saspro/mfdeconv_earlystop.py +71 -0
- setiastro/saspro/mfdeconvcudnn.py +3263 -0
- setiastro/saspro/mfdeconvsport.py +2382 -0
- setiastro/saspro/minorbodycatalog.py +567 -0
- setiastro/saspro/morphology.py +407 -0
- setiastro/saspro/multiscale_decomp.py +1293 -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 +1102 -0
- setiastro/saspro/ops/scripts.py +1473 -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 +1071 -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 +404 -0
- setiastro/saspro/resources.py +501 -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 +1552 -0
- setiastro/saspro/sfcc.py +1472 -0
- setiastro/saspro/shortcuts.py +3043 -0
- setiastro/saspro/signature_insert.py +1102 -0
- setiastro/saspro/stacking_suite.py +18470 -0
- setiastro/saspro/star_alignment.py +7435 -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 +3328 -0
- setiastro/saspro/supernovaasteroidhunter.py +1719 -0
- setiastro/saspro/swap_manager.py +99 -0
- setiastro/saspro/torch_backend.py +89 -0
- setiastro/saspro/torch_rejection.py +434 -0
- setiastro/saspro/translations/all_source_strings.json +3654 -0
- setiastro/saspro/translations/ar_translations.py +3865 -0
- setiastro/saspro/translations/de_translations.py +3749 -0
- setiastro/saspro/translations/es_translations.py +3939 -0
- setiastro/saspro/translations/fr_translations.py +3858 -0
- setiastro/saspro/translations/hi_translations.py +3571 -0
- setiastro/saspro/translations/integrate_translations.py +270 -0
- setiastro/saspro/translations/it_translations.py +3678 -0
- setiastro/saspro/translations/ja_translations.py +3601 -0
- setiastro/saspro/translations/pt_translations.py +3869 -0
- setiastro/saspro/translations/ru_translations.py +2848 -0
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +255 -0
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +253 -0
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +12520 -0
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +12514 -0
- setiastro/saspro/translations/saspro_hi.qm +0 -0
- setiastro/saspro/translations/saspro_hi.ts +257 -0
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +12520 -0
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +257 -0
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +257 -0
- setiastro/saspro/translations/saspro_ru.qm +0 -0
- setiastro/saspro/translations/saspro_ru.ts +237 -0
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +257 -0
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +10771 -0
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +12520 -0
- setiastro/saspro/translations/sw_translations.py +3671 -0
- setiastro/saspro/translations/uk_translations.py +3700 -0
- setiastro/saspro/translations/zh_translations.py +3675 -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 +492 -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 +986 -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 +237 -0
- setiastro/saspro/widgets/spinboxes.py +275 -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 +1123 -0
- setiastrosuitepro-1.6.2.post1.dist-info/METADATA +278 -0
- setiastrosuitepro-1.6.2.post1.dist-info/RECORD +367 -0
- setiastrosuitepro-1.6.2.post1.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.2.post1.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.2.post1.dist-info/licenses/LICENSE +674 -0
- setiastrosuitepro-1.6.2.post1.dist-info/licenses/license.txt +2580 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
# pro/wavescale_hdr.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from PyQt6.QtCore import Qt, QObject, pyqtSignal, QThread, QTimer, QSettings
|
|
6
|
+
from PyQt6.QtGui import QImage, QPixmap, QIcon
|
|
7
|
+
from PyQt6.QtWidgets import (
|
|
8
|
+
QDialog, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout, QLabel, QPushButton,
|
|
9
|
+
QSlider, QGraphicsScene, QGraphicsPixmapItem, QScrollArea,
|
|
10
|
+
QMessageBox, QProgressBar
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# Import centralized widget
|
|
14
|
+
from setiastro.saspro.widgets.graphics_views import ZoomableGraphicsView
|
|
15
|
+
from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
|
|
16
|
+
|
|
17
|
+
# Import shared wavelet utilities
|
|
18
|
+
from setiastro.saspro.widgets.wavelet_utils import (
|
|
19
|
+
conv_sep_reflect as _conv_sep_reflect,
|
|
20
|
+
build_spaced_kernel as _build_spaced_kernel,
|
|
21
|
+
atrous_decompose as _atrous_decompose,
|
|
22
|
+
atrous_reconstruct as _atrous_reconstruct,
|
|
23
|
+
rgb_to_lab as _rgb_to_lab,
|
|
24
|
+
lab_to_rgb as _lab_to_rgb,
|
|
25
|
+
B3_KERNEL as _B3,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
# Core math (shared by dialog + headless apply)
|
|
30
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
def _mask_from_L(L: np.ndarray, gamma: float) -> np.ndarray:
|
|
33
|
+
m = np.clip(L / 100.0, 0.0, 1.0).astype(np.float32)
|
|
34
|
+
if gamma != 1.0:
|
|
35
|
+
m = np.power(m, gamma, dtype=np.float32)
|
|
36
|
+
return m
|
|
37
|
+
|
|
38
|
+
def _apply_dim_curve(rgb: np.ndarray, gamma: float) -> np.ndarray:
|
|
39
|
+
return np.power(np.clip(rgb, 0.0, 1.0), gamma, dtype=np.float32)
|
|
40
|
+
|
|
41
|
+
def compute_wavescale_hdr(rgb_image: np.ndarray,
|
|
42
|
+
n_scales: int = 5,
|
|
43
|
+
compression_factor: float = 1.5,
|
|
44
|
+
mask_gamma: float = 1.0,
|
|
45
|
+
base_kernel: np.ndarray = _B3,
|
|
46
|
+
decay_rate: float = 0.5) -> tuple[np.ndarray, np.ndarray]:
|
|
47
|
+
"""
|
|
48
|
+
Returns (transformed_rgb, luminance_mask). transformed_rgb is already
|
|
49
|
+
reconstructed from modified L and gamma-dimmed.
|
|
50
|
+
"""
|
|
51
|
+
lab = _rgb_to_lab(rgb_image)
|
|
52
|
+
L0 = lab[..., 0].astype(np.float32, copy=True)
|
|
53
|
+
scales = _atrous_decompose(L0, n_scales, base_kernel)
|
|
54
|
+
|
|
55
|
+
mask = _mask_from_L(L0, mask_gamma)
|
|
56
|
+
planes, residual = scales[:-1], scales[-1]
|
|
57
|
+
|
|
58
|
+
for i, wp in enumerate(planes):
|
|
59
|
+
decay = decay_rate ** i
|
|
60
|
+
scale = (1.0 + (compression_factor - 1.0) * mask * decay) * 2.0
|
|
61
|
+
planes[i] = wp * scale
|
|
62
|
+
|
|
63
|
+
Lr = _atrous_reconstruct(planes + [residual])
|
|
64
|
+
|
|
65
|
+
# midtones alignment
|
|
66
|
+
med0 = float(np.median(L0))
|
|
67
|
+
med1 = float(np.median(Lr)) or 1.0
|
|
68
|
+
Lr = np.clip(Lr * (med0 / med1), 0.0, 100.0)
|
|
69
|
+
|
|
70
|
+
lab[..., 0] = Lr
|
|
71
|
+
rgb = _lab_to_rgb(lab)
|
|
72
|
+
|
|
73
|
+
# gentle dimming curve to tame highlights
|
|
74
|
+
rgb = _apply_dim_curve(rgb, gamma=1.0 + n_scales * 0.2)
|
|
75
|
+
return rgb, mask
|
|
76
|
+
|
|
77
|
+
def compute_wavescale_hdr(rgb_image: np.ndarray,
|
|
78
|
+
n_scales: int = 5,
|
|
79
|
+
compression_factor: float = 1.5,
|
|
80
|
+
mask_gamma: float = 1.0,
|
|
81
|
+
base_kernel: np.ndarray = _B3,
|
|
82
|
+
decay_rate: float = 0.5,
|
|
83
|
+
dim_gamma: float | None = None) -> tuple[np.ndarray, np.ndarray]:
|
|
84
|
+
"""
|
|
85
|
+
Returns (transformed_rgb, luminance_mask).
|
|
86
|
+
If dim_gamma is None, uses auto gamma = 1.0 + 0.2 * n_scales.
|
|
87
|
+
"""
|
|
88
|
+
lab = _rgb_to_lab(rgb_image)
|
|
89
|
+
L0 = lab[..., 0].astype(np.float32, copy=True)
|
|
90
|
+
scales = _atrous_decompose(L0, n_scales, base_kernel)
|
|
91
|
+
|
|
92
|
+
mask = _mask_from_L(L0, mask_gamma)
|
|
93
|
+
planes, residual = scales[:-1], scales[-1]
|
|
94
|
+
|
|
95
|
+
for i, wp in enumerate(planes):
|
|
96
|
+
decay = decay_rate ** i
|
|
97
|
+
scale = (1.0 + (compression_factor - 1.0) * mask * decay) * 2.0
|
|
98
|
+
planes[i] = wp * scale
|
|
99
|
+
|
|
100
|
+
Lr = _atrous_reconstruct(planes + [residual])
|
|
101
|
+
|
|
102
|
+
# midtones alignment
|
|
103
|
+
med0 = float(np.median(L0))
|
|
104
|
+
med1 = float(np.median(Lr)) or 1.0
|
|
105
|
+
Lr = np.clip(Lr * (med0 / med1), 0.0, 100.0)
|
|
106
|
+
|
|
107
|
+
lab[..., 0] = Lr
|
|
108
|
+
rgb = _lab_to_rgb(lab)
|
|
109
|
+
|
|
110
|
+
# dimming curve
|
|
111
|
+
g = (1.0 + n_scales * 0.2) if dim_gamma is None else float(dim_gamma)
|
|
112
|
+
rgb = _apply_dim_curve(rgb, gamma=g)
|
|
113
|
+
return rgb, mask
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
117
|
+
# Worker (QObject in its own QThread) for the dialog
|
|
118
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
class HDRWorker(QObject):
|
|
121
|
+
progress_update = pyqtSignal(str, int) # (step, percent)
|
|
122
|
+
finished = pyqtSignal(np.ndarray, np.ndarray) # (transformed_rgb, mask)
|
|
123
|
+
|
|
124
|
+
def __init__(self, rgb_image: np.ndarray, n_scales: int, compression_factor: float,
|
|
125
|
+
mask_gamma: float, base_kernel: np.ndarray):
|
|
126
|
+
super().__init__()
|
|
127
|
+
self.rgb_image = rgb_image
|
|
128
|
+
self.n_scales = n_scales
|
|
129
|
+
self.compression_factor = compression_factor
|
|
130
|
+
self.mask_gamma = mask_gamma
|
|
131
|
+
self.base_kernel = base_kernel
|
|
132
|
+
|
|
133
|
+
def run(self):
|
|
134
|
+
try:
|
|
135
|
+
self.progress_update.emit(self.tr("Converting to Lab color space…"), 10)
|
|
136
|
+
# progress checkpoints inline here are cosmetic
|
|
137
|
+
self.progress_update.emit(self.tr("Decomposing luminance with starlet…"), 20)
|
|
138
|
+
# full compute
|
|
139
|
+
transformed, mask = compute_wavescale_hdr(
|
|
140
|
+
self.rgb_image, self.n_scales, self.compression_factor, self.mask_gamma, self.base_kernel
|
|
141
|
+
)
|
|
142
|
+
self.progress_update.emit(self.tr("Finalizing…"), 95)
|
|
143
|
+
self.finished.emit(transformed, mask)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
print("WaveScale HDR error:", e)
|
|
146
|
+
self.finished.emit(None, None)
|
|
147
|
+
|
|
148
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
149
|
+
# Simple mask window
|
|
150
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
class MaskDisplayWindow(QDialog):
|
|
153
|
+
def __init__(self, parent=None):
|
|
154
|
+
super().__init__(parent)
|
|
155
|
+
self.setWindowTitle(self.tr("HDR Mask (L-based)"))
|
|
156
|
+
self.lbl = QLabel(alignment=Qt.AlignmentFlag.AlignCenter)
|
|
157
|
+
self.lbl.setFixedSize(400, 400) # keep it small
|
|
158
|
+
lay = QVBoxLayout(self)
|
|
159
|
+
lay.addWidget(self.lbl)
|
|
160
|
+
|
|
161
|
+
def update_mask(self, mask: np.ndarray):
|
|
162
|
+
if mask is None:
|
|
163
|
+
return
|
|
164
|
+
m = np.clip(mask, 0, 1).astype(np.float32)
|
|
165
|
+
m8 = (m * 255.0).astype(np.uint8)
|
|
166
|
+
if m8.ndim == 2:
|
|
167
|
+
h, w = m8.shape
|
|
168
|
+
rgb = np.repeat(m8[..., None], 3, axis=2)
|
|
169
|
+
else:
|
|
170
|
+
h, w, _ = m8.shape
|
|
171
|
+
rgb = m8
|
|
172
|
+
qimg = QImage(rgb.data, w, h, 3*w, QImage.Format.Format_RGB888)
|
|
173
|
+
pix = QPixmap.fromImage(qimg).scaled(
|
|
174
|
+
self.lbl.size(), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation
|
|
175
|
+
)
|
|
176
|
+
self.lbl.setPixmap(pix)
|
|
177
|
+
|
|
178
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
179
|
+
# Dialog
|
|
180
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
class WaveScaleHDRDialogPro(QDialog):
|
|
183
|
+
applied_preset = pyqtSignal(object, dict)
|
|
184
|
+
|
|
185
|
+
def __init__(self, parent, doc, icon_path: str | None = None, *, headless: bool=False, bypass_guard: bool=False):
|
|
186
|
+
super().__init__(parent)
|
|
187
|
+
self.setWindowTitle(self.tr("WaveScale HDR"))
|
|
188
|
+
self._headless = bool(headless)
|
|
189
|
+
self._bypass_guard = bool(bypass_guard)
|
|
190
|
+
if self._headless:
|
|
191
|
+
# Don’t show any windows; we’ll still exec() to run the event loop.
|
|
192
|
+
try: self.setAttribute(Qt.WidgetAttribute.WA_DontShowOnScreen, True)
|
|
193
|
+
except Exception as e:
|
|
194
|
+
import logging
|
|
195
|
+
logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
|
|
196
|
+
if icon_path:
|
|
197
|
+
try: self.setWindowIcon(QIcon(icon_path))
|
|
198
|
+
except Exception as e:
|
|
199
|
+
import logging
|
|
200
|
+
logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
|
|
201
|
+
self.resize(980, 700)
|
|
202
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
203
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
204
|
+
self.setModal(False)
|
|
205
|
+
|
|
206
|
+
self._doc = doc
|
|
207
|
+
base = getattr(doc, "image", None)
|
|
208
|
+
if base is None:
|
|
209
|
+
raise RuntimeError("Active document has no image.")
|
|
210
|
+
|
|
211
|
+
# normalize to float32 [0..1] RGB for processing/preview
|
|
212
|
+
img = np.asarray(base, dtype=np.float32)
|
|
213
|
+
if img.ndim == 2:
|
|
214
|
+
img_rgb = np.repeat(img[:, :, None], 3, axis=2)
|
|
215
|
+
self._was_mono = True
|
|
216
|
+
self._mono_shape = img.shape
|
|
217
|
+
elif img.ndim == 3 and img.shape[2] == 1:
|
|
218
|
+
img_rgb = np.repeat(img, 3, axis=2)
|
|
219
|
+
self._was_mono = True
|
|
220
|
+
self._mono_shape = img.shape
|
|
221
|
+
else:
|
|
222
|
+
img_rgb = img[:, :, :3]
|
|
223
|
+
self._was_mono = False
|
|
224
|
+
self._mono_shape = None
|
|
225
|
+
|
|
226
|
+
if img.dtype.kind in "ui":
|
|
227
|
+
maxv = float(np.nanmax(img_rgb)) or 1.0
|
|
228
|
+
img_rgb = img_rgb / max(1.0, maxv)
|
|
229
|
+
img_rgb = np.clip(img_rgb, 0.0, 1.0).astype(np.float32, copy=False)
|
|
230
|
+
|
|
231
|
+
self.original_rgb = img_rgb
|
|
232
|
+
self.preview_rgb = img_rgb.copy()
|
|
233
|
+
|
|
234
|
+
# scene/view (⚠️ use ZoomableGraphicsView)
|
|
235
|
+
self.scene = QGraphicsScene(self)
|
|
236
|
+
self.view = ZoomableGraphicsView(self.scene, self)
|
|
237
|
+
self.view.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
238
|
+
self.pix = QGraphicsPixmapItem()
|
|
239
|
+
self.scene.addItem(self.pix)
|
|
240
|
+
|
|
241
|
+
# optional: keep your scroll area wrapper
|
|
242
|
+
self.scroll = QScrollArea(self)
|
|
243
|
+
self.scroll.setWidgetResizable(True)
|
|
244
|
+
self.scroll.setWidget(self.view)
|
|
245
|
+
|
|
246
|
+
# controls (add zoom row)
|
|
247
|
+
self.grp = QGroupBox(self.tr("HDR Controls"))
|
|
248
|
+
form = QFormLayout(self.grp)
|
|
249
|
+
|
|
250
|
+
self.s_scales = QSlider(Qt.Orientation.Horizontal); self.s_scales.setRange(2, 10); self.s_scales.setValue(5)
|
|
251
|
+
self.s_comp = QSlider(Qt.Orientation.Horizontal); self.s_comp.setRange(10, 500); self.s_comp.setValue(150)
|
|
252
|
+
self.s_gamma = QSlider(Qt.Orientation.Horizontal); self.s_gamma.setRange(10, 1000); self.s_gamma.setValue(500)
|
|
253
|
+
|
|
254
|
+
form.addRow(self.tr("Number of Scales:"), self.s_scales)
|
|
255
|
+
form.addRow(self.tr("Coarse Compression:"), self.s_comp)
|
|
256
|
+
form.addRow(self.tr("Mask Gamma:"), self.s_gamma)
|
|
257
|
+
|
|
258
|
+
row = QHBoxLayout()
|
|
259
|
+
self.btn_preview = QPushButton(self.tr("Preview"))
|
|
260
|
+
self.btn_toggle = QPushButton(self.tr("Show Original")); self.btn_toggle.setCheckable(True)
|
|
261
|
+
row.addWidget(self.btn_preview); row.addWidget(self.btn_toggle)
|
|
262
|
+
form.addRow(row)
|
|
263
|
+
|
|
264
|
+
# ↓ NEW: zoom controls
|
|
265
|
+
zoom_row = QHBoxLayout()
|
|
266
|
+
self.btn_zoom_in = QPushButton(self.tr("Zoom In"))
|
|
267
|
+
self.btn_zoom_out = QPushButton(self.tr("Zoom Out"))
|
|
268
|
+
self.btn_fit = QPushButton(self.tr("Fit to Preview"))
|
|
269
|
+
zoom_row.addWidget(self.btn_zoom_in)
|
|
270
|
+
zoom_row.addWidget(self.btn_zoom_out)
|
|
271
|
+
zoom_row.addWidget(self.btn_fit)
|
|
272
|
+
form.addRow(zoom_row)
|
|
273
|
+
|
|
274
|
+
# progress group (unchanged)
|
|
275
|
+
self.prog_grp = QGroupBox(self.tr("Processing Progress"))
|
|
276
|
+
vprog = QVBoxLayout(self.prog_grp)
|
|
277
|
+
self.lbl_step = QLabel(self.tr("Idle"))
|
|
278
|
+
self.bar = QProgressBar(); self.bar.setRange(0, 100); self.bar.setValue(0)
|
|
279
|
+
vprog.addWidget(self.lbl_step); vprog.addWidget(self.bar)
|
|
280
|
+
|
|
281
|
+
# bottom buttons (unchanged)
|
|
282
|
+
bot = QHBoxLayout()
|
|
283
|
+
self.btn_apply = QPushButton(self.tr("Apply to Document")); self.btn_apply.setEnabled(False)
|
|
284
|
+
self.btn_reset = QPushButton(self.tr("Reset"))
|
|
285
|
+
self.btn_close = QPushButton(self.tr("Close"))
|
|
286
|
+
bot.addStretch(1); bot.addWidget(self.btn_apply); bot.addWidget(self.btn_reset); bot.addWidget(self.btn_close)
|
|
287
|
+
|
|
288
|
+
# layout (unchanged)
|
|
289
|
+
main = QVBoxLayout(self)
|
|
290
|
+
main.addWidget(self.scroll)
|
|
291
|
+
h = QHBoxLayout()
|
|
292
|
+
h.addWidget(self.grp, 3)
|
|
293
|
+
h.addWidget(self.prog_grp, 1)
|
|
294
|
+
main.addLayout(h)
|
|
295
|
+
main.addLayout(bot)
|
|
296
|
+
|
|
297
|
+
# mask window
|
|
298
|
+
self.mask_win = MaskDisplayWindow(self)
|
|
299
|
+
if not self._headless:
|
|
300
|
+
self.mask_win.show()
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# kernel
|
|
304
|
+
self.base_kernel = _B3
|
|
305
|
+
|
|
306
|
+
# connections
|
|
307
|
+
self.btn_preview.clicked.connect(self._start_preview)
|
|
308
|
+
self.btn_apply.clicked.connect(self._apply_to_doc)
|
|
309
|
+
self.btn_close.clicked.connect(self.reject)
|
|
310
|
+
self.btn_reset.clicked.connect(self._reset)
|
|
311
|
+
self.btn_toggle.clicked.connect(self._toggle)
|
|
312
|
+
|
|
313
|
+
self.btn_zoom_in.clicked.connect(self.view.zoom_in)
|
|
314
|
+
self.btn_zoom_out.clicked.connect(self.view.zoom_out)
|
|
315
|
+
self.btn_fit.clicked.connect(lambda: self.view.fit_item(self.pix))
|
|
316
|
+
|
|
317
|
+
# ── Mask shown immediately ───────────────────────────────────────────
|
|
318
|
+
# Precompute L from original and push initial mask to the small window
|
|
319
|
+
self._lab_original = _rgb_to_lab(self.original_rgb)
|
|
320
|
+
self._L_original = self._lab_original[..., 0].astype(np.float32, copy=True)
|
|
321
|
+
self._mask_timer = QTimer(self)
|
|
322
|
+
self._mask_timer.setSingleShot(True)
|
|
323
|
+
self._mask_timer.timeout.connect(self._update_mask_from_gamma)
|
|
324
|
+
self.s_gamma.valueChanged.connect(self._schedule_mask_refresh)
|
|
325
|
+
|
|
326
|
+
# show initial mask right away
|
|
327
|
+
self._update_mask_from_gamma()
|
|
328
|
+
|
|
329
|
+
# initial pix
|
|
330
|
+
self._set_pix(self.preview_rgb)
|
|
331
|
+
|
|
332
|
+
def apply_preset(self, p: dict):
|
|
333
|
+
# sliders are integer; map floats to their scales
|
|
334
|
+
ns = int(p.get("n_scales", 5))
|
|
335
|
+
comp = float(p.get("compression_factor", 1.5))
|
|
336
|
+
mg = float(p.get("mask_gamma", 5.0)) # dialog default is 5.0 (slider 500)
|
|
337
|
+
# clamp safely
|
|
338
|
+
ns = max(2, min(10, ns))
|
|
339
|
+
comp_i = int(max(10, min(500, round(comp*100)))) # 1.0..5.0 -> 100..500
|
|
340
|
+
mg_i = int(max(10, min(1000, round(mg*100)))) # 0.1..10.0 -> 10..1000
|
|
341
|
+
self.s_scales.setValue(ns)
|
|
342
|
+
self.s_comp.setValue(comp_i)
|
|
343
|
+
self.s_gamma.setValue(mg_i)
|
|
344
|
+
# refresh mask preview (even if window is hidden)
|
|
345
|
+
self._update_mask_from_gamma()
|
|
346
|
+
|
|
347
|
+
def _headless_guard_active(self) -> bool:
|
|
348
|
+
"""Only guard true concurrent *headless* runs; ignore stale locks."""
|
|
349
|
+
# If we are not launching headless, never block the interactive UI.
|
|
350
|
+
if not self._headless:
|
|
351
|
+
return False
|
|
352
|
+
|
|
353
|
+
# Parent flags
|
|
354
|
+
p = self.parent()
|
|
355
|
+
if p and (getattr(p, "_wavescale_guard", False) or getattr(p, "_wavescale_headless_running", False)):
|
|
356
|
+
return True
|
|
357
|
+
|
|
358
|
+
# Settings lock with TTL
|
|
359
|
+
try:
|
|
360
|
+
s = QSettings()
|
|
361
|
+
in_prog = bool(s.value("wavescale/headless_in_progress", False))
|
|
362
|
+
started = float(s.value("wavescale/headless_started_at", 0.0))
|
|
363
|
+
except Exception:
|
|
364
|
+
in_prog, started = False, 0.0
|
|
365
|
+
|
|
366
|
+
if not in_prog:
|
|
367
|
+
return False
|
|
368
|
+
|
|
369
|
+
# consider anything older than 5 minutes stale
|
|
370
|
+
import time
|
|
371
|
+
if (time.time() - started) > 5 * 60:
|
|
372
|
+
try:
|
|
373
|
+
s.remove("wavescale/headless_in_progress")
|
|
374
|
+
s.remove("wavescale/headless_started_at")
|
|
375
|
+
except Exception:
|
|
376
|
+
pass
|
|
377
|
+
return False
|
|
378
|
+
|
|
379
|
+
return True
|
|
380
|
+
|
|
381
|
+
def showEvent(self, e):
|
|
382
|
+
super().showEvent(e)
|
|
383
|
+
if not self._bypass_guard and self._headless_guard_active():
|
|
384
|
+
# Soft warning instead of rejecting the dialog
|
|
385
|
+
try:
|
|
386
|
+
QMessageBox.information(
|
|
387
|
+
self, self.tr("WaveScale HDR"),
|
|
388
|
+
self.tr("A headless HDR run appears to be in progress. "
|
|
389
|
+
"This window will remain open; you can still preview safely.")
|
|
390
|
+
)
|
|
391
|
+
except Exception:
|
|
392
|
+
pass
|
|
393
|
+
|
|
394
|
+
def exec(self) -> int:
|
|
395
|
+
if not self._bypass_guard and self._headless_guard_active():
|
|
396
|
+
return 0
|
|
397
|
+
return super().exec()
|
|
398
|
+
|
|
399
|
+
def _get_doc_active_mask_2d(self) -> np.ndarray | None:
|
|
400
|
+
"""
|
|
401
|
+
Return the document's active mask as a 2-D float32 in [0..1],
|
|
402
|
+
resized to the current image size. If none, return None.
|
|
403
|
+
"""
|
|
404
|
+
doc = getattr(self, "_doc", None)
|
|
405
|
+
if doc is None:
|
|
406
|
+
return None
|
|
407
|
+
|
|
408
|
+
mid = getattr(doc, "active_mask_id", None)
|
|
409
|
+
if not mid:
|
|
410
|
+
return None
|
|
411
|
+
|
|
412
|
+
masks = getattr(doc, "masks", {}) or {}
|
|
413
|
+
layer = masks.get(mid)
|
|
414
|
+
if layer is None:
|
|
415
|
+
return None
|
|
416
|
+
|
|
417
|
+
# Safely pick the first non-None payload without using boolean 'or'
|
|
418
|
+
data = None
|
|
419
|
+
# object with attributes
|
|
420
|
+
for attr in ("data", "mask", "image", "array"):
|
|
421
|
+
if hasattr(layer, attr):
|
|
422
|
+
val = getattr(layer, attr)
|
|
423
|
+
if val is not None:
|
|
424
|
+
data = val
|
|
425
|
+
break
|
|
426
|
+
# plain ndarray?
|
|
427
|
+
if data is None and isinstance(layer, np.ndarray):
|
|
428
|
+
data = layer
|
|
429
|
+
# dict-like layer?
|
|
430
|
+
if data is None and isinstance(layer, dict):
|
|
431
|
+
for key in ("data", "mask", "image", "array"):
|
|
432
|
+
if key in layer and layer[key] is not None:
|
|
433
|
+
data = layer[key]
|
|
434
|
+
break
|
|
435
|
+
|
|
436
|
+
if data is None:
|
|
437
|
+
return None
|
|
438
|
+
|
|
439
|
+
m = np.asarray(data)
|
|
440
|
+
|
|
441
|
+
# collapse RGB/alpha to gray if needed
|
|
442
|
+
if m.ndim == 3:
|
|
443
|
+
m = m.mean(axis=2)
|
|
444
|
+
|
|
445
|
+
m = m.astype(np.float32, copy=False)
|
|
446
|
+
# normalize to [0,1] if it looks like 0..255 or 0..65535
|
|
447
|
+
if m.dtype.kind in "ui":
|
|
448
|
+
m /= float(np.iinfo(m.dtype).max)
|
|
449
|
+
else:
|
|
450
|
+
mx = float(m.max()) if m.size else 1.0
|
|
451
|
+
if mx > 1.0:
|
|
452
|
+
m /= mx
|
|
453
|
+
m = np.clip(m, 0.0, 1.0)
|
|
454
|
+
|
|
455
|
+
# resize to current image size (nearest)
|
|
456
|
+
H, W = self.original_rgb.shape[:2]
|
|
457
|
+
if m.shape != (H, W):
|
|
458
|
+
yi = (np.linspace(0, m.shape[0] - 1, H)).astype(np.int32)
|
|
459
|
+
xi = (np.linspace(0, m.shape[1] - 1, W)).astype(np.int32)
|
|
460
|
+
m = m[yi][:, xi]
|
|
461
|
+
|
|
462
|
+
return m
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _combine_with_doc_mask(self, hdr_mask: np.ndarray) -> np.ndarray:
|
|
466
|
+
"""
|
|
467
|
+
Multiply the HDR luminance mask by the document active mask (if any).
|
|
468
|
+
Shapes are matched to image size.
|
|
469
|
+
"""
|
|
470
|
+
m_doc = self._get_doc_active_mask_2d()
|
|
471
|
+
if m_doc is None:
|
|
472
|
+
return hdr_mask
|
|
473
|
+
# both are already (H, W) float32 in [0..1]
|
|
474
|
+
return np.clip(hdr_mask * m_doc, 0.0, 1.0)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _set_pix(self, rgb: np.ndarray):
|
|
478
|
+
arr = (np.clip(rgb, 0, 1) * 255).astype(np.uint8)
|
|
479
|
+
h, w, _ = arr.shape
|
|
480
|
+
q = QImage(arr.data, w, h, 3*w, QImage.Format.Format_RGB888)
|
|
481
|
+
self.pix.setPixmap(QPixmap.fromImage(q))
|
|
482
|
+
self.view.setSceneRect(self.pix.boundingRect())
|
|
483
|
+
|
|
484
|
+
def _toggle(self):
|
|
485
|
+
if self.btn_toggle.isChecked():
|
|
486
|
+
self.btn_toggle.setText(self.tr("Show Preview"))
|
|
487
|
+
self._set_pix(self.original_rgb)
|
|
488
|
+
else:
|
|
489
|
+
self.btn_toggle.setText(self.tr("Show Original"))
|
|
490
|
+
self._set_pix(self.preview_rgb)
|
|
491
|
+
|
|
492
|
+
def _reset(self):
|
|
493
|
+
self.s_scales.setValue(5)
|
|
494
|
+
self.s_comp.setValue(150)
|
|
495
|
+
self.s_gamma.setValue(500)
|
|
496
|
+
self.preview_rgb = self.original_rgb.copy()
|
|
497
|
+
self._set_pix(self.preview_rgb)
|
|
498
|
+
self.lbl_step.setText(self.tr("Idle")); self.bar.setValue(0)
|
|
499
|
+
self.btn_apply.setEnabled(False)
|
|
500
|
+
self.btn_toggle.setChecked(False); self.btn_toggle.setText(self.tr("Show Original"))
|
|
501
|
+
|
|
502
|
+
def _start_preview(self):
|
|
503
|
+
self.btn_preview.setEnabled(False); self.btn_apply.setEnabled(False)
|
|
504
|
+
n_scales = int(self.s_scales.value())
|
|
505
|
+
comp = float(self.s_comp.value()) / 100.0
|
|
506
|
+
mgamma = float(self.s_gamma.value()) / 100.0
|
|
507
|
+
|
|
508
|
+
self.thread = QThread(self)
|
|
509
|
+
self.worker = HDRWorker(self.original_rgb, n_scales, comp, mgamma, self.base_kernel)
|
|
510
|
+
self.worker.moveToThread(self.thread)
|
|
511
|
+
self.thread.started.connect(self.worker.run)
|
|
512
|
+
self.worker.progress_update.connect(self._on_progress)
|
|
513
|
+
self.worker.finished.connect(self._on_finished)
|
|
514
|
+
self.worker.finished.connect(self.thread.quit)
|
|
515
|
+
self.worker.finished.connect(self.worker.deleteLater)
|
|
516
|
+
self.thread.finished.connect(self.thread.deleteLater)
|
|
517
|
+
self.thread.start()
|
|
518
|
+
|
|
519
|
+
def _on_progress(self, step: str, pct: int):
|
|
520
|
+
self.lbl_step.setText(step); self.bar.setValue(pct)
|
|
521
|
+
|
|
522
|
+
def _on_finished(self, transformed_rgb: np.ndarray, mask: np.ndarray):
|
|
523
|
+
self.btn_preview.setEnabled(True)
|
|
524
|
+
if transformed_rgb is None:
|
|
525
|
+
QMessageBox.critical(self, self.tr("WaveScale HDR"), self.tr("Processing failed."))
|
|
526
|
+
return
|
|
527
|
+
|
|
528
|
+
# ← NEW: combine HDR's luminance mask with the doc's active mask (if present)
|
|
529
|
+
mask_comb = self._combine_with_doc_mask(mask)
|
|
530
|
+
|
|
531
|
+
# blend preview: original*(1-mask) + transformed*mask
|
|
532
|
+
m3 = np.repeat(mask_comb[..., None], 3, axis=2)
|
|
533
|
+
self.preview_rgb = self.original_rgb * (1.0 - m3) + transformed_rgb * m3
|
|
534
|
+
self._set_pix(self.preview_rgb)
|
|
535
|
+
|
|
536
|
+
# show the *combined* mask in the little window
|
|
537
|
+
self.mask_win.setWindowTitle(
|
|
538
|
+
self.tr("HDR Mask (L × Active Mask)") if self._get_doc_active_mask_2d() is not None else self.tr("HDR Mask (L-based)")
|
|
539
|
+
)
|
|
540
|
+
self.mask_win.update_mask(mask_comb)
|
|
541
|
+
|
|
542
|
+
self.btn_apply.setEnabled(True)
|
|
543
|
+
self.btn_toggle.setChecked(False); self.btn_toggle.setText(self.tr("Show Original"))
|
|
544
|
+
self.lbl_step.setText(self.tr("Preview ready")); self.bar.setValue(100)
|
|
545
|
+
# Headless: apply immediately (exactly like clicking "Apply to Document")
|
|
546
|
+
if self._headless:
|
|
547
|
+
QTimer.singleShot(0, self._apply_to_doc)
|
|
548
|
+
|
|
549
|
+
def _apply_to_doc(self):
|
|
550
|
+
out = self.preview_rgb
|
|
551
|
+
if self._was_mono:
|
|
552
|
+
# collapse back to mono (keep original shape: 2D or H×W×1)
|
|
553
|
+
mono = np.mean(out, axis=2, dtype=np.float32)
|
|
554
|
+
if self._mono_shape and len(self._mono_shape) == 3 and self._mono_shape[2] == 1:
|
|
555
|
+
mono = mono[:, :, None]
|
|
556
|
+
out = mono
|
|
557
|
+
|
|
558
|
+
out = np.clip(out, 0.0, 1.0).astype(np.float32, copy=False)
|
|
559
|
+
try:
|
|
560
|
+
if hasattr(self._doc, "set_image"):
|
|
561
|
+
self._doc.set_image(out, step_name="WaveScale HDR")
|
|
562
|
+
elif hasattr(self._doc, "apply_numpy"):
|
|
563
|
+
self._doc.apply_numpy(out, step_name="WaveScale HDR")
|
|
564
|
+
else:
|
|
565
|
+
self._doc.image = out
|
|
566
|
+
except Exception as e:
|
|
567
|
+
QMessageBox.critical(self, self.tr("WaveScale HDR"), self.tr("Failed to write to document:\n{0}").format(e))
|
|
568
|
+
return
|
|
569
|
+
|
|
570
|
+
# ── Build preset from current sliders ─────────────────────────
|
|
571
|
+
try:
|
|
572
|
+
preset = {
|
|
573
|
+
"n_scales": int(self.s_scales.value()),
|
|
574
|
+
"compression_factor": float(self.s_comp.value()) / 100.0,
|
|
575
|
+
"mask_gamma": float(self.s_gamma.value()) / 100.0,
|
|
576
|
+
}
|
|
577
|
+
except Exception:
|
|
578
|
+
preset = {}
|
|
579
|
+
|
|
580
|
+
# ── Register as last_headless_command on the main window ─────
|
|
581
|
+
try:
|
|
582
|
+
main = self.parent()
|
|
583
|
+
if main is not None:
|
|
584
|
+
payload = {
|
|
585
|
+
"command_id": "wavescale_hdr",
|
|
586
|
+
"preset": dict(preset),
|
|
587
|
+
}
|
|
588
|
+
setattr(main, "_last_headless_command", payload)
|
|
589
|
+
|
|
590
|
+
# Optional debug log (mirrors other tools)
|
|
591
|
+
try:
|
|
592
|
+
if hasattr(main, "_log"):
|
|
593
|
+
ns = int(preset.get("n_scales", 5))
|
|
594
|
+
comp = float(preset.get("compression_factor", 1.5))
|
|
595
|
+
mg = float(preset.get("mask_gamma", 5.0))
|
|
596
|
+
main._log(
|
|
597
|
+
f"[Replay] Registered WaveScale HDR as last action "
|
|
598
|
+
f"(n_scales={ns}, compression={comp:.2f}, mask_gamma={mg:.2f})"
|
|
599
|
+
)
|
|
600
|
+
except Exception:
|
|
601
|
+
pass
|
|
602
|
+
except Exception:
|
|
603
|
+
# never let replay wiring break the apply
|
|
604
|
+
pass
|
|
605
|
+
|
|
606
|
+
# ── (optional) keep emitting signal if you want it elsewhere ──
|
|
607
|
+
try:
|
|
608
|
+
self.applied_preset.emit(self._doc, preset)
|
|
609
|
+
except Exception:
|
|
610
|
+
pass
|
|
611
|
+
|
|
612
|
+
# Dialog stays open so user can apply to other images
|
|
613
|
+
# Refresh document reference for next operation
|
|
614
|
+
self._refresh_document_from_active()
|
|
615
|
+
|
|
616
|
+
def _refresh_document_from_active(self):
|
|
617
|
+
"""
|
|
618
|
+
Refresh the dialog's document reference to the currently active document.
|
|
619
|
+
This allows reusing the same dialog on different images.
|
|
620
|
+
"""
|
|
621
|
+
try:
|
|
622
|
+
main = self.parent()
|
|
623
|
+
if main and hasattr(main, "_active_doc"):
|
|
624
|
+
new_doc = main._active_doc()
|
|
625
|
+
if new_doc is not None and new_doc is not self._doc:
|
|
626
|
+
self._doc = new_doc
|
|
627
|
+
# Reset L channel and refresh preview for new document
|
|
628
|
+
self._L_original = None
|
|
629
|
+
self._last_preview = None
|
|
630
|
+
except Exception:
|
|
631
|
+
pass
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _schedule_mask_refresh(self, _value):
|
|
635
|
+
# debounce to ~0.25s
|
|
636
|
+
self._mask_timer.start(250)
|
|
637
|
+
|
|
638
|
+
def _update_mask_from_gamma(self):
|
|
639
|
+
gamma = float(self.s_gamma.value()) / 100.0
|
|
640
|
+
hdr_mask = _mask_from_L(self._L_original, gamma=gamma)
|
|
641
|
+
mask_comb = self._combine_with_doc_mask(hdr_mask)
|
|
642
|
+
self.mask_win.setWindowTitle(
|
|
643
|
+
self.tr("HDR Mask (L × Active Mask)") if self._get_doc_active_mask_2d() is not None else self.tr("HDR Mask (L-based)")
|
|
644
|
+
)
|
|
645
|
+
self.mask_win.update_mask(mask_comb)
|