setiastrosuitepro 1.6.5.post3__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.
- setiastro/__init__.py +2 -0
- setiastro/data/SASP_data.fits +0 -0
- setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
- setiastro/data/catalogs/astrobin_filters.csv +890 -0
- setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
- setiastro/data/catalogs/cali2.csv +63 -0
- setiastro/data/catalogs/cali2color.csv +65 -0
- setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
- setiastro/data/catalogs/celestial_catalog.csv +24031 -0
- setiastro/data/catalogs/detected_stars.csv +24784 -0
- setiastro/data/catalogs/fits_header_data.csv +46 -0
- setiastro/data/catalogs/test.csv +8 -0
- setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
- setiastro/images/Astro_Spikes.png +0 -0
- setiastro/images/Background_startup.jpg +0 -0
- setiastro/images/HRDiagram.png +0 -0
- setiastro/images/LExtract.png +0 -0
- setiastro/images/LInsert.png +0 -0
- setiastro/images/Oxygenation-atm-2.svg.png +0 -0
- setiastro/images/RGB080604.png +0 -0
- setiastro/images/abeicon.png +0 -0
- setiastro/images/aberration.png +0 -0
- setiastro/images/andromedatry.png +0 -0
- setiastro/images/andromedatry_satellited.png +0 -0
- setiastro/images/annotated.png +0 -0
- setiastro/images/aperture.png +0 -0
- setiastro/images/astrosuite.ico +0 -0
- setiastro/images/astrosuite.png +0 -0
- setiastro/images/astrosuitepro.icns +0 -0
- setiastro/images/astrosuitepro.ico +0 -0
- setiastro/images/astrosuitepro.png +0 -0
- setiastro/images/background.png +0 -0
- setiastro/images/background2.png +0 -0
- setiastro/images/benchmark.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
- setiastro/images/blaster.png +0 -0
- setiastro/images/blink.png +0 -0
- setiastro/images/clahe.png +0 -0
- setiastro/images/collage.png +0 -0
- setiastro/images/colorwheel.png +0 -0
- setiastro/images/contsub.png +0 -0
- setiastro/images/convo.png +0 -0
- setiastro/images/copyslot.png +0 -0
- setiastro/images/cosmic.png +0 -0
- setiastro/images/cosmicsat.png +0 -0
- setiastro/images/crop1.png +0 -0
- setiastro/images/cropicon.png +0 -0
- setiastro/images/curves.png +0 -0
- setiastro/images/cvs.png +0 -0
- setiastro/images/debayer.png +0 -0
- setiastro/images/denoise_cnn_custom.png +0 -0
- setiastro/images/denoise_cnn_graph.png +0 -0
- setiastro/images/disk.png +0 -0
- setiastro/images/dse.png +0 -0
- setiastro/images/exoicon.png +0 -0
- setiastro/images/eye.png +0 -0
- setiastro/images/fliphorizontal.png +0 -0
- setiastro/images/flipvertical.png +0 -0
- setiastro/images/font.png +0 -0
- setiastro/images/freqsep.png +0 -0
- setiastro/images/functionbundle.png +0 -0
- setiastro/images/graxpert.png +0 -0
- setiastro/images/green.png +0 -0
- setiastro/images/gridicon.png +0 -0
- setiastro/images/halo.png +0 -0
- setiastro/images/hdr.png +0 -0
- setiastro/images/histogram.png +0 -0
- setiastro/images/hubble.png +0 -0
- setiastro/images/imagecombine.png +0 -0
- setiastro/images/invert.png +0 -0
- setiastro/images/isophote.png +0 -0
- setiastro/images/isophote_demo_figure.png +0 -0
- setiastro/images/isophote_demo_image.png +0 -0
- setiastro/images/isophote_demo_model.png +0 -0
- setiastro/images/isophote_demo_residual.png +0 -0
- setiastro/images/jwstpupil.png +0 -0
- setiastro/images/linearfit.png +0 -0
- setiastro/images/livestacking.png +0 -0
- setiastro/images/mask.png +0 -0
- setiastro/images/maskapply.png +0 -0
- setiastro/images/maskcreate.png +0 -0
- setiastro/images/maskremove.png +0 -0
- setiastro/images/morpho.png +0 -0
- setiastro/images/mosaic.png +0 -0
- setiastro/images/multiscale_decomp.png +0 -0
- setiastro/images/nbtorgb.png +0 -0
- setiastro/images/neutral.png +0 -0
- setiastro/images/nuke.png +0 -0
- setiastro/images/openfile.png +0 -0
- setiastro/images/pedestal.png +0 -0
- setiastro/images/pen.png +0 -0
- setiastro/images/pixelmath.png +0 -0
- setiastro/images/platesolve.png +0 -0
- setiastro/images/ppp.png +0 -0
- setiastro/images/pro.png +0 -0
- setiastro/images/project.png +0 -0
- setiastro/images/psf.png +0 -0
- setiastro/images/redo.png +0 -0
- setiastro/images/redoicon.png +0 -0
- setiastro/images/rescale.png +0 -0
- setiastro/images/rgbalign.png +0 -0
- setiastro/images/rgbcombo.png +0 -0
- setiastro/images/rgbextract.png +0 -0
- setiastro/images/rotate180.png +0 -0
- setiastro/images/rotatearbitrary.png +0 -0
- setiastro/images/rotateclockwise.png +0 -0
- setiastro/images/rotatecounterclockwise.png +0 -0
- setiastro/images/satellite.png +0 -0
- setiastro/images/script.png +0 -0
- setiastro/images/selectivecolor.png +0 -0
- setiastro/images/simbad.png +0 -0
- setiastro/images/slot0.png +0 -0
- setiastro/images/slot1.png +0 -0
- setiastro/images/slot2.png +0 -0
- setiastro/images/slot3.png +0 -0
- setiastro/images/slot4.png +0 -0
- setiastro/images/slot5.png +0 -0
- setiastro/images/slot6.png +0 -0
- setiastro/images/slot7.png +0 -0
- setiastro/images/slot8.png +0 -0
- setiastro/images/slot9.png +0 -0
- setiastro/images/spcc.png +0 -0
- setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
- setiastro/images/spinner.gif +0 -0
- setiastro/images/stacking.png +0 -0
- setiastro/images/staradd.png +0 -0
- setiastro/images/staralign.png +0 -0
- setiastro/images/starnet.png +0 -0
- setiastro/images/starregistration.png +0 -0
- setiastro/images/starspike.png +0 -0
- setiastro/images/starstretch.png +0 -0
- setiastro/images/statstretch.png +0 -0
- setiastro/images/supernova.png +0 -0
- setiastro/images/uhs.png +0 -0
- setiastro/images/undoicon.png +0 -0
- setiastro/images/upscale.png +0 -0
- setiastro/images/viewbundle.png +0 -0
- setiastro/images/whitebalance.png +0 -0
- setiastro/images/wimi_icon_256x256.png +0 -0
- setiastro/images/wimilogo.png +0 -0
- setiastro/images/wims.png +0 -0
- setiastro/images/wrench_icon.png +0 -0
- setiastro/images/xisfliberator.png +0 -0
- setiastro/qml/ResourceMonitor.qml +126 -0
- setiastro/saspro/__init__.py +20 -0
- setiastro/saspro/__main__.py +958 -0
- setiastro/saspro/_generated/__init__.py +7 -0
- setiastro/saspro/_generated/build_info.py +3 -0
- setiastro/saspro/abe.py +1346 -0
- setiastro/saspro/abe_preset.py +196 -0
- setiastro/saspro/aberration_ai.py +698 -0
- setiastro/saspro/aberration_ai_preset.py +224 -0
- setiastro/saspro/accel_installer.py +218 -0
- setiastro/saspro/accel_workers.py +30 -0
- setiastro/saspro/add_stars.py +624 -0
- setiastro/saspro/astrobin_exporter.py +1010 -0
- setiastro/saspro/astrospike.py +153 -0
- setiastro/saspro/astrospike_python.py +1841 -0
- setiastro/saspro/autostretch.py +198 -0
- setiastro/saspro/backgroundneutral.py +611 -0
- setiastro/saspro/batch_convert.py +328 -0
- setiastro/saspro/batch_renamer.py +522 -0
- setiastro/saspro/blemish_blaster.py +491 -0
- setiastro/saspro/blink_comparator_pro.py +3149 -0
- setiastro/saspro/bundles.py +61 -0
- setiastro/saspro/bundles_dock.py +114 -0
- setiastro/saspro/cheat_sheet.py +213 -0
- setiastro/saspro/clahe.py +368 -0
- setiastro/saspro/comet_stacking.py +1442 -0
- setiastro/saspro/common_tr.py +107 -0
- setiastro/saspro/config.py +38 -0
- setiastro/saspro/config_bootstrap.py +40 -0
- setiastro/saspro/config_manager.py +316 -0
- setiastro/saspro/continuum_subtract.py +1617 -0
- setiastro/saspro/convo.py +1400 -0
- setiastro/saspro/convo_preset.py +414 -0
- setiastro/saspro/copyastro.py +190 -0
- setiastro/saspro/cosmicclarity.py +1589 -0
- setiastro/saspro/cosmicclarity_preset.py +407 -0
- setiastro/saspro/crop_dialog_pro.py +983 -0
- setiastro/saspro/crop_preset.py +189 -0
- setiastro/saspro/curve_editor_pro.py +2562 -0
- setiastro/saspro/curves_preset.py +375 -0
- setiastro/saspro/debayer.py +673 -0
- setiastro/saspro/debug_utils.py +29 -0
- setiastro/saspro/dnd_mime.py +35 -0
- setiastro/saspro/doc_manager.py +2664 -0
- setiastro/saspro/exoplanet_detector.py +2166 -0
- setiastro/saspro/file_utils.py +284 -0
- setiastro/saspro/fitsmodifier.py +748 -0
- setiastro/saspro/fix_bom.py +32 -0
- setiastro/saspro/free_torch_memory.py +48 -0
- setiastro/saspro/frequency_separation.py +1349 -0
- setiastro/saspro/function_bundle.py +1596 -0
- setiastro/saspro/generate_translations.py +3092 -0
- setiastro/saspro/ghs_dialog_pro.py +663 -0
- setiastro/saspro/ghs_preset.py +284 -0
- setiastro/saspro/graxpert.py +637 -0
- setiastro/saspro/graxpert_preset.py +287 -0
- setiastro/saspro/gui/__init__.py +0 -0
- setiastro/saspro/gui/main_window.py +8792 -0
- setiastro/saspro/gui/mixins/__init__.py +33 -0
- setiastro/saspro/gui/mixins/dock_mixin.py +375 -0
- setiastro/saspro/gui/mixins/file_mixin.py +450 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +503 -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 +390 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +1619 -0
- setiastro/saspro/gui/mixins/update_mixin.py +323 -0
- setiastro/saspro/gui/mixins/view_mixin.py +435 -0
- setiastro/saspro/gui/statistics_dialog.py +47 -0
- setiastro/saspro/halobgon.py +488 -0
- setiastro/saspro/header_viewer.py +448 -0
- setiastro/saspro/headless_utils.py +88 -0
- setiastro/saspro/histogram.py +756 -0
- setiastro/saspro/history_explorer.py +941 -0
- setiastro/saspro/i18n.py +168 -0
- setiastro/saspro/image_combine.py +417 -0
- setiastro/saspro/image_peeker_pro.py +1604 -0
- setiastro/saspro/imageops/__init__.py +37 -0
- setiastro/saspro/imageops/mdi_snap.py +292 -0
- setiastro/saspro/imageops/scnr.py +36 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
- setiastro/saspro/imageops/stretch.py +236 -0
- setiastro/saspro/isophote.py +1182 -0
- setiastro/saspro/layers.py +208 -0
- setiastro/saspro/layers_dock.py +714 -0
- setiastro/saspro/lazy_imports.py +193 -0
- setiastro/saspro/legacy/__init__.py +2 -0
- setiastro/saspro/legacy/image_manager.py +2360 -0
- setiastro/saspro/legacy/numba_utils.py +3676 -0
- setiastro/saspro/legacy/xisf.py +1213 -0
- setiastro/saspro/linear_fit.py +537 -0
- setiastro/saspro/live_stacking.py +1854 -0
- setiastro/saspro/log_bus.py +5 -0
- setiastro/saspro/logging_config.py +460 -0
- setiastro/saspro/luminancerecombine.py +510 -0
- setiastro/saspro/main_helpers.py +201 -0
- setiastro/saspro/mask_creation.py +1086 -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 +3909 -0
- setiastro/saspro/mfdeconv_earlystop.py +71 -0
- setiastro/saspro/mfdeconvcudnn.py +3312 -0
- setiastro/saspro/mfdeconvsport.py +2459 -0
- setiastro/saspro/minorbodycatalog.py +567 -0
- setiastro/saspro/morphology.py +407 -0
- setiastro/saspro/multiscale_decomp.py +1747 -0
- setiastro/saspro/nbtorgb_stars.py +541 -0
- setiastro/saspro/numba_utils.py +3145 -0
- setiastro/saspro/numba_warmup.py +141 -0
- setiastro/saspro/ops/__init__.py +9 -0
- setiastro/saspro/ops/command_help_dialog.py +623 -0
- setiastro/saspro/ops/command_runner.py +217 -0
- setiastro/saspro/ops/commands.py +1594 -0
- setiastro/saspro/ops/script_editor.py +1105 -0
- setiastro/saspro/ops/scripts.py +1476 -0
- setiastro/saspro/ops/settings.py +637 -0
- setiastro/saspro/parallel_utils.py +554 -0
- setiastro/saspro/pedestal.py +121 -0
- setiastro/saspro/perfect_palette_picker.py +1105 -0
- setiastro/saspro/pipeline.py +110 -0
- setiastro/saspro/pixelmath.py +1604 -0
- setiastro/saspro/plate_solver.py +2445 -0
- setiastro/saspro/project_io.py +797 -0
- setiastro/saspro/psf_utils.py +136 -0
- setiastro/saspro/psf_viewer.py +549 -0
- setiastro/saspro/pyi_rthook_astroquery.py +95 -0
- setiastro/saspro/remove_green.py +331 -0
- setiastro/saspro/remove_stars.py +1599 -0
- setiastro/saspro/remove_stars_preset.py +446 -0
- setiastro/saspro/resources.py +503 -0
- setiastro/saspro/rgb_combination.py +208 -0
- setiastro/saspro/rgb_extract.py +19 -0
- setiastro/saspro/rgbalign.py +723 -0
- setiastro/saspro/runtime_imports.py +7 -0
- setiastro/saspro/runtime_torch.py +754 -0
- setiastro/saspro/save_options.py +73 -0
- setiastro/saspro/selective_color.py +1611 -0
- setiastro/saspro/sfcc.py +1472 -0
- setiastro/saspro/shortcuts.py +3116 -0
- setiastro/saspro/signature_insert.py +1102 -0
- setiastro/saspro/stacking_suite.py +19066 -0
- setiastro/saspro/star_alignment.py +7380 -0
- setiastro/saspro/star_alignment_preset.py +329 -0
- setiastro/saspro/star_metrics.py +49 -0
- setiastro/saspro/star_spikes.py +765 -0
- setiastro/saspro/star_stretch.py +507 -0
- setiastro/saspro/stat_stretch.py +538 -0
- setiastro/saspro/status_log_dock.py +78 -0
- setiastro/saspro/subwindow.py +3407 -0
- setiastro/saspro/supernovaasteroidhunter.py +1719 -0
- setiastro/saspro/swap_manager.py +134 -0
- setiastro/saspro/torch_backend.py +89 -0
- setiastro/saspro/torch_rejection.py +434 -0
- setiastro/saspro/translations/all_source_strings.json +4726 -0
- setiastro/saspro/translations/ar_translations.py +4096 -0
- setiastro/saspro/translations/de_translations.py +3728 -0
- setiastro/saspro/translations/es_translations.py +4169 -0
- setiastro/saspro/translations/fr_translations.py +4090 -0
- setiastro/saspro/translations/hi_translations.py +3803 -0
- setiastro/saspro/translations/integrate_translations.py +271 -0
- setiastro/saspro/translations/it_translations.py +4728 -0
- setiastro/saspro/translations/ja_translations.py +3834 -0
- setiastro/saspro/translations/pt_translations.py +3847 -0
- setiastro/saspro/translations/ru_translations.py +3082 -0
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +16019 -0
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +14548 -0
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +16202 -0
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +15870 -0
- setiastro/saspro/translations/saspro_hi.qm +0 -0
- setiastro/saspro/translations/saspro_hi.ts +14855 -0
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +19046 -0
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +14980 -0
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +15024 -0
- setiastro/saspro/translations/saspro_ru.qm +0 -0
- setiastro/saspro/translations/saspro_ru.ts +11835 -0
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +15237 -0
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +15248 -0
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +15289 -0
- setiastro/saspro/translations/sw_translations.py +3897 -0
- setiastro/saspro/translations/uk_translations.py +3929 -0
- setiastro/saspro/translations/zh_translations.py +3910 -0
- setiastro/saspro/versioning.py +77 -0
- setiastro/saspro/view_bundle.py +1558 -0
- setiastro/saspro/wavescale_hdr.py +645 -0
- setiastro/saspro/wavescale_hdr_preset.py +101 -0
- setiastro/saspro/wavescalede.py +680 -0
- setiastro/saspro/wavescalede_preset.py +230 -0
- setiastro/saspro/wcs_update.py +374 -0
- setiastro/saspro/whitebalance.py +513 -0
- setiastro/saspro/widgets/__init__.py +48 -0
- setiastro/saspro/widgets/common_utilities.py +306 -0
- setiastro/saspro/widgets/graphics_views.py +122 -0
- setiastro/saspro/widgets/image_utils.py +518 -0
- setiastro/saspro/widgets/minigame/game.js +991 -0
- setiastro/saspro/widgets/minigame/index.html +53 -0
- setiastro/saspro/widgets/minigame/style.css +241 -0
- setiastro/saspro/widgets/preview_dialogs.py +280 -0
- setiastro/saspro/widgets/resource_monitor.py +263 -0
- setiastro/saspro/widgets/spinboxes.py +290 -0
- setiastro/saspro/widgets/themed_buttons.py +13 -0
- setiastro/saspro/widgets/wavelet_utils.py +331 -0
- setiastro/saspro/wimi.py +7996 -0
- setiastro/saspro/wims.py +578 -0
- setiastro/saspro/window_shelf.py +185 -0
- setiastro/saspro/xisf.py +1213 -0
- setiastrosuitepro-1.6.5.post3.dist-info/METADATA +278 -0
- setiastrosuitepro-1.6.5.post3.dist-info/RECORD +368 -0
- setiastrosuitepro-1.6.5.post3.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.5.post3.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.5.post3.dist-info/licenses/LICENSE +674 -0
- setiastrosuitepro-1.6.5.post3.dist-info/licenses/license.txt +2580 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
# pro/linear_fit.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Optional, Tuple, List
|
|
7
|
+
|
|
8
|
+
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
|
9
|
+
from PyQt6.QtWidgets import (
|
|
10
|
+
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QDialogButtonBox,
|
|
11
|
+
QPushButton, QGroupBox, QMessageBox, QGridLayout, QWidget, QProgressBar
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# --------------------------------------------------------------------------------------
|
|
15
|
+
# Preset editor (used by Shortcuts “Edit Preset…”). Import into shortcuts.py like:
|
|
16
|
+
# from setiastro.saspro.linear_fit import _LinearFitPresetDialog
|
|
17
|
+
# and then store/load via your existing _load_preset/_save_preset helpers.
|
|
18
|
+
# --------------------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
class _LinearFitPresetDialog(QDialog):
|
|
21
|
+
"""
|
|
22
|
+
Stores defaults for Linear Fit when run via shortcuts/DnD.
|
|
23
|
+
For mono images the preset does not store a specific reference;
|
|
24
|
+
we will ask the user if needed.
|
|
25
|
+
"""
|
|
26
|
+
def __init__(self, parent=None, initial: dict | None = None):
|
|
27
|
+
super().__init__(parent)
|
|
28
|
+
self.setWindowTitle("Linear Fit — Preset")
|
|
29
|
+
init = dict(initial or {})
|
|
30
|
+
v = QVBoxLayout(self)
|
|
31
|
+
|
|
32
|
+
gb = QGroupBox("RGB strategy", self)
|
|
33
|
+
grid = QGridLayout(gb)
|
|
34
|
+
self.combo_rgb_mode = QComboBox(self)
|
|
35
|
+
self.combo_rgb_mode.addItems([
|
|
36
|
+
"Match to Highest Median",
|
|
37
|
+
"Match to Lowest Median",
|
|
38
|
+
"Match to Red",
|
|
39
|
+
"Match to Green",
|
|
40
|
+
"Match to Blue",
|
|
41
|
+
])
|
|
42
|
+
self.combo_rgb_mode.setCurrentIndex(int(init.get("rgb_mode_idx", 0)))
|
|
43
|
+
grid.addWidget(QLabel("Target channel:"), 0, 0)
|
|
44
|
+
grid.addWidget(self.combo_rgb_mode, 0, 1)
|
|
45
|
+
v.addWidget(gb)
|
|
46
|
+
|
|
47
|
+
gb2 = QGroupBox("Out-of-range handling", self)
|
|
48
|
+
h2 = QHBoxLayout(gb2)
|
|
49
|
+
self.combo_rescale = QComboBox(self)
|
|
50
|
+
self.combo_rescale.addItems([
|
|
51
|
+
"Clip to [0..1]",
|
|
52
|
+
"Normalize to [0..1] if needed",
|
|
53
|
+
"Leave values as-is",
|
|
54
|
+
])
|
|
55
|
+
self.combo_rescale.setCurrentIndex(int(init.get("rescale_mode_idx", 1)))
|
|
56
|
+
h2.addWidget(QLabel("Mode:"))
|
|
57
|
+
h2.addWidget(self.combo_rescale, 1)
|
|
58
|
+
v.addWidget(gb2)
|
|
59
|
+
|
|
60
|
+
info = QLabel("Mono images will be matched to a reference view's median.\n"
|
|
61
|
+
"If reference isn’t provided in the headless path, you'll be asked.")
|
|
62
|
+
info.setWordWrap(True)
|
|
63
|
+
v.addWidget(info)
|
|
64
|
+
|
|
65
|
+
btns = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, parent=self)
|
|
66
|
+
btns.accepted.connect(self.accept); btns.rejected.connect(self.reject)
|
|
67
|
+
v.addWidget(btns)
|
|
68
|
+
|
|
69
|
+
def result_dict(self) -> dict:
|
|
70
|
+
return {
|
|
71
|
+
"rgb_mode_idx": int(self.combo_rgb_mode.currentIndex()),
|
|
72
|
+
"rescale_mode_idx": int(self.combo_rescale.currentIndex()),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# --------------------------------------------------------------------------------------
|
|
76
|
+
# Engine: pure NumPy Linear Fit helpers
|
|
77
|
+
# --------------------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
def _nanmedian(x: np.ndarray) -> float:
|
|
80
|
+
try:
|
|
81
|
+
m = float(np.nanmedian(x))
|
|
82
|
+
if np.isfinite(m):
|
|
83
|
+
return m
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
return 0.0
|
|
87
|
+
|
|
88
|
+
def _postprocess(arr: np.ndarray, rescale_mode_idx: int) -> np.ndarray:
|
|
89
|
+
"""
|
|
90
|
+
rescale_mode_idx:
|
|
91
|
+
0 = clip to [0..1]
|
|
92
|
+
1 = normalize to [0..1] if min<0 or max>1
|
|
93
|
+
2 = leave values as-is
|
|
94
|
+
"""
|
|
95
|
+
if rescale_mode_idx == 2:
|
|
96
|
+
return arr
|
|
97
|
+
if rescale_mode_idx == 0:
|
|
98
|
+
return np.clip(arr, 0.0, 1.0)
|
|
99
|
+
# normalize if needed
|
|
100
|
+
a_min = float(np.nanmin(arr))
|
|
101
|
+
a_max = float(np.nanmax(arr))
|
|
102
|
+
if a_min >= 0.0 and a_max <= 1.0:
|
|
103
|
+
return arr
|
|
104
|
+
rng = max(a_max - a_min, 1e-12)
|
|
105
|
+
return (arr - a_min) / rng
|
|
106
|
+
|
|
107
|
+
def linear_fit_rgb(img: np.ndarray, rgb_mode_idx: int, rescale_mode_idx: int) -> Tuple[np.ndarray, int, List[float], List[float]]:
|
|
108
|
+
"""
|
|
109
|
+
Fit each channel to a reference channel by median.
|
|
110
|
+
Returns (out, ref_idx, medians_before, scales).
|
|
111
|
+
"""
|
|
112
|
+
assert img.ndim == 3 and img.shape[2] >= 3, "RGB image expected"
|
|
113
|
+
work = img.astype(np.float32, copy=False)
|
|
114
|
+
meds = [_nanmedian(work[..., c]) for c in range(3)]
|
|
115
|
+
eps = 1e-12
|
|
116
|
+
|
|
117
|
+
if rgb_mode_idx == 0: # Highest
|
|
118
|
+
ref_idx = int(np.argmax(meds))
|
|
119
|
+
elif rgb_mode_idx == 1: # Lowest
|
|
120
|
+
ref_idx = int(np.argmin(meds))
|
|
121
|
+
elif rgb_mode_idx == 2: # Red
|
|
122
|
+
ref_idx = 0
|
|
123
|
+
elif rgb_mode_idx == 3: # Green
|
|
124
|
+
ref_idx = 1
|
|
125
|
+
else: # Blue
|
|
126
|
+
ref_idx = 2
|
|
127
|
+
|
|
128
|
+
m_ref = max(meds[ref_idx], eps)
|
|
129
|
+
scales = []
|
|
130
|
+
out = work.copy()
|
|
131
|
+
for c in range(3):
|
|
132
|
+
m_c = max(meds[c], eps)
|
|
133
|
+
s = m_ref / m_c
|
|
134
|
+
scales.append(float(s))
|
|
135
|
+
out[..., c] *= float(s)
|
|
136
|
+
|
|
137
|
+
out = _postprocess(out, rescale_mode_idx)
|
|
138
|
+
return out, ref_idx, meds, scales
|
|
139
|
+
|
|
140
|
+
def linear_fit_mono_to_ref(mono: np.ndarray, ref: np.ndarray, rescale_mode_idx: int) -> Tuple[np.ndarray, float, float]:
|
|
141
|
+
"""
|
|
142
|
+
Scale mono image median to the reference image median (RGB ref uses luminance proxy).
|
|
143
|
+
Returns (out, m_src, m_ref).
|
|
144
|
+
"""
|
|
145
|
+
mono = mono.astype(np.float32, copy=False)
|
|
146
|
+
if ref.ndim == 3 and ref.shape[2] >= 3:
|
|
147
|
+
ref_lum = 0.2126*ref[...,0] + 0.7152*ref[...,1] + 0.0722*ref[...,2]
|
|
148
|
+
m_ref = _nanmedian(ref_lum)
|
|
149
|
+
else:
|
|
150
|
+
m_ref = _nanmedian(ref)
|
|
151
|
+
|
|
152
|
+
m_src = _nanmedian(mono)
|
|
153
|
+
eps = 1e-12
|
|
154
|
+
s = (m_ref) / max(m_src, eps)
|
|
155
|
+
out = mono * float(s)
|
|
156
|
+
out = _postprocess(out, rescale_mode_idx)
|
|
157
|
+
return out, m_src, m_ref
|
|
158
|
+
|
|
159
|
+
# --------------------------------------------------------------------------------------
|
|
160
|
+
# Worker
|
|
161
|
+
# --------------------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class _Job:
|
|
165
|
+
mode: str # "rgb" or "mono"
|
|
166
|
+
rgb_mode_idx: int = 0
|
|
167
|
+
rescale_mode_idx: int = 1
|
|
168
|
+
src: Optional[np.ndarray] = None
|
|
169
|
+
ref: Optional[np.ndarray] = None # only for mono mode
|
|
170
|
+
|
|
171
|
+
class _LinearFitWorker(QThread):
|
|
172
|
+
progress = pyqtSignal(int, str)
|
|
173
|
+
failed = pyqtSignal(str)
|
|
174
|
+
done = pyqtSignal(object, str) # (np.ndarray, step_name)
|
|
175
|
+
|
|
176
|
+
def __init__(self, job: _Job):
|
|
177
|
+
super().__init__()
|
|
178
|
+
self.job = job
|
|
179
|
+
|
|
180
|
+
def run(self):
|
|
181
|
+
try:
|
|
182
|
+
j = self.job
|
|
183
|
+
if j.src is None:
|
|
184
|
+
raise RuntimeError("No source image")
|
|
185
|
+
self.progress.emit(5, "Analyzing…")
|
|
186
|
+
|
|
187
|
+
if j.mode == "rgb":
|
|
188
|
+
out, ref_idx, meds, scales = linear_fit_rgb(j.src, j.rgb_mode_idx, j.rescale_mode_idx)
|
|
189
|
+
names = ["R", "G", "B"]
|
|
190
|
+
target = {
|
|
191
|
+
0: "highest median", 1: "lowest median",
|
|
192
|
+
2: "Red", 3: "Green", 4: "Blue"
|
|
193
|
+
}.get(j.rgb_mode_idx, "highest median")
|
|
194
|
+
step = f"Linear Fit (RGB → {names[ref_idx]} / {target})"
|
|
195
|
+
self.progress.emit(100, "Done")
|
|
196
|
+
self.done.emit(out, step)
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
if j.mode == "mono":
|
|
200
|
+
if j.ref is None:
|
|
201
|
+
raise RuntimeError("No reference image selected")
|
|
202
|
+
out, m_src, m_ref = linear_fit_mono_to_ref(j.src, j.ref, j.rescale_mode_idx)
|
|
203
|
+
step = "Linear Fit (mono → reference median)"
|
|
204
|
+
self.progress.emit(100, "Done")
|
|
205
|
+
self.done.emit(out, step)
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
raise RuntimeError("Unknown mode")
|
|
209
|
+
|
|
210
|
+
except Exception as e:
|
|
211
|
+
self.failed.emit(str(e))
|
|
212
|
+
|
|
213
|
+
# --------------------------------------------------------------------------------------
|
|
214
|
+
# Modal dialog to configure & run on the ACTIVE view
|
|
215
|
+
# --------------------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
class LinearFitDialog(QDialog):
|
|
218
|
+
"""
|
|
219
|
+
One-shot UI: works on the active doc image.
|
|
220
|
+
For RGB → choose target channel strategy.
|
|
221
|
+
For mono → pick a reference view from doc_manager.
|
|
222
|
+
Applies result back through doc_manager.apply_edit_to_active().
|
|
223
|
+
"""
|
|
224
|
+
def __init__(self, parent, doc_manager, active_doc):
|
|
225
|
+
super().__init__(parent)
|
|
226
|
+
self.setWindowTitle("Linear Fit")
|
|
227
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
228
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
229
|
+
self.setModal(False)
|
|
230
|
+
self.dm = doc_manager
|
|
231
|
+
self.doc = active_doc
|
|
232
|
+
self.worker: Optional[_LinearFitWorker] = None
|
|
233
|
+
|
|
234
|
+
if active_doc is None or getattr(active_doc, "image", None) is None:
|
|
235
|
+
raise RuntimeError("No active image/view")
|
|
236
|
+
|
|
237
|
+
img = np.asarray(active_doc.image)
|
|
238
|
+
self._src = img.astype(np.float32, copy=False)
|
|
239
|
+
|
|
240
|
+
v = QVBoxLayout(self)
|
|
241
|
+
|
|
242
|
+
# Determine mode
|
|
243
|
+
is_rgb = (self._src.ndim == 3 and self._src.shape[2] >= 3)
|
|
244
|
+
self.mode = "rgb" if is_rgb else "mono"
|
|
245
|
+
|
|
246
|
+
if self.mode == "rgb":
|
|
247
|
+
gb = QGroupBox("RGB options", self)
|
|
248
|
+
g = QGridLayout(gb)
|
|
249
|
+
self.combo_rgb = QComboBox(self)
|
|
250
|
+
self.combo_rgb.addItems([
|
|
251
|
+
"Match to Highest Median",
|
|
252
|
+
"Match to Lowest Median",
|
|
253
|
+
"Match to Red",
|
|
254
|
+
"Match to Green",
|
|
255
|
+
"Match to Blue",
|
|
256
|
+
])
|
|
257
|
+
self.combo_rgb.setCurrentIndex(0)
|
|
258
|
+
g.addWidget(QLabel("Target channel:"), 0, 0)
|
|
259
|
+
g.addWidget(self.combo_rgb, 0, 1)
|
|
260
|
+
v.addWidget(gb)
|
|
261
|
+
else:
|
|
262
|
+
gb = QGroupBox("Mono reference", self)
|
|
263
|
+
g = QGridLayout(gb)
|
|
264
|
+
self.combo_ref = QComboBox(self)
|
|
265
|
+
self._ref_docs: list = []
|
|
266
|
+
for d in self.dm.all_documents():
|
|
267
|
+
if d is active_doc:
|
|
268
|
+
continue
|
|
269
|
+
if getattr(d, "image", None) is None:
|
|
270
|
+
continue
|
|
271
|
+
self._ref_docs.append(d)
|
|
272
|
+
self.combo_ref.addItem(d.display_name())
|
|
273
|
+
if not self._ref_docs:
|
|
274
|
+
self.combo_ref.addItem("(no other views open)")
|
|
275
|
+
g.addWidget(QLabel("Reference view:"), 0, 0)
|
|
276
|
+
g.addWidget(self.combo_ref, 0, 1)
|
|
277
|
+
note = QLabel("If the reference is RGB, a luminance proxy is used to compute its median.")
|
|
278
|
+
note.setWordWrap(True)
|
|
279
|
+
g.addWidget(note, 1, 0, 1, 2)
|
|
280
|
+
v.addWidget(gb)
|
|
281
|
+
|
|
282
|
+
# Common: out-of-range handling
|
|
283
|
+
gb2 = QGroupBox("Out-of-range handling", self)
|
|
284
|
+
h2 = QHBoxLayout(gb2)
|
|
285
|
+
self.combo_rescale = QComboBox(self)
|
|
286
|
+
self.combo_rescale.addItems([
|
|
287
|
+
"Clip to [0..1]",
|
|
288
|
+
"Normalize to [0..1] if needed",
|
|
289
|
+
"Leave values as-is",
|
|
290
|
+
])
|
|
291
|
+
self.combo_rescale.setCurrentIndex(1)
|
|
292
|
+
h2.addWidget(QLabel("Mode:"))
|
|
293
|
+
h2.addWidget(self.combo_rescale, 1)
|
|
294
|
+
v.addWidget(gb2)
|
|
295
|
+
|
|
296
|
+
# Progress
|
|
297
|
+
self.status = QLabel("")
|
|
298
|
+
self.bar = QProgressBar(self); self.bar.setRange(0, 100)
|
|
299
|
+
v.addWidget(self.status)
|
|
300
|
+
v.addWidget(self.bar)
|
|
301
|
+
|
|
302
|
+
# Buttons
|
|
303
|
+
btns = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, parent=self)
|
|
304
|
+
btns.accepted.connect(self._go)
|
|
305
|
+
btns.rejected.connect(self.reject)
|
|
306
|
+
v.addWidget(btns)
|
|
307
|
+
|
|
308
|
+
# Small pre-read medians for info (non-blocking)
|
|
309
|
+
try:
|
|
310
|
+
if self.mode == "rgb":
|
|
311
|
+
meds = [_nanmedian(self._src[...,i]) for i in range(3)]
|
|
312
|
+
self.status.setText(f"Channel medians R/G/B: {meds[0]:.4g} / {meds[1]:.4g} / {meds[2]:.4g}")
|
|
313
|
+
else:
|
|
314
|
+
self.status.setText("Mono image selected. Choose a reference view.")
|
|
315
|
+
except Exception:
|
|
316
|
+
pass
|
|
317
|
+
|
|
318
|
+
def _go(self):
|
|
319
|
+
rescale_idx = int(self.combo_rescale.currentIndex())
|
|
320
|
+
job = _Job(mode=self.mode, rescale_mode_idx=rescale_idx, src=self._src)
|
|
321
|
+
|
|
322
|
+
if self.mode == "rgb":
|
|
323
|
+
job.rgb_mode_idx = int(self.combo_rgb.currentIndex())
|
|
324
|
+
else:
|
|
325
|
+
if not self._ref_docs:
|
|
326
|
+
QMessageBox.warning(self, "Linear Fit", "No reference view available.")
|
|
327
|
+
return
|
|
328
|
+
ref_doc = self._ref_docs[self.combo_ref.currentIndex()]
|
|
329
|
+
job.ref = np.asarray(ref_doc.image).astype(np.float32, copy=False)
|
|
330
|
+
|
|
331
|
+
self._run(job)
|
|
332
|
+
|
|
333
|
+
def _run(self, job: _Job):
|
|
334
|
+
self.bar.setValue(0)
|
|
335
|
+
self.status.setText("Working…")
|
|
336
|
+
self.setEnabled(False)
|
|
337
|
+
|
|
338
|
+
self.worker = _LinearFitWorker(job)
|
|
339
|
+
self.worker.progress.connect(self._on_prog)
|
|
340
|
+
self.worker.failed.connect(self._on_fail)
|
|
341
|
+
self.worker.done.connect(self._on_done)
|
|
342
|
+
self.worker.start()
|
|
343
|
+
|
|
344
|
+
def _on_prog(self, pct: int, msg: str):
|
|
345
|
+
self.bar.setValue(pct); self.status.setText(msg)
|
|
346
|
+
|
|
347
|
+
def _on_fail(self, err: str):
|
|
348
|
+
self.setEnabled(True)
|
|
349
|
+
self.status.setText("Failed.")
|
|
350
|
+
QMessageBox.critical(self, "Linear Fit", err)
|
|
351
|
+
|
|
352
|
+
def _on_done(self, out_img: np.ndarray, step_name: str):
|
|
353
|
+
self.setEnabled(True)
|
|
354
|
+
self.status.setText("Done.")
|
|
355
|
+
|
|
356
|
+
# 1) Apply result via DocManager (ROI/full handled there)
|
|
357
|
+
try:
|
|
358
|
+
self.dm.apply_edit_to_active(out_img, step_name=step_name)
|
|
359
|
+
except Exception as e:
|
|
360
|
+
QMessageBox.warning(self, "Linear Fit", f"Applied, but could not update document:\n{e}")
|
|
361
|
+
|
|
362
|
+
# 2) Remember this as the last headless-style command for Replay
|
|
363
|
+
try:
|
|
364
|
+
preset: dict = {
|
|
365
|
+
"rescale_mode_idx": int(self.combo_rescale.currentIndex()),
|
|
366
|
+
"mode": self.mode,
|
|
367
|
+
}
|
|
368
|
+
if self.mode == "rgb":
|
|
369
|
+
preset["rgb_mode_idx"] = int(self.combo_rgb.currentIndex())
|
|
370
|
+
else:
|
|
371
|
+
# Mono: stash reference info for future enhancements
|
|
372
|
+
if getattr(self, "_ref_docs", None):
|
|
373
|
+
idx = int(self.combo_ref.currentIndex())
|
|
374
|
+
if 0 <= idx < len(self._ref_docs):
|
|
375
|
+
ref_doc = self._ref_docs[idx]
|
|
376
|
+
ref_uid = getattr(ref_doc, "uid", None)
|
|
377
|
+
if ref_uid:
|
|
378
|
+
preset["ref_uid"] = ref_uid
|
|
379
|
+
preset["ref_name"] = ref_doc.display_name()
|
|
380
|
+
|
|
381
|
+
# Walk up to a parent that knows how to remember headless commands
|
|
382
|
+
mw = self.parent()
|
|
383
|
+
while mw is not None and not hasattr(mw, "_remember_last_headless_command"):
|
|
384
|
+
mw = mw.parent() if hasattr(mw, "parent") else None
|
|
385
|
+
|
|
386
|
+
if mw is not None and hasattr(mw, "_remember_last_headless_command"):
|
|
387
|
+
mw._remember_last_headless_command(
|
|
388
|
+
"linear_fit",
|
|
389
|
+
preset,
|
|
390
|
+
description=step_name or "Linear Fit",
|
|
391
|
+
)
|
|
392
|
+
except Exception:
|
|
393
|
+
# Replay tracking should never break the dialog
|
|
394
|
+
pass
|
|
395
|
+
|
|
396
|
+
self.accept()
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# --------------------------------------------------------------------------------------
|
|
400
|
+
# Public helpers for wiring into MainWindow
|
|
401
|
+
# --------------------------------------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
def open_linear_fit_dialog(parent, doc_manager) -> None:
|
|
404
|
+
"""
|
|
405
|
+
Bring up the Linear Fit dialog for the active view.
|
|
406
|
+
Applies to active view via doc_manager on success.
|
|
407
|
+
"""
|
|
408
|
+
doc = getattr(doc_manager, "get_active_document", lambda: None)()
|
|
409
|
+
if doc is None or getattr(doc, "image", None) is None:
|
|
410
|
+
QMessageBox.information(parent, "Linear Fit", "No active image.")
|
|
411
|
+
return
|
|
412
|
+
try:
|
|
413
|
+
dlg = LinearFitDialog(parent, doc_manager, doc)
|
|
414
|
+
dlg.exec()
|
|
415
|
+
except Exception as e:
|
|
416
|
+
QMessageBox.critical(parent, "Linear Fit", str(e))
|
|
417
|
+
|
|
418
|
+
def apply_linear_fit_via_preset(parent, doc_manager, active_doc, preset: dict | None) -> None:
|
|
419
|
+
"""
|
|
420
|
+
Headless/DnD path: apply using a preset dict (from Shortcuts).
|
|
421
|
+
If mono and no reference provided, asks the user to pick one.
|
|
422
|
+
Expected preset keys:
|
|
423
|
+
- rgb_mode_idx (int: 0..4)
|
|
424
|
+
- rescale_mode_idx (int: 0..2)
|
|
425
|
+
"""
|
|
426
|
+
preset = dict(preset or {})
|
|
427
|
+
rescale_idx = int(preset.get("rescale_mode_idx", 1))
|
|
428
|
+
|
|
429
|
+
img = np.asarray(active_doc.image)
|
|
430
|
+
if img.ndim == 3 and img.shape[2] >= 3:
|
|
431
|
+
rgb_idx = int(preset.get("rgb_mode_idx", 0))
|
|
432
|
+
out, ref_idx, _, _ = linear_fit_rgb(img, rgb_idx, rescale_idx)
|
|
433
|
+
names = ["R","G","B"]
|
|
434
|
+
target = {0:"highest median", 1:"lowest median", 2:"Red", 3:"Green", 4:"Blue"}.get(rgb_idx, "highest median")
|
|
435
|
+
step = f"Linear Fit (RGB → {names[ref_idx]} / {target})"
|
|
436
|
+
doc_manager.apply_edit_to_active(out, step_name=step)
|
|
437
|
+
return
|
|
438
|
+
|
|
439
|
+
# MONO → prompt for reference
|
|
440
|
+
# Enumerate other docs
|
|
441
|
+
others = []
|
|
442
|
+
for d in doc_manager.all_documents():
|
|
443
|
+
if d is active_doc:
|
|
444
|
+
continue
|
|
445
|
+
if getattr(d, "image", None) is None:
|
|
446
|
+
continue
|
|
447
|
+
others.append(d)
|
|
448
|
+
|
|
449
|
+
if not others:
|
|
450
|
+
QMessageBox.information(parent, "Linear Fit", "Mono image requires a reference view.\nOpen another image and try again.")
|
|
451
|
+
return
|
|
452
|
+
|
|
453
|
+
# small inline pick
|
|
454
|
+
pick = QDialog(parent)
|
|
455
|
+
pick.setWindowTitle("Choose Reference View")
|
|
456
|
+
vv = QVBoxLayout(pick)
|
|
457
|
+
cb = QComboBox(pick)
|
|
458
|
+
for d in others:
|
|
459
|
+
cb.addItem(d.display_name())
|
|
460
|
+
vv.addWidget(QLabel("Reference view (median target):"))
|
|
461
|
+
vv.addWidget(cb)
|
|
462
|
+
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, parent=pick)
|
|
463
|
+
bb.accepted.connect(pick.accept); bb.rejected.connect(pick.reject)
|
|
464
|
+
vv.addWidget(bb)
|
|
465
|
+
if pick.exec() != QDialog.DialogCode.Accepted:
|
|
466
|
+
return
|
|
467
|
+
ref = np.asarray(others[cb.currentIndex()].image)
|
|
468
|
+
|
|
469
|
+
out, _, _ = linear_fit_mono_to_ref(img, ref, rescale_idx)
|
|
470
|
+
step = f"Linear Fit (mono → {others[cb.currentIndex()].display_name()})"
|
|
471
|
+
doc_manager.apply_edit_to_active(out, step_name=step)
|
|
472
|
+
|
|
473
|
+
def apply_linear_fit_to_doc(parent, target_doc, preset: dict | None) -> None:
|
|
474
|
+
"""
|
|
475
|
+
Replay helper: apply Linear Fit to a specific ImageDocument
|
|
476
|
+
(usually the *base* doc when 'Replay Last on Base' is used).
|
|
477
|
+
|
|
478
|
+
Currently supports RGB images; mono replay-on-base will just
|
|
479
|
+
show a friendly message so you don't get a silent no-op.
|
|
480
|
+
"""
|
|
481
|
+
if target_doc is None or getattr(target_doc, "image", None) is None:
|
|
482
|
+
QMessageBox.information(parent, "Linear Fit", "No target image.")
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
preset = dict(preset or {})
|
|
486
|
+
rescale_idx = int(preset.get("rescale_mode_idx", 1))
|
|
487
|
+
|
|
488
|
+
img = np.asarray(target_doc.image)
|
|
489
|
+
if img.ndim == 3 and img.shape[2] >= 3:
|
|
490
|
+
rgb_idx = int(preset.get("rgb_mode_idx", 0))
|
|
491
|
+
out, ref_idx, _, _ = linear_fit_rgb(img, rgb_idx, rescale_idx)
|
|
492
|
+
|
|
493
|
+
names = ["R", "G", "B"]
|
|
494
|
+
target = {
|
|
495
|
+
0: "highest median",
|
|
496
|
+
1: "lowest median",
|
|
497
|
+
2: "Red",
|
|
498
|
+
3: "Green",
|
|
499
|
+
4: "Blue",
|
|
500
|
+
}.get(rgb_idx, "highest median")
|
|
501
|
+
|
|
502
|
+
step = f"Linear Fit (RGB → {names[ref_idx]} / {target})"
|
|
503
|
+
meta = {"step_name": step, "bit_depth": "32-bit floating point"}
|
|
504
|
+
try:
|
|
505
|
+
target_doc.apply_edit(out.astype(np.float32, copy=False),
|
|
506
|
+
metadata=meta,
|
|
507
|
+
step_name=step)
|
|
508
|
+
except Exception as e:
|
|
509
|
+
QMessageBox.warning(parent, "Linear Fit", f"Replay apply failed:\n{e}")
|
|
510
|
+
return
|
|
511
|
+
|
|
512
|
+
# Mono replay-on-base: we don't have the reference baked into the preset yet.
|
|
513
|
+
QMessageBox.information(
|
|
514
|
+
parent,
|
|
515
|
+
"Linear Fit",
|
|
516
|
+
"Replay-on-base for mono Linear Fit is not implemented yet.\n"
|
|
517
|
+
"Please re-run Linear Fit on this image via the dialog."
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
# -------- headless command runner (Scripts / Presets / Replay) ---------------
|
|
521
|
+
from setiastro.saspro.headless_utils import normalize_headless_main, unwrap_docproxy
|
|
522
|
+
|
|
523
|
+
def run_linear_fit_via_preset(main, preset=None, target_doc=None):
|
|
524
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
525
|
+
from setiastro.saspro.linear_fit import apply_linear_fit_via_preset
|
|
526
|
+
|
|
527
|
+
p = dict(preset or {})
|
|
528
|
+
main, doc, dm = normalize_headless_main(main, target_doc)
|
|
529
|
+
|
|
530
|
+
if dm is None:
|
|
531
|
+
QMessageBox.warning(main or None, "Linear Fit", "DocManager not available.")
|
|
532
|
+
return
|
|
533
|
+
if doc is None or getattr(doc, "image", None) is None:
|
|
534
|
+
QMessageBox.warning(main or None, "Linear Fit", "Load an image first.")
|
|
535
|
+
return
|
|
536
|
+
|
|
537
|
+
apply_linear_fit_via_preset(main, dm, doc, p)
|