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.
Files changed (196) hide show
  1. {ocdkit-0.0.3/src/ocdkit.egg-info → ocdkit-0.0.4}/PKG-INFO +2 -2
  2. ocdkit-0.0.4/badges/coverage.svg +1 -0
  3. ocdkit-0.0.4/badges/tests.svg +1 -0
  4. ocdkit-0.0.4/docs/plot-backend-roadmap.md +96 -0
  5. {ocdkit-0.0.3 → ocdkit-0.0.4}/pyproject.toml +5 -1
  6. ocdkit-0.0.4/scripts/bench_contour_alignment.py +82 -0
  7. ocdkit-0.0.4/scripts/bench_vector_contours.py +311 -0
  8. ocdkit-0.0.4/scripts/bench_vector_contours_tier2.py +746 -0
  9. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/ops.py +25 -3
  10. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/logging/handler.py +43 -2
  11. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/measure/bbox.py +57 -0
  12. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/measure/medoid.py +1 -58
  13. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/plot/defaults.py +17 -0
  14. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/plot/display.py +75 -0
  15. ocdkit-0.0.4/src/ocdkit/testing/__init__.py +55 -0
  16. ocdkit-0.0.4/src/ocdkit/testing/imports.py +237 -0
  17. ocdkit-0.0.4/src/ocdkit/tls/__init__.py +142 -0
  18. ocdkit-0.0.4/src/ocdkit/tls/external_ca.py +104 -0
  19. ocdkit-0.0.4/src/ocdkit/tls/hostnames.py +110 -0
  20. ocdkit-0.0.4/src/ocdkit/tls/imports.py +11 -0
  21. ocdkit-0.0.4/src/ocdkit/tls/local_ca.py +288 -0
  22. ocdkit-0.0.4/src/ocdkit/tls/paths.py +26 -0
  23. ocdkit-0.0.4/src/ocdkit/tls/trust.py +205 -0
  24. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/utils/gpu.py +29 -0
  25. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/utils/kwargs.py +16 -0
  26. {ocdkit-0.0.3 → ocdkit-0.0.4/src/ocdkit.egg-info}/PKG-INFO +2 -2
  27. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit.egg-info/SOURCES.txt +16 -1
  28. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit.egg-info/requires.txt +1 -1
  29. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_array.py +35 -0
  30. ocdkit-0.0.4/tests/test_import_cycles.py +10 -0
  31. ocdkit-0.0.4/tests/test_module_discovery.py +11 -0
  32. ocdkit-0.0.3/src/ocdkit/testing/__init__.py +0 -27
  33. ocdkit-0.0.3/src/ocdkit/tls.py +0 -792
  34. {ocdkit-0.0.3 → ocdkit-0.0.4}/.github/workflows/test_and_deploy.yml +0 -0
  35. {ocdkit-0.0.3 → ocdkit-0.0.4}/.gitignore +0 -0
  36. {ocdkit-0.0.3 → ocdkit-0.0.4}/LICENSE +0 -0
  37. {ocdkit-0.0.3 → ocdkit-0.0.4}/MANIFEST.in +0 -0
  38. {ocdkit-0.0.3 → ocdkit-0.0.4}/README.md +0 -0
  39. {ocdkit-0.0.3 → ocdkit-0.0.4}/docs/plugin-authoring.md +0 -0
  40. {ocdkit-0.0.3 → ocdkit-0.0.4}/docs/pywebview-desktop-integration.md +0 -0
  41. {ocdkit-0.0.3 → ocdkit-0.0.4}/scripts/bench_colorize.py +0 -0
  42. {ocdkit-0.0.3 → ocdkit-0.0.4}/scripts/coverage_cross_device.sh +0 -0
  43. {ocdkit-0.0.3 → ocdkit-0.0.4}/setup.cfg +0 -0
  44. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/__init__.py +0 -0
  45. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/__main__.py +0 -0
  46. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/__init__.py +0 -0
  47. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/convert.py +0 -0
  48. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/filters.py +0 -0
  49. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/imports.py +0 -0
  50. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/index.py +0 -0
  51. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/morphology.py +0 -0
  52. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/normalize.py +0 -0
  53. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/parallel.py +0 -0
  54. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/spatial.py +0 -0
  55. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/transform.py +0 -0
  56. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/union_find.py +0 -0
  57. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/array/warp.py +0 -0
  58. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/cli/__init__.py +0 -0
  59. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/cli/__main__.py +0 -0
  60. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/cli/main.py +0 -0
  61. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/cli/migrate.py +0 -0
  62. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/cli/paths.py +0 -0
  63. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/desktop/__init__.py +0 -0
  64. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/desktop/pinning.py +0 -0
  65. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/imports.py +0 -0
  66. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/io/__init__.py +0 -0
  67. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/io/files.py +0 -0
  68. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/io/image.py +0 -0
  69. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/io/imports.py +0 -0
  70. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/io/path.py +0 -0
  71. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/io/result.py +0 -0
  72. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/load/__init__.py +0 -0
  73. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/load/module.py +0 -0
  74. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/load/object.py +0 -0
  75. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/logging/__init__.py +0 -0
  76. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/measure/__init__.py +0 -0
  77. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/measure/diameter.py +0 -0
  78. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/measure/imports.py +0 -0
  79. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/measure/metrics.py +0 -0
  80. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/plot/__init__.py +0 -0
  81. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/plot/color.py +0 -0
  82. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/plot/contour.py +0 -0
  83. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/plot/export.py +0 -0
  84. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/plot/figure.py +0 -0
  85. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/plot/grid.py +0 -0
  86. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/plot/imports.py +0 -0
  87. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/plot/label.py +0 -0
  88. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/plot/ncolor.py +0 -0
  89. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/testing/collisions.py +0 -0
  90. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/utils/__init__.py +0 -0
  91. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/utils/collections.py +0 -0
  92. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/utils/paths.py +0 -0
  93. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/__init__.py +0 -0
  94. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/__main__.py +0 -0
  95. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/app.py +0 -0
  96. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/assets.py +0 -0
  97. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/cli.py +0 -0
  98. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/demo.html +0 -0
  99. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/dependencies.py +0 -0
  100. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/exceptions.py +0 -0
  101. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/masks.py +0 -0
  102. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/middleware.py +0 -0
  103. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/model_registry.py +0 -0
  104. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/plugins/__init__.py +0 -0
  105. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/plugins/base.py +0 -0
  106. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/plugins/registry.py +0 -0
  107. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/plugins/schema.py +0 -0
  108. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/plugins/threshold.py +0 -0
  109. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/routers/__init__.py +0 -0
  110. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/routers/index.py +0 -0
  111. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/routers/log.py +0 -0
  112. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/routers/mask.py +0 -0
  113. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/routers/plugin.py +0 -0
  114. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/routers/segment.py +0 -0
  115. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/routers/session_routes.py +0 -0
  116. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/routers/system.py +0 -0
  117. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/routers/trust.py +0 -0
  118. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/routes.py +0 -0
  119. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/sample_image.py +0 -0
  120. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/schemas.py +0 -0
  121. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/segmentation.py +0 -0
  122. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/session.py +0 -0
  123. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/system.py +0 -0
  124. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/app.js +0 -0
  125. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/css/controls.css +0 -0
  126. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/css/layout.css +0 -0
  127. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/css/tools.css +0 -0
  128. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/css/viewer.css +0 -0
  129. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/html/left-panel.html +0 -0
  130. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/html/sidebar.html +0 -0
  131. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/html/viewer.html +0 -0
  132. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/affinity.svg +0 -0
  133. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/arrow-back-up.svg +0 -0
  134. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/arrow-forward-up.svg +0 -0
  135. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/dbscan-nested-arcs.svg +0 -0
  136. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/download.svg +0 -0
  137. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/droplet-half-2.svg +0 -0
  138. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/eraser.svg +0 -0
  139. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/home-2.svg +0 -0
  140. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/minus.svg +0 -0
  141. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/palette.svg +0 -0
  142. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/pencil.svg +0 -0
  143. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/plus.svg +0 -0
  144. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/rotate-rectangle.svg +0 -0
  145. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/icons/topology-star.svg +0 -0
  146. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/index.html +0 -0
  147. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/brush.js +0 -0
  148. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/colormap.js +0 -0
  149. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/debug-apple-material.js +0 -0
  150. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/file-navigation.js +0 -0
  151. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/history.js +0 -0
  152. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/interactions.js +0 -0
  153. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/logging.js +0 -0
  154. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/mask-pipeline.js +0 -0
  155. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/painting.js +0 -0
  156. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/plugin-panel.js +0 -0
  157. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/pointer-state.js +0 -0
  158. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/state-persistence.js +0 -0
  159. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/tooltip-editor.js +0 -0
  160. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/ui-utils.js +0 -0
  161. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit/viewer/web/js/wasm_fill.c +0 -0
  162. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit.egg-info/dependency_links.txt +0 -0
  163. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit.egg-info/entry_points.txt +0 -0
  164. {ocdkit-0.0.3 → ocdkit-0.0.4}/src/ocdkit.egg-info/top_level.txt +0 -0
  165. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/e2e/__init__.py +0 -0
  166. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/e2e/conftest.py +0 -0
  167. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/e2e/test_browser_smoke.py +0 -0
  168. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/e2e/test_pywebview_snapshot.py +0 -0
  169. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/fixtures/multichan_3c_4x4.czi +0 -0
  170. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/fixtures/tiny_8x8.czi +0 -0
  171. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_gpu.py +0 -0
  172. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_io.py +0 -0
  173. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_measure.py +0 -0
  174. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_module_collisions.py +0 -0
  175. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_morphology.py +0 -0
  176. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_paths_migration.py +0 -0
  177. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_plot_color.py +0 -0
  178. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_plot_contour.py +0 -0
  179. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_plot_display.py +0 -0
  180. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_plot_export.py +0 -0
  181. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_plot_figure.py +0 -0
  182. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_plot_grid.py +0 -0
  183. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_plot_label.py +0 -0
  184. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_plot_notebook.py +0 -0
  185. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_registration.py +0 -0
  186. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_slice.py +0 -0
  187. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/test_spatial.py +0 -0
  188. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/viewer/__init__.py +0 -0
  189. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/viewer/test_active_plugin_cache.py +0 -0
  190. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/viewer/test_app.py +0 -0
  191. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/viewer/test_async_dispatch.py +0 -0
  192. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/viewer/test_envelope_and_middleware.py +0 -0
  193. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/viewer/test_plugin_contract.py +0 -0
  194. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/viewer/test_session_eviction.py +0 -0
  195. {ocdkit-0.0.3 → ocdkit-0.0.4}/tests/viewer/test_title_config.py +0 -0
  196. {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
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
- "numpy",
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()