setiastrosuitepro 1.6.7__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/__init__.py +2 -0
- 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/acv_icon.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/first_quarter.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/full_moon.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/last_quarter.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/new_moon.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/waning_crescent_1.png +0 -0
- setiastro/images/waning_crescent_2.png +0 -0
- setiastro/images/waning_crescent_3.png +0 -0
- setiastro/images/waning_crescent_4.png +0 -0
- setiastro/images/waning_crescent_5.png +0 -0
- setiastro/images/waning_gibbous_1.png +0 -0
- setiastro/images/waning_gibbous_2.png +0 -0
- setiastro/images/waning_gibbous_3.png +0 -0
- setiastro/images/waning_gibbous_4.png +0 -0
- setiastro/images/waning_gibbous_5.png +0 -0
- setiastro/images/waxing_crescent_1.png +0 -0
- setiastro/images/waxing_crescent_2.png +0 -0
- setiastro/images/waxing_crescent_3.png +0 -0
- setiastro/images/waxing_crescent_4.png +0 -0
- setiastro/images/waxing_crescent_5.png +0 -0
- setiastro/images/waxing_gibbous_1.png +0 -0
- setiastro/images/waxing_gibbous_2.png +0 -0
- setiastro/images/waxing_gibbous_3.png +0 -0
- setiastro/images/waxing_gibbous_4.png +0 -0
- setiastro/images/waxing_gibbous_5.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 +128 -0
- setiastro/saspro/__init__.py +20 -0
- setiastro/saspro/__main__.py +964 -0
- setiastro/saspro/_generated/__init__.py +7 -0
- setiastro/saspro/_generated/build_info.py +3 -0
- setiastro/saspro/abe.py +1379 -0
- setiastro/saspro/abe_preset.py +196 -0
- setiastro/saspro/aberration_ai.py +910 -0
- setiastro/saspro/aberration_ai_preset.py +224 -0
- setiastro/saspro/accel_installer.py +218 -0
- setiastro/saspro/accel_workers.py +30 -0
- setiastro/saspro/acv_exporter.py +379 -0
- setiastro/saspro/add_stars.py +627 -0
- setiastro/saspro/astrobin_exporter.py +1010 -0
- setiastro/saspro/astrospike.py +153 -0
- setiastro/saspro/astrospike_python.py +1841 -0
- setiastro/saspro/autostretch.py +198 -0
- setiastro/saspro/backgroundneutral.py +639 -0
- setiastro/saspro/batch_convert.py +328 -0
- setiastro/saspro/batch_renamer.py +522 -0
- setiastro/saspro/blemish_blaster.py +494 -0
- setiastro/saspro/blink_comparator_pro.py +3149 -0
- setiastro/saspro/bundles.py +61 -0
- setiastro/saspro/bundles_dock.py +114 -0
- setiastro/saspro/cheat_sheet.py +213 -0
- setiastro/saspro/clahe.py +371 -0
- setiastro/saspro/comet_stacking.py +1442 -0
- setiastro/saspro/common_tr.py +107 -0
- setiastro/saspro/config.py +38 -0
- setiastro/saspro/config_bootstrap.py +40 -0
- setiastro/saspro/config_manager.py +316 -0
- setiastro/saspro/continuum_subtract.py +1620 -0
- setiastro/saspro/convo.py +1403 -0
- setiastro/saspro/convo_preset.py +414 -0
- setiastro/saspro/copyastro.py +190 -0
- setiastro/saspro/cosmicclarity.py +1593 -0
- setiastro/saspro/cosmicclarity_preset.py +407 -0
- setiastro/saspro/crop_dialog_pro.py +1005 -0
- setiastro/saspro/crop_preset.py +189 -0
- setiastro/saspro/curve_editor_pro.py +2608 -0
- setiastro/saspro/curves_preset.py +375 -0
- setiastro/saspro/debayer.py +673 -0
- setiastro/saspro/debug_utils.py +29 -0
- setiastro/saspro/dnd_mime.py +35 -0
- setiastro/saspro/doc_manager.py +2727 -0
- setiastro/saspro/exoplanet_detector.py +2258 -0
- setiastro/saspro/file_utils.py +284 -0
- setiastro/saspro/fitsmodifier.py +748 -0
- setiastro/saspro/fix_bom.py +32 -0
- setiastro/saspro/free_torch_memory.py +48 -0
- setiastro/saspro/frequency_separation.py +1352 -0
- setiastro/saspro/function_bundle.py +1596 -0
- setiastro/saspro/generate_translations.py +3092 -0
- setiastro/saspro/ghs_dialog_pro.py +728 -0
- setiastro/saspro/ghs_preset.py +284 -0
- setiastro/saspro/graxpert.py +638 -0
- setiastro/saspro/graxpert_preset.py +287 -0
- setiastro/saspro/gui/__init__.py +0 -0
- setiastro/saspro/gui/main_window.py +8928 -0
- setiastro/saspro/gui/mixins/__init__.py +33 -0
- setiastro/saspro/gui/mixins/dock_mixin.py +375 -0
- setiastro/saspro/gui/mixins/file_mixin.py +450 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +503 -0
- setiastro/saspro/gui/mixins/header_mixin.py +441 -0
- setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
- setiastro/saspro/gui/mixins/menu_mixin.py +391 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +1824 -0
- setiastro/saspro/gui/mixins/update_mixin.py +323 -0
- setiastro/saspro/gui/mixins/view_mixin.py +477 -0
- setiastro/saspro/gui/statistics_dialog.py +47 -0
- setiastro/saspro/halobgon.py +492 -0
- setiastro/saspro/header_viewer.py +448 -0
- setiastro/saspro/headless_utils.py +88 -0
- setiastro/saspro/histogram.py +760 -0
- setiastro/saspro/history_explorer.py +941 -0
- setiastro/saspro/i18n.py +168 -0
- setiastro/saspro/image_combine.py +421 -0
- setiastro/saspro/image_peeker_pro.py +1608 -0
- setiastro/saspro/imageops/__init__.py +37 -0
- setiastro/saspro/imageops/mdi_snap.py +292 -0
- setiastro/saspro/imageops/scnr.py +36 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
- setiastro/saspro/imageops/stretch.py +236 -0
- setiastro/saspro/isophote.py +1186 -0
- setiastro/saspro/layers.py +208 -0
- setiastro/saspro/layers_dock.py +714 -0
- setiastro/saspro/lazy_imports.py +193 -0
- setiastro/saspro/legacy/__init__.py +2 -0
- setiastro/saspro/legacy/image_manager.py +2360 -0
- setiastro/saspro/legacy/numba_utils.py +3676 -0
- setiastro/saspro/legacy/xisf.py +1213 -0
- setiastro/saspro/linear_fit.py +537 -0
- setiastro/saspro/live_stacking.py +1854 -0
- setiastro/saspro/log_bus.py +5 -0
- setiastro/saspro/logging_config.py +460 -0
- setiastro/saspro/luminancerecombine.py +510 -0
- setiastro/saspro/main_helpers.py +201 -0
- setiastro/saspro/mask_creation.py +1090 -0
- setiastro/saspro/masks_core.py +56 -0
- setiastro/saspro/mdi_widgets.py +353 -0
- setiastro/saspro/memory_utils.py +666 -0
- setiastro/saspro/metadata_patcher.py +75 -0
- setiastro/saspro/mfdeconv.py +3909 -0
- setiastro/saspro/mfdeconv_earlystop.py +71 -0
- setiastro/saspro/mfdeconvcudnn.py +3312 -0
- setiastro/saspro/mfdeconvsport.py +2459 -0
- setiastro/saspro/minorbodycatalog.py +567 -0
- setiastro/saspro/morphology.py +411 -0
- setiastro/saspro/multiscale_decomp.py +1751 -0
- setiastro/saspro/nbtorgb_stars.py +541 -0
- setiastro/saspro/numba_utils.py +3145 -0
- setiastro/saspro/numba_warmup.py +141 -0
- setiastro/saspro/ops/__init__.py +9 -0
- setiastro/saspro/ops/command_help_dialog.py +623 -0
- setiastro/saspro/ops/command_runner.py +217 -0
- setiastro/saspro/ops/commands.py +1594 -0
- setiastro/saspro/ops/script_editor.py +1105 -0
- setiastro/saspro/ops/scripts.py +1476 -0
- setiastro/saspro/ops/settings.py +637 -0
- setiastro/saspro/parallel_utils.py +554 -0
- setiastro/saspro/pedestal.py +121 -0
- setiastro/saspro/perfect_palette_picker.py +1105 -0
- setiastro/saspro/pipeline.py +110 -0
- setiastro/saspro/pixelmath.py +1604 -0
- setiastro/saspro/plate_solver.py +2480 -0
- setiastro/saspro/project_io.py +797 -0
- setiastro/saspro/psf_utils.py +136 -0
- setiastro/saspro/psf_viewer.py +631 -0
- setiastro/saspro/pyi_rthook_astroquery.py +95 -0
- setiastro/saspro/remove_green.py +331 -0
- setiastro/saspro/remove_stars.py +1599 -0
- setiastro/saspro/remove_stars_preset.py +446 -0
- setiastro/saspro/resources.py +570 -0
- setiastro/saspro/rgb_combination.py +208 -0
- setiastro/saspro/rgb_extract.py +19 -0
- setiastro/saspro/rgbalign.py +727 -0
- setiastro/saspro/runtime_imports.py +7 -0
- setiastro/saspro/runtime_torch.py +754 -0
- setiastro/saspro/save_options.py +73 -0
- setiastro/saspro/selective_color.py +1614 -0
- setiastro/saspro/sfcc.py +1530 -0
- setiastro/saspro/shortcuts.py +3125 -0
- setiastro/saspro/signature_insert.py +1106 -0
- setiastro/saspro/stacking_suite.py +19069 -0
- setiastro/saspro/star_alignment.py +7383 -0
- setiastro/saspro/star_alignment_preset.py +329 -0
- setiastro/saspro/star_metrics.py +49 -0
- setiastro/saspro/star_spikes.py +769 -0
- setiastro/saspro/star_stretch.py +542 -0
- setiastro/saspro/stat_stretch.py +554 -0
- setiastro/saspro/status_log_dock.py +78 -0
- setiastro/saspro/subwindow.py +3523 -0
- setiastro/saspro/supernovaasteroidhunter.py +1719 -0
- setiastro/saspro/swap_manager.py +134 -0
- setiastro/saspro/torch_backend.py +89 -0
- setiastro/saspro/torch_rejection.py +434 -0
- 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 +1558 -0
- setiastro/saspro/wavescale_hdr.py +648 -0
- setiastro/saspro/wavescale_hdr_preset.py +101 -0
- setiastro/saspro/wavescalede.py +683 -0
- setiastro/saspro/wavescalede_preset.py +230 -0
- setiastro/saspro/wcs_update.py +374 -0
- setiastro/saspro/whitebalance.py +540 -0
- setiastro/saspro/widgets/__init__.py +48 -0
- setiastro/saspro/widgets/common_utilities.py +306 -0
- setiastro/saspro/widgets/graphics_views.py +122 -0
- setiastro/saspro/widgets/image_utils.py +518 -0
- 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 +280 -0
- setiastro/saspro/widgets/resource_monitor.py +313 -0
- setiastro/saspro/widgets/spinboxes.py +290 -0
- setiastro/saspro/widgets/themed_buttons.py +13 -0
- setiastro/saspro/widgets/wavelet_utils.py +331 -0
- setiastro/saspro/wimi.py +7367 -0
- setiastro/saspro/wims.py +588 -0
- setiastro/saspro/window_shelf.py +185 -0
- setiastro/saspro/xisf.py +1213 -0
- setiastrosuitepro-1.6.7.dist-info/METADATA +279 -0
- setiastrosuitepro-1.6.7.dist-info/RECORD +394 -0
- setiastrosuitepro-1.6.7.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.7.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.7.dist-info/licenses/LICENSE +674 -0
- setiastrosuitepro-1.6.7.dist-info/licenses/license.txt +2580 -0
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
# pro/aberration_ai.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import os
|
|
4
|
+
import webbrowser
|
|
5
|
+
import requests
|
|
6
|
+
import numpy as np
|
|
7
|
+
import sys
|
|
8
|
+
import platform # add
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
IS_APPLE_ARM = (sys.platform == "darwin" and platform.machine() == "arm64")
|
|
12
|
+
|
|
13
|
+
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QStandardPaths, QSettings
|
|
14
|
+
from PyQt6.QtWidgets import (
|
|
15
|
+
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFileDialog,
|
|
16
|
+
QComboBox, QSpinBox, QProgressBar, QMessageBox, QCheckBox, QLineEdit
|
|
17
|
+
)
|
|
18
|
+
from PyQt6.QtGui import QIcon
|
|
19
|
+
from setiastro.saspro.config import Config
|
|
20
|
+
|
|
21
|
+
# Optional import (soft dep)
|
|
22
|
+
try:
|
|
23
|
+
import onnxruntime as ort
|
|
24
|
+
except Exception:
|
|
25
|
+
ort = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------- GitHub model fetching ----------
|
|
29
|
+
GITHUB_REPO = Config.GITHUB_ABERRATION_REPO
|
|
30
|
+
LATEST_API = f"https://api.github.com/repos/{GITHUB_REPO}/releases/latest"
|
|
31
|
+
|
|
32
|
+
def _model_required_patch(model_path: str) -> int | None:
|
|
33
|
+
"""
|
|
34
|
+
Returns the fixed spatial size the model expects (e.g. 512), or None if dynamic.
|
|
35
|
+
"""
|
|
36
|
+
if ort is None or not os.path.isfile(model_path):
|
|
37
|
+
return None
|
|
38
|
+
try:
|
|
39
|
+
sess = ort.InferenceSession(model_path, providers=["CPUExecutionProvider"])
|
|
40
|
+
shp = sess.get_inputs()[0].shape # e.g. [1, 1, 512, 512] or ['N','C',512,512]
|
|
41
|
+
h = shp[-2]; w = shp[-1]
|
|
42
|
+
if isinstance(h, int) and isinstance(w, int) and h == w:
|
|
43
|
+
return int(h)
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _app_model_dir() -> str:
|
|
50
|
+
d = Config.get_aberration_models_dir()
|
|
51
|
+
os.makedirs(d, exist_ok=True)
|
|
52
|
+
return d
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class _DownloadWorker(QThread):
|
|
56
|
+
progressed = pyqtSignal(int) # 0..100 (downloaded)
|
|
57
|
+
failed = pyqtSignal(str)
|
|
58
|
+
finished_ok= pyqtSignal(str) # path
|
|
59
|
+
|
|
60
|
+
def __init__(self, dst_dir: str):
|
|
61
|
+
super().__init__()
|
|
62
|
+
self.dst_dir = dst_dir
|
|
63
|
+
|
|
64
|
+
def run(self):
|
|
65
|
+
try:
|
|
66
|
+
r = requests.get(LATEST_API, timeout=10)
|
|
67
|
+
if r.status_code != 200:
|
|
68
|
+
raise RuntimeError(f"GitHub API error: {r.status_code}")
|
|
69
|
+
js = r.json()
|
|
70
|
+
assets = js.get("assets", [])
|
|
71
|
+
onnx_assets = [a for a in assets if a.get("name","").lower().endswith(".onnx")]
|
|
72
|
+
if not onnx_assets:
|
|
73
|
+
raise RuntimeError("No .onnx asset found in latest release.")
|
|
74
|
+
asset = onnx_assets[0]
|
|
75
|
+
url = asset["browser_download_url"]
|
|
76
|
+
name = asset["name"]
|
|
77
|
+
out_path = os.path.join(self.dst_dir, name)
|
|
78
|
+
|
|
79
|
+
with requests.get(url, stream=True, timeout=60) as rr:
|
|
80
|
+
rr.raise_for_status()
|
|
81
|
+
total = int(rr.headers.get("Content-Length", "0") or 0)
|
|
82
|
+
got = 0
|
|
83
|
+
chunk = 1 << 20
|
|
84
|
+
with open(out_path, "wb") as f:
|
|
85
|
+
for blk in rr.iter_content(chunk):
|
|
86
|
+
if blk:
|
|
87
|
+
f.write(blk)
|
|
88
|
+
got += len(blk)
|
|
89
|
+
if total > 0:
|
|
90
|
+
self.progressed.emit(int(got * 100 / total))
|
|
91
|
+
self.finished_ok.emit(out_path)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
self.failed.emit(str(e))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------- core: tiling + hann blend ----------
|
|
97
|
+
def _hann2d(n: int) -> np.ndarray:
|
|
98
|
+
w = np.hanning(n).astype(np.float32)
|
|
99
|
+
return (w[:, None] * w[None, :])
|
|
100
|
+
|
|
101
|
+
def _tile_indices(n: int, patch: int, overlap: int) -> list[int]:
|
|
102
|
+
stride = patch - overlap
|
|
103
|
+
if patch >= n:
|
|
104
|
+
return [0]
|
|
105
|
+
idx, pos = [], 0
|
|
106
|
+
while True:
|
|
107
|
+
if pos + patch >= n:
|
|
108
|
+
idx.append(n - patch)
|
|
109
|
+
break
|
|
110
|
+
idx.append(pos); pos += stride
|
|
111
|
+
return sorted(set(idx))
|
|
112
|
+
|
|
113
|
+
def _pad_C_HW(arr: np.ndarray, patch: int) -> tuple[np.ndarray, int, int]:
|
|
114
|
+
C, H, W = arr.shape
|
|
115
|
+
pad_h = max(0, patch - H)
|
|
116
|
+
pad_w = max(0, patch - W)
|
|
117
|
+
if pad_h or pad_w:
|
|
118
|
+
arr = np.pad(arr, ((0,0),(0,pad_h),(0,pad_w)), mode="edge")
|
|
119
|
+
return arr, H, W
|
|
120
|
+
|
|
121
|
+
def _prepare_input(img: np.ndarray) -> tuple[np.ndarray, bool, bool]:
|
|
122
|
+
"""
|
|
123
|
+
Returns (C,H,W) float32 in [0..1]; also returns (channels_last, was_uint16)
|
|
124
|
+
"""
|
|
125
|
+
channels_last = (img.ndim == 3)
|
|
126
|
+
if channels_last:
|
|
127
|
+
arr = img.transpose(2,0,1) # (C,H,W)
|
|
128
|
+
else:
|
|
129
|
+
arr = img[np.newaxis, ...] # (1,H,W)
|
|
130
|
+
was_uint16 = (arr.dtype == np.uint16)
|
|
131
|
+
if was_uint16:
|
|
132
|
+
arr = arr.astype(np.float32) / 65535.0
|
|
133
|
+
else:
|
|
134
|
+
arr = arr.astype(np.float32)
|
|
135
|
+
return arr, channels_last, was_uint16
|
|
136
|
+
|
|
137
|
+
def _restore_output(arr: np.ndarray, channels_last: bool, was_uint16: bool, H: int, W: int) -> np.ndarray:
|
|
138
|
+
arr = arr[:, :H, :W]
|
|
139
|
+
arr = np.clip(np.nan_to_num(arr), 0.0, 1.0)
|
|
140
|
+
if was_uint16:
|
|
141
|
+
arr = (arr * 65535.0).astype(np.uint16)
|
|
142
|
+
if channels_last:
|
|
143
|
+
arr = arr.transpose(1,2,0) # (H,W,C)
|
|
144
|
+
else:
|
|
145
|
+
arr = arr[0] # (H,W)
|
|
146
|
+
return arr
|
|
147
|
+
|
|
148
|
+
def run_onnx_tiled(session, img: np.ndarray, patch_size=512, overlap=64, progress_cb=None) -> np.ndarray:
|
|
149
|
+
"""
|
|
150
|
+
session: onnxruntime.InferenceSession
|
|
151
|
+
img: mono (H,W) or RGB (H,W,3) numpy array
|
|
152
|
+
"""
|
|
153
|
+
arr, channels_last, was_uint16 = _prepare_input(img) # (C,H,W)
|
|
154
|
+
arr, H0, W0 = _pad_C_HW(arr, patch_size)
|
|
155
|
+
C, H, W = arr.shape
|
|
156
|
+
|
|
157
|
+
win = _hann2d(patch_size)
|
|
158
|
+
out = np.zeros_like(arr, dtype=np.float32)
|
|
159
|
+
wgt = np.zeros_like(arr, dtype=np.float32)
|
|
160
|
+
|
|
161
|
+
hs = _tile_indices(H, patch_size, overlap)
|
|
162
|
+
ws = _tile_indices(W, patch_size, overlap)
|
|
163
|
+
|
|
164
|
+
inp_name = session.get_inputs()[0].name
|
|
165
|
+
total = len(hs) * len(ws) * C
|
|
166
|
+
done = 0
|
|
167
|
+
|
|
168
|
+
for c in range(C):
|
|
169
|
+
for i in hs:
|
|
170
|
+
for j in ws:
|
|
171
|
+
patch = arr[c:c+1, i:i+patch_size, j:j+patch_size] # (1, P, P)
|
|
172
|
+
inp = np.ascontiguousarray(patch[np.newaxis, ...], dtype=np.float32) # (1,1,P,P)
|
|
173
|
+
|
|
174
|
+
out_patch = session.run(None, {inp_name: inp})[0] # (1,1,P,P)
|
|
175
|
+
out_patch = np.squeeze(out_patch, axis=0) # (1,P,P)
|
|
176
|
+
out[c:c+1, i:i+patch_size, j:j+patch_size] += out_patch * win
|
|
177
|
+
wgt[c:c+1, i:i+patch_size, j:j+patch_size] += win
|
|
178
|
+
|
|
179
|
+
done += 1
|
|
180
|
+
if progress_cb:
|
|
181
|
+
progress_cb(done / max(1, total))
|
|
182
|
+
|
|
183
|
+
wgt[wgt == 0] = 1.0
|
|
184
|
+
arr = out / wgt
|
|
185
|
+
return _restore_output(arr, channels_last, was_uint16, H0, W0)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ---------- providers ----------
|
|
189
|
+
def pick_providers(auto_gpu=True) -> list[str]:
|
|
190
|
+
"""
|
|
191
|
+
Windows: DirectML → CUDA → CPU
|
|
192
|
+
mac(Intel): CPU → CoreML (optional)
|
|
193
|
+
mac(Apple Silicon): **CPU only** (avoid CoreML artifact path)
|
|
194
|
+
"""
|
|
195
|
+
if ort is None:
|
|
196
|
+
return []
|
|
197
|
+
|
|
198
|
+
avail = set(ort.get_available_providers())
|
|
199
|
+
|
|
200
|
+
# Apple Silicon: always CPU ( CoreML has 16,384-dim constraint and can artifact )
|
|
201
|
+
if IS_APPLE_ARM:
|
|
202
|
+
return ["CPUExecutionProvider"] if "CPUExecutionProvider" in avail else []
|
|
203
|
+
|
|
204
|
+
# Non-Apple ARM
|
|
205
|
+
if not auto_gpu:
|
|
206
|
+
return ["CPUExecutionProvider"] if "CPUExecutionProvider" in avail else []
|
|
207
|
+
|
|
208
|
+
order = []
|
|
209
|
+
if "DmlExecutionProvider" in avail:
|
|
210
|
+
order.append("DmlExecutionProvider")
|
|
211
|
+
if "CUDAExecutionProvider" in avail:
|
|
212
|
+
order.append("CUDAExecutionProvider")
|
|
213
|
+
|
|
214
|
+
# mac(Intel) can still use CoreML if someone insists, but we won't put it first.
|
|
215
|
+
if "CPUExecutionProvider" in avail:
|
|
216
|
+
order.append("CPUExecutionProvider")
|
|
217
|
+
if "CoreMLExecutionProvider" in avail:
|
|
218
|
+
order.append("CoreMLExecutionProvider")
|
|
219
|
+
|
|
220
|
+
return order
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _preserve_border(dst: np.ndarray, src: np.ndarray, px: int = 10) -> np.ndarray:
|
|
224
|
+
"""
|
|
225
|
+
Copy a px-wide ring from src → dst, in-place. Handles mono/RGB.
|
|
226
|
+
Expects same shape for src and dst. Clamps px to image size.
|
|
227
|
+
"""
|
|
228
|
+
if px <= 0 or dst is None or src is None:
|
|
229
|
+
return dst
|
|
230
|
+
if dst.shape != src.shape:
|
|
231
|
+
return dst # shapes differ; skip quietly
|
|
232
|
+
|
|
233
|
+
h, w = dst.shape[:2]
|
|
234
|
+
px = int(max(0, min(px, h // 2, w // 2)))
|
|
235
|
+
if px == 0:
|
|
236
|
+
return dst
|
|
237
|
+
|
|
238
|
+
s = src.astype(dst.dtype, copy=False)
|
|
239
|
+
|
|
240
|
+
# top & bottom
|
|
241
|
+
dst[:px, ...] = s[:px, ...]
|
|
242
|
+
dst[-px:, ...] = s[-px:, ...]
|
|
243
|
+
# left & right
|
|
244
|
+
dst[:, :px, ...] = s[:, :px, ...]
|
|
245
|
+
dst[:, -px:, ...] = s[:, -px:, ...]
|
|
246
|
+
|
|
247
|
+
return dst
|
|
248
|
+
|
|
249
|
+
# ---------- worker ----------
|
|
250
|
+
class _ONNXWorker(QThread):
|
|
251
|
+
progressed = pyqtSignal(int) # 0..100
|
|
252
|
+
failed = pyqtSignal(str)
|
|
253
|
+
finished_ok= pyqtSignal(np.ndarray)
|
|
254
|
+
|
|
255
|
+
def __init__(self, model_path: str, image: np.ndarray, patch: int, overlap: int, providers: list[str]):
|
|
256
|
+
super().__init__()
|
|
257
|
+
self.model_path = model_path
|
|
258
|
+
self.image = image
|
|
259
|
+
self.patch = patch
|
|
260
|
+
self.overlap = overlap
|
|
261
|
+
self.providers = providers
|
|
262
|
+
self.used_provider = None
|
|
263
|
+
|
|
264
|
+
def run(self):
|
|
265
|
+
if ort is None:
|
|
266
|
+
self.failed.emit("onnxruntime is not installed.")
|
|
267
|
+
return
|
|
268
|
+
try:
|
|
269
|
+
sess = ort.InferenceSession(self.model_path, providers=self.providers)
|
|
270
|
+
self.used_provider = (sess.get_providers()[0] if sess.get_providers() else None)
|
|
271
|
+
except Exception:
|
|
272
|
+
# fallback CPU if GPU fails
|
|
273
|
+
try:
|
|
274
|
+
sess = ort.InferenceSession(self.model_path, providers=["CPUExecutionProvider"])
|
|
275
|
+
self.used_provider = "CPUExecutionProvider" # NEW
|
|
276
|
+
except Exception as e2:
|
|
277
|
+
self.failed.emit(f"Failed to init ONNX session:\n{e2}")
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
def cb(frac):
|
|
281
|
+
self.progressed.emit(int(frac * 100))
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
out = run_onnx_tiled(sess, self.image, self.patch, self.overlap, cb)
|
|
285
|
+
except Exception as e:
|
|
286
|
+
self.failed.emit(str(e)); return
|
|
287
|
+
|
|
288
|
+
self.finished_ok.emit(out)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ---------- dialog ----------
|
|
292
|
+
class AberrationAIDialog(QDialog):
|
|
293
|
+
def __init__(self, parent, docman, get_active_doc_callable, icon: QIcon | None = None):
|
|
294
|
+
super().__init__(parent)
|
|
295
|
+
self.setWindowTitle(self.tr("R.A.'s Aberration Correction (AI)"))
|
|
296
|
+
if icon is not None:
|
|
297
|
+
self.setWindowIcon(icon)
|
|
298
|
+
|
|
299
|
+
# Normalize window behavior across platforms
|
|
300
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
301
|
+
# Non-modal: allow user to switch between images while dialog is open
|
|
302
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
303
|
+
self.setModal(False)
|
|
304
|
+
#self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
305
|
+
|
|
306
|
+
self.docman = docman
|
|
307
|
+
self.get_active_doc = get_active_doc_callable
|
|
308
|
+
self._t_start = None
|
|
309
|
+
self._last_used_provider = None
|
|
310
|
+
|
|
311
|
+
v = QVBoxLayout(self)
|
|
312
|
+
|
|
313
|
+
# Model row
|
|
314
|
+
row = QHBoxLayout()
|
|
315
|
+
row.addWidget(QLabel(self.tr("Model:")))
|
|
316
|
+
self.model_label = QLabel("—")
|
|
317
|
+
self.model_label.setToolTip("")
|
|
318
|
+
btn_browse = QPushButton(self.tr("Browse…")); btn_browse.clicked.connect(self._browse_active_model)
|
|
319
|
+
row.addWidget(self.model_label, 1)
|
|
320
|
+
row.addWidget(btn_browse)
|
|
321
|
+
v.addLayout(row)
|
|
322
|
+
# Custom model row (NEW)
|
|
323
|
+
row_custom = QHBoxLayout()
|
|
324
|
+
self.chk_use_custom = QCheckBox(self.tr("Use custom model file"))
|
|
325
|
+
self.chk_use_custom.setChecked(False)
|
|
326
|
+
self.chk_use_custom.toggled.connect(self._on_use_custom_toggled)
|
|
327
|
+
|
|
328
|
+
self.le_custom_model = QLineEdit()
|
|
329
|
+
self.le_custom_model.setReadOnly(True)
|
|
330
|
+
self.le_custom_model.setPlaceholderText(self.tr("No custom model selected"))
|
|
331
|
+
self.le_custom_model.setToolTip("")
|
|
332
|
+
|
|
333
|
+
btn_custom_clear = QPushButton(self.tr("Clear"))
|
|
334
|
+
btn_custom_clear.clicked.connect(self._clear_custom_model)
|
|
335
|
+
|
|
336
|
+
row_custom.addWidget(self.chk_use_custom)
|
|
337
|
+
row_custom.addWidget(self.le_custom_model, 1)
|
|
338
|
+
|
|
339
|
+
row_custom.addWidget(btn_custom_clear)
|
|
340
|
+
v.addLayout(row_custom)
|
|
341
|
+
# Providers row
|
|
342
|
+
row2 = QHBoxLayout()
|
|
343
|
+
self.chk_auto = QCheckBox(self.tr("Auto GPU (if available)"))
|
|
344
|
+
self.chk_auto.setChecked(True)
|
|
345
|
+
row2.addWidget(self.chk_auto)
|
|
346
|
+
self.cmb_provider = QComboBox()
|
|
347
|
+
row2.addWidget(QLabel(self.tr("Provider:")))
|
|
348
|
+
row2.addWidget(self.cmb_provider, 1)
|
|
349
|
+
v.addLayout(row2)
|
|
350
|
+
|
|
351
|
+
# Params row
|
|
352
|
+
row3 = QHBoxLayout()
|
|
353
|
+
row3.addWidget(QLabel(self.tr("Patch")))
|
|
354
|
+
self.spin_patch = QSpinBox(minimum=128, maximum=2048); self.spin_patch.setValue(512)
|
|
355
|
+
row3.addWidget(self.spin_patch)
|
|
356
|
+
row3.addWidget(QLabel(self.tr("Overlap")))
|
|
357
|
+
self.spin_overlap = QSpinBox(minimum=16, maximum=512); self.spin_overlap.setValue(64)
|
|
358
|
+
row3.addWidget(self.spin_overlap)
|
|
359
|
+
v.addLayout(row3)
|
|
360
|
+
|
|
361
|
+
# Download / Open folder
|
|
362
|
+
row4 = QHBoxLayout()
|
|
363
|
+
btn_latest = QPushButton(self.tr("Download latest model…"))
|
|
364
|
+
btn_latest.clicked.connect(self._download_latest_model)
|
|
365
|
+
row4.addWidget(btn_latest)
|
|
366
|
+
btn_openfolder = QPushButton(self.tr("Open model folder"))
|
|
367
|
+
btn_openfolder.clicked.connect(self._open_model_folder)
|
|
368
|
+
row4.addWidget(btn_openfolder)
|
|
369
|
+
row4.addStretch(1)
|
|
370
|
+
v.addLayout(row4)
|
|
371
|
+
|
|
372
|
+
# Progress + actions
|
|
373
|
+
self.progress = QProgressBar(); self.progress.setRange(0, 100); v.addWidget(self.progress)
|
|
374
|
+
row5 = QHBoxLayout()
|
|
375
|
+
self.btn_run = QPushButton(self.tr("Run")); self.btn_run.clicked.connect(self._run)
|
|
376
|
+
btn_close = QPushButton(self.tr("Close")); btn_close.clicked.connect(self.reject)
|
|
377
|
+
row5.addStretch(1); row5.addWidget(self.btn_run); row5.addWidget(btn_close)
|
|
378
|
+
v.addLayout(row5)
|
|
379
|
+
|
|
380
|
+
info = QLabel(
|
|
381
|
+
"Model and weights © Riccardo Alberghi — "
|
|
382
|
+
"<a href='https://github.com/riccardoalberghi'>more information</a>."
|
|
383
|
+
)
|
|
384
|
+
info.setTextFormat(Qt.TextFormat.RichText)
|
|
385
|
+
info.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
|
|
386
|
+
info.setOpenExternalLinks(True)
|
|
387
|
+
info.setWordWrap(True)
|
|
388
|
+
info.setStyleSheet("color:#888; font-size:11px; margin-top:4px;")
|
|
389
|
+
v.addWidget(info)
|
|
390
|
+
|
|
391
|
+
self._model_path = None
|
|
392
|
+
self._refresh_providers()
|
|
393
|
+
self._load_last_model_from_settings()
|
|
394
|
+
self._load_last_custom_model_from_settings()
|
|
395
|
+
use_custom = QSettings().value("AberrationAI/use_custom_model", False, type=bool)
|
|
396
|
+
self.chk_use_custom.setChecked(bool(use_custom))
|
|
397
|
+
if IS_APPLE_ARM:
|
|
398
|
+
self.chk_auto.setChecked(False)
|
|
399
|
+
self.chk_auto.setEnabled(False)
|
|
400
|
+
|
|
401
|
+
# ----- model helpers -----
|
|
402
|
+
def _set_model_path(self, p: str | None):
|
|
403
|
+
self._model_path = p
|
|
404
|
+
if p:
|
|
405
|
+
self.model_label.setText(os.path.basename(p))
|
|
406
|
+
self.model_label.setToolTip(p)
|
|
407
|
+
QSettings().setValue("AberrationAI/model_path", p)
|
|
408
|
+
else:
|
|
409
|
+
self.model_label.setText("—")
|
|
410
|
+
self.model_label.setToolTip("")
|
|
411
|
+
QSettings().remove("AberrationAI/model_path")
|
|
412
|
+
|
|
413
|
+
def _load_last_model_from_settings(self):
|
|
414
|
+
p = QSettings().value("AberrationAI/model_path", type=str)
|
|
415
|
+
if p and os.path.isfile(p):
|
|
416
|
+
self._set_model_path(p)
|
|
417
|
+
|
|
418
|
+
def _browse_active_model(self):
|
|
419
|
+
"""
|
|
420
|
+
Single Browse button.
|
|
421
|
+
- If user picks a file inside the app model folder -> treat as "downloaded" selection (use_custom_model=False)
|
|
422
|
+
- If user picks a file outside -> treat as "custom" (use_custom_model=True)
|
|
423
|
+
"""
|
|
424
|
+
app_dir = os.path.abspath(_app_model_dir())
|
|
425
|
+
|
|
426
|
+
# Start in last-used folder if possible
|
|
427
|
+
last_custom = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
|
|
428
|
+
last_downloaded = QSettings().value("AberrationAI/model_path", type=str) or ""
|
|
429
|
+
start_dir = None
|
|
430
|
+
for candidate in (last_custom, last_downloaded):
|
|
431
|
+
if candidate and os.path.isfile(candidate):
|
|
432
|
+
d = os.path.dirname(candidate)
|
|
433
|
+
if os.path.isdir(d):
|
|
434
|
+
start_dir = d
|
|
435
|
+
break
|
|
436
|
+
if start_dir is None:
|
|
437
|
+
start_dir = app_dir
|
|
438
|
+
|
|
439
|
+
p, _ = QFileDialog.getOpenFileName(self, "Select ONNX model", start_dir, "ONNX (*.onnx)")
|
|
440
|
+
if not p:
|
|
441
|
+
return
|
|
442
|
+
|
|
443
|
+
p_abs = os.path.abspath(p)
|
|
444
|
+
# Determine if picked file is inside app model folder
|
|
445
|
+
in_app_dir = False
|
|
446
|
+
try:
|
|
447
|
+
in_app_dir = os.path.commonpath([app_dir, p_abs]) == app_dir
|
|
448
|
+
except Exception:
|
|
449
|
+
in_app_dir = p_abs.startswith(app_dir)
|
|
450
|
+
|
|
451
|
+
if in_app_dir:
|
|
452
|
+
# "Downloaded" selection
|
|
453
|
+
self._set_model_path(p_abs)
|
|
454
|
+
self._set_custom_model_path(None)
|
|
455
|
+
QSettings().setValue("AberrationAI/use_custom_model", False)
|
|
456
|
+
if hasattr(self, "chk_use_custom"):
|
|
457
|
+
self.chk_use_custom.setChecked(False)
|
|
458
|
+
else:
|
|
459
|
+
# "Custom" selection
|
|
460
|
+
self._set_custom_model_path(p_abs)
|
|
461
|
+
QSettings().setValue("AberrationAI/use_custom_model", True)
|
|
462
|
+
if hasattr(self, "chk_use_custom"):
|
|
463
|
+
self.chk_use_custom.setChecked(True)
|
|
464
|
+
|
|
465
|
+
# Keep visuals in sync
|
|
466
|
+
self._refresh_model_label()
|
|
467
|
+
self._refresh_custom_row_visibility()
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _refresh_model_label(self):
|
|
471
|
+
downloaded = QSettings().value("AberrationAI/model_path", type=str) or ""
|
|
472
|
+
custom = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
|
|
473
|
+
use_custom = QSettings().value("AberrationAI/use_custom_model", False, type=bool)
|
|
474
|
+
|
|
475
|
+
if use_custom and custom:
|
|
476
|
+
self.model_label.setText(f"Custom: {os.path.basename(custom)}")
|
|
477
|
+
self.model_label.setToolTip(custom)
|
|
478
|
+
elif downloaded:
|
|
479
|
+
self.model_label.setText(f"Downloaded: {os.path.basename(downloaded)}")
|
|
480
|
+
self.model_label.setToolTip(downloaded)
|
|
481
|
+
else:
|
|
482
|
+
self.model_label.setText("—")
|
|
483
|
+
self.model_label.setToolTip("")
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _open_model_folder(self):
|
|
487
|
+
d = _app_model_dir()
|
|
488
|
+
try:
|
|
489
|
+
if os.name == "nt":
|
|
490
|
+
os.startfile(d) # type: ignore
|
|
491
|
+
elif sys.platform == "darwin":
|
|
492
|
+
import subprocess; subprocess.Popen(["open", d])
|
|
493
|
+
else:
|
|
494
|
+
import subprocess; subprocess.Popen(["xdg-open", d])
|
|
495
|
+
except Exception:
|
|
496
|
+
webbrowser.open(f"file://{d}")
|
|
497
|
+
# ----- custom model helpers (NEW) -----
|
|
498
|
+
def _set_custom_model_path(self, p: str | None):
|
|
499
|
+
if p:
|
|
500
|
+
self.le_custom_model.setText(os.path.basename(p))
|
|
501
|
+
self.le_custom_model.setToolTip(p)
|
|
502
|
+
QSettings().setValue("AberrationAI/custom_model_path", p)
|
|
503
|
+
else:
|
|
504
|
+
self.le_custom_model.clear()
|
|
505
|
+
self.le_custom_model.setToolTip("")
|
|
506
|
+
QSettings().remove("AberrationAI/custom_model_path")
|
|
507
|
+
|
|
508
|
+
def _load_last_custom_model_from_settings(self):
|
|
509
|
+
p = QSettings().value("AberrationAI/custom_model_path", type=str)
|
|
510
|
+
if p:
|
|
511
|
+
if os.path.isfile(p):
|
|
512
|
+
self._set_custom_model_path(p)
|
|
513
|
+
else:
|
|
514
|
+
# Keep the broken path visible in tooltip for debugging
|
|
515
|
+
if hasattr(self, "le_custom_model"):
|
|
516
|
+
self.le_custom_model.setText(os.path.basename(p) + " (missing)")
|
|
517
|
+
self.le_custom_model.setToolTip(p)
|
|
518
|
+
|
|
519
|
+
# After both loads, sync labels/visibility
|
|
520
|
+
self._refresh_model_label()
|
|
521
|
+
self._refresh_custom_row_visibility()
|
|
522
|
+
|
|
523
|
+
def _refresh_custom_row_visibility(self):
|
|
524
|
+
"""
|
|
525
|
+
If you keep the custom row in the UI, hide the path field unless custom is enabled.
|
|
526
|
+
"""
|
|
527
|
+
if not hasattr(self, "le_custom_model"):
|
|
528
|
+
return
|
|
529
|
+
use_custom = QSettings().value("AberrationAI/use_custom_model", False, type=bool)
|
|
530
|
+
self.le_custom_model.setVisible(bool(use_custom))
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _refresh_model_label(self):
|
|
534
|
+
downloaded = QSettings().value("AberrationAI/model_path", type=str) or ""
|
|
535
|
+
custom = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
|
|
536
|
+
use_custom = QSettings().value("AberrationAI/use_custom_model", False, type=bool)
|
|
537
|
+
|
|
538
|
+
# Prefer custom only if enabled AND the file exists
|
|
539
|
+
if use_custom and custom:
|
|
540
|
+
if os.path.isfile(custom):
|
|
541
|
+
self.model_label.setText(f"Custom: {os.path.basename(custom)}")
|
|
542
|
+
self.model_label.setToolTip(custom)
|
|
543
|
+
return
|
|
544
|
+
else:
|
|
545
|
+
self.model_label.setText(f"Custom: {os.path.basename(custom)} (missing)")
|
|
546
|
+
self.model_label.setToolTip(custom)
|
|
547
|
+
return
|
|
548
|
+
|
|
549
|
+
# Otherwise show downloaded if valid
|
|
550
|
+
if downloaded and os.path.isfile(downloaded):
|
|
551
|
+
self.model_label.setText(f"Downloaded: {os.path.basename(downloaded)}")
|
|
552
|
+
self.model_label.setToolTip(downloaded)
|
|
553
|
+
else:
|
|
554
|
+
self.model_label.setText("—")
|
|
555
|
+
self.model_label.setToolTip("")
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _browse_custom_model(self):
|
|
559
|
+
# Start at last dir if possible, else app model dir
|
|
560
|
+
last = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
|
|
561
|
+
start_dir = os.path.dirname(last) if last and os.path.isdir(os.path.dirname(last)) else _app_model_dir()
|
|
562
|
+
p, _ = QFileDialog.getOpenFileName(self, "Select custom ONNX model", start_dir, "ONNX (*.onnx)")
|
|
563
|
+
if p:
|
|
564
|
+
self._set_custom_model_path(p)
|
|
565
|
+
QSettings().setValue("AberrationAI/use_custom_model", True)
|
|
566
|
+
if not self.chk_use_custom.isChecked():
|
|
567
|
+
self.chk_use_custom.setChecked(True)
|
|
568
|
+
|
|
569
|
+
def _clear_custom_model(self):
|
|
570
|
+
self._set_custom_model_path(None)
|
|
571
|
+
QSettings().setValue("AberrationAI/use_custom_model", False)
|
|
572
|
+
if hasattr(self, "chk_use_custom"):
|
|
573
|
+
self.chk_use_custom.setChecked(False)
|
|
574
|
+
|
|
575
|
+
self._refresh_model_label()
|
|
576
|
+
self._refresh_custom_row_visibility()
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _on_use_custom_toggled(self, on: bool):
|
|
580
|
+
QSettings().setValue("AberrationAI/use_custom_model", bool(on))
|
|
581
|
+
|
|
582
|
+
if on:
|
|
583
|
+
p = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
|
|
584
|
+
if not (p and os.path.isfile(p)):
|
|
585
|
+
# Don’t spawn another browse button path; use the ONE browse if they want
|
|
586
|
+
QMessageBox.information(
|
|
587
|
+
self,
|
|
588
|
+
self.tr("Custom model"),
|
|
589
|
+
self.tr("Custom model is enabled, but no custom file is selected.\n"
|
|
590
|
+
"Click Browse… to choose a model file.")
|
|
591
|
+
)
|
|
592
|
+
# Optional: auto-open the single browse:
|
|
593
|
+
# self._browse_active_model()
|
|
594
|
+
# return
|
|
595
|
+
|
|
596
|
+
self._refresh_model_label()
|
|
597
|
+
self._refresh_custom_row_visibility()
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
# ----- provider UI -----
|
|
601
|
+
def _log(self, msg: str): # NEW
|
|
602
|
+
mw = self.parent()
|
|
603
|
+
try:
|
|
604
|
+
if hasattr(mw, "_log"):
|
|
605
|
+
mw._log(msg)
|
|
606
|
+
elif hasattr(mw, "update_status"):
|
|
607
|
+
mw.update_status(msg)
|
|
608
|
+
except Exception:
|
|
609
|
+
pass
|
|
610
|
+
|
|
611
|
+
def _refresh_providers(self):
|
|
612
|
+
if ort is None:
|
|
613
|
+
self.cmb_provider.clear()
|
|
614
|
+
self.cmb_provider.addItem("onnxruntime not installed")
|
|
615
|
+
self.cmb_provider.setEnabled(False)
|
|
616
|
+
return
|
|
617
|
+
|
|
618
|
+
avail = ort.get_available_providers()
|
|
619
|
+
self.cmb_provider.clear()
|
|
620
|
+
|
|
621
|
+
if IS_APPLE_ARM:
|
|
622
|
+
# Hard lock to CPU on M-series
|
|
623
|
+
self.cmb_provider.addItem("CPUExecutionProvider")
|
|
624
|
+
self.cmb_provider.setCurrentText("CPUExecutionProvider")
|
|
625
|
+
self.cmb_provider.setEnabled(False)
|
|
626
|
+
# also turn off Auto GPU and disable that checkbox
|
|
627
|
+
self.chk_auto.setChecked(False)
|
|
628
|
+
self.chk_auto.setEnabled(False)
|
|
629
|
+
return
|
|
630
|
+
|
|
631
|
+
# Other platforms: show all, sane default
|
|
632
|
+
for name in avail:
|
|
633
|
+
self.cmb_provider.addItem(name)
|
|
634
|
+
|
|
635
|
+
if "DmlExecutionProvider" in avail:
|
|
636
|
+
self.cmb_provider.setCurrentText("DmlExecutionProvider")
|
|
637
|
+
elif "CUDAExecutionProvider" in avail:
|
|
638
|
+
self.cmb_provider.setCurrentText("CUDAExecutionProvider")
|
|
639
|
+
elif "CPUExecutionProvider" in avail:
|
|
640
|
+
self.cmb_provider.setCurrentText("CPUExecutionProvider")
|
|
641
|
+
elif "CoreMLExecutionProvider" in avail:
|
|
642
|
+
self.cmb_provider.setCurrentText("CoreMLExecutionProvider")
|
|
643
|
+
|
|
644
|
+
# ----- download -----
|
|
645
|
+
def _download_latest_model(self):
|
|
646
|
+
if requests is None:
|
|
647
|
+
QMessageBox.warning(self, "Network", "The 'requests' package is required."); return
|
|
648
|
+
dst = _app_model_dir()
|
|
649
|
+
self.progress.setRange(0, 0) # busy
|
|
650
|
+
self.btn_run.setEnabled(False)
|
|
651
|
+
self._dl = _DownloadWorker(dst)
|
|
652
|
+
self._dl.progressed.connect(self.progress.setValue)
|
|
653
|
+
self._dl.failed.connect(self._on_download_failed)
|
|
654
|
+
self._dl.finished_ok.connect(self._on_download_ok)
|
|
655
|
+
self._dl.finished.connect(lambda: (self.progress.setRange(0, 100), self.btn_run.setEnabled(True)))
|
|
656
|
+
self._dl.start()
|
|
657
|
+
|
|
658
|
+
def _on_download_failed(self, msg: str):
|
|
659
|
+
QMessageBox.critical(self, "Download", msg)
|
|
660
|
+
|
|
661
|
+
def _on_download_ok(self, path: str):
|
|
662
|
+
self.progress.setValue(100)
|
|
663
|
+
self._set_model_path(path)
|
|
664
|
+
|
|
665
|
+
# Download becomes the active model unless custom is explicitly enabled
|
|
666
|
+
if not QSettings().value("AberrationAI/use_custom_model", False, type=bool):
|
|
667
|
+
self._set_custom_model_path(None)
|
|
668
|
+
|
|
669
|
+
QMessageBox.information(self, "Model", f"Downloaded: {os.path.basename(path)}")
|
|
670
|
+
|
|
671
|
+
self._refresh_model_label()
|
|
672
|
+
self._refresh_custom_row_visibility()
|
|
673
|
+
|
|
674
|
+
# ----- run -----
|
|
675
|
+
def _run(self):
|
|
676
|
+
if ort is None:
|
|
677
|
+
QMessageBox.critical(
|
|
678
|
+
self,
|
|
679
|
+
"Unsupported ONNX Runtime",
|
|
680
|
+
"The currently installed onnxruntime is not supported on this machine.\n"
|
|
681
|
+
"Please try installing an earlier version (for example 1.19.x) and try again."
|
|
682
|
+
)
|
|
683
|
+
return
|
|
684
|
+
|
|
685
|
+
# Choose model path (normal vs custom)
|
|
686
|
+
use_custom = QSettings().value("AberrationAI/use_custom_model", False, type=bool)
|
|
687
|
+
downloaded = QSettings().value("AberrationAI/model_path", type=str) or ""
|
|
688
|
+
custom = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
|
|
689
|
+
|
|
690
|
+
model_path = custom if use_custom else downloaded
|
|
691
|
+
if self.chk_use_custom.isChecked():
|
|
692
|
+
cp = QSettings().value("AberrationAI/custom_model_path", type=str)
|
|
693
|
+
if cp and os.path.isfile(cp):
|
|
694
|
+
model_path = cp
|
|
695
|
+
else:
|
|
696
|
+
QMessageBox.warning(self, "Model", "Custom model is enabled but the file is missing. Please browse to a valid .onnx.")
|
|
697
|
+
return
|
|
698
|
+
|
|
699
|
+
if not model_path or not os.path.isfile(model_path):
|
|
700
|
+
QMessageBox.warning(self, "Model", "Please select or download a valid .onnx model first.")
|
|
701
|
+
return
|
|
702
|
+
|
|
703
|
+
doc = self.get_active_doc()
|
|
704
|
+
if doc is None or getattr(doc, "image", None) is None:
|
|
705
|
+
QMessageBox.warning(self, "Image", "No active image.")
|
|
706
|
+
return
|
|
707
|
+
|
|
708
|
+
img = np.asarray(doc.image)
|
|
709
|
+
self._orig_for_border = img.copy()
|
|
710
|
+
|
|
711
|
+
patch = int(self.spin_patch.value())
|
|
712
|
+
overlap = int(self.spin_overlap.value())
|
|
713
|
+
|
|
714
|
+
# -------- providers (always choose, then always run) --------
|
|
715
|
+
if IS_APPLE_ARM:
|
|
716
|
+
providers = ["CPUExecutionProvider"]
|
|
717
|
+
self.chk_auto.setChecked(False)
|
|
718
|
+
else:
|
|
719
|
+
if self.chk_auto.isChecked():
|
|
720
|
+
providers = pick_providers(auto_gpu=True)
|
|
721
|
+
else:
|
|
722
|
+
sel = self.cmb_provider.currentText()
|
|
723
|
+
providers = [sel] if sel else ["CPUExecutionProvider"]
|
|
724
|
+
|
|
725
|
+
# --- make patch match the model's requirement (if fixed) ---
|
|
726
|
+
req = _model_required_patch(model_path)
|
|
727
|
+
if req and req > 0:
|
|
728
|
+
patch = req
|
|
729
|
+
try:
|
|
730
|
+
self.spin_patch.blockSignals(True)
|
|
731
|
+
self.spin_patch.setValue(req)
|
|
732
|
+
finally:
|
|
733
|
+
self.spin_patch.blockSignals(False)
|
|
734
|
+
|
|
735
|
+
# --- CoreML guard on Intel: if model needs >128, run on CPU instead ---
|
|
736
|
+
if ("CoreMLExecutionProvider" in providers) and (req and req > 128):
|
|
737
|
+
self._log(f"CoreML limited to small tiles; model requires {req}px → using CPU.")
|
|
738
|
+
providers = ["CPUExecutionProvider"]
|
|
739
|
+
try:
|
|
740
|
+
self.cmb_provider.setCurrentText("CPUExecutionProvider")
|
|
741
|
+
self.chk_auto.setChecked(False)
|
|
742
|
+
except Exception:
|
|
743
|
+
pass
|
|
744
|
+
|
|
745
|
+
self._t_start = time.perf_counter()
|
|
746
|
+
prov_txt = ("auto" if self.chk_auto.isChecked() else self.cmb_provider.currentText() or "CPU")
|
|
747
|
+
self._log(f"🚀 Aberration AI: model={os.path.basename(model_path)}, "
|
|
748
|
+
f"provider={prov_txt}, patch={patch}, overlap={overlap}")
|
|
749
|
+
|
|
750
|
+
self._effective_model_path = model_path
|
|
751
|
+
|
|
752
|
+
# -------- run worker --------
|
|
753
|
+
self.progress.setValue(0)
|
|
754
|
+
self.btn_run.setEnabled(False)
|
|
755
|
+
|
|
756
|
+
self._worker = _ONNXWorker(model_path, img, patch, overlap, providers)
|
|
757
|
+
self._worker.progressed.connect(self.progress.setValue)
|
|
758
|
+
self._worker.failed.connect(self._on_failed)
|
|
759
|
+
self._worker.finished_ok.connect(self._on_ok)
|
|
760
|
+
self._worker.finished.connect(self._on_worker_finished)
|
|
761
|
+
self._worker.start()
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _on_failed(self, msg: str):
|
|
765
|
+
model_path = getattr(self, "_effective_model_path", self._model_path)
|
|
766
|
+
self._log(f"❌ Aberration AI failed: {msg}")
|
|
767
|
+
QMessageBox.critical(self, "ONNX Error", msg)
|
|
768
|
+
self.reject() # closes the dialog
|
|
769
|
+
|
|
770
|
+
def _on_ok(self, out: np.ndarray):
|
|
771
|
+
used = getattr(self._worker, "used_provider", None) or \
|
|
772
|
+
(self.cmb_provider.currentText() if not self.chk_auto.isChecked() else "auto")
|
|
773
|
+
model_path = getattr(self, "_effective_model_path", self._model_path)
|
|
774
|
+
doc = self.get_active_doc()
|
|
775
|
+
if doc is None or getattr(doc, "image", None) is None:
|
|
776
|
+
QMessageBox.warning(self, "Image", "No active image.")
|
|
777
|
+
return
|
|
778
|
+
|
|
779
|
+
# 1) Preserve a thin border from the original image (prevents “eaten” edges)
|
|
780
|
+
BORDER_PX = 10
|
|
781
|
+
src = getattr(self, "_orig_for_border", None)
|
|
782
|
+
if src is None or src.shape != out.shape:
|
|
783
|
+
try:
|
|
784
|
+
src = np.asarray(doc.image)
|
|
785
|
+
except Exception:
|
|
786
|
+
src = None
|
|
787
|
+
out = _preserve_border(out, src, BORDER_PX)
|
|
788
|
+
|
|
789
|
+
# 2) Metadata for this step (stored on the document)
|
|
790
|
+
meta = {
|
|
791
|
+
"is_mono": (out.ndim == 2),
|
|
792
|
+
"processing_parameters": {
|
|
793
|
+
**(getattr(doc, "metadata", {}) or {}).get("processing_parameters", {}),
|
|
794
|
+
"AberrationAI": {
|
|
795
|
+
"model_path": model_path,
|
|
796
|
+
"patch_size": int(self.spin_patch.value()),
|
|
797
|
+
"overlap": int(self.spin_overlap.value()),
|
|
798
|
+
"provider": used,
|
|
799
|
+
"border_px": BORDER_PX,
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
# 3) Apply through history-aware API (either path is fine)
|
|
805
|
+
try:
|
|
806
|
+
# Preferred: directly on the document
|
|
807
|
+
if hasattr(doc, "apply_edit"):
|
|
808
|
+
doc.apply_edit(out, meta, step_name="Aberration AI")
|
|
809
|
+
# Or via DocManager (same effect)
|
|
810
|
+
elif hasattr(self.docman, "update_active_document"):
|
|
811
|
+
self.docman.update_active_document(out, metadata=meta, step_name="Aberration AI")
|
|
812
|
+
else:
|
|
813
|
+
# Last-resort fallback (no undo): avoid if possible
|
|
814
|
+
doc.image = out
|
|
815
|
+
try:
|
|
816
|
+
doc.metadata.update(meta)
|
|
817
|
+
doc.changed.emit()
|
|
818
|
+
except Exception:
|
|
819
|
+
pass
|
|
820
|
+
except Exception as e:
|
|
821
|
+
self._log(f"❌ Aberration AI apply failed: {e}")
|
|
822
|
+
QMessageBox.critical(self, "Apply Error", f"Failed to apply result:\n{e}")
|
|
823
|
+
return
|
|
824
|
+
|
|
825
|
+
# 3.5) Register this as last_headless_command for Replay Last Action ← NEW
|
|
826
|
+
try:
|
|
827
|
+
main = self.parent()
|
|
828
|
+
if main is not None:
|
|
829
|
+
auto_gpu = bool(self.chk_auto.isChecked())
|
|
830
|
+
preset = {
|
|
831
|
+
"model": model_path,
|
|
832
|
+
"patch": int(self.spin_patch.value()),
|
|
833
|
+
"overlap": int(self.spin_overlap.value()),
|
|
834
|
+
"border_px": int(BORDER_PX),
|
|
835
|
+
"auto_gpu": auto_gpu,
|
|
836
|
+
}
|
|
837
|
+
if not auto_gpu:
|
|
838
|
+
preset["provider"] = self.cmb_provider.currentText() or "CPUExecutionProvider"
|
|
839
|
+
|
|
840
|
+
payload = {
|
|
841
|
+
"command_id": "aberrationai",
|
|
842
|
+
"preset": preset,
|
|
843
|
+
}
|
|
844
|
+
setattr(main, "_last_headless_command", payload)
|
|
845
|
+
|
|
846
|
+
# optional log
|
|
847
|
+
try:
|
|
848
|
+
if hasattr(main, "_log"):
|
|
849
|
+
prov = preset.get("provider", "auto" if auto_gpu else "CPUExecutionProvider")
|
|
850
|
+
main._log(
|
|
851
|
+
f"[Replay] Registered Aberration AI as last action "
|
|
852
|
+
f"(patch={preset['patch']}, overlap={preset['overlap']}, "
|
|
853
|
+
f"border={preset['border_px']}px, provider={prov})"
|
|
854
|
+
)
|
|
855
|
+
except Exception:
|
|
856
|
+
pass
|
|
857
|
+
except Exception:
|
|
858
|
+
# never break the tool if replay wiring fails
|
|
859
|
+
pass
|
|
860
|
+
|
|
861
|
+
# 4) Refresh the active view
|
|
862
|
+
mw = self.parent()
|
|
863
|
+
sw = getattr(getattr(mw, "mdi", None), "activeSubWindow", lambda: None)()
|
|
864
|
+
if sw and hasattr(sw, "widget"):
|
|
865
|
+
w = sw.widget()
|
|
866
|
+
if hasattr(w, "reload_from_doc"):
|
|
867
|
+
try: w.reload_from_doc()
|
|
868
|
+
except Exception as e:
|
|
869
|
+
import logging
|
|
870
|
+
logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
|
|
871
|
+
elif hasattr(w, "update_view"):
|
|
872
|
+
try: w.update_view()
|
|
873
|
+
except Exception as e:
|
|
874
|
+
import logging
|
|
875
|
+
logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
|
|
876
|
+
elif hasattr(w, "update"):
|
|
877
|
+
w.update()
|
|
878
|
+
|
|
879
|
+
dt = 0.0
|
|
880
|
+
try:
|
|
881
|
+
if self._t_start is not None:
|
|
882
|
+
dt = time.perf_counter() - self._t_start
|
|
883
|
+
except Exception:
|
|
884
|
+
pass
|
|
885
|
+
used = getattr(self._worker, "used_provider", None) or \
|
|
886
|
+
(self.cmb_provider.currentText() if not self.chk_auto.isChecked() else "auto")
|
|
887
|
+
BORDER_PX = 10 # same value used above
|
|
888
|
+
self._log(
|
|
889
|
+
f"✅ Aberration AI applied "
|
|
890
|
+
f"(model={os.path.basename(model_path)}, provider={used}, "
|
|
891
|
+
f"patch={int(self.spin_patch.value())}, overlap={int(self.spin_overlap.value())}, "
|
|
892
|
+
f"border={BORDER_PX}px, time={dt:.2f}s)"
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
self.progress.setValue(100)
|
|
896
|
+
# NEW: close this UI after a successful run
|
|
897
|
+
self.accept() # or self.close()
|
|
898
|
+
return
|
|
899
|
+
|
|
900
|
+
def _on_worker_finished(self):
|
|
901
|
+
# Dialog might have been closed by _on_ok()
|
|
902
|
+
if not self.isVisible():
|
|
903
|
+
return
|
|
904
|
+
|
|
905
|
+
if hasattr(self, "btn_run"):
|
|
906
|
+
try:
|
|
907
|
+
self.btn_run.setEnabled(True)
|
|
908
|
+
except RuntimeError:
|
|
909
|
+
pass
|
|
910
|
+
self._worker = None
|