setiastrosuitepro 1.6.1.post1__py3-none-any.whl → 1.6.2__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.
- setiastro/images/Background_startup.jpg +0 -0
- setiastro/qml/ResourceMonitor.qml +126 -0
- setiastro/saspro/__main__.py +162 -25
- setiastro/saspro/_generated/build_info.py +2 -1
- setiastro/saspro/abe.py +62 -11
- setiastro/saspro/aberration_ai.py +3 -3
- setiastro/saspro/add_stars.py +5 -2
- setiastro/saspro/astrobin_exporter.py +3 -0
- setiastro/saspro/astrospike_python.py +3 -1
- setiastro/saspro/autostretch.py +4 -2
- setiastro/saspro/backgroundneutral.py +52 -10
- setiastro/saspro/batch_convert.py +3 -0
- setiastro/saspro/batch_renamer.py +3 -0
- setiastro/saspro/blemish_blaster.py +3 -0
- setiastro/saspro/cheat_sheet.py +50 -15
- setiastro/saspro/clahe.py +27 -1
- setiastro/saspro/comet_stacking.py +103 -38
- setiastro/saspro/convo.py +3 -0
- setiastro/saspro/copyastro.py +3 -0
- setiastro/saspro/cosmicclarity.py +70 -45
- setiastro/saspro/crop_dialog_pro.py +17 -0
- setiastro/saspro/curve_editor_pro.py +18 -0
- setiastro/saspro/debayer.py +3 -0
- setiastro/saspro/doc_manager.py +39 -16
- setiastro/saspro/fitsmodifier.py +3 -0
- setiastro/saspro/frequency_separation.py +8 -2
- setiastro/saspro/function_bundle.py +2 -0
- setiastro/saspro/generate_translations.py +715 -1
- setiastro/saspro/ghs_dialog_pro.py +3 -0
- setiastro/saspro/graxpert.py +3 -0
- setiastro/saspro/gui/main_window.py +272 -29
- setiastro/saspro/gui/mixins/dock_mixin.py +100 -1
- setiastro/saspro/gui/mixins/file_mixin.py +7 -0
- setiastro/saspro/gui/mixins/menu_mixin.py +28 -0
- setiastro/saspro/gui/statistics_dialog.py +47 -0
- setiastro/saspro/halobgon.py +29 -3
- setiastro/saspro/histogram.py +3 -0
- setiastro/saspro/history_explorer.py +2 -0
- setiastro/saspro/i18n.py +22 -10
- setiastro/saspro/image_combine.py +3 -0
- setiastro/saspro/image_peeker_pro.py +3 -0
- setiastro/saspro/imageops/stretch.py +5 -13
- setiastro/saspro/isophote.py +3 -0
- setiastro/saspro/legacy/numba_utils.py +64 -47
- setiastro/saspro/linear_fit.py +3 -0
- setiastro/saspro/live_stacking.py +13 -2
- setiastro/saspro/mask_creation.py +3 -0
- setiastro/saspro/mfdeconv.py +5 -0
- setiastro/saspro/morphology.py +30 -5
- setiastro/saspro/multiscale_decomp.py +3 -0
- setiastro/saspro/nbtorgb_stars.py +12 -2
- setiastro/saspro/numba_utils.py +148 -47
- setiastro/saspro/ops/scripts.py +77 -17
- setiastro/saspro/ops/settings.py +1 -43
- setiastro/saspro/perfect_palette_picker.py +1 -0
- setiastro/saspro/pixelmath.py +6 -2
- setiastro/saspro/plate_solver.py +1 -0
- setiastro/saspro/remove_green.py +18 -1
- setiastro/saspro/remove_stars.py +136 -162
- setiastro/saspro/resources.py +7 -0
- setiastro/saspro/rgb_combination.py +1 -0
- setiastro/saspro/rgbalign.py +4 -4
- setiastro/saspro/save_options.py +1 -0
- setiastro/saspro/sfcc.py +50 -8
- setiastro/saspro/signature_insert.py +3 -0
- setiastro/saspro/stacking_suite.py +630 -341
- setiastro/saspro/star_alignment.py +16 -1
- setiastro/saspro/star_spikes.py +116 -32
- setiastro/saspro/star_stretch.py +38 -1
- setiastro/saspro/stat_stretch.py +35 -3
- setiastro/saspro/subwindow.py +63 -2
- setiastro/saspro/supernovaasteroidhunter.py +3 -0
- setiastro/saspro/translations/all_source_strings.json +3654 -0
- setiastro/saspro/translations/ar_translations.py +3865 -0
- setiastro/saspro/translations/de_translations.py +16 -0
- setiastro/saspro/translations/es_translations.py +16 -0
- setiastro/saspro/translations/fr_translations.py +16 -0
- setiastro/saspro/translations/hi_translations.py +3571 -0
- setiastro/saspro/translations/integrate_translations.py +36 -0
- setiastro/saspro/translations/it_translations.py +16 -0
- setiastro/saspro/translations/ja_translations.py +16 -0
- setiastro/saspro/translations/pt_translations.py +16 -0
- setiastro/saspro/translations/ru_translations.py +2848 -0
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +255 -0
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +3 -3
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +3 -3
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +3 -3
- setiastro/saspro/translations/saspro_hi.qm +0 -0
- setiastro/saspro/translations/saspro_hi.ts +257 -0
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +3 -3
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +4 -4
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +3 -3
- setiastro/saspro/translations/saspro_ru.qm +0 -0
- setiastro/saspro/translations/saspro_ru.ts +237 -0
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +257 -0
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +10771 -0
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +3 -3
- setiastro/saspro/translations/sw_translations.py +3671 -0
- setiastro/saspro/translations/uk_translations.py +3700 -0
- setiastro/saspro/translations/zh_translations.py +16 -0
- setiastro/saspro/versioning.py +36 -5
- setiastro/saspro/view_bundle.py +3 -0
- setiastro/saspro/wavescale_hdr.py +22 -1
- setiastro/saspro/wavescalede.py +23 -1
- setiastro/saspro/whitebalance.py +39 -3
- setiastro/saspro/widgets/minigame/game.js +986 -0
- setiastro/saspro/widgets/minigame/index.html +53 -0
- setiastro/saspro/widgets/minigame/style.css +241 -0
- setiastro/saspro/widgets/resource_monitor.py +237 -0
- setiastro/saspro/widgets/wavelet_utils.py +52 -20
- setiastro/saspro/wimi.py +35 -15
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.2.dist-info}/METADATA +15 -4
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.2.dist-info}/RECORD +127 -104
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.2.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.2.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.2.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.2.dist-info}/licenses/license.txt +0 -0
|
@@ -24,6 +24,7 @@ def _make_executor(max_workers: int):
|
|
|
24
24
|
# return ProcessPoolExecutor(max_workers=max_workers)
|
|
25
25
|
return ThreadPoolExecutor(max_workers=max_workers)
|
|
26
26
|
|
|
27
|
+
|
|
27
28
|
import gc # For explicit memory cleanup after heavy operations
|
|
28
29
|
import os as _os
|
|
29
30
|
import threading as _threading
|
|
@@ -286,8 +287,18 @@ def _warp_like_ref(target_img: np.ndarray, M_2x3: np.ndarray, ref_shape_hw: tupl
|
|
|
286
287
|
return cv2.warpAffine(target_img, M_2x3, (W, H),
|
|
287
288
|
flags=cv2.INTER_LANCZOS4, borderMode=cv2.BORDER_CONSTANT, borderValue=0)
|
|
288
289
|
|
|
290
|
+
# Optimization: If standard RGB/BGR (3 channels) or 4 channels, OpenCV handles it natively.
|
|
291
|
+
# Note: OpenCV warpAffine support n-channel images, but typically 1, 3, or 4.
|
|
292
|
+
C = target_img.shape[2]
|
|
293
|
+
if C <= 4:
|
|
294
|
+
if not target_img.flags['C_CONTIGUOUS']:
|
|
295
|
+
target_img = np.ascontiguousarray(target_img)
|
|
296
|
+
return cv2.warpAffine(target_img, M_2x3, (W, H),
|
|
297
|
+
flags=cv2.INTER_LANCZOS4, borderMode=cv2.BORDER_CONSTANT, borderValue=0)
|
|
298
|
+
|
|
299
|
+
# Fallback for >4 channels (e.g. hyperspectral or special stacks)
|
|
289
300
|
chs = []
|
|
290
|
-
for i in range(
|
|
301
|
+
for i in range(C):
|
|
291
302
|
ch = target_img[..., i]
|
|
292
303
|
if not ch.flags['C_CONTIGUOUS']:
|
|
293
304
|
ch = np.ascontiguousarray(ch)
|
|
@@ -618,6 +629,7 @@ class StellarAlignmentDialog(QDialog):
|
|
|
618
629
|
super().__init__(parent)
|
|
619
630
|
self.setWindowTitle("Stellar Alignment")
|
|
620
631
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
632
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
621
633
|
self.setModal(False)
|
|
622
634
|
#self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
623
635
|
|
|
@@ -4639,6 +4651,9 @@ class MosaicMasterDialog(QDialog):
|
|
|
4639
4651
|
self._list_open_docs_fn = list_open_docs_fn
|
|
4640
4652
|
|
|
4641
4653
|
self.setWindowTitle("Mosaic Master")
|
|
4654
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
4655
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
4656
|
+
self.setModal(False)
|
|
4642
4657
|
self.wrench_path = wrench_path
|
|
4643
4658
|
self.spinner_path = spinner_path
|
|
4644
4659
|
|
setiastro/saspro/star_spikes.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pro/tools/star_spikes.py
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
import numpy as np
|
|
4
|
-
|
|
4
|
+
import math
|
|
5
5
|
from PyQt6.QtCore import Qt
|
|
6
6
|
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QSplitter, QSizePolicy, QWidget, QApplication,
|
|
7
7
|
QFormLayout, QGroupBox, QDoubleSpinBox, QSpinBox,
|
|
@@ -50,6 +50,9 @@ class StarSpikesDialogPro(QDialog):
|
|
|
50
50
|
spinner_path: str | None = None):
|
|
51
51
|
super().__init__(parent)
|
|
52
52
|
self.setWindowTitle(self.tr("Diffraction Spikes"))
|
|
53
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
54
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
55
|
+
self.setModal(False)
|
|
53
56
|
self.docman = doc_manager
|
|
54
57
|
self.doc = initial_doc or (self.docman.get_active_document() if self.docman else None)
|
|
55
58
|
self.jwstpupil_path = jwstpupil_path
|
|
@@ -381,6 +384,13 @@ class StarSpikesDialogPro(QDialog):
|
|
|
381
384
|
shrink_max = self.advanced["shrink_max"]
|
|
382
385
|
color_boost = self.color_boost.value()
|
|
383
386
|
|
|
387
|
+
# Try OpenCV for faster zoom/blur
|
|
388
|
+
try:
|
|
389
|
+
import cv2
|
|
390
|
+
_HAS_CV2 = True
|
|
391
|
+
except ImportError:
|
|
392
|
+
_HAS_CV2 = False
|
|
393
|
+
|
|
384
394
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
385
395
|
def star_runner(x, y, flux, a, b):
|
|
386
396
|
brightness = np.clip(np.log1p(flux)/8.0, 0.1, 3.0)
|
|
@@ -388,39 +398,79 @@ class StarSpikesDialogPro(QDialog):
|
|
|
388
398
|
tile_size = min(tile_size, 768)
|
|
389
399
|
tile_size += tile_size % 2
|
|
390
400
|
pad = tile_size // 2
|
|
391
|
-
|
|
401
|
+
|
|
402
|
+
# Guard against fully out-of-bounds, but allow partial overlaps
|
|
403
|
+
if not (0 <= x < W and 0 <= y < H):
|
|
392
404
|
return None
|
|
393
405
|
|
|
406
|
+
# Measure star color
|
|
394
407
|
r_ratio, g_ratio, b_ratio = self._measure_star_color(img, x, y, sampling_radius=3)
|
|
408
|
+
|
|
409
|
+
# Extract PSF tiles
|
|
395
410
|
tile_r = self._extract_center_tile(psf_r, tile_size) * brightness * r_ratio * color_boost
|
|
396
411
|
tile_g = self._extract_center_tile(psf_g, tile_size) * brightness * g_ratio * color_boost
|
|
397
412
|
tile_b = self._extract_center_tile(psf_b, tile_size) * brightness * b_ratio * color_boost
|
|
398
413
|
|
|
414
|
+
# Boost/Shrink
|
|
399
415
|
b_scale, s_factor = self._boost_shrink_from_flux(flux, self.flux_min.value(), flux_max,
|
|
400
416
|
bscale_min, bscale_max, shrink_min, shrink_max)
|
|
401
|
-
final_r = self._shrink_and_boost(tile_r, b_scale, s_factor)
|
|
402
|
-
final_g = self._shrink_and_boost(tile_g, b_scale, s_factor)
|
|
403
|
-
final_b = self._shrink_and_boost(tile_b, b_scale, s_factor)
|
|
404
|
-
|
|
405
|
-
new_size = final_r.shape[0]
|
|
406
|
-
pad_new = new_size // 2
|
|
407
|
-
y0, y1 = y - pad_new, y - pad_new + new_size
|
|
408
|
-
x0, x1 = x - pad_new, x - pad_new + new_size
|
|
409
|
-
if (y0 < 0 or y1 > H or x0 < 0 or x1 > W):
|
|
410
|
-
return None
|
|
411
417
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
418
|
+
# --- Fast Resize (Zoom) ---
|
|
419
|
+
def _fast_zoom(arr, z):
|
|
420
|
+
if z == 1.0: return arr
|
|
421
|
+
if _HAS_CV2:
|
|
422
|
+
h, w = arr.shape
|
|
423
|
+
nw, nh = int(round(w * z)), int(round(h * z))
|
|
424
|
+
if nw <= 0 or nh <= 0: return np.zeros((2,2), dtype=np.float32)
|
|
425
|
+
return cv2.resize(arr, (nw, nh), interpolation=cv2.INTER_LINEAR)
|
|
426
|
+
else:
|
|
427
|
+
return ndi.zoom(arr, z, order=1)
|
|
428
|
+
|
|
429
|
+
final_r = np.clip(_fast_zoom(tile_r * b_scale, 1.0/s_factor), 0.0, 1.0)
|
|
430
|
+
final_g = np.clip(_fast_zoom(tile_g * b_scale, 1.0/s_factor), 0.0, 1.0)
|
|
431
|
+
final_b = np.clip(_fast_zoom(tile_b * b_scale, 1.0/s_factor), 0.0, 1.0)
|
|
432
|
+
|
|
433
|
+
# --- Return Patch Data (y, x, patch) ---
|
|
434
|
+
new_h, new_w = final_r.shape
|
|
435
|
+
|
|
436
|
+
# Coords of the *patch top-left* relative to the image
|
|
437
|
+
# The star is at (x,y), and the patch center is approx (new_w//2, new_h//2)
|
|
438
|
+
# We want to center the patch on the star.
|
|
439
|
+
py0 = y - (new_h // 2)
|
|
440
|
+
px0 = x - (new_w // 2)
|
|
441
|
+
|
|
442
|
+
# Combine channels
|
|
443
|
+
patch = np.dstack((final_r, final_g, final_b)).astype(np.float32)
|
|
444
|
+
return (int(py0), int(px0), patch)
|
|
417
445
|
|
|
418
446
|
with ThreadPoolExecutor() as ex:
|
|
419
447
|
futs = [ex.submit(star_runner, *s) for s in stars]
|
|
420
448
|
for f in as_completed(futs):
|
|
421
|
-
|
|
422
|
-
if
|
|
423
|
-
|
|
449
|
+
res = f.result()
|
|
450
|
+
if res is None:
|
|
451
|
+
continue
|
|
452
|
+
|
|
453
|
+
py0, px0, patch = res
|
|
454
|
+
ph, pw, _ = patch.shape
|
|
455
|
+
|
|
456
|
+
# Calculate intersection with canvas
|
|
457
|
+
y_start = max(0, py0)
|
|
458
|
+
y_end = min(H, py0 + ph)
|
|
459
|
+
x_start = max(0, px0)
|
|
460
|
+
x_end = min(W, px0 + pw)
|
|
461
|
+
|
|
462
|
+
# If no overlap, skip
|
|
463
|
+
if y_start >= y_end or x_start >= x_end:
|
|
464
|
+
continue
|
|
465
|
+
|
|
466
|
+
# Offsets into the patch
|
|
467
|
+
patch_y_start = y_start - py0
|
|
468
|
+
patch_y_end = patch_y_start + (y_end - y_start)
|
|
469
|
+
patch_x_start = x_start - px0
|
|
470
|
+
patch_x_end = patch_x_start + (x_end - x_start)
|
|
471
|
+
|
|
472
|
+
# Add to canvas
|
|
473
|
+
canvas[y_start:y_end, x_start:x_end] += patch[patch_y_start:patch_y_end, patch_x_start:patch_x_end]
|
|
424
474
|
|
|
425
475
|
self.status.setText("Compositing…")
|
|
426
476
|
QApplication.processEvents()
|
|
@@ -543,14 +593,56 @@ class StarSpikesDialogPro(QDialog):
|
|
|
543
593
|
return img
|
|
544
594
|
|
|
545
595
|
def _simulate_psf(self, pupil, wavelength_scale=1.0, blur_sigma=1.0):
|
|
546
|
-
|
|
596
|
+
# Try to use OpenCV for speed
|
|
597
|
+
if getattr(self, "_cv2_checked", False):
|
|
598
|
+
has_cv2 = True
|
|
599
|
+
import cv2
|
|
600
|
+
else:
|
|
601
|
+
try:
|
|
602
|
+
import cv2
|
|
603
|
+
has_cv2 = True
|
|
604
|
+
except ImportError:
|
|
605
|
+
has_cv2 = False
|
|
606
|
+
self._cv2_checked = has_cv2
|
|
607
|
+
|
|
608
|
+
if has_cv2:
|
|
609
|
+
# Gaussian blur on pupil
|
|
610
|
+
# kernel size usually ~6*sigma, must be odd
|
|
611
|
+
k_pupil = int(math.ceil(6 * (0.1 * wavelength_scale))) | 1
|
|
612
|
+
sp = cv2.GaussianBlur(pupil, (k_pupil, k_pupil), 0.1 * wavelength_scale)
|
|
613
|
+
else:
|
|
614
|
+
sp = gaussian_filter(pupil, sigma=0.1 * wavelength_scale)
|
|
615
|
+
|
|
547
616
|
fft = np.fft.fftshift(np.fft.fft2(np.fft.ifftshift(sp)))
|
|
548
617
|
intensity = np.abs(fft)**2
|
|
549
618
|
intensity /= (intensity.max() + 1e-8)
|
|
550
|
-
|
|
619
|
+
|
|
620
|
+
if has_cv2 and blur_sigma > 0:
|
|
621
|
+
k_blur = int(math.ceil(6 * blur_sigma)) | 1
|
|
622
|
+
blurred = cv2.GaussianBlur(intensity, (k_blur, k_blur), blur_sigma)
|
|
623
|
+
else:
|
|
624
|
+
blurred = gaussian_filter(intensity, sigma=blur_sigma)
|
|
625
|
+
|
|
551
626
|
psf = blurred / max(blurred.max(), 1e-8)
|
|
627
|
+
|
|
552
628
|
if wavelength_scale != 1.0:
|
|
553
|
-
|
|
629
|
+
if has_cv2:
|
|
630
|
+
h, w = psf.shape
|
|
631
|
+
# Zoom uses size, NOT scale factor in resize(..., dsize=(w,h))
|
|
632
|
+
# wavelength_scale > 1 => zoom in => crop middle? or simply scale?
|
|
633
|
+
# The original used ndi.zoom(psf, zoom=wavelength_scale).
|
|
634
|
+
# New size:
|
|
635
|
+
nw, nh = int(round(w * wavelength_scale)), int(round(h * wavelength_scale))
|
|
636
|
+
if nw > 0 and nh > 0:
|
|
637
|
+
scaled = cv2.resize(psf, (nw, nh), interpolation=cv2.INTER_LINEAR)
|
|
638
|
+
# We might need to crop back to original size or pad?
|
|
639
|
+
# ndi.zoom changes the array size.
|
|
640
|
+
# The simulator seems to assume we handle whatever size comes out?
|
|
641
|
+
# Let's check _extract_center_tile usage.
|
|
642
|
+
psf = scaled
|
|
643
|
+
else:
|
|
644
|
+
psf = ndi.zoom(psf, zoom=wavelength_scale, order=1)
|
|
645
|
+
|
|
554
646
|
psf /= psf.max() + 1e-12
|
|
555
647
|
return psf
|
|
556
648
|
|
|
@@ -591,15 +683,7 @@ class StarSpikesDialogPro(QDialog):
|
|
|
591
683
|
stars.append((int(obj['x']), int(obj['y']), float(flux), float(a), float(b)))
|
|
592
684
|
return stars
|
|
593
685
|
|
|
594
|
-
|
|
595
|
-
def _shrink_and_boost(tile, brightness_scale=2.0, shrink_factor=1.5):
|
|
596
|
-
tile = np.clip(tile * float(brightness_scale), 0.0, 1.0)
|
|
597
|
-
in_sz = tile.shape[0]
|
|
598
|
-
out_sz = int(in_sz // float(shrink_factor))
|
|
599
|
-
out_sz += out_sz % 2
|
|
600
|
-
if out_sz <= 0: out_sz = 2
|
|
601
|
-
z = out_sz / float(in_sz)
|
|
602
|
-
return np.clip(ndi.zoom(tile, z, order=1), 0.0, 1.0)
|
|
686
|
+
# _shrink_and_boost removed (replaced by inline _fast_zoom for performance)
|
|
603
687
|
|
|
604
688
|
@staticmethod
|
|
605
689
|
def _boost_shrink_from_flux(flux, flux_min, flux_max, bmin, bmax, smin, smax):
|
setiastro/saspro/star_stretch.py
CHANGED
|
@@ -127,8 +127,16 @@ class StarStretchDialog(QDialog):
|
|
|
127
127
|
def __init__(self, parent, document):
|
|
128
128
|
super().__init__(parent)
|
|
129
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
|
|
130
134
|
self.doc = document
|
|
131
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)
|
|
132
140
|
self._pix: QPixmap | None = None
|
|
133
141
|
self._zoom = 0.25
|
|
134
142
|
self._panning = False
|
|
@@ -225,6 +233,15 @@ class StarStretchDialog(QDialog):
|
|
|
225
233
|
# initialize preview with current doc image
|
|
226
234
|
self._update_preview_pix(self.doc.image)
|
|
227
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
|
+
|
|
228
245
|
# --- UI change handlers ---
|
|
229
246
|
def _on_stretch_changed(self, v: int):
|
|
230
247
|
self.lbl_st.setText(f"Stretch Amount: {v/100.0:.2f}")
|
|
@@ -325,7 +342,27 @@ class StarStretchDialog(QDialog):
|
|
|
325
342
|
except Exception as e:
|
|
326
343
|
QMessageBox.critical(self, "Apply failed", str(e))
|
|
327
344
|
return
|
|
328
|
-
|
|
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
|
|
329
366
|
|
|
330
367
|
|
|
331
368
|
# --- preview rendering ---
|
setiastro/saspro/stat_stretch.py
CHANGED
|
@@ -25,13 +25,18 @@ class StatisticalStretchDialog(QDialog):
|
|
|
25
25
|
# --- IMPORTANT: avoid “attached modal” behavior on some Linux WMs ---
|
|
26
26
|
# Make this a proper top-level window (tool-style) rather than an attached sheet.
|
|
27
27
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
28
|
-
#
|
|
29
|
-
self.setWindowModality(Qt.WindowModality.
|
|
28
|
+
# Non-modal: allow user to switch between images while dialog is open
|
|
29
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
30
30
|
# Don’t let the generic modal flag override the explicit modality
|
|
31
31
|
self.setModal(False)
|
|
32
32
|
|
|
33
|
+
self._main = parent
|
|
33
34
|
self.doc = document
|
|
34
35
|
self._last_preview = None
|
|
36
|
+
|
|
37
|
+
# Connect to active document change signal
|
|
38
|
+
if hasattr(self._main, "currentDocumentChanged"):
|
|
39
|
+
self._main.currentDocumentChanged.connect(self._on_active_doc_changed)
|
|
35
40
|
self._panning = False
|
|
36
41
|
self._pan_last = None # QPoint
|
|
37
42
|
self._preview_scale = 1.0 # NEW: zoom factor for preview
|
|
@@ -329,6 +334,14 @@ class StatisticalStretchDialog(QDialog):
|
|
|
329
334
|
self._preview_qimg = qimg
|
|
330
335
|
self._apply_current_zoom()
|
|
331
336
|
|
|
337
|
+
# ----- active document change -----
|
|
338
|
+
def _on_active_doc_changed(self, doc):
|
|
339
|
+
"""Called when user clicks a different image window."""
|
|
340
|
+
if doc is None or getattr(doc, "image", None) is None:
|
|
341
|
+
return
|
|
342
|
+
self.doc = doc
|
|
343
|
+
self._populate_initial_preview()
|
|
344
|
+
|
|
332
345
|
# ----- slots -----
|
|
333
346
|
def _populate_initial_preview(self):
|
|
334
347
|
# show the current (unstretched) image as baseline
|
|
@@ -433,12 +446,31 @@ class StatisticalStretchDialog(QDialog):
|
|
|
433
446
|
# optional debug
|
|
434
447
|
print("Statistical Stretch: replay recording suppressed for this apply()")
|
|
435
448
|
|
|
436
|
-
|
|
449
|
+
# Dialog stays open so user can apply to other images
|
|
450
|
+
# Update the document reference to reflect the now-active document
|
|
451
|
+
self._refresh_document_from_active()
|
|
437
452
|
|
|
438
453
|
|
|
439
454
|
except Exception as e:
|
|
440
455
|
QMessageBox.critical(self, "Apply failed", str(e))
|
|
441
456
|
|
|
457
|
+
def _refresh_document_from_active(self):
|
|
458
|
+
"""
|
|
459
|
+
Refresh the dialog's document reference to the currently active document.
|
|
460
|
+
This allows reusing the same dialog on different images.
|
|
461
|
+
"""
|
|
462
|
+
try:
|
|
463
|
+
main = self.parent()
|
|
464
|
+
if main and hasattr(main, "_active_doc"):
|
|
465
|
+
new_doc = main._active_doc()
|
|
466
|
+
if new_doc is not None and new_doc is not self.doc:
|
|
467
|
+
self.doc = new_doc
|
|
468
|
+
# Reset preview state for new document
|
|
469
|
+
self._last_preview = None
|
|
470
|
+
self._preview_qimg = None
|
|
471
|
+
except Exception:
|
|
472
|
+
pass
|
|
473
|
+
|
|
442
474
|
|
|
443
475
|
def _update_preview_scaled(self):
|
|
444
476
|
if self._preview_qimg is None:
|
setiastro/saspro/subwindow.py
CHANGED
|
@@ -371,7 +371,8 @@ class ImageSubWindow(QWidget):
|
|
|
371
371
|
# pixel readout live-probe state
|
|
372
372
|
self._space_down = False
|
|
373
373
|
self._readout_dragging = False
|
|
374
|
-
|
|
374
|
+
# Pinch gesture state (macOS trackpad)
|
|
375
|
+
self._gesture_zoom_start = None
|
|
375
376
|
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
|
376
377
|
|
|
377
378
|
# Title (doc/view) sync
|
|
@@ -484,6 +485,7 @@ class ImageSubWindow(QWidget):
|
|
|
484
485
|
|
|
485
486
|
self.scroll = QScrollArea(full_host)
|
|
486
487
|
self.scroll.setWidgetResizable(False)
|
|
488
|
+
self.scroll.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
487
489
|
self.label = QLabel(alignment=Qt.AlignmentFlag.AlignCenter)
|
|
488
490
|
self.scroll.setWidget(self.label)
|
|
489
491
|
self.scroll.viewport().setMouseTracking(True)
|
|
@@ -2526,6 +2528,43 @@ class ImageSubWindow(QWidget):
|
|
|
2526
2528
|
p = p.parent()
|
|
2527
2529
|
return p
|
|
2528
2530
|
|
|
2531
|
+
def event(self, e):
|
|
2532
|
+
"""Override event() to handle native macOS gestures (pinch zoom)."""
|
|
2533
|
+
# Handle native gestures (macOS trackpad pinch zoom)
|
|
2534
|
+
if e.type() == QEvent.Type.NativeGesture:
|
|
2535
|
+
gesture_type = e.gestureType()
|
|
2536
|
+
|
|
2537
|
+
if gesture_type == Qt.NativeGestureType.BeginNativeGesture:
|
|
2538
|
+
# Start of pinch gesture - store initial scale
|
|
2539
|
+
self._gesture_zoom_start = self.scale
|
|
2540
|
+
e.accept()
|
|
2541
|
+
return True
|
|
2542
|
+
|
|
2543
|
+
elif gesture_type == Qt.NativeGestureType.ZoomNativeGesture:
|
|
2544
|
+
# Ongoing pinch zoom - value() is cumulative scale factor
|
|
2545
|
+
# Typical values: -0.5 to +0.5 for moderate pinches
|
|
2546
|
+
zoom_delta = e.value()
|
|
2547
|
+
|
|
2548
|
+
# Convert delta to zoom factor
|
|
2549
|
+
# Use smaller multiplier for smoother feel (0.5x damping)
|
|
2550
|
+
factor = 1.0 + (zoom_delta * 0.5)
|
|
2551
|
+
|
|
2552
|
+
# Apply incremental zoom
|
|
2553
|
+
self._zoom_at_anchor(factor)
|
|
2554
|
+
e.accept()
|
|
2555
|
+
return True
|
|
2556
|
+
|
|
2557
|
+
elif gesture_type == Qt.NativeGestureType.EndNativeGesture:
|
|
2558
|
+
# End of pinch gesture - cleanup
|
|
2559
|
+
self._gesture_zoom_start = None
|
|
2560
|
+
e.accept()
|
|
2561
|
+
return True
|
|
2562
|
+
|
|
2563
|
+
# Let parent handle all other events
|
|
2564
|
+
return super().event(e)
|
|
2565
|
+
|
|
2566
|
+
|
|
2567
|
+
|
|
2529
2568
|
def eventFilter(self, obj, ev):
|
|
2530
2569
|
is_on_view = (obj is self.label) or (obj is self.scroll.viewport())
|
|
2531
2570
|
|
|
@@ -2557,7 +2596,29 @@ class ImageSubWindow(QWidget):
|
|
|
2557
2596
|
# 1) Ctrl + wheel → zoom
|
|
2558
2597
|
if ev.type() == QEvent.Type.Wheel:
|
|
2559
2598
|
if ev.modifiers() & Qt.KeyboardModifier.ControlModifier:
|
|
2560
|
-
|
|
2599
|
+
# Try pixelDelta first (macOS trackpad gives smooth values)
|
|
2600
|
+
dy = ev.pixelDelta().y()
|
|
2601
|
+
|
|
2602
|
+
if dy != 0:
|
|
2603
|
+
# Smooth trackpad scrolling: use smaller base factor
|
|
2604
|
+
# Scale proportionally to delta magnitude for natural feel
|
|
2605
|
+
# Typical trackpad deltas are 1-10 pixels per event
|
|
2606
|
+
abs_dy = abs(dy)
|
|
2607
|
+
if abs_dy <= 3:
|
|
2608
|
+
base_factor = 1.01 # Very gentle for tiny movements
|
|
2609
|
+
elif abs_dy <= 10:
|
|
2610
|
+
base_factor = 1.02 # Gentle for small movements
|
|
2611
|
+
else:
|
|
2612
|
+
base_factor = 1.03 # Moderate for larger gestures
|
|
2613
|
+
|
|
2614
|
+
factor = base_factor if dy > 0 else 1/base_factor
|
|
2615
|
+
else:
|
|
2616
|
+
# Traditional mouse wheel: use angleDelta with moderate factor
|
|
2617
|
+
dy = ev.angleDelta().y()
|
|
2618
|
+
if dy == 0:
|
|
2619
|
+
return True
|
|
2620
|
+
# Use 1.15 for mouse wheel (gentler than original 1.25)
|
|
2621
|
+
factor = 1.15 if dy > 0 else 1/1.15
|
|
2561
2622
|
self._zoom_at_anchor(factor)
|
|
2562
2623
|
return True
|
|
2563
2624
|
return False
|
|
@@ -362,6 +362,9 @@ class SupernovaAsteroidHunterDialog(QDialog):
|
|
|
362
362
|
supernova_path=None, wrench_path=None, spinner_path=None):
|
|
363
363
|
super().__init__(parent)
|
|
364
364
|
self.setWindowTitle(self.tr("Supernova / Asteroid Hunter"))
|
|
365
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
366
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
367
|
+
self.setModal(False)
|
|
365
368
|
if supernova_path:
|
|
366
369
|
self.setWindowIcon(QIcon(supernova_path))
|
|
367
370
|
# keep icon path for previews
|