This commit is contained in:
Cristian Predut
2025-12-03 18:15:54 +01:00
parent 0a716a0bf1
commit a187d26987
10 changed files with 383 additions and 632 deletions
@@ -2,7 +2,7 @@
"input": "../inputs/nukeCam_unit-1_wRotation.asci",
"input_translation": "../inputs/nukeCam_unit-1-split_tr.asci",
"input_rotation": "../inputs/nukeCam_unit-1-split_rot.asci",
"output": "../outputs/nukeCam_unit-1_wRotation_converted.txt",
"output": "../outputs/nukeCam_unit-1_2-files.txt",
"fps": 25.0,
"unit_scale": 1,
"lens": {
@@ -1,131 +0,0 @@
# Camera → Wan Pipeline Cheatsheet
This is the quick reference for translating DCC camera exports (Nuke / Houdini) into something Wan understands.
### Mental model
**Nuke / Houdini (Scanline, Karma, etc.)**
- Camera input: native camera node from the scene graph
- Coordinate frame: whatever the scene uses
- Intrinsics: stored on the camera node (filmback + focal)
- Translation: camera center `C` in scene units
**Wan video pipeline**
- Camera input: RealEstate10k TXT (`timestamp fx fy cx cy width height R\|t`)
- Coordinate frame: fixed OpenCV axes (+X right, +Y down, +Z forward)
- Intrinsics: requires normalized values (`fx = f_mm / sensor_w_mm`)
- Translation: needs **world→camera** translation `t = –R · C` in meters
Wan was trained on RealEstate10k, so its camera encoder only understands that schema. Anything else (raw ASCI, FBX, etc.) must be converted first.
## Why the converter is mandatory
1. **Different math objects**
- **Nuke’s ASCI** stores the camera center `(Tx, Ty, Tz)` plus ZXY Euler angles `(Rx, Ry, Rz)` in degrees. It tells you where the camera sits in world space and which way it’s pointing.
- **Wan / RealEstate** wants the inverse: a 3×4 matrix `[R|t]` that transforms any world point into the camera’s local space. In other words, “rotate + shift the world so the camera becomes the origin.”
- To bridge the two, we:
1. Convert the Euler angles to a rotation matrix `R` (standard Z→X→Y order).
2. Turn the camera center `C` into the translation part of the inverse matrix via `t = -R · C`.
The path doesn’t change—we simply express the same motion from the camera’s frame of reference so Wan’s math matches Nuke’s.
2. **Coordinate transforms**
- Nuke: +X right, +Y up, +Z forward.
- RealEstate: +X right, +Y down, +Z forward (OpenCV).
- Wan’s visualizer applies another transform for Matplotlib’s +Y-up space.
The converter reorients the axes so Wan’s `ray_condition()` can produce sensible rays.
3. **Units**
- Wan assumes translations are in meters.
- Nuke scenes often live in centimeters or abstract units, so we apply `unit_scale = real_distance / asci_delta` before writing the TXT.
## Practical workflow
1. **Export Nuke camera as ASCI** (columns `[Tx Rx Ty Ry Rz Tz]`).
2. **Measure real-world scale** so you can compute `unit_scale`.
Example: camera dolly moves 3.5 m, ASCI `Tz` delta is `0.11` ⇒ `unit_scale = 3.5 / 0.11 = 31.82`.
3. **Fill `configs/camera_config_*.json`** with:
- Input/output paths
- Lens data (focal + filmback) → converter derives `fx/fy/cx/cy`
- Render resolution placeholders
- `unit_scale` and optional frame caps
4. **Run** `python nuke_ASCI-2-Pose_converter.py --config configs/<shot>.json`
5. **Load TXT in ComfyUI** via `Koolook Load Camera Poses` → `WanVideoFunCameraEmbeds`.
Following this flow keeps the rendered motion aligned with the original plate, even though the raw numbers look different after conversion.***
## Where things happen inside the ComfyUI graph
1. **`Text Multiline` → `Koolook Load Camera Poses`**
- You paste the RealEstate10k TXT path.
- Koolook parses the header plus each row and returns `CAMERACTRL_POSES` (the same intrinsics + `[R|t]` data we exported).
2. **`WanVideo FunCamera Embeds`**
This node performs all the heavy lifting described in the converter:
- `process_poses()` rescales intrinsics to match the render width/height.
- `get_relative_pose()` turns each `[R|t]` row into camera-to-world matrices.
- `ray_condition()` converts those matrices into Plücker rays (origin + direction per pixel), the representation the Wan motion model was trained on.
- The tensor is reshaped and averaged to match Wan’s token budget (21 camera tokens per chunk) and the VAE stride.
- The result is a dictionary:
```python
{
"target_shape": (channels, t, h/8, w/8),
"num_frames": len(poses),
"control_embeds": {
"control_camera_latents": …,
"control_camera_start_percent": …,
"control_camera_end_percent": …,
...
}
}
```
- This payload is what the node outputs on `image_embeds`.
3. **`WanVideo ImageToVideo Encode`**
- Takes VAE/CLIP/start/end latents plus the `image_embeds` dict.
- Reads `control_embeds["control_camera_latents"]` and injects those camera rays into the diffusion network so every generated frame follows the exact path from Nuke.
- `target_shape` and `num_frames` keep the camera tokens aligned with the render resolution and animation length.
So all the conversions (intrinsics, `t = –R·C`, Plücker rays, token packing) happen before the encoder ever runs; by the time `image_embeds` hits Wan, it’s a set of “camera control instructions” telling the model where to point each ray for every frame.***
## Why Wan still drifts relative to the original plate
Even with perfect pose conversion, Wan isn’t a deterministic renderer—it’s still a diffusion model inventing geometry on the fly. Keep these limitations in mind:
- **Diffusion ≠ projection.** Wan reconstructs a scene that *statistically* fits the camera rays; it doesn’t ray-trace your tracked mesh, so pixels can land a little off compared to a Nuke render.
- **No shared 3D points.** Your tracked points in Nuke are actual world markers. Wan fabricates its own depth, so there’s nothing forcing it to align to those anchors.
- **Token downsampling.** Camera embeddings get compressed (Plücker rays → 21 tokens → VAE stride). That’s enough to guide global motion but not sub-pixel parallax.
- **Scale sensitivity.** Any tiny error in focal length, sensor size, or `unit_scale` shows up as sliding when you composite back with live-action elements.
### Tips when you need zero sliding
1. **Lock scale & intrinsics.** Double-check the measurement and lens metadata; millimeter accuracy matters.
2. **Treat Wan as a background pass.** Keep your foreground in Nuke and use Wan only for the new environment, then composite the two with projections/cards to fix residual offsets.
3. **Use camera projections for critical geometry.** Project the plate (or a painted matte) onto simple geo driven by your tracked camera, and blend that with Wan’s output.
4. **Post-warp if needed.** If the drift is small, track Wan’s render and apply a 2D/3D match-move or morph to align with the original plate.
Wan’s camera control ensures the *motion* matches; matching the *exact* pixel positions still requires traditional VFX techniques when you need perfectly locked foreground/background interaction.***
## Getting `unit_scale` and lens data dialed in
The most common reason for visible drift is a scale mismatch. Because Wan assumes the translations in `[R|t]` are in meters, a small error gets amplified—especially on wide lenses where foreground points travel further per degree of rotation.
### Why it matters
- **Focal length & filmback set the field of view.** If the RealEstate TXT says `fx = 33 mm / 36 mm = 0.916` but the real lens was 31 mm, Wan thinks the camera is *slightly tighter*. To match the same screen-space travel, it pushes the scene farther away, and your foreground doesn’t line up.
- **`unit_scale` scales the entire translation vector.** Doubling `unit_scale` doubles the apparent dolly move. On a 24 mm lens a 5 cm error is huge; on a 135 mm it’s barely noticeable.
### How to measure accurately
1. **Grab real production data whenever possible.** Use the on-set focal length and documented dolly track distance. Enter those directly into the config.
2. **Use Nuke’s reference tools.** In CameraTracker you can “Set Reference Distance” between two tracked points. If you tell it “this wall is exactly 4.2 m apart,” the entire camera solves into meters and `unit_scale` becomes 1.0.
3. **Manual measurement workflow:**
- Pick two frames (start/end) and read the raw ASCI translation delta (e.g., Tz changes by 0.11842).
Formula:
```
unit_scale = real_world_distance_meters / abs(asci_delta)
```
Example: camera travels 6.30 m, ASCI delta is 0.1891 ⇒ `unit_scale = 6.30 / 0.1891 = 33.31`.
- Measure the real-world distance for that move (tape measure on set, LiDAR scan, or deduced from perspective grids).
- Compute the ratio using the formula above. Keep at least 3 significant decimals.
- Verify by projecting a checkerboard onto set geometry: if it still drifts, adjust by the measured percent difference.
4. **Wide lenses need tighter tolerances.** For anything 35 mm or wider, aim for centimeter-level accuracy. Every 1% error in scale equals ~1% sliding on screen.
Document the measurement in the config file (e.g. `"reference_distance_m": 6.53`). That way anyone regenerating the poses knows where the number came from and can double-check if the set changes.***
@@ -0,0 +1,81 @@
source:translation:configs/../inputs/nukeCam_unit-1-split_tr.asci rotation:configs/../inputs/nukeCam_unit-1-split_rot.asci fps:25.0 fx:0.9722222222222222 fy:1.4583333333333333 cx:0.5 cy:0.5 width:1920 height:1080 unit_scale:1
0.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 1.000000000000 -0.000000000000 0.000000000000 -0.000000000000 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 0.000000000000 -0.000000000000 1.000000000000 -0.500000000000
40000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.999997620177 -0.000000000000 0.002181659834 -0.006063547170 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.002181659834 -0.000000000000 0.999997620177 -0.487487931338
80000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.999990480721 -0.000000000000 0.004363309285 -0.012072476516 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.004363309285 -0.000000000000 0.999990480721 -0.474951844950
120000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.999978581664 -0.000000000000 0.006544937967 -0.018026712232 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.006544937967 -0.000000000000 0.999978581664 -0.462391919452
160000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.999961923064 -0.000000000000 0.008726535498 -0.023926179030 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.008726535498 -0.000000000000 0.999961923064 -0.449808334072
200000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.999940505000 -0.000000000000 0.010908091494 -0.029770802145 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.010908091494 -0.000000000000 0.999940505000 -0.437201267856
240000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.999914327574 -0.000000000000 0.013089595571 -0.035560507433 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.013089595571 -0.000000000000 0.999914327574 -0.424570900458
280000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.999883390910 -0.000000000000 0.015271037347 -0.041295220972 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.015271037347 -0.000000000000 0.999883390910 -0.411917411453
320000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.999847695156 -0.000000000000 0.017452406437 -0.046974869762 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.017452406437 -0.000000000000 0.999847695156 -0.399240980716
360000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.999807240482 -0.000000000000 0.019633692461 -0.052599380927 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.019633692461 -0.000000000000 0.999807240482 -0.386541788340
400000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.999762027080 -0.000000000000 0.021814885035 -0.058168682516 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.021814885035 -0.000000000000 0.999762027080 -0.373820014719
440000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.999712055165 -0.000000000000 0.023995973777 -0.063682702697 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.023995973777 -0.000000000000 0.999712055165 -0.361075840159
480000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.999657324976 -0.000000000000 0.026176948308 -0.069141370573 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.026176948308 -0.000000000000 0.999657324976 -0.348309445564
520000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.999597836772 -0.000000000000 0.028357798245 -0.074544615459 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.028357798245 -0.000000000000 0.999597836772 -0.335521011648
560000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.999533590837 -0.000000000000 0.030538513210 -0.079892367309 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.030538513210 -0.000000000000 0.999533590837 -0.322710719722
600000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.999464587476 -0.000000000000 0.032719082822 -0.085184556497 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.032719082822 -0.000000000000 0.999464587476 -0.309878751005
640000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.999390827019 -0.000000000000 0.034899496703 -0.090421114224 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.034899496703 -0.000000000000 0.999390827019 -0.297025287002
680000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.999312309816 -0.000000000000 0.037079744474 -0.095601971919 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.037079744474 -0.000000000000 0.999312309816 -0.284150509426
720000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.999229036241 -0.000000000000 0.039259815759 -0.100727061641 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.039259815759 -0.000000000000 0.999229036241 -0.271254600184
760000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.999141006690 -0.000000000000 0.041439700181 -0.105796316076 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.041439700181 -0.000000000000 0.999141006690 -0.258337741373
800000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.999048221582 -0.000000000000 0.043619387365 -0.110809668139 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.043619387365 -0.000000000000 0.999048221582 -0.245400115295
840000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.998950681359 -0.000000000000 0.045798866937 -0.115767051577 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.045798866937 -0.000000000000 0.998950681359 -0.232441904433
880000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.998848386485 -0.000000000000 0.047978128521 -0.120668400569 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.047978128521 -0.000000000000 0.998848386485 -0.219463291562
920000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.998741337447 -0.000000000000 0.050157161747 -0.125513649814 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.050157161747 -0.000000000000 0.998741337447 -0.206464459348
960000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.998629534755 -0.000000000000 0.052335956243 -0.130302734552 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.052335956243 -0.000000000000 0.998629534755 -0.193445590945
1000000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.998512978940 -0.000000000000 0.054514501638 -0.135035590660 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.054514501638 -0.000000000000 0.998512978940 -0.180406869686
1040000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.998391670557 -0.000000000000 0.056692787563 -0.139712154235 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.056692787563 -0.000000000000 0.998391670557 -0.167348478906
1080000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.998265610185 -0.000000000000 0.058870803651 -0.144332362305 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.058870803651 -0.000000000000 0.998265610185 -0.154270602099
1120000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.998134798422 -0.000000000000 0.061048539535 -0.148896152049 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.061048539535 -0.000000000000 0.998134798422 -0.141173423267
1160000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.997999235891 -0.000000000000 0.063225984849 -0.153403461465 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.063225984849 -0.000000000000 0.997999235891 -0.128057126272
1200000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.997858923239 -0.000000000000 0.065403129230 -0.157854229082 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.065403129230 -0.000000000000 0.997858923239 -0.114921895155
1240000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.997713861131 -0.000000000000 0.067579962315 -0.162248393683 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.067579962315 -0.000000000000 0.997713861131 -0.101767914454
1280000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.997564050260 -0.000000000000 0.069756473744 -0.166585894968 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.069756473744 -0.000000000000 0.997564050260 -0.088595368557
1320000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.997409491337 -0.000000000000 0.071932653157 -0.170866672780 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.071932653157 -0.000000000000 0.997409491337 -0.075404442151
1360000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.997250185099 -0.000000000000 0.074108490195 -0.175090667888 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.074108490195 -0.000000000000 0.997250185099 -0.062195319973
1400000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.997086132304 -0.000000000000 0.076283974504 -0.179257821223 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.076283974504 -0.000000000000 0.997086132304 -0.048968187255
1440000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.996917333733 -0.000000000000 0.078459095728 -0.183368074628 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.078459095728 -0.000000000000 0.996917333733 -0.035723229073
1480000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.996743790189 -0.000000000000 0.080633843514 -0.187421370093 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.080633843514 -0.000000000000 0.996743790189 -0.022460630799
1520000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.996565502498 -0.000000000000 0.082808207512 -0.191417650546 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.082808207512 -0.000000000000 0.996565502498 -0.009180577944
1560000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.996382471508 -0.000000000000 0.084982177372 -0.195356859053 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.084982177372 -0.000000000000 0.996382471508 0.004116743785
1600000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.996194698092 -0.000000000000 0.087155742748 -0.199238939618 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.087155742748 -0.000000000000 0.996194698092 0.017431148550
1640000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.996002183142 -0.000000000000 0.089328893292 -0.203063836486 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.089328893292 -0.000000000000 0.996002183142 0.030762450324
1680000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.995804927575 -0.000000000000 0.091501618663 -0.206831494442 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.091501618663 -0.000000000000 0.995804927575 0.044110462919
1720000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.995602932329 -0.000000000000 0.093673908519 -0.210541859109 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.093673908519 -0.000000000000 0.995602932329 0.057475000014
1760000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.995396198367 -0.000000000000 0.095845752520 -0.214194876252 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.095845752520 -0.000000000000 0.995396198367 0.070855875094
1800000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.995184726672 -0.000000000000 0.098017140330 -0.217790492578 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.098017140330 -0.000000000000 0.995184726672 0.084252901523
1840000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.994968518251 -0.000000000000 0.100188061612 -0.221328654935 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.100188061612 -0.000000000000 0.994968518251 0.097665892473
1880000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.994747574132 -0.000000000000 0.102358506035 -0.224809311102 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.102358506035 -0.000000000000 0.994747574132 0.111094661099
1920000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.994521895368 -0.000000000000 0.104528463268 -0.228232409033 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.104528463268 -0.000000000000 0.994521895368 0.124539020067
1960000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.994291483033 -0.000000000000 0.106697922982 -0.231597897590 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.106697922982 -0.000000000000 0.994291483033 0.137998782230
2000000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.994056338222 -0.000000000000 0.108866874852 -0.234905725794 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.108866874852 -0.000000000000 0.994056338222 0.151473760151
2040000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.993816462056 -0.000000000000 0.111035308554 -0.238155843594 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.111035308554 -0.000000000000 0.993816462056 0.164963766386
2080000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.993571855677 -0.000000000000 0.113203213768 -0.241348201120 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.113203213768 -0.000000000000 0.993571855677 0.178468613005
2120000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.993322520247 -0.000000000000 0.115370580174 -0.244482749310 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.115370580174 -0.000000000000 0.993322520247 0.191988112263
2160000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.993068456955 -0.000000000000 0.117537397458 -0.247559439647 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.117537397458 -0.000000000000 0.993068456955 0.205522076271
2200000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.992809667009 -0.000000000000 0.119703655305 -0.250578223896 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.119703655305 -0.000000000000 0.992809667009 0.219070316665
2240000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.992546151641 -0.000000000000 0.121869343405 -0.253539054731 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.121869343405 -0.000000000000 0.992546151641 0.232632645289
2280000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.992277912106 -0.000000000000 0.124034451450 -0.256441884972 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.124034451450 -0.000000000000 0.992277912106 0.246208873795
2320000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.992004949680 -0.000000000000 0.126198969136 -0.259286668309 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.126198969136 -0.000000000000 0.992004949680 0.259798813539
2360000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.991727265662 -0.000000000000 0.128362886159 -0.262073358881 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.128362886159 -0.000000000000 0.991727265662 0.273402275726
2400000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.991444861374 -0.000000000000 0.130526192220 -0.264801911333 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.130526192220 -0.000000000000 0.991444861374 0.287019071726
2440000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.991157738160 -0.000000000000 0.132688877023 -0.267472280998 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.132688877023 -0.000000000000 0.991157738160 0.300649012491
2480000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.990865897387 -0.000000000000 0.134850930274 -0.270084423445 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.134850930274 -0.000000000000 0.990865897387 0.314291908900
2520000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.990569340444 -0.000000000000 0.137012341682 -0.272638295089 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.137012341682 -0.000000000000 0.990569340444 0.327947571744
2560000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.990268068742 -0.000000000000 0.139173100960 -0.275133852795 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.139173100960 -0.000000000000 0.990268068742 0.341615811669
2600000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.989962083715 -0.000000000000 0.141333197824 -0.277571053976 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.141333197824 -0.000000000000 0.989962083715 0.355296439195
2640000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.989651386820 -0.000000000000 0.143492621991 -0.279949856496 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.143492621991 -0.000000000000 0.989651386820 0.368989264703
2680000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.989335979535 -0.000000000000 0.145651363185 -0.282270219065 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.145651363185 -0.000000000000 0.989335979535 0.382694098491
2720000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.989015863362 -0.000000000000 0.147809411130 -0.284532100631 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.147809411130 -0.000000000000 0.989015863362 0.396410750793
2760000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.988691039824 -0.000000000000 0.149966755554 -0.286735460837 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.149966755554 -0.000000000000 0.988691039824 0.410139031437
2800000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.988361510468 -0.000000000000 0.152123386190 -0.288880259717 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.152123386190 -0.000000000000 0.988361510468 0.423878750512
2840000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.988027276861 -0.000000000000 0.154279292772 -0.290966458213 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.154279292772 -0.000000000000 0.988027276861 0.437629717640
2880000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.987688340595 -0.000000000000 0.156434465040 -0.292994017363 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.156434465040 -0.000000000000 0.987688340595 0.451391742660
2920000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.987344703283 -0.000000000000 0.158588892735 -0.294962899196 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.158588892735 -0.000000000000 0.987344703283 0.465164635060
2960000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.986996366560 -0.000000000000 0.160742565604 -0.296873065884 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.160742565604 -0.000000000000 0.986996366560 0.478948204254
3000000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.986643332085 -0.000000000000 0.162895473395 -0.298724480545 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.162895473395 -0.000000000000 0.986643332085 0.492742259602
3040000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.986285601537 -0.000000000000 0.165047605861 -0.300517106440 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.165047605861 -0.000000000000 0.986285601537 0.506546610393
3080000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.985923176620 -0.000000000000 0.167198952759 -0.302250907727 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.167198952759 -0.000000000000 0.985923176620 0.520361065556
3120000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.985556059058 -0.000000000000 0.169349503849 -0.303925848952 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.169349503849 -0.000000000000 0.985556059058 0.534185434292
3160000.000000000000 0.972222222222 1.458333333333 0.500000000000 0.500000000000 1920.000000000000 1080.000000000000 0.985184250599 -0.000000000000 0.171499248896 -0.305541895283 -0.000000000000 1.000000000000 -0.000000000000 -0.500000000000 -0.171499248896 -0.000000000000 0.985184250599 0.548019525301
@@ -1,111 +0,0 @@
#!/usr/bin/env python3
import math
from pathlib import Path
from typing import Sequence
ASCI_PATH = Path(__file__).resolve().parents[1] / "inputs" / "nukeCam_unit-1_v2.asci"
POSE_PATH = Path(__file__).resolve().parents[1] / "outputs" / "nukeCam_unit-1_v4_converted.txt"
def mat_mul(a: Sequence[Sequence[float]], b: Sequence[Sequence[float]]) -> list[list[float]]:
return [
[a[r][0] * b[0][c] + a[r][1] * b[1][c] + a[r][2] * b[2][c] for c in range(3)]
for r in range(3)
]
def euler_zxy(rx: float, ry: float, rz: float) -> list[list[float]]:
sx, cx = math.sin(rx), math.cos(rx)
sy, cy = math.sin(ry), math.cos(ry)
sz, cz = math.sin(rz), math.cos(rz)
Rx = (
(1.0, 0.0, 0.0),
(0.0, cx, -sx),
(0.0, sx, cx),
)
Ry = (
(cy, 0.0, sy),
(0.0, 1.0, 0.0),
(-sy, 0.0, cy),
)
Rz = (
(cz, -sz, 0.0),
(sz, cz, 0.0),
(0.0, 0.0, 1.0),
)
return mat_mul(Ry, mat_mul(Rx, Rz))
def convert_yup_to_ydown(R: list[list[float]]) -> list[list[float]]:
# T = diag(1, -1, 1)
return [
[R[0][0], -R[0][1], R[0][2]],
[-R[1][0], R[1][1], -R[1][2]],
[R[2][0], -R[2][1], R[2][2]],
]
def mat_vec_mul(R: Sequence[Sequence[float]], v: Sequence[float]) -> list[float]:
return [
R[0][0] * v[0] + R[0][1] * v[1] + R[0][2] * v[2],
R[1][0] * v[0] + R[1][1] * v[1] + R[1][2] * v[2],
R[2][0] * v[0] + R[2][1] * v[1] + R[2][2] * v[2],
]
def read_rows(path: Path) -> list[list[float]]:
rows: list[list[float]] = []
for line in path.read_text().splitlines():
stripped = line.strip()
if not stripped:
continue
# Skip header/metadata lines (e.g. "source:…") that are not numeric.
first = stripped[0]
if first not in "0123456789-.":
continue
rows.append(list(map(float, stripped.split())))
return rows
def compare_rows(indices: list[int]) -> None:
asci_rows = read_rows(ASCI_PATH)
pose_rows = read_rows(POSE_PATH)
print(f"Frames in ASCI: {len(asci_rows)}")
print(f"Frames in TXT: {len(pose_rows)}")
for idx in indices:
if idx >= len(pose_rows):
continue
tx, rx, ty, ry, tz, rz = asci_rows[idx]
R_nuke = euler_zxy(math.radians(rx), math.radians(ry), math.radians(rz))
R_cv = convert_yup_to_ydown(R_nuke)
t = [-val for val in mat_vec_mul(R_cv, (tx, ty, tz))]
pose_vals = pose_rows[idx]
R_export = [
pose_vals[7:11],
pose_vals[11:15],
pose_vals[15:19],
]
rot_diff = max(abs(R_export[r][c] - R_cv[r][c]) for r in range(3) for c in range(3))
print(f"\nFrame {idx}")
print("Nuke Tx Ty Tz :", (tx, ty, tz))
print("Export t :", tuple(round(pose_vals[10 + i * 4], 6) for i in range(3)))
print("Expected t :", tuple(round(val, 6) for val in t))
print("Rotation diff :", rot_diff)
print("R_nuke→OpenCV :")
for row in R_cv:
print(" ", " ".join(f"{val: .6f}" for val in row))
print("R_export :")
for row in R_export:
print(" ", " ".join(f"{val: .6f}" for val in row))
if __name__ == "__main__":
compare_rows([0, 79])
@@ -1,180 +0,0 @@
#!/usr/bin/env python3
"""
Debug tool to analyze camera rotations and verify coordinate system conversion.
This helps identify if a 90° Y-axis rotation (or any rotation) is being handled
correctly by the converter.
"""
import argparse
import math
import numpy as np
from pathlib import Path
from typing import Tuple
def euler_zxy_to_matrix(rx: float, ry: float, rz: float) -> np.ndarray:
"""Convert ZXY Euler angles to rotation matrix (Nuke's system)."""
sx, cx = math.sin(rx), math.cos(rx)
sy, cy = math.sin(ry), math.cos(ry)
sz, cz = math.sin(rz), math.cos(rz)
Rx = np.array([[1.0, 0.0, 0.0], [0.0, cx, -sx], [0.0, sx, cx]])
Ry = np.array([[cy, 0.0, sy], [0.0, 1.0, 0.0], [-sy, 0.0, cy]])
Rz = np.array([[cz, -sz, 0.0], [sz, cz, 0.0], [0.0, 0.0, 1.0]])
return Ry @ Rx @ Rz
def convert_yup_to_ydown(rotation: np.ndarray) -> np.ndarray:
"""Convert from Nuke Y-up to OpenCV Y-down coordinate system."""
r = rotation
return np.array([
[r[0, 0], -r[0, 1], r[0, 2]],
[-r[1, 0], r[1, 1], -r[1, 2]],
[r[2, 0], -r[2, 1], r[2, 2]],
])
def rotation_to_euler_xyz(R: np.ndarray) -> Tuple[float, float, float]:
"""Extract XYZ Euler angles from rotation matrix (for debugging)."""
# Extract angles (this is approximate, works for most cases)
sy = -R[2, 0]
cy = math.sqrt(max(0, 1 - sy * sy))
if abs(cy) > 1e-6:
sx = R[2, 1] / cy
cx = R[2, 2] / cy
sz = R[1, 0] / cy
cz = R[0, 0] / cy
else:
# Gimbal lock case
sx = R[1, 2]
cx = R[1, 1]
sz = 0.0
cz = 1.0
return math.atan2(sx, cx), math.atan2(sy, cy), math.atan2(sz, cz)
def analyze_asci_rotations(path: Path, num_frames: int = 5) -> None:
"""Analyze rotations in ASCI file to identify patterns."""
print(f"Analyzing rotations in: {path}")
print("=" * 80)
lines = [l.strip() for l in path.read_text().splitlines() if l.strip()]
num_frames = min(num_frames, len(lines))
print(f"\nFirst {num_frames} frames:")
print("-" * 80)
for i in range(num_frames):
values = lines[i].split()
if len(values) != 6:
continue
tx, rx_deg, ty, ry_deg, rz_deg, tz = [float(v) for v in values]
print(f"\nFrame {i+1}:")
print(f" Translation: ({tx:.3f}, {ty:.3f}, {tz:.3f})")
print(f" Rotation (degrees): X={rx_deg:.2f}°, Y={ry_deg:.2f}°, Z={rz_deg:.2f}°")
# Convert to rotation matrix
rx, ry, rz = math.radians(rx_deg), math.radians(ry_deg), math.radians(rz_deg)
R_nuke = euler_zxy_to_matrix(rx, ry, rz)
# Check if Y rotation is near 90°
if abs(ry_deg - 90.0) < 5.0 or abs(ry_deg - (-90.0)) < 5.0:
print(f" ⚠️ WARNING: Y rotation is near ±90° ({ry_deg:.2f}°)")
print(f" This might indicate a coordinate system orientation issue")
# Convert to OpenCV coordinate system
R_opencv = convert_yup_to_ydown(R_nuke)
# Verify rotation matrix properties
det_nuke = np.linalg.det(R_nuke)
det_opencv = np.linalg.det(R_opencv)
print(f" Rotation matrix determinant: Nuke={det_nuke:.6f}, OpenCV={det_opencv:.6f}")
# Check forward direction (Z axis in camera space)
forward_nuke = R_nuke[:, 2] # Third column
forward_opencv = R_opencv[:, 2]
print(f" Forward direction (Nuke): ({forward_nuke[0]:.3f}, {forward_nuke[1]:.3f}, {forward_nuke[2]:.3f})")
print(f" Forward direction (OpenCV): ({forward_opencv[0]:.3f}, {forward_opencv[1]:.3f}, {forward_opencv[2]:.3f})")
# Check if coordinate conversion preserved the rotation correctly
# The forward direction should point in the same physical direction
# (just expressed in different coordinate systems)
dot_product = np.dot(forward_nuke, forward_opencv)
if abs(dot_product - 1.0) < 0.01:
print(f" ✓ Coordinate conversion preserves forward direction")
else:
print(f" ⚠️ Forward directions differ (dot={dot_product:.6f})")
print("\n" + "=" * 80)
print("\nInterpretation:")
print("- If Y rotation is consistently near ±90°, this might indicate:")
print(" 1. Camera solver oriented the scene differently (this is OK)")
print(" 2. Coordinate system mismatch in the solver (check Nuke settings)")
print("- The converter should preserve all rotations correctly")
print("- If motion doesn't match, check unit_scale and lens parameters")
def test_90_degree_rotation() -> None:
"""Test if 90° Y rotation is handled correctly."""
print("\n" + "=" * 80)
print("Testing 90° Y-axis rotation conversion:")
print("-" * 80)
# Test case: 90° rotation around Y axis
ry_90 = math.radians(90.0)
R_nuke = euler_zxy_to_matrix(0.0, ry_90, 0.0)
print(f"\nNuke rotation matrix (90° Y):")
print(R_nuke)
R_opencv = convert_yup_to_ydown(R_nuke)
print(f"\nOpenCV rotation matrix (after conversion):")
print(R_opencv)
# Verify properties
det_nuke = np.linalg.det(R_nuke)
det_opencv = np.linalg.det(R_opencv)
print(f"\nDeterminants: Nuke={det_nuke:.6f}, OpenCV={det_opencv:.6f}")
# Test forward direction
forward_nuke = R_nuke[:, 2]
forward_opencv = R_opencv[:, 2]
print(f"\nForward direction:")
print(f" Nuke: ({forward_nuke[0]:.3f}, {forward_nuke[1]:.3f}, {forward_nuke[2]:.3f})")
print(f" OpenCV: ({forward_opencv[0]:.3f}, {forward_opencv[1]:.3f}, {forward_opencv[2]:.3f})")
# In Nuke: +Z forward, so 90° Y rotation should point +X
# After conversion, should still point in same physical direction
print(f"\n✓ 90° Y rotation conversion test complete")
def main():
parser = argparse.ArgumentParser(description="Debug camera rotations")
parser.add_argument("asci", type=Path, help="ASCI file to analyze")
parser.add_argument("--frames", type=int, default=5, help="Number of frames to analyze")
parser.add_argument("--test-90deg", action="store_true", help="Run 90° rotation test")
args = parser.parse_args()
if args.test_90deg:
test_90_degree_rotation()
if args.asci.exists():
analyze_asci_rotations(args.asci, args.frames)
else:
print(f"Error: {args.asci} not found")
if __name__ == "__main__":
main()
@@ -0,0 +1,301 @@
#!/usr/bin/env python3
"""
Pose diagnostics CLI.
Replaces the previous individual helpers (check_mapping, validate_conversion,
debug_rotation) with a single entry point that exposes three subcommands:
- validate: compare ASCI vs. converted TXT in detail.
- check: probe specific frames for translation/rotation parity.
- rotations:inspect raw ASCI rotations or run a 90° sanity test.
"""
from __future__ import annotations
import argparse
import math
from pathlib import Path
from typing import Iterable, List, Sequence, Tuple
import numpy as np
# ---------------------------------------------------------------------------
# Shared math helpers
# ---------------------------------------------------------------------------
def euler_zxy_to_matrix(rx: float, ry: float, rz: float) -> List[List[float]]:
sx, cx = math.sin(rx), math.cos(rx)
sy, cy = math.sin(ry), math.cos(ry)
sz, cz = math.sin(rz), math.cos(rz)
Rx = [[1.0, 0.0, 0.0], [0.0, cx, -sx], [0.0, sx, cx]]
Ry = [[cy, 0.0, sy], [0.0, 1.0, 0.0], [-sy, 0.0, cy]]
Rz = [[cz, -sz, 0.0], [sz, cz, 0.0], [0.0, 0.0, 1.0]]
return (np.dot(np.dot(Ry, Rx), Rz)).tolist()
def convert_yup_to_ydown(rotation: Sequence[Sequence[float]]) -> List[List[float]]:
r = rotation
return [
[r[0][0], -r[0][1], r[0][2]],
[-r[1][0], r[1][1], -r[1][2]],
[r[2][0], -r[2][1], r[2][2]],
]
def mat_vec_mul(R: Sequence[Sequence[float]], v: Sequence[float]) -> List[float]:
return [
R[0][0] * v[0] + R[0][1] * v[1] + R[0][2] * v[2],
R[1][0] * v[0] + R[1][1] * v[1] + R[1][2] * v[2],
R[2][0] * v[0] + R[2][1] * v[1] + R[2][2] * v[2],
]
def read_numeric_rows(path: Path) -> List[List[float]]:
rows: List[List[float]] = []
for line in path.read_text().splitlines():
stripped = line.strip()
if not stripped:
continue
if stripped[0] not in "0123456789-.":
continue
rows.append(list(map(float, stripped.split())))
return rows
# ---------------------------------------------------------------------------
# validate command
# ---------------------------------------------------------------------------
def parse_pose_line(line: str) -> Tuple[float, List[float], List[List[float]]]:
values = line.strip().split()
if len(values) != 19:
raise ValueError(f"Expected 19 columns, got {len(values)}")
timestamp = float(values[0])
intrinsics = [float(v) for v in values[1:7]]
matrix_values = [float(v) for v in values[7:19]]
R = [
matrix_values[0:4],
matrix_values[4:8],
matrix_values[8:12],
]
return timestamp, intrinsics, R
def validate_rotation_matrix(R: Sequence[Sequence[float]], name: str) -> List[str]:
issues: List[str] = []
R_np = np.array(R)
det = np.linalg.det(R_np)
if abs(det - 1.0) > 0.01:
issues.append(f"{name}: determinant {det:.6f} (expected 1.0)")
should_be_identity = np.dot(R_np, R_np.T)
identity = np.eye(3)
max_diff = np.abs(should_be_identity - identity).max()
if max_diff > 0.01:
issues.append(f"{name}: not orthogonal (max diff {max_diff:.6f})")
return issues
def cmd_validate(args: argparse.Namespace) -> None:
asci_path = args.asci
pose_path = args.pose
unit_scale = args.unit_scale
print(f"Reading ASCI: {asci_path}")
print(f"Reading poses: {pose_path}")
print(f"Using unit_scale: {unit_scale}")
print()
asci_lines = [l for l in asci_path.read_text().splitlines() if l.strip()]
pose_lines = pose_path.read_text().splitlines()
header = pose_lines[0] if pose_lines else ""
pose_data = [parse_pose_line(l) for l in pose_lines[1:] if l.strip()]
print(f"Found {len(asci_lines)} frames in ASCI")
print(f"Found {len(pose_data)} frames in pose file")
print(f"Header: {header}")
if len(asci_lines) != len(pose_data):
print(
f"WARNING: Frame count mismatch! ASCI: {len(asci_lines)}, Poses: {len(pose_data)}"
)
print()
num_check = min(args.frames, len(asci_lines), len(pose_data))
print(f"Detailed comparison of first {num_check} frames:")
print("-" * 80)
all_issues: List[str] = []
for i in range(num_check):
tx, rx_deg, ty, ry_deg, tz, rz_deg = map(float, asci_lines[i].split())
rx, ry, rz = math.radians(rx_deg), math.radians(ry_deg), math.radians(rz_deg)
print(f"\nFrame {i}:")
print(f" ASCI: T=({tx:.3f}, {ty:.3f}, {tz:.3f}), R=({rx_deg:.2f}°, {ry_deg:.2f}°, {rz_deg:.2f}°)")
R_asci = euler_zxy_to_matrix(rx, ry, rz)
R_pose = [row[:3] for row in pose_data[i][2]]
t_pose = [row[3] for row in pose_data[i][2]]
print(f" Pose t: ({t_pose[0]:.6f}, {t_pose[1]:.6f}, {t_pose[2]:.6f})")
all_issues.extend(validate_rotation_matrix(R_asci, f"ASCI_R_{i}"))
all_issues.extend(validate_rotation_matrix(R_pose, f"Pose_R_{i}"))
# Translation check
camera_center_scaled = np.array([tx * unit_scale, ty * unit_scale, tz * unit_scale])
expected_t = -np.array(R_pose) @ camera_center_scaled
t_diff = np.abs(np.array(t_pose) - expected_t)
max_t_diff = t_diff.max()
if max_t_diff > 0.01:
print(f" WARNING: translation mismatch (max diff {max_t_diff:.6f})")
all_issues.append(f"Frame {i}: translation mismatch ({max_t_diff:.6f})")
else:
print(f" ✓ translation matches (max diff {max_t_diff:.6f})")
print("\n" + "=" * 80)
if all_issues:
print("SUMMARY: issues detected:")
for issue in all_issues:
print(" -", issue)
else:
print("SUMMARY: All checks passed ✓")
# ---------------------------------------------------------------------------
# check command (specific frame parity)
# ---------------------------------------------------------------------------
def cmd_check(args: argparse.Namespace) -> None:
asci_rows = read_numeric_rows(args.asci)
pose_rows = read_numeric_rows(args.pose)[1:] # skip header already filtered
print(f"Frames in ASCI: {len(asci_rows)}")
print(f"Frames in TXT : {len(pose_rows)}")
indices = args.frames or [0, len(pose_rows) - 1]
for idx in indices:
actual_idx = idx if idx >= 0 else len(pose_rows) + idx
if actual_idx < 0 or actual_idx >= len(pose_rows):
continue
tx, rx, ty, ry, tz, rz = asci_rows[actual_idx]
R_nuke = euler_zxy_to_matrix(math.radians(rx), math.radians(ry), math.radians(rz))
R_cv = convert_yup_to_ydown(R_nuke)
t_expected = [-val for val in mat_vec_mul(R_cv, (tx, ty, tz))]
pose_vals = pose_rows[actual_idx]
R_export = [
pose_vals[7:11],
pose_vals[11:15],
pose_vals[15:19],
]
rot_diff = max(abs(R_export[r][c] - R_cv[r][c]) for r in range(3) for c in range(3))
print(f"\nFrame {actual_idx}")
print("Nuke Tx Ty Tz :", (tx, ty, tz))
print("Export t :", tuple(round(pose_vals[10 + i * 4], 6) for i in range(3)))
print("Expected t :", tuple(round(val, 6) for val in t_expected))
print("Rotation diff :", rot_diff)
print("R_nuke→OpenCV :")
for row in R_cv:
print(" ", " ".join(f"{val: .6f}" for val in row))
print("R_export :")
for row in R_export:
print(" ", " ".join(f"{val: .6f}" for val in row))
# ---------------------------------------------------------------------------
# rotations command (ASCI analysis)
# ---------------------------------------------------------------------------
def analyze_asci_rotations(path: Path, num_frames: int = 5) -> None:
print(f"Analyzing rotations in: {path}")
print("=" * 80)
lines = [l.strip() for l in path.read_text().splitlines() if l.strip()]
num_frames = min(num_frames, len(lines))
for i in range(num_frames):
values = list(map(float, lines[i].split()))
tx, rx_deg, ty, ry_deg, tz, rz_deg = values
print(f"\nFrame {i}: T=({tx:.3f}, {ty:.3f}, {tz:.3f}) R=({rx_deg:.2f}°, {ry_deg:.2f}°, {rz_deg:.2f}°)")
R_nuke = np.array(euler_zxy_to_matrix(math.radians(rx_deg), math.radians(ry_deg), math.radians(rz_deg)))
R_cv = np.array(convert_yup_to_ydown(R_nuke))
print(f" det(Nuke)={np.linalg.det(R_nuke):.6f} det(OpenCV)={np.linalg.det(R_cv):.6f}")
def test_90deg_rotation() -> None:
print("=" * 80)
print("Testing 90° Y-axis rotation conversion")
ry = math.radians(90.0)
R_nuke = np.array(euler_zxy_to_matrix(0.0, ry, 0.0))
R_cv = np.array(convert_yup_to_ydown(R_nuke))
print("Nuke rotation:\n", R_nuke)
print("OpenCV rotation:\n", R_cv)
print(f"det Nuke={np.linalg.det(R_nuke):.6f}, det OpenCV={np.linalg.det(R_cv):.6f}")
def cmd_rotations(args: argparse.Namespace) -> None:
if args.test_90deg:
test_90deg_rotation()
analyze_asci_rotations(args.asci, args.frames)
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Pose diagnostics helper")
subparsers = parser.add_subparsers(dest="command", required=True)
validate_parser = subparsers.add_parser(
"validate", help="Cross-check ASCI vs converted TXT (detailed)"
)
validate_parser.add_argument("asci", type=Path)
validate_parser.add_argument("pose", type=Path)
validate_parser.add_argument("--unit-scale", type=float, default=1.0)
validate_parser.add_argument(
"--frames", type=int, default=5, help="Number of initial frames to inspect"
)
validate_parser.set_defaults(func=cmd_validate)
check_parser = subparsers.add_parser(
"check", help="Inspect specific frame indices for rotation/translation parity"
)
check_parser.add_argument("asci", type=Path)
check_parser.add_argument("pose", type=Path)
check_parser.add_argument(
"--frames",
type=int,
nargs="*",
default=None,
help="Frame indices to inspect (use negative indices for reverse order)",
)
check_parser.set_defaults(func=cmd_check)
rot_parser = subparsers.add_parser(
"rotations", help="Inspect rotations inside an ASCI export"
)
rot_parser.add_argument("asci", type=Path)
rot_parser.add_argument("--frames", type=int, default=5)
rot_parser.add_argument("--test-90deg", action="store_true")
rot_parser.set_defaults(func=cmd_rotations)
return parser
def main() -> None:
parser = build_parser()
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()
@@ -1,209 +0,0 @@
#!/usr/bin/env python3
"""
Validation script to check camera pose conversion accuracy.
This script:
1. Reads the original ASCI file
2. Reads the converted pose file
3. Validates the transformations
4. Reports any issues with coordinate systems, scales, etc.
"""
import argparse
import math
from pathlib import Path
from typing import List, Tuple
import numpy as np
def parse_asci_line(line: str) -> Tuple[float, float, float, float, float, float]:
"""Parse one line of ASCI file: [Tx, Rx, Ty, Ry, Tz, Rz]"""
values = line.strip().split()
if len(values) != 6:
raise ValueError(f"Expected 6 columns, got {len(values)}")
tx, rx, ty, ry, tz, rz = (float(v) for v in values)
return tx, rx, ty, ry, tz, rz
def parse_pose_line(line: str) -> Tuple[float, List[float], List[float]]:
"""Parse one line of pose file: timestamp, intrinsics, [R|t] matrix"""
values = line.strip().split()
if len(values) != 19:
raise ValueError(f"Expected 19 columns, got {len(values)}")
timestamp = float(values[0])
intrinsics = [float(v) for v in values[1:7]] # fx, fy, cx, cy, width, height
# Extract 3x4 [R|t] matrix (row-major)
matrix_values = [float(v) for v in values[7:19]]
R = [
matrix_values[0:4], # Row 0: r00, r01, r02, t0
matrix_values[4:8], # Row 1: r10, r11, r12, t1
matrix_values[8:12], # Row 2: r20, r21, r22, t2
]
return timestamp, intrinsics, R
def euler_zxy_to_matrix(rx: float, ry: float, rz: float) -> List[List[float]]:
"""Convert ZXY Euler angles to rotation matrix."""
sx, cx = math.sin(rx), math.cos(rx)
sy, cy = math.sin(ry), math.cos(ry)
sz, cz = math.sin(rz), math.cos(rz)
Rx = [[1.0, 0.0, 0.0], [0.0, cx, -sx], [0.0, sx, cx]]
Ry = [[cy, 0.0, sy], [0.0, 1.0, 0.0], [-sy, 0.0, cy]]
Rz = [[cz, -sz, 0.0], [sz, cz, 0.0], [0.0, 0.0, 1.0]]
# R = Ry * Rx * Rz
R_temp = np.dot(Ry, Rx)
R = np.dot(R_temp, Rz)
return R.tolist()
def validate_rotation_matrix(R: List[List[float]], name: str = "R") -> List[str]:
"""Check if rotation matrix is valid (orthogonal, determinant = 1)."""
issues = []
R_np = np.array(R)
# Check determinant (should be ~1.0 for rotation matrix)
det = np.linalg.det(R_np)
if abs(det - 1.0) > 0.01:
issues.append(f"{name}: determinant = {det:.6f} (expected ~1.0)")
# Check orthogonality (R * R^T should be identity)
should_be_identity = np.dot(R_np, R_np.T)
identity = np.eye(3)
max_diff = np.abs(should_be_identity - identity).max()
if max_diff > 0.01:
issues.append(f"{name}: not orthogonal (max diff: {max_diff:.6f})")
return issues
def compare_poses(asci_path: Path, pose_path: Path, unit_scale: float = 1.0) -> None:
"""Compare ASCI input with converted pose output."""
print(f"Reading ASCI: {asci_path}")
print(f"Reading poses: {pose_path}")
print(f"Using unit_scale: {unit_scale}")
print()
# Read ASCI
asci_lines = [l for l in asci_path.read_text().splitlines() if l.strip()]
print(f"Found {len(asci_lines)} frames in ASCI")
# Read pose header
pose_lines = pose_path.read_text().splitlines()
header = pose_lines[0]
pose_data = [parse_pose_line(l) for l in pose_lines[1:] if l.strip()]
print(f"Found {len(pose_data)} frames in pose file")
print(f"Header: {header}")
print()
if len(asci_lines) != len(pose_data):
print(f"WARNING: Frame count mismatch! ASCI: {len(asci_lines)}, Poses: {len(pose_data)}")
print()
# Compare first few frames in detail
num_check = min(5, len(asci_lines), len(pose_data))
print(f"Detailed comparison of first {num_check} frames:")
print("-" * 80)
all_issues = []
for i in range(num_check):
print(f"\nFrame {i}:")
# Parse ASCI
tx, rx_deg, ty, ry_deg, tz, rz_deg = parse_asci_line(asci_lines[i])
rx, ry, rz = math.radians(rx_deg), math.radians(ry_deg), math.radians(rz_deg)
print(f" ASCI: T=({tx:.3f}, {ty:.3f}, {tz:.3f}), R=({rx_deg:.1f}°, {ry_deg:.1f}°, {rz_deg:.1f}°)")
# Reconstruct rotation matrix from ASCI
R_asci = euler_zxy_to_matrix(rx, ry, rz)
R_asci_np = np.array(R_asci)
# Get pose data (already parsed on line 97)
timestamp, intrinsics, R_pose_row = pose_data[i]
R_pose = [row[:3] for row in R_pose_row] # Extract rotation part
t_pose = [row[3] for row in R_pose_row] # Extract translation part
R_pose_np = np.array(R_pose)
print(f" Pose: timestamp={timestamp:.0f}µs, fx={intrinsics[0]:.6f}, fy={intrinsics[1]:.6f}")
print(f" Pose R[0]: [{R_pose[0][0]:.6f}, {R_pose[0][1]:.6f}, {R_pose[0][2]:.6f}]")
print(f" Pose R[1]: [{R_pose[1][0]:.6f}, {R_pose[1][1]:.6f}, {R_pose[1][2]:.6f}]")
print(f" Pose t: ({t_pose[0]:.6f}, {t_pose[1]:.6f}, {t_pose[2]:.6f})")
# Validate rotation matrices
issues_asci = validate_rotation_matrix(R_asci, f"ASCI_R_frame{i}")
issues_pose = validate_rotation_matrix(R_pose, f"Pose_R_frame{i}")
if issues_asci:
print(f" ISSUES with ASCI rotation matrix:")
for issue in issues_asci:
print(f" - {issue}")
all_issues.extend(issues_asci)
if issues_pose:
print(f" ISSUES with Pose rotation matrix:")
for issue in issues_pose:
print(f" - {issue}")
all_issues.extend(issues_pose)
# Check coordinate system conversion
# In Y-up to Y-down conversion, Y row and Y column should be negated
# Check if this pattern exists
y_row_flipped = (
abs(R_pose[1][0] + R_asci[1][0]) < 0.01 and
abs(R_pose[1][1] - R_asci[1][1]) < 0.01 and
abs(R_pose[1][2] + R_asci[1][2]) < 0.01
)
y_col_flipped = (
abs(R_pose[0][1] + R_asci[0][1]) < 0.01 and
abs(R_pose[2][1] + R_asci[2][1]) < 0.01
)
if not y_row_flipped or not y_col_flipped:
print(f" WARNING: Coordinate system conversion may be incorrect!")
print(f" Y-row flipped: {y_row_flipped}, Y-col flipped: {y_col_flipped}")
all_issues.append(f"Frame {i}: Coordinate system conversion issue")
# Check translation conversion
# Expected: t = -R_opencv * C_scaled
camera_center_scaled = np.array([tx * unit_scale, ty * unit_scale, tz * unit_scale])
expected_t = -R_pose_np @ camera_center_scaled
t_diff = np.abs(np.array(t_pose) - expected_t)
max_t_diff = t_diff.max()
if max_t_diff > 0.01:
print(f" WARNING: Translation mismatch!")
print(f" Expected t: ({expected_t[0]:.6f}, {expected_t[1]:.6f}, {expected_t[2]:.6f})")
print(f" Actual t: ({t_pose[0]:.6f}, {t_pose[1]:.6f}, {t_pose[2]:.6f})")
print(f" Max diff: {max_t_diff:.6f}")
all_issues.append(f"Frame {i}: Translation conversion issue (max diff: {max_t_diff:.6f})")
else:
print(f" ✓ Translation conversion looks correct (max diff: {max_t_diff:.6f})")
print("\n" + "=" * 80)
if all_issues:
print(f"SUMMARY: Found {len(all_issues)} issues:")
for issue in all_issues:
print(f" - {issue}")
else:
print("SUMMARY: All checks passed! ✓")
def main():
parser = argparse.ArgumentParser(description="Validate camera pose conversion")
parser.add_argument("asci", type=Path, help="Input ASCI file")
parser.add_argument("pose", type=Path, help="Converted pose file")
parser.add_argument("--unit-scale", type=float, default=1.0, help="Unit scale used in conversion")
args = parser.parse_args()
compare_poses(args.asci, args.pose, args.unit_scale)
if __name__ == "__main__":
main()