def test_wrong_method(): with pytest.raises(ValueError): mcubes.smooth( np.zeros((10, 10), dtype=np.bool_), method='wrong', max_iters=500, rel_tol=1e-4 )
def test_sphere(): # Create sphere with radius 25 centered at (50, 50, 50) x, y, z = np.mgrid[:100, :100, :100] levelset = np.sqrt((x - 50)**2 + (y - 50)**2 + (z - 50)**2) - 25 # vertices, triangles = mcubes.marching_cubes(levelset, 0) # mcubes.export_obj(vertices, triangles, 'sphere1.obj') binary_levelset = levelset > 0 smoothed_levelset = mcubes.smooth( binary_levelset, method='constrained', max_iters=500, rel_tol=1e-4 ) vertices, _ = mcubes.marching_cubes(smoothed_levelset, 0.0) # Check all vertices have same distance to (50, 50, 50) dist = np.sqrt(np.sum((vertices - [50, 50, 50])**2, axis=1)) assert dist.min() > 24.5 and dist.max() < 25.5 assert np.all(np.abs(smoothed_levelset - levelset) < 1) assert np.all((smoothed_levelset > 0) == binary_levelset)
def test_wrong_ndim(): binary_levelset = np.random.uniform(size=(10)) < 0.5 with pytest.raises(ValueError): mcubes.smooth( binary_levelset, method='constrained', max_iters=500, rel_tol=1e-4 ) binary_levelset = np.random.uniform(size=(10, 10, 10, 10)) < 0.5 with pytest.raises(ValueError): mcubes.smooth( binary_levelset, method='constrained', max_iters=500, rel_tol=1e-4 )
def test_circle(): x, y = np.mgrid[:100, :100] levelset = np.sqrt((x - 50)**2 + (y - 50)**2) - 25 binary_levelset = levelset > 0 smoothed_levelset = mcubes.smooth( binary_levelset, max_iters=500, rel_tol=1e-4 ) assert np.all(np.abs(smoothed_levelset - levelset) < 1) assert np.all((smoothed_levelset > 0) == binary_levelset)
def smooth(volume: np.ndarray, mode: str) -> Tuple[np.ndarray, float]: isovalue = 0.0 if mode == 'auto': volume = mcubes.smooth(volume) elif mode == 'constrained': volume = mcubes.smooth_constrained(volume, max_iters=500) elif mode == 'gaussian': volume = mcubes.smooth_gaussian(volume) elif mode == 'no': isovalue = 0.5 else: raise ValueError("unknown mode '{}'".format(mode)) return volume, isovalue
def test_gaussian_smoothing(): # Create sphere with radius 25 centered at (50, 50, 50) x, y, z = np.mgrid[:100, :100, :100] levelset = np.sqrt((x - 50)**2 + (y - 50)**2 + (z - 50)**2) - 25 binary_levelset = levelset > 0 smoothed_levelset = mcubes.smooth( binary_levelset, method='gaussian', sigma=3 ) vertices, _ = mcubes.marching_cubes(smoothed_levelset, 0.0) # Check all vertices have same distance to (50, 50, 50) dist = np.sqrt(np.sum((vertices - [50, 50, 50])**2, axis=1)) assert dist.min() > 24 and dist.max() < 25
def marchingCubes(cls, voxel_model: VoxelModel, smooth: bool = False, color: Tuple[float, float, float, float] = (0.8, 0.8, 0.8, 1)): """ Generate a mesh object from a VoxelModel object using a marching cubes algorithm. This meshing approach is best suited to high resolution models where some smoothing is acceptable. Args: voxel_model: VoxelModel object to be converted to a mesh smooth: Enable smoothing color: Mesh color in the format (r, g, b, a) Returns: None """ voxel_model_fit = voxel_model.fitWorkspace().getOccupied() voxels = voxel_model_fit.voxels.astype(np.uint16) x, y, z = voxels.shape model_offsets = voxel_model_fit.coords voxels_padded = np.zeros((x + 2, y + 2, z + 2)) voxels_padded[1:-1, 1:-1, 1:-1] = voxels if smooth: voxels_padded = mcubes.smooth(voxels_padded) levelset = 0 else: levelset = 0.5 verts, tris = mcubes.marching_cubes(voxels_padded, levelset) # Shift model to align with origin verts = np.subtract(verts, 0.5) verts[:, 0] = np.add(verts[:, 0], model_offsets[0]) verts[:, 1] = np.add(verts[:, 1], model_offsets[1]) verts[:, 2] = np.add(verts[:, 2], model_offsets[2]) verts_colors = generateColors(len(verts), color) return cls(voxels_padded, verts, verts_colors, tris, voxel_model.resolution)
def compute_isosurfaces( bands: Dict[Spin, np.ndarray], kpoint_dim: Tuple[int, int, int], fermi_level: float, reciprocal_space: ReciprocalCell, decimate_factor: Optional[float] = None, decimate_method: str = "quadric", smooth: bool = False, ) -> Dict[Spin, List[Tuple[np.ndarray, np.ndarray]]]: """ Compute the isosurfaces at a particular energy level. Args: bands: The band energies, given as a dictionary of ``{spin: energies}``, where energies has the shape (nbands, nkpoints). kpoint_dim: The k-point mesh dimensions. fermi_level: The energy at which to calculate the Fermi surface. reciprocal_space: The reciprocal space representation. decimate_factor: If method is "quadric", factor is the scaling factor by which to reduce the number of faces. I.e., final # faces = initial # faces * factor. If method is "cluster", factor is the voxel size in which to cluster points. Default is None (no decimation). decimate_method: Algorithm to use for decimation. Options are "quadric" or "cluster". smooth: If True, will smooth resulting isosurface. Requires PyMCubes. Smoothing algorithm will use constrained smoothing algorithm to preserve fine details if input dimension is lower than (500, 500, 500), otherwise will apply a Gaussian filter. Returns: A dictionary containing a list of isosurfaces as ``(vertices, faces)`` for each spin channel. """ rlat = reciprocal_space.reciprocal_lattice spacing = 1 / (np.array(kpoint_dim) - 1) if smooth and mcubes is None: smooth = False warnings.warn("Smoothing disabled, install PyMCubes to enable smoothing.") if decimate_factor is not None and open3d is None: decimate_factor = None warnings.warn("Decimation disabled, install open3d to enable decimation.") isosurfaces = {} for spin, ebands in bands.items(): ebands -= fermi_level spin_isosurface = [] for band in ebands: # check if band crosses fermi level if np.nanmax(band) > 0 > np.nanmin(band): band_data = band.reshape(kpoint_dim) if smooth: #MDF_COMMENT trying smooth # smoothing algorithm requires input to have surface at 0.5 #MDF_COMMENT actually the smooth algorith takes the whole range of #MDF_COMMENT values, you give the isosurface later, and the band_data is already #MDF_COMMENT translated to the fermi level. #MDF_COMMENT in the example at #MDF_COMMENT https://github.com/pmneila/PyMCubes/tree/b6ce8cc7a4e86c9b4b1bedea7073f573b6c7b739 # 0.5 is the level of the sphere they want to show, but you can set # whatever offset you want. you can also not set an offset and then ask # for any other isolevel to the marching_cubes routine. smoothed_band_data = mcubes.smooth(band_data) #-band_data.max() +0.5 )#MDF_COMMENT - 0.5) # and outputs embedding array with values 0 and 1 verts, faces = mcubes.marching_cubes(smoothed_band_data, 0) # have to manually set spacing with PyMCubes verts = verts * spacing # comes out as np.uint64, but trimesh doesn't like this faces = faces.astype(np.int32) else: verts, faces, _, _ = marching_cubes(band_data, 0, spacing=spacing) if decimate_factor: verts, faces = decimate_mesh( verts, faces, decimate_factor, method=decimate_method ) if isinstance(reciprocal_space, WignerSeitzCell): verts = np.dot(verts - 0.5, rlat) * 3 verts, faces = _trim_surface(reciprocal_space, verts, faces) else: # convert coords to cartesian verts = np.dot(verts - 0.5, rlat) spin_isosurface.append((verts, faces)) isosurfaces[spin] = spin_isosurface return isosurfaces
def _calculate_band_isosurfaces( spin: Spin, band_idx: int, energies: np.ndarray, kpoint_dim: Tuple[int, int, int], spacing: np.ndarray, reference: np.ndarray, reciprocal_space: ReciprocalCell, decimate_factor: Optional[float], decimate_method: str, smooth: bool, calculate_dimensionality: bool, property_interpolator: Optional[LinearInterpolator], ): """Helper function to calculate the connected isosurfaces for a band.""" from skimage.measure import marching_cubes from ifermi.analysis import ( connected_subsurfaces, equivalent_surfaces, isosurface_dimensionality, ) rlat = reciprocal_space.reciprocal_lattice if smooth and mcubes is None: smooth = False warnings.warn( "Smoothing disabled, install PyMCubes to enable smoothing.") if decimate_factor is not None and open3d is None: decimate_factor = None warnings.warn( "Decimation disabled, install open3d to enable decimation.") band_data = energies.reshape(kpoint_dim) if smooth: smoothed_band_data = mcubes.smooth(band_data) verts, faces = mcubes.marching_cubes(smoothed_band_data, 0) # have to manually set spacing with PyMCubes verts *= spacing # comes out as np.uint64, but trimesh doesn't like this faces = faces.astype(np.int32) else: verts, faces, _, _ = marching_cubes(band_data, 0, spacing=spacing) if decimate_factor: verts, faces = decimate_mesh(verts, faces, decimate_factor, method=decimate_method) verts += reference # break the isosurface into connected subsurfaces subsurfaces = connected_subsurfaces(verts, faces) if calculate_dimensionality: # calculate dimensionality of periodically equivalent surfaces. dimensionalities = {} mapping = equivalent_surfaces([s[0] for s in subsurfaces]) for idx in mapping: dimensionalities[idx] = isosurface_dimensionality( *subsurfaces[idx]) else: dimensionalities = None mapping = np.zeros(len(subsurfaces)) isosurfaces = [] dimensionality = None orientation = None properties = None for (subverts, subfaces), idx in zip(subsurfaces, mapping): # convert vertices to cartesian coordinates subverts = np.dot(subverts, rlat) subverts, subfaces = trim_surface(reciprocal_space, subverts, subfaces) if len(subverts) == 0: # skip surfaces that do not enter the reciprocal space boundaries continue if calculate_dimensionality: dimensionality, orientation = dimensionalities[idx] if property_interpolator is not None: properties = face_properties(property_interpolator, subverts, subfaces, band_idx, spin, rlat) isosurface = Isosurface( vertices=subverts, faces=subfaces, band_idx=band_idx, properties=properties, dimensionality=dimensionality, orientation=orientation, ) isosurfaces.append(isosurface) return isosurfaces
def npy2point(folder='ct_train', to_save='v', number_points=300, dim=3, crop_size=224, tocrop=False): """ convert .npy to point cloud :param folder: the folder name of the data set :param to_save: choose which oen to save. 'v' represents vertices, 'p' represents plots, '' represents all. :param number_points: number of points for each point cloud :param dim: the dimension of the point clouds :param crop_size: the size of the cropped masks / gt :param tocrop: whether to crop the mask / gt :return: """ assert to_save=='' or to_save=='v' or to_save=='p' import mcubes crop_from = 128 - crop_size//2 crop_to = 128 + crop_size//2 vertices_fold = os.path.join('../../input/PnpAda_release_data/', folder, 'vertices/') plots_fold = os.path.join('../../input/PnpAda_release_data/', folder, 'plots/') if not os.path.exists(vertices_fold): os.mkdir(vertices_fold) if not os.path.exists(plots_fold): os.mkdir(plots_fold) folder_path = os.path.join('../../input/PnpAda_release_data/', folder, "mask/") for path in tqdm(glob.glob(folder_path + '*.npy')): filename = os.path.splitext(os.path.basename(path))[0] vertices_path = os.path.join(vertices_fold, filename + '.npy') plot_path = os.path.join(plots_fold, filename + '.npy') if not os.path.exists(vertices_path): mask = np.load(path) if args.toplot: from matplotlib import pyplot as plt temp = mask[...,0][crop_from:crop_to, crop_from:crop_to] if tocrop else mask[...,0] plt.imshow(temp) plt.show() mask = np.where(mask > 0, 1, 0) mask = np.moveaxis(mask, -1, 0) if tocrop: mask = crop_volume(mask, crop_size=crop_size) mask = np.concatenate([mask, mask, mask], axis=0) point_cloud = np.zeros((crop_size, crop_size)) if tocrop else np.zeros((256, 256)) vertices_array = np.zeros((number_points, dim)) if mask.sum() > 50: vol = mcubes.smooth(mask) vertices, triangles = mcubes.marching_cubes(vol, 0) try: vertices = graipher(vertices, number_points, dim=dim) except: print(filename) exit() vertices_array = np.array(vertices, dtype=np.int) if args.toplot: fig = plt.figure() from mpl_toolkits.mplot3d import Axes3D ax = fig.add_subplot(111, projection='3d') ax.scatter(vertices[:, 0], vertices[:, 1], vertices[:, 2], s=10) plt.show() point_cloud[vertices_array[:,1], vertices_array[:,2]] = 1 if args.toplot: plt.imshow(point_cloud, cmap='gray') plt.show() if to_save=='v' or to_save=='': np.save(vertices_path, vertices_array) if to_save=='p' or to_save=='': np.save(plot_path, point_cloud) # mcubes.export_mesh(vertices, triangles, "heart_single_slice.dae", "MyHeart_s") print("finish")
def array2mesh(array, thresh=0., dim=3, coords=None, bbox=np.array([[-1,-1,-1],[1,1,1]]), return_coords=False, \ if_decimate=False, decimate_face=4096, cart_coord=True, gaussian_sigma=None): """from 1-D array to 3D mesh Args: array (np.ndarray): 1-D array thresh (float, optional): threshold. Defaults to 0.. dim (int, optional): 2 or 3, curve or mesh. Defaults to 3. coords (np.ndarray, optional): array's coordinates (num_points, x_dim). Defaults to None. bbox (np.ndarray, optional): bounding box of coords. Defaults to np.array([[-1,-1],[1,1]]). return_coords (bool, optional): whether return the coords. Defaults to False. decimate_face (int, optional): whether to simplify the mesh. Defaults to 4096. cart_coord (bool, optional): cartesian coordinate in array form, x->i, y->j,... and all varibles increases monotonically. Defaults to True. gaussian_sigma (float, optional): sigma value for gaussian filter (set None if there is no filter) Returns: tuple: `verts`, `faces`, `coords` or `verts`, `faces` according to `return_coords` """ grid = nputil.array2NDCube(array, N=dim) if gaussian_sigma is not None: #from scipy.ndimage import gaussian_filter #grid = gaussian_filter(grid.astype(float), sigma=gaussian_sigma) grid = mcubes.smooth(grid) if dim == 3: verts, faces = mcubes.marching_cubes(grid, thresh) if cart_coord == False: verts = verts[:, [1, 0, 2]] verts = verts / (grid.shape[0] - 1) # rearrange order and rescale elif dim == 2: contours = find_contours(grid, thresh) vcount, points, edges = 0, [], [] for contour in contours: ind = np.arange(len(contour)) points.append(contour) edges.append(np.c_[vcount + ind, vcount + (ind + 1) % len(contour)]) vcount += len(contour) if len(contours) == 0: return None, None verts = np.concatenate(points, axis=0)[:, [1, 0]] / (grid.shape[0] - 1) #verts = verts[:,[1,0]] faces = np.concatenate(edges, axis=0) #levelset_samples = igl.sample_edges(points, edges, 10) if coords is not None: bbmin, bbmax = nputil.arrayBBox(coords) else: bbmin, bbmax = bbox coords = nputil.makeGrid(bb_min=bbmin, bb_max=bbmax, shape=grid.shape) verts = verts * (bbmax - bbmin) + bbmin verts, faces = verts, faces.astype(int) if if_decimate == True: if dim != 3: print("Warning! decimation only works for 3D") elif faces.shape[0] > decimate_face: # Only decimate when appropriate reached, verts, faces, _, _ = igl.decimate(verts, faces, decimate_face) faces = faces.astype(int) if return_coords == True: return verts, faces, coords else: return verts, faces