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.
- rotobot_nuke-0.3.0/CHANGELOG.md +119 -0
- rotobot_nuke-0.3.0/LICENSE +21 -0
- rotobot_nuke-0.3.0/MANIFEST.in +7 -0
- rotobot_nuke-0.3.0/PKG-INFO +280 -0
- rotobot_nuke-0.3.0/README.md +231 -0
- rotobot_nuke-0.3.0/pyproject.toml +44 -0
- rotobot_nuke-0.3.0/setup.cfg +4 -0
- rotobot_nuke-0.3.0/src/rotobot_nuke/__init__.py +81 -0
- rotobot_nuke-0.3.0/src/rotobot_nuke/_version.py +1 -0
- rotobot_nuke-0.3.0/src/rotobot_nuke/cache.py +152 -0
- rotobot_nuke-0.3.0/src/rotobot_nuke/hierarchy.py +339 -0
- rotobot_nuke-0.3.0/src/rotobot_nuke/importer.py +596 -0
- rotobot_nuke-0.3.0/src/rotobot_nuke/menu.py +151 -0
- rotobot_nuke-0.3.0/src/rotobot_nuke/py.typed +0 -0
- rotobot_nuke-0.3.0/src/rotobot_nuke/reader.py +506 -0
- rotobot_nuke-0.3.0/src/rotobot_nuke/undersample.py +830 -0
- rotobot_nuke-0.3.0/src/rotobot_nuke.egg-info/PKG-INFO +280 -0
- rotobot_nuke-0.3.0/src/rotobot_nuke.egg-info/SOURCES.txt +37 -0
- rotobot_nuke-0.3.0/src/rotobot_nuke.egg-info/dependency_links.txt +1 -0
- rotobot_nuke-0.3.0/src/rotobot_nuke.egg-info/entry_points.txt +2 -0
- rotobot_nuke-0.3.0/src/rotobot_nuke.egg-info/requires.txt +3 -0
- rotobot_nuke-0.3.0/src/rotobot_nuke.egg-info/top_level.txt +1 -0
- rotobot_nuke-0.3.0/tests/conftest.py +258 -0
- rotobot_nuke-0.3.0/tests/fixtures/v1_small.json +17 -0
- rotobot_nuke-0.3.0/tests/fixtures/v2_hd_1920_1080.json +82 -0
- rotobot_nuke-0.3.0/tests/fixtures/v2_small.json +46 -0
- rotobot_nuke-0.3.0/tests/fixtures/v3_real_cut.json +1 -0
- rotobot_nuke-0.3.0/tests/fixtures/v3_small.json +101 -0
- rotobot_nuke-0.3.0/tests/test_body_local_metric.py +162 -0
- rotobot_nuke-0.3.0/tests/test_fixtures_are_valid.py +27 -0
- rotobot_nuke-0.3.0/tests/test_hierarchical_import.py +273 -0
- rotobot_nuke-0.3.0/tests/test_hierarchy.py +192 -0
- rotobot_nuke-0.3.0/tests/test_import_cache.py +191 -0
- rotobot_nuke-0.3.0/tests/test_importer.py +175 -0
- rotobot_nuke-0.3.0/tests/test_reader.py +185 -0
- rotobot_nuke-0.3.0/tests/test_undersample.py +223 -0
- rotobot_nuke-0.3.0/tests/test_undersample_cli.py +139 -0
- rotobot_nuke-0.3.0/tests/test_undersample_hierarchy.py +171 -0
- 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,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
|
+
[](https://pypi.org/project/rotobot-nuke/)
|
|
53
|
+
[](https://opensource.org/licenses/MIT)
|
|
54
|
+
[](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
|
+
[](https://pypi.org/project/rotobot-nuke/)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](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).
|