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.
- setiastro/data/SASP_data.fits +0 -0
- setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
- setiastro/data/catalogs/astrobin_filters.csv +890 -0
- setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
- setiastro/data/catalogs/cali2.csv +63 -0
- setiastro/data/catalogs/cali2color.csv +65 -0
- setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
- setiastro/data/catalogs/celestial_catalog.csv +24031 -0
- setiastro/data/catalogs/detected_stars.csv +24784 -0
- setiastro/data/catalogs/fits_header_data.csv +46 -0
- setiastro/data/catalogs/test.csv +8 -0
- setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
- setiastro/images/Astro_Spikes.png +0 -0
- setiastro/images/Background_startup.jpg +0 -0
- setiastro/images/HRDiagram.png +0 -0
- setiastro/images/LExtract.png +0 -0
- setiastro/images/LInsert.png +0 -0
- setiastro/images/Oxygenation-atm-2.svg.png +0 -0
- setiastro/images/RGB080604.png +0 -0
- setiastro/images/abeicon.png +0 -0
- setiastro/images/aberration.png +0 -0
- setiastro/images/andromedatry.png +0 -0
- setiastro/images/andromedatry_satellited.png +0 -0
- setiastro/images/annotated.png +0 -0
- setiastro/images/aperture.png +0 -0
- setiastro/images/astrosuite.ico +0 -0
- setiastro/images/astrosuite.png +0 -0
- setiastro/images/astrosuitepro.icns +0 -0
- setiastro/images/astrosuitepro.ico +0 -0
- setiastro/images/astrosuitepro.png +0 -0
- setiastro/images/background.png +0 -0
- setiastro/images/background2.png +0 -0
- setiastro/images/benchmark.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
- setiastro/images/blaster.png +0 -0
- setiastro/images/blink.png +0 -0
- setiastro/images/clahe.png +0 -0
- setiastro/images/collage.png +0 -0
- setiastro/images/colorwheel.png +0 -0
- setiastro/images/contsub.png +0 -0
- setiastro/images/convo.png +0 -0
- setiastro/images/copyslot.png +0 -0
- setiastro/images/cosmic.png +0 -0
- setiastro/images/cosmicsat.png +0 -0
- setiastro/images/crop1.png +0 -0
- setiastro/images/cropicon.png +0 -0
- setiastro/images/curves.png +0 -0
- setiastro/images/cvs.png +0 -0
- setiastro/images/debayer.png +0 -0
- setiastro/images/denoise_cnn_custom.png +0 -0
- setiastro/images/denoise_cnn_graph.png +0 -0
- setiastro/images/disk.png +0 -0
- setiastro/images/dse.png +0 -0
- setiastro/images/exoicon.png +0 -0
- setiastro/images/eye.png +0 -0
- setiastro/images/fliphorizontal.png +0 -0
- setiastro/images/flipvertical.png +0 -0
- setiastro/images/font.png +0 -0
- setiastro/images/freqsep.png +0 -0
- setiastro/images/functionbundle.png +0 -0
- setiastro/images/graxpert.png +0 -0
- setiastro/images/green.png +0 -0
- setiastro/images/gridicon.png +0 -0
- setiastro/images/halo.png +0 -0
- setiastro/images/hdr.png +0 -0
- setiastro/images/histogram.png +0 -0
- setiastro/images/hubble.png +0 -0
- setiastro/images/imagecombine.png +0 -0
- setiastro/images/invert.png +0 -0
- setiastro/images/isophote.png +0 -0
- setiastro/images/isophote_demo_figure.png +0 -0
- setiastro/images/isophote_demo_image.png +0 -0
- setiastro/images/isophote_demo_model.png +0 -0
- setiastro/images/isophote_demo_residual.png +0 -0
- setiastro/images/jwstpupil.png +0 -0
- setiastro/images/linearfit.png +0 -0
- setiastro/images/livestacking.png +0 -0
- setiastro/images/mask.png +0 -0
- setiastro/images/maskapply.png +0 -0
- setiastro/images/maskcreate.png +0 -0
- setiastro/images/maskremove.png +0 -0
- setiastro/images/morpho.png +0 -0
- setiastro/images/mosaic.png +0 -0
- setiastro/images/multiscale_decomp.png +0 -0
- setiastro/images/nbtorgb.png +0 -0
- setiastro/images/neutral.png +0 -0
- setiastro/images/nuke.png +0 -0
- setiastro/images/openfile.png +0 -0
- setiastro/images/pedestal.png +0 -0
- setiastro/images/pen.png +0 -0
- setiastro/images/pixelmath.png +0 -0
- setiastro/images/platesolve.png +0 -0
- setiastro/images/ppp.png +0 -0
- setiastro/images/pro.png +0 -0
- setiastro/images/project.png +0 -0
- setiastro/images/psf.png +0 -0
- setiastro/images/redo.png +0 -0
- setiastro/images/redoicon.png +0 -0
- setiastro/images/rescale.png +0 -0
- setiastro/images/rgbalign.png +0 -0
- setiastro/images/rgbcombo.png +0 -0
- setiastro/images/rgbextract.png +0 -0
- setiastro/images/rotate180.png +0 -0
- setiastro/images/rotatearbitrary.png +0 -0
- setiastro/images/rotateclockwise.png +0 -0
- setiastro/images/rotatecounterclockwise.png +0 -0
- setiastro/images/satellite.png +0 -0
- setiastro/images/script.png +0 -0
- setiastro/images/selectivecolor.png +0 -0
- setiastro/images/simbad.png +0 -0
- setiastro/images/slot0.png +0 -0
- setiastro/images/slot1.png +0 -0
- setiastro/images/slot2.png +0 -0
- setiastro/images/slot3.png +0 -0
- setiastro/images/slot4.png +0 -0
- setiastro/images/slot5.png +0 -0
- setiastro/images/slot6.png +0 -0
- setiastro/images/slot7.png +0 -0
- setiastro/images/slot8.png +0 -0
- setiastro/images/slot9.png +0 -0
- setiastro/images/spcc.png +0 -0
- setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
- setiastro/images/spinner.gif +0 -0
- setiastro/images/stacking.png +0 -0
- setiastro/images/staradd.png +0 -0
- setiastro/images/staralign.png +0 -0
- setiastro/images/starnet.png +0 -0
- setiastro/images/starregistration.png +0 -0
- setiastro/images/starspike.png +0 -0
- setiastro/images/starstretch.png +0 -0
- setiastro/images/statstretch.png +0 -0
- setiastro/images/supernova.png +0 -0
- setiastro/images/uhs.png +0 -0
- setiastro/images/undoicon.png +0 -0
- setiastro/images/upscale.png +0 -0
- setiastro/images/viewbundle.png +0 -0
- setiastro/images/whitebalance.png +0 -0
- setiastro/images/wimi_icon_256x256.png +0 -0
- setiastro/images/wimilogo.png +0 -0
- setiastro/images/wims.png +0 -0
- setiastro/images/wrench_icon.png +0 -0
- setiastro/images/xisfliberator.png +0 -0
- setiastro/qml/ResourceMonitor.qml +126 -0
- setiastro/saspro/__main__.py +228 -67
- setiastro/saspro/_generated/build_info.py +2 -1
- setiastro/saspro/abe.py +76 -25
- setiastro/saspro/aberration_ai.py +14 -14
- setiastro/saspro/add_stars.py +15 -12
- setiastro/saspro/astrobin_exporter.py +61 -58
- setiastro/saspro/astrospike_python.py +3 -1
- setiastro/saspro/autostretch.py +4 -2
- setiastro/saspro/backgroundneutral.py +65 -14
- setiastro/saspro/batch_convert.py +8 -5
- setiastro/saspro/batch_renamer.py +39 -36
- setiastro/saspro/blemish_blaster.py +15 -12
- setiastro/saspro/blink_comparator_pro.py +605 -379
- setiastro/saspro/cheat_sheet.py +62 -17
- setiastro/saspro/clahe.py +34 -8
- setiastro/saspro/comet_stacking.py +103 -38
- setiastro/saspro/common_tr.py +107 -0
- setiastro/saspro/continuum_subtract.py +7 -7
- setiastro/saspro/convo.py +12 -9
- setiastro/saspro/copyastro.py +3 -0
- setiastro/saspro/cosmicclarity.py +77 -52
- setiastro/saspro/crop_dialog_pro.py +80 -45
- setiastro/saspro/curve_editor_pro.py +51 -33
- setiastro/saspro/debayer.py +6 -3
- setiastro/saspro/doc_manager.py +49 -19
- setiastro/saspro/exoplanet_detector.py +11 -11
- setiastro/saspro/fitsmodifier.py +48 -44
- setiastro/saspro/fix_bom.py +32 -0
- setiastro/saspro/frequency_separation.py +18 -12
- setiastro/saspro/function_bundle.py +18 -16
- setiastro/saspro/generate_translations.py +3092 -0
- setiastro/saspro/ghs_dialog_pro.py +19 -16
- setiastro/saspro/graxpert.py +3 -0
- setiastro/saspro/gui/main_window.py +471 -126
- setiastro/saspro/gui/mixins/dock_mixin.py +123 -11
- setiastro/saspro/gui/mixins/file_mixin.py +25 -20
- setiastro/saspro/gui/mixins/geometry_mixin.py +115 -15
- setiastro/saspro/gui/mixins/header_mixin.py +6 -6
- setiastro/saspro/gui/mixins/mask_mixin.py +8 -8
- setiastro/saspro/gui/mixins/menu_mixin.py +62 -33
- setiastro/saspro/gui/mixins/toolbar_mixin.py +382 -226
- setiastro/saspro/gui/mixins/update_mixin.py +26 -26
- setiastro/saspro/gui/statistics_dialog.py +47 -0
- setiastro/saspro/halobgon.py +29 -3
- setiastro/saspro/header_viewer.py +21 -18
- setiastro/saspro/histogram.py +29 -26
- setiastro/saspro/history_explorer.py +2 -0
- setiastro/saspro/i18n.py +168 -0
- setiastro/saspro/image_combine.py +3 -0
- setiastro/saspro/image_peeker_pro.py +52 -44
- 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 +180 -22
- setiastro/saspro/mfdeconv.py +5 -0
- setiastro/saspro/morphology.py +38 -13
- setiastro/saspro/multiscale_decomp.py +713 -256
- setiastro/saspro/nbtorgb_stars.py +12 -2
- setiastro/saspro/numba_utils.py +149 -48
- setiastro/saspro/ops/scripts.py +77 -17
- setiastro/saspro/ops/settings.py +177 -100
- setiastro/saspro/perfect_palette_picker.py +25 -7
- setiastro/saspro/pixelmath.py +114 -110
- setiastro/saspro/plate_solver.py +118 -108
- setiastro/saspro/remove_green.py +24 -7
- setiastro/saspro/remove_stars.py +136 -162
- setiastro/saspro/remove_stars_preset.py +55 -13
- setiastro/saspro/resources.py +46 -15
- setiastro/saspro/rgb_combination.py +19 -18
- setiastro/saspro/rgbalign.py +11 -11
- setiastro/saspro/save_options.py +5 -4
- setiastro/saspro/selective_color.py +84 -25
- setiastro/saspro/sfcc.py +119 -72
- setiastro/saspro/shortcuts.py +345 -36
- setiastro/saspro/signature_insert.py +4 -1
- setiastro/saspro/stacking_suite.py +2066 -1119
- setiastro/saspro/star_alignment.py +291 -331
- setiastro/saspro/star_spikes.py +137 -53
- setiastro/saspro/star_stretch.py +47 -10
- setiastro/saspro/stat_stretch.py +52 -16
- setiastro/saspro/status_log_dock.py +1 -1
- setiastro/saspro/subwindow.py +97 -36
- setiastro/saspro/supernovaasteroidhunter.py +68 -61
- setiastro/saspro/swap_manager.py +77 -42
- setiastro/saspro/translations/all_source_strings.json +4726 -0
- setiastro/saspro/translations/ar_translations.py +4096 -0
- setiastro/saspro/translations/de_translations.py +3728 -0
- setiastro/saspro/translations/es_translations.py +4169 -0
- setiastro/saspro/translations/fr_translations.py +4090 -0
- setiastro/saspro/translations/hi_translations.py +3803 -0
- setiastro/saspro/translations/integrate_translations.py +271 -0
- setiastro/saspro/translations/it_translations.py +4728 -0
- setiastro/saspro/translations/ja_translations.py +3834 -0
- setiastro/saspro/translations/pt_translations.py +3847 -0
- setiastro/saspro/translations/ru_translations.py +3082 -0
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +16019 -0
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +14548 -0
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +16202 -0
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +15870 -0
- setiastro/saspro/translations/saspro_hi.qm +0 -0
- setiastro/saspro/translations/saspro_hi.ts +14855 -0
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +19046 -0
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +14980 -0
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +15024 -0
- setiastro/saspro/translations/saspro_ru.qm +0 -0
- setiastro/saspro/translations/saspro_ru.ts +11835 -0
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +15237 -0
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +15248 -0
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +15289 -0
- setiastro/saspro/translations/sw_translations.py +3897 -0
- setiastro/saspro/translations/uk_translations.py +3929 -0
- setiastro/saspro/translations/zh_translations.py +3910 -0
- setiastro/saspro/versioning.py +77 -0
- setiastro/saspro/view_bundle.py +20 -17
- setiastro/saspro/wavescale_hdr.py +54 -33
- setiastro/saspro/wavescale_hdr_preset.py +6 -5
- setiastro/saspro/wavescalede.py +54 -31
- setiastro/saspro/wavescalede_preset.py +9 -7
- setiastro/saspro/whitebalance.py +58 -22
- setiastro/saspro/widgets/common_utilities.py +12 -11
- setiastro/saspro/widgets/minigame/game.js +991 -0
- setiastro/saspro/widgets/minigame/index.html +53 -0
- setiastro/saspro/widgets/minigame/style.css +241 -0
- setiastro/saspro/widgets/preview_dialogs.py +8 -8
- setiastro/saspro/widgets/resource_monitor.py +263 -0
- setiastro/saspro/widgets/spinboxes.py +18 -0
- setiastro/saspro/widgets/wavelet_utils.py +52 -20
- setiastro/saspro/wimi.py +7996 -0
- setiastro/saspro/wims.py +578 -0
- setiastro/saspro/window_shelf.py +2 -2
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/METADATA +15 -3
- setiastrosuitepro-1.6.4.post1.dist-info/RECORD +368 -0
- setiastrosuitepro-1.6.0.dist-info/RECORD +0 -174
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.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
|
|
@@ -158,7 +159,7 @@ def _align_prefs(settings: QSettings | None = None) -> dict:
|
|
|
158
159
|
prefs = {
|
|
159
160
|
"model": model, # "affine" | "homography" | "poly3" | "poly4"
|
|
160
161
|
"max_cp": _get("max_cp", 250, int),
|
|
161
|
-
"downsample": _get("downsample",
|
|
162
|
+
"downsample": _get("downsample", 3, int),
|
|
162
163
|
"h_reproj": _get("h_reproj", 3.0, float),
|
|
163
164
|
|
|
164
165
|
# Star detection / solve limits
|
|
@@ -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
|
|
|
@@ -1426,6 +1438,34 @@ class RegistrationWorkerSignals(QObject):
|
|
|
1426
1438
|
# Identity transform (2x3)
|
|
1427
1439
|
IDENTITY_2x3 = np.array([[1, 0, 0], [0, 1, 0]], dtype=np.float64)
|
|
1428
1440
|
|
|
1441
|
+
def _to3x3_affine(A2x3: np.ndarray) -> np.ndarray:
|
|
1442
|
+
A = np.asarray(A2x3, np.float64).reshape(2,3)
|
|
1443
|
+
return np.vstack([A, [0,0,1]])
|
|
1444
|
+
|
|
1445
|
+
def _from3x3_affine(A3: np.ndarray) -> np.ndarray:
|
|
1446
|
+
return np.asarray(A3, np.float64)[:2,:]
|
|
1447
|
+
|
|
1448
|
+
def _S(ds: float) -> np.ndarray:
|
|
1449
|
+
ds = float(ds)
|
|
1450
|
+
return np.array([[1.0/ds, 0, 0],
|
|
1451
|
+
[0, 1.0/ds, 0],
|
|
1452
|
+
[0, 0, 1]], np.float64)
|
|
1453
|
+
|
|
1454
|
+
def lift_affine_2x3_from_ds(A_ds_2x3: np.ndarray, ds: float) -> np.ndarray:
|
|
1455
|
+
S = _S(ds); Si = np.linalg.inv(S)
|
|
1456
|
+
A3_full = Si @ _to3x3_affine(A_ds_2x3) @ S
|
|
1457
|
+
return _from3x3_affine(A3_full)
|
|
1458
|
+
|
|
1459
|
+
def downscale_affine_2x3_to_ds(A_full_2x3: np.ndarray, ds: float) -> np.ndarray:
|
|
1460
|
+
S = _S(ds); Si = np.linalg.inv(S)
|
|
1461
|
+
A3_ds = S @ _to3x3_affine(A_full_2x3) @ Si
|
|
1462
|
+
return _from3x3_affine(A3_ds)
|
|
1463
|
+
|
|
1464
|
+
def lift_homography_from_ds(H_ds: np.ndarray, ds: float) -> np.ndarray:
|
|
1465
|
+
S = _S(ds); Si = np.linalg.inv(S)
|
|
1466
|
+
return Si @ np.asarray(H_ds, np.float64) @ S
|
|
1467
|
+
|
|
1468
|
+
|
|
1429
1469
|
def compute_affine_transform_astroalign_cropped(source_img, reference_img,
|
|
1430
1470
|
scale: float = 1.20,
|
|
1431
1471
|
limit_stars: int | None = None,
|
|
@@ -1867,31 +1907,34 @@ def project_affine_to_similarity(A2x3: np.ndarray) -> np.ndarray:
|
|
|
1867
1907
|
def _solve_delta_job(args):
|
|
1868
1908
|
"""
|
|
1869
1909
|
Worker: compute incremental affine/similarity delta for one frame against the ref preview.
|
|
1870
|
-
args =
|
|
1871
|
-
|
|
1872
|
-
|
|
1910
|
+
args =
|
|
1911
|
+
(orig_path, current_transform_2x3,
|
|
1912
|
+
ref_small_ds, Wref_ds, Href_ds,
|
|
1913
|
+
resample_flag, det_sigma, limit_stars, minarea,
|
|
1914
|
+
model, h_reproj, ds)
|
|
1873
1915
|
"""
|
|
1874
1916
|
try:
|
|
1875
1917
|
import os
|
|
1876
1918
|
import numpy as np
|
|
1877
1919
|
import cv2
|
|
1878
|
-
import sep
|
|
1879
1920
|
from astropy.io import fits
|
|
1880
1921
|
|
|
1881
|
-
(orig_path, current_transform_2x3,
|
|
1922
|
+
(orig_path, current_transform_2x3,
|
|
1923
|
+
ref_small_ds, Wref_ds, Href_ds,
|
|
1882
1924
|
resample_flag, det_sigma, limit_stars, minarea,
|
|
1883
|
-
model, h_reproj) = args
|
|
1925
|
+
model, h_reproj, ds) = args
|
|
1926
|
+
|
|
1927
|
+
ds = max(1, int(ds))
|
|
1884
1928
|
|
|
1885
1929
|
try:
|
|
1886
1930
|
cv2.setNumThreads(1)
|
|
1887
1931
|
try: cv2.ocl.setUseOpenCL(False)
|
|
1888
|
-
except Exception
|
|
1889
|
-
|
|
1890
|
-
logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
|
|
1932
|
+
except Exception:
|
|
1933
|
+
pass
|
|
1891
1934
|
except Exception:
|
|
1892
1935
|
pass
|
|
1893
1936
|
|
|
1894
|
-
# 1) read → gray float32
|
|
1937
|
+
# 1) read → gray float32 (full)
|
|
1895
1938
|
with fits.open(orig_path, memmap=True) as hdul:
|
|
1896
1939
|
arr = hdul[0].data
|
|
1897
1940
|
if arr is None:
|
|
@@ -1899,48 +1942,66 @@ def _solve_delta_job(args):
|
|
|
1899
1942
|
gray = arr if arr.ndim == 2 else np.mean(arr, axis=2)
|
|
1900
1943
|
gray = np.nan_to_num(gray, nan=0.0, posinf=0.0, neginf=0.0).astype(np.float32, copy=False)
|
|
1901
1944
|
|
|
1902
|
-
# 2)
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1945
|
+
# 2) downsample source to DS space
|
|
1946
|
+
if ds > 1:
|
|
1947
|
+
Wds = max(1, int(gray.shape[1] // ds))
|
|
1948
|
+
Hds = max(1, int(gray.shape[0] // ds))
|
|
1949
|
+
gray_ds = cv2.resize(gray, (Wds, Hds), interpolation=cv2.INTER_AREA)
|
|
1950
|
+
else:
|
|
1951
|
+
gray_ds = gray
|
|
1952
|
+
|
|
1953
|
+
# 3) pre-warp in DS space using downscaled transform
|
|
1954
|
+
T_prev_full = np.asarray(current_transform_2x3, np.float64).reshape(2, 3)
|
|
1955
|
+
T_prev_ds = downscale_affine_2x3_to_ds(T_prev_full, ds).astype(np.float32)
|
|
1956
|
+
|
|
1957
|
+
# Warp DS source into DS ref geometry
|
|
1958
|
+
src_for_match_ds = cv2.warpAffine(
|
|
1959
|
+
gray_ds, T_prev_ds, (int(Wref_ds), int(Href_ds)),
|
|
1906
1960
|
flags=resample_flag, borderMode=cv2.BORDER_REFLECT_101
|
|
1907
1961
|
)
|
|
1908
1962
|
|
|
1909
|
-
#
|
|
1910
|
-
|
|
1911
|
-
|
|
1963
|
+
# 4) denoise sparse islands in DS space (cheaper)
|
|
1964
|
+
src_for_match_ds = _suppress_tiny_islands(src_for_match_ds, det_sigma=det_sigma, minarea=minarea)
|
|
1965
|
+
ref_for_match_ds = _suppress_tiny_islands(np.asarray(ref_small_ds, np.float32, order="C"),
|
|
1966
|
+
det_sigma=det_sigma, minarea=minarea)
|
|
1912
1967
|
|
|
1913
|
-
#
|
|
1968
|
+
# 5) AA delta solve in DS space
|
|
1914
1969
|
m = (model or "affine").lower()
|
|
1915
1970
|
if m in ("no_distortion", "nodistortion"):
|
|
1916
1971
|
m = "similarity"
|
|
1917
1972
|
|
|
1918
1973
|
if m == "similarity":
|
|
1919
|
-
|
|
1920
|
-
|
|
1974
|
+
tform_ds = compute_similarity_transform_astroalign_cropped(
|
|
1975
|
+
src_for_match_ds, ref_for_match_ds,
|
|
1921
1976
|
limit_stars=int(limit_stars) if limit_stars is not None else None,
|
|
1922
1977
|
det_sigma=float(det_sigma),
|
|
1923
1978
|
minarea=int(minarea),
|
|
1924
1979
|
h_reproj=float(h_reproj)
|
|
1925
1980
|
)
|
|
1926
1981
|
else:
|
|
1927
|
-
|
|
1928
|
-
|
|
1982
|
+
tform_ds = compute_affine_transform_astroalign_cropped(
|
|
1983
|
+
src_for_match_ds, ref_for_match_ds,
|
|
1929
1984
|
limit_stars=int(limit_stars) if limit_stars is not None else None,
|
|
1930
1985
|
det_sigma=float(det_sigma),
|
|
1931
1986
|
minarea=int(minarea)
|
|
1932
1987
|
)
|
|
1933
1988
|
|
|
1934
|
-
if
|
|
1989
|
+
if tform_ds is None:
|
|
1935
1990
|
return (orig_path, None,
|
|
1936
1991
|
f"Astroalign failed for {os.path.basename(orig_path)} – skipping (no transform returned)")
|
|
1937
1992
|
|
|
1938
|
-
|
|
1939
|
-
|
|
1993
|
+
# 6) lift DS delta back to full-res coords
|
|
1994
|
+
T_new_full = lift_affine_2x3_from_ds(np.asarray(tform_ds, np.float64).reshape(2, 3), ds)
|
|
1995
|
+
|
|
1996
|
+
return (orig_path, np.asarray(T_new_full, np.float64).reshape(2, 3), None)
|
|
1940
1997
|
|
|
1941
1998
|
except Exception as e:
|
|
1999
|
+
try:
|
|
2000
|
+
base = os.path.basename(args[0]) if args else "<unknown>"
|
|
2001
|
+
except Exception:
|
|
2002
|
+
base = "<unknown>"
|
|
1942
2003
|
return (args[0] if args else "<unknown>", None,
|
|
1943
|
-
f"Astroalign failed for {
|
|
2004
|
+
f"Astroalign failed for {base}: {e}")
|
|
1944
2005
|
|
|
1945
2006
|
|
|
1946
2007
|
|
|
@@ -2024,7 +2085,7 @@ def _suppress_tiny_islands(img32: np.ndarray, det_sigma: float, minarea: int) ->
|
|
|
2024
2085
|
# ─────────────────────────────────────────────────────────────
|
|
2025
2086
|
def _finalize_write_job(args):
|
|
2026
2087
|
"""
|
|
2027
|
-
Process-safe worker: read full-res,
|
|
2088
|
+
Process-safe worker: read full-res, choose model, warp, save.
|
|
2028
2089
|
Returns (orig_path, out_path or "", msg, success, drizzle_tuple or None)
|
|
2029
2090
|
drizzle_tuple = (kind, matrix_or_None)
|
|
2030
2091
|
"""
|
|
@@ -2045,17 +2106,19 @@ def _finalize_write_job(args):
|
|
|
2045
2106
|
try:
|
|
2046
2107
|
cv2.setNumThreads(1)
|
|
2047
2108
|
try: cv2.ocl.setUseOpenCL(False)
|
|
2048
|
-
except Exception
|
|
2049
|
-
|
|
2050
|
-
logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
|
|
2109
|
+
except Exception:
|
|
2110
|
+
pass
|
|
2051
2111
|
except Exception:
|
|
2052
2112
|
pass
|
|
2053
2113
|
|
|
2054
2114
|
debug_lines = []
|
|
2055
2115
|
def dbg(s: str):
|
|
2056
|
-
# keep it short-ish; UI emits each line
|
|
2057
2116
|
debug_lines.append(str(s))
|
|
2058
2117
|
|
|
2118
|
+
def _A3(A2x3):
|
|
2119
|
+
A = np.asarray(A2x3, np.float64).reshape(2, 3)
|
|
2120
|
+
return np.vstack([A, [0, 0, 1]])
|
|
2121
|
+
|
|
2059
2122
|
try:
|
|
2060
2123
|
# 1) load source (full-res)
|
|
2061
2124
|
with fits.open(orig_path, memmap=True) as hdul:
|
|
@@ -2064,12 +2127,12 @@ def _finalize_write_job(args):
|
|
|
2064
2127
|
if img is None:
|
|
2065
2128
|
return (orig_path, "", f"⚠️ Failed to read {os.path.basename(orig_path)}", False, None)
|
|
2066
2129
|
|
|
2067
|
-
#
|
|
2130
|
+
# normalize ints
|
|
2068
2131
|
if img.dtype == np.uint16:
|
|
2069
2132
|
img = img.astype(np.float32) / 65535.0
|
|
2070
2133
|
elif img.dtype == np.uint8:
|
|
2071
2134
|
img = img.astype(np.float32) / 255.0
|
|
2072
|
-
|
|
2135
|
+
|
|
2073
2136
|
is_mono = (img.ndim == 2)
|
|
2074
2137
|
src_gray_full = img if is_mono else np.mean(img, axis=2)
|
|
2075
2138
|
src_gray_full = np.nan_to_num(src_gray_full, nan=0.0, posinf=0.0, neginf=0.0).astype(np.float32, copy=False)
|
|
@@ -2078,40 +2141,61 @@ def _finalize_write_job(args):
|
|
|
2078
2141
|
|
|
2079
2142
|
Href, Wref = ref_shape
|
|
2080
2143
|
|
|
2081
|
-
# 2) load reference via memmap
|
|
2144
|
+
# 2) load reference (full-res) via memmap
|
|
2082
2145
|
ref2d = np.load(ref_npy_path, mmap_mode="r").astype(np.float32, copy=False)
|
|
2083
2146
|
if ref2d.shape[:2] != (Href, Wref):
|
|
2084
2147
|
return (orig_path, "", f"⚠️ Ref shape mismatch for {os.path.basename(orig_path)}", False, None)
|
|
2085
2148
|
|
|
2086
2149
|
base = os.path.basename(orig_path)
|
|
2087
2150
|
|
|
2088
|
-
# helper: force affine to similarity (no shear)
|
|
2089
|
-
def _affine_to_similarity(A2x3: np.ndarray) -> np.ndarray:
|
|
2090
|
-
A2x3 = np.asarray(A2x3, np.float64).reshape(2, 3)
|
|
2091
|
-
R = A2x3[:, :2]
|
|
2092
|
-
t = A2x3[:, 2]
|
|
2093
|
-
U, S, Vt = np.linalg.svd(R)
|
|
2094
|
-
rot = U @ Vt
|
|
2095
|
-
if np.linalg.det(rot) < 0:
|
|
2096
|
-
U[:, -1] *= -1
|
|
2097
|
-
rot = U @ Vt
|
|
2098
|
-
s = float((S[0] + S[1]) * 0.5)
|
|
2099
|
-
Rsim = rot * s
|
|
2100
|
-
out = np.zeros((2, 3), dtype=np.float64)
|
|
2101
|
-
out[:, :2] = Rsim
|
|
2102
|
-
out[:, 2] = t
|
|
2103
|
-
return out
|
|
2104
|
-
|
|
2105
|
-
# 3) choose transform
|
|
2106
2151
|
model = (align_model or "affine").lower()
|
|
2107
2152
|
if model in ("no_distortion", "nodistortion"):
|
|
2108
2153
|
model = "similarity"
|
|
2109
2154
|
|
|
2155
|
+
# Base (accumulated) affine from refinement
|
|
2156
|
+
A_prev = np.asarray(affine_2x3, np.float64).reshape(2, 3)
|
|
2157
|
+
A_prev3 = _A3(A_prev)
|
|
2158
|
+
|
|
2159
|
+
# Default finalize is just the affine refinement result
|
|
2110
2160
|
kind = "affine"
|
|
2111
|
-
X =
|
|
2161
|
+
X = A_prev.copy()
|
|
2112
2162
|
|
|
2163
|
+
# ---- Non-affine finalize: DS solve + lift, but KEEP affine-as-start ----
|
|
2113
2164
|
if model != "affine":
|
|
2114
|
-
|
|
2165
|
+
dbg(f"[finalize] base={base} model={model} det_sigma={det_sigma} minarea={minarea} limit_stars={limit_stars}")
|
|
2166
|
+
|
|
2167
|
+
ds = 2 # ✅ keep simple/safe; only DS+lift change requested
|
|
2168
|
+
ds = max(1, int(ds))
|
|
2169
|
+
|
|
2170
|
+
# DS reference
|
|
2171
|
+
if ds > 1:
|
|
2172
|
+
ref_ds = cv2.resize(ref2d, (max(1, Wref // ds), max(1, Href // ds)), interpolation=cv2.INTER_AREA)
|
|
2173
|
+
else:
|
|
2174
|
+
ref_ds = np.ascontiguousarray(ref2d)
|
|
2175
|
+
|
|
2176
|
+
ref_ds = np.ascontiguousarray(ref_ds.astype(np.float32, copy=False))
|
|
2177
|
+
Hds, Wds = ref_ds.shape[:2]
|
|
2178
|
+
|
|
2179
|
+
# DS source
|
|
2180
|
+
if ds > 1:
|
|
2181
|
+
src_ds0 = cv2.resize(src_gray_full, (Wds, Hds), interpolation=cv2.INTER_AREA)
|
|
2182
|
+
else:
|
|
2183
|
+
src_ds0 = cv2.resize(src_gray_full, (Wds, Hds), interpolation=cv2.INTER_AREA) if (src_gray_full.shape[:2] != (Hds, Wds)) else src_gray_full
|
|
2184
|
+
|
|
2185
|
+
src_ds0 = np.ascontiguousarray(src_ds0.astype(np.float32, copy=False))
|
|
2186
|
+
|
|
2187
|
+
# Pre-warp source in DS space using downscaled accumulated affine
|
|
2188
|
+
A_prev_ds = downscale_affine_2x3_to_ds(A_prev, ds).astype(np.float32)
|
|
2189
|
+
src_pre_ds = cv2.warpAffine(
|
|
2190
|
+
src_ds0, A_prev_ds, (Wds, Hds),
|
|
2191
|
+
flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101
|
|
2192
|
+
)
|
|
2193
|
+
|
|
2194
|
+
# Optional suppress tiny islands (your existing helper)
|
|
2195
|
+
src_pre_ds = _suppress_tiny_islands(src_pre_ds, det_sigma=float(det_sigma), minarea=int(minarea))
|
|
2196
|
+
ref_ds = _suppress_tiny_islands(ref_ds, det_sigma=float(det_sigma), minarea=int(minarea))
|
|
2197
|
+
|
|
2198
|
+
# AA correspondences in DS space: prewarped src vs ref
|
|
2115
2199
|
max_cp = None
|
|
2116
2200
|
try:
|
|
2117
2201
|
if limit_stars is not None and int(limit_stars) > 0:
|
|
@@ -2119,13 +2203,10 @@ def _finalize_write_job(args):
|
|
|
2119
2203
|
except Exception:
|
|
2120
2204
|
max_cp = None
|
|
2121
2205
|
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
AA_SCALE = 0.80 # finalize-only
|
|
2206
|
+
AA_SCALE = 0.80
|
|
2125
2207
|
|
|
2126
|
-
# ---- tiles=1 (center crop) ----
|
|
2127
2208
|
src_xy, tgt_xy, best_P, best_xy0 = _aa_find_pairs_multitile(
|
|
2128
|
-
|
|
2209
|
+
src_pre_ds, ref_ds,
|
|
2129
2210
|
scale=AA_SCALE,
|
|
2130
2211
|
tiles=1,
|
|
2131
2212
|
det_sigma=float(det_sigma),
|
|
@@ -2133,143 +2214,72 @@ def _finalize_write_job(args):
|
|
|
2133
2214
|
max_control_points=max_cp,
|
|
2134
2215
|
_dbg=dbg
|
|
2135
2216
|
)
|
|
2136
|
-
|
|
2137
2217
|
if src_xy is None or len(src_xy) < 8:
|
|
2138
|
-
|
|
2139
|
-
raise RuntimeError("astroalign produced too few matches")
|
|
2140
|
-
|
|
2141
|
-
dbg(f"[AA] tiles=1 matches={len(src_xy)} best_tile_xy0={best_xy0}")
|
|
2142
|
-
|
|
2143
|
-
spread_ok1 = _points_spread_ok(tgt_xy, Wref, Href, _dbg=dbg)
|
|
2144
|
-
dbg(f"[AA] spread_ok(tiles=1)={spread_ok1}")
|
|
2145
|
-
|
|
2146
|
-
# ---- fallback: tiles=5 (corners + center) ----
|
|
2147
|
-
if not spread_ok1:
|
|
2148
|
-
src_xy5, tgt_xy5, best_P5, best_xy0_5 = _aa_find_pairs_multitile(
|
|
2149
|
-
src_gray_full, ref2d,
|
|
2150
|
-
scale=AA_SCALE,
|
|
2151
|
-
tiles=5, # <-- NEW primary fallback
|
|
2152
|
-
det_sigma=float(det_sigma),
|
|
2153
|
-
minarea=int(minarea),
|
|
2154
|
-
max_control_points=max_cp,
|
|
2155
|
-
_dbg=dbg
|
|
2156
|
-
)
|
|
2157
|
-
|
|
2158
|
-
if src_xy5 is None or len(src_xy5) < 8:
|
|
2159
|
-
dbg("[AA] tiles=5 too few matches; keeping tiles=1")
|
|
2160
|
-
else:
|
|
2161
|
-
dbg(f"[AA] tiles=5 matches={len(src_xy5)} best_tile_xy0={best_xy0_5}")
|
|
2162
|
-
spread_ok5 = _points_spread_ok(tgt_xy5, Wref, Href, _dbg=dbg)
|
|
2163
|
-
dbg(f"[AA] spread_ok(tiles=5)={spread_ok5}")
|
|
2164
|
-
|
|
2165
|
-
# choose tiles=5 if it spreads better OR gives more matches
|
|
2166
|
-
if spread_ok5 or len(src_xy5) > len(src_xy):
|
|
2167
|
-
dbg("[AA] switching to tiles=5 result")
|
|
2168
|
-
src_xy, tgt_xy = src_xy5, tgt_xy5
|
|
2169
|
-
best_P, best_xy0 = best_P5, best_xy0_5
|
|
2170
|
-
else:
|
|
2171
|
-
dbg("[AA] keeping tiles=1 result (tiles=5 not better)")
|
|
2172
|
-
|
|
2173
|
-
# ---- tertiary fallback: tiles=3 grid ----
|
|
2174
|
-
spread_ok_after = _points_spread_ok(tgt_xy, Wref, Href, _dbg=dbg)
|
|
2175
|
-
dbg(f"[AA] spread_ok(after tiles=5 check)={spread_ok_after}")
|
|
2176
|
-
|
|
2177
|
-
if not spread_ok_after:
|
|
2178
|
-
src_xy3, tgt_xy3, best_P3, best_xy0_3 = _aa_find_pairs_multitile(
|
|
2179
|
-
src_gray_full, ref2d,
|
|
2180
|
-
scale=AA_SCALE,
|
|
2181
|
-
tiles=3,
|
|
2182
|
-
det_sigma=float(det_sigma),
|
|
2183
|
-
minarea=int(minarea),
|
|
2184
|
-
max_control_points=max_cp,
|
|
2185
|
-
_dbg=dbg
|
|
2186
|
-
)
|
|
2187
|
-
|
|
2188
|
-
if src_xy3 is None or len(src_xy3) < 8:
|
|
2189
|
-
dbg("[AA] tiles=3 too few matches; keeping current result")
|
|
2190
|
-
else:
|
|
2191
|
-
dbg(f"[AA] tiles=3 matches={len(src_xy3)} best_tile_xy0={best_xy0_3}")
|
|
2192
|
-
spread_ok3 = _points_spread_ok(tgt_xy3, Wref, Href, _dbg=dbg)
|
|
2193
|
-
dbg(f"[AA] spread_ok(tiles=3)={spread_ok3}")
|
|
2194
|
-
|
|
2195
|
-
if spread_ok3 or len(src_xy3) > len(src_xy):
|
|
2196
|
-
dbg("[AA] switching to tiles=3 result")
|
|
2197
|
-
src_xy, tgt_xy = src_xy3, tgt_xy3
|
|
2198
|
-
best_P, best_xy0 = best_P3, best_xy0_3
|
|
2199
|
-
else:
|
|
2200
|
-
dbg("[AA] keeping current result (tiles=3 not better)")
|
|
2201
|
-
|
|
2202
|
-
x0, y0 = best_xy0
|
|
2203
|
-
P = np.asarray(best_P, np.float64)
|
|
2204
|
-
|
|
2205
|
-
# ---- base full-ref from best_P + best_xy0 ----
|
|
2206
|
-
if P.shape == (3, 3):
|
|
2207
|
-
base_kind0 = "homography"
|
|
2208
|
-
T = np.array([[1,0,x0],[0,1,y0],[0,0,1]], dtype=np.float64)
|
|
2209
|
-
base_X0 = T @ P
|
|
2210
|
-
else:
|
|
2211
|
-
base_kind0 = "affine"
|
|
2212
|
-
A3 = np.vstack([P[0:2, :], [0,0,1]])
|
|
2213
|
-
T = np.array([[1,0,x0],[0,1,y0],[0,0,1]], dtype=np.float64)
|
|
2214
|
-
base_X0 = (T @ A3)[0:2, :]
|
|
2218
|
+
raise RuntimeError("astroalign produced too few matches (finalize)")
|
|
2215
2219
|
|
|
2220
|
+
# RANSAC threshold in DS pixels
|
|
2216
2221
|
hth = float(h_reproj)
|
|
2217
2222
|
|
|
2218
2223
|
if model == "homography":
|
|
2219
|
-
|
|
2224
|
+
# Delta homography maps prewarped -> ref (both in DS coords)
|
|
2225
|
+
H_delta_ds, inl = cv2.findHomography(src_xy, tgt_xy, cv2.RANSAC, ransacReprojThreshold=hth)
|
|
2220
2226
|
ninl = int(inl.sum()) if inl is not None else 0
|
|
2221
|
-
dbg(f"[RANSAC] homography inliers={ninl}/{len(src_xy)} thr={hth}")
|
|
2227
|
+
dbg(f"[RANSAC] homography delta(DS) inliers={ninl}/{len(src_xy)} thr={hth}")
|
|
2222
2228
|
|
|
2223
|
-
if
|
|
2224
|
-
|
|
2229
|
+
if H_delta_ds is None:
|
|
2230
|
+
# fallback to just affine refinement
|
|
2231
|
+
kind, X = "affine", A_prev.copy()
|
|
2225
2232
|
else:
|
|
2226
|
-
|
|
2233
|
+
H_delta_full = lift_homography_from_ds(H_delta_ds, ds)
|
|
2234
|
+
H_final = np.asarray(H_delta_full, np.float64) @ A_prev3
|
|
2235
|
+
kind, X = "homography", H_final
|
|
2227
2236
|
|
|
2228
2237
|
elif model == "similarity":
|
|
2229
|
-
|
|
2238
|
+
# Delta similarity (affine partial) maps prewarped -> ref in DS coords
|
|
2239
|
+
A_delta_ds, inl = cv2.estimateAffinePartial2D(
|
|
2240
|
+
src_xy, tgt_xy, cv2.RANSAC, ransacReprojThreshold=hth
|
|
2241
|
+
)
|
|
2230
2242
|
ninl = int(inl.sum()) if inl is not None else 0
|
|
2231
|
-
dbg(f"[RANSAC] similarity inliers={ninl}/{len(src_xy)} thr={hth}")
|
|
2243
|
+
dbg(f"[RANSAC] similarity delta(DS) inliers={ninl}/{len(src_xy)} thr={hth}")
|
|
2232
2244
|
|
|
2233
|
-
if
|
|
2234
|
-
kind, X = "similarity",
|
|
2245
|
+
if A_delta_ds is None:
|
|
2246
|
+
kind, X = "similarity", _project_to_similarity(A_prev)
|
|
2235
2247
|
else:
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
elif model == "affine":
|
|
2242
|
-
kind, X = "affine", np.asarray(affine_2x3, np.float64)
|
|
2248
|
+
A_delta_full = lift_affine_2x3_from_ds(A_delta_ds, ds)
|
|
2249
|
+
# Compose delta ∘ prev in affine space
|
|
2250
|
+
A_final3 = _A3(A_delta_full) @ A_prev3
|
|
2251
|
+
A_final = A_final3[:2, :]
|
|
2252
|
+
kind, X = "similarity", _project_to_similarity(A_final)
|
|
2243
2253
|
|
|
2244
2254
|
elif model in ("poly3", "poly4"):
|
|
2255
|
+
# Keep behavior simple: poly fit in FULL coords using pairs from prewarped DS,
|
|
2256
|
+
# then apply as remap on the ORIGINAL image (same as your current poly path).
|
|
2257
|
+
# (If you later want true "poly residual after affine", we can do that safely,
|
|
2258
|
+
# but that is a pattern change beyond DS+lift.)
|
|
2245
2259
|
order = 3 if model == "poly3" else 4
|
|
2246
|
-
|
|
2260
|
+
src_full = (np.asarray(src_xy, np.float32) * float(ds)).astype(np.float32)
|
|
2261
|
+
tgt_full = (np.asarray(tgt_xy, np.float32) * float(ds)).astype(np.float32)
|
|
2262
|
+
|
|
2263
|
+
cx, cy = _fit_poly_xy(src_full, tgt_full, order=order)
|
|
2247
2264
|
map_x, map_y = _poly_eval_grid(cx, cy, Wref, Href, order=order)
|
|
2248
2265
|
kind, X = model, (map_x, map_y)
|
|
2249
2266
|
|
|
2250
2267
|
else:
|
|
2251
|
-
|
|
2252
|
-
kind, X =
|
|
2268
|
+
# Unknown model -> just write affine refinement
|
|
2269
|
+
kind, X = "affine", A_prev.copy()
|
|
2253
2270
|
|
|
2254
|
-
# 4) warp
|
|
2271
|
+
# 4) warp full-res
|
|
2255
2272
|
Hh, Ww = Href, Wref
|
|
2256
2273
|
|
|
2257
2274
|
if kind in ("affine", "similarity"):
|
|
2258
2275
|
A = np.asarray(X, np.float64).reshape(2, 3)
|
|
2259
|
-
|
|
2260
2276
|
if is_mono:
|
|
2261
|
-
aligned = cv2.warpAffine(
|
|
2262
|
-
|
|
2263
|
-
flags=cv2.INTER_LANCZOS4,
|
|
2264
|
-
borderMode=cv2.BORDER_CONSTANT, borderValue=0
|
|
2265
|
-
)
|
|
2277
|
+
aligned = cv2.warpAffine(img, A, (Ww, Hh), flags=cv2.INTER_LANCZOS4,
|
|
2278
|
+
borderMode=cv2.BORDER_CONSTANT, borderValue=0)
|
|
2266
2279
|
else:
|
|
2267
2280
|
aligned = np.stack([
|
|
2268
|
-
cv2.warpAffine(
|
|
2269
|
-
|
|
2270
|
-
flags=cv2.INTER_LANCZOS4,
|
|
2271
|
-
borderMode=cv2.BORDER_CONSTANT, borderValue=0
|
|
2272
|
-
)
|
|
2281
|
+
cv2.warpAffine(img[..., c], A, (Ww, Hh), flags=cv2.INTER_LANCZOS4,
|
|
2282
|
+
borderMode=cv2.BORDER_CONSTANT, borderValue=0)
|
|
2273
2283
|
for c in range(img.shape[2])
|
|
2274
2284
|
], axis=2)
|
|
2275
2285
|
|
|
@@ -2278,34 +2288,27 @@ def _finalize_write_job(args):
|
|
|
2278
2288
|
|
|
2279
2289
|
elif kind == "homography":
|
|
2280
2290
|
Hm = np.asarray(X, np.float64).reshape(3, 3)
|
|
2281
|
-
|
|
2282
2291
|
if is_mono:
|
|
2283
|
-
aligned = cv2.warpPerspective(
|
|
2284
|
-
|
|
2285
|
-
flags=cv2.INTER_LANCZOS4,
|
|
2286
|
-
borderMode=cv2.BORDER_CONSTANT, borderValue=0
|
|
2287
|
-
)
|
|
2292
|
+
aligned = cv2.warpPerspective(img, Hm, (Ww, Hh), flags=cv2.INTER_LANCZOS4,
|
|
2293
|
+
borderMode=cv2.BORDER_CONSTANT, borderValue=0)
|
|
2288
2294
|
else:
|
|
2289
2295
|
aligned = np.stack([
|
|
2290
|
-
cv2.warpPerspective(
|
|
2291
|
-
|
|
2292
|
-
flags=cv2.INTER_LANCZOS4,
|
|
2293
|
-
borderMode=cv2.BORDER_CONSTANT, borderValue=0
|
|
2294
|
-
)
|
|
2296
|
+
cv2.warpPerspective(img[..., c], Hm, (Ww, Hh), flags=cv2.INTER_LANCZOS4,
|
|
2297
|
+
borderMode=cv2.BORDER_CONSTANT, borderValue=0)
|
|
2295
2298
|
for c in range(img.shape[2])
|
|
2296
2299
|
], axis=2)
|
|
2297
2300
|
|
|
2298
2301
|
drizzle_tuple = ("homography", Hm.astype(np.float64))
|
|
2299
2302
|
warp_label = "homography"
|
|
2300
2303
|
|
|
2301
|
-
elif kind in ("poly3","poly4"):
|
|
2304
|
+
elif kind in ("poly3", "poly4"):
|
|
2302
2305
|
map_x, map_y = X
|
|
2303
2306
|
if is_mono:
|
|
2304
2307
|
aligned = cv2.remap(img, map_x, map_y, cv2.INTER_LANCZOS4,
|
|
2305
2308
|
borderMode=cv2.BORDER_CONSTANT, borderValue=0)
|
|
2306
2309
|
else:
|
|
2307
2310
|
aligned = np.stack([
|
|
2308
|
-
cv2.remap(img[...,c], map_x, map_y, cv2.INTER_LANCZOS4,
|
|
2311
|
+
cv2.remap(img[..., c], map_x, map_y, cv2.INTER_LANCZOS4,
|
|
2309
2312
|
borderMode=cv2.BORDER_CONSTANT, borderValue=0)
|
|
2310
2313
|
for c in range(img.shape[2])
|
|
2311
2314
|
], axis=2)
|
|
@@ -2366,14 +2369,10 @@ class StarRegistrationWorker(QRunnable):
|
|
|
2366
2369
|
|
|
2367
2370
|
def run(self):
|
|
2368
2371
|
"""
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
Non-affine (homography/poly3/4):
|
|
2375
|
-
- This QRunnable does not try to do residuals; it just reports and emits identity.
|
|
2376
|
-
The multi-process residual pass is handled by StarRegistrationThread.
|
|
2372
|
+
Refinement worker ALWAYS computes incremental deltas in affine/similarity space,
|
|
2373
|
+
even if the FINAL requested model is homography/poly3/poly4.
|
|
2374
|
+
|
|
2375
|
+
The final non-affine model (if any) is applied in _finalize_write_job only.
|
|
2377
2376
|
"""
|
|
2378
2377
|
try:
|
|
2379
2378
|
_cap_native_threads_once()
|
|
@@ -2399,21 +2398,19 @@ class StarRegistrationWorker(QRunnable):
|
|
|
2399
2398
|
return
|
|
2400
2399
|
Href, Wref = ref_small.shape[:2]
|
|
2401
2400
|
|
|
2402
|
-
model
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
"emitting identity transform (handled by thread pass)."
|
|
2409
|
-
)
|
|
2410
|
-
self.signals.result_transform.emit(os.path.normpath(self.original_file), IDENTITY_2x3.copy())
|
|
2411
|
-
self.signals.result.emit(self.original_file)
|
|
2412
|
-
return
|
|
2401
|
+
# ✅ Refinement solve model: always affine or similarity
|
|
2402
|
+
model_req = (self.model_name or "affine").lower()
|
|
2403
|
+
if model_req in ("no_distortion", "nodistortion", "similarity"):
|
|
2404
|
+
refine_model = "similarity"
|
|
2405
|
+
else:
|
|
2406
|
+
refine_model = "affine" # includes when final requested is homography/poly*
|
|
2413
2407
|
|
|
2414
|
-
# --- Affine incremental
|
|
2415
2408
|
T_prev = np.array(self.current_transform, dtype=np.float32).reshape(2, 3)
|
|
2416
|
-
use_warp = not np.allclose(
|
|
2409
|
+
use_warp = not np.allclose(
|
|
2410
|
+
T_prev,
|
|
2411
|
+
np.array([[1, 0, 0], [0, 1, 0]], dtype=np.float32),
|
|
2412
|
+
rtol=1e-5, atol=1e-5
|
|
2413
|
+
)
|
|
2417
2414
|
|
|
2418
2415
|
if use_warp and cv2 is not None:
|
|
2419
2416
|
src_for_match = cv2.warpAffine(
|
|
@@ -2427,9 +2424,21 @@ class StarRegistrationWorker(QRunnable):
|
|
|
2427
2424
|
src_for_match = gray_small
|
|
2428
2425
|
|
|
2429
2426
|
try:
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2427
|
+
if refine_model == "similarity":
|
|
2428
|
+
transform = compute_similarity_transform_astroalign_cropped(
|
|
2429
|
+
src_for_match, ref_small,
|
|
2430
|
+
limit_stars=getattr(self, "limit_stars", None),
|
|
2431
|
+
det_sigma=getattr(self, "det_sigma", 12.0),
|
|
2432
|
+
minarea=getattr(self, "minarea", 10),
|
|
2433
|
+
h_reproj=getattr(self, "h_reproj", 3.0),
|
|
2434
|
+
)
|
|
2435
|
+
else:
|
|
2436
|
+
transform = self.compute_affine_transform_astroalign(
|
|
2437
|
+
src_for_match, ref_small,
|
|
2438
|
+
limit_stars=getattr(self, "limit_stars", None),
|
|
2439
|
+
det_sigma=getattr(self, "det_sigma", 12.0),
|
|
2440
|
+
minarea=getattr(self, "minarea", 10),
|
|
2441
|
+
)
|
|
2433
2442
|
except Exception as e:
|
|
2434
2443
|
msg = str(e)
|
|
2435
2444
|
base = os.path.basename(self.original_file)
|
|
@@ -2445,19 +2454,22 @@ class StarRegistrationWorker(QRunnable):
|
|
|
2445
2454
|
return
|
|
2446
2455
|
|
|
2447
2456
|
transform = np.array(transform, dtype=np.float64).reshape(2, 3)
|
|
2457
|
+
|
|
2458
|
+
# Similarity projection safety (no shear)
|
|
2459
|
+
if refine_model == "similarity":
|
|
2460
|
+
transform = _project_to_similarity(transform)
|
|
2461
|
+
|
|
2448
2462
|
key = os.path.normpath(self.original_file)
|
|
2449
2463
|
self.signals.result_transform.emit(key, transform)
|
|
2450
2464
|
self.signals.progress.emit(
|
|
2451
2465
|
f"Astroalign delta for {os.path.basename(self.original_file)} "
|
|
2452
|
-
f"(
|
|
2466
|
+
f"(refine={refine_model}, final={self.model_name}): dx={transform[0,2]:.2f}, dy={transform[1,2]:.2f}"
|
|
2453
2467
|
)
|
|
2454
2468
|
self.signals.result.emit(self.original_file)
|
|
2455
2469
|
|
|
2456
2470
|
except Exception as e:
|
|
2457
2471
|
self.signals.error.emit(f"Error processing {self.original_file}: {e}")
|
|
2458
2472
|
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
2473
|
@staticmethod
|
|
2462
2474
|
def compute_affine_transform_astroalign(source_img, reference_img,
|
|
2463
2475
|
scale=1.20,
|
|
@@ -2607,7 +2619,7 @@ class StarRegistrationThread(QThread):
|
|
|
2607
2619
|
self.det_sigma = float(self.align_prefs.get("det_sigma", 12.0))
|
|
2608
2620
|
self.limit_stars = int(self.align_prefs.get("limit_stars", 500))
|
|
2609
2621
|
self.minarea = int(self.align_prefs.get("minarea", 10))
|
|
2610
|
-
self.downsample = int(self.align_prefs.get("downsample",
|
|
2622
|
+
self.downsample = int(self.align_prefs.get("downsample", 3))
|
|
2611
2623
|
self.drizzle_xforms = {} # {orig_norm_path: (kind, matrix)}
|
|
2612
2624
|
|
|
2613
2625
|
@staticmethod
|
|
@@ -2957,23 +2969,37 @@ class StarRegistrationThread(QThread):
|
|
|
2957
2969
|
|
|
2958
2970
|
# ✂️ No DAO/RANSAC: astroalign handles detection internally.
|
|
2959
2971
|
|
|
2960
|
-
#
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
#
|
|
2964
|
-
#
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2972
|
+
# --- Build shared ref at full + downsampled solve-res ---
|
|
2973
|
+
self.ref_small_full = np.ascontiguousarray(ref2d.astype(np.float32, copy=False))
|
|
2974
|
+
|
|
2975
|
+
# Use existing preference key you already have: self.downsample
|
|
2976
|
+
# (you load it in __init__: self.downsample = int(self.align_prefs.get("downsample", 2)))
|
|
2977
|
+
ds = max(1, int(self.downsample))
|
|
2978
|
+
self.solve_downsample = ds
|
|
2979
|
+
|
|
2980
|
+
if ds > 1 and cv2 is not None:
|
|
2981
|
+
new_hw = (max(1, ref2d.shape[1] // ds), max(1, ref2d.shape[0] // ds)) # (W, H)
|
|
2982
|
+
ref_ds = cv2.resize(self.ref_small_full, new_hw, interpolation=cv2.INTER_AREA)
|
|
2983
|
+
else:
|
|
2984
|
+
ref_ds = self.ref_small_full
|
|
2985
|
+
|
|
2986
|
+
self.ref_small = self.ref_small_full # keep existing attribute name (full)
|
|
2987
|
+
self.ref_small_ds = np.ascontiguousarray(ref_ds.astype(np.float32, copy=False))
|
|
2969
2988
|
|
|
2970
2989
|
# Initialize transforms to identity for EVERY original frame
|
|
2971
2990
|
self.alignment_matrices = {os.path.normpath(f): IDENTITY_2x3.copy() for f in self.original_files}
|
|
2972
2991
|
self.delta_transforms = {}
|
|
2973
2992
|
|
|
2974
2993
|
# Progress totals (units = number of worker completions across passes)
|
|
2994
|
+
# Progress totals:
|
|
2995
|
+
# passes = N * passes
|
|
2996
|
+
# finalize = N
|
|
2997
|
+
N = len(self.original_files)
|
|
2998
|
+
P = max(1, int(self.max_refinement_passes))
|
|
2999
|
+
|
|
2975
3000
|
self._done = 0
|
|
2976
|
-
self._total =
|
|
3001
|
+
self._total = (N * P) + N # <-- IMPORTANT: include finalize
|
|
3002
|
+
self.progress_step.emit(self._done, self._total) # optional but helps UI reset immediately
|
|
2977
3003
|
|
|
2978
3004
|
# Registration passes (compute deltas only)
|
|
2979
3005
|
for pass_idx in range(self.max_refinement_passes):
|
|
@@ -3015,109 +3041,30 @@ class StarRegistrationThread(QThread):
|
|
|
3015
3041
|
def run_one_registration_pass(self, _ref_stars_unused, _ref_triangles_unused, pass_index):
|
|
3016
3042
|
_cap_native_threads_once()
|
|
3017
3043
|
import os
|
|
3018
|
-
import shutil
|
|
3019
|
-
import tempfile
|
|
3020
3044
|
import cv2
|
|
3045
|
+
import time
|
|
3021
3046
|
|
|
3022
|
-
model
|
|
3023
|
-
|
|
3024
|
-
|
|
3047
|
+
# Requested final model (used ONLY in finalize)
|
|
3048
|
+
final_model = (self.align_model or "affine").lower()
|
|
3049
|
+
|
|
3050
|
+
# ✅ Refinement model: affine or similarity only
|
|
3051
|
+
if final_model in ("no_distortion", "nodistortion", "similarity"):
|
|
3052
|
+
refine_model = "similarity"
|
|
3053
|
+
else:
|
|
3054
|
+
refine_model = "affine"
|
|
3055
|
+
|
|
3056
|
+
ref_small_ds = np.ascontiguousarray(self.ref_small_ds.astype(np.float32, copy=False))
|
|
3057
|
+
Href_ds, Wref_ds = ref_small_ds.shape[:2]
|
|
3058
|
+
ds = max(1, int(getattr(self, "solve_downsample", 1)))
|
|
3025
3059
|
|
|
3026
|
-
# ---
|
|
3060
|
+
# --- reverse map: current_path -> original_key
|
|
3027
3061
|
rev_current_to_orig = {}
|
|
3028
3062
|
for orig_k, curr_p in self.file_key_to_current_path.items():
|
|
3029
3063
|
rev_current_to_orig[os.path.normpath(curr_p)] = os.path.normpath(orig_k)
|
|
3030
3064
|
|
|
3031
|
-
# ---------- NON-AFFINE PATH: residuals-only ----------
|
|
3032
|
-
if model in ("homography", "poly3", "poly4"):
|
|
3033
|
-
work_list = list(self.original_files)
|
|
3034
|
-
|
|
3035
|
-
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
3036
|
-
procs = max(2, min((os.cpu_count() or 8), 32))
|
|
3037
|
-
self.progress_update.emit(f"Using {procs} processes to measure residuals (model={model}).")
|
|
3038
|
-
|
|
3039
|
-
tmpdir = tempfile.mkdtemp(prefix="sas_resid_")
|
|
3040
|
-
ref_npy = os.path.join(tmpdir, "ref_small.npy")
|
|
3041
|
-
try:
|
|
3042
|
-
np.save(ref_npy, ref_small)
|
|
3043
|
-
except Exception as e:
|
|
3044
|
-
try: shutil.rmtree(tmpdir, ignore_errors=True)
|
|
3045
|
-
except Exception as e:
|
|
3046
|
-
import logging
|
|
3047
|
-
logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
|
|
3048
|
-
self.on_worker_error(f"Failed to persist residual reference: {e}")
|
|
3049
|
-
return False, "Residual pass aborted."
|
|
3050
|
-
|
|
3051
|
-
pass_deltas = []
|
|
3052
|
-
try:
|
|
3053
|
-
|
|
3054
|
-
import time
|
|
3055
|
-
|
|
3056
|
-
jobs = [
|
|
3057
|
-
(p, ref_npy, model, self.h_reproj, self.det_sigma, self.minarea, self.limit_stars)
|
|
3058
|
-
for p in work_list
|
|
3059
|
-
]
|
|
3060
|
-
total = len(jobs)
|
|
3061
|
-
done = 0
|
|
3062
|
-
|
|
3063
|
-
self.progress_update.emit(f"Using {procs} processes to measure residuals (model={model}).")
|
|
3064
|
-
self.progress_step.emit(0, total)
|
|
3065
|
-
|
|
3066
|
-
with _make_executor(procs) as ex:
|
|
3067
|
-
pending = {ex.submit(_residual_job_worker, j): j[0] for j in jobs}
|
|
3068
|
-
last_heartbeat = time.monotonic()
|
|
3069
|
-
|
|
3070
|
-
while pending:
|
|
3071
|
-
done_set, pending = wait(pending, timeout=0.6, return_when=FIRST_COMPLETED)
|
|
3072
|
-
# heartbeat if nothing finished for a bit
|
|
3073
|
-
now = time.monotonic()
|
|
3074
|
-
if not done_set and (now - last_heartbeat) > 2.0:
|
|
3075
|
-
self.progress_update.emit(f"… measuring residuals ({done}/{total} done)")
|
|
3076
|
-
last_heartbeat = now
|
|
3077
|
-
|
|
3078
|
-
for fut in done_set:
|
|
3079
|
-
orig_pth = os.path.normpath(pending.pop(fut, "<unknown>")) if fut in pending else "<unknown>"
|
|
3080
|
-
try:
|
|
3081
|
-
pth, rms, err = fut.result()
|
|
3082
|
-
except Exception as e:
|
|
3083
|
-
pth, rms, err = (orig_pth, float("inf"), f"Worker crashed: {e}")
|
|
3084
|
-
|
|
3085
|
-
k_orig = os.path.normpath(pth or orig_pth)
|
|
3086
|
-
if err:
|
|
3087
|
-
self.on_worker_error(f"Residual measure failed for {os.path.basename(k_orig)}: {err}")
|
|
3088
|
-
self.delta_transforms[k_orig] = float("inf")
|
|
3089
|
-
else:
|
|
3090
|
-
self.delta_transforms[k_orig] = float(rms)
|
|
3091
|
-
self.progress_update.emit(
|
|
3092
|
-
f"[residuals] {os.path.basename(k_orig)} → RMS={rms:.2f}px"
|
|
3093
|
-
)
|
|
3094
|
-
|
|
3095
|
-
done += 1
|
|
3096
|
-
self.progress_step.emit(done, total)
|
|
3097
|
-
last_heartbeat = now
|
|
3098
|
-
|
|
3099
|
-
for orig in self.original_files:
|
|
3100
|
-
pass_deltas.append(self.delta_transforms.get(os.path.normpath(orig), float("inf")))
|
|
3101
|
-
self.transform_deltas.append(pass_deltas)
|
|
3102
|
-
|
|
3103
|
-
preview = ", ".join([f"{d:.2f}" if np.isfinite(d) else "∞" for d in pass_deltas[:10]])
|
|
3104
|
-
if len(pass_deltas) > 10:
|
|
3105
|
-
preview += f" … ({len(pass_deltas)} total)"
|
|
3106
|
-
self.progress_update.emit(f"Pass {pass_index + 1}: residual RMS px [{preview}]")
|
|
3107
|
-
|
|
3108
|
-
aligned_count = sum(1 for d in pass_deltas if np.isfinite(d) and d <= self.shift_tolerance)
|
|
3109
|
-
if aligned_count:
|
|
3110
|
-
self.progress_update.emit(f"Within tolerance (≤ {self.shift_tolerance:.2f}px): {aligned_count} frame(s)")
|
|
3111
|
-
return True, "Residual pass complete."
|
|
3112
|
-
finally:
|
|
3113
|
-
try: shutil.rmtree(tmpdir, ignore_errors=True)
|
|
3114
|
-
except Exception as e:
|
|
3115
|
-
import logging
|
|
3116
|
-
logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
|
|
3117
|
-
|
|
3118
|
-
# ---------- AFFINE PATH (incremental delta accumulation) ----------
|
|
3119
3065
|
resample_flag = cv2.INTER_AREA if pass_index == 0 else cv2.INTER_LINEAR
|
|
3120
3066
|
|
|
3067
|
+
# Work list: pass 0 all; later passes skip within tolerance
|
|
3121
3068
|
if pass_index == 0:
|
|
3122
3069
|
work_list = list(self.original_files)
|
|
3123
3070
|
else:
|
|
@@ -3144,30 +3091,36 @@ class StarRegistrationThread(QThread):
|
|
|
3144
3091
|
return True, "Pass complete (nothing to refine)."
|
|
3145
3092
|
|
|
3146
3093
|
procs = max(2, min((os.cpu_count() or 8), 32))
|
|
3147
|
-
self.progress_update.emit(f"Using {procs} processes for stellar alignment (
|
|
3094
|
+
self.progress_update.emit(f"Using {procs} processes for stellar alignment (refine={refine_model}).")
|
|
3148
3095
|
|
|
3149
3096
|
timeout_sec = int(self.align_prefs.get("timeout_per_job_sec", 300))
|
|
3097
|
+
|
|
3150
3098
|
jobs = []
|
|
3151
3099
|
for orig_key in work_list:
|
|
3152
3100
|
ok = os.path.normpath(orig_key)
|
|
3153
|
-
|
|
3101
|
+
|
|
3102
|
+
# IMPORTANT: refinement reads ORIGINAL frame (no intermediate saves)
|
|
3103
|
+
current_path = ok
|
|
3104
|
+
|
|
3154
3105
|
current_transform = self.alignment_matrices.get(ok, IDENTITY_2x3)
|
|
3106
|
+
|
|
3155
3107
|
jobs.append((
|
|
3156
3108
|
current_path,
|
|
3157
3109
|
current_transform,
|
|
3158
|
-
|
|
3159
|
-
resample_flag, float(self.det_sigma),
|
|
3160
|
-
|
|
3110
|
+
ref_small_ds, int(Wref_ds), int(Href_ds),
|
|
3111
|
+
resample_flag, float(self.det_sigma),
|
|
3112
|
+
int(self.limit_stars) if self.limit_stars is not None else None,
|
|
3113
|
+
int(self.minarea),
|
|
3114
|
+
refine_model, float(self.h_reproj),
|
|
3115
|
+
int(ds)
|
|
3161
3116
|
))
|
|
3162
3117
|
|
|
3163
|
-
import time
|
|
3164
3118
|
executor = _make_executor(procs)
|
|
3165
|
-
|
|
3166
3119
|
try:
|
|
3167
3120
|
fut_info, pending = {}, set()
|
|
3168
3121
|
for j in jobs:
|
|
3169
3122
|
f = executor.submit(_solve_delta_job, j)
|
|
3170
|
-
fut_info[f] = (time.monotonic(), j[0])
|
|
3123
|
+
fut_info[f] = (time.monotonic(), j[0])
|
|
3171
3124
|
pending.add(f)
|
|
3172
3125
|
|
|
3173
3126
|
while pending:
|
|
@@ -3179,7 +3132,7 @@ class StarRegistrationThread(QThread):
|
|
|
3179
3132
|
except Exception as e:
|
|
3180
3133
|
curr_path_r, T_new, err = (returned_path or "<unknown>", None, f"Worker crashed: {e}")
|
|
3181
3134
|
|
|
3182
|
-
# Map
|
|
3135
|
+
# Map back to ORIGINAL key
|
|
3183
3136
|
curr_norm = os.path.normpath(curr_path_r)
|
|
3184
3137
|
k_orig = rev_current_to_orig.get(curr_norm, curr_norm)
|
|
3185
3138
|
|
|
@@ -3189,10 +3142,13 @@ class StarRegistrationThread(QThread):
|
|
|
3189
3142
|
continue
|
|
3190
3143
|
|
|
3191
3144
|
T_new = np.array(T_new, dtype=np.float64).reshape(2, 3)
|
|
3192
|
-
|
|
3193
|
-
|
|
3145
|
+
|
|
3146
|
+
if refine_model == "similarity":
|
|
3147
|
+
T_new = _project_to_similarity(T_new)
|
|
3148
|
+
|
|
3194
3149
|
self.delta_transforms[k_orig] = float(np.hypot(T_new[0, 2], T_new[1, 2]))
|
|
3195
3150
|
|
|
3151
|
+
# Accumulate: T_total = T_new ∘ T_prev
|
|
3196
3152
|
T_prev = np.array(self.alignment_matrices.get(k_orig, IDENTITY_2x3), dtype=np.float64).reshape(2, 3)
|
|
3197
3153
|
prev_3 = np.vstack([T_prev, [0, 0, 1]])
|
|
3198
3154
|
new_3 = np.vstack([T_new, [0, 0, 1]])
|
|
@@ -3200,7 +3156,7 @@ class StarRegistrationThread(QThread):
|
|
|
3200
3156
|
|
|
3201
3157
|
self.on_worker_progress(
|
|
3202
3158
|
f"Astroalign delta for {os.path.basename(curr_path_r)} "
|
|
3203
|
-
f"(
|
|
3159
|
+
f"(refine={refine_model}, final={final_model}): dx={T_new[0,2]:.2f}, dy={T_new[1,2]:.2f}"
|
|
3204
3160
|
)
|
|
3205
3161
|
self._increment_progress()
|
|
3206
3162
|
|
|
@@ -3235,7 +3191,7 @@ class StarRegistrationThread(QThread):
|
|
|
3235
3191
|
preview += f" … ({len(pass_deltas)} total)"
|
|
3236
3192
|
self.progress_update.emit(f"Pass {pass_index + 1} delta shifts: [{preview}]")
|
|
3237
3193
|
if aligned_count:
|
|
3238
|
-
self.progress_update.emit(f"
|
|
3194
|
+
self.progress_update.emit(f"Within tolerance (≤ {self.shift_tolerance:.2f}px): {aligned_count} frame(s)")
|
|
3239
3195
|
return True, "Pass complete."
|
|
3240
3196
|
finally:
|
|
3241
3197
|
try:
|
|
@@ -3243,7 +3199,6 @@ class StarRegistrationThread(QThread):
|
|
|
3243
3199
|
except Exception:
|
|
3244
3200
|
pass
|
|
3245
3201
|
|
|
3246
|
-
|
|
3247
3202
|
def on_worker_result_transform(self, persistent_key, new_transform):
|
|
3248
3203
|
k = os.path.normpath(persistent_key)
|
|
3249
3204
|
T_new = np.array(new_transform, dtype=np.float64).reshape(2, 3)
|
|
@@ -3384,8 +3339,8 @@ class StarRegistrationThread(QThread):
|
|
|
3384
3339
|
A = np.asarray(self.alignment_matrices.get(k, IDENTITY_2x3), dtype=np.float64)
|
|
3385
3340
|
|
|
3386
3341
|
# 👉 If non-affine, we pass identity to make workers solve from scratch
|
|
3387
|
-
if self.align_model.lower() in ("homography", "poly3", "poly4"):
|
|
3388
|
-
|
|
3342
|
+
#if self.align_model.lower() in ("homography", "poly3", "poly4"):
|
|
3343
|
+
# A = IDENTITY_2x3.copy()
|
|
3389
3344
|
|
|
3390
3345
|
jobs.append((
|
|
3391
3346
|
orig_path,
|
|
@@ -3411,6 +3366,7 @@ class StarRegistrationThread(QThread):
|
|
|
3411
3366
|
orig_path, out_path, msg, success, drizzle = fut.result()
|
|
3412
3367
|
except Exception as e:
|
|
3413
3368
|
self.progress_update.emit(f"⚠️ Finalize worker crashed: {e}")
|
|
3369
|
+
self._increment_progress()
|
|
3414
3370
|
continue
|
|
3415
3371
|
|
|
3416
3372
|
if msg:
|
|
@@ -3433,6 +3389,7 @@ class StarRegistrationThread(QThread):
|
|
|
3433
3389
|
self.drizzle_xforms[k] = (str(kind), None) # poly3/4
|
|
3434
3390
|
except Exception:
|
|
3435
3391
|
pass
|
|
3392
|
+
self._increment_progress()
|
|
3436
3393
|
finally:
|
|
3437
3394
|
try: shutil.rmtree(tmpdir, ignore_errors=True)
|
|
3438
3395
|
except Exception as e:
|
|
@@ -4639,6 +4596,9 @@ class MosaicMasterDialog(QDialog):
|
|
|
4639
4596
|
self._list_open_docs_fn = list_open_docs_fn
|
|
4640
4597
|
|
|
4641
4598
|
self.setWindowTitle("Mosaic Master")
|
|
4599
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
4600
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
4601
|
+
self.setModal(False)
|
|
4642
4602
|
self.wrench_path = wrench_path
|
|
4643
4603
|
self.spinner_path = spinner_path
|
|
4644
4604
|
|