File size: 3,144 Bytes
bc4c433
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""AABB normalization for occupancy XYZ.

Maps a point cloud into a roughly ``[-1, 1]^3`` cube so the MLP sees
comparable coordinates across differently sized meshes.

  center = midpoint of the axis-aligned bounding box
  scale  = maximum half-extent (longest AABB side / 2)

Normalized point: ``(xyz - center) / scale``.
This module does not touch the occupancy model.
"""

from __future__ import annotations

import numpy as np
from numpy.typing import NDArray

PointsArray = NDArray[np.float32]


def compute_center_scale(points: np.ndarray) -> tuple[PointsArray, float]:
    """
    AABB center and max half-extent for an ``(N, 3)`` point array.

    Parameters
    ----------
    points:
        Query XYZ, shape ``(N, 3)``, at least one row.

    Returns
    -------
    center:
        ``float32`` vector of shape ``(3,)``.
    scale:
        Positive float (max half-extent). Raises if the cloud has no extent.
    """
    pts = np.asarray(points, dtype=np.float32)
    if pts.ndim != 2 or pts.shape[1] != 3:
        raise ValueError(f"points must have shape (N, 3), got {tuple(pts.shape)}")
    if pts.shape[0] == 0:
        raise ValueError("points must contain at least one row")

    xyz_min = pts.min(axis=0)
    xyz_max = pts.max(axis=0)
    center = 0.5 * (xyz_min + xyz_max)
    half_extents = 0.5 * (xyz_max - xyz_min)
    scale = float(np.max(half_extents))
    if scale <= 0.0:
        raise ValueError(
            "scale must be > 0; all points appear to share the same location"
        )
    return center.astype(np.float32, copy=False), scale


def apply_normalization(
    points: np.ndarray,
    center: np.ndarray,
    scale: float,
) -> PointsArray:
    """
    Return ``(points - center) / scale`` as ``float32 (N, 3)``.

    Parameters
    ----------
    points:
        Query XYZ, shape ``(N, 3)``.
    center:
        AABB midpoint, shape ``(3,)``.
    scale:
        Positive max half-extent.

    Returns
    -------
    ndarray
        Normalized points, ``float32 (N, 3)``.
    """
    if scale <= 0.0:
        raise ValueError(f"scale must be > 0, got {scale}")
    pts = np.asarray(points, dtype=np.float32)
    if pts.ndim != 2 or pts.shape[1] != 3:
        raise ValueError(f"points must have shape (N, 3), got {tuple(pts.shape)}")
    c = np.asarray(center, dtype=np.float32).reshape(3)
    return (pts - c) / np.float32(scale)


if __name__ == "__main__":
    from scatteringnet.config import load_config
    from scatteringnet.data_npz import load_points_labels

    sample = (
        load_config().data_dir
        / "exports"
        / "dataset_test"
        / "sphere__raycast_z_raut_s0.15_inout.npz"
    )
    points, _labels = load_points_labels(sample)
    center, scale = compute_center_scale(points)
    normed = apply_normalization(points, center, scale)
    recovered = normed[:3] * np.float32(scale) + center
    print(f"file={sample}")
    print(f"center={center.tolist()} scale={scale:.6f}")
    print(f"normed_min={normed.min(axis=0).tolist()}")
    print(f"normed_max={normed.max(axis=0).tolist()}")
    print(f"inverse_ok={np.allclose(recovered, points[:3], rtol=1e-5, atol=1e-5)}")