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,507 @@
1
+ # pro/star_stretch.py
2
+ from __future__ import annotations
3
+ import os
4
+ import numpy as np
5
+
6
+ from PyQt6.QtCore import Qt, QThread, pyqtSignal, QEvent, QPointF
7
+ from PyQt6.QtWidgets import (
8
+ QDialog, QVBoxLayout, QHBoxLayout, QLabel, QSlider, QCheckBox,
9
+ QPushButton, QScrollArea, QWidget, QMessageBox
10
+ )
11
+ from PyQt6.QtGui import QPixmap, QImage, QMovie
12
+ from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
13
+
14
+ # Shared utilities
15
+ from setiastro.saspro.widgets.image_utils import to_float01 as _to_float01
16
+
17
+ # --- use your Numba kernels; fall back to pure numpy SCNR if needed ----
18
+ try:
19
+ from setiastro.saspro.legacy.numba_utils import applyPixelMath_numba, applySCNR_numba
20
+ _HAS_NUMBA = True
21
+ except Exception:
22
+ _HAS_NUMBA = False
23
+ # Fallback SCNR (Average Neutral) if legacy.numba_utils is unavailable
24
+ def applySCNR_numba(image_array: np.ndarray) -> np.ndarray:
25
+ img = image_array.astype(np.float32, copy=False)
26
+ if img.ndim != 3 or img.shape[2] != 3:
27
+ return img
28
+ r = img[..., 0]; g = img[..., 1]; b = img[..., 2]
29
+ g2 = np.minimum(g, 0.5 * (r + b))
30
+ out = img.copy()
31
+ out[..., 1] = g2
32
+ return np.clip(out, 0.0, 1.0)
33
+
34
+ # ---- small helpers --------------------------------------------------------
35
+
36
+ def _as_qimage_rgb8(float01: np.ndarray) -> QImage:
37
+ f = np.asarray(float01, dtype=np.float32)
38
+
39
+ # Ensure 3-channel RGB for preview
40
+ if f.ndim == 2:
41
+ f = np.stack([f]*3, axis=-1)
42
+ elif f.ndim == 3 and f.shape[2] == 1:
43
+ f = np.repeat(f, 3, axis=2)
44
+
45
+ # [0,1] -> uint8 and force C-contiguous
46
+ buf8 = (np.clip(f, 0.0, 1.0) * 255.0).astype(np.uint8, copy=False)
47
+ buf8 = np.ascontiguousarray(buf8)
48
+ h, w, _ = buf8.shape
49
+ bpl = int(buf8.strides[0])
50
+
51
+ # Prefer zero-copy via sip pointer if available; fall back to bytes
52
+ try:
53
+ from PyQt6 import sip
54
+ qimg = QImage(sip.voidptr(buf8.ctypes.data), w, h, bpl, QImage.Format.Format_RGB888)
55
+ qimg._keepalive = buf8 # keep numpy alive while qimg exists
56
+ return qimg.copy() # detach so Qt owns the pixels (safe for QPixmap.fromImage)
57
+ except Exception:
58
+ data = buf8.tobytes()
59
+ qimg = QImage(data, w, h, bpl, QImage.Format.Format_RGB888)
60
+ return qimg.copy() # detach to avoid lifetime issues
61
+
62
+ def _saturation_boost(rgb01: np.ndarray, amount: float) -> np.ndarray:
63
+ """
64
+ Fast saturation-like boost without HSV dependency:
65
+ C' = mean + (C - mean) * amount
66
+ """
67
+ if rgb01.ndim != 3 or rgb01.shape[2] != 3:
68
+ return rgb01
69
+ mean = rgb01.mean(axis=2, keepdims=True)
70
+ out = mean + (rgb01 - mean) * float(amount)
71
+ return np.clip(out, 0.0, 1.0)
72
+
73
+ # ---- background thread ----------------------------------------------------
74
+
75
+ class _StarStretchWorker(QThread):
76
+ preview_ready = pyqtSignal(object) # np.ndarray float32 0..1
77
+
78
+ def __init__(self, image: np.ndarray, stretch_factor: float, sat_amount: float, do_scnr: bool):
79
+ super().__init__()
80
+ self.image = image
81
+ self.stretch_factor = float(stretch_factor) # this is the "amount" for your pixel math
82
+ self.sat_amount = float(sat_amount)
83
+ self.do_scnr = bool(do_scnr)
84
+
85
+ def run(self):
86
+ imgf = _to_float01(self.image)
87
+ if imgf is None:
88
+ return
89
+
90
+ # If grayscale, make it 3-channel to keep the kernels happy, then restore shape
91
+ orig_ndim = imgf.ndim
92
+ need_collapse = False
93
+ if imgf.ndim == 2:
94
+ imgf = np.stack([imgf]*3, axis=-1)
95
+ need_collapse = True
96
+ elif imgf.ndim == 3 and imgf.shape[2] == 1:
97
+ imgf = np.repeat(imgf, 3, axis=2)
98
+ need_collapse = True
99
+
100
+ # --- Star Stretch: your Numba pixel math ---
101
+ # amount maps to the SASv2 slider (0..8); kernel uses: f=3**amount
102
+ out = applyPixelMath_numba(imgf.astype(np.float32, copy=False), self.stretch_factor)
103
+
104
+ # --- Optional saturation (RGB only) ---
105
+ if out.ndim == 3 and out.shape[2] == 3 and abs(self.sat_amount - 1.0) > 1e-6:
106
+ out = _saturation_boost(out, self.sat_amount)
107
+
108
+ # --- Optional SCNR (Average Neutral via your Numba kernel) ---
109
+ if self.do_scnr and out.ndim == 3 and out.shape[2] == 3:
110
+ out = applySCNR_numba(out.astype(np.float32, copy=False))
111
+
112
+ # collapse back to mono if we expanded earlier
113
+ if need_collapse:
114
+ out = out[..., 0]
115
+
116
+ self.preview_ready.emit(out.astype(np.float32, copy=False))
117
+
118
+ # ---- dialog ---------------------------------------------------------------
119
+
120
+ class StarStretchDialog(QDialog):
121
+ """
122
+ Star Stretch for SASpro.
123
+ - Works on active ImageDocument (passed in).
124
+ - Preview is computed in background thread.
125
+ - 'Apply to Document' records history via doc.apply_edit(..., step_name="Star Stretch").
126
+ """
127
+ def __init__(self, parent, document):
128
+ super().__init__(parent)
129
+ self.setWindowTitle(self.tr("Star Stretch"))
130
+ self.setWindowFlag(Qt.WindowType.Window, True)
131
+ self.setWindowModality(Qt.WindowModality.NonModal)
132
+ self.setModal(False)
133
+ self._main = parent
134
+ self.doc = document
135
+ self._preview: np.ndarray | None = None
136
+
137
+ # Connect to active document change signal
138
+ if hasattr(self._main, "currentDocumentChanged"):
139
+ self._main.currentDocumentChanged.connect(self._on_active_doc_changed)
140
+ self._pix: QPixmap | None = None
141
+ self._zoom = 0.25
142
+ self._panning = False
143
+ self._pan_start = QPointF()
144
+ self._apply_when_ready = False
145
+
146
+ # UI
147
+ main = QHBoxLayout(self)
148
+
149
+ # Left column (controls)
150
+ left = QVBoxLayout()
151
+ info = QLabel(
152
+ "Instructions:\n"
153
+ "1) Adjust stretch and options.\n"
154
+ "2) Preview the result.\n"
155
+ "3) Apply to the current document."
156
+ )
157
+ info.setWordWrap(True)
158
+ left.addWidget(info)
159
+
160
+ # Stretch slider (0..8.00)
161
+ self.lbl_st = QLabel(self.tr("Stretch Amount:") + " 5.00")
162
+ self.sld_st = QSlider(Qt.Orientation.Horizontal)
163
+ self.sld_st.setRange(0, 800)
164
+ self.sld_st.setValue(500)
165
+ self.sld_st.valueChanged.connect(self._on_stretch_changed)
166
+ left.addWidget(self.lbl_st)
167
+ left.addWidget(self.sld_st)
168
+
169
+ # Saturation slider (0..2.00)
170
+ self.lbl_sat = QLabel(self.tr("Color Boost:") + " 1.00")
171
+ self.sld_sat = QSlider(Qt.Orientation.Horizontal)
172
+ self.sld_sat.setRange(0, 200)
173
+ self.sld_sat.setValue(100)
174
+ self.sld_sat.valueChanged.connect(self._on_sat_changed)
175
+ left.addWidget(self.lbl_sat)
176
+ left.addWidget(self.sld_sat)
177
+
178
+ # SCNR checkbox
179
+ self.chk_scnr = QCheckBox(self.tr("Remove Green via SCNR (Optional)"))
180
+ left.addWidget(self.chk_scnr)
181
+
182
+ # Buttons row
183
+ rowb = QHBoxLayout()
184
+ self.btn_preview = QPushButton(self.tr("Preview"))
185
+ self.btn_apply = QPushButton(self.tr("Apply to Document"))
186
+ rowb.addWidget(self.btn_preview)
187
+ rowb.addWidget(self.btn_apply)
188
+ left.addLayout(rowb)
189
+
190
+ # Spinner
191
+ self.lbl_spin = QLabel()
192
+ self.lbl_spin.setAlignment(Qt.AlignmentFlag.AlignCenter)
193
+ self.lbl_spin.hide()
194
+ spinner_gif = _guess_spinner_path()
195
+ if spinner_gif and os.path.exists(spinner_gif):
196
+ mv = QMovie(spinner_gif)
197
+ self.lbl_spin.setMovie(mv)
198
+ self._spinner = mv
199
+ else:
200
+ self._spinner = None
201
+ left.addWidget(self.lbl_spin)
202
+
203
+ left.addStretch(1)
204
+ main.addLayout(left, 0)
205
+
206
+ # Right column (preview with zoom/pan)
207
+ right = QVBoxLayout()
208
+ zoombar = QHBoxLayout()
209
+ b_out = QPushButton(self.tr("Zoom Out"))
210
+ b_in = QPushButton(self.tr("Zoom In"))
211
+ b_fit = QPushButton(self.tr("Fit to Preview"))
212
+ b_out.clicked.connect(self._zoom_out)
213
+ b_in.clicked.connect(self._zoom_in)
214
+ b_fit.clicked.connect(self._fit)
215
+ zoombar.addWidget(b_out); zoombar.addWidget(b_in); zoombar.addWidget(b_fit)
216
+ right.addLayout(zoombar)
217
+
218
+ self.scroll = QScrollArea()
219
+ self.scroll.setWidgetResizable(True)
220
+ self.scroll.setAlignment(Qt.AlignmentFlag.AlignCenter)
221
+ self.scroll.viewport().installEventFilter(self)
222
+
223
+ self.label = QLabel(alignment=Qt.AlignmentFlag.AlignCenter)
224
+ self.scroll.setWidget(self.label)
225
+
226
+ right.addWidget(self.scroll, 1)
227
+ main.addLayout(right, 1)
228
+
229
+ # signals
230
+ self.btn_preview.clicked.connect(self._run_preview)
231
+ self.btn_apply.clicked.connect(self._apply_to_doc)
232
+
233
+ # initialize preview with current doc image
234
+ self._update_preview_pix(self.doc.image)
235
+
236
+ # --- active document change ---
237
+ def _on_active_doc_changed(self, doc):
238
+ """Called when user clicks a different image window."""
239
+ if doc is None or getattr(doc, "image", None) is None:
240
+ return
241
+ self.doc = doc
242
+ self._preview = None
243
+ self._update_preview_pix(self.doc.image)
244
+
245
+ # --- UI change handlers ---
246
+ def _on_stretch_changed(self, v: int):
247
+ self.lbl_st.setText(f"Stretch Amount: {v/100.0:.2f}")
248
+
249
+ def _on_sat_changed(self, v: int):
250
+ self.lbl_sat.setText(f"Color Boost: {v/100.0:.2f}")
251
+
252
+ # --- preview / processing ---
253
+ def _run_preview(self):
254
+ img = self.doc.image
255
+ if img is None:
256
+ QMessageBox.information(self, "No image", "Open an image first.")
257
+ return
258
+ self._show_spinner(True)
259
+ self.btn_preview.setEnabled(False)
260
+ self.btn_apply.setEnabled(False)
261
+
262
+ self._thr = _StarStretchWorker(
263
+ image=img,
264
+ stretch_factor=self.sld_st.value()/100.0,
265
+ sat_amount=self.sld_sat.value()/100.0,
266
+ do_scnr=self.chk_scnr.isChecked()
267
+ )
268
+ self._thr.preview_ready.connect(self._on_preview_ready)
269
+ self._thr.finished.connect(lambda: self._show_spinner(False))
270
+ self._thr.start()
271
+
272
+ def _on_preview_ready(self, out: np.ndarray):
273
+ out_masked = self._blend_with_mask(out)
274
+ self._preview = out_masked
275
+ self.btn_preview.setEnabled(True)
276
+ self.btn_apply.setEnabled(True)
277
+ self._update_preview_pix(out_masked)
278
+
279
+ mw = self._find_main_window()
280
+ if mw and hasattr(mw, "_log"):
281
+ mw._log("Star Stretch: preview generated.")
282
+
283
+ # NEW: if Apply was pressed before preview completed, finish now.
284
+ if self._apply_when_ready:
285
+ self._apply_when_ready = False
286
+ self._finish_apply()
287
+
288
+ def _apply_to_doc(self):
289
+ # If we don't have a preview yet, compute it and auto-apply when ready.
290
+ if self._preview is None:
291
+ if getattr(self, "_thr", None) and self._thr.isRunning():
292
+ # already computing; just mark to apply when it lands
293
+ self._apply_when_ready = True
294
+ return
295
+ self._apply_when_ready = True
296
+ self._run_preview()
297
+ return
298
+
299
+ # We do have a preview → finish immediately
300
+ self._finish_apply()
301
+
302
+ def _finish_apply(self):
303
+ try:
304
+ _marr, mid, mname = self._active_mask_layer()
305
+ meta = {
306
+ "step_name": "Star Stretch",
307
+ "star_stretch": {
308
+ "stretch_factor": self.sld_st.value()/100.0,
309
+ "color_boost": self.sld_sat.value()/100.0,
310
+ "scnr_green": self.chk_scnr.isChecked(),
311
+ "numba": _HAS_NUMBA,
312
+ },
313
+ # ✅ mask bookkeeping
314
+ "masked": bool(mid),
315
+ "mask_id": mid,
316
+ "mask_name": mname,
317
+ "mask_blend": "m*out + (1-m)*src",
318
+ }
319
+ self.doc.apply_edit(self._preview.copy(), metadata=meta, step_name="Star Stretch")
320
+
321
+ mw = self._find_main_window()
322
+ if mw and hasattr(mw, "_log"):
323
+ mw._log("Star Stretch: applied to document.")
324
+
325
+ # 🔁 Record as last headless-style command for Replay
326
+ try:
327
+ if mw and hasattr(mw, "_remember_last_headless_command"):
328
+ preset = {
329
+ "stretch_factor": self.sld_st.value()/100.0,
330
+ "color_boost": self.sld_sat.value()/100.0,
331
+ "scnr_green": self.chk_scnr.isChecked(),
332
+ }
333
+ mw._remember_last_headless_command(
334
+ "star_stretch",
335
+ preset,
336
+ description="Star Stretch",
337
+ )
338
+ except Exception:
339
+ # Don't let replay bookkeeping break the dialog
340
+ pass
341
+
342
+ except Exception as e:
343
+ QMessageBox.critical(self, "Apply failed", str(e))
344
+ return
345
+
346
+ # Dialog stays open so user can apply to other images
347
+ # Refresh document reference for next operation
348
+ self._refresh_document_from_active()
349
+
350
+ def _refresh_document_from_active(self):
351
+ """
352
+ Refresh the dialog's document reference to the currently active document.
353
+ This allows reusing the same dialog on different images.
354
+ """
355
+ try:
356
+ main = self._find_main_window()
357
+ if main and hasattr(main, "_active_doc"):
358
+ new_doc = main._active_doc()
359
+ if new_doc is not None and new_doc is not self.doc:
360
+ self.doc = new_doc
361
+ # Reset preview for new document
362
+ self._preview = None
363
+ self._compute_and_show_preview()
364
+ except Exception:
365
+ pass
366
+
367
+
368
+ # --- preview rendering ---
369
+ def _update_preview_pix(self, img: np.ndarray | None):
370
+ if img is None:
371
+ self.label.clear(); self._pix = None; return
372
+ qimg = _as_qimage_rgb8(_to_float01(img))
373
+ pm = QPixmap.fromImage(qimg)
374
+ self._pix = pm
375
+ self._apply_zoom()
376
+
377
+ def _apply_zoom(self):
378
+ if self._pix is None:
379
+ return
380
+ scaled = self._pix.scaled(self._pix.size()*self._zoom,
381
+ Qt.AspectRatioMode.KeepAspectRatio,
382
+ Qt.TransformationMode.SmoothTransformation)
383
+ self.label.setPixmap(scaled)
384
+ self.label.resize(scaled.size())
385
+
386
+ # --- zoom/pan ---
387
+ def _zoom_in(self): self._set_zoom(self._zoom * 1.25)
388
+ def _zoom_out(self): self._set_zoom(self._zoom / 1.25)
389
+ def _fit(self):
390
+ if self._pix is None: return
391
+ vp = self.scroll.viewport().size()
392
+ if self._pix.width()==0 or self._pix.height()==0: return
393
+ s = min(vp.width()/self._pix.width(), vp.height()/self._pix.height())
394
+ self._set_zoom(max(0.05, s))
395
+
396
+ def _set_zoom(self, z: float):
397
+ self._zoom = float(max(0.05, min(z, 8.0)))
398
+ self._apply_zoom()
399
+
400
+ # --- spinner ---
401
+ def _show_spinner(self, on: bool):
402
+ if self._spinner is None:
403
+ self.lbl_spin.setVisible(on)
404
+ return
405
+ if on:
406
+ self.lbl_spin.show(); self._spinner.start()
407
+ else:
408
+ self._spinner.stop(); self.lbl_spin.hide()
409
+
410
+ # --- event filter (wheel zoom + panning) ---
411
+ def eventFilter(self, obj, ev):
412
+ if obj is self.scroll.viewport():
413
+ if ev.type() == QEvent.Type.Wheel and (ev.modifiers() & Qt.KeyboardModifier.ControlModifier):
414
+ self._set_zoom(self._zoom * (1.25 if ev.angleDelta().y() > 0 else 0.8))
415
+ ev.accept(); return True
416
+ if ev.type() == QEvent.Type.MouseButtonPress and ev.button() == Qt.MouseButton.LeftButton:
417
+ self._panning = True; self._pan_start = ev.position()
418
+ self.scroll.viewport().setCursor(Qt.CursorShape.ClosedHandCursor)
419
+ ev.accept(); return True
420
+ if ev.type() == QEvent.Type.MouseMove and self._panning:
421
+ d = ev.position() - self._pan_start
422
+ h = self.scroll.horizontalScrollBar(); v = self.scroll.verticalScrollBar()
423
+ h.setValue(h.value() - int(d.x())); v.setValue(v.value() - int(d.y()))
424
+ self._pan_start = ev.position()
425
+ ev.accept(); return True
426
+ if ev.type() == QEvent.Type.MouseButtonRelease and ev.button() == Qt.MouseButton.LeftButton:
427
+ self._panning = False
428
+ self.scroll.viewport().setCursor(Qt.CursorShape.ArrowCursor)
429
+ ev.accept(); return True
430
+ return super().eventFilter(obj, ev)
431
+
432
+ # --- helper ---
433
+ def _find_main_window(self):
434
+ p = self.parent()
435
+ while p is not None and not hasattr(p, "docman"):
436
+ p = p.parent()
437
+ return p
438
+
439
+ # --- mask helpers ---------------------------------------------------
440
+ def _active_mask_layer(self):
441
+ """Return (mask_array_float01, mask_id, mask_name) or (None, None, None)."""
442
+ doc = self.doc
443
+ mid = getattr(doc, "active_mask_id", None)
444
+ if not mid:
445
+ return None, None, None
446
+ layer = getattr(doc, "masks", {}).get(mid)
447
+ if layer is None:
448
+ return None, None, None
449
+ m = np.asarray(getattr(layer, "data", None), dtype=np.float32)
450
+ if m is None or m.size == 0:
451
+ return None, None, None
452
+ # ensure [0..1]
453
+ if m.dtype.kind in "ui":
454
+ m = m / float(np.iinfo(m.dtype).max)
455
+ else:
456
+ mx = float(m.max()) if m.size else 1.0
457
+ if mx > 1.0:
458
+ m = m / mx
459
+ m = np.clip(m, 0.0, 1.0)
460
+ return m, mid, getattr(layer, "name", "Mask")
461
+
462
+ def _resample_mask_if_needed(self, mask: np.ndarray, out_hw: tuple[int,int]) -> np.ndarray:
463
+ """Nearest-neighbor resize using integer indexing (fast, dependency-free)."""
464
+ mh, mw = mask.shape[:2]
465
+ th, tw = out_hw
466
+ if (mh, mw) == (th, tw):
467
+ return mask
468
+ yi = np.linspace(0, mh - 1, th).astype(np.int32)
469
+ xi = np.linspace(0, mw - 1, tw).astype(np.int32)
470
+ return mask[yi][:, xi]
471
+
472
+ def _blend_with_mask(self, stretched: np.ndarray) -> np.ndarray:
473
+ """Blend preview/apply with original using active mask if present."""
474
+ mask, _mid, _name = self._active_mask_layer()
475
+ if mask is None:
476
+ return stretched
477
+ src = _to_float01(self.doc.image)
478
+ out = stretched.astype(np.float32, copy=False)
479
+
480
+ # Make sure spatial size matches mask
481
+ th, tw = out.shape[:2]
482
+ m = self._resample_mask_if_needed(mask, (th, tw))
483
+
484
+ # Broadcast mask to 3ch when needed
485
+ if out.ndim == 3 and out.shape[2] == 3:
486
+ m = m[..., None]
487
+
488
+ # If preview changed mono↔RGB shape, match src first
489
+ if src.ndim == 2 and out.ndim == 3 and out.shape[2] == 3:
490
+ src = np.stack([src]*3, axis=-1)
491
+ elif src.ndim == 3 and src.shape[2] == 3 and out.ndim == 2:
492
+ src = src[..., 0] # collapse to mono
493
+
494
+ return (m * out + (1.0 - m) * src).astype(np.float32, copy=False)
495
+
496
+
497
+ def _guess_spinner_path() -> str | None:
498
+ here = os.path.dirname(__file__)
499
+ cands = [
500
+ os.path.join(here, "spinner.gif"),
501
+ os.path.join(os.path.dirname(here), "spinner.gif"),
502
+ os.path.join(os.getcwd(), "spinner.gif"),
503
+ ]
504
+ for c in cands:
505
+ if os.path.exists(c):
506
+ return c
507
+ return None