setiastrosuitepro 1.6.7__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 (394) 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/acv_icon.png +0 -0
  24. setiastro/images/andromedatry.png +0 -0
  25. setiastro/images/andromedatry_satellited.png +0 -0
  26. setiastro/images/annotated.png +0 -0
  27. setiastro/images/aperture.png +0 -0
  28. setiastro/images/astrosuite.ico +0 -0
  29. setiastro/images/astrosuite.png +0 -0
  30. setiastro/images/astrosuitepro.icns +0 -0
  31. setiastro/images/astrosuitepro.ico +0 -0
  32. setiastro/images/astrosuitepro.png +0 -0
  33. setiastro/images/background.png +0 -0
  34. setiastro/images/background2.png +0 -0
  35. setiastro/images/benchmark.png +0 -0
  36. setiastro/images/big_moon_stabilizer_timeline.png +0 -0
  37. setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
  38. setiastro/images/blaster.png +0 -0
  39. setiastro/images/blink.png +0 -0
  40. setiastro/images/clahe.png +0 -0
  41. setiastro/images/collage.png +0 -0
  42. setiastro/images/colorwheel.png +0 -0
  43. setiastro/images/contsub.png +0 -0
  44. setiastro/images/convo.png +0 -0
  45. setiastro/images/copyslot.png +0 -0
  46. setiastro/images/cosmic.png +0 -0
  47. setiastro/images/cosmicsat.png +0 -0
  48. setiastro/images/crop1.png +0 -0
  49. setiastro/images/cropicon.png +0 -0
  50. setiastro/images/curves.png +0 -0
  51. setiastro/images/cvs.png +0 -0
  52. setiastro/images/debayer.png +0 -0
  53. setiastro/images/denoise_cnn_custom.png +0 -0
  54. setiastro/images/denoise_cnn_graph.png +0 -0
  55. setiastro/images/disk.png +0 -0
  56. setiastro/images/dse.png +0 -0
  57. setiastro/images/exoicon.png +0 -0
  58. setiastro/images/eye.png +0 -0
  59. setiastro/images/first_quarter.png +0 -0
  60. setiastro/images/fliphorizontal.png +0 -0
  61. setiastro/images/flipvertical.png +0 -0
  62. setiastro/images/font.png +0 -0
  63. setiastro/images/freqsep.png +0 -0
  64. setiastro/images/full_moon.png +0 -0
  65. setiastro/images/functionbundle.png +0 -0
  66. setiastro/images/graxpert.png +0 -0
  67. setiastro/images/green.png +0 -0
  68. setiastro/images/gridicon.png +0 -0
  69. setiastro/images/halo.png +0 -0
  70. setiastro/images/hdr.png +0 -0
  71. setiastro/images/histogram.png +0 -0
  72. setiastro/images/hubble.png +0 -0
  73. setiastro/images/imagecombine.png +0 -0
  74. setiastro/images/invert.png +0 -0
  75. setiastro/images/isophote.png +0 -0
  76. setiastro/images/isophote_demo_figure.png +0 -0
  77. setiastro/images/isophote_demo_image.png +0 -0
  78. setiastro/images/isophote_demo_model.png +0 -0
  79. setiastro/images/isophote_demo_residual.png +0 -0
  80. setiastro/images/jwstpupil.png +0 -0
  81. setiastro/images/last_quarter.png +0 -0
  82. setiastro/images/linearfit.png +0 -0
  83. setiastro/images/livestacking.png +0 -0
  84. setiastro/images/mask.png +0 -0
  85. setiastro/images/maskapply.png +0 -0
  86. setiastro/images/maskcreate.png +0 -0
  87. setiastro/images/maskremove.png +0 -0
  88. setiastro/images/morpho.png +0 -0
  89. setiastro/images/mosaic.png +0 -0
  90. setiastro/images/multiscale_decomp.png +0 -0
  91. setiastro/images/nbtorgb.png +0 -0
  92. setiastro/images/neutral.png +0 -0
  93. setiastro/images/new_moon.png +0 -0
  94. setiastro/images/nuke.png +0 -0
  95. setiastro/images/openfile.png +0 -0
  96. setiastro/images/pedestal.png +0 -0
  97. setiastro/images/pen.png +0 -0
  98. setiastro/images/pixelmath.png +0 -0
  99. setiastro/images/platesolve.png +0 -0
  100. setiastro/images/ppp.png +0 -0
  101. setiastro/images/pro.png +0 -0
  102. setiastro/images/project.png +0 -0
  103. setiastro/images/psf.png +0 -0
  104. setiastro/images/redo.png +0 -0
  105. setiastro/images/redoicon.png +0 -0
  106. setiastro/images/rescale.png +0 -0
  107. setiastro/images/rgbalign.png +0 -0
  108. setiastro/images/rgbcombo.png +0 -0
  109. setiastro/images/rgbextract.png +0 -0
  110. setiastro/images/rotate180.png +0 -0
  111. setiastro/images/rotatearbitrary.png +0 -0
  112. setiastro/images/rotateclockwise.png +0 -0
  113. setiastro/images/rotatecounterclockwise.png +0 -0
  114. setiastro/images/satellite.png +0 -0
  115. setiastro/images/script.png +0 -0
  116. setiastro/images/selectivecolor.png +0 -0
  117. setiastro/images/simbad.png +0 -0
  118. setiastro/images/slot0.png +0 -0
  119. setiastro/images/slot1.png +0 -0
  120. setiastro/images/slot2.png +0 -0
  121. setiastro/images/slot3.png +0 -0
  122. setiastro/images/slot4.png +0 -0
  123. setiastro/images/slot5.png +0 -0
  124. setiastro/images/slot6.png +0 -0
  125. setiastro/images/slot7.png +0 -0
  126. setiastro/images/slot8.png +0 -0
  127. setiastro/images/slot9.png +0 -0
  128. setiastro/images/spcc.png +0 -0
  129. setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
  130. setiastro/images/spinner.gif +0 -0
  131. setiastro/images/stacking.png +0 -0
  132. setiastro/images/staradd.png +0 -0
  133. setiastro/images/staralign.png +0 -0
  134. setiastro/images/starnet.png +0 -0
  135. setiastro/images/starregistration.png +0 -0
  136. setiastro/images/starspike.png +0 -0
  137. setiastro/images/starstretch.png +0 -0
  138. setiastro/images/statstretch.png +0 -0
  139. setiastro/images/supernova.png +0 -0
  140. setiastro/images/uhs.png +0 -0
  141. setiastro/images/undoicon.png +0 -0
  142. setiastro/images/upscale.png +0 -0
  143. setiastro/images/viewbundle.png +0 -0
  144. setiastro/images/waning_crescent_1.png +0 -0
  145. setiastro/images/waning_crescent_2.png +0 -0
  146. setiastro/images/waning_crescent_3.png +0 -0
  147. setiastro/images/waning_crescent_4.png +0 -0
  148. setiastro/images/waning_crescent_5.png +0 -0
  149. setiastro/images/waning_gibbous_1.png +0 -0
  150. setiastro/images/waning_gibbous_2.png +0 -0
  151. setiastro/images/waning_gibbous_3.png +0 -0
  152. setiastro/images/waning_gibbous_4.png +0 -0
  153. setiastro/images/waning_gibbous_5.png +0 -0
  154. setiastro/images/waxing_crescent_1.png +0 -0
  155. setiastro/images/waxing_crescent_2.png +0 -0
  156. setiastro/images/waxing_crescent_3.png +0 -0
  157. setiastro/images/waxing_crescent_4.png +0 -0
  158. setiastro/images/waxing_crescent_5.png +0 -0
  159. setiastro/images/waxing_gibbous_1.png +0 -0
  160. setiastro/images/waxing_gibbous_2.png +0 -0
  161. setiastro/images/waxing_gibbous_3.png +0 -0
  162. setiastro/images/waxing_gibbous_4.png +0 -0
  163. setiastro/images/waxing_gibbous_5.png +0 -0
  164. setiastro/images/whitebalance.png +0 -0
  165. setiastro/images/wimi_icon_256x256.png +0 -0
  166. setiastro/images/wimilogo.png +0 -0
  167. setiastro/images/wims.png +0 -0
  168. setiastro/images/wrench_icon.png +0 -0
  169. setiastro/images/xisfliberator.png +0 -0
  170. setiastro/qml/ResourceMonitor.qml +128 -0
  171. setiastro/saspro/__init__.py +20 -0
  172. setiastro/saspro/__main__.py +964 -0
  173. setiastro/saspro/_generated/__init__.py +7 -0
  174. setiastro/saspro/_generated/build_info.py +3 -0
  175. setiastro/saspro/abe.py +1379 -0
  176. setiastro/saspro/abe_preset.py +196 -0
  177. setiastro/saspro/aberration_ai.py +910 -0
  178. setiastro/saspro/aberration_ai_preset.py +224 -0
  179. setiastro/saspro/accel_installer.py +218 -0
  180. setiastro/saspro/accel_workers.py +30 -0
  181. setiastro/saspro/acv_exporter.py +379 -0
  182. setiastro/saspro/add_stars.py +627 -0
  183. setiastro/saspro/astrobin_exporter.py +1010 -0
  184. setiastro/saspro/astrospike.py +153 -0
  185. setiastro/saspro/astrospike_python.py +1841 -0
  186. setiastro/saspro/autostretch.py +198 -0
  187. setiastro/saspro/backgroundneutral.py +639 -0
  188. setiastro/saspro/batch_convert.py +328 -0
  189. setiastro/saspro/batch_renamer.py +522 -0
  190. setiastro/saspro/blemish_blaster.py +494 -0
  191. setiastro/saspro/blink_comparator_pro.py +3149 -0
  192. setiastro/saspro/bundles.py +61 -0
  193. setiastro/saspro/bundles_dock.py +114 -0
  194. setiastro/saspro/cheat_sheet.py +213 -0
  195. setiastro/saspro/clahe.py +371 -0
  196. setiastro/saspro/comet_stacking.py +1442 -0
  197. setiastro/saspro/common_tr.py +107 -0
  198. setiastro/saspro/config.py +38 -0
  199. setiastro/saspro/config_bootstrap.py +40 -0
  200. setiastro/saspro/config_manager.py +316 -0
  201. setiastro/saspro/continuum_subtract.py +1620 -0
  202. setiastro/saspro/convo.py +1403 -0
  203. setiastro/saspro/convo_preset.py +414 -0
  204. setiastro/saspro/copyastro.py +190 -0
  205. setiastro/saspro/cosmicclarity.py +1593 -0
  206. setiastro/saspro/cosmicclarity_preset.py +407 -0
  207. setiastro/saspro/crop_dialog_pro.py +1005 -0
  208. setiastro/saspro/crop_preset.py +189 -0
  209. setiastro/saspro/curve_editor_pro.py +2608 -0
  210. setiastro/saspro/curves_preset.py +375 -0
  211. setiastro/saspro/debayer.py +673 -0
  212. setiastro/saspro/debug_utils.py +29 -0
  213. setiastro/saspro/dnd_mime.py +35 -0
  214. setiastro/saspro/doc_manager.py +2727 -0
  215. setiastro/saspro/exoplanet_detector.py +2258 -0
  216. setiastro/saspro/file_utils.py +284 -0
  217. setiastro/saspro/fitsmodifier.py +748 -0
  218. setiastro/saspro/fix_bom.py +32 -0
  219. setiastro/saspro/free_torch_memory.py +48 -0
  220. setiastro/saspro/frequency_separation.py +1352 -0
  221. setiastro/saspro/function_bundle.py +1596 -0
  222. setiastro/saspro/generate_translations.py +3092 -0
  223. setiastro/saspro/ghs_dialog_pro.py +728 -0
  224. setiastro/saspro/ghs_preset.py +284 -0
  225. setiastro/saspro/graxpert.py +638 -0
  226. setiastro/saspro/graxpert_preset.py +287 -0
  227. setiastro/saspro/gui/__init__.py +0 -0
  228. setiastro/saspro/gui/main_window.py +8928 -0
  229. setiastro/saspro/gui/mixins/__init__.py +33 -0
  230. setiastro/saspro/gui/mixins/dock_mixin.py +375 -0
  231. setiastro/saspro/gui/mixins/file_mixin.py +450 -0
  232. setiastro/saspro/gui/mixins/geometry_mixin.py +503 -0
  233. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  234. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  235. setiastro/saspro/gui/mixins/menu_mixin.py +391 -0
  236. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  237. setiastro/saspro/gui/mixins/toolbar_mixin.py +1824 -0
  238. setiastro/saspro/gui/mixins/update_mixin.py +323 -0
  239. setiastro/saspro/gui/mixins/view_mixin.py +477 -0
  240. setiastro/saspro/gui/statistics_dialog.py +47 -0
  241. setiastro/saspro/halobgon.py +492 -0
  242. setiastro/saspro/header_viewer.py +448 -0
  243. setiastro/saspro/headless_utils.py +88 -0
  244. setiastro/saspro/histogram.py +760 -0
  245. setiastro/saspro/history_explorer.py +941 -0
  246. setiastro/saspro/i18n.py +168 -0
  247. setiastro/saspro/image_combine.py +421 -0
  248. setiastro/saspro/image_peeker_pro.py +1608 -0
  249. setiastro/saspro/imageops/__init__.py +37 -0
  250. setiastro/saspro/imageops/mdi_snap.py +292 -0
  251. setiastro/saspro/imageops/scnr.py +36 -0
  252. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  253. setiastro/saspro/imageops/stretch.py +236 -0
  254. setiastro/saspro/isophote.py +1186 -0
  255. setiastro/saspro/layers.py +208 -0
  256. setiastro/saspro/layers_dock.py +714 -0
  257. setiastro/saspro/lazy_imports.py +193 -0
  258. setiastro/saspro/legacy/__init__.py +2 -0
  259. setiastro/saspro/legacy/image_manager.py +2360 -0
  260. setiastro/saspro/legacy/numba_utils.py +3676 -0
  261. setiastro/saspro/legacy/xisf.py +1213 -0
  262. setiastro/saspro/linear_fit.py +537 -0
  263. setiastro/saspro/live_stacking.py +1854 -0
  264. setiastro/saspro/log_bus.py +5 -0
  265. setiastro/saspro/logging_config.py +460 -0
  266. setiastro/saspro/luminancerecombine.py +510 -0
  267. setiastro/saspro/main_helpers.py +201 -0
  268. setiastro/saspro/mask_creation.py +1090 -0
  269. setiastro/saspro/masks_core.py +56 -0
  270. setiastro/saspro/mdi_widgets.py +353 -0
  271. setiastro/saspro/memory_utils.py +666 -0
  272. setiastro/saspro/metadata_patcher.py +75 -0
  273. setiastro/saspro/mfdeconv.py +3909 -0
  274. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  275. setiastro/saspro/mfdeconvcudnn.py +3312 -0
  276. setiastro/saspro/mfdeconvsport.py +2459 -0
  277. setiastro/saspro/minorbodycatalog.py +567 -0
  278. setiastro/saspro/morphology.py +411 -0
  279. setiastro/saspro/multiscale_decomp.py +1751 -0
  280. setiastro/saspro/nbtorgb_stars.py +541 -0
  281. setiastro/saspro/numba_utils.py +3145 -0
  282. setiastro/saspro/numba_warmup.py +141 -0
  283. setiastro/saspro/ops/__init__.py +9 -0
  284. setiastro/saspro/ops/command_help_dialog.py +623 -0
  285. setiastro/saspro/ops/command_runner.py +217 -0
  286. setiastro/saspro/ops/commands.py +1594 -0
  287. setiastro/saspro/ops/script_editor.py +1105 -0
  288. setiastro/saspro/ops/scripts.py +1476 -0
  289. setiastro/saspro/ops/settings.py +637 -0
  290. setiastro/saspro/parallel_utils.py +554 -0
  291. setiastro/saspro/pedestal.py +121 -0
  292. setiastro/saspro/perfect_palette_picker.py +1105 -0
  293. setiastro/saspro/pipeline.py +110 -0
  294. setiastro/saspro/pixelmath.py +1604 -0
  295. setiastro/saspro/plate_solver.py +2480 -0
  296. setiastro/saspro/project_io.py +797 -0
  297. setiastro/saspro/psf_utils.py +136 -0
  298. setiastro/saspro/psf_viewer.py +631 -0
  299. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  300. setiastro/saspro/remove_green.py +331 -0
  301. setiastro/saspro/remove_stars.py +1599 -0
  302. setiastro/saspro/remove_stars_preset.py +446 -0
  303. setiastro/saspro/resources.py +570 -0
  304. setiastro/saspro/rgb_combination.py +208 -0
  305. setiastro/saspro/rgb_extract.py +19 -0
  306. setiastro/saspro/rgbalign.py +727 -0
  307. setiastro/saspro/runtime_imports.py +7 -0
  308. setiastro/saspro/runtime_torch.py +754 -0
  309. setiastro/saspro/save_options.py +73 -0
  310. setiastro/saspro/selective_color.py +1614 -0
  311. setiastro/saspro/sfcc.py +1530 -0
  312. setiastro/saspro/shortcuts.py +3125 -0
  313. setiastro/saspro/signature_insert.py +1106 -0
  314. setiastro/saspro/stacking_suite.py +19069 -0
  315. setiastro/saspro/star_alignment.py +7383 -0
  316. setiastro/saspro/star_alignment_preset.py +329 -0
  317. setiastro/saspro/star_metrics.py +49 -0
  318. setiastro/saspro/star_spikes.py +769 -0
  319. setiastro/saspro/star_stretch.py +542 -0
  320. setiastro/saspro/stat_stretch.py +554 -0
  321. setiastro/saspro/status_log_dock.py +78 -0
  322. setiastro/saspro/subwindow.py +3523 -0
  323. setiastro/saspro/supernovaasteroidhunter.py +1719 -0
  324. setiastro/saspro/swap_manager.py +134 -0
  325. setiastro/saspro/torch_backend.py +89 -0
  326. setiastro/saspro/torch_rejection.py +434 -0
  327. setiastro/saspro/translations/all_source_strings.json +4726 -0
  328. setiastro/saspro/translations/ar_translations.py +4096 -0
  329. setiastro/saspro/translations/de_translations.py +3728 -0
  330. setiastro/saspro/translations/es_translations.py +4169 -0
  331. setiastro/saspro/translations/fr_translations.py +4090 -0
  332. setiastro/saspro/translations/hi_translations.py +3803 -0
  333. setiastro/saspro/translations/integrate_translations.py +271 -0
  334. setiastro/saspro/translations/it_translations.py +4728 -0
  335. setiastro/saspro/translations/ja_translations.py +3834 -0
  336. setiastro/saspro/translations/pt_translations.py +3847 -0
  337. setiastro/saspro/translations/ru_translations.py +3082 -0
  338. setiastro/saspro/translations/saspro_ar.qm +0 -0
  339. setiastro/saspro/translations/saspro_ar.ts +16019 -0
  340. setiastro/saspro/translations/saspro_de.qm +0 -0
  341. setiastro/saspro/translations/saspro_de.ts +14548 -0
  342. setiastro/saspro/translations/saspro_es.qm +0 -0
  343. setiastro/saspro/translations/saspro_es.ts +16202 -0
  344. setiastro/saspro/translations/saspro_fr.qm +0 -0
  345. setiastro/saspro/translations/saspro_fr.ts +15870 -0
  346. setiastro/saspro/translations/saspro_hi.qm +0 -0
  347. setiastro/saspro/translations/saspro_hi.ts +14855 -0
  348. setiastro/saspro/translations/saspro_it.qm +0 -0
  349. setiastro/saspro/translations/saspro_it.ts +19046 -0
  350. setiastro/saspro/translations/saspro_ja.qm +0 -0
  351. setiastro/saspro/translations/saspro_ja.ts +14980 -0
  352. setiastro/saspro/translations/saspro_pt.qm +0 -0
  353. setiastro/saspro/translations/saspro_pt.ts +15024 -0
  354. setiastro/saspro/translations/saspro_ru.qm +0 -0
  355. setiastro/saspro/translations/saspro_ru.ts +11835 -0
  356. setiastro/saspro/translations/saspro_sw.qm +0 -0
  357. setiastro/saspro/translations/saspro_sw.ts +15237 -0
  358. setiastro/saspro/translations/saspro_uk.qm +0 -0
  359. setiastro/saspro/translations/saspro_uk.ts +15248 -0
  360. setiastro/saspro/translations/saspro_zh.qm +0 -0
  361. setiastro/saspro/translations/saspro_zh.ts +15289 -0
  362. setiastro/saspro/translations/sw_translations.py +3897 -0
  363. setiastro/saspro/translations/uk_translations.py +3929 -0
  364. setiastro/saspro/translations/zh_translations.py +3910 -0
  365. setiastro/saspro/versioning.py +77 -0
  366. setiastro/saspro/view_bundle.py +1558 -0
  367. setiastro/saspro/wavescale_hdr.py +648 -0
  368. setiastro/saspro/wavescale_hdr_preset.py +101 -0
  369. setiastro/saspro/wavescalede.py +683 -0
  370. setiastro/saspro/wavescalede_preset.py +230 -0
  371. setiastro/saspro/wcs_update.py +374 -0
  372. setiastro/saspro/whitebalance.py +540 -0
  373. setiastro/saspro/widgets/__init__.py +48 -0
  374. setiastro/saspro/widgets/common_utilities.py +306 -0
  375. setiastro/saspro/widgets/graphics_views.py +122 -0
  376. setiastro/saspro/widgets/image_utils.py +518 -0
  377. setiastro/saspro/widgets/minigame/game.js +991 -0
  378. setiastro/saspro/widgets/minigame/index.html +53 -0
  379. setiastro/saspro/widgets/minigame/style.css +241 -0
  380. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  381. setiastro/saspro/widgets/resource_monitor.py +313 -0
  382. setiastro/saspro/widgets/spinboxes.py +290 -0
  383. setiastro/saspro/widgets/themed_buttons.py +13 -0
  384. setiastro/saspro/widgets/wavelet_utils.py +331 -0
  385. setiastro/saspro/wimi.py +7367 -0
  386. setiastro/saspro/wims.py +588 -0
  387. setiastro/saspro/window_shelf.py +185 -0
  388. setiastro/saspro/xisf.py +1213 -0
  389. setiastrosuitepro-1.6.7.dist-info/METADATA +279 -0
  390. setiastrosuitepro-1.6.7.dist-info/RECORD +394 -0
  391. setiastrosuitepro-1.6.7.dist-info/WHEEL +4 -0
  392. setiastrosuitepro-1.6.7.dist-info/entry_points.txt +6 -0
  393. setiastrosuitepro-1.6.7.dist-info/licenses/LICENSE +674 -0
  394. setiastrosuitepro-1.6.7.dist-info/licenses/license.txt +2580 -0
@@ -0,0 +1,727 @@
1
+ # pro/rgbalign.py
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import numpy as np
6
+
7
+ from PyQt6.QtCore import Qt, QThread, pyqtSignal
8
+ from PyQt6.QtWidgets import (
9
+ QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QApplication,
10
+ QComboBox, QCheckBox, QMessageBox, QProgressBar, QPlainTextEdit, QSpinBox
11
+ )
12
+
13
+
14
+ import astroalign
15
+
16
+ import sep
17
+
18
+ import cv2
19
+
20
+ # try to reuse poly from star_alignment if present
21
+ try:
22
+ from setiastro.saspro.star_alignment import PolynomialTransform
23
+ except Exception:
24
+ PolynomialTransform = None
25
+
26
+
27
+ # ─────────────────────────────────────────────────────────────────────
28
+ # Worker
29
+ # ─────────────────────────────────────────────────────────────────────
30
+ class RGBAlignWorker(QThread):
31
+ progress = pyqtSignal(int, str) # (percent, message)
32
+ done = pyqtSignal(np.ndarray) # aligned RGB image
33
+ failed = pyqtSignal(str)
34
+ EDGE_FRAC = 0.55 # 55% of max radius
35
+ MIN_EDGE_PTS = 6 # want at least 6 out there
36
+ EDGE_INNER_FRAC = 0.38 # toss center 38% radius
37
+ MATCH_MAX_DIST = 10.0 # px, generous for CA
38
+ MIN_MATCHES = 6
39
+
40
+
41
+ def __init__(self, img: np.ndarray, model: str, sep_sigma: float = 3.0):
42
+ super().__init__()
43
+ self.img = img
44
+ self.model = model
45
+ self.sep_sigma = float(sep_sigma)
46
+
47
+ self.r_xform = None
48
+ self.b_xform = None
49
+ self.r_pairs = None
50
+ self.b_pairs = None
51
+
52
+ def _pts_too_central(self, pts: np.ndarray | None, shape) -> bool:
53
+ """
54
+ Return True if the matched points are all bunched near the center.
55
+ pts: (N, 2) in x,y
56
+ shape: (H, W)
57
+ """
58
+ if pts is None or len(pts) == 0:
59
+ return True
60
+ h, w = shape[:2]
61
+ cx, cy = w * 0.5, h * 0.5
62
+ # distance of each point from center
63
+ r = np.hypot(pts[:, 0] - cx, pts[:, 1] - cy)
64
+ rmax = np.hypot(cx, cy) # radius to corner
65
+ edge_mask = r > (self.EDGE_FRAC * rmax)
66
+ return edge_mask.sum() < self.MIN_EDGE_PTS
67
+
68
+ def _sep_detect_points(self, img: np.ndarray):
69
+ """Return (N,2) points from SEP, brightest first, using user sigma."""
70
+ if sep is None:
71
+ return None
72
+ data = img.astype(np.float32, copy=False)
73
+ bkg = sep.Background(data)
74
+ data_sub = data - bkg
75
+ # use the promoted sigma here 👇
76
+ objs = sep.extract(data_sub, self.sep_sigma, err=bkg.globalrms)
77
+ if objs is None or len(objs) == 0:
78
+ return None
79
+ idx = np.argsort(objs["peak"])[::-1]
80
+ pts = np.stack([objs["x"][idx], objs["y"][idx]], axis=1)
81
+ return pts
82
+
83
+ def _filter_edge_ring(self, pts: np.ndarray, shape, inner_frac=EDGE_INNER_FRAC):
84
+ """Keep only points outside inner_frac * Rmax."""
85
+ if pts is None or pts.size == 0:
86
+ return None
87
+ h, w = shape[:2]
88
+ cx, cy = w * 0.5, h * 0.5
89
+ r = np.hypot(pts[:,0] - cx, pts[:,1] - cy)
90
+ rmax = np.hypot(cx, cy)
91
+ mask = r >= (inner_frac * rmax)
92
+ pts_edge = pts[mask]
93
+ return pts_edge if pts_edge.size else None
94
+
95
+ def _pair_edge_points(self, src_img, ref_img, shape):
96
+ """Detect in BOTH images, keep only edge ring in REF, then NN-match in SRC."""
97
+ ref_pts = self._sep_detect_points(ref_img)
98
+ src_pts = self._sep_detect_points(src_img)
99
+ if ref_pts is None or src_pts is None:
100
+ return None, None
101
+
102
+ ref_edge = self._filter_edge_ring(ref_pts, shape)
103
+ if ref_edge is None:
104
+ return None, None
105
+
106
+ # brute-force NN, small N, so ok
107
+ src_arr = np.asarray(src_pts, dtype=np.float32)
108
+ pairs_src = []
109
+ pairs_dst = []
110
+ for (x_ref, y_ref) in ref_edge:
111
+ dxy = src_arr - np.array([x_ref, y_ref], dtype=np.float32)
112
+ dist = np.hypot(dxy[:,0], dxy[:,1])
113
+ j = np.argmin(dist)
114
+ if dist[j] <= self.MATCH_MAX_DIST:
115
+ # src point is in the channel we want to warp → source
116
+ pairs_src.append(src_arr[j])
117
+ # ref point is the green channel → destination
118
+ pairs_dst.append([x_ref, y_ref])
119
+
120
+ if len(pairs_src) < self.MIN_MATCHES:
121
+ return None, None
122
+
123
+ return (np.array(pairs_src, dtype=np.float32),
124
+ np.array(pairs_dst, dtype=np.float32))
125
+
126
+
127
+ def run(self):
128
+ if self.img is None or self.img.ndim != 3 or self.img.shape[2] < 3:
129
+ self.failed.emit("Image must be RGB (3 channels).")
130
+ return
131
+ if astroalign is None:
132
+ self.failed.emit("astroalign is not available.")
133
+ return
134
+
135
+ try:
136
+ self.progress.emit(5, "Preparing channels…")
137
+ R = np.ascontiguousarray(self.img[..., 0].astype(np.float32, copy=False))
138
+ G = np.ascontiguousarray(self.img[..., 1].astype(np.float32, copy=False))
139
+ B = np.ascontiguousarray(self.img[..., 2].astype(np.float32, copy=False))
140
+
141
+ # R → G
142
+ self.progress.emit(15, "Aligning Red → Green…")
143
+ kind_R, X_R, (r_src, r_dst) = self._estimate_transform(R, G, self.model)
144
+ self.r_xform = (kind_R, X_R)
145
+ self.r_pairs = (r_src, r_dst)
146
+ self.progress.emit(35, f"Red transform = {kind_R}")
147
+ R_aligned = self._warp_channel(R, kind_R, X_R, G.shape)
148
+
149
+ # B → G
150
+ self.progress.emit(55, "Aligning Blue → Green…")
151
+ kind_B, X_B, (b_src, b_dst) = self._estimate_transform(B, G, self.model)
152
+ self.b_xform = (kind_B, X_B)
153
+ self.b_pairs = (b_src, b_dst)
154
+ self.progress.emit(75, f"Blue transform = {kind_B}")
155
+ B_aligned = self._warp_channel(B, kind_B, X_B, G.shape)
156
+
157
+ out = np.stack([R_aligned, G, B_aligned], axis=2).astype(self.img.dtype, copy=False)
158
+ self.progress.emit(100, "Done.")
159
+ self.done.emit(out)
160
+ except Exception as e:
161
+ self.failed.emit(str(e))
162
+
163
+
164
+ # ───── helpers (basically mini versions of your big star alignment logic) ─────
165
+ def _estimate_transform(self, src: np.ndarray, ref: np.ndarray, model: str):
166
+ H, W = ref.shape[:2]
167
+
168
+ # ── 0) edge-only, SEP-based path ─────────────────────────────
169
+ if model == "edge-sep":
170
+ src_xy, dst_xy = self._pair_edge_points(src, ref, (H, W))
171
+ if src_xy is not None and dst_xy is not None and cv2 is not None:
172
+ # 0a) try homography first (better for corner warp)
173
+ Hh, inliers = cv2.findHomography(
174
+ src_xy, dst_xy,
175
+ method=cv2.RANSAC,
176
+ ransacReprojThreshold=2.5,
177
+ maxIters=2000,
178
+ confidence=0.999,
179
+ )
180
+ if Hh is not None:
181
+ return ("homography", Hh, (src_xy, dst_xy))
182
+
183
+ # 0b) fallback → affine
184
+ A, inliers = cv2.estimateAffine2D(
185
+ src_xy, dst_xy,
186
+ method=cv2.RANSAC,
187
+ ransacReprojThreshold=2.5,
188
+ maxIters=2000,
189
+ confidence=0.999,
190
+ )
191
+ if A is not None:
192
+ return ("affine", A, (src_xy, dst_xy))
193
+ # if SEP failed or cv2 missing → fall through to astroalign normal path
194
+
195
+ # ─────────────────────────────────────────────────────────────
196
+ # 1) astroalign normal pass
197
+ # ─────────────────────────────────────────────────────────────
198
+ tform, (src_pts, dst_pts) = astroalign.find_transform(
199
+ np.ascontiguousarray(src),
200
+ np.ascontiguousarray(ref),
201
+ max_control_points=50,
202
+ detection_sigma=5.0,
203
+ min_area=5,
204
+ )
205
+
206
+ # 2) 'hungry' pass if too central
207
+ if self._pts_too_central(dst_pts, ref.shape):
208
+ tform2, (src_pts2, dst_pts2) = astroalign.find_transform(
209
+ np.ascontiguousarray(src),
210
+ np.ascontiguousarray(ref),
211
+ max_control_points=120,
212
+ detection_sigma=3.0,
213
+ min_area=3,
214
+ )
215
+ if not self._pts_too_central(dst_pts2, ref.shape):
216
+ tform, src_pts, dst_pts = tform2, src_pts2, dst_pts2
217
+
218
+ # 3) original branching
219
+ P = np.asarray(tform.params, dtype=np.float64)
220
+ src_xy = np.asarray(src_pts, dtype=np.float32)
221
+ dst_xy = np.asarray(dst_pts, dtype=np.float32)
222
+
223
+ # affine
224
+ if model == "affine":
225
+ if cv2 is None:
226
+ return ("affine", P[0:2, :], (src_xy, dst_xy))
227
+ A, _ = cv2.estimateAffine2D(
228
+ src_xy, dst_xy, method=cv2.RANSAC, ransacReprojThreshold=3.0
229
+ )
230
+ if A is None:
231
+ return ("affine", P[0:2, :], (src_xy, dst_xy))
232
+ return ("affine", A, (src_xy, dst_xy))
233
+
234
+ # homography
235
+ if model == "homography":
236
+ if cv2 is None:
237
+ if P.shape == (3, 3):
238
+ return ("homography", P, (src_xy, dst_xy))
239
+ A3 = np.vstack([P[0:2, :], [0, 0, 1]])
240
+ return ("homography", A3, (src_xy, dst_xy))
241
+ Hh, _ = cv2.findHomography(
242
+ src_xy, dst_xy, method=cv2.RANSAC, ransacReprojThreshold=3.0
243
+ )
244
+ if Hh is None:
245
+ if P.shape == (3, 3):
246
+ return ("homography", P, (src_xy, dst_xy))
247
+ A3 = np.vstack([P[0:2, :], [0, 0, 1]])
248
+ return ("homography", A3, (src_xy, dst_xy))
249
+ return ("homography", Hh, (src_xy, dst_xy))
250
+
251
+ # poly3 / poly4
252
+ if model in ("poly3", "poly4") and PolynomialTransform is not None and cv2 is not None:
253
+ order = 3 if model == "poly3" else 4
254
+ scale_vec = np.array([W, H], dtype=np.float32)
255
+ src_n = src_xy / scale_vec
256
+ dst_n = dst_xy / scale_vec
257
+
258
+ t_poly = PolynomialTransform()
259
+ ok = t_poly.estimate(dst_n, src_n, order=order) # dst → src
260
+ if not ok:
261
+ Hh, _ = cv2.findHomography(
262
+ src_xy, dst_xy, method=cv2.RANSAC, ransacReprojThreshold=3.0
263
+ )
264
+ return ("homography", Hh, (src_xy, dst_xy))
265
+
266
+ def _warp_poly(img: np.ndarray, out_shape: tuple[int, int]):
267
+ Hh_, Ww_ = out_shape
268
+ yy, xx = np.mgrid[0:Hh_, 0:Ww_].astype(np.float32)
269
+ coords = np.stack([xx, yy], axis=-1).reshape(-1, 2)
270
+ coords_n = coords / scale_vec
271
+ mapped_n = t_poly(coords_n)
272
+ mapped = mapped_n * scale_vec
273
+ map_x = mapped[:, 0].reshape(Hh_, Ww_).astype(np.float32)
274
+ map_y = mapped[:, 1].reshape(Hh_, Ww_).astype(np.float32)
275
+ return cv2.remap(
276
+ img, map_x, map_y,
277
+ interpolation=cv2.INTER_LANCZOS4,
278
+ borderMode=cv2.BORDER_CONSTANT, borderValue=0
279
+ )
280
+
281
+ return (model, _warp_poly, (src_xy, dst_xy))
282
+
283
+ # fallback → homography
284
+ if cv2 is None:
285
+ if P.shape == (3, 3):
286
+ return ("homography", P, (src_xy, dst_xy))
287
+ A3 = np.vstack([P[0:2, :], [0, 0, 1]])
288
+ return ("homography", A3, (src_xy, dst_xy))
289
+
290
+ Hh, _ = cv2.findHomography(
291
+ src_xy, dst_xy, method=cv2.RANSAC, ransacReprojThreshold=3.0
292
+ )
293
+ return ("homography", Hh, (src_xy, dst_xy))
294
+
295
+
296
+
297
+ def _pick_edge_stars_with_sep(self, img, tiles=(3,3), per_tile=2):
298
+ if sep is None:
299
+ return []
300
+ data = img.astype(np.float32, copy=False)
301
+ bkg = sep.Background(data)
302
+ data_sub = data - bkg
303
+ objs = sep.extract(data_sub, 1.5, err=bkg.globalrms)
304
+ H, W = data.shape[:2]
305
+ th, tw = H // tiles[0], W // tiles[1]
306
+ picked = []
307
+ for ty in range(tiles[0]):
308
+ for tx in range(tiles[1]):
309
+ y0, y1 = ty*th, min((ty+1)*th, H)
310
+ x0, x1 = tx*tw, min((tx+1)*tw, W)
311
+ box = objs[
312
+ (objs['y'] >= y0) & (objs['y'] < y1) &
313
+ (objs['x'] >= x0) & (objs['x'] < x1)
314
+ ]
315
+ if len(box) == 0:
316
+ continue
317
+ # brightest first
318
+ box = box[np.argsort(box['peak'])][::-1][:per_tile]
319
+ for o in box:
320
+ picked.append((float(o['x']), float(o['y'])))
321
+ return picked
322
+
323
+
324
+ def _warp_channel(self, ch: np.ndarray, kind: str, X, ref_shape):
325
+ H, W = ref_shape[:2]
326
+ if kind == "affine":
327
+ # Just assume cv2 is available (standard dependency) for perf
328
+ A = np.asarray(X, dtype=np.float32).reshape(2, 3)
329
+ return cv2.warpAffine(ch, A, (W, H), flags=cv2.INTER_LANCZOS4,
330
+ borderMode=cv2.BORDER_CONSTANT, borderValue=0)
331
+
332
+ if kind == "homography":
333
+ Hm = np.asarray(X, dtype=np.float32).reshape(3, 3)
334
+ return cv2.warpPerspective(ch, Hm, (W, H), flags=cv2.INTER_LANCZOS4,
335
+ borderMode=cv2.BORDER_CONSTANT, borderValue=0)
336
+
337
+ if kind.startswith("poly"):
338
+ return X(ch, (H, W))
339
+
340
+ return ch
341
+
342
+
343
+ # ─────────────────────────────────────────────────────────────────────
344
+ # Dialog
345
+ # ─────────────────────────────────────────────────────────────────────
346
+ class RGBAlignDialog(QDialog):
347
+ def __init__(self, parent=None, document=None):
348
+ super().__init__(parent)
349
+ self.setWindowTitle(self.tr("RGB Align"))
350
+ self.setWindowFlag(Qt.WindowType.Window, True)
351
+ self.setWindowModality(Qt.WindowModality.NonModal)
352
+ self.setModal(False)
353
+ try:
354
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
355
+ except Exception:
356
+ pass # older PyQt6 versions
357
+ self.parent = parent
358
+ # document could be a view; try to unwrap
359
+ self.doc_view = document
360
+ self.doc = getattr(document, "document", document)
361
+ self.image = getattr(self.doc, "image", None) if self.doc is not None else None
362
+
363
+ lay = QVBoxLayout(self)
364
+ lay.addWidget(QLabel("Align R and B channels to G.\n"
365
+ "Select model and run."))
366
+
367
+ hl = QHBoxLayout()
368
+ hl.addWidget(QLabel(self.tr("Alignment model:")))
369
+ self.model_combo = QComboBox()
370
+ self.model_combo.addItems([
371
+ "EDGE", # ← first, new default
372
+ "Homography",
373
+ "Affine",
374
+ "Poly 3",
375
+ "Poly 4",
376
+ ])
377
+ self.model_combo.setCurrentIndex(0)
378
+
379
+ # tooltips for each mode
380
+ self.model_combo.setItemData(
381
+ 0,
382
+ (
383
+ "EDGE (Edge-Detected Guided Estimator)\n"
384
+ "• Detect stars in both channels with SEP\n"
385
+ "• Keep only outer-ring stars (ignore center)\n"
386
+ "• Try homography first for corner CA\n"
387
+ "• If homography fails → try affine\n"
388
+ "• If that fails → fall back to astroalign"
389
+ ),
390
+ Qt.ItemDataRole.ToolTipRole,
391
+ )
392
+ self.model_combo.setItemData(
393
+ 1,
394
+ "Standard homography using astroalign matches (good general-purpose choice).",
395
+ Qt.ItemDataRole.ToolTipRole,
396
+ )
397
+ self.model_combo.setItemData(
398
+ 2,
399
+ "Affine (shift + scale + rotate + shear). Good when channels are mostly parallel.",
400
+ Qt.ItemDataRole.ToolTipRole,
401
+ )
402
+ self.model_combo.setItemData(
403
+ 3,
404
+ "Polynomial (order 3). Use when you have mild field distortion.",
405
+ Qt.ItemDataRole.ToolTipRole,
406
+ )
407
+ self.model_combo.setItemData(
408
+ 4,
409
+ "Polynomial (order 4). Use for stronger distortion, but needs more/better matches.",
410
+ Qt.ItemDataRole.ToolTipRole,
411
+ )
412
+ hl.addWidget(self.model_combo)
413
+ lay.addLayout(hl)
414
+
415
+ # ── SEP controls ─────────────────────────
416
+ sep_row = QHBoxLayout()
417
+ sep_row.addWidget(QLabel(self.tr("SEP sigma:")))
418
+
419
+ self.sep_spin = QSpinBox()
420
+ self.sep_spin.setRange(1, 100)
421
+ self.sep_spin.setValue(5) # default; 1.5 was too hungry
422
+ self.sep_spin.setToolTip("Detection threshold (σ) for SEP star finding in EDGE mode.\n"
423
+ "Higher = fewer stars, lower = more stars.")
424
+ sep_row.addWidget(self.sep_spin)
425
+
426
+ self.btn_trial_sep = QPushButton(self.tr("Trial detect stars"))
427
+ self.btn_trial_sep.setToolTip("Run SEP on the green channel with this sigma and report how many "
428
+ "stars it finds and how many are in the EDGE ring.")
429
+ self.btn_trial_sep.clicked.connect(self._trial_sep_detect)
430
+ sep_row.addWidget(self.btn_trial_sep)
431
+
432
+ lay.addLayout(sep_row)
433
+
434
+
435
+ self.chk_new_doc = QCheckBox(self.tr("Create new document (keep original)"))
436
+ self.chk_new_doc.setChecked(True)
437
+ lay.addWidget(self.chk_new_doc)
438
+
439
+ # progress
440
+ self.progress_label = QLabel("Idle.")
441
+ self.progress_bar = QProgressBar()
442
+ self.progress_bar.setRange(0, 100)
443
+ self.progress_bar.setValue(0)
444
+ lay.addWidget(self.progress_label)
445
+ lay.addWidget(self.progress_bar)
446
+
447
+ self.summary_box = QPlainTextEdit()
448
+ self.summary_box.setReadOnly(True)
449
+ self.summary_box.setPlaceholderText("Transform summary will appear here…")
450
+ self.summary_box.setMinimumHeight(140)
451
+ # optional: monospace
452
+ self.summary_box.setStyleSheet("font-family: Consolas, 'Courier New', monospace; font-size: 11px;")
453
+ lay.addWidget(self.summary_box)
454
+
455
+ btns = QHBoxLayout()
456
+ self.btn_run = QPushButton(self.tr("Align"))
457
+ self.btn_close = QPushButton(self.tr("Close"))
458
+ btns.addWidget(self.btn_run)
459
+ btns.addWidget(self.btn_close)
460
+ lay.addLayout(btns)
461
+
462
+ self.btn_run.clicked.connect(self._start_align)
463
+ self.btn_close.clicked.connect(self.close)
464
+
465
+ self.worker: RGBAlignWorker | None = None
466
+
467
+ def _trial_sep_detect(self):
468
+ if self.image is None:
469
+ QMessageBox.warning(self, "RGB Align", "No image loaded.")
470
+ return
471
+ if sep is None:
472
+ QMessageBox.warning(self, "RGB Align", "python-sep is not available.")
473
+ return
474
+ self.progress_label.setText(f"Trial Detection In Progress…")
475
+ QApplication.processEvents()
476
+ # use green channel as reference, same as align
477
+ G = np.ascontiguousarray(self.image[..., 1].astype(np.float32, copy=False))
478
+ sigma = float(self.sep_spin.value())
479
+
480
+ # run a mini version of what the worker does
481
+ bkg = sep.Background(G)
482
+ data_sub = G - bkg
483
+ objs = sep.extract(data_sub, sigma, err=bkg.globalrms)
484
+ total = 0 if objs is None else len(objs)
485
+
486
+ # compute how many are in the EDGE ring, using same logic/constants
487
+ h, w = G.shape[:2]
488
+ cx, cy = w * 0.5, h * 0.5
489
+ rmax = np.hypot(cx, cy)
490
+ edge_inner = RGBAlignWorker.EDGE_INNER_FRAC * rmax
491
+
492
+ if objs is not None and total > 0:
493
+ r = np.hypot(objs["x"] - cx, objs["y"] - cy)
494
+ edge_mask = r >= edge_inner
495
+ edge_count = int(edge_mask.sum())
496
+ else:
497
+ edge_count = 0
498
+
499
+ msg = (f"[Trial SEP]\n"
500
+ f"sigma = {sigma}\n"
501
+ f"total stars (green): {total}\n"
502
+ f"outer-ring stars (used by EDGE): {edge_count}")
503
+ self.summary_box.setPlainText(msg)
504
+ self.progress_label.setText(f"Trial SEP: {total} stars, {edge_count} edge")
505
+
506
+
507
+ def _start_align(self):
508
+ if self.image is None:
509
+ QMessageBox.warning(self, "RGB Align", "No image found in active view.")
510
+ return
511
+ if self.image.ndim != 3 or self.image.shape[2] < 3:
512
+ QMessageBox.warning(self, "RGB Align", "Image must be RGB (3 channels).")
513
+ return
514
+ if astroalign is None:
515
+ QMessageBox.warning(self, "RGB Align", "astroalign is not available.")
516
+ return
517
+
518
+ model = self._selected_model()
519
+ sep_sigma = float(self.sep_spin.value())
520
+ self.progress_label.setText("Starting…")
521
+ self.progress_bar.setValue(0)
522
+
523
+ self.worker = RGBAlignWorker(self.image, model, sep_sigma=sep_sigma)
524
+ self.worker.progress.connect(self._on_worker_progress)
525
+ self.worker.done.connect(self._on_worker_done)
526
+ self.worker.failed.connect(self._on_worker_failed)
527
+ self.worker.start()
528
+ self.btn_run.setEnabled(False)
529
+
530
+ def _selected_model(self) -> str:
531
+ txt = self.model_combo.currentText().lower()
532
+ if "edge" in txt:
533
+ return "edge-sep"
534
+ if "affine" in txt:
535
+ return "affine"
536
+ if "poly 3" in txt:
537
+ return "poly3"
538
+ if "poly 4" in txt:
539
+ return "poly4"
540
+ if "homography" in txt:
541
+ return "homography"
542
+ return "edge-sep" # super-safe fallback
543
+
544
+ # slots
545
+ def _on_worker_progress(self, pct: int, msg: str):
546
+ self.progress_bar.setValue(pct)
547
+ self.progress_label.setText(msg)
548
+
549
+ def _on_worker_failed(self, err: str):
550
+ self.btn_run.setEnabled(True)
551
+ self.progress_bar.setValue(0)
552
+ self.progress_label.setText("Failed.")
553
+ QMessageBox.critical(self, "RGB Align", err)
554
+
555
+ def _on_worker_done(self, out: np.ndarray):
556
+ self.btn_run.setEnabled(True)
557
+ self.progress_bar.setValue(100)
558
+ self.progress_label.setText("Applying…")
559
+
560
+ summary_lines = []
561
+ w = self.worker # type: ignore
562
+
563
+ if w is not None:
564
+ def _fmt_mat(M):
565
+ return "\n".join(
566
+ [" " + " ".join(f"{v: .6f}" for v in row) for row in M]
567
+ )
568
+
569
+ def _spread_stats(pts, shape):
570
+ if pts is None:
571
+ return " points: 0"
572
+ pts = np.asarray(pts, dtype=float)
573
+ if pts.size == 0:
574
+ return " points: 0"
575
+ h, w_ = shape[:2]
576
+ cx, cy = w_ * 0.5, h * 0.5
577
+ if pts.ndim != 2 or pts.shape[1] != 2:
578
+ return f" points: {len(pts)} (unusual shape {pts.shape})"
579
+ r = np.hypot(pts[:, 0] - cx, pts[:, 1] - cy)
580
+ rmax = np.hypot(cx, cy)
581
+ edge = r > (RGBAlignWorker.EDGE_FRAC * rmax)
582
+ return (
583
+ f" points: {len(pts)} "
584
+ f"(edge: {edge.sum()} ≥{RGBAlignWorker.EDGE_FRAC*100:.0f}%Rmax)"
585
+ )
586
+
587
+ h_img, w_img = self.image.shape[:2]
588
+
589
+ # ── R → G ──
590
+ if w.r_xform is not None:
591
+ kind, X = w.r_xform
592
+ summary_lines.append("Red → Green:")
593
+ if w.r_pairs is not None and len(w.r_pairs) == 2:
594
+ summary_lines.append(_spread_stats(w.r_pairs[1], (h_img, w_img)))
595
+ summary_lines.append(f" model: {kind}")
596
+ if kind == "affine":
597
+ A = np.asarray(X, dtype=float).reshape(2, 3)
598
+ M = np.vstack([A, [0, 0, 1]])
599
+ summary_lines.append(_fmt_mat(M))
600
+ elif kind == "homography":
601
+ Hm = np.asarray(X, dtype=float).reshape(3, 3)
602
+ summary_lines.append(_fmt_mat(Hm))
603
+ else:
604
+ summary_lines.append(" (non-matrix; warp callable)")
605
+
606
+ # ── B → G ──
607
+ if w.b_xform is not None:
608
+ kind, X = w.b_xform
609
+ summary_lines.append("")
610
+ summary_lines.append("Blue → Green:")
611
+ if w.b_pairs is not None and len(w.b_pairs) == 2:
612
+ summary_lines.append(_spread_stats(w.b_pairs[1], (h_img, w_img)))
613
+ summary_lines.append(f" model: {kind}")
614
+ if kind == "affine":
615
+ A = np.asarray(X, dtype=float).reshape(2, 3)
616
+ M = np.vstack([A, [0, 0, 1]])
617
+ summary_lines.append(_fmt_mat(M))
618
+ elif kind == "homography":
619
+ Hm = np.asarray(X, dtype=float).reshape(3, 3)
620
+ summary_lines.append(_fmt_mat(Hm))
621
+ else:
622
+ summary_lines.append(" (non-matrix; warp callable)")
623
+
624
+ summary_text = "\n".join(summary_lines) if summary_lines else "No transform info."
625
+ self.summary_box.setPlainText(summary_text)
626
+
627
+ if self.parent is not None and hasattr(self.parent, "_log") and callable(self.parent._log):
628
+ self.parent._log("[RGB Align]\n" + summary_text)
629
+
630
+ try:
631
+ if self.chk_new_doc.isChecked():
632
+ dm = getattr(self.parent, "docman", None)
633
+ if dm is not None:
634
+ dm.open_array(out, {"display_name": "RGB Aligned"}, title="RGB Aligned")
635
+ else:
636
+ if hasattr(self.doc, "apply_edit"):
637
+ self.doc.apply_edit(out, {"step_name": "RGB Align"}, step_name="RGB Align")
638
+ else:
639
+ self.doc.image = out
640
+ else:
641
+ if hasattr(self.doc, "apply_edit"):
642
+ self.doc.apply_edit(out, {"step_name": "RGB Align"}, step_name="RGB Align")
643
+ else:
644
+ self.doc.image = out
645
+
646
+ self.progress_label.setText("Done.")
647
+ except Exception as e:
648
+ self.progress_label.setText("Apply failed.")
649
+ QMessageBox.warning(self, "RGB Align", f"Aligned image created, but applying failed:\n{e}")
650
+
651
+
652
+
653
+
654
+
655
+ def align_rgb_array(img: np.ndarray, model: str = "edge-sep", sep_sigma: float = 3.0) -> np.ndarray:
656
+ """
657
+ Headless core: returns a new RGB image with R,B aligned to G.
658
+ Raises RuntimeError on problems.
659
+ """
660
+ if img is None or img.ndim != 3 or img.shape[2] < 3:
661
+ raise RuntimeError("Image must be RGB (3 channels).")
662
+ if astroalign is None:
663
+ raise RuntimeError("astroalign is not available.")
664
+
665
+ worker = RGBAlignWorker(img, model, sep_sigma=sep_sigma)
666
+
667
+ try:
668
+ R = np.ascontiguousarray(img[..., 0].astype(np.float32, copy=False))
669
+ G = np.ascontiguousarray(img[..., 1].astype(np.float32, copy=False))
670
+ B = np.ascontiguousarray(img[..., 2].astype(np.float32, copy=False))
671
+
672
+ def _estimate_and_warp(src, ref):
673
+ # NOTE: _estimate_transform now returns 3 values
674
+ kind, X, _pairs = worker._estimate_transform(src, ref, model)
675
+ return worker._warp_channel(src, kind, X, ref.shape)
676
+
677
+ R_aligned = _estimate_and_warp(R, G)
678
+ B_aligned = _estimate_and_warp(B, G)
679
+
680
+ out = np.stack([R_aligned, G, B_aligned], axis=2)
681
+ if img.dtype != out.dtype:
682
+ out = out.astype(img.dtype, copy=False)
683
+ return out
684
+ except Exception as e:
685
+ raise RuntimeError(str(e))
686
+
687
+ def run_rgb_align_headless(main_window, document, preset: dict | None = None):
688
+ if document is None:
689
+ QMessageBox.warning(main_window, "RGB Align", "No active document.")
690
+ return
691
+
692
+ img = np.asarray(document.image)
693
+ p = dict(preset or {})
694
+ model = p.get("model", "edge").lower()
695
+ sep_sigma = float(p.get("sep_sigma", 3.0))
696
+ create_new = bool(p.get("new_doc", False))
697
+
698
+ sb = getattr(main_window, "statusBar", None)
699
+ if callable(sb):
700
+ sb().showMessage(f"RGB Align ({model})…", 3000)
701
+
702
+ try:
703
+ out = align_rgb_array(img, model=model if model != "edge" else "edge-sep",
704
+ sep_sigma=sep_sigma)
705
+ except Exception as e:
706
+ QMessageBox.critical(main_window, "RGB Align (headless)", str(e))
707
+ return
708
+
709
+ if create_new:
710
+ dm = getattr(main_window, "docman", None)
711
+ if dm is not None:
712
+ dm.open_array(out, {"display_name": "RGB Aligned"}, title="RGB Aligned")
713
+ else:
714
+ # fallback to replace if we can't create new
715
+ try:
716
+ document.apply_edit(out, {"step_name": "RGB Align"})
717
+ except Exception:
718
+ document.image = out
719
+ else:
720
+ # in-place
721
+ if hasattr(document, "apply_edit"):
722
+ document.apply_edit(out, {"step_name": "RGB Align"}, step_name="RGB Align")
723
+ else:
724
+ document.image = out
725
+
726
+ if callable(sb):
727
+ sb().showMessage("RGB Align done.", 3000)