TwoPy 0.3.8__tar.gz → 0.3.9__tar.gz
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.
- {twopy-0.3.8 → twopy-0.3.9}/PKG-INFO +1 -1
- {twopy-0.3.8 → twopy-0.3.9}/pyproject.toml +1 -1
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/conversion/alignment_crop.py +34 -2
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/conversion/hdf5_writing.py +5 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/conversion/types.py +1 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/converted.py +34 -2
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/empty_state.py +2 -2
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/docks/options_panel.py +6 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/docks/response_plot_widget.py +3 -0
- twopy-0.3.9/src/twopy/napari/plotting/motion_summary.py +324 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/panels.py +4 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy.egg-info/PKG-INFO +1 -1
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy.egg-info/SOURCES.txt +1 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_background_subtraction.py +4 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_conversion.py +33 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_converted.py +48 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_load_workflow.py +1 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari.py +4 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_plot_controls.py +176 -1
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_parity_psycho5.py +1 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_real_fixture.py +4 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_roi.py +4 -0
- {twopy-0.3.8 → twopy-0.3.9}/README.md +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/setup.cfg +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/__init__.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/__init__.pyi +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/_segments.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/_version.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/__init__.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/background_subtraction.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/dff.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/dff_options.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/epoch_mapping.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/group_matching.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/kernels/__init__.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/kernels/_fitting.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/kernels/_stimulus.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/motion.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/persistence.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/response_context.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/response_maps.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/response_plotting.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/response_processing/__init__.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/response_processing/apply.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/response_processing/options.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/response_processing/persistence.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/response_processing/qc.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/response_processing/signals.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/response_window_options.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/responses.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/timing.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/trials.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/workflow.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis_cache.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/api.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/cache_inventory.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/calibrations/pixel_calibration_profiles.csv +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/calibrations/pixel_size.csv +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/config.example.yml +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/config.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/conversion/__init__.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/conversion/api.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/conversion/matlab_values.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/conversion/orientation.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/conversion/source_loading.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/conversion/stimulus_code.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/__init__.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/context.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/discovery.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/metadata.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/native.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/native_workflows/__init__.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/native_workflows/direction_selectivity.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/native_workflows/response_kernels.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/parameters.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/provenance.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/registry.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/results.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/runner.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/workflows.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/database/__init__.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/database/cache.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/database/catalog.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/database/connection.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/database/generic.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/database/modeled.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/database/search.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/database/sql.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/database/types.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/filenames.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/frame_ranges.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/hdf5_utils.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/inspection.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/matlab.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/__init__.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/__main__.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/assets/twopy-app-icon-128.png +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/assets/twopy-app-icon-16.png +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/assets/twopy-app-icon-256.png +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/assets/twopy-app-icon-32.png +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/assets/twopy-app-icon-512.png +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/assets/twopy-app-icon-64.png +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/assets/twopy-app-icon.icns +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/assets/twopy-app-icon.png +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/controls.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/custom_tab.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/database_favorite_editor.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/database_favorites.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/database_search.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/dims.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/display.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/display_paths.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/errors.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/__init__.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/cards.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/fov_assignment.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/fov_cards.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/images.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/protocols.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/response_preview.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/responses.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/roi_assignment.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/roi_cards.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/style.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/tables.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/window.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/interactive.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/latest_worker.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/launcher.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/live_analysis.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/load_controller.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/load_progress.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/load_workflow.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/loaded_recordings_csv.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/loading.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/movie.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/output_routing.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/paths.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/__init__.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/data.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/dff_options.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/docks/__init__.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/docks/api.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/docks/dynamic_options.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/docks/paths.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/docks/plot_area.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/docks/response_map_area.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/docks/save_actions.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/docks/visibility_state.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/export.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/export_controls.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/form_controls.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/label_visibility.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/normalization_options.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/options.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/preview_strip.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/processing_options.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/response_map_colors.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/response_map_display.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/response_map_options.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/response_window_options.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/roi_generation/__init__.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/roi_generation/actions.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/roi_generation/controls.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/roi_generation/options.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/widgets.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/protocols.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/responses.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/roi.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/roi_signatures.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/session.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/sidebar.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/state.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/text.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/trial_timeline.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/types.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/version_check.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/viewer.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/parity/__init__.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/parity/dff.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/parity/psycho5.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/parity/roi.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/parity/roi_extraction.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/parity/saved_analysis.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/photodiode.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/photodiode_classification.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/pixel_calibration.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/pixel_calibration_profiles.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/response_roi_extraction.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/roi.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/roi_extraction.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/roi_mask_cleanup.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/session.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/spatial.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/spatial_orientation.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/stimulus.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/synchronization.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy/typing_guards.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy.egg-info/dependency_links.txt +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy.egg-info/entry_points.txt +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy.egg-info/requires.txt +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/src/twopy.egg-info/top_level.txt +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_api.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_cache_inventory.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_config.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_custom_workflows.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_database.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_dff.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_epoch_mapping.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_filenames.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_frame_ranges.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_function_inventory.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_group_matching.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_hdf5_utils.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_import.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_inspection.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_kernels.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_matlab.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_motion.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_database_search.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_display.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_export.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_group_matching_responses.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_group_matching_roi_cards.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_live_controller.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_load_controller.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_load_progress.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_load_tab.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_loaded_recordings.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_path_resolution.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_plot_data.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_plot_widgets.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_processing_controls.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_response_map_display.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_response_workflow.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_roi_generation.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_roi_visibility.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_text.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_timeline.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_packaging.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_parity.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_parity_roi_extraction.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_persistence.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_photodiode_classification.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_pixel_calibration.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_pixel_calibration_profiles.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_recording_timing.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_response_map_area.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_response_maps.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_response_processing.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_response_roi_extraction.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_response_window_options.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_responses.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_roi_extraction.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_roi_mask_cleanup.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_saved_analysis.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_session.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_stimulus.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_synchronization.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_trials.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_version_check.py +0 -0
- {twopy-0.3.8 → twopy-0.3.9}/tests/test_workflow.py +0 -0
|
@@ -67,6 +67,12 @@ def load_alignment_valid_crop(
|
|
|
67
67
|
f"[{frame_start}, {frame_stop}) for {alignment.shape[0]} rows"
|
|
68
68
|
)
|
|
69
69
|
raise ValueError(msg)
|
|
70
|
+
if alignment.shape[0] < aligned_movie.shape[0]:
|
|
71
|
+
msg = (
|
|
72
|
+
"Alignment text must have at least one row per aligned movie frame: "
|
|
73
|
+
f"{alignment.shape[0]} rows for {aligned_movie.shape[0]} frames"
|
|
74
|
+
)
|
|
75
|
+
raise ValueError(msg)
|
|
70
76
|
|
|
71
77
|
alignment_window = alignment[frame_start:frame_stop, :]
|
|
72
78
|
line_scan = _is_line_scan(acquisition)
|
|
@@ -79,10 +85,14 @@ def load_alignment_valid_crop(
|
|
|
79
85
|
source_x_pixel_count=source_spatial_shape[0],
|
|
80
86
|
line_scan=line_scan,
|
|
81
87
|
)
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
full_alignment = alignment[: aligned_movie.shape[0], :]
|
|
89
|
+
alignment_offset_pixels = _alignment_offset_pixels(
|
|
90
|
+
full_alignment,
|
|
84
91
|
line_scan=line_scan,
|
|
85
92
|
)
|
|
93
|
+
alignment_shift_pixels = _alignment_shift_pixels(
|
|
94
|
+
full_alignment, line_scan=line_scan
|
|
95
|
+
)
|
|
86
96
|
motion_artifact_mask = alignment_shift_pixels > motion_threshold_pixels
|
|
87
97
|
kept_alignment, over_moved_count = _remove_over_moved_alignment_rows(
|
|
88
98
|
alignment_window,
|
|
@@ -103,6 +113,7 @@ def load_alignment_valid_crop(
|
|
|
103
113
|
y_cutoff_pixels=y_cutoff,
|
|
104
114
|
over_moved_frame_count=over_moved_count,
|
|
105
115
|
motion_threshold_pixels=motion_threshold_pixels,
|
|
116
|
+
alignment_offset_pixels=alignment_offset_pixels,
|
|
106
117
|
alignment_shift_pixels=alignment_shift_pixels,
|
|
107
118
|
motion_artifact_mask=motion_artifact_mask,
|
|
108
119
|
)
|
|
@@ -212,6 +223,27 @@ def _alignment_shift_pixels(
|
|
|
212
223
|
return np.sqrt(alignment[:, 0] ** 2 + alignment[:, 1] ** 2)
|
|
213
224
|
|
|
214
225
|
|
|
226
|
+
def _alignment_offset_pixels(
|
|
227
|
+
alignment: npt.NDArray[np.float64],
|
|
228
|
+
*,
|
|
229
|
+
line_scan: bool,
|
|
230
|
+
) -> npt.NDArray[np.float64]:
|
|
231
|
+
"""Return signed x/y alignment offsets in pixels.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
alignment: Alignment table with x/y offset columns.
|
|
235
|
+
line_scan: Whether this recording is a line scan.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Array shaped ``(frames, 2)`` with x then y offsets.
|
|
239
|
+
"""
|
|
240
|
+
offsets = np.zeros((alignment.shape[0], 2), dtype=np.float64)
|
|
241
|
+
offsets[:, 0] = alignment[:, 0]
|
|
242
|
+
if not line_scan:
|
|
243
|
+
offsets[:, 1] = alignment[:, 1]
|
|
244
|
+
return offsets
|
|
245
|
+
|
|
246
|
+
|
|
215
247
|
def _alignment_crop_from_offsets(
|
|
216
248
|
alignment: npt.NDArray[np.float64],
|
|
217
249
|
*,
|
|
@@ -198,6 +198,11 @@ def _write_alignment_crop_group(
|
|
|
198
198
|
crop_group.attrs["y_cutoff_pixels"] = alignment_crop.y_cutoff_pixels
|
|
199
199
|
crop_group.attrs["over_moved_frame_count"] = alignment_crop.over_moved_frame_count
|
|
200
200
|
crop_group.attrs["motion_threshold_pixels"] = alignment_crop.motion_threshold_pixels
|
|
201
|
+
crop_group.create_dataset(
|
|
202
|
+
"alignment_offset_pixels",
|
|
203
|
+
data=alignment_crop.alignment_offset_pixels,
|
|
204
|
+
compression="gzip",
|
|
205
|
+
)
|
|
201
206
|
crop_group.create_dataset(
|
|
202
207
|
"alignment_shift_pixels",
|
|
203
208
|
data=alignment_crop.alignment_shift_pixels,
|
|
@@ -255,6 +255,7 @@ class AlignmentCropAudit:
|
|
|
255
255
|
y_cutoff_pixels: int
|
|
256
256
|
over_moved_frame_count: int
|
|
257
257
|
motion_threshold_pixels: float
|
|
258
|
+
alignment_offset_pixels: npt.NDArray[np.float64]
|
|
258
259
|
alignment_shift_pixels: npt.NDArray[np.float64]
|
|
259
260
|
motion_artifact_mask: npt.NDArray[np.bool_]
|
|
260
261
|
|
|
@@ -159,8 +159,8 @@ class RecordingData:
|
|
|
159
159
|
|
|
160
160
|
Inputs: twopy-converted HDF5 files.
|
|
161
161
|
Outputs: metadata dictionaries, stimulus arrays with column names,
|
|
162
|
-
photodiode vectors, mean image,
|
|
163
|
-
reader.
|
|
162
|
+
photodiode vectors, mean image, alignment crop and motion arrays,
|
|
163
|
+
frame-count audit values, and a lazy movie reader.
|
|
164
164
|
|
|
165
165
|
This object is the analysis-facing representation of one converted
|
|
166
166
|
recording. It deliberately excludes source MATLAB/TIFF ownership.
|
|
@@ -181,6 +181,7 @@ class RecordingData:
|
|
|
181
181
|
high_res_pd: npt.NDArray[np.float64]
|
|
182
182
|
mean_image: npt.NDArray[np.float64]
|
|
183
183
|
alignment_valid_crop: SpatialCrop
|
|
184
|
+
alignment_offset_pixels: npt.NDArray[np.float64] | None
|
|
184
185
|
alignment_shift_pixels: npt.NDArray[np.float64]
|
|
185
186
|
motion_artifact_mask: npt.NDArray[np.bool_]
|
|
186
187
|
frame_counts: FrameCountAudit
|
|
@@ -278,6 +279,7 @@ def load_converted_recording(
|
|
|
278
279
|
ndim=2,
|
|
279
280
|
),
|
|
280
281
|
alignment_valid_crop=_read_alignment_valid_crop(h5_file),
|
|
282
|
+
alignment_offset_pixels=_read_alignment_offset_pixels(h5_file),
|
|
281
283
|
alignment_shift_pixels=require_float64_array(
|
|
282
284
|
h5_file["movie/alignment_valid_crop/alignment_shift_pixels"][()],
|
|
283
285
|
name="movie/alignment_valid_crop/alignment_shift_pixels",
|
|
@@ -440,6 +442,15 @@ def _validate_loaded_recording(recording: RecordingData) -> None:
|
|
|
440
442
|
f"{recording.movie.shape[1:]}"
|
|
441
443
|
)
|
|
442
444
|
raise ValueError(msg)
|
|
445
|
+
if (
|
|
446
|
+
recording.alignment_offset_pixels is not None
|
|
447
|
+
and recording.alignment_offset_pixels.shape != (frame_count, 2)
|
|
448
|
+
):
|
|
449
|
+
msg = (
|
|
450
|
+
"alignment_offset_pixels must have x/y values for each movie frame; "
|
|
451
|
+
f"got {recording.alignment_offset_pixels.shape} for {frame_count} frames"
|
|
452
|
+
)
|
|
453
|
+
raise ValueError(msg)
|
|
443
454
|
if recording.alignment_shift_pixels.shape != (frame_count,):
|
|
444
455
|
msg = (
|
|
445
456
|
"alignment_shift_pixels must have one value per movie frame; "
|
|
@@ -712,6 +723,27 @@ def _read_alignment_valid_crop(h5_file: h5py.File) -> SpatialCrop:
|
|
|
712
723
|
)
|
|
713
724
|
|
|
714
725
|
|
|
726
|
+
def _read_alignment_offset_pixels(
|
|
727
|
+
h5_file: h5py.File,
|
|
728
|
+
) -> npt.NDArray[np.float64] | None:
|
|
729
|
+
"""Read optional signed x/y alignment offsets from converted HDF5.
|
|
730
|
+
|
|
731
|
+
Args:
|
|
732
|
+
h5_file: Open ``recording_data.h5`` file.
|
|
733
|
+
|
|
734
|
+
Returns:
|
|
735
|
+
Per-frame x/y offsets, or ``None`` for older converted files.
|
|
736
|
+
"""
|
|
737
|
+
dataset_name = "movie/alignment_valid_crop/alignment_offset_pixels"
|
|
738
|
+
if dataset_name not in h5_file:
|
|
739
|
+
return None
|
|
740
|
+
return require_float64_array(
|
|
741
|
+
h5_file[dataset_name][()],
|
|
742
|
+
name=dataset_name,
|
|
743
|
+
ndim=2,
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
|
|
715
747
|
def _normalize_spatial_crop(
|
|
716
748
|
spatial_shape: tuple[int, int],
|
|
717
749
|
spatial_crop: SpatialCrop | None,
|
|
@@ -135,13 +135,13 @@ def empty_viewer_message(*, update_command: str | None = None) -> str:
|
|
|
135
135
|
"""Return the empty-viewer text for the current update state."""
|
|
136
136
|
if not update_command:
|
|
137
137
|
return _BASE_EMPTY_VIEWER_MESSAGE
|
|
138
|
-
return f"{_BASE_EMPTY_VIEWER_MESSAGE}\n\
|
|
138
|
+
return f"{_BASE_EMPTY_VIEWER_MESSAGE}\n\n{update_command}"
|
|
139
139
|
|
|
140
140
|
|
|
141
141
|
def _is_empty_viewer_message(text: str) -> bool:
|
|
142
142
|
"""Return whether text is owned by twopy's empty-recording overlay."""
|
|
143
143
|
return text == EMPTY_VIEWER_MESSAGE or text.startswith(
|
|
144
|
-
f"{EMPTY_VIEWER_MESSAGE}\n\
|
|
144
|
+
f"{EMPTY_VIEWER_MESSAGE}\n\n"
|
|
145
145
|
)
|
|
146
146
|
|
|
147
147
|
|
|
@@ -27,6 +27,7 @@ from twopy.napari.plotting.export_controls import (
|
|
|
27
27
|
ResponseExportState,
|
|
28
28
|
create_response_export_tab,
|
|
29
29
|
)
|
|
30
|
+
from twopy.napari.plotting.motion_summary import MotionSummaryWidget
|
|
30
31
|
from twopy.napari.plotting.normalization_options import NormalizationOptionsWidget
|
|
31
32
|
from twopy.napari.plotting.panels import (
|
|
32
33
|
SidebarTextLabel,
|
|
@@ -56,6 +57,7 @@ class ResponseOptionsPanel:
|
|
|
56
57
|
roi_save_path_label: Metadata-tab label showing the ROI output path.
|
|
57
58
|
update_status_label: Metadata-tab label showing save or processing status.
|
|
58
59
|
update_notice_label: Metadata-tab label showing the update command.
|
|
60
|
+
motion_summary_widget: Metadata-tab widget showing alignment movement.
|
|
59
61
|
export_status_label: Export-tab label showing save/export action status.
|
|
60
62
|
reload_saved_button: Load-tab button that reloads persisted response
|
|
61
63
|
outputs.
|
|
@@ -83,6 +85,7 @@ class ResponseOptionsPanel:
|
|
|
83
85
|
roi_save_path_label: SidebarTextLabel
|
|
84
86
|
update_status_label: SidebarTextLabel
|
|
85
87
|
update_notice_label: SidebarTextLabel
|
|
88
|
+
motion_summary_widget: MotionSummaryWidget
|
|
86
89
|
export_status_label: SidebarTextLabel
|
|
87
90
|
reload_saved_button: QPushButton
|
|
88
91
|
plot_options_layout: QVBoxLayout
|
|
@@ -153,6 +156,7 @@ def create_response_options_panel(
|
|
|
153
156
|
roi_save_path_label = SidebarTextLabel(f"ROI output: {DEFAULT_PATH_TEXT}")
|
|
154
157
|
update_status_label = SidebarTextLabel("")
|
|
155
158
|
update_notice_label = SidebarTextLabel("")
|
|
159
|
+
motion_summary_widget = MotionSummaryWidget()
|
|
156
160
|
export_status_label = SidebarTextLabel("Exports save beside the recording.")
|
|
157
161
|
|
|
158
162
|
plot_options_layout = QVBoxLayout()
|
|
@@ -209,6 +213,7 @@ def create_response_options_panel(
|
|
|
209
213
|
roi_output_label=roi_save_path_label,
|
|
210
214
|
status_label=update_status_label,
|
|
211
215
|
update_notice_label=update_notice_label,
|
|
216
|
+
motion_summary_widget=motion_summary_widget,
|
|
212
217
|
),
|
|
213
218
|
"Metadata",
|
|
214
219
|
)
|
|
@@ -232,6 +237,7 @@ def create_response_options_panel(
|
|
|
232
237
|
roi_save_path_label=roi_save_path_label,
|
|
233
238
|
update_status_label=update_status_label,
|
|
234
239
|
update_notice_label=update_notice_label,
|
|
240
|
+
motion_summary_widget=motion_summary_widget,
|
|
235
241
|
export_status_label=export_status_label,
|
|
236
242
|
reload_saved_button=reload_saved_button,
|
|
237
243
|
plot_options_layout=plot_options_layout,
|
|
@@ -275,6 +275,7 @@ class _ResponsePlotWidget(QWidget):
|
|
|
275
275
|
self._roi_save_path_label = options_panel.roi_save_path_label
|
|
276
276
|
self._update_status_label = options_panel.update_status_label
|
|
277
277
|
self._update_notice_label = options_panel.update_notice_label
|
|
278
|
+
self._motion_summary_widget = options_panel.motion_summary_widget
|
|
278
279
|
self._export_status_label = options_panel.export_status_label
|
|
279
280
|
self._reload_saved_button = options_panel.reload_saved_button
|
|
280
281
|
self._plot_options_layout = options_panel.plot_options_layout
|
|
@@ -496,6 +497,7 @@ class _ResponsePlotWidget(QWidget):
|
|
|
496
497
|
self._analysis_path_label.setText(f"Analysis output: {DEFAULT_PATH_TEXT}")
|
|
497
498
|
self._roi_save_path_label.setText(f"ROI output: {DEFAULT_PATH_TEXT}")
|
|
498
499
|
self._update_status_label.setText("")
|
|
500
|
+
self._motion_summary_widget.set_recording(None)
|
|
499
501
|
self._export_status_label.setText("Exports save beside the recording.")
|
|
500
502
|
self._reset_empty_option_tabs()
|
|
501
503
|
self._set_status("No recording loaded.")
|
|
@@ -528,6 +530,7 @@ class _ResponsePlotWidget(QWidget):
|
|
|
528
530
|
self._recording = recording
|
|
529
531
|
self._output_route = output_route or recording_output_route(recording)
|
|
530
532
|
self._roi_generation_widget.set_recording(recording)
|
|
533
|
+
self._motion_summary_widget.set_recording(recording)
|
|
531
534
|
self._analysis_path = resolved_analysis_path(None, recording)
|
|
532
535
|
epoch_names = stimulus_epoch_names_by_number(recording)
|
|
533
536
|
self._delta_f_over_f_options_widget.set_epoch_choices(
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""Inline Motion metadata widget for the twopy napari sidebar.
|
|
2
|
+
|
|
3
|
+
Inputs: converted recording alignment crop, per-frame signed movement offsets,
|
|
4
|
+
movement magnitudes, and motion-artifact masks.
|
|
5
|
+
Outputs: a compact Metadata-tab section with crop metrics and a movement plot.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import numpy.typing as npt
|
|
10
|
+
from qtpy.QtCore import QRect, Qt
|
|
11
|
+
from qtpy.QtGui import QColor, QPainter, QPainterPath, QPaintEvent, QPen
|
|
12
|
+
from qtpy.QtWidgets import QGroupBox, QSizePolicy, QVBoxLayout, QWidget
|
|
13
|
+
|
|
14
|
+
from twopy.converted import RecordingData
|
|
15
|
+
from twopy.napari.plotting.panels import SidebarTextLabel
|
|
16
|
+
|
|
17
|
+
__all__ = ["MotionSummaryWidget"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MotionSummaryWidget(QGroupBox):
|
|
21
|
+
"""Metadata-tab section that shows alignment movement over recording frames.
|
|
22
|
+
|
|
23
|
+
Inputs: a loaded converted recording.
|
|
24
|
+
Outputs: a compact crop/motion text summary plus a movement plot.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
"""Create an empty motion summary section."""
|
|
29
|
+
super().__init__("Motion")
|
|
30
|
+
self._summary_label = SidebarTextLabel("No recording loaded.")
|
|
31
|
+
self._plot = _MotionPlotWidget()
|
|
32
|
+
layout = QVBoxLayout()
|
|
33
|
+
layout.setSpacing(4)
|
|
34
|
+
layout.addWidget(self._summary_label)
|
|
35
|
+
layout.addWidget(self._plot)
|
|
36
|
+
self.setLayout(layout)
|
|
37
|
+
|
|
38
|
+
def set_recording(self, recording: RecordingData | None) -> None:
|
|
39
|
+
"""Show movement metadata for one recording.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
recording: Loaded converted recording, or ``None`` for empty state.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
None.
|
|
46
|
+
"""
|
|
47
|
+
if recording is None:
|
|
48
|
+
self._summary_label.setText("No recording loaded.")
|
|
49
|
+
self._plot.set_motion(None, None, None)
|
|
50
|
+
return
|
|
51
|
+
self._summary_label.setText(_motion_summary_text(recording))
|
|
52
|
+
self._plot.set_motion(
|
|
53
|
+
recording.alignment_offset_pixels,
|
|
54
|
+
recording.alignment_shift_pixels,
|
|
55
|
+
recording.motion_artifact_mask,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class _MotionPlotWidget(QWidget):
|
|
60
|
+
"""Small inline plot for per-frame left-right and up-down movement."""
|
|
61
|
+
|
|
62
|
+
def __init__(self) -> None:
|
|
63
|
+
"""Create an empty plot widget sized for the metadata sidebar."""
|
|
64
|
+
super().__init__()
|
|
65
|
+
self._values = np.empty((0, 0), dtype=np.float64)
|
|
66
|
+
self._has_xy_offsets = False
|
|
67
|
+
self._motion_mask = np.array([], dtype=np.bool_)
|
|
68
|
+
self.setFixedHeight(120)
|
|
69
|
+
self.setMinimumWidth(1)
|
|
70
|
+
self.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Fixed)
|
|
71
|
+
|
|
72
|
+
def set_motion(
|
|
73
|
+
self,
|
|
74
|
+
offset_pixels: npt.NDArray[np.float64] | None,
|
|
75
|
+
shift_pixels: npt.NDArray[np.float64] | None,
|
|
76
|
+
motion_mask: npt.NDArray[np.bool_] | None,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Replace the plotted movement arrays.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
offset_pixels: Optional signed x/y movement offsets per movie frame.
|
|
82
|
+
shift_pixels: Total movement magnitude in pixels per movie frame.
|
|
83
|
+
motion_mask: Boolean mask marking high-motion frames.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
None.
|
|
87
|
+
"""
|
|
88
|
+
values: npt.NDArray[np.float64]
|
|
89
|
+
self._has_xy_offsets = offset_pixels is not None
|
|
90
|
+
if offset_pixels is not None:
|
|
91
|
+
values = np.asarray(offset_pixels, dtype=np.float64)
|
|
92
|
+
if values.ndim != 2 or values.shape[1] != 2:
|
|
93
|
+
values = np.empty((0, 0), dtype=np.float64)
|
|
94
|
+
elif shift_pixels is not None:
|
|
95
|
+
values = np.ravel(np.asarray(shift_pixels, dtype=np.float64))[:, None]
|
|
96
|
+
else:
|
|
97
|
+
self._values = np.empty((0, 0), dtype=np.float64)
|
|
98
|
+
self._motion_mask = np.array([], dtype=np.bool_)
|
|
99
|
+
self.update()
|
|
100
|
+
return
|
|
101
|
+
self._values = (
|
|
102
|
+
values
|
|
103
|
+
if bool(np.all(np.isfinite(values)))
|
|
104
|
+
else np.nan_to_num(values, nan=0.0, posinf=0.0, neginf=0.0)
|
|
105
|
+
)
|
|
106
|
+
if motion_mask is None:
|
|
107
|
+
self._motion_mask = np.zeros(self._values.shape[0], dtype=np.bool_)
|
|
108
|
+
else:
|
|
109
|
+
mask = np.ravel(np.asarray(motion_mask, dtype=np.bool_))
|
|
110
|
+
self._motion_mask = (
|
|
111
|
+
mask
|
|
112
|
+
if mask.shape == (self._values.shape[0],)
|
|
113
|
+
else np.zeros(self._values.shape[0], dtype=np.bool_)
|
|
114
|
+
)
|
|
115
|
+
self.update()
|
|
116
|
+
|
|
117
|
+
def paintEvent(self, a0: QPaintEvent | None) -> None:
|
|
118
|
+
"""Draw the movement plot.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
a0: Qt paint event.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
None.
|
|
125
|
+
"""
|
|
126
|
+
del a0
|
|
127
|
+
painter = QPainter(self)
|
|
128
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
129
|
+
painter.fillRect(self.rect(), QColor("#20252d"))
|
|
130
|
+
if self._values.size == 0:
|
|
131
|
+
painter.setPen(QColor("#a9b4c0"))
|
|
132
|
+
painter.drawText(
|
|
133
|
+
self.rect(),
|
|
134
|
+
Qt.AlignmentFlag.AlignCenter,
|
|
135
|
+
"No motion data",
|
|
136
|
+
)
|
|
137
|
+
painter.end()
|
|
138
|
+
return
|
|
139
|
+
_draw_motion_plot(
|
|
140
|
+
painter,
|
|
141
|
+
self.rect(),
|
|
142
|
+
self._values,
|
|
143
|
+
self._motion_mask,
|
|
144
|
+
has_xy_offsets=self._has_xy_offsets,
|
|
145
|
+
)
|
|
146
|
+
painter.end()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _motion_summary_text(recording: RecordingData) -> str:
|
|
150
|
+
"""Return compact crop and motion metrics for the Metadata tab."""
|
|
151
|
+
crop = recording.alignment_valid_crop
|
|
152
|
+
top = crop.axis0_start
|
|
153
|
+
bottom = crop.original_shape[0] - crop.axis0_stop
|
|
154
|
+
left = crop.axis1_start
|
|
155
|
+
right = crop.original_shape[1] - crop.axis1_stop
|
|
156
|
+
shifts = np.ravel(recording.alignment_shift_pixels)
|
|
157
|
+
finite_shifts = shifts[np.isfinite(shifts)]
|
|
158
|
+
bad_count = int(np.count_nonzero(recording.motion_artifact_mask))
|
|
159
|
+
frame_count = int(shifts.shape[0])
|
|
160
|
+
p95 = float(np.percentile(finite_shifts, 95)) if finite_shifts.size else 0.0
|
|
161
|
+
max_shift = float(np.max(finite_shifts)) if finite_shifts.size else 0.0
|
|
162
|
+
return (
|
|
163
|
+
f"Visible crop: {crop.shape[1]} x {crop.shape[0]} px "
|
|
164
|
+
f"from a {crop.original_shape[1]} x {crop.original_shape[0]} px frame\n"
|
|
165
|
+
"Removed border: "
|
|
166
|
+
f"left {left} px, right {right} px, top {top} px, bottom {bottom} px\n"
|
|
167
|
+
f"Total movement: 95% of frames moved <= {p95:.2f} px; "
|
|
168
|
+
f"max {max_shift:.2f} px; high-motion {bad_count}/{frame_count} frames\n"
|
|
169
|
+
+ _plot_description(recording)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _plot_description(recording: RecordingData) -> str:
|
|
174
|
+
"""Return the plot legend text for available movement data."""
|
|
175
|
+
if recording.alignment_offset_pixels is None:
|
|
176
|
+
return (
|
|
177
|
+
"Plot: total movement only. Reconvert this recording to see "
|
|
178
|
+
"independent x/y pixel movement."
|
|
179
|
+
)
|
|
180
|
+
return "Plot: left-right movement in blue, up-down movement in yellow"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _draw_motion_plot(
|
|
184
|
+
painter: QPainter,
|
|
185
|
+
widget_rect: QRect,
|
|
186
|
+
values: npt.NDArray[np.float64],
|
|
187
|
+
motion_mask: npt.NDArray[np.bool_],
|
|
188
|
+
*,
|
|
189
|
+
has_xy_offsets: bool,
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Draw axes, movement trace, and high-motion markers."""
|
|
192
|
+
if not has_xy_offsets:
|
|
193
|
+
_draw_magnitude_plot(painter, widget_rect, values[:, 0], motion_mask)
|
|
194
|
+
return
|
|
195
|
+
rect = widget_rect
|
|
196
|
+
plot_left = float(rect.left()) + 42.0
|
|
197
|
+
plot_top = float(rect.top()) + 10.0
|
|
198
|
+
plot_width = max(1.0, float(rect.width()) - 54.0)
|
|
199
|
+
plot_height = max(1.0, float(rect.height()) - 32.0)
|
|
200
|
+
plot_bottom = plot_top + plot_height
|
|
201
|
+
y_mid = plot_top + plot_height / 2.0
|
|
202
|
+
y_abs_max = max(1.0, float(np.max(np.abs(values))))
|
|
203
|
+
|
|
204
|
+
painter.setPen(QPen(QColor("#697584"), 1))
|
|
205
|
+
painter.drawLine(
|
|
206
|
+
int(plot_left),
|
|
207
|
+
int(plot_bottom),
|
|
208
|
+
int(plot_left + plot_width),
|
|
209
|
+
int(plot_bottom),
|
|
210
|
+
)
|
|
211
|
+
painter.drawLine(int(plot_left), int(plot_top), int(plot_left), int(plot_bottom))
|
|
212
|
+
painter.setPen(QColor("#a9b4c0"))
|
|
213
|
+
painter.drawText(2, int(plot_top + 8), f"+{y_abs_max:.1f}")
|
|
214
|
+
painter.drawText(2, int(y_mid + 4), "0")
|
|
215
|
+
painter.drawText(2, int(plot_bottom), f"-{y_abs_max:.1f}")
|
|
216
|
+
|
|
217
|
+
frame_count = int(values.shape[0])
|
|
218
|
+
if frame_count == 1:
|
|
219
|
+
x_values = np.array([plot_left], dtype=np.float64)
|
|
220
|
+
else:
|
|
221
|
+
x_values = np.linspace(plot_left, plot_left + plot_width, frame_count)
|
|
222
|
+
|
|
223
|
+
_draw_offset_line(
|
|
224
|
+
painter,
|
|
225
|
+
x_values=x_values,
|
|
226
|
+
offsets=values[:, 0],
|
|
227
|
+
y_mid=y_mid,
|
|
228
|
+
y_scale=plot_height / (2.0 * y_abs_max),
|
|
229
|
+
color=QColor("#6ec6ff"),
|
|
230
|
+
)
|
|
231
|
+
_draw_offset_line(
|
|
232
|
+
painter,
|
|
233
|
+
x_values=x_values,
|
|
234
|
+
offsets=values[:, 1],
|
|
235
|
+
y_mid=y_mid,
|
|
236
|
+
y_scale=plot_height / (2.0 * y_abs_max),
|
|
237
|
+
color=QColor("#f2c14e"),
|
|
238
|
+
)
|
|
239
|
+
painter.setPen(QColor("#6ec6ff"))
|
|
240
|
+
painter.drawText(int(plot_left + plot_width - 34.0), int(plot_top + 12.0), "x")
|
|
241
|
+
painter.setPen(QColor("#f2c14e"))
|
|
242
|
+
painter.drawText(int(plot_left + plot_width - 18.0), int(plot_top + 12.0), "y")
|
|
243
|
+
|
|
244
|
+
if motion_mask.shape == (frame_count,) and bool(np.any(motion_mask)):
|
|
245
|
+
painter.setPen(QPen(QColor("#ff6b6b"), 1))
|
|
246
|
+
for x_value in x_values[motion_mask]:
|
|
247
|
+
painter.drawLine(
|
|
248
|
+
int(x_value),
|
|
249
|
+
int(plot_top),
|
|
250
|
+
int(x_value),
|
|
251
|
+
int(plot_bottom),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _draw_magnitude_plot(
|
|
256
|
+
painter: QPainter,
|
|
257
|
+
widget_rect: QRect,
|
|
258
|
+
shift_pixels: npt.NDArray[np.float64],
|
|
259
|
+
motion_mask: npt.NDArray[np.bool_],
|
|
260
|
+
) -> None:
|
|
261
|
+
"""Draw one total movement magnitude trace."""
|
|
262
|
+
rect = widget_rect
|
|
263
|
+
plot_left = float(rect.left()) + 42.0
|
|
264
|
+
plot_top = float(rect.top()) + 10.0
|
|
265
|
+
plot_width = max(1.0, float(rect.width()) - 54.0)
|
|
266
|
+
plot_height = max(1.0, float(rect.height()) - 32.0)
|
|
267
|
+
plot_bottom = plot_top + plot_height
|
|
268
|
+
y_max = max(1.0, float(np.max(shift_pixels)))
|
|
269
|
+
|
|
270
|
+
painter.setPen(QPen(QColor("#697584"), 1))
|
|
271
|
+
painter.drawLine(
|
|
272
|
+
int(plot_left),
|
|
273
|
+
int(plot_bottom),
|
|
274
|
+
int(plot_left + plot_width),
|
|
275
|
+
int(plot_bottom),
|
|
276
|
+
)
|
|
277
|
+
painter.drawLine(int(plot_left), int(plot_top), int(plot_left), int(plot_bottom))
|
|
278
|
+
painter.setPen(QColor("#a9b4c0"))
|
|
279
|
+
painter.drawText(2, int(plot_top + 8), f"{y_max:.1f}")
|
|
280
|
+
painter.drawText(2, int(plot_bottom), "0")
|
|
281
|
+
|
|
282
|
+
frame_count = int(shift_pixels.shape[0])
|
|
283
|
+
if frame_count == 1:
|
|
284
|
+
x_values = np.array([plot_left], dtype=np.float64)
|
|
285
|
+
else:
|
|
286
|
+
x_values = np.linspace(plot_left, plot_left + plot_width, frame_count)
|
|
287
|
+
|
|
288
|
+
y_values = plot_bottom - shift_pixels * (plot_height / y_max)
|
|
289
|
+
path = QPainterPath()
|
|
290
|
+
path.moveTo(float(x_values[0]), float(y_values[0]))
|
|
291
|
+
for x_value, y_value in zip(x_values[1:], y_values[1:], strict=False):
|
|
292
|
+
path.lineTo(float(x_value), float(y_value))
|
|
293
|
+
painter.setPen(QPen(QColor("#6ec6ff"), 1.5))
|
|
294
|
+
painter.drawPath(path)
|
|
295
|
+
painter.drawText(int(plot_left + plot_width - 48.0), int(plot_top + 12.0), "total")
|
|
296
|
+
|
|
297
|
+
if motion_mask.shape == (frame_count,) and bool(np.any(motion_mask)):
|
|
298
|
+
painter.setPen(QPen(QColor("#ff6b6b"), 1))
|
|
299
|
+
for x_value in x_values[motion_mask]:
|
|
300
|
+
painter.drawLine(
|
|
301
|
+
int(x_value),
|
|
302
|
+
int(plot_top),
|
|
303
|
+
int(x_value),
|
|
304
|
+
int(plot_bottom),
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _draw_offset_line(
|
|
309
|
+
painter: QPainter,
|
|
310
|
+
*,
|
|
311
|
+
x_values: npt.NDArray[np.float64],
|
|
312
|
+
offsets: npt.NDArray[np.float64],
|
|
313
|
+
y_mid: float,
|
|
314
|
+
y_scale: float,
|
|
315
|
+
color: QColor,
|
|
316
|
+
) -> None:
|
|
317
|
+
"""Draw one signed alignment-offset line."""
|
|
318
|
+
y_values = y_mid - offsets * y_scale
|
|
319
|
+
path = QPainterPath()
|
|
320
|
+
path.moveTo(float(x_values[0]), float(y_values[0]))
|
|
321
|
+
for x_value, y_value in zip(x_values[1:], y_values[1:], strict=False):
|
|
322
|
+
path.lineTo(float(x_value), float(y_value))
|
|
323
|
+
painter.setPen(QPen(color, 1.5))
|
|
324
|
+
painter.drawPath(path)
|
|
@@ -135,6 +135,7 @@ def response_metadata_tab(
|
|
|
135
135
|
roi_output_label: SidebarTextLabel,
|
|
136
136
|
status_label: SidebarTextLabel,
|
|
137
137
|
update_notice_label: SidebarTextLabel,
|
|
138
|
+
motion_summary_widget: QWidget | None = None,
|
|
138
139
|
) -> QWidget:
|
|
139
140
|
"""Create the tab that shows selected-recording metadata.
|
|
140
141
|
|
|
@@ -146,6 +147,7 @@ def response_metadata_tab(
|
|
|
146
147
|
roi_output_label: Label showing the ROI output path.
|
|
147
148
|
status_label: Label showing the latest app action or update command.
|
|
148
149
|
update_notice_label: Label showing the copyable update command.
|
|
150
|
+
motion_summary_widget: Optional section showing alignment movement.
|
|
149
151
|
|
|
150
152
|
Returns:
|
|
151
153
|
Scrollable Qt widget for the Metadata tab.
|
|
@@ -158,6 +160,8 @@ def response_metadata_tab(
|
|
|
158
160
|
_metadata_group("Outputs", analysis_output_label, roi_output_label),
|
|
159
161
|
)
|
|
160
162
|
layout.addWidget(_metadata_group("Status", status_label, update_notice_label))
|
|
163
|
+
if motion_summary_widget is not None:
|
|
164
|
+
layout.addWidget(motion_summary_widget)
|
|
161
165
|
layout.addStretch(1)
|
|
162
166
|
return scrolling_tab(layout)
|
|
163
167
|
|
|
@@ -158,6 +158,7 @@ src/twopy/napari/plotting/export.py
|
|
|
158
158
|
src/twopy/napari/plotting/export_controls.py
|
|
159
159
|
src/twopy/napari/plotting/form_controls.py
|
|
160
160
|
src/twopy/napari/plotting/label_visibility.py
|
|
161
|
+
src/twopy/napari/plotting/motion_summary.py
|
|
161
162
|
src/twopy/napari/plotting/normalization_options.py
|
|
162
163
|
src/twopy/napari/plotting/options.py
|
|
163
164
|
src/twopy/napari/plotting/panels.py
|
|
@@ -580,6 +580,10 @@ class BackgroundSubtractionTest(unittest.TestCase):
|
|
|
580
580
|
high_res_pd=np.zeros(0, dtype=np.float64),
|
|
581
581
|
mean_image=movie_values.mean(axis=0),
|
|
582
582
|
alignment_valid_crop=alignment_crop,
|
|
583
|
+
alignment_offset_pixels=np.zeros(
|
|
584
|
+
(movie_values.shape[0], 2),
|
|
585
|
+
dtype=np.float64,
|
|
586
|
+
),
|
|
583
587
|
alignment_shift_pixels=np.zeros(movie_values.shape[0], dtype=np.float64),
|
|
584
588
|
motion_artifact_mask=np.zeros(movie_values.shape[0], dtype=np.bool_),
|
|
585
589
|
frame_counts=FrameCountAudit(
|