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.

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