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.
Files changed (368) 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/Background_startup.jpg +0 -0
  16. setiastro/images/HRDiagram.png +0 -0
  17. setiastro/images/LExtract.png +0 -0
  18. setiastro/images/LInsert.png +0 -0
  19. setiastro/images/Oxygenation-atm-2.svg.png +0 -0
  20. setiastro/images/RGB080604.png +0 -0
  21. setiastro/images/abeicon.png +0 -0
  22. setiastro/images/aberration.png +0 -0
  23. setiastro/images/andromedatry.png +0 -0
  24. setiastro/images/andromedatry_satellited.png +0 -0
  25. setiastro/images/annotated.png +0 -0
  26. setiastro/images/aperture.png +0 -0
  27. setiastro/images/astrosuite.ico +0 -0
  28. setiastro/images/astrosuite.png +0 -0
  29. setiastro/images/astrosuitepro.icns +0 -0
  30. setiastro/images/astrosuitepro.ico +0 -0
  31. setiastro/images/astrosuitepro.png +0 -0
  32. setiastro/images/background.png +0 -0
  33. setiastro/images/background2.png +0 -0
  34. setiastro/images/benchmark.png +0 -0
  35. setiastro/images/big_moon_stabilizer_timeline.png +0 -0
  36. setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
  37. setiastro/images/blaster.png +0 -0
  38. setiastro/images/blink.png +0 -0
  39. setiastro/images/clahe.png +0 -0
  40. setiastro/images/collage.png +0 -0
  41. setiastro/images/colorwheel.png +0 -0
  42. setiastro/images/contsub.png +0 -0
  43. setiastro/images/convo.png +0 -0
  44. setiastro/images/copyslot.png +0 -0
  45. setiastro/images/cosmic.png +0 -0
  46. setiastro/images/cosmicsat.png +0 -0
  47. setiastro/images/crop1.png +0 -0
  48. setiastro/images/cropicon.png +0 -0
  49. setiastro/images/curves.png +0 -0
  50. setiastro/images/cvs.png +0 -0
  51. setiastro/images/debayer.png +0 -0
  52. setiastro/images/denoise_cnn_custom.png +0 -0
  53. setiastro/images/denoise_cnn_graph.png +0 -0
  54. setiastro/images/disk.png +0 -0
  55. setiastro/images/dse.png +0 -0
  56. setiastro/images/exoicon.png +0 -0
  57. setiastro/images/eye.png +0 -0
  58. setiastro/images/fliphorizontal.png +0 -0
  59. setiastro/images/flipvertical.png +0 -0
  60. setiastro/images/font.png +0 -0
  61. setiastro/images/freqsep.png +0 -0
  62. setiastro/images/functionbundle.png +0 -0
  63. setiastro/images/graxpert.png +0 -0
  64. setiastro/images/green.png +0 -0
  65. setiastro/images/gridicon.png +0 -0
  66. setiastro/images/halo.png +0 -0
  67. setiastro/images/hdr.png +0 -0
  68. setiastro/images/histogram.png +0 -0
  69. setiastro/images/hubble.png +0 -0
  70. setiastro/images/imagecombine.png +0 -0
  71. setiastro/images/invert.png +0 -0
  72. setiastro/images/isophote.png +0 -0
  73. setiastro/images/isophote_demo_figure.png +0 -0
  74. setiastro/images/isophote_demo_image.png +0 -0
  75. setiastro/images/isophote_demo_model.png +0 -0
  76. setiastro/images/isophote_demo_residual.png +0 -0
  77. setiastro/images/jwstpupil.png +0 -0
  78. setiastro/images/linearfit.png +0 -0
  79. setiastro/images/livestacking.png +0 -0
  80. setiastro/images/mask.png +0 -0
  81. setiastro/images/maskapply.png +0 -0
  82. setiastro/images/maskcreate.png +0 -0
  83. setiastro/images/maskremove.png +0 -0
  84. setiastro/images/morpho.png +0 -0
  85. setiastro/images/mosaic.png +0 -0
  86. setiastro/images/multiscale_decomp.png +0 -0
  87. setiastro/images/nbtorgb.png +0 -0
  88. setiastro/images/neutral.png +0 -0
  89. setiastro/images/nuke.png +0 -0
  90. setiastro/images/openfile.png +0 -0
  91. setiastro/images/pedestal.png +0 -0
  92. setiastro/images/pen.png +0 -0
  93. setiastro/images/pixelmath.png +0 -0
  94. setiastro/images/platesolve.png +0 -0
  95. setiastro/images/ppp.png +0 -0
  96. setiastro/images/pro.png +0 -0
  97. setiastro/images/project.png +0 -0
  98. setiastro/images/psf.png +0 -0
  99. setiastro/images/redo.png +0 -0
  100. setiastro/images/redoicon.png +0 -0
  101. setiastro/images/rescale.png +0 -0
  102. setiastro/images/rgbalign.png +0 -0
  103. setiastro/images/rgbcombo.png +0 -0
  104. setiastro/images/rgbextract.png +0 -0
  105. setiastro/images/rotate180.png +0 -0
  106. setiastro/images/rotatearbitrary.png +0 -0
  107. setiastro/images/rotateclockwise.png +0 -0
  108. setiastro/images/rotatecounterclockwise.png +0 -0
  109. setiastro/images/satellite.png +0 -0
  110. setiastro/images/script.png +0 -0
  111. setiastro/images/selectivecolor.png +0 -0
  112. setiastro/images/simbad.png +0 -0
  113. setiastro/images/slot0.png +0 -0
  114. setiastro/images/slot1.png +0 -0
  115. setiastro/images/slot2.png +0 -0
  116. setiastro/images/slot3.png +0 -0
  117. setiastro/images/slot4.png +0 -0
  118. setiastro/images/slot5.png +0 -0
  119. setiastro/images/slot6.png +0 -0
  120. setiastro/images/slot7.png +0 -0
  121. setiastro/images/slot8.png +0 -0
  122. setiastro/images/slot9.png +0 -0
  123. setiastro/images/spcc.png +0 -0
  124. setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
  125. setiastro/images/spinner.gif +0 -0
  126. setiastro/images/stacking.png +0 -0
  127. setiastro/images/staradd.png +0 -0
  128. setiastro/images/staralign.png +0 -0
  129. setiastro/images/starnet.png +0 -0
  130. setiastro/images/starregistration.png +0 -0
  131. setiastro/images/starspike.png +0 -0
  132. setiastro/images/starstretch.png +0 -0
  133. setiastro/images/statstretch.png +0 -0
  134. setiastro/images/supernova.png +0 -0
  135. setiastro/images/uhs.png +0 -0
  136. setiastro/images/undoicon.png +0 -0
  137. setiastro/images/upscale.png +0 -0
  138. setiastro/images/viewbundle.png +0 -0
  139. setiastro/images/whitebalance.png +0 -0
  140. setiastro/images/wimi_icon_256x256.png +0 -0
  141. setiastro/images/wimilogo.png +0 -0
  142. setiastro/images/wims.png +0 -0
  143. setiastro/images/wrench_icon.png +0 -0
  144. setiastro/images/xisfliberator.png +0 -0
  145. setiastro/qml/ResourceMonitor.qml +126 -0
  146. setiastro/saspro/__init__.py +20 -0
  147. setiastro/saspro/__main__.py +958 -0
  148. setiastro/saspro/_generated/__init__.py +7 -0
  149. setiastro/saspro/_generated/build_info.py +3 -0
  150. setiastro/saspro/abe.py +1346 -0
  151. setiastro/saspro/abe_preset.py +196 -0
  152. setiastro/saspro/aberration_ai.py +698 -0
  153. setiastro/saspro/aberration_ai_preset.py +224 -0
  154. setiastro/saspro/accel_installer.py +218 -0
  155. setiastro/saspro/accel_workers.py +30 -0
  156. setiastro/saspro/add_stars.py +624 -0
  157. setiastro/saspro/astrobin_exporter.py +1010 -0
  158. setiastro/saspro/astrospike.py +153 -0
  159. setiastro/saspro/astrospike_python.py +1841 -0
  160. setiastro/saspro/autostretch.py +198 -0
  161. setiastro/saspro/backgroundneutral.py +611 -0
  162. setiastro/saspro/batch_convert.py +328 -0
  163. setiastro/saspro/batch_renamer.py +522 -0
  164. setiastro/saspro/blemish_blaster.py +491 -0
  165. setiastro/saspro/blink_comparator_pro.py +3149 -0
  166. setiastro/saspro/bundles.py +61 -0
  167. setiastro/saspro/bundles_dock.py +114 -0
  168. setiastro/saspro/cheat_sheet.py +213 -0
  169. setiastro/saspro/clahe.py +368 -0
  170. setiastro/saspro/comet_stacking.py +1442 -0
  171. setiastro/saspro/common_tr.py +107 -0
  172. setiastro/saspro/config.py +38 -0
  173. setiastro/saspro/config_bootstrap.py +40 -0
  174. setiastro/saspro/config_manager.py +316 -0
  175. setiastro/saspro/continuum_subtract.py +1617 -0
  176. setiastro/saspro/convo.py +1400 -0
  177. setiastro/saspro/convo_preset.py +414 -0
  178. setiastro/saspro/copyastro.py +190 -0
  179. setiastro/saspro/cosmicclarity.py +1589 -0
  180. setiastro/saspro/cosmicclarity_preset.py +407 -0
  181. setiastro/saspro/crop_dialog_pro.py +983 -0
  182. setiastro/saspro/crop_preset.py +189 -0
  183. setiastro/saspro/curve_editor_pro.py +2562 -0
  184. setiastro/saspro/curves_preset.py +375 -0
  185. setiastro/saspro/debayer.py +673 -0
  186. setiastro/saspro/debug_utils.py +29 -0
  187. setiastro/saspro/dnd_mime.py +35 -0
  188. setiastro/saspro/doc_manager.py +2664 -0
  189. setiastro/saspro/exoplanet_detector.py +2166 -0
  190. setiastro/saspro/file_utils.py +284 -0
  191. setiastro/saspro/fitsmodifier.py +748 -0
  192. setiastro/saspro/fix_bom.py +32 -0
  193. setiastro/saspro/free_torch_memory.py +48 -0
  194. setiastro/saspro/frequency_separation.py +1349 -0
  195. setiastro/saspro/function_bundle.py +1596 -0
  196. setiastro/saspro/generate_translations.py +3092 -0
  197. setiastro/saspro/ghs_dialog_pro.py +663 -0
  198. setiastro/saspro/ghs_preset.py +284 -0
  199. setiastro/saspro/graxpert.py +637 -0
  200. setiastro/saspro/graxpert_preset.py +287 -0
  201. setiastro/saspro/gui/__init__.py +0 -0
  202. setiastro/saspro/gui/main_window.py +8792 -0
  203. setiastro/saspro/gui/mixins/__init__.py +33 -0
  204. setiastro/saspro/gui/mixins/dock_mixin.py +375 -0
  205. setiastro/saspro/gui/mixins/file_mixin.py +450 -0
  206. setiastro/saspro/gui/mixins/geometry_mixin.py +503 -0
  207. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  208. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  209. setiastro/saspro/gui/mixins/menu_mixin.py +390 -0
  210. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  211. setiastro/saspro/gui/mixins/toolbar_mixin.py +1619 -0
  212. setiastro/saspro/gui/mixins/update_mixin.py +323 -0
  213. setiastro/saspro/gui/mixins/view_mixin.py +435 -0
  214. setiastro/saspro/gui/statistics_dialog.py +47 -0
  215. setiastro/saspro/halobgon.py +488 -0
  216. setiastro/saspro/header_viewer.py +448 -0
  217. setiastro/saspro/headless_utils.py +88 -0
  218. setiastro/saspro/histogram.py +756 -0
  219. setiastro/saspro/history_explorer.py +941 -0
  220. setiastro/saspro/i18n.py +168 -0
  221. setiastro/saspro/image_combine.py +417 -0
  222. setiastro/saspro/image_peeker_pro.py +1604 -0
  223. setiastro/saspro/imageops/__init__.py +37 -0
  224. setiastro/saspro/imageops/mdi_snap.py +292 -0
  225. setiastro/saspro/imageops/scnr.py +36 -0
  226. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  227. setiastro/saspro/imageops/stretch.py +236 -0
  228. setiastro/saspro/isophote.py +1182 -0
  229. setiastro/saspro/layers.py +208 -0
  230. setiastro/saspro/layers_dock.py +714 -0
  231. setiastro/saspro/lazy_imports.py +193 -0
  232. setiastro/saspro/legacy/__init__.py +2 -0
  233. setiastro/saspro/legacy/image_manager.py +2360 -0
  234. setiastro/saspro/legacy/numba_utils.py +3676 -0
  235. setiastro/saspro/legacy/xisf.py +1213 -0
  236. setiastro/saspro/linear_fit.py +537 -0
  237. setiastro/saspro/live_stacking.py +1854 -0
  238. setiastro/saspro/log_bus.py +5 -0
  239. setiastro/saspro/logging_config.py +460 -0
  240. setiastro/saspro/luminancerecombine.py +510 -0
  241. setiastro/saspro/main_helpers.py +201 -0
  242. setiastro/saspro/mask_creation.py +1086 -0
  243. setiastro/saspro/masks_core.py +56 -0
  244. setiastro/saspro/mdi_widgets.py +353 -0
  245. setiastro/saspro/memory_utils.py +666 -0
  246. setiastro/saspro/metadata_patcher.py +75 -0
  247. setiastro/saspro/mfdeconv.py +3909 -0
  248. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  249. setiastro/saspro/mfdeconvcudnn.py +3312 -0
  250. setiastro/saspro/mfdeconvsport.py +2459 -0
  251. setiastro/saspro/minorbodycatalog.py +567 -0
  252. setiastro/saspro/morphology.py +407 -0
  253. setiastro/saspro/multiscale_decomp.py +1747 -0
  254. setiastro/saspro/nbtorgb_stars.py +541 -0
  255. setiastro/saspro/numba_utils.py +3145 -0
  256. setiastro/saspro/numba_warmup.py +141 -0
  257. setiastro/saspro/ops/__init__.py +9 -0
  258. setiastro/saspro/ops/command_help_dialog.py +623 -0
  259. setiastro/saspro/ops/command_runner.py +217 -0
  260. setiastro/saspro/ops/commands.py +1594 -0
  261. setiastro/saspro/ops/script_editor.py +1105 -0
  262. setiastro/saspro/ops/scripts.py +1476 -0
  263. setiastro/saspro/ops/settings.py +637 -0
  264. setiastro/saspro/parallel_utils.py +554 -0
  265. setiastro/saspro/pedestal.py +121 -0
  266. setiastro/saspro/perfect_palette_picker.py +1105 -0
  267. setiastro/saspro/pipeline.py +110 -0
  268. setiastro/saspro/pixelmath.py +1604 -0
  269. setiastro/saspro/plate_solver.py +2445 -0
  270. setiastro/saspro/project_io.py +797 -0
  271. setiastro/saspro/psf_utils.py +136 -0
  272. setiastro/saspro/psf_viewer.py +549 -0
  273. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  274. setiastro/saspro/remove_green.py +331 -0
  275. setiastro/saspro/remove_stars.py +1599 -0
  276. setiastro/saspro/remove_stars_preset.py +446 -0
  277. setiastro/saspro/resources.py +503 -0
  278. setiastro/saspro/rgb_combination.py +208 -0
  279. setiastro/saspro/rgb_extract.py +19 -0
  280. setiastro/saspro/rgbalign.py +723 -0
  281. setiastro/saspro/runtime_imports.py +7 -0
  282. setiastro/saspro/runtime_torch.py +754 -0
  283. setiastro/saspro/save_options.py +73 -0
  284. setiastro/saspro/selective_color.py +1611 -0
  285. setiastro/saspro/sfcc.py +1472 -0
  286. setiastro/saspro/shortcuts.py +3116 -0
  287. setiastro/saspro/signature_insert.py +1102 -0
  288. setiastro/saspro/stacking_suite.py +19066 -0
  289. setiastro/saspro/star_alignment.py +7380 -0
  290. setiastro/saspro/star_alignment_preset.py +329 -0
  291. setiastro/saspro/star_metrics.py +49 -0
  292. setiastro/saspro/star_spikes.py +765 -0
  293. setiastro/saspro/star_stretch.py +507 -0
  294. setiastro/saspro/stat_stretch.py +538 -0
  295. setiastro/saspro/status_log_dock.py +78 -0
  296. setiastro/saspro/subwindow.py +3407 -0
  297. setiastro/saspro/supernovaasteroidhunter.py +1719 -0
  298. setiastro/saspro/swap_manager.py +134 -0
  299. setiastro/saspro/torch_backend.py +89 -0
  300. setiastro/saspro/torch_rejection.py +434 -0
  301. setiastro/saspro/translations/all_source_strings.json +4726 -0
  302. setiastro/saspro/translations/ar_translations.py +4096 -0
  303. setiastro/saspro/translations/de_translations.py +3728 -0
  304. setiastro/saspro/translations/es_translations.py +4169 -0
  305. setiastro/saspro/translations/fr_translations.py +4090 -0
  306. setiastro/saspro/translations/hi_translations.py +3803 -0
  307. setiastro/saspro/translations/integrate_translations.py +271 -0
  308. setiastro/saspro/translations/it_translations.py +4728 -0
  309. setiastro/saspro/translations/ja_translations.py +3834 -0
  310. setiastro/saspro/translations/pt_translations.py +3847 -0
  311. setiastro/saspro/translations/ru_translations.py +3082 -0
  312. setiastro/saspro/translations/saspro_ar.qm +0 -0
  313. setiastro/saspro/translations/saspro_ar.ts +16019 -0
  314. setiastro/saspro/translations/saspro_de.qm +0 -0
  315. setiastro/saspro/translations/saspro_de.ts +14548 -0
  316. setiastro/saspro/translations/saspro_es.qm +0 -0
  317. setiastro/saspro/translations/saspro_es.ts +16202 -0
  318. setiastro/saspro/translations/saspro_fr.qm +0 -0
  319. setiastro/saspro/translations/saspro_fr.ts +15870 -0
  320. setiastro/saspro/translations/saspro_hi.qm +0 -0
  321. setiastro/saspro/translations/saspro_hi.ts +14855 -0
  322. setiastro/saspro/translations/saspro_it.qm +0 -0
  323. setiastro/saspro/translations/saspro_it.ts +19046 -0
  324. setiastro/saspro/translations/saspro_ja.qm +0 -0
  325. setiastro/saspro/translations/saspro_ja.ts +14980 -0
  326. setiastro/saspro/translations/saspro_pt.qm +0 -0
  327. setiastro/saspro/translations/saspro_pt.ts +15024 -0
  328. setiastro/saspro/translations/saspro_ru.qm +0 -0
  329. setiastro/saspro/translations/saspro_ru.ts +11835 -0
  330. setiastro/saspro/translations/saspro_sw.qm +0 -0
  331. setiastro/saspro/translations/saspro_sw.ts +15237 -0
  332. setiastro/saspro/translations/saspro_uk.qm +0 -0
  333. setiastro/saspro/translations/saspro_uk.ts +15248 -0
  334. setiastro/saspro/translations/saspro_zh.qm +0 -0
  335. setiastro/saspro/translations/saspro_zh.ts +15289 -0
  336. setiastro/saspro/translations/sw_translations.py +3897 -0
  337. setiastro/saspro/translations/uk_translations.py +3929 -0
  338. setiastro/saspro/translations/zh_translations.py +3910 -0
  339. setiastro/saspro/versioning.py +77 -0
  340. setiastro/saspro/view_bundle.py +1558 -0
  341. setiastro/saspro/wavescale_hdr.py +645 -0
  342. setiastro/saspro/wavescale_hdr_preset.py +101 -0
  343. setiastro/saspro/wavescalede.py +680 -0
  344. setiastro/saspro/wavescalede_preset.py +230 -0
  345. setiastro/saspro/wcs_update.py +374 -0
  346. setiastro/saspro/whitebalance.py +513 -0
  347. setiastro/saspro/widgets/__init__.py +48 -0
  348. setiastro/saspro/widgets/common_utilities.py +306 -0
  349. setiastro/saspro/widgets/graphics_views.py +122 -0
  350. setiastro/saspro/widgets/image_utils.py +518 -0
  351. setiastro/saspro/widgets/minigame/game.js +991 -0
  352. setiastro/saspro/widgets/minigame/index.html +53 -0
  353. setiastro/saspro/widgets/minigame/style.css +241 -0
  354. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  355. setiastro/saspro/widgets/resource_monitor.py +263 -0
  356. setiastro/saspro/widgets/spinboxes.py +290 -0
  357. setiastro/saspro/widgets/themed_buttons.py +13 -0
  358. setiastro/saspro/widgets/wavelet_utils.py +331 -0
  359. setiastro/saspro/wimi.py +7996 -0
  360. setiastro/saspro/wims.py +578 -0
  361. setiastro/saspro/window_shelf.py +185 -0
  362. setiastro/saspro/xisf.py +1213 -0
  363. setiastrosuitepro-1.6.5.post3.dist-info/METADATA +278 -0
  364. setiastrosuitepro-1.6.5.post3.dist-info/RECORD +368 -0
  365. setiastrosuitepro-1.6.5.post3.dist-info/WHEEL +4 -0
  366. setiastrosuitepro-1.6.5.post3.dist-info/entry_points.txt +6 -0
  367. setiastrosuitepro-1.6.5.post3.dist-info/licenses/LICENSE +674 -0
  368. 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)