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,37 @@
|
|
|
1
|
+
# imageops/__init__.py
|
|
2
|
+
|
|
3
|
+
from .stretch import (
|
|
4
|
+
stretch_mono_image,
|
|
5
|
+
stretch_color_image,
|
|
6
|
+
apply_curves_adjustment,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
# --- Backward-compatible aliases (old SASv2-style names) ---
|
|
10
|
+
def stretch_color_image_linked(img, target_median, normalize=False,
|
|
11
|
+
apply_curves=False, curves_boost=0.0):
|
|
12
|
+
return stretch_color_image(
|
|
13
|
+
img, target_median,
|
|
14
|
+
linked=True,
|
|
15
|
+
normalize=normalize,
|
|
16
|
+
apply_curves=apply_curves,
|
|
17
|
+
curves_boost=curves_boost,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def stretch_color_image_unlinked(img, target_median, normalize=False,
|
|
21
|
+
apply_curves=False, curves_boost=0.0):
|
|
22
|
+
return stretch_color_image(
|
|
23
|
+
img, target_median,
|
|
24
|
+
linked=False,
|
|
25
|
+
normalize=normalize,
|
|
26
|
+
apply_curves=apply_curves,
|
|
27
|
+
curves_boost=curves_boost,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"stretch_mono_image",
|
|
32
|
+
"stretch_color_image",
|
|
33
|
+
"apply_curves_adjustment",
|
|
34
|
+
"stretch_color_image_linked",
|
|
35
|
+
"stretch_color_image_unlinked",
|
|
36
|
+
"apply_average_neutral_scnr",
|
|
37
|
+
]
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# pro/mdi_snap.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from PyQt6.QtCore import Qt, QRect, QPoint, QObject, QEvent, QSize
|
|
4
|
+
from PyQt6.QtGui import QPainter, QPen, QPalette
|
|
5
|
+
from PyQt6.QtWidgets import QWidget, QMdiArea, QMdiSubWindow
|
|
6
|
+
|
|
7
|
+
def _dpi_scaled(widget: QWidget, px: int) -> int:
|
|
8
|
+
try:
|
|
9
|
+
ratio = float(widget.devicePixelRatioF())
|
|
10
|
+
except Exception:
|
|
11
|
+
ratio = 1.0
|
|
12
|
+
return max(1, int(round(px * ratio)))
|
|
13
|
+
|
|
14
|
+
class _GuideOverlay(QWidget):
|
|
15
|
+
"""Thin, non-interactive overlay that draws snap guides on the MDI viewport."""
|
|
16
|
+
def __init__(self, parent: QWidget):
|
|
17
|
+
super().__init__(parent)
|
|
18
|
+
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
|
|
19
|
+
self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground, True)
|
|
20
|
+
self._rects: list[QRect] = []
|
|
21
|
+
self.hide()
|
|
22
|
+
|
|
23
|
+
def set_guides(self, rects: list[QRect]):
|
|
24
|
+
self._rects = rects or []
|
|
25
|
+
if self._rects:
|
|
26
|
+
if self.isHidden():
|
|
27
|
+
self.show()
|
|
28
|
+
else:
|
|
29
|
+
if self.isVisible():
|
|
30
|
+
self.hide()
|
|
31
|
+
self.update()
|
|
32
|
+
|
|
33
|
+
def paintEvent(self, _ev):
|
|
34
|
+
if not self._rects:
|
|
35
|
+
return
|
|
36
|
+
p = QPainter(self)
|
|
37
|
+
pen = QPen()
|
|
38
|
+
pen.setWidth(1)
|
|
39
|
+
pen.setColor(self.palette().color(QPalette.ColorRole.Highlight))
|
|
40
|
+
pen.setStyle(Qt.PenStyle.SolidLine)
|
|
41
|
+
p.setPen(pen)
|
|
42
|
+
for r in self._rects:
|
|
43
|
+
p.drawRect(r)
|
|
44
|
+
|
|
45
|
+
class MdiSnapController(QObject):
|
|
46
|
+
"""
|
|
47
|
+
Adds 'sticky' snapping for QMdiSubWindow edges inside a QMdiArea (Qt6-safe).
|
|
48
|
+
- Snaps to sibling subwindow edges and viewport edges.
|
|
49
|
+
- Shows faint guide lines while snapping.
|
|
50
|
+
- Hold Alt to temporarily disable snapping.
|
|
51
|
+
"""
|
|
52
|
+
def __init__(self, mdi: QMdiArea, threshold_px: int = 8, show_guides: bool = False):
|
|
53
|
+
super().__init__(mdi)
|
|
54
|
+
self.mdi = mdi
|
|
55
|
+
self.view = mdi.viewport() # geometry space of subwindows
|
|
56
|
+
self.overlay = _GuideOverlay(self.view)
|
|
57
|
+
self.threshold = max(1, int(threshold_px))
|
|
58
|
+
self._active: QMdiSubWindow | None = None
|
|
59
|
+
self._snap_enabled = True
|
|
60
|
+
self._show_guides = bool(show_guides)
|
|
61
|
+
self._install()
|
|
62
|
+
|
|
63
|
+
def set_show_guides(self, enabled: bool):
|
|
64
|
+
self._show_guides = bool(enabled)
|
|
65
|
+
if not self._show_guides:
|
|
66
|
+
self.overlay.set_guides([])
|
|
67
|
+
|
|
68
|
+
# --- public knobs ---
|
|
69
|
+
def set_threshold(self, px: int):
|
|
70
|
+
self.threshold = max(1, int(px))
|
|
71
|
+
|
|
72
|
+
def install_on(self, sub: QMdiSubWindow):
|
|
73
|
+
# Avoid double-install
|
|
74
|
+
sub.removeEventFilter(self)
|
|
75
|
+
sub.installEventFilter(self)
|
|
76
|
+
|
|
77
|
+
# --- internals ---
|
|
78
|
+
def _install(self):
|
|
79
|
+
# Track active subwindow
|
|
80
|
+
self.mdi.subWindowActivated.connect(self._on_activated)
|
|
81
|
+
|
|
82
|
+
# Watch existing subs
|
|
83
|
+
for sw in self.mdi.subWindowList():
|
|
84
|
+
self.install_on(sw)
|
|
85
|
+
|
|
86
|
+
# Keep overlay matched to viewport size/visibility
|
|
87
|
+
self.view.installEventFilter(self)
|
|
88
|
+
self.overlay.setGeometry(self.view.rect())
|
|
89
|
+
|
|
90
|
+
# Periodically re-check list on activation (new windows)
|
|
91
|
+
self.mdi.subWindowActivated.connect(lambda _sw: self._refresh_watch_list())
|
|
92
|
+
|
|
93
|
+
def _refresh_watch_list(self):
|
|
94
|
+
for sw in self.mdi.subWindowList():
|
|
95
|
+
self.install_on(sw)
|
|
96
|
+
|
|
97
|
+
def _on_activated(self, sw: QMdiSubWindow | None):
|
|
98
|
+
self._active = sw
|
|
99
|
+
|
|
100
|
+
# Gather candidate snap edges (x/y positions) from siblings & viewport
|
|
101
|
+
def _collect_edges(self, ignore: QMdiSubWindow | None):
|
|
102
|
+
siblings = [s for s in self.mdi.subWindowList() if s is not ignore]
|
|
103
|
+
vp = self.view.rect()
|
|
104
|
+
|
|
105
|
+
xs, ys = set(), set()
|
|
106
|
+
rects = []
|
|
107
|
+
for s in siblings:
|
|
108
|
+
r = s.geometry() # already in viewport coords
|
|
109
|
+
rects.append(r)
|
|
110
|
+
xs.update([r.left(), r.right()])
|
|
111
|
+
ys.update([r.top(), r.bottom()])
|
|
112
|
+
|
|
113
|
+
# viewport edges (no center)
|
|
114
|
+
xs.update([vp.left(), vp.right()])
|
|
115
|
+
ys.update([vp.top(), vp.bottom()])
|
|
116
|
+
return sorted(xs), sorted(ys), rects, vp
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def _nearest(value: int, candidates: list[int], tol: int) -> tuple[bool, int]:
|
|
120
|
+
"""
|
|
121
|
+
Return (True, snapped_value) if any candidate is within tol of value,
|
|
122
|
+
otherwise (False, value).
|
|
123
|
+
"""
|
|
124
|
+
best_val = value
|
|
125
|
+
best_d = tol + 1
|
|
126
|
+
for c in candidates:
|
|
127
|
+
d = abs(c - value)
|
|
128
|
+
if d < best_d:
|
|
129
|
+
best_d = d
|
|
130
|
+
best_val = c
|
|
131
|
+
if best_d <= tol:
|
|
132
|
+
return True, best_val
|
|
133
|
+
return False, value
|
|
134
|
+
|
|
135
|
+
def _build_guides(self, snap_rect: QRect, vp: QRect) -> list[QRect]:
|
|
136
|
+
"""Horizontal and vertical guides along the snapped rect edges."""
|
|
137
|
+
lines: list[QRect] = []
|
|
138
|
+
w = _dpi_scaled(self.view, 2)
|
|
139
|
+
# horizontal
|
|
140
|
+
lines.append(QRect(vp.left(), snap_rect.top(), vp.width(), w))
|
|
141
|
+
lines.append(QRect(vp.left(), snap_rect.bottom(), vp.width(), w))
|
|
142
|
+
# vertical
|
|
143
|
+
lines.append(QRect(snap_rect.left(), vp.top(), w, vp.height()))
|
|
144
|
+
lines.append(QRect(snap_rect.right(), vp.top(), w, vp.height()))
|
|
145
|
+
return lines
|
|
146
|
+
|
|
147
|
+
def _snap_geometry(
|
|
148
|
+
self,
|
|
149
|
+
g: QRect,
|
|
150
|
+
xs: list[int],
|
|
151
|
+
ys: list[int],
|
|
152
|
+
tol: int,
|
|
153
|
+
size_snap: bool
|
|
154
|
+
) -> tuple[QRect, list[QRect]]:
|
|
155
|
+
"""
|
|
156
|
+
- If size_snap is False (Move): keep W/H fixed and move the rect so the
|
|
157
|
+
nearest edges line up with candidates.
|
|
158
|
+
- If size_snap is True (Resize): keep top/left fixed and adjust W/H so
|
|
159
|
+
right/bottom edges snap to nearby candidates.
|
|
160
|
+
"""
|
|
161
|
+
L, T = g.left(), g.top()
|
|
162
|
+
W, H = g.width(), g.height()
|
|
163
|
+
R, B = L + W - 1, T + H - 1
|
|
164
|
+
|
|
165
|
+
vp = self.view.rect()
|
|
166
|
+
snapped = False
|
|
167
|
+
|
|
168
|
+
if not size_snap:
|
|
169
|
+
# --- MOVE MODE: translate the rect, no size change ---
|
|
170
|
+
okL, snapL = self._nearest(L, xs, tol)
|
|
171
|
+
okR, snapR = self._nearest(R, xs, tol)
|
|
172
|
+
|
|
173
|
+
dx = 0
|
|
174
|
+
if okL and okR:
|
|
175
|
+
dL = snapL - L
|
|
176
|
+
dR = snapR - R
|
|
177
|
+
dx = dL if abs(dL) <= abs(dR) else dR
|
|
178
|
+
snapped = True
|
|
179
|
+
elif okL:
|
|
180
|
+
dx = snapL - L
|
|
181
|
+
snapped = True
|
|
182
|
+
elif okR:
|
|
183
|
+
dx = snapR - R
|
|
184
|
+
snapped = True
|
|
185
|
+
|
|
186
|
+
okT, snapT = self._nearest(T, ys, tol)
|
|
187
|
+
okB, snapB = self._nearest(B, ys, tol)
|
|
188
|
+
|
|
189
|
+
dy = 0
|
|
190
|
+
if okT and okB:
|
|
191
|
+
dT = snapT - T
|
|
192
|
+
dB = snapB - B
|
|
193
|
+
dy = dT if abs(dT) <= abs(dB) else dB
|
|
194
|
+
snapped = True
|
|
195
|
+
elif okT:
|
|
196
|
+
dy = snapT - T
|
|
197
|
+
snapped = True
|
|
198
|
+
elif okB:
|
|
199
|
+
dy = snapB - B
|
|
200
|
+
snapped = True
|
|
201
|
+
|
|
202
|
+
new_L = L + dx
|
|
203
|
+
new_T = T + dy
|
|
204
|
+
g2 = QRect(QPoint(new_L, new_T), QSize(W, H))
|
|
205
|
+
|
|
206
|
+
else:
|
|
207
|
+
# --- RESIZE MODE: keep L/T fixed, snap R/B by changing W/H ---
|
|
208
|
+
okR, snapR = self._nearest(R, xs, tol)
|
|
209
|
+
okB, snapB = self._nearest(B, ys, tol)
|
|
210
|
+
|
|
211
|
+
new_W = W
|
|
212
|
+
new_H = H
|
|
213
|
+
|
|
214
|
+
if okR:
|
|
215
|
+
new_W = max(1, (snapR - L + 1))
|
|
216
|
+
snapped = True
|
|
217
|
+
if okB:
|
|
218
|
+
new_H = max(1, (snapB - T + 1))
|
|
219
|
+
snapped = True
|
|
220
|
+
|
|
221
|
+
g2 = QRect(QPoint(L, T), QSize(new_W, new_H))
|
|
222
|
+
|
|
223
|
+
guides = (
|
|
224
|
+
self._build_guides(g2, vp)
|
|
225
|
+
if (snapped and self._show_guides)
|
|
226
|
+
else []
|
|
227
|
+
)
|
|
228
|
+
return g2, guides
|
|
229
|
+
|
|
230
|
+
# --- Event filter on each subwindow + viewport ---
|
|
231
|
+
def eventFilter(self, obj: QObject, ev: QEvent) -> bool:
|
|
232
|
+
t = ev.type()
|
|
233
|
+
|
|
234
|
+
# Keep overlay sized to the viewport
|
|
235
|
+
if obj is self.view:
|
|
236
|
+
if t == QEvent.Type.Resize:
|
|
237
|
+
self.overlay.setGeometry(self.view.rect())
|
|
238
|
+
self.overlay.update()
|
|
239
|
+
elif t in _CLEAR_EVENTS:
|
|
240
|
+
self.overlay.set_guides([])
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
if not isinstance(obj, QMdiSubWindow):
|
|
244
|
+
return super().eventFilter(obj, ev)
|
|
245
|
+
|
|
246
|
+
# Alt disables snapping while held
|
|
247
|
+
try:
|
|
248
|
+
mods = obj.window().keyboardModifiers()
|
|
249
|
+
self._snap_enabled = not bool(mods & Qt.KeyboardModifier.AltModifier)
|
|
250
|
+
except Exception:
|
|
251
|
+
self._snap_enabled = True
|
|
252
|
+
|
|
253
|
+
if t in (QEvent.Type.Move, QEvent.Type.Resize):
|
|
254
|
+
if not self._snap_enabled:
|
|
255
|
+
self.overlay.set_guides([])
|
|
256
|
+
return super().eventFilter(obj, ev)
|
|
257
|
+
|
|
258
|
+
xs, ys, _rects, _vp = self._collect_edges(ignore=obj)
|
|
259
|
+
tol = _dpi_scaled(self.view, self.threshold)
|
|
260
|
+
cur = obj.geometry()
|
|
261
|
+
|
|
262
|
+
# <<< key change: size_snap only during Resize >>>
|
|
263
|
+
size_snap = (t == QEvent.Type.Resize)
|
|
264
|
+
snapped_rect, guides = self._snap_geometry(
|
|
265
|
+
cur, xs, ys, tol, size_snap=size_snap
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if snapped_rect != cur:
|
|
269
|
+
# Minimize feedback loops
|
|
270
|
+
obj.blockSignals(True)
|
|
271
|
+
try:
|
|
272
|
+
obj.setGeometry(snapped_rect)
|
|
273
|
+
finally:
|
|
274
|
+
obj.blockSignals(False)
|
|
275
|
+
|
|
276
|
+
self.overlay.set_guides(guides if (self._show_guides and guides) else [])
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
# Qt6-safe "clear overlay" conditions
|
|
280
|
+
if t in (QEvent.Type.Hide, QEvent.Type.Leave):
|
|
281
|
+
if self._show_guides:
|
|
282
|
+
self.overlay.set_guides([])
|
|
283
|
+
|
|
284
|
+
return super().eventFilter(obj, ev)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# ---- Qt6-safe clear events set (no MoveAboutToBeAnimated) -------------------
|
|
288
|
+
_CLEAR_EVENTS = set()
|
|
289
|
+
for _name in ("Hide", "Leave", "MouseButtonRelease", "WindowDeactivate", "FocusOut"):
|
|
290
|
+
_val = getattr(QEvent.Type, _name, None)
|
|
291
|
+
if _val is not None:
|
|
292
|
+
_CLEAR_EVENTS.add(_val)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# pro/imageops/scnr.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
def apply_average_neutral_scnr(image: np.ndarray, amount: float = 1.0) -> np.ndarray:
|
|
6
|
+
"""
|
|
7
|
+
Average Neutral SCNR (green removal).
|
|
8
|
+
Expects an RGB image normalized to [0, 1]. Returns float32 in [0, 1].
|
|
9
|
+
|
|
10
|
+
amount: 0.0 → no effect, 1.0 → full SCNR
|
|
11
|
+
"""
|
|
12
|
+
if not isinstance(image, np.ndarray):
|
|
13
|
+
raise TypeError("Input image must be a NumPy array.")
|
|
14
|
+
if image.ndim != 3 or image.shape[2] != 3:
|
|
15
|
+
raise ValueError("Input image must have three channels (RGB).")
|
|
16
|
+
if not (0.0 <= amount <= 1.0):
|
|
17
|
+
raise ValueError("Amount parameter must be between 0.0 and 1.0.")
|
|
18
|
+
|
|
19
|
+
img = image.astype(np.float32, copy=False)
|
|
20
|
+
|
|
21
|
+
R = img[..., 0]
|
|
22
|
+
G = img[..., 1]
|
|
23
|
+
B = img[..., 2]
|
|
24
|
+
|
|
25
|
+
# G' = min(G, 0.5*(R + B)) - optimized: compute blended G directly without full array copy
|
|
26
|
+
G_scnr = np.minimum(G, 0.5 * (R + B))
|
|
27
|
+
|
|
28
|
+
# Blend original G and SCNR G directly: avoids allocating a full copy of the image
|
|
29
|
+
G_blended = G + amount * (G_scnr - G) # Equivalent to (1-amount)*G + amount*G_scnr
|
|
30
|
+
|
|
31
|
+
# Build output array only once
|
|
32
|
+
out = np.empty_like(img, dtype=np.float32)
|
|
33
|
+
out[..., 0] = R
|
|
34
|
+
out[..., 1] = np.clip(G_blended, 0.0, 1.0)
|
|
35
|
+
out[..., 2] = B
|
|
36
|
+
return out
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# imageops/starbasedwhitebalance.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
# Optional deps
|
|
7
|
+
try:
|
|
8
|
+
import cv2 # for ellipse overlay
|
|
9
|
+
except Exception: # pragma: no cover
|
|
10
|
+
cv2 = None
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import sep # Source Extractor
|
|
14
|
+
except Exception as e: # pragma: no cover
|
|
15
|
+
sep = None
|
|
16
|
+
_sep_import_error = e
|
|
17
|
+
else:
|
|
18
|
+
_sep_import_error = None
|
|
19
|
+
|
|
20
|
+
from typing import Tuple, Optional
|
|
21
|
+
from .stretch import stretch_color_image
|
|
22
|
+
|
|
23
|
+
# Shared utilities
|
|
24
|
+
from setiastro.saspro.widgets.image_utils import to_float01 as _to_float01
|
|
25
|
+
|
|
26
|
+
__all__ = ["apply_star_based_white_balance"]
|
|
27
|
+
|
|
28
|
+
# simple cache (reused when reuse_cached_sources=True)
|
|
29
|
+
cached_star_sources: Optional[np.ndarray] = None
|
|
30
|
+
cached_flux_radii: Optional[np.ndarray] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _tone_preserve_bg_neutralize(rgb: np.ndarray) -> np.ndarray:
|
|
34
|
+
"""
|
|
35
|
+
Neutralize background using the darkest grid patch in a tone-preserving way.
|
|
36
|
+
Operates in-place on a copy; returns the neutralized image (float32 [0,1]).
|
|
37
|
+
"""
|
|
38
|
+
h, w = rgb.shape[:2]
|
|
39
|
+
patch_size = 10
|
|
40
|
+
ph = max(1, h // patch_size)
|
|
41
|
+
pw = max(1, w // patch_size)
|
|
42
|
+
|
|
43
|
+
best = None
|
|
44
|
+
best_sum = float("inf")
|
|
45
|
+
for i in range(patch_size):
|
|
46
|
+
for j in range(patch_size):
|
|
47
|
+
y0, x0 = i * ph, j * pw
|
|
48
|
+
y1, x1 = min(y0 + ph, h), min(x0 + pw, w)
|
|
49
|
+
patch = rgb[y0:y1, x0:x1, :]
|
|
50
|
+
med = np.median(patch, axis=(0, 1))
|
|
51
|
+
s = float(np.sum(med))
|
|
52
|
+
if s < best_sum:
|
|
53
|
+
best_sum = s
|
|
54
|
+
best = med
|
|
55
|
+
|
|
56
|
+
out = rgb.copy()
|
|
57
|
+
if best is not None:
|
|
58
|
+
avg = float(np.mean(best))
|
|
59
|
+
# “tone-preserving” shift+scale channel-wise toward avg
|
|
60
|
+
for c in range(3):
|
|
61
|
+
diff = float(best[c] - avg)
|
|
62
|
+
denom = (1.0 - diff) if abs(1.0 - diff) > 1e-8 else 1e-8
|
|
63
|
+
out[:, :, c] = np.clip((out[:, :, c] - diff) / denom, 0.0, 1.0)
|
|
64
|
+
return out
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def apply_star_based_white_balance(
|
|
68
|
+
image: np.ndarray,
|
|
69
|
+
threshold: float = 1.5,
|
|
70
|
+
autostretch: bool = True,
|
|
71
|
+
reuse_cached_sources: bool = False,
|
|
72
|
+
return_star_colors: bool = False
|
|
73
|
+
) -> Tuple[np.ndarray, int, np.ndarray, np.ndarray, np.ndarray] | Tuple[np.ndarray, int, np.ndarray]:
|
|
74
|
+
"""
|
|
75
|
+
Star-based white balance with background neutralization and an RGB overlay of detected stars.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
image : np.ndarray
|
|
80
|
+
RGB image (any dtype). Assumed RGB ordering.
|
|
81
|
+
threshold : float
|
|
82
|
+
SExtractor detection threshold (in background sigma).
|
|
83
|
+
autostretch : bool
|
|
84
|
+
If True, overlay is built from an autostretched view for visibility.
|
|
85
|
+
reuse_cached_sources : bool
|
|
86
|
+
If True, reuses star positions measured on a previous call (same scene).
|
|
87
|
+
return_star_colors : bool
|
|
88
|
+
If True, also returns (raw_star_pixels, after_star_pixels).
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
balanced_rgb : float32 RGB in [0,1]
|
|
93
|
+
star_count : int
|
|
94
|
+
overlay_rgb : float32 RGB in [0,1] with star ellipses drawn
|
|
95
|
+
(optional) raw_star_pixels : (N,3) float array, colors sampled from ORIGINAL image
|
|
96
|
+
(optional) after_star_pixels : (N,3) float array, colors sampled after WB
|
|
97
|
+
"""
|
|
98
|
+
if image.ndim != 3 or image.shape[2] != 3:
|
|
99
|
+
raise ValueError("apply_star_based_white_balance: input must be an RGB image (H,W,3).")
|
|
100
|
+
|
|
101
|
+
# 0) normalize
|
|
102
|
+
img_rgb = _to_float01(image)
|
|
103
|
+
|
|
104
|
+
# 1) first background neutralization (tone-preserving)
|
|
105
|
+
bg_neutral = _tone_preserve_bg_neutralize(img_rgb)
|
|
106
|
+
|
|
107
|
+
# 2) detect / reuse star positions
|
|
108
|
+
if sep is None:
|
|
109
|
+
raise ImportError(
|
|
110
|
+
"apply_star_based_white_balance requires the 'sep' package. "
|
|
111
|
+
f"Import error was: {_sep_import_error!r}"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
gray = np.mean(bg_neutral, axis=2).astype(np.float32, copy=False)
|
|
115
|
+
bkg = sep.Background(gray)
|
|
116
|
+
data_sub = gray - bkg.back()
|
|
117
|
+
err_val = float(bkg.globalrms)
|
|
118
|
+
|
|
119
|
+
global cached_star_sources, cached_flux_radii
|
|
120
|
+
|
|
121
|
+
if reuse_cached_sources and cached_star_sources is not None:
|
|
122
|
+
sources = cached_star_sources
|
|
123
|
+
r = cached_flux_radii
|
|
124
|
+
else:
|
|
125
|
+
sources = sep.extract(data_sub, threshold, err=err_val)
|
|
126
|
+
if sources is None or len(sources) == 0:
|
|
127
|
+
raise ValueError("No sources detected for Star-Based White Balance.")
|
|
128
|
+
r, _ = sep.flux_radius(
|
|
129
|
+
gray,
|
|
130
|
+
sources["x"], sources["y"],
|
|
131
|
+
2.0 * sources["a"], 0.2,
|
|
132
|
+
normflux=sources["flux"],
|
|
133
|
+
subpix=5
|
|
134
|
+
)
|
|
135
|
+
cached_star_sources = sources
|
|
136
|
+
cached_flux_radii = r
|
|
137
|
+
|
|
138
|
+
# filter: small-ish, star-like
|
|
139
|
+
mask = (r > 0) & (r <= 10)
|
|
140
|
+
sources = sources[mask]
|
|
141
|
+
r = r[mask]
|
|
142
|
+
if len(sources) == 0:
|
|
143
|
+
raise ValueError("All detected sources were rejected as non-stellar (too large).")
|
|
144
|
+
|
|
145
|
+
h, w = gray.shape
|
|
146
|
+
# raw colors from ORIGINAL image - optimized vectorized extraction
|
|
147
|
+
xs = sources["x"].astype(np.int32)
|
|
148
|
+
ys = sources["y"].astype(np.int32)
|
|
149
|
+
valid = (xs >= 0) & (xs < w) & (ys >= 0) & (ys < h)
|
|
150
|
+
raw_star_pixels = img_rgb[ys[valid], xs[valid], :]
|
|
151
|
+
|
|
152
|
+
# 3) build overlay (autostretched if requested) and draw ellipses
|
|
153
|
+
disp = stretch_color_image(bg_neutral.copy(), 0.25) if autostretch else bg_neutral.copy()
|
|
154
|
+
|
|
155
|
+
if cv2 is not None:
|
|
156
|
+
overlay_bgr = cv2.cvtColor((disp * 255).astype(np.uint8), cv2.COLOR_RGB2BGR)
|
|
157
|
+
for i in range(len(sources)):
|
|
158
|
+
cx = float(sources["x"][i]); cy = float(sources["y"][i])
|
|
159
|
+
a = float(sources["a"][i]); b = float(sources["b"][i])
|
|
160
|
+
theta_deg = float(sources["theta"][i] * 180.0 / np.pi)
|
|
161
|
+
center = (int(round(cx)), int(round(cy)))
|
|
162
|
+
axes = (max(1, int(round(3 * a))), max(1, int(round(3 * b))))
|
|
163
|
+
# red ellipse in BGR
|
|
164
|
+
cv2.ellipse(overlay_bgr, center, axes, angle=theta_deg, startAngle=0, endAngle=360,
|
|
165
|
+
color=(0, 0, 255), thickness=1)
|
|
166
|
+
overlay_rgb = cv2.cvtColor(overlay_bgr, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
|
|
167
|
+
else:
|
|
168
|
+
# fallback: no ellipses, just the display image
|
|
169
|
+
overlay_rgb = disp.astype(np.float32, copy=False)
|
|
170
|
+
|
|
171
|
+
# 4) compute WB scale using star colors sampled on bg_neutral image
|
|
172
|
+
# Optimized: vectorized extraction instead of Python loop (10-50x faster)
|
|
173
|
+
xs = sources["x"].astype(np.int32)
|
|
174
|
+
ys = sources["y"].astype(np.int32)
|
|
175
|
+
valid_mask = (xs >= 0) & (xs < w) & (ys >= 0) & (ys < h)
|
|
176
|
+
|
|
177
|
+
if not np.any(valid_mask):
|
|
178
|
+
raise ValueError("No stellar samples available for white balance.")
|
|
179
|
+
|
|
180
|
+
star_pixels = bg_neutral[ys[valid_mask], xs[valid_mask], :].astype(np.float32)
|
|
181
|
+
avg_color = np.mean(star_pixels, axis=0)
|
|
182
|
+
max_val = float(np.max(avg_color))
|
|
183
|
+
# protect against divide-by-zero
|
|
184
|
+
avg_color = np.where(avg_color <= 1e-8, 1e-8, avg_color)
|
|
185
|
+
scaling = max_val / avg_color
|
|
186
|
+
|
|
187
|
+
balanced = (bg_neutral * scaling.reshape((1, 1, 3))).clip(0.0, 1.0)
|
|
188
|
+
|
|
189
|
+
# 5) second background neutralization pass on balanced image
|
|
190
|
+
balanced = _tone_preserve_bg_neutralize(balanced)
|
|
191
|
+
|
|
192
|
+
# 6) collect after-WB star samples - optimized vectorized extraction
|
|
193
|
+
after_star_pixels = balanced[ys[valid_mask], xs[valid_mask], :]
|
|
194
|
+
|
|
195
|
+
if return_star_colors:
|
|
196
|
+
return (
|
|
197
|
+
balanced.astype(np.float32, copy=False),
|
|
198
|
+
int(len(star_pixels)),
|
|
199
|
+
overlay_rgb.astype(np.float32, copy=False),
|
|
200
|
+
np.asarray(raw_star_pixels, dtype=np.float32),
|
|
201
|
+
np.asarray(after_star_pixels, dtype=np.float32),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
balanced.astype(np.float32, copy=False),
|
|
206
|
+
int(len(star_pixels)),
|
|
207
|
+
overlay_rgb.astype(np.float32, copy=False),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|