kylinfires commited on
Commit
18f7e34
·
verified ·
1 Parent(s): f2de094

WorldGen: perspective 3DGS

Browse files
Files changed (1) hide show
  1. worldgen/render_splat_perspective.py +144 -0
worldgen/render_splat_perspective.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Perspective 3DGS renderer with covariance projection (numba)."""
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ from numba import njit
8
+ from PIL import Image
9
+
10
+ SH_C0 = 0.28209479177387814
11
+
12
+
13
+ def load_ply(path: Path):
14
+ b = path.read_bytes()
15
+ hs = b.index(b"end_header\n") + 11
16
+ head = b[:hs].decode()
17
+ n = int([l for l in head.splitlines() if l.startswith("element vertex")][0].split()[2])
18
+ return np.frombuffer(b[hs:hs + n * 17 * 4], dtype="<f4").reshape(n, 17)
19
+
20
+
21
+ @njit
22
+ def render_persp(xc, yc, zc, scales, quats, colors, opacity, W, H, fx, fy, cyaw, syaw, max_radius):
23
+ img = np.zeros((H, W, 3), np.float32)
24
+ trans = np.zeros((H, W), np.float32)
25
+ n = xc.shape[0]
26
+ for i in range(n):
27
+ z = zc[i]
28
+ if z < 0.05:
29
+ continue
30
+ w, qx, qy, qz = quats[i, 0], quats[i, 1], quats[i, 2], quats[i, 3]
31
+ qn = np.sqrt(w * w + qx * qx + qy * qy + qz * qz)
32
+ if qn < 1e-8:
33
+ w, qx, qy, qz = 1.0, 0.0, 0.0, 0.0
34
+ else:
35
+ w, qx, qy, qz = w / qn, qx / qn, qy / qn, qz / qn
36
+ R = np.empty((3, 3), np.float32)
37
+ R[0, 0] = 1 - 2 * (qy * qy + qz * qz)
38
+ R[0, 1] = 2 * (qx * qy - w * qz)
39
+ R[0, 2] = 2 * (qx * qz + w * qy)
40
+ R[1, 0] = 2 * (qx * qy + w * qz)
41
+ R[1, 1] = 1 - 2 * (qx * qx + qz * qz)
42
+ R[1, 2] = 2 * (qy * qz - w * qx)
43
+ R[2, 0] = 2 * (qx * qz - w * qy)
44
+ R[2, 1] = 2 * (qy * qz + w * qx)
45
+ R[2, 2] = 1 - 2 * (qx * qx + qy * qy)
46
+ s = np.exp(scales[i])
47
+ C = np.empty((3, 3), np.float32)
48
+ for a in range(3):
49
+ for b in range(3):
50
+ t = 0.0
51
+ for k in range(3):
52
+ t += R[a, k] * s[k] * s[k] * R[b, k]
53
+ C[a, b] = t
54
+ # camera-space covariance (yaw about Y)
55
+ cxx = C[0, 0]; cxz = C[0, 2]; czz = C[2, 2]
56
+ cc = np.empty((3, 3), np.float32)
57
+ cc[0, 0] = cyaw * cyaw * cxx - 2 * cyaw * syaw * cxz + syaw * syaw * czz
58
+ cc[0, 2] = cyaw * syaw * (cxx - czz) + (cyaw * cyaw - syaw * syaw) * cxz
59
+ cc[2, 0] = cc[0, 2]
60
+ cc[2, 2] = syaw * syaw * cxx + 2 * syaw * cyaw * cxz + cyaw * cyaw * czz
61
+ cc[1, 1] = C[1, 1]
62
+ cc[0, 1] = cyaw * C[0, 1] - syaw * C[2, 1]
63
+ cc[1, 0] = cc[0, 1]
64
+ cc[1, 2] = syaw * C[0, 1] + cyaw * C[2, 1]
65
+ cc[2, 1] = cc[1, 2]
66
+ # perspective Jacobian
67
+ iz = 1.0 / z
68
+ j00 = fx * iz
69
+ j02 = -fx * xc[i] * iz * iz
70
+ j11 = fy * iz
71
+ j12 = -fy * yc[i] * iz * iz
72
+ a11 = j00 * j00 * cc[0, 0] + 2 * j00 * j02 * cc[0, 2] + j02 * j02 * cc[2, 2]
73
+ a22 = j11 * j11 * cc[1, 1] + 2 * j11 * j12 * cc[1, 2] + j12 * j12 * cc[2, 2]
74
+ a12 = j00 * j11 * cc[0, 1] + j00 * j12 * cc[0, 2] + j02 * j11 * cc[2, 1] + j02 * j12 * cc[2, 2]
75
+ tr = a11 + a22
76
+ det = a11 * a22 - a12 * a12
77
+ if det <= 1e-12:
78
+ continue
79
+ l1 = 0.5 * tr + np.sqrt(max(0.25 * tr * tr - det, 0.0))
80
+ rad = int(min(max_radius, max(1.0, 3.0 * np.sqrt(l1))))
81
+ u = fx * xc[i] * iz + W / 2
82
+ v = fy * yc[i] * iz + H / 2
83
+ u0 = int(np.floor(u))
84
+ v0 = int(np.floor(v))
85
+ inv_det = 1.0 / det
86
+ op = opacity[i]
87
+ for yy in range(v0 - rad, v0 + rad + 1):
88
+ if yy < 0 or yy >= H:
89
+ continue
90
+ dy = yy + 0.5 - v
91
+ for xx in range(u0 - rad, u0 + rad + 1):
92
+ if xx < 0 or xx >= W:
93
+ continue
94
+ dx = xx + 0.5 - u
95
+ q = a22 * dx * dx - 2 * a12 * dx * dy + a11 * dy * dy
96
+ a = op * np.exp(-0.5 * q * inv_det)
97
+ if a < 0.003:
98
+ continue
99
+ img[yy, xx, 0] = img[yy, xx, 0] * (1 - a) + colors[i, 0] * a
100
+ img[yy, xx, 1] = img[yy, xx, 1] * (1 - a) + colors[i, 1] * a
101
+ img[yy, xx, 2] = img[yy, xx, 2] * (1 - a) + colors[i, 2] * a
102
+ trans[yy, xx] = trans[yy, xx] * (1 - a) + a
103
+ for yy in range(H):
104
+ for xx in range(W):
105
+ t = trans[yy, xx]
106
+ img[yy, xx] = img[yy, xx] + (1 - t) * np.array([0.78, 0.87, 0.93])
107
+ return img
108
+
109
+
110
+ def main():
111
+ ap = argparse.ArgumentParser()
112
+ ap.add_argument("--ply", required=True)
113
+ ap.add_argument("--out", required=True)
114
+ ap.add_argument("--width", type=int, default=832)
115
+ ap.add_argument("--height", type=int, default=480)
116
+ ap.add_argument("--fov", type=float, default=90.0)
117
+ ap.add_argument("--yaw", type=float, default=0.0)
118
+ ap.add_argument("--scale-factor", type=float, default=1.0)
119
+ ap.add_argument("--max-radius", type=int, default=8)
120
+ args = ap.parse_args()
121
+
122
+ v = load_ply(Path(args.ply))
123
+ yaw = np.radians(args.yaw)
124
+ cy, sy = np.cos(yaw), np.sin(yaw)
125
+ c = v[:, :3].astype(np.float32)
126
+ xc = cy * c[:, 0] - sy * c[:, 2]
127
+ zc = sy * c[:, 0] + cy * c[:, 2]
128
+ yc = c[:, 1].copy()
129
+ order = np.argsort(-zc)
130
+ v = v[order]
131
+ xc, yc, zc = xc[order], yc[order], zc[order]
132
+ colors = np.clip(0.5 + SH_C0 * v[:, 6:9], 0, 1).astype(np.float32)
133
+ opacity = (1.0 / (1.0 + np.exp(-v[:, 16]))).astype(np.float32)
134
+ fx = (args.width / 2) / np.tan(np.radians(args.fov) / 2)
135
+ fy = fx
136
+ img = render_persp(xc, yc, zc, (v[:, 10:13] + np.log(args.scale_factor)).astype(np.float32),
137
+ v[:, 13:17].astype(np.float32), colors, opacity,
138
+ args.width, args.height, fx, fy, cy, sy, args.max_radius)
139
+ Image.fromarray((np.clip(img, 0, 1) * 255).astype(np.uint8)).save(args.out)
140
+ print("perspective", len(v), "->", args.out)
141
+
142
+
143
+ if __name__ == "__main__":
144
+ main()