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,666 @@
|
|
|
1
|
+
# pro/memory_utils.py
|
|
2
|
+
"""
|
|
3
|
+
Memory management utilities for Seti Astro Suite Pro.
|
|
4
|
+
|
|
5
|
+
Provides:
|
|
6
|
+
- Memory-mapped array creation for large datasets
|
|
7
|
+
- Reusable buffer pools to reduce allocation overhead
|
|
8
|
+
- Lazy image loading with preview-first strategy
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
import os
|
|
12
|
+
import tempfile
|
|
13
|
+
import hashlib
|
|
14
|
+
import numpy as np
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Tuple, Optional, Dict, Any
|
|
17
|
+
import threading
|
|
18
|
+
import weakref
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ============================================================================
|
|
22
|
+
# COPY-ON-WRITE ARRAY WRAPPER
|
|
23
|
+
# ============================================================================
|
|
24
|
+
|
|
25
|
+
class CopyOnWriteArray:
|
|
26
|
+
"""
|
|
27
|
+
A wrapper that defers copying a numpy array until it's actually modified.
|
|
28
|
+
|
|
29
|
+
This is used to optimize duplicate_document: instead of copying the
|
|
30
|
+
full image immediately, we share the source array and only copy when
|
|
31
|
+
the duplicate is about to be modified (via apply_edit).
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
cow = CopyOnWriteArray(source_array)
|
|
35
|
+
arr = cow.get_array() # Returns view of source (no copy)
|
|
36
|
+
arr = cow.get_writable() # Forces copy if not already copied
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
__slots__ = ('_source', '_copy', '_lock', '_copied')
|
|
40
|
+
|
|
41
|
+
def __init__(self, source: np.ndarray):
|
|
42
|
+
"""
|
|
43
|
+
Initialize with source array (no copy made yet).
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
source: The source numpy array to potentially copy later
|
|
47
|
+
"""
|
|
48
|
+
self._source = source
|
|
49
|
+
self._copy: Optional[np.ndarray] = None
|
|
50
|
+
self._lock = threading.Lock()
|
|
51
|
+
self._copied = False
|
|
52
|
+
|
|
53
|
+
def get_array(self) -> np.ndarray:
|
|
54
|
+
"""
|
|
55
|
+
Get the array for read-only access.
|
|
56
|
+
|
|
57
|
+
Returns the copy if one was made, otherwise the source.
|
|
58
|
+
This does NOT make a copy.
|
|
59
|
+
"""
|
|
60
|
+
if self._copied:
|
|
61
|
+
return self._copy
|
|
62
|
+
return self._source
|
|
63
|
+
|
|
64
|
+
def get_writable(self) -> np.ndarray:
|
|
65
|
+
"""
|
|
66
|
+
Get a writable copy of the array.
|
|
67
|
+
|
|
68
|
+
Forces a copy if one hasn't been made yet.
|
|
69
|
+
Thread-safe.
|
|
70
|
+
"""
|
|
71
|
+
if self._copied:
|
|
72
|
+
return self._copy
|
|
73
|
+
|
|
74
|
+
with self._lock:
|
|
75
|
+
# Double-check after acquiring lock
|
|
76
|
+
if self._copied:
|
|
77
|
+
return self._copy
|
|
78
|
+
|
|
79
|
+
# Make the copy now
|
|
80
|
+
if self._source is not None:
|
|
81
|
+
self._copy = self._source.copy()
|
|
82
|
+
else:
|
|
83
|
+
self._copy = None
|
|
84
|
+
self._copied = True
|
|
85
|
+
self._source = None # Release reference to source
|
|
86
|
+
return self._copy
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def is_copied(self) -> bool:
|
|
90
|
+
"""Check if a copy has been made."""
|
|
91
|
+
return self._copied
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def shape(self) -> tuple:
|
|
95
|
+
"""Get shape of the underlying array."""
|
|
96
|
+
arr = self.get_array()
|
|
97
|
+
return arr.shape if arr is not None else ()
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def ndim(self) -> int:
|
|
101
|
+
"""Get number of dimensions."""
|
|
102
|
+
arr = self.get_array()
|
|
103
|
+
return arr.ndim if arr is not None else 0
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def dtype(self):
|
|
107
|
+
"""Get dtype of the underlying array."""
|
|
108
|
+
arr = self.get_array()
|
|
109
|
+
return arr.dtype if arr is not None else None
|
|
110
|
+
|
|
111
|
+
def __array__(self, dtype=None):
|
|
112
|
+
"""Support numpy array conversion."""
|
|
113
|
+
arr = self.get_array()
|
|
114
|
+
if dtype is None:
|
|
115
|
+
return arr
|
|
116
|
+
return arr.astype(dtype)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ============================================================================
|
|
120
|
+
# LRU DICTIONARY FOR BOUNDED CACHES
|
|
121
|
+
# ============================================================================
|
|
122
|
+
|
|
123
|
+
from collections import OrderedDict
|
|
124
|
+
|
|
125
|
+
class LRUDict(OrderedDict):
|
|
126
|
+
"""
|
|
127
|
+
Simple LRU cache based on OrderedDict.
|
|
128
|
+
When maxsize is exceeded, oldest items are evicted.
|
|
129
|
+
Thread-safe for basic operations.
|
|
130
|
+
"""
|
|
131
|
+
__slots__ = ('maxsize',)
|
|
132
|
+
|
|
133
|
+
def __init__(self, maxsize: int = 500):
|
|
134
|
+
super().__init__()
|
|
135
|
+
self.maxsize = maxsize
|
|
136
|
+
|
|
137
|
+
def __getitem__(self, key):
|
|
138
|
+
# Move to end on access (most recently used)
|
|
139
|
+
self.move_to_end(key)
|
|
140
|
+
return super().__getitem__(key)
|
|
141
|
+
|
|
142
|
+
def get(self, key, default=None):
|
|
143
|
+
if key in self:
|
|
144
|
+
self.move_to_end(key)
|
|
145
|
+
return super().__getitem__(key)
|
|
146
|
+
return default
|
|
147
|
+
|
|
148
|
+
def __setitem__(self, key, value):
|
|
149
|
+
if key in self:
|
|
150
|
+
self.move_to_end(key)
|
|
151
|
+
super().__setitem__(key, value)
|
|
152
|
+
# Evict oldest if over limit
|
|
153
|
+
while len(self) > self.maxsize:
|
|
154
|
+
self.popitem(last=False) # Remove oldest
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ============================================================================
|
|
158
|
+
# MEMORY-MAPPED ARRAY UTILITIES
|
|
159
|
+
# ============================================================================
|
|
160
|
+
|
|
161
|
+
_TEMP_DIR: Optional[Path] = None
|
|
162
|
+
_MEMMAP_FILES: weakref.WeakSet = weakref.WeakSet()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_temp_dir() -> Path:
|
|
166
|
+
"""Get or create the temporary directory for memory-mapped files."""
|
|
167
|
+
global _TEMP_DIR
|
|
168
|
+
if _TEMP_DIR is None or not _TEMP_DIR.exists():
|
|
169
|
+
_TEMP_DIR = Path(tempfile.mkdtemp(prefix="sasp_memmap_"))
|
|
170
|
+
return _TEMP_DIR
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def create_memmap_array(
|
|
174
|
+
shape: Tuple[int, ...],
|
|
175
|
+
dtype: np.dtype = np.float32,
|
|
176
|
+
mode: str = 'w+',
|
|
177
|
+
prefix: str = "array_"
|
|
178
|
+
) -> Tuple[np.memmap, Path]:
|
|
179
|
+
"""
|
|
180
|
+
Create a memory-mapped array backed by a temporary file.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
shape: Shape of the array
|
|
184
|
+
dtype: Data type (default float32)
|
|
185
|
+
mode: File mode ('w+' for read/write, 'r+' for existing)
|
|
186
|
+
prefix: Prefix for temp file name
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Tuple of (memmap array, path to backing file)
|
|
190
|
+
"""
|
|
191
|
+
temp_dir = get_temp_dir()
|
|
192
|
+
temp_file = tempfile.NamedTemporaryFile(
|
|
193
|
+
prefix=prefix,
|
|
194
|
+
suffix=".npy",
|
|
195
|
+
dir=temp_dir,
|
|
196
|
+
delete=False
|
|
197
|
+
)
|
|
198
|
+
temp_path = Path(temp_file.name)
|
|
199
|
+
temp_file.close()
|
|
200
|
+
|
|
201
|
+
mm = np.memmap(str(temp_path), dtype=dtype, mode=mode, shape=shape)
|
|
202
|
+
return mm, temp_path
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def cleanup_memmap(mm: np.memmap, path: Path) -> None:
|
|
206
|
+
"""
|
|
207
|
+
Properly cleanup a memory-mapped array and its backing file.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
mm: The memmap array to cleanup
|
|
211
|
+
path: Path to the backing file
|
|
212
|
+
"""
|
|
213
|
+
try:
|
|
214
|
+
del mm
|
|
215
|
+
except Exception:
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
if path.exists():
|
|
220
|
+
path.unlink()
|
|
221
|
+
except Exception:
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def should_use_memmap(shape: Tuple[int, ...], dtype: np.dtype = np.float32) -> bool:
|
|
226
|
+
"""
|
|
227
|
+
Determine if memmap should be used based on array size.
|
|
228
|
+
|
|
229
|
+
Uses memmap for arrays larger than 500MB to reduce RAM usage.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
shape: Shape of the array
|
|
233
|
+
dtype: Data type
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
True if memmap should be used
|
|
237
|
+
"""
|
|
238
|
+
itemsize = np.dtype(dtype).itemsize
|
|
239
|
+
size_bytes = int(np.prod(shape)) * itemsize
|
|
240
|
+
threshold = 500 * 1024 * 1024 # 500 MB
|
|
241
|
+
return size_bytes > threshold
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def smart_zeros(
|
|
245
|
+
shape: Tuple[int, ...],
|
|
246
|
+
dtype: np.dtype = np.float32,
|
|
247
|
+
force_memmap: bool = False
|
|
248
|
+
) -> Tuple[np.ndarray, Optional[Path]]:
|
|
249
|
+
"""
|
|
250
|
+
Create a zeros array, using memmap for large arrays.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
shape: Shape of the array
|
|
254
|
+
dtype: Data type
|
|
255
|
+
force_memmap: Force use of memmap regardless of size
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Tuple of (array, optional path to memmap file)
|
|
259
|
+
"""
|
|
260
|
+
if force_memmap or should_use_memmap(shape, dtype):
|
|
261
|
+
mm, path = create_memmap_array(shape, dtype, 'w+', "zeros_")
|
|
262
|
+
mm[:] = 0
|
|
263
|
+
return mm, path
|
|
264
|
+
else:
|
|
265
|
+
return np.zeros(shape, dtype=dtype), None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def smart_empty(
|
|
269
|
+
shape: Tuple[int, ...],
|
|
270
|
+
dtype: np.dtype = np.float32,
|
|
271
|
+
force_memmap: bool = False
|
|
272
|
+
) -> Tuple[np.ndarray, Optional[Path]]:
|
|
273
|
+
"""
|
|
274
|
+
Create an empty array, using memmap for large arrays.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
shape: Shape of the array
|
|
278
|
+
dtype: Data type
|
|
279
|
+
force_memmap: Force use of memmap regardless of size
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Tuple of (array, optional path to memmap file)
|
|
283
|
+
"""
|
|
284
|
+
if force_memmap or should_use_memmap(shape, dtype):
|
|
285
|
+
mm, path = create_memmap_array(shape, dtype, 'w+', "empty_")
|
|
286
|
+
return mm, path
|
|
287
|
+
else:
|
|
288
|
+
return np.empty(shape, dtype=dtype), None
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ============================================================================
|
|
292
|
+
# BUFFER POOL FOR REUSABLE MEMORY
|
|
293
|
+
# ============================================================================
|
|
294
|
+
|
|
295
|
+
class BufferPool:
|
|
296
|
+
"""
|
|
297
|
+
A pool of reusable numpy buffers to reduce allocation overhead.
|
|
298
|
+
|
|
299
|
+
Thread-safe buffer management for frequently allocated arrays.
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
def __init__(self, max_buffers: int = 8):
|
|
303
|
+
"""
|
|
304
|
+
Initialize buffer pool.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
max_buffers: Maximum number of buffers to keep per shape/dtype
|
|
308
|
+
"""
|
|
309
|
+
self._pools: Dict[Tuple, list] = {}
|
|
310
|
+
self._lock = threading.Lock()
|
|
311
|
+
self._max_buffers = max_buffers
|
|
312
|
+
|
|
313
|
+
def _key(self, shape: Tuple[int, ...], dtype: np.dtype) -> Tuple:
|
|
314
|
+
"""Create a hashable key for shape/dtype combination."""
|
|
315
|
+
return (shape, str(dtype))
|
|
316
|
+
|
|
317
|
+
def get(self, shape: Tuple[int, ...], dtype: np.dtype = np.float32) -> np.ndarray:
|
|
318
|
+
"""
|
|
319
|
+
Get a buffer from the pool or create a new one.
|
|
320
|
+
|
|
321
|
+
The buffer contents are NOT zeroed - caller should initialize if needed.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
shape: Desired shape
|
|
325
|
+
dtype: Desired dtype
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
A numpy array of the requested shape/dtype
|
|
329
|
+
"""
|
|
330
|
+
key = self._key(shape, dtype)
|
|
331
|
+
|
|
332
|
+
with self._lock:
|
|
333
|
+
pool = self._pools.get(key, [])
|
|
334
|
+
if pool:
|
|
335
|
+
return pool.pop()
|
|
336
|
+
|
|
337
|
+
# Create new buffer
|
|
338
|
+
return np.empty(shape, dtype=dtype)
|
|
339
|
+
|
|
340
|
+
def get_zeros(self, shape: Tuple[int, ...], dtype: np.dtype = np.float32) -> np.ndarray:
|
|
341
|
+
"""
|
|
342
|
+
Get a zeroed buffer from the pool.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
shape: Desired shape
|
|
346
|
+
dtype: Desired dtype
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
A zeroed numpy array of the requested shape/dtype
|
|
350
|
+
"""
|
|
351
|
+
buf = self.get(shape, dtype)
|
|
352
|
+
buf.fill(0)
|
|
353
|
+
return buf
|
|
354
|
+
|
|
355
|
+
def release(self, buf: np.ndarray) -> None:
|
|
356
|
+
"""
|
|
357
|
+
Return a buffer to the pool for reuse.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
buf: Buffer to return
|
|
361
|
+
"""
|
|
362
|
+
if buf is None:
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
key = self._key(buf.shape, buf.dtype)
|
|
366
|
+
|
|
367
|
+
with self._lock:
|
|
368
|
+
if key not in self._pools:
|
|
369
|
+
self._pools[key] = []
|
|
370
|
+
|
|
371
|
+
if len(self._pools[key]) < self._max_buffers:
|
|
372
|
+
self._pools[key].append(buf)
|
|
373
|
+
|
|
374
|
+
def clear(self) -> None:
|
|
375
|
+
"""Clear all buffers from the pool."""
|
|
376
|
+
with self._lock:
|
|
377
|
+
self._pools.clear()
|
|
378
|
+
|
|
379
|
+
def stats(self) -> Dict[str, int]:
|
|
380
|
+
"""Get statistics about pool usage."""
|
|
381
|
+
with self._lock:
|
|
382
|
+
return {
|
|
383
|
+
"num_shapes": len(self._pools),
|
|
384
|
+
"total_buffers": sum(len(p) for p in self._pools.values()),
|
|
385
|
+
"shapes": list(self._pools.keys())
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
# Global buffer pool instance
|
|
390
|
+
_global_pool: Optional[BufferPool] = None
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def get_buffer_pool() -> BufferPool:
|
|
394
|
+
"""Get the global buffer pool instance."""
|
|
395
|
+
global _global_pool
|
|
396
|
+
if _global_pool is None:
|
|
397
|
+
_global_pool = BufferPool(max_buffers=8)
|
|
398
|
+
return _global_pool
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
# ============================================================================
|
|
402
|
+
# THUMBNAIL/PREVIEW CACHE
|
|
403
|
+
# ============================================================================
|
|
404
|
+
|
|
405
|
+
class ThumbnailCache:
|
|
406
|
+
"""
|
|
407
|
+
Disk-based cache for image thumbnails/previews.
|
|
408
|
+
|
|
409
|
+
Speeds up repeated loading of the same images by caching
|
|
410
|
+
downscaled preview versions.
|
|
411
|
+
"""
|
|
412
|
+
|
|
413
|
+
def __init__(self, cache_dir: Optional[Path] = None, max_size_mb: int = 500):
|
|
414
|
+
"""
|
|
415
|
+
Initialize thumbnail cache.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
cache_dir: Directory for cache files (default: temp dir)
|
|
419
|
+
max_size_mb: Maximum cache size in MB
|
|
420
|
+
"""
|
|
421
|
+
if cache_dir is None:
|
|
422
|
+
cache_dir = Path(tempfile.gettempdir()) / "sasp_thumb_cache"
|
|
423
|
+
|
|
424
|
+
self._cache_dir = cache_dir
|
|
425
|
+
self._cache_dir.mkdir(parents=True, exist_ok=True)
|
|
426
|
+
self._max_size = max_size_mb * 1024 * 1024
|
|
427
|
+
self._lock = threading.Lock()
|
|
428
|
+
self._memory_cache: Dict[str, np.ndarray] = {} # LRU in-memory cache
|
|
429
|
+
self._max_memory_items = 50
|
|
430
|
+
|
|
431
|
+
def _get_cache_key(self, path: str, target_size: Tuple[int, int]) -> str:
|
|
432
|
+
"""Generate a unique cache key for a file and target size."""
|
|
433
|
+
# Include file path, mtime, and target size in hash
|
|
434
|
+
try:
|
|
435
|
+
mtime = os.path.getmtime(path)
|
|
436
|
+
except Exception:
|
|
437
|
+
mtime = 0
|
|
438
|
+
|
|
439
|
+
key_str = f"{path}|{mtime}|{target_size[0]}x{target_size[1]}"
|
|
440
|
+
return hashlib.md5(key_str.encode()).hexdigest()
|
|
441
|
+
|
|
442
|
+
def _get_cache_path(self, key: str) -> Path:
|
|
443
|
+
"""Get the cache file path for a key."""
|
|
444
|
+
return self._cache_dir / f"{key}.npy"
|
|
445
|
+
|
|
446
|
+
def get(self, path: str, target_size: Tuple[int, int]) -> Optional[np.ndarray]:
|
|
447
|
+
"""
|
|
448
|
+
Get a cached thumbnail if available.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
path: Original image path
|
|
452
|
+
target_size: Target thumbnail size (width, height)
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
Cached thumbnail array or None
|
|
456
|
+
"""
|
|
457
|
+
key = self._get_cache_key(path, target_size)
|
|
458
|
+
|
|
459
|
+
# Check in-memory cache first
|
|
460
|
+
with self._lock:
|
|
461
|
+
if key in self._memory_cache:
|
|
462
|
+
return self._memory_cache[key].copy()
|
|
463
|
+
|
|
464
|
+
# Check disk cache
|
|
465
|
+
cache_path = self._get_cache_path(key)
|
|
466
|
+
if cache_path.exists():
|
|
467
|
+
try:
|
|
468
|
+
thumb = np.load(str(cache_path))
|
|
469
|
+
# Add to memory cache
|
|
470
|
+
with self._lock:
|
|
471
|
+
self._memory_cache[key] = thumb
|
|
472
|
+
self._trim_memory_cache()
|
|
473
|
+
return thumb.copy()
|
|
474
|
+
except Exception:
|
|
475
|
+
# Corrupted cache file, remove it
|
|
476
|
+
try:
|
|
477
|
+
cache_path.unlink()
|
|
478
|
+
except Exception:
|
|
479
|
+
pass
|
|
480
|
+
|
|
481
|
+
return None
|
|
482
|
+
|
|
483
|
+
def put(self, path: str, target_size: Tuple[int, int], thumb: np.ndarray) -> None:
|
|
484
|
+
"""
|
|
485
|
+
Store a thumbnail in the cache.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
path: Original image path
|
|
489
|
+
target_size: Target thumbnail size
|
|
490
|
+
thumb: Thumbnail array to cache
|
|
491
|
+
"""
|
|
492
|
+
key = self._get_cache_key(path, target_size)
|
|
493
|
+
|
|
494
|
+
# Store in memory cache
|
|
495
|
+
with self._lock:
|
|
496
|
+
self._memory_cache[key] = thumb.copy()
|
|
497
|
+
self._trim_memory_cache()
|
|
498
|
+
|
|
499
|
+
# Store on disk
|
|
500
|
+
cache_path = self._get_cache_path(key)
|
|
501
|
+
try:
|
|
502
|
+
np.save(str(cache_path), thumb)
|
|
503
|
+
except Exception:
|
|
504
|
+
pass
|
|
505
|
+
|
|
506
|
+
# Trim disk cache if needed
|
|
507
|
+
self._trim_disk_cache()
|
|
508
|
+
|
|
509
|
+
def _trim_memory_cache(self) -> None:
|
|
510
|
+
"""Trim memory cache to max size (caller must hold lock)."""
|
|
511
|
+
while len(self._memory_cache) > self._max_memory_items:
|
|
512
|
+
# Remove oldest item (first key)
|
|
513
|
+
oldest = next(iter(self._memory_cache))
|
|
514
|
+
del self._memory_cache[oldest]
|
|
515
|
+
|
|
516
|
+
def _trim_disk_cache(self) -> None:
|
|
517
|
+
"""Trim disk cache to max size."""
|
|
518
|
+
try:
|
|
519
|
+
files = list(self._cache_dir.glob("*.npy"))
|
|
520
|
+
total_size = sum(f.stat().st_size for f in files)
|
|
521
|
+
|
|
522
|
+
if total_size > self._max_size:
|
|
523
|
+
# Sort by access time, oldest first
|
|
524
|
+
files.sort(key=lambda f: f.stat().st_atime)
|
|
525
|
+
|
|
526
|
+
while total_size > self._max_size * 0.8 and files: # Trim to 80%
|
|
527
|
+
oldest = files.pop(0)
|
|
528
|
+
try:
|
|
529
|
+
size = oldest.stat().st_size
|
|
530
|
+
oldest.unlink()
|
|
531
|
+
total_size -= size
|
|
532
|
+
except Exception:
|
|
533
|
+
pass
|
|
534
|
+
except Exception:
|
|
535
|
+
pass
|
|
536
|
+
|
|
537
|
+
def clear(self) -> None:
|
|
538
|
+
"""Clear all cached thumbnails."""
|
|
539
|
+
with self._lock:
|
|
540
|
+
self._memory_cache.clear()
|
|
541
|
+
|
|
542
|
+
try:
|
|
543
|
+
for f in self._cache_dir.glob("*.npy"):
|
|
544
|
+
try:
|
|
545
|
+
f.unlink()
|
|
546
|
+
except Exception:
|
|
547
|
+
pass
|
|
548
|
+
except Exception:
|
|
549
|
+
pass
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
# Global thumbnail cache instance
|
|
553
|
+
_thumb_cache: Optional[ThumbnailCache] = None
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def get_thumbnail_cache() -> ThumbnailCache:
|
|
557
|
+
"""Get the global thumbnail cache instance."""
|
|
558
|
+
global _thumb_cache
|
|
559
|
+
if _thumb_cache is None:
|
|
560
|
+
_thumb_cache = ThumbnailCache()
|
|
561
|
+
return _thumb_cache
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
# ============================================================================
|
|
565
|
+
# LAZY IMAGE LOADER
|
|
566
|
+
# ============================================================================
|
|
567
|
+
|
|
568
|
+
class LazyImage:
|
|
569
|
+
"""
|
|
570
|
+
Lazy image loader that loads full resolution on demand.
|
|
571
|
+
|
|
572
|
+
Initially loads only a preview/thumbnail, deferring full
|
|
573
|
+
resolution loading until actually needed.
|
|
574
|
+
"""
|
|
575
|
+
|
|
576
|
+
def __init__(
|
|
577
|
+
self,
|
|
578
|
+
path: str,
|
|
579
|
+
preview_size: Tuple[int, int] = (512, 512),
|
|
580
|
+
load_preview_fn: Optional[callable] = None,
|
|
581
|
+
load_full_fn: Optional[callable] = None
|
|
582
|
+
):
|
|
583
|
+
"""
|
|
584
|
+
Initialize lazy image.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
path: Path to the image file
|
|
588
|
+
preview_size: Size for preview image
|
|
589
|
+
load_preview_fn: Function to load preview (path, size) -> array
|
|
590
|
+
load_full_fn: Function to load full image (path) -> array
|
|
591
|
+
"""
|
|
592
|
+
self.path = path
|
|
593
|
+
self.preview_size = preview_size
|
|
594
|
+
self._preview: Optional[np.ndarray] = None
|
|
595
|
+
self._full: Optional[np.ndarray] = None
|
|
596
|
+
self._load_preview_fn = load_preview_fn
|
|
597
|
+
self._load_full_fn = load_full_fn
|
|
598
|
+
self._lock = threading.Lock()
|
|
599
|
+
|
|
600
|
+
@property
|
|
601
|
+
def preview(self) -> Optional[np.ndarray]:
|
|
602
|
+
"""Get preview image, loading if necessary."""
|
|
603
|
+
if self._preview is None and self._load_preview_fn is not None:
|
|
604
|
+
with self._lock:
|
|
605
|
+
if self._preview is None:
|
|
606
|
+
# Check cache first
|
|
607
|
+
cache = get_thumbnail_cache()
|
|
608
|
+
cached = cache.get(self.path, self.preview_size)
|
|
609
|
+
if cached is not None:
|
|
610
|
+
self._preview = cached
|
|
611
|
+
else:
|
|
612
|
+
self._preview = self._load_preview_fn(self.path, self.preview_size)
|
|
613
|
+
if self._preview is not None:
|
|
614
|
+
cache.put(self.path, self.preview_size, self._preview)
|
|
615
|
+
return self._preview
|
|
616
|
+
|
|
617
|
+
@property
|
|
618
|
+
def full(self) -> Optional[np.ndarray]:
|
|
619
|
+
"""Get full resolution image, loading if necessary."""
|
|
620
|
+
if self._full is None and self._load_full_fn is not None:
|
|
621
|
+
with self._lock:
|
|
622
|
+
if self._full is None:
|
|
623
|
+
self._full = self._load_full_fn(self.path)
|
|
624
|
+
return self._full
|
|
625
|
+
|
|
626
|
+
@property
|
|
627
|
+
def is_full_loaded(self) -> bool:
|
|
628
|
+
"""Check if full resolution image is loaded."""
|
|
629
|
+
return self._full is not None
|
|
630
|
+
|
|
631
|
+
def unload_full(self) -> None:
|
|
632
|
+
"""Unload full resolution to free memory."""
|
|
633
|
+
with self._lock:
|
|
634
|
+
self._full = None
|
|
635
|
+
|
|
636
|
+
def unload_all(self) -> None:
|
|
637
|
+
"""Unload all images to free memory."""
|
|
638
|
+
with self._lock:
|
|
639
|
+
self._preview = None
|
|
640
|
+
self._full = None
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
# ============================================================================
|
|
644
|
+
# CLEANUP UTILITIES
|
|
645
|
+
# ============================================================================
|
|
646
|
+
|
|
647
|
+
def cleanup_temp_files() -> None:
|
|
648
|
+
"""Cleanup all temporary memory-mapped files."""
|
|
649
|
+
global _TEMP_DIR
|
|
650
|
+
if _TEMP_DIR is not None and _TEMP_DIR.exists():
|
|
651
|
+
try:
|
|
652
|
+
import shutil
|
|
653
|
+
shutil.rmtree(_TEMP_DIR, ignore_errors=True)
|
|
654
|
+
except Exception:
|
|
655
|
+
pass
|
|
656
|
+
_TEMP_DIR = None
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def get_memory_usage_mb() -> float:
|
|
660
|
+
"""Get current process memory usage in MB."""
|
|
661
|
+
try:
|
|
662
|
+
import psutil
|
|
663
|
+
process = psutil.Process()
|
|
664
|
+
return process.memory_info().rss / (1024 * 1024)
|
|
665
|
+
except Exception:
|
|
666
|
+
return 0.0
|