From f6ce9d2d5bcc2945a7192cc9fb920d9a43e80a5d Mon Sep 17 00:00:00 2001 From: xzeng Date: Mon, 13 Mar 2023 16:41:11 -0400 Subject: [PATCH] add rendering code --- README.md | 1 + utils/render_mitsuba_pc.py | 418 ++++++++++++++++++++++++++++++++++++ utils/render_voxel_cubes.py | 128 +++++++++++ 3 files changed, 547 insertions(+) create mode 100644 utils/render_mitsuba_pc.py create mode 100644 utils/render_voxel_cubes.py diff --git a/README.md b/README.md index 3a3d8bb..2a7a971 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@

## Update +* add pointclouds rendering code used for paper figure, see `utils/render_mitsuba_pc.py` * When opening an issue, please add @ZENGXH so that I can reponse faster! ## Install diff --git a/utils/render_mitsuba_pc.py b/utils/render_mitsuba_pc.py new file mode 100644 index 0000000..30588bd --- /dev/null +++ b/utils/render_mitsuba_pc.py @@ -0,0 +1,418 @@ +# Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# NVIDIA CORPORATION & AFFILIATES and its licensors retain all intellectual property +# and proprietary rights in and to this software, related documentation +# and any modifications thereto. Any use, reproduction, disclosure or +# distribution of this software and related documentation without an express +# license agreement from NVIDIA CORPORATION & AFFILIATES is strictly prohibited. + +import numpy as np +import mitsuba as mi +mi.set_variant("cuda_ad_rgb") +from loguru import logger +import sys, os, subprocess +import copy +import OpenEXR +import Imath +from PIL import Image +## from plyfile import PlyData, PlyElement +import torch +import open3d as o3d +from PIL import Image, ImageChops +import time +random_str = hex(int(time.time() + 12345))[2:] +PATH_TO_MITSUBA2 = "/home/xzeng/code/mitsuba2/build/dist/mitsuba" ## Codes/mitsuba2/build/dist/mitsuba" # mitsuba exectuable + +# replaced by command line arguments +def standardize_bbox_based_on(pcl, eps): + pcl = pcl.numpy()[:, [0,2,1]] + eps = eps.numpy()[:, [0,2,1]] + pcl, center, scale = standardize_bbox(pcl, return_center_scale=1) + eps = (eps - center) / scale if eps is not None else None + offset = - 0.475 - pcl[:,2].min() + eps[:,2] += offset + return torch.from_numpy(eps) + +# PATH_TO_NPY = 'pcl_ex.npy' # the tensor to load +def rotate_pts(pts, r, axis=1, do_transform=0, is_point_flow_data=1, eps=None): + assert(len(pts.shape) == 2), f'require N,3 tensor, get: {pts.shape}' + ## logger.info('rotating pts: {}, get eps: {} ', pts.shape, eps is not None ) + is_tensor = torch.is_tensor(pts) + if not is_tensor: + pts = torch.from_numpy(pts) + if eps is not None and not torch.is_tensor(eps): + eps = torch.from_numpy(eps) + if do_transform: + pcl = pts.cpu().numpy() + eps = eps.cpu().numpy() if eps is not None else None + if not is_point_flow_data: + pcl[:,0] *= -1 + pcl = pcl[:, [2,1,0]] + + if eps is not None: + eps[:,0] *= -1 + eps = eps[:, [2,1,0]] + + pcl, center, scale = standardize_bbox(pcl, return_center_scale=1) + eps = (eps - center) / scale if eps is not None else None + pcl = pcl[:, [2, 0, 1]] + pcl[:,0] *= -1 + pcl[:,2] += 0.0125 + if eps is not None: + eps = eps[:, [2, 0, 1]] + eps[:,0] *= -1 + eps[:,2] += 0.0125 + + offset = - 0.475 - pcl[:,2].min() + pcl[:,2] += offset + if eps is not None: + eps[:,2] += offset + pts = torch.from_numpy(pcl) + eps = torch.from_numpy(eps) if eps is not None else None + + pcl = o3d.geometry.PointCloud() + pcl.points = o3d.utility.Vector3dVector(pts.cpu()) + if axis == 1: + R = pcl.get_rotation_matrix_from_xyz((0, - r * np.pi / 2, 0)) + elif axis == 2: + R = pcl.get_rotation_matrix_from_xyz((0, 0, - r * np.pi / 2)) + elif axis == 0: + R = pcl.get_rotation_matrix_from_xyz((- r * np.pi / 2, 0, 0)) + + #mesh_r = copy.deepcopy(pcl) + #mesh_r.rotate(R, center=(0, 0, 0)) + #pts = np.asarray(mesh_r.points) + + h_center = w_center = 0 + center = np.array([h_center, w_center, 0]).reshape(-1,3) + pts = np.matmul(pts.numpy() - center, R.T) + center + eps = np.matmul(eps.numpy() - center, R.T) + center if eps is not None else eps + + if is_tensor: + pts = torch.from_numpy(pts) + eps = torch.from_numpy(eps) if eps is not None and not torch.is_tensor(eps) else eps + + if eps is not None: + return pts, eps + return pts + +# note that sampler is changed to 'independent' and the ldrfilm is changed to hdrfilm +xml_head_segment = \ + """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + +# I also use a smaller point size +xml_ball_segment = ['']*10 +xml_ball_segment[0] = \ + """ + + + + + + + + + +""" +xml_ball_segment[1] = \ +""" + + + + + + + + + + +""" + + +xml_ball_segment[2] = \ +""" + + + + + + + + + + +""" + +xml_ball_segment[3] = \ +""" + + + + + + + + + + + + +""" +xml_ball_segment[4] = \ +""" + + + + + + + + + + + + +""" +xml_ball_segment[5] = \ + """ + + + + + + + + + + + + +""" +xml_tail = \ +""" + + + + + + + + + + + + + + + + + + +""" +def trim(im): + bg = Image.new(im.mode, im.size, im.getpixel((0,0))) ##border) + diff = ImageChops.difference(im, bg) + bbox = diff.getbbox() + if bbox: + return im.crop(bbox) + else: + return im + + +def colormap(x, y, z): + if torch.is_tensor(x): + x = x.cpu().numpy() + vec = np.array([x, y, z]) + vec = np.clip(vec, 0.001, 1.0) + norm = np.sqrt(np.sum(vec ** 2)) + vec /= norm + return [vec[0], vec[1], vec[2]] + + +def standardize_bbox(pcl, return_center_scale=0): + #pt_indices = np.random.choice(pcl.shape[0], points_per_object, replace=False) + #np.random.shuffle(pt_indices) + #pcl = pcl[pt_indices] # n by 3 + if torch.is_tensor(pcl): + pcl = pcl.numpy() + mins = np.amin(pcl, axis=0) + maxs = np.amax(pcl, axis=0) + center = (mins + maxs) / 2. + scale = np.amax(maxs - mins) + #print("Center: {}, Scale: {}".format(center, scale)) + result = ((pcl - center) / scale).astype(np.float32) # [-0.5, 0.5] + if return_center_scale: + return result, center, scale + return result + + +# only for debugging reasons +def writeply(vertices, ply_file): + sv = np.shape(vertices) + points = [] + for v in range(sv[0]): + vertex = vertices[v] + points.append("%f %f %f\n" % (vertex[0], vertex[1], vertex[2])) + print(np.shape(points)) + file = open(ply_file, "w") + file.write('''ply + format ascii 1.0 + element vertex %d + property float x + property float y + property float z + end_header + %s + ''' % (len(vertices), "".join(points))) + file.close() + + +# as done in https://gist.github.com/drakeguan/6303065 +def ConvertEXRToJPG(exrfile, jpgfile, trim_img): + File = OpenEXR.InputFile(exrfile) + PixType = Imath.PixelType(Imath.PixelType.FLOAT) + DW = File.header()['dataWindow'] + Size = (DW.max.x - DW.min.x + 1, DW.max.y - DW.min.y + 1) + + rgb = [np.fromstring(File.channel(c, PixType), dtype=np.float32) for c in 'RGB'] + for i in range(3): + rgb[i] = np.where(rgb[i] <= 0.0031308, + (rgb[i] * 12.92) * 255.0, + (1.055 * (rgb[i] ** (1.0 / 2.4)) - 0.055) * 255.0) + + rgb8 = [Image.frombytes("F", Size, c.tostring()).convert("L") for c in rgb] + Image.merge("RGB", rgb8).save(jpgfile, "PNG") ##JPEG", quality=95) + img = Image.open(jpgfile) + if trim_img: + img = trim(img) + img.save(jpgfile) + +def pts2png(input_pts, file_name, colorm=[24,107,239], + skip_if_exists=False, is_color_list=False, + sample_count=256, out_width=1600, out_height=1200, + ball_size=0.025, do_standardize=0, same_computed_loc_color=0, material_id=0, precomputed_color=None, + output_xml_file=None, + use_loc_color=False, lookat_1=3, lookat_2=3, lookat_3=3, do_transform=1, trim_img=0): + """ + Argus: + input_pts: (B,N,3) the points to be render + file_name: list; output image name + """ + assert(len(input_pts.shape) == 3), f'expect: B,N,3; get: {input_pts.shape}' + assert(type(file_name) is list), f'require file_name as list' + xml_head = xml_head_segment.format( + lookat_1, lookat_2, lookat_3, + sample_count, out_width, out_height) + input_pts = input_pts.cpu() + # print('get shape; ', input_pts.shape) + color_list = [] + for pcli in range(0, input_pts.shape[0]): + xmlFile = '/tmp/tmp_%s.xml'%random_str if output_xml_file is None else output_xml_file + # ("%s/xml/%s.xml" % (folder, filename)) + exrFile = '/tmp/tmp_%s.exr'%random_str ##("%s/exr/%s.exr" % (folder, filename)) + png = file_name[pcli] + if skip_if_exists and os.path.exists(png): + print(f'find png: {png}, skip ') + continue + pcl = input_pts[pcli, :, :] + if do_transform: + pcl = standardize_bbox(pcl) + pcl = pcl[:, [2, 0, 1]] + pcl[:, 0] *= -1 + pcl[:, 2] += 0.0125 + + offset = - 0.475 - pcl[:,2].min() + pcl[:,2] += offset + if do_standardize: + pcl = standardize_bbox(pcl) + offset = - 0.475 - pcl[:,2].min() + pcl[:,2] += offset + + xml_segments = [xml_head] + for i in range(pcl.shape[0]): + if precomputed_color is not None: + color = precomputed_color[i] + elif use_loc_color and not same_computed_loc_color: + color = colormap(pcl[i, 0] + 0.5, pcl[i, 1] + 0.5, pcl[i, 2] + 0.5 - 0.0125) + elif use_loc_color and same_computed_loc_color: + if pcli == 0: + color = colormap(pcl[i, 0] + 0.5, pcl[i, 1] + 0.5, pcl[i, 2] + 0.5 - 0.0125) + color_list.append(color) + else: + color = color_list[i] # same color as first shape + elif is_color_list: + color = colorm[pcli] + color = [c/255.0 for c in color] + else: + color = [c/255.0 for c in colorm] + xml_segments.append(xml_ball_segment[material_id].format( + ball_size, + pcl[i, 0], pcl[i, 1], pcl[i, 2], *color)) + ## print('using color: ', color) + xml_segments.append(xml_tail) + + xml_content = str.join('', xml_segments) + + if not os.path.exists(os.path.dirname(xmlFile)): + os.makedirs(os.path.dirname(xmlFile)) + with open(xmlFile, 'w') as f: + f.write(xml_content) + logger.info('[render_mitsuba_pc] write output at: {}', xmlFile) + f.close() + + if not os.path.exists(os.path.dirname(exrFile)): + os.makedirs(os.path.dirname(exrFile)) + if not os.path.exists(os.path.dirname(png)): + os.makedirs(os.path.dirname(png)) + logger.info('*'*20 + f'{png}' +'*'*20) + # mitsuba2 + #subprocess.run([PATH_TO_MITSUBA2, '-o', exrFile, xmlFile]) + #ConvertEXRToJPG(exrFile, png, trim_img) + scene = mi.load_file(xmlFile) + image = mi.render(scene) ##, spp=256) + mi.util.write_bitmap(png, image) + if trim_img: + img = Image.open(png) + img.save(png) + + return png + + +if __name__ == "__main__": + if (len(sys.argv) < 2): + print('filename to npy/ply is not passed as argument. terminated.') + raise ValueError + + pathToFile = sys.argv[1] + + + main(pathToFile) diff --git a/utils/render_voxel_cubes.py b/utils/render_voxel_cubes.py new file mode 100644 index 0000000..9eb496f --- /dev/null +++ b/utils/render_voxel_cubes.py @@ -0,0 +1,128 @@ +# Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# NVIDIA CORPORATION & AFFILIATES and its licensors retain all intellectual property +# and proprietary rights in and to this software, related documentation +# and any modifications thereto. Any use, reproduction, disclosure or +# distribution of this software and related documentation without an express +# license agreement from NVIDIA CORPORATION & AFFILIATES is strictly prohibited. + +import os +import numpy as np +import open3d as o3d +import sys +import torch +import point_cloud_utils as pcu +from PIL import Image + +sys.path.append('.') +import torch +from utils._render_mitsuba_cubes import render_cubes2png +# from script.paper.vis_mesh.render_mitsuba import reformat_ply +def reformat_ply(input, output, r=0, is_point_flow_data=0, + ascii=False, write_normal=False, fixed_trimesh=1): + m = open3d.io.read_triangle_mesh(input) + pcl = np.asarray(m.vertices) + if not is_point_flow_data: + pcl[:,0] *= -1 + pcl = pcl[:, [2,1,0]] + + pcl = standardize_bbox(pcl) + pcl = pcl[:, [2, 0, 1]] + pcl[:,0] *= -1 + pcl[:,2] += 0.0125 + + offset = - 0.475 - pcl[:,2].min() + pcl[:,2] += offset + m.vertices = open3d.utility.Vector3dVector(pcl) + + R = m.get_rotation_matrix_from_xyz((0, 0, - r * np.pi / 2)) + mesh_r = copy.deepcopy(m) + mesh_r.rotate(R, center=(0, 0, 0)) + ## o3d.visualization.draw_geometries([mesh_r]) + open3d.io.write_triangle_mesh(output, mesh_r, write_ascii=ascii, write_vertex_normals=False) + if fixed_trimesh: + mesh = trimesh.load_mesh(output) ## '../models/featuretype.STL') + trimesh.repair.fix_inversion(mesh) + trimesh.repair.fix_normals(mesh) + mesh.export(output) + logger.info(f'load {input}, write as {output}; ascii={ascii}, write_normal: {write_normal}') + return output + + +def get_vpts(cubes): + import kaolin + v,f = kaolin.ops.conversions.voxelgrids_to_cubic_meshes(cubes) ##voxelgrids_to_trianglemeshes(voxel_volume) + v = [vi.cpu() for vi in v] + f = [fi.cpu() for fi in f] + return v, f + +def create_dir(output_path): + if not os.path.exists(output_path): + os.makedirs(output_path) + +def convert_cube_2_mesh(voxel_size, center_list, output_path, overwrite=0, colorm=[93,64,211], rotate=None, config={}): + scale = 0.5 * (center_list.max() - center_list.min()) + pcl = center_list + mins = np.amin(pcl, axis=0).reshape(1,pcl.shape[-1]) ##np.amin(pcl, axis=1)[:, None, :], axis=0)[None, None, :] + maxs = np.amax(pcl, axis=0).reshape(1,pcl.shape[-1]) ##np.amin(pcl, axis=1)[:, None, :], axis=0)[None, None, :] + + center = ( mins + maxs ) / 2. + scale = np.amax(maxs-mins) + + center_list = (center_list - center) / scale ## (center_list.max() - center_list.min()) + + center_list = center_list[:,[2,0,1]] + center_list[:,0] *= -1 #center_list + offset = - 0.475 - center_list[:,2].min() + center_list[:,2] += offset + if rotate is not None: + pts = torch.from_numpy(center_list) + pcl = o3d.geometry.PointCloud() + pcl.points = o3d.utility.Vector3dVector(pts.cpu()) + R = pcl.get_rotation_matrix_from_xyz((0, 0, - rotate * np.pi / 2)) + center = np.array([0, 0, 0]).reshape(-1,3) + pts = np.matmul(pts.numpy() - center, R.T) + center + center_list = pts + + output_name = output_path + print('center_list: ', center_list.shape) + if not os.path.exists(os.path.dirname(output_name)): + os.makedirs(os.path.dirname(output_name)) + if os.path.exists(output_name) and not overwrite: + print(f'find rendered output: {output_name}, skip') + return + out = render_cubes2png(pcl=center_list, filename=output_name, + vs_size=0.9*voxel_size/scale, colorm=colorm, **config) + print(' save as: ', out) + #img = Image.open(out) + #img.show() + print('output_path: ', output_path) + return out + +def create_unit_cube(): + voxelgrid = torch.tensor([1], device='cuda', dtype=torch.uint8).view(1,1,1,1) + v, f = get_vpts(voxelgrid) + v = v[0] + f = f[0] + v = v - 0.5 # [-0.5, 0.5] + print('v: ', v) + print('f: ', v) + output_name = './script/paper/vis_voxel_exp/unit_cube.ply' + print('save output as: ', output_name) + pcu.save_mesh_vf(output_name, v.numpy(), f.numpy()) + reformat_ply(output_name, output_name, r=0) + +def write_small_cube(): + mesh_compare = './script/paper/vis_voxel_exp/unit_cube2.ply' + m = o3d.io.read_triangle_mesh(mesh_compare) + pcl = np.asarray(m.vertices) * 0.5 + m.vertices = o3d.utility.Vector3dVector(pcl) ##.float()) ## torch.from_numpy(pcl)) + o3d.io.write_triangle_mesh(mesh_compare.replace('.ply', 'w1.ply'), + m, write_ascii=True, write_vertex_normals=True) + +# write_small_cube() +if __name__ == '__main__': + input_path = '/home/xzeng/plots/voxel_exp/voxel_cube/raw' + index = "3 328 91 83 74 73 64 63 54 51 48 45 41 30 22" + convert_cube_2_mesh(input_path, index) +# create_unit_cube()