├── .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 | --------------------------------------------------------------------------------