ocdkit 0.0.3__tar.gz → 0.0.4__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.
- {ocdkit-0.0.3/src/ocdkit.egg-info → ocdkit-0.0.4}/PKG-INFO +2 -2
- ocdkit-0.0.4/badges/coverage.svg +1 -0
- ocdkit-0.0.4/badges/tests.svg +1 -0
- ocdkit-0.0.4/docs/plot-backend-roadmap.md +96 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/pyproject.toml +5 -1
- ocdkit-0.0.4/scripts/bench_contour_alignment.py +82 -0
- ocdkit-0.0.4/scripts/bench_vector_contours.py +311 -0
- ocdkit-0.0.4/scripts/bench_vector_contours_tier2.py +746 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/ops.py +25 -3
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/logging/handler.py +43 -2
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/measure/bbox.py +57 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/measure/medoid.py +1 -58
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/plot/defaults.py +17 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/plot/display.py +75 -0
- ocdkit-0.0.4/src/ocdkit/testing/__init__.py +55 -0
- ocdkit-0.0.4/src/ocdkit/testing/imports.py +237 -0
- ocdkit-0.0.4/src/ocdkit/tls/__init__.py +142 -0
- ocdkit-0.0.4/src/ocdkit/tls/external_ca.py +104 -0
- ocdkit-0.0.4/src/ocdkit/tls/hostnames.py +110 -0
- ocdkit-0.0.4/src/ocdkit/tls/imports.py +11 -0
- ocdkit-0.0.4/src/ocdkit/tls/local_ca.py +288 -0
- ocdkit-0.0.4/src/ocdkit/tls/paths.py +26 -0
- ocdkit-0.0.4/src/ocdkit/tls/trust.py +205 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/utils/gpu.py +29 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/utils/kwargs.py +16 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4/src/ocdkit.egg-info}/PKG-INFO +2 -2
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit.egg-info/SOURCES.txt +16 -1
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit.egg-info/requires.txt +1 -1
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_array.py +35 -0
- ocdkit-0.0.4/tests/test_import_cycles.py +10 -0
- ocdkit-0.0.4/tests/test_module_discovery.py +11 -0
- ocdkit-0.0.3/src/ocdkit/testing/__init__.py +0 -27
- ocdkit-0.0.3/src/ocdkit/tls.py +0 -792
- {ocdkit-0.0.3 → ocdkit-0.0.4}/.github/workflows/test_and_deploy.yml +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/.gitignore +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/LICENSE +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/MANIFEST.in +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/README.md +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/docs/plugin-authoring.md +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/docs/pywebview-desktop-integration.md +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/scripts/bench_colorize.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/scripts/coverage_cross_device.sh +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/setup.cfg +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/__init__.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/__main__.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/__init__.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/convert.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/filters.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/imports.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/index.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/morphology.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/normalize.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/parallel.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/spatial.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/transform.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/union_find.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/warp.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/cli/__init__.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/cli/__main__.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/cli/main.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/cli/migrate.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/cli/paths.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/desktop/__init__.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/desktop/pinning.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/imports.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/io/__init__.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/io/files.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/io/image.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/io/imports.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/io/path.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/io/result.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/load/__init__.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/load/module.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/load/object.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/logging/__init__.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/measure/__init__.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/measure/diameter.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/measure/imports.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/measure/metrics.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/plot/__init__.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/plot/color.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/plot/contour.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/plot/export.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/plot/figure.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/plot/grid.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/plot/imports.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/plot/label.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/plot/ncolor.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/testing/collisions.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/utils/__init__.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/utils/collections.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/utils/paths.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/__init__.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/__main__.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/app.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/assets.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/cli.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/demo.html +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/dependencies.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/exceptions.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/masks.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/middleware.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/model_registry.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/plugins/__init__.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/plugins/base.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/plugins/registry.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/plugins/schema.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/plugins/threshold.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/routers/__init__.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/routers/index.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/routers/log.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/routers/mask.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/routers/plugin.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/routers/segment.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/routers/session_routes.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/routers/system.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/routers/trust.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/routes.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/sample_image.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/schemas.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/segmentation.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/session.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/system.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/app.js +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/css/controls.css +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/css/layout.css +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/css/tools.css +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/css/viewer.css +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/html/left-panel.html +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/html/sidebar.html +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/html/viewer.html +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/affinity.svg +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/arrow-back-up.svg +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/arrow-forward-up.svg +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/dbscan-nested-arcs.svg +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/download.svg +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/droplet-half-2.svg +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/eraser.svg +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/home-2.svg +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/minus.svg +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/palette.svg +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/pencil.svg +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/plus.svg +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/rotate-rectangle.svg +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/topology-star.svg +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/index.html +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/brush.js +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/colormap.js +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/debug-apple-material.js +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/file-navigation.js +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/history.js +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/interactions.js +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/logging.js +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/mask-pipeline.js +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/painting.js +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/plugin-panel.js +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/pointer-state.js +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/state-persistence.js +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/tooltip-editor.js +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/ui-utils.js +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/wasm_fill.c +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit.egg-info/dependency_links.txt +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit.egg-info/entry_points.txt +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit.egg-info/top_level.txt +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/e2e/__init__.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/e2e/conftest.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/e2e/test_browser_smoke.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/e2e/test_pywebview_snapshot.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/fixtures/multichan_3c_4x4.czi +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/fixtures/tiny_8x8.czi +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_gpu.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_io.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_measure.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_module_collisions.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_morphology.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_paths_migration.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_plot_color.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_plot_contour.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_plot_display.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_plot_export.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_plot_figure.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_plot_grid.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_plot_label.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_plot_notebook.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_registration.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_slice.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_spatial.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/viewer/__init__.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/viewer/test_active_plugin_cache.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/viewer/test_app.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/viewer/test_async_dispatch.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/viewer/test_envelope_and_middleware.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/viewer/test_plugin_contract.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/viewer/test_session_eviction.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/viewer/test_title_config.py +0 -0
- {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/viewer/test_ui_mode.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ocdkit
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: Obsessive Coder's Dependency Toolkit — Python utilities for array manipulation, GPU dispatch, image I/O, morphology, and plotting.
|
|
5
5
|
License: BSD-3-Clause
|
|
6
6
|
Requires-Python: >=3.11
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
License-File: LICENSE
|
|
9
|
-
Requires-Dist: numpy
|
|
9
|
+
Requires-Dist: numpy>=2.0
|
|
10
10
|
Requires-Dist: scipy
|
|
11
11
|
Requires-Dist: scikit-image>=0.26
|
|
12
12
|
Requires-Dist: tifffile
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="114" height="20" role="img" aria-label="coverage: 43.71%"><title>coverage: 43.71%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="114" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="53" height="20" fill="#e05d44"/><rect width="114" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="315" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="510">coverage</text><text x="315" y="140" transform="scale(.1)" fill="#fff" textLength="510">coverage</text><text aria-hidden="true" x="865" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">43.71%</text><text x="865" y="140" transform="scale(.1)" fill="#fff" textLength="430">43.71%</text></g></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="68" height="20" role="img" aria-label="tests: 400"><title>tests: 400</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">400</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">400</text></g></svg>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Plot backend roadmap
|
|
2
|
+
|
|
3
|
+
**Status:** Design discussion / pending decision.
|
|
4
|
+
**Owner:** kevin@kanvasbio.com
|
|
5
|
+
**Driver:** matplotlib accounts for ~80% of warm-call time in `hiprpy.plot.plot_spectra` (~115 ms of ~140 ms) and ~1.5 s of cold-import overhead. We also want native hover/tooltip behavior like the classification-debugger GUI, which matplotlib can't deliver inline.
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
`ocdkit.plot` and `hiprpy.plot` already own ~10 K LOC of plotting code:
|
|
10
|
+
|
|
11
|
+
| Package | LOC |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `ocdkit.plot` (figure, grid, label, color, defaults, display, contour, export) | 1,935 |
|
|
14
|
+
| `hiprpy.plot` non-WGPU (datashade, cell, text, barcode, line, background, …) | 4,641 |
|
|
15
|
+
| `hiprpy.plot.wgpu` (lines, scatter, aggregators — already custom GPU primitives) | 4,783 |
|
|
16
|
+
|
|
17
|
+
What we still depend on matplotlib for is narrow: 2D Cartesian axes, ticks, labels, legends, image display, save-to-PNG/PDF/SVG, inline-in-Jupyter. **Not** 3D, animation, multi-backend abstraction (Qt/GTK/MacOSX), most chart types, complex tick locators/formatters, or any of the other ~180 K LOC of matplotlib's surface.
|
|
18
|
+
|
|
19
|
+
The driver question: replace matplotlib with what?
|
|
20
|
+
|
|
21
|
+
## Library survey (2026-05-07)
|
|
22
|
+
|
|
23
|
+
Shallow-cloned each candidate, counted Python LOC excluding tests, sphinx, sample data, and auto-generated validators:
|
|
24
|
+
|
|
25
|
+
| Library | Repo | Relevant Py LOC | Native code | Notes |
|
|
26
|
+
|---|---|---|---|---|
|
|
27
|
+
| matplotlib | — | 186 K | C extensions | What we're replacing |
|
|
28
|
+
| bokeh | 120 MB | 64 K | 150 K TypeScript | UI lives in TS frontend; Py is scene graph + serialization |
|
|
29
|
+
| plotly | 66 MB | 13 K relevant (+ ~600 K autogen) | JS bundle as data | Most LOC is auto-generated trait validators |
|
|
30
|
+
| datoviz | 137 MB | 8 K Py wrapper | 140 K C++ Vulkan engine | Heaviest binary footprint, fastest renderer |
|
|
31
|
+
| vispy | 11 MB | 32 K | None — pure Py + OpenGL | OpenGL stack, not WGPU |
|
|
32
|
+
| pygfx | 84 MB | 20 K | None — uses `wgpu-py` | Same backend ocdkit/hiprpy already use |
|
|
33
|
+
|
|
34
|
+
## Tradeoff summary
|
|
35
|
+
|
|
36
|
+
**Bokeh** — toolbar/logo *can* be removed (`figure(toolbar_location=None)`, `fig.toolbar.logo = None`), but the interaction model lives in a separate TypeScript frontend. To customize hover/zoom UX we'd be writing or forking TypeScript, not Python. The 150 K-LOC TS frontend is essentially the part we'd want to control, and it's the part we can't easily touch.
|
|
37
|
+
|
|
38
|
+
**Plotly** — same shape. Most LOC is auto-generated validators; the actual rendering and interaction is in the JS bundle. `displayModeBar=False` removes the toolbar but customizing hover beyond what their config exposes means reaching into JS.
|
|
39
|
+
|
|
40
|
+
**Datoviz** — Vulkan engine is fast and well-architected, but adds a 137 MB binary dependency and a different graphics stack from our existing wgpu-py code. Two graphics backends to maintain instead of one.
|
|
41
|
+
|
|
42
|
+
**Vispy** — pure Python and has scene + visuals modules that overlap heavily with what we want. But it's OpenGL via PyOpenGL, not WebGPU. We'd be running a second graphics stack alongside our wgpu-py code.
|
|
43
|
+
|
|
44
|
+
**pygfx** — uses *exactly* the `wgpu-py` we already depend on. Its `gfx.Lines`, `gfx.Mesh`, `gfx.Text`, `gfx.OrthographicCamera` primitives compose directly with our `DensityLineRenderer`. The most architecturally compatible third-party option.
|
|
45
|
+
|
|
46
|
+
**Roll our own** — given that we already have ~10 K LOC of plotting infrastructure, including a working WGPU line/scatter rasterizer, the increment to "complete 2D plotting library covering our actual needs" is roughly 3–5 K LOC. The scope is well-defined: axes, ticks, labels, legend, image display, hover, export.
|
|
47
|
+
|
|
48
|
+
## Recommendation
|
|
49
|
+
|
|
50
|
+
**Two-step.**
|
|
51
|
+
|
|
52
|
+
### Step 1: pygfx prototype (1 day)
|
|
53
|
+
|
|
54
|
+
Spike a `plot_spectra_pygfx` that uses `pygfx.Lines` + `pygfx.Text` + an `OrthographicCamera` to reproduce the current spectra layout. This is cheap because pygfx and our existing WGPU code share the same `wgpu-py` device — they can literally run in the same process without backend conflict.
|
|
55
|
+
|
|
56
|
+
What to evaluate:
|
|
57
|
+
- Visual quality vs. our current WGPU-rendered density lines (especially anti-aliasing)
|
|
58
|
+
- Text rendering quality (pygfx uses FreeType-rendered SDFs)
|
|
59
|
+
- Hover/picking — does pygfx's built-in picking suffice for our tooltip needs?
|
|
60
|
+
- Export — pygfx renders to a wgpu canvas; PNG export is straightforward, PDF/SVG would need our own rasterize-and-vectorize path
|
|
61
|
+
|
|
62
|
+
If pygfx covers ≥80% of our needs at ≥80% of the visual quality, **adopt pygfx**. We get hover, zoom, pan, picking essentially for free, plus the `pygfx` ecosystem (geometries, materials, post-processing).
|
|
63
|
+
|
|
64
|
+
### Step 2 (only if pygfx is insufficient): roll our own
|
|
65
|
+
|
|
66
|
+
Estimated scope, building on existing `hiprpy.plot.wgpu.lines`:
|
|
67
|
+
|
|
68
|
+
| Module | LOC est. | Notes |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `ocdkit.plot.figure_v2` | 400 | New `Figure` class owning the wgpu canvas + axes layout; coexists with current matplotlib `figure()` so migration is piecewise |
|
|
71
|
+
| `ocdkit.plot.axis` | 800 | `LinearAxis`, `LogAxis` — tick locator + formatter + render-time placement. The matplotlib equivalent is ~3 K LOC; we don't need most of it |
|
|
72
|
+
| `hiprpy.plot.wgpu.text` | 600 | FreeType-rendered glyph atlas + WGSL textured-quad shader. The one piece of genuinely new rasterization work |
|
|
73
|
+
| `ocdkit.plot.legend` | 200 | Boxed layout: text + marker swatches |
|
|
74
|
+
| `ocdkit.plot.hover` | 300 | Mouse → data-coord lookup → DOM/Jupyter tooltip overlay. Generalize the classification-debugger pattern |
|
|
75
|
+
| `ocdkit.plot.export` | 400 | PNG (numpy→PIL), SVG (string templates), maybe PDF (skip if PIL→PNG covers science exports) |
|
|
76
|
+
| Migration: rewrite `plot_spectra`, `plot_image_grid`, `key_slice_grid`, `label_axes` against new primitives | ~600 | Mostly drop-in replacements at the call sites |
|
|
77
|
+
| **Total** | **~3.3 K** | New code, all in our control |
|
|
78
|
+
|
|
79
|
+
Plus: deletion of matplotlib-specific code paths in `hiprpy.plot` (~1 K LOC saved) and removal of matplotlib runtime dep.
|
|
80
|
+
|
|
81
|
+
### Why not pygfx + own axes?
|
|
82
|
+
|
|
83
|
+
A hybrid path is also viable: use pygfx for the rasterization layer (lines/text/transform/camera) and write our own axis/tick/legend/hover layer on top. That'd be the best of both — pygfx handles the rendering substrate, we own the plotting semantics. Estimated ~1.5 K LOC of new code on top of pygfx.
|
|
84
|
+
|
|
85
|
+
## Open questions
|
|
86
|
+
|
|
87
|
+
1. **Hover semantics** — does the classification-debugger tooltip pattern generalize cleanly, or do different plots need different hover content models?
|
|
88
|
+
2. **Export fidelity** — do we need true vector PDF, or is high-DPI PNG enough? Vector requires re-rasterizing axes/text on the CPU side, which is non-trivial.
|
|
89
|
+
3. **Notebook + standalone parity** — pygfx renders to a wgpu canvas that displays inline in Jupyter via `wgpu_jupyter`. Does that path work in VS Code's Jupyter extension and in standalone scripts (`savefig`-equivalent)?
|
|
90
|
+
4. **Remoting** — if we ever want a server-side render pipeline (for cloud GUIs), pygfx's WGPU backend can render headlessly; matplotlib `Agg` does the same. Bokeh/plotly's JS-frontend assumption is harder to remote.
|
|
91
|
+
|
|
92
|
+
## Concrete next action
|
|
93
|
+
|
|
94
|
+
Spike `plot_spectra_pygfx` using `scope.mixed_spectra[-1]` from `notebooks/hiprpy_demo_notebook.ipynb` as the test case. Compare visual output side-by-side with `plot_spectra_wgpu` and `plot_spectra_cpu`. Decide pygfx-vs-roll-own from that single comparison.
|
|
95
|
+
|
|
96
|
+
If the answer ends up being "pygfx + own axis layer", the new module structure would live in `ocdkit.plot.gfx_*` (parallel to the current matplotlib-based modules) so the migration is a per-call-site flip rather than a big-bang rewrite.
|
|
@@ -10,7 +10,11 @@ readme = "README.md"
|
|
|
10
10
|
license = {text = "BSD-3-Clause"}
|
|
11
11
|
requires-python = ">=3.11"
|
|
12
12
|
dependencies = [
|
|
13
|
-
|
|
13
|
+
# numpy >= 2.0 — np.unique was reworked / sped up in the 2.0 cycle
|
|
14
|
+
# and matches fastremap.unique on label arrays. Helpers like
|
|
15
|
+
# array.ops.unique_nonzero / is_sequential rely on that parity to
|
|
16
|
+
# avoid hard-importing fastremap.
|
|
17
|
+
"numpy>=2.0",
|
|
14
18
|
"scipy",
|
|
15
19
|
"scikit-image>=0.26",
|
|
16
20
|
"tifffile",
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Synthetic single-cell test for contour alignment.
|
|
2
|
+
|
|
3
|
+
Plots a small mask with grid lines at integer pixel boundaries so we can
|
|
4
|
+
see exactly where each pipeline puts its outline relative to:
|
|
5
|
+
- the pixels of the cell (filled gray squares spanning [j-.5, j+.5] x [i-.5, i+.5])
|
|
6
|
+
- the cell boundary (gridline at half-integer y and x at the cell edge)
|
|
7
|
+
|
|
8
|
+
The three pipelines are:
|
|
9
|
+
- current (B-spline through pixel centers)
|
|
10
|
+
- tier 1 (Gaussian-smoothed pixel-center walk)
|
|
11
|
+
- tier 2 (marching squares; geometric cell boundary)
|
|
12
|
+
|
|
13
|
+
Also tests the new `offset` knob in tier 2.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import matplotlib.pyplot as plt
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
25
|
+
|
|
26
|
+
from bench_vector_contours import vector_contours_fast
|
|
27
|
+
from bench_vector_contours_tier2 import vector_contours_marching
|
|
28
|
+
from ocdkit.plot.contour import vector_contours
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def make_mask():
|
|
32
|
+
m = np.zeros((20, 20), dtype=np.int32)
|
|
33
|
+
m[5:11, 5:11] = 1 # 6x6 square cell
|
|
34
|
+
m[14:17, 13:18] = 2 # 3x5 rectangle cell
|
|
35
|
+
return m
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def add_grid(ax, shape):
|
|
39
|
+
H, W = shape
|
|
40
|
+
for x in np.arange(-0.5, W, 1):
|
|
41
|
+
ax.axvline(x, color='cyan', linewidth=0.3, alpha=0.35, zorder=0)
|
|
42
|
+
for y in np.arange(-0.5, H, 1):
|
|
43
|
+
ax.axhline(y, color='cyan', linewidth=0.3, alpha=0.35, zorder=0)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main():
|
|
47
|
+
mask = make_mask()
|
|
48
|
+
|
|
49
|
+
fig, axes = plt.subplots(2, 2, figsize=(12, 12))
|
|
50
|
+
|
|
51
|
+
titles = [
|
|
52
|
+
"Current vector_contours (splprep) — note up-left bias",
|
|
53
|
+
"Tier 1 (gaussian on pixel-center walk)",
|
|
54
|
+
"Tier 2 default offset=0 (geometric cell boundary)",
|
|
55
|
+
"Tier 2 offset=0.5 (through pixel centers, no bias)",
|
|
56
|
+
]
|
|
57
|
+
for ax, title in zip(axes.flat, titles):
|
|
58
|
+
ax.imshow(mask, cmap='gray_r', interpolation='nearest', vmin=0, vmax=2)
|
|
59
|
+
add_grid(ax, mask.shape)
|
|
60
|
+
ax.set_title(title, fontsize=10)
|
|
61
|
+
ax.set_xticks(range(mask.shape[1]))
|
|
62
|
+
ax.set_yticks(range(mask.shape[0]))
|
|
63
|
+
ax.tick_params(labelsize=6)
|
|
64
|
+
|
|
65
|
+
vector_contours(fig, axes[0, 0], mask, smooth_factor=5,
|
|
66
|
+
color='red', linewidth=1.5)
|
|
67
|
+
vector_contours_fast(fig, axes[0, 1], mask, smooth_sigma=1.0,
|
|
68
|
+
color='red', linewidth=1.5)
|
|
69
|
+
vector_contours_marching(fig, axes[1, 0], mask, smooth_sigma=1.0,
|
|
70
|
+
color='red', linewidth=1.5)
|
|
71
|
+
# Tier 2 with offset (still need to add the knob — we'll do that next)
|
|
72
|
+
vector_contours_marching(fig, axes[1, 1], mask, smooth_sigma=1.0,
|
|
73
|
+
color='red', linewidth=1.5)
|
|
74
|
+
|
|
75
|
+
fig.tight_layout()
|
|
76
|
+
out = Path('/Volumes/DataDrive/ocdkit/scripts/bench_contour_alignment.png')
|
|
77
|
+
fig.savefig(out, dpi=150, bbox_inches='tight')
|
|
78
|
+
print(f"saved: {out}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == '__main__':
|
|
82
|
+
main()
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""Benchmark current vector_contours pipeline vs a vectorized prototype.
|
|
2
|
+
|
|
3
|
+
Loads the ncolor example mask and renders both the current
|
|
4
|
+
ocdkit.plot.contour.vector_contours output and a prototype implementation
|
|
5
|
+
side by side, with timings.
|
|
6
|
+
|
|
7
|
+
Run:
|
|
8
|
+
python scripts/bench_vector_contours.py
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import matplotlib.patches as mpatches
|
|
17
|
+
import matplotlib.path as mpath
|
|
18
|
+
import matplotlib.pyplot as plt
|
|
19
|
+
import numpy as np
|
|
20
|
+
import skimage.io
|
|
21
|
+
from matplotlib.collections import LineCollection
|
|
22
|
+
from numba import njit
|
|
23
|
+
from scipy.ndimage import gaussian_filter1d
|
|
24
|
+
|
|
25
|
+
from ocdkit.array.spatial import (
|
|
26
|
+
boundary_to_masks,
|
|
27
|
+
get_neighbors,
|
|
28
|
+
kernel_setup,
|
|
29
|
+
masks_to_affinity,
|
|
30
|
+
)
|
|
31
|
+
from ocdkit.plot.contour import vector_contours
|
|
32
|
+
from skimage.segmentation import find_boundaries
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Faster numba contour walker — fixes the O(N^2) `i in contour` list lookup
|
|
37
|
+
# in ocdkit.array.spatial.parametrize_contours by using a global boolean
|
|
38
|
+
# `seen` array indexed by pixel id (O(1) membership check).
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
@njit(cache=True, fastmath=True)
|
|
42
|
+
def _parametrize_contours_fast(steps, labs, unique_L, neigh_inds, step_ok, csum):
|
|
43
|
+
sign = np.sum(np.abs(steps), axis=1) # noqa: F841 (kept for parity)
|
|
44
|
+
s0 = 4 # center index for 2D (3**2 // 2)
|
|
45
|
+
npix = neigh_inds.shape[1]
|
|
46
|
+
seen = np.zeros(npix, dtype=np.bool_)
|
|
47
|
+
|
|
48
|
+
contours = []
|
|
49
|
+
for l in unique_L:
|
|
50
|
+
sel = labs == l
|
|
51
|
+
indices = np.argwhere(sel).flatten()
|
|
52
|
+
if len(indices) == 0:
|
|
53
|
+
continue
|
|
54
|
+
index = indices[np.argmin(csum[sel])]
|
|
55
|
+
|
|
56
|
+
# local list to record this contour (numba reflected-list, OK)
|
|
57
|
+
contour = [np.int64(0)]
|
|
58
|
+
contour.clear()
|
|
59
|
+
|
|
60
|
+
n_iter = 0
|
|
61
|
+
max_iter = len(indices) + 1
|
|
62
|
+
while n_iter < max_iter:
|
|
63
|
+
here = neigh_inds[s0, index]
|
|
64
|
+
contour.append(here)
|
|
65
|
+
seen[here] = True
|
|
66
|
+
|
|
67
|
+
neighbor_inds = neigh_inds[:, index]
|
|
68
|
+
step_ok_here = step_ok[:, index]
|
|
69
|
+
|
|
70
|
+
# find best unseen neighbor
|
|
71
|
+
best_select = -1
|
|
72
|
+
best_cost = 1 << 30
|
|
73
|
+
best_count = 0
|
|
74
|
+
for k in range(neighbor_inds.shape[0]):
|
|
75
|
+
if not step_ok_here[k]:
|
|
76
|
+
continue
|
|
77
|
+
ni = neighbor_inds[k]
|
|
78
|
+
if ni < 0 or seen[ni]:
|
|
79
|
+
continue
|
|
80
|
+
# directional cost = sum(steps[k] * steps[3])
|
|
81
|
+
cost = 0
|
|
82
|
+
for d in range(steps.shape[1]):
|
|
83
|
+
cost += steps[k, d] * steps[3, d]
|
|
84
|
+
if cost < best_cost:
|
|
85
|
+
best_cost = cost
|
|
86
|
+
best_select = k
|
|
87
|
+
best_count += 1
|
|
88
|
+
|
|
89
|
+
if best_select < 0:
|
|
90
|
+
# closed
|
|
91
|
+
break
|
|
92
|
+
index = neighbor_inds[best_select]
|
|
93
|
+
n_iter += 1
|
|
94
|
+
|
|
95
|
+
# reset seen flags for THIS contour only (so other labels can reuse)
|
|
96
|
+
for px in contour:
|
|
97
|
+
seen[px] = False
|
|
98
|
+
contours.append(contour)
|
|
99
|
+
|
|
100
|
+
return contours
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _get_contour_fast(labels, affinity_graph, coords, neighbors, cardinal_only=True):
|
|
104
|
+
"""Drop-in replacement for ocdkit.array.spatial.get_contour using the
|
|
105
|
+
fast walker above. Still builds the affinity graph the same way upstream."""
|
|
106
|
+
from ocdkit.array.spatial import get_neigh_inds # late import
|
|
107
|
+
|
|
108
|
+
dim = labels.ndim
|
|
109
|
+
steps, inds, idx, fact, sign = kernel_setup(dim)
|
|
110
|
+
if cardinal_only:
|
|
111
|
+
allowed_inds = np.concatenate(inds[1:2])
|
|
112
|
+
else:
|
|
113
|
+
allowed_inds = np.concatenate(inds[1:])
|
|
114
|
+
|
|
115
|
+
indexes, neigh_inds, ind_matrix = get_neigh_inds(neighbors, coords, labels.shape)
|
|
116
|
+
csum = np.sum(affinity_graph, axis=0)
|
|
117
|
+
step_ok = np.zeros(affinity_graph.shape, bool)
|
|
118
|
+
for s in allowed_inds:
|
|
119
|
+
step_ok[s] = np.logical_and.reduce((
|
|
120
|
+
affinity_graph[s] > 0,
|
|
121
|
+
csum[neigh_inds[s]] < (3 ** dim - 1),
|
|
122
|
+
neigh_inds[s] > -1,
|
|
123
|
+
))
|
|
124
|
+
|
|
125
|
+
labs = labels[coords]
|
|
126
|
+
import fastremap
|
|
127
|
+
unique_L = fastremap.unique(labs)
|
|
128
|
+
contours = _parametrize_contours_fast(
|
|
129
|
+
steps, np.int32(labs), np.int32(unique_L), neigh_inds, step_ok, csum
|
|
130
|
+
)
|
|
131
|
+
return contours, unique_L
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
# Vectorized Chaikin corner-cutting (replaces per-cell scipy.splprep loop).
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
def _chaikin_closed(P, iterations=2):
|
|
139
|
+
"""Chaikin corner-cutting on a closed polygon (vectorized, no Python loop)."""
|
|
140
|
+
P = np.asarray(P, dtype=np.float64)
|
|
141
|
+
for _ in range(iterations):
|
|
142
|
+
Pn = np.roll(P, -1, axis=0)
|
|
143
|
+
Q = 0.75 * P + 0.25 * Pn
|
|
144
|
+
R = 0.25 * P + 0.75 * Pn
|
|
145
|
+
out = np.empty((2 * len(P), 2), dtype=np.float64)
|
|
146
|
+
out[0::2] = Q
|
|
147
|
+
out[1::2] = R
|
|
148
|
+
P = out
|
|
149
|
+
return P
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _gaussian_smooth_closed(P, sigma=2.0):
|
|
153
|
+
"""Periodic Gaussian smoothing of a closed polygon (low-pass filter).
|
|
154
|
+
|
|
155
|
+
Treats x and y as 1D periodic signals along the contour parameter and
|
|
156
|
+
applies a Gaussian filter — this is the linear low-pass equivalent of
|
|
157
|
+
scipy.interpolate.splprep with a smoothing factor, but ~50x cheaper.
|
|
158
|
+
"""
|
|
159
|
+
if len(P) < 3 or sigma <= 0:
|
|
160
|
+
return P
|
|
161
|
+
x = gaussian_filter1d(P[:, 0], sigma=sigma, mode='wrap')
|
|
162
|
+
y = gaussian_filter1d(P[:, 1], sigma=sigma, mode='wrap')
|
|
163
|
+
return np.column_stack([x, y])
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
# Prototype: vectorized vector outlines
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
def vector_contours_fast(fig, ax, mask, smooth_sigma=2.0, color='r', linewidth=1,
|
|
171
|
+
x_offset=0, y_offset=0, pad=2, zorder=1,
|
|
172
|
+
skip_despur=False):
|
|
173
|
+
"""Prototype.
|
|
174
|
+
|
|
175
|
+
- Reuses the affinity-graph machinery (it is already vectorized/numba'd).
|
|
176
|
+
- Uses a fast contour walker that fixes the O(N^2) Python-list membership
|
|
177
|
+
check in the original parametrize_contours.
|
|
178
|
+
- Replaces the per-cell scipy.splprep loop with vectorized Chaikin
|
|
179
|
+
corner-cutting (2 iterations ~ visually equivalent to a B-spline).
|
|
180
|
+
- Builds ONE concatenated matplotlib Path with MOVETO/LINETO/CLOSEPOLY for
|
|
181
|
+
all contours, wrapped in a single PathPatch (instead of N PathPatches).
|
|
182
|
+
- Optionally skips the boundary_to_masks despur step (cheap labels
|
|
183
|
+
typically don't need it).
|
|
184
|
+
"""
|
|
185
|
+
msk = np.pad(mask, pad, mode='edge')
|
|
186
|
+
msk = np.pad(msk, 1, mode='constant', constant_values=0)
|
|
187
|
+
dim = msk.ndim
|
|
188
|
+
shape = msk.shape
|
|
189
|
+
|
|
190
|
+
steps, inds, idx, fact, sign = kernel_setup(dim)
|
|
191
|
+
|
|
192
|
+
if not skip_despur:
|
|
193
|
+
bd = find_boundaries(msk, mode='inner', connectivity=2)
|
|
194
|
+
msk, _, _ = boundary_to_masks(bd, binary_mask=msk > 0,
|
|
195
|
+
connectivity=1, min_size=0)
|
|
196
|
+
|
|
197
|
+
coords = np.nonzero(msk)
|
|
198
|
+
neighbors = get_neighbors(tuple(coords), steps, dim, shape)
|
|
199
|
+
affinity_graph = masks_to_affinity(msk, coords, steps, inds, idx,
|
|
200
|
+
fact, sign, dim, neighbors)
|
|
201
|
+
contour_list, unique_L = _get_contour_fast(
|
|
202
|
+
msk, affinity_graph, coords, neighbors, cardinal_only=True
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Build a list of (N, 2) closed polylines; one LineCollection draws them
|
|
206
|
+
# all at once. Much faster than a Path with N CLOSEPOLY subpaths or N
|
|
207
|
+
# PathPatches in a PatchCollection.
|
|
208
|
+
cy, cx = coords
|
|
209
|
+
polylines = []
|
|
210
|
+
for contour in contour_list:
|
|
211
|
+
if len(contour) < 3:
|
|
212
|
+
continue
|
|
213
|
+
c = np.asarray(contour, dtype=np.int64)
|
|
214
|
+
pts = np.column_stack([cx[c], cy[c]]).astype(np.float64)
|
|
215
|
+
pts[:, 0] -= (pad + 1) - x_offset
|
|
216
|
+
pts[:, 1] -= (pad + 1) - y_offset
|
|
217
|
+
pts = _gaussian_smooth_closed(pts, sigma=smooth_sigma)
|
|
218
|
+
# close the loop by repeating the first point
|
|
219
|
+
polylines.append(np.vstack([pts, pts[:1]]))
|
|
220
|
+
|
|
221
|
+
if not polylines:
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
def _make_lc():
|
|
225
|
+
return LineCollection(polylines, colors=color, linewidths=linewidth,
|
|
226
|
+
zorder=zorder, capstyle='round')
|
|
227
|
+
|
|
228
|
+
if isinstance(ax, list):
|
|
229
|
+
for a in ax:
|
|
230
|
+
a.add_collection(_make_lc())
|
|
231
|
+
else:
|
|
232
|
+
ax.add_collection(_make_lc())
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ---------------------------------------------------------------------------
|
|
236
|
+
# Benchmark + figure
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
def _time_call(fn, repeat=3):
|
|
240
|
+
fn() # warm-up (incl numba compile)
|
|
241
|
+
ts = []
|
|
242
|
+
for _ in range(repeat):
|
|
243
|
+
t0 = time.perf_counter(); fn(); ts.append(time.perf_counter() - t0)
|
|
244
|
+
return min(ts), ts
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def main():
|
|
248
|
+
mask = skimage.io.imread('/Volumes/DataDrive/ncolor/test_files/example.png')
|
|
249
|
+
print(f"mask shape={mask.shape} n_labels={len(np.unique(mask)) - 1}")
|
|
250
|
+
|
|
251
|
+
def run_current():
|
|
252
|
+
fig, ax = plt.subplots(figsize=(5, 5))
|
|
253
|
+
ax.imshow(mask, cmap='gray', interpolation='nearest')
|
|
254
|
+
vector_contours(fig, ax, mask, smooth_factor=5, color='r', linewidth=1.0)
|
|
255
|
+
ax.set_axis_off()
|
|
256
|
+
plt.close(fig)
|
|
257
|
+
|
|
258
|
+
def run_fast():
|
|
259
|
+
fig, ax = plt.subplots(figsize=(5, 5))
|
|
260
|
+
ax.imshow(mask, cmap='gray', interpolation='nearest')
|
|
261
|
+
vector_contours_fast(fig, ax, mask, smooth_sigma=2.0, color='r', linewidth=1.0)
|
|
262
|
+
ax.set_axis_off()
|
|
263
|
+
plt.close(fig)
|
|
264
|
+
|
|
265
|
+
print("\nBenchmarking current pipeline (best of 3) ...")
|
|
266
|
+
cur_best, cur_all = _time_call(run_current, repeat=3)
|
|
267
|
+
print(f" current: {cur_best*1000:.1f} ms (all: {[f'{t*1000:.1f}' for t in cur_all]})")
|
|
268
|
+
|
|
269
|
+
print("Benchmarking prototype (best of 3) ...")
|
|
270
|
+
fast_best, fast_all = _time_call(run_fast, repeat=3)
|
|
271
|
+
print(f" proto: {fast_best*1000:.1f} ms (all: {[f'{t*1000:.1f}' for t in fast_all]})")
|
|
272
|
+
print(f"\nspeedup: {cur_best / fast_best:.1f}x")
|
|
273
|
+
|
|
274
|
+
# Top row: full image. Bottom row: zoomed crop so outline detail is visible.
|
|
275
|
+
H, W = mask.shape
|
|
276
|
+
crop_y = slice(H // 3, H // 3 + 80)
|
|
277
|
+
crop_x = slice(W // 3, W // 3 + 80)
|
|
278
|
+
|
|
279
|
+
fig, axes = plt.subplots(2, 2, figsize=(11, 11))
|
|
280
|
+
|
|
281
|
+
for a in axes[0]:
|
|
282
|
+
a.imshow(mask, cmap='gray', interpolation='nearest')
|
|
283
|
+
a.set_axis_off()
|
|
284
|
+
vector_contours(fig, axes[0, 0], mask, smooth_factor=5, color='r', linewidth=1.0)
|
|
285
|
+
vector_contours_fast(fig, axes[0, 1], mask, smooth_sigma=2.0, color='r', linewidth=1.0)
|
|
286
|
+
axes[0, 0].set_title(f"Current vector_contours\n{cur_best*1000:.1f} ms",
|
|
287
|
+
fontsize=11)
|
|
288
|
+
axes[0, 1].set_title(
|
|
289
|
+
f"Prototype (fast walker + Gaussian smooth + LineCollection)\n"
|
|
290
|
+
f"{fast_best*1000:.1f} ms ({cur_best/fast_best:.1f}x speedup)",
|
|
291
|
+
fontsize=11)
|
|
292
|
+
|
|
293
|
+
for a in axes[1]:
|
|
294
|
+
a.imshow(mask[crop_y, crop_x], cmap='gray', interpolation='nearest',
|
|
295
|
+
extent=(crop_x.start, crop_x.stop, crop_y.stop, crop_y.start))
|
|
296
|
+
a.set_xlim(crop_x.start, crop_x.stop)
|
|
297
|
+
a.set_ylim(crop_y.stop, crop_y.start)
|
|
298
|
+
a.set_axis_off()
|
|
299
|
+
vector_contours(fig, axes[1, 0], mask, smooth_factor=5, color='r', linewidth=1.5)
|
|
300
|
+
vector_contours_fast(fig, axes[1, 1], mask, smooth_sigma=2.0, color='r', linewidth=1.5)
|
|
301
|
+
axes[1, 0].set_title("zoomed crop", fontsize=10)
|
|
302
|
+
axes[1, 1].set_title("zoomed crop", fontsize=10)
|
|
303
|
+
fig.tight_layout()
|
|
304
|
+
|
|
305
|
+
out = Path('/Volumes/DataDrive/ocdkit/scripts/bench_vector_contours.png')
|
|
306
|
+
fig.savefig(out, dpi=150, bbox_inches='tight')
|
|
307
|
+
print(f"\nsaved: {out}")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
if __name__ == '__main__':
|
|
311
|
+
main()
|