├── .gitignore
├── 3dgs.py
├── README.md
├── assets
├── 3dgs.png
├── README_ch.md
├── tranformation_2d.png
└── transformation_3d.png
├── render_python
├── __init__.py
├── graphic.py
├── raster.py
└── sh.py
├── requirements.txt
└── transformation.py
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | .vscode
3 | .DS_store
4 | demod
5 | tmp
6 | testcases
7 | backup
8 | outputs
9 | testmodels
10 | output
11 | tmp*
--------------------------------------------------------------------------------
/3dgs.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- encoding: utf-8 -*-
3 | """
4 | @ Description:
5 | @ Date : 2024/05/20 17:20:00
6 | @ Author : sunyifan
7 | @ Version : 1.0
8 | """
9 |
10 | import math
11 | import numpy as np
12 | from tqdm import tqdm
13 | from loguru import logger
14 | from math import sqrt, ceil
15 |
16 | from render_python import computeColorFromSH
17 | from render_python import computeCov2D, computeCov3D
18 | from render_python import transformPoint4x4, in_frustum
19 | from render_python import getWorld2View2, getProjectionMatrix, ndc2Pix, in_frustum
20 |
21 |
22 | class Rasterizer:
23 | def __init__(self) -> None:
24 | pass
25 |
26 | def forward(
27 | self,
28 | P, # int, num of guassians
29 | D, # int, degree of spherical harmonics
30 | M, # int, num of sh base function
31 | background, # color of background, default black
32 | width, # int, width of output image
33 | height, # int, height of output image
34 | means3D, # ()center position of 3d gaussian
35 | shs, # spherical harmonics coefficient
36 | colors_precomp,
37 | opacities, # opacities
38 | scales, # scale of 3d gaussians
39 | scale_modifier, # default 1
40 | rotations, # rotation of 3d gaussians
41 | cov3d_precomp,
42 | viewmatrix, # matrix for view transformation
43 | projmatrix, # *(4, 4), matrix for transformation, aka mvp
44 | cam_pos, # position of camera
45 | tan_fovx, # float, tan value of fovx
46 | tan_fovy, # float, tan value of fovy
47 | prefiltered,
48 | ) -> None:
49 |
50 | focal_y = height / (2 * tan_fovy) # focal of y axis
51 | focal_x = width / (2 * tan_fovx)
52 |
53 | # run preprocessing per-Gaussians
54 | # transformation, bounding, conversion of SHs to RGB
55 | logger.info("Starting preprocess per 3d gaussian...")
56 | preprocessed = self.preprocess(
57 | P,
58 | D,
59 | M,
60 | means3D,
61 | scales,
62 | scale_modifier,
63 | rotations,
64 | opacities,
65 | shs,
66 | viewmatrix,
67 | projmatrix,
68 | cam_pos,
69 | width,
70 | height,
71 | focal_x,
72 | focal_y,
73 | tan_fovx,
74 | tan_fovy,
75 | )
76 |
77 | # produce [depth] key and corresponding guassian indices
78 | # sort indices by depth
79 | depths = preprocessed["depths"]
80 | point_list = np.argsort(depths)
81 |
82 | # render
83 | logger.info("Starting render...")
84 | out_color = self.render(
85 | point_list,
86 | width,
87 | height,
88 | preprocessed["points_xy_image"],
89 | preprocessed["rgbs"],
90 | preprocessed["conic_opacity"],
91 | background,
92 | )
93 | return out_color
94 |
95 | def preprocess(
96 | self,
97 | P,
98 | D,
99 | M,
100 | orig_points,
101 | scales,
102 | scale_modifier,
103 | rotations,
104 | opacities,
105 | shs,
106 | viewmatrix,
107 | projmatrix,
108 | cam_pos,
109 | W,
110 | H,
111 | focal_x,
112 | focal_y,
113 | tan_fovx,
114 | tan_fovy,
115 | ):
116 |
117 | rgbs = [] # rgb colors of gaussians
118 | cov3Ds = [] # covariance of 3d gaussians
119 | depths = [] # depth of 3d gaussians after view&proj transformation
120 | radii = [] # radius of 2d gaussians
121 | conic_opacity = [] # covariance inverse of 2d gaussian and opacity
122 | points_xy_image = [] # mean of 2d guassians
123 | for idx in range(P):
124 | # make sure point in frustum
125 | p_orig = orig_points[idx]
126 | p_view = in_frustum(p_orig, viewmatrix)
127 | if p_view is None:
128 | continue
129 | depths.append(p_view[2])
130 |
131 | # transform point, from world to ndc
132 | # Notice, projmatrix already processed as mvp matrix
133 | p_hom = transformPoint4x4(p_orig, projmatrix)
134 | p_w = 1 / (p_hom[3] + 0.0000001)
135 | p_proj = [p_hom[0] * p_w, p_hom[1] * p_w, p_hom[2] * p_w]
136 |
137 | # compute 3d covarance by scaling and rotation parameters
138 | scale = scales[idx]
139 | rotation = rotations[idx]
140 | cov3D = computeCov3D(scale, scale_modifier, rotation)
141 | cov3Ds.append(cov3D)
142 |
143 | # compute 2D screen-space covariance matrix
144 | # based on splatting, -> JW Sigma W^T J^T
145 | cov = computeCov2D(
146 | p_orig, focal_x, focal_y, tan_fovx, tan_fovy, cov3D, viewmatrix
147 | )
148 |
149 | # invert covarance(EWA splatting)
150 | det = cov[0] * cov[2] - cov[1] * cov[1]
151 | if det == 0:
152 | depths.pop()
153 | cov3Ds.pop()
154 | continue
155 | det_inv = 1 / det
156 | conic = [cov[2] * det_inv, -cov[1] * det_inv, cov[0] * det_inv]
157 | conic_opacity.append([conic[0], conic[1], conic[2], opacities[idx]])
158 |
159 | # compute radius, by finding eigenvalues of 2d covariance
160 | # transfrom point from NDC to Pixel
161 | mid = 0.5 * (cov[0] + cov[1])
162 | lambda1 = mid + sqrt(max(0.1, mid * mid - det))
163 | lambda2 = mid - sqrt(max(0.1, mid * mid - det))
164 | my_radius = ceil(3 * sqrt(max(lambda1, lambda2)))
165 | point_image = [ndc2Pix(p_proj[0], W), ndc2Pix(p_proj[1], H)]
166 |
167 | radii.append(my_radius)
168 | points_xy_image.append(point_image)
169 |
170 | # convert spherical harmonics coefficients to RGB color
171 | sh = shs[idx]
172 | result = computeColorFromSH(D, p_orig, cam_pos, sh)
173 | rgbs.append(result)
174 |
175 | return dict(
176 | rgbs=rgbs,
177 | cov3Ds=cov3Ds,
178 | depths=depths,
179 | radii=radii,
180 | conic_opacity=conic_opacity,
181 | points_xy_image=points_xy_image,
182 | )
183 |
184 | def render(
185 | self, point_list, W, H, points_xy_image, features, conic_opacity, bg_color
186 | ):
187 |
188 | out_color = np.zeros((H, W, 3))
189 | pbar = tqdm(range(H * W))
190 |
191 | # loop pixel
192 | for i in range(H):
193 | for j in range(W):
194 | pbar.update(1)
195 | pixf = [i, j]
196 | C = [0, 0, 0]
197 |
198 | # loop gaussian
199 | for idx in point_list:
200 |
201 | # init helper variables, transmirrance
202 | T = 1
203 |
204 | # Resample using conic matrix
205 | # (cf. "Surface Splatting" by Zwicker et al., 2001)
206 | xy = points_xy_image[idx] # center of 2d gaussian
207 | d = [
208 | xy[0] - pixf[0],
209 | xy[1] - pixf[1],
210 | ] # distance from center of pixel
211 | con_o = conic_opacity[idx]
212 | power = (
213 | -0.5 * (con_o[0] * d[0] * d[0] + con_o[2] * d[1] * d[1])
214 | - con_o[1] * d[0] * d[1]
215 | )
216 | if power > 0:
217 | continue
218 |
219 | # Eq. (2) from 3D Gaussian splatting paper.
220 | # Compute color
221 | alpha = min(0.99, con_o[3] * np.exp(power))
222 | if alpha < 1 / 255:
223 | continue
224 | test_T = T * (1 - alpha)
225 | if test_T < 0.0001:
226 | break
227 |
228 | # Eq. (3) from 3D Gaussian splatting paper.
229 | color = features[idx]
230 | for ch in range(3):
231 | C[ch] += color[ch] * alpha * T
232 |
233 | T = test_T
234 |
235 | # get final color
236 | for ch in range(3):
237 | out_color[j, i, ch] = C[ch] + T * bg_color[ch]
238 |
239 | return out_color
240 |
241 |
242 | if __name__ == "__main__":
243 | # set guassian
244 | pts = np.array([[2, 0, -2], [0, 2, -2], [-2, 0, -2]])
245 | n = len(pts)
246 | shs = np.random.random((n, 16, 3))
247 | opacities = np.ones((n, 1))
248 | scales = np.ones((n, 3))
249 | rotations = np.array([np.eye(3)] * n)
250 |
251 | # set camera
252 | cam_pos = np.array([0, 0, 5])
253 | R = np.array([[1, 0, 0], [0, 1, 0], [0, 0, -1]])
254 | proj_param = {"znear": 0.01, "zfar": 100, "fovX": 45, "fovY": 45}
255 | viewmatrix = getWorld2View2(R=R, t=cam_pos)
256 | projmatrix = getProjectionMatrix(**proj_param)
257 | projmatrix = np.dot(projmatrix, viewmatrix)
258 | tanfovx = math.tan(proj_param["fovX"] * 0.5)
259 | tanfovy = math.tan(proj_param["fovY"] * 0.5)
260 |
261 | # render
262 | rasterizer = Rasterizer()
263 | out_color = rasterizer.forward(
264 | P=len(pts),
265 | D=3,
266 | M=16,
267 | background=np.array([0, 0, 0]),
268 | width=700,
269 | height=700,
270 | means3D=pts,
271 | shs=shs,
272 | colors_precomp=None,
273 | opacities=opacities,
274 | scales=scales,
275 | scale_modifier=1,
276 | rotations=rotations,
277 | cov3d_precomp=None,
278 | viewmatrix=viewmatrix,
279 | projmatrix=projmatrix,
280 | cam_pos=cam_pos,
281 | tan_fovx=tanfovx,
282 | tan_fovy=tanfovy,
283 | prefiltered=None,
284 | )
285 |
286 | import matplotlib.pyplot as plt
287 |
288 | plt.imshow(out_color)
289 | plt.show()
290 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🌟 3dgs_render_python
2 |
3 | English | [中文](assets/README_ch.md)
4 |
5 | ## 🚀 Introduction
6 | **3dgs_render_python** is a project aimed at reimplementing the CUDA code part of [3DGS](https://github.com/graphdeco-inria/gaussian-splatting) using Python. As a result, we have not only preserved the core functionality of the algorithm but also greatly enhanced the readability and maintainability of the code.
7 |
8 | ### 🌈 Advantages
9 | - **Transparency**: Rewriting CUDA code in Python makes the internal logic of the algorithm clearer, facilitating understanding and learning.
10 | - **Readability**: For beginners and researchers, this is an excellent opportunity to delve into parallel computing and 3DGS algorithms.
11 |
12 | ### 🔍 Disadvantages
13 | - **Performance**: Since the project uses the CPU to simulate tasks originally handled by the GPU, the execution speed is slower than the native CUDA implementation.
14 | - **Resource Consumption**: Simulating GPU operations with the CPU may lead to high CPU usage and memory consumption.
15 |
16 | ### 🛠️ Objective
17 | The goal of this project is to provide an implementation of the 3DGS rendering part algorithm that is easier to understand and to offer a platform for users who wish to learn and experiment with 3D graphics algorithms without GPU hardware support.
18 |
19 | ## 📚 Applicable Scenarios
20 | - **Education and Research**: Providing the academic community with the opportunity to delve into the study of 3DGS algorithms.
21 | - **Personal Learning**: Helping individual learners understand the complexities of parallel computing and 3DGS.
22 |
23 | Through **3dgs_render_python**, we hope to stimulate the community's interest in 3D graphics algorithms and promote broader learning and innovation.
24 |
25 | ## 🔧 Quick Start
26 |
27 | ### Installation Steps
28 |
29 | ```bash
30 | # Clone the project using Git
31 | git clone https://github.com/SY-007-Research/3dgs_render_python.git
32 |
33 | # Enter the project directory
34 | cd 3dgs_render_python
35 |
36 | # install requirements
37 | pip install -r requirements.txt
38 | ```
39 |
40 | ### Running the Project
41 |
42 | ```bash
43 | # Transformation demo
44 | python transformation.py
45 | ```
46 |
47 |
48 | |transformation 3d|transformation 2d|
49 | |---|---|
50 | |
|
|
51 |
52 | ```bash
53 | # 3DGS demo
54 | python 3dgs.py
55 | ```
56 |
57 |
58 |
59 | ## 🏅 Support
60 |
61 | If you like this project, you can support us in the following ways:
62 |
63 | - [GitHub Star](https://github.com/SY-007-Research/3dgs_render_python)
64 | - [bilibili](https://space.bilibili.com/644569334)
65 |
--------------------------------------------------------------------------------
/assets/3dgs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SY-007-Research/3dgs_render_python/24e92257b8b60158cda17a21109609ed957930c2/assets/3dgs.png
--------------------------------------------------------------------------------
/assets/README_ch.md:
--------------------------------------------------------------------------------
1 | # 🌟 3dgs_render_python
2 |
3 | ## 🚀 简介
4 | **3dgs_render_python** 旨在将[3DGS](https://github.com/graphdeco-inria/gaussian-splatting)中的CUDA代码部分用Python重新实现。由此,我们不仅保留了算法的核心功能,还极大地提高了代码的可读性和可维护性。
5 |
6 | ### 🌈 优势
7 | - **透明性**: 使用Python重写CUDA代码,使得算法的内部逻辑更加清晰,便于理解和学习。
8 | - **易读性**: 对于初学者和研究者来说,这是一个深入理解并行计算和3dgs算法的绝佳机会。
9 |
10 | ### 🔍 缺点
11 | - **性能**: 由于使用CPU来模拟原本由GPU处理的任务,项目在执行速度上不如原生CUDA实现,速度慢。
12 | - **资源消耗**: CPU模拟GPU操作可能会导致较高的CPU使用率和内存消耗。
13 |
14 | ### 🛠️ 目标
15 | 本项目的目标是提供一个更加易于理解的3DGS的渲染部分算法实现,同时为那些希望在没有GPU硬件支持的情况下学习和实验3D图形算法的用户提供一个平台。
16 |
17 |
18 | ## 📚 适用场景
19 | - 教育和研究:为学术界提供深入研究3DGS算法的机会。
20 | - 个人学习:帮助个人学习者理解并行计算和3DGS的复杂性。
21 |
22 | 通过**3dgs_render_python**,我们希望能够激发社区对3D图形算法的兴趣,并促进更广泛的学习和创新。
23 |
24 |
25 |
26 | ## 🔧 快速开始
27 |
28 |
29 |
30 | ### 安装步骤
31 |
32 | ```bash
33 | # 使用Git克隆项目
34 | git clone https://github.com/SY-007-Research/3dgs_render_python.git
35 |
36 | # 进入项目目录
37 | cd 3dgs_render_python
38 |
39 | # 安装依赖
40 | pip install -r requirements.txt
41 | ```
42 |
43 | ### 运行项目
44 |
45 | ```bash
46 | # transformation demo
47 | python transformation.py
48 | ```
49 |
50 |
51 | |transformation 3d|transformation 2d|
52 | |---|---|
53 | |
|
|
54 |
55 | ```bash
56 | # 3dgs demo
57 | python 3dgs.py
58 | ```
59 |
60 |
61 |
62 | ## 🏅 支持
63 |
64 | 如果你喜欢这个项目,可以通过以下方式支持我们:
65 |
66 | - [GitHub Star](https://github.com/SY-007-Research/3dgs_render_python)
67 | - [bilibili](https://space.bilibili.com/644569334?spm_id_from=333.1296.0.0)
--------------------------------------------------------------------------------
/assets/tranformation_2d.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SY-007-Research/3dgs_render_python/24e92257b8b60158cda17a21109609ed957930c2/assets/tranformation_2d.png
--------------------------------------------------------------------------------
/assets/transformation_3d.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SY-007-Research/3dgs_render_python/24e92257b8b60158cda17a21109609ed957930c2/assets/transformation_3d.png
--------------------------------------------------------------------------------
/render_python/__init__.py:
--------------------------------------------------------------------------------
1 | from .sh import computeColorFromSH
2 | from .raster import computeCov2D, computeCov3D
3 | from .raster import transformPoint4x4, in_frustum, ndc2Pix, in_frustum
4 | from .graphic import getWorld2View2, getProjectionMatrix
5 |
--------------------------------------------------------------------------------
/render_python/graphic.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- encoding: utf-8 -*-
3 | """
4 | @ Description:
5 | @ Date : 2024/05/21 11:31:03
6 | @ Author : sunyifan
7 | @ Version : 1.0
8 | """
9 |
10 | import math
11 | import numpy as np
12 |
13 |
14 | def getWorld2View2(R, t, translate=np.array([0.0, 0.0, 0.0]), scale=1.0):
15 | Rt = np.zeros((4, 4))
16 | Rt[:3, :3] = R.transpose()
17 | Rt[:3, 3] = t
18 | Rt[3, 3] = 1.0
19 |
20 | C2W = np.linalg.inv(Rt)
21 | cam_center = C2W[:3, 3]
22 | cam_center = (cam_center + translate) * scale
23 | C2W[:3, 3] = cam_center
24 | Rt = np.linalg.inv(C2W)
25 | return np.float32(Rt)
26 |
27 |
28 | def getProjectionMatrix(znear, zfar, fovX, fovY):
29 | tanHalfFovY = math.tan((fovY / 2))
30 | tanHalfFovX = math.tan((fovX / 2))
31 |
32 | top = tanHalfFovY * znear
33 | bottom = -top
34 | right = tanHalfFovX * znear
35 | left = -right
36 |
37 | P = np.zeros((4, 4))
38 |
39 | z_sign = 1.0
40 |
41 | P[0, 0] = 2.0 * znear / (right - left)
42 | P[1, 1] = 2.0 * znear / (top - bottom)
43 | P[0, 2] = (right + left) / (right - left)
44 | P[1, 2] = (top + bottom) / (top - bottom)
45 | P[3, 2] = z_sign
46 | P[2, 2] = z_sign * zfar / (zfar - znear)
47 | P[2, 3] = -(zfar * znear) / (zfar - znear)
48 | return P
49 |
50 |
51 | if __name__ == "__main__":
52 | p = [2, 0, -2]
53 | proj_param = {"znear": 0.01, "zfar": 100, "fovX": 45, "fovY": 45}
54 | projmatrix = getProjectionMatrix(**proj_param)
55 | print(projmatrix)
56 |
--------------------------------------------------------------------------------
/render_python/raster.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- encoding: utf-8 -*-
3 | """
4 | @ Description:
5 | @ Date : 2024/05/21 11:31:03
6 | @ Author : sunyifan
7 | @ Version : 1.0
8 | """
9 |
10 | import numpy as np
11 | from .graphic import getProjectionMatrix
12 |
13 |
14 | def ndc2Pix(v, S):
15 | return ((v + 1.0) * S - 1.0) * 0.5
16 |
17 |
18 | def in_frustum(p_orig, viewmatrix):
19 | # bring point to screen space
20 | p_view = transformPoint4x3(p_orig, viewmatrix)
21 |
22 | if p_view[2] <= 0.2:
23 | return None
24 | return p_view
25 |
26 |
27 | def transformPoint4x4(p, matrix):
28 | matrix = np.array(matrix).flatten(order="F")
29 | x, y, z = p
30 | transformed = np.array(
31 | [
32 | matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12],
33 | matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13],
34 | matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14],
35 | matrix[3] * x + matrix[7] * y + matrix[11] * z + matrix[15],
36 | ]
37 | )
38 | return transformed
39 |
40 |
41 | def transformPoint4x3(p, matrix):
42 | matrix = np.array(matrix).flatten(order="F")
43 | x, y, z = p
44 | transformed = np.array(
45 | [
46 | matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12],
47 | matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13],
48 | matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14],
49 | ]
50 | )
51 | return transformed
52 |
53 |
54 | # covariance = RS[S^T][R^T]
55 | def computeCov3D(scale, mod, rot):
56 | # create scaling matrix
57 | S = np.array(
58 | [[scale[0] * mod, 0, 0], [0, scale[1] * mod, 0], [0, 0, scale[2] * mod]]
59 | )
60 |
61 | # normalize quaternion to get valid rotation
62 | # we use rotation matrix
63 | R = rot
64 |
65 | # compute 3d world covariance matrix Sigma
66 | M = np.dot(R, S)
67 | cov3D = np.dot(M, M.T)
68 |
69 | return cov3D
70 |
71 |
72 | def computeCov2D(mean, focal_x, focal_y, tan_fovx, tan_fovy, cov3D, viewmatrix):
73 | # The following models the steps outlined by equations 29
74 | # and 31 in "EWA Splatting" (Zwicker et al., 2002).
75 | # Additionally considers aspect / scaling of viewport.
76 | # Transposes used to account for row-/column-major conventions.
77 |
78 | t = transformPoint4x3(mean, viewmatrix)
79 |
80 | limx = 1.3 * tan_fovx
81 | limy = 1.3 * tan_fovy
82 | txtz = t[0] / t[2]
83 | tytz = t[1] / t[2]
84 | t[0] = min(limx, max(-limx, txtz)) * t[2]
85 | t[1] = min(limy, max(-limy, tytz)) * t[2]
86 |
87 | J = np.array(
88 | [
89 | [focal_x / t[2], 0, -(focal_x * t[0]) / (t[2] * t[2])],
90 | [0, focal_y / t[2], -(focal_y * t[1]) / (t[2] * t[2])],
91 | [0, 0, 0],
92 | ]
93 | )
94 | W = viewmatrix[:3, :3]
95 | T = np.dot(J, W)
96 |
97 | cov = np.dot(T, cov3D)
98 | cov = np.dot(cov, T.T)
99 |
100 | # Apply low-pass filter
101 | # Every Gaussia should be at least one pixel wide/high
102 | # Discard 3rd row and column
103 | cov[0, 0] += 0.3
104 | cov[1, 1] += 0.3
105 | return [cov[0, 0], cov[0, 1], cov[1, 1]]
106 |
107 |
108 | if __name__ == "__main__":
109 | p = [2, 0, -2]
110 | proj_param = {"znear": 0.01, "zfar": 100, "fovX": 45, "fovY": 45}
111 | projmatrix = getProjectionMatrix(**proj_param)
112 | transformed = transformPoint4x4(p, projmatrix)
113 | print(transformed)
114 |
--------------------------------------------------------------------------------
/render_python/sh.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- encoding: utf-8 -*-
3 |
4 | # Copyright 2021 The PlenOctree Authors.
5 | # Redistribution and use in source and binary forms, with or without
6 | # modification, are permitted provided that the following conditions are met:
7 | #
8 | # 1. Redistributions of source code must retain the above copyright notice,
9 | # this list of conditions and the following disclaimer.
10 | #
11 | # 2. Redistributions in binary form must reproduce the above copyright notice,
12 | # this list of conditions and the following disclaimer in the documentation
13 | # and/or other materials provided with the distribution.
14 | #
15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
19 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25 | # POSSIBILITY OF SUCH DAMAGE.
26 |
27 | """
28 | @ Description:
29 | @ Date : 2024/05/22 14:19:32
30 | @ Author : sunyifan
31 | @ Version : 1.0
32 | """
33 |
34 | import numpy as np
35 |
36 |
37 | SH_C0 = 0.28209479177387814
38 | SH_C1 = 0.4886025119029199
39 | SH_C2 = [
40 | 1.0925484305920792,
41 | -1.0925484305920792,
42 | 0.31539156525252005,
43 | -1.0925484305920792,
44 | 0.5462742152960396,
45 | ]
46 | SH_C3 = [
47 | -0.5900435899266435,
48 | 2.890611442640554,
49 | -0.4570457994644658,
50 | 0.3731763325901154,
51 | -0.4570457994644658,
52 | 1.445305721320277,
53 | -0.5900435899266435,
54 | ]
55 |
56 |
57 | def computeColorFromSH(deg, pos, campos, sh):
58 | # The implementation is loosely based on code for
59 | # "Differentiable Point-Based Radiance Fields for
60 | # Efficient View Synthesis" by Zhang et al. (2022)
61 |
62 | dir = pos - campos
63 | dir = dir / np.linalg.norm(dir)
64 |
65 | result = SH_C0 * sh[0]
66 |
67 | if deg > 0:
68 | x, y, z = dir
69 | result = result - SH_C1 * y * sh[1] + SH_C1 * z * sh[2] - SH_C1 * x * sh[3]
70 |
71 | if deg > 1:
72 | xx = x * x
73 | yy = y * y
74 | zz = z * z
75 | xy = x * y
76 | yz = y * z
77 | xz = x * z
78 | result = (
79 | result
80 | + SH_C2[0] * xy * sh[4]
81 | + SH_C2[1] * yz * sh[5]
82 | + SH_C2[2] * (2.0 * zz - xx - yy) * sh[6]
83 | + SH_C2[3] * xz * sh[7]
84 | + SH_C2[4] * (xx - yy) * sh[8]
85 | )
86 |
87 | if deg > 2:
88 | result = (
89 | result
90 | + SH_C3[0] * y * (3.0 * xx - yy) * sh[9]
91 | + SH_C3[1] * xy * z * sh[10]
92 | + SH_C3[2] * y * (4.0 * zz - xx - yy) * sh[11]
93 | + SH_C3[3] * z * (2.0 * zz - 3.0 * xx - 3.0 * yy) * sh[12]
94 | + SH_C3[4] * x * (4.0 * zz - xx - yy) * sh[13]
95 | + SH_C3[5] * z * (xx - yy) * sh[14]
96 | + SH_C3[6] * x * (xx - 3.0 * yy) * sh[15]
97 | )
98 | result += 0.5
99 | return np.clip(result, a_min=0, a_max=1)
100 |
101 |
102 | if __name__ == "__main__":
103 | deg = 3
104 | pos = np.array([2, 0, -2])
105 | campos = np.array([0, 0, 5])
106 | sh = np.random.random((16, 3))
107 | computeColorFromSH(deg, pos, campos, sh)
108 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | opencv-python==4.10.0.82
2 | matplotlib==3.5.3
3 | loguru
4 | tqdm
--------------------------------------------------------------------------------
/transformation.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- encoding: utf-8 -*-
3 | """
4 | @ Description:
5 | @ Date : 2024/05/17 11:13:25
6 | @ Author : sunyifan
7 | @ Version : 1.0
8 | """
9 |
10 | import cv2
11 | import numpy as np
12 | import matplotlib.pyplot as plt
13 | from mpl_toolkits.mplot3d import Axes3D
14 |
15 |
16 | # get (h, w, 3) cavas
17 | def create_canvas(h, w):
18 | return np.zeros((h, w, 3))
19 |
20 |
21 | def get_model_matrix(angle):
22 | angle *= np.pi / 180
23 | return np.array(
24 | [
25 | [np.cos(angle), -np.sin(angle), 0, 0],
26 | [np.sin(angle), np.cos(angle), 0, 0],
27 | [0, 0, 1, 0],
28 | [0, 0, 0, 1],
29 | ]
30 | )
31 |
32 |
33 | # from world to camera
34 | def get_view_matrix(eye_pose):
35 | return np.array(
36 | [
37 | [1, 0, 0, -eye_pose[0]],
38 | [0, 1, 0, -eye_pose[1]],
39 | [0, 0, 1, -eye_pose[2]],
40 | [0, 0, 0, 1],
41 | ]
42 | )
43 |
44 |
45 | # get projection, including perspective and orthographic
46 | def get_proj_matrix(fov, aspect, near, far):
47 | t2a = np.tan(fov / 2.0)
48 | return np.array(
49 | [
50 | [1 / (aspect * t2a), 0, 0, 0],
51 | [0, 1 / t2a, 0, 0],
52 | [0, 0, (near + far) / (near - far), 2 * near * far / (near - far)],
53 | [0, 0, -1, 0],
54 | ]
55 | )
56 |
57 |
58 | def get_viewport_matrix(h, w):
59 | return np.array(
60 | [[w / 2, 0, 0, w / 2], [0, h / 2, 0, h / 2], [0, 0, 1, 0], [0, 0, 0, 1]]
61 | )
62 |
63 |
64 | if __name__ == "__main__":
65 | frame = create_canvas(700, 700)
66 | angle = 0
67 | eye = [0, 0, 5]
68 | pts = [[2, 0, -2], [0, 2, -2], [-2, 0, -2]]
69 | viewport = get_viewport_matrix(700, 700)
70 |
71 | # get mvp matrix
72 | mvp = get_model_matrix(angle)
73 | mvp = np.dot(get_view_matrix(eye), mvp)
74 | mvp = np.dot(get_proj_matrix(45, 1, 0.1, 50), mvp) # 4x4
75 |
76 | # loop points
77 | pts_2d = []
78 | for p in pts:
79 | p = np.array(p + [1]) # 3x1 -> 4x1
80 | p = np.dot(mvp, p)
81 | p /= p[3]
82 |
83 | # viewport
84 | p = np.dot(viewport, p)[:2]
85 | pts_2d.append([int(p[0]), int(p[1])])
86 |
87 | vis = 1
88 | if vis:
89 | # visualize 3d
90 | fig = plt.figure()
91 | pts = np.array(pts)
92 | x, y, z = pts[:, 0], pts[:, 1], pts[:, 2]
93 |
94 | ax = Axes3D(fig)
95 | ax.scatter(x, y, z, s=80, marker="^", c="g")
96 | ax.scatter([eye[0]], [eye[1]], [eye[2]], s=180, marker=7, c="r")
97 | ax.plot_trisurf(x, y, z, linewidth=0.2, antialiased=True, alpha=0.5)
98 | plt.show()
99 |
100 | # visualize 2d
101 | c = (255, 255, 255)
102 | for i in range(3):
103 | for j in range(i + 1, 3):
104 | cv2.line(frame, pts_2d[i], pts_2d[j], c, 2)
105 | cv2.imshow("screen", frame)
106 | cv2.waitKey(0)
107 |
--------------------------------------------------------------------------------