From 92ee4c6e312cecaced5f18b608b9a3d8996f5e70 Mon Sep 17 00:00:00 2001 From: marcomusy Date: Fri, 24 Nov 2023 22:05:02 +0100 Subject: [PATCH] add ex. slab_vol.py madcad1.py --- docs/changes.md | 7 +- examples/other/madcad1.py | 55 ++++++++++++ examples/volumetric/slab_vol.py | 20 +++++ vedo/core.py | 2 +- vedo/plotter.py | 3 + vedo/shapes.py | 15 +++- vedo/utils.py | 153 ++++++++++++++++++++++++++------ vedo/version.py | 2 +- vedo/volume.py | 24 +++-- 9 files changed, 241 insertions(+), 40 deletions(-) create mode 100644 examples/other/madcad1.py create mode 100644 examples/volumetric/slab_vol.py diff --git a/docs/changes.md b/docs/changes.md index 1bdff54c..b3aef5d5 100644 --- a/docs/changes.md +++ b/docs/changes.md @@ -1,16 +1,19 @@ ## Main changes - - fixes to `extrude()` thanks to @JeffreyWardman - filter out triangle strips in Ribbon and extrude() - improvements in doc strings +- add `utils.madcad2vedo` conversion as per #976 by @JeffreyWardman +- add `utils.camera_to_dict()` ## Breaking changes - improvements to `shapes.Ellipsoid()` and bug fixes in #978 by @daniel-a-diaz - improvements to `pointcloud.pca_ellipsoid()` and bug fixes - improvements to `pointcloud.pca_ellipse()` and bug fixes + ### Renaming + ### Other changes - add `core.apply_transform_from_actor()` - add `add volume.slab()` @@ -23,6 +26,8 @@ ## New/Revised Examples ``` +examples/volumetric/slab_vol.py +examples/other/madcad1.py ``` ### Broken Examples diff --git a/examples/other/madcad1.py b/examples/other/madcad1.py new file mode 100644 index 00000000..ea31a8d8 --- /dev/null +++ b/examples/other/madcad1.py @@ -0,0 +1,55 @@ +# Example of usage of the madcad library +# See https://pymadcad.readthedocs.io/en/latest/index.html +import vedo +from madcad import * + +########################################################################## +points = [O, X, X + Z, 2 * X + Z, 2 * (X + Z), X + 2 * Z, X + 5 * Z, 5 * Z] +section = Wire(points).segmented().flip() +rev = revolution(2 * pi, (O, Z), section) +rev.mergeclose() +vedo.show("Revolution of a wire", rev, axes=7).close() + + +########################################################################## +m = screw(10, 20) +m["part"].option(color=vec3(70, 130, 180) / 255) # RGB +vedo.show("A blue screw", m, axes=1).close() + + +########################################################################## +# Obtain two different shapes that has noting to to with each other +m1 = brick(width=vec3(2)) +m2 = m1.transform(vec3(0.5, 0.3, 0.4)).transform(quat(0.7 * vec3(1, 1, 0))) +# Remove the volume of the second to the first +diff = difference(m1, m2) +vedo.show("Boolean difference", diff, axes=14).close() + + +########################################################################## +cube = brick(width=vec3(2)) +bevel( + cube, + [(0, 1), (1, 2), (2, 3), (0, 3), (1, 5), (0, 4)], # Edges to smooth + ("width", 0.3), # Cutting description, known as 'cutter' +) +vedo.show("A bevel cube", cube, axes=1).close() + + +########################################################################## +square_profile = square((O, Z), 5).flip() +primitives = [ + ArcCentered((5 * X, Y), O, 10 * X), + ArcCentered((15 * X, -Y), 10 * X, 20 * X), +] +# Generate a path +path = web(primitives) +path.mergeclose() +m = tube(square_profile, path) + +vmesh = vedo.utils.madcad2vedo(m) # <-- convert to vedo.Mesh +print(vmesh) + +scalar = vmesh.vertices[:, 0] +vmesh.cmap("rainbow", scalar).add_scalarbar(title="x-value") +vedo.show("Generating a path", vmesh, axes=7).close() diff --git a/examples/volumetric/slab_vol.py b/examples/volumetric/slab_vol.py new file mode 100644 index 00000000..919e8e63 --- /dev/null +++ b/examples/volumetric/slab_vol.py @@ -0,0 +1,20 @@ +"""Use slab() to extract a "thick" 2D slice from a 3D volume""" +from vedo import Axes, Volume, Box, dataurl, settings, show +from vedo.pyplot import histogram + +settings.default_font = "Calco" + +vol = Volume(dataurl + "embryo.tif") +vaxes = Axes(vol, xygrid=False) + +slab = vol.slab([45,55], axis='z', operation='mean') +slab.cmap('Set1_r', vmin=10, vmax=80).add_scalarbar("intensity") +# histogram(slab).show().close() # quickly inspect it + +bbox = slab.metadata["slab_bounding_box"] +slab.z(-bbox[5] + vol.zbounds()[0]) # move slab to the bottom + +# create a box around the slab for reference +slab_box = Box(bbox).wireframe().c("black") + +show(__doc__, vol, slab, slab_box, vaxes, axes=14, viewup='z') diff --git a/vedo/core.py b/vedo/core.py index 526ae35d..fa51cc4e 100644 --- a/vedo/core.py +++ b/vedo/core.py @@ -492,7 +492,7 @@ def box(self, scale=1, padding=0): padding = [padding, padding, padding] length, width, height = b[1] - b[0], b[3] - b[2], b[5] - b[4] tol = (length + width + height) / 30000 # useful for boxing text - pos = [(b[0] + b[1]) / 2, (b[3] + b[2]) / 2, (b[5] + b[4]) / 2 - tol] + pos = [(b[0] + b[1])/2, (b[3] + b[2])/2, (b[5] + b[4])/2 - tol] bx = vedo.shapes.Box( pos, length * scale + padding[0], diff --git a/vedo/plotter.py b/vedo/plotter.py index 733b8cd4..efe32569 100644 --- a/vedo/plotter.py +++ b/vedo/plotter.py @@ -3040,6 +3040,9 @@ def _scan_input_return_acts(self, objs): scanned_acts.append(dlf.MeshActor(a).actor) + elif "madcad" in str(type(a)): + scanned_acts.append(utils.madcad2vedo(a)) + else: vedo.logger.error(f"cannot understand input in show(): {type(a)}") diff --git a/vedo/shapes.py b/vedo/shapes.py index 059a0344..fd86230d 100644 --- a/vedo/shapes.py +++ b/vedo/shapes.py @@ -3312,14 +3312,24 @@ def __init__( ![](https://vedo.embl.es/images/simulations/50738955-7e891800-11d9-11e9-85cd-02bd4f3f13ea.gif) """ src = vtk.new("CubeSource") + + if len(pos) == 2: + pos = (pos[0], pos[1], 0) + if len(pos) == 6: src.SetBounds(pos) - pos = [0,0,0] elif len(size) == 3: length, width, height = size src.SetXLength(length) src.SetYLength(width) src.SetZLength(height) + src.SetCenter(pos) + else: + src.SetXLength(length) + src.SetYLength(width) + src.SetZLength(height) + src.SetCenter(pos) + src.Update() pd = src.GetOutput() @@ -3352,9 +3362,6 @@ def __init__( vtc = utils.numpy2vtk(tc) pd.GetPointData().SetTCoords(vtc) super().__init__(pd, c, alpha) - if len(pos) == 2: - pos = (pos[0], pos[1], 0) - self.pos(pos) self.name = "Box" diff --git a/vedo/utils.py b/vedo/utils.py index 15b744fc..fa171223 100644 --- a/vedo/utils.py +++ b/vedo/utils.py @@ -21,7 +21,6 @@ "ProgressBar", "progressbar", "geometry", - "extract_cells_by_type", "is_sequence", "lin_interpolate", "vector", @@ -41,6 +40,8 @@ "print_inheritance_tree", "camera_from_quaternion", "camera_from_neuroglancer", + "camera_from_dict", + "camera_to_dict", "oriented_camera", "vedo2trimesh", "trimesh2vedo", @@ -566,7 +567,7 @@ def make3d(pts): return np.c_[pts, np.zeros(pts.shape[0], dtype=pts.dtype)] if pts.shape[1] != 3: - raise ValueError("input shape is not supported.") + raise ValueError(f"input shape is not supported: {pts.shape}") return pts @@ -592,31 +593,6 @@ def geometry(obj, extent=None): return vedo.Mesh(gf.GetOutput()) -def extract_cells_by_type(obj, types=()): - """ - Extract cells of a specified type from a vtk dataset. - - Given an input `vtkDataSet` and a list of cell types, produce an output - containing only cells of the specified type(s). - - Find [here](https://vtk.org/doc/nightly/html/vtkCellType_8h_source.html) - the list of possible cell types. - - Return: - a `vtkDataSet` object which can be wrapped. - """ - ef = vtk.new("ExtractCellsByType") - try: - ef.SetInputData(obj.dataset) - except AttributeError: - ef.SetInputData(obj) - - for ct in types: - ef.AddCellType(ct) - ef.Update() - return ef.GetOutput() - - def buildPolyData(vertices, faces=None, lines=None, index_offset=0, tetras=False): """ Build a `vtkPolyData` object from a list of vertices @@ -1917,6 +1893,38 @@ def camera_from_dict(camera, modify_inplace=None): if cm_roll is not None: vcam.SetRoll(cm_roll) return vcam +def camera_to_dict(vtkcam): + """ + Convert a [vtkCamera](https://vtk.org/doc/nightly/html/classvtkCamera.html) + object into a python dictionary. + + Parameters of the camera are: + - `position` (3-tuple) + - `focal_point` (3-tuple) + - `viewup` (3-tuple) + - `distance` (float) + - `clipping_range` (2-tuple) + - `parallel_scale` (float) + - `thickness` (float) + - `view_angle` (float) + - `roll` (float) + + Arguments: + vtkcam: (vtkCamera) + a `vtkCamera` object to convert. + """ + cam = dict() + cam["position"] = np.array(vtkcam.GetPosition()) + cam["focal_point"] = np.array(vtkcam.GetFocalPoint()) + cam["viewup"] = np.array(vtkcam.GetViewUp()) + cam["distance"] = vtkcam.GetDistance() + cam["clipping_range"] = np.array(vtkcam.GetClippingRange()) + cam["parallel_scale"] = vtkcam.GetParallelScale() + cam["thickness"] = vtkcam.GetThickness() + cam["view_angle"] = vtkcam.GetViewAngle() + cam["roll"] = vtkcam.GetRoll() + return cam + def vtkCameraToK3D(vtkcam): """ @@ -2374,6 +2382,97 @@ def vedo2open3d(vedo_mesh): return o3d_mesh +def madcad2vedo(madcad_mesh): + """ + Convert a `madcad.Mesh` to a `vedo.Mesh`. + + See [pymadcad website](https://pymadcad.readthedocs.io/en/latest/index.html) + for more info. + """ + try: + madcad_mesh = madcad_mesh["part"] + except: + pass + + ppp = [] + for p in madcad_mesh.points: + ppp.append([float(p[0]), float(p[1]), float(p[2])]) + ppp = np.array(ppp) + + fff = [] + try: + for f in madcad_mesh.faces: + fff.append([int(f[0]), int(f[1]), int(f[2])]) + fff = np.array(fff).astype(np.uint16) + except AttributeError: + # print("no faces") + pass + + eee = [] + try: + edges = madcad_mesh.edges + for e in edges: + eee.append([int(e[0]), int(e[1])]) + eee = np.array(eee).astype(np.uint16) + except (AttributeError, TypeError): + # print("no edges") + pass + + try: + line = np.array(madcad_mesh.indices).astype(np.uint16) + eee.append(line) + except AttributeError: + # print("no indices") + pass + + ttt = [] + try: + for t in madcad_mesh.tracks: + ttt.append(int(t)) + ttt = np.array(ttt).astype(np.uint16) + except AttributeError: + # print("no tracks") + pass + + ############################### + poly = vedo.utils.buildPolyData(ppp, fff, eee) + if len(fff) == 0 and len(eee) == 0: + m = vedo.Points(poly) + else: + m = vedo.Mesh(poly) + + if len(ttt) == len(fff): + m.celldata["tracks"] = ttt + maxt = np.max(ttt) + m.mapper.SetScalarRange(0, np.max(ttt)) + if maxt==0: m.mapper.SetScalarVisibility(0) + elif len(ttt) == len(ppp): + m.pointdata["tracks"] = ttt + maxt = np.max(ttt) + m.mapper.SetScalarRange(0, maxt) + if maxt==0: m.mapper.SetScalarVisibility(0) + + try: + m.info["madcad_groups"] = madcad_mesh.groups + except AttributeError: + # print("no groups") + pass + + try: + options = dict(madcad_mesh.options) + if "display_wire" in options and options["display_wire"]: + m.lw(1).lc(madcad_mesh.c()) + if "display_faces" in options and not options["display_faces"]: + m.alpha(0.2) + if "color" in options: + m.c(options["color"]) + except AttributeError: + # print("no options") + pass + + return m + + def vtk_version_at_least(major, minor=0, build=0): """ Check the installed VTK version. diff --git a/vedo/version.py b/vedo/version.py index 3cb48677..c7f0ed36 100644 --- a/vedo/version.py +++ b/vedo/version.py @@ -1 +1 @@ -_version = '2023.5.0+dev6' +_version = '2023.5.0+dev7' diff --git a/vedo/volume.py b/vedo/volume.py index 690850fb..5b38bebe 100644 --- a/vedo/volume.py +++ b/vedo/volume.py @@ -489,7 +489,6 @@ def slab(self, slice_range=(), axis='z', operation="mean"): islab = vtk.new("ImageSlab") islab.SetInputData(self.dataset) - islab.SetSliceRange(slice_range) if operation in ["+", "add", "sum"]: islab.SetOperationToSum() @@ -502,17 +501,31 @@ def slab(self, slice_range=(), axis='z', operation="mean"): else: vedo.logger.error(f"in slab(): unknown operation {operation}") raise ValueError() - + + dims = self.dimensions() if axis == 'x': islab.SetOrientationToX() + if slice_range[0] > dims[0]-1: + slice_range[0] = dims[0]-1 + if slice_range[1] > dims[0]-1: + slice_range[1] = dims[0]-1 elif axis == 'y': islab.SetOrientationToY() + if slice_range[0] > dims[1]-1: + slice_range[0] = dims[1]-1 + if slice_range[1] > dims[1]-1: + slice_range[1] = dims[1]-1 elif axis == 'z': islab.SetOrientationToZ() + if slice_range[0] > dims[2]-1: + slice_range[0] = dims[2]-1 + if slice_range[1] > dims[2]-1: + slice_range[1] = dims[2]-1 else: vedo.logger.error(f"Error in slab(): unknown axis {axis}") raise RuntimeError() + islab.SetSliceRange(slice_range) islab.Update() msh = Mesh(islab.GetOutput()).lighting('off') @@ -520,11 +533,10 @@ def slab(self, slice_range=(), axis='z', operation="mean"): msh.mapper.SetScalarRange(self.scalar_range()) msh.metadata["slab_range"] = slice_range - msh.metadata["slab_axis"] = axis + msh.metadata["slab_axis"] = axis msh.metadata["slab_operation"] = operation - # compute bounds of slices - dims = self.dimensions() + # compute bounds of slab origin = self.origin() spacing = self.spacing() if axis == 'x': @@ -561,7 +573,7 @@ def slab(self, slice_range=(), axis='z', operation="mean"): parents=[self], c="#4cc9f0:#e9c46a", ) - msh.name = "VolumeSlabMesh" + msh.name = "SlabMesh" return msh