OpenPIV 0.25.4__tar.gz → 0.26.0__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.
- {openpiv-0.25.4 → openpiv-0.26.0}/PKG-INFO +3 -1
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/conf.py +1 -1
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/index.rst +1 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/developers.rst +61 -0
- openpiv-0.26.0/openpiv/docs/src/fft_correlation_backends.rst +162 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/lib.py +26 -42
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/pyprocess.py +129 -59
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/settings.py +22 -4
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_lib.py +28 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_performance.py +4 -4
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_process.py +2 -2
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_pyprocess.py +62 -14
- openpiv-0.26.0/openpiv/test/test_rust_backend.py +174 -0
- openpiv-0.26.0/openpiv/test/test_rust_subpixel.py +101 -0
- openpiv-0.26.0/openpiv/test/test_rust_windef.py +121 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_validation.py +26 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/tools.py +2 -2
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/validation.py +34 -17
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/windef.py +241 -204
- {openpiv-0.25.4 → openpiv-0.26.0}/pyproject.toml +7 -2
- {openpiv-0.25.4 → openpiv-0.26.0}/LICENSE.txt +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/README.md +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/PIV_3D_plotting.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/__init__.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test1/exp1_001_a.bmp +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test1/exp1_001_b.bmp +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test1/test_data.vec +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_00.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_01.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_10.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_11.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_20.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_21.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_30.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_31.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_40.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_41.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_50.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test2/2image_51.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test3/pair_4_frame_0.jpg +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test3/pair_4_frame_1.jpg +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test4/Camera1-0101.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test4/Camera1-0102.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_10_A.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_10_B.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_1_A.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_1_B.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_2_A.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_2_B.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_3_A.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_3_B.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_4_A.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_4_B.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_5_A.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_5_B.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_6_A.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_6_B.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_7_A.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_7_B.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_8_A.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_8_B.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_9_A.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test5/Pattern_9_B.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test6/Pattern_0001_A.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test6/Pattern_0002_A.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test6/Pattern_0003_A.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test6/Pattern_0004_A.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test6/Pattern_0005_A.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test6/Pattern_1001_B.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test6/Pattern_1002_B.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test6/Pattern_1003_B.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test6/Pattern_1004_B.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/data/test6/Pattern_1005_B.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/Makefile +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/OpenPIV.pdf +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/OpenPIV_advanced_processing_parameters.pdf +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/images/B005_1.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/images/B005_2.tif +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/images/image1.png +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/images/image2.png +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/images/image3.png +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/images/image4.png +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/make.bat +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/requirements.txt +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/api_reference.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/faq.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.filters._gaussian_kernel.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.filters.gaussian.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.filters.replace_outliers.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.lib.replace_nans.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.lib.sincinterp.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.preprocess.dynamic_masking.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.process.CorrelationFunction.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.process.correlate_windows.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.process.extended_search_area_piv.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.process.get_coordinates.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.process.get_field_shape.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.process.normalize_intensity.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.pyprocess.correlate_windows.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.pyprocess.find_first_peak.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.pyprocess.find_second_peak.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.pyprocess.find_subpixel_peak_position.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.pyprocess.get_coordinates.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.pyprocess.get_field_shape.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.pyprocess.moving_window_array.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.pyprocess.normalize_intensity.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.pyprocess.piv.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.scaling.uniform.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.tools.Multiprocesser.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.tools.display.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.tools.display_vector_field.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.tools.imread.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.tools.save.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.validation.global_std.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.validation.global_val.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.validation.local_median_val.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/generated/openpiv.validation.sig2noise_val.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/gui_doc.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/installation_instruction.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/introduction.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/masking.ipynb +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/modules.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/openpiv.rst +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/openpiv_pivuq.ipynb +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/piv_basics.ipynb +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/tutorial1.ipynb +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/docs/src/windef.ipynb +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/filters.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/phase_separation.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/piv.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/preprocess.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/preprocess.py,cover +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/pyprocess3D.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/scaling.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/smoothn.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/OpenPIV_results_16_test1/field_A0000.png +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/__init__.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/conftest.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/extended_search_area_vectorized.ipynb +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/moon.png +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_PIV_3D_plotting.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_filters.ipynb +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_filters.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_find_peaks.ipynb +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_package_metadata.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_piv.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_preprocess.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_process.ipynb +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_pyprocess3D.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_scaling.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_smoothn.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_tools.ipynb +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_tools.png +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_tools.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_tools_background.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_tools_basic_utils.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_tools_image_processing.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_tools_multiprocessing.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_tools_vector_field.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_validation.ipynb +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_vectorized_extended_search.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_windef.ipynb +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_windef.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_windef_coverage.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_windef_detailed.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/test/test_windef_final.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/tutorials/Example for overlap setting change.ipynb +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/tutorials/masking_tutorial.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/tutorials/tutorial1.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/tutorials/tutorial2.py +0 -0
- {openpiv-0.25.4 → openpiv-0.26.0}/openpiv/tutorials/windef_tutorial.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: OpenPIV
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.26.0
|
|
4
4
|
Summary: OpenPIV consists in a Python and Cython modules for scripting and executing the analysis of a set of PIV image pairs. In addition, a Qt and Tk graphical user interfaces are in development, to ease the use for those users who don't have python skills.
|
|
5
5
|
License: GPLv3
|
|
6
6
|
License-File: LICENSE.txt
|
|
@@ -22,10 +22,12 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.14
|
|
24
24
|
Classifier: Topic :: Scientific/Engineering
|
|
25
|
+
Provides-Extra: rust
|
|
25
26
|
Requires-Dist: imageio (>=2.35.0)
|
|
26
27
|
Requires-Dist: matplotlib (>=3.8.0)
|
|
27
28
|
Requires-Dist: natsort (>=8.4.0)
|
|
28
29
|
Requires-Dist: numpy (>=2.0.0)
|
|
30
|
+
Requires-Dist: openpiv-rust (>=0.1.0) ; extra == "rust"
|
|
29
31
|
Requires-Dist: scikit-image (>=0.23.0)
|
|
30
32
|
Requires-Dist: scipy (>=1.11.0)
|
|
31
33
|
Requires-Dist: tqdm (>=4.66.0)
|
|
@@ -63,3 +63,64 @@ If you need to install cv2::
|
|
|
63
63
|
conda install -c conda-forge opencv
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
Developing with or without Rust
|
|
67
|
+
--------------------------------
|
|
68
|
+
|
|
69
|
+
OpenPIV supports dual computational backends: **pure Python / SciPy** (default) and **compiled Rust** (via ``openpiv_rust``).
|
|
70
|
+
|
|
71
|
+
Option A: Developing in Pure Python (without Rust)
|
|
72
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
73
|
+
|
|
74
|
+
You do not need a Rust compiler installed. Simply install OpenPIV in editable mode:
|
|
75
|
+
|
|
76
|
+
.. code-block:: bash
|
|
77
|
+
|
|
78
|
+
poetry install
|
|
79
|
+
# or: pip install -e .
|
|
80
|
+
|
|
81
|
+
All core functionality, single-pass, and multi-pass deformation will use the optimized
|
|
82
|
+
``scipy.fft`` backend automatically.
|
|
83
|
+
|
|
84
|
+
To run the test suite:
|
|
85
|
+
|
|
86
|
+
.. code-block:: bash
|
|
87
|
+
|
|
88
|
+
pytest openpiv/test
|
|
89
|
+
|
|
90
|
+
Option B: Developing with the Rust Extension
|
|
91
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
92
|
+
|
|
93
|
+
If you have the Rust toolchain (``cargo``) installed:
|
|
94
|
+
|
|
95
|
+
1. Install ``maturin``:
|
|
96
|
+
|
|
97
|
+
.. code-block:: bash
|
|
98
|
+
|
|
99
|
+
pip install maturin
|
|
100
|
+
|
|
101
|
+
2. Compile and link ``openpiv_rust`` directly into your Python environment:
|
|
102
|
+
|
|
103
|
+
.. code-block:: bash
|
|
104
|
+
|
|
105
|
+
cd crates/openpiv_rust
|
|
106
|
+
maturin develop --release
|
|
107
|
+
cd ../..
|
|
108
|
+
|
|
109
|
+
3. Verify that the Rust backend is active:
|
|
110
|
+
|
|
111
|
+
.. code-block:: bash
|
|
112
|
+
|
|
113
|
+
python -c "import openpiv_rust; print('Rust backend ready!')"
|
|
114
|
+
|
|
115
|
+
Running the Performance Benchmarks
|
|
116
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
117
|
+
|
|
118
|
+
OpenPIV includes a dedicated benchmark suite comparing cross-correlation, subpixel
|
|
119
|
+
peak finding, and end-to-end multi-pass Windef across backends:
|
|
120
|
+
|
|
121
|
+
.. code-block:: bash
|
|
122
|
+
|
|
123
|
+
python benchmarks/run_benchmarks.py
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
FFT Correlation Backends & Performance Study
|
|
2
|
+
============================================
|
|
3
|
+
|
|
4
|
+
This document details the comparative study of 2D cross-correlation algorithms,
|
|
5
|
+
backends, and performance characteristics in OpenPIV, focusing on both **Circular**
|
|
6
|
+
and **Linear** correlation, `scipy.fft` (PocketFFT), and the `openpiv_rust` compiled backend.
|
|
7
|
+
|
|
8
|
+
.. contents:: Table of Contents
|
|
9
|
+
:depth: 2
|
|
10
|
+
:local:
|
|
11
|
+
|
|
12
|
+
Overview
|
|
13
|
+
--------
|
|
14
|
+
|
|
15
|
+
In Particle Image Velocimetry (PIV), cross-correlation between interrogation windows
|
|
16
|
+
is the core computational step. For an image pair split into :math:`N` interrogation
|
|
17
|
+
windows of size :math:`(H \times W)`, the correlation maps are computed using the
|
|
18
|
+
Wiener-Khinchin theorem via Fast Fourier Transforms:
|
|
19
|
+
|
|
20
|
+
.. math::
|
|
21
|
+
|
|
22
|
+
C = \mathcal{F}^{-1} \left( \mathcal{F}(I_B) \cdot \mathcal{F}^*(I_A) \right)
|
|
23
|
+
|
|
24
|
+
OpenPIV supports two primary correlation paradigms:
|
|
25
|
+
|
|
26
|
+
1. **Circular Correlation (Standard OpenPIV)**:
|
|
27
|
+
Assumes periodic boundary conditions (toroidal wraparound).
|
|
28
|
+
Inputs of size :math:`(N \times N)` yield correlation maps of size :math:`(N \times N)`.
|
|
29
|
+
No zero-padding is required.
|
|
30
|
+
|
|
31
|
+
2. **Linear Correlation (Full / Extended)**:
|
|
32
|
+
Evaluates true spatial shift without periodic wraparound.
|
|
33
|
+
For windows of size :math:`s_1` and :math:`s_2`, the full linear correlation has size
|
|
34
|
+
:math:`(s_1 + s_2 - 1)`. To prevent time-domain aliasing, inputs must be zero-padded
|
|
35
|
+
to at least this size before applying the FFT.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
The 63x63 vs 64x64 Power-of-2 Finding
|
|
39
|
+
-------------------------------------
|
|
40
|
+
|
|
41
|
+
In previous versions of OpenPIV, linear correlation in ``openpiv.pyprocess.fft_correlate_images``
|
|
42
|
+
computed the padded transform size as:
|
|
43
|
+
|
|
44
|
+
.. code-block:: python
|
|
45
|
+
|
|
46
|
+
size = s1 + s2 - 1 # e.g., 32 + 32 - 1 = 63
|
|
47
|
+
fsize = 2 ** np.ceil(np.log2(size)).astype(int) - 1 # 64 - 1 = 63 (BUG!)
|
|
48
|
+
|
|
49
|
+
Why 63 Disabled PocketFFT Optimization
|
|
50
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
51
|
+
|
|
52
|
+
SciPy delegates FFT operations to **PocketFFT** (written in C++ by Martin Reinecke),
|
|
53
|
+
which includes hand-tuned AVX2/FMA vector kernels. However, these SIMD routines rely on
|
|
54
|
+
smooth radix factors (powers of 2: 2, 4, 8, 16, 32, 64).
|
|
55
|
+
|
|
56
|
+
1. Subtracting 1 produced an **odd composite size** :math:`63 = 3 \times 3 \times 7`.
|
|
57
|
+
2. This forced PocketFFT into slow Bluestein/composite transforms, disabling SIMD vectorization.
|
|
58
|
+
3. Furthermore, padding to 63 left an off-by-one boundary alignment when slicing the central window.
|
|
59
|
+
|
|
60
|
+
The Power-of-2 Solution
|
|
61
|
+
^^^^^^^^^^^^^^^^^^^^^^^
|
|
62
|
+
|
|
63
|
+
By keeping the clean power-of-2 transform size :math:`fsize = 2^{\lceil \log_2(size) \rceil}`
|
|
64
|
+
(e.g., :math:`64 \times 64` for :math:`32 \times 32` windows), PocketFFT achieves optimal
|
|
65
|
+
SIMD execution speed.
|
|
66
|
+
|
|
67
|
+
To recover the exact central correlation window matching ``scipy.signal.correlate``
|
|
68
|
+
to machine precision (:math:`\le 10^{-13}`), the centered slice is:
|
|
69
|
+
|
|
70
|
+
.. code-block:: python
|
|
71
|
+
|
|
72
|
+
# Exact power of 2 transform size
|
|
73
|
+
fsize = 2 ** np.ceil(np.log2(size)).astype(int)
|
|
74
|
+
|
|
75
|
+
# Centered slice extracting (s1) around the zero-displacement lag
|
|
76
|
+
fslice = (
|
|
77
|
+
slice(0, image_a.shape[0]),
|
|
78
|
+
slice(fsize[0] // 2 - s1[0] // 2, fsize[0] // 2 - s1[0] // 2 + s1[0]),
|
|
79
|
+
slice(fsize[1] // 2 - s1[1] // 2, fsize[1] // 2 - s1[1] // 2 + s1[1]),
|
|
80
|
+
)
|
|
81
|
+
f2a = conj(rfft2(image_a, fsize, axes=(-2, -1), workers=workers))
|
|
82
|
+
f2b = rfft2(image_b, fsize, axes=(-2, -1), workers=workers)
|
|
83
|
+
corr = fftshift(irfft2(f2a * f2b, axes=(-2, -1)).real, axes=(-2, -1))[fslice]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
Batched Multi-Threading in scipy.fft
|
|
87
|
+
------------------------------------
|
|
88
|
+
|
|
89
|
+
``scipy.fft`` supports a ``workers`` parameter (e.g., ``workers=-1`` for all logical cores).
|
|
90
|
+
However, profiling reveals:
|
|
91
|
+
|
|
92
|
+
* On large 2D or 3D volumes, PocketFFT multi-threading scales well.
|
|
93
|
+
* On **batches of tiny 2D windows** (:math:`16 \times 16` or :math:`32 \times 32`),
|
|
94
|
+
thread synchronization overhead inside PocketFFT often negates multi-core gains
|
|
95
|
+
(scaling from :math:`1.0\times` to :math:`1.1\times`), because thread splitting occurs
|
|
96
|
+
per-transform rather than distributing independent windows across workers.
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
The Rust Acceleration Engine (openpiv_rust)
|
|
100
|
+
-------------------------------------------
|
|
101
|
+
|
|
102
|
+
To overcome Python GIL overhead and achieve linear CPU scaling across window batches,
|
|
103
|
+
OpenPIV provides an optional compiled Rust extension (``openpiv_rust``) built with
|
|
104
|
+
PyO3, Rayon, and RealFFT.
|
|
105
|
+
|
|
106
|
+
Key Architectural Optimizations in Rust
|
|
107
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
108
|
+
|
|
109
|
+
1. **Zero-Allocation Rayon Scratch Pool**:
|
|
110
|
+
Window transforms use ``par_chunks_exact_mut().for_each_init(...)``, pre-allocating
|
|
111
|
+
frequency buffers once per worker thread. For 961 windows, this eliminates over
|
|
112
|
+
8,600 heap allocations per pass.
|
|
113
|
+
|
|
114
|
+
2. **Native Power-of-2 Real FFT**:
|
|
115
|
+
Uses ``realfft`` (Real-to-Complex forward and Complex-to-Real inverse), cutting
|
|
116
|
+
arithmetic and memory bandwidth by 50% compared to full complex transforms.
|
|
117
|
+
|
|
118
|
+
3. **Batched Subpixel Peak Finding**:
|
|
119
|
+
In addition to cross-correlation, `openpiv_rust` provides
|
|
120
|
+
``batch_correlation_to_displacement``, executing Gaussian/Centroid/Parabolic subpixel
|
|
121
|
+
peak fitting directly across all windows in parallel, yielding a **28x to 65x speedup**
|
|
122
|
+
over the nested Python loop.
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
Benchmark Results
|
|
126
|
+
-----------------
|
|
127
|
+
|
|
128
|
+
Linear Correlation (225 windows of 32x32)
|
|
129
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
130
|
+
|
|
131
|
+
============================================ ==================== ==================
|
|
132
|
+
Implementation Runtime Relative Speed
|
|
133
|
+
============================================ ==================== ==================
|
|
134
|
+
``scipy.signal.correlate`` (Python loop) 70.63 ms 1.0x (baseline)
|
|
135
|
+
OpenPIV SciPy (legacy ``fsize=63``) 49.28 ms 1.4x
|
|
136
|
+
OpenPIV SciPy (power-of-2 ``fsize=64``) 39.10 ms 1.8x
|
|
137
|
+
**openpiv_rust** (Rayon + realfft) **8.26 ms** **8.5x**
|
|
138
|
+
============================================ ==================== ==================
|
|
139
|
+
|
|
140
|
+
Displacement Calculation (Peak Finding)
|
|
141
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
142
|
+
|
|
143
|
+
============================================ ==================== ==================
|
|
144
|
+
Grid / Windows Python Loop openpiv_rust
|
|
145
|
+
============================================ ==================== ==================
|
|
146
|
+
225 windows (32x32) 5.19 ms **0.18 ms (28x)**
|
|
147
|
+
961 windows (16x16) 19.84 ms **0.30 ms (65x)**
|
|
148
|
+
============================================ ==================== ==================
|
|
149
|
+
|
|
150
|
+
End-to-End Multi-Pass Windef (3 passes on 256x256 image)
|
|
151
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
152
|
+
|
|
153
|
+
* **SciPy backend**: 110.59 ms
|
|
154
|
+
* **Rust backend (with subpixel Rust engine)**: **81.66 ms** (:math:`1.35\times` speedup)
|
|
155
|
+
|
|
156
|
+
Conclusion & Usage Guidelines
|
|
157
|
+
-----------------------------
|
|
158
|
+
|
|
159
|
+
* For systems with compiled binary extensions available, set ``settings.backend = 'rust'``
|
|
160
|
+
to maximize throughput in both single-pass and multi-pass deformation workflows.
|
|
161
|
+
* On pure Python / NumPy / SciPy environments, using the exact power-of-2 padding rule
|
|
162
|
+
ensures optimal PocketFFT vectorization while maintaining exact numerical consistency.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
+
from numpy.lib.stride_tricks import sliding_window_view
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
def replace_nans(array, max_iter, tol, kernel_size=2, method="disk"):
|
|
@@ -92,56 +93,39 @@ def replace_nans(array, max_iter, tol, kernel_size=2, method="disk"):
|
|
|
92
93
|
n_nans = len(nan_indices)
|
|
93
94
|
|
|
94
95
|
# arrays which contain replaced values to check for convergence
|
|
95
|
-
replaced_new = np.zeros(n_nans)
|
|
96
96
|
replaced_old = np.zeros(n_nans)
|
|
97
97
|
|
|
98
|
+
# broadcastable view of the kernel weights, one axis per array dimension
|
|
99
|
+
# plus one matching axis per kernel dimension (for the windowed reduction)
|
|
100
|
+
kernel_b = kernel.reshape((1,) * n_dim + kernel.shape)
|
|
101
|
+
window_axes = tuple(range(n_dim, 2 * n_dim))
|
|
102
|
+
|
|
98
103
|
# make several passes
|
|
99
104
|
# until we reach convergence
|
|
100
105
|
for _ in range(max_iter):
|
|
101
106
|
# note: identifying new nan indices and looping other the new indices
|
|
102
107
|
# would give slightly different result
|
|
103
108
|
|
|
104
|
-
#
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
for i in range(n_dim)
|
|
125
|
-
]
|
|
126
|
-
)
|
|
127
|
-
# logical and over x,y (and z) indices
|
|
128
|
-
in_mask = np.prod(in_mask, axis=0).astype(bool)
|
|
129
|
-
|
|
130
|
-
# extract window from array
|
|
131
|
-
win = filled[tuple(slice_indices[:, in_mask])]
|
|
132
|
-
|
|
133
|
-
# selecting the same points from the kernel
|
|
134
|
-
kernel_in = kernel[in_mask]
|
|
135
|
-
|
|
136
|
-
# sum of elements of the kernel that are not nan in the window
|
|
137
|
-
non_nan = np.sum(kernel_in[~np.isnan(win)])
|
|
138
|
-
|
|
139
|
-
if non_nan > 0:
|
|
140
|
-
# convolution with the kernel
|
|
141
|
-
replaced_new[k] = np.nansum(win * kernel_in) / non_nan
|
|
142
|
-
else:
|
|
143
|
-
# don't do anything if there is only nans around
|
|
144
|
-
replaced_new[k] = np.nan
|
|
109
|
+
# NaN-pad by kernel_size so that out-of-bounds neighbours behave like
|
|
110
|
+
# the boundary check in the original per-element loop (i.e. they are
|
|
111
|
+
# excluded from both the weighted sum and its normalization).
|
|
112
|
+
padded = np.pad(np.asarray(filled), kernel_size, mode="constant",
|
|
113
|
+
constant_values=np.nan)
|
|
114
|
+
# windows.shape == array.shape + kernel.shape: one (2*kernel_size+1)^n_dim
|
|
115
|
+
# neighbourhood per array element, vectorized instead of a per-NaN
|
|
116
|
+
# Python loop with a fresh np.meshgrid call each time.
|
|
117
|
+
windows = sliding_window_view(padded, kernel.shape)
|
|
118
|
+
|
|
119
|
+
valid = ~np.isnan(windows)
|
|
120
|
+
weights = np.where(valid, kernel_b, 0)
|
|
121
|
+
non_nan = weights.sum(axis=window_axes)
|
|
122
|
+
weighted_sum = (np.where(valid, windows, 0) * kernel_b).sum(axis=window_axes)
|
|
123
|
+
|
|
124
|
+
# convolution with the kernel; stays NaN where only NaNs are around
|
|
125
|
+
full_new = np.divide(weighted_sum, non_nan,
|
|
126
|
+
out=np.full(array.shape, np.nan), where=non_nan > 0)
|
|
127
|
+
|
|
128
|
+
replaced_new = full_new[tuple(nan_indices.T)]
|
|
145
129
|
|
|
146
130
|
# bulk replace all new values in array
|
|
147
131
|
filled[tuple(nan_indices.T)] = replaced_new
|