splatthis 0.2.2__tar.gz → 0.2.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 (128) hide show
  1. {splatthis-0.2.2 → splatthis-0.2.4}/CHANGELOG.md +77 -0
  2. {splatthis-0.2.2 → splatthis-0.2.4}/MANIFEST.in +1 -0
  3. {splatthis-0.2.2/src/splatthis.egg-info → splatthis-0.2.4}/PKG-INFO +24 -17
  4. {splatthis-0.2.2 → splatthis-0.2.4}/README.md +23 -16
  5. {splatthis-0.2.2 → splatthis-0.2.4}/pyproject.toml +3 -1
  6. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/__init__.py +3 -0
  7. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/_version.py +1 -1
  8. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/artifact_backends.py +21 -21
  9. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/artifact_evaluation.py +12 -11
  10. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/browser_export.py +3 -2
  11. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/cli.py +9 -0
  12. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/color.py +17 -6
  13. splatthis-0.2.4/src/splatthis/data/compositor-fidelity.json +175 -0
  14. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/distillation.py +12 -11
  15. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/domain.py +2 -1
  16. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/engine_artifacts.py +4 -3
  17. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/engine_configuration.py +27 -11
  18. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/engine_densification.py +12 -11
  19. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/engine_guidance.py +19 -16
  20. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/engine_initialization.py +10 -9
  21. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/engine_optimization.py +37 -13
  22. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/engine_postfit.py +10 -9
  23. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/engine_state.py +32 -29
  24. splatthis-0.2.4/src/splatthis/expectations.py +102 -0
  25. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/export_common.py +10 -7
  26. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/features.py +23 -16
  27. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/fidelity/analysis.py +16 -11
  28. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/fidelity/evaluator.py +7 -6
  29. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/fidelity/metrics.py +19 -14
  30. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/fidelity/operators.py +3 -2
  31. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/mixed_primitives.py +9 -8
  32. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/mlx_optimizer.py +5 -2
  33. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/mlx_renderer.py +3 -2
  34. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/mlx_stage.py +4 -3
  35. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/pipeline_artifacts.py +2 -2
  36. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/pipeline_phases.py +28 -5
  37. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/pixel_runtime.py +5 -4
  38. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/pptx_export.py +7 -6
  39. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/quality.py +33 -10
  40. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/renderer.py +16 -11
  41. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/splat.py +14 -13
  42. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/storage.py +5 -4
  43. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/svg_export.py +28 -25
  44. {splatthis-0.2.2 → splatthis-0.2.4/src/splatthis.egg-info}/PKG-INFO +24 -17
  45. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis.egg-info/SOURCES.txt +2 -0
  46. {splatthis-0.2.2 → splatthis-0.2.4}/CONTRIBUTING.md +0 -0
  47. {splatthis-0.2.2 → splatthis-0.2.4}/LICENSE +0 -0
  48. {splatthis-0.2.2 → splatthis-0.2.4}/setup.cfg +0 -0
  49. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/adaptive_compute.py +0 -0
  50. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/artifact_gates.py +0 -0
  51. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/artifact_io.py +0 -0
  52. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/browser_capture.py +0 -0
  53. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/budgets.py +0 -0
  54. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/canvas_parity.py +0 -0
  55. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/config.py +0 -0
  56. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/conversion_engine.py +0 -0
  57. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/converter.py +0 -0
  58. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/fidelity/__init__.py +0 -0
  59. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/fidelity/config.py +0 -0
  60. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/fidelity/report.py +0 -0
  61. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/fidelity/stage.py +0 -0
  62. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/io.py +0 -0
  63. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/mlx_losses.py +0 -0
  64. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/mlx_runtime.py +0 -0
  65. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/optimizer.py +0 -0
  66. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/pipeline.py +0 -0
  67. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/profiles.py +0 -0
  68. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/proxies.py +0 -0
  69. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/py.typed +0 -0
  70. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/reporting.py +0 -0
  71. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/roundtrip.py +0 -0
  72. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/svg_recipe_gate.py +0 -0
  73. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/template_assets.py +0 -0
  74. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/__init__.py +0 -0
  75. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/drawingml/background_shape.xml +0 -0
  76. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/drawingml/blur.xml +0 -0
  77. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/drawingml/close_group.xml +0 -0
  78. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/drawingml/edge_segment.xml +0 -0
  79. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/drawingml/effect_list.xml +0 -0
  80. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/drawingml/gradient_fill.xml +0 -0
  81. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/drawingml/gradient_stop.xml +0 -0
  82. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/drawingml/group.xml +0 -0
  83. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/drawingml/shape.xml +0 -0
  84. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/drawingml/shape_no_effect.xml +0 -0
  85. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/drawingml/slide.xml +0 -0
  86. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/drawingml/soft_edge.xml +0 -0
  87. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/drawingml/solid_fill.xml +0 -0
  88. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/pptx/app_props.xml +0 -0
  89. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/pptx/content_types.xml +0 -0
  90. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/pptx/core_props.xml +0 -0
  91. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/pptx/pres_props.xml +0 -0
  92. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/pptx/presentation.xml +0 -0
  93. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/pptx/presentation_rels.xml +0 -0
  94. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/pptx/raster_slide.xml +0 -0
  95. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/pptx/raster_slide_rels.xml +0 -0
  96. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/pptx/root_rels.xml +0 -0
  97. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/pptx/slide_layout.xml +0 -0
  98. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/pptx/slide_layout_rels.xml +0 -0
  99. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/pptx/slide_master.xml +0 -0
  100. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/pptx/slide_master_rels.xml +0 -0
  101. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/pptx/theme.xml +0 -0
  102. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/pptx/vector_slide_rels.xml +0 -0
  103. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/pptx/view_props.xml +0 -0
  104. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/reporting/side_by_side.html +0 -0
  105. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/svg/background.svg +0 -0
  106. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/svg/blur_document.svg +0 -0
  107. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/svg/blur_ellipse.svg +0 -0
  108. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/svg/blur_filter.svg +0 -0
  109. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/svg/close.svg +0 -0
  110. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/svg/edge_group.svg +0 -0
  111. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/svg/edge_line.svg +0 -0
  112. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/svg/edge_path.svg +0 -0
  113. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/svg/ellipse.svg +0 -0
  114. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/svg/empty_document.svg +0 -0
  115. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/svg/palette_background.svg +0 -0
  116. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/svg/palette_document.svg +0 -0
  117. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/svg/palette_ellipse.svg +0 -0
  118. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/svg/palette_gradient.svg +0 -0
  119. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/svg/palette_stop.svg +0 -0
  120. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/svg/scripted_document.svg +0 -0
  121. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/svg/scripted_runtime.js +0 -0
  122. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/svg/standard_document.svg +0 -0
  123. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/svg/standard_gradient.svg +0 -0
  124. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis/templates/svg/standard_stop.svg +0 -0
  125. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis.egg-info/dependency_links.txt +0 -0
  126. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis.egg-info/entry_points.txt +0 -0
  127. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis.egg-info/requires.txt +0 -0
  128. {splatthis-0.2.2 → splatthis-0.2.4}/src/splatthis.egg-info/top_level.txt +0 -0
@@ -2,6 +2,83 @@
2
2
 
3
3
  All notable changes to SplatThis are documented here.
4
4
 
5
+ ## 0.2.4 - 2026-08-01
6
+
7
+ ### Added
8
+
9
+ - `splatthis.compositor_fidelity()`, publishing how faithfully a deployed
10
+ compositor reproduces the *same* splat population the internal reference
11
+ renderer draws. A consumer that supplies its own splats -- projecting 3D
12
+ Gaussians through EWA rather than fitting a bitmap -- inherits only this
13
+ term, not the fitting loss that dominates the headline source-fidelity
14
+ numbers, and previously had to re-derive it from the corpus.
15
+
16
+ compositor SSIM_sRGB min 0.5965 median 0.7540 max 0.8991 (n=21)
17
+ r(content gradient) -0.470
18
+
19
+ The correlation is the substantive result. End-to-end fidelity correlates
20
+ with content gradient at -0.84, which invites the conclusion that a splat
21
+ view's quality is predictable from what it depicts. It is not: that
22
+ dependence is almost entirely the fitting stage, and compositor loss is
23
+ broadly uniform. `is_content_predictable()` returns that judgement so it
24
+ cannot be re-inferred wrongly.
25
+
26
+ - `--save-json` on the CLI. `ConversionRequest.save_json` was honoured but
27
+ unreachable from the command line.
28
+
29
+ ### Changed
30
+
31
+ - Acceptance no longer gates on wall-clock by default. `max_runtime_sec: 60.0`
32
+ failed correct runs against presets that train for minutes -- 8 of 70 stored
33
+ manifests failed on runtime alone, including full-quality runs at SSIM 0.91.
34
+ Runtime remains measured under `acceptance.measured` and honoured when set
35
+ explicitly or by `--time-budget`.
36
+ - Run manifests record `artifact_hash_stable` (false under MLX, with a note)
37
+ and `memory_guard`, so reproducibility and host-memory reductions travel with
38
+ the data instead of living in prose.
39
+ - Corpus result rows carry `schema_version`, and resume re-scores anything
40
+ older, so an evidence level can no longer vanish across a resumed run.
41
+
42
+ ### Fixed
43
+
44
+ - A failing SSIM silently became an inflated one: `except Exception` around the
45
+ skimage call fell through to a global SSIM reading ~0.10 higher, far above
46
+ the 0.50 acceptance floor. Only an absent dependency or an input too small to
47
+ carry a window may fall back now.
48
+ - Colour transforms no longer raise a negative base to a fractional exponent on
49
+ the discarded branch of `np.where`, which warned during ordinary compositing.
50
+ - The splat-orientation convention is pinned by direct tests, and
51
+ `mixed_primitives.py` no longer open-codes it.
52
+
53
+ ## 0.2.3 - 2026-08-01
54
+
55
+ ### Changed
56
+
57
+ - The README showcase renders all four outputs at one size and adds scriptless
58
+ CSS as a fourth column. The previous table sized each image by its own
59
+ intrinsic pixels -- 476x502, 364x384 and 728x768 -- so a three-way comparison
60
+ appeared at three different scales.
61
+
62
+ Sized with explicit `width` attributes rather than by resampling the files:
63
+ PyPI's `readme_renderer` allowlist keeps `width` and `height` on `<img>`, and
64
+ GitHub does too, so every asset stays at native resolution and stays crisp on
65
+ high-DPI displays.
66
+
67
+ CSS appears as a Chromium capture rather than live. Both platforms sanitize
68
+ README markup through an allowlist that omits the `<style>` tag and the
69
+ `style` attribute -- `readme_renderer` runs `nh3.clean()` with no CSS
70
+ sanitizer configured -- so a build made entirely of CSS radial gradients
71
+ arrives as 1,615 unstyled `<div>` elements. Each cell is labelled as live
72
+ vector or as a capture, and captures are noted as lower bounds: they carry
73
+ screen-capture and rescaling losses on top of the compositor's own.
74
+
75
+ ### Fixed
76
+
77
+ - The relative-link guard now covers `src="..."` as well as markdown `](...)`.
78
+ The showcase moved to raw `<img>` tags for sizing, which had made the
79
+ markdown-only check blind to every image on the page -- the same class of
80
+ defect that put 18 dead links on the 0.2.0 project page.
81
+
5
82
  ## 0.2.2 - 2026-08-01
6
83
 
7
84
  ### Changed
@@ -1,4 +1,5 @@
1
1
  include CHANGELOG.md
2
2
  include CONTRIBUTING.md
3
3
  include src/splatthis/py.typed
4
+ recursive-include src/splatthis/data *.json
4
5
  recursive-include src/splatthis/templates *.html *.js *.svg *.xml
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: splatthis
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Fit 2D Gaussian splats to an image and deploy them as SVG, CSS, Canvas, a pixel runtime, or editable PowerPoint shapes
5
5
  Author: Bram Alkema
6
6
  License-Expression: MIT
@@ -47,6 +47,11 @@ Dynamic: license-file
47
47
 
48
48
  # SplatThis
49
49
 
50
+ [![CI](https://github.com/BramAlkema/SplatThis/actions/workflows/ci.yml/badge.svg)](https://github.com/BramAlkema/SplatThis/actions/workflows/ci.yml)
51
+ [![PyPI](https://img.shields.io/pypi/v/splatthis)](https://pypi.org/project/splatthis/)
52
+ [![Python](https://img.shields.io/pypi/pyversions/splatthis)](https://pypi.org/project/splatthis/)
53
+ [![License](https://img.shields.io/pypi/l/splatthis)](https://github.com/BramAlkema/SplatThis/blob/main/LICENSE)
54
+
50
55
  Convert a bitmap into Gaussian splats and deploy them as browser-rendered SVG,
51
56
  scriptless CSS, browser-native Canvas primitives, an accelerated pixel runtime
52
57
  with exact CPU fallbacks, or native editable PowerPoint shapes.
@@ -71,26 +76,28 @@ deployed-fidelity claim.
71
76
  ## See it
72
77
 
73
78
  One 476x502 photograph, fitted to Gaussian splats and emitted to three
74
- compositors that share no rendering code. The middle image is a real SVG, not a
75
- raster of one — your browser is drawing 1,615 gradient ellipses.
79
+ compositors that share no rendering code — vector, stylesheet, and DrawingML.
76
80
 
77
- | Source photograph | Native SVG | Native PowerPoint |
78
- |---|---|---|
79
- | ![source image](https://bramalkema.github.io/SplatThis/demo/source.png) | ![Browser-rendered SVG](https://bramalkema.github.io/SplatThis/demo/chameleon.svg) | ![PowerPoint slideshow capture](https://bramalkema.github.io/SplatThis/demo/chameleon-pptx.png) |
80
- | the input | **0.8665** SSIM · vector · 1.5 MB | **0.7885** SSIM · editable shapes · 161 KB |
81
+ | Source | SVG | Scriptless CSS | PowerPoint |
82
+ |:---:|:---:|:---:|:---:|
83
+ | <img src="https://bramalkema.github.io/SplatThis/demo/source.png" alt="Source photograph" width="210"> | <img src="https://bramalkema.github.io/SplatThis/demo/chameleon.svg" alt="Browser-rendered SVG" width="210"> | <img src="https://bramalkema.github.io/SplatThis/demo/chameleon-css.png" alt="Chromium capture of the scriptless CSS build" width="210"> | <img src="https://bramalkema.github.io/SplatThis/demo/chameleon-pptx.png" alt="Microsoft PowerPoint slideshow capture" width="210"> |
84
+ | the input | **0.8665** SSIM<br>vector · 1.5 MB | **0.8748** SSIM<br>DOM + CSS · 0.8 MB | **0.7885** SSIM<br>editable shapes · 161 KB |
85
+ | — | live vector | Chromium capture | PowerPoint capture |
81
86
 
82
- The third image is the only screenshot here, and it has to be: it is Microsoft
83
- PowerPoint in slideshow, photographed, because there is no other honest way to
84
- show what PowerPoint actually draws. Its score is measured through that capture
85
- and is therefore a lower bound.
87
+ The SVG is the real thing: your browser is drawing 1,615 gradient ellipses, not
88
+ displaying a picture of them. The other two are captures because they have to
89
+ be. GitHub and PyPI both sanitize README markup through an allowlist that omits
90
+ the `<style>` tag and the `style` attribute — PyPI's `readme_renderer` runs
91
+ `nh3.clean()` with no CSS sanitizer configured — so a build made entirely of
92
+ CSS radial gradients arrives as 1,615 unstyled `<div>` elements on either site.
93
+ PowerPoint, obviously, does not run in a browser at all.
86
94
 
87
- **[Open the scriptless CSS build →](https://bramalkema.github.io/SplatThis/demo/chameleon-css.html)**
88
- &nbsp;&nbsp;**0.8748** SSIM · 0.8 MB · no script, no canvas, no SVG, no bitmap
95
+ Both captures are therefore lower bounds: they carry the losses of screen
96
+ capture and rescaling on top of whatever the compositor itself costs.
89
97
 
90
- That one is deliberately a link rather than a picture. It is 1,615 DOM elements
91
- with CSS radial gradients, and a screenshot of it would be indistinguishable
92
- from a screenshot of anything else — the point is that your browser is
93
- compositing it from a stylesheet. View source on it.
98
+ **[Open the scriptless CSS build →](https://bramalkema.github.io/SplatThis/demo/chameleon-css.html)**
99
+ — no script, no canvas, no SVG, no bitmap. View source on it; the capture above
100
+ cannot show that the page is 1,615 DOM elements composited from a stylesheet.
94
101
 
95
102
  Also available: the
96
103
  [editable .pptx](https://bramalkema.github.io/SplatThis/demo/chameleon.pptx),
@@ -1,5 +1,10 @@
1
1
  # SplatThis
2
2
 
3
+ [![CI](https://github.com/BramAlkema/SplatThis/actions/workflows/ci.yml/badge.svg)](https://github.com/BramAlkema/SplatThis/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/splatthis)](https://pypi.org/project/splatthis/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/splatthis)](https://pypi.org/project/splatthis/)
6
+ [![License](https://img.shields.io/pypi/l/splatthis)](https://github.com/BramAlkema/SplatThis/blob/main/LICENSE)
7
+
3
8
  Convert a bitmap into Gaussian splats and deploy them as browser-rendered SVG,
4
9
  scriptless CSS, browser-native Canvas primitives, an accelerated pixel runtime
5
10
  with exact CPU fallbacks, or native editable PowerPoint shapes.
@@ -24,26 +29,28 @@ deployed-fidelity claim.
24
29
  ## See it
25
30
 
26
31
  One 476x502 photograph, fitted to Gaussian splats and emitted to three
27
- compositors that share no rendering code. The middle image is a real SVG, not a
28
- raster of one — your browser is drawing 1,615 gradient ellipses.
32
+ compositors that share no rendering code — vector, stylesheet, and DrawingML.
29
33
 
30
- | Source photograph | Native SVG | Native PowerPoint |
31
- |---|---|---|
32
- | ![source image](https://bramalkema.github.io/SplatThis/demo/source.png) | ![Browser-rendered SVG](https://bramalkema.github.io/SplatThis/demo/chameleon.svg) | ![PowerPoint slideshow capture](https://bramalkema.github.io/SplatThis/demo/chameleon-pptx.png) |
33
- | the input | **0.8665** SSIM · vector · 1.5 MB | **0.7885** SSIM · editable shapes · 161 KB |
34
+ | Source | SVG | Scriptless CSS | PowerPoint |
35
+ |:---:|:---:|:---:|:---:|
36
+ | <img src="https://bramalkema.github.io/SplatThis/demo/source.png" alt="Source photograph" width="210"> | <img src="https://bramalkema.github.io/SplatThis/demo/chameleon.svg" alt="Browser-rendered SVG" width="210"> | <img src="https://bramalkema.github.io/SplatThis/demo/chameleon-css.png" alt="Chromium capture of the scriptless CSS build" width="210"> | <img src="https://bramalkema.github.io/SplatThis/demo/chameleon-pptx.png" alt="Microsoft PowerPoint slideshow capture" width="210"> |
37
+ | the input | **0.8665** SSIM<br>vector · 1.5 MB | **0.8748** SSIM<br>DOM + CSS · 0.8 MB | **0.7885** SSIM<br>editable shapes · 161 KB |
38
+ | — | live vector | Chromium capture | PowerPoint capture |
34
39
 
35
- The third image is the only screenshot here, and it has to be: it is Microsoft
36
- PowerPoint in slideshow, photographed, because there is no other honest way to
37
- show what PowerPoint actually draws. Its score is measured through that capture
38
- and is therefore a lower bound.
40
+ The SVG is the real thing: your browser is drawing 1,615 gradient ellipses, not
41
+ displaying a picture of them. The other two are captures because they have to
42
+ be. GitHub and PyPI both sanitize README markup through an allowlist that omits
43
+ the `<style>` tag and the `style` attribute — PyPI's `readme_renderer` runs
44
+ `nh3.clean()` with no CSS sanitizer configured — so a build made entirely of
45
+ CSS radial gradients arrives as 1,615 unstyled `<div>` elements on either site.
46
+ PowerPoint, obviously, does not run in a browser at all.
39
47
 
40
- **[Open the scriptless CSS build →](https://bramalkema.github.io/SplatThis/demo/chameleon-css.html)**
41
- &nbsp;&nbsp;**0.8748** SSIM · 0.8 MB · no script, no canvas, no SVG, no bitmap
48
+ Both captures are therefore lower bounds: they carry the losses of screen
49
+ capture and rescaling on top of whatever the compositor itself costs.
42
50
 
43
- That one is deliberately a link rather than a picture. It is 1,615 DOM elements
44
- with CSS radial gradients, and a screenshot of it would be indistinguishable
45
- from a screenshot of anything else — the point is that your browser is
46
- compositing it from a stylesheet. View source on it.
51
+ **[Open the scriptless CSS build →](https://bramalkema.github.io/SplatThis/demo/chameleon-css.html)**
52
+ — no script, no canvas, no SVG, no bitmap. View source on it; the capture above
53
+ cannot show that the page is 1,615 DOM elements composited from a stylesheet.
47
54
 
48
55
  Also available: the
49
56
  [editable .pptx](https://bramalkema.github.io/SplatThis/demo/chameleon.pptx),
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splatthis"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  description = "Fit 2D Gaussian splats to an image and deploy them as SVG, CSS, Canvas, a pixel runtime, or editable PowerPoint shapes"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -87,6 +87,8 @@ splatthis = [
87
87
  # PEP 561 marker: without it, type checkers ignore this package's
88
88
  # annotations entirely and every consumer sees Any.
89
89
  "py.typed",
90
+ # Published fidelity expectations, read by consumers at runtime.
91
+ "data/*.json",
90
92
  "templates/**/*.html",
91
93
  "templates/**/*.js",
92
94
  "templates/**/*.svg",
@@ -10,6 +10,7 @@ from .browser_export import generate_css_splat_html, generate_native_canvas_html
10
10
  from .config import ConversionRequest, ConverterConfig
11
11
  from .converter import PNG2SVGConverter
12
12
  from .domain import EvidenceLevel, SplatScene
13
+ from .expectations import CompositorFidelity, compositor_fidelity
13
14
  from .pixel_runtime import (
14
15
  generate_parallax_pixel_runtime_html,
15
16
  generate_pixel_runtime_html,
@@ -28,6 +29,8 @@ __all__ = [
28
29
  "RawSplat",
29
30
  "SplatScene",
30
31
  "EvidenceLevel",
32
+ "CompositorFidelity",
33
+ "compositor_fidelity",
31
34
  "ConversionRequest",
32
35
  "ConverterConfig",
33
36
  "load_png",
@@ -1,3 +1,3 @@
1
1
  """Single source of truth for the public package version."""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.2.4"
@@ -6,7 +6,7 @@ from abc import ABC, abstractmethod
6
6
  from pathlib import Path
7
7
  from typing import TYPE_CHECKING, Any, Dict, Mapping
8
8
 
9
- import numpy as np
9
+ import numpy.typing as npt
10
10
 
11
11
  from .artifact_evaluation import (
12
12
  evaluate_css_export_quality,
@@ -56,7 +56,7 @@ class ArtifactBackend(ABC):
56
56
  converter: "ConversionEngine",
57
57
  request: ConversionRequest,
58
58
  scene: SplatScene,
59
- target_linear_rgb: np.ndarray,
59
+ target_linear_rgb: npt.NDArray[Any],
60
60
  artifacts_path: Path | None,
61
61
  ) -> ArtifactPayload:
62
62
  """Emit the primary target exactly once."""
@@ -79,8 +79,8 @@ class ArtifactBackend(ABC):
79
79
  *,
80
80
  converter: "ConversionEngine",
81
81
  request: ConversionRequest,
82
- target_linear_rgb: np.ndarray,
83
- fallback_linear_rgb: np.ndarray,
82
+ target_linear_rgb: npt.NDArray[Any],
83
+ fallback_linear_rgb: npt.NDArray[Any],
84
84
  proxy_quality: Mapping[str, Any],
85
85
  ) -> ArtifactEvaluation:
86
86
  quality = dict(proxy_quality)
@@ -104,8 +104,8 @@ class ArtifactBackend(ABC):
104
104
  evaluate: Any,
105
105
  success_kind: str,
106
106
  unavailable_kind: str,
107
- target_linear_rgb: np.ndarray,
108
- fallback_linear_rgb: np.ndarray,
107
+ target_linear_rgb: npt.NDArray[Any],
108
+ fallback_linear_rgb: npt.NDArray[Any],
109
109
  ) -> ArtifactEvaluation:
110
110
  quality = dict(proxy_quality)
111
111
  if request.output_path is not None:
@@ -159,7 +159,7 @@ class SvgArtifactBackend(ArtifactBackend):
159
159
  converter: "ConversionEngine",
160
160
  request: ConversionRequest,
161
161
  scene: SplatScene,
162
- target_linear_rgb: np.ndarray,
162
+ target_linear_rgb: npt.NDArray[Any],
163
163
  artifacts_path: Path | None,
164
164
  ) -> ArtifactPayload:
165
165
  metadata: Dict[str, Any] = {}
@@ -229,8 +229,8 @@ class SvgArtifactBackend(ArtifactBackend):
229
229
  *,
230
230
  converter: "ConversionEngine",
231
231
  request: ConversionRequest,
232
- target_linear_rgb: np.ndarray,
233
- fallback_linear_rgb: np.ndarray,
232
+ target_linear_rgb: npt.NDArray[Any],
233
+ fallback_linear_rgb: npt.NDArray[Any],
234
234
  proxy_quality: Mapping[str, Any],
235
235
  ) -> ArtifactEvaluation:
236
236
  return self._browser_evaluation(
@@ -258,7 +258,7 @@ class DrawingMLArtifactBackend(ArtifactBackend):
258
258
  converter: "ConversionEngine",
259
259
  request: ConversionRequest,
260
260
  scene: SplatScene,
261
- target_linear_rgb: np.ndarray,
261
+ target_linear_rgb: npt.NDArray[Any],
262
262
  artifacts_path: Path | None,
263
263
  ) -> ArtifactPayload:
264
264
  return ArtifactPayload(
@@ -304,8 +304,8 @@ class PptxArtifactBackend(DrawingMLArtifactBackend):
304
304
  *,
305
305
  converter: "ConversionEngine",
306
306
  request: ConversionRequest,
307
- target_linear_rgb: np.ndarray,
308
- fallback_linear_rgb: np.ndarray,
307
+ target_linear_rgb: npt.NDArray[Any],
308
+ fallback_linear_rgb: npt.NDArray[Any],
309
309
  proxy_quality: Mapping[str, Any],
310
310
  ) -> ArtifactEvaluation:
311
311
  return ArtifactEvaluation(
@@ -335,7 +335,7 @@ class CanvasArtifactBackend(ArtifactBackend):
335
335
  converter: "ConversionEngine",
336
336
  request: ConversionRequest,
337
337
  scene: SplatScene,
338
- target_linear_rgb: np.ndarray,
338
+ target_linear_rgb: npt.NDArray[Any],
339
339
  artifacts_path: Path | None,
340
340
  ) -> ArtifactPayload:
341
341
  parallax_strength = float(
@@ -361,8 +361,8 @@ class CanvasArtifactBackend(ArtifactBackend):
361
361
  *,
362
362
  converter: "ConversionEngine",
363
363
  request: ConversionRequest,
364
- target_linear_rgb: np.ndarray,
365
- fallback_linear_rgb: np.ndarray,
364
+ target_linear_rgb: npt.NDArray[Any],
365
+ fallback_linear_rgb: npt.NDArray[Any],
366
366
  proxy_quality: Mapping[str, Any],
367
367
  ) -> ArtifactEvaluation:
368
368
  return self._browser_evaluation(
@@ -392,7 +392,7 @@ class CssArtifactBackend(ArtifactBackend):
392
392
  converter: "ConversionEngine",
393
393
  request: ConversionRequest,
394
394
  scene: SplatScene,
395
- target_linear_rgb: np.ndarray,
395
+ target_linear_rgb: npt.NDArray[Any],
396
396
  artifacts_path: Path | None,
397
397
  ) -> ArtifactPayload:
398
398
  parallax_strength = float(
@@ -425,8 +425,8 @@ class CssArtifactBackend(ArtifactBackend):
425
425
  *,
426
426
  converter: "ConversionEngine",
427
427
  request: ConversionRequest,
428
- target_linear_rgb: np.ndarray,
429
- fallback_linear_rgb: np.ndarray,
428
+ target_linear_rgb: npt.NDArray[Any],
429
+ fallback_linear_rgb: npt.NDArray[Any],
430
430
  proxy_quality: Mapping[str, Any],
431
431
  ) -> ArtifactEvaluation:
432
432
  return self._browser_evaluation(
@@ -468,7 +468,7 @@ class PixelRuntimeArtifactBackend(ArtifactBackend):
468
468
  converter: "ConversionEngine",
469
469
  request: ConversionRequest,
470
470
  scene: SplatScene,
471
- target_linear_rgb: np.ndarray,
471
+ target_linear_rgb: npt.NDArray[Any],
472
472
  artifacts_path: Path | None,
473
473
  ) -> ArtifactPayload:
474
474
  parallax_strength = self._parallax_strength(converter)
@@ -502,8 +502,8 @@ class PixelRuntimeArtifactBackend(ArtifactBackend):
502
502
  *,
503
503
  converter: "ConversionEngine",
504
504
  request: ConversionRequest,
505
- target_linear_rgb: np.ndarray,
506
- fallback_linear_rgb: np.ndarray,
505
+ target_linear_rgb: npt.NDArray[Any],
506
+ fallback_linear_rgb: npt.NDArray[Any],
507
507
  proxy_quality: Mapping[str, Any],
508
508
  ) -> ArtifactEvaluation:
509
509
  if self.requires_governing_render(converter):
@@ -6,6 +6,7 @@ import logging
6
6
  from typing import Any, Callable, Dict, Optional, Tuple
7
7
 
8
8
  import numpy as np
9
+ import numpy.typing as npt
9
10
 
10
11
  from .quality import compute_quality_metrics
11
12
 
@@ -13,11 +14,11 @@ logger = logging.getLogger(__name__)
13
14
 
14
15
 
15
16
  def _evaluate_browser_export_quality(
16
- target_linear_rgb: np.ndarray,
17
- fallback_linear_rgb: Optional[np.ndarray] = None,
17
+ target_linear_rgb: npt.NDArray[Any],
18
+ fallback_linear_rgb: Optional[npt.NDArray[Any]] = None,
18
19
  *,
19
20
  artifact_name: str,
20
- capture: Callable[[int, int], Tuple[np.ndarray, str]],
21
+ capture: Callable[[int, int], Tuple[npt.NDArray[Any], str]],
21
22
  ) -> Dict[str, Any]:
22
23
  """Grade a browser artifact while keeping proxy evidence diagnostic-only."""
23
24
 
@@ -59,9 +60,9 @@ def _evaluate_browser_export_quality(
59
60
 
60
61
 
61
62
  def evaluate_svg_export_quality(
62
- target_linear_rgb: np.ndarray,
63
+ target_linear_rgb: npt.NDArray[Any],
63
64
  svg_path: str,
64
- fallback_linear_rgb: Optional[np.ndarray] = None,
65
+ fallback_linear_rgb: Optional[npt.NDArray[Any]] = None,
65
66
  ) -> Dict[str, Any]:
66
67
  from .browser_capture import render_svg_in_browser_to_linear_rgb
67
68
 
@@ -76,9 +77,9 @@ def evaluate_svg_export_quality(
76
77
 
77
78
 
78
79
  def evaluate_css_export_quality(
79
- target_linear_rgb: np.ndarray,
80
+ target_linear_rgb: npt.NDArray[Any],
80
81
  html_path: str,
81
- fallback_linear_rgb: Optional[np.ndarray] = None,
82
+ fallback_linear_rgb: Optional[npt.NDArray[Any]] = None,
82
83
  ) -> Dict[str, Any]:
83
84
  from .browser_capture import render_css_html_in_browser_to_linear_rgb
84
85
 
@@ -93,9 +94,9 @@ def evaluate_css_export_quality(
93
94
 
94
95
 
95
96
  def evaluate_native_canvas_export_quality(
96
- target_linear_rgb: np.ndarray,
97
+ target_linear_rgb: npt.NDArray[Any],
97
98
  html_path: str,
98
- fallback_linear_rgb: Optional[np.ndarray] = None,
99
+ fallback_linear_rgb: Optional[npt.NDArray[Any]] = None,
99
100
  ) -> Dict[str, Any]:
100
101
  from .browser_capture import render_canvas_html_in_browser_to_linear_rgb
101
102
 
@@ -110,9 +111,9 @@ def evaluate_native_canvas_export_quality(
110
111
 
111
112
 
112
113
  def evaluate_pixel_runtime_export_quality(
113
- target_linear_rgb: np.ndarray,
114
+ target_linear_rgb: npt.NDArray[Any],
114
115
  html_path: str,
115
- fallback_linear_rgb: Optional[np.ndarray] = None,
116
+ fallback_linear_rgb: Optional[npt.NDArray[Any]] = None,
116
117
  ) -> Dict[str, Any]:
117
118
  from .browser_capture import render_pixel_runtime_html_in_browser_to_linear_rgb
118
119
 
@@ -7,6 +7,7 @@ from html import escape as escape_html
7
7
  from typing import Any, Dict, List, Optional
8
8
 
9
9
  import numpy as np
10
+ import numpy.typing as npt
10
11
 
11
12
  from .color import linear_to_srgb
12
13
  from .export_common import (
@@ -24,7 +25,7 @@ def generate_css_splat_html(
24
25
  splats: List[GaussianSplat],
25
26
  width: int,
26
27
  height: int,
27
- background_linear_rgb: Optional[np.ndarray] = None,
28
+ background_linear_rgb: Optional[npt.NDArray[Any]] = None,
28
29
  title: str = "SplatThis CSS",
29
30
  parallax_strength: float = 0.0,
30
31
  hover_grid_size: int = 10,
@@ -210,7 +211,7 @@ def generate_native_canvas_html(
210
211
  splats: List[GaussianSplat],
211
212
  width: int,
212
213
  height: int,
213
- background_linear_rgb: Optional[np.ndarray] = None,
214
+ background_linear_rgb: Optional[npt.NDArray[Any]] = None,
214
215
  title: str = "SplatThis Canvas",
215
216
  parallax_strength: float = 0.0,
216
217
  k_sigma: float = 2.5,
@@ -551,6 +551,14 @@ def build_parser() -> argparse.ArgumentParser:
551
551
  default=None,
552
552
  help="Optional directory for run manifest + iteration dumps",
553
553
  )
554
+ parser.add_argument(
555
+ "--save-json",
556
+ action="store_true",
557
+ help=(
558
+ "Also write the splat population as canonical raw JSON beside the "
559
+ "output, for re-scoring or re-emitting without retraining"
560
+ ),
561
+ )
554
562
  parser.add_argument("-v", "--verbose", action="store_true", help="Verbose logging")
555
563
  return parser
556
564
 
@@ -696,6 +704,7 @@ def _run_conversion(args: argparse.Namespace) -> int:
696
704
  output_format=args.fmt,
697
705
  seed=args.seed,
698
706
  artifacts_dir=args.artifacts_dir,
707
+ save_json=args.save_json,
699
708
  verbose=args.verbose,
700
709
  )
701
710
  print(f"Wrote {output}")
@@ -2,33 +2,44 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from typing import Any
6
+
5
7
  import numpy as np
8
+ import numpy.typing as npt
9
+
10
+ # `np.where` evaluates both branches, so the power term sees every input --
11
+ # including the negatives the linear branch exists to handle. Raising a
12
+ # negative base to a fractional exponent yields NaN and a RuntimeWarning, which
13
+ # `where` then discards. The result is correct but the warning is real, and it
14
+ # fires during ordinary compositing wherever a linear value dips below zero.
15
+ # The float32 variants below already clamped for this reason; these did not.
16
+ _POWER_FLOOR = 1e-12
6
17
 
7
18
 
8
- def srgb_to_linear(srgb: np.ndarray) -> np.ndarray:
19
+ def srgb_to_linear(srgb: npt.NDArray[Any]) -> npt.NDArray[Any]:
9
20
  """Convert normalized sRGB values to linear RGB."""
10
21
 
11
22
  values = np.asarray(srgb)
12
23
  return np.where(
13
24
  values <= 0.04045,
14
25
  values / 12.92,
15
- np.power((values + 0.055) / 1.055, 2.4),
26
+ np.power(np.maximum((values + 0.055) / 1.055, _POWER_FLOOR), 2.4),
16
27
  )
17
28
 
18
29
 
19
- def linear_to_srgb(linear: np.ndarray) -> np.ndarray:
30
+ def linear_to_srgb(linear: npt.NDArray[Any]) -> npt.NDArray[Any]:
20
31
  """Convert normalized linear RGB values to clipped sRGB."""
21
32
 
22
33
  values = np.asarray(linear)
23
34
  srgb = np.where(
24
35
  values <= 0.0031308,
25
36
  12.92 * values,
26
- 1.055 * np.power(values, 1.0 / 2.4) - 0.055,
37
+ 1.055 * np.power(np.maximum(values, _POWER_FLOOR), 1.0 / 2.4) - 0.055,
27
38
  )
28
39
  return np.clip(srgb, 0.0, 1.0)
29
40
 
30
41
 
31
- def linear_to_srgb_float32(linear: np.ndarray) -> np.ndarray:
42
+ def linear_to_srgb_float32(linear: npt.NDArray[Any]) -> npt.NDArray[Any]:
32
43
  """Convert to sRGB with the clipped float32 semantics used by scorers."""
33
44
 
34
45
  values = np.clip(np.asarray(linear), 0.0, 1.0)
@@ -39,7 +50,7 @@ def linear_to_srgb_float32(linear: np.ndarray) -> np.ndarray:
39
50
  ).astype(np.float32)
40
51
 
41
52
 
42
- def srgb_to_linear_float32(srgb: np.ndarray) -> np.ndarray:
53
+ def srgb_to_linear_float32(srgb: npt.NDArray[Any]) -> npt.NDArray[Any]:
43
54
  """Convert to linear RGB with the clipped float32 scorer semantics."""
44
55
 
45
56
  values = np.clip(np.asarray(srgb), 0.0, 1.0)