File size: 5,358 Bytes
dc09dcc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
"""Camera JSON validation shared by inference and the CPU-only conversion tool."""
import json
from pathlib import Path

import numpy as np
from PIL import Image


IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.bmp', '.tif', '.tiff', '.webp'}


def list_image_files(image_dir):
    folder = Path(image_dir)
    if not folder.is_dir():
        raise ValueError(f'Image directory not found: {folder}')
    images = sorted(p for p in folder.iterdir()
                    if p.is_file() and p.suffix.lower() in IMAGE_EXTENSIONS)
    if not images:
        raise ValueError(f'No supported images found in {folder}')
    stems = [p.stem for p in images]
    if len(stems) != len(set(stems)):
        raise ValueError('Image filename stems must be unique for camera matching and output names.')
    return images


def validate_pose(matrix, camera_id):
    if matrix.shape != (4, 4) or not np.isfinite(matrix).all():
        raise ValueError(f'Camera {camera_id}: pose must be a finite 4x4 matrix.')
    rotation = matrix[:3, :3]
    if (not np.allclose(matrix[3], [0, 0, 0, 1], atol=1e-4)
            or not np.allclose(rotation.T @ rotation, np.eye(3), atol=1e-3)
            or not np.isclose(np.linalg.det(rotation), 1, atol=1e-3)):
        raise ValueError(f'Camera {camera_id}: expected a rigid transform with a proper rotation '
                         'and last row [0, 0, 0, 1].')


def validate_intrinsics(matrix, camera_id):
    if (matrix.shape != (3, 3) or not np.isfinite(matrix).all()
            or matrix[0, 0] <= 0 or matrix[1, 1] <= 0
            or not np.allclose(matrix[2], [0, 0, 1])):
        raise ValueError(f'Camera {camera_id}: intrinsics must be a finite 3x3 pixel matrix '
                         'with positive focal lengths and last row [0, 0, 1].')
    if not np.allclose([matrix[0, 1], matrix[1, 0]], 0, atol=1e-6):
        raise ValueError(f'Camera {camera_id}: skewed intrinsics are unsupported; '
                         'use rectified pinhole images and their corresponding intrinsics.')


def load_cameras_json(path):
    """Read OpenCV c2w poses and pixel intrinsics, indexed by image filename stem."""
    with Path(path).open(encoding='utf-8') as handle:
        data = json.load(handle)
    if not isinstance(data, dict):
        raise ValueError('Camera JSON must be an object containing extrinsics and intrinsics lists.')
    result = []
    for key, validator in [('extrinsics', validate_pose), ('intrinsics', validate_intrinsics)]:
        entries = data.get(key)
        if not isinstance(entries, list) or not entries:
            raise ValueError(f'Camera JSON requires a non-empty {key} list.')
        matrices = {}
        for entry in entries:
            if not isinstance(entry, dict):
                raise ValueError(f'Each {key} entry must contain camera_id and matrix.')
            camera_id = entry.get('camera_id')
            if not isinstance(camera_id, str) or not camera_id:
                raise ValueError(f'Each {key} camera_id must be a non-empty string filename stem.')
            if camera_id in matrices:
                raise ValueError(f'Duplicate camera_id {camera_id!r} in {key}.')
            try:
                matrix = np.asarray(entry['matrix'], dtype=np.float32)
            except (KeyError, TypeError, ValueError) as exc:
                raise ValueError(f'Camera {camera_id}: {key} requires a numeric matrix.') from exc
            validator(matrix, camera_id)
            matrices[camera_id] = matrix
        result.append(matrices)
    return tuple(result)


def validate_cameras_for_images(images, poses, intrinsics):
    """Check coverage and principal points against the original, unprocessed images."""
    missing = [p.name for p in images if p.stem not in poses or p.stem not in intrinsics]
    if missing:
        raise ValueError(f'Missing camera poses/intrinsics for {len(missing)} images: '
                         f'{", ".join(missing[:5])}. Every input image needs both entries.')
    for path in images:
        validate_pose(poses[path.stem], path.stem)
        matrix = intrinsics[path.stem]
        validate_intrinsics(matrix, path.stem)
        with Image.open(path) as image:
            width, height = image.size
        if not (0 < matrix[0, 2] < width and 0 < matrix[1, 2] < height):
            raise ValueError(f'Camera {path.stem}: principal point must lie inside the original '
                             f'{width}x{height} image. Supply pixel intrinsics at this resolution.')


def write_cameras_json(path, images, poses, intrinsics, overwrite=False):
    """Validate all inputs before writing the JSON consumed by inference.py."""
    validate_cameras_for_images(images, poses, intrinsics)
    data = {'num_cameras': len(images), 'extrinsics': [], 'intrinsics': []}
    for image in images:
        for key, matrices in [('extrinsics', poses), ('intrinsics', intrinsics)]:
            data[key].append({'camera_id': image.stem, 'matrix': matrices[image.stem].tolist()})
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    try:
        with path.open('w' if overwrite else 'x', encoding='utf-8') as handle:
            handle.write(json.dumps(data, indent=2, allow_nan=False) + '\n')
    except FileExistsError as exc:
        raise FileExistsError(f'{path} already exists. Use --overwrite to replace it.') from exc