|
import os |
|
from typing import * |
|
|
|
import numpy as np |
|
import moderngl |
|
|
|
from . import transforms, utils, mesh |
|
|
|
|
|
__all__ = [ |
|
'RastContext', |
|
'rasterize_triangle_faces', |
|
'rasterize_edges', |
|
'texture', |
|
'warp_image_by_depth', |
|
] |
|
|
|
|
|
def map_np_dtype(dtype) -> str: |
|
if dtype == int: |
|
return 'i4' |
|
elif dtype == np.uint8: |
|
return 'u1' |
|
elif dtype == np.uint32: |
|
return 'u2' |
|
elif dtype == np.float16: |
|
return 'f2' |
|
elif dtype == np.float32: |
|
return 'f4' |
|
|
|
|
|
def one_value(dtype): |
|
if dtype == 'u1': |
|
return 255 |
|
elif dtype == 'u2': |
|
return 65535 |
|
else: |
|
return 1 |
|
|
|
|
|
class RastContext: |
|
def __init__(self, standalone: bool = True, backend: str = None, **kwargs): |
|
""" |
|
Create a moderngl context. |
|
|
|
Args: |
|
standalone (bool, optional): whether to create a standalone context. Defaults to True. |
|
backend (str, optional): backend to use. Defaults to None. |
|
|
|
Keyword Args: |
|
See moderngl.create_context |
|
""" |
|
if backend is None: |
|
self.mgl_ctx = moderngl.create_context(standalone=standalone, **kwargs) |
|
else: |
|
self.mgl_ctx = moderngl.create_context(standalone=standalone, backend=backend, **kwargs) |
|
|
|
self.__prog_src = {} |
|
self.__prog = {} |
|
|
|
def __del__(self): |
|
self.mgl_ctx.release() |
|
|
|
def screen_quad(self) -> moderngl.VertexArray: |
|
self.screen_quad_vbo = self.mgl_ctx.buffer(np.array([[-1, -1], [1, -1], [1, 1], [-1, 1]], dtype='f4')) |
|
self.screen_quad_ibo = self.mgl_ctx.buffer(np.array([0, 1, 2, 0, 2, 3], dtype=np.int32)) |
|
|
|
def program_vertex_attribute(self, n: int) -> moderngl.Program: |
|
assert n in [1, 2, 3, 4], 'vertex attribute only supports channels 1, 2, 3, 4' |
|
|
|
if 'vertex_attribute_vsh' not in self.__prog_src: |
|
with open(os.path.join(os.path.dirname(__file__), 'shaders', 'vertex_attribute.vsh'), 'r') as f: |
|
self.__prog_src['vertex_attribute_vsh'] = f.read() |
|
if 'vertex_attribute_fsh' not in self.__prog_src: |
|
with open(os.path.join(os.path.dirname(__file__), 'shaders', 'vertex_attribute.fsh'), 'r') as f: |
|
self.__prog_src['vertex_attribute_fsh'] = f.read() |
|
|
|
if f'vertex_attribute_{n}' not in self.__prog: |
|
vsh = self.__prog_src['vertex_attribute_vsh'].replace('vecN', f'vec{n}') |
|
fsh = self.__prog_src['vertex_attribute_fsh'].replace('vecN', f'vec{n}') |
|
self.__prog[f'vertex_attribute_{n}'] = self.mgl_ctx.program(vertex_shader=vsh, fragment_shader=fsh) |
|
|
|
return self.__prog[f'vertex_attribute_{n}'] |
|
|
|
def program_texture(self, n: int) -> moderngl.Program: |
|
assert n in [1, 2, 3, 4], 'texture only supports channels 1, 2, 3, 4' |
|
|
|
if 'texture_vsh' not in self.__prog_src: |
|
with open(os.path.join(os.path.dirname(__file__), 'shaders', 'texture.vsh'), 'r') as f: |
|
self.__prog_src['texture_vsh'] = f.read() |
|
if 'texture_fsh' not in self.__prog_src: |
|
with open(os.path.join(os.path.dirname(__file__), 'shaders', 'texture.fsh'), 'r') as f: |
|
self.__prog_src['texture_fsh'] = f.read() |
|
|
|
if f'texture_{n}' not in self.__prog: |
|
vsh = self.__prog_src['texture_vsh'].replace('vecN', f'vec{n}') |
|
fsh = self.__prog_src['texture_fsh'].replace('vecN', f'vec{n}') |
|
self.__prog[f'texture_{n}'] = self.mgl_ctx.program(vertex_shader=vsh, fragment_shader=fsh) |
|
self.__prog[f'texture_{n}']['tex'] = 0 |
|
self.__prog[f'texture_{n}']['uv'] = 1 |
|
|
|
return self.__prog[f'texture_{n}'] |
|
|
|
|
|
def rasterize_triangle_faces( |
|
ctx: RastContext, |
|
vertices: np.ndarray, |
|
faces: np.ndarray, |
|
attr: np.ndarray, |
|
width: int, |
|
height: int, |
|
transform: np.ndarray = None, |
|
cull_backface: bool = True, |
|
return_depth: bool = False, |
|
image: np.ndarray = None, |
|
depth: np.ndarray = None |
|
) -> Tuple[np.ndarray, np.ndarray]: |
|
""" |
|
Rasterize vertex attribute. |
|
|
|
Args: |
|
vertices (np.ndarray): [N, 3] |
|
faces (np.ndarray): [T, 3] |
|
attr (np.ndarray): [N, C] |
|
width (int): width of rendered image |
|
height (int): height of rendered image |
|
transform (np.ndarray): [4, 4] model-view-projection transformation matrix. |
|
cull_backface (bool): whether to cull backface |
|
image: (np.ndarray): [H, W, C] background image |
|
depth: (np.ndarray): [H, W] background depth |
|
|
|
Returns: |
|
image (np.ndarray): [H, W, C] rendered image |
|
depth (np.ndarray): [H, W] screen space depth, ranging from 0 to 1. If return_depth is False, it is None. |
|
""" |
|
assert vertices.ndim == 2 and vertices.shape[1] == 3 |
|
assert faces.ndim == 2 and faces.shape[1] == 3, f"Faces should be a 2D array with shape (T, 3), but got {faces.shape}" |
|
assert attr.ndim == 2 and attr.shape[1] in [1, 2, 3, 4], f'Vertex attribute only supports channels 1, 2, 3, 4, but got {attr.shape}' |
|
assert vertices.shape[0] == attr.shape[0] |
|
assert vertices.dtype == np.float32 |
|
assert faces.dtype == np.uint32 or faces.dtype == np.int32 |
|
assert attr.dtype == np.float32, "Attribute should be float32" |
|
|
|
C = attr.shape[1] |
|
prog = ctx.program_vertex_attribute(C) |
|
|
|
transform = np.eye(4, np.float32) if transform is None else transform |
|
|
|
|
|
ibo = ctx.mgl_ctx.buffer(np.ascontiguousarray(faces, dtype='i4')) |
|
vbo_vertices = ctx.mgl_ctx.buffer(np.ascontiguousarray(vertices, dtype='f4')) |
|
vbo_attr = ctx.mgl_ctx.buffer(np.ascontiguousarray(attr, dtype='f4')) |
|
vao = ctx.mgl_ctx.vertex_array( |
|
prog, |
|
[ |
|
(vbo_vertices, '3f', 'i_position'), |
|
(vbo_attr, f'{C}f', 'i_attr'), |
|
], |
|
ibo, |
|
mode=moderngl.TRIANGLES, |
|
) |
|
|
|
|
|
image_tex = ctx.mgl_ctx.texture((width, height), C, dtype='f4', data=np.ascontiguousarray(image[::-1, :, :]) if image is not None else None) |
|
depth_tex = ctx.mgl_ctx.depth_texture((width, height), data=np.ascontiguousarray(depth[::-1, :]) if depth is not None else None) |
|
fbo = ctx.mgl_ctx.framebuffer( |
|
color_attachments=[image_tex], |
|
depth_attachment=depth_tex, |
|
) |
|
|
|
|
|
prog['u_mvp'].write(transform.transpose().copy().astype('f4')) |
|
fbo.use() |
|
fbo.viewport = (0, 0, width, height) |
|
ctx.mgl_ctx.depth_func = '<' |
|
ctx.mgl_ctx.enable(ctx.mgl_ctx.DEPTH_TEST) |
|
if cull_backface: |
|
ctx.mgl_ctx.enable(ctx.mgl_ctx.CULL_FACE) |
|
else: |
|
ctx.mgl_ctx.disable(ctx.mgl_ctx.CULL_FACE) |
|
vao.render() |
|
ctx.mgl_ctx.disable(ctx.mgl_ctx.DEPTH_TEST) |
|
|
|
|
|
image = np.zeros((height, width, C), dtype='f4') |
|
image_tex.read_into(image) |
|
image = image[::-1, :, :] |
|
if return_depth: |
|
depth = np.zeros((height, width), dtype='f4') |
|
depth_tex.read_into(depth) |
|
depth = depth[::-1, :] |
|
else: |
|
depth = None |
|
|
|
|
|
vao.release() |
|
ibo.release() |
|
vbo_vertices.release() |
|
vbo_attr.release() |
|
fbo.release() |
|
image_tex.release() |
|
depth_tex.release() |
|
|
|
return image, depth |
|
|
|
|
|
def rasterize_edges( |
|
ctx: RastContext, |
|
vertices: np.ndarray, |
|
edges: np.ndarray, |
|
attr: np.ndarray, |
|
width: int, |
|
height: int, |
|
transform: np.ndarray = None, |
|
line_width: float = 1.0, |
|
return_depth: bool = False, |
|
image: np.ndarray = None, |
|
depth: np.ndarray = None |
|
) -> Tuple[np.ndarray, ...]: |
|
""" |
|
Rasterize vertex attribute. |
|
|
|
Args: |
|
vertices (np.ndarray): [N, 3] |
|
faces (np.ndarray): [T, 3] |
|
attr (np.ndarray): [N, C] |
|
width (int): width of rendered image |
|
height (int): height of rendered image |
|
transform (np.ndarray): [4, 4] model-view-projection matrix |
|
line_width (float): width of line. Defaults to 1.0. NOTE: Values other than 1.0 may not work across all platforms. |
|
cull_backface (bool): whether to cull backface |
|
|
|
Returns: |
|
image (np.ndarray): [H, W, C] rendered image |
|
depth (np.ndarray): [H, W] screen space depth, ranging from 0 to 1. If return_depth is False, it is None. |
|
""" |
|
assert vertices.ndim == 2 and vertices.shape[1] == 3 |
|
assert edges.ndim == 2 and edges.shape[1] == 2, f"Edges should be a 2D array with shape (T, 2), but got {edges.shape}" |
|
assert attr.ndim == 2 and attr.shape[1] in [1, 2, 3, 4], f'Vertex attribute only supports channels 1, 2, 3, 4, but got {attr.shape}' |
|
assert vertices.shape[0] == attr.shape[0] |
|
assert vertices.dtype == np.float32 |
|
assert edges.dtype == np.uint32 or edges.dtype == np.int32 |
|
assert attr.dtype == np.float32, "Attribute should be float32" |
|
|
|
C = attr.shape[1] |
|
prog = ctx.program_vertex_attribute(C) |
|
|
|
transform = transform if transform is not None else np.eye(4, np.float32) |
|
|
|
|
|
ibo = ctx.mgl_ctx.buffer(np.ascontiguousarray(edges, dtype='i4')) |
|
vbo_vertices = ctx.mgl_ctx.buffer(np.ascontiguousarray(vertices, dtype='f4')) |
|
vbo_attr = ctx.mgl_ctx.buffer(np.ascontiguousarray(attr, dtype='f4')) |
|
vao = ctx.mgl_ctx.vertex_array( |
|
prog, |
|
[ |
|
(vbo_vertices, '3f', 'i_position'), |
|
(vbo_attr, f'{C}f', 'i_attr'), |
|
], |
|
ibo, |
|
mode=moderngl.LINES, |
|
) |
|
|
|
|
|
image_tex = ctx.mgl_ctx.texture((width, height), C, dtype='f4', data=np.ascontiguousarray(image[::-1, :, :]) if image is not None else None) |
|
depth_tex = ctx.mgl_ctx.depth_texture((width, height), data=np.ascontiguousarray(depth[::-1, :]) if depth is not None else None) |
|
fbo = ctx.mgl_ctx.framebuffer( |
|
color_attachments=[image_tex], |
|
depth_attachment=depth_tex, |
|
) |
|
|
|
|
|
prog['u_mvp'].write(transform.transpose().copy().astype('f4')) |
|
fbo.use() |
|
fbo.viewport = (0, 0, width, height) |
|
ctx.mgl_ctx.depth_func = '<' |
|
ctx.mgl_ctx.enable(ctx.mgl_ctx.DEPTH_TEST) |
|
ctx.mgl_ctx.line_width = line_width |
|
vao.render() |
|
ctx.mgl_ctx.disable(ctx.mgl_ctx.DEPTH_TEST) |
|
|
|
|
|
image = np.zeros((height, width, C), dtype='f4') |
|
image_tex.read_into(image) |
|
image = image[::-1, :, :] |
|
if return_depth: |
|
depth = np.zeros((height, width), dtype='f4') |
|
depth_tex.read_into(depth) |
|
depth = depth[::-1, :] |
|
else: |
|
depth = None |
|
|
|
|
|
vao.release() |
|
ibo.release() |
|
vbo_vertices.release() |
|
vbo_attr.release() |
|
fbo.release() |
|
image_tex.release() |
|
depth_tex.release() |
|
|
|
return image, depth |
|
|
|
|
|
def texture( |
|
ctx: RastContext, |
|
uv: np.ndarray, |
|
texture: np.ndarray, |
|
interpolation: str= 'linear', |
|
wrap: str = 'clamp' |
|
) -> np.ndarray: |
|
""" |
|
Given an UV image, texturing from the texture map |
|
""" |
|
assert len(texture.shape) == 3 and 1 <= texture.shape[2] <= 4 |
|
assert uv.shape[2] == 2 |
|
height, width = uv.shape[:2] |
|
texture_dtype = map_np_dtype(texture.dtype) |
|
|
|
|
|
screen_quad_vbo = ctx.mgl_ctx.buffer(np.array([[-1, -1], [1, -1], [1, 1], [-1, 1]], dtype='f4')) |
|
screen_quad_ibo = ctx.mgl_ctx.buffer(np.array([0, 1, 2, 0, 2, 3], dtype=np.int32)) |
|
screen_quad_vao = ctx.mgl_ctx.vertex_array(ctx.program_texture(texture.shape[2]), [(screen_quad_vbo, '2f4', 'in_vert')], index_buffer=screen_quad_ibo, index_element_size=4) |
|
|
|
|
|
texture_tex = ctx.mgl_ctx.texture((texture.shape[1], texture.shape[0]), texture.shape[2], dtype=texture_dtype, data=np.ascontiguousarray(texture)) |
|
if interpolation == 'linear': |
|
texture_tex.filter = (moderngl.LINEAR, moderngl.LINEAR) |
|
elif interpolation == 'nearest': |
|
texture_tex.filter = (moderngl.NEAREST, moderngl.NEAREST) |
|
texture_tex.use(location=0) |
|
texture_uv = ctx.mgl_ctx.texture((width, height), 2, dtype='f4', data=np.ascontiguousarray(uv.astype('f4', copy=False))) |
|
texture_uv.filter = (moderngl.NEAREST, moderngl.NEAREST) |
|
texture_uv.use(location=1) |
|
|
|
|
|
rb = ctx.mgl_ctx.renderbuffer((uv.shape[1], uv.shape[0]), texture.shape[2], dtype=texture_dtype) |
|
fbo = ctx.mgl_ctx.framebuffer(color_attachments=[rb]) |
|
|
|
|
|
fbo.use() |
|
fbo.viewport = (0, 0, width, height) |
|
ctx.mgl_ctx.disable(ctx.mgl_ctx.BLEND) |
|
screen_quad_vao.render() |
|
|
|
|
|
image_buffer = np.frombuffer(fbo.read(components=texture.shape[2], attachment=0, dtype=texture_dtype), dtype=texture_dtype).reshape((height, width, texture.shape[2])) |
|
|
|
|
|
texture_tex.release() |
|
rb.release() |
|
fbo.release() |
|
|
|
return image_buffer |
|
|
|
|
|
def warp_image_by_depth( |
|
ctx: RastContext, |
|
src_depth: np.ndarray, |
|
src_image: np.ndarray = None, |
|
width: int = None, |
|
height: int = None, |
|
*, |
|
extrinsics_src: np.ndarray = None, |
|
extrinsics_tgt: np.ndarray = None, |
|
intrinsics_src: np.ndarray = None, |
|
intrinsics_tgt: np.ndarray = None, |
|
near: float = 0.1, |
|
far: float = 100.0, |
|
cull_backface: bool = True, |
|
ssaa: int = 1, |
|
return_depth: bool = False, |
|
) -> Tuple[np.ndarray, ...]: |
|
""" |
|
Warp image by depth map. |
|
|
|
Args: |
|
ctx (RastContext): rasterizer context |
|
src_depth (np.ndarray): [H, W] |
|
src_image (np.ndarray, optional): [H, W, C]. The image to warp. Defaults to None (use uv coordinates). |
|
width (int, optional): width of the output image. None to use depth map width. Defaults to None. |
|
height (int, optional): height of the output image. None to use depth map height. Defaults to None. |
|
extrinsics_src (np.ndarray, optional): extrinsics matrix of the source camera. Defaults to None (identity). |
|
extrinsics_tgt (np.ndarray, optional): extrinsics matrix of the target camera. Defaults to None (identity). |
|
intrinsics_src (np.ndarray, optional): intrinsics matrix of the source camera. Defaults to None (use the same as intrinsics_tgt). |
|
intrinsics_tgt (np.ndarray, optional): intrinsics matrix of the target camera. Defaults to None (use the same as intrinsics_src). |
|
cull_backface (bool, optional): whether to cull backface. Defaults to True. |
|
ssaa (int, optional): super sampling anti-aliasing. Defaults to 1. |
|
|
|
Returns: |
|
tgt_image (np.ndarray): [H, W, C] warped image (or uv coordinates if image is None). |
|
tgt_depth (np.ndarray): [H, W] screen space depth, ranging from 0 to 1. If return_depth is False, it is None. |
|
""" |
|
assert src_depth.ndim == 2 |
|
|
|
if width is None: |
|
width = src_depth.shape[1] |
|
if height is None: |
|
height = src_depth.shape[0] |
|
if src_image is not None: |
|
assert src_image.shape[-2:] == src_depth.shape[-2:], f'Shape of source image {src_image.shape} does not match shape of source depth {src_depth.shape}' |
|
|
|
|
|
extrinsics_src = np.eye(4) if extrinsics_src is None else extrinsics_src |
|
extrinsics_tgt = np.eye(4) if extrinsics_tgt is None else extrinsics_tgt |
|
intrinsics_src = intrinsics_tgt if intrinsics_src is None else intrinsics_src |
|
intrinsics_tgt = intrinsics_src if intrinsics_tgt is None else intrinsics_tgt |
|
|
|
assert all(x is not None for x in [extrinsics_src, extrinsics_tgt, intrinsics_src, intrinsics_tgt]), "Make sure you have provided all the necessary camera parameters." |
|
|
|
|
|
assert extrinsics_src.shape == (4, 4) and extrinsics_tgt.shape == (4, 4) |
|
assert intrinsics_src.shape == (3, 3) and intrinsics_tgt.shape == (3, 3) |
|
|
|
|
|
view_tgt = transforms.extrinsics_to_view(extrinsics_tgt) |
|
perspective_tgt = transforms.intrinsics_to_perspective(intrinsics_tgt, near=near, far=far) |
|
|
|
|
|
uv, faces = utils.image_mesh(*src_depth.shape[-2:]) |
|
pts = transforms.unproject_cv(uv, src_depth.reshape(-1), extrinsics_src, intrinsics_src) |
|
faces = mesh.triangulate(faces, vertices=pts) |
|
|
|
|
|
if src_image is not None: |
|
attr = src_image.reshape(-1, src_image.shape[-1]) |
|
else: |
|
attr = uv |
|
|
|
tgt_image, tgt_depth = rasterize_triangle_faces( |
|
ctx, |
|
pts, |
|
faces, |
|
attr, |
|
width * ssaa, |
|
height * ssaa, |
|
transform=perspective_tgt @ view_tgt, |
|
cull_backface=cull_backface, |
|
return_depth=return_depth, |
|
) |
|
|
|
if ssaa > 1: |
|
tgt_image = tgt_image.reshape(height, ssaa, width, ssaa, -1).mean(axis=(1, 3)) |
|
tgt_depth = tgt_depth.reshape(height, ssaa, width, ssaa, -1).mean(axis=(1, 3)) if return_depth else None |
|
|
|
return tgt_image, tgt_depth |
|
|
|
def test(): |
|
""" |
|
Test if rasterization works. It will render a cube with random colors and save it as a CHECKME.png file. |
|
""" |
|
ctx = RastContext(backend='egl') |
|
vertices, faces = utils.cube(tri=True) |
|
attr = np.random.rand(len(vertices), 3).astype(np.float32) |
|
perspective = transforms.perspective(np.deg2rad(60), 1, 0.01, 100) |
|
view = transforms.view_look_at(np.array([2, 2, 2]), np.array([0, 0, 0]), np.array([0, 1, 0])) |
|
image, _ = rasterize_triangle_faces( |
|
ctx, |
|
vertices, |
|
faces, |
|
attr, |
|
512, 512, |
|
view=view, |
|
projection=perspective, |
|
cull_backface=True, |
|
ssaa=1, |
|
return_depth=True, |
|
) |
|
import cv2 |
|
cv2.imwrite('CHECKME.png', cv2.cvtColor((image.clip(0, 1) * 255).astype(np.uint8), cv2.COLOR_RGB2BGR)) |
|
|