setiastrosuitepro 1.6.1__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 (342) 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/HRDiagram.png +0 -0
  16. setiastro/images/LExtract.png +0 -0
  17. setiastro/images/LInsert.png +0 -0
  18. setiastro/images/Oxygenation-atm-2.svg.png +0 -0
  19. setiastro/images/RGB080604.png +0 -0
  20. setiastro/images/abeicon.png +0 -0
  21. setiastro/images/aberration.png +0 -0
  22. setiastro/images/andromedatry.png +0 -0
  23. setiastro/images/andromedatry_satellited.png +0 -0
  24. setiastro/images/annotated.png +0 -0
  25. setiastro/images/aperture.png +0 -0
  26. setiastro/images/astrosuite.ico +0 -0
  27. setiastro/images/astrosuite.png +0 -0
  28. setiastro/images/astrosuitepro.icns +0 -0
  29. setiastro/images/astrosuitepro.ico +0 -0
  30. setiastro/images/astrosuitepro.png +0 -0
  31. setiastro/images/background.png +0 -0
  32. setiastro/images/background2.png +0 -0
  33. setiastro/images/benchmark.png +0 -0
  34. setiastro/images/big_moon_stabilizer_timeline.png +0 -0
  35. setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
  36. setiastro/images/blaster.png +0 -0
  37. setiastro/images/blink.png +0 -0
  38. setiastro/images/clahe.png +0 -0
  39. setiastro/images/collage.png +0 -0
  40. setiastro/images/colorwheel.png +0 -0
  41. setiastro/images/contsub.png +0 -0
  42. setiastro/images/convo.png +0 -0
  43. setiastro/images/copyslot.png +0 -0
  44. setiastro/images/cosmic.png +0 -0
  45. setiastro/images/cosmicsat.png +0 -0
  46. setiastro/images/crop1.png +0 -0
  47. setiastro/images/cropicon.png +0 -0
  48. setiastro/images/curves.png +0 -0
  49. setiastro/images/cvs.png +0 -0
  50. setiastro/images/debayer.png +0 -0
  51. setiastro/images/denoise_cnn_custom.png +0 -0
  52. setiastro/images/denoise_cnn_graph.png +0 -0
  53. setiastro/images/disk.png +0 -0
  54. setiastro/images/dse.png +0 -0
  55. setiastro/images/exoicon.png +0 -0
  56. setiastro/images/eye.png +0 -0
  57. setiastro/images/fliphorizontal.png +0 -0
  58. setiastro/images/flipvertical.png +0 -0
  59. setiastro/images/font.png +0 -0
  60. setiastro/images/freqsep.png +0 -0
  61. setiastro/images/functionbundle.png +0 -0
  62. setiastro/images/graxpert.png +0 -0
  63. setiastro/images/green.png +0 -0
  64. setiastro/images/gridicon.png +0 -0
  65. setiastro/images/halo.png +0 -0
  66. setiastro/images/hdr.png +0 -0
  67. setiastro/images/histogram.png +0 -0
  68. setiastro/images/hubble.png +0 -0
  69. setiastro/images/imagecombine.png +0 -0
  70. setiastro/images/invert.png +0 -0
  71. setiastro/images/isophote.png +0 -0
  72. setiastro/images/isophote_demo_figure.png +0 -0
  73. setiastro/images/isophote_demo_image.png +0 -0
  74. setiastro/images/isophote_demo_model.png +0 -0
  75. setiastro/images/isophote_demo_residual.png +0 -0
  76. setiastro/images/jwstpupil.png +0 -0
  77. setiastro/images/linearfit.png +0 -0
  78. setiastro/images/livestacking.png +0 -0
  79. setiastro/images/mask.png +0 -0
  80. setiastro/images/maskapply.png +0 -0
  81. setiastro/images/maskcreate.png +0 -0
  82. setiastro/images/maskremove.png +0 -0
  83. setiastro/images/morpho.png +0 -0
  84. setiastro/images/mosaic.png +0 -0
  85. setiastro/images/multiscale_decomp.png +0 -0
  86. setiastro/images/nbtorgb.png +0 -0
  87. setiastro/images/neutral.png +0 -0
  88. setiastro/images/nuke.png +0 -0
  89. setiastro/images/openfile.png +0 -0
  90. setiastro/images/pedestal.png +0 -0
  91. setiastro/images/pen.png +0 -0
  92. setiastro/images/pixelmath.png +0 -0
  93. setiastro/images/platesolve.png +0 -0
  94. setiastro/images/ppp.png +0 -0
  95. setiastro/images/pro.png +0 -0
  96. setiastro/images/project.png +0 -0
  97. setiastro/images/psf.png +0 -0
  98. setiastro/images/redo.png +0 -0
  99. setiastro/images/redoicon.png +0 -0
  100. setiastro/images/rescale.png +0 -0
  101. setiastro/images/rgbalign.png +0 -0
  102. setiastro/images/rgbcombo.png +0 -0
  103. setiastro/images/rgbextract.png +0 -0
  104. setiastro/images/rotate180.png +0 -0
  105. setiastro/images/rotateclockwise.png +0 -0
  106. setiastro/images/rotatecounterclockwise.png +0 -0
  107. setiastro/images/satellite.png +0 -0
  108. setiastro/images/script.png +0 -0
  109. setiastro/images/selectivecolor.png +0 -0
  110. setiastro/images/simbad.png +0 -0
  111. setiastro/images/slot0.png +0 -0
  112. setiastro/images/slot1.png +0 -0
  113. setiastro/images/slot2.png +0 -0
  114. setiastro/images/slot3.png +0 -0
  115. setiastro/images/slot4.png +0 -0
  116. setiastro/images/slot5.png +0 -0
  117. setiastro/images/slot6.png +0 -0
  118. setiastro/images/slot7.png +0 -0
  119. setiastro/images/slot8.png +0 -0
  120. setiastro/images/slot9.png +0 -0
  121. setiastro/images/spcc.png +0 -0
  122. setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
  123. setiastro/images/spinner.gif +0 -0
  124. setiastro/images/stacking.png +0 -0
  125. setiastro/images/staradd.png +0 -0
  126. setiastro/images/staralign.png +0 -0
  127. setiastro/images/starnet.png +0 -0
  128. setiastro/images/starregistration.png +0 -0
  129. setiastro/images/starspike.png +0 -0
  130. setiastro/images/starstretch.png +0 -0
  131. setiastro/images/statstretch.png +0 -0
  132. setiastro/images/supernova.png +0 -0
  133. setiastro/images/uhs.png +0 -0
  134. setiastro/images/undoicon.png +0 -0
  135. setiastro/images/upscale.png +0 -0
  136. setiastro/images/viewbundle.png +0 -0
  137. setiastro/images/whitebalance.png +0 -0
  138. setiastro/images/wimi_icon_256x256.png +0 -0
  139. setiastro/images/wimilogo.png +0 -0
  140. setiastro/images/wims.png +0 -0
  141. setiastro/images/wrench_icon.png +0 -0
  142. setiastro/images/xisfliberator.png +0 -0
  143. setiastro/saspro/__init__.py +20 -0
  144. setiastro/saspro/__main__.py +809 -0
  145. setiastro/saspro/_generated/__init__.py +7 -0
  146. setiastro/saspro/_generated/build_info.py +2 -0
  147. setiastro/saspro/abe.py +1295 -0
  148. setiastro/saspro/abe_preset.py +196 -0
  149. setiastro/saspro/aberration_ai.py +694 -0
  150. setiastro/saspro/aberration_ai_preset.py +224 -0
  151. setiastro/saspro/accel_installer.py +218 -0
  152. setiastro/saspro/accel_workers.py +30 -0
  153. setiastro/saspro/add_stars.py +621 -0
  154. setiastro/saspro/astrobin_exporter.py +1007 -0
  155. setiastro/saspro/astrospike.py +153 -0
  156. setiastro/saspro/astrospike_python.py +1839 -0
  157. setiastro/saspro/autostretch.py +196 -0
  158. setiastro/saspro/backgroundneutral.py +560 -0
  159. setiastro/saspro/batch_convert.py +325 -0
  160. setiastro/saspro/batch_renamer.py +519 -0
  161. setiastro/saspro/blemish_blaster.py +488 -0
  162. setiastro/saspro/blink_comparator_pro.py +2926 -0
  163. setiastro/saspro/bundles.py +61 -0
  164. setiastro/saspro/bundles_dock.py +114 -0
  165. setiastro/saspro/cheat_sheet.py +178 -0
  166. setiastro/saspro/clahe.py +342 -0
  167. setiastro/saspro/comet_stacking.py +1377 -0
  168. setiastro/saspro/common_tr.py +107 -0
  169. setiastro/saspro/config.py +38 -0
  170. setiastro/saspro/config_bootstrap.py +40 -0
  171. setiastro/saspro/config_manager.py +316 -0
  172. setiastro/saspro/continuum_subtract.py +1617 -0
  173. setiastro/saspro/convo.py +1397 -0
  174. setiastro/saspro/convo_preset.py +414 -0
  175. setiastro/saspro/copyastro.py +187 -0
  176. setiastro/saspro/cosmicclarity.py +1564 -0
  177. setiastro/saspro/cosmicclarity_preset.py +407 -0
  178. setiastro/saspro/crop_dialog_pro.py +956 -0
  179. setiastro/saspro/crop_preset.py +189 -0
  180. setiastro/saspro/curve_editor_pro.py +2544 -0
  181. setiastro/saspro/curves_preset.py +375 -0
  182. setiastro/saspro/debayer.py +670 -0
  183. setiastro/saspro/debug_utils.py +29 -0
  184. setiastro/saspro/dnd_mime.py +35 -0
  185. setiastro/saspro/doc_manager.py +2641 -0
  186. setiastro/saspro/exoplanet_detector.py +2166 -0
  187. setiastro/saspro/file_utils.py +284 -0
  188. setiastro/saspro/fitsmodifier.py +745 -0
  189. setiastro/saspro/fix_bom.py +32 -0
  190. setiastro/saspro/free_torch_memory.py +48 -0
  191. setiastro/saspro/frequency_separation.py +1343 -0
  192. setiastro/saspro/function_bundle.py +1594 -0
  193. setiastro/saspro/generate_translations.py +2378 -0
  194. setiastro/saspro/ghs_dialog_pro.py +660 -0
  195. setiastro/saspro/ghs_preset.py +284 -0
  196. setiastro/saspro/graxpert.py +634 -0
  197. setiastro/saspro/graxpert_preset.py +287 -0
  198. setiastro/saspro/gui/__init__.py +0 -0
  199. setiastro/saspro/gui/main_window.py +8567 -0
  200. setiastro/saspro/gui/mixins/__init__.py +33 -0
  201. setiastro/saspro/gui/mixins/dock_mixin.py +263 -0
  202. setiastro/saspro/gui/mixins/file_mixin.py +443 -0
  203. setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
  204. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  205. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  206. setiastro/saspro/gui/mixins/menu_mixin.py +361 -0
  207. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  208. setiastro/saspro/gui/mixins/toolbar_mixin.py +1457 -0
  209. setiastro/saspro/gui/mixins/update_mixin.py +309 -0
  210. setiastro/saspro/gui/mixins/view_mixin.py +435 -0
  211. setiastro/saspro/halobgon.py +462 -0
  212. setiastro/saspro/header_viewer.py +448 -0
  213. setiastro/saspro/headless_utils.py +88 -0
  214. setiastro/saspro/histogram.py +753 -0
  215. setiastro/saspro/history_explorer.py +939 -0
  216. setiastro/saspro/i18n.py +156 -0
  217. setiastro/saspro/image_combine.py +414 -0
  218. setiastro/saspro/image_peeker_pro.py +1601 -0
  219. setiastro/saspro/imageops/__init__.py +37 -0
  220. setiastro/saspro/imageops/mdi_snap.py +292 -0
  221. setiastro/saspro/imageops/scnr.py +36 -0
  222. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  223. setiastro/saspro/imageops/stretch.py +244 -0
  224. setiastro/saspro/isophote.py +1179 -0
  225. setiastro/saspro/layers.py +208 -0
  226. setiastro/saspro/layers_dock.py +714 -0
  227. setiastro/saspro/lazy_imports.py +193 -0
  228. setiastro/saspro/legacy/__init__.py +2 -0
  229. setiastro/saspro/legacy/image_manager.py +2226 -0
  230. setiastro/saspro/legacy/numba_utils.py +3659 -0
  231. setiastro/saspro/legacy/xisf.py +1071 -0
  232. setiastro/saspro/linear_fit.py +534 -0
  233. setiastro/saspro/live_stacking.py +1830 -0
  234. setiastro/saspro/log_bus.py +5 -0
  235. setiastro/saspro/logging_config.py +460 -0
  236. setiastro/saspro/luminancerecombine.py +309 -0
  237. setiastro/saspro/main_helpers.py +201 -0
  238. setiastro/saspro/mask_creation.py +928 -0
  239. setiastro/saspro/masks_core.py +56 -0
  240. setiastro/saspro/mdi_widgets.py +353 -0
  241. setiastro/saspro/memory_utils.py +666 -0
  242. setiastro/saspro/metadata_patcher.py +75 -0
  243. setiastro/saspro/mfdeconv.py +3826 -0
  244. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  245. setiastro/saspro/mfdeconvcudnn.py +3263 -0
  246. setiastro/saspro/mfdeconvsport.py +2382 -0
  247. setiastro/saspro/minorbodycatalog.py +567 -0
  248. setiastro/saspro/morphology.py +382 -0
  249. setiastro/saspro/multiscale_decomp.py +1290 -0
  250. setiastro/saspro/nbtorgb_stars.py +531 -0
  251. setiastro/saspro/numba_utils.py +3044 -0
  252. setiastro/saspro/numba_warmup.py +141 -0
  253. setiastro/saspro/ops/__init__.py +9 -0
  254. setiastro/saspro/ops/command_help_dialog.py +623 -0
  255. setiastro/saspro/ops/command_runner.py +217 -0
  256. setiastro/saspro/ops/commands.py +1594 -0
  257. setiastro/saspro/ops/script_editor.py +1102 -0
  258. setiastro/saspro/ops/scripts.py +1413 -0
  259. setiastro/saspro/ops/settings.py +679 -0
  260. setiastro/saspro/parallel_utils.py +554 -0
  261. setiastro/saspro/pedestal.py +121 -0
  262. setiastro/saspro/perfect_palette_picker.py +1070 -0
  263. setiastro/saspro/pipeline.py +110 -0
  264. setiastro/saspro/pixelmath.py +1600 -0
  265. setiastro/saspro/plate_solver.py +2444 -0
  266. setiastro/saspro/project_io.py +797 -0
  267. setiastro/saspro/psf_utils.py +136 -0
  268. setiastro/saspro/psf_viewer.py +549 -0
  269. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  270. setiastro/saspro/remove_green.py +314 -0
  271. setiastro/saspro/remove_stars.py +1625 -0
  272. setiastro/saspro/remove_stars_preset.py +404 -0
  273. setiastro/saspro/resources.py +477 -0
  274. setiastro/saspro/rgb_combination.py +207 -0
  275. setiastro/saspro/rgb_extract.py +19 -0
  276. setiastro/saspro/rgbalign.py +723 -0
  277. setiastro/saspro/runtime_imports.py +7 -0
  278. setiastro/saspro/runtime_torch.py +754 -0
  279. setiastro/saspro/save_options.py +72 -0
  280. setiastro/saspro/selective_color.py +1552 -0
  281. setiastro/saspro/sfcc.py +1430 -0
  282. setiastro/saspro/shortcuts.py +3043 -0
  283. setiastro/saspro/signature_insert.py +1099 -0
  284. setiastro/saspro/stacking_suite.py +18181 -0
  285. setiastro/saspro/star_alignment.py +7420 -0
  286. setiastro/saspro/star_alignment_preset.py +329 -0
  287. setiastro/saspro/star_metrics.py +49 -0
  288. setiastro/saspro/star_spikes.py +681 -0
  289. setiastro/saspro/star_stretch.py +470 -0
  290. setiastro/saspro/stat_stretch.py +506 -0
  291. setiastro/saspro/status_log_dock.py +78 -0
  292. setiastro/saspro/subwindow.py +3267 -0
  293. setiastro/saspro/supernovaasteroidhunter.py +1716 -0
  294. setiastro/saspro/swap_manager.py +99 -0
  295. setiastro/saspro/torch_backend.py +89 -0
  296. setiastro/saspro/torch_rejection.py +434 -0
  297. setiastro/saspro/translations/de_translations.py +3733 -0
  298. setiastro/saspro/translations/es_translations.py +3923 -0
  299. setiastro/saspro/translations/fr_translations.py +3842 -0
  300. setiastro/saspro/translations/integrate_translations.py +234 -0
  301. setiastro/saspro/translations/it_translations.py +3662 -0
  302. setiastro/saspro/translations/ja_translations.py +3585 -0
  303. setiastro/saspro/translations/pt_translations.py +3853 -0
  304. setiastro/saspro/translations/saspro_de.qm +0 -0
  305. setiastro/saspro/translations/saspro_de.ts +253 -0
  306. setiastro/saspro/translations/saspro_es.qm +0 -0
  307. setiastro/saspro/translations/saspro_es.ts +12520 -0
  308. setiastro/saspro/translations/saspro_fr.qm +0 -0
  309. setiastro/saspro/translations/saspro_fr.ts +12514 -0
  310. setiastro/saspro/translations/saspro_it.qm +0 -0
  311. setiastro/saspro/translations/saspro_it.ts +12520 -0
  312. setiastro/saspro/translations/saspro_ja.qm +0 -0
  313. setiastro/saspro/translations/saspro_ja.ts +257 -0
  314. setiastro/saspro/translations/saspro_pt.qm +0 -0
  315. setiastro/saspro/translations/saspro_pt.ts +257 -0
  316. setiastro/saspro/translations/saspro_zh.qm +0 -0
  317. setiastro/saspro/translations/saspro_zh.ts +12520 -0
  318. setiastro/saspro/translations/zh_translations.py +3659 -0
  319. setiastro/saspro/versioning.py +71 -0
  320. setiastro/saspro/view_bundle.py +1555 -0
  321. setiastro/saspro/wavescale_hdr.py +624 -0
  322. setiastro/saspro/wavescale_hdr_preset.py +101 -0
  323. setiastro/saspro/wavescalede.py +658 -0
  324. setiastro/saspro/wavescalede_preset.py +230 -0
  325. setiastro/saspro/wcs_update.py +374 -0
  326. setiastro/saspro/whitebalance.py +456 -0
  327. setiastro/saspro/widgets/__init__.py +48 -0
  328. setiastro/saspro/widgets/common_utilities.py +306 -0
  329. setiastro/saspro/widgets/graphics_views.py +122 -0
  330. setiastro/saspro/widgets/image_utils.py +518 -0
  331. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  332. setiastro/saspro/widgets/spinboxes.py +275 -0
  333. setiastro/saspro/widgets/themed_buttons.py +13 -0
  334. setiastro/saspro/widgets/wavelet_utils.py +299 -0
  335. setiastro/saspro/window_shelf.py +185 -0
  336. setiastro/saspro/xisf.py +1123 -0
  337. setiastrosuitepro-1.6.1.dist-info/METADATA +267 -0
  338. setiastrosuitepro-1.6.1.dist-info/RECORD +342 -0
  339. setiastrosuitepro-1.6.1.dist-info/WHEEL +4 -0
  340. setiastrosuitepro-1.6.1.dist-info/entry_points.txt +6 -0
  341. setiastrosuitepro-1.6.1.dist-info/licenses/LICENSE +674 -0
  342. setiastrosuitepro-1.6.1.dist-info/licenses/license.txt +2580 -0
@@ -0,0 +1,506 @@
1
+ # pro/stat_stretch.py
2
+ from __future__ import annotations
3
+ from PyQt6.QtCore import Qt, QSize, QEvent
4
+ from PyQt6.QtWidgets import (
5
+ QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLabel, QDoubleSpinBox,
6
+ QCheckBox, QPushButton, QScrollArea, QWidget, QMessageBox, QSlider, QToolBar, QToolButton
7
+ )
8
+ from PyQt6.QtGui import QImage, QPixmap, QMouseEvent, QCursor
9
+ import numpy as np
10
+ from PyQt6 import sip
11
+
12
+ from .doc_manager import ImageDocument
13
+ # use your existing stretch code
14
+ from setiastro.saspro.imageops.stretch import stretch_mono_image, stretch_color_image
15
+ from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
16
+
17
+ class StatisticalStretchDialog(QDialog):
18
+ """
19
+ Non-destructive preview; Apply commits to the document image.
20
+ """
21
+ def __init__(self, parent, document: ImageDocument):
22
+ super().__init__(parent)
23
+ self.setWindowTitle(self.tr("Statistical Stretch"))
24
+
25
+ # --- IMPORTANT: avoid “attached modal” behavior on some Linux WMs ---
26
+ # Make this a proper top-level window (tool-style) rather than an attached sheet.
27
+ self.setWindowFlag(Qt.WindowType.Window, True)
28
+ # Block the app if you want, but don't use WindowModal
29
+ self.setWindowModality(Qt.WindowModality.ApplicationModal)
30
+ # Don’t let the generic modal flag override the explicit modality
31
+ self.setModal(False)
32
+
33
+ self.doc = document
34
+ self._last_preview = None
35
+ self._panning = False
36
+ self._pan_last = None # QPoint
37
+ self._preview_scale = 1.0 # NEW: zoom factor for preview
38
+ self._preview_qimg = None # NEW: store unscaled QImage for clean scaling
39
+ self._suppress_replay_record = False
40
+
41
+ # --- Controls ---
42
+ self.spin_target = QDoubleSpinBox()
43
+ self.spin_target.setRange(0.01, 0.99)
44
+ self.spin_target.setSingleStep(0.01)
45
+ self.spin_target.setValue(0.25)
46
+ self.spin_target.setDecimals(3)
47
+
48
+ self.chk_linked = QCheckBox(self.tr("Linked channels"))
49
+ self.chk_linked.setChecked(False)
50
+
51
+ self.chk_normalize = QCheckBox(self.tr("Normalize to [0..1]"))
52
+ self.chk_normalize.setChecked(False)
53
+
54
+ # NEW: Curves boost
55
+ self.chk_curves = QCheckBox(self.tr("Curves boost"))
56
+ self.chk_curves.setChecked(False)
57
+
58
+ self.curves_row = QWidget()
59
+ cr_lay = QHBoxLayout(self.curves_row); cr_lay.setContentsMargins(0,0,0,0)
60
+ cr_lay.setSpacing(8)
61
+ cr_lay.addWidget(QLabel(self.tr("Strength:")))
62
+ self.sld_curves = QSlider(Qt.Orientation.Horizontal)
63
+ self.sld_curves.setRange(0, 100) # 0.00 … 1.00 mapped to 0…100
64
+ self.sld_curves.setSingleStep(1)
65
+ self.sld_curves.setPageStep(5)
66
+ self.sld_curves.setValue(20) # default 0.20
67
+ self.lbl_curves_val = QLabel("0.20")
68
+ self.sld_curves.valueChanged.connect(lambda v: self.lbl_curves_val.setText(f"{v/100:.2f}"))
69
+ cr_lay.addWidget(self.sld_curves, 1)
70
+ cr_lay.addWidget(self.lbl_curves_val)
71
+ self.curves_row.setEnabled(False) # disabled until checkbox is ticked
72
+ self.chk_curves.toggled.connect(self.curves_row.setEnabled)
73
+
74
+ # Preview area
75
+ self.preview_label = QLabel(alignment=Qt.AlignmentFlag.AlignCenter)
76
+ self.preview_label.setMinimumSize(QSize(320, 240))
77
+ self.preview_label.setScaledContents(False)
78
+ self.preview_scroll = QScrollArea()
79
+ self.preview_scroll.setWidgetResizable(False) # <- was True; we manage size
80
+ self.preview_scroll.setWidget(self.preview_label)
81
+ self.preview_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
82
+ self.preview_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
83
+
84
+ self._fit_mode = True # NEW: start in Fit mode
85
+
86
+ # --- Zoom buttons row (place before the main layout or right above preview) ---
87
+ # --- Zoom buttons row ---
88
+ zoom_row = QHBoxLayout()
89
+
90
+ # Use themed tool buttons (consistent with the rest of SASpro)
91
+ self.btn_zoom_out = themed_toolbtn("zoom-out", "Zoom Out")
92
+ self.btn_zoom_in = themed_toolbtn("zoom-in", "Zoom In")
93
+ self.btn_zoom_100 = themed_toolbtn("zoom-original", "1:1")
94
+ self.btn_zoom_fit = themed_toolbtn("zoom-fit-best", "Fit")
95
+
96
+
97
+ zoom_row.addStretch(1)
98
+ for b in (self.btn_zoom_out, self.btn_zoom_in, self.btn_zoom_100, self.btn_zoom_fit):
99
+ zoom_row.addWidget(b)
100
+ zoom_row.addStretch(1)
101
+
102
+ # Buttons
103
+ self.btn_preview = QPushButton(self.tr("Preview"))
104
+ self.btn_apply = QPushButton(self.tr("Apply"))
105
+ self.btn_close = QPushButton(self.tr("Close"))
106
+
107
+ self.btn_preview.clicked.connect(self._do_preview)
108
+ self.btn_apply.clicked.connect(self._do_apply)
109
+ self.btn_close.clicked.connect(self.close)
110
+
111
+ # --- Layout ---
112
+ form = QFormLayout()
113
+ form.addRow(self.tr("Target median:"), self.spin_target)
114
+ form.addRow("", self.chk_linked)
115
+ form.addRow("", self.chk_normalize)
116
+ form.addRow("", self.chk_curves)
117
+ form.addRow("", self.curves_row)
118
+
119
+ left = QVBoxLayout()
120
+ left.addLayout(form)
121
+ row = QHBoxLayout()
122
+ row.addWidget(self.btn_preview)
123
+ row.addWidget(self.btn_apply)
124
+ row.addStretch(1)
125
+ left.addLayout(row)
126
+ left.addStretch(1)
127
+
128
+ main = QHBoxLayout(self)
129
+ main.addLayout(left, 0)
130
+
131
+ # NEW: right column with zoom row + preview
132
+ right = QVBoxLayout()
133
+ right.addLayout(zoom_row) # ← actually add the zoom controls
134
+ right.addWidget(self.preview_scroll, 1) # preview below the buttons
135
+ main.addLayout(right, 1)
136
+
137
+ self.btn_zoom_in.clicked.connect(lambda: self._zoom_by(1.25))
138
+ self.btn_zoom_out.clicked.connect(lambda: self._zoom_by(1/1.25))
139
+ self.btn_zoom_100.clicked.connect(self._zoom_reset_100)
140
+ self.btn_zoom_fit.clicked.connect(self._fit_preview)
141
+
142
+ self.preview_scroll.viewport().installEventFilter(self)
143
+ self.preview_label.installEventFilter(self)
144
+
145
+ self._populate_initial_preview()
146
+
147
+ # ----- helpers -----
148
+ def _get_source_float(self) -> np.ndarray:
149
+ """
150
+ Return a float32 array scaled into ~[0..1] for stretching.
151
+ """
152
+ src = np.asarray(self.doc.image)
153
+ if src is None or src.size == 0:
154
+ return None
155
+
156
+ if np.issubdtype(src.dtype, np.integer):
157
+ # Assume 16-bit astro sources by default; adjust if you prefer
158
+ scale = 65535.0 if src.dtype.itemsize >= 2 else 255.0
159
+ return (src.astype(np.float32) / scale).clip(0, 1)
160
+ else:
161
+ f = src.astype(np.float32)
162
+ # If values are way above 1 (linear calibrated data), compress softly
163
+ mx = float(f.max()) if f.size else 1.0
164
+ if mx > 5.0:
165
+ f = f / mx
166
+ return f
167
+
168
+ def _apply_current_zoom(self):
169
+ """Apply the current zoom mode (fit or manual) to the preview image."""
170
+ if self._preview_qimg is None:
171
+ return
172
+ if self._fit_mode:
173
+ self._fit_preview()
174
+ else:
175
+ self._update_preview_scaled()
176
+
177
+ def _fit_preview(self):
178
+ """Fit the image into the visible scroll viewport."""
179
+ if self._preview_qimg is None:
180
+ return
181
+ vp = self.preview_scroll.viewport().size()
182
+ if vp.width() <= 1 or vp.height() <= 1:
183
+ return
184
+ iw, ih = self._preview_qimg.width(), self._preview_qimg.height()
185
+ if iw <= 0 or ih <= 0:
186
+ return
187
+ # compute scale to fit
188
+ sx = vp.width() / iw
189
+ sy = vp.height() / ih
190
+ self._preview_scale = max(0.05, min(sx, sy))
191
+ self._fit_mode = True
192
+ self._update_preview_scaled()
193
+
194
+ def _zoom_reset_100(self):
195
+ """Set zoom to 100% (1:1)."""
196
+ self._fit_mode = False
197
+ self._preview_scale = 1.0
198
+ self._update_preview_scaled()
199
+
200
+ def _zoom_by(self, factor: float):
201
+ """Incremental zoom around the current center; exits Fit mode."""
202
+ self._fit_mode = False
203
+ new_scale = self._preview_scale * float(factor)
204
+ self._preview_scale = max(0.05, min(new_scale, 8.0))
205
+ self._update_preview_scaled()
206
+
207
+
208
+ # --- MASK helpers ----------------------------------------------------
209
+ def _active_mask_array(self) -> np.ndarray | None:
210
+ """Return active mask as float32 [H,W] in 0..1, resized to doc image."""
211
+ try:
212
+ mid = getattr(self.doc, "active_mask_id", None)
213
+ if not mid:
214
+ return None
215
+ layer = getattr(self.doc, "masks", {}).get(mid)
216
+ if layer is None:
217
+ return None
218
+
219
+ m = np.asarray(getattr(layer, "data", None))
220
+ if m is None or m.size == 0:
221
+ return None
222
+
223
+ # squeeze to 2D
224
+ if m.ndim == 3 and m.shape[2] == 1:
225
+ m = m[..., 0]
226
+ elif m.ndim == 3: # RGB/whatever → luminance
227
+ m = (0.2126*m[...,0] + 0.7152*m[...,1] + 0.0722*m[...,2])
228
+
229
+ m = m.astype(np.float32, copy=False)
230
+ # normalize if integer / out-of-range
231
+ if m.dtype.kind in "ui":
232
+ m /= float(np.iinfo(m.dtype).max)
233
+ m = np.clip(m, 0.0, 1.0)
234
+
235
+ th, tw = self.doc.image.shape[:2]
236
+ sh, sw = m.shape[:2]
237
+ if (sh, sw) != (th, tw):
238
+ yi = (np.linspace(0, sh-1, th)).astype(np.int32)
239
+ xi = (np.linspace(0, sw-1, tw)).astype(np.int32)
240
+ m = m[yi][:, xi]
241
+
242
+ # honor opacity if present
243
+ opacity = float(getattr(layer, "opacity", 1.0) or 1.0)
244
+ if opacity < 1.0:
245
+ m *= opacity
246
+ return m
247
+ except Exception:
248
+ return None
249
+
250
+ def _blend_with_mask(self, base: np.ndarray, out: np.ndarray, mask: np.ndarray) -> np.ndarray:
251
+ """base/out can be mono or 3ch; mask is [H,W] in 0..1."""
252
+ if out.ndim == 3 and out.shape[2] == 3:
253
+ m = mask[..., None]
254
+ else:
255
+ m = mask
256
+ return base * (1.0 - m) + out * m
257
+
258
+
259
+ def _run_stretch(self) -> np.ndarray | None:
260
+ imgf = self._get_source_float()
261
+ if imgf is None:
262
+ return None
263
+
264
+ target = float(self.spin_target.value())
265
+ linked = bool(self.chk_linked.isChecked())
266
+ normalize = bool(self.chk_normalize.isChecked())
267
+ apply_curves = bool(self.chk_curves.isChecked())
268
+ curves_boost = float(self.sld_curves.value()) / 100.0
269
+
270
+ if imgf.ndim == 2 or (imgf.ndim == 3 and imgf.shape[2] == 1):
271
+ out = stretch_mono_image(
272
+ imgf.squeeze(),
273
+ target_median=target,
274
+ normalize=normalize,
275
+ apply_curves=apply_curves,
276
+ curves_boost=curves_boost,
277
+ )
278
+ else:
279
+ out = stretch_color_image(
280
+ imgf,
281
+ target_median=target,
282
+ linked=linked,
283
+ normalize=normalize,
284
+ apply_curves=apply_curves,
285
+ curves_boost=curves_boost,
286
+ )
287
+
288
+ # ✅ If a mask is active, blend stretched result with original
289
+ m = self._active_mask_array()
290
+ if m is not None:
291
+ base = imgf.astype(np.float32, copy=False)
292
+ out = self._blend_with_mask(base, out, m)
293
+
294
+ return out
295
+
296
+
297
+ def _set_preview_pixmap(self, arr: np.ndarray):
298
+ vis = arr
299
+ if vis is None or vis.size == 0:
300
+ self.preview_label.clear()
301
+ return
302
+
303
+ # Ensure 3 channels for display
304
+ if vis.ndim == 2:
305
+ vis3 = np.stack([vis] * 3, axis=-1)
306
+ elif vis.ndim == 3 and vis.shape[2] == 1:
307
+ vis3 = np.repeat(vis, 3, axis=2)
308
+ else:
309
+ vis3 = vis
310
+
311
+ # Convert to 8-bit RGB
312
+ if vis3.dtype == np.uint8:
313
+ buf8 = vis3
314
+ elif vis3.dtype == np.uint16:
315
+ buf8 = (vis3.astype(np.float32) / 65535.0 * 255.0).clip(0, 255).astype(np.uint8)
316
+ else:
317
+ buf8 = (np.clip(vis3, 0.0, 1.0) * 255.0).astype(np.uint8)
318
+
319
+ # Must be C-contiguous for QImage
320
+ buf8 = np.ascontiguousarray(buf8)
321
+ h, w, _ = buf8.shape
322
+ bytes_per_line = buf8.strides[0]
323
+
324
+ # Build QImage from raw pointer; keep references alive
325
+ self._last_preview = buf8 # keep backing store alive
326
+ ptr = sip.voidptr(self._last_preview.ctypes.data)
327
+ qimg = QImage(ptr, w, h, bytes_per_line, QImage.Format.Format_RGB888)
328
+
329
+ self._preview_qimg = qimg
330
+ self._apply_current_zoom()
331
+
332
+ # ----- slots -----
333
+ def _populate_initial_preview(self):
334
+ # show the current (unstretched) image as baseline
335
+ src = self._get_source_float()
336
+ if src is not None:
337
+ self._set_preview_pixmap(np.clip(src, 0, 1))
338
+
339
+ def _do_preview(self):
340
+ try:
341
+ out = self._run_stretch()
342
+ if out is None:
343
+ QMessageBox.information(self, "No image", "No image is loaded in the active document.")
344
+ return
345
+ self._set_preview_pixmap(out)
346
+ except Exception as e:
347
+ QMessageBox.warning(self, "Preview failed", str(e))
348
+
349
+ def _do_apply(self):
350
+ try:
351
+ out = self._run_stretch()
352
+ if out is None:
353
+ QMessageBox.information(self, "No image", "No image is loaded in the active document.")
354
+ return
355
+
356
+ # Preserve mono vs color shape
357
+ if out.ndim == 3 and out.shape[2] == 3 and (self.doc.image.ndim == 2 or self.doc.image.shape[-1] == 1):
358
+ out = out[..., 0]
359
+
360
+ # --- Gather current UI state ------------------------------------
361
+ target = float(self.spin_target.value())
362
+ linked = bool(self.chk_linked.isChecked())
363
+ normalize = bool(self.chk_normalize.isChecked())
364
+ apply_curves = bool(getattr(self, "chk_curves", None) and self.chk_curves.isChecked())
365
+ curves_boost = 0.0
366
+ if getattr(self, "sld_curves", None) is not None:
367
+ curves_boost = float(self.sld_curves.value()) / 100.0
368
+
369
+ # Build human-readable step name
370
+ parts = [f"target={target:.2f}", "linked" if linked else "unlinked"]
371
+ if normalize:
372
+ parts.append("norm")
373
+ if apply_curves:
374
+ parts.append(f"curves={curves_boost:.2f}")
375
+ if self._active_mask_array() is not None:
376
+ parts.append("masked")
377
+ step_name = f"Statistical Stretch ({', '.join(parts)})"
378
+
379
+ # Apply to document
380
+ self.doc.apply_edit(out.astype(np.float32, copy=False), step_name=step_name)
381
+
382
+ # Turn off display stretch on the active view, if any
383
+ mw = self.parent()
384
+ if hasattr(mw, "mdi") and mw.mdi.activeSubWindow():
385
+ view = mw.mdi.activeSubWindow().widget()
386
+ if getattr(view, "autostretch_enabled", False):
387
+ view.set_autostretch(False)
388
+
389
+ # Existing logging, now using the same values as above
390
+ if hasattr(mw, "_log"):
391
+ curves_on = apply_curves
392
+ boost_val = curves_boost if curves_on else 0.0
393
+ mw._log(
394
+ "Applied Statistical Stretch "
395
+ f"(target={target:.3f}, linked={linked}, normalize={normalize}, "
396
+ f"curves={'ON' if curves_on else 'OFF'}"
397
+ f"{', boost='+str(round(boost_val,2)) if curves_on else ''}, "
398
+ f"mask={'ON' if self._active_mask_array() is not None else 'OFF'})"
399
+ )
400
+
401
+ # --- Build preset for headless replay ---------------------------
402
+ # --- Build preset for headless replay ---------------------------
403
+ preset = {
404
+ "target_median": target,
405
+ "linked": linked,
406
+ "normalize": normalize,
407
+ "apply_curves": apply_curves,
408
+ "curves_boost": curves_boost,
409
+ }
410
+
411
+ # ✅ Remember this as the last headless-style command
412
+ # (unless we are in a headless/suppressed call)
413
+ suppress = bool(getattr(self, "_suppress_replay_record", False))
414
+ if not suppress:
415
+ from PyQt6.QtWidgets import QMainWindow
416
+ try:
417
+ mw2 = self.parent()
418
+ while mw2 is not None and not isinstance(mw2, QMainWindow):
419
+ mw2 = mw2.parent()
420
+
421
+ if mw2 is not None and hasattr(mw2, "remember_last_headless_command"):
422
+ mw2.remember_last_headless_command(
423
+ command_id="stat_stretch",
424
+ preset=preset,
425
+ description="Statistical Stretch",
426
+ )
427
+ print(f"Remembered Statistical Stretch last headless command: {preset}")
428
+ else:
429
+ print("No main window with remember_last_headless_command; cannot store stat_stretch preset")
430
+ except Exception as e:
431
+ print(f"Failed to remember Statistical Stretch last headless command: {e}")
432
+ else:
433
+ # optional debug
434
+ print("Statistical Stretch: replay recording suppressed for this apply()")
435
+
436
+ self.accept()
437
+
438
+
439
+ except Exception as e:
440
+ QMessageBox.critical(self, "Apply failed", str(e))
441
+
442
+
443
+ def _update_preview_scaled(self):
444
+ if self._preview_qimg is None:
445
+ self.preview_label.clear()
446
+ return
447
+ sw = max(1, int(self._preview_qimg.width() * self._preview_scale))
448
+ sh = max(1, int(self._preview_qimg.height() * self._preview_scale))
449
+ scaled = self._preview_qimg.scaled(
450
+ sw, sh,
451
+ Qt.AspectRatioMode.KeepAspectRatio,
452
+ Qt.TransformationMode.SmoothTransformation
453
+ )
454
+ self.preview_label.setPixmap(QPixmap.fromImage(scaled))
455
+ self.preview_label.resize(scaled.size()) # <- crucial for scrollbars
456
+
457
+ def resizeEvent(self, ev):
458
+ super().resizeEvent(ev)
459
+ if self._fit_mode:
460
+ self._fit_preview()
461
+
462
+ def eventFilter(self, obj, ev):
463
+ # Ctrl+wheel zoom
464
+ if ev.type() == QEvent.Type.Wheel and (obj is self.preview_scroll.viewport() or obj is self.preview_label):
465
+ if ev.modifiers() & Qt.KeyboardModifier.ControlModifier:
466
+ factor = 1.25 if ev.angleDelta().y() > 0 else 1/1.25
467
+ self._fit_mode = False # ← ensure we exit Fit mode
468
+ self._preview_scale = max(0.05, min(self._preview_scale * factor, 8.0))
469
+ self._update_preview_scaled()
470
+ return True
471
+ return False
472
+
473
+ # Click+drag pan (left or middle mouse)
474
+ if obj is self.preview_scroll.viewport() or obj is self.preview_label:
475
+ if ev.type() == QEvent.Type.MouseButtonPress:
476
+ if ev.buttons() & (Qt.MouseButton.LeftButton | Qt.MouseButton.MiddleButton):
477
+ self._panning = True
478
+ self._pan_last = ev.position().toPoint()
479
+ # show a "grab" cursor where the drag begins
480
+ if obj is self.preview_label:
481
+ self.preview_label.setCursor(Qt.CursorShape.ClosedHandCursor)
482
+ else:
483
+ self.preview_scroll.viewport().setCursor(Qt.CursorShape.ClosedHandCursor)
484
+ return True
485
+
486
+ elif ev.type() == QEvent.Type.MouseMove and self._panning:
487
+ pos = ev.position().toPoint()
488
+ delta = pos - self._pan_last
489
+ self._pan_last = pos
490
+
491
+ hsb = self.preview_scroll.horizontalScrollBar()
492
+ vsb = self.preview_scroll.verticalScrollBar()
493
+ hsb.setValue(hsb.value() - delta.x())
494
+ vsb.setValue(vsb.value() - delta.y())
495
+ return True
496
+
497
+ elif ev.type() == QEvent.Type.MouseButtonRelease and self._panning:
498
+ self._panning = False
499
+ self._pan_last = None
500
+ # restore cursor
501
+ self.preview_label.unsetCursor()
502
+ self.preview_scroll.viewport().unsetCursor()
503
+ return True
504
+
505
+ return super().eventFilter(obj, ev)
506
+
@@ -0,0 +1,78 @@
1
+ # pro/status_log_dock.py
2
+ from PyQt6.QtCore import Qt, pyqtSlot
3
+ from PyQt6.QtGui import QTextCursor
4
+ from PyQt6.QtWidgets import (
5
+ QDockWidget, QWidget, QVBoxLayout, QPlainTextEdit, QPushButton, QHBoxLayout
6
+ )
7
+
8
+ class StatusLogDock(QDockWidget):
9
+ MAX_BLOCKS = 2000
10
+
11
+ def __init__(self, parent=None):
12
+ super().__init__("Stacking Log", parent)
13
+ self.setObjectName("StackingLogDock")
14
+ self.setAllowedAreas(
15
+ Qt.DockWidgetArea.BottomDockWidgetArea
16
+ | Qt.DockWidgetArea.LeftDockWidgetArea
17
+ | Qt.DockWidgetArea.RightDockWidgetArea
18
+ )
19
+
20
+ w = QWidget(self)
21
+ lay = QVBoxLayout(w); lay.setContentsMargins(6,6,6,6)
22
+
23
+ self.view = QPlainTextEdit(w)
24
+ self.view.setReadOnly(True)
25
+ self.view.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
26
+ self.view.setStyleSheet(
27
+ "background-color: black; color: white; font-family: Monospace; padding: 6px;"
28
+ )
29
+ lay.addWidget(self.view, 1)
30
+
31
+ row = QHBoxLayout()
32
+ btn_clear = QPushButton("Clear", w)
33
+ btn_clear.clicked.connect(self.view.clear)
34
+ row.addWidget(btn_clear)
35
+ row.addStretch(1)
36
+ lay.addLayout(row)
37
+
38
+ self.setWidget(w)
39
+
40
+ @pyqtSlot(str)
41
+ def append_line(self, message: str):
42
+ doc = self.view.document()
43
+
44
+ # coalesce “Normalizing …” lines (replace last if same prefix)
45
+ if message.startswith("🔄 Normalizing") and doc.blockCount() > 0:
46
+ last = doc.findBlockByNumber(doc.blockCount() - 1)
47
+ if last.isValid() and last.text().startswith("🔄 Normalizing"):
48
+ cur = self.view.textCursor()
49
+ cur.movePosition(QTextCursor.MoveOperation.End)
50
+ cur.movePosition(QTextCursor.MoveOperation.StartOfBlock,
51
+ QTextCursor.MoveMode.KeepAnchor)
52
+ cur.removeSelectedText()
53
+ cur.insertText(message)
54
+ self.view.setTextCursor(cur)
55
+ else:
56
+ self.view.appendPlainText(message)
57
+ else:
58
+ self.view.appendPlainText(message)
59
+
60
+ # trim earliest lines
61
+ if doc.blockCount() > self.MAX_BLOCKS:
62
+ extra = doc.blockCount() - self.MAX_BLOCKS
63
+ cur = self.view.textCursor()
64
+ cur.movePosition(QTextCursor.MoveOperation.Start)
65
+ cur.movePosition(QTextCursor.MoveOperation.Down,
66
+ QTextCursor.MoveMode.KeepAnchor, extra)
67
+ cur.removeSelectedText()
68
+ self.view.setTextCursor(self.view.textCursor())
69
+
70
+ # autoscroll
71
+ sb = self.view.verticalScrollBar()
72
+ sb.setValue(sb.maximum())
73
+
74
+ def show_raise(self):
75
+ self.setVisible(True)
76
+ self.raise_()
77
+ if self.widget():
78
+ self.widget().setFocus()