ocdkit 0.0.4__tar.gz → 0.0.5__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 (217) hide show
  1. {ocdkit-0.0.4 → ocdkit-0.0.5}/.gitignore +11 -0
  2. {ocdkit-0.0.4/src/ocdkit.egg-info → ocdkit-0.0.5}/PKG-INFO +3 -1
  3. {ocdkit-0.0.4 → ocdkit-0.0.5}/badges/coverage.svg +1 -1
  4. {ocdkit-0.0.4 → ocdkit-0.0.5}/badges/tests.svg +1 -1
  5. {ocdkit-0.0.4 → ocdkit-0.0.5}/docs/plot-backend-roadmap.md +9 -10
  6. {ocdkit-0.0.4 → ocdkit-0.0.5}/pyproject.toml +6 -5
  7. {ocdkit-0.0.4 → ocdkit-0.0.5}/scripts/bench_contour_alignment.py +2 -3
  8. ocdkit-0.0.5/scripts/bench_hdr_cmap.py +181 -0
  9. {ocdkit-0.0.4 → ocdkit-0.0.5}/scripts/bench_vector_contours.py +13 -4
  10. ocdkit-0.0.5/scripts/bench_vector_contours_tier2.py +108 -0
  11. ocdkit-0.0.5/scripts/check_compare_pixels.py +77 -0
  12. ocdkit-0.0.5/scripts/check_hdr_cmap.py +65 -0
  13. ocdkit-0.0.5/scripts/check_jxl_p3_bytes.py +64 -0
  14. ocdkit-0.0.5/scripts/check_playwright_render.py +136 -0
  15. ocdkit-0.0.5/scripts/check_uhdr_sdr_base.py +73 -0
  16. ocdkit-0.0.5/scripts/coverage_cross_device.env.example +11 -0
  17. ocdkit-0.0.5/scripts/coverage_cross_device.sh +56 -0
  18. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/array/parallel.py +22 -2
  19. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/cli/paths.py +1 -1
  20. ocdkit-0.0.5/src/ocdkit/io/figure.py +2897 -0
  21. ocdkit-0.0.5/src/ocdkit/io/figure_server.py +1210 -0
  22. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/load/module.py +22 -3
  23. ocdkit-0.0.5/src/ocdkit/plot/__init__.py +58 -0
  24. ocdkit-0.0.5/src/ocdkit/plot/bench.py +208 -0
  25. ocdkit-0.0.5/src/ocdkit/plot/composite.py +141 -0
  26. ocdkit-0.0.5/src/ocdkit/plot/composite_grid.py +371 -0
  27. ocdkit-0.0.4/scripts/bench_vector_contours_tier2.py → ocdkit-0.0.5/src/ocdkit/plot/contour.py +236 -266
  28. ocdkit-0.0.5/src/ocdkit/plot/display.py +534 -0
  29. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/plot/figure.py +5 -2
  30. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/plot/grid.py +6 -2
  31. ocdkit-0.0.5/src/ocdkit/plot/hdr_cmap.py +342 -0
  32. ocdkit-0.0.5/src/ocdkit/plot/image_grid.py +877 -0
  33. ocdkit-0.0.5/src/ocdkit/plot/imports.py +19 -0
  34. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/plot/label.py +3 -0
  35. ocdkit-0.0.5/src/ocdkit/plot/layout.py +589 -0
  36. ocdkit-0.0.5/src/ocdkit/plot/style.py +132 -0
  37. ocdkit-0.0.5/src/ocdkit/plot/svg.py +927 -0
  38. ocdkit-0.0.5/src/ocdkit/plot/text_metrics.py +190 -0
  39. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/testing/__init__.py +1 -1
  40. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/utils/kwargs.py +2 -0
  41. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/utils/paths.py +1 -1
  42. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/app.py +4 -4
  43. {ocdkit-0.0.4 → ocdkit-0.0.5/src/ocdkit.egg-info}/PKG-INFO +3 -1
  44. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit.egg-info/SOURCES.txt +18 -0
  45. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit.egg-info/requires.txt +2 -0
  46. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/test_import_cycles.py +1 -1
  47. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/test_module_collisions.py +1 -1
  48. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/test_module_discovery.py +1 -1
  49. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/test_plot_grid.py +12 -12
  50. ocdkit-0.0.4/scripts/coverage_cross_device.sh +0 -41
  51. ocdkit-0.0.4/src/ocdkit/plot/__init__.py +0 -11
  52. ocdkit-0.0.4/src/ocdkit/plot/contour.py +0 -102
  53. ocdkit-0.0.4/src/ocdkit/plot/display.py +0 -208
  54. ocdkit-0.0.4/src/ocdkit/plot/imports.py +0 -9
  55. {ocdkit-0.0.4 → ocdkit-0.0.5}/.github/workflows/test_and_deploy.yml +0 -0
  56. {ocdkit-0.0.4 → ocdkit-0.0.5}/LICENSE +0 -0
  57. {ocdkit-0.0.4 → ocdkit-0.0.5}/MANIFEST.in +0 -0
  58. {ocdkit-0.0.4 → ocdkit-0.0.5}/README.md +0 -0
  59. {ocdkit-0.0.4 → ocdkit-0.0.5}/docs/plugin-authoring.md +0 -0
  60. {ocdkit-0.0.4 → ocdkit-0.0.5}/docs/pywebview-desktop-integration.md +0 -0
  61. {ocdkit-0.0.4 → ocdkit-0.0.5}/scripts/bench_colorize.py +0 -0
  62. {ocdkit-0.0.4 → ocdkit-0.0.5}/setup.cfg +0 -0
  63. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/__init__.py +0 -0
  64. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/__main__.py +0 -0
  65. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/array/__init__.py +0 -0
  66. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/array/convert.py +0 -0
  67. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/array/filters.py +0 -0
  68. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/array/imports.py +0 -0
  69. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/array/index.py +0 -0
  70. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/array/morphology.py +0 -0
  71. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/array/normalize.py +0 -0
  72. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/array/ops.py +0 -0
  73. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/array/spatial.py +0 -0
  74. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/array/transform.py +0 -0
  75. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/array/union_find.py +0 -0
  76. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/array/warp.py +0 -0
  77. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/cli/__init__.py +0 -0
  78. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/cli/__main__.py +0 -0
  79. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/cli/main.py +0 -0
  80. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/cli/migrate.py +0 -0
  81. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/desktop/__init__.py +0 -0
  82. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/desktop/pinning.py +0 -0
  83. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/imports.py +0 -0
  84. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/io/__init__.py +0 -0
  85. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/io/files.py +0 -0
  86. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/io/image.py +0 -0
  87. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/io/imports.py +0 -0
  88. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/io/path.py +0 -0
  89. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/io/result.py +0 -0
  90. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/load/__init__.py +0 -0
  91. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/load/object.py +0 -0
  92. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/logging/__init__.py +0 -0
  93. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/logging/handler.py +0 -0
  94. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/measure/__init__.py +0 -0
  95. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/measure/bbox.py +0 -0
  96. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/measure/diameter.py +0 -0
  97. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/measure/imports.py +0 -0
  98. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/measure/medoid.py +0 -0
  99. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/measure/metrics.py +0 -0
  100. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/plot/color.py +0 -0
  101. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/plot/defaults.py +0 -0
  102. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/plot/export.py +0 -0
  103. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/plot/ncolor.py +0 -0
  104. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/testing/collisions.py +0 -0
  105. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/testing/imports.py +0 -0
  106. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/tls/__init__.py +0 -0
  107. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/tls/external_ca.py +0 -0
  108. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/tls/hostnames.py +0 -0
  109. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/tls/imports.py +0 -0
  110. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/tls/local_ca.py +0 -0
  111. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/tls/paths.py +0 -0
  112. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/tls/trust.py +0 -0
  113. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/utils/__init__.py +0 -0
  114. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/utils/collections.py +0 -0
  115. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/utils/gpu.py +0 -0
  116. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/__init__.py +0 -0
  117. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/__main__.py +0 -0
  118. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/assets.py +0 -0
  119. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/cli.py +0 -0
  120. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/demo.html +0 -0
  121. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/dependencies.py +0 -0
  122. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/exceptions.py +0 -0
  123. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/masks.py +0 -0
  124. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/middleware.py +0 -0
  125. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/model_registry.py +0 -0
  126. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/plugins/__init__.py +0 -0
  127. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/plugins/base.py +0 -0
  128. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/plugins/registry.py +0 -0
  129. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/plugins/schema.py +0 -0
  130. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/plugins/threshold.py +0 -0
  131. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/routers/__init__.py +0 -0
  132. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/routers/index.py +0 -0
  133. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/routers/log.py +0 -0
  134. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/routers/mask.py +0 -0
  135. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/routers/plugin.py +0 -0
  136. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/routers/segment.py +0 -0
  137. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/routers/session_routes.py +0 -0
  138. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/routers/system.py +0 -0
  139. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/routers/trust.py +0 -0
  140. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/routes.py +0 -0
  141. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/sample_image.py +0 -0
  142. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/schemas.py +0 -0
  143. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/segmentation.py +0 -0
  144. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/session.py +0 -0
  145. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/system.py +0 -0
  146. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/app.js +0 -0
  147. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/css/controls.css +0 -0
  148. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/css/layout.css +0 -0
  149. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/css/tools.css +0 -0
  150. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/css/viewer.css +0 -0
  151. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/html/left-panel.html +0 -0
  152. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/html/sidebar.html +0 -0
  153. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/html/viewer.html +0 -0
  154. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/affinity.svg +0 -0
  155. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/arrow-back-up.svg +0 -0
  156. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/arrow-forward-up.svg +0 -0
  157. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/dbscan-nested-arcs.svg +0 -0
  158. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/download.svg +0 -0
  159. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/droplet-half-2.svg +0 -0
  160. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/eraser.svg +0 -0
  161. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/home-2.svg +0 -0
  162. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/minus.svg +0 -0
  163. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/palette.svg +0 -0
  164. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/pencil.svg +0 -0
  165. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/plus.svg +0 -0
  166. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/rotate-rectangle.svg +0 -0
  167. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/icons/topology-star.svg +0 -0
  168. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/index.html +0 -0
  169. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/brush.js +0 -0
  170. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/colormap.js +0 -0
  171. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/debug-apple-material.js +0 -0
  172. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/file-navigation.js +0 -0
  173. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/history.js +0 -0
  174. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/interactions.js +0 -0
  175. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/logging.js +0 -0
  176. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/mask-pipeline.js +0 -0
  177. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/painting.js +0 -0
  178. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/plugin-panel.js +0 -0
  179. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/pointer-state.js +0 -0
  180. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/state-persistence.js +0 -0
  181. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/tooltip-editor.js +0 -0
  182. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/ui-utils.js +0 -0
  183. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit/viewer/web/js/wasm_fill.c +0 -0
  184. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit.egg-info/dependency_links.txt +0 -0
  185. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit.egg-info/entry_points.txt +0 -0
  186. {ocdkit-0.0.4 → ocdkit-0.0.5}/src/ocdkit.egg-info/top_level.txt +0 -0
  187. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/e2e/__init__.py +0 -0
  188. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/e2e/conftest.py +0 -0
  189. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/e2e/test_browser_smoke.py +0 -0
  190. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/e2e/test_pywebview_snapshot.py +0 -0
  191. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/fixtures/multichan_3c_4x4.czi +0 -0
  192. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/fixtures/tiny_8x8.czi +0 -0
  193. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/test_array.py +0 -0
  194. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/test_gpu.py +0 -0
  195. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/test_io.py +0 -0
  196. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/test_measure.py +0 -0
  197. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/test_morphology.py +0 -0
  198. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/test_paths_migration.py +0 -0
  199. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/test_plot_color.py +0 -0
  200. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/test_plot_contour.py +0 -0
  201. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/test_plot_display.py +0 -0
  202. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/test_plot_export.py +0 -0
  203. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/test_plot_figure.py +0 -0
  204. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/test_plot_label.py +0 -0
  205. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/test_plot_notebook.py +0 -0
  206. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/test_registration.py +0 -0
  207. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/test_slice.py +0 -0
  208. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/test_spatial.py +0 -0
  209. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/viewer/__init__.py +0 -0
  210. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/viewer/test_active_plugin_cache.py +0 -0
  211. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/viewer/test_app.py +0 -0
  212. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/viewer/test_async_dispatch.py +0 -0
  213. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/viewer/test_envelope_and_middleware.py +0 -0
  214. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/viewer/test_plugin_contract.py +0 -0
  215. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/viewer/test_session_eviction.py +0 -0
  216. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/viewer/test_title_config.py +0 -0
  217. {ocdkit-0.0.4 → ocdkit-0.0.5}/tests/viewer/test_ui_mode.py +0 -0
@@ -6,6 +6,17 @@
6
6
  # Local tooling
7
7
  .claude/
8
8
 
9
+ # Local machine-specific config for bench / coverage scripts
10
+ scripts/coverage_cross_device.env
11
+ scripts/bench_paths.env
12
+
13
+ # Generated artifacts from bench / demo scripts
14
+ scripts/bench_*.png
15
+ scripts/check_*.png
16
+ scripts/check_*.svg
17
+ scripts/check_*.html
18
+ figures/
19
+
9
20
  # Editor / NAS junk
10
21
  *.bak
11
22
  .!*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ocdkit
3
- Version: 0.0.4
3
+ Version: 0.0.5
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
@@ -11,6 +11,7 @@ Requires-Dist: scipy
11
11
  Requires-Dist: scikit-image>=0.26
12
12
  Requires-Dist: tifffile
13
13
  Requires-Dist: imagecodecs
14
+ Requires-Dist: opencodecs>=0.1.12
14
15
  Requires-Dist: matplotlib
15
16
  Requires-Dist: fastremap
16
17
  Requires-Dist: edt
@@ -24,6 +25,7 @@ Requires-Dist: cmap
24
25
  Requires-Dist: tqdm
25
26
  Requires-Dist: platformdirs
26
27
  Requires-Dist: cryptography
28
+ Requires-Dist: lxml
27
29
  Provides-Extra: viewer
28
30
  Requires-Dist: fastapi; extra == "viewer"
29
31
  Requires-Dist: uvicorn[standard]; extra == "viewer"
@@ -1 +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>
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="114" height="20" role="img" aria-label="coverage: 44.55%"><title>coverage: 44.55%</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">44.55%</text><text x="865" y="140" transform="scale(.1)" fill="#fff" textLength="430">44.55%</text></g></svg>
@@ -1 +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>
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="68" height="20" role="img" aria-label="tests: 408"><title>tests: 408</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">408</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">408</text></g></svg>
@@ -1,18 +1,17 @@
1
1
  # Plot backend roadmap
2
2
 
3
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.
4
+ **Driver:** matplotlib accounts for ~80% of warm-call time in a typical spectra-plotting call (~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
5
 
7
6
  ## Context
8
7
 
9
- `ocdkit.plot` and `hiprpy.plot` already own ~10 K LOC of plotting code:
8
+ `ocdkit.plot` and a downstream plot package together own ~10 K LOC of plotting code:
10
9
 
11
10
  | Package | LOC |
12
11
  |---|---|
13
12
  | `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 |
13
+ | Downstream non-WGPU plot code (datashade, cell, text, barcode, line, background, …) | 4,641 |
14
+ | Downstream WGPU plot code (lines, scatter, aggregators — already custom GPU primitives) | 4,783 |
16
15
 
17
16
  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
17
 
@@ -29,7 +28,7 @@ Shallow-cloned each candidate, counted Python LOC excluding tests, sphinx, sampl
29
28
  | plotly | 66 MB | 13 K relevant (+ ~600 K autogen) | JS bundle as data | Most LOC is auto-generated trait validators |
30
29
  | datoviz | 137 MB | 8 K Py wrapper | 140 K C++ Vulkan engine | Heaviest binary footprint, fastest renderer |
31
30
  | 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 |
31
+ | pygfx | 84 MB | 20 K | None — uses `wgpu-py` | Same backend ocdkit already uses |
33
32
 
34
33
  ## Tradeoff summary
35
34
 
@@ -63,20 +62,20 @@ If pygfx covers ≥80% of our needs at ≥80% of the visual quality, **adopt pyg
63
62
 
64
63
  ### Step 2 (only if pygfx is insufficient): roll our own
65
64
 
66
- Estimated scope, building on existing `hiprpy.plot.wgpu.lines`:
65
+ Estimated scope, building on the existing WGPU line rasterizer:
67
66
 
68
67
  | Module | LOC est. | Notes |
69
68
  |---|---|---|
70
69
  | `ocdkit.plot.figure_v2` | 400 | New `Figure` class owning the wgpu canvas + axes layout; coexists with current matplotlib `figure()` so migration is piecewise |
71
70
  | `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 |
71
+ | `ocdkit.plot.wgpu.text` | 600 | FreeType-rendered glyph atlas + WGSL textured-quad shader. The one piece of genuinely new rasterization work |
73
72
  | `ocdkit.plot.legend` | 200 | Boxed layout: text + marker swatches |
74
73
  | `ocdkit.plot.hover` | 300 | Mouse → data-coord lookup → DOM/Jupyter tooltip overlay. Generalize the classification-debugger pattern |
75
74
  | `ocdkit.plot.export` | 400 | PNG (numpy→PIL), SVG (string templates), maybe PDF (skip if PIL→PNG covers science exports) |
76
75
  | 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
76
  | **Total** | **~3.3 K** | New code, all in our control |
78
77
 
79
- Plus: deletion of matplotlib-specific code paths in `hiprpy.plot` (~1 K LOC saved) and removal of matplotlib runtime dep.
78
+ Plus: deletion of matplotlib-specific code paths in the downstream plot package (~1 K LOC saved) and removal of matplotlib runtime dep.
80
79
 
81
80
  ### Why not pygfx + own axes?
82
81
 
@@ -91,6 +90,6 @@ A hybrid path is also viable: use pygfx for the rasterization layer (lines/text/
91
90
 
92
91
  ## Concrete next action
93
92
 
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.
93
+ Spike `plot_spectra_pygfx` against a representative spectra notebook (e.g. `scope.mixed_spectra[-1]` from any existing demo). Compare visual output side-by-side with `plot_spectra_wgpu` and `plot_spectra_cpu`. Decide pygfx-vs-roll-own from that single comparison.
95
94
 
96
95
  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.
@@ -19,6 +19,10 @@ dependencies = [
19
19
  "scikit-image>=0.26",
20
20
  "tifffile",
21
21
  "imagecodecs",
22
+ # opencodecs.uhdr.read_thumbnail_bytes (≥0.1.12) is the fast path
23
+ # in resolve_uhdr_thumb_bytes; ≥0.1.10 has the _fast_log2 fix
24
+ # required for correct gain-map encoding via encode_native.
25
+ "opencodecs>=0.1.12",
22
26
  "matplotlib",
23
27
  "fastremap",
24
28
  "edt",
@@ -32,6 +36,7 @@ dependencies = [
32
36
  "tqdm",
33
37
  "platformdirs",
34
38
  "cryptography",
39
+ "lxml",
35
40
  ]
36
41
 
37
42
  [project.optional-dependencies]
@@ -82,11 +87,7 @@ testpaths = ["tests"]
82
87
  source = ["ocdkit"]
83
88
 
84
89
  [tool.coverage.paths]
85
- source = [
86
- "src/ocdkit",
87
- "/Volumes/DataDrive/ocdkit/src/ocdkit",
88
- "/home/kcutler/DataDrive/ocdkit/src/ocdkit",
89
- ]
90
+ source = ["src/ocdkit"]
90
91
 
91
92
  [tool.coverage.report]
92
93
  show_missing = true
@@ -24,8 +24,7 @@ import numpy as np
24
24
  sys.path.insert(0, str(Path(__file__).parent))
25
25
 
26
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
27
+ from ocdkit.plot.contour import vector_contours, vector_contours_marching
29
28
 
30
29
 
31
30
  def make_mask():
@@ -73,7 +72,7 @@ def main():
73
72
  color='red', linewidth=1.5)
74
73
 
75
74
  fig.tight_layout()
76
- out = Path('/Volumes/DataDrive/ocdkit/scripts/bench_contour_alignment.png')
75
+ out = Path(__file__).resolve().parent / 'bench_contour_alignment.png'
77
76
  fig.savefig(out, dpi=150, bbox_inches='tight')
78
77
  print(f"saved: {out}")
79
78
 
@@ -0,0 +1,181 @@
1
+ """Demo: HDR-lifted colormaps through the css-img / SVG image_grid pipe.
2
+
3
+ Renders a grayscale gradient with viridis / magma / inferno / rdbu in:
4
+ - SDR (uint8 P3, current default behavior)
5
+ - HDR (float linear-P3 with values > 1.0, peak ≈ 1600 nits)
6
+
7
+ Saves figures/hdr_cmap_demo.svg — open in Safari to see HDR on an EDR
8
+ display (Chrome on macOS works too). Also prints the lifted LUTs'
9
+ out-of-SDR-range fractions so you can confirm the lift actually used
10
+ the headroom.
11
+ """
12
+ import sys
13
+ import time
14
+ from pathlib import Path
15
+
16
+ import numpy as np
17
+
18
+ import matplotlib.pyplot as plt
19
+ plt.rcParams.update({
20
+ 'figure.facecolor': 'none',
21
+ 'axes.facecolor': 'none',
22
+ 'savefig.facecolor': 'none',
23
+ 'axes.edgecolor': 'gray',
24
+ 'axes.labelcolor': 'gray',
25
+ 'xtick.color': 'gray',
26
+ 'ytick.color': 'gray',
27
+ 'text.color': 'gray',
28
+ 'axes.titlecolor': 'gray',
29
+ })
30
+
31
+ REPO = Path(__file__).resolve().parents[1]
32
+ sys.path.insert(0, str(REPO / 'src'))
33
+
34
+ from ocdkit.plot.hdr_cmap import ( # noqa: E402
35
+ make_hdr_cmap_lut, apply_hdr_cmap,
36
+ SDR_WHITE_NITS, HDR_PEAK_NITS_DEFAULT,
37
+ )
38
+ from ocdkit.plot import imshow # noqa: E402
39
+
40
+ # linear Display-P3 → Y (relative luminance)
41
+ P3_Y_WEIGHTS = np.array([0.2289745641, 0.6917385218, 0.0792869141])
42
+
43
+
44
+ def lut_stats(name):
45
+ sdr = make_hdr_cmap_lut(name, hdr_jz=0.155,
46
+ hdr_peak_nits=SDR_WHITE_NITS) # SDR baseline
47
+ hdr = make_hdr_cmap_lut(name, hdr_peak_nits=HDR_PEAK_NITS_DEFAULT)
48
+ # both LUTs are linear-P3 normalized so 1.0 ≡ their respective peak.
49
+ # Convert to absolute nits via Y-weighted sum × peak-nits.
50
+ peak_sdr_nits = float((sdr @ P3_Y_WEIGHTS).max() * SDR_WHITE_NITS)
51
+ peak_hdr_nits = float((hdr @ P3_Y_WEIGHTS).max() * HDR_PEAK_NITS_DEFAULT)
52
+ print(
53
+ f" {name:12s} "
54
+ f"SDR peak Y={peak_sdr_nits:6.1f} nits "
55
+ f"HDR peak Y={peak_hdr_nits:6.1f} nits "
56
+ f"lift {peak_hdr_nits / max(peak_sdr_nits, 1e-3):4.2f}x"
57
+ )
58
+
59
+
60
+ def main():
61
+ print("Lift stats (peak_mult=7.88 ≈ 1600 nits):")
62
+ for name in ('viridis', 'magma', 'inferno', 'plasma',
63
+ 'cividis', 'RdBu', 'twilight'):
64
+ lut_stats(name)
65
+
66
+ H, W = 64, 1024
67
+ grad = np.tile(np.linspace(0, 1, W, dtype=np.float32), (H, 1))
68
+
69
+ figures_dir = REPO / 'figures'
70
+ figures_dir.mkdir(exist_ok=True)
71
+
72
+ print("\nFull pipeline (HDR + SDR side by side):")
73
+ cmaps = ['viridis', 'magma', 'inferno', 'plasma', 'cividis']
74
+ for name in cmaps:
75
+ t0 = time.perf_counter()
76
+ rgb_hdr = apply_hdr_cmap(grad, name)
77
+ dt = (time.perf_counter() - t0) * 1000
78
+ peak_nits = float((rgb_hdr @ P3_Y_WEIGHTS).max() * HDR_PEAK_NITS_DEFAULT)
79
+ print(f" apply_hdr_cmap({name}) {dt:6.1f} ms "
80
+ f"linear-P3 max={float(rgb_hdr.max()):.3f} "
81
+ f"peak Y={peak_nits:6.1f} nits dtype={rgb_hdr.dtype}")
82
+
83
+ # Two routes through the new `cmap=` kwarg on imshow:
84
+ out_hdr = figures_dir / 'hdr_cmap_demo_hdr.svg'
85
+ out_sdr = figures_dir / 'hdr_cmap_demo_sdr.svg'
86
+
87
+ # HDR route: pre-applied float linear-P3 tiles. (imshow's per-call
88
+ # cmap kwarg applies one cmap to all 2D items, so to show per-cmap
89
+ # tiles in a single grid we apply manually here.)
90
+ hdr_tiles = [apply_hdr_cmap(grad, c) for c in cmaps]
91
+ fig_hdr = imshow(hdr_tiles, figsize=2, titles=cmaps)
92
+ out_hdr.write_text(fig_hdr._inner.to_string())
93
+ print(f"\nSaved HDR demo: {out_hdr}")
94
+
95
+ # SDR baseline through the same `cmap=` kwarg, using hdr=False.
96
+ from cmap import Colormap
97
+ sdr_tiles = []
98
+ for c in cmaps:
99
+ cm = Colormap(c)
100
+ rgba = np.asarray(cm(grad))
101
+ sdr_tiles.append((rgba[..., :3] * 255).astype(np.uint8))
102
+ fig_sdr = imshow(sdr_tiles, figsize=2, titles=cmaps)
103
+ out_sdr.write_text(fig_sdr._inner.to_string())
104
+ print(f"Saved SDR demo: {out_sdr}")
105
+
106
+ # And one figure exercising the new imshow(cmap=, hdr=True) wiring
107
+ # directly on a single 2D array.
108
+ out_kw = figures_dir / 'hdr_cmap_demo_kwarg.svg'
109
+ fig_kw = imshow(grad, cmap='viridis', hdr=True, figsize=4,
110
+ titles='imshow(grad, cmap="viridis", hdr=True)')
111
+ out_kw.write_text(fig_kw._inner.to_string())
112
+ print(f"Saved kwarg demo: {out_kw}")
113
+
114
+ # ─────────────────────────────────────────────────────────────────
115
+ # SDR-fallback A/B test
116
+ # ─────────────────────────────────────────────────────────────────
117
+ # LEFT tile : jxl-p3, uint8 sRGB-curve viridis (native SDR cmap).
118
+ # RIGHT tile : Ultra-HDR JPEG (apply_hdr_cmap → linear-P3 float →
119
+ # libuhdr base + gain map). The SDR base layer of the
120
+ # UHDR JPEG is the *native non-lifted cmap* (via
121
+ # HdrCmapArray._sdr_base_p3_u8), not libuhdr's
122
+ # auto-tone-map — which used to desaturate bright
123
+ # stops (the original reason for switching off PQ-JXL).
124
+ #
125
+ # Expected on HDR display: RIGHT tile glows (~600 nits peak).
126
+ # Expected on SDR display (or HDR display toggled off): LEFT and
127
+ # RIGHT should look pixel-identical — both are the same SDR cmap.
128
+ bumps = np.tile(np.linspace(0, 1, 1024, dtype=np.float32), (192, 1))
129
+ out_sdr_only = figures_dir / 'hdr_cmap_compare_sdr.svg'
130
+ out_hdr_only = figures_dir / 'hdr_cmap_compare_hdr.svg'
131
+
132
+ fig_sdr_only = imshow(bumps, cmap='viridis', hdr=False, figsize=6,
133
+ titles='viridis · jxl-p3 (native SDR cmap)')
134
+ out_sdr_only.write_text(fig_sdr_only._inner.to_string())
135
+
136
+ fig_hdr_only = imshow(bumps, cmap='viridis', hdr=True, figsize=6,
137
+ titles='viridis · UHDR (HDR-lifted, SDR base = native cmap)')
138
+ out_hdr_only.write_text(fig_hdr_only._inner.to_string())
139
+
140
+ out_html = figures_dir / 'hdr_cmap_compare.html'
141
+ out_html.write_text(f"""<!doctype html>
142
+ <html><head><meta charset="utf-8">
143
+ <title>HDR vs SDR cmap A/B</title>
144
+ <style>
145
+ body {{
146
+ background: #111; color: #ccc; font-family: -apple-system, sans-serif;
147
+ margin: 20px; line-height: 1.45;
148
+ }}
149
+ .row {{ display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; }}
150
+ .col {{ flex: 1 1 480px; min-width: 380px; }}
151
+ h2 {{ font-size: 14px; color: #888; margin: 6px 0; font-weight: normal; }}
152
+ p {{ font-size: 13px; color: #888; max-width: 80ch; }}
153
+ code {{ color: #ddc; }}
154
+ </style></head>
155
+ <body>
156
+ <h1 style="font-size:16px;color:#aaa">HDR vs SDR cmap A/B (Ultra-HDR + native SDR base)</h1>
157
+ <p>Toggle display HDR (macOS: System Settings → Displays → "High Dynamic
158
+ Range"). On HDR the right tile glows. On SDR the two should be
159
+ pixel-identical — the UHDR's SDR base layer is the same native viridis
160
+ the left tile uses.</p>
161
+ <div class="row">
162
+ <div class="col"><h2>SDR baseline (jxl-p3, uint8 viridis)</h2>
163
+ <object data="hdr_cmap_compare_sdr.svg" type="image/svg+xml"
164
+ style="width:100%"></object>
165
+ </div>
166
+ <div class="col"><h2>HDR UHDR (apply_hdr_cmap viridis)</h2>
167
+ <object data="hdr_cmap_compare_hdr.svg" type="image/svg+xml"
168
+ style="width:100%"></object>
169
+ </div>
170
+ </div>
171
+ </body></html>""")
172
+ print(f"Saved A/B page: {out_html}")
173
+ print(f" SDR JXL: {out_sdr_only}")
174
+ print(f" HDR UHDR: {out_hdr_only}")
175
+ print(" Open the HTML in Safari, then toggle Displays → HDR off.")
176
+ print(" Tiles should now look identical in SDR mode; HDR mode the")
177
+ print(" right tile gains brightness from the gain map.")
178
+
179
+
180
+ if __name__ == '__main__':
181
+ main()
@@ -1,15 +1,18 @@
1
1
  """Benchmark current vector_contours pipeline vs a vectorized prototype.
2
2
 
3
- Loads the ncolor example mask and renders both the current
3
+ Loads a label-mask image and renders both the current
4
4
  ocdkit.plot.contour.vector_contours output and a prototype implementation
5
5
  side by side, with timings.
6
6
 
7
+ Set ``OCDKIT_BENCH_MASK`` to the path of a label-mask PNG before running.
8
+
7
9
  Run:
8
- python scripts/bench_vector_contours.py
10
+ OCDKIT_BENCH_MASK=/path/to/labels.png python scripts/bench_vector_contours.py
9
11
  """
10
12
 
11
13
  from __future__ import annotations
12
14
 
15
+ import os
13
16
  import time
14
17
  from pathlib import Path
15
18
 
@@ -245,7 +248,13 @@ def _time_call(fn, repeat=3):
245
248
 
246
249
 
247
250
  def main():
248
- mask = skimage.io.imread('/Volumes/DataDrive/ncolor/test_files/example.png')
251
+ mask_path = os.environ.get('OCDKIT_BENCH_MASK')
252
+ if not mask_path:
253
+ raise SystemExit(
254
+ "Set OCDKIT_BENCH_MASK to the path of a label-mask PNG "
255
+ "(e.g. export OCDKIT_BENCH_MASK=/path/to/labels.png)."
256
+ )
257
+ mask = skimage.io.imread(mask_path)
249
258
  print(f"mask shape={mask.shape} n_labels={len(np.unique(mask)) - 1}")
250
259
 
251
260
  def run_current():
@@ -302,7 +311,7 @@ def main():
302
311
  axes[1, 1].set_title("zoomed crop", fontsize=10)
303
312
  fig.tight_layout()
304
313
 
305
- out = Path('/Volumes/DataDrive/ocdkit/scripts/bench_vector_contours.png')
314
+ out = Path(__file__).resolve().parent / 'bench_vector_contours.png'
306
315
  fig.savefig(out, dpi=150, bbox_inches='tight')
307
316
  print(f"\nsaved: {out}")
308
317
 
@@ -0,0 +1,108 @@
1
+ """Bench: marching-squares vector contour pipeline vs legacy.
2
+
3
+ The library implementation now lives in ``ocdkit.plot.contour``
4
+ (``vector_contours_marching`` / ``cells_to_polygons`` / ``cells_to_webgpu_mesh``)
5
+ -- this file is just a bench runner that compares it against the legacy
6
+ ``vector_contours`` and the Tier-1 prototype.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import time
12
+ from pathlib import Path
13
+
14
+ import matplotlib.pyplot as plt
15
+ import numpy as np
16
+ import skimage.io
17
+
18
+ from ocdkit.plot.contour import (
19
+ vector_contours,
20
+ vector_contours_marching,
21
+ )
22
+ # Tier 1 prototype kept in scripts/ for comparison only.
23
+ from bench_vector_contours import vector_contours_fast
24
+
25
+
26
+ def _time_call(fn, repeat=3):
27
+ fn() # warm-up (incl numba compile)
28
+ ts = []
29
+ for _ in range(repeat):
30
+ t0 = time.perf_counter(); fn(); ts.append(time.perf_counter() - t0)
31
+ return min(ts), ts
32
+
33
+
34
+ def main():
35
+ mask_path = os.environ.get('OCDKIT_BENCH_MASK')
36
+ if not mask_path:
37
+ raise SystemExit(
38
+ "Set OCDKIT_BENCH_MASK to the path of a label-mask PNG "
39
+ "(e.g. export OCDKIT_BENCH_MASK=/path/to/labels.png)."
40
+ )
41
+ mask = skimage.io.imread(mask_path)
42
+ print(f"mask shape={mask.shape} n_labels={len(np.unique(mask)) - 1}")
43
+
44
+ def run_current():
45
+ fig, ax = plt.subplots(figsize=(5, 5))
46
+ ax.imshow(mask, cmap='gray', interpolation='nearest')
47
+ vector_contours(fig, ax, mask, smooth_factor=5, color='r', linewidth=1.0)
48
+ ax.set_axis_off()
49
+ plt.close(fig)
50
+
51
+ def run_tier1():
52
+ fig, ax = plt.subplots(figsize=(5, 5))
53
+ ax.imshow(mask, cmap='gray', interpolation='nearest')
54
+ vector_contours_fast(fig, ax, mask, smooth_sigma=2.0, color='r', linewidth=1.0)
55
+ ax.set_axis_off()
56
+ plt.close(fig)
57
+
58
+ def run_tier2():
59
+ fig, ax = plt.subplots(figsize=(5, 5))
60
+ ax.imshow(mask, cmap='gray', interpolation='nearest')
61
+ vector_contours_marching(fig, ax, mask, smooth_sigma=2.0, color='r', linewidth=1.0)
62
+ ax.set_axis_off()
63
+ plt.close(fig)
64
+
65
+ print("\nBenchmarking (best of 3) ...")
66
+ cur_best, _ = _time_call(run_current, repeat=3)
67
+ print(f" current : {cur_best*1000:6.1f} ms")
68
+ t1_best, _ = _time_call(run_tier1, repeat=3)
69
+ print(f" tier 1 : {t1_best*1000:6.1f} ms ({cur_best/t1_best:.2f}x)")
70
+ t2_best, _ = _time_call(run_tier2, repeat=3)
71
+ print(f" tier 2 : {t2_best*1000:6.1f} ms ({cur_best/t2_best:.2f}x)")
72
+
73
+ H, W = mask.shape
74
+ crop_y = slice(H // 3, H // 3 + 80)
75
+ crop_x = slice(W // 3, W // 3 + 80)
76
+
77
+ fig, axes = plt.subplots(2, 3, figsize=(15, 11))
78
+ for a in axes[0]:
79
+ a.imshow(mask, cmap='gray', interpolation='nearest')
80
+ a.set_axis_off()
81
+ vector_contours(fig, axes[0, 0], mask, smooth_factor=5, color='r', linewidth=1.0)
82
+ vector_contours_fast(fig, axes[0, 1], mask, smooth_sigma=2.0, color='r', linewidth=1.0)
83
+ vector_contours_marching(fig, axes[0, 2], mask, smooth_sigma=2.0, color='r', linewidth=1.0)
84
+ axes[0, 0].set_title(f"Current vector_contours\n{cur_best*1000:.1f} ms", fontsize=11)
85
+ axes[0, 1].set_title(f"Tier 1 (graph + Gaussian + LineCollection)\n"
86
+ f"{t1_best*1000:.1f} ms ({cur_best/t1_best:.2f}x)", fontsize=11)
87
+ axes[0, 2].set_title(f"Tier 2 (marching squares numba kernel)\n"
88
+ f"{t2_best*1000:.1f} ms ({cur_best/t2_best:.2f}x)", fontsize=11)
89
+
90
+ for a in axes[1]:
91
+ a.imshow(mask[crop_y, crop_x], cmap='gray', interpolation='nearest',
92
+ extent=(crop_x.start, crop_x.stop, crop_y.stop, crop_y.start))
93
+ a.set_xlim(crop_x.start, crop_x.stop)
94
+ a.set_ylim(crop_y.stop, crop_y.start)
95
+ a.set_axis_off()
96
+ a.set_title("zoomed crop", fontsize=10)
97
+ vector_contours(fig, axes[1, 0], mask, smooth_factor=5, color='r', linewidth=1.5)
98
+ vector_contours_fast(fig, axes[1, 1], mask, smooth_sigma=2.0, color='r', linewidth=1.5)
99
+ vector_contours_marching(fig, axes[1, 2], mask, smooth_sigma=2.0, color='r', linewidth=1.5)
100
+
101
+ fig.tight_layout()
102
+ out = Path(__file__).resolve().parent / 'bench_vector_contours_tier2.png'
103
+ fig.savefig(out, dpi=150, bbox_inches='tight')
104
+ print(f"\nsaved: {out}")
105
+
106
+
107
+ if __name__ == '__main__':
108
+ main()
@@ -0,0 +1,77 @@
1
+ """Direct pixel diff between the two A/B SVGs:
2
+ - hdr_cmap_compare_sdr.svg → decode the embedded JXL.
3
+ - hdr_cmap_compare_hdr.svg → decode the UHDR JPEG's SDR base.
4
+
5
+ If the byte diff is below JPEG-q95 noise (~3-5 max, <1 mean), the two
6
+ tiles contain the same SDR pixels, and any visible difference on screen
7
+ is browser/decoder behavior, not our encoder."""
8
+ import base64
9
+ import re
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ import numpy as np
14
+
15
+ REPO = Path(__file__).resolve().parents[1]
16
+ sys.path.insert(0, str(REPO / 'src'))
17
+
18
+ import opencodecs
19
+ import opencodecs.uhdr as uhdr
20
+
21
+ figs = REPO / 'figures'
22
+ sdr_svg = (figs / 'hdr_cmap_compare_sdr.svg').read_text()
23
+ hdr_svg = (figs / 'hdr_cmap_compare_hdr.svg').read_text()
24
+
25
+
26
+ def extract_b64(svg, mime):
27
+ m = re.search(fr'data:{re.escape(mime)};base64,([A-Za-z0-9+/=]+)', svg)
28
+ if not m:
29
+ raise SystemExit(f"no {mime} in svg")
30
+ return base64.b64decode(m.group(1))
31
+
32
+
33
+ # SDR tile: plain JXL with display-p3 color tag.
34
+ sdr_jxl_bytes = extract_b64(sdr_svg, 'image/jxl')
35
+ sdr_tile_u8 = opencodecs.read(sdr_jxl_bytes)
36
+ if sdr_tile_u8.shape[-1] == 4:
37
+ sdr_tile_u8 = sdr_tile_u8[..., :3]
38
+ print(f"SDR tile JXL → {sdr_tile_u8.shape} {sdr_tile_u8.dtype}")
39
+
40
+ # HDR tile: UHDR JPEG. Pull the raw SDR base out via libuhdr's api-4
41
+ # decode and decode the inner JPEG with imagecodecs.
42
+ hdr_jpeg = extract_b64(hdr_svg, 'image/jpeg')
43
+ parts = uhdr.decode(hdr_jpeg, want_hdr=False, want_gainmap=False, want_base=True)
44
+ base_jpeg = parts['base_compressed']
45
+ sdr_base_u8 = opencodecs.read(base_jpeg)
46
+ if sdr_base_u8.shape[-1] == 4:
47
+ sdr_base_u8 = sdr_base_u8[..., :3]
48
+ print(f"UHDR SDR base → {sdr_base_u8.shape} {sdr_base_u8.dtype}")
49
+
50
+ if sdr_tile_u8.shape != sdr_base_u8.shape:
51
+ print(f"\nSHAPE MISMATCH — can't diff directly")
52
+ sys.exit(1)
53
+
54
+ diff = sdr_tile_u8.astype(np.int16) - sdr_base_u8.astype(np.int16)
55
+ print(f"\nPixel-level diff (SDR-JXL vs UHDR-SDR-base):")
56
+ print(f" max abs: {int(np.abs(diff).max())}")
57
+ print(f" mean abs: {float(np.abs(diff).mean()):.2f}")
58
+ print(f" p99 abs: {int(np.percentile(np.abs(diff), 99))}")
59
+
60
+ # Per-row diff to spot banding (which would alternate across rows in
61
+ # a column-gradient image — i.e. the gradient is column-only, all rows
62
+ # identical).
63
+ mid = sdr_tile_u8.shape[0] // 2
64
+ print(f"\nSample row {mid}, first 10 columns:")
65
+ print(f" SDR JXL: {sdr_tile_u8[mid, :10].tolist()}")
66
+ print(f" UHDR base: {sdr_base_u8[mid, :10].tolist()}")
67
+
68
+ # Banding signature: count unique colors per row. Banding from a
69
+ # 256-stop LUT would compress 1024 columns into ≤256 unique values.
70
+ unique_jxl = len(np.unique(sdr_tile_u8[mid].view(np.dtype((np.void,
71
+ sdr_tile_u8[mid].dtype.itemsize * 3)))))
72
+ unique_uhdr = len(np.unique(sdr_base_u8[mid].view(np.dtype((np.void,
73
+ sdr_base_u8[mid].dtype.itemsize * 3)))))
74
+ print(f"\nUnique colors in mid row "
75
+ f"({sdr_tile_u8.shape[1]} cols wide):")
76
+ print(f" SDR JXL: {unique_jxl}")
77
+ print(f" UHDR base: {unique_uhdr}")
@@ -0,0 +1,65 @@
1
+ """Quick numpy-only sanity check for ocdkit.plot.hdr_cmap.
2
+
3
+ No matplotlib — imports just the colormap-lift math + cmap package.
4
+ Prints peak Y luminance (in nits) for SDR baseline vs HDR-lifted LUT
5
+ to confirm the lift actually pushes brightness up.
6
+ """
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import numpy as np
11
+
12
+ REPO = Path(__file__).resolve().parents[1]
13
+ sys.path.insert(0, str(REPO / 'src'))
14
+
15
+ from ocdkit.plot.hdr_cmap import ( # noqa: E402
16
+ make_hdr_cmap_lut,
17
+ SDR_WHITE_NITS, HDR_PEAK_NITS_DEFAULT,
18
+ _XYZ_FROM_SRGB, _P3_FROM_XYZ, _srgb_to_linear,
19
+ )
20
+ from cmap import Colormap # noqa: E402
21
+
22
+ # Linear Display-P3 → relative Y (BT.2020-ish weights for P3)
23
+ P3_Y_WEIGHTS = np.array([0.2289745641, 0.6917385218, 0.0792869141])
24
+
25
+
26
+ def naive_sdr_lut(name, hdr_peak_nits=HDR_PEAK_NITS_DEFAULT, n=256):
27
+ """Original SDR cmap converted to linear-P3, normalized to encoder peak.
28
+ No JzAzBz lift — yardstick for what the lifted LUT improves on."""
29
+ cm = Colormap(name)
30
+ rgba = np.asarray(cm(np.linspace(0, 1, n)))
31
+ lin_srgb = _srgb_to_linear(rgba[:, :3])
32
+ XYZ_abs = lin_srgb @ _XYZ_FROM_SRGB.T * SDR_WHITE_NITS
33
+ return np.maximum(XYZ_abs @ _P3_FROM_XYZ.T / hdr_peak_nits, 0)
34
+
35
+
36
+ def peak_nits(lut_normed, peak):
37
+ return float((lut_normed @ P3_Y_WEIGHTS).max() * peak)
38
+
39
+
40
+ names = ['viridis', 'magma', 'inferno', 'plasma', 'cividis',
41
+ 'RdBu', 'twilight']
42
+ print(f"{'cmap':12s} {'SDR peak':>10s} {'HDR peak':>10s} lift "
43
+ f"max linear-P3")
44
+ for nm in names:
45
+ sdr = naive_sdr_lut(nm)
46
+ hdr = make_hdr_cmap_lut(nm)
47
+ p_sdr = peak_nits(sdr, HDR_PEAK_NITS_DEFAULT)
48
+ p_hdr = peak_nits(hdr, HDR_PEAK_NITS_DEFAULT)
49
+ print(
50
+ f" {nm:10s} "
51
+ f"{p_sdr:7.1f} nits {p_hdr:7.1f} nits "
52
+ f"{p_hdr / max(p_sdr, 1e-3):4.2f}x "
53
+ f"SDR={float(sdr.max()):.3f} HDR={float(hdr.max()):.3f}"
54
+ )
55
+
56
+ # Quick visual sanity: how the brightest viridis stop differs.
57
+ sdr_v = naive_sdr_lut('viridis')
58
+ hdr_v = make_hdr_cmap_lut('viridis')
59
+ print("\nviridis brightest stop (last index):")
60
+ print(f" SDR lin-P3 RGB = {sdr_v[-1]} ({sdr_v[-1] @ P3_Y_WEIGHTS * HDR_PEAK_NITS_DEFAULT:.1f} nits)")
61
+ print(f" HDR lin-P3 RGB = {hdr_v[-1]} ({hdr_v[-1] @ P3_Y_WEIGHTS * HDR_PEAK_NITS_DEFAULT:.1f} nits)")
62
+
63
+ print("\nDarkest stop (purple):")
64
+ print(f" SDR {sdr_v[0]} ({sdr_v[0] @ P3_Y_WEIGHTS * HDR_PEAK_NITS_DEFAULT:.2f} nits)")
65
+ print(f" HDR {hdr_v[0]} ({hdr_v[0] @ P3_Y_WEIGHTS * HDR_PEAK_NITS_DEFAULT:.2f} nits)")