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,503 @@
1
+ # pro/gui/mixins/geometry_mixin.py
2
+ """
3
+ Geometry operations mixin for AstroSuiteProMainWindow.
4
+
5
+ This mixin contains all geometry-related functionality: invert, flip,
6
+ rotate, rescale, and WCS transformation handling.
7
+ """
8
+ from __future__ import annotations
9
+ from typing import TYPE_CHECKING
10
+
11
+ import numpy as np
12
+ from PyQt6.QtCore import Qt
13
+ from PyQt6.QtGui import QIcon
14
+ from PyQt6.QtWidgets import QMessageBox, QInputDialog, QDialog
15
+
16
+ # Import numba-accelerated functions
17
+ try:
18
+ from setiastro.saspro.legacy.numba_utils import (
19
+ invert_image_numba,
20
+ flip_horizontal_numba,
21
+ flip_vertical_numba,
22
+ rotate_90_clockwise_numba,
23
+ rotate_90_counterclockwise_numba,
24
+ rotate_180_numba,
25
+ rescale_image_numba,
26
+ )
27
+ except ImportError:
28
+ # Fallback stubs if numba_utils is not available
29
+ def invert_image_numba(arr):
30
+ return 1.0 - arr
31
+
32
+ def flip_horizontal_numba(arr):
33
+ return arr[:, ::-1].copy()
34
+
35
+ def flip_vertical_numba(arr):
36
+ return arr[::-1, :].copy()
37
+
38
+ def rotate_90_clockwise_numba(arr):
39
+ return np.rot90(arr, k=-1)
40
+
41
+ def rotate_90_counterclockwise_numba(arr):
42
+ return np.rot90(arr, k=1)
43
+
44
+ def rotate_180_numba(arr):
45
+ return np.rot90(arr, k=2)
46
+
47
+ def rescale_image_numba(arr, factor):
48
+ import cv2
49
+ h, w = arr.shape[:2]
50
+ new_h, new_w = int(h * factor), int(w * factor)
51
+ return cv2.resize(arr, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
52
+
53
+
54
+ from setiastro.saspro.wcs_update import update_wcs_after_crop
55
+
56
+ import cv2
57
+ import math
58
+
59
+ if TYPE_CHECKING:
60
+ pass
61
+
62
+
63
+ class GeometryMixin:
64
+ """
65
+ Mixin for geometry operations.
66
+
67
+ Provides methods for inverting, flipping, rotating, and rescaling images,
68
+ with automatic WCS (World Coordinate System) updates when applicable.
69
+ """
70
+
71
+ def _apply_geom_with_wcs(self, doc, out_image: np.ndarray,
72
+ M_src_to_dst: np.ndarray | None,
73
+ step_name: str):
74
+ """
75
+ Apply a geometry transform to `doc` and update WCS (if present)
76
+ using the same machinery as crop (update_wcs_after_crop).
77
+
78
+ Args:
79
+ doc: Document to apply transform to
80
+ out_image: Transformed image array
81
+ M_src_to_dst: 3x3 transformation matrix (source to destination)
82
+ step_name: Name of the operation for history
83
+ """
84
+ out_h, out_w = out_image.shape[:2]
85
+ meta = dict(getattr(doc, "metadata", {}) or {})
86
+
87
+ if update_wcs_after_crop is not None and M_src_to_dst is not None:
88
+ try:
89
+ meta = update_wcs_after_crop(
90
+ meta,
91
+ M_src_to_dst=M_src_to_dst,
92
+ out_w=out_w,
93
+ out_h=out_h,
94
+ )
95
+ except Exception as e:
96
+ print(f"[WCS-GEOM] WCS update failed for {step_name}: {e}")
97
+
98
+ # Push the image + updated metadata back into the document
99
+ if hasattr(doc, "apply_edit"):
100
+ doc.apply_edit(
101
+ out_image,
102
+ metadata={**meta, "step_name": step_name},
103
+ step_name=step_name,
104
+ )
105
+ else:
106
+ doc.image = out_image
107
+ try:
108
+ setattr(doc, "metadata", {**meta, "step_name": step_name})
109
+ except Exception:
110
+ pass
111
+ if hasattr(doc, "changed"):
112
+ try:
113
+ doc.changed.emit()
114
+ except Exception:
115
+ pass
116
+
117
+ # If WCS was successfully refit, update_wcs_after_crop
118
+ # will have stashed a '__wcs_debug__' payload in metadata.
119
+ dbg = meta.get("__wcs_debug__")
120
+ if isinstance(dbg, dict):
121
+ try:
122
+ self._show_wcs_update_popup(dbg, step_name=step_name)
123
+ except Exception as e:
124
+ print(f"[WCS-GEOM] Failed to show WCS popup for {step_name}: {e}")
125
+
126
+ def _exec_geom_invert(self):
127
+ """Execute invert operation on active view."""
128
+ sw = self.mdi.activeSubWindow() if hasattr(self, "mdi") else None
129
+ view = sw.widget() if sw else None
130
+ doc = getattr(view, "document", None)
131
+ if doc is None or getattr(doc, "image", None) is None:
132
+ QMessageBox.information(self, self.tr("Invert"), self.tr("Active view has no image."))
133
+ return
134
+ try:
135
+ self._apply_geom_invert_to_doc(doc)
136
+ self._log("Invert applied to active view")
137
+ except Exception as e:
138
+ QMessageBox.critical(self, "Invert", str(e))
139
+
140
+ def _exec_geom_flip_h(self):
141
+ """Execute horizontal flip on active view."""
142
+ sw = self.mdi.activeSubWindow() if hasattr(self, "mdi") else None
143
+ view = sw.widget() if sw else None
144
+ doc = getattr(view, "document", None)
145
+ if doc is None or getattr(doc, "image", None) is None:
146
+ QMessageBox.information(self, self.tr("Flip Horizontal"), self.tr("Active view has no image."))
147
+ return
148
+ try:
149
+ self._apply_geom_flip_h_to_doc(doc)
150
+ self._log("Flip Horizontal applied to active view")
151
+ except Exception as e:
152
+ QMessageBox.critical(self, "Flip Horizontal", str(e))
153
+
154
+ def _exec_geom_flip_v(self):
155
+ """Execute vertical flip on active view."""
156
+ sw = self.mdi.activeSubWindow() if hasattr(self, "mdi") else None
157
+ view = sw.widget() if sw else None
158
+ doc = getattr(view, "document", None)
159
+ if doc is None or getattr(doc, "image", None) is None:
160
+ QMessageBox.information(self, self.tr("Flip Vertical"), self.tr("Active view has no image."))
161
+ return
162
+ try:
163
+ self._apply_geom_flip_v_to_doc(doc)
164
+ self._log("Flip Vertical applied to active view")
165
+ except Exception as e:
166
+ QMessageBox.critical(self, "Flip Vertical", str(e))
167
+
168
+ def _exec_geom_rot_cw(self):
169
+ """Execute 90 degree clockwise rotation on active view."""
170
+ sw = self.mdi.activeSubWindow() if hasattr(self, "mdi") else None
171
+ view = sw.widget() if sw else None
172
+ doc = getattr(view, "document", None)
173
+ if doc is None or getattr(doc, "image", None) is None:
174
+ QMessageBox.information(self, self.tr("Rotate 90° CW"), self.tr("Active view has no image."))
175
+ return
176
+ try:
177
+ self._apply_geom_rot_cw_to_doc(doc)
178
+ self._log("Rotate 90° CW applied to active view")
179
+ except Exception as e:
180
+ QMessageBox.critical(self, "Rotate 90° CW", str(e))
181
+
182
+ def _exec_geom_rot_ccw(self):
183
+ """Execute 90 degree counterclockwise rotation on active view."""
184
+ sw = self.mdi.activeSubWindow() if hasattr(self, "mdi") else None
185
+ view = sw.widget() if sw else None
186
+ doc = getattr(view, "document", None)
187
+ if doc is None or getattr(doc, "image", None) is None:
188
+ QMessageBox.information(self, self.tr("Rotate 90° CCW"), self.tr("Active view has no image."))
189
+ return
190
+ try:
191
+ self._apply_geom_rot_ccw_to_doc(doc)
192
+ self._log("Rotate 90° CCW applied to active view")
193
+ except Exception as e:
194
+ QMessageBox.critical(self, "Rotate 90° CCW", str(e))
195
+
196
+ def _exec_geom_rot_180(self):
197
+ """Execute 180 degree rotation on active view."""
198
+ sw = self.mdi.activeSubWindow() if hasattr(self, "mdi") else None
199
+ view = sw.widget() if sw else None
200
+ doc = getattr(view, "document", None)
201
+ if doc is None or getattr(doc, "image", None) is None:
202
+ QMessageBox.information(self, self.tr("Rotate 180°"), self.tr("Active view has no image."))
203
+ return
204
+ try:
205
+ self._apply_geom_rot_180_to_doc(doc)
206
+ self._log("Rotate 180° applied to active view")
207
+ except Exception as e:
208
+ QMessageBox.critical(self, "Rotate 180°", str(e))
209
+
210
+ def _exec_geom_rot_any(self):
211
+ sw = self.mdi.activeSubWindow() if hasattr(self, "mdi") else None
212
+ view = sw.widget() if sw else None
213
+ doc = getattr(view, "document", None)
214
+ if doc is None or getattr(doc, "image", None) is None:
215
+ QMessageBox.information(self, self.tr("Rotate..."), self.tr("Active view has no image."))
216
+ return
217
+
218
+ if cv2 is None:
219
+ QMessageBox.warning(self, self.tr("Rotate..."), self.tr("OpenCV (cv2) is required for arbitrary rotation."))
220
+ return
221
+
222
+ dlg = QInputDialog(self)
223
+ dlg.setWindowTitle(self.tr("Rotate..."))
224
+ dlg.setLabelText(self.tr("Angle in degrees (positive = CCW):"))
225
+ dlg.setInputMode(QInputDialog.InputMode.DoubleInput)
226
+ dlg.setDoubleRange(-360.0, 360.0)
227
+ dlg.setDoubleDecimals(2)
228
+ dlg.setDoubleValue(0.0)
229
+ dlg.setWindowFlag(Qt.WindowType.Window, True)
230
+
231
+ try:
232
+ from setiastro.saspro.resources import rotatearbitrary_path
233
+ dlg.setWindowIcon(QIcon(rotatearbitrary_path))
234
+ except Exception:
235
+ pass
236
+
237
+ if dlg.exec() != QDialog.DialogCode.Accepted:
238
+ return
239
+
240
+ angle = float(dlg.doubleValue())
241
+ try:
242
+ self._apply_geom_rot_any_to_doc(doc, angle_deg=angle)
243
+ self._log(f"Rotate ({angle:g}°) applied to active view")
244
+ except Exception as e:
245
+ QMessageBox.critical(self, self.tr("Rotate..."), str(e))
246
+
247
+
248
+ def _exec_geom_rescale(self):
249
+ """Execute rescale operation on active view with dialog."""
250
+ sw = self.mdi.activeSubWindow() if hasattr(self, "mdi") else None
251
+ view = sw.widget() if sw else None
252
+ doc = getattr(view, "document", None)
253
+ if doc is None or getattr(doc, "image", None) is None:
254
+ QMessageBox.information(self, self.tr("Rescale Image"), self.tr("Active view has no image."))
255
+ return
256
+
257
+ # remember last value
258
+ if not hasattr(self, "_last_rescale_factor"):
259
+ self._last_rescale_factor = 1.0
260
+
261
+ dlg = QInputDialog(self)
262
+ dlg.setWindowTitle(self.tr("Rescale Image"))
263
+ dlg.setLabelText(self.tr("Enter scaling factor (e.g., 0.5 for 50%, 2 for 200%):"))
264
+ dlg.setInputMode(QInputDialog.InputMode.DoubleInput)
265
+ dlg.setDoubleRange(0.1, 10.0)
266
+ dlg.setDoubleDecimals(2)
267
+ dlg.setDoubleValue(self._last_rescale_factor)
268
+
269
+ # make sure it's a true window so the icon shows on all platforms
270
+ dlg.setWindowFlag(Qt.WindowType.Window, True)
271
+
272
+ # set the icon from rescale_path if available
273
+ try:
274
+ from setiastro.saspro.resources import rescale_path
275
+ dlg.setWindowIcon(QIcon(rescale_path))
276
+ except Exception:
277
+ pass
278
+
279
+ if dlg.exec() != QDialog.DialogCode.Accepted:
280
+ return
281
+ factor = dlg.doubleValue()
282
+
283
+ try:
284
+ self._apply_geom_rescale_to_doc(doc, factor=factor)
285
+ self._last_rescale_factor = factor
286
+ self._log(f"Rescale ({factor:g}×) applied to active view")
287
+ except Exception as e:
288
+ QMessageBox.critical(self, self.tr("Rescale Image"), str(e))
289
+
290
+ # --- Geometry: headless apply-to-doc helpers ---
291
+
292
+ def _apply_geom_invert_to_doc(self, doc):
293
+ """Apply invert to document."""
294
+ arr = np.asarray(doc.image, dtype=np.float32)
295
+ out = invert_image_numba(arr)
296
+ if hasattr(doc, "set_image"):
297
+ doc.set_image(out, step_name="Invert")
298
+ else:
299
+ doc.image = out
300
+
301
+ def _apply_geom_flip_h_to_doc(self, doc):
302
+ """Apply horizontal flip to document with WCS update."""
303
+ arr = np.asarray(doc.image, dtype=np.float32)
304
+ h, w = arr.shape[:2]
305
+ out = flip_horizontal_numba(arr)
306
+
307
+ M = np.array([
308
+ [-1.0, 0.0, w - 1.0],
309
+ [0.0, 1.0, 0.0],
310
+ [0.0, 0.0, 1.0],
311
+ ], dtype=float)
312
+
313
+ self._apply_geom_with_wcs(doc, out, M_src_to_dst=M, step_name="Flip Horizontal")
314
+
315
+ def _apply_geom_flip_v_to_doc(self, doc):
316
+ """Apply vertical flip to document with WCS update."""
317
+ arr = np.asarray(doc.image, dtype=np.float32)
318
+ h, w = arr.shape[:2]
319
+ out = flip_vertical_numba(arr)
320
+
321
+ M = np.array([
322
+ [1.0, 0.0, 0.0],
323
+ [0.0, -1.0, h - 1.0],
324
+ [0.0, 0.0, 1.0],
325
+ ], dtype=float)
326
+
327
+ self._apply_geom_with_wcs(doc, out, M_src_to_dst=M, step_name="Flip Vertical")
328
+
329
+ def _apply_geom_rot_cw_to_doc(self, doc):
330
+ """Apply 90° clockwise rotation to document with WCS update."""
331
+ arr = np.asarray(doc.image, dtype=np.float32)
332
+ h, w = arr.shape[:2]
333
+ out = rotate_90_clockwise_numba(arr) # out shape: (w, h)
334
+
335
+ M = np.array([
336
+ [0.0, -1.0, h - 1.0],
337
+ [1.0, 0.0, 0.0],
338
+ [0.0, 0.0, 1.0],
339
+ ], dtype=float)
340
+
341
+ self._apply_geom_with_wcs(doc, out, M_src_to_dst=M, step_name="Rotate 90° Clockwise")
342
+
343
+ def _apply_geom_rot_ccw_to_doc(self, doc):
344
+ """Apply 90° counterclockwise rotation to document with WCS update."""
345
+ arr = np.asarray(doc.image, dtype=np.float32)
346
+ h, w = arr.shape[:2]
347
+ out = rotate_90_counterclockwise_numba(arr) # out shape: (w, h)
348
+
349
+ M = np.array([
350
+ [0.0, 1.0, 0.0],
351
+ [-1.0, 0.0, w - 1.0],
352
+ [0.0, 0.0, 1.0],
353
+ ], dtype=float)
354
+
355
+ self._apply_geom_with_wcs(doc, out, M_src_to_dst=M, step_name="Rotate 90° Counterclockwise")
356
+
357
+ def _apply_geom_rot_180_to_doc(self, doc):
358
+ """Apply 180° rotation to document with WCS update."""
359
+ arr = np.asarray(doc.image, dtype=np.float32)
360
+ h, w = arr.shape[:2]
361
+ out = rotate_180_numba(arr) # out shape: (h, w)
362
+
363
+ # 180° rotation around the image center:
364
+ # (x, y) -> (w-1 - x, h-1 - y)
365
+ M = np.array([
366
+ [-1.0, 0.0, w - 1.0],
367
+ [0.0, -1.0, h - 1.0],
368
+ [0.0, 0.0, 1.0],
369
+ ], dtype=float)
370
+
371
+ self._apply_geom_with_wcs(doc, out, M_src_to_dst=M, step_name="Rotate 180°")
372
+
373
+ def _apply_geom_rot_any_to_doc(self, doc, *, angle_deg: float):
374
+ if cv2 is None:
375
+ raise RuntimeError("cv2 is required for arbitrary rotation")
376
+
377
+ src = np.asarray(doc.image, dtype=np.float32, order="C")
378
+ h, w = src.shape[:2]
379
+
380
+ # Rotation about center
381
+ cx = (w - 1) * 0.5
382
+ cy = (h - 1) * 0.5
383
+
384
+ # OpenCV uses CCW degrees
385
+ A2 = cv2.getRotationMatrix2D((cx, cy), angle_deg, 1.0) # 2x3
386
+
387
+ # Convert to 3x3
388
+ M = np.array([
389
+ [A2[0,0], A2[0,1], A2[0,2]],
390
+ [A2[1,0], A2[1,1], A2[1,2]],
391
+ [0.0, 0.0, 1.0 ],
392
+ ], dtype=np.float32)
393
+
394
+ # Compute output bounds by rotating the four corners
395
+ corners = np.array([
396
+ [0.0, 0.0, 1.0],
397
+ [w - 1.0, 0.0, 1.0],
398
+ [w - 1.0, h - 1.0, 1.0],
399
+ [0.0, h - 1.0, 1.0],
400
+ ], dtype=np.float32).T # 3x4
401
+
402
+ rc = (M @ corners) # 3x4
403
+ xs = rc[0, :]
404
+ ys = rc[1, :]
405
+
406
+ min_x = float(xs.min())
407
+ max_x = float(xs.max())
408
+ min_y = float(ys.min())
409
+ max_y = float(ys.max())
410
+
411
+ out_w = int(math.ceil(max_x - min_x + 1.0))
412
+ out_h = int(math.ceil(max_y - min_y + 1.0))
413
+ if out_w <= 0 or out_h <= 0:
414
+ raise RuntimeError("Invalid output size after rotation")
415
+
416
+ # Shift so that min corner maps to (0,0)
417
+ T = np.array([
418
+ [1.0, 0.0, -min_x],
419
+ [0.0, 1.0, -min_y],
420
+ [0.0, 0.0, 1.0],
421
+ ], dtype=np.float32)
422
+
423
+ M = (T @ M).astype(np.float32) # final src->dst 3x3
424
+
425
+ # Warp
426
+ # cv2.warpPerspective expects (W,H)
427
+ flags = cv2.INTER_LANCZOS4
428
+ if src.ndim == 2:
429
+ out = cv2.warpPerspective(src, M, (out_w, out_h), flags=flags)
430
+ else:
431
+ # warpPerspective works on multi-channel too
432
+ out = cv2.warpPerspective(src, M, (out_w, out_h), flags=flags)
433
+
434
+ self._apply_geom_with_wcs(doc, out, M_src_to_dst=M, step_name=f"Rotate ({angle_deg:g}°)")
435
+
436
+
437
+ def _apply_geom_rescale_to_doc(self, doc, *, factor: float):
438
+ """Apply rescale to document with WCS update."""
439
+ factor = float(max(0.1, min(10.0, factor)))
440
+ arr = np.asarray(doc.image, dtype=np.float32)
441
+ h, w = arr.shape[:2]
442
+ out = rescale_image_numba(arr, factor)
443
+
444
+ M = np.array([
445
+ [factor, 0.0, 0.0],
446
+ [0.0, factor, 0.0],
447
+ [0.0, 0.0, 1.0],
448
+ ], dtype=float)
449
+
450
+ self._apply_geom_with_wcs(doc, out, M_src_to_dst=M,
451
+ step_name=f"Rescale ({factor:g}×)")
452
+
453
+ def _apply_geom_rescale_preset_to_doc(self, doc, preset):
454
+ """
455
+ Accepts flexible presets:
456
+ - dict with 'factor' or 'scale'
457
+ - a lone float/int
458
+ - a '0.5x'/'2x' string
459
+ - (factor, ...) tuple/list
460
+ Falls back to 1.0 if unparsable.
461
+ """
462
+ factor = None
463
+ try:
464
+ if isinstance(preset, dict):
465
+ factor = preset.get("factor", preset.get("scale", None))
466
+ elif isinstance(preset, (float, int)):
467
+ factor = float(preset)
468
+ elif isinstance(preset, str):
469
+ s = preset.strip().lower().replace("×", "x")
470
+ if s.endswith("x"):
471
+ s = s[:-1]
472
+ factor = float(s)
473
+ elif isinstance(preset, (list, tuple)) and preset:
474
+ factor = float(preset[0])
475
+ except Exception:
476
+ factor = None
477
+
478
+ if factor is None:
479
+ factor = getattr(self, "_last_rescale_factor", 1.0) or 1.0
480
+
481
+ self._apply_geom_rescale_to_doc(doc, factor=factor)
482
+
483
+ def _apply_rescale_preset_to_doc(self, doc, preset: dict):
484
+ """
485
+ Headless rescale for drag-and-drop / shortcut preset application.
486
+ Expects preset like {"factor": 1.25}.
487
+ """
488
+ factor = float(preset.get("factor", 1.0))
489
+ if not (0.10 <= factor <= 10.0):
490
+ raise ValueError("Rescale factor must be between 0.10 and 10.0")
491
+
492
+ if getattr(doc, "image", None) is None:
493
+ raise RuntimeError("Target document has no image")
494
+
495
+ src = np.asarray(doc.image, dtype=np.float32, order="C")
496
+ out = rescale_image_numba(src, factor)
497
+
498
+ if hasattr(doc, "set_image"):
499
+ doc.set_image(out, step_name=f"Rescale ×{factor:.2f}")
500
+ elif hasattr(doc, "apply_numpy"):
501
+ doc.apply_numpy(out, step_name=f"Rescale ×{factor:.2f}")
502
+ else:
503
+ doc.image = out