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,680 @@
|
|
|
1
|
+
# pro/wavescalede.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import math
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from PyQt6.QtCore import Qt, QObject, pyqtSignal, QThread, QTimer
|
|
7
|
+
from PyQt6.QtGui import QImage, QPixmap, QIcon, QWheelEvent
|
|
8
|
+
from PyQt6.QtWidgets import (
|
|
9
|
+
QDialog, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout, QLabel, QPushButton,
|
|
10
|
+
QSlider, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QScrollArea,
|
|
11
|
+
QMessageBox, QProgressBar, QMainWindow
|
|
12
|
+
)
|
|
13
|
+
from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
|
|
14
|
+
|
|
15
|
+
# Import shared wavelet utilities
|
|
16
|
+
from setiastro.saspro.widgets.wavelet_utils import (
|
|
17
|
+
conv_sep_reflect as _conv_sep_reflect,
|
|
18
|
+
build_spaced_kernel as _build_spaced_kernel,
|
|
19
|
+
atrous_decompose as _atrous_decompose,
|
|
20
|
+
atrous_reconstruct as _atrous_reconstruct,
|
|
21
|
+
rgb_to_lab as _rgb_to_lab,
|
|
22
|
+
lab_to_rgb as _lab_to_rgb,
|
|
23
|
+
gauss_blur as _gauss_blur,
|
|
24
|
+
B3_KERNEL as _B3,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
28
|
+
# Core math (shared)
|
|
29
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
def _resize_mask_nn(mask2d: np.ndarray, target_hw: tuple[int, int]) -> np.ndarray:
|
|
32
|
+
H, W = target_hw
|
|
33
|
+
if mask2d.shape == (H, W):
|
|
34
|
+
return mask2d.astype(np.float32, copy=False)
|
|
35
|
+
yi = (np.linspace(0, mask2d.shape[0] - 1, H)).astype(np.int32)
|
|
36
|
+
xi = (np.linspace(0, mask2d.shape[1] - 1, W)).astype(np.int32)
|
|
37
|
+
return mask2d[yi][:, xi].astype(np.float32, copy=False)
|
|
38
|
+
|
|
39
|
+
# Darkness mask (scales 2–4, negative parts, mean → normalize → gamma → smooth → mild S-curve)
|
|
40
|
+
def _darkness_mask(L: np.ndarray, n_scales: int, base_k: np.ndarray, gamma: float) -> np.ndarray:
|
|
41
|
+
planes = _atrous_decompose(L, n_scales, base_k)
|
|
42
|
+
# mid-scales: 1:4 (0-based → skip 0)
|
|
43
|
+
sel = planes[1:4]
|
|
44
|
+
neg = [np.clip(-p, 0, None) for p in sel]
|
|
45
|
+
if len(neg) == 0:
|
|
46
|
+
m = np.zeros_like(L, dtype=np.float32)
|
|
47
|
+
else:
|
|
48
|
+
combined = np.mean(neg, axis=0).astype(np.float32)
|
|
49
|
+
denom = float(np.max(combined) + 1e-8)
|
|
50
|
+
m = combined / denom
|
|
51
|
+
if gamma != 1.0:
|
|
52
|
+
m = np.power(m, float(gamma), dtype=np.float32)
|
|
53
|
+
m = _gauss_blur(m, sigma=3.0).astype(np.float32)
|
|
54
|
+
# gentle brighten of mids
|
|
55
|
+
m = np.clip(1.5 * m - 0.5 * (m * m), 0.0, 1.0).astype(np.float32)
|
|
56
|
+
return m
|
|
57
|
+
|
|
58
|
+
# Main compute (mono or RGB)
|
|
59
|
+
def compute_wavescale_dse(image: np.ndarray,
|
|
60
|
+
n_scales: int = 6,
|
|
61
|
+
boost_factor: float = 5.0,
|
|
62
|
+
mask_gamma: float = 1.0,
|
|
63
|
+
iterations: int = 2,
|
|
64
|
+
base_kernel: np.ndarray = _B3,
|
|
65
|
+
decay_rate: float = 0.5,
|
|
66
|
+
external_mask: np.ndarray | None = None # ← NEW
|
|
67
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
68
|
+
"""
|
|
69
|
+
WaveScale Dark Enhancer.
|
|
70
|
+
Returns (output_image, darkness_mask_used).
|
|
71
|
+
If external_mask is provided (2-D [0..1]), it will be multiplied into the darkness mask.
|
|
72
|
+
"""
|
|
73
|
+
arr = np.asarray(image, dtype=np.float32)
|
|
74
|
+
|
|
75
|
+
# normalize external mask now
|
|
76
|
+
ext = None
|
|
77
|
+
if external_mask is not None:
|
|
78
|
+
m = np.asarray(external_mask)
|
|
79
|
+
if m.ndim == 3: # collapse RGB(A)
|
|
80
|
+
m = m.mean(axis=2)
|
|
81
|
+
m = np.clip(m.astype(np.float32), 0.0, 1.0)
|
|
82
|
+
ext = _resize_mask_nn(m, arr.shape[:2])
|
|
83
|
+
|
|
84
|
+
if arr.ndim == 2 or (arr.ndim == 3 and arr.shape[2] == 1):
|
|
85
|
+
L = arr.squeeze().astype(np.float32, copy=True) # [0..1]
|
|
86
|
+
mask = np.zeros_like(L, dtype=np.float32) # define for return
|
|
87
|
+
for it in range(int(iterations)):
|
|
88
|
+
mask = _darkness_mask(L, n_scales, base_kernel, mask_gamma)
|
|
89
|
+
if ext is not None:
|
|
90
|
+
mask = np.clip(mask * ext, 0.0, 1.0) # ← combine here
|
|
91
|
+
|
|
92
|
+
planes = _atrous_decompose(L, n_scales, base_kernel)
|
|
93
|
+
residual = planes.pop()
|
|
94
|
+
for i in range(len(planes)):
|
|
95
|
+
if i == 0:
|
|
96
|
+
continue # skip highest frequency
|
|
97
|
+
decay = decay_rate ** i
|
|
98
|
+
neg = np.clip(-planes[i], 0, None)
|
|
99
|
+
enhancement = neg * mask * (boost_factor - 1.0) * decay
|
|
100
|
+
planes[i] = planes[i] - enhancement
|
|
101
|
+
L = np.clip(_atrous_reconstruct(planes + [residual]), 0.0, 1.0)
|
|
102
|
+
|
|
103
|
+
out = L.astype(np.float32, copy=False)
|
|
104
|
+
return out, mask.astype(np.float32, copy=False)
|
|
105
|
+
|
|
106
|
+
# RGB path
|
|
107
|
+
rgb = np.clip(arr[:, :, :3], 0.0, 1.0).astype(np.float32, copy=False)
|
|
108
|
+
lab = _rgb_to_lab(rgb)
|
|
109
|
+
L = lab[..., 0].astype(np.float32, copy=True)
|
|
110
|
+
mask = np.zeros(L.shape, dtype=np.float32) # define for return
|
|
111
|
+
for it in range(int(iterations)):
|
|
112
|
+
mask = _darkness_mask(np.clip(L / 100.0, 0.0, 1.0), n_scales, base_kernel, mask_gamma)
|
|
113
|
+
if ext is not None:
|
|
114
|
+
mask = np.clip(mask * ext, 0.0, 1.0) # ← combine here
|
|
115
|
+
|
|
116
|
+
planes = _atrous_decompose(L, n_scales, base_kernel)
|
|
117
|
+
residual = planes.pop()
|
|
118
|
+
for i in range(len(planes)):
|
|
119
|
+
if i == 0:
|
|
120
|
+
continue
|
|
121
|
+
decay = decay_rate ** i
|
|
122
|
+
neg = np.clip(-planes[i], 0, None)
|
|
123
|
+
enhancement = neg * mask * (boost_factor - 1.0) * decay
|
|
124
|
+
planes[i] = planes[i] - enhancement
|
|
125
|
+
L = np.clip(_atrous_reconstruct(planes + [residual]), 0.0, 100.0)
|
|
126
|
+
|
|
127
|
+
lab[..., 0] = L
|
|
128
|
+
out_rgb = _lab_to_rgb(lab)
|
|
129
|
+
return out_rgb.astype(np.float32, copy=False), mask.astype(np.float32, copy=False)
|
|
130
|
+
|
|
131
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
132
|
+
# Worker
|
|
133
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
134
|
+
class DSEWorker(QObject):
|
|
135
|
+
progress_update = pyqtSignal(str, int)
|
|
136
|
+
finished = pyqtSignal(np.ndarray, np.ndarray) # (output, mask)
|
|
137
|
+
|
|
138
|
+
def __init__(self, image: np.ndarray, n_scales: int, boost: float, gamma: float,
|
|
139
|
+
base_kernel: np.ndarray, iterations: int,
|
|
140
|
+
external_mask: np.ndarray | None = None):
|
|
141
|
+
super().__init__()
|
|
142
|
+
self.image = image
|
|
143
|
+
self.n_scales = n_scales
|
|
144
|
+
self.boost = boost
|
|
145
|
+
self.gamma = gamma
|
|
146
|
+
self.base_kernel = base_kernel
|
|
147
|
+
self.iterations = iterations
|
|
148
|
+
self.external_mask = external_mask
|
|
149
|
+
|
|
150
|
+
def run(self):
|
|
151
|
+
try:
|
|
152
|
+
self.progress_update.emit(self.tr("Analyzing dark structure…"), 20)
|
|
153
|
+
out, mask = compute_wavescale_dse(
|
|
154
|
+
self.image, self.n_scales, self.boost, self.gamma,
|
|
155
|
+
self.iterations, self.base_kernel,
|
|
156
|
+
external_mask=self.external_mask # ← NEW
|
|
157
|
+
)
|
|
158
|
+
self.progress_update.emit(self.tr("Finalizing…"), 95)
|
|
159
|
+
self.finished.emit(out, mask)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
print("WaveScale DSE error:", e)
|
|
162
|
+
self.finished.emit(None, None)
|
|
163
|
+
|
|
164
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
165
|
+
# Small mask window (fixed ~400×400, always shows a zoomed-out mask)
|
|
166
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
167
|
+
class _MaskWindow(QDialog):
|
|
168
|
+
def __init__(self, parent=None):
|
|
169
|
+
super().__init__(parent)
|
|
170
|
+
self.setWindowTitle(self.tr("Dark Mask"))
|
|
171
|
+
self.setMinimumSize(300, 300)
|
|
172
|
+
self.resize(400, 400)
|
|
173
|
+
v = QVBoxLayout(self)
|
|
174
|
+
self.lbl = QLabel(alignment=Qt.AlignmentFlag.AlignCenter)
|
|
175
|
+
v.addWidget(self.lbl)
|
|
176
|
+
|
|
177
|
+
def set_mask(self, mask: np.ndarray):
|
|
178
|
+
m = np.clip(mask, 0, 1).astype(np.float32)
|
|
179
|
+
m8 = (m * 255.0).astype(np.uint8)
|
|
180
|
+
if m8.ndim == 2:
|
|
181
|
+
h, w = m8.shape
|
|
182
|
+
q = QImage(m8.data, w, h, w, QImage.Format.Format_Grayscale8)
|
|
183
|
+
else:
|
|
184
|
+
h, w, _ = m8.shape
|
|
185
|
+
q = QImage(m8.data, w, h, 3*w, QImage.Format.Format_RGB888)
|
|
186
|
+
pm = QPixmap.fromImage(q)
|
|
187
|
+
box = self.size()
|
|
188
|
+
pm2 = pm.scaled(box, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
|
|
189
|
+
self.lbl.setPixmap(pm2)
|
|
190
|
+
|
|
191
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
192
|
+
# Dialog
|
|
193
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
194
|
+
class WaveScaleDarkEnhancerDialogPro(QDialog):
|
|
195
|
+
def __init__(self, parent, doc, icon_path: str | None = None):
|
|
196
|
+
super().__init__(parent)
|
|
197
|
+
self.setWindowTitle(self.tr("WaveScale Dark Enhancer"))
|
|
198
|
+
if icon_path:
|
|
199
|
+
try: self.setWindowIcon(QIcon(icon_path))
|
|
200
|
+
except Exception as e:
|
|
201
|
+
import logging
|
|
202
|
+
logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
|
|
203
|
+
self.resize(980, 700)
|
|
204
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
205
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
206
|
+
self.setModal(False)
|
|
207
|
+
|
|
208
|
+
self._doc = doc
|
|
209
|
+
base = getattr(doc, "image", None)
|
|
210
|
+
if base is None:
|
|
211
|
+
raise RuntimeError("Active document has no image.")
|
|
212
|
+
|
|
213
|
+
img = np.asarray(base, dtype=np.float32)
|
|
214
|
+
if img.ndim == 2:
|
|
215
|
+
self._was_mono = True
|
|
216
|
+
self._mono_shape = img.shape
|
|
217
|
+
rgb = np.repeat(img[:, :, None], 3, axis=2)
|
|
218
|
+
elif img.ndim == 3 and img.shape[2] == 1:
|
|
219
|
+
self._was_mono = True
|
|
220
|
+
self._mono_shape = img.shape
|
|
221
|
+
rgb = np.repeat(img, 3, axis=2)
|
|
222
|
+
else:
|
|
223
|
+
self._was_mono = False
|
|
224
|
+
self._mono_shape = None
|
|
225
|
+
rgb = img[:, :, :3]
|
|
226
|
+
if img.dtype.kind in "ui":
|
|
227
|
+
maxv = float(np.nanmax(rgb)) or 1.0
|
|
228
|
+
rgb = rgb / max(1.0, maxv)
|
|
229
|
+
rgb = np.clip(rgb, 0.0, 1.0).astype(np.float32, copy=False)
|
|
230
|
+
|
|
231
|
+
self.original = rgb
|
|
232
|
+
self.preview = rgb.copy()
|
|
233
|
+
|
|
234
|
+
# scene/view
|
|
235
|
+
self.scene = QGraphicsScene(self)
|
|
236
|
+
self.view = QGraphicsView(self.scene)
|
|
237
|
+
self.view.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
238
|
+
self.pix = QGraphicsPixmapItem()
|
|
239
|
+
self.scene.addItem(self.pix)
|
|
240
|
+
self.scroll = QScrollArea(self); self.scroll.setWidgetResizable(True); self.scroll.setWidget(self.view)
|
|
241
|
+
|
|
242
|
+
# zoom state
|
|
243
|
+
self.zoom_factor = 1.0
|
|
244
|
+
self.zoom_step = 1.25
|
|
245
|
+
self.zoom_min = 0.1
|
|
246
|
+
self.zoom_max = 5.0
|
|
247
|
+
|
|
248
|
+
# controls
|
|
249
|
+
self.grp = QGroupBox(self.tr("Dark Enhancer Controls"))
|
|
250
|
+
form = QFormLayout(self.grp)
|
|
251
|
+
|
|
252
|
+
self.s_scales = QSlider(Qt.Orientation.Horizontal); self.s_scales.setRange(2, 10); self.s_scales.setValue(6)
|
|
253
|
+
self.s_boost = QSlider(Qt.Orientation.Horizontal); self.s_boost.setRange(10, 1000); self.s_boost.setValue(500) # 0.10..10.00
|
|
254
|
+
self.s_gamma = QSlider(Qt.Orientation.Horizontal); self.s_gamma.setRange(10, 1000); self.s_gamma.setValue(100) # 0.10..10.00
|
|
255
|
+
self.s_iters = QSlider(Qt.Orientation.Horizontal); self.s_iters.setRange(1, 10); self.s_iters.setValue(2)
|
|
256
|
+
|
|
257
|
+
form.addRow(self.tr("Number of Scales:"), self.s_scales)
|
|
258
|
+
form.addRow(self.tr("Boost Factor:"), self.s_boost)
|
|
259
|
+
form.addRow(self.tr("Mask Gamma:"), self.s_gamma)
|
|
260
|
+
form.addRow(self.tr("Iterations:"), self.s_iters)
|
|
261
|
+
|
|
262
|
+
row = QHBoxLayout()
|
|
263
|
+
self.btn_preview = QPushButton(self.tr("Preview"))
|
|
264
|
+
self.btn_toggle = QPushButton(self.tr("Show Original")); self.btn_toggle.setCheckable(True)
|
|
265
|
+
row.addWidget(self.btn_preview); row.addWidget(self.btn_toggle)
|
|
266
|
+
form.addRow(row)
|
|
267
|
+
|
|
268
|
+
# progress
|
|
269
|
+
self.prog_grp = QGroupBox(self.tr("Progress"))
|
|
270
|
+
vprog = QVBoxLayout(self.prog_grp)
|
|
271
|
+
self.lbl_step = QLabel(self.tr("Idle"))
|
|
272
|
+
self.bar = QProgressBar(); self.bar.setRange(0, 100); self.bar.setValue(0)
|
|
273
|
+
vprog.addWidget(self.lbl_step); vprog.addWidget(self.bar)
|
|
274
|
+
|
|
275
|
+
# bottom
|
|
276
|
+
bot = QHBoxLayout()
|
|
277
|
+
self.btn_apply = QPushButton(self.tr("Apply to Document")); self.btn_apply.setEnabled(False)
|
|
278
|
+
self.btn_reset = QPushButton(self.tr("Reset"))
|
|
279
|
+
self.btn_close = QPushButton(self.tr("Close"))
|
|
280
|
+
bot.addStretch(1); bot.addWidget(self.btn_apply); bot.addWidget(self.btn_reset); bot.addWidget(self.btn_close)
|
|
281
|
+
|
|
282
|
+
# layout
|
|
283
|
+
main = QVBoxLayout(self)
|
|
284
|
+
main.addWidget(self.scroll)
|
|
285
|
+
|
|
286
|
+
zoom_box = QGroupBox(self.tr("Zoom Controls"))
|
|
287
|
+
zr = QHBoxLayout(zoom_box)
|
|
288
|
+
self.btn_zin = QPushButton(self.tr("Zoom In"))
|
|
289
|
+
self.btn_zout = QPushButton(self.tr("Zoom Out"))
|
|
290
|
+
self.btn_fit = QPushButton(self.tr("Fit to Preview"))
|
|
291
|
+
zr.addWidget(self.btn_zin); zr.addWidget(self.btn_zout); zr.addWidget(self.btn_fit)
|
|
292
|
+
main.addWidget(zoom_box)
|
|
293
|
+
|
|
294
|
+
h = QHBoxLayout()
|
|
295
|
+
h.addWidget(self.grp, 3)
|
|
296
|
+
h.addWidget(self.prog_grp, 1)
|
|
297
|
+
main.addLayout(h)
|
|
298
|
+
main.addLayout(bot)
|
|
299
|
+
|
|
300
|
+
# mask window (show immediately)
|
|
301
|
+
self.mask_win = _MaskWindow(self); self.mask_win.show()
|
|
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_zin.clicked.connect(self._zoom_in)
|
|
314
|
+
self.btn_zout.clicked.connect(self._zoom_out)
|
|
315
|
+
self.btn_fit.clicked.connect(self._fit_to_preview)
|
|
316
|
+
|
|
317
|
+
# gamma debounce → live mask updates (250ms)
|
|
318
|
+
self._gamma_timer = QTimer(self)
|
|
319
|
+
self._gamma_timer.setSingleShot(True)
|
|
320
|
+
self._gamma_timer.timeout.connect(self._update_mask_only)
|
|
321
|
+
self.s_gamma.valueChanged.connect(lambda _v: self._gamma_timer.start(250))
|
|
322
|
+
|
|
323
|
+
# init preview & initial mask
|
|
324
|
+
self._set_pix(self.preview)
|
|
325
|
+
self._update_mask_only()
|
|
326
|
+
|
|
327
|
+
def _combine_with_doc_mask(self, op_mask: np.ndarray | None) -> np.ndarray | None:
|
|
328
|
+
doc_m = self._get_doc_active_mask_2d()
|
|
329
|
+
if doc_m is None:
|
|
330
|
+
return op_mask
|
|
331
|
+
if op_mask is None:
|
|
332
|
+
return doc_m
|
|
333
|
+
H, W = op_mask.shape[:2]
|
|
334
|
+
if doc_m.shape != (H, W):
|
|
335
|
+
yi = (np.linspace(0, doc_m.shape[0] - 1, H)).astype(np.int32)
|
|
336
|
+
xi = (np.linspace(0, doc_m.shape[1] - 1, W)).astype(np.int32)
|
|
337
|
+
doc_m = doc_m[yi][:, xi]
|
|
338
|
+
return np.clip(op_mask * doc_m, 0.0, 1.0)
|
|
339
|
+
|
|
340
|
+
def _get_doc_active_mask_2d(self) -> np.ndarray | None:
|
|
341
|
+
"""
|
|
342
|
+
Return active document mask as 2-D float32 [0..1], resized to current image.
|
|
343
|
+
"""
|
|
344
|
+
doc = getattr(self, "_doc", None)
|
|
345
|
+
if doc is None:
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
mid = getattr(doc, "active_mask_id", None)
|
|
349
|
+
if not mid:
|
|
350
|
+
return None
|
|
351
|
+
|
|
352
|
+
masks = getattr(doc, "masks", {}) or {}
|
|
353
|
+
layer = masks.get(mid)
|
|
354
|
+
if layer is None:
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
# pick first non-None payload without boolean 'or'
|
|
358
|
+
data = None
|
|
359
|
+
for attr in ("data", "mask", "image", "array"):
|
|
360
|
+
if hasattr(layer, attr):
|
|
361
|
+
val = getattr(layer, attr)
|
|
362
|
+
if val is not None:
|
|
363
|
+
data = val
|
|
364
|
+
break
|
|
365
|
+
if data is None and isinstance(layer, dict):
|
|
366
|
+
for key in ("data", "mask", "image", "array"):
|
|
367
|
+
if key in layer and layer[key] is not None:
|
|
368
|
+
data = layer[key]
|
|
369
|
+
break
|
|
370
|
+
if data is None and isinstance(layer, np.ndarray):
|
|
371
|
+
data = layer
|
|
372
|
+
if data is None:
|
|
373
|
+
return None
|
|
374
|
+
|
|
375
|
+
m = np.asarray(data)
|
|
376
|
+
if m.ndim == 3:
|
|
377
|
+
m = m.mean(axis=2)
|
|
378
|
+
|
|
379
|
+
m = m.astype(np.float32, copy=False)
|
|
380
|
+
# normalize to [0,1] if needed
|
|
381
|
+
mx = float(m.max()) if m.size else 1.0
|
|
382
|
+
if mx > 1.0:
|
|
383
|
+
m /= mx
|
|
384
|
+
m = np.clip(m, 0.0, 1.0)
|
|
385
|
+
|
|
386
|
+
# resize (nearest) to current image size
|
|
387
|
+
H, W = self.original.shape[:2]
|
|
388
|
+
if m.shape != (H, W):
|
|
389
|
+
yi = (np.linspace(0, m.shape[0] - 1, H)).astype(np.int32)
|
|
390
|
+
xi = (np.linspace(0, m.shape[1] - 1, W)).astype(np.int32)
|
|
391
|
+
m = m[yi][:, xi]
|
|
392
|
+
|
|
393
|
+
return m
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _combine_with_doc_mask(self, algo_mask: np.ndarray) -> np.ndarray:
|
|
397
|
+
m_doc = self._get_doc_active_mask_2d()
|
|
398
|
+
if m_doc is None:
|
|
399
|
+
return algo_mask
|
|
400
|
+
return np.clip(algo_mask.astype(np.float32) * m_doc.astype(np.float32), 0.0, 1.0)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# --- preview pixmap ---
|
|
404
|
+
def _set_pix(self, rgb: np.ndarray):
|
|
405
|
+
arr = (np.clip(rgb, 0, 1) * 255).astype(np.uint8)
|
|
406
|
+
h, w, _ = arr.shape
|
|
407
|
+
q = QImage(arr.data, w, h, 3*w, QImage.Format.Format_RGB888)
|
|
408
|
+
self.pix.setPixmap(QPixmap.fromImage(q))
|
|
409
|
+
self.view.setSceneRect(self.pix.boundingRect())
|
|
410
|
+
|
|
411
|
+
# --- toggle ---
|
|
412
|
+
def _toggle(self):
|
|
413
|
+
if self.btn_toggle.isChecked():
|
|
414
|
+
self.btn_toggle.setText(self.tr("Show Preview"))
|
|
415
|
+
self._set_pix(self.original)
|
|
416
|
+
else:
|
|
417
|
+
self.btn_toggle.setText(self.tr("Show Original"))
|
|
418
|
+
self._set_pix(self.preview)
|
|
419
|
+
|
|
420
|
+
# --- reset ---
|
|
421
|
+
def _reset(self):
|
|
422
|
+
self.s_scales.setValue(6)
|
|
423
|
+
self.s_boost.setValue(500)
|
|
424
|
+
self.s_gamma.setValue(100)
|
|
425
|
+
self.s_iters.setValue(2)
|
|
426
|
+
self.preview = self.original.copy()
|
|
427
|
+
self._set_pix(self.preview)
|
|
428
|
+
self.lbl_step.setText(self.tr("Idle")); self.bar.setValue(0)
|
|
429
|
+
self.btn_apply.setEnabled(False)
|
|
430
|
+
self.btn_toggle.setChecked(False); self.btn_toggle.setText(self.tr("Show Original"))
|
|
431
|
+
self._update_mask_only()
|
|
432
|
+
|
|
433
|
+
# --- zoom + Ctrl+Wheel ---
|
|
434
|
+
def wheelEvent(self, e: QWheelEvent):
|
|
435
|
+
if e.modifiers() & Qt.KeyboardModifier.ControlModifier:
|
|
436
|
+
if e.angleDelta().y() > 0: self._zoom_in()
|
|
437
|
+
else: self._zoom_out()
|
|
438
|
+
e.accept(); return
|
|
439
|
+
super().wheelEvent(e)
|
|
440
|
+
|
|
441
|
+
def _zoom_in(self):
|
|
442
|
+
z = self.zoom_factor * self.zoom_step
|
|
443
|
+
if z <= self.zoom_max:
|
|
444
|
+
self.zoom_factor = z
|
|
445
|
+
self._apply_zoom()
|
|
446
|
+
|
|
447
|
+
def _zoom_out(self):
|
|
448
|
+
z = self.zoom_factor / self.zoom_step
|
|
449
|
+
if z >= self.zoom_min:
|
|
450
|
+
self.zoom_factor = z
|
|
451
|
+
self._apply_zoom()
|
|
452
|
+
|
|
453
|
+
def _fit_to_preview(self):
|
|
454
|
+
if not self.pix.pixmap().isNull():
|
|
455
|
+
self.view.fitInView(self.pix, Qt.AspectRatioMode.KeepAspectRatio)
|
|
456
|
+
self.zoom_factor = 1.0
|
|
457
|
+
|
|
458
|
+
def _apply_zoom(self):
|
|
459
|
+
self.view.resetTransform()
|
|
460
|
+
self.view.scale(self.zoom_factor, self.zoom_factor)
|
|
461
|
+
|
|
462
|
+
# --- live mask (no full recompute) ---
|
|
463
|
+
def _update_mask_only(self):
|
|
464
|
+
mgamma = float(self.s_gamma.value()) / 100.0
|
|
465
|
+
base = self.original
|
|
466
|
+
lab = _rgb_to_lab(base)
|
|
467
|
+
L = lab[..., 0] / 100.0
|
|
468
|
+
algo_mask = _darkness_mask(np.clip(L, 0.0, 1.0),
|
|
469
|
+
int(self.s_scales.value()),
|
|
470
|
+
self.base_kernel, mgamma)
|
|
471
|
+
mask_comb = self._combine_with_doc_mask(algo_mask)
|
|
472
|
+
self.mask_win.setWindowTitle(
|
|
473
|
+
self.tr("Dark Mask (Algo × Active Mask)") if self._get_doc_active_mask_2d() is not None else self.tr("Dark Mask")
|
|
474
|
+
)
|
|
475
|
+
self.mask_win.set_mask(mask_comb)
|
|
476
|
+
# --- threaded preview ---
|
|
477
|
+
def _start_preview(self):
|
|
478
|
+
self.btn_preview.setEnabled(False); self.btn_apply.setEnabled(False)
|
|
479
|
+
n_scales = int(self.s_scales.value())
|
|
480
|
+
boost = float(self.s_boost.value()) / 100.0
|
|
481
|
+
mgamma = float(self.s_gamma.value()) / 100.0
|
|
482
|
+
iters = int(self.s_iters.value())
|
|
483
|
+
docmask = self._get_doc_active_mask_2d()
|
|
484
|
+
|
|
485
|
+
self.thread = QThread(self)
|
|
486
|
+
self.worker = DSEWorker(self.original, n_scales, boost, mgamma,
|
|
487
|
+
self.base_kernel, iters,
|
|
488
|
+
external_mask=docmask)
|
|
489
|
+
self.worker.moveToThread(self.thread)
|
|
490
|
+
self.thread.started.connect(self.worker.run)
|
|
491
|
+
self.worker.progress_update.connect(self._on_progress)
|
|
492
|
+
self.worker.finished.connect(self._on_finished)
|
|
493
|
+
self.worker.finished.connect(self.thread.quit)
|
|
494
|
+
self.worker.finished.connect(self.worker.deleteLater)
|
|
495
|
+
self.thread.finished.connect(self.thread.deleteLater)
|
|
496
|
+
self.thread.start()
|
|
497
|
+
|
|
498
|
+
def _on_progress(self, step: str, pct: int):
|
|
499
|
+
self.lbl_step.setText(step); self.bar.setValue(pct)
|
|
500
|
+
|
|
501
|
+
def _on_finished(self, out: np.ndarray, mask: np.ndarray):
|
|
502
|
+
self.btn_preview.setEnabled(True)
|
|
503
|
+
if out is None:
|
|
504
|
+
QMessageBox.critical(self, self.tr("WaveScale Dark Enhancer"), self.tr("Processing failed."))
|
|
505
|
+
return
|
|
506
|
+
|
|
507
|
+
# Respect the document mask
|
|
508
|
+
doc_m = self._get_doc_active_mask_2d()
|
|
509
|
+
if out.ndim == 2:
|
|
510
|
+
out_rgb = np.repeat(out[:, :, None], 3, axis=2)
|
|
511
|
+
else:
|
|
512
|
+
out_rgb = out
|
|
513
|
+
|
|
514
|
+
if doc_m is not None:
|
|
515
|
+
M3 = np.repeat(doc_m[:, :, None], 3, axis=2).astype(np.float32)
|
|
516
|
+
self.preview = self.original * (1.0 - M3) + out_rgb * M3
|
|
517
|
+
else:
|
|
518
|
+
self.preview = out_rgb
|
|
519
|
+
|
|
520
|
+
# show combined mask (internal darkness mask × doc mask)
|
|
521
|
+
mask = self._combine_with_doc_mask(mask)
|
|
522
|
+
|
|
523
|
+
self._set_pix(self.preview)
|
|
524
|
+
self.mask_win.set_mask(mask)
|
|
525
|
+
self.btn_apply.setEnabled(True)
|
|
526
|
+
self.btn_toggle.setChecked(False); self.btn_toggle.setText(self.tr("Show Original"))
|
|
527
|
+
self.lbl_step.setText(self.tr("Preview ready")); self.bar.setValue(100)
|
|
528
|
+
|
|
529
|
+
# --- apply back to doc ---
|
|
530
|
+
# --- apply back to doc ---
|
|
531
|
+
def _apply_to_doc(self):
|
|
532
|
+
out = self.preview
|
|
533
|
+
if self._was_mono:
|
|
534
|
+
mono = np.mean(out, axis=2, dtype=np.float32)
|
|
535
|
+
if self._mono_shape and len(self._mono_shape) == 3 and self._mono_shape[2] == 1:
|
|
536
|
+
mono = mono[:, :, None]
|
|
537
|
+
out = mono
|
|
538
|
+
out = np.clip(out, 0.0, 1.0).astype(np.float32, copy=False)
|
|
539
|
+
try:
|
|
540
|
+
if hasattr(self._doc, "set_image"):
|
|
541
|
+
self._doc.set_image(out, step_name="WaveScale Dark Enhancer")
|
|
542
|
+
elif hasattr(self._doc, "apply_numpy"):
|
|
543
|
+
self._doc.apply_numpy(out, step_name="WaveScale Dark Enhancer")
|
|
544
|
+
else:
|
|
545
|
+
self._doc.image = out
|
|
546
|
+
except Exception as e:
|
|
547
|
+
QMessageBox.critical(self, self.tr("WaveScale Dark Enhancer"), self.tr("Failed to write to document:\n{0}").format(e))
|
|
548
|
+
return
|
|
549
|
+
|
|
550
|
+
# ── Build preset from current sliders ─────────────────────────
|
|
551
|
+
try:
|
|
552
|
+
preset = {
|
|
553
|
+
"n_scales": int(self.s_scales.value()),
|
|
554
|
+
"boost_factor": float(self.s_boost.value()) / 100.0,
|
|
555
|
+
"mask_gamma": float(self.s_gamma.value()) / 100.0,
|
|
556
|
+
"iterations": int(self.s_iters.value()),
|
|
557
|
+
}
|
|
558
|
+
except Exception:
|
|
559
|
+
preset = {}
|
|
560
|
+
|
|
561
|
+
# ── Register as last_headless_command on the main window ─────
|
|
562
|
+
try:
|
|
563
|
+
main = self.parent()
|
|
564
|
+
if main is not None:
|
|
565
|
+
payload = {
|
|
566
|
+
"command_id": "wavescale_dark_enhance",
|
|
567
|
+
"preset": dict(preset),
|
|
568
|
+
}
|
|
569
|
+
setattr(main, "_last_headless_command", payload)
|
|
570
|
+
|
|
571
|
+
# Optional debug logging similar to other tools
|
|
572
|
+
try:
|
|
573
|
+
if hasattr(main, "_log"):
|
|
574
|
+
main._log(
|
|
575
|
+
"[Replay] Registered WaveScale Dark Enhancer as "
|
|
576
|
+
f"last action (n_scales={preset.get('n_scales')}, "
|
|
577
|
+
f"boost={preset.get('boost_factor')}, "
|
|
578
|
+
f"mask_gamma={preset.get('mask_gamma')}, "
|
|
579
|
+
f"iterations={preset.get('iterations')})"
|
|
580
|
+
)
|
|
581
|
+
except Exception:
|
|
582
|
+
pass
|
|
583
|
+
except Exception:
|
|
584
|
+
# Never let replay wiring break the apply
|
|
585
|
+
pass
|
|
586
|
+
|
|
587
|
+
# Dialog stays open so user can apply to other images
|
|
588
|
+
# Refresh document reference for next operation
|
|
589
|
+
self._refresh_document_from_active()
|
|
590
|
+
|
|
591
|
+
def _refresh_document_from_active(self):
|
|
592
|
+
"""
|
|
593
|
+
Refresh the dialog's document reference to the currently active document.
|
|
594
|
+
This allows reusing the same dialog on different images.
|
|
595
|
+
"""
|
|
596
|
+
try:
|
|
597
|
+
main = self.parent()
|
|
598
|
+
if main and hasattr(main, "_active_doc"):
|
|
599
|
+
new_doc = main._active_doc()
|
|
600
|
+
if new_doc is not None and new_doc is not self._doc:
|
|
601
|
+
self._doc = new_doc
|
|
602
|
+
# Reset state and refresh for new document
|
|
603
|
+
self._L_original = None
|
|
604
|
+
self._last_preview = None
|
|
605
|
+
except Exception:
|
|
606
|
+
pass
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
610
|
+
# Installer helpers
|
|
611
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
612
|
+
def install_wavescale_dark_enhancer(main_window: QMainWindow,
|
|
613
|
+
dse_icon_path: str,
|
|
614
|
+
*,
|
|
615
|
+
command_id: str = "wavescale_dark_enhancer",
|
|
616
|
+
menu_name: str = "Pro",
|
|
617
|
+
toolbar_name: str = "Pro Tools"):
|
|
618
|
+
"""
|
|
619
|
+
Creates the QAction, hooks it into menu+toolbar, and registers it
|
|
620
|
+
with your ShortcutManager under `command_id`.
|
|
621
|
+
Expects main_window to expose:
|
|
622
|
+
• .docman.current_document() → returns doc with .image
|
|
623
|
+
• ._spawn_subwindow_for(doc) (normal in your app)
|
|
624
|
+
• .shortcut_manager (your ShortcutManager) — optional
|
|
625
|
+
"""
|
|
626
|
+
# 1) QAction
|
|
627
|
+
act = getattr(main_window, "act_wavescalede", None)
|
|
628
|
+
if act is None:
|
|
629
|
+
from PyQt6.QtGui import QAction
|
|
630
|
+
act = QAction(QIcon(dse_icon_path), "WaveScale Dark Enhancer", main_window)
|
|
631
|
+
act.setObjectName(command_id)
|
|
632
|
+
act.setProperty("command_id", command_id)
|
|
633
|
+
|
|
634
|
+
def _run_dialog():
|
|
635
|
+
docman = getattr(main_window, "docman", None)
|
|
636
|
+
doc = None
|
|
637
|
+
if docman and hasattr(docman, "current_document"):
|
|
638
|
+
doc = docman.current_document()
|
|
639
|
+
if doc is None or getattr(doc, "image", None) is None:
|
|
640
|
+
from PyQt6.QtCore import QCoreApplication
|
|
641
|
+
QMessageBox.warning(main_window, QCoreApplication.translate("WaveScaleDarkEnhancerDialogPro", "WaveScale Dark Enhancer"), QCoreApplication.translate("WaveScaleDarkEnhancerDialogPro", "No active image."))
|
|
642
|
+
return
|
|
643
|
+
dlg = WaveScaleDarkEnhancerDialogPro(main_window, doc, icon_path=dse_icon_path)
|
|
644
|
+
dlg.exec()
|
|
645
|
+
|
|
646
|
+
act.triggered.connect(_run_dialog)
|
|
647
|
+
setattr(main_window, "act_wavescalede", act)
|
|
648
|
+
|
|
649
|
+
# 2) Menu hookup
|
|
650
|
+
menubar = main_window.menuBar()
|
|
651
|
+
menu = None
|
|
652
|
+
for m in menubar.findChildren(type(menubar)):
|
|
653
|
+
# best-effort: ignore; we’ll just create/find by title
|
|
654
|
+
pass
|
|
655
|
+
menu = None
|
|
656
|
+
for i in range(menubar.actions().__len__()):
|
|
657
|
+
if menubar.actions()[i].text().replace("&", "") == menu_name:
|
|
658
|
+
menu = menubar.actions()[i].menu()
|
|
659
|
+
break
|
|
660
|
+
if menu is None:
|
|
661
|
+
menu = menubar.addMenu(menu_name)
|
|
662
|
+
menu.addAction(act)
|
|
663
|
+
|
|
664
|
+
# 3) Toolbar hookup
|
|
665
|
+
tb = None
|
|
666
|
+
for t in main_window.findChildren(type(main_window.addToolBar("tmp"))):
|
|
667
|
+
# naive scan (we won't rely on this); we'll create if needed
|
|
668
|
+
pass
|
|
669
|
+
tb = getattr(main_window, "_tb_" + toolbar_name.replace(" ", "_").lower(), None)
|
|
670
|
+
if tb is None:
|
|
671
|
+
tb = main_window.addToolBar(toolbar_name)
|
|
672
|
+
setattr(main_window, "_tb_" + toolbar_name.replace(" ", "_").lower(), tb)
|
|
673
|
+
tb.addAction(act)
|
|
674
|
+
|
|
675
|
+
# 4) Register with ShortcutManager (if present)
|
|
676
|
+
sm = getattr(main_window, "shortcut_manager", None)
|
|
677
|
+
if sm and hasattr(sm, "register_action"):
|
|
678
|
+
sm.register_action(command_id, act)
|
|
679
|
+
|
|
680
|
+
return act
|