reorganize
This commit is contained in:
@@ -1,208 +0,0 @@
|
||||
# Camera Conversion Pipeline - Fixes and Improvements
|
||||
|
||||
## Summary
|
||||
|
||||
This document outlines the issues found and fixes applied to the Nuke → ComfyUI camera conversion pipeline.
|
||||
|
||||
## Critical Issue Found: Missing Coordinate System Conversion
|
||||
|
||||
### Problem
|
||||
The converter was **not converting from Nuke's Y-up coordinate system to OpenCV's Y-down coordinate system**. This causes camera motion to appear incorrect in ComfyUI, as the coordinate axes don't match what the Wan model expects.
|
||||
|
||||
### Root Cause
|
||||
- **Nuke uses**: +X right, +Y up, +Z forward
|
||||
- **OpenCV/RealEstate10k uses**: +X right, +Y down, +Z forward
|
||||
- The converter was building rotation matrices in Nuke's coordinate system but not transforming them to OpenCV's system
|
||||
|
||||
### Fix Applied
|
||||
Added `convert_yup_to_ydown()` function that properly transforms rotation matrices:
|
||||
|
||||
```python
|
||||
def convert_yup_to_ydown(rotation: List[List[float]]) -> List[List[float]]:
|
||||
"""
|
||||
Convert rotation matrix from Nuke's Y-up coordinate system to OpenCV's Y-down.
|
||||
|
||||
Transformation: R_opencv = T * R_nuke * T, where T = diag(1, -1, 1)
|
||||
This negates the Y row and Y column of the rotation matrix.
|
||||
"""
|
||||
r = rotation
|
||||
return [
|
||||
[r[0][0], -r[0][1], r[0][2]], # X row: keep X, negate Y, keep Z
|
||||
[-r[1][0], r[1][1], -r[1][2]], # Y row: negate X, keep Y, negate Z
|
||||
[r[2][0], -r[2][1], r[2][2]], # Z row: keep X, negate Y, keep Z
|
||||
]
|
||||
```
|
||||
|
||||
This function is now called in `main()` before computing translations and writing pose data.
|
||||
|
||||
### Impact
|
||||
- **Before**: Camera motion would be flipped/mirrored on the Y axis
|
||||
- **After**: Camera motion matches the original Nuke camera path
|
||||
|
||||
## Code Improvements
|
||||
|
||||
### 1. Added Documentation
|
||||
- Added docstrings to key functions explaining coordinate system transformations
|
||||
- Clarified the purpose of `camera_center_to_translation()` function
|
||||
|
||||
### 2. Improved Code Structure
|
||||
- Separated coordinate system conversion into its own function
|
||||
- Made the transformation explicit and testable
|
||||
|
||||
## Testing Tools Created
|
||||
|
||||
### 1. `test_nuke_scene.py`
|
||||
Generates a Nuke script (.nk) with:
|
||||
- A simple camera animation (forward dolly + pan)
|
||||
- Known lens parameters (35mm focal, 36x24mm filmback)
|
||||
- Reference geometry (grid) for visualization
|
||||
- Easy to validate motion
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
python test_nuke_scene.py --output test_scene.nk --frames 50
|
||||
```
|
||||
|
||||
### 2. `validate_conversion.py`
|
||||
Validates the conversion by:
|
||||
- Reading both ASCI input and pose output
|
||||
- Checking rotation matrix validity (orthogonality, determinant = 1)
|
||||
- Verifying coordinate system conversion (Y-up → Y-down)
|
||||
- Validating translation conversion (camera center → world-to-camera)
|
||||
- Reporting any issues found
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
python validate_conversion.py inputs/camera.asci outputs/camera_converted.txt --unit-scale 0.1
|
||||
```
|
||||
|
||||
### 3. `test_pipeline.sh`
|
||||
Automated test script that runs the full pipeline:
|
||||
1. Generates test scene
|
||||
2. (Manual) Export from Nuke
|
||||
3. Converts ASCI to pose
|
||||
4. Validates conversion
|
||||
|
||||
### 4. `README_TESTING.md`
|
||||
Comprehensive testing guide with:
|
||||
- Step-by-step workflow
|
||||
- Common issues and fixes
|
||||
- Integration instructions for ComfyUI
|
||||
|
||||
## How to Test the Fix
|
||||
|
||||
### Quick Test
|
||||
1. **Generate test scene:**
|
||||
```bash
|
||||
python test_nuke_scene.py --output test_scene.nk
|
||||
```
|
||||
|
||||
2. **Export camera from Nuke:**
|
||||
- Open `test_scene.nk` in Nuke
|
||||
- Select Camera2 node
|
||||
- Export Ascii with columns: `[translate.x, rotate.x, translate.y, rotate.y, translate.z, rotate.z]`
|
||||
- Save as `inputs/test_camera.asci`
|
||||
|
||||
3. **Convert:**
|
||||
```bash
|
||||
python nuke_ASCI-2-Pose_converter.py \
|
||||
inputs/test_camera.asci \
|
||||
outputs/test_camera_converted.txt \
|
||||
--fps 25.0 \
|
||||
--focal-length-mm 35.0 \
|
||||
--sensor-width-mm 36.0 \
|
||||
--sensor-height-mm 24.0 \
|
||||
--width 1920 \
|
||||
--height 1080 \
|
||||
--unit-scale 1.0
|
||||
```
|
||||
|
||||
4. **Validate:**
|
||||
```bash
|
||||
python validate_conversion.py \
|
||||
inputs/test_camera.asci \
|
||||
outputs/test_camera_converted.txt \
|
||||
--unit-scale 1.0
|
||||
```
|
||||
|
||||
### Test with Existing Data
|
||||
To test with your existing `camTrack_1.7m_v02.asci`:
|
||||
|
||||
```bash
|
||||
# Convert with fix
|
||||
python nuke_ASCI-2-Pose_converter.py \
|
||||
inputs/camTrack_1.7m_v02.asci \
|
||||
outputs/camTrack_1.7m_v02_converted_FIXED.txt \
|
||||
--config configs/camera_config_v01.json
|
||||
|
||||
# Validate
|
||||
python validate_conversion.py \
|
||||
inputs/camTrack_1.7m_v02.asci \
|
||||
outputs/camTrack_1.7m_v02_converted_FIXED.txt \
|
||||
--unit-scale 0.1
|
||||
|
||||
# Compare with old output
|
||||
python validate_conversion.py \
|
||||
inputs/camTrack_1.7m_v02.asci \
|
||||
outputs/camTrack_1.7m_v02_converted.txt \
|
||||
--unit-scale 0.1
|
||||
```
|
||||
|
||||
## Expected Results
|
||||
|
||||
### Before Fix
|
||||
- Validation would show coordinate system conversion warnings
|
||||
- Camera motion in ComfyUI would be flipped/mirrored on Y axis
|
||||
- Rotation matrices would be in Nuke's coordinate system
|
||||
|
||||
### After Fix
|
||||
- Validation should pass coordinate system checks
|
||||
- Camera motion should match original Nuke camera path
|
||||
- Rotation matrices properly converted to OpenCV coordinate system
|
||||
- Translation values correctly computed using converted rotation matrix
|
||||
|
||||
## Additional Considerations
|
||||
|
||||
### Unit Scale
|
||||
The `unit_scale` parameter is critical for correct motion:
|
||||
- Measure real-world distance traveled by camera
|
||||
- Calculate: `unit_scale = real_distance_meters / asci_delta`
|
||||
- Example: Camera moves 1.7m, ASCI Tz delta is 17.0 → `unit_scale = 0.1`
|
||||
|
||||
### Lens Parameters
|
||||
Ensure these match your Nuke camera:
|
||||
- `--focal-length-mm`: Focal length in millimeters
|
||||
- `--sensor-width-mm`: Horizontal filmback/aperture
|
||||
- `--sensor-height-mm`: Vertical filmback/aperture
|
||||
|
||||
### Frame Range
|
||||
Match the frame range between:
|
||||
- Nuke ASCI export
|
||||
- ComfyUI render
|
||||
- Use `--start-frame` and `--end-frame` if needed
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Test the fix** with your existing data
|
||||
2. **Validate** using the validation script
|
||||
3. **Compare** old vs new output in ComfyUI
|
||||
4. **Adjust** `unit_scale` if motion scale is still off
|
||||
5. **Verify** lens parameters match production data
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `nuke_ASCI-2-Pose_converter.py`: Added coordinate system conversion
|
||||
- `test_nuke_scene.py`: New test scene generator
|
||||
- `validate_conversion.py`: New validation tool
|
||||
- `test_pipeline.sh`: New automated test script
|
||||
- `README_TESTING.md`: New testing documentation
|
||||
|
||||
## Questions?
|
||||
|
||||
If you encounter issues:
|
||||
1. Run `validate_conversion.py` to identify problems
|
||||
2. Check `README_TESTING.md` for common issues
|
||||
3. Verify `unit_scale` calculation
|
||||
4. Ensure lens parameters match Nuke camera settings
|
||||
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
# Testing the Camera Conversion Pipeline
|
||||
|
||||
This document explains how to test and validate the Nuke → ComfyUI camera conversion pipeline.
|
||||
|
||||
## Quick Test Workflow
|
||||
|
||||
### 1. Generate Test Scene
|
||||
|
||||
```bash
|
||||
python test_nuke_scene.py --output test_scene.nk --frames 81
|
||||
```
|
||||
|
||||
This creates a Nuke script with:
|
||||
- A camera that moves forward (dollies in) from Z=-5 to Z=-3
|
||||
- A slight pan (Y rotation from -10° to -15°)
|
||||
- Known lens parameters (35mm focal, 36x24mm filmback)
|
||||
- A reference grid for visualization
|
||||
|
||||
### 2. Export Camera from Nuke
|
||||
|
||||
1. Open `test_scene.nk` in Nuke
|
||||
2. Select the `Camera2` node
|
||||
3. Right-click → **Export Ascii**
|
||||
4. Configure export:
|
||||
- **Columns**: `translate.x`, `rotate.x`, `translate.y`, `rotate.y`, `translate.z`, `rotate.z`
|
||||
- **Rotation order**: ZXY (Nuke's default)
|
||||
- **Rotation units**: Degrees
|
||||
5. Save as `inputs/test_camera.asci`
|
||||
|
||||
### 3. Convert to Pose Format
|
||||
|
||||
```bash
|
||||
python nuke_ASCI-2-Pose_converter.py \
|
||||
inputs/test_camera.asci \
|
||||
outputs/test_camera_converted.txt \
|
||||
--fps 25.0 \
|
||||
--focal-length-mm 35.0 \
|
||||
--sensor-width-mm 36.0 \
|
||||
--sensor-height-mm 24.0 \
|
||||
--width 1920 \
|
||||
--height 1080 \
|
||||
--unit-scale 1.0
|
||||
```
|
||||
|
||||
Or use a config file:
|
||||
|
||||
```bash
|
||||
python nuke_ASCI-2-Pose_converter.py --config configs/test_camera.json
|
||||
```
|
||||
|
||||
### 4. Validate Conversion
|
||||
|
||||
```bash
|
||||
python validate_conversion.py \
|
||||
inputs/test_camera.asci \
|
||||
outputs/test_camera_converted.txt \
|
||||
--unit-scale 1.0
|
||||
```
|
||||
|
||||
This script checks:
|
||||
- Rotation matrix validity (orthogonality, determinant = 1)
|
||||
- Coordinate system conversion (Y-up → Y-down)
|
||||
- Translation conversion (camera center → world-to-camera translation)
|
||||
- Frame count consistency
|
||||
|
||||
## Common Issues and Fixes
|
||||
|
||||
### Issue: Camera motion doesn't match original plate
|
||||
|
||||
**Possible causes:**
|
||||
|
||||
1. **Missing coordinate system conversion**
|
||||
- ✅ Fixed: The converter now includes `convert_yup_to_ydown()` function
|
||||
- Check validation output for coordinate system warnings
|
||||
|
||||
2. **Camera solver rotated 90° (or any large rotation)**
|
||||
- **This is normal!** Nuke's camera solver may orient the camera with large rotations (e.g., 90° Y-axis)
|
||||
- The converter preserves all rotations correctly, including 90° rotations
|
||||
- Use `python debug_rotation.py inputs/your_camera.asci` to analyze rotations
|
||||
- If motion still doesn't match, check `unit_scale` and lens parameters (see below)
|
||||
|
||||
3. **Incorrect unit_scale**
|
||||
- Measure real-world distance traveled by camera
|
||||
- Calculate: `unit_scale = real_distance_meters / asci_delta`
|
||||
- Example: Camera moves 1.7m, ASCI Tz delta is 17.0 → `unit_scale = 0.1`
|
||||
|
||||
4. **Wrong lens parameters**
|
||||
- Verify focal length matches Nuke camera settings
|
||||
- Check sensor size (filmback) matches camera node
|
||||
- Use `--focal-length-mm` and `--sensor-width-mm` / `--sensor-height-mm` flags
|
||||
|
||||
5. **Frame range mismatch**
|
||||
- Ensure ASCI export includes all frames you want
|
||||
- Use `--start-frame` and `--end-frame` to slice if needed
|
||||
|
||||
### Issue: Validation shows coordinate system errors
|
||||
|
||||
If `validate_conversion.py` reports coordinate system issues:
|
||||
|
||||
1. Check that `convert_yup_to_ydown()` is being called in the converter
|
||||
2. Verify the Y-row and Y-column negation pattern:
|
||||
- `R_opencv[1,0] ≈ -R_nuke[1,0]`
|
||||
- `R_opencv[1,2] ≈ -R_nuke[1,2]`
|
||||
- `R_opencv[0,1] ≈ -R_nuke[0,1]`
|
||||
- `R_opencv[2,1] ≈ -R_nuke[2,1]`
|
||||
|
||||
### Issue: Translation values look wrong
|
||||
|
||||
The converter uses `t = -R * C` where:
|
||||
- `R` is the rotation matrix (after Y-up→Y-down conversion)
|
||||
- `C` is the camera center scaled by `unit_scale`
|
||||
|
||||
If translations are off:
|
||||
1. Verify `unit_scale` is correct
|
||||
2. Check that camera center values from ASCI are being read correctly
|
||||
3. Ensure rotation matrix is valid (orthogonal, det=1)
|
||||
|
||||
## Testing with Real Production Data
|
||||
|
||||
For production shots:
|
||||
|
||||
1. **Measure reference distance**
|
||||
- Use CameraTracker's "Set Reference Distance" feature
|
||||
- Or measure a known object size in the scene
|
||||
- Calculate `unit_scale` from this measurement
|
||||
|
||||
2. **Verify lens metadata**
|
||||
- Get focal length from camera report or EXIF data
|
||||
- Get sensor size from camera specs (e.g., Full Frame = 36x24mm)
|
||||
|
||||
3. **Export with correct frame range**
|
||||
- Match the frame range you'll use in ComfyUI
|
||||
- Use `--start-frame` and `--end-frame` to match exactly
|
||||
|
||||
4. **Validate before using in ComfyUI**
|
||||
- Run `validate_conversion.py` to catch issues early
|
||||
- Check that rotation matrices are valid
|
||||
- Verify coordinate system conversion
|
||||
|
||||
## Integration with ComfyUI
|
||||
|
||||
After conversion:
|
||||
|
||||
1. Load the pose file in ComfyUI using `Koolook Load Camera Poses`
|
||||
2. Connect to `WanVideo FunCamera Embeds`
|
||||
3. Set render resolution to match your pose file's width/height
|
||||
4. Test with a simple prompt first to verify motion
|
||||
|
||||
If motion still doesn't match:
|
||||
- Double-check `unit_scale` calculation
|
||||
- Verify lens parameters match production data
|
||||
- Check that ComfyUI render resolution matches pose file resolution
|
||||
- Consider using camera projection in Nuke to validate the converted poses
|
||||
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
# Understanding Camera Rotations and Coordinate Systems
|
||||
|
||||
## The 90° Y-Axis Rotation Question
|
||||
|
||||
If you notice that your solved camera has a **90° rotation on the Y-axis** (or any large rotation), this is **normal and expected**. Here's why:
|
||||
|
||||
### Why Nuke Camera Solver May Rotate the Camera
|
||||
|
||||
1. **Ambiguity in camera solving**: Camera tracking has inherent ambiguities - the solver can't always determine the absolute world orientation
|
||||
2. **Coordinate system choice**: The solver picks a convenient coordinate system that may not align with your scene's "natural" orientation
|
||||
3. **Plate orientation**: If your plate is rotated (e.g., portrait vs landscape), the solver may rotate the camera to match
|
||||
|
||||
### Does This Affect the Conversion?
|
||||
|
||||
**No!** The converter handles **any rotation correctly**, including:
|
||||
- 90° rotations
|
||||
- 180° rotations
|
||||
- Arbitrary rotations
|
||||
|
||||
The coordinate system conversion (`convert_yup_to_ydown`) is a **linear transformation** that preserves all rotations correctly. It only changes how the coordinate system is expressed (Y-up → Y-down), not the actual camera orientation.
|
||||
|
||||
### How to Verify Rotations Are Correct
|
||||
|
||||
Use the debug tool to analyze your camera rotations:
|
||||
|
||||
```bash
|
||||
python debug_rotation.py inputs/your_camera.asci --frames 10
|
||||
```
|
||||
|
||||
This will show:
|
||||
- Rotation values for each frame
|
||||
- Whether Y rotation is near ±90° (warns if so)
|
||||
- Forward direction vectors (should be preserved after conversion)
|
||||
- Rotation matrix determinants (should be ~1.0)
|
||||
|
||||
### Test 90° Rotation Handling
|
||||
|
||||
To verify the converter handles 90° rotations correctly:
|
||||
|
||||
```bash
|
||||
python debug_rotation.py inputs/your_camera.asci --test-90deg
|
||||
```
|
||||
|
||||
This runs a specific test case with a 90° Y rotation to ensure the conversion preserves it correctly.
|
||||
|
||||
### What If Motion Still Doesn't Match?
|
||||
|
||||
If your camera has a 90° rotation but motion still doesn't match in ComfyUI, check:
|
||||
|
||||
1. **Unit scale** - This is the most common issue
|
||||
```bash
|
||||
# Measure real distance, calculate scale
|
||||
unit_scale = real_distance_meters / asci_translation_delta
|
||||
```
|
||||
|
||||
2. **Lens parameters** - Must match Nuke camera exactly
|
||||
- Focal length (mm)
|
||||
- Sensor width/height (mm)
|
||||
|
||||
3. **Coordinate system** - The converter handles this, but verify:
|
||||
```bash
|
||||
python validate_conversion.py inputs/camera.asci outputs/camera_converted.txt --unit-scale 0.1
|
||||
```
|
||||
|
||||
4. **Frame range** - Ensure you're comparing the same frames
|
||||
|
||||
### Example: Analyzing Your Camera
|
||||
|
||||
If your camera has rotations like:
|
||||
```
|
||||
Frame 1: Y rotation = -102.4°
|
||||
Frame 2: Y rotation = -102.6°
|
||||
```
|
||||
|
||||
This is fine! The converter will:
|
||||
1. Convert Euler angles → rotation matrix (preserving the -102° rotation)
|
||||
2. Convert coordinate system (Y-up → Y-down)
|
||||
3. Compute world-to-camera translation
|
||||
|
||||
The **-102° rotation is preserved** - it's just expressed in OpenCV's coordinate system instead of Nuke's.
|
||||
|
||||
### Key Takeaway
|
||||
|
||||
**A 90° (or any) rotation in the solved camera is NOT a problem.** The converter preserves all rotations correctly. If motion doesn't match, look at:
|
||||
- `unit_scale` (most common)
|
||||
- Lens parameters
|
||||
- Frame range
|
||||
- Coordinate system validation (use `validate_conversion.py`)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"input": "../inputs/camTrack_v01.asci",
|
||||
"input_translation": null,
|
||||
"input_rotation": null,
|
||||
"output": "../outputs/camTrack_v01_converted.txt",
|
||||
"fps": 25.0,
|
||||
"unit_scale": 1.0,
|
||||
|
||||
@@ -1,5 +1,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",
|
||||
"fps": 25.0,
|
||||
"unit_scale": 1,
|
||||
|
||||
@@ -6,6 +6,7 @@ This document captures the end-to-end workflow we established for converting Nuk
|
||||
- **Nuke camera**: standard `Camera` node with Translation / Rotation channels keyed over time.
|
||||
- **Export step**: use Nuke's *Export Ascii* tool with the column mapping shown in `ASCI_source_data/AnimationChannels_and_values_firstFrame_plus-ASCI-window.png`.
|
||||
- Column order: `[translate.x, rotate.x, translate.y, rotate.y, translate.z, rotate.z]`.
|
||||
- Prefer to stick to the combined export unless you want to split the channels into two files. When splitting, export translations as `Tx Ty Tz` and rotations as `Rx Ry Rz` (degrees) and reference them via the config keys described below.
|
||||
- Rotations are exported in **degrees** using Nuke's **ZXY rotation order** (see `DataValidation-nuke.png`).
|
||||
- **Raw file**: `nuke_CAM_exporter/inputs/camTrack_v01.asci` (162 lines covering frames 1001–1161 in this example).
|
||||
|
||||
@@ -55,6 +56,8 @@ Use the template in `configs/camera_config_template.json` (copy + rename per sho
|
||||
```json
|
||||
{
|
||||
"input": "../inputs/camTrack_v01.asci",
|
||||
"input_translation": null,
|
||||
"input_rotation": null,
|
||||
"output": "../outputs/camTrack_v06_converted.txt",
|
||||
"fps": 25.0,
|
||||
"unit_scale": 1.0,
|
||||
@@ -77,6 +80,8 @@ Use the template in `configs/camera_config_template.json` (copy + rename per sho
|
||||
}
|
||||
```
|
||||
|
||||
Set `"input_translation"` / `"input_rotation"` when you export translations and rotations as two separate ASCI files. Leave them `null` (or omit entirely) when you rely on the combined `input` file.
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -26,7 +26,7 @@ def parse_args() -> argparse.Namespace:
|
||||
type=Path,
|
||||
nargs="?",
|
||||
default=Path("inputs/camTrack_v01.asci"),
|
||||
help="Path to Nuke ASCI export",
|
||||
help="Path to Nuke ASCI export (combined order: Tx Rx Ty Ry Tz Rz)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"output",
|
||||
@@ -35,6 +35,16 @@ def parse_args() -> argparse.Namespace:
|
||||
default=Path("outputs/camTrack_v01_converted.txt"),
|
||||
help="Destination RealEstate-style TXT file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input-translation",
|
||||
type=Path,
|
||||
help="Optional ASCI file containing translation columns only (order: Tx Ty Tz)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input-rotation",
|
||||
type=Path,
|
||||
help="Optional ASCI file containing rotation columns only (order: Rx Ry Rz in degrees)",
|
||||
)
|
||||
parser.add_argument("--config", type=Path, help="Optional JSON pipeline config")
|
||||
parser.add_argument("--fps", type=float, default=DEFAULT_FPS, help="Frame rate for timestamps")
|
||||
parser.add_argument("--fx", type=float, default=DEFAULT_FX, help="Normalized focal length x")
|
||||
@@ -65,7 +75,9 @@ def parse_args() -> argparse.Namespace:
|
||||
|
||||
@dataclass
|
||||
class ConverterSettings:
|
||||
input_path: Path
|
||||
input_path: Path | None
|
||||
translation_input_path: Path | None
|
||||
rotation_input_path: Path | None
|
||||
output_path: Path
|
||||
fps: float
|
||||
fx: float
|
||||
@@ -138,6 +150,18 @@ def resolve_settings(args: argparse.Namespace) -> ConverterSettings:
|
||||
config = load_config(args.config)
|
||||
|
||||
input_path = resolve_path(config, "input", args.input)
|
||||
|
||||
def resolve_optional_path(config_key: str, arg_value: Path | None) -> Path | None:
|
||||
if arg_value is not None:
|
||||
return arg_value
|
||||
cfg_val = config.get(config_key)
|
||||
if cfg_val is None:
|
||||
return None
|
||||
return resolve_path(config, config_key, Path(cfg_val))
|
||||
|
||||
translation_input_path = resolve_optional_path("input_translation", args.input_translation)
|
||||
rotation_input_path = resolve_optional_path("input_rotation", args.input_rotation)
|
||||
|
||||
output_path = resolve_path(config, "output", args.output)
|
||||
fps = config.get("fps", args.fps)
|
||||
|
||||
@@ -157,6 +181,9 @@ def resolve_settings(args: argparse.Namespace) -> ConverterSettings:
|
||||
|
||||
metadata = {
|
||||
"config": str(args.config) if args.config else None,
|
||||
"input": str(input_path) if input_path else None,
|
||||
"input_translation": str(translation_input_path) if translation_input_path else None,
|
||||
"input_rotation": str(rotation_input_path) if rotation_input_path else None,
|
||||
"lens": config.get("lens"),
|
||||
"resolution": resolution_cfg or {"width_px": width, "height_px": height},
|
||||
"unit_scale": unit_scale,
|
||||
@@ -169,6 +196,8 @@ def resolve_settings(args: argparse.Namespace) -> ConverterSettings:
|
||||
|
||||
return ConverterSettings(
|
||||
input_path=input_path,
|
||||
translation_input_path=translation_input_path,
|
||||
rotation_input_path=rotation_input_path,
|
||||
output_path=output_path,
|
||||
fps=fps,
|
||||
fx=fx,
|
||||
@@ -255,6 +284,29 @@ def iter_asci_poses(path: Path) -> Iterator[Tuple[List[List[float]], Tuple[float
|
||||
yield euler_zxy_to_matrix(rx, ry, rz), (tx, ty, tz)
|
||||
|
||||
|
||||
def iter_split_asci_poses(
|
||||
translation_path: Path, rotation_path: Path
|
||||
) -> Iterator[Tuple[List[List[float]], Tuple[float, float, float]]]:
|
||||
translation_lines = [line.strip() for line in translation_path.read_text().splitlines() if line.strip()]
|
||||
rotation_lines = [line.strip() for line in rotation_path.read_text().splitlines() if line.strip()]
|
||||
if len(translation_lines) != len(rotation_lines):
|
||||
raise ValueError(
|
||||
f"Translation ({len(translation_lines)}) and rotation ({len(rotation_lines)}) ASCI files "
|
||||
"must contain the same number of rows."
|
||||
)
|
||||
|
||||
for idx, (t_line, r_line) in enumerate(zip(translation_lines, rotation_lines)):
|
||||
t_values = t_line.split()
|
||||
r_values = r_line.split()
|
||||
if len(t_values) != 3:
|
||||
raise ValueError(f"Expected 3 translation columns per line in {translation_path}, got {len(t_values)} (line {idx})")
|
||||
if len(r_values) != 3:
|
||||
raise ValueError(f"Expected 3 rotation columns per line in {rotation_path}, got {len(r_values)} (line {idx})")
|
||||
tx, ty, tz = map(float, t_values)
|
||||
rx, ry, rz = (math.radians(float(val)) for val in r_values)
|
||||
yield euler_zxy_to_matrix(rx, ry, rz), (tx, ty, tz)
|
||||
|
||||
|
||||
def camera_center_to_translation(
|
||||
rotation: Sequence[Sequence[float]], camera_center: Tuple[float, float, float]
|
||||
) -> Tuple[float, float, float]:
|
||||
@@ -288,7 +340,23 @@ def format_pose_rows(
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
settings = resolve_settings(args)
|
||||
poses = list(iter_asci_poses(settings.input_path))
|
||||
# Determine data source: single combined ASCI or split translation/rotation files.
|
||||
if settings.translation_input_path and settings.rotation_input_path:
|
||||
poses = list(
|
||||
iter_split_asci_poses(settings.translation_input_path, settings.rotation_input_path)
|
||||
)
|
||||
source_label = (
|
||||
f"translation:{settings.translation_input_path.as_posix()} "
|
||||
f"rotation:{settings.rotation_input_path.as_posix()}"
|
||||
)
|
||||
elif settings.translation_input_path or settings.rotation_input_path:
|
||||
raise ValueError(
|
||||
"Both input_translation and input_rotation must be provided together. "
|
||||
"Either supply both split files or rely on the combined 'input' ASCI path."
|
||||
)
|
||||
else:
|
||||
poses = list(iter_asci_poses(settings.input_path))
|
||||
source_label = settings.input_path.as_posix()
|
||||
total_available = len(poses)
|
||||
start_idx = max(0, settings.frame_start)
|
||||
end_idx = settings.frame_end if settings.frame_end is not None else total_available
|
||||
@@ -302,7 +370,7 @@ def main() -> None:
|
||||
frame_time_us = int(round(1_000_000 / settings.fps))
|
||||
|
||||
header = (
|
||||
f"source:{settings.input_path.as_posix()} fps:{settings.fps} fx:{settings.fx} fy:{settings.fy} "
|
||||
f"source:{source_label} fps:{settings.fps} fx:{settings.fx} fy:{settings.fy} "
|
||||
f"cx:{settings.cx} cy:{settings.cy} width:{settings.width} height:{settings.height} unit_scale:{settings.unit_scale}"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Nuke script generator for testing camera export pipeline.
|
||||
|
||||
This script generates a Nuke script (.nk) that creates a simple test scene with:
|
||||
- A camera with known, predictable motion
|
||||
- Simple geometry (a grid/card) for reference
|
||||
- Camera animation that can be easily validated
|
||||
|
||||
Usage:
|
||||
python test_nuke_scene.py --output test_scene.nk
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def generate_nuke_script(output_path: Path, num_frames: int = 50) -> None:
|
||||
"""Generate a Nuke script with a test camera animation."""
|
||||
|
||||
# Create a simple camera animation:
|
||||
# - Start at (0, 0, -5) looking at origin
|
||||
# - Move forward along Z axis (dolly in)
|
||||
# - Slight pan left
|
||||
# - Known focal length and filmback for easy validation
|
||||
|
||||
script_lines = [
|
||||
"set cut_paste_input [stack 0]",
|
||||
"version 15.0 v1",
|
||||
"push $cut_paste_input",
|
||||
"",
|
||||
"# Test Camera - Simple forward dolly with pan",
|
||||
"Camera2 {",
|
||||
" inputs 0",
|
||||
f" translate {{curve x {0.0} {num_frames-1} {0.0}; curve y {0.0} {num_frames-1} {0.0}; curve z {-5.0} {num_frames-1} {-3.0}}}",
|
||||
f" rotate {{curve x {0.0} {num_frames-1} {0.0}; curve y {-10.0} {num_frames-1} {-15.0}; curve z {0.0} {num_frames-1} {0.0}}}",
|
||||
" focal {35.0}",
|
||||
" haperture {36.0}",
|
||||
" vaperture {24.0}",
|
||||
" near {0.1}",
|
||||
" far {1000.0}",
|
||||
" win_translate {{0.5 0.5}}",
|
||||
" win_scale {{1 1}}",
|
||||
" winroll {0}",
|
||||
" fov {54.432}",
|
||||
" f_stop {2.8}",
|
||||
" focus_distance {5.0}",
|
||||
" lens {35.0}",
|
||||
" name Camera2",
|
||||
" selected true",
|
||||
" xpos 0",
|
||||
" ypos 0",
|
||||
"}",
|
||||
"",
|
||||
"# Reference Grid - helps visualize camera motion",
|
||||
"Grid2 {",
|
||||
" inputs 0",
|
||||
" display {luminance}",
|
||||
" name Grid2",
|
||||
" selected false",
|
||||
" xpos 0",
|
||||
" ypos -100",
|
||||
"}",
|
||||
"",
|
||||
"# Render node for validation",
|
||||
"Write1 {",
|
||||
" file {test_render_%04d.exr}",
|
||||
" file_type exr",
|
||||
" datatype {32 bit float}",
|
||||
" compression {Zip (1 scanline)}",
|
||||
" name Write1",
|
||||
" selected false",
|
||||
" xpos 0",
|
||||
" ypos -200",
|
||||
"}",
|
||||
]
|
||||
|
||||
output_path.write_text("\n".join(script_lines))
|
||||
print(f"Generated Nuke script: {output_path}")
|
||||
print(f" - {num_frames} frames")
|
||||
print(f" - Camera starts at (0, 0, -5), ends at (0, 0, -3)")
|
||||
print(f" - Camera rotates Y from -10° to -15°")
|
||||
print(f" - Focal: 35mm, Filmback: 36x24mm")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate Nuke test scene")
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
type=Path,
|
||||
default=Path("test_scene.nk"),
|
||||
help="Output Nuke script path",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--frames",
|
||||
type=int,
|
||||
default=50,
|
||||
help="Number of frames in animation",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
generate_nuke_script(args.output, args.frames)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Test pipeline script for validating the camera conversion workflow
|
||||
#
|
||||
# This script:
|
||||
# 1. Generates a test Nuke scene
|
||||
# 2. (Manual step) Export camera from Nuke as ASCI
|
||||
# 3. Converts ASCI to pose format
|
||||
# 4. Validates the conversion
|
||||
#
|
||||
# Usage: ./test_pipeline.sh
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
echo "=== Step 1: Generate Nuke test scene ==="
|
||||
python3 test_nuke_scene.py --output test_scene.nk --frames 50
|
||||
echo "✓ Generated test_scene.nk"
|
||||
echo ""
|
||||
echo "=== Step 2: Manual step - Export camera from Nuke ==="
|
||||
echo "1. Open test_scene.nk in Nuke"
|
||||
echo "2. Select Camera2 node"
|
||||
echo "3. Export Ascii with columns: [translate.x, rotate.x, translate.y, rotate.y, translate.z, rotate.z]"
|
||||
echo "4. Save as: inputs/test_camera.asci"
|
||||
echo ""
|
||||
read -p "Press Enter when you've exported the ASCI file..."
|
||||
|
||||
if [ ! -f "inputs/test_camera.asci" ]; then
|
||||
echo "ERROR: inputs/test_camera.asci not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Step 3: Convert ASCI to pose format ==="
|
||||
python3 nuke_ASCI-2-Pose_converter.py \
|
||||
inputs/test_camera.asci \
|
||||
outputs/test_camera_converted.txt \
|
||||
--fps 25.0 \
|
||||
--focal-length-mm 35.0 \
|
||||
--sensor-width-mm 36.0 \
|
||||
--sensor-height-mm 24.0 \
|
||||
--width 1920 \
|
||||
--height 1080 \
|
||||
--unit-scale 1.0
|
||||
|
||||
echo ""
|
||||
echo "=== Step 4: Validate conversion ==="
|
||||
python3 validate_conversion.py \
|
||||
inputs/test_camera.asci \
|
||||
outputs/test_camera_converted.txt \
|
||||
--unit-scale 1.0
|
||||
|
||||
echo ""
|
||||
echo "=== Pipeline test complete! ==="
|
||||
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
set cut_paste_input [stack 0]
|
||||
version 15.0 v1
|
||||
push $cut_paste_input
|
||||
|
||||
# Test Camera - Simple forward dolly with pan
|
||||
Camera2 {
|
||||
inputs 0
|
||||
translate {curve x 0.0 80 0.0; curve y 0.0 80 0.0; curve z -5.0 80 -3.0}
|
||||
rotate {curve x 0.0 80 0.0; curve y -10.0 80 -15.0; curve z 0.0 80 0.0}
|
||||
focal {35.0}
|
||||
haperture {36.0}
|
||||
vaperture {24.0}
|
||||
near {0.1}
|
||||
far {1000.0}
|
||||
win_translate {{0.5 0.5}}
|
||||
win_scale {{1 1}}
|
||||
winroll {0}
|
||||
fov {54.432}
|
||||
f_stop {2.8}
|
||||
focus_distance {5.0}
|
||||
lens {35.0}
|
||||
name Camera2
|
||||
selected true
|
||||
xpos 0
|
||||
ypos 0
|
||||
}
|
||||
|
||||
# Reference Grid - helps visualize camera motion
|
||||
Grid2 {
|
||||
inputs 0
|
||||
display {luminance}
|
||||
name Grid2
|
||||
selected false
|
||||
xpos 0
|
||||
ypos -100
|
||||
}
|
||||
|
||||
# Render node for validation
|
||||
Write1 {
|
||||
file {test_render_%04d.exr}
|
||||
file_type exr
|
||||
datatype {32 bit float}
|
||||
compression {Zip (1 scanline)}
|
||||
name Write1
|
||||
selected false
|
||||
xpos 0
|
||||
ypos -200
|
||||
}
|
||||
Reference in New Issue
Block a user