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,462 @@
|
|
|
1
|
+
# pro/halobgon.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import numpy as np
|
|
4
|
+
import cv2
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from PyQt6.QtCore import Qt, QTimer, QRectF
|
|
8
|
+
from PyQt6.QtGui import QImage, QPixmap, QIcon, QTransform, QPainter
|
|
9
|
+
from PyQt6.QtWidgets import (
|
|
10
|
+
QDialog, QVBoxLayout, QHBoxLayout, QGroupBox, QGridLayout, QLabel, QPushButton,
|
|
11
|
+
QSlider, QCheckBox, QComboBox, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem,
|
|
12
|
+
QMessageBox, QWidget, QRadioButton
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# -------- Optional numba utils (LUT in-place speedups) --------------------
|
|
16
|
+
try:
|
|
17
|
+
from setiastro.saspro.legacy.numba_utils import apply_lut_mono_inplace as _lut_mono_inplace
|
|
18
|
+
from setiastro.saspro.legacy.numba_utils import apply_lut_color_inplace as _lut_color_inplace
|
|
19
|
+
except Exception:
|
|
20
|
+
_lut_mono_inplace = None
|
|
21
|
+
_lut_color_inplace = None
|
|
22
|
+
|
|
23
|
+
from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# =============================================================================
|
|
27
|
+
# Helpers
|
|
28
|
+
# =============================================================================
|
|
29
|
+
def _as_rgb(a: np.ndarray) -> np.ndarray:
|
|
30
|
+
a = np.asarray(a, dtype=np.float32)
|
|
31
|
+
a = np.clip(a, 0.0, 1.0)
|
|
32
|
+
if a.ndim == 2:
|
|
33
|
+
a = np.repeat(a[..., None], 3, axis=2)
|
|
34
|
+
elif a.ndim == 3 and a.shape[2] == 1:
|
|
35
|
+
a = np.repeat(a, 3, axis=2)
|
|
36
|
+
else:
|
|
37
|
+
a = a[:, :, :3]
|
|
38
|
+
return a
|
|
39
|
+
|
|
40
|
+
def _qimage_from_rgb01(a: np.ndarray) -> QImage:
|
|
41
|
+
a = np.clip(a, 0, 1).astype(np.float32)
|
|
42
|
+
a8 = (a * 255.0).astype(np.uint8)
|
|
43
|
+
h, w = a8.shape[:2]
|
|
44
|
+
return QImage(a8.data, w, h, w*3, QImage.Format.Format_RGB888).copy()
|
|
45
|
+
|
|
46
|
+
def _maybe_get_mask(parent, ref_img: np.ndarray) -> Optional[np.ndarray]:
|
|
47
|
+
"""Fetch an applied mask as float [0..1], broadcastable to ref_img."""
|
|
48
|
+
try:
|
|
49
|
+
mm = None
|
|
50
|
+
if hasattr(parent, "mask_manager"):
|
|
51
|
+
mm = parent.mask_manager
|
|
52
|
+
elif hasattr(parent, "image_manager") and getattr(parent.image_manager, "mask_manager", None):
|
|
53
|
+
mm = parent.image_manager.mask_manager
|
|
54
|
+
if mm is None:
|
|
55
|
+
return None
|
|
56
|
+
m = mm.get_applied_mask()
|
|
57
|
+
if m is None:
|
|
58
|
+
return None
|
|
59
|
+
m = np.asarray(m)
|
|
60
|
+
if m.dtype.kind in "ui":
|
|
61
|
+
m = m.astype(np.float32) / 255.0
|
|
62
|
+
m = np.clip(m, 0.0, 1.0)
|
|
63
|
+
if ref_img.ndim == 3 and m.ndim == 2:
|
|
64
|
+
m = m[..., None]
|
|
65
|
+
if m.shape[:2] != ref_img.shape[:2]:
|
|
66
|
+
return None
|
|
67
|
+
if ref_img.ndim == 3 and m.shape[-1] == 1:
|
|
68
|
+
m = np.repeat(m, ref_img.shape[2], axis=2)
|
|
69
|
+
return m
|
|
70
|
+
except Exception:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
# -------- LUTs (curves) ---------------------------------------------------
|
|
74
|
+
def _curve_lut(reduction_level: int) -> np.ndarray:
|
|
75
|
+
"""
|
|
76
|
+
SASv2 used stronger darkening as level increases:
|
|
77
|
+
0→γ=1.2, 1→1.5, 2→1.8, 3→2.2
|
|
78
|
+
"""
|
|
79
|
+
gammas = [1.2, 1.5, 1.8, 2.2]
|
|
80
|
+
g = gammas[max(0, min(3, int(reduction_level)))]
|
|
81
|
+
x = np.linspace(0, 1, 256, dtype=np.float32)
|
|
82
|
+
y = np.power(x, g)
|
|
83
|
+
lut = np.clip((y * 255.0).round(), 0, 255).astype(np.uint8)
|
|
84
|
+
return lut
|
|
85
|
+
|
|
86
|
+
def _apply_curve_inplace(img01: np.ndarray, lut: np.ndarray) -> np.ndarray:
|
|
87
|
+
"""
|
|
88
|
+
Apply 8-bit LUT to an [0..1] float image. We *always* go through cv2.LUT to
|
|
89
|
+
avoid any view/aliasing surprises. Returns img01 (modified in place).
|
|
90
|
+
"""
|
|
91
|
+
u8 = (np.clip(img01, 0.0, 1.0) * 255.0).astype(np.uint8, copy=False)
|
|
92
|
+
mapped = cv2.LUT(u8, lut).astype(np.float32) / 255.0
|
|
93
|
+
np.copyto(img01, mapped)
|
|
94
|
+
return img01
|
|
95
|
+
|
|
96
|
+
# =============================================================================
|
|
97
|
+
# Core algorithm
|
|
98
|
+
# =============================================================================
|
|
99
|
+
def compute_halo_b_gon(image: np.ndarray, reduction_level: int = 0, is_linear: bool = False) -> np.ndarray:
|
|
100
|
+
"""
|
|
101
|
+
Exact port of SASv2 (HaloProcessingThread.applyHaloReduction):
|
|
102
|
+
- operate in [0..1]
|
|
103
|
+
- optional gamma-domain pre-pass for linear data (x ** 1/5)
|
|
104
|
+
- lightness mask is built in 8-bit scale (divide by 255.0), then unsharp
|
|
105
|
+
- enhanced_mask = (1 - unsharp) - blur(unsharp) * (level * 0.33)
|
|
106
|
+
- cv2.multiply(image, enhanced_mask)
|
|
107
|
+
- per-level curves (gamma) via LUT: [1.2, 1.5, 1.8, 2.2]
|
|
108
|
+
Returns the SAME shape as input (2D stays 2D; RGB stays RGB; 1-chan stays 1-chan).
|
|
109
|
+
"""
|
|
110
|
+
img = np.clip(np.asarray(image, dtype=np.float32), 0.0, 1.0)
|
|
111
|
+
|
|
112
|
+
# Work buffer (apply gamma-domain if linear)
|
|
113
|
+
work = img
|
|
114
|
+
if is_linear:
|
|
115
|
+
with np.errstate(invalid='ignore'):
|
|
116
|
+
work = np.power(np.clip(work, 0.0, 1.0), 1.0 / 5.0)
|
|
117
|
+
|
|
118
|
+
# --- Lightness mask (SASv2 did /255.0 even though image is [0..1]) ---
|
|
119
|
+
if work.ndim == 2 or (work.ndim == 3 and work.shape[2] == 1):
|
|
120
|
+
# treat as grayscale
|
|
121
|
+
light = work if work.ndim == 2 else work[..., 0]
|
|
122
|
+
light = (light.astype(np.float32)) / 255.0
|
|
123
|
+
else:
|
|
124
|
+
# RGB → gray, then scale to 8-bit range
|
|
125
|
+
light = cv2.cvtColor(work, cv2.COLOR_RGB2GRAY) / 255.0
|
|
126
|
+
|
|
127
|
+
blurred = cv2.GaussianBlur(light, (0, 0), sigmaX=2)
|
|
128
|
+
unsharp = cv2.addWeighted(light, 1.66, blurred, -0.66, 0.0)
|
|
129
|
+
|
|
130
|
+
# --- Enhanced mask (exact SASv2 order) ---
|
|
131
|
+
inv = 1.0 - unsharp
|
|
132
|
+
dup = cv2.GaussianBlur(unsharp, (0, 0), sigmaX=2)
|
|
133
|
+
scale = float(max(0, min(3, int(reduction_level)))) * 0.33
|
|
134
|
+
enhanced_mask = inv - dup * scale
|
|
135
|
+
|
|
136
|
+
# Match mask shape to image shape
|
|
137
|
+
if work.ndim == 3 and work.shape[2] == 3:
|
|
138
|
+
mask = np.repeat(enhanced_mask[:, :, None], 3, axis=2).astype(work.dtype, copy=False)
|
|
139
|
+
else:
|
|
140
|
+
mask = enhanced_mask.astype(work.dtype, copy=False)
|
|
141
|
+
|
|
142
|
+
if work.shape != mask.shape:
|
|
143
|
+
raise ValueError(f"Shape mismatch between image {work.shape} and enhanced_mask {mask.shape}")
|
|
144
|
+
|
|
145
|
+
# Multiply
|
|
146
|
+
masked = cv2.multiply(work, mask)
|
|
147
|
+
|
|
148
|
+
# Curves via LUT (gamma > 1 darkens), exactly SASv2 levels
|
|
149
|
+
gammas = [1.2, 1.5, 1.8, 2.2]
|
|
150
|
+
g = gammas[int(max(0, min(3, reduction_level)))]
|
|
151
|
+
lut = (np.clip((np.linspace(0, 1, 256, dtype=np.float32) ** g) * 255.0, 0, 255)).astype(np.uint8)
|
|
152
|
+
|
|
153
|
+
u8 = (np.clip(masked, 0.0, 1.0) * 255.0).astype(np.uint8, copy=False)
|
|
154
|
+
mapped = cv2.LUT(u8, lut).astype(np.float32) / 255.0
|
|
155
|
+
|
|
156
|
+
out = np.clip(mapped, 0.0, 1.0).astype(np.float32, copy=False)
|
|
157
|
+
|
|
158
|
+
# restore 1-channel shape if input was (H,W,1)
|
|
159
|
+
if img.ndim == 3 and img.shape[2] == 1 and out.ndim == 2:
|
|
160
|
+
out = out[:, :, None]
|
|
161
|
+
|
|
162
|
+
return out
|
|
163
|
+
|
|
164
|
+
# =============================================================================
|
|
165
|
+
# Headless apply
|
|
166
|
+
# =============================================================================
|
|
167
|
+
def apply_halo_b_gon_to_doc(parent, doc, preset: dict | None):
|
|
168
|
+
"""
|
|
169
|
+
preset keys:
|
|
170
|
+
- reduction: int 0..3 (0=Extra Low, 1=Low, 2=Medium, 3=High) [default 0]
|
|
171
|
+
- linear: bool (operate in gamma domain) [default False]
|
|
172
|
+
"""
|
|
173
|
+
if doc is None or getattr(doc, "image", None) is None:
|
|
174
|
+
raise RuntimeError("Document has no image.")
|
|
175
|
+
|
|
176
|
+
img = np.asarray(doc.image, dtype=np.float32)
|
|
177
|
+
lvl = int((preset or {}).get("reduction", 0))
|
|
178
|
+
lin = bool((preset or {}).get("linear", False))
|
|
179
|
+
|
|
180
|
+
base = _as_rgb(img) if img.ndim != 2 else img
|
|
181
|
+
out = compute_halo_b_gon(img, reduction_level=lvl, is_linear=lin)
|
|
182
|
+
|
|
183
|
+
# Blend with active mask if present
|
|
184
|
+
ref = _as_rgb(img)
|
|
185
|
+
m = _maybe_get_mask(parent, ref)
|
|
186
|
+
if m is not None:
|
|
187
|
+
out_rgb = _as_rgb(out)
|
|
188
|
+
blended = np.clip(out_rgb * m + ref * (1.0 - m), 0.0, 1.0)
|
|
189
|
+
# restore mono if needed
|
|
190
|
+
if out.ndim == 2 or (out.ndim == 3 and out.shape[2] == 1):
|
|
191
|
+
blended = np.mean(blended, axis=2, dtype=np.float32)
|
|
192
|
+
if out.ndim == 3 and out.shape[2] == 1:
|
|
193
|
+
blended = blended[:, :, None]
|
|
194
|
+
out = blended
|
|
195
|
+
|
|
196
|
+
out = np.clip(out, 0.0, 1.0).astype(np.float32, copy=False)
|
|
197
|
+
|
|
198
|
+
if hasattr(doc, "set_image"):
|
|
199
|
+
doc.set_image(out, step_name="Halo-B-Gon")
|
|
200
|
+
elif hasattr(doc, "apply_numpy"):
|
|
201
|
+
doc.apply_numpy(out, step_name="Halo-B-Gon")
|
|
202
|
+
else:
|
|
203
|
+
doc.image = out
|
|
204
|
+
|
|
205
|
+
# =============================================================================
|
|
206
|
+
# UI: dialog with preview, zoom/pan, fit, overwrite/new view
|
|
207
|
+
# =============================================================================
|
|
208
|
+
class _PreviewView(QGraphicsView):
|
|
209
|
+
def __init__(self, scene):
|
|
210
|
+
super().__init__(scene)
|
|
211
|
+
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
212
|
+
# ✅ Use QPainter.RenderHint.* here
|
|
213
|
+
self.setRenderHints(
|
|
214
|
+
self.renderHints()
|
|
215
|
+
| QPainter.RenderHint.Antialiasing
|
|
216
|
+
| QPainter.RenderHint.SmoothPixmapTransform
|
|
217
|
+
)
|
|
218
|
+
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
|
|
219
|
+
self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
|
220
|
+
self._zoom = 1.0
|
|
221
|
+
|
|
222
|
+
def wheelEvent(self, e):
|
|
223
|
+
if e.modifiers() & Qt.KeyboardModifier.ControlModifier:
|
|
224
|
+
step = 1.15 if e.angleDelta().y() > 0 else 1/1.15
|
|
225
|
+
self.set_zoom(self._zoom * step)
|
|
226
|
+
e.accept(); return
|
|
227
|
+
super().wheelEvent(e)
|
|
228
|
+
|
|
229
|
+
def set_zoom(self, z):
|
|
230
|
+
z = max(0.1, min(10.0, float(z)))
|
|
231
|
+
self._zoom = z
|
|
232
|
+
self.setTransform(QTransform().scale(z, z))
|
|
233
|
+
|
|
234
|
+
def zoom_in(self): self.set_zoom(self._zoom * 1.15)
|
|
235
|
+
def zoom_out(self): self.set_zoom(self._zoom / 1.15)
|
|
236
|
+
def fit_to(self, rect: QRectF):
|
|
237
|
+
if rect.isEmpty(): return
|
|
238
|
+
self.fitInView(rect, Qt.AspectRatioMode.KeepAspectRatio)
|
|
239
|
+
self._zoom = 1.0
|
|
240
|
+
|
|
241
|
+
class HaloBGonDialogPro(QDialog):
|
|
242
|
+
"""
|
|
243
|
+
Minimal, fast UI:
|
|
244
|
+
• Reduction level (0..3)
|
|
245
|
+
• Linear-data checkbox
|
|
246
|
+
• Live preview (debounced)
|
|
247
|
+
• Apply to: Overwrite active / Create new view
|
|
248
|
+
"""
|
|
249
|
+
def __init__(self, parent, doc, icon: Optional[QIcon] = None):
|
|
250
|
+
super().__init__(parent)
|
|
251
|
+
self.setWindowTitle("Halo-B-Gon")
|
|
252
|
+
if icon:
|
|
253
|
+
try: self.setWindowIcon(icon)
|
|
254
|
+
except Exception as e:
|
|
255
|
+
import logging
|
|
256
|
+
logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
|
|
257
|
+
|
|
258
|
+
self.parent_ref = parent
|
|
259
|
+
self.doc = doc
|
|
260
|
+
self.orig = np.clip(np.asarray(doc.image, dtype=np.float32), 0.0, 1.0)
|
|
261
|
+
|
|
262
|
+
# Display base is RGB
|
|
263
|
+
disp = _as_rgb(self.orig)
|
|
264
|
+
self._disp_base = disp
|
|
265
|
+
|
|
266
|
+
# ---- UI ----
|
|
267
|
+
v = QVBoxLayout(self)
|
|
268
|
+
|
|
269
|
+
# Controls
|
|
270
|
+
grp = QGroupBox("Halo-B-Gon Parameters")
|
|
271
|
+
grid = QGridLayout(grp)
|
|
272
|
+
|
|
273
|
+
grid.addWidget(QLabel("Reduction:"), 0, 0)
|
|
274
|
+
self.sl = QSlider(Qt.Orientation.Horizontal); self.sl.setRange(0, 3); self.sl.setValue(0)
|
|
275
|
+
self.lbl = QLabel("Extra Low")
|
|
276
|
+
def _lab(v):
|
|
277
|
+
self.lbl.setText(["Extra Low","Low","Medium","High"][int(v)])
|
|
278
|
+
self.sl.valueChanged.connect(_lab); _lab(0)
|
|
279
|
+
self.sl.valueChanged.connect(self._debounce)
|
|
280
|
+
grid.addWidget(self.sl, 0, 1); grid.addWidget(self.lbl, 0, 2)
|
|
281
|
+
|
|
282
|
+
self.cb_linear = QCheckBox("Linear data"); self.cb_linear.setChecked(False)
|
|
283
|
+
self.cb_linear.toggled.connect(self._debounce)
|
|
284
|
+
grid.addWidget(self.cb_linear, 1, 1)
|
|
285
|
+
|
|
286
|
+
# Apply target
|
|
287
|
+
grid.addWidget(QLabel("Apply to:"), 2, 0)
|
|
288
|
+
self.cmb_target = QComboBox(); self.cmb_target.addItems(["Overwrite active view", "Create new view"])
|
|
289
|
+
grid.addWidget(self.cmb_target, 2, 1, 1, 2)
|
|
290
|
+
|
|
291
|
+
v.addWidget(grp)
|
|
292
|
+
|
|
293
|
+
# Preview
|
|
294
|
+
self.scene = QGraphicsScene(self)
|
|
295
|
+
self.view = _PreviewView(self.scene)
|
|
296
|
+
self.pix = QGraphicsPixmapItem()
|
|
297
|
+
self.scene.addItem(self.pix)
|
|
298
|
+
v.addWidget(self.view, 1)
|
|
299
|
+
|
|
300
|
+
# Buttons (themed)
|
|
301
|
+
row = QHBoxLayout()
|
|
302
|
+
row.addWidget(QLabel("Zoom: Ctrl+Wheel"))
|
|
303
|
+
|
|
304
|
+
b_minus = themed_toolbtn("zoom-out", "Zoom Out")
|
|
305
|
+
b_plus = themed_toolbtn("zoom-in", "Zoom In")
|
|
306
|
+
b_fit = themed_toolbtn("zoom-fit-best", "Fit to View")
|
|
307
|
+
|
|
308
|
+
b_minus.clicked.connect(self.view.zoom_out)
|
|
309
|
+
b_plus .clicked.connect(self.view.zoom_in)
|
|
310
|
+
b_fit .clicked.connect(lambda: self.view.fit_to(self.scene.itemsBoundingRect()))
|
|
311
|
+
|
|
312
|
+
row.addWidget(b_minus)
|
|
313
|
+
row.addWidget(b_plus)
|
|
314
|
+
row.addWidget(b_fit)
|
|
315
|
+
row.addStretch(1)
|
|
316
|
+
v.addLayout(row)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
row2 = QHBoxLayout()
|
|
320
|
+
b_apply = QPushButton("Apply"); b_apply.clicked.connect(self._apply)
|
|
321
|
+
b_reset = QPushButton("Reset"); b_reset.clicked.connect(self._reset)
|
|
322
|
+
b_cancel= QPushButton("Cancel"); b_cancel.clicked.connect(self.reject)
|
|
323
|
+
row2.addStretch(1); row2.addWidget(b_apply); row2.addWidget(b_reset); row2.addWidget(b_cancel)
|
|
324
|
+
v.addLayout(row2)
|
|
325
|
+
|
|
326
|
+
self._timer = QTimer(self); self._timer.setSingleShot(True); self._timer.timeout.connect(self._update_preview)
|
|
327
|
+
|
|
328
|
+
self._set_pix(self._disp_base)
|
|
329
|
+
self._update_preview()
|
|
330
|
+
self.resize(900, 620)
|
|
331
|
+
|
|
332
|
+
def _debounce(self): self._timer.start(180)
|
|
333
|
+
|
|
334
|
+
def _set_pix(self, rgb):
|
|
335
|
+
q = _qimage_from_rgb01(rgb)
|
|
336
|
+
self.pix.setPixmap(QPixmap.fromImage(q))
|
|
337
|
+
self.view.setSceneRect(self.pix.boundingRect())
|
|
338
|
+
|
|
339
|
+
def _params(self):
|
|
340
|
+
return int(self.sl.value()), bool(self.cb_linear.isChecked())
|
|
341
|
+
|
|
342
|
+
def _update_preview(self):
|
|
343
|
+
lvl, lin = self._params()
|
|
344
|
+
try:
|
|
345
|
+
out = compute_halo_b_gon(self.orig, reduction_level=lvl, is_linear=lin)
|
|
346
|
+
# Display only: convert mono → RGB for the pixmap
|
|
347
|
+
self._set_pix(_as_rgb(out))
|
|
348
|
+
except Exception as e:
|
|
349
|
+
QMessageBox.warning(self, "Halo-B-Gon", f"Preview failed:\n{e}")
|
|
350
|
+
|
|
351
|
+
def _apply_overwrite(self, out: np.ndarray):
|
|
352
|
+
if hasattr(self.doc, "set_image"):
|
|
353
|
+
self.doc.set_image(out, step_name="Halo-B-Gon")
|
|
354
|
+
elif hasattr(self.doc, "apply_numpy"):
|
|
355
|
+
self.doc.apply_numpy(out, step_name="Halo-B-Gon")
|
|
356
|
+
else:
|
|
357
|
+
self.doc.image = out
|
|
358
|
+
|
|
359
|
+
def _apply(self):
|
|
360
|
+
lvl, lin = self._params()
|
|
361
|
+
try:
|
|
362
|
+
out = compute_halo_b_gon(self.orig, reduction_level=lvl, is_linear=lin)
|
|
363
|
+
|
|
364
|
+
# Mask blend (same as your preview path)
|
|
365
|
+
m = _maybe_get_mask(self.parent_ref, np.asarray(self.orig, dtype=np.float32))
|
|
366
|
+
if m is not None:
|
|
367
|
+
if self.orig.ndim == 2 and m.ndim == 3:
|
|
368
|
+
m = m[..., 0]
|
|
369
|
+
if self.orig.ndim == 3 and self.orig.shape[2] == 1 and m.ndim == 2:
|
|
370
|
+
m = m[:, :, None]
|
|
371
|
+
out = np.clip(out * m + self.orig * (1.0 - m), 0.0, 1.0)
|
|
372
|
+
|
|
373
|
+
out = np.clip(out, 0.0, 1.0).astype(np.float32, copy=False)
|
|
374
|
+
|
|
375
|
+
# ── Register as last_headless_command for replay ──────────
|
|
376
|
+
try:
|
|
377
|
+
main = self.parent()
|
|
378
|
+
if main is not None:
|
|
379
|
+
preset = {
|
|
380
|
+
"reduction": int(lvl),
|
|
381
|
+
"linear": bool(lin),
|
|
382
|
+
}
|
|
383
|
+
payload = {
|
|
384
|
+
"command_id": "halo_b_gon",
|
|
385
|
+
"preset": dict(preset),
|
|
386
|
+
}
|
|
387
|
+
setattr(main, "_last_headless_command", payload)
|
|
388
|
+
|
|
389
|
+
# optional log
|
|
390
|
+
try:
|
|
391
|
+
if hasattr(main, "_log"):
|
|
392
|
+
main._log(
|
|
393
|
+
f"[Replay] Registered Halo-B-Gon as last action "
|
|
394
|
+
f"(level={int(lvl)}, linear={bool(lin)})"
|
|
395
|
+
)
|
|
396
|
+
except Exception:
|
|
397
|
+
pass
|
|
398
|
+
except Exception:
|
|
399
|
+
# don't break apply if replay wiring fails
|
|
400
|
+
pass
|
|
401
|
+
# ───────────────────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
# If user chose "Create new view", go through DocManager so the UI spawns the window.
|
|
404
|
+
create_new = (self.cmb_target.currentIndex() == 1)
|
|
405
|
+
|
|
406
|
+
if create_new:
|
|
407
|
+
# Try to find a DocManager on the dialog's parent (preferred) or parent_ref.
|
|
408
|
+
dm = getattr(self.parent(), "doc_manager", None)
|
|
409
|
+
if dm is None:
|
|
410
|
+
dm = getattr(self.parent_ref, "doc_manager", None)
|
|
411
|
+
|
|
412
|
+
if dm is not None:
|
|
413
|
+
# Carry forward useful metadata; let DocManager’s signal create the view.
|
|
414
|
+
title = self.doc.display_name() if hasattr(self.doc, "display_name") else "Image"
|
|
415
|
+
meta = dict(getattr(self.doc, "metadata", {}) or {})
|
|
416
|
+
# Ensure expected fields exist
|
|
417
|
+
try:
|
|
418
|
+
meta.setdefault("bit_depth", "32-bit floating point")
|
|
419
|
+
if "is_mono" not in meta:
|
|
420
|
+
meta["is_mono"] = (out.ndim == 2 or (out.ndim == 3 and out.shape[2] == 1))
|
|
421
|
+
except Exception:
|
|
422
|
+
pass
|
|
423
|
+
|
|
424
|
+
new_doc = dm.create_document(out.copy(), metadata=meta, name=f"{title} [Halo-B-Gon]")
|
|
425
|
+
try:
|
|
426
|
+
dm.set_active_document(new_doc)
|
|
427
|
+
except Exception:
|
|
428
|
+
pass
|
|
429
|
+
|
|
430
|
+
self.accept()
|
|
431
|
+
return
|
|
432
|
+
else:
|
|
433
|
+
# Fallback: try legacy spawner if present; else warn and overwrite.
|
|
434
|
+
spawner = getattr(self.parent(), "_spawn_new_view_from_numpy", None)
|
|
435
|
+
if spawner is None:
|
|
436
|
+
spawner = getattr(self.parent_ref, "_spawn_new_view_from_numpy", None)
|
|
437
|
+
if callable(spawner):
|
|
438
|
+
title = self.doc.display_name() if hasattr(self.doc, "display_name") else "Image"
|
|
439
|
+
spawner(out, f"{title} [Halo-B-Gon]")
|
|
440
|
+
self.accept()
|
|
441
|
+
return
|
|
442
|
+
else:
|
|
443
|
+
QMessageBox.warning(
|
|
444
|
+
self, "Halo-B-Gon",
|
|
445
|
+
"Could not find DocManager or window spawner; applying to the active view instead."
|
|
446
|
+
)
|
|
447
|
+
# fall through to overwrite
|
|
448
|
+
|
|
449
|
+
# Overwrite current (original behavior)
|
|
450
|
+
self._apply_overwrite(out)
|
|
451
|
+
self.accept()
|
|
452
|
+
|
|
453
|
+
except Exception as e:
|
|
454
|
+
QMessageBox.critical(self, "Halo-B-Gon", f"Failed to apply:\n{e}")
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _reset(self):
|
|
459
|
+
self.sl.setValue(0)
|
|
460
|
+
self.cb_linear.setChecked(False)
|
|
461
|
+
self._set_pix(self._disp_base)
|
|
462
|
+
self._update_preview()
|