setiastrosuitepro 1.6.1__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/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/saspro/__init__.py +20 -0
- setiastro/saspro/__main__.py +809 -0
- setiastro/saspro/_generated/__init__.py +7 -0
- setiastro/saspro/_generated/build_info.py +2 -0
- setiastro/saspro/abe.py +1295 -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 +621 -0
- setiastro/saspro/astrobin_exporter.py +1007 -0
- setiastro/saspro/astrospike.py +153 -0
- setiastro/saspro/astrospike_python.py +1839 -0
- setiastro/saspro/autostretch.py +196 -0
- setiastro/saspro/backgroundneutral.py +560 -0
- setiastro/saspro/batch_convert.py +325 -0
- setiastro/saspro/batch_renamer.py +519 -0
- setiastro/saspro/blemish_blaster.py +488 -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 +178 -0
- setiastro/saspro/clahe.py +342 -0
- setiastro/saspro/comet_stacking.py +1377 -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 +1397 -0
- setiastro/saspro/convo_preset.py +414 -0
- setiastro/saspro/copyastro.py +187 -0
- setiastro/saspro/cosmicclarity.py +1564 -0
- setiastro/saspro/cosmicclarity_preset.py +407 -0
- setiastro/saspro/crop_dialog_pro.py +956 -0
- setiastro/saspro/crop_preset.py +189 -0
- setiastro/saspro/curve_editor_pro.py +2544 -0
- setiastro/saspro/curves_preset.py +375 -0
- setiastro/saspro/debayer.py +670 -0
- setiastro/saspro/debug_utils.py +29 -0
- setiastro/saspro/dnd_mime.py +35 -0
- setiastro/saspro/doc_manager.py +2641 -0
- setiastro/saspro/exoplanet_detector.py +2166 -0
- setiastro/saspro/file_utils.py +284 -0
- setiastro/saspro/fitsmodifier.py +745 -0
- setiastro/saspro/fix_bom.py +32 -0
- setiastro/saspro/free_torch_memory.py +48 -0
- setiastro/saspro/frequency_separation.py +1343 -0
- setiastro/saspro/function_bundle.py +1594 -0
- setiastro/saspro/generate_translations.py +2378 -0
- setiastro/saspro/ghs_dialog_pro.py +660 -0
- setiastro/saspro/ghs_preset.py +284 -0
- setiastro/saspro/graxpert.py +634 -0
- setiastro/saspro/graxpert_preset.py +287 -0
- setiastro/saspro/gui/__init__.py +0 -0
- setiastro/saspro/gui/main_window.py +8567 -0
- setiastro/saspro/gui/mixins/__init__.py +33 -0
- setiastro/saspro/gui/mixins/dock_mixin.py +263 -0
- setiastro/saspro/gui/mixins/file_mixin.py +443 -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 +361 -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/halobgon.py +462 -0
- setiastro/saspro/header_viewer.py +448 -0
- setiastro/saspro/headless_utils.py +88 -0
- setiastro/saspro/histogram.py +753 -0
- setiastro/saspro/history_explorer.py +939 -0
- setiastro/saspro/i18n.py +156 -0
- setiastro/saspro/image_combine.py +414 -0
- setiastro/saspro/image_peeker_pro.py +1601 -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 +244 -0
- setiastro/saspro/isophote.py +1179 -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 +3659 -0
- setiastro/saspro/legacy/xisf.py +1071 -0
- setiastro/saspro/linear_fit.py +534 -0
- setiastro/saspro/live_stacking.py +1830 -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 +928 -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 +3826 -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 +382 -0
- setiastro/saspro/multiscale_decomp.py +1290 -0
- setiastro/saspro/nbtorgb_stars.py +531 -0
- setiastro/saspro/numba_utils.py +3044 -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 +1413 -0
- setiastro/saspro/ops/settings.py +679 -0
- setiastro/saspro/parallel_utils.py +554 -0
- setiastro/saspro/pedestal.py +121 -0
- setiastro/saspro/perfect_palette_picker.py +1070 -0
- setiastro/saspro/pipeline.py +110 -0
- setiastro/saspro/pixelmath.py +1600 -0
- setiastro/saspro/plate_solver.py +2444 -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 +314 -0
- setiastro/saspro/remove_stars.py +1625 -0
- setiastro/saspro/remove_stars_preset.py +404 -0
- setiastro/saspro/resources.py +477 -0
- setiastro/saspro/rgb_combination.py +207 -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 +72 -0
- setiastro/saspro/selective_color.py +1552 -0
- setiastro/saspro/sfcc.py +1430 -0
- setiastro/saspro/shortcuts.py +3043 -0
- setiastro/saspro/signature_insert.py +1099 -0
- setiastro/saspro/stacking_suite.py +18181 -0
- setiastro/saspro/star_alignment.py +7420 -0
- setiastro/saspro/star_alignment_preset.py +329 -0
- setiastro/saspro/star_metrics.py +49 -0
- setiastro/saspro/star_spikes.py +681 -0
- setiastro/saspro/star_stretch.py +470 -0
- setiastro/saspro/stat_stretch.py +506 -0
- setiastro/saspro/status_log_dock.py +78 -0
- setiastro/saspro/subwindow.py +3267 -0
- setiastro/saspro/supernovaasteroidhunter.py +1716 -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/de_translations.py +3733 -0
- setiastro/saspro/translations/es_translations.py +3923 -0
- setiastro/saspro/translations/fr_translations.py +3842 -0
- setiastro/saspro/translations/integrate_translations.py +234 -0
- setiastro/saspro/translations/it_translations.py +3662 -0
- setiastro/saspro/translations/ja_translations.py +3585 -0
- setiastro/saspro/translations/pt_translations.py +3853 -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_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_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +12520 -0
- setiastro/saspro/translations/zh_translations.py +3659 -0
- setiastro/saspro/versioning.py +71 -0
- setiastro/saspro/view_bundle.py +1555 -0
- setiastro/saspro/wavescale_hdr.py +624 -0
- setiastro/saspro/wavescale_hdr_preset.py +101 -0
- setiastro/saspro/wavescalede.py +658 -0
- setiastro/saspro/wavescalede_preset.py +230 -0
- setiastro/saspro/wcs_update.py +374 -0
- setiastro/saspro/whitebalance.py +456 -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/preview_dialogs.py +280 -0
- setiastro/saspro/widgets/spinboxes.py +275 -0
- setiastro/saspro/widgets/themed_buttons.py +13 -0
- setiastro/saspro/widgets/wavelet_utils.py +299 -0
- setiastro/saspro/window_shelf.py +185 -0
- setiastro/saspro/xisf.py +1123 -0
- setiastrosuitepro-1.6.1.dist-info/METADATA +267 -0
- setiastrosuitepro-1.6.1.dist-info/RECORD +342 -0
- setiastrosuitepro-1.6.1.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.1.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.1.dist-info/licenses/LICENSE +674 -0
- setiastrosuitepro-1.6.1.dist-info/licenses/license.txt +2580 -0
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
# pro/ghs_dialog_pro.py
|
|
2
|
+
from PyQt6.QtCore import Qt, QEvent, QPointF, QTimer
|
|
3
|
+
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
|
4
|
+
QScrollArea, QComboBox, QSlider, QToolButton, QWidget, QMessageBox)
|
|
5
|
+
from PyQt6.QtGui import QPixmap, QImage, QPen, QColor
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Reuse the engine from curves_editor_pro
|
|
11
|
+
from .curve_editor_pro import (
|
|
12
|
+
CurveEditor, _CurvesWorker, _apply_mode_any, build_curve_lut,
|
|
13
|
+
_float_to_qimage_rgb8, _downsample_for_preview, ImageLabel
|
|
14
|
+
)
|
|
15
|
+
from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
|
|
16
|
+
|
|
17
|
+
class GhsDialogPro(QDialog):
|
|
18
|
+
"""
|
|
19
|
+
Hyperbolic Stretch dialog:
|
|
20
|
+
- Left: α/β/γ + LP/HP + channel selector
|
|
21
|
+
- Right: same preview/zoom/pan as CurvesDialogPro
|
|
22
|
+
- Uses CurveEditor for the actual curve, but the points are generated from parameters.
|
|
23
|
+
"""
|
|
24
|
+
def __init__(self, parent, document):
|
|
25
|
+
super().__init__(parent)
|
|
26
|
+
self.setWindowTitle(self.tr("Hyperbolic Stretch"))
|
|
27
|
+
self.doc = document
|
|
28
|
+
self._preview_img = None
|
|
29
|
+
self._full_img = None
|
|
30
|
+
self._pix = None
|
|
31
|
+
self._zoom = 0.25
|
|
32
|
+
self._panning = False
|
|
33
|
+
self._pan_start = QPointF()
|
|
34
|
+
self._sym_u = 0.5 # pivot in [0..1]
|
|
35
|
+
|
|
36
|
+
# ---------- layout ----------
|
|
37
|
+
main = QHBoxLayout(self)
|
|
38
|
+
|
|
39
|
+
# Left controls
|
|
40
|
+
left = QVBoxLayout()
|
|
41
|
+
self.editor = CurveEditor(self)
|
|
42
|
+
left.addWidget(self.editor)
|
|
43
|
+
|
|
44
|
+
hint = QLabel(self.tr("Tip: Ctrl+Click (or double-click) the image to set the symmetry pivot"))
|
|
45
|
+
hint.setStyleSheet("color: #888; font-size: 11px;")
|
|
46
|
+
left.addWidget(hint)
|
|
47
|
+
self.editor.setToolTip(self.tr("Ctrl+Click (or double-click) the image to set the symmetry pivot"))
|
|
48
|
+
|
|
49
|
+
# channel selector
|
|
50
|
+
ch_row = QHBoxLayout()
|
|
51
|
+
ch_row.addWidget(QLabel(self.tr("Channel:")))
|
|
52
|
+
self.cmb_ch = QComboBox(self)
|
|
53
|
+
self.cmb_ch.addItems(["K (Brightness)", "R", "G", "B"])
|
|
54
|
+
ch_row.addWidget(self.cmb_ch)
|
|
55
|
+
left.addLayout(ch_row)
|
|
56
|
+
|
|
57
|
+
# α / β / γ
|
|
58
|
+
def _mk_slider_row(name, rng, val):
|
|
59
|
+
row = QHBoxLayout()
|
|
60
|
+
lab = QLabel(name); row.addWidget(lab)
|
|
61
|
+
s = QSlider(Qt.Orientation.Horizontal); s.setRange(*rng); s.setValue(val); row.addWidget(s)
|
|
62
|
+
v = QLabel(f"{val/100:.2f}" if name=="γ" else f"{val/50:.2f}"); row.addWidget(v)
|
|
63
|
+
return row, s, v
|
|
64
|
+
|
|
65
|
+
rowA, self.sA, self.labA = _mk_slider_row("α", (1, 500), 50) # 1.0
|
|
66
|
+
rowB, self.sB, self.labB = _mk_slider_row("β", (1, 500), 50) # 1.0
|
|
67
|
+
rowG, self.sG, self.labG = _mk_slider_row("γ", (1, 500), 100) # 1.0
|
|
68
|
+
left.addLayout(rowA); left.addLayout(rowB); left.addLayout(rowG)
|
|
69
|
+
|
|
70
|
+
# LP / HP (protect)
|
|
71
|
+
rowLP = QHBoxLayout(); rowHP = QHBoxLayout()
|
|
72
|
+
rowLP.addWidget(QLabel("LP")); self.sLP = QSlider(Qt.Orientation.Horizontal); self.sLP.setRange(0,360); rowLP.addWidget(self.sLP); self.labLP = QLabel("0.00"); rowLP.addWidget(self.labLP)
|
|
73
|
+
rowHP.addWidget(QLabel("HP")); self.sHP = QSlider(Qt.Orientation.Horizontal); self.sHP.setRange(0,360); rowHP.addWidget(self.sHP); self.labHP = QLabel("0.00"); rowHP.addWidget(self.labHP)
|
|
74
|
+
left.addLayout(rowLP); left.addLayout(rowHP)
|
|
75
|
+
|
|
76
|
+
# Buttons
|
|
77
|
+
rowb = QHBoxLayout()
|
|
78
|
+
self.btn_apply = QPushButton(self.tr("Apply"))
|
|
79
|
+
self.btn_reset = QToolButton(); self.btn_reset.setText(self.tr("Reset"))
|
|
80
|
+
self.btn_hist = QToolButton(); self.btn_hist.setText(self.tr("Histogram"))
|
|
81
|
+
self.btn_hist.setToolTip(self.tr("Open a Histogram for this image.\n"
|
|
82
|
+
"Ctrl+Click on the histogram to set the GHS pivot."))
|
|
83
|
+
rowb.addWidget(self.btn_apply)
|
|
84
|
+
rowb.addWidget(self.btn_reset)
|
|
85
|
+
rowb.addWidget(self.btn_hist)
|
|
86
|
+
left.addLayout(rowb)
|
|
87
|
+
left.addStretch(1)
|
|
88
|
+
|
|
89
|
+
main.addLayout(left, 0)
|
|
90
|
+
|
|
91
|
+
# --- Right preview panel ---
|
|
92
|
+
right = QVBoxLayout()
|
|
93
|
+
zoombar = QHBoxLayout()
|
|
94
|
+
zoombar.addStretch(1)
|
|
95
|
+
|
|
96
|
+
b_out = themed_toolbtn("zoom-out", self.tr("Zoom Out"))
|
|
97
|
+
b_in = themed_toolbtn("zoom-in", self.tr("Zoom In"))
|
|
98
|
+
b_fit = themed_toolbtn("zoom-fit-best", self.tr("Fit to Preview"))
|
|
99
|
+
|
|
100
|
+
zoombar.addWidget(b_out)
|
|
101
|
+
zoombar.addWidget(b_in)
|
|
102
|
+
zoombar.addWidget(b_fit)
|
|
103
|
+
|
|
104
|
+
right.addLayout(zoombar)
|
|
105
|
+
self.scroll = QScrollArea()
|
|
106
|
+
self.scroll.setWidgetResizable(True)
|
|
107
|
+
self.scroll.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
108
|
+
|
|
109
|
+
# CREATE LABEL FIRST
|
|
110
|
+
self.label = ImageLabel(self) # <- make sure ImageLabel is imported
|
|
111
|
+
self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
112
|
+
self.label.mouseMoved.connect(self._on_preview_mouse_moved)
|
|
113
|
+
self.label.installEventFilter(self)
|
|
114
|
+
|
|
115
|
+
self.scroll.setWidget(self.label)
|
|
116
|
+
# INSTALL FILTERS AFTER label exists
|
|
117
|
+
self.scroll.viewport().installEventFilter(self)
|
|
118
|
+
|
|
119
|
+
right.addWidget(self.scroll, 1)
|
|
120
|
+
main.addLayout(right, 1)
|
|
121
|
+
|
|
122
|
+
# ---------- wiring ----------
|
|
123
|
+
self.editor.setPreviewCallback(lambda _lut8: self._quick_preview())
|
|
124
|
+
self.editor.setSymmetryCallback(self._on_symmetry_pick)
|
|
125
|
+
|
|
126
|
+
self.sA.valueChanged.connect(self._rebuild_from_params)
|
|
127
|
+
self.sB.valueChanged.connect(self._rebuild_from_params)
|
|
128
|
+
self.sG.valueChanged.connect(self._rebuild_from_params)
|
|
129
|
+
self.sLP.valueChanged.connect(self._rebuild_from_params)
|
|
130
|
+
self.sHP.valueChanged.connect(self._rebuild_from_params)
|
|
131
|
+
self.cmb_ch.currentTextChanged.connect(self._recolor_curve)
|
|
132
|
+
|
|
133
|
+
self.btn_apply.clicked.connect(self._apply)
|
|
134
|
+
self.btn_reset.clicked.connect(self._reset)
|
|
135
|
+
self._hist_dlg = None # will hold our per-GHS histogram dialog
|
|
136
|
+
self.btn_hist.clicked.connect(self._open_histogram)
|
|
137
|
+
|
|
138
|
+
b_out.clicked.connect(lambda: self._set_zoom(self._zoom / 1.25))
|
|
139
|
+
b_in .clicked.connect(lambda: self._set_zoom(self._zoom * 1.25))
|
|
140
|
+
b_fit.clicked.connect(self._fit)
|
|
141
|
+
|
|
142
|
+
# seed image data
|
|
143
|
+
self._load_from_doc()
|
|
144
|
+
|
|
145
|
+
# start with Fit to Preview (avoids offset issues)
|
|
146
|
+
QTimer.singleShot(0, self._fit)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# first curve
|
|
150
|
+
self._rebuild_from_params()
|
|
151
|
+
|
|
152
|
+
# ---------- params → handles/curve ----------
|
|
153
|
+
def _open_histogram(self):
|
|
154
|
+
"""Open (or raise) a HistogramDialog bound to this document and
|
|
155
|
+
connect its pivot signal to our symmetry pivot."""
|
|
156
|
+
try:
|
|
157
|
+
from .histogram import HistogramDialog
|
|
158
|
+
except Exception as e:
|
|
159
|
+
QMessageBox.warning(self, self.tr("Histogram"),
|
|
160
|
+
self.tr("Could not import histogram module:\n{0}").format(e))
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
# If we already created one and it's still alive, just bring it forward.
|
|
164
|
+
if self._hist_dlg is not None:
|
|
165
|
+
try:
|
|
166
|
+
if self._hist_dlg.isVisible():
|
|
167
|
+
self._hist_dlg.raise_()
|
|
168
|
+
self._hist_dlg.activateWindow()
|
|
169
|
+
return
|
|
170
|
+
except RuntimeError:
|
|
171
|
+
# dialog was destroyed; fall through to recreate
|
|
172
|
+
self._hist_dlg = None
|
|
173
|
+
|
|
174
|
+
dlg = HistogramDialog(self, self.doc)
|
|
175
|
+
self._hist_dlg = dlg
|
|
176
|
+
try:
|
|
177
|
+
dlg.pivotPicked.connect(self._on_hist_pivot)
|
|
178
|
+
except Exception:
|
|
179
|
+
# if signal isn't there for some reason, just ignore
|
|
180
|
+
pass
|
|
181
|
+
dlg.show()
|
|
182
|
+
|
|
183
|
+
def _on_hist_pivot(self, u: float):
|
|
184
|
+
"""
|
|
185
|
+
Receive normalized pivot from Histogram (0..1) and update our symmetry
|
|
186
|
+
point & curve.
|
|
187
|
+
"""
|
|
188
|
+
u = float(np.clip(u, 0.0, 1.0))
|
|
189
|
+
self._sym_u = u
|
|
190
|
+
# CurveEditor uses 0..360 in X; Y doesn't matter for the vertical line
|
|
191
|
+
self.editor.setSymmetryPoint(u * 360.0, 0.0)
|
|
192
|
+
self._rebuild_from_params()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _on_symmetry_pick(self, u, v):
|
|
196
|
+
self._sym_u = float(u)
|
|
197
|
+
self._rebuild_from_params()
|
|
198
|
+
|
|
199
|
+
def _rebuild_from_params(self):
|
|
200
|
+
a = self.sA.value()/50.0
|
|
201
|
+
b = self.sB.value()/50.0
|
|
202
|
+
g = self.sG.value()/100.0
|
|
203
|
+
self.labA.setText(f"{a:.2f}")
|
|
204
|
+
self.labB.setText(f"{b:.2f}")
|
|
205
|
+
self.labG.setText(f"{g:.2f}")
|
|
206
|
+
|
|
207
|
+
# number of handles (keep existing count or default to 20)
|
|
208
|
+
N = len(self.editor.control_points) or 20
|
|
209
|
+
if len(self.editor.control_points) == 0:
|
|
210
|
+
for _ in range(N):
|
|
211
|
+
self.editor.addControlPoint(0, 0)
|
|
212
|
+
|
|
213
|
+
SP = float(self._sym_u)
|
|
214
|
+
eps = 1e-6
|
|
215
|
+
|
|
216
|
+
# --- sample around 0.5, then REMAP x to SP (this is the key) ---
|
|
217
|
+
us = np.linspace(0.0, 1.0, N) # even sampling
|
|
218
|
+
left = us <= 0.5
|
|
219
|
+
right = ~left
|
|
220
|
+
|
|
221
|
+
# generalized hyperbolic (two shapes, mirrored at 0.5)
|
|
222
|
+
rawL = us**a / (us**a + b*(1.0-us)**a)
|
|
223
|
+
rawR = us**a / (us**a + (1.0/b)*(1.0-us)**a)
|
|
224
|
+
|
|
225
|
+
midL = (0.5**a) / (0.5**a + b*(0.5)**a)
|
|
226
|
+
midR = (0.5**a) / (0.5**a + (1.0/b)*(0.5)**a)
|
|
227
|
+
|
|
228
|
+
# map domain to pivoted x ("up") and scaled y ("vp")
|
|
229
|
+
up = np.empty_like(us)
|
|
230
|
+
vp = np.empty_like(us)
|
|
231
|
+
|
|
232
|
+
# left half → [0 .. SP]
|
|
233
|
+
up[left] = 2.0 * SP * us[left]
|
|
234
|
+
vp[left] = rawL[left] * (SP / max(midL, eps))
|
|
235
|
+
|
|
236
|
+
# right half → [SP .. 1]
|
|
237
|
+
up[right] = SP + 2.0*(1.0 - SP)*(us[right] - 0.5)
|
|
238
|
+
vp[right] = SP + (rawR[right] - midR) * ((1.0 - SP) / max(1.0 - midR, eps))
|
|
239
|
+
|
|
240
|
+
# LP/HP protection: blend toward identity (vp == up)
|
|
241
|
+
LP = self.sLP.value()/360.0
|
|
242
|
+
HP = self.sHP.value()/360.0
|
|
243
|
+
|
|
244
|
+
if LP > 0:
|
|
245
|
+
m = up <= SP
|
|
246
|
+
vp[m] = (1.0 - LP)*vp[m] + LP*up[m]
|
|
247
|
+
if HP > 0:
|
|
248
|
+
m = up >= SP
|
|
249
|
+
vp[m] = (1.0 - HP)*vp[m] + HP*up[m]
|
|
250
|
+
|
|
251
|
+
self.labLP.setText(f"{LP:.2f}")
|
|
252
|
+
self.labHP.setText(f"{HP:.2f}")
|
|
253
|
+
|
|
254
|
+
# gamma lift
|
|
255
|
+
if abs(g - 1.0) > 1e-6:
|
|
256
|
+
vp = np.clip(vp, 0.0, 1.0) ** (1.0 / g)
|
|
257
|
+
|
|
258
|
+
# keep in range & gently enforce monotonicity to avoid tiny dips
|
|
259
|
+
vp = np.clip(vp, 0.0, 1.0)
|
|
260
|
+
vp = np.maximum.accumulate(vp)
|
|
261
|
+
|
|
262
|
+
# write handles back (x rightward, y inverted for the grid)
|
|
263
|
+
xs = up * 360.0
|
|
264
|
+
ys = (1.0 - vp) * 360.0
|
|
265
|
+
pts = list(zip(xs.astype(float), ys.astype(float)))
|
|
266
|
+
|
|
267
|
+
cps_sorted = sorted(self.editor.control_points, key=lambda p: p.scenePos().x())
|
|
268
|
+
for p, (x, y) in zip(cps_sorted, pts):
|
|
269
|
+
p.setPos(x, y)
|
|
270
|
+
|
|
271
|
+
self._recolor_curve()
|
|
272
|
+
self.editor.updateCurve()
|
|
273
|
+
self._quick_preview()
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _recolor_curve(self):
|
|
277
|
+
color_map = {
|
|
278
|
+
"K (Brightness)": Qt.GlobalColor.white,
|
|
279
|
+
"R": Qt.GlobalColor.red, "G": Qt.GlobalColor.green, "B": Qt.GlobalColor.blue
|
|
280
|
+
}
|
|
281
|
+
ch = self.cmb_ch.currentText()
|
|
282
|
+
if getattr(self.editor, "curve_item", None):
|
|
283
|
+
pen = QPen(color_map[ch]); pen.setWidth(3)
|
|
284
|
+
self.editor.curve_item.setPen(pen)
|
|
285
|
+
self._quick_preview()
|
|
286
|
+
|
|
287
|
+
# ---------- preview/apply (same as CurvesDialogPro) ----------
|
|
288
|
+
def _build_lut01(self):
|
|
289
|
+
fn = getattr(self.editor, "getCurveFunction", None)
|
|
290
|
+
if not fn: return None
|
|
291
|
+
f = fn()
|
|
292
|
+
if f is None: return None
|
|
293
|
+
try:
|
|
294
|
+
return build_curve_lut(f, size=65536)
|
|
295
|
+
except Exception:
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
def _quick_preview(self):
|
|
299
|
+
if self._preview_img is None:
|
|
300
|
+
return
|
|
301
|
+
lut01 = self._build_lut01()
|
|
302
|
+
if lut01 is None:
|
|
303
|
+
return
|
|
304
|
+
mode = self.cmb_ch.currentText()
|
|
305
|
+
out = _apply_mode_any(self._preview_img, mode, lut01)
|
|
306
|
+
out = self._blend_with_mask(out) # ✅ blend with mask
|
|
307
|
+
self._update_preview_pix(out)
|
|
308
|
+
|
|
309
|
+
def _apply(self):
|
|
310
|
+
if self._full_img is None:
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
luts = self._build_all_active_luts()
|
|
314
|
+
|
|
315
|
+
self.btn_apply.setEnabled(False)
|
|
316
|
+
self._thr = _CurvesWorker(self._full_img, luts, self)
|
|
317
|
+
# ⬇️ use the handler you ALREADY have, which commits + metadata + reset
|
|
318
|
+
self._thr.done.connect(self._on_apply_ready)
|
|
319
|
+
self._thr.finished.connect(lambda: self.btn_apply.setEnabled(True))
|
|
320
|
+
self._thr.start()
|
|
321
|
+
|
|
322
|
+
def _build_all_active_luts(self) -> dict[str, np.ndarray]:
|
|
323
|
+
"""
|
|
324
|
+
For GHS we really only have ONE curve at a time – the one in self.editor –
|
|
325
|
+
and we apply it to the currently selected channel.
|
|
326
|
+
The worker wants a dict like {"K": lut} or {"R": lut}.
|
|
327
|
+
"""
|
|
328
|
+
lut = self._build_lut01()
|
|
329
|
+
if lut is None:
|
|
330
|
+
return {}
|
|
331
|
+
|
|
332
|
+
ch = self.cmb_ch.currentText()
|
|
333
|
+
# map UI text → worker key
|
|
334
|
+
ui2key = {
|
|
335
|
+
"K (Brightness)": "K",
|
|
336
|
+
"R": "R",
|
|
337
|
+
"G": "G",
|
|
338
|
+
"B": "B",
|
|
339
|
+
}
|
|
340
|
+
key = ui2key.get(ch, "K")
|
|
341
|
+
return {key: lut}
|
|
342
|
+
|
|
343
|
+
def _apply_all_curves_once(self, img: np.ndarray, luts: dict[str, np.ndarray]) -> np.ndarray:
|
|
344
|
+
"""
|
|
345
|
+
This is what _CurvesWorker will call.
|
|
346
|
+
We only ever expect 0 or 1 LUT here.
|
|
347
|
+
"""
|
|
348
|
+
if not luts:
|
|
349
|
+
return img
|
|
350
|
+
|
|
351
|
+
# pull the single entry
|
|
352
|
+
(key, lut), = luts.items()
|
|
353
|
+
|
|
354
|
+
# map worker key → mode string used by _apply_mode_any
|
|
355
|
+
key2mode = {
|
|
356
|
+
"K": "K (Brightness)",
|
|
357
|
+
"R": "R",
|
|
358
|
+
"G": "G",
|
|
359
|
+
"B": "B",
|
|
360
|
+
}
|
|
361
|
+
mode = key2mode.get(key, "K (Brightness)")
|
|
362
|
+
|
|
363
|
+
out = _apply_mode_any(img, mode, lut)
|
|
364
|
+
return out.astype(np.float32, copy=False)
|
|
365
|
+
|
|
366
|
+
def _on_apply_commit_ready(self, out01: np.ndarray):
|
|
367
|
+
# honor mask, same as preview
|
|
368
|
+
out01 = self._blend_with_mask(out01)
|
|
369
|
+
|
|
370
|
+
# 🔴 safety: if the document currently holds RGB but we got mono back,
|
|
371
|
+
# make it 3-channel so apply_edit doesn’t silently ignore it
|
|
372
|
+
doc_img = np.asarray(self.doc.image)
|
|
373
|
+
if doc_img.ndim == 3 and out01.ndim == 2:
|
|
374
|
+
out01 = np.repeat(out01[..., None], 3, axis=2)
|
|
375
|
+
|
|
376
|
+
# now do the normal commit (history, reload, reset curves, etc.)
|
|
377
|
+
self._commit(out01)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _on_apply_ready(self, out01: np.ndarray):
|
|
381
|
+
try:
|
|
382
|
+
# honor mask, same as preview
|
|
383
|
+
out_masked = self._blend_with_mask(out01)
|
|
384
|
+
|
|
385
|
+
# 🔹 build a single params dict used by:
|
|
386
|
+
# - metadata["ghs"]
|
|
387
|
+
# - replay_last_action preset
|
|
388
|
+
ghs_params = {
|
|
389
|
+
"alpha": self.sA.value() / 50.0,
|
|
390
|
+
"beta": self.sB.value() / 50.0,
|
|
391
|
+
"gamma": self.sG.value() / 100.0,
|
|
392
|
+
"lp": self.sLP.value() / 360.0,
|
|
393
|
+
"hp": self.sHP.value() / 360.0,
|
|
394
|
+
"pivot": float(self._sym_u),
|
|
395
|
+
"channel": self.cmb_ch.currentText(),
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
_marr, mid, mname = self._active_mask_layer()
|
|
399
|
+
meta = {
|
|
400
|
+
"step_name": "Hyperbolic Stretch",
|
|
401
|
+
"ghs": ghs_params,
|
|
402
|
+
"masked": bool(mid),
|
|
403
|
+
"mask_id": mid,
|
|
404
|
+
"mask_name": mname,
|
|
405
|
+
"mask_blend": "m*out + (1-m)*src",
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
# 🔁 Register this as "last action" for *both* dialog-replay and headless replay
|
|
409
|
+
mw = self.parent()
|
|
410
|
+
# Walk up to the main window
|
|
411
|
+
while mw is not None and not (
|
|
412
|
+
hasattr(mw, "_remember_last_action_from_dialog")
|
|
413
|
+
or hasattr(mw, "_remember_last_headless_command")
|
|
414
|
+
):
|
|
415
|
+
mw = mw.parent()
|
|
416
|
+
|
|
417
|
+
if mw is not None:
|
|
418
|
+
# Dialog-style (keeps your existing mechanism, if used elsewhere)
|
|
419
|
+
if hasattr(mw, "_remember_last_action_from_dialog"):
|
|
420
|
+
try:
|
|
421
|
+
mw._remember_last_action_from_dialog("ghs", ghs_params)
|
|
422
|
+
except Exception:
|
|
423
|
+
pass
|
|
424
|
+
|
|
425
|
+
# Headless-style (this is what ROI replay uses)
|
|
426
|
+
if hasattr(mw, "_remember_last_headless_command"):
|
|
427
|
+
try:
|
|
428
|
+
mw._remember_last_headless_command(
|
|
429
|
+
"ghs",
|
|
430
|
+
ghs_params,
|
|
431
|
+
description="Hyperbolic Stretch",
|
|
432
|
+
)
|
|
433
|
+
# DEBUG
|
|
434
|
+
try:
|
|
435
|
+
mw._log(
|
|
436
|
+
f"[Replay] GHS stored as headless command: "
|
|
437
|
+
f"preset_keys={list(ghs_params.keys())}"
|
|
438
|
+
)
|
|
439
|
+
except Exception:
|
|
440
|
+
print(
|
|
441
|
+
"[Replay] GHS stored as headless command, "
|
|
442
|
+
"preset_keys=",
|
|
443
|
+
list(ghs_params.keys()),
|
|
444
|
+
)
|
|
445
|
+
except Exception as e:
|
|
446
|
+
print("[Replay] GHS remember_last_headless_command failed:", e)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
# Commit result to the document
|
|
450
|
+
self.doc.apply_edit(out_masked.copy(),
|
|
451
|
+
metadata=meta,
|
|
452
|
+
step_name="Hyperbolic Stretch")
|
|
453
|
+
|
|
454
|
+
# 🔄 Refresh buffers from the updated doc
|
|
455
|
+
self._load_from_doc()
|
|
456
|
+
|
|
457
|
+
# 🔄 Reset pivot + curve drawing for the next pass
|
|
458
|
+
self._sym_u = 0.5
|
|
459
|
+
self.editor.clearSymmetryLine()
|
|
460
|
+
self.editor.initCurve()
|
|
461
|
+
self.sA.setValue(50); self.sB.setValue(50); self.sG.setValue(100)
|
|
462
|
+
self.sLP.setValue(0); self.sHP.setValue(0)
|
|
463
|
+
self._rebuild_from_params()
|
|
464
|
+
QTimer.singleShot(0, self._fit)
|
|
465
|
+
|
|
466
|
+
except Exception as e:
|
|
467
|
+
QMessageBox.critical(self, self.tr("Apply failed"), str(e))
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
# ---------- image plumbing / zoom/pan ----------
|
|
471
|
+
def _load_from_doc(self):
|
|
472
|
+
img = self.doc.image
|
|
473
|
+
if img is None:
|
|
474
|
+
QMessageBox.information(self, self.tr("No image"), self.tr("Open an image first."))
|
|
475
|
+
return
|
|
476
|
+
arr = np.asarray(img).astype(np.float32)
|
|
477
|
+
if arr.dtype.kind in "ui":
|
|
478
|
+
arr = arr / np.iinfo(img.dtype).max
|
|
479
|
+
self._full_img = arr
|
|
480
|
+
self._preview_img = _downsample_for_preview(arr, 1200)
|
|
481
|
+
self._update_preview_pix(self._preview_img)
|
|
482
|
+
|
|
483
|
+
def _update_preview_pix(self, img01):
|
|
484
|
+
if img01 is None:
|
|
485
|
+
self.label.clear(); self._pix = None; return
|
|
486
|
+
qimg = _float_to_qimage_rgb8(img01)
|
|
487
|
+
pm = QPixmap.fromImage(qimg)
|
|
488
|
+
self._pix = pm
|
|
489
|
+
self._apply_zoom()
|
|
490
|
+
|
|
491
|
+
def _apply_zoom(self):
|
|
492
|
+
if self._pix is None: return
|
|
493
|
+
scaled = self._pix.scaled(self._pix.size()*self._zoom,
|
|
494
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
|
495
|
+
Qt.TransformationMode.SmoothTransformation)
|
|
496
|
+
self.label.setPixmap(scaled)
|
|
497
|
+
self.label.resize(scaled.size())
|
|
498
|
+
|
|
499
|
+
def _set_zoom(self, z):
|
|
500
|
+
self._zoom = float(max(0.05, min(z, 8.0)))
|
|
501
|
+
self._apply_zoom()
|
|
502
|
+
|
|
503
|
+
def _fit(self):
|
|
504
|
+
if self._pix is None: return
|
|
505
|
+
vp = self.scroll.viewport().size()
|
|
506
|
+
if self._pix.width()==0 or self._pix.height()==0: return
|
|
507
|
+
s = min(vp.width()/self._pix.width(), vp.height()/self._pix.height())
|
|
508
|
+
self._set_zoom(max(0.05, s))
|
|
509
|
+
|
|
510
|
+
def _k_from_label_point(self, lbl_pt):
|
|
511
|
+
"""lbl_pt is in label (pixmap) coordinates."""
|
|
512
|
+
if self._preview_img is None or self.label.pixmap() is None:
|
|
513
|
+
return None
|
|
514
|
+
pix = self.label.pixmap()
|
|
515
|
+
pw, ph = pix.width(), pix.height()
|
|
516
|
+
x, y = int(lbl_pt.x()), int(lbl_pt.y())
|
|
517
|
+
if not (0 <= x < pw and 0 <= y < ph):
|
|
518
|
+
return None
|
|
519
|
+
ih, iw = self._preview_img.shape[:2]
|
|
520
|
+
ix = int(x * iw / pw)
|
|
521
|
+
iy = int(y * ih / ph)
|
|
522
|
+
ix = max(0, min(iw - 1, ix))
|
|
523
|
+
iy = max(0, min(ih - 1, iy))
|
|
524
|
+
px = self._preview_img[iy, ix]
|
|
525
|
+
k = float(np.mean(px)) if self._preview_img.ndim == 3 else float(px)
|
|
526
|
+
return max(0.0, min(1.0, k))
|
|
527
|
+
|
|
528
|
+
# ctrl+wheel zoom + panning + ctrl+click on preview to move pivot
|
|
529
|
+
def eventFilter(self, obj, ev):
|
|
530
|
+
lbl = getattr(self, "label", None)
|
|
531
|
+
if lbl is None:
|
|
532
|
+
return False
|
|
533
|
+
# --- set pivot on DOUBLE-CLICK (or Ctrl+click) anywhere over the image ---
|
|
534
|
+
if (obj is self.label or obj is self.scroll.viewport()):
|
|
535
|
+
# Double-click → set pivot
|
|
536
|
+
if ev.type() == QEvent.Type.MouseButtonDblClick and ev.button() == Qt.MouseButton.LeftButton:
|
|
537
|
+
lbl_pt = (ev.position().toPoint() if obj is self.label
|
|
538
|
+
else self.label.mapFrom(self.scroll.viewport(), ev.position().toPoint()))
|
|
539
|
+
k = self._k_from_label_point(lbl_pt)
|
|
540
|
+
if k is not None:
|
|
541
|
+
self._sym_u = k
|
|
542
|
+
self.editor.setSymmetryPoint(k * 360.0, 0)
|
|
543
|
+
self._rebuild_from_params()
|
|
544
|
+
ev.accept(); return True
|
|
545
|
+
|
|
546
|
+
# Keep Ctrl+single-click support too
|
|
547
|
+
if (ev.type() == QEvent.Type.MouseButtonPress and ev.button() == Qt.MouseButton.LeftButton
|
|
548
|
+
and (ev.modifiers() & Qt.KeyboardModifier.ControlModifier)):
|
|
549
|
+
lbl_pt = (ev.position().toPoint() if obj is self.label
|
|
550
|
+
else self.label.mapFrom(self.scroll.viewport(), ev.position().toPoint()))
|
|
551
|
+
k = self._k_from_label_point(lbl_pt)
|
|
552
|
+
if k is not None:
|
|
553
|
+
self._sym_u = k
|
|
554
|
+
self.editor.setSymmetryPoint(k * 360.0, 0)
|
|
555
|
+
self._rebuild_from_params()
|
|
556
|
+
ev.accept(); return True
|
|
557
|
+
|
|
558
|
+
# --- existing zoom/pan handling (unchanged) ---
|
|
559
|
+
if obj is self.scroll.viewport():
|
|
560
|
+
if ev.type() == QEvent.Type.Wheel and (ev.modifiers() & Qt.KeyboardModifier.ControlModifier):
|
|
561
|
+
self._set_zoom(self._zoom * (1.25 if ev.angleDelta().y() > 0 else 0.8))
|
|
562
|
+
ev.accept(); return True
|
|
563
|
+
if ev.type() == QEvent.Type.MouseButtonPress and ev.button() == Qt.MouseButton.LeftButton:
|
|
564
|
+
self._panning = True; self._pan_start = ev.position()
|
|
565
|
+
self.scroll.viewport().setCursor(Qt.CursorShape.ClosedHandCursor)
|
|
566
|
+
ev.accept(); return True
|
|
567
|
+
if ev.type() == QEvent.Type.MouseMove and self._panning:
|
|
568
|
+
d = ev.position() - self._pan_start
|
|
569
|
+
h = self.scroll.horizontalScrollBar(); v = self.scroll.verticalScrollBar()
|
|
570
|
+
h.setValue(h.value() - int(d.x())); v.setValue(v.value() - int(d.y()))
|
|
571
|
+
self._pan_start = ev.position()
|
|
572
|
+
ev.accept(); return True
|
|
573
|
+
if ev.type() == QEvent.Type.MouseButtonRelease and ev.button() == Qt.MouseButton.LeftButton:
|
|
574
|
+
self._panning = False
|
|
575
|
+
self.scroll.viewport().setCursor(Qt.CursorShape.ArrowCursor)
|
|
576
|
+
ev.accept(); return True
|
|
577
|
+
|
|
578
|
+
return super().eventFilter(obj, ev)
|
|
579
|
+
|
|
580
|
+
def _on_preview_mouse_moved(self, x: float, y: float):
|
|
581
|
+
if self._panning or self._preview_img is None or self._pix is None:
|
|
582
|
+
return
|
|
583
|
+
ix = int(x / max(self._zoom, 1e-6))
|
|
584
|
+
iy = int(y / max(self._zoom, 1e-6))
|
|
585
|
+
ix = max(0, min(self._pix.width() - 1, ix))
|
|
586
|
+
iy = max(0, min(self._pix.height() - 1, iy))
|
|
587
|
+
|
|
588
|
+
img = self._preview_img
|
|
589
|
+
if img.ndim == 2 or (img.ndim == 3 and img.shape[2] == 1):
|
|
590
|
+
v = float(img[iy, ix] if img.ndim == 2 else img[iy, ix, 0])
|
|
591
|
+
v = float(np.clip(v, 0.0, 1.0))
|
|
592
|
+
self.editor.updateValueLines(v, 0.0, 0.0, grayscale=True)
|
|
593
|
+
else:
|
|
594
|
+
r, g, b = img[iy, ix, 0], img[iy, ix, 1], img[iy, ix, 2]
|
|
595
|
+
r = float(np.clip(r, 0.0, 1.0)); g = float(np.clip(g, 0.0, 1.0)); b = float(np.clip(b, 0.0, 1.0))
|
|
596
|
+
self.editor.updateValueLines(r, g, b, grayscale=False)
|
|
597
|
+
|
|
598
|
+
# --- mask helpers ---------------------------------------------------
|
|
599
|
+
def _active_mask_layer(self):
|
|
600
|
+
"""Return (mask_float01, mask_id, mask_name) or (None, None, None)."""
|
|
601
|
+
mid = getattr(self.doc, "active_mask_id", None)
|
|
602
|
+
if not mid: return None, None, None
|
|
603
|
+
layer = getattr(self.doc, "masks", {}).get(mid)
|
|
604
|
+
if layer is None: return None, None, None
|
|
605
|
+
m = np.asarray(getattr(layer, "data", None))
|
|
606
|
+
if m is None or m.size == 0: return None, None, None
|
|
607
|
+
m = m.astype(np.float32, copy=False)
|
|
608
|
+
if m.dtype.kind in "ui":
|
|
609
|
+
m /= float(np.iinfo(m.dtype).max)
|
|
610
|
+
else:
|
|
611
|
+
mx = float(m.max()) if m.size else 1.0
|
|
612
|
+
if mx > 1.0: m /= mx
|
|
613
|
+
return np.clip(m, 0.0, 1.0), mid, getattr(layer, "name", "Mask")
|
|
614
|
+
|
|
615
|
+
def _resample_mask_if_needed(self, mask: np.ndarray, out_hw: tuple[int,int]) -> np.ndarray:
|
|
616
|
+
"""Nearest-neighbor resize via integer indexing."""
|
|
617
|
+
mh, mw = mask.shape[:2]
|
|
618
|
+
th, tw = out_hw
|
|
619
|
+
if (mh, mw) == (th, tw): return mask
|
|
620
|
+
yi = np.linspace(0, mh - 1, th).astype(np.int32)
|
|
621
|
+
xi = np.linspace(0, mw - 1, tw).astype(np.int32)
|
|
622
|
+
return mask[yi][:, xi]
|
|
623
|
+
|
|
624
|
+
def _blend_with_mask(self, processed: np.ndarray) -> np.ndarray:
|
|
625
|
+
"""
|
|
626
|
+
Blend processed image with original using active mask (if any).
|
|
627
|
+
Chooses original from preview/full buffers to match shape.
|
|
628
|
+
"""
|
|
629
|
+
mask, _mid, _mname = self._active_mask_layer()
|
|
630
|
+
if mask is None:
|
|
631
|
+
return processed
|
|
632
|
+
|
|
633
|
+
out = processed.astype(np.float32, copy=False)
|
|
634
|
+
|
|
635
|
+
# choose the matching original buffer (same HxW as 'out')
|
|
636
|
+
if (hasattr(self, "_full_img") and self._full_img is not None
|
|
637
|
+
and out.shape[:2] == self._full_img.shape[:2]):
|
|
638
|
+
src = self._full_img
|
|
639
|
+
else:
|
|
640
|
+
src = self._preview_img
|
|
641
|
+
|
|
642
|
+
m = self._resample_mask_if_needed(mask, out.shape[:2])
|
|
643
|
+
if out.ndim == 3 and out.shape[2] == 3:
|
|
644
|
+
m = m[..., None]
|
|
645
|
+
|
|
646
|
+
# reconcile mono vs RGB
|
|
647
|
+
if src.ndim == 2 and out.ndim == 3:
|
|
648
|
+
src = np.stack([src]*3, axis=-1)
|
|
649
|
+
elif src.ndim == 3 and out.ndim == 2:
|
|
650
|
+
src = src[..., 0]
|
|
651
|
+
|
|
652
|
+
return (m * out + (1.0 - m) * src).astype(np.float32, copy=False)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _reset(self):
|
|
656
|
+
self.sA.setValue(50); self.sB.setValue(50); self.sG.setValue(100)
|
|
657
|
+
self.sLP.setValue(0); self.sHP.setValue(0)
|
|
658
|
+
self._sym_u = 0.5
|
|
659
|
+
self.editor.clearSymmetryLine()
|
|
660
|
+
self._rebuild_from_params()
|