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,518 @@
1
+ # pro/widgets/image_utils.py
2
+ """
3
+ Centralized image conversion utilities for Seti Astro Suite Pro.
4
+
5
+ Provides common numpy <-> QImage conversion functions.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import numpy as np
10
+ from PyQt6.QtGui import QImage, QPixmap
11
+
12
+
13
+ def ensure_contiguous(arr: np.ndarray) -> np.ndarray:
14
+ """
15
+ Ensure array is C-contiguous without unnecessary copy.
16
+
17
+ This is an optimization over np.ascontiguousarray()
18
+ which always copies if the array is not contiguous.
19
+
20
+ Args:
21
+ arr: Input array
22
+
23
+ Returns:
24
+ C-contiguous array (same object if already contiguous)
25
+ """
26
+ if arr is None:
27
+ return arr
28
+ if arr.flags.c_contiguous:
29
+ return arr
30
+ return np.ascontiguousarray(arr)
31
+
32
+
33
+ def numpy_to_qimage(arr: np.ndarray, normalize: bool = True) -> QImage:
34
+ """
35
+ Convert a numpy array to QImage.
36
+
37
+ Args:
38
+ arr: Image array. Can be:
39
+ - 2D grayscale (H, W)
40
+ - 3D grayscale (H, W, 1)
41
+ - 3D RGB (H, W, 3)
42
+ - float32 [0, 1] or uint8 [0, 255]
43
+ normalize: If True, clip and scale float arrays to [0, 1]
44
+
45
+ Returns:
46
+ QImage in appropriate format
47
+ """
48
+ if arr is None:
49
+ raise ValueError("Input array is None")
50
+
51
+ arr = ensure_contiguous(arr)
52
+
53
+ # Handle float vs uint8
54
+ if arr.dtype in (np.float32, np.float64):
55
+ if normalize:
56
+ arr = np.clip(arr, 0.0, 1.0)
57
+ arr = (arr * 255).astype(np.uint8)
58
+ elif arr.dtype != np.uint8:
59
+ # Convert other integer types
60
+ arr = arr.astype(np.uint8)
61
+
62
+ # Ensure contiguous
63
+ arr = ensure_contiguous(arr)
64
+
65
+ # Handle dimensions
66
+ if arr.ndim == 2:
67
+ # Grayscale
68
+ h, w = arr.shape
69
+ return QImage(arr.data, w, h, w, QImage.Format.Format_Grayscale8).copy()
70
+
71
+ elif arr.ndim == 3:
72
+ h, w, c = arr.shape
73
+
74
+ if c == 1:
75
+ # Grayscale with channel dim
76
+ arr = arr.squeeze()
77
+ return QImage(arr.data, w, h, w, QImage.Format.Format_Grayscale8).copy()
78
+
79
+ elif c == 3:
80
+ # RGB
81
+ bytes_per_line = 3 * w
82
+ return QImage(arr.data, w, h, bytes_per_line, QImage.Format.Format_RGB888).copy()
83
+
84
+ elif c == 4:
85
+ # RGBA
86
+ bytes_per_line = 4 * w
87
+ return QImage(arr.data, w, h, bytes_per_line, QImage.Format.Format_RGBA8888).copy()
88
+
89
+ else:
90
+ raise ValueError(f"Unsupported number of channels: {c}")
91
+
92
+ else:
93
+ raise ValueError(f"Unsupported array dimensions: {arr.ndim}")
94
+
95
+
96
+ def numpy_to_qpixmap(arr: np.ndarray, normalize: bool = True) -> QPixmap:
97
+ """
98
+ Convert a numpy array to QPixmap.
99
+
100
+ Args:
101
+ arr: Image array (see numpy_to_qimage for formats)
102
+ normalize: If True, clip and scale float arrays to [0, 1]
103
+
104
+ Returns:
105
+ QPixmap
106
+ """
107
+ return QPixmap.fromImage(numpy_to_qimage(arr, normalize))
108
+
109
+
110
+ def float_to_qimage_rgb8(arr: np.ndarray) -> QImage:
111
+ """
112
+ Convert float32 [0, 1] array to QImage RGB888 format.
113
+
114
+ This is a shared implementation replacing duplicates in:
115
+ - pro/pixelmath.py (_float_to_qimage_rgb8)
116
+ - pro/curve_editor_pro.py (_float_to_qimage_rgb8)
117
+
118
+ Args:
119
+ arr: float32 array in [0, 1], can be:
120
+ - 2D grayscale (H, W) - expanded to RGB
121
+ - 3D with 1 channel (H, W, 1) - expanded to RGB
122
+ - 3D RGB (H, W, 3)
123
+
124
+ Returns:
125
+ QImage in RGB888 format
126
+ """
127
+ f = np.asarray(arr, dtype=np.float32)
128
+ if f.ndim == 2:
129
+ f = np.stack([f, f, f], axis=-1)
130
+ elif f.ndim == 3 and f.shape[2] == 1:
131
+ f = np.repeat(f, 3, axis=2)
132
+
133
+ buf8 = (np.clip(f, 0.0, 1.0) * 255.0 + 0.5).astype(np.uint8)
134
+ buf8 = ensure_contiguous(buf8)
135
+ h, w, _ = buf8.shape
136
+ img = QImage(buf8.data, w, h, 3 * w, QImage.Format.Format_RGB888)
137
+ # Keep reference so bytes stay alive
138
+ img._buf = buf8
139
+ return img
140
+
141
+
142
+ def qimage_to_numpy(qimg: QImage) -> np.ndarray:
143
+ """
144
+ Convert a QImage to numpy array.
145
+
146
+ Args:
147
+ qimg: QImage to convert
148
+
149
+ Returns:
150
+ numpy array in RGB format (H, W, 3) or grayscale (H, W)
151
+ """
152
+ # Convert to a standard format
153
+ fmt = qimg.format()
154
+
155
+ if fmt == QImage.Format.Format_Grayscale8:
156
+ w, h = qimg.width(), qimg.height()
157
+ ptr = qimg.bits()
158
+ ptr.setsize(h * w)
159
+ arr = np.frombuffer(ptr, dtype=np.uint8).reshape((h, w))
160
+ return arr.copy()
161
+
162
+ # Convert to RGB888 for other formats
163
+ if fmt != QImage.Format.Format_RGB888:
164
+ qimg = qimg.convertToFormat(QImage.Format.Format_RGB888)
165
+
166
+ w, h = qimg.width(), qimg.height()
167
+ ptr = qimg.bits()
168
+ ptr.setsize(h * w * 3)
169
+ arr = np.frombuffer(ptr, dtype=np.uint8).reshape((h, w, 3))
170
+ return arr.copy()
171
+
172
+
173
+ def create_preview_image(arr: np.ndarray, max_size: int = 1024) -> np.ndarray:
174
+ """
175
+ Create a preview-sized version of an image.
176
+
177
+ Args:
178
+ arr: Full resolution image array
179
+ max_size: Maximum dimension for preview
180
+
181
+ Returns:
182
+ Downscaled image if needed, original otherwise
183
+ """
184
+ if arr is None:
185
+ return arr
186
+
187
+ h = arr.shape[0]
188
+ w = arr.shape[1]
189
+
190
+ if max(h, w) <= max_size:
191
+ return arr
192
+
193
+ # Calculate scale factor
194
+ scale = max_size / max(h, w)
195
+ new_h = int(h * scale)
196
+ new_w = int(w * scale)
197
+
198
+ # Use simple slicing for speed (subsampling)
199
+ step_h = max(1, h // new_h)
200
+ step_w = max(1, w // new_w)
201
+
202
+ if arr.ndim == 2:
203
+ return arr[::step_h, ::step_w]
204
+ else:
205
+ return arr[::step_h, ::step_w, :]
206
+
207
+
208
+ def normalize_image(arr: np.ndarray, target_max: float = 1.0) -> np.ndarray:
209
+ """
210
+ Normalize image to [0, target_max] range.
211
+
212
+ Args:
213
+ arr: Image array
214
+ target_max: Maximum value after normalization
215
+
216
+ Returns:
217
+ Normalized float32 array
218
+ """
219
+ arr = np.asarray(arr, dtype=np.float32)
220
+ vmin = np.nanmin(arr)
221
+ vmax = np.nanmax(arr)
222
+
223
+ if vmax > vmin:
224
+ arr = (arr - vmin) / (vmax - vmin) * target_max
225
+ else:
226
+ arr = np.zeros_like(arr)
227
+
228
+ return arr
229
+
230
+
231
+ # ---------------------------------------------------------------------------
232
+ # Shared float normalization (replaces 10+ duplicate implementations)
233
+ # ---------------------------------------------------------------------------
234
+
235
+ def to_float01(arr: np.ndarray) -> np.ndarray:
236
+ """
237
+ Convert image to float32 in [0, 1] range.
238
+
239
+ Handles:
240
+ - uint8: divides by 255
241
+ - uint16: divides by 65535
242
+ - float: clips to [0, 1]
243
+ - Already float32 in [0, 1]: returns as-is
244
+
245
+ Args:
246
+ arr: Input image array
247
+
248
+ Returns:
249
+ float32 array normalized to [0, 1]
250
+ """
251
+ if arr is None:
252
+ return None
253
+ arr = np.asarray(arr)
254
+ if arr.dtype == np.uint8:
255
+ return arr.astype(np.float32) / 255.0
256
+ elif arr.dtype == np.uint16:
257
+ return arr.astype(np.float32) / 65535.0
258
+ else:
259
+ arr = arr.astype(np.float32, copy=False)
260
+ if arr.max() > 1.0 or arr.min() < 0.0:
261
+ return np.clip(arr, 0.0, 1.0)
262
+ return arr
263
+
264
+
265
+ def to_float01_strict(arr: np.ndarray) -> np.ndarray:
266
+ """
267
+ Strictly convert image to float32 in [0, 1] with explicit scaling.
268
+
269
+ Always clips output to ensure [0, 1] range.
270
+
271
+ Args:
272
+ arr: Input image array
273
+
274
+ Returns:
275
+ float32 array strictly in [0, 1]
276
+ """
277
+ if arr is None:
278
+ return None
279
+ arr = np.asarray(arr)
280
+ if arr.dtype == np.uint8:
281
+ return arr.astype(np.float32) / 255.0
282
+ elif arr.dtype == np.uint16:
283
+ return arr.astype(np.float32) / 65535.0
284
+ else:
285
+ return np.clip(arr.astype(np.float32), 0.0, 1.0)
286
+
287
+
288
+ def ensure_rgb(img: np.ndarray) -> np.ndarray:
289
+ """
290
+ Ensure image is 3-channel RGB format.
291
+
292
+ Args:
293
+ img: Input image (grayscale or RGB)
294
+
295
+ Returns:
296
+ 3-channel RGB array (H, W, 3)
297
+ """
298
+ if img is None:
299
+ return None
300
+ img = np.asarray(img)
301
+ if img.ndim == 2:
302
+ return np.stack([img, img, img], axis=-1)
303
+ elif img.ndim == 3 and img.shape[2] == 1:
304
+ return np.concatenate([img, img, img], axis=-1)
305
+ elif img.ndim == 3 and img.shape[2] == 3:
306
+ return img
307
+ elif img.ndim == 3 and img.shape[2] == 4:
308
+ return img[..., :3] # Drop alpha
309
+ else:
310
+ raise ValueError(f"Cannot convert shape {img.shape} to RGB")
311
+
312
+
313
+ def ensure_grayscale(img: np.ndarray) -> np.ndarray:
314
+ """
315
+ Ensure image is grayscale format.
316
+
317
+ Args:
318
+ img: Input image (grayscale or RGB)
319
+
320
+ Returns:
321
+ 2D grayscale array (H, W)
322
+ """
323
+ if img is None:
324
+ return None
325
+ img = np.asarray(img)
326
+ if img.ndim == 2:
327
+ return img
328
+ elif img.ndim == 3 and img.shape[2] == 1:
329
+ return img[..., 0]
330
+ elif img.ndim == 3 and img.shape[2] in (3, 4):
331
+ # Luminance weights: 0.2126 R + 0.7152 G + 0.0722 B
332
+ return (0.2126 * img[..., 0] + 0.7152 * img[..., 1] + 0.0722 * img[..., 2]).astype(img.dtype)
333
+ else:
334
+ raise ValueError(f"Cannot convert shape {img.shape} to grayscale")
335
+
336
+
337
+ # ---------------------------------------------------------------------------
338
+ # Mask extraction helper (replaces 4+ duplicate implementations)
339
+ # ---------------------------------------------------------------------------
340
+
341
+ try:
342
+ import cv2 as _cv2
343
+ except ImportError:
344
+ _cv2 = None
345
+
346
+
347
+ # ---------------------------------------------------------------------------
348
+ # Resize utilities (replaces 4+ duplicate implementations)
349
+ # ---------------------------------------------------------------------------
350
+
351
+ def nearest_resize_2d(m: np.ndarray, H: int, W: int) -> np.ndarray:
352
+ """
353
+ Resize a 2D array to (H, W) using nearest neighbor interpolation.
354
+
355
+ This is a shared implementation replacing duplicates in:
356
+ - pro/clahe.py
357
+ - pro/morphology.py
358
+ - pro/pixelmath.py
359
+ - pro/stacking_suite.py
360
+
361
+ Args:
362
+ m: 2D input array
363
+ H: Target height
364
+ W: Target width
365
+
366
+ Returns:
367
+ Resized float32 array (H, W)
368
+ """
369
+ m = np.asarray(m, dtype=np.float32)
370
+ if m.shape == (H, W):
371
+ return m
372
+ if _cv2 is not None:
373
+ try:
374
+ return _cv2.resize(m, (W, H), interpolation=_cv2.INTER_NEAREST)
375
+ except Exception:
376
+ pass
377
+ # Fallback without cv2
378
+ yi = np.linspace(0, m.shape[0] - 1, H).astype(np.int32)
379
+ xi = np.linspace(0, m.shape[1] - 1, W).astype(np.int32)
380
+ return m[yi][:, xi].astype(np.float32, copy=False)
381
+
382
+
383
+ def extract_mask_resized(doc, H: int, W: int) -> np.ndarray | None:
384
+ """
385
+ Extract active mask from document and resize to (H, W).
386
+
387
+ This is a shared implementation replacing duplicates in:
388
+ - pro/clahe.py (_get_active_mask_resized)
389
+ - pro/morphology.py (_get_active_mask_resized)
390
+
391
+ Args:
392
+ doc: Document object with active_mask_id and masks attributes
393
+ H: Target height
394
+ W: Target width
395
+
396
+ Returns:
397
+ Resized mask (H, W) float32 in [0, 1], or None if not found
398
+ """
399
+ if doc is None:
400
+ return None
401
+ mid = getattr(doc, "active_mask_id", None)
402
+ if not mid:
403
+ return None
404
+
405
+ masks = getattr(doc, "masks", {}) or {}
406
+ layer = masks.get(mid)
407
+ if layer is None:
408
+ return None
409
+
410
+ # Extract data from layer (object, dict, or raw ndarray)
411
+ data = None
412
+ for attr in ("data", "mask", "image", "array"):
413
+ if hasattr(layer, attr):
414
+ val = getattr(layer, attr)
415
+ if val is not None:
416
+ data = val
417
+ break
418
+ if data is None and isinstance(layer, dict):
419
+ for key in ("data", "mask", "image", "array"):
420
+ if key in layer and layer[key] is not None:
421
+ data = layer[key]
422
+ break
423
+ if data is None and isinstance(layer, np.ndarray):
424
+ data = layer
425
+ if data is None:
426
+ return None
427
+
428
+ m = np.asarray(data)
429
+ if m.ndim == 3: # collapse RGB(A) → gray
430
+ m = m.mean(axis=2)
431
+ m = m.astype(np.float32, copy=False)
432
+
433
+ # Normalize to [0, 1]
434
+ mx = float(m.max()) if m.size else 1.0
435
+ if mx > 1.0:
436
+ m = m / mx
437
+ m = np.clip(m, 0.0, 1.0)
438
+
439
+ return nearest_resize_2d(m, H, W)
440
+
441
+
442
+ def blend_with_mask(base: np.ndarray, out: np.ndarray, mask: np.ndarray) -> np.ndarray:
443
+ """
444
+ Blend base and out arrays using mask weights.
445
+
446
+ result = base * (1 - mask) + out * mask
447
+
448
+ This is a shared implementation replacing duplicates in:
449
+ - pro/morphology.py (_blend_with_mask)
450
+ - pro/ghs_preset.py (_blend_with_mask)
451
+
452
+ Args:
453
+ base: Original image (2D or 3D)
454
+ out: Processed image (2D or 3D)
455
+ mask: 2D mask in [0, 1] range
456
+
457
+ Returns:
458
+ Blended image in [0, 1]
459
+ """
460
+ base = np.asarray(base, dtype=np.float32)
461
+ out = np.asarray(out, dtype=np.float32)
462
+ mask = np.clip(np.asarray(mask, dtype=np.float32), 0.0, 1.0)
463
+
464
+ if out.ndim == 3:
465
+ # Ensure base is 3D
466
+ if base.ndim == 2:
467
+ base = base[:, :, None].repeat(out.shape[2], axis=2)
468
+ elif base.ndim == 3 and base.shape[2] == 1:
469
+ base = base.repeat(out.shape[2], axis=2)
470
+ # Expand mask to 3D
471
+ M = mask[:, :, None].repeat(out.shape[2], axis=2)
472
+ return np.clip(base * (1.0 - M) + out * M, 0.0, 1.0)
473
+
474
+ # 2D output
475
+ if base.ndim == 3 and base.shape[2] == 1:
476
+ base = base.squeeze(axis=2)
477
+ return np.clip(base * (1.0 - mask) + out * mask, 0.0, 1.0)
478
+
479
+
480
+ # ---------------------------------------------------------------------------
481
+ # Mask extraction helper (replaces 4+ duplicate implementations)
482
+ # ---------------------------------------------------------------------------
483
+
484
+ def extract_mask_from_document(doc) -> np.ndarray | None:
485
+ """
486
+ Extract active mask (H, W) float32 in [0, 1] from a document.
487
+
488
+ This is a shared implementation replacing duplicates in:
489
+ - pro/add_stars.py
490
+ - pro/remove_stars.py
491
+ - pro/remove_green.py
492
+ - pro/image_combine.py
493
+
494
+ Args:
495
+ doc: Document object with active_mask_id and masks attributes
496
+
497
+ Returns:
498
+ Mask array (H, W) float32 in [0, 1], or None if not found
499
+ """
500
+ try:
501
+ mid = getattr(doc, "active_mask_id", None)
502
+ if not mid:
503
+ return None
504
+ masks = getattr(doc, "masks", {}) or {}
505
+ layer = masks.get(mid)
506
+ data = getattr(layer, "data", None) if layer is not None else None
507
+ if data is None:
508
+ return None
509
+ a = np.asarray(data)
510
+ if a.ndim == 3:
511
+ if _cv2 is not None:
512
+ a = _cv2.cvtColor(a, _cv2.COLOR_BGR2GRAY)
513
+ else:
514
+ a = a.mean(axis=2)
515
+ a = a.astype(np.float32, copy=False)
516
+ return np.clip(a, 0.0, 1.0)
517
+ except Exception:
518
+ return None