setiastrosuitepro 1.6.0__py3-none-any.whl → 1.6.4.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 (293) hide show
  1. setiastro/data/SASP_data.fits +0 -0
  2. setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
  3. setiastro/data/catalogs/astrobin_filters.csv +890 -0
  4. setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
  5. setiastro/data/catalogs/cali2.csv +63 -0
  6. setiastro/data/catalogs/cali2color.csv +65 -0
  7. setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
  8. setiastro/data/catalogs/celestial_catalog.csv +24031 -0
  9. setiastro/data/catalogs/detected_stars.csv +24784 -0
  10. setiastro/data/catalogs/fits_header_data.csv +46 -0
  11. setiastro/data/catalogs/test.csv +8 -0
  12. setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
  13. setiastro/images/Astro_Spikes.png +0 -0
  14. setiastro/images/Background_startup.jpg +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/rotatearbitrary.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/__main__.py +228 -67
  146. setiastro/saspro/_generated/build_info.py +2 -1
  147. setiastro/saspro/abe.py +76 -25
  148. setiastro/saspro/aberration_ai.py +14 -14
  149. setiastro/saspro/add_stars.py +15 -12
  150. setiastro/saspro/astrobin_exporter.py +61 -58
  151. setiastro/saspro/astrospike_python.py +3 -1
  152. setiastro/saspro/autostretch.py +4 -2
  153. setiastro/saspro/backgroundneutral.py +65 -14
  154. setiastro/saspro/batch_convert.py +8 -5
  155. setiastro/saspro/batch_renamer.py +39 -36
  156. setiastro/saspro/blemish_blaster.py +15 -12
  157. setiastro/saspro/blink_comparator_pro.py +605 -379
  158. setiastro/saspro/cheat_sheet.py +62 -17
  159. setiastro/saspro/clahe.py +34 -8
  160. setiastro/saspro/comet_stacking.py +103 -38
  161. setiastro/saspro/common_tr.py +107 -0
  162. setiastro/saspro/continuum_subtract.py +7 -7
  163. setiastro/saspro/convo.py +12 -9
  164. setiastro/saspro/copyastro.py +3 -0
  165. setiastro/saspro/cosmicclarity.py +77 -52
  166. setiastro/saspro/crop_dialog_pro.py +80 -45
  167. setiastro/saspro/curve_editor_pro.py +51 -33
  168. setiastro/saspro/debayer.py +6 -3
  169. setiastro/saspro/doc_manager.py +49 -19
  170. setiastro/saspro/exoplanet_detector.py +11 -11
  171. setiastro/saspro/fitsmodifier.py +48 -44
  172. setiastro/saspro/fix_bom.py +32 -0
  173. setiastro/saspro/frequency_separation.py +18 -12
  174. setiastro/saspro/function_bundle.py +18 -16
  175. setiastro/saspro/generate_translations.py +3092 -0
  176. setiastro/saspro/ghs_dialog_pro.py +19 -16
  177. setiastro/saspro/graxpert.py +3 -0
  178. setiastro/saspro/gui/main_window.py +471 -126
  179. setiastro/saspro/gui/mixins/dock_mixin.py +123 -11
  180. setiastro/saspro/gui/mixins/file_mixin.py +25 -20
  181. setiastro/saspro/gui/mixins/geometry_mixin.py +115 -15
  182. setiastro/saspro/gui/mixins/header_mixin.py +6 -6
  183. setiastro/saspro/gui/mixins/mask_mixin.py +8 -8
  184. setiastro/saspro/gui/mixins/menu_mixin.py +62 -33
  185. setiastro/saspro/gui/mixins/toolbar_mixin.py +382 -226
  186. setiastro/saspro/gui/mixins/update_mixin.py +26 -26
  187. setiastro/saspro/gui/statistics_dialog.py +47 -0
  188. setiastro/saspro/halobgon.py +29 -3
  189. setiastro/saspro/header_viewer.py +21 -18
  190. setiastro/saspro/histogram.py +29 -26
  191. setiastro/saspro/history_explorer.py +2 -0
  192. setiastro/saspro/i18n.py +168 -0
  193. setiastro/saspro/image_combine.py +3 -0
  194. setiastro/saspro/image_peeker_pro.py +52 -44
  195. setiastro/saspro/imageops/stretch.py +5 -13
  196. setiastro/saspro/isophote.py +3 -0
  197. setiastro/saspro/legacy/numba_utils.py +64 -47
  198. setiastro/saspro/linear_fit.py +3 -0
  199. setiastro/saspro/live_stacking.py +13 -2
  200. setiastro/saspro/mask_creation.py +180 -22
  201. setiastro/saspro/mfdeconv.py +5 -0
  202. setiastro/saspro/morphology.py +38 -13
  203. setiastro/saspro/multiscale_decomp.py +713 -256
  204. setiastro/saspro/nbtorgb_stars.py +12 -2
  205. setiastro/saspro/numba_utils.py +149 -48
  206. setiastro/saspro/ops/scripts.py +77 -17
  207. setiastro/saspro/ops/settings.py +177 -100
  208. setiastro/saspro/perfect_palette_picker.py +25 -7
  209. setiastro/saspro/pixelmath.py +114 -110
  210. setiastro/saspro/plate_solver.py +118 -108
  211. setiastro/saspro/remove_green.py +24 -7
  212. setiastro/saspro/remove_stars.py +136 -162
  213. setiastro/saspro/remove_stars_preset.py +55 -13
  214. setiastro/saspro/resources.py +46 -15
  215. setiastro/saspro/rgb_combination.py +19 -18
  216. setiastro/saspro/rgbalign.py +11 -11
  217. setiastro/saspro/save_options.py +5 -4
  218. setiastro/saspro/selective_color.py +84 -25
  219. setiastro/saspro/sfcc.py +119 -72
  220. setiastro/saspro/shortcuts.py +345 -36
  221. setiastro/saspro/signature_insert.py +4 -1
  222. setiastro/saspro/stacking_suite.py +2066 -1119
  223. setiastro/saspro/star_alignment.py +291 -331
  224. setiastro/saspro/star_spikes.py +137 -53
  225. setiastro/saspro/star_stretch.py +47 -10
  226. setiastro/saspro/stat_stretch.py +52 -16
  227. setiastro/saspro/status_log_dock.py +1 -1
  228. setiastro/saspro/subwindow.py +97 -36
  229. setiastro/saspro/supernovaasteroidhunter.py +68 -61
  230. setiastro/saspro/swap_manager.py +77 -42
  231. setiastro/saspro/translations/all_source_strings.json +4726 -0
  232. setiastro/saspro/translations/ar_translations.py +4096 -0
  233. setiastro/saspro/translations/de_translations.py +3728 -0
  234. setiastro/saspro/translations/es_translations.py +4169 -0
  235. setiastro/saspro/translations/fr_translations.py +4090 -0
  236. setiastro/saspro/translations/hi_translations.py +3803 -0
  237. setiastro/saspro/translations/integrate_translations.py +271 -0
  238. setiastro/saspro/translations/it_translations.py +4728 -0
  239. setiastro/saspro/translations/ja_translations.py +3834 -0
  240. setiastro/saspro/translations/pt_translations.py +3847 -0
  241. setiastro/saspro/translations/ru_translations.py +3082 -0
  242. setiastro/saspro/translations/saspro_ar.qm +0 -0
  243. setiastro/saspro/translations/saspro_ar.ts +16019 -0
  244. setiastro/saspro/translations/saspro_de.qm +0 -0
  245. setiastro/saspro/translations/saspro_de.ts +14548 -0
  246. setiastro/saspro/translations/saspro_es.qm +0 -0
  247. setiastro/saspro/translations/saspro_es.ts +16202 -0
  248. setiastro/saspro/translations/saspro_fr.qm +0 -0
  249. setiastro/saspro/translations/saspro_fr.ts +15870 -0
  250. setiastro/saspro/translations/saspro_hi.qm +0 -0
  251. setiastro/saspro/translations/saspro_hi.ts +14855 -0
  252. setiastro/saspro/translations/saspro_it.qm +0 -0
  253. setiastro/saspro/translations/saspro_it.ts +19046 -0
  254. setiastro/saspro/translations/saspro_ja.qm +0 -0
  255. setiastro/saspro/translations/saspro_ja.ts +14980 -0
  256. setiastro/saspro/translations/saspro_pt.qm +0 -0
  257. setiastro/saspro/translations/saspro_pt.ts +15024 -0
  258. setiastro/saspro/translations/saspro_ru.qm +0 -0
  259. setiastro/saspro/translations/saspro_ru.ts +11835 -0
  260. setiastro/saspro/translations/saspro_sw.qm +0 -0
  261. setiastro/saspro/translations/saspro_sw.ts +15237 -0
  262. setiastro/saspro/translations/saspro_uk.qm +0 -0
  263. setiastro/saspro/translations/saspro_uk.ts +15248 -0
  264. setiastro/saspro/translations/saspro_zh.qm +0 -0
  265. setiastro/saspro/translations/saspro_zh.ts +15289 -0
  266. setiastro/saspro/translations/sw_translations.py +3897 -0
  267. setiastro/saspro/translations/uk_translations.py +3929 -0
  268. setiastro/saspro/translations/zh_translations.py +3910 -0
  269. setiastro/saspro/versioning.py +77 -0
  270. setiastro/saspro/view_bundle.py +20 -17
  271. setiastro/saspro/wavescale_hdr.py +54 -33
  272. setiastro/saspro/wavescale_hdr_preset.py +6 -5
  273. setiastro/saspro/wavescalede.py +54 -31
  274. setiastro/saspro/wavescalede_preset.py +9 -7
  275. setiastro/saspro/whitebalance.py +58 -22
  276. setiastro/saspro/widgets/common_utilities.py +12 -11
  277. setiastro/saspro/widgets/minigame/game.js +991 -0
  278. setiastro/saspro/widgets/minigame/index.html +53 -0
  279. setiastro/saspro/widgets/minigame/style.css +241 -0
  280. setiastro/saspro/widgets/preview_dialogs.py +8 -8
  281. setiastro/saspro/widgets/resource_monitor.py +263 -0
  282. setiastro/saspro/widgets/spinboxes.py +18 -0
  283. setiastro/saspro/widgets/wavelet_utils.py +52 -20
  284. setiastro/saspro/wimi.py +7996 -0
  285. setiastro/saspro/wims.py +578 -0
  286. setiastro/saspro/window_shelf.py +2 -2
  287. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/METADATA +15 -3
  288. setiastrosuitepro-1.6.4.post1.dist-info/RECORD +368 -0
  289. setiastrosuitepro-1.6.0.dist-info/RECORD +0 -174
  290. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/WHEEL +0 -0
  291. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/entry_points.txt +0 -0
  292. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/licenses/LICENSE +0 -0
  293. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/licenses/license.txt +0 -0
@@ -48,6 +48,9 @@ class LiveStackSettingsDialog(QDialog):
48
48
  def __init__(self, parent):
49
49
  super().__init__(parent)
50
50
  self.setWindowTitle("Live Stack & Culling Settings")
51
+ self.setWindowFlag(Qt.WindowType.Window, True)
52
+ self.setWindowModality(Qt.WindowModality.NonModal)
53
+ self.setModal(False)
51
54
 
52
55
  # — Live Stack Settings —
53
56
  # Bootstrap frames (int)
@@ -290,8 +293,16 @@ def estimate_global_snr(
290
293
 
291
294
  # 1) Collapse to simple 2D float array (grayscale)
292
295
  if stack_image.ndim == 3 and stack_image.shape[2] == 3:
293
- # RGB → grayscale by averaging channels
294
- gray = stack_image.mean(axis=2).astype(np.float32)
296
+ try:
297
+ import cv2
298
+ # cv2.cvtColor is significantly faster than mean(axis=2)
299
+ # Assuming RGB input, but even if BGR, for SNR estimation luma difference is negligible
300
+ gray = cv2.cvtColor(stack_image, cv2.COLOR_RGB2GRAY)
301
+ if gray.dtype != np.float32:
302
+ gray = gray.astype(np.float32)
303
+ except ImportError:
304
+ # Fallback
305
+ gray = stack_image.mean(axis=2).astype(np.float32)
295
306
  else:
296
307
  # Already mono: just cast to float32
297
308
  gray = stack_image.astype(np.float32)
@@ -17,7 +17,7 @@ except Exception:
17
17
  from PyQt6.QtCore import Qt, QPointF, QRectF, QTimer, QEvent
18
18
  from PyQt6.QtGui import (
19
19
  QImage, QPixmap, QPainter, QColor, QPen, QBrush,
20
- QPainterPath, QWheelEvent, QPolygonF
20
+ QPainterPath, QWheelEvent, QPolygonF, QMouseEvent
21
21
  )
22
22
  from PyQt6.QtWidgets import (
23
23
  QInputDialog, QMessageBox, QFileDialog, # QFileDialog only used if you later add “export”
@@ -32,7 +32,7 @@ from PyQt6.QtWidgets import (
32
32
 
33
33
  from .masks_core import MaskLayer
34
34
  from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
35
-
35
+ from setiastro.saspro.imageops.stretch import stretch_color_image
36
36
 
37
37
  # ---------- small utils ----------
38
38
 
@@ -48,6 +48,38 @@ def _to_qpixmap01(img01: np.ndarray) -> QPixmap:
48
48
  qimg = QImage(buf.data, w, h, buf.strides[0], QImage.Format.Format_RGB888)
49
49
  return QPixmap.fromImage(qimg)
50
50
 
51
+ def _display_stretch(img01: np.ndarray) -> np.ndarray:
52
+ """
53
+ Display-only stretch. Does NOT modify underlying data used for mask creation.
54
+ Returns float32 in [0,1].
55
+ """
56
+ a = np.asarray(img01, dtype=np.float32)
57
+ a = np.clip(a, 0.0, 1.0)
58
+
59
+ # Color: use your existing stretch if available
60
+ if a.ndim == 3 and a.shape[2] == 3 and stretch_color_image is not None:
61
+ try:
62
+ return np.clip(stretch_color_image(a, 0.25, linked=False, normalize=False), 0.0, 1.0).astype(np.float32)
63
+ except Exception:
64
+ pass
65
+
66
+ # Mono (or fallback): simple robust stretch around median
67
+ # (keeps it predictable and fast; display-only)
68
+ m = float(np.nanmedian(a))
69
+ if not np.isfinite(m):
70
+ return a.astype(np.float32, copy=False)
71
+
72
+ # Simple gamma-like lift using median anchor
73
+ # If median is tiny, boost; if already bright, minimal change.
74
+ target = 0.25
75
+ eps = 1e-8
76
+ scale = target / max(m, eps)
77
+ out = np.clip(a * scale, 0.0, 1.0)
78
+
79
+ # Gentle midtone curve
80
+ out = np.sqrt(out)
81
+ return np.clip(out, 0.0, 1.0).astype(np.float32, copy=False)
82
+
51
83
 
52
84
  def _find_main_window(w):
53
85
  p = w
@@ -248,10 +280,14 @@ class MaskCanvas(QGraphicsView):
248
280
  super().__init__(parent)
249
281
  self.setRenderHint(QPainter.RenderHint.Antialiasing)
250
282
 
283
+ self._base_image01 = np.asarray(image01, dtype=np.float32)
284
+ self._display_stretch_enabled = False
285
+
251
286
  # scene + background image
252
287
  self.scene = QGraphicsScene(self)
253
288
  self.setScene(self.scene)
254
- self.bg_item = QGraphicsPixmapItem(_to_qpixmap01(image01))
289
+
290
+ self.bg_item = QGraphicsPixmapItem(_to_qpixmap01(self._base_image01))
255
291
  self.scene.addItem(self.bg_item)
256
292
 
257
293
  # --- NEW: basic zoom state ---
@@ -306,6 +342,38 @@ class MaskCanvas(QGraphicsView):
306
342
  super().wheelEvent(ev)
307
343
  # ----------------- END: Zoom API ---------------------
308
344
 
345
+ def set_display_stretch_enabled(self, enabled: bool):
346
+ enabled = bool(enabled)
347
+ if enabled == self._display_stretch_enabled:
348
+ return
349
+ self._display_stretch_enabled = enabled
350
+ self._refresh_background_pixmap(keep_view=True)
351
+
352
+ def display_stretch_enabled(self) -> bool:
353
+ return bool(self._display_stretch_enabled)
354
+
355
+ def current_display_image01(self) -> np.ndarray:
356
+ """Returns the image currently used for *display* (not for mask math)."""
357
+ if self._display_stretch_enabled:
358
+ return _display_stretch(self._base_image01)
359
+ return self._base_image01
360
+
361
+ def _refresh_background_pixmap(self, keep_view: bool = True):
362
+ # Preserve current view transform/center so toggling doesn't “jump”
363
+ old_transform = self.transform()
364
+ old_center = self.mapToScene(self.viewport().rect().center())
365
+
366
+ disp = self.current_display_image01()
367
+ self.bg_item.setPixmap(_to_qpixmap01(disp))
368
+
369
+ # Ensure scene rect still matches image pixels
370
+ self.setSceneRect(self.bg_item.boundingRect())
371
+
372
+ if keep_view:
373
+ self.setTransform(old_transform)
374
+ self.centerOn(old_center)
375
+
376
+
309
377
  def set_mode(self, mode: str):
310
378
  assert mode in ('polygon', 'ellipse', 'select')
311
379
  self.mode = mode
@@ -432,7 +500,7 @@ class MaskCanvas(QGraphicsView):
432
500
  class LivePreviewDialog(QDialog):
433
501
  def __init__(self, original_image01: np.ndarray, parent=None):
434
502
  super().__init__(parent)
435
- self.setWindowTitle("Live Mask Preview")
503
+ self.setWindowTitle(self.tr("Live Mask Preview"))
436
504
  self.label = QLabel(alignment=Qt.AlignmentFlag.AlignCenter)
437
505
  lay = QVBoxLayout(self); lay.addWidget(self.label)
438
506
  self.resize(300, 300)
@@ -453,39 +521,61 @@ class LivePreviewDialog(QDialog):
453
521
  Qt.AspectRatioMode.KeepAspectRatio,
454
522
  Qt.TransformationMode.SmoothTransformation))
455
523
 
524
+ def set_base_image(self, image01: np.ndarray):
525
+ self.base_pixmap = _to_qpixmap01(image01)
456
526
 
457
527
  # ---------- Preview (push-as-doc) ----------
458
-
459
528
  class MaskPreviewDialog(QDialog):
460
529
  """Scrollable preview + 'Push as New Document…'."""
461
530
  def __init__(self, mask01: np.ndarray, parent=None):
462
531
  super().__init__(parent)
463
- self.setWindowTitle("Mask Preview")
532
+ self.setWindowTitle(self.tr("Mask Preview"))
464
533
  self.mask = np.clip(mask01, 0, 1).astype(np.float32)
465
534
 
466
- self.scroll = QScrollArea(self); self.scroll.setWidgetResizable(False)
535
+ # --- drag-pan state ---
536
+ self._dragging = False
537
+ self._drag_start = None
538
+ self._h_start = 0
539
+ self._v_start = 0
540
+
541
+ # Build UI first
542
+ self.scroll = QScrollArea(self)
543
+ self.scroll.setWidgetResizable(False)
467
544
  self.scroll.setAlignment(Qt.AlignmentFlag.AlignCenter)
545
+
468
546
  self.label = QLabel(alignment=Qt.AlignmentFlag.AlignCenter)
469
547
  self.label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored)
470
- self.pixmap = self._to_pixmap(self.mask); self.label.setPixmap(self.pixmap)
548
+
549
+ self.pixmap = self._to_pixmap(self.mask)
550
+ self.label.setPixmap(self.pixmap)
551
+ self.label.resize(self.pixmap.size())
552
+
471
553
  self.scroll.setWidget(self.label)
472
554
 
555
+ # Enable mouse drag panning on the label (NOW label exists)
556
+ self.label.setMouseTracking(True)
557
+ self.label.installEventFilter(self)
558
+
473
559
  btns = QHBoxLayout()
474
560
  b_in = themed_toolbtn("zoom-in", "Zoom In")
475
561
  b_out = themed_toolbtn("zoom-out", "Zoom Out")
476
562
  b_fit = themed_toolbtn("zoom-fit-best", "Fit to Preview")
563
+ b_push = QPushButton(self.tr("Push as New Document…"))
477
564
 
478
-
479
- b_push = QPushButton("Push as New Document…")
480
565
  b_in.clicked.connect(lambda: self._zoom(1.2))
481
566
  b_out.clicked.connect(lambda: self._zoom(1/1.2))
482
567
  b_fit.clicked.connect(self._fit)
483
568
  b_push.clicked.connect(self.push_as_new_document)
569
+
484
570
  for b in (b_in, b_out, b_fit, b_push):
485
571
  btns.addWidget(b)
486
572
 
487
- lay = QVBoxLayout(self); lay.addWidget(self.scroll); lay.addLayout(btns)
488
- self.scale = 1.0; self.setMinimumSize(600, 400)
573
+ lay = QVBoxLayout(self)
574
+ lay.addWidget(self.scroll)
575
+ lay.addLayout(btns)
576
+
577
+ self.scale = 1.0
578
+ self.setMinimumSize(600, 400)
489
579
 
490
580
  def _to_pixmap(self, mask01: np.ndarray) -> QPixmap:
491
581
  m8 = (np.clip(mask01, 0, 1) * 255).astype(np.uint8)
@@ -495,17 +585,52 @@ class MaskPreviewDialog(QDialog):
495
585
 
496
586
  def _zoom(self, factor: float):
497
587
  self.scale *= factor
498
- scaled = self.pixmap.scaled(self.pixmap.size() * self.scale,
499
- Qt.AspectRatioMode.KeepAspectRatio,
500
- Qt.TransformationMode.SmoothTransformation)
501
- self.label.setPixmap(scaled); self.label.resize(scaled.size())
588
+ scaled = self.pixmap.scaled(
589
+ self.pixmap.size() * self.scale,
590
+ Qt.AspectRatioMode.KeepAspectRatio,
591
+ Qt.TransformationMode.SmoothTransformation
592
+ )
593
+ self.label.setPixmap(scaled)
594
+ self.label.resize(scaled.size())
502
595
 
503
596
  def _fit(self):
504
597
  vp = self.scroll.viewport().size()
505
598
  if self.pixmap.width() and self.pixmap.height():
506
599
  s = min(vp.width()/self.pixmap.width(), vp.height()/self.pixmap.height())
507
600
  self.scale = max(0.05, s)
508
- self._zoom(1.0)
601
+ # re-render at the new scale (don’t multiply again)
602
+ scaled = self.pixmap.scaled(
603
+ self.pixmap.size() * self.scale,
604
+ Qt.AspectRatioMode.KeepAspectRatio,
605
+ Qt.TransformationMode.SmoothTransformation
606
+ )
607
+ self.label.setPixmap(scaled)
608
+ self.label.resize(scaled.size())
609
+
610
+ def eventFilter(self, obj, ev):
611
+ if obj is self.label:
612
+ if ev.type() == QEvent.Type.MouseButtonPress and ev.button() == Qt.MouseButton.LeftButton:
613
+ self._dragging = True
614
+ self._drag_start = ev.globalPosition().toPoint()
615
+ self._h_start = self.scroll.horizontalScrollBar().value()
616
+ self._v_start = self.scroll.verticalScrollBar().value()
617
+ self.setCursor(Qt.CursorShape.ClosedHandCursor)
618
+ return True
619
+
620
+ if ev.type() == QEvent.Type.MouseMove and self._dragging:
621
+ p = ev.globalPosition().toPoint()
622
+ d = p - self._drag_start
623
+ self.scroll.horizontalScrollBar().setValue(self._h_start - d.x())
624
+ self.scroll.verticalScrollBar().setValue(self._v_start - d.y())
625
+ return True
626
+
627
+ if ev.type() == QEvent.Type.MouseButtonRelease and ev.button() == Qt.MouseButton.LeftButton:
628
+ self._dragging = False
629
+ self._drag_start = None
630
+ self.unsetCursor()
631
+ return True
632
+
633
+ return super().eventFilter(obj, ev)
509
634
 
510
635
  def push_as_new_document(self):
511
636
  if self.mask is None:
@@ -554,7 +679,10 @@ class MaskCreationDialog(QDialog):
554
679
  """Mask creation UI for SASpro documents (returns a np mask on OK)."""
555
680
  def __init__(self, image01: np.ndarray, parent=None, auto_push_on_ok: bool = True):
556
681
  super().__init__(parent)
557
- self.setWindowTitle("Mask Creation")
682
+ self.setWindowTitle(self.tr("Mask Creation"))
683
+ self.setWindowFlag(Qt.WindowType.Window, True)
684
+ self.setWindowModality(Qt.WindowModality.NonModal)
685
+ self.setModal(False)
558
686
  self.image = np.asarray(image01, dtype=np.float32).copy()
559
687
  self.mask: np.ndarray | None = None
560
688
  self.live_preview = LivePreviewDialog(self.image, parent=self)
@@ -572,9 +700,9 @@ class MaskCreationDialog(QDialog):
572
700
 
573
701
  # Mode toolbar
574
702
  mode_bar = QHBoxLayout()
575
- self.free_btn = QPushButton("Freehand"); self.free_btn.setCheckable(True)
576
- self.ellipse_btn = QPushButton("Ellipse"); self.ellipse_btn.setCheckable(True)
577
- self.select_btn = QPushButton("Select Entire Image"); self.select_btn.setCheckable(True)
703
+ self.free_btn = QPushButton(self.tr("Freehand")); self.free_btn.setCheckable(True)
704
+ self.ellipse_btn = QPushButton(self.tr("Ellipse")); self.ellipse_btn.setCheckable(True)
705
+ self.select_btn = QPushButton(self.tr("Select Entire Image")); self.select_btn.setCheckable(True)
578
706
  group = QButtonGroup(self); group.setExclusive(True)
579
707
  for b in (self.free_btn, self.ellipse_btn, self.select_btn):
580
708
  b.setAutoExclusive(True); group.addButton(b)
@@ -598,6 +726,18 @@ class MaskCreationDialog(QDialog):
598
726
  zoom_bar.addWidget(z_out); zoom_bar.addWidget(z_in); zoom_bar.addWidget(z_fit)
599
727
  layout.addLayout(zoom_bar)
600
728
 
729
+ # Display stretch toggle (display-only; never modifies image data)
730
+ self.btn_disp_stretch = QPushButton(self.tr("Toggle Display Stretch"))
731
+ self.btn_disp_stretch.setCheckable(True)
732
+ self.btn_disp_stretch.setToolTip(
733
+ "Display-only stretch for easier masking on linear images.\n"
734
+ "This does NOT change the image data or the generated mask."
735
+ )
736
+ self.btn_disp_stretch.toggled.connect(self._toggle_display_stretch)
737
+ self.btn_disp_stretch.setChecked(False)
738
+ self.btn_disp_stretch.setText("Enable Display Stretch")
739
+ zoom_bar.addWidget(self.btn_disp_stretch)
740
+
601
741
  # Canvas
602
742
  self.canvas = MaskCanvas(self.image)
603
743
  layout.addWidget(self.canvas, 1)
@@ -614,7 +754,7 @@ class MaskCreationDialog(QDialog):
614
754
  self.type_dd.currentTextChanged.connect(lambda t: setattr(self, 'mask_type', t))
615
755
  controls.addWidget(self.type_dd)
616
756
 
617
- controls.addWidget(QLabel("Edge Blur (px):"))
757
+ controls.addWidget(QLabel(self.tr("Edge Blur (px):")))
618
758
  self.blur_slider = QSlider(Qt.Orientation.Horizontal); self.blur_slider.setRange(0, 300)
619
759
  self.blur_slider.valueChanged.connect(lambda v: setattr(self, 'blur_amount', int(v)))
620
760
  controls.addWidget(self.blur_slider)
@@ -704,6 +844,24 @@ class MaskCreationDialog(QDialog):
704
844
  if self.link_cb.isChecked():
705
845
  self.upper_sl.setValue(v)
706
846
 
847
+ def _toggle_display_stretch(self, enabled: bool):
848
+ try:
849
+ self.canvas.set_display_stretch_enabled(bool(enabled))
850
+
851
+ # keep button label in sync
852
+ self.btn_disp_stretch.setText(
853
+ self.tr("Disable Display Stretch") if enabled else self.tr("Enable Display Stretch")
854
+ )
855
+
856
+ # Keep the live preview background in sync (Range Selection uses it)
857
+ if hasattr(self, "live_preview") and self.live_preview is not None:
858
+ self.live_preview.set_base_image(self.canvas.current_display_image01())
859
+ if self.live_preview.isVisible():
860
+ self._update_live_preview()
861
+ except Exception:
862
+ pass
863
+
864
+
707
865
  # ---- generators
708
866
  def _component_lightness(self) -> np.ndarray:
709
867
  if self.image.ndim == 3:
@@ -822,6 +822,11 @@ def _to_luma_local(a: np.ndarray) -> np.ndarray:
822
822
  return a
823
823
  # (H,W,3) or (3,H,W)
824
824
  if a.ndim == 3 and a.shape[-1] == 3:
825
+ try:
826
+ import cv2
827
+ return cv2.cvtColor(a, cv2.COLOR_RGB2GRAY).astype(np.float32, copy=False)
828
+ except Exception:
829
+ pass
825
830
  r, g, b = a[..., 0], a[..., 1], a[..., 2]
826
831
  return (0.2126*r + 0.7152*g + 0.0722*b).astype(np.float32, copy=False)
827
832
  if a.ndim == 3 and a.shape[0] == 3:
@@ -46,10 +46,9 @@ def apply_morphology(image: np.ndarray, *, operation: str = "erosion",
46
46
 
47
47
  if img.ndim == 3 and img.shape[2] == 3:
48
48
  u8 = (img * 255.0).astype(np.uint8)
49
- ch = cv2.split(u8)
50
- ch = [_do(c) for c in ch]
51
- out = cv2.merge(ch).astype(np.float32) / 255.0
52
- return np.clip(out, 0.0, 1.0)
49
+ # OpenCV morphology functions handle multi-channel images natively (independent channels)
50
+ out_u8 = _do(u8)
51
+ return (out_u8.astype(np.float32) / 255.0).clip(0.0, 1.0)
53
52
 
54
53
  raise ValueError("Input image must be mono (H,W)/(H,W,1) or RGB (H,W,3).")
55
54
 
@@ -91,7 +90,10 @@ class MorphologyDialogPro(QDialog):
91
90
 
92
91
  def __init__(self, parent, doc, icon: QIcon | None = None, initial: dict | None = None):
93
92
  super().__init__(parent)
94
- self.setWindowTitle("Morphological Operations")
93
+ self.setWindowTitle(self.tr("Morphological Operations"))
94
+ self.setWindowFlag(Qt.WindowType.Window, True)
95
+ self.setWindowModality(Qt.WindowModality.NonModal)
96
+ self.setModal(False)
95
97
  if icon:
96
98
  try: self.setWindowIcon(icon)
97
99
  except Exception as e:
@@ -109,7 +111,7 @@ class MorphologyDialogPro(QDialog):
109
111
  v = QVBoxLayout(self)
110
112
 
111
113
  # ---- Params (unchanged) ----
112
- grp = QGroupBox("Morphological Parameters")
114
+ grp = QGroupBox(self.tr("Morphological Parameters"))
113
115
  grid = QGridLayout(grp)
114
116
  self.cb_op = QComboBox(); self.cb_op.addItems(self.OPS)
115
117
  self.sp_kernel = QSpinBox(); self.sp_kernel.setRange(1, 31); self.sp_kernel.setSingleStep(2)
@@ -125,9 +127,9 @@ class MorphologyDialogPro(QDialog):
125
127
  self.sp_kernel.valueChanged.connect(self._debounce)
126
128
  self.sp_iter.valueChanged.connect(self._debounce)
127
129
 
128
- grid.addWidget(QLabel("Operation:"), 0, 0); grid.addWidget(self.cb_op, 0, 1, 1, 2)
129
- grid.addWidget(QLabel("Kernel size:"), 1, 0); grid.addWidget(self.sp_kernel, 1, 1)
130
- grid.addWidget(QLabel("Iterations:"), 2, 0); grid.addWidget(self.sp_iter, 2, 1)
130
+ grid.addWidget(QLabel(self.tr("Operation:")), 0, 0); grid.addWidget(self.cb_op, 0, 1, 1, 2)
131
+ grid.addWidget(QLabel(self.tr("Kernel size:")), 1, 0); grid.addWidget(self.sp_kernel, 1, 1)
132
+ grid.addWidget(QLabel(self.tr("Iterations:")), 2, 0); grid.addWidget(self.sp_iter, 2, 1)
131
133
  v.addWidget(grp)
132
134
 
133
135
  # ---- Preview with zoom/pan ----
@@ -158,9 +160,9 @@ class MorphologyDialogPro(QDialog):
158
160
 
159
161
  # ---- Buttons (unchanged) ----
160
162
  row = QHBoxLayout()
161
- btn_apply = QPushButton("Apply"); btn_apply.clicked.connect(self._apply)
162
- btn_reset = QPushButton("Reset"); btn_reset.clicked.connect(self._reset)
163
- btn_cancel= QPushButton("Cancel"); btn_cancel.clicked.connect(self.reject)
163
+ btn_apply = QPushButton(self.tr("Apply")); btn_apply.clicked.connect(self._apply)
164
+ btn_reset = QPushButton(self.tr("Reset")); btn_reset.clicked.connect(self._reset)
165
+ btn_cancel= QPushButton(self.tr("Cancel")); btn_cancel.clicked.connect(self.reject)
164
166
  row.addStretch(1); row.addWidget(btn_apply); row.addWidget(btn_reset); row.addWidget(btn_cancel)
165
167
  v.addLayout(row)
166
168
 
@@ -258,10 +260,33 @@ class MorphologyDialogPro(QDialog):
258
260
  pass
259
261
  # ────────────────────────────────────────────────────────────
260
262
 
261
- self.accept()
263
+ # Dialog stays open so user can apply to other images
264
+ # Refresh document reference for next operation
265
+ self._refresh_document_from_active()
262
266
  except Exception as e:
263
267
  QMessageBox.critical(self, "Morphology", f"Failed to apply:\n{e}")
264
268
 
269
+ def _refresh_document_from_active(self):
270
+ """
271
+ Refresh the dialog's document reference to the currently active document.
272
+ This allows reusing the same dialog on different images.
273
+ """
274
+ try:
275
+ main = self.parent()
276
+ if main and hasattr(main, "_active_doc"):
277
+ new_doc = main._active_doc()
278
+ if new_doc is not None and new_doc is not self.doc:
279
+ self.doc = new_doc
280
+ # Refresh preview for new document
281
+ self.orig = np.clip(np.asarray(new_doc.image, dtype=np.float32), 0.0, 1.0)
282
+ disp = self.orig
283
+ if disp.ndim == 2: disp = disp[..., None].repeat(3, axis=2)
284
+ elif disp.ndim == 3 and disp.shape[2] == 1: disp = disp.repeat(3, axis=2)
285
+ self._disp_base = disp
286
+ self._update_preview()
287
+ except Exception:
288
+ pass
289
+
265
290
 
266
291
 
267
292
  def _reset(self):