ocdkit 0.0.2__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 (202) hide show
  1. {ocdkit-0.0.2 → ocdkit-0.0.4}/.github/workflows/test_and_deploy.yml +5 -1
  2. {ocdkit-0.0.2 → ocdkit-0.0.4}/.gitignore +4 -0
  3. {ocdkit-0.0.2 → ocdkit-0.0.4}/MANIFEST.in +4 -0
  4. {ocdkit-0.0.2/src/ocdkit.egg-info → ocdkit-0.0.4}/PKG-INFO +44 -7
  5. ocdkit-0.0.2/PKG-INFO → ocdkit-0.0.4/README.md +28 -28
  6. ocdkit-0.0.4/badges/coverage.svg +1 -0
  7. ocdkit-0.0.4/badges/tests.svg +1 -0
  8. ocdkit-0.0.4/docs/plot-backend-roadmap.md +96 -0
  9. ocdkit-0.0.4/docs/plugin-authoring.md +220 -0
  10. ocdkit-0.0.4/docs/pywebview-desktop-integration.md +689 -0
  11. ocdkit-0.0.4/pyproject.toml +94 -0
  12. ocdkit-0.0.4/scripts/bench_contour_alignment.py +82 -0
  13. ocdkit-0.0.4/scripts/bench_vector_contours.py +311 -0
  14. ocdkit-0.0.4/scripts/bench_vector_contours_tier2.py +746 -0
  15. ocdkit-0.0.4/src/ocdkit/__init__.py +40 -0
  16. ocdkit-0.0.4/src/ocdkit/__main__.py +6 -0
  17. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/array/convert.py +38 -0
  18. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/array/ops.py +25 -3
  19. ocdkit-0.0.4/src/ocdkit/array/parallel.py +164 -0
  20. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/array/spatial.py +4 -1
  21. ocdkit-0.0.4/src/ocdkit/cli/__init__.py +14 -0
  22. ocdkit-0.0.4/src/ocdkit/cli/__main__.py +6 -0
  23. ocdkit-0.0.4/src/ocdkit/cli/main.py +41 -0
  24. ocdkit-0.0.4/src/ocdkit/cli/migrate.py +77 -0
  25. ocdkit-0.0.4/src/ocdkit/cli/paths.py +170 -0
  26. ocdkit-0.0.4/src/ocdkit/desktop/__init__.py +8 -0
  27. ocdkit-0.0.4/src/ocdkit/desktop/pinning.py +1007 -0
  28. ocdkit-0.0.4/src/ocdkit/io/__init__.py +3 -0
  29. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/io/image.py +6 -3
  30. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/load/__init__.py +1 -1
  31. ocdkit-0.0.4/src/ocdkit/load/module.py +192 -0
  32. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/logging/handler.py +43 -2
  33. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/measure/bbox.py +57 -0
  34. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/measure/medoid.py +1 -58
  35. ocdkit-0.0.4/src/ocdkit/plot/__init__.py +11 -0
  36. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/plot/defaults.py +17 -0
  37. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/plot/display.py +75 -0
  38. ocdkit-0.0.4/src/ocdkit/testing/__init__.py +55 -0
  39. ocdkit-0.0.4/src/ocdkit/testing/collisions.py +211 -0
  40. ocdkit-0.0.4/src/ocdkit/testing/imports.py +237 -0
  41. ocdkit-0.0.4/src/ocdkit/tls/__init__.py +142 -0
  42. ocdkit-0.0.4/src/ocdkit/tls/external_ca.py +104 -0
  43. ocdkit-0.0.4/src/ocdkit/tls/hostnames.py +110 -0
  44. ocdkit-0.0.4/src/ocdkit/tls/imports.py +11 -0
  45. ocdkit-0.0.4/src/ocdkit/tls/local_ca.py +288 -0
  46. ocdkit-0.0.4/src/ocdkit/tls/paths.py +26 -0
  47. ocdkit-0.0.4/src/ocdkit/tls/trust.py +205 -0
  48. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/utils/gpu.py +29 -0
  49. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/utils/kwargs.py +16 -0
  50. ocdkit-0.0.4/src/ocdkit/utils/paths.py +186 -0
  51. ocdkit-0.0.4/src/ocdkit/viewer/__init__.py +29 -0
  52. ocdkit-0.0.4/src/ocdkit/viewer/__main__.py +8 -0
  53. ocdkit-0.0.4/src/ocdkit/viewer/app.py +830 -0
  54. ocdkit-0.0.4/src/ocdkit/viewer/assets.py +640 -0
  55. ocdkit-0.0.4/src/ocdkit/viewer/cli.py +162 -0
  56. ocdkit-0.0.4/src/ocdkit/viewer/demo.html +253 -0
  57. ocdkit-0.0.4/src/ocdkit/viewer/dependencies.py +73 -0
  58. ocdkit-0.0.4/src/ocdkit/viewer/exceptions.py +125 -0
  59. ocdkit-0.0.4/src/ocdkit/viewer/masks.py +71 -0
  60. ocdkit-0.0.4/src/ocdkit/viewer/middleware.py +81 -0
  61. ocdkit-0.0.4/src/ocdkit/viewer/model_registry.py +96 -0
  62. ocdkit-0.0.4/src/ocdkit/viewer/plugins/__init__.py +25 -0
  63. ocdkit-0.0.4/src/ocdkit/viewer/plugins/base.py +259 -0
  64. ocdkit-0.0.4/src/ocdkit/viewer/plugins/registry.py +160 -0
  65. ocdkit-0.0.4/src/ocdkit/viewer/plugins/schema.py +178 -0
  66. ocdkit-0.0.4/src/ocdkit/viewer/plugins/threshold.py +104 -0
  67. ocdkit-0.0.4/src/ocdkit/viewer/routers/__init__.py +9 -0
  68. ocdkit-0.0.4/src/ocdkit/viewer/routers/index.py +61 -0
  69. ocdkit-0.0.4/src/ocdkit/viewer/routers/log.py +36 -0
  70. ocdkit-0.0.4/src/ocdkit/viewer/routers/mask.py +46 -0
  71. ocdkit-0.0.4/src/ocdkit/viewer/routers/plugin.py +129 -0
  72. ocdkit-0.0.4/src/ocdkit/viewer/routers/segment.py +39 -0
  73. ocdkit-0.0.4/src/ocdkit/viewer/routers/session_routes.py +192 -0
  74. ocdkit-0.0.4/src/ocdkit/viewer/routers/system.py +41 -0
  75. ocdkit-0.0.4/src/ocdkit/viewer/routers/trust.py +298 -0
  76. ocdkit-0.0.4/src/ocdkit/viewer/routes.py +217 -0
  77. ocdkit-0.0.4/src/ocdkit/viewer/sample_image.py +113 -0
  78. ocdkit-0.0.4/src/ocdkit/viewer/schemas.py +150 -0
  79. ocdkit-0.0.4/src/ocdkit/viewer/segmentation.py +270 -0
  80. ocdkit-0.0.4/src/ocdkit/viewer/session.py +311 -0
  81. ocdkit-0.0.4/src/ocdkit/viewer/system.py +260 -0
  82. ocdkit-0.0.4/src/ocdkit/viewer/web/app.js +11415 -0
  83. ocdkit-0.0.4/src/ocdkit/viewer/web/css/controls.css +2309 -0
  84. ocdkit-0.0.4/src/ocdkit/viewer/web/css/layout.css +276 -0
  85. ocdkit-0.0.4/src/ocdkit/viewer/web/css/tools.css +383 -0
  86. ocdkit-0.0.4/src/ocdkit/viewer/web/css/viewer.css +85 -0
  87. ocdkit-0.0.4/src/ocdkit/viewer/web/html/left-panel.html +253 -0
  88. ocdkit-0.0.4/src/ocdkit/viewer/web/html/sidebar.html +34 -0
  89. ocdkit-0.0.4/src/ocdkit/viewer/web/html/viewer.html +7 -0
  90. ocdkit-0.0.4/src/ocdkit/viewer/web/icons/affinity.svg +25 -0
  91. ocdkit-0.0.4/src/ocdkit/viewer/web/icons/arrow-back-up.svg +1 -0
  92. ocdkit-0.0.4/src/ocdkit/viewer/web/icons/arrow-forward-up.svg +1 -0
  93. ocdkit-0.0.4/src/ocdkit/viewer/web/icons/dbscan-nested-arcs.svg +6 -0
  94. ocdkit-0.0.4/src/ocdkit/viewer/web/icons/download.svg +1 -0
  95. ocdkit-0.0.4/src/ocdkit/viewer/web/icons/droplet-half-2.svg +1 -0
  96. ocdkit-0.0.4/src/ocdkit/viewer/web/icons/eraser.svg +1 -0
  97. ocdkit-0.0.4/src/ocdkit/viewer/web/icons/home-2.svg +1 -0
  98. ocdkit-0.0.4/src/ocdkit/viewer/web/icons/minus.svg +1 -0
  99. ocdkit-0.0.4/src/ocdkit/viewer/web/icons/palette.svg +1 -0
  100. ocdkit-0.0.4/src/ocdkit/viewer/web/icons/pencil.svg +1 -0
  101. ocdkit-0.0.4/src/ocdkit/viewer/web/icons/plus.svg +1 -0
  102. ocdkit-0.0.4/src/ocdkit/viewer/web/icons/rotate-rectangle.svg +1 -0
  103. ocdkit-0.0.4/src/ocdkit/viewer/web/icons/topology-star.svg +1 -0
  104. ocdkit-0.0.4/src/ocdkit/viewer/web/index.html +16 -0
  105. ocdkit-0.0.4/src/ocdkit/viewer/web/js/brush.js +330 -0
  106. ocdkit-0.0.4/src/ocdkit/viewer/web/js/colormap.js +462 -0
  107. ocdkit-0.0.4/src/ocdkit/viewer/web/js/debug-apple-material.js +133 -0
  108. ocdkit-0.0.4/src/ocdkit/viewer/web/js/file-navigation.js +620 -0
  109. ocdkit-0.0.4/src/ocdkit/viewer/web/js/history.js +143 -0
  110. ocdkit-0.0.4/src/ocdkit/viewer/web/js/interactions.js +379 -0
  111. ocdkit-0.0.4/src/ocdkit/viewer/web/js/logging.js +166 -0
  112. ocdkit-0.0.4/src/ocdkit/viewer/web/js/mask-pipeline.js +176 -0
  113. ocdkit-0.0.4/src/ocdkit/viewer/web/js/painting.js +2347 -0
  114. ocdkit-0.0.4/src/ocdkit/viewer/web/js/plugin-panel.js +891 -0
  115. ocdkit-0.0.4/src/ocdkit/viewer/web/js/pointer-state.js +115 -0
  116. ocdkit-0.0.4/src/ocdkit/viewer/web/js/state-persistence.js +379 -0
  117. ocdkit-0.0.4/src/ocdkit/viewer/web/js/tooltip-editor.js +342 -0
  118. ocdkit-0.0.4/src/ocdkit/viewer/web/js/ui-utils.js +1419 -0
  119. ocdkit-0.0.4/src/ocdkit/viewer/web/js/wasm_fill.c +101 -0
  120. ocdkit-0.0.4/src/ocdkit.egg-info/PKG-INFO +103 -0
  121. ocdkit-0.0.4/src/ocdkit.egg-info/SOURCES.txt +192 -0
  122. ocdkit-0.0.4/src/ocdkit.egg-info/entry_points.txt +6 -0
  123. ocdkit-0.0.4/src/ocdkit.egg-info/requires.txt +33 -0
  124. ocdkit-0.0.4/tests/e2e/__init__.py +0 -0
  125. ocdkit-0.0.4/tests/e2e/conftest.py +125 -0
  126. ocdkit-0.0.4/tests/e2e/test_browser_smoke.py +93 -0
  127. ocdkit-0.0.4/tests/e2e/test_pywebview_snapshot.py +81 -0
  128. {ocdkit-0.0.2 → ocdkit-0.0.4}/tests/test_array.py +191 -0
  129. ocdkit-0.0.4/tests/test_import_cycles.py +10 -0
  130. ocdkit-0.0.4/tests/test_module_collisions.py +10 -0
  131. ocdkit-0.0.4/tests/test_module_discovery.py +11 -0
  132. ocdkit-0.0.4/tests/test_paths_migration.py +114 -0
  133. {ocdkit-0.0.2 → ocdkit-0.0.4}/tests/test_plot_figure.py +1 -1
  134. {ocdkit-0.0.2 → ocdkit-0.0.4}/tests/test_plot_notebook.py +1 -1
  135. ocdkit-0.0.4/tests/viewer/__init__.py +0 -0
  136. ocdkit-0.0.4/tests/viewer/test_active_plugin_cache.py +70 -0
  137. ocdkit-0.0.4/tests/viewer/test_app.py +158 -0
  138. ocdkit-0.0.4/tests/viewer/test_async_dispatch.py +97 -0
  139. ocdkit-0.0.4/tests/viewer/test_envelope_and_middleware.py +144 -0
  140. ocdkit-0.0.4/tests/viewer/test_plugin_contract.py +204 -0
  141. ocdkit-0.0.4/tests/viewer/test_session_eviction.py +68 -0
  142. ocdkit-0.0.4/tests/viewer/test_title_config.py +71 -0
  143. ocdkit-0.0.4/tests/viewer/test_ui_mode.py +108 -0
  144. ocdkit-0.0.2/README.md +0 -39
  145. ocdkit-0.0.2/pyproject.toml +0 -51
  146. ocdkit-0.0.2/src/ocdkit/__init__.py +0 -10
  147. ocdkit-0.0.2/src/ocdkit/load/module.py +0 -132
  148. ocdkit-0.0.2/src/ocdkit/plot/__init__.py +0 -5
  149. ocdkit-0.0.2/src/ocdkit/utils/__init__.py +0 -3
  150. ocdkit-0.0.2/src/ocdkit.egg-info/SOURCES.txt +0 -77
  151. ocdkit-0.0.2/src/ocdkit.egg-info/requires.txt +0 -17
  152. {ocdkit-0.0.2 → ocdkit-0.0.4}/LICENSE +0 -0
  153. {ocdkit-0.0.2 → ocdkit-0.0.4}/scripts/bench_colorize.py +0 -0
  154. {ocdkit-0.0.2 → ocdkit-0.0.4}/scripts/coverage_cross_device.sh +0 -0
  155. {ocdkit-0.0.2 → ocdkit-0.0.4}/setup.cfg +0 -0
  156. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/array/__init__.py +0 -0
  157. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/array/filters.py +0 -0
  158. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/array/imports.py +0 -0
  159. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/array/index.py +0 -0
  160. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/array/morphology.py +0 -0
  161. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/array/normalize.py +0 -0
  162. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/array/transform.py +0 -0
  163. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/array/union_find.py +0 -0
  164. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/array/warp.py +0 -0
  165. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/imports.py +0 -0
  166. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/io/files.py +0 -0
  167. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/io/imports.py +0 -0
  168. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/io/path.py +0 -0
  169. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/io/result.py +0 -0
  170. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/load/object.py +0 -0
  171. {ocdkit-0.0.2/src/ocdkit/io → ocdkit-0.0.4/src/ocdkit/logging}/__init__.py +0 -0
  172. {ocdkit-0.0.2/src/ocdkit/logging → ocdkit-0.0.4/src/ocdkit/measure}/__init__.py +0 -0
  173. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/measure/diameter.py +0 -0
  174. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/measure/imports.py +0 -0
  175. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/measure/metrics.py +0 -0
  176. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/plot/color.py +0 -0
  177. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/plot/contour.py +0 -0
  178. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/plot/export.py +0 -0
  179. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/plot/figure.py +0 -0
  180. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/plot/grid.py +0 -0
  181. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/plot/imports.py +0 -0
  182. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/plot/label.py +0 -0
  183. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/plot/ncolor.py +0 -0
  184. {ocdkit-0.0.2/src/ocdkit/measure → ocdkit-0.0.4/src/ocdkit/utils}/__init__.py +0 -0
  185. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit/utils/collections.py +0 -0
  186. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit.egg-info/dependency_links.txt +0 -0
  187. {ocdkit-0.0.2 → ocdkit-0.0.4}/src/ocdkit.egg-info/top_level.txt +0 -0
  188. {ocdkit-0.0.2 → ocdkit-0.0.4}/tests/fixtures/multichan_3c_4x4.czi +0 -0
  189. {ocdkit-0.0.2 → ocdkit-0.0.4}/tests/fixtures/tiny_8x8.czi +0 -0
  190. {ocdkit-0.0.2 → ocdkit-0.0.4}/tests/test_gpu.py +0 -0
  191. {ocdkit-0.0.2 → ocdkit-0.0.4}/tests/test_io.py +0 -0
  192. {ocdkit-0.0.2 → ocdkit-0.0.4}/tests/test_measure.py +0 -0
  193. {ocdkit-0.0.2 → ocdkit-0.0.4}/tests/test_morphology.py +0 -0
  194. {ocdkit-0.0.2 → ocdkit-0.0.4}/tests/test_plot_color.py +0 -0
  195. {ocdkit-0.0.2 → ocdkit-0.0.4}/tests/test_plot_contour.py +0 -0
  196. {ocdkit-0.0.2 → ocdkit-0.0.4}/tests/test_plot_display.py +0 -0
  197. {ocdkit-0.0.2 → ocdkit-0.0.4}/tests/test_plot_export.py +0 -0
  198. {ocdkit-0.0.2 → ocdkit-0.0.4}/tests/test_plot_grid.py +0 -0
  199. {ocdkit-0.0.2 → ocdkit-0.0.4}/tests/test_plot_label.py +0 -0
  200. {ocdkit-0.0.2 → ocdkit-0.0.4}/tests/test_registration.py +0 -0
  201. {ocdkit-0.0.2 → ocdkit-0.0.4}/tests/test_slice.py +0 -0
  202. {ocdkit-0.0.2 → ocdkit-0.0.4}/tests/test_spatial.py +0 -0
@@ -13,7 +13,7 @@ jobs:
13
13
  fail-fast: false
14
14
  matrix:
15
15
  os: [ubuntu-latest, windows-latest, macos-latest] # macos-latest = Apple Silicon (MPS)
16
- python_version: ['3.11', '3.12']
16
+ python_version: ['3.11', '3.12', '3.13']
17
17
  steps:
18
18
  - uses: actions/checkout@v4
19
19
  with:
@@ -85,8 +85,12 @@ jobs:
85
85
  runs-on: ubuntu-latest
86
86
  needs: test
87
87
  if: startsWith(github.ref, 'refs/tags/v')
88
+ permissions:
89
+ contents: write
88
90
  steps:
89
91
  - uses: actions/checkout@v4
92
+ with:
93
+ ref: main
90
94
 
91
95
  - name: Download test artifacts
92
96
  uses: actions/download-artifact@v4
@@ -6,6 +6,10 @@
6
6
  # Local tooling
7
7
  .claude/
8
8
 
9
+ # Editor / NAS junk
10
+ *.bak
11
+ .!*
12
+
9
13
  # C extensions
10
14
  *.so
11
15
 
@@ -1,9 +1,13 @@
1
1
  include README.md
2
2
  include LICENSE
3
3
  recursive-include src *.py
4
+ recursive-include src/ocdkit/viewer/web *.html *.css *.js *.svg *.c
4
5
  recursive-include tests *.py
5
6
  recursive-include tests/fixtures *
7
+ prune src/ocdkit/viewer/web/icons/_unused
6
8
  prune tests/__pycache__
7
9
  prune **/__pycache__
8
10
  global-exclude ._*
9
11
  global-exclude .DS_Store
12
+ global-exclude *.bak
13
+ global-exclude .!*
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ocdkit
3
- Version: 0.0.2
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
- Requires-Python: >=3.9
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
- Requires-Dist: scikit-image
11
+ Requires-Dist: scikit-image>=0.26
12
12
  Requires-Dist: tifffile
13
13
  Requires-Dist: imagecodecs
14
14
  Requires-Dist: matplotlib
@@ -18,16 +18,26 @@ Requires-Dist: torch>=1.12
18
18
  Requires-Dist: dask[array]
19
19
  Requires-Dist: natsort
20
20
  Requires-Dist: numba
21
- Requires-Dist: bioio
22
- Requires-Dist: bioio-czi
21
+ Requires-Dist: aicspylibczi
23
22
  Requires-Dist: ncolor>=1.5.1
24
23
  Requires-Dist: cmap
25
24
  Requires-Dist: tqdm
25
+ Requires-Dist: platformdirs
26
+ Requires-Dist: cryptography
27
+ Provides-Extra: viewer
28
+ Requires-Dist: fastapi; extra == "viewer"
29
+ Requires-Dist: uvicorn[standard]; extra == "viewer"
30
+ Requires-Dist: imageio; extra == "viewer"
31
+ Requires-Dist: python-multipart; extra == "viewer"
32
+ Provides-Extra: desktop
33
+ Requires-Dist: pywebview; extra == "desktop"
34
+ Requires-Dist: pywin32; sys_platform == "win32" and extra == "desktop"
35
+ Requires-Dist: pyobjc-framework-Cocoa; sys_platform == "darwin" and extra == "desktop"
26
36
  Dynamic: license-file
27
37
 
28
38
  # ocdkit
29
39
 
30
- **Obsessive Coder's Dependency Toolkit** — Python utilities for array manipulation, GPU dispatch, image I/O, spatial operations, morphology, and plotting.
40
+ A toolkit for array manipulation, GPU dispatch, image I/O, spatial operations, morphology, and plotting.
31
41
 
32
42
  ## Install
33
43
 
@@ -61,6 +71,33 @@ from ocdkit.plot import figure, image_grid
61
71
  device = resolve_device() # auto-detect CUDA / MPS / CPU
62
72
  ```
63
73
 
74
+ ## Performance tips
75
+
76
+ ### Pin numba's JIT cache to local disk
77
+
78
+ If your project source lives on a network filesystem (SMB / NFS), set
79
+ `NUMBA_CACHE_DIR` to a local-disk location. By default numba writes its
80
+ JIT cache to `__pycache__` next to the source file, which on a
81
+ NAS-mounted tree means dozens of small SMB ops per fresh subprocess —
82
+ several seconds of overhead on every cold import.
83
+
84
+ ocdkit auto-applies `$HOME/.cache/numba` as the default if you haven't
85
+ set it (see `src/ocdkit/__init__.py`), but for shells, test runners, and
86
+ non-ocdkit code, set it explicitly:
87
+
88
+ ```bash
89
+ # Linux / macOS — add to ~/.zshrc, ~/.bashrc, or ~/.profile
90
+ export NUMBA_CACHE_DIR="$HOME/.cache/numba"
91
+ ```
92
+
93
+ ```powershell
94
+ # Windows — add to $PROFILE
95
+ [Environment]::SetEnvironmentVariable('NUMBA_CACHE_DIR', "$env:USERPROFILE\.cache\numba", 'User')
96
+ ```
97
+
98
+ Compiled artifacts are machine-local anyway (CPU- and Python-version
99
+ specific), so they don't belong on shared NAS regardless of perf.
100
+
64
101
  ## License
65
102
 
66
103
  BSD-3-Clause
@@ -1,33 +1,6 @@
1
- Metadata-Version: 2.4
2
- Name: ocdkit
3
- Version: 0.0.2
4
- Summary: Obsessive Coder's Dependency Toolkit — Python utilities for array manipulation, GPU dispatch, image I/O, morphology, and plotting.
5
- License: BSD-3-Clause
6
- Requires-Python: >=3.9
7
- Description-Content-Type: text/markdown
8
- License-File: LICENSE
9
- Requires-Dist: numpy
10
- Requires-Dist: scipy
11
- Requires-Dist: scikit-image
12
- Requires-Dist: tifffile
13
- Requires-Dist: imagecodecs
14
- Requires-Dist: matplotlib
15
- Requires-Dist: fastremap
16
- Requires-Dist: edt
17
- Requires-Dist: torch>=1.12
18
- Requires-Dist: dask[array]
19
- Requires-Dist: natsort
20
- Requires-Dist: numba
21
- Requires-Dist: bioio
22
- Requires-Dist: bioio-czi
23
- Requires-Dist: ncolor>=1.5.1
24
- Requires-Dist: cmap
25
- Requires-Dist: tqdm
26
- Dynamic: license-file
27
-
28
1
  # ocdkit
29
2
 
30
- **Obsessive Coder's Dependency Toolkit** — Python utilities for array manipulation, GPU dispatch, image I/O, spatial operations, morphology, and plotting.
3
+ A toolkit for array manipulation, GPU dispatch, image I/O, spatial operations, morphology, and plotting.
31
4
 
32
5
  ## Install
33
6
 
@@ -61,6 +34,33 @@ from ocdkit.plot import figure, image_grid
61
34
  device = resolve_device() # auto-detect CUDA / MPS / CPU
62
35
  ```
63
36
 
37
+ ## Performance tips
38
+
39
+ ### Pin numba's JIT cache to local disk
40
+
41
+ If your project source lives on a network filesystem (SMB / NFS), set
42
+ `NUMBA_CACHE_DIR` to a local-disk location. By default numba writes its
43
+ JIT cache to `__pycache__` next to the source file, which on a
44
+ NAS-mounted tree means dozens of small SMB ops per fresh subprocess —
45
+ several seconds of overhead on every cold import.
46
+
47
+ ocdkit auto-applies `$HOME/.cache/numba` as the default if you haven't
48
+ set it (see `src/ocdkit/__init__.py`), but for shells, test runners, and
49
+ non-ocdkit code, set it explicitly:
50
+
51
+ ```bash
52
+ # Linux / macOS — add to ~/.zshrc, ~/.bashrc, or ~/.profile
53
+ export NUMBA_CACHE_DIR="$HOME/.cache/numba"
54
+ ```
55
+
56
+ ```powershell
57
+ # Windows — add to $PROFILE
58
+ [Environment]::SetEnvironmentVariable('NUMBA_CACHE_DIR', "$env:USERPROFILE\.cache\numba", 'User')
59
+ ```
60
+
61
+ Compiled artifacts are machine-local anyway (CPU- and Python-version
62
+ specific), so they don't belong on shared NAS regardless of perf.
63
+
64
64
  ## License
65
65
 
66
66
  BSD-3-Clause
@@ -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.
@@ -0,0 +1,220 @@
1
+ # Authoring an ocdkit.viewer plugin
2
+
3
+ This document is the source-of-truth contract for adding a new segmentation
4
+ backend to the ocdkit viewer. It is written so an LLM can read it and produce
5
+ a working plugin in one shot.
6
+
7
+ ## What a plugin is
8
+
9
+ A plugin wires a segmentation tool (Cellpose, StarDist, SAM, your own model,
10
+ …) into the ocdkit viewer. The viewer handles:
11
+
12
+ - Image loading, display, panning, zooming
13
+ - Mask rendering (n-coloring, opacity, color tables)
14
+ - Manual mask editing (brush, fill, split, merge)
15
+ - Affinity-graph editing
16
+ - Session persistence
17
+
18
+ Your plugin only needs to:
19
+
20
+ 1. Declare the parameters the user can tune.
21
+ 2. Provide a `run(image, params) -> mask` function.
22
+ 3. (Optionally) declare lifecycle hooks: model loading, GPU toggle, cache.
23
+
24
+ ## The `SegmentationPlugin` contract
25
+
26
+ ```python
27
+ from ocdkit.viewer import SegmentationPlugin, WidgetSpec
28
+
29
+ plugin = SegmentationPlugin(
30
+ name="my_tool", # stable plugin id (lowercase, no spaces)
31
+ version="0.1.0",
32
+ description="Brief one-line description.",
33
+ homepage="https://github.com/you/my_tool",
34
+ widgets=[
35
+ WidgetSpec(
36
+ name="threshold", # key passed into params
37
+ label="Threshold", # UI label
38
+ kind="slider", # widget kind (see below)
39
+ default=0.5,
40
+ min=0.0, max=1.0, step=0.01,
41
+ help="Pixels above this become foreground.",
42
+ group="Detection", # subsection header
43
+ ),
44
+ # ... more WidgetSpec entries ...
45
+ ],
46
+ run=my_segmentation_function,
47
+ # optional:
48
+ load_models=lambda: ["model_a", "model_b"],
49
+ warmup=lambda model_id: None,
50
+ set_use_gpu=lambda enabled: None,
51
+ clear_cache=lambda: None,
52
+ )
53
+ ```
54
+
55
+ ## `run(image, params) -> mask` contract
56
+
57
+ ```python
58
+ def my_segmentation_function(image: np.ndarray, params: dict) -> np.ndarray:
59
+ """
60
+ image: uint8 numpy array.
61
+ - Shape (H, W) for grayscale.
62
+ - Shape (H, W, C) for multichannel/RGB. C is 1, 2, 3, or 4.
63
+ params: dict whose keys are the WidgetSpec.name strings you declared.
64
+ Values are typed: numbers for slider/number, bool for toggle, str for
65
+ dropdown/text/file/color/colormap.
66
+ returns: 2D int32 (or smaller int) numpy array, shape (H, W).
67
+ 0 = background. Positive integers are instance ids.
68
+ The viewer takes care of n-coloring and rendering.
69
+ """
70
+ ...
71
+ ```
72
+
73
+ ## Widget kinds
74
+
75
+ | `kind` | Type | Required fields | Notes |
76
+ | ------------- | ------- | -------------------------- | ----------------------------------------------- |
77
+ | `slider` | float | `min`, `max` (`step`) | Continuous range with handle |
78
+ | `slider_log` | float | `min` > 0, `max` (`step`) | Log-scaled slider |
79
+ | `number` | float | `min`, `max` (`step`) | Plain number input box |
80
+ | `toggle` | bool | `default` must be bool | Checkbox / switch |
81
+ | `dropdown` | str | `choices` | Single-select from list of strings |
82
+ | `text` | str | — | Free-form text input |
83
+ | `file` | str | — | File path picker |
84
+ | `color` | str | — | Hex color string (`"#ff0000"`) |
85
+ | `colormap` | str | — | Colormap name from ocdkit.cmap |
86
+
87
+ ### Conditional visibility
88
+
89
+ A widget can be hidden until other widgets have specific values:
90
+
91
+ ```python
92
+ WidgetSpec(
93
+ name="manual_value",
94
+ label="Manual value",
95
+ kind="slider", default=0.5, min=0.0, max=1.0,
96
+ visible_when={"method": "manual"}, # show only when method == "manual"
97
+ )
98
+ ```
99
+
100
+ Multiple keys are AND-ed: the widget shows only when all entries match.
101
+
102
+ ### Grouping
103
+
104
+ `group="Detection"` puts widgets under a collapsible "Detection" section in
105
+ the pane. Widgets without `group` go into a default top section.
106
+
107
+ ## Lifecycle hooks
108
+
109
+ All optional. Skip the ones you don't need.
110
+
111
+ | Hook | Signature | When called |
112
+ | --------------- | ---------------------- | ---------------------------------------------------- |
113
+ | `load_models` | `() -> list[str]` | Once on plugin selection — populates the model list. |
114
+ | `warmup` | `(model_id) -> None` | When the user picks a model — preload it. |
115
+ | `set_use_gpu` | `(enabled: bool) -> None` | When the user toggles the GPU switch. |
116
+ | `clear_cache` | `() -> None` | When the user clicks "Clear cache". |
117
+
118
+ If `load_models` is provided, the viewer renders a model dropdown at the top
119
+ of the pane and passes the chosen `model` value in `params["model"]` on each
120
+ `run()` call — you do not need to declare a `WidgetSpec` for it.
121
+
122
+ ## Registering the plugin
123
+
124
+ ### Option A — entry point (recommended for installed packages)
125
+
126
+ In your `pyproject.toml`:
127
+
128
+ ```toml
129
+ [project.entry-points."ocdkit.plugins"]
130
+ my_tool = "my_tool.ocdkit_plugin:plugin"
131
+ ```
132
+
133
+ Where `my_tool/ocdkit_plugin.py` contains:
134
+
135
+ ```python
136
+ from ocdkit.viewer import SegmentationPlugin, WidgetSpec
137
+ plugin = SegmentationPlugin(name="my_tool", ..., run=...)
138
+ ```
139
+
140
+ The viewer auto-discovers all `ocdkit.plugins` entry points at startup.
141
+
142
+ ### Option B — explicit registration (for in-process or tests)
143
+
144
+ ```python
145
+ from ocdkit.viewer import register_plugin
146
+ register_plugin(plugin)
147
+ ```
148
+
149
+ ## Validation
150
+
151
+ Both `WidgetSpec(...)` and `SegmentationPlugin(...)` validate their arguments
152
+ in `__post_init__`. Common errors raised at construction time:
153
+
154
+ - `WidgetSpec.name` empty or non-string → `ValueError`
155
+ - numeric kinds without `min`/`max` → `ValueError`
156
+ - `slider_log` with `min <= 0` → `ValueError`
157
+ - `dropdown` without `choices` → `ValueError`
158
+ - `dropdown` whose `default` is not in `choices` → `ValueError`
159
+ - `toggle` with non-bool `default` → `ValueError`
160
+ - duplicate widget `name` within one plugin → `ValueError`
161
+
162
+ Programmatic schemas for tools and tests:
163
+
164
+ ```python
165
+ from ocdkit.viewer.plugins.schema import (
166
+ widget_spec_schema, # JSON Schema for one WidgetSpec
167
+ plugin_manifest_schema, # JSON Schema for plugin.manifest()
168
+ )
169
+ ```
170
+
171
+ ## Minimal complete example
172
+
173
+ ```python
174
+ # my_tool/ocdkit_plugin.py
175
+ import numpy as np
176
+ from skimage.filters import threshold_otsu
177
+ from skimage.measure import label
178
+ from ocdkit.viewer import SegmentationPlugin, WidgetSpec
179
+
180
+
181
+ def _run(image: np.ndarray, params: dict) -> np.ndarray:
182
+ if image.ndim == 3:
183
+ image = image.mean(axis=-1).astype(image.dtype)
184
+ cutoff = float(params["threshold"]) * 255.0
185
+ if params.get("method") == "otsu":
186
+ cutoff = float(threshold_otsu(image))
187
+ binary = (image > cutoff)
188
+ if params.get("invert"):
189
+ binary = ~binary
190
+ return label(binary).astype(np.int32)
191
+
192
+
193
+ plugin = SegmentationPlugin(
194
+ name="simple_threshold",
195
+ version="0.1.0",
196
+ description="Otsu / manual threshold + connected components.",
197
+ widgets=[
198
+ WidgetSpec("method", "Method", "dropdown",
199
+ default="otsu", choices=["otsu", "manual"]),
200
+ WidgetSpec("threshold", "Threshold", "slider",
201
+ default=0.5, min=0.0, max=1.0, step=0.01,
202
+ visible_when={"method": "manual"}),
203
+ WidgetSpec("invert", "Invert", "toggle", default=False),
204
+ ],
205
+ run=_run,
206
+ )
207
+ ```
208
+
209
+ That's the entire plugin. Once entry-pointed, it appears in the viewer's
210
+ plugin dropdown automatically.
211
+
212
+ ## Conventions
213
+
214
+ - Plugin `name` is lowercase, snake_case. It appears in URLs.
215
+ - Widget `name`s are also snake_case and are the keys in `params`.
216
+ - Don't mutate `image` in `run()`; return a new array.
217
+ - Return masks as 2D `int32` (or `uint16`/`int64` work too). Background = 0.
218
+ - Lifecycle hooks should be cheap or fork to a worker thread internally.
219
+ - Heavy imports (torch, your model library) belong **inside** `run()` or
220
+ inside `warmup()`, not at module top — keeps viewer startup fast.