setiastrosuitepro 1.6.2.post1__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 (367) 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/rotateclockwise.png +0 -0
  107. setiastro/images/rotatecounterclockwise.png +0 -0
  108. setiastro/images/satellite.png +0 -0
  109. setiastro/images/script.png +0 -0
  110. setiastro/images/selectivecolor.png +0 -0
  111. setiastro/images/simbad.png +0 -0
  112. setiastro/images/slot0.png +0 -0
  113. setiastro/images/slot1.png +0 -0
  114. setiastro/images/slot2.png +0 -0
  115. setiastro/images/slot3.png +0 -0
  116. setiastro/images/slot4.png +0 -0
  117. setiastro/images/slot5.png +0 -0
  118. setiastro/images/slot6.png +0 -0
  119. setiastro/images/slot7.png +0 -0
  120. setiastro/images/slot8.png +0 -0
  121. setiastro/images/slot9.png +0 -0
  122. setiastro/images/spcc.png +0 -0
  123. setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
  124. setiastro/images/spinner.gif +0 -0
  125. setiastro/images/stacking.png +0 -0
  126. setiastro/images/staradd.png +0 -0
  127. setiastro/images/staralign.png +0 -0
  128. setiastro/images/starnet.png +0 -0
  129. setiastro/images/starregistration.png +0 -0
  130. setiastro/images/starspike.png +0 -0
  131. setiastro/images/starstretch.png +0 -0
  132. setiastro/images/statstretch.png +0 -0
  133. setiastro/images/supernova.png +0 -0
  134. setiastro/images/uhs.png +0 -0
  135. setiastro/images/undoicon.png +0 -0
  136. setiastro/images/upscale.png +0 -0
  137. setiastro/images/viewbundle.png +0 -0
  138. setiastro/images/whitebalance.png +0 -0
  139. setiastro/images/wimi_icon_256x256.png +0 -0
  140. setiastro/images/wimilogo.png +0 -0
  141. setiastro/images/wims.png +0 -0
  142. setiastro/images/wrench_icon.png +0 -0
  143. setiastro/images/xisfliberator.png +0 -0
  144. setiastro/qml/ResourceMonitor.qml +126 -0
  145. setiastro/saspro/__init__.py +20 -0
  146. setiastro/saspro/__main__.py +945 -0
  147. setiastro/saspro/_generated/__init__.py +7 -0
  148. setiastro/saspro/_generated/build_info.py +3 -0
  149. setiastro/saspro/abe.py +1346 -0
  150. setiastro/saspro/abe_preset.py +196 -0
  151. setiastro/saspro/aberration_ai.py +694 -0
  152. setiastro/saspro/aberration_ai_preset.py +224 -0
  153. setiastro/saspro/accel_installer.py +218 -0
  154. setiastro/saspro/accel_workers.py +30 -0
  155. setiastro/saspro/add_stars.py +624 -0
  156. setiastro/saspro/astrobin_exporter.py +1010 -0
  157. setiastro/saspro/astrospike.py +153 -0
  158. setiastro/saspro/astrospike_python.py +1841 -0
  159. setiastro/saspro/autostretch.py +198 -0
  160. setiastro/saspro/backgroundneutral.py +602 -0
  161. setiastro/saspro/batch_convert.py +328 -0
  162. setiastro/saspro/batch_renamer.py +522 -0
  163. setiastro/saspro/blemish_blaster.py +491 -0
  164. setiastro/saspro/blink_comparator_pro.py +2926 -0
  165. setiastro/saspro/bundles.py +61 -0
  166. setiastro/saspro/bundles_dock.py +114 -0
  167. setiastro/saspro/cheat_sheet.py +213 -0
  168. setiastro/saspro/clahe.py +368 -0
  169. setiastro/saspro/comet_stacking.py +1442 -0
  170. setiastro/saspro/common_tr.py +107 -0
  171. setiastro/saspro/config.py +38 -0
  172. setiastro/saspro/config_bootstrap.py +40 -0
  173. setiastro/saspro/config_manager.py +316 -0
  174. setiastro/saspro/continuum_subtract.py +1617 -0
  175. setiastro/saspro/convo.py +1400 -0
  176. setiastro/saspro/convo_preset.py +414 -0
  177. setiastro/saspro/copyastro.py +190 -0
  178. setiastro/saspro/cosmicclarity.py +1589 -0
  179. setiastro/saspro/cosmicclarity_preset.py +407 -0
  180. setiastro/saspro/crop_dialog_pro.py +973 -0
  181. setiastro/saspro/crop_preset.py +189 -0
  182. setiastro/saspro/curve_editor_pro.py +2562 -0
  183. setiastro/saspro/curves_preset.py +375 -0
  184. setiastro/saspro/debayer.py +673 -0
  185. setiastro/saspro/debug_utils.py +29 -0
  186. setiastro/saspro/dnd_mime.py +35 -0
  187. setiastro/saspro/doc_manager.py +2664 -0
  188. setiastro/saspro/exoplanet_detector.py +2166 -0
  189. setiastro/saspro/file_utils.py +284 -0
  190. setiastro/saspro/fitsmodifier.py +748 -0
  191. setiastro/saspro/fix_bom.py +32 -0
  192. setiastro/saspro/free_torch_memory.py +48 -0
  193. setiastro/saspro/frequency_separation.py +1349 -0
  194. setiastro/saspro/function_bundle.py +1596 -0
  195. setiastro/saspro/generate_translations.py +3092 -0
  196. setiastro/saspro/ghs_dialog_pro.py +663 -0
  197. setiastro/saspro/ghs_preset.py +284 -0
  198. setiastro/saspro/graxpert.py +637 -0
  199. setiastro/saspro/graxpert_preset.py +287 -0
  200. setiastro/saspro/gui/__init__.py +0 -0
  201. setiastro/saspro/gui/main_window.py +8810 -0
  202. setiastro/saspro/gui/mixins/__init__.py +33 -0
  203. setiastro/saspro/gui/mixins/dock_mixin.py +362 -0
  204. setiastro/saspro/gui/mixins/file_mixin.py +450 -0
  205. setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
  206. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  207. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  208. setiastro/saspro/gui/mixins/menu_mixin.py +389 -0
  209. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  210. setiastro/saspro/gui/mixins/toolbar_mixin.py +1457 -0
  211. setiastro/saspro/gui/mixins/update_mixin.py +309 -0
  212. setiastro/saspro/gui/mixins/view_mixin.py +435 -0
  213. setiastro/saspro/gui/statistics_dialog.py +47 -0
  214. setiastro/saspro/halobgon.py +488 -0
  215. setiastro/saspro/header_viewer.py +448 -0
  216. setiastro/saspro/headless_utils.py +88 -0
  217. setiastro/saspro/histogram.py +756 -0
  218. setiastro/saspro/history_explorer.py +941 -0
  219. setiastro/saspro/i18n.py +168 -0
  220. setiastro/saspro/image_combine.py +417 -0
  221. setiastro/saspro/image_peeker_pro.py +1604 -0
  222. setiastro/saspro/imageops/__init__.py +37 -0
  223. setiastro/saspro/imageops/mdi_snap.py +292 -0
  224. setiastro/saspro/imageops/scnr.py +36 -0
  225. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  226. setiastro/saspro/imageops/stretch.py +236 -0
  227. setiastro/saspro/isophote.py +1182 -0
  228. setiastro/saspro/layers.py +208 -0
  229. setiastro/saspro/layers_dock.py +714 -0
  230. setiastro/saspro/lazy_imports.py +193 -0
  231. setiastro/saspro/legacy/__init__.py +2 -0
  232. setiastro/saspro/legacy/image_manager.py +2226 -0
  233. setiastro/saspro/legacy/numba_utils.py +3676 -0
  234. setiastro/saspro/legacy/xisf.py +1071 -0
  235. setiastro/saspro/linear_fit.py +537 -0
  236. setiastro/saspro/live_stacking.py +1841 -0
  237. setiastro/saspro/log_bus.py +5 -0
  238. setiastro/saspro/logging_config.py +460 -0
  239. setiastro/saspro/luminancerecombine.py +309 -0
  240. setiastro/saspro/main_helpers.py +201 -0
  241. setiastro/saspro/mask_creation.py +931 -0
  242. setiastro/saspro/masks_core.py +56 -0
  243. setiastro/saspro/mdi_widgets.py +353 -0
  244. setiastro/saspro/memory_utils.py +666 -0
  245. setiastro/saspro/metadata_patcher.py +75 -0
  246. setiastro/saspro/mfdeconv.py +3831 -0
  247. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  248. setiastro/saspro/mfdeconvcudnn.py +3263 -0
  249. setiastro/saspro/mfdeconvsport.py +2382 -0
  250. setiastro/saspro/minorbodycatalog.py +567 -0
  251. setiastro/saspro/morphology.py +407 -0
  252. setiastro/saspro/multiscale_decomp.py +1293 -0
  253. setiastro/saspro/nbtorgb_stars.py +541 -0
  254. setiastro/saspro/numba_utils.py +3145 -0
  255. setiastro/saspro/numba_warmup.py +141 -0
  256. setiastro/saspro/ops/__init__.py +9 -0
  257. setiastro/saspro/ops/command_help_dialog.py +623 -0
  258. setiastro/saspro/ops/command_runner.py +217 -0
  259. setiastro/saspro/ops/commands.py +1594 -0
  260. setiastro/saspro/ops/script_editor.py +1102 -0
  261. setiastro/saspro/ops/scripts.py +1473 -0
  262. setiastro/saspro/ops/settings.py +637 -0
  263. setiastro/saspro/parallel_utils.py +554 -0
  264. setiastro/saspro/pedestal.py +121 -0
  265. setiastro/saspro/perfect_palette_picker.py +1071 -0
  266. setiastro/saspro/pipeline.py +110 -0
  267. setiastro/saspro/pixelmath.py +1604 -0
  268. setiastro/saspro/plate_solver.py +2445 -0
  269. setiastro/saspro/project_io.py +797 -0
  270. setiastro/saspro/psf_utils.py +136 -0
  271. setiastro/saspro/psf_viewer.py +549 -0
  272. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  273. setiastro/saspro/remove_green.py +331 -0
  274. setiastro/saspro/remove_stars.py +1599 -0
  275. setiastro/saspro/remove_stars_preset.py +404 -0
  276. setiastro/saspro/resources.py +501 -0
  277. setiastro/saspro/rgb_combination.py +208 -0
  278. setiastro/saspro/rgb_extract.py +19 -0
  279. setiastro/saspro/rgbalign.py +723 -0
  280. setiastro/saspro/runtime_imports.py +7 -0
  281. setiastro/saspro/runtime_torch.py +754 -0
  282. setiastro/saspro/save_options.py +73 -0
  283. setiastro/saspro/selective_color.py +1552 -0
  284. setiastro/saspro/sfcc.py +1472 -0
  285. setiastro/saspro/shortcuts.py +3043 -0
  286. setiastro/saspro/signature_insert.py +1102 -0
  287. setiastro/saspro/stacking_suite.py +18470 -0
  288. setiastro/saspro/star_alignment.py +7435 -0
  289. setiastro/saspro/star_alignment_preset.py +329 -0
  290. setiastro/saspro/star_metrics.py +49 -0
  291. setiastro/saspro/star_spikes.py +765 -0
  292. setiastro/saspro/star_stretch.py +507 -0
  293. setiastro/saspro/stat_stretch.py +538 -0
  294. setiastro/saspro/status_log_dock.py +78 -0
  295. setiastro/saspro/subwindow.py +3328 -0
  296. setiastro/saspro/supernovaasteroidhunter.py +1719 -0
  297. setiastro/saspro/swap_manager.py +99 -0
  298. setiastro/saspro/torch_backend.py +89 -0
  299. setiastro/saspro/torch_rejection.py +434 -0
  300. setiastro/saspro/translations/all_source_strings.json +3654 -0
  301. setiastro/saspro/translations/ar_translations.py +3865 -0
  302. setiastro/saspro/translations/de_translations.py +3749 -0
  303. setiastro/saspro/translations/es_translations.py +3939 -0
  304. setiastro/saspro/translations/fr_translations.py +3858 -0
  305. setiastro/saspro/translations/hi_translations.py +3571 -0
  306. setiastro/saspro/translations/integrate_translations.py +270 -0
  307. setiastro/saspro/translations/it_translations.py +3678 -0
  308. setiastro/saspro/translations/ja_translations.py +3601 -0
  309. setiastro/saspro/translations/pt_translations.py +3869 -0
  310. setiastro/saspro/translations/ru_translations.py +2848 -0
  311. setiastro/saspro/translations/saspro_ar.qm +0 -0
  312. setiastro/saspro/translations/saspro_ar.ts +255 -0
  313. setiastro/saspro/translations/saspro_de.qm +0 -0
  314. setiastro/saspro/translations/saspro_de.ts +253 -0
  315. setiastro/saspro/translations/saspro_es.qm +0 -0
  316. setiastro/saspro/translations/saspro_es.ts +12520 -0
  317. setiastro/saspro/translations/saspro_fr.qm +0 -0
  318. setiastro/saspro/translations/saspro_fr.ts +12514 -0
  319. setiastro/saspro/translations/saspro_hi.qm +0 -0
  320. setiastro/saspro/translations/saspro_hi.ts +257 -0
  321. setiastro/saspro/translations/saspro_it.qm +0 -0
  322. setiastro/saspro/translations/saspro_it.ts +12520 -0
  323. setiastro/saspro/translations/saspro_ja.qm +0 -0
  324. setiastro/saspro/translations/saspro_ja.ts +257 -0
  325. setiastro/saspro/translations/saspro_pt.qm +0 -0
  326. setiastro/saspro/translations/saspro_pt.ts +257 -0
  327. setiastro/saspro/translations/saspro_ru.qm +0 -0
  328. setiastro/saspro/translations/saspro_ru.ts +237 -0
  329. setiastro/saspro/translations/saspro_sw.qm +0 -0
  330. setiastro/saspro/translations/saspro_sw.ts +257 -0
  331. setiastro/saspro/translations/saspro_uk.qm +0 -0
  332. setiastro/saspro/translations/saspro_uk.ts +10771 -0
  333. setiastro/saspro/translations/saspro_zh.qm +0 -0
  334. setiastro/saspro/translations/saspro_zh.ts +12520 -0
  335. setiastro/saspro/translations/sw_translations.py +3671 -0
  336. setiastro/saspro/translations/uk_translations.py +3700 -0
  337. setiastro/saspro/translations/zh_translations.py +3675 -0
  338. setiastro/saspro/versioning.py +77 -0
  339. setiastro/saspro/view_bundle.py +1558 -0
  340. setiastro/saspro/wavescale_hdr.py +645 -0
  341. setiastro/saspro/wavescale_hdr_preset.py +101 -0
  342. setiastro/saspro/wavescalede.py +680 -0
  343. setiastro/saspro/wavescalede_preset.py +230 -0
  344. setiastro/saspro/wcs_update.py +374 -0
  345. setiastro/saspro/whitebalance.py +492 -0
  346. setiastro/saspro/widgets/__init__.py +48 -0
  347. setiastro/saspro/widgets/common_utilities.py +306 -0
  348. setiastro/saspro/widgets/graphics_views.py +122 -0
  349. setiastro/saspro/widgets/image_utils.py +518 -0
  350. setiastro/saspro/widgets/minigame/game.js +986 -0
  351. setiastro/saspro/widgets/minigame/index.html +53 -0
  352. setiastro/saspro/widgets/minigame/style.css +241 -0
  353. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  354. setiastro/saspro/widgets/resource_monitor.py +237 -0
  355. setiastro/saspro/widgets/spinboxes.py +275 -0
  356. setiastro/saspro/widgets/themed_buttons.py +13 -0
  357. setiastro/saspro/widgets/wavelet_utils.py +331 -0
  358. setiastro/saspro/wimi.py +7996 -0
  359. setiastro/saspro/wims.py +578 -0
  360. setiastro/saspro/window_shelf.py +185 -0
  361. setiastro/saspro/xisf.py +1123 -0
  362. setiastrosuitepro-1.6.2.post1.dist-info/METADATA +278 -0
  363. setiastrosuitepro-1.6.2.post1.dist-info/RECORD +367 -0
  364. setiastrosuitepro-1.6.2.post1.dist-info/WHEEL +4 -0
  365. setiastrosuitepro-1.6.2.post1.dist-info/entry_points.txt +6 -0
  366. setiastrosuitepro-1.6.2.post1.dist-info/licenses/LICENSE +674 -0
  367. setiastrosuitepro-1.6.2.post1.dist-info/licenses/license.txt +2580 -0
@@ -0,0 +1,403 @@
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
+ # Try to import WCS update function
55
+ try:
56
+ from setiastro.saspro.wcs_utils import update_wcs_after_crop
57
+ except ImportError:
58
+ update_wcs_after_crop = None
59
+
60
+
61
+ if TYPE_CHECKING:
62
+ pass
63
+
64
+
65
+ class GeometryMixin:
66
+ """
67
+ Mixin for geometry operations.
68
+
69
+ Provides methods for inverting, flipping, rotating, and rescaling images,
70
+ with automatic WCS (World Coordinate System) updates when applicable.
71
+ """
72
+
73
+ def _apply_geom_with_wcs(self, doc, out_image: np.ndarray,
74
+ M_src_to_dst: np.ndarray | None,
75
+ step_name: str):
76
+ """
77
+ Apply a geometry transform to `doc` and update WCS (if present)
78
+ using the same machinery as crop (update_wcs_after_crop).
79
+
80
+ Args:
81
+ doc: Document to apply transform to
82
+ out_image: Transformed image array
83
+ M_src_to_dst: 3x3 transformation matrix (source to destination)
84
+ step_name: Name of the operation for history
85
+ """
86
+ out_h, out_w = out_image.shape[:2]
87
+ meta = dict(getattr(doc, "metadata", {}) or {})
88
+
89
+ if update_wcs_after_crop is not None and M_src_to_dst is not None:
90
+ try:
91
+ meta = update_wcs_after_crop(
92
+ meta,
93
+ M_src_to_dst=M_src_to_dst,
94
+ out_w=out_w,
95
+ out_h=out_h,
96
+ )
97
+ except Exception as e:
98
+ print(f"[WCS-GEOM] WCS update failed for {step_name}: {e}")
99
+
100
+ # Push the image + updated metadata back into the document
101
+ if hasattr(doc, "apply_edit"):
102
+ doc.apply_edit(
103
+ out_image,
104
+ metadata={**meta, "step_name": step_name},
105
+ step_name=step_name,
106
+ )
107
+ else:
108
+ doc.image = out_image
109
+ try:
110
+ setattr(doc, "metadata", {**meta, "step_name": step_name})
111
+ except Exception:
112
+ pass
113
+ if hasattr(doc, "changed"):
114
+ try:
115
+ doc.changed.emit()
116
+ except Exception:
117
+ pass
118
+
119
+ # If WCS was successfully refit, update_wcs_after_crop
120
+ # will have stashed a '__wcs_debug__' payload in metadata.
121
+ dbg = meta.get("__wcs_debug__")
122
+ if isinstance(dbg, dict):
123
+ try:
124
+ self._show_wcs_update_popup(dbg, step_name=step_name)
125
+ except Exception as e:
126
+ print(f"[WCS-GEOM] Failed to show WCS popup for {step_name}: {e}")
127
+
128
+ def _exec_geom_invert(self):
129
+ """Execute invert operation on active view."""
130
+ sw = self.mdi.activeSubWindow() if hasattr(self, "mdi") else None
131
+ view = sw.widget() if sw else None
132
+ doc = getattr(view, "document", None)
133
+ if doc is None or getattr(doc, "image", None) is None:
134
+ QMessageBox.information(self, self.tr("Invert"), self.tr("Active view has no image."))
135
+ return
136
+ try:
137
+ self._apply_geom_invert_to_doc(doc)
138
+ self._log("Invert applied to active view")
139
+ except Exception as e:
140
+ QMessageBox.critical(self, "Invert", str(e))
141
+
142
+ def _exec_geom_flip_h(self):
143
+ """Execute horizontal flip on active view."""
144
+ sw = self.mdi.activeSubWindow() if hasattr(self, "mdi") else None
145
+ view = sw.widget() if sw else None
146
+ doc = getattr(view, "document", None)
147
+ if doc is None or getattr(doc, "image", None) is None:
148
+ QMessageBox.information(self, self.tr("Flip Horizontal"), self.tr("Active view has no image."))
149
+ return
150
+ try:
151
+ self._apply_geom_flip_h_to_doc(doc)
152
+ self._log("Flip Horizontal applied to active view")
153
+ except Exception as e:
154
+ QMessageBox.critical(self, "Flip Horizontal", str(e))
155
+
156
+ def _exec_geom_flip_v(self):
157
+ """Execute vertical flip on active view."""
158
+ sw = self.mdi.activeSubWindow() if hasattr(self, "mdi") else None
159
+ view = sw.widget() if sw else None
160
+ doc = getattr(view, "document", None)
161
+ if doc is None or getattr(doc, "image", None) is None:
162
+ QMessageBox.information(self, self.tr("Flip Vertical"), self.tr("Active view has no image."))
163
+ return
164
+ try:
165
+ self._apply_geom_flip_v_to_doc(doc)
166
+ self._log("Flip Vertical applied to active view")
167
+ except Exception as e:
168
+ QMessageBox.critical(self, "Flip Vertical", str(e))
169
+
170
+ def _exec_geom_rot_cw(self):
171
+ """Execute 90 degree clockwise rotation on active view."""
172
+ sw = self.mdi.activeSubWindow() if hasattr(self, "mdi") else None
173
+ view = sw.widget() if sw else None
174
+ doc = getattr(view, "document", None)
175
+ if doc is None or getattr(doc, "image", None) is None:
176
+ QMessageBox.information(self, self.tr("Rotate 90° CW"), self.tr("Active view has no image."))
177
+ return
178
+ try:
179
+ self._apply_geom_rot_cw_to_doc(doc)
180
+ self._log("Rotate 90° CW applied to active view")
181
+ except Exception as e:
182
+ QMessageBox.critical(self, "Rotate 90° CW", str(e))
183
+
184
+ def _exec_geom_rot_ccw(self):
185
+ """Execute 90 degree counterclockwise rotation on active view."""
186
+ sw = self.mdi.activeSubWindow() if hasattr(self, "mdi") else None
187
+ view = sw.widget() if sw else None
188
+ doc = getattr(view, "document", None)
189
+ if doc is None or getattr(doc, "image", None) is None:
190
+ QMessageBox.information(self, self.tr("Rotate 90° CCW"), self.tr("Active view has no image."))
191
+ return
192
+ try:
193
+ self._apply_geom_rot_ccw_to_doc(doc)
194
+ self._log("Rotate 90° CCW applied to active view")
195
+ except Exception as e:
196
+ QMessageBox.critical(self, "Rotate 90° CCW", str(e))
197
+
198
+ def _exec_geom_rot_180(self):
199
+ """Execute 180 degree rotation on active view."""
200
+ sw = self.mdi.activeSubWindow() if hasattr(self, "mdi") else None
201
+ view = sw.widget() if sw else None
202
+ doc = getattr(view, "document", None)
203
+ if doc is None or getattr(doc, "image", None) is None:
204
+ QMessageBox.information(self, self.tr("Rotate 180°"), self.tr("Active view has no image."))
205
+ return
206
+ try:
207
+ self._apply_geom_rot_180_to_doc(doc)
208
+ self._log("Rotate 180° applied to active view")
209
+ except Exception as e:
210
+ QMessageBox.critical(self, "Rotate 180°", str(e))
211
+
212
+ def _exec_geom_rescale(self):
213
+ """Execute rescale operation on active view with dialog."""
214
+ sw = self.mdi.activeSubWindow() if hasattr(self, "mdi") else None
215
+ view = sw.widget() if sw else None
216
+ doc = getattr(view, "document", None)
217
+ if doc is None or getattr(doc, "image", None) is None:
218
+ QMessageBox.information(self, self.tr("Rescale Image"), self.tr("Active view has no image."))
219
+ return
220
+
221
+ # remember last value
222
+ if not hasattr(self, "_last_rescale_factor"):
223
+ self._last_rescale_factor = 1.0
224
+
225
+ dlg = QInputDialog(self)
226
+ dlg.setWindowTitle(self.tr("Rescale Image"))
227
+ dlg.setLabelText(self.tr("Enter scaling factor (e.g., 0.5 for 50%, 2 for 200%):"))
228
+ dlg.setInputMode(QInputDialog.InputMode.DoubleInput)
229
+ dlg.setDoubleRange(0.1, 10.0)
230
+ dlg.setDoubleDecimals(2)
231
+ dlg.setDoubleValue(self._last_rescale_factor)
232
+
233
+ # make sure it's a true window so the icon shows on all platforms
234
+ dlg.setWindowFlag(Qt.WindowType.Window, True)
235
+
236
+ # set the icon from rescale_path if available
237
+ try:
238
+ from setiastro.saspro.resources import rescale_path
239
+ dlg.setWindowIcon(QIcon(rescale_path))
240
+ except Exception:
241
+ pass
242
+
243
+ if dlg.exec() != QDialog.DialogCode.Accepted:
244
+ return
245
+ factor = dlg.doubleValue()
246
+
247
+ try:
248
+ self._apply_geom_rescale_to_doc(doc, factor=factor)
249
+ self._last_rescale_factor = factor
250
+ self._log(f"Rescale ({factor:g}×) applied to active view")
251
+ except Exception as e:
252
+ QMessageBox.critical(self, self.tr("Rescale Image"), str(e))
253
+
254
+ # --- Geometry: headless apply-to-doc helpers ---
255
+
256
+ def _apply_geom_invert_to_doc(self, doc):
257
+ """Apply invert to document."""
258
+ arr = np.asarray(doc.image, dtype=np.float32)
259
+ out = invert_image_numba(arr)
260
+ if hasattr(doc, "set_image"):
261
+ doc.set_image(out, step_name="Invert")
262
+ else:
263
+ doc.image = out
264
+
265
+ def _apply_geom_flip_h_to_doc(self, doc):
266
+ """Apply horizontal flip to document with WCS update."""
267
+ arr = np.asarray(doc.image, dtype=np.float32)
268
+ h, w = arr.shape[:2]
269
+ out = flip_horizontal_numba(arr)
270
+
271
+ M = np.array([
272
+ [-1.0, 0.0, w - 1.0],
273
+ [0.0, 1.0, 0.0],
274
+ [0.0, 0.0, 1.0],
275
+ ], dtype=float)
276
+
277
+ self._apply_geom_with_wcs(doc, out, M_src_to_dst=M, step_name="Flip Horizontal")
278
+
279
+ def _apply_geom_flip_v_to_doc(self, doc):
280
+ """Apply vertical flip to document with WCS update."""
281
+ arr = np.asarray(doc.image, dtype=np.float32)
282
+ h, w = arr.shape[:2]
283
+ out = flip_vertical_numba(arr)
284
+
285
+ M = np.array([
286
+ [1.0, 0.0, 0.0],
287
+ [0.0, -1.0, h - 1.0],
288
+ [0.0, 0.0, 1.0],
289
+ ], dtype=float)
290
+
291
+ self._apply_geom_with_wcs(doc, out, M_src_to_dst=M, step_name="Flip Vertical")
292
+
293
+ def _apply_geom_rot_cw_to_doc(self, doc):
294
+ """Apply 90° clockwise rotation to document with WCS update."""
295
+ arr = np.asarray(doc.image, dtype=np.float32)
296
+ h, w = arr.shape[:2]
297
+ out = rotate_90_clockwise_numba(arr) # out shape: (w, h)
298
+
299
+ M = np.array([
300
+ [0.0, -1.0, h - 1.0],
301
+ [1.0, 0.0, 0.0],
302
+ [0.0, 0.0, 1.0],
303
+ ], dtype=float)
304
+
305
+ self._apply_geom_with_wcs(doc, out, M_src_to_dst=M, step_name="Rotate 90° Clockwise")
306
+
307
+ def _apply_geom_rot_ccw_to_doc(self, doc):
308
+ """Apply 90° counterclockwise rotation to document with WCS update."""
309
+ arr = np.asarray(doc.image, dtype=np.float32)
310
+ h, w = arr.shape[:2]
311
+ out = rotate_90_counterclockwise_numba(arr) # out shape: (w, h)
312
+
313
+ M = np.array([
314
+ [0.0, 1.0, 0.0],
315
+ [-1.0, 0.0, w - 1.0],
316
+ [0.0, 0.0, 1.0],
317
+ ], dtype=float)
318
+
319
+ self._apply_geom_with_wcs(doc, out, M_src_to_dst=M, step_name="Rotate 90° Counterclockwise")
320
+
321
+ def _apply_geom_rot_180_to_doc(self, doc):
322
+ """Apply 180° rotation to document with WCS update."""
323
+ arr = np.asarray(doc.image, dtype=np.float32)
324
+ h, w = arr.shape[:2]
325
+ out = rotate_180_numba(arr) # out shape: (h, w)
326
+
327
+ # 180° rotation around the image center:
328
+ # (x, y) -> (w-1 - x, h-1 - y)
329
+ M = np.array([
330
+ [-1.0, 0.0, w - 1.0],
331
+ [0.0, -1.0, h - 1.0],
332
+ [0.0, 0.0, 1.0],
333
+ ], dtype=float)
334
+
335
+ self._apply_geom_with_wcs(doc, out, M_src_to_dst=M, step_name="Rotate 180°")
336
+
337
+ def _apply_geom_rescale_to_doc(self, doc, *, factor: float):
338
+ """Apply rescale to document with WCS update."""
339
+ factor = float(max(0.1, min(10.0, factor)))
340
+ arr = np.asarray(doc.image, dtype=np.float32)
341
+ h, w = arr.shape[:2]
342
+ out = rescale_image_numba(arr, factor)
343
+
344
+ M = np.array([
345
+ [factor, 0.0, 0.0],
346
+ [0.0, factor, 0.0],
347
+ [0.0, 0.0, 1.0],
348
+ ], dtype=float)
349
+
350
+ self._apply_geom_with_wcs(doc, out, M_src_to_dst=M,
351
+ step_name=f"Rescale ({factor:g}×)")
352
+
353
+ def _apply_geom_rescale_preset_to_doc(self, doc, preset):
354
+ """
355
+ Accepts flexible presets:
356
+ - dict with 'factor' or 'scale'
357
+ - a lone float/int
358
+ - a '0.5x'/'2x' string
359
+ - (factor, ...) tuple/list
360
+ Falls back to 1.0 if unparsable.
361
+ """
362
+ factor = None
363
+ try:
364
+ if isinstance(preset, dict):
365
+ factor = preset.get("factor", preset.get("scale", None))
366
+ elif isinstance(preset, (float, int)):
367
+ factor = float(preset)
368
+ elif isinstance(preset, str):
369
+ s = preset.strip().lower().replace("×", "x")
370
+ if s.endswith("x"):
371
+ s = s[:-1]
372
+ factor = float(s)
373
+ elif isinstance(preset, (list, tuple)) and preset:
374
+ factor = float(preset[0])
375
+ except Exception:
376
+ factor = None
377
+
378
+ if factor is None:
379
+ factor = getattr(self, "_last_rescale_factor", 1.0) or 1.0
380
+
381
+ self._apply_geom_rescale_to_doc(doc, factor=factor)
382
+
383
+ def _apply_rescale_preset_to_doc(self, doc, preset: dict):
384
+ """
385
+ Headless rescale for drag-and-drop / shortcut preset application.
386
+ Expects preset like {"factor": 1.25}.
387
+ """
388
+ factor = float(preset.get("factor", 1.0))
389
+ if not (0.10 <= factor <= 10.0):
390
+ raise ValueError("Rescale factor must be between 0.10 and 10.0")
391
+
392
+ if getattr(doc, "image", None) is None:
393
+ raise RuntimeError("Target document has no image")
394
+
395
+ src = np.asarray(doc.image, dtype=np.float32, order="C")
396
+ out = rescale_image_numba(src, factor)
397
+
398
+ if hasattr(doc, "set_image"):
399
+ doc.set_image(out, step_name=f"Rescale ×{factor:.2f}")
400
+ elif hasattr(doc, "apply_numpy"):
401
+ doc.apply_numpy(out, step_name=f"Rescale ×{factor:.2f}")
402
+ else:
403
+ doc.image = out