bioimage-cpp 0.4.0__tar.gz → 0.6.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.
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/MIGRATION_GUIDE.md +92 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/PKG-INFO +1 -1
- bioimage_cpp-0.6.0/development/distance/PERFORMANCE_NOTES.md +103 -0
- bioimage_cpp-0.6.0/development/distance/_geodesic_reference.py +155 -0
- bioimage_cpp-0.6.0/development/distance/benchmark_geodesic.py +243 -0
- bioimage_cpp-0.6.0/development/distance/check_geodesic_distance.py +224 -0
- bioimage_cpp-0.6.0/examples/distance/complex-shape.tif +0 -0
- bioimage_cpp-0.6.0/examples/distance/geodesic_2d.py +21 -0
- bioimage_cpp-0.6.0/examples/distance/geodesic_3d.py +20 -0
- bioimage_cpp-0.6.0/examples/distance/geodesic_mesh.py +36 -0
- bioimage_cpp-0.6.0/examples/distance/shape.py +10 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/detail/grid.hxx +20 -0
- bioimage_cpp-0.6.0/include/bioimage_cpp/distance/detail/fast_marching.hxx +264 -0
- bioimage_cpp-0.6.0/include/bioimage_cpp/distance/detail/mesh_fast_marching.hxx +246 -0
- bioimage_cpp-0.6.0/include/bioimage_cpp/distance/geodesic_mask.hxx +140 -0
- bioimage_cpp-0.6.0/include/bioimage_cpp/distance/geodesic_mesh.hxx +121 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/transformation/affine.hxx +37 -48
- bioimage_cpp-0.6.0/include/bioimage_cpp/transformation/coordinate.hxx +129 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/distance.cxx +281 -2
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/transformation.cxx +112 -0
- bioimage_cpp-0.6.0/src/bioimage_cpp/_version.py +1 -0
- bioimage_cpp-0.6.0/src/bioimage_cpp/distance/__init__.py +25 -0
- bioimage_cpp-0.6.0/src/bioimage_cpp/distance/_geodesic.py +310 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/transformation/__init__.py +2 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/transformation/_transformation.py +106 -0
- bioimage_cpp-0.6.0/tests/distance/test_geodesic_distance.py +438 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/test_transformation.py +121 -0
- bioimage_cpp-0.4.0/src/bioimage_cpp/_version.py +0 -1
- bioimage_cpp-0.4.0/src/bioimage_cpp/distance/__init__.py +0 -13
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/.github/workflows/docs.yml +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/.github/workflows/tests.yml +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/.github/workflows/wheels.yml +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/.gitignore +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/AGENTS.md +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/CMakeLists.txt +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/LICENSE +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/README.md +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/affinities/check_compute_affinities.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/affinities/check_compute_embedding_distances.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/distance/benchmark.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/distance/check_non_maximum_distance_suppression.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/filters/PERFORMANCE_NOTES.md +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/filters/_bench_utils.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/filters/benchmark.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/filters/check_parity.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/flow/PERFORMANCE_NOTES.md +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/flow/_reference_impl.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/flow/benchmark.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/flow/check_flow_density.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/flow/create_test_data.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/_grid_affinity_compatibility.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/_rag_compatibility.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/agglomeration/PERFORMANCE_NOTES.md +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/agglomeration/_compatibility.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/agglomeration/check_edge_weighted.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/agglomeration/check_gasp.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/agglomeration/check_mala.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/agglomeration/check_node_and_edge_weighted.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/agglomeration/diagnose.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/benchmark_accumulate_labels.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/benchmark_bfs.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/check_grid_affinity_edges.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/check_grid_affinity_lifted_edges.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/check_rag_2d.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/check_rag_3d.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/lifted_multicut/PERFORMANCE_NOTES.md +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/lifted_multicut/_compatibility.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/lifted_multicut/check_fusion_move.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/lifted_multicut/check_greedy_additive.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/lifted_multicut/check_kernighan_lin.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/lifted_multicut/evaluate_solvers.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/lifted_multicut/repro_lifted_edges_segfault.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/multicut/PERFORMANCE_NOTES.md +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/multicut/_compatibility.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/multicut/check_chained.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/multicut/check_decomposer.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/multicut/check_fusion_move.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/multicut/check_greedy_additive.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/multicut/check_greedy_fixation.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/multicut/check_kernighan_lin.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/multicut/evaluate_solvers.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/mutex_watershed/check_mutex_clustering.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/graph/mutex_watershed/check_semantic_mutex_clustering.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/label_multiset/benchmark.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/local_maxima/benchmark.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/segmentation/PERFORMANCE_NOTES.md +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/segmentation/_label_equivalence.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/segmentation/_mutex_watershed_equivalence.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/segmentation/_semantic_mws_equivalence.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/segmentation/_watershed_equivalence.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/segmentation/_watershed_from_affinities_equivalence.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/segmentation/benchmark_mutex_watershed.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/segmentation/check_label_2d.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/segmentation/check_label_3d.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/segmentation/check_mutex_watershed_2d.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/segmentation/check_mutex_watershed_3d.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/segmentation/check_relabel_sequential.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/segmentation/check_semantic_mutex_watershed_2d.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/segmentation/check_semantic_mutex_watershed_3d.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/segmentation/check_watershed_2d.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/segmentation/check_watershed_3d.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/segmentation/check_watershed_from_affinities_2d.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/segmentation/check_watershed_from_affinities_3d.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/transformation/PERFORMANCE_NOTES.md +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/transformation/check_affine.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/utils/_mesh_smoothing_reference.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/utils/benchmark_mesh_smoothing.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/development/utils/benchmark_union_find.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/examples/segmentation/_lifted_problem.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/examples/segmentation/lifted_multicut_from_affinities.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/examples/segmentation/multicut_from_affinities.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/examples/segmentation/mutex_watershed.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/examples/segmentation/serialize_grid_lifted_problem.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/examples/segmentation/serialize_lifted_problem.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/examples/segmentation/watershed.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/affinities/compute_affinities.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/affinities/compute_embedding_distances.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/array_view.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/blocking.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/detail/edge_hash.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/detail/indexed_heap.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/detail/mutex_storage.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/detail/profile.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/detail/relabel.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/detail/semantic_labels.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/detail/threading.hxx +0 -0
- {bioimage_cpp-0.4.0/include/bioimage_cpp → bioimage_cpp-0.6.0/include/bioimage_cpp/distance}/distance_transform.hxx +0 -0
- {bioimage_cpp-0.4.0/include/bioimage_cpp → bioimage_cpp-0.6.0/include/bioimage_cpp/distance}/non_maximum_distance_suppression.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/filters/convolve.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/filters/eigenvalues.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/filters/gaussian.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/filters/kernel.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/flow/flow_density.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/agglomeration/agglomerative_clustering.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/agglomeration/cluster_policy_base.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/agglomeration/detail.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/agglomeration/edge_weighted.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/agglomeration/gasp.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/agglomeration/mala.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/agglomeration/node_and_edge_weighted.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/agglomeration.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/breadth_first_search.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/connected_components.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/detail/fusion_contract.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/edge_weighted_watershed.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/feature_accumulation.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/grid_edge_projection.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/grid_features.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/grid_graph.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/label_accumulation.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/lifted_from_affinities.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/lifted_multicut/detail.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/lifted_multicut/fusion_move.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/lifted_multicut/greedy_additive.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/lifted_multicut/kernighan_lin.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/lifted_multicut/lifted_from_node_labels.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/lifted_multicut/objective.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/lifted_multicut.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/multicut/detail.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/multicut/fusion_move.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/multicut/greedy_additive.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/multicut/greedy_fixation.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/multicut/kernighan_lin.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/multicut/objective.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/multicut.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/mutex_watershed/clustering.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/mutex_watershed/semantic.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/mutex_watershed.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/node_label_projection.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/proposal_generator.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/proposal_generators/greedy_additive_multicut.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/proposal_generators/watershed.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/rag_coordinates.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/region_adjacency_graph.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/undirected_graph.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/label_multiset/downsample.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/label_multiset/from_labels.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/label_multiset/merger.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/label_multiset/multiset.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/label_multiset/read_subset.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/mesh_smoothing.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/overlap.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/run_length.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/segmentation/connected_components.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/segmentation/mutex_watershed.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/segmentation/relabel_sequential.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/segmentation/semantic_mutex_watershed.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/segmentation/watershed.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/take_dict.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/util/union_find.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/pyproject.toml +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/affinities.cxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/affinities.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/blocking.cxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/blocking.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/distance.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/filters.cxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/filters.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/flow.cxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/flow.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/graph.cxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/graph.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/ground_truth.cxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/ground_truth.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/label_multiset.cxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/label_multiset.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/module.cxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/segmentation.cxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/segmentation.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/transformation.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/util.cxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/util.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/utils.cxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bindings/utils.hxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/__init__.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/_data.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/affinities/__init__.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/affinities/compute_affinities.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/affinities/compute_embedding_distances.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/distance/_distance.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/filters/__init__.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/filters/_filters.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/flow/__init__.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/flow/_flow.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/graph/__init__.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/graph/_external.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/graph/_shared.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/graph/agglomeration.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/graph/features/__init__.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/graph/lifted_multicut/__init__.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/graph/multicut/__init__.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/graph/mutex_watershed.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/label_multiset/__init__.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/segmentation/__init__.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/segmentation/label.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/segmentation/mutex_watershed.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/segmentation/relabel_sequential.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/segmentation/watershed.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/utils.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/cpp/segmentation/mutex_watershed.cxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/cpp/segmentation/semantic_mutex_watershed.cxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/src/cpp/take_dict.cxx +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/affinities/test_compute_affinities.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/affinities/test_compute_embedding_distances.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/distance/test_distance_transform.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/distance/test_non_maximum_distance_suppression.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/agglomeration/__init__.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/agglomeration/_helpers.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/agglomeration/test_edge_weighted.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/agglomeration/test_gasp.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/agglomeration/test_mala.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/agglomeration/test_node_and_edge_weighted.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/lifted_multicut/__init__.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/lifted_multicut/_helpers.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/lifted_multicut/conftest.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/lifted_multicut/test_external_problem.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/lifted_multicut/test_fusion_move.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/lifted_multicut/test_greedy_additive.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/lifted_multicut/test_kernighan_lin.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/lifted_multicut/test_lifted_edges_from_node_labels.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/lifted_multicut/test_objective.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/multicut/__init__.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/multicut/_helpers.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/multicut/conftest.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/multicut/test_chain_decomposer.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/multicut/test_external_problem.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/multicut/test_fusion_move.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/multicut/test_greedy_additive.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/multicut/test_greedy_fixation.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/multicut/test_kernighan_lin.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/multicut/test_objective.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/multicut/test_proposal_generators.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/test_breadth_first_search.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/test_connected_components.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/test_edge_weighted_watershed.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/test_grid_affinity_multicut_integration.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/test_grid_features.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/test_grid_graph.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/test_mutex_watershed_graph.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/test_node_label_projection.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/test_rag_accumulate_labels.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/test_rag_coordinates.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/test_rag_features.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/test_rag_lifted_features.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/test_rag_multicut_integration.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/test_region_adjacency_graph.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/test_semantic_mutex_watershed_graph.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/graph/test_undirected_graph.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/label_multiset/__init__.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/label_multiset/test_against_nifty.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/label_multiset/test_downsample.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/label_multiset/test_merger.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/label_multiset/test_read_subset.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/segmentation/test_label.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/segmentation/test_mutex_watershed.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/segmentation/test_relabel_sequential.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/segmentation/test_semantic_mutex_watershed.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/segmentation/test_watershed.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/segmentation/test_watershed_from_affinities.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/test_blocking.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/test_filters.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/test_flow.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/test_ground_truth_overlap.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/test_util_union_find.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/test_utils_compute_rle.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/test_utils_mesh_smoothing.py +0 -0
- {bioimage_cpp-0.4.0 → bioimage_cpp-0.6.0}/tests/test_utils_take_dict.py +0 -0
|
@@ -2188,6 +2188,98 @@ Important differences:
|
|
|
2188
2188
|
pairwise distance matrix; threshold the distance map first to keep the
|
|
2189
2189
|
candidate count modest.
|
|
2190
2190
|
|
|
2191
|
+
## scikit-fmm
|
|
2192
|
+
|
|
2193
|
+
`scikit-fmm` computes geodesic distances on regular grids with the fast
|
|
2194
|
+
marching method. `bioimage-cpp` groups geodesic distances under `bic.distance`
|
|
2195
|
+
with two operations — a **distance field** to a set of sources, and the full
|
|
2196
|
+
**pairwise distance matrix** between a set of points — for two geometry types:
|
|
2197
|
+
regular-grid **masks** (the scikit-fmm equivalent) and triangle **meshes**
|
|
2198
|
+
(which scikit-fmm does not support; the reference for those is the exact MMP
|
|
2199
|
+
algorithm in `pygeodesic`).
|
|
2200
|
+
|
|
2201
|
+
Both are solved with a first-order fast marching method (Godunov/Eikonal on the
|
|
2202
|
+
grid, Kimmel–Sethian on triangles). Masks match scikit-fmm's `order=1` scheme;
|
|
2203
|
+
mesh distances are first-order approximations of the exact surface geodesics.
|
|
2204
|
+
|
|
2205
|
+
scikit-fmm:
|
|
2206
|
+
|
|
2207
|
+
```python
|
|
2208
|
+
import numpy as np
|
|
2209
|
+
import skfmm
|
|
2210
|
+
|
|
2211
|
+
# Distance from a set of seed voxels, constrained to a mask (domain).
|
|
2212
|
+
phi = np.ones(mask.shape)
|
|
2213
|
+
phi[tuple(sources.T)] = -1 # zero contour marks the seeds
|
|
2214
|
+
phi = np.ma.MaskedArray(phi, ~mask) # obstacles / outside-domain
|
|
2215
|
+
field = np.abs(skfmm.distance(phi, dx=sampling))
|
|
2216
|
+
# Weighted (travel-time) variant:
|
|
2217
|
+
tt = skfmm.travel_time(phi, speed, dx=sampling)
|
|
2218
|
+
```
|
|
2219
|
+
|
|
2220
|
+
bioimage-cpp:
|
|
2221
|
+
|
|
2222
|
+
```python
|
|
2223
|
+
import bioimage_cpp as bic
|
|
2224
|
+
|
|
2225
|
+
# masks (regular grid); sources/points are (n, ndim) int64 voxel coordinates
|
|
2226
|
+
field = bic.distance.geodesic_distance_field(mask, sources, sampling=None,
|
|
2227
|
+
speed=None) # -> mask.shape, float64
|
|
2228
|
+
matrix = bic.distance.geodesic_distances(mask, points) # -> (N, N) float64
|
|
2229
|
+
|
|
2230
|
+
# optional per-axis gradient of the field (like vector_difference_transform)
|
|
2231
|
+
field, grad = bic.distance.geodesic_distance_field(mask, sources,
|
|
2232
|
+
return_gradient=True)
|
|
2233
|
+
# grad: (*mask.shape, ndim) float32, d(field)/d(axis), points away from source
|
|
2234
|
+
gradient = bic.distance.geodesic_gradient_field(mask, sources) # just the gradient
|
|
2235
|
+
|
|
2236
|
+
# surfaces (triangle mesh); sources/points are 1-D int64 vertex indices
|
|
2237
|
+
field = bic.distance.geodesic_distance_field_mesh(vertices, faces, sources,
|
|
2238
|
+
speed=None) # -> (n_vertices,) float64
|
|
2239
|
+
matrix = bic.distance.geodesic_distances_mesh(vertices, faces, points) # -> (N, N) float64
|
|
2240
|
+
```
|
|
2241
|
+
|
|
2242
|
+
Name mapping:
|
|
2243
|
+
|
|
2244
|
+
| scikit-fmm / pygeodesic | bioimage-cpp name |
|
|
2245
|
+
| --- | --- |
|
|
2246
|
+
| `skfmm.distance` / `skfmm.travel_time` (from seed voxels, masked) | `geodesic_distance_field` |
|
|
2247
|
+
| gradient of the distance field (per-axis) | `geodesic_distance_field(..., return_gradient=True)` / `geodesic_gradient_field` |
|
|
2248
|
+
| pairwise via repeated `skfmm.distance` | `geodesic_distances` |
|
|
2249
|
+
| `pygeodesic … geodesicDistances(sources, None)` | `geodesic_distance_field_mesh` |
|
|
2250
|
+
| `pygeodesic … geodesicDistances` (pairwise) | `geodesic_distances_mesh` |
|
|
2251
|
+
|
|
2252
|
+
Important differences:
|
|
2253
|
+
|
|
2254
|
+
- Explicit, geometry-specific functions rather than a level-set encoding: the
|
|
2255
|
+
domain is passed directly as a `mask` (nonzero = inside) or as a
|
|
2256
|
+
`(vertices, faces)` mesh, and the sources are given as coordinates / vertex
|
|
2257
|
+
indices instead of being baked into a signed `phi`.
|
|
2258
|
+
- Outputs are `float64`. Voxels outside the mask and points/vertices
|
|
2259
|
+
unreachable from any source are `+inf`; pairwise matrices are symmetric with
|
|
2260
|
+
a zero diagonal.
|
|
2261
|
+
- `sampling` (per-axis voxel spacing, scalar or per-axis) maps to scikit-fmm's
|
|
2262
|
+
`dx` and applies to masks only — meshes carry real vertex coordinates.
|
|
2263
|
+
- `speed` is optional (`None` = unit-speed geodesic distance). When supplied it
|
|
2264
|
+
generalizes to a weighted travel time, matching `skfmm.travel_time`; for
|
|
2265
|
+
masks it has the mask's shape, for meshes it is per-vertex.
|
|
2266
|
+
- `number_of_threads` follows the `bic.distance` convention (`1` default,
|
|
2267
|
+
`0` = hardware concurrency); the pairwise solves parallelize over sources.
|
|
2268
|
+
- `geodesic_distance_field(..., return_gradient=True)` also returns the per-axis
|
|
2269
|
+
gradient `∇T` of the field (or use `geodesic_gradient_field` for the gradient
|
|
2270
|
+
alone), analogous to `vector_difference_transform`. It is `float32` with shape
|
|
2271
|
+
`(*mask.shape, ndim)` (channel-last, NumPy axis order); component `i` is
|
|
2272
|
+
`d(field)/d(axis_i)`, pointing **away** from the nearest source with
|
|
2273
|
+
`‖∇T‖ ≈ 1/speed`. Negate it to trace back toward the source — e.g. feed
|
|
2274
|
+
`-grad` (transposed to channel-first) to `bic.flow.compute_flow_density`. It
|
|
2275
|
+
is zero at sources, background, and unreachable voxels. Masks only.
|
|
2276
|
+
- Mesh geodesics are surface (2-manifold) distances from the Kimmel–Sethian
|
|
2277
|
+
triangle fast-marching method — a first-order approximation (a few % mean
|
|
2278
|
+
error vs the exact `pygeodesic` MMP algorithm, larger on very obtuse
|
|
2279
|
+
triangles, since obtuse-angle unfolding is not done yet). Like the mask
|
|
2280
|
+
solver it slightly overestimates. See `development/distance/` for the
|
|
2281
|
+
reference oracles and `benchmark_geodesic.py` for timings.
|
|
2282
|
+
|
|
2191
2283
|
## I/O and Build Dependencies
|
|
2192
2284
|
|
|
2193
2285
|
`bioimage-cpp` intentionally does not replace nifty or affogato I/O helpers.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Geodesic FMM performance notes
|
|
2
|
+
|
|
3
|
+
Optimization of the two geodesic Fast Marching Method solvers, with the
|
|
4
|
+
before/after measurements that justify the changes. Both changes are pure
|
|
5
|
+
reorganizations of identical arithmetic — the solved fields are **bitwise
|
|
6
|
+
identical** to the pre-optimization solvers on every code path tested.
|
|
7
|
+
|
|
8
|
+
## What changed
|
|
9
|
+
|
|
10
|
+
- **Lead A — mask/grid solver** (`distance/detail/fast_marching.hxx`): the FMM
|
|
11
|
+
march no longer calls `detail::valid_offset_target` (one integer division +
|
|
12
|
+
modulo *per axis, per call*, ~42 calls / popped voxel in 3D). Each popped voxel
|
|
13
|
+
is decoded **once** with the new `detail::coords_from_index` (divide-and-subtract,
|
|
14
|
+
no modulo); axis neighbours are reached with a single `p ± strides_[d]` add and an
|
|
15
|
+
integer bounds compare, and the neighbour's coordinates are handed to
|
|
16
|
+
`solve_eikonal` so it never re-decodes. The `offsets_` `vector<vector>` is gone.
|
|
17
|
+
- **Lead B — mesh solver** (`distance/detail/mesh_fast_marching.hxx`): when a
|
|
18
|
+
vertex `v` freezes, each incident face relaxes its other corners from *that face
|
|
19
|
+
alone* instead of calling `update_vertex`, which rescanned every incident face of
|
|
20
|
+
every touched vertex. `dist_[w]` is a running minimum that already reflects the
|
|
21
|
+
other faces' contributions (applied when their corners froze), so the single-face
|
|
22
|
+
update reaches the same value. `update_vertex` was removed. Operands are passed to
|
|
23
|
+
`triangle_update` in face-storage order so the acute update is bit-identical, not
|
|
24
|
+
just equal to within rounding.
|
|
25
|
+
|
|
26
|
+
## Measurement setup
|
|
27
|
+
|
|
28
|
+
8-core machine, `powersave` governor (frequency-scaling noise present; `perf`
|
|
29
|
+
unavailable, `perf_event_paranoid=4`). Mitigations: 1 warmup call + 20 timed
|
|
30
|
+
repeats, **min** used as the headline estimator (least-interference), spread
|
|
31
|
+
reported. Single-source **field** solve, `--threads 1` — the single-source solve is
|
|
32
|
+
the whole kernel (pairwise = N × field). Baseline and optimized were built
|
|
33
|
+
separately; the **`mesh/field` case is a null control for Lead A** (its code is
|
|
34
|
+
untouched between the baseline and A-only builds) and moved <1%, bounding machine
|
|
35
|
+
drift below the measured mask gains. Lead A's mask numbers also reproduced within
|
|
36
|
+
~1% across two independent builds (A-only and A+B).
|
|
37
|
+
|
|
38
|
+
Reproduce (from `development/distance/`):
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
python benchmark_geodesic.py --large --only field --threads 1 --repeats 20 --no-ref --json base.json
|
|
42
|
+
python benchmark_geodesic.py --xlarge --only field --threads 1 --repeats 20 --no-ref --json opt.json
|
|
43
|
+
python /path/to/compare.py base.json opt.json # or diff the JSON directly
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Wall-clock: baseline → optimized (single-thread field, min of 20)
|
|
47
|
+
|
|
48
|
+
| case | baseline min | optimized min | speedup |
|
|
49
|
+
|---|---|---|---|
|
|
50
|
+
| mask2d/field (1024²) | 294.0 ms | 208.9 ms | **1.41×** |
|
|
51
|
+
| mask2d/field (1536²) | 685.6 ms | 492.7 ms | **1.39×** |
|
|
52
|
+
| mask3d/field (64·128²) | 745.1 ms | 370.2 ms | **2.01×** |
|
|
53
|
+
| mask3d/field (128³) | 1811.6 ms | 912.4 ms | **1.99×** |
|
|
54
|
+
| mesh/field (V=20000) | 23.2 ms | 7.9 ms | **2.94×** |
|
|
55
|
+
| mesh/field (V=40000) | 49.1 ms | 17.5 ms | **2.81×** |
|
|
56
|
+
|
|
57
|
+
Lead A: ~2.0× in 3D (matches the div/mod arithmetic dominating 3 axes), ~1.4× in
|
|
58
|
+
2D. Lead B: ~2.8–2.9× on the mesh. Both exceed the pre-work predictions (A: 1.5–2×
|
|
59
|
+
3D; B: ~2×).
|
|
60
|
+
|
|
61
|
+
## Mechanism confirmation (BIOIMAGE_PROFILE=ON, one field solve)
|
|
62
|
+
|
|
63
|
+
Coarse phases in `solve()`: `pop` (heap pop, untouched by either change) vs `relax`
|
|
64
|
+
(the neighbour/face loop where the removed work lived).
|
|
65
|
+
|
|
66
|
+
| solve | phase | baseline | optimized | change |
|
|
67
|
+
|---|---|---|---|---|
|
|
68
|
+
| mask 128³ | pop | 0.319 s | 0.316 s | unchanged |
|
|
69
|
+
| mask 128³ | relax | 1.700 s | 0.762 s | **2.23× smaller** |
|
|
70
|
+
| mesh 40k | pop | 0.0035 s | 0.0034 s | unchanged |
|
|
71
|
+
| mesh 40k | relax | 0.0500 s | 0.0163 s | **3.07× smaller** |
|
|
72
|
+
|
|
73
|
+
The heap phase is invariant; the targeted `relax` phase shrank by 2.2× (mask) and
|
|
74
|
+
3.1× (mesh). `pop`'s *share* rose (15.8%→29.3% mask; 6.5%→17.1% mesh) only because
|
|
75
|
+
`relax` shrank around it — the gain came from exactly the phase each change targeted.
|
|
76
|
+
|
|
77
|
+
## Versus reference (single-thread field, min of 10)
|
|
78
|
+
|
|
79
|
+
| case | bioimage-cpp | reference | speedup |
|
|
80
|
+
|---|---|---|---|
|
|
81
|
+
| mask2d/field (1024²) | 210.5 ms | scikit-fmm 331.8 ms | **1.58×** |
|
|
82
|
+
| mask3d/field (64·128²) | 368.9 ms | scikit-fmm 761.4 ms | **2.06×** |
|
|
83
|
+
| mesh/field (V=20000) | 8.4 ms | pygeodesic 1745 ms | 208× (apples-to-oranges: pygeodesic is *exact* MMP, ours is first-order) |
|
|
84
|
+
|
|
85
|
+
The mask solver went from parity with scikit-fmm's compiled C (pre-work ~1.0–1.1×)
|
|
86
|
+
to a clear lead (1.58× in 2D, 2.06× in 3D).
|
|
87
|
+
|
|
88
|
+
## Correctness (all green on the shipping build)
|
|
89
|
+
|
|
90
|
+
- `pytest tests/distance/ -q` — 64 passed. The tight external cross-checks ran
|
|
91
|
+
(not skipped): `test_mask_matches_scikit_fmm` (residual < 1e-6),
|
|
92
|
+
`test_mesh_matches_pygeodesic` (mean rel < 0.06, p95 < 0.12).
|
|
93
|
+
- `python check_geodesic_distance.py` — all 6 cases OK, exit 0 (mask residuals
|
|
94
|
+
~1e-12 vs scikit-fmm; mesh rel mean 0.027).
|
|
95
|
+
- **Bitwise equivalence**: 13 solver outputs (2D/3D field, anisotropic `sampling`,
|
|
96
|
+
`speed`, gradient, mask + mesh pairwise, mesh `speed`) are exactly equal
|
|
97
|
+
(`np.array_equal`) between the baseline and optimized builds. Determinism verified
|
|
98
|
+
(repeat solves identical). This is the dev-time check behind the "bitwise
|
|
99
|
+
identical" claim; the pytest suite guards correctness against the external
|
|
100
|
+
references on every run.
|
|
101
|
+
|
|
102
|
+
The profiler instrumentation (`BIOIMAGE_PROFILE_SCOPE` "pop"/"relax") is left in both
|
|
103
|
+
solvers; it is a no-op unless built with `-C cmake.define.BIOIMAGE_PROFILE=ON`.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Reference geodesic-distance implementations for development cross-checks.
|
|
2
|
+
|
|
3
|
+
These are the oracles that ``bic.distance`` geodesic functions are validated
|
|
4
|
+
against. There is one backend per geometry, because scikit-fmm only supports
|
|
5
|
+
regular Cartesian grids:
|
|
6
|
+
|
|
7
|
+
- **masks** -> scikit-fmm (``skfmm``): fast marching / Eikonal solver.
|
|
8
|
+
- **meshes** -> pygeodesic: exact Mitchell-Mount-Papadimitriou (MMP) geodesics.
|
|
9
|
+
|
|
10
|
+
Not part of the pytest suite; requires ``scikit-fmm`` and/or ``pygeodesic``.
|
|
11
|
+
Importing this module is cheap (numpy only); each function imports its backend
|
|
12
|
+
lazily and raises a clear ``ImportError`` if the backend is missing.
|
|
13
|
+
|
|
14
|
+
Note on the scikit-fmm point-source idiom: to get the distance to a set of
|
|
15
|
+
seed voxels, we set ``phi = +1`` everywhere and ``phi = -1`` at the seeds and
|
|
16
|
+
take ``|skfmm.distance(phi)|``. The zero contour then sits roughly half a cell
|
|
17
|
+
away from each seed, so the reference is offset from a "distance is exactly 0 at
|
|
18
|
+
the seed" convention by ~0.5 * dx near the sources. Compare with a tolerance,
|
|
19
|
+
or exclude the immediate neighbourhood of the seeds, when checking against an
|
|
20
|
+
implementation that initialises the seeds to exactly 0.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# --------------------------------------------------------------------------- #
|
|
29
|
+
# masks: scikit-fmm
|
|
30
|
+
# --------------------------------------------------------------------------- #
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _skfmm():
|
|
34
|
+
try:
|
|
35
|
+
import skfmm
|
|
36
|
+
except ImportError as error: # pragma: no cover - dev script
|
|
37
|
+
raise ImportError(
|
|
38
|
+
"scikit-fmm is required for the mask geodesic reference "
|
|
39
|
+
"(`pip install scikit-fmm`)"
|
|
40
|
+
) from error
|
|
41
|
+
return skfmm
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _normalize_dx(sampling, ndim):
|
|
45
|
+
if sampling is None:
|
|
46
|
+
return 1.0
|
|
47
|
+
if np.isscalar(sampling):
|
|
48
|
+
return float(sampling)
|
|
49
|
+
dx = [float(s) for s in sampling]
|
|
50
|
+
if len(dx) != ndim:
|
|
51
|
+
raise ValueError(f"sampling must have length {ndim}, got {len(dx)}")
|
|
52
|
+
return dx
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def reference_geodesic_field_mask(mask, sources, sampling=None, speed=None):
|
|
56
|
+
"""Geodesic distance field within a mask from a set of source coordinates.
|
|
57
|
+
|
|
58
|
+
Mirrors ``bic.distance.geodesic_distance_field``: for every voxel, the
|
|
59
|
+
shortest-path distance to the nearest source, constrained to the nonzero
|
|
60
|
+
region of ``mask``. Voxels outside the mask (and voxels unreachable from any
|
|
61
|
+
source) are returned as ``+inf``.
|
|
62
|
+
"""
|
|
63
|
+
skfmm = _skfmm()
|
|
64
|
+
mask = np.ascontiguousarray(mask) != 0
|
|
65
|
+
sources = np.atleast_2d(np.asarray(sources, dtype=np.int64))
|
|
66
|
+
dx = _normalize_dx(sampling, mask.ndim)
|
|
67
|
+
|
|
68
|
+
phi = np.ones(mask.shape, dtype=np.float64)
|
|
69
|
+
phi[tuple(sources.T)] = -1.0
|
|
70
|
+
phi = np.ma.MaskedArray(phi, mask=~mask)
|
|
71
|
+
|
|
72
|
+
# order=1 matches bioimage-cpp's first-order Godunov scheme (so agreement is
|
|
73
|
+
# limited only by the ~0.5-cell seed offset documented above, not by the
|
|
74
|
+
# stencil order).
|
|
75
|
+
if speed is None:
|
|
76
|
+
field = np.ma.abs(skfmm.distance(phi, dx=dx, order=1))
|
|
77
|
+
else:
|
|
78
|
+
speed = np.ascontiguousarray(speed, dtype=np.float64)
|
|
79
|
+
field = skfmm.travel_time(phi, speed, dx=dx, order=1)
|
|
80
|
+
|
|
81
|
+
out = np.full(mask.shape, np.inf, dtype=np.float64)
|
|
82
|
+
field_data = np.ma.getdata(field)
|
|
83
|
+
valid = ~np.ma.getmaskarray(field)
|
|
84
|
+
out[valid] = field_data[valid]
|
|
85
|
+
return out
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def reference_geodesic_distances_mask(mask, points, sampling=None, speed=None):
|
|
89
|
+
"""Full pairwise geodesic distance matrix between points within a mask.
|
|
90
|
+
|
|
91
|
+
Runs the field solve once per point and reads the field at the other
|
|
92
|
+
points. The result is symmetrized (per-source solves can differ by a tiny
|
|
93
|
+
numerical amount) with a zero diagonal.
|
|
94
|
+
"""
|
|
95
|
+
points = np.atleast_2d(np.asarray(points, dtype=np.int64))
|
|
96
|
+
n = len(points)
|
|
97
|
+
out = np.full((n, n), np.inf, dtype=np.float64)
|
|
98
|
+
index = tuple(points.T)
|
|
99
|
+
for i in range(n):
|
|
100
|
+
field = reference_geodesic_field_mask(mask, points[i][None, :], sampling, speed)
|
|
101
|
+
out[i] = field[index]
|
|
102
|
+
out = 0.5 * (out + out.T)
|
|
103
|
+
np.fill_diagonal(out, 0.0)
|
|
104
|
+
return out
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# --------------------------------------------------------------------------- #
|
|
108
|
+
# meshes: pygeodesic (exact MMP)
|
|
109
|
+
# --------------------------------------------------------------------------- #
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _pygeodesic():
|
|
113
|
+
try:
|
|
114
|
+
import pygeodesic.geodesic as geodesic
|
|
115
|
+
except ImportError as error: # pragma: no cover - dev script
|
|
116
|
+
raise ImportError(
|
|
117
|
+
"pygeodesic is required for the mesh geodesic reference "
|
|
118
|
+
"(`pip install pygeodesic`)"
|
|
119
|
+
) from error
|
|
120
|
+
return geodesic
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _mesh_algorithm(vertices, faces):
|
|
124
|
+
geodesic = _pygeodesic()
|
|
125
|
+
vertices = np.ascontiguousarray(vertices, dtype=np.float64)
|
|
126
|
+
faces = np.ascontiguousarray(faces, dtype=np.int32)
|
|
127
|
+
return geodesic.PyGeodesicAlgorithmExact(vertices, faces)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def reference_geodesic_field_mesh(vertices, faces, sources):
|
|
131
|
+
"""Geodesic distance field on a triangle mesh from a set of source vertices.
|
|
132
|
+
|
|
133
|
+
Mirrors ``bic.distance.geodesic_distance_field_mesh``. Returns a
|
|
134
|
+
``(n_vertices,)`` array of surface geodesic distance to the nearest source.
|
|
135
|
+
"""
|
|
136
|
+
algo = _mesh_algorithm(vertices, faces)
|
|
137
|
+
sources = np.atleast_1d(np.asarray(sources, dtype=np.int32))
|
|
138
|
+
distances, _ = algo.geodesicDistances(sources, None)
|
|
139
|
+
return np.asarray(distances, dtype=np.float64)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def reference_geodesic_distances_mesh(vertices, faces, points):
|
|
143
|
+
"""Full pairwise geodesic distance matrix between mesh vertices."""
|
|
144
|
+
algo = _mesh_algorithm(vertices, faces)
|
|
145
|
+
points = np.atleast_1d(np.asarray(points, dtype=np.int32))
|
|
146
|
+
n = len(points)
|
|
147
|
+
out = np.full((n, n), np.inf, dtype=np.float64)
|
|
148
|
+
for i in range(n):
|
|
149
|
+
# Read the full single-source field and index the targets; passing an
|
|
150
|
+
# explicit target array to pygeodesic can overflow on some meshes.
|
|
151
|
+
distances, _ = algo.geodesicDistances(points[i : i + 1], None)
|
|
152
|
+
out[i] = np.asarray(distances, dtype=np.float64)[points]
|
|
153
|
+
out = 0.5 * (out + out.T)
|
|
154
|
+
np.fill_diagonal(out, 0.0)
|
|
155
|
+
return out
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Benchmark geodesic distances: bioimage-cpp vs scikit-fmm / pygeodesic.
|
|
2
|
+
|
|
3
|
+
Times the ``bic.distance`` geodesic entry points against their reference oracles
|
|
4
|
+
(scikit-fmm for masks, pygeodesic exact for meshes) on a range of mask grids and
|
|
5
|
+
sphere meshes, reporting per-call statistics and (optionally) the speedup versus
|
|
6
|
+
the reference. Reference columns are skipped when the backend is not installed or
|
|
7
|
+
when ``--no-ref`` is passed.
|
|
8
|
+
|
|
9
|
+
For each case it reports the min, median and spread (p75-p25) of the timed calls
|
|
10
|
+
plus ``ns/unit`` — nanoseconds per element processed (voxels for a field solve,
|
|
11
|
+
sources x voxels for a pairwise matrix), a size-independent throughput number that
|
|
12
|
+
is directly comparable across sizes and dimensionalities and is the right metric
|
|
13
|
+
for before/after optimization comparisons.
|
|
14
|
+
|
|
15
|
+
For rigorous before/after measurement use a high ``--repeats`` (>=15), pin
|
|
16
|
+
``--threads 1`` on the field cases, run on a quiet machine, and dump ``--json`` for
|
|
17
|
+
each build so the two runs can be diffed. Interleave baseline/optimized builds to
|
|
18
|
+
cancel slow thermal/scheduler drift.
|
|
19
|
+
|
|
20
|
+
Not part of the pytest suite; requires scikit-fmm, pygeodesic and scipy (the last
|
|
21
|
+
only for the sphere-mesh construction; ``--no-ref`` still needs scipy for meshes).
|
|
22
|
+
|
|
23
|
+
Run::
|
|
24
|
+
|
|
25
|
+
python benchmark_geodesic.py --small
|
|
26
|
+
python benchmark_geodesic.py --large --threads 0
|
|
27
|
+
python benchmark_geodesic.py --xlarge --only field --threads 1 --repeats 20 \\
|
|
28
|
+
--no-ref --json /tmp/baseline.json
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import argparse
|
|
34
|
+
import importlib.util
|
|
35
|
+
import json
|
|
36
|
+
import sys
|
|
37
|
+
from statistics import median
|
|
38
|
+
from time import perf_counter
|
|
39
|
+
|
|
40
|
+
import numpy as np
|
|
41
|
+
|
|
42
|
+
import bioimage_cpp as bic
|
|
43
|
+
|
|
44
|
+
from _geodesic_reference import (
|
|
45
|
+
reference_geodesic_distances_mask,
|
|
46
|
+
reference_geodesic_distances_mesh,
|
|
47
|
+
reference_geodesic_field_mask,
|
|
48
|
+
reference_geodesic_field_mesh,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def available(name: str) -> bool:
|
|
53
|
+
return importlib.util.find_spec(name) is not None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def time_call(fn, repeats: int, warmup: int = 1) -> list[float]:
|
|
57
|
+
"""Return the list of per-call wall-clock times (seconds), after warmup."""
|
|
58
|
+
for _ in range(warmup):
|
|
59
|
+
fn()
|
|
60
|
+
timings = []
|
|
61
|
+
for _ in range(repeats):
|
|
62
|
+
start = perf_counter()
|
|
63
|
+
fn()
|
|
64
|
+
timings.append(perf_counter() - start)
|
|
65
|
+
return timings
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def stats(timings: list[float]) -> dict:
|
|
69
|
+
ordered = sorted(timings)
|
|
70
|
+
n = len(ordered)
|
|
71
|
+
p25 = ordered[max(0, (n - 1) // 4)]
|
|
72
|
+
p75 = ordered[min(n - 1, (3 * (n - 1)) // 4)]
|
|
73
|
+
return {
|
|
74
|
+
"min": ordered[0],
|
|
75
|
+
"median": median(ordered),
|
|
76
|
+
"max": ordered[-1],
|
|
77
|
+
"spread": p75 - p25,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def make_sphere(n_points: int, radius: float = 5.0, seed: int = 0):
|
|
82
|
+
from scipy.spatial import ConvexHull
|
|
83
|
+
|
|
84
|
+
rng = np.random.default_rng(seed)
|
|
85
|
+
pts = rng.standard_normal((n_points, 3))
|
|
86
|
+
pts /= np.linalg.norm(pts, axis=1, keepdims=True)
|
|
87
|
+
pts *= radius
|
|
88
|
+
verts = np.ascontiguousarray(pts, dtype=np.float64)
|
|
89
|
+
faces = np.ascontiguousarray(ConvexHull(pts).simplices, dtype=np.int64)
|
|
90
|
+
return verts, faces
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def scattered_points(n_axis_points: int, ndim: int, extent: int) -> np.ndarray:
|
|
94
|
+
coords = np.linspace(1, extent - 2, num=n_axis_points, dtype=np.int64)
|
|
95
|
+
grids = np.meshgrid(*([coords] * ndim), indexing="ij")
|
|
96
|
+
return np.stack([g.ravel() for g in grids], axis=1).astype(np.int64)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def build_cases(args):
|
|
100
|
+
"""Return a list of (name, bic_fn, ref_fn, n_units) tuples for the size.
|
|
101
|
+
|
|
102
|
+
``n_units`` is the number of elements processed by one call: the voxel/vertex
|
|
103
|
+
count for a field solve, or ``n_sources * count`` for a pairwise matrix. It is
|
|
104
|
+
the denominator for the ns/unit throughput metric.
|
|
105
|
+
"""
|
|
106
|
+
if args.small:
|
|
107
|
+
mask2d_shape, mask3d_shape, mesh_n = (256, 256), (24, 48, 48), 1500
|
|
108
|
+
elif args.large:
|
|
109
|
+
mask2d_shape, mask3d_shape, mesh_n = (1024, 1024), (64, 128, 128), 20000
|
|
110
|
+
elif args.xlarge:
|
|
111
|
+
mask2d_shape, mask3d_shape, mesh_n = (1536, 1536), (128, 128, 128), 40000
|
|
112
|
+
else:
|
|
113
|
+
mask2d_shape, mask3d_shape, mesh_n = (512, 512), (32, 96, 96), 5000
|
|
114
|
+
|
|
115
|
+
def prod(shape):
|
|
116
|
+
n = 1
|
|
117
|
+
for s in shape:
|
|
118
|
+
n *= s
|
|
119
|
+
return n
|
|
120
|
+
|
|
121
|
+
cases = []
|
|
122
|
+
|
|
123
|
+
mask2d = np.ones(mask2d_shape, np.uint8)
|
|
124
|
+
src2d = np.array([[mask2d_shape[0] // 2, mask2d_shape[1] // 2]], np.int64)
|
|
125
|
+
pts2d = scattered_points(args.n_pairwise, 2, min(mask2d_shape))
|
|
126
|
+
cases.append((
|
|
127
|
+
f"mask2d/field {mask2d_shape}",
|
|
128
|
+
lambda: bic.distance.geodesic_distance_field(mask2d, src2d, number_of_threads=args.threads),
|
|
129
|
+
lambda: reference_geodesic_field_mask(mask2d, src2d),
|
|
130
|
+
prod(mask2d_shape),
|
|
131
|
+
))
|
|
132
|
+
cases.append((
|
|
133
|
+
f"mask2d/pairwise N={len(pts2d)}",
|
|
134
|
+
lambda: bic.distance.geodesic_distances(mask2d, pts2d, number_of_threads=args.threads),
|
|
135
|
+
lambda: reference_geodesic_distances_mask(mask2d, pts2d),
|
|
136
|
+
len(pts2d) * prod(mask2d_shape),
|
|
137
|
+
))
|
|
138
|
+
|
|
139
|
+
mask3d = np.ones(mask3d_shape, np.uint8)
|
|
140
|
+
src3d = np.array([[s // 2 for s in mask3d_shape]], np.int64)
|
|
141
|
+
pts3d = scattered_points(args.n_pairwise, 3, min(mask3d_shape))
|
|
142
|
+
cases.append((
|
|
143
|
+
f"mask3d/field {mask3d_shape}",
|
|
144
|
+
lambda: bic.distance.geodesic_distance_field(mask3d, src3d, number_of_threads=args.threads),
|
|
145
|
+
lambda: reference_geodesic_field_mask(mask3d, src3d),
|
|
146
|
+
prod(mask3d_shape),
|
|
147
|
+
))
|
|
148
|
+
cases.append((
|
|
149
|
+
f"mask3d/pairwise N={len(pts3d)}",
|
|
150
|
+
lambda: bic.distance.geodesic_distances(mask3d, pts3d, number_of_threads=args.threads),
|
|
151
|
+
lambda: reference_geodesic_distances_mask(mask3d, pts3d),
|
|
152
|
+
len(pts3d) * prod(mask3d_shape),
|
|
153
|
+
))
|
|
154
|
+
|
|
155
|
+
verts, faces = make_sphere(mesh_n, seed=args.seed)
|
|
156
|
+
src_v = np.array([0], np.int64)
|
|
157
|
+
pts_v = np.linspace(0, len(verts) - 1, num=args.n_pairwise, dtype=np.int64)
|
|
158
|
+
cases.append((
|
|
159
|
+
f"mesh/field V={len(verts)}",
|
|
160
|
+
lambda: bic.distance.geodesic_distance_field_mesh(verts, faces, src_v, number_of_threads=args.threads),
|
|
161
|
+
lambda: reference_geodesic_field_mesh(verts, faces, src_v),
|
|
162
|
+
len(verts),
|
|
163
|
+
))
|
|
164
|
+
cases.append((
|
|
165
|
+
f"mesh/pairwise N={len(pts_v)}",
|
|
166
|
+
lambda: bic.distance.geodesic_distances_mesh(verts, faces, pts_v, number_of_threads=args.threads),
|
|
167
|
+
lambda: reference_geodesic_distances_mesh(verts, faces, pts_v),
|
|
168
|
+
len(pts_v) * len(verts),
|
|
169
|
+
))
|
|
170
|
+
|
|
171
|
+
if args.only:
|
|
172
|
+
cases = [c for c in cases if args.only in c[0]]
|
|
173
|
+
return cases
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def main():
|
|
177
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
178
|
+
parser.add_argument("--repeats", type=int, default=7)
|
|
179
|
+
parser.add_argument("--small", action="store_true")
|
|
180
|
+
parser.add_argument("--large", action="store_true")
|
|
181
|
+
parser.add_argument("--xlarge", action="store_true")
|
|
182
|
+
parser.add_argument("--threads", type=int, default=1, help="0 = hardware concurrency")
|
|
183
|
+
parser.add_argument("--n-pairwise", type=int, default=8, dest="n_pairwise",
|
|
184
|
+
help="points per axis (mask) / total points (mesh) for pairwise")
|
|
185
|
+
parser.add_argument("--seed", type=int, default=0)
|
|
186
|
+
parser.add_argument("--no-ref", action="store_true", help="skip reference timings")
|
|
187
|
+
parser.add_argument("--only", type=str, default="",
|
|
188
|
+
help="substring filter on case name, e.g. 'field' or 'mask3d'")
|
|
189
|
+
parser.add_argument("--json", type=str, default="", help="dump results to this JSON path")
|
|
190
|
+
args = parser.parse_args()
|
|
191
|
+
n_sizes = sum(bool(x) for x in (args.small, args.large, args.xlarge))
|
|
192
|
+
if n_sizes > 1:
|
|
193
|
+
print("choose at most one of --small / --large / --xlarge", file=sys.stderr)
|
|
194
|
+
return 2
|
|
195
|
+
|
|
196
|
+
have_mask_ref = available("skfmm") and not args.no_ref
|
|
197
|
+
have_mesh_ref = available("pygeodesic") and not args.no_ref
|
|
198
|
+
if not args.no_ref:
|
|
199
|
+
if not available("skfmm"):
|
|
200
|
+
print("scikit-fmm not installed: mask reference column skipped", file=sys.stderr)
|
|
201
|
+
if not available("pygeodesic"):
|
|
202
|
+
print("pygeodesic not installed: mesh reference column skipped", file=sys.stderr)
|
|
203
|
+
|
|
204
|
+
cases = build_cases(args)
|
|
205
|
+
|
|
206
|
+
header = (f"{'case':>26} {'bic_min':>10} {'bic_med':>10} {'spread':>9} "
|
|
207
|
+
f"{'ns/unit':>9} {'ref_med':>10} {'speedup':>9}")
|
|
208
|
+
print(f"threads={args.threads} repeats={args.repeats}")
|
|
209
|
+
print(header)
|
|
210
|
+
print("-" * len(header))
|
|
211
|
+
results = []
|
|
212
|
+
for name, bic_fn, ref_fn, n_units in cases:
|
|
213
|
+
bic_t = time_call(bic_fn, args.repeats)
|
|
214
|
+
bs = stats(bic_t)
|
|
215
|
+
ns_per_unit = bs["median"] / n_units * 1e9
|
|
216
|
+
is_mesh = name.startswith("mesh")
|
|
217
|
+
has_ref = have_mesh_ref if is_mesh else have_mask_ref
|
|
218
|
+
entry = {"case": name, "n_units": n_units, "threads": args.threads,
|
|
219
|
+
"bic_s": bs, "ns_per_unit": ns_per_unit}
|
|
220
|
+
if has_ref:
|
|
221
|
+
ref_t = time_call(ref_fn, args.repeats)
|
|
222
|
+
rs = stats(ref_t)
|
|
223
|
+
speed = rs["median"] / bs["median"]
|
|
224
|
+
entry["ref_s"] = rs
|
|
225
|
+
entry["speedup"] = speed
|
|
226
|
+
ref_col, speed_col = f"{rs['median']*1e3:10.2f}", f"{speed:6.2f}x"
|
|
227
|
+
else:
|
|
228
|
+
ref_col, speed_col = f"{'n/a':>10}", f"{'n/a':>9}"
|
|
229
|
+
print(f"{name:>26} {bs['min']*1e3:10.2f} {bs['median']*1e3:10.2f} "
|
|
230
|
+
f"{bs['spread']*1e3:9.2f} {ns_per_unit:9.1f} {ref_col} {speed_col:>9}")
|
|
231
|
+
results.append(entry)
|
|
232
|
+
|
|
233
|
+
if args.json:
|
|
234
|
+
with open(args.json, "w") as fh:
|
|
235
|
+
json.dump({"threads": args.threads, "repeats": args.repeats,
|
|
236
|
+
"results": results}, fh, indent=2)
|
|
237
|
+
print(f"wrote {args.json}", file=sys.stderr)
|
|
238
|
+
|
|
239
|
+
return 0
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
if __name__ == "__main__":
|
|
243
|
+
sys.exit(main())
|