Esempio n. 1
0
def _average_distance(points, transform1, transform2, translate=True):
    assert points.shape == (points.shape[0], 3)
    assert transform1.shape == (4, 4)
    assert transform2.shape == (4, 4)
    points1 = tf.transform_points(points, transform1, translate=translate)
    points2 = tf.transform_points(points, transform2, translate=translate)

    add = np.linalg.norm(points1 - points2, axis=1).mean()

    kdtree = sklearn.neighbors.KDTree(points2)
    indices = kdtree.query(points1, return_distance=False)[:, 0]
    add_s = np.linalg.norm(points1 - points2[indices], axis=1).mean()

    return add, add_s
Esempio n. 2
0
    def callback(dt):
        if window.rotate:
            for widget in widgets.values():
                if isinstance(widget, trimesh.viewer.SceneWidget):
                    scene = widget.scene
                    camera = scene.camera
                    axis = tf.transform_points([[0, 1, 0]],
                                               camera.transform,
                                               translate=False)[0]
                    camera.transform = tf.rotation_matrix(
                        np.deg2rad(window.rotate),
                        axis,
                        point=scene.centroid,
                    ) @ camera.transform
                    widget.view['ball']._n_pose = camera.transform
            return

        if window.scenes_group and (window.next or window.play):
            try:
                scenes = next(window.scenes_group)
                for key, widget in widgets.items():
                    if isinstance(widget, trimesh.viewer.SceneWidget):
                        widget.scene.geometry.update(scenes[key].geometry)
                        widget.scene.graph.load(
                            scenes[key].graph.to_edgelist())
                        widget._draw()
                    elif isinstance(widget, glooey.Image):
                        widget.set_image(numpy_to_image(scenes[key]))
            except StopIteration:
                window.play = False
            window.next = False
Esempio n. 3
0
    def get_reciprocal_slice(self,
                             plane_normal: Tuple[int, int, int],
                             distance: float = 0) -> ReciprocalSlice:
        """
        Get a reciprocal slice through the Brillouin zone, defined by the intersection
        of a plane with the lattice.

        Args:
            plane_normal: The plane normal in fractional indices. E.g., ``(1, 0, 0)``.
            distance: The distance from the center of the Brillouin zone (the Gamma
                point).

        Returns:
            The reciprocal slice.
        """
        cart_normal = np.dot(plane_normal, self.reciprocal_lattice)
        cart_center = cart_normal * distance

        # get the intersections with the faces
        intersections, _ = plane_lines(cart_center, cart_normal,
                                       self.lines.transpose(1, 0, 2))

        if len(intersections) == 0:
            raise ValueError("Plane does not intersect reciprocal cell")

        #  transform the intersections from 3D space to 2D coordinates
        transformation = plane_transform(origin=cart_center,
                                         normal=cart_normal)
        points = transform_points(intersections, transformation)[:, :2]

        return ReciprocalSlice(self, points, transformation)
Esempio n. 4
0
 def world_to_camera(self, points):
     """Transform points from world to camera coordinates.
     Useful for understanding where the objects are, as seen by the camera.
     :param points: either n * 3 array, or a single 3-vector
     """
     points = np.atleast_2d(points)
     return tt.transform_points(points, self._world_to_camera_4x4)
    def update_octree(self, instance_id):
        T_cad2cam_pred = self._Ts_cad2cam_pred[instance_id]

        class_id = self._class_ids[instance_id]
        # points = self._models.get_pcd(class_id=class_id)
        points = self._models.get_solid_voxel_grid(class_id=class_id).points
        points = ttf.transform_points(points, T_cad2cam_pred)

        self._mapping.update(instance_id, points)
Esempio n. 6
0
def main():
    models = morefusion.datasets.YCBVideoModels()
    points = models.get_pcd(class_id=2)

    quaternion_true = tf.random_quaternion()
    quaternion_pred = quaternion_true + [0.1, 0, 0, 0]

    transform_true = tf.quaternion_matrix(quaternion_true)
    transform_pred = tf.quaternion_matrix(quaternion_pred)

    scenes = {}
    for use_translation in [False, True]:
        if use_translation:
            translation_true = np.random.uniform(-0.02, 0.02, (3,))
            translation_pred = np.random.uniform(-0.02, 0.02, (3,))
            transform_true[:3, 3] = translation_true
            transform_pred[:3, 3] = translation_pred

        add = morefusion.metrics.average_distance(
            [points], [transform_true], [transform_pred]
        )[0][0]

        # ---------------------------------------------------------------------

        scene = trimesh.Scene()

        points_true = tf.transform_points(points, transform_true)
        colors = np.full((points_true.shape[0], 3), [1.0, 0, 0])
        geom = trimesh.PointCloud(vertices=points_true, color=colors)
        scene.add_geometry(geom)

        points_pred = tf.transform_points(points, transform_pred)
        colors = np.full((points_true.shape[0], 3), [0, 0, 1.0])
        geom = trimesh.PointCloud(vertices=points_pred, color=colors)
        scene.add_geometry(geom)

        scenes[f"use_translation: {use_translation}, add: {add}"] = scene
        if scenes:
            camera_transform = list(scenes.values())[0].camera_transform
            scene.camera_transform = camera_transform

    morefusion.extra.trimesh.display_scenes(scenes)
Esempio n. 7
0
 def camera_to_world(self, points, translate=True):
     """Transform points from camera to world coordinates.
     Useful for understanding where objects bound to camera
     (e.g., image pixels) are in the world.
     :param points: either n * 3 array, or a single 3-vector
     :param translate: if True, also translate the points
     """
     points = np.atleast_2d(points)
     return tt.transform_points(points,
                                self._camera_to_world_4x4,
                                translate=translate)
Esempio n. 8
0
    def callback(dt):
        if window.rotate:
            for widget in widgets.values():
                if isinstance(widget, trimesh.viewer.SceneWidget):
                    axis = tf.transform_points(
                        [[0, 1, 0]],
                        widget.scene.camera_transform,
                        translate=False,
                    )[0]
                    widget.scene.camera_transform[...] = (tf.rotation_matrix(
                        np.deg2rad(window.rotate * rotation_scaling),
                        axis,
                        point=widget.scene.centroid,
                    ) @ widget.scene.camera_transform)
                    widget.view["ball"]._n_pose = widget.scene.camera_transform
            return

        if window.scenes_group and (window.next or window.play):
            try:
                scenes = next(window.scenes_group)
                clear = scenes.get("__clear__", False) or window._clear
                window._clear = False
                for key, widget in widgets.items():
                    scene = scenes[key]
                    if isinstance(widget, trimesh.viewer.SceneWidget):
                        assert isinstance(scene, trimesh.Scene)
                        if clear:
                            widget.clear()
                            widget.scene = scene
                        else:
                            widget.scene.geometry.update(scene.geometry)
                            widget.scene.graph.load(scene.graph.to_edgelist())
                        widget.scene.camera_transform[
                            ...] = scene.camera_transform
                        widget.view[
                            "ball"]._n_pose = widget.scene.camera_transform
                        widget._draw()
                    elif isinstance(widget, glooey.Image):
                        widget.set_image(numpy_to_image(scene))
            except StopIteration:
                print("Reached the end of the scenes")
                window.play = False
            window.next = False
Esempio n. 9
0
    def __getitem__(self, index):

        # find shape that contains the point with given global index
        shape_ind, patch_ind = self.shape_index(index)

        def get_patch_points(shape, query_point):

            from source.base import point_cloud

            # optionally always pick the same points for a given patch index (mainly for debugging)
            if self.identical_epochs:
                self.rng.seed((self.seed + index) % (2**32))

            patch_pts_ids = point_cloud.get_patch_kdtree(
                kdtree=shape.kdtree, rng=self.rng, query_point=query_point,
                patch_radius=self.patch_radius,
                points_per_patch=self.points_per_patch, n_jobs=1)

            # find -1 ids for padding
            patch_pts_pad_ids = patch_pts_ids == -1
            patch_pts_ids[patch_pts_pad_ids] = 0
            pts_patch_ms = shape.pts[patch_pts_ids, :]
            # replace padding points with query point so that they appear in the patch origin
            pts_patch_ms[patch_pts_pad_ids, :] = query_point
            patch_radius_ms = utils.get_patch_radii(pts_patch_ms, query_point)\
                if self.patch_radius <= 0.0 else self.patch_radius
            pts_patch_ps = utils.model_space_to_patch_space(
                pts_to_convert_ms=pts_patch_ms, pts_patch_center_ms=query_point,
                patch_radius_ms=patch_radius_ms)

            return patch_pts_ids, pts_patch_ps, pts_patch_ms, patch_radius_ms

        shape = self.shape_cache.get(shape_ind)
        imp_surf_query_point_ms = shape.imp_surf_query_point_ms[patch_ind]

        # get neighboring points
        patch_pts_ids, patch_pts_ps, pts_patch_ms, patch_radius_ms = \
            get_patch_points(shape=shape, query_point=imp_surf_query_point_ms)
        imp_surf_query_point_ps = utils.model_space_to_patch_space_single_point(
            imp_surf_query_point_ms, imp_surf_query_point_ms, patch_radius_ms)

        # surf dist can be None because we have no ground truth for evaluation
        # need a number or Pytorch will complain when assembling the batch
        if self.reconstruction:
            imp_surf_dist_ms = np.array([np.inf])
            imp_surf_dist_sign_ms = np.array([np.inf])
        else:
            imp_surf_dist_ms = shape.imp_surf_dist_ms[patch_ind]
            imp_surf_dist_sign_ms = np.sign(imp_surf_dist_ms)
            imp_surf_dist_sign_ms = 0.0 if imp_surf_dist_sign_ms < 0.0 else 1.0

        if self.sub_sample_size > 0:
            pts_sub_sample_ms = utils.get_point_cloud_sub_sample(
                sub_sample_size=self.sub_sample_size, pts_ms=shape.pts,
                query_point_ms=imp_surf_query_point_ms, uniform=self.uniform_subsample)
        else:
            pts_sub_sample_ms = np.array([], dtype=np.float32)

        if not self.reconstruction:
            import trimesh.transformations as trafo
            # random rotation of shape and patch as data augmentation
            rand_rot = trimesh.transformations.random_rotation_matrix(self.rng.rand(3))
            # rand_rot = trimesh.transformations.identity_matrix()
            pts_sub_sample_ms = \
                trafo.transform_points(pts_sub_sample_ms, rand_rot).astype(np.float32)
            patch_pts_ps = \
                trafo.transform_points(patch_pts_ps, rand_rot).astype(np.float32)
            imp_surf_query_point_ms = \
                trafo.transform_points(np.expand_dims(imp_surf_query_point_ms, 0), rand_rot)[0].astype(np.float32)
            imp_surf_query_point_ps = \
                trafo.transform_points(np.expand_dims(imp_surf_query_point_ps, 0), rand_rot)[0].astype(np.float32)

        patch_data = dict()
        # create new arrays to close the memory mapped files
        patch_data['patch_pts_ps'] = patch_pts_ps
        patch_data['patch_radius_ms'] = np.array(patch_radius_ms, dtype=np.float32)
        patch_data['pts_sub_sample_ms'] = pts_sub_sample_ms
        patch_data['imp_surf_query_point_ms'] = imp_surf_query_point_ms
        patch_data['imp_surf_query_point_ps'] = np.array(imp_surf_query_point_ps)
        patch_data['imp_surf_ms'] = np.array([imp_surf_dist_ms], dtype=np.float32)
        patch_data['imp_surf_magnitude_ms'] = np.array([np.abs(imp_surf_dist_ms)], dtype=np.float32)
        patch_data['imp_surf_dist_sign_ms'] = np.array([imp_surf_dist_sign_ms], dtype=np.float32)

        # un-comment to get a debug output of a training sample
        # import evaluation
        # evaluation.visualize_patch(
        #     patch_pts_ps=patch_data['patch_pts_ps'], patch_pts_ms=pts_patch_ms,
        #     query_point_ps=patch_data['imp_surf_query_point_ps'],
        #     pts_sub_sample_ms=patch_data['pts_sub_sample_ms'], query_point_ms=patch_data['imp_surf_query_point_ms'],
        #     file_path='debug/patch_local_and_global.ply')
        # patch_sphere = trimesh.primitives.Sphere(radius=self.patch_radius, center=imp_surf_query_point_ms)
        # patch_sphere.export(file_obj='debug/patch_sphere.ply')
        # print('Debug patch outputs with radius {} in "debug" dir'.format(self.patch_radius))

        # convert to tensors
        for key in patch_data.keys():
            patch_data[key] = torch.from_numpy(patch_data[key])

        return patch_data
Esempio n. 10
0
def extrude_triangulation(vertices,
                          faces,
                          height,
                          cap=True,
                          base=True,
                          transform=None):
    """
    Based on Trimesh extrude_triangulation, but allows to exclude cap and base.
    """
    vertices = np.asanyarray(vertices, dtype=np.float64)
    height = float(height)
    faces = np.asanyarray(faces, dtype=np.int64)

    if not util.is_shape(vertices, (-1, 2)):
        raise ValueError('Vertices must be (n,2)')
    if not util.is_shape(faces, (-1, 3)):
        raise ValueError('Faces must be (n,3)')
    if np.abs(height) < constants.tol.merge:
        raise ValueError('Height must be nonzero!')

    # Make sure triangulation winding is pointing up
    normal_test = triangles.normals([util.stack_3D(vertices[faces[0]])])[0]

    normal_dot = np.dot(normal_test, [0.0, 0.0, np.sign(height)])[0]

    # Make sure the triangulation is aligned with the sign of
    # the height we've been passed
    if normal_dot < 0.0: faces = np.fliplr(faces)

    # stack the (n,3) faces into (3*n, 2) edges
    edges = geometry.faces_to_edges(faces)
    edges_sorted = np.sort(edges, axis=1)
    # Edges which only occur once are on the boundary of the polygon
    # since the triangulation may have subdivided the boundary of the
    # shapely polygon, we need to find it again
    edges_unique = grouping.group_rows(edges_sorted, require_count=1)

    # (n, 2, 2) set of line segments (positions, not references)
    boundary = vertices[edges[edges_unique]]

    # We are creating two vertical  triangles for every 2D line segment
    # on the boundary of the 2D triangulation
    vertical = np.tile(boundary.reshape((-1, 2)), 2).reshape((-1, 2))
    vertical = np.column_stack(
        (vertical, np.tile([0, height, 0, height], len(boundary))))
    vertical_faces = np.tile([3, 1, 2, 2, 1, 0], (len(boundary), 1))
    vertical_faces += np.arange(len(boundary)).reshape((-1, 1)) * 4
    vertical_faces = vertical_faces.reshape((-1, 3))

    # Stack the (n,2) vertices with zeros to make them (n, 3)
    vertices_3D = util.stack_3D(vertices)

    # A sequence of zero- indexed faces, which will then be appended
    # with offsets to create the final mesh

    if not base and not cap:
        vertices_seq = [vertical]
        faces_seq = [vertical_faces]
    elif not base and cap:
        vertices_seq = [vertices_3D.copy() + [0.0, 0, height], vertical]
        faces_seq = [faces.copy(), vertical_faces]
    elif base and not cap:
        vertices_seq = [vertices_3D, vertical]
        faces_seq = [faces[:, ::-1], vertical_faces]
    else:
        vertices_seq = [
            vertices_3D,
            vertices_3D.copy() + [0.0, 0, height], vertical
        ]
        faces_seq = [faces[:, ::-1], faces.copy(), vertical_faces]

    # Append sequences into flat nicely indexed arrays
    vertices, faces = util.append_faces(vertices_seq, faces_seq)

    # Apply transform here to avoid later bookkeeping
    if transform is not None:
        vertices = transformations.transform_points(vertices, transform)
        # If the transform flips the winding flip faces back so that the normals will be facing outwards
        if transformations.flips_winding(transform):
            faces = np.ascontiguousarray(
                np.fliplr(faces))  # fliplr makes arrays non-contiguous

    # create mesh object with passed keywords
    mesh = Trimesh(vertices=vertices, faces=faces)

    return mesh
Esempio n. 11
0
def _pcd_files_to_pts(pcd_files,
                      pts_file_npy,
                      pts_file,
                      obj_locations,
                      obj_rotations,
                      min_pts_size=0,
                      debug=False):
    """
    Convert pcd blensor results to xyz or directly to npy files. Merge front and back scans.
    Moving the object instead of the camera because the point cloud is in some very weird space that behaves
    crazy when the camera moves. A full day wasted on this shit!
    :param pcd_files:
    :param pts_file_npy:
    :param pts_file:
    :param trafos_inv:
    :param debug:
    :return:
    """

    import gzip

    def revert_offset(pts_data: np.ndarray, inv_offset: np.ndarray):
        pts_reverted = pts_data
        # don't just check the header because missing rays may be added with NaNs
        if pts_reverted.shape[0] > 0:
            pts_offset_correction = np.broadcast_to(inv_offset,
                                                    pts_reverted.shape)
            pts_reverted += pts_offset_correction

        return pts_reverted

    # https://www.blensor.org/numpy_import.html
    def extract_xyz_from_blensor_numpy(arr_raw):
        # timestamp
        # yaw, pitch
        # distance,distance_noise
        # x,y,z
        # x_noise,y_noise,z_noise
        # object_id
        # 255*color[0]
        # 255*color[1]
        # 255*color[2]
        # idx
        hits = arr_raw[arr_raw[:, 3] != 0.0]  # distance != 0.0 --> hit
        noisy_xyz = hits[:, [8, 9, 10]]
        return noisy_xyz

    pts_data_to_cat = []
    for fi, f in enumerate(pcd_files):
        try:
            if f.endswith('.numpy.gz'):
                pts_data_vs = extract_xyz_from_blensor_numpy(
                    np.loadtxt(gzip.GzipFile(f, "r")))
            elif f.endswith('.numpy'):
                pts_data_vs = extract_xyz_from_blensor_numpy(np.loadtxt(f))
            elif f.endswith('.pcd'):
                pts_data_vs, header_info = point_cloud.load_pcd(file_in=f)
            else:
                raise ValueError(
                    'Input file {} has an unknown format!'.format(f))
        except EOFError as er:
            print('Error processing {}: {}'.format(f, er))
            continue

        # undo coordinate system changes
        pts_data_vs = utils.right_handed_to_left_handed(pts_data_vs)

        # move back from camera distance, always along x axis
        obj_location = np.array(obj_locations[fi])
        revert_offset(pts_data_vs, -obj_location)

        # get and apply inverse rotation matrix of camera
        scanner_rotation_inv = trafo.quaternion_matrix(
            trafo.quaternion_conjugate(obj_rotations[fi]))
        pts_data_ws_test_inv = trafo.transform_points(pts_data_vs,
                                                      scanner_rotation_inv,
                                                      translate=False)
        pts_data_ws = pts_data_ws_test_inv

        if pts_data_ws.shape[0] > 0:
            pts_data_to_cat += [pts_data_ws.astype(np.float32)]

        # debug outputs to check the rotations... the pointcloud MUST align exactly with the mesh
        if debug:
            point_cloud.write_xyz(file_path=os.path.join(
                'debug', 'test_{}.xyz'.format(str(fi))),
                                  points=pts_data_ws)

    if len(pts_data_to_cat) > 0:
        pts_data = np.concatenate(tuple(pts_data_to_cat), axis=0)

        if pts_data.shape[0] > min_pts_size:
            point_cloud.write_xyz(file_path=pts_file, points=pts_data)
            np.save(pts_file_npy, pts_data)
Esempio n. 12
0
K = camera.K
print("\n# # Camera intrinsic")
print(K)
scene.show()

# render scene
from io import BytesIO
img = scene.save_image(resolution=(640, 480))
from PIL import Image
rendered = Image.open(BytesIO(img)).convert("RGB")
rendered.save("rendered.jpg")

# numpy version

vertices = colors
transformed = transform_points(vertices, transform)  # transformation
projected = np.matmul(transformed, K.T)  # homogeneous projection
xy = projected[:, :2] / projected[:, 2:]  # make non-homogeneous

print("\n# # Projected 2D points\n", xy)


def show(xy):
    import matplotlib.pyplot as plt
    fig, ax = plt.subplots(2, 1)
    x, y = xy.T
    # ax = plt.gca()
    ax[0].scatter(x, y, c=colors)
    ax[1].imshow(rendered)
    plt.show()
Esempio n. 13
0
    def on_mouse_double_click(self, x, y):
        res = self._scene.camera.resolution
        fov_y = np.radians(self._scene.camera.fov[1] / 2.0)
        fov_x = fov_y * (res[0] / float(res[1]))
        half_fov = np.stack([fov_x, fov_y])

        right_top = np.tan(half_fov)
        right_top *= 1 - (1.0 / res)
        left_bottom = -right_top

        right, top = right_top
        left, bottom = left_bottom

        xy_vec = tu.grid_linspace(bounds=[[left, top], [right, bottom]], count=res).astype(np.float64)
        pixels = tu.grid_linspace(bounds=[[0, 0], [res[0] - 1, res[1] - 1]], count=res).astype(np.int64)
        assert xy_vec.shape == pixels.shape

        transform = self._scene.camera_transform
        vectors = tu.unitize(np.column_stack((xy_vec, -np.ones_like(xy_vec[:, :1]))))
        vectors = tf.transform_points(vectors, transform, translate=False)
        origins = (np.ones_like(vectors) * tf.translation_from_matrix(transform))

        indices = np.where(np.all(pixels == np.array([x, y]), axis=1))
        if len(indices) > 0 and len(indices[0]) > 0:
            pixel_id = indices[0][0]
            ray_origin = np.expand_dims(origins[pixel_id], 0)
            ray_direction = np.expand_dims(vectors[pixel_id], 0)
            # print(x, y, pixel_id, ray_origin, ray_direction)

            mesh = self._scene.geometry['geometry_0']

            locations, index_ray, index_tri = mesh.ray.intersects_location(
                ray_origins=ray_origin,
                ray_directions=ray_direction)

            if locations.size == 0:
                return

            ray_origins = np.tile(ray_origin, [locations.shape[0], 1])
            distances = np.linalg.norm(locations - ray_origins, axis=1)
            idx = np.argsort(distances)  # sort by disctances

            # color closest hit
            tri_color = mesh.visual.face_colors[index_tri[idx[0]]]
            if not np.alltrue(tri_color == [255, 0, 0, 255]):
                tri_color = [255, 0, 0, 255]
            else:
                # unselect triangle
                tri_color = [200, 200, 200, 255]

            mesh.visual.face_colors[index_tri[idx[0]]] = tri_color

            # collect clicked triangle ids
            tri_ids = np.where(np.all(mesh.visual.face_colors == [255, 0, 0, 255], axis=-1))[0]

            if len(tri_ids) >= self._settings_loader.min_triangles:
                # get center of triangles
                barycentric = mesh.triangles_center[tri_ids]
                joint_x = np.mean(barycentric[:, 0])
                joint_y = np.mean(barycentric[:, 1])
                joint_z = np.mean(barycentric[:, 2])
                joint = np.stack([joint_x, joint_y, joint_z])

                if 'joint_0' in self._scene.geometry:
                    self._scene.delete_geometry('joint_0')

                joint = np.expand_dims(joint, 0)
                joint = PointCloud(joint, process=False)
                self._scene.add_geometry(joint, geom_name='joint_0')

            if self.view['rays']:
                from trimesh import load_path
                ray_visualize = load_path(np.hstack((ray_origin, ray_origin + ray_direction)).reshape(-1, 2, 3))
                self._scene.add_geometry(ray_visualize, geom_name='cam_rays')

                # draw path where camera ray hits with mesh (only take 2 closest hits)
                path = np.hstack(locations[:2]).reshape(-1, 2, 3)
                ray_visualize = load_path(path)
                self._scene.add_geometry(ray_visualize, geom_name='cam_rays_hits')
Esempio n. 14
0
def SplitByPlaneOneSide(mesh,
                        plane_normal,
                        plane_origin,
                        cap=False,
                        cached_dots=None,
                        reversed=False,
                        **kwargs):
    """
    Slice a mesh with a plane, returning a new mesh that is the
    portion of the original mesh to the positive normal side of the plane
    Parameters
    ---------
    mesh : Trimesh object
      Source mesh to slice
    plane_normal : (3,) float
      Normal vector of plane to intersect with mesh
    plane_origin :  (3,) float
      Point on plane to intersect with mesh
    cap : bool
      If True, cap the result with a triangulated polygon
    cached_dots : (n, 3) float
      If an external function has stored dot
      products pass them here to avoid recomputing
    kwargs : dict
      Passed to the newly created sliced mesh
    Returns
    ----------
    new_vertices : (n, 3) float
        Vertices of sliced mesh
    new_faces : (n, 3) int
        Faces of sliced mesh
    """
    # check input for none
    if mesh is None:
        return None

    # check input plane
    plane_normal = np.asanyarray(plane_normal, dtype=np.float64)
    plane_origin = np.asanyarray(plane_origin, dtype=np.float64)

    # check to make sure origins and normals have acceptable shape
    shape_ok = (
        (plane_origin.shape == (3, ) or util.is_shape(plane_origin,
                                                      (-1, 3))) and
        (plane_normal.shape == (3, ) or util.is_shape(plane_normal, (-1, 3)))
        and plane_origin.shape == plane_normal.shape)
    if not shape_ok:
        raise ValueError('plane origins and normals must be (n, 3)!')

    # start with copy of original mesh, faces, and vertices
    sliced_mesh = mesh.copy()
    vertices = mesh.vertices.copy()
    faces = mesh.faces.copy()

    # slice away specified planes
    for origin, normal in zip(plane_origin.reshape((-1, 3)),
                              plane_normal.reshape((-1, 3))):

        # calculate dots here if not passed in to save time
        # in case of cap
        if cached_dots is None:
            # dot product of each vertex with the plane normal indexed by face
            # so for each face the dot product of each vertex is a row
            # shape is the same as faces (n,3)
            dots = np.einsum('i,ij->j', normal, (vertices - origin).T)[faces]
        else:
            dots = cached_dots
        # save the new vertices and faces
        vertices, faces = intersections.slice_faces_plane(vertices=vertices,
                                                          faces=faces,
                                                          plane_normal=normal,
                                                          plane_origin=origin,
                                                          cached_dots=dots)

        # check if cap arg specified
        if cap:
            # check if mesh is watertight (can't cap if not)
            if not sliced_mesh.is_watertight:
                raise ValueError('Input mesh must be watertight to cap slice')
            path = sliced_mesh.section(plane_normal=normal,
                                       plane_origin=origin)
            if not path:
                if reversed:
                    return sliced_mesh
                else:
                    return None
            # transform Path3D onto XY plane for triangulation
            on_plane, to_3D = path.to_planar()

            # triangulate each closed region of 2D cap
            # without adding any new vertices
            v, f = [], []
            for polygon in on_plane.polygons_full:
                t = triangulate_polygon(polygon,
                                        triangle_args='pY',
                                        engine='triangle')
                v.append(t[0])
                f.append(t[1])

                if tol.strict:
                    # in unit tests make sure that our triangulation didn't
                    # insert any new vertices which would break watertightness
                    from scipy.spatial import cKDTree
                    # get all interior and exterior points on tree
                    check = [np.array(polygon.exterior.coords)]
                    check.extend(np.array(i.coords) for i in polygon.interiors)
                    tree = cKDTree(np.vstack(check))
                    # every new vertex should be on an old vertex
                    assert np.allclose(tree.query(v[-1])[0], 0.0)

            # append regions and reindex
            vf, ff = util.append_faces(v, f)

            # make vertices 3D and transform back to mesh frame
            vf = tf.transform_points(np.column_stack((vf, np.zeros(len(vf)))),
                                     to_3D)

            # check to see if our new faces are aligned with our normal
            #check = windings_aligned(vf[ff], normal)
            #
            ## if 50% of our new faces are aligned with the normal flip
            #if check.astype(np.float64).mean() > 0.5:
            #    ff = np.fliplr(ff)

            # add cap vertices and faces and reindex
            vertices, faces = util.append_faces([vertices, vf], [faces, ff])

            # Update mesh with cap (processing needed to merge vertices)
            sliced_mesh = Trimesh(vertices=vertices, faces=faces)
            vertices, faces = sliced_mesh.vertices.copy(
            ), sliced_mesh.faces.copy()

    return sliced_mesh