def test_mask_2(self): a = ChunkGrid(2, bool, False) a[0, 0, 0] = True a.ensure_chunk_at_index((1, 1, 1)).set_fill(True) a.set_value((2, 1, 1), True) b = a.astype(np.int8) adense = a.to_dense() bdense = b.to_dense() self.assertEqual(adense.shape, bdense.shape) self.assertEqual(str(adense.astype(np.int8)), str(bdense)) mask = ChunkGrid(2, bool, False) mask.ensure_chunk_at_index((1, 0, 0)).set_fill(True) mask.ensure_chunk_at_index((0, 1, 0)).set_fill(True) mask.ensure_chunk_at_index((0, 0, 1))[0, 0, 0] = True b[mask] = 2 bdense = b.to_dense() expected = np.zeros((4, 4, 4), dtype=np.int8) expected[0, 0, 0] = 1 expected[2:4, 2:4, 2:4] = 1 expected[2, 1, 1] = 1 expected[2:4, 0:2, 0:2] = 2 expected[0:2, 2:4, 0:2] = 2 expected[0, 0, 2] = 2 self.assertEqual(expected.shape, bdense.shape) self.assertEqual(str(expected), str(bdense))
def test_padding(self): grid = ChunkGrid(2, int, -1) grid.ensure_chunk_at_index((0, 0, 0)).set_fill(1) grid.ensure_chunk_at_index((0, 0, 1)).set_fill(2) grid.ensure_chunk_at_index((0, 1, 0)).set_fill(3) grid.ensure_chunk_at_index((0, 1, 1)).set_fill(4) t = grid.chunks[(0, 0, 1)] t.set_array(np.array([ [(111, 112), (121, 122)], [(211, 212), (221, 222)] ])) expected1 = t.to_array()[:, :, 0] c = grid.chunks.get((0, 0, 0)) pad = c.padding(grid, 1) actual = pad expected = np.ones((4, 4, 4), int) * -1 expected[1:3, 1:3, 1:3] = 1 expected[1:-1, 1:-1, -1] = expected1 expected[1:-1, -1, 1:-1] = 3 self.assertEqual(expected.shape, actual.shape) # self.assertTrue(np.all(actual == expected), f"Failure! \n{actual}\n-------\n{expected}") self.assertEqual(str(expected), str(actual))
def test_eq_int(self): a = ChunkGrid(2, int, 0) a.set_value((1, 1, 1), 1) a.ensure_chunk_at_index((1, 1, 1)) result = a == 0 dense = result.to_dense() expected = np.ones((4, 4, 4), dtype=bool) expected[1, 1, 1] = False self.assertEqual(expected.shape, dense.shape) self.assertEqual(str(expected), str(dense)) self.assertTrue(result.any()) self.assertFalse(result.all())
def test_filled(self): a = ChunkGrid(2, bool, False) a.ensure_chunk_at_index((0, 0, 0)).set_fill(True) res = dilate(a, steps=1) self.assertEqual(7, len(res.chunks)) c0 = res.chunks.get((0, 0, 0)) self.assertIsNotNone(c0) self.assertTrue(c0.is_filled()) self.assertTrue(c0.value) expected = np.zeros((6, 6, 6), dtype=bool) expected[2:-2, 2:-2, 1:-1] = True expected[2:-2, 1:-1, 2:-2] = True expected[1:-1, 2:-2, 2:-2] = True self.assertEqual(str(expected), str(res.to_dense()))
def test_getitem(self): CS = 2 shape = (CS * 3, CS * 3, CS * 3) soll = np.arange(shape[0] * shape[1] * shape[2]).reshape(shape) a = ChunkGrid(CS, int, -1) for u in range(shape[0] // CS): for v in range(shape[1] // CS): for w in range(shape[2] // CS): x, y, z = u * CS, v * CS, w * CS index = (u, v, w) a.ensure_chunk_at_index(index).set_array(soll[x:x + CS, y:y + CS, z:z + CS]) dense = a.to_dense() self.assertEqual(soll.shape, dense.shape) self.assertEqual(str(soll), str(dense)) self.assertEqual(str(soll[1: shape[0] - 1, 1: shape[1] - 1, 1: shape[2] - 1]), str(a[1: shape[0] - 1, 1: shape[1] - 1, 1: shape[2] - 1]))
def test_getitem_offset(self): CS = 2 shape = (CS * 3, CS * 3, CS * 3) soll = np.arange(shape[0] * shape[1] * shape[2]).reshape(shape) offset_chunk = (-1, 0, 1) a = ChunkGrid(CS, int, -1) for u in range(shape[0] // CS): for v in range(shape[1] // CS): for w in range(shape[2] // CS): index = np.add((u, v, w), offset_chunk) x, y, z = np.multiply((u, v, w), CS) a.ensure_chunk_at_index(index).set_array(soll[x:x + CS, y:y + CS, z:z + CS]) offset_voxel = np.multiply(offset_chunk, CS) dense, off = a.to_dense(return_offset=True) self.assertEqual(soll.shape, dense.shape) self.assertEqual(str(soll), str(dense)) self.assertEqual(list(offset_voxel), list(off)) ox, oy, oz = off self.assertEqual(str(soll[1: shape[0] - 1, 1: shape[1] - 1, 1: shape[2] - 1]), str(a[1 + ox: shape[0] - 1 + ox, 1 + oy: shape[1] - 1 + oy, 1 + oz: shape[2] - 1 + oz]))
data_min, data_max = np.min(data, axis=0), np.max(data, axis=0) data_delta_max = np.max(data_max - data_min) resolution = 64 grid = ChunkGrid(16, dtype=int, fill_value=0) scaled = (data - data_min) * resolution / data_delta_max assert scaled.shape[1] == 3 grid[scaled] = 1 # Add padding filled = set(tuple(c.index) for c in grid.chunks) extra = set( tuple(n) for i in grid.chunks.keys() for f, n in grid.iter_neighbors_indices(i)) for e in extra: grid.ensure_chunk_at_index(e) fill_mask = flood_fill_at((7, 9, 7), grid == 0) grid[fill_mask] = 3 ren = VoxelRender() fig = ren.make_figure() fig.add_trace(ren.grid_voxel(grid == 1, opacity=1.0, flatshading=True)) fig.add_trace(ren.grid_wireframe(grid == 1, opacity=1.0, size=2.0)) # fig.add_trace(ren.grid_voxel(grid == 3, opacity=0.1, flatshading=True)) fig.add_trace(CloudRender().make_scatter(scaled, marker=dict(size=1))) fig.show()
def crust_fix( crust: ChunkGrid[np.bool8], outer_fill: ChunkGrid[np.bool8], crust_outer: ChunkGrid[np.bool8], crust_inner: ChunkGrid[np.bool8], min_distance: int = 1, data_pts: Optional[np.ndarray] = None, # for plotting, export_path: Optional[str] = None): CHUNKSIZE = crust.chunk_size normal_kernel = make_normal_kernel() inv_outer_fill = ~outer_fill # Method cache (prevent lookup in loop) __grid_set_value = ChunkGrid.set_value __np_sum = np.sum print("\tCreate Normals: ") with timed("\t\tTime: "): # normal_zero = np.zeros(3, dtype=np.float32) normal_pos = np.array(list(crust_outer.where())) normal_val = np.full((len(normal_pos), 3), 0.0, dtype=np.float32) for n, p in enumerate(normal_pos): x, y, z = p mask: np.ndarray = outer_fill[x - 1:x + 2, y - 1:y + 2, z - 1:z + 2] normal_val[n] = __np_sum(normal_kernel[mask], axis=0) normal_val = (normal_val.T / np.linalg.norm(normal_val, axis=1)).T print("\tGrid Normals: ") with timed("\t\tTime: "): normals: ChunkGrid[np.float32] = ChunkGrid( CHUNKSIZE, np.dtype((np.float32, (3, ))), 0.0) normals[normal_pos] = normal_val print("\tRender Normal Propagation: ") with timed("\t\tTime: "): markers_outer = np.array([ v for p, n in normals.items(mask=crust_outer) for v in (p, p + n, (np.nan, np.nan, np.nan)) ], dtype=np.float32) + 0.5 markers_outer_tips = np.array( [p + n for p, n in normals.items(mask=crust_outer)], dtype=np.float32) + 0.5 ren = CloudRender() fig = ren.make_figure(title="Crust-Fix: Start Normal Propagation") fig.add_trace( ren.make_scatter(markers_outer, marker=dict(opacity=0.5, ), mode="lines", name="Start normal")) fig.add_trace( ren.make_scatter(markers_outer_tips, marker=dict(size=1, symbol='x'), name="Start nromal end")) if data_pts is not None: fig.add_trace( ren.make_scatter(data_pts, opacity=0.1, size=1, name='Model')) if export_path: fig.write_html(os.path.join(export_path, "normal_start.html")) fig.show() print("\tNormal Propagation") with timed("\t\tTime: "): iterations = CHUNKSIZE * 2 nfield = propagate_normals(iterations, normals, crust_outer, inv_outer_fill) field_reset_mask = outer_fill ^ crust_outer nfield[field_reset_mask] = 0 nfield.cleanup(remove=True) # print("\tRender Normal Field: ") # with timed("\t\tTime: "): # # markers_crust = np.array( # [v for p, n in nfield.items(mask=crust) for v in (p, p + n, (np.nan, np.nan, np.nan))], # dtype=np.float32) + 0.5 # markers_outer = np.array( # [v for p, n in nfield.items(mask=crust_outer) for v in (p, p + n, (np.nan, np.nan, np.nan))], # dtype=np.float32) + 0.5 # markers_outer_tips = np.array( # [p + n for p, n in nfield.items(mask=crust_outer)], # dtype=np.float32) + 0.5 # # ren = CloudRender() # fig = ren.make_figure(title="Crust-Fix: Normal Field") # fig.add_trace(ren.make_scatter(markers_outer, marker=dict(opacity=0.5, ), mode="lines", name="Start normal")) # fig.add_trace(ren.make_scatter(markers_outer_tips, marker=dict(size=1, symbol='x'), name="Start normal end")) # fig.add_trace(ren.make_scatter(markers_crust, marker=dict(opacity=0.5, ), mode="lines", name="Normal field")) # if data_pts is not None: # fig.add_trace(ren.make_scatter(data_pts, opacity=0.1, size=1, name='Model')) # if export_path: # fig.write_html(os.path.join(export_path, "normal_field.html")) # fig.show() print("\tNormal cone: ") with timed("\t\tTime: "): medial = ChunkGrid(crust.chunk_size, np.bool8, False) cone_threshold: float = 0.5 * np.pi min_norm: float = 1e-15 for chunk in nfield.chunks: padded = nfield.padding_at(chunk.index, 1, corners=True, edges=True) cones = normal_cone_angles(padded, cone_threshold, min_norm) medial.ensure_chunk_at_index(chunk.index).set_array(cones.copy()) medial.cleanup(remove=True) print("\tResult: ") with timed("\t\tTime: "): # Remove artifacts where the inner and outer crusts are touching artifacts_fix = outer_fill.copy().pad_chunks(1) artifacts_fix.fill_value = False artifacts_fix = ~dilate(artifacts_fix, steps=max(0, min_distance) + 2) & ~outer_fill medial_cleaned = medial & artifacts_fix medial_cleaned.cleanup(remove=True) print("\tRender 2: ") with timed("\t\tTime: "): time.sleep(0.01) ren = VoxelRender() fig = ren.make_figure(title="Crust-Normal-Fix: Result before cleanup") print("Ren2-medial") fig.add_trace(ren.grid_voxel(medial, opacity=0.3, name='Medial')) # fig.add_trace(ren.grid_voxel(medial_cleaned, opacity=0.05, name='Fixed')) print("Ren2-crust_outer") fig.add_trace(ren.grid_voxel(crust_outer, opacity=0.05, name='Outer')) if data_pts is not None: print("Ren2-data_pts") fig.add_trace(CloudRender().make_scatter(data_pts, opacity=0.2, size=1, name='Model')) print("Ren2-show") if export_path: fig.write_html(os.path.join(export_path, "medial.html")) fig.show() print("\tRender 3: ") with timed("\t\tTime: "): time.sleep(0.01) ren = VoxelRender() fig = ren.make_figure(title="Crust-Fix: Result after cleanup") # fig.add_trace(ren.grid_voxel(medial, opacity=0.3, name='Fixed')) print("Ren2-medial_cleaned") fig.add_trace( ren.grid_voxel(medial_cleaned, opacity=0.3, name='Medial-Cleaned')) print("Ren3-crust_outer") fig.add_trace(ren.grid_voxel(crust_outer, opacity=0.05, name='Outer')) if data_pts is not None: print("Ren3-data_pts") fig.add_trace(CloudRender().make_scatter(data_pts, opacity=0.2, size=1, name='Model')) print("Ren3-show") if export_path: fig.write_html(os.path.join(export_path, "medial_cleaned.html")) fig.show() return medial_cleaned