rotobot-nuke 0.3.0__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 (39) hide show
  1. rotobot_nuke-0.3.0/CHANGELOG.md +119 -0
  2. rotobot_nuke-0.3.0/LICENSE +21 -0
  3. rotobot_nuke-0.3.0/MANIFEST.in +7 -0
  4. rotobot_nuke-0.3.0/PKG-INFO +280 -0
  5. rotobot_nuke-0.3.0/README.md +231 -0
  6. rotobot_nuke-0.3.0/pyproject.toml +44 -0
  7. rotobot_nuke-0.3.0/setup.cfg +4 -0
  8. rotobot_nuke-0.3.0/src/rotobot_nuke/__init__.py +81 -0
  9. rotobot_nuke-0.3.0/src/rotobot_nuke/_version.py +1 -0
  10. rotobot_nuke-0.3.0/src/rotobot_nuke/cache.py +152 -0
  11. rotobot_nuke-0.3.0/src/rotobot_nuke/hierarchy.py +339 -0
  12. rotobot_nuke-0.3.0/src/rotobot_nuke/importer.py +596 -0
  13. rotobot_nuke-0.3.0/src/rotobot_nuke/menu.py +151 -0
  14. rotobot_nuke-0.3.0/src/rotobot_nuke/py.typed +0 -0
  15. rotobot_nuke-0.3.0/src/rotobot_nuke/reader.py +506 -0
  16. rotobot_nuke-0.3.0/src/rotobot_nuke/undersample.py +830 -0
  17. rotobot_nuke-0.3.0/src/rotobot_nuke.egg-info/PKG-INFO +280 -0
  18. rotobot_nuke-0.3.0/src/rotobot_nuke.egg-info/SOURCES.txt +37 -0
  19. rotobot_nuke-0.3.0/src/rotobot_nuke.egg-info/dependency_links.txt +1 -0
  20. rotobot_nuke-0.3.0/src/rotobot_nuke.egg-info/entry_points.txt +2 -0
  21. rotobot_nuke-0.3.0/src/rotobot_nuke.egg-info/requires.txt +3 -0
  22. rotobot_nuke-0.3.0/src/rotobot_nuke.egg-info/top_level.txt +1 -0
  23. rotobot_nuke-0.3.0/tests/conftest.py +258 -0
  24. rotobot_nuke-0.3.0/tests/fixtures/v1_small.json +17 -0
  25. rotobot_nuke-0.3.0/tests/fixtures/v2_hd_1920_1080.json +82 -0
  26. rotobot_nuke-0.3.0/tests/fixtures/v2_small.json +46 -0
  27. rotobot_nuke-0.3.0/tests/fixtures/v3_real_cut.json +1 -0
  28. rotobot_nuke-0.3.0/tests/fixtures/v3_small.json +101 -0
  29. rotobot_nuke-0.3.0/tests/test_body_local_metric.py +162 -0
  30. rotobot_nuke-0.3.0/tests/test_fixtures_are_valid.py +27 -0
  31. rotobot_nuke-0.3.0/tests/test_hierarchical_import.py +273 -0
  32. rotobot_nuke-0.3.0/tests/test_hierarchy.py +192 -0
  33. rotobot_nuke-0.3.0/tests/test_import_cache.py +191 -0
  34. rotobot_nuke-0.3.0/tests/test_importer.py +175 -0
  35. rotobot_nuke-0.3.0/tests/test_reader.py +185 -0
  36. rotobot_nuke-0.3.0/tests/test_undersample.py +223 -0
  37. rotobot_nuke-0.3.0/tests/test_undersample_cli.py +139 -0
  38. rotobot_nuke-0.3.0/tests/test_undersample_hierarchy.py +171 -0
  39. rotobot_nuke-0.3.0/tests/test_v3_reader.py +97 -0
@@ -0,0 +1,119 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.3.0] — 2026-10-08
11
+
12
+ First PyPI release. Bundles all the work shipped since the v0.1.0
13
+ scaffold: the `.nk` curves-knob import cache, the hierarchical
14
+ three-transform cascade import mode (issue #279), the v3 reader
15
+ (camera + persons blocks) and composed-tolerance undersampler, retuned
16
+ `PRESET_*` triples, body-local articulation metric, and the
17
+ `rotobot-undersample` CLI with per-level tolerance flags. Full details
18
+ below.
19
+ ### Added — host-agnostic hierarchy core (`rotobot_nuke.hierarchy`, #279)
20
+ - `decompose(doc)` splits every knot of a v3 document into
21
+ `plate = T1(T2(T3(local)))`: T1 the camera as the **exact** ECC
22
+ homography (not reduced to an affine — on a real 4K clip the affine
23
+ reduction was off by up to 236 px at the plate corners), T2 the person's
24
+ pelvis in stabilised pixels, T3 the body part (bone pt0 relative to the
25
+ pelvis, plus the bone angle, unwrapped), and the knots and tangent
26
+ handles in bone-local pixels.
27
+ - No `nuke` import: the After Effects and Silhouette converters use it.
28
+ - Missing or bad data is held at the last good value, never replaced by
29
+ identity or the origin: `ecc-failed` cameras, `identity` cameras after
30
+ the track has started, absent camera/person frames, `pelvis_px == [0,0]`
31
+ and frames without a bone. Holds are listed in `Hierarchy.held`.
32
+ - T1 and T2 are keyed on every frame of the range, so a host never
33
+ interpolates a value the knots were not computed with.
34
+ - `round_trip_error(doc, hier)` re-composes the hierarchy and reports the
35
+ worst plate-pixel error; writers emit only within
36
+ `ROUND_TRIP_TOLERANCE_PX` (0.05). `corner_pin(H, w, h)` gives the four
37
+ corners that reproduce H in a host's corner-pin transform.
38
+ - New fixture `tests/fixtures/v3_real_cut.json`, cut from a real v0.10.0
39
+ run (projective camera, real pelvis track).
40
+ - Not yet used by this package's own Nuke importer, whose hierarchical
41
+ mode still uses the affine reduction and treats missing frames as
42
+ identity; moving it onto this core is a follow-up.
43
+
44
+ ### Added — `.nk` curves-knob import cache
45
+ - New `rotobot_nuke.cache` module. When `build_roto(..., cache_path=...)`
46
+ is called, the roto's `curves` knob text is written under
47
+ `$XDG_CACHE_HOME/rotobot_nuke/` (default `~/.cache/rotobot_nuke/`) on
48
+ first import; subsequent imports of the same JSON (same mtime, mode,
49
+ curve_type, undersample_tolerance) reload via
50
+ `curves.fromScript(text)` — a measured **188× speedup** on a dense
51
+ 323-object UHD plate (82.87s Python-API build → 0.44s cache-load).
52
+ - The slow per-knot `Shape.append(AnimControlPoint)` path (where ≥95%
53
+ of build time is spent on dense plates) runs once per unique JSON;
54
+ later imports skip it entirely.
55
+ - Any build-parameter or mtime change invalidates automatically.
56
+ Public helpers: `make_key`, `get`, `put`, `invalidate`, `cache_stats`.
57
+ - `cache_path` is opt-in; omitting it preserves the pre-cache behaviour
58
+ (no file reads, no file writes).
59
+
60
+ ### Added — v3 reader + hierarchical undersampler (issue #279)
61
+ - `rotobot_nuke.reader` now parses the `lozenge_bezier_anim` v3
62
+ additions: top-level `camera` → `CameraFrame` and `persons` → nested
63
+ `PersonFrame`. Both are optional; v2 JSONs still parse with
64
+ `doc.camera is None` and `doc.persons is None`.
65
+ - `undersample_doc` grows a hierarchical composed-tolerance path. Pass
66
+ any of `camera_tolerance`, `person_tolerance`, `articulation_tolerance`
67
+ (default to `PRESET_BALANCED` individually) and the function runs
68
+ three sequential RDP passes (camera, person root, articulation) and
69
+ keeps the union of their retained frames per object. The legacy
70
+ single-pass `tolerance=` kwarg still works and is still the default
71
+ when no hierarchical kwarg is passed.
72
+ - `TolerancePreset` named-tuple + `PRESET_COARSE` / `PRESET_BALANCED` /
73
+ `PRESET_FINE` ready-made triples. Unpack with `**PRESET_BALANCED._asdict()`.
74
+ - Articulation state vector now subtracts the person's pelvis pixel
75
+ position before measuring, so the per-object RDP sees articulation
76
+ only — bone-origin drift from whole-body translation no longer
77
+ dominates.
78
+
79
+ ### Added — v0.1 undersampler (initial)
80
+ - `rotobot_nuke.undersample` — RDP keyframe reduction on bone-local
81
+ state vectors. Public API: `undersample_doc(doc, tolerance)`,
82
+ `undersample_object(obj, tolerance)`, `undersample_json(in, out,
83
+ tolerance)`, `rdp_reduction(points, tolerance)`.
84
+ - `build_roto(doc, undersample_tolerance=5.0)` kwarg — reduce
85
+ keyframes inline during Nuke import.
86
+ - `rotobot-undersample` console script for file-to-file batch use.
87
+ - Tolerance preset constants `TOLERANCE_CONSERVATIVE`,
88
+ `TOLERANCE_BALANCED` (default), `TOLERANCE_AGGRESSIVE`,
89
+ `TOLERANCE_VERY_AGGRESSIVE`, picked from a wedge sweep across four
90
+ real UHD/HD production JSONs.
91
+ - Nuke menu entries for the balanced + aggressive presets and a
92
+ "prompt for tolerance" variant.
93
+
94
+ Algorithm ported from the `key_reduction` branch of
95
+ [`tokgan_silhouette_import`](https://github.com/samhodge-aiml/tokgan_silhouette_import/tree/key_reduction)
96
+ (MIT); re-shaped to operate on `LozengeDoc` in memory rather than
97
+ file-to-file.
98
+
99
+ ## [0.1.0] — 2026-10-07
100
+
101
+ ### Added
102
+ - `rotobot_nuke.reader.load_json(path, *, resolution=None)` — pure Python parser
103
+ for the `lozenge_bezier_anim` schema (v1 + v2). No `nuke` import; testable
104
+ without a Nuke license.
105
+ - `rotobot_nuke.importer.build_roto(doc, *, roto_name, curve_type, set_project_fps)`
106
+ — turns a parsed `LozengeDoc` into a Nuke `Roto` node with proper hierarchy
107
+ (person → body → side → part, with per-finger grouping for hands).
108
+ - `rotobot_nuke.menu` — Nuke menu hook `File → Import → Rotobot JSON…`.
109
+ - `MissingResolutionError` raised when a v1 JSON (no `resolution`/`width`/`height`
110
+ fields) is loaded without an explicit `resolution=(w, h)` kwarg. **The importer
111
+ no longer silently falls back to Nuke's project `format`** — the artist's
112
+ project settings do not have to match the plate.
113
+
114
+ ### Fixed
115
+ - Y-flip resolution policy now never silently uses the wrong value (prior
116
+ behaviour fell through to `nuke.root()["format"].value().height()` and, if
117
+ that missed, produced `None` with no error).
118
+ - Duplicate `shape.name = …; part_layer.append(shape)` emission that attached
119
+ every shape twice to its parent layer.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sam Hodge
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,7 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include pyproject.toml
5
+ recursive-include tests *.py *.json
6
+ global-exclude __pycache__
7
+ global-exclude *.py[cod]
@@ -0,0 +1,280 @@
1
+ Metadata-Version: 2.4
2
+ Name: rotobot-nuke
3
+ Version: 0.3.0
4
+ Summary: Import Rotobot-Next lozenge_bezier_anim JSON into The Foundry's Nuke as a Roto node.
5
+ Author-email: Sam Hodge <sam.hodge@tokgan.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Sam Hodge
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://tokgan.com
29
+ Project-URL: Source, https://github.com/samhodge-tokgan/rotobot-nuke
30
+ Project-URL: Issues, https://github.com/samhodge-tokgan/rotobot-nuke/issues
31
+ Keywords: nuke,rotobot,roto,vfx,bezier,spline
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: End Users/Desktop
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.9
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Topic :: Multimedia :: Video
42
+ Classifier: Topic :: Multimedia :: Graphics
43
+ Requires-Python: >=3.9
44
+ Description-Content-Type: text/markdown
45
+ License-File: LICENSE
46
+ Provides-Extra: dev
47
+ Requires-Dist: pytest>=7; extra == "dev"
48
+ Dynamic: license-file
49
+
50
+ # rotobot-nuke
51
+
52
+ [![PyPI](https://img.shields.io/pypi/v/rotobot-nuke.svg)](https://pypi.org/project/rotobot-nuke/)
53
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
54
+ [![tests](https://github.com/samhodge-tokgan/rotobot-nuke/actions/workflows/test.yml/badge.svg)](https://github.com/samhodge-tokgan/rotobot-nuke/actions/workflows/test.yml)
55
+
56
+ Import `lozenge_bezier_anim` JSON output
57
+ ([Tokgan](https://tokgan.com)) into The Foundry's **Nuke** as a `Roto`
58
+ node — one shape per anatomical segment, keyframed per frame, with a tidy
59
+ `person → body → side → part` layer hierarchy.
60
+
61
+ **You no longer have to pre-set your Nuke project format to the plate size.**
62
+ The importer takes the base resolution from the JSON (or from an explicit
63
+ `resolution=(w, h)` kwarg); if neither is available it raises a clear error
64
+ rather than silently Y-flipping with the wrong height.
65
+
66
+ ## Install
67
+
68
+ ```bash
69
+ pip install rotobot-nuke
70
+ ```
71
+
72
+ Until the first PyPI release, install from GitHub instead:
73
+
74
+ ```bash
75
+ pip install git+https://github.com/samhodge-tokgan/rotobot-nuke
76
+ ```
77
+
78
+ The package is pure Python with no dependencies. The `nuke` module comes from
79
+ your Nuke install at runtime and is not fetched from PyPI. Only the Nuke
80
+ import needs Nuke: reading and undersampling a JSON work in any Python 3.9+
81
+ (see [Reducing keyframes without Nuke](#reducing-keyframes-without-nuke)).
82
+
83
+ ### Nuke menu entry
84
+
85
+ Add one line to `~/.nuke/menu.py` (or your facility `menu.py`):
86
+
87
+ ```python
88
+ import rotobot_nuke.menu # adds a "Rotobot" menu to Nuke's menu bar
89
+ ```
90
+
91
+ The **Rotobot** menu then offers:
92
+
93
+ | Command | What it builds |
94
+ |---|---|
95
+ | Import Rotobot JSON (B-spline)… | a `Roto` node, every frame keyed |
96
+ | Import Rotobot JSON (Bezier)… | the same with Bezier shapes |
97
+ | Import Rotobot JSON — balanced undersample… | B-spline, keyframes reduced at tolerance 5 |
98
+ | Import Rotobot JSON — aggressive undersample… | B-spline, keyframes reduced at tolerance 10 |
99
+ | Import Rotobot JSON — undersample (prompt for tolerance)… | asks for the tolerance |
100
+
101
+ Each one asks for the JSON and builds a `Roto` node named `Tokgan_Roto`, with
102
+ a `person → body → side → part` layer hierarchy.
103
+
104
+ If `rotobot-nuke` is installed into the same Python that Nuke uses, that's
105
+ all you need. If Nuke's embedded Python can't see your site-packages, append the
106
+ install prefix explicitly:
107
+
108
+ ```python
109
+ import sys
110
+ sys.path.append("/path/to/your/site-packages")
111
+ import rotobot_nuke.menu
112
+ ```
113
+
114
+ ### Programmatic use (Python API)
115
+
116
+ ```python
117
+ from rotobot_nuke import load_json, build_roto
118
+
119
+ doc = load_json("/path/to/clip.json") # v2 JSON: resolution read from file
120
+ # or:
121
+ doc = load_json("/path/to/legacy_v1.json", # v1 JSON: resolution must be supplied
122
+ resolution=(1920, 1080))
123
+
124
+ node = build_roto(doc, curve_type="bspline") # or "bezier"
125
+ print(node.name()) # => "Tokgan_Roto" (or Nuke-uniquified)
126
+ ```
127
+
128
+ #### Camera / person hierarchy (v3 JSON, experimental)
129
+
130
+ A v3 JSON (Rotobot Next 0.10.0 and later) carries the plate camera and each
131
+ person's pelvis. `mode="hierarchical"` builds nested Roto layers from them:
132
+
133
+ ```python
134
+ node = build_roto(doc, mode="hierarchical")
135
+ # camera_track the plate camera
136
+ # p0_pelvis each person's root
137
+ # p0:arm:L:forearm each body part, positioned and rotated by its bone
138
+ ```
139
+
140
+ It is not in the menu yet, for two known reasons:
141
+
142
+ * the camera layer uses an affine (translate/rotate/scale) approximation of
143
+ the camera solve, so it does not fully stabilise a shot with perspective;
144
+ * frames with missing camera or pelvis data are not held, so shapes can jump
145
+ on those frames.
146
+
147
+ The plate positions of the shapes are still correct. The Silhouette and
148
+ After Effects importers already use the exact camera and hold missing data,
149
+ through `rotobot_nuke.hierarchy`. Moving this importer onto that code is
150
+ planned.
151
+
152
+ ### Undersampling (RDP keyframe reduction)
153
+
154
+ Rotobot Next writes one keyframe per video frame. A 6 s UHD clip with about
155
+ 140 segments can carry 8,000+ keyframes, which an artist then scrubs through
156
+ for every adjustment. `rotobot-nuke` removes the ones a straight line between
157
+ their neighbours already reproduces, using
158
+ [Ramer-Douglas-Peucker](https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm)
159
+ on each part's bone-local state. The algorithm is ported from the
160
+ [`key_reduction` branch of `tokgan_silhouette_import`](https://github.com/samhodge-aiml/tokgan_silhouette_import/tree/key_reduction)
161
+ (MIT).
162
+
163
+ #### Reducing keyframes without Nuke
164
+
165
+ The `rotobot-undersample` command and `undersample_doc()` are pure Python:
166
+ no Nuke needed. They write a smaller JSON that **any** importer reads,
167
+ including the Silhouette `.fxs` and After Effects converters, and their
168
+ camera / person hierarchies.
169
+
170
+ ```bash
171
+ rotobot-undersample shapes.json shapes_reduced.json --preset balanced
172
+ ```
173
+
174
+ Two kinds of setting, used one at a time:
175
+
176
+ * **`--preset fine | balanced | coarse`** — for v3 JSON. It measures the
177
+ error in three parts: camera, person root, and each part's motion relative
178
+ to its body. A camera pan alone therefore does not keep every keyframe.
179
+ It also works on v2 JSON, where it measures the body-relative part only.
180
+ * **`--tolerance N`** — the original single measurement, in pixel-equivalents
181
+ (default 5). Works on any schema version.
182
+
183
+ Measured on a real Rotobot Next 0.10.0 shot (24 4K frames, 38 parts,
184
+ 874 keyframes):
185
+
186
+ | Setting | Keyframes kept |
187
+ |---|---:|
188
+ | `--preset fine` | 100% |
189
+ | `--preset balanced` | 92% |
190
+ | `--preset coarse` | 59% |
191
+ | `--tolerance 5` | 91% |
192
+ | `--tolerance 10` | 73% |
193
+ | `--tolerance 25` | 50% |
194
+
195
+ Short shots keep proportionally more, because every part keeps its first and
196
+ last frame. Across four longer real clips (see
197
+ [`benchmarks/real_results_cross_clip.md`](benchmarks/real_results_cross_clip.md))
198
+ the presets kept about 98% / 89% / 73%. Review an aggressive reduction
199
+ before handing it on. The camera and person blocks are copied through
200
+ unchanged, and parts with fewer than three frames, or without per-frame
201
+ bones (v1), are left alone.
202
+
203
+ The `--camera-tolerance`, `--person-tolerance` and `--articulation-tolerance`
204
+ options override one part of a preset, e.g.
205
+ `--preset balanced --articulation-tolerance 0.1`.
206
+
207
+ From Python:
208
+
209
+ ```python
210
+ from rotobot_nuke import load_json, undersample_doc, PRESET_BALANCED
211
+
212
+ doc = load_json("shapes.json")
213
+ reduced = undersample_doc(doc, **PRESET_BALANCED._asdict()) # or tolerance=5.0
214
+ ```
215
+
216
+ #### Reducing keyframes while importing into Nuke
217
+
218
+ The menu's *undersample* commands do this as part of the import, and so does
219
+ `build_roto`:
220
+
221
+ ```python
222
+ from rotobot_nuke import load_json, build_roto, TOLERANCE_BALANCED
223
+
224
+ build_roto(load_json("shapes.json"), undersample_tolerance=TOLERANCE_BALANCED) # 5.0
225
+ ```
226
+
227
+ This uses the single `--tolerance` measurement. For a preset, reduce the JSON
228
+ first, then import the reduced file. Building the Roto node needs Nuke
229
+ (including `nuke -t`): it imports `nuke.rotopaint`, which only exists in
230
+ Nuke's bundled Python.
231
+
232
+ #### Tolerance values
233
+
234
+ | Constant | Value | Keyframes kept (four real UHD/HD clips) |
235
+ |---|---:|---|
236
+ | `TOLERANCE_CONSERVATIVE` | 2.0 | 99% or more: barely trims |
237
+ | `TOLERANCE_BALANCED` (default) | 5.0 | 87–99%: a useful middle ground |
238
+ | `TOLERANCE_AGGRESSIVE` | 10.0 | about 75%: faster to scrub, still close |
239
+ | `TOLERANCE_VERY_AGGRESSIVE` | 25.0 | 45–80%: review before use |
240
+
241
+ The unit mixes pixels and degrees, and bone-origin pixel positions dominate it
242
+ on a typical plate. So `tolerance = n` roughly means: keep a frame if any part
243
+ of its state is more than `n` pixel-equivalents away from a straight line
244
+ between the frames kept either side of it.
245
+
246
+ ## What it imports
247
+
248
+ `rotobot-nuke` understands the `lozenge_bezier_anim` schema.
249
+
250
+ - **Schema v2** (`"schema_version": 2`) — current output. Carries
251
+ `resolution`, `width`, `height`, `fps`, per-object `visibility`, per-frame
252
+ Bezier `points`, optional `person_depth`, optional `bone` capsule
253
+ endpoints.
254
+ - **Schema v1** — pre-2026-02 captures that lack any in-band resolution.
255
+ Pass `resolution=(w, h)` explicitly when loading these.
256
+
257
+ Coordinate space is **absolute pixels, Y-down** (image / OpenCV convention).
258
+ The importer flips to Nuke's Y-up on the way in.
259
+
260
+ Curves are **closed cubic Beziers** with absolute tangent handles
261
+ (`left_x`, `left_y`, `right_x`, `right_y`); the Nuke-side half converts
262
+ to the vertex-relative deltas Nuke's RotoPaint API expects.
263
+
264
+ ## What's intentionally not imported
265
+
266
+ - **Silhouette** output (`tokgan_json_to_fxs.py`) — belongs in a sibling repo
267
+ if there's demand.
268
+ - **Signals-JSON** sidecar (`signals_<clip>.json`) — out of scope for v0.1.
269
+ - **`person_depth`** — read into `LozengeDoc.person_depth` but not applied
270
+ to the Roto node. Open an issue if you want a layer-ordering convention
271
+ on top of it.
272
+
273
+ ## Related
274
+
275
+ - [Tokgan](https://tokgan.com) — the broader VFX / AI-roto product this
276
+ JSON schema comes from.
277
+
278
+ ## License
279
+
280
+ MIT. See [LICENSE](./LICENSE).
@@ -0,0 +1,231 @@
1
+ # rotobot-nuke
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/rotobot-nuke.svg)](https://pypi.org/project/rotobot-nuke/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![tests](https://github.com/samhodge-tokgan/rotobot-nuke/actions/workflows/test.yml/badge.svg)](https://github.com/samhodge-tokgan/rotobot-nuke/actions/workflows/test.yml)
6
+
7
+ Import `lozenge_bezier_anim` JSON output
8
+ ([Tokgan](https://tokgan.com)) into The Foundry's **Nuke** as a `Roto`
9
+ node — one shape per anatomical segment, keyframed per frame, with a tidy
10
+ `person → body → side → part` layer hierarchy.
11
+
12
+ **You no longer have to pre-set your Nuke project format to the plate size.**
13
+ The importer takes the base resolution from the JSON (or from an explicit
14
+ `resolution=(w, h)` kwarg); if neither is available it raises a clear error
15
+ rather than silently Y-flipping with the wrong height.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install rotobot-nuke
21
+ ```
22
+
23
+ Until the first PyPI release, install from GitHub instead:
24
+
25
+ ```bash
26
+ pip install git+https://github.com/samhodge-tokgan/rotobot-nuke
27
+ ```
28
+
29
+ The package is pure Python with no dependencies. The `nuke` module comes from
30
+ your Nuke install at runtime and is not fetched from PyPI. Only the Nuke
31
+ import needs Nuke: reading and undersampling a JSON work in any Python 3.9+
32
+ (see [Reducing keyframes without Nuke](#reducing-keyframes-without-nuke)).
33
+
34
+ ### Nuke menu entry
35
+
36
+ Add one line to `~/.nuke/menu.py` (or your facility `menu.py`):
37
+
38
+ ```python
39
+ import rotobot_nuke.menu # adds a "Rotobot" menu to Nuke's menu bar
40
+ ```
41
+
42
+ The **Rotobot** menu then offers:
43
+
44
+ | Command | What it builds |
45
+ |---|---|
46
+ | Import Rotobot JSON (B-spline)… | a `Roto` node, every frame keyed |
47
+ | Import Rotobot JSON (Bezier)… | the same with Bezier shapes |
48
+ | Import Rotobot JSON — balanced undersample… | B-spline, keyframes reduced at tolerance 5 |
49
+ | Import Rotobot JSON — aggressive undersample… | B-spline, keyframes reduced at tolerance 10 |
50
+ | Import Rotobot JSON — undersample (prompt for tolerance)… | asks for the tolerance |
51
+
52
+ Each one asks for the JSON and builds a `Roto` node named `Tokgan_Roto`, with
53
+ a `person → body → side → part` layer hierarchy.
54
+
55
+ If `rotobot-nuke` is installed into the same Python that Nuke uses, that's
56
+ all you need. If Nuke's embedded Python can't see your site-packages, append the
57
+ install prefix explicitly:
58
+
59
+ ```python
60
+ import sys
61
+ sys.path.append("/path/to/your/site-packages")
62
+ import rotobot_nuke.menu
63
+ ```
64
+
65
+ ### Programmatic use (Python API)
66
+
67
+ ```python
68
+ from rotobot_nuke import load_json, build_roto
69
+
70
+ doc = load_json("/path/to/clip.json") # v2 JSON: resolution read from file
71
+ # or:
72
+ doc = load_json("/path/to/legacy_v1.json", # v1 JSON: resolution must be supplied
73
+ resolution=(1920, 1080))
74
+
75
+ node = build_roto(doc, curve_type="bspline") # or "bezier"
76
+ print(node.name()) # => "Tokgan_Roto" (or Nuke-uniquified)
77
+ ```
78
+
79
+ #### Camera / person hierarchy (v3 JSON, experimental)
80
+
81
+ A v3 JSON (Rotobot Next 0.10.0 and later) carries the plate camera and each
82
+ person's pelvis. `mode="hierarchical"` builds nested Roto layers from them:
83
+
84
+ ```python
85
+ node = build_roto(doc, mode="hierarchical")
86
+ # camera_track the plate camera
87
+ # p0_pelvis each person's root
88
+ # p0:arm:L:forearm each body part, positioned and rotated by its bone
89
+ ```
90
+
91
+ It is not in the menu yet, for two known reasons:
92
+
93
+ * the camera layer uses an affine (translate/rotate/scale) approximation of
94
+ the camera solve, so it does not fully stabilise a shot with perspective;
95
+ * frames with missing camera or pelvis data are not held, so shapes can jump
96
+ on those frames.
97
+
98
+ The plate positions of the shapes are still correct. The Silhouette and
99
+ After Effects importers already use the exact camera and hold missing data,
100
+ through `rotobot_nuke.hierarchy`. Moving this importer onto that code is
101
+ planned.
102
+
103
+ ### Undersampling (RDP keyframe reduction)
104
+
105
+ Rotobot Next writes one keyframe per video frame. A 6 s UHD clip with about
106
+ 140 segments can carry 8,000+ keyframes, which an artist then scrubs through
107
+ for every adjustment. `rotobot-nuke` removes the ones a straight line between
108
+ their neighbours already reproduces, using
109
+ [Ramer-Douglas-Peucker](https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm)
110
+ on each part's bone-local state. The algorithm is ported from the
111
+ [`key_reduction` branch of `tokgan_silhouette_import`](https://github.com/samhodge-aiml/tokgan_silhouette_import/tree/key_reduction)
112
+ (MIT).
113
+
114
+ #### Reducing keyframes without Nuke
115
+
116
+ The `rotobot-undersample` command and `undersample_doc()` are pure Python:
117
+ no Nuke needed. They write a smaller JSON that **any** importer reads,
118
+ including the Silhouette `.fxs` and After Effects converters, and their
119
+ camera / person hierarchies.
120
+
121
+ ```bash
122
+ rotobot-undersample shapes.json shapes_reduced.json --preset balanced
123
+ ```
124
+
125
+ Two kinds of setting, used one at a time:
126
+
127
+ * **`--preset fine | balanced | coarse`** — for v3 JSON. It measures the
128
+ error in three parts: camera, person root, and each part's motion relative
129
+ to its body. A camera pan alone therefore does not keep every keyframe.
130
+ It also works on v2 JSON, where it measures the body-relative part only.
131
+ * **`--tolerance N`** — the original single measurement, in pixel-equivalents
132
+ (default 5). Works on any schema version.
133
+
134
+ Measured on a real Rotobot Next 0.10.0 shot (24 4K frames, 38 parts,
135
+ 874 keyframes):
136
+
137
+ | Setting | Keyframes kept |
138
+ |---|---:|
139
+ | `--preset fine` | 100% |
140
+ | `--preset balanced` | 92% |
141
+ | `--preset coarse` | 59% |
142
+ | `--tolerance 5` | 91% |
143
+ | `--tolerance 10` | 73% |
144
+ | `--tolerance 25` | 50% |
145
+
146
+ Short shots keep proportionally more, because every part keeps its first and
147
+ last frame. Across four longer real clips (see
148
+ [`benchmarks/real_results_cross_clip.md`](benchmarks/real_results_cross_clip.md))
149
+ the presets kept about 98% / 89% / 73%. Review an aggressive reduction
150
+ before handing it on. The camera and person blocks are copied through
151
+ unchanged, and parts with fewer than three frames, or without per-frame
152
+ bones (v1), are left alone.
153
+
154
+ The `--camera-tolerance`, `--person-tolerance` and `--articulation-tolerance`
155
+ options override one part of a preset, e.g.
156
+ `--preset balanced --articulation-tolerance 0.1`.
157
+
158
+ From Python:
159
+
160
+ ```python
161
+ from rotobot_nuke import load_json, undersample_doc, PRESET_BALANCED
162
+
163
+ doc = load_json("shapes.json")
164
+ reduced = undersample_doc(doc, **PRESET_BALANCED._asdict()) # or tolerance=5.0
165
+ ```
166
+
167
+ #### Reducing keyframes while importing into Nuke
168
+
169
+ The menu's *undersample* commands do this as part of the import, and so does
170
+ `build_roto`:
171
+
172
+ ```python
173
+ from rotobot_nuke import load_json, build_roto, TOLERANCE_BALANCED
174
+
175
+ build_roto(load_json("shapes.json"), undersample_tolerance=TOLERANCE_BALANCED) # 5.0
176
+ ```
177
+
178
+ This uses the single `--tolerance` measurement. For a preset, reduce the JSON
179
+ first, then import the reduced file. Building the Roto node needs Nuke
180
+ (including `nuke -t`): it imports `nuke.rotopaint`, which only exists in
181
+ Nuke's bundled Python.
182
+
183
+ #### Tolerance values
184
+
185
+ | Constant | Value | Keyframes kept (four real UHD/HD clips) |
186
+ |---|---:|---|
187
+ | `TOLERANCE_CONSERVATIVE` | 2.0 | 99% or more: barely trims |
188
+ | `TOLERANCE_BALANCED` (default) | 5.0 | 87–99%: a useful middle ground |
189
+ | `TOLERANCE_AGGRESSIVE` | 10.0 | about 75%: faster to scrub, still close |
190
+ | `TOLERANCE_VERY_AGGRESSIVE` | 25.0 | 45–80%: review before use |
191
+
192
+ The unit mixes pixels and degrees, and bone-origin pixel positions dominate it
193
+ on a typical plate. So `tolerance = n` roughly means: keep a frame if any part
194
+ of its state is more than `n` pixel-equivalents away from a straight line
195
+ between the frames kept either side of it.
196
+
197
+ ## What it imports
198
+
199
+ `rotobot-nuke` understands the `lozenge_bezier_anim` schema.
200
+
201
+ - **Schema v2** (`"schema_version": 2`) — current output. Carries
202
+ `resolution`, `width`, `height`, `fps`, per-object `visibility`, per-frame
203
+ Bezier `points`, optional `person_depth`, optional `bone` capsule
204
+ endpoints.
205
+ - **Schema v1** — pre-2026-02 captures that lack any in-band resolution.
206
+ Pass `resolution=(w, h)` explicitly when loading these.
207
+
208
+ Coordinate space is **absolute pixels, Y-down** (image / OpenCV convention).
209
+ The importer flips to Nuke's Y-up on the way in.
210
+
211
+ Curves are **closed cubic Beziers** with absolute tangent handles
212
+ (`left_x`, `left_y`, `right_x`, `right_y`); the Nuke-side half converts
213
+ to the vertex-relative deltas Nuke's RotoPaint API expects.
214
+
215
+ ## What's intentionally not imported
216
+
217
+ - **Silhouette** output (`tokgan_json_to_fxs.py`) — belongs in a sibling repo
218
+ if there's demand.
219
+ - **Signals-JSON** sidecar (`signals_<clip>.json`) — out of scope for v0.1.
220
+ - **`person_depth`** — read into `LozengeDoc.person_depth` but not applied
221
+ to the Roto node. Open an issue if you want a layer-ordering convention
222
+ on top of it.
223
+
224
+ ## Related
225
+
226
+ - [Tokgan](https://tokgan.com) — the broader VFX / AI-roto product this
227
+ JSON schema comes from.
228
+
229
+ ## License
230
+
231
+ MIT. See [LICENSE](./LICENSE).