Surface
A Surface
(also referred to as a triangulation), contains points and facets (or faces) and edges that form the boundary of each facet.
Some examples in this page use libraries not installed by default.
Libraries referenced:
Surface examples
Creating a surface
This example will demonstrates basic Surface
construction by making a tetrahedron.
from mapteksdk.data import Surface from mapteksdk.project import Project proj = Project() # Connect to default project path = "/surfaces/tetra" with proj.new(path, Surface, overwrite=True) as tetra: size = 3 tetra.points = [[-size, -size, -size], # [x, y, z] [-size, size, size], [size, -size, size], [size, size, -size]] tetra.facets = [[0, 1, 2], [0, 2, 3], [0, 1, 3], [1, 2, 3]] # [i1, i2, i3] tetra.facet_colours = [[255, 0, 0, 255], # Red [R, G, B, A] [0, 255, 0, 255], # Blue [0, 0, 255, 255], # Green [255, 255, 255, 255]] # White
Creating a surface from a 00t file
This example will import data to use for the rest of the examples, starting with a Vulcan 00t triangulation. For more examples on converting Surface
s to/from 00t files, see Built-in IO (Input/Output).
Note: The data used in this and the following examples can be downloaded here: pit_model.00t
from mapteksdk.project import Project from mapteksdk.data import io, Surface proj = Project() # Connect to default project # The file path you have exported the sample data to: file_path = "F:\\Python SDK Help\\data\\pit_model.00t" # Import the 00t: imported_surface = io.import_00t(file_path) save_as = "/surfaces/pit_model" if imported_surface: # Successful imported, now assign a path # After add_object, set imported_surface as the new ObjectID: imported_surface = proj.add_object(save_as, imported_surface, overwrite=True) else: print("{} failed to import.".format(file_path))
Getting information about a surface
This example returns basic information about the Surface
including primitive counts and extents.
from mapteksdk.project import Project from mapteksdk.data import io, Surface proj = Project() # Connect to default project # Where you stored the surface: path = "/surfaces/pit_model" found = proj.find_object(path) if found and found.is_a(Surface): with proj.read(path) as surface: print("Name: {}\nPath: {}\nParent: {}".format( found.name, found.path, found.parent.path)) print("Number of points: {}\nNumber of edges: {}\nNumber of facets: {}".format( surface.point_count, surface.edge_count, surface.facet_count )) extent = surface.extent print("Extent - Minimum: X: {}, Y: {}, Z: {}".format( extent.minimum[0], extent.minimum[1], extent.minimum[2] )) print("Extent - Maximum: X: {}, Y: {}, Z: {}".format( extent.maximum[0], extent.maximum[1], extent.maximum[2] )) print("Centre: X: {}, Y: {}, Z: {}".format( extent.centre[0], extent.centre[1], extent.centre[2] )) print("Maximum length between points: {}".format( extent.length )) else: print("Please import Surface to '{}' first.".format(path)) # Expected output: # >>> Name: pit_model # >>> Path: /surfaces/pit_model # >>> Parent: /surfaces # >>> Number of points: 64268 # >>> Number of edges: 192775 # >>> Number of facets: 128508 # >>> Extent - Minimum: X: 939.0660536488951, Y: 1654.8985445524079, Z: 284.4358990415397 # >>> Extent - Maximum: X: 1425.4774194945908, Y: 2208.9500778135116, Z: 384.14591444660067 # >>> Centre: X: 1182.271736571743, Y: 1931.9243111829596, Z: 334.2909067440702 # >>> Maximum length between points: 554.0515332611037
Converting a Surface into a PointSet
Converting a Surface to a PointSet
involves copying the points array and optionally point_colours
array.
from mapteksdk.project import Project from mapteksdk.data import Surface, PointSet proj = Project() # Connect to default project # Where you stored the surface: path = "/surfaces/pit_model" output = path + " (points)" found = proj.find_object(path) if found and found.is_a(Surface): with proj.read(path) as surface: with proj.new(output, PointSet, overwrite=True) as points: points.points, points.point_colours = surface.points, surface.point_colours else: print("Please import Surface to '{}' first.".format(path))
Converting a Surface into a an EdgeNetwork
A surface can be converted into a wireframe representation using an EdgeNetwork
. This involves copying the points and edges arrays and optionally the point_colours
array.
from mapteksdk.project import Project from mapteksdk.data import Surface, EdgeNetwork proj = Project() # Connect to default project # Where you stored the surface: path = "/surfaces/pit_model" output = path + " (wireframe)" found = proj.find_object(path) if found and found.is_a(Surface): with proj.read(path) as surface: with proj.new(output, EdgeNetwork, overwrite=True) as edges: edges.points = surface.points edges.point_colours = surface.point_colours edges.edges = surface.edges else: print("Please import Surface to '{}' first.".format(path))
Rotating a surface
Rotating a Surface
can be performed exactly the same way as rotating a PointSet
, demonstrated here: Rotating a point set.
Translating a surface
Translating a Surface
can be performed exactly the same way as translating a PointSet
, demonstrated here: Translating a point set
Scaling a surface
Scaling a Surface
can be performed exactly the same way as scaling a PointSet
, demonstrated here: Scaling a point set
Determining the elevation of a coordinate on a surface
This example demonstrates how to find the height on a Surface
that a point will intersect with it vertically.
from mapteksdk.project import Project from mapteksdk.data import Surface import numpy as np def find_intersection(triangles, point): """ From an array of triangles, find which one the point of interest intersects """ p1, p2, p3 = triangles[:, 0, :], triangles[:, 1, :], triangles[:, 2, :] v1 = p3 - p1 v2 = p2 - p1 cp = np.cross(v1, v2) a, b, c = cp[:, 0], cp[:, 1], cp[:, 2] # Create an array of the same point for the length of the triangles array # This is to allow vectorisation of the intersection calculations point_as_array = np.empty([triangles.shape[0], 3], dtype=np.float) point_as_array[:] = point # Calculate intersection height at each facet d = np.einsum("ij,ij->i", cp, p3) intersect_height = (d - a * point_as_array[:,0] - b * point_as_array[:, 1]) / c # Determine which triangles the point sits inside of Area = 0.5 *(-p2[:,1]*p3[:,0] + p1[:,1]*(-p2[:,0] + p3[:,0]) + p1[:,0]*(p2[:,1] - p3[:,1]) + p2[:,0]*p3[:,1]) s = 1/(2*Area)*(p1[:,1]*p3[:,0] - p1[:,0]*p3[:,1] + (p3[:,1] - p1[:,1])*point[0] + (p1[:,0] - p3[:,0])*point[1]) t = 1/(2*Area)*(p1[:,0]*p2[:,1] - p1[:,1]*p2[:,0] + (p1[:,1] - p2[:,1])*point[0] + (p2[:,0] - p1[:,0])*point[1]) possible_intersections = np.where(((s>=0) & (s<=1)) & ((t>=0) & (t<=1)) & (1-s-t > 0)) if len(possible_intersections[0]) > 0: return intersect_height[possible_intersections[0]][0] return None if __name__ == "__main__": proj = Project() # Connect to default project # Where you stored the surface: path = "/surfaces/pit_model" # The horizontal coordinate to determine intersection height on the surface intersect_at = np.array([1165, 1985, 0]) found = proj.find_object(path) if found and found.is_a(Surface): with proj.read(path) as surface: # Generate array of 'triangles' [[[x1,y1,z1],[x2,y2,z2],[x3,y3,z3]],] triangles = surface.points[surface.facets] # Query the intersection height (if any) result = find_intersection(triangles, intersect_at) if result: print("Intersected X: {}, Y: {}, with height: {}".format(intersect_at[0], intersect_at[1], result)) else: print("Couldn't find any facets to intersect with.") else: print("Please import Surface to '{}' first.".format(path)) # Example output: # Intersected X: 1165, Y: 1985, with height: 289.4469219564403
Converting a surface to a STL file
This example demonstrates converting a Surface
object to an STL file.
Important! This makes use of the trimesh library which is not included by default with Python. You may need to install it before trying the script.
Note: The following example imports a maptekobj file containing a single surface to apply to the marker. It can be downloaded here: PythonSDK_SampleData_Truck.maptekobj
More comprehensive examples for importing maptekobj files are provided in Built-in IO (Input/Output)
from mapteksdk.project import Project from mapteksdk.data import io, Surface import trimesh proj = Project() # Connect to default project # Use the path you have saved the example maptekobj file: maptekobj_path = "F:\\Python SDK Help\\data\\PythonSDK_SampleData_Truck.maptekobj" export_path = "F:\\Python SDK Help\\data\\Truck.stl" # Import the data imported_data = io.import_maptekobj(maptekobj_path) path = "surfaces/truck" # Assign a path to the surface proj.rename_object(imported_data, path, overwrite=True) with proj.read(path) as surface: # Convert the Surface to trimesh output = trimesh.Trimesh(vertices=surface.points, faces=surface.facets) # Convert to binary representation of stl output_bytes = trimesh.exchange.stl.export_stl(output) # Save to stl file with open(export_path, 'wb') as f: f.write(output_bytes)
Creating a surface from a STL file
Using the STL file created in the previous example, the following example will import the STL data to create a Surface
.
Important! This makes use of the trimesh library which is not included by default with Python. You may need to install it before trying the script.
from mapteksdk.project import Project from mapteksdk.data import io, Surface import trimesh proj = Project() # Connect to default project # Path to your stl file: import_path = "F:\\Python SDK Help\\data\\Truck.stl" mesh = trimesh.load_mesh(import_path) with proj.new("surfaces/imported stl", Surface, overwrite=True) as surface: # Copy the points and facets from the trimesh object surface.points, surface.facets = mesh.vertices, mesh.faces
Conditionally colouring a facet selection
This example demonstrates iterating over selected facet primitives, calculating dip angle of each facet and highlighting those with a dip greater than 70 degrees by colouring them red.
from mapteksdk.project import Project from mapteksdk.data import Surface import numpy as np import math proj = Project() # Connect to default project def calculate_dip(p1, p2, p3): """ Calculate dip angle of a facet, where p1, p2, p3 are the 3d points it is created from """ AC = p1 - p3 AB = p1 - p2 ui, uj, uk = np.cross(AC, AB) return abs(math.degrees(math.asin(math.sqrt((ui**2 + uj**2)) / math.sqrt(ui**2 + uj**2 + uk**2)))) # For all surfaces in selection that have selected facets: for item in proj.get_selected(): if item.is_a(Surface): with proj.edit(item) as surface: # Get list of indices of selected facets selection = np.where(surface.facet_selection) # Iterate the indices as i for i in np.nditer(selection): # If the dip is >70 degres, colour the facet red # surface.facets[i] -> [i1,i2,i3] # surface.points[surface.facets[i][0]] -> point[i1] -> p1 [x, y, z] if calculate_dip(surface.points[surface.facets[i][0]], surface.points[surface.facets[i][1]], surface.points[surface.facets[i][2]]) > 70: surface.facet_colours[i] = [255, 0, 0, 255]
Deleting a facet selection
This example demonstrates efficient removal of facets from a Surface
that have been selected in the View.
from mapteksdk.project import Project from mapteksdk.data import Surface import numpy as np proj = Project() # Connect to default project # For all surfaces in selection that have selected facets: for item in proj.get_selected(): if item.is_a(Surface): with proj.edit(item) as surface: # Get list of indices of selected facets selection = np.where(surface.facet_selection) # Remove selected facet indices surface.remove_facets(selection) # Clear selection surface.facet_selection = [False]
Creating a new surface from a facet selection
This example demonstrates how to copy selected primitives from an object, however will create a new object for each object selected if there are multiple. Other techniques can be used to merge multiple object primitive selections into a single new object.
from mapteksdk.project import Project from mapteksdk.data import Surface import numpy as np proj = Project() # Connect to default project # For all surfaces in selection that have selected facets: for item in proj.get_selected(): if item.is_a(Surface): # Prepare path to store new object: new_path = item.path + "_selection" with proj.edit(item) as surface: with proj.new(new_path, Surface, overwrite=True) as surface_new: # Orphaned points will be removed automatically surface_new.points = surface.points surface_new.point_colours = surface.point_colours # Copy only the selected facets and any colour information surface_new.facets = surface.facets[surface.facet_selection] surface_new.facet_colours = surface.facet_colours[surface.facet_selection]
Create facet primitive attributes
This example will demonstrate applying a 'dip' attribute to each facet on the surface which can be referenced by the Custom Colour tool in the software or used in conjunction with colour maps.
Currently the SDK supports directly applying colour maps to points, but not edges or facets.
from mapteksdk.project import Project from mapteksdk.data import Surface, PointSet import numpy as np proj = Project() # Connect to default project # Where you stored the surface: path = "/surfaces/pit_model" found = proj.find_object(path) if found and found.is_a(Surface): with proj.edit(path) as surface: # Below is a vectorised calculation for dip angle for each facet # Generate array of 'triangles' [[[x1,y1,z1],[x2,y2,z2],[x3,y3,z3]],] triangles = surface.points[surface.facets] p1, p2, p3 = triangles[:, 0, :], triangles[:, 1, :], triangles[:, 2, :] AC = p1 - p3 AB = p1 - p2 cp = np.cross(AC, AB) a, b, c = cp[:, 0], cp[:, 1], cp[:, 2] dip_results = np.abs(np.degrees(np.arcsin(np.sqrt((a**2 + b**2)) / np.sqrt((a**2 + b**2 + c**2))))) # Save the dip values in an attribute "dip" for the software to refer to surface.save_facet_attribute("dip", dip_results) else: print("Please import Surface to '{}' first.".format(path))
Subdividing a surface
This example shows how to subdivide a Surface
where each facet will be replaced with four smaller facets.
Important! This makes use of the trimesh library which is not included by default with Python. You may need to install it before trying the script.
from mapteksdk.project import Project from mapteksdk.data import Surface import trimesh.remesh proj = Project() # Connect to default project path = "/surfaces/pit_model" if proj.find_object(path): with proj.read(path) as surface: with proj.new(path + " divided", Surface, overwrite=True) as new_surface: new_surface.points, new_surface.facets = trimesh.remesh.subdivide(surface.points, surface.facets)
Creating a Delaunay surface from a CSV file
The following is a basic example that imports a CSV (format: X, Y, Z, R, G, B), triangulates it and makes a Surface object.
Note: The data used in this and the following examples can be downloaded here: pointset_3d.csv
Important! This makes use of the scipy.spatial.Delaunay library which may not be included by default with Python. You may need to install it before trying the script.
from mapteksdk.project import Project from mapteksdk.data import io, Surface from numpy import genfromtxt from scipy.spatial import Delaunay import numpy as np # The file path you have exported the sample data to: file_path = "F:\\Python SDK Help\\data\\pointset_3d.csv" # Read the CSV using numpy raw_data = genfromtxt(file_path, delimiter=',') # [x,y,z,r,g,b] # Extract the points and colours into separate arrays points = raw_data[:,[0, 1, 2]] # x,y,z colours = raw_data[:,[3, 4, 5]] # r,g,b # Attach a column of same length of 255 to populate alpha colour alpha = np.empty([colours.shape[0], 1]) alpha.fill(255) colours = np.column_stack((colours, alpha)) # r,g,b,a # Compute triangulation - Only x, y needed for delaunay triangulation tri = Delaunay(points[:, :2]) # x, y proj = Project() # Connect to default project with proj.new("/surfaces/imported csv", Surface, overwrite=True) as surface: surface.points = points surface.point_colours = colours surface.facets = tri.simplices
Creating a surface from visible points in selected objects
This example builds on examples provided in the example Creating a point set, creating a Delaunay triangulation using the scipy.spatial.Delaunay module.
Important! This makes use of the scipy.spatial.Delaunay library which may not be included by default with Python. You may need to install it before trying the script.
from mapteksdk.project import Project from mapteksdk.data import Surface from mapteksdk.data.points import PointProperties import numpy as np from scipy.spatial import Delaunay proj = Project() # Connect to default project save_as = "scrapbook/create_surface_example" # Temporarily store points and colours in these arrays all_points = np.empty([0,3], dtype=np.float64) all_colours = np.empty([0,4], dtype=np.uint8) for item in proj.get_selected(): with proj.read(item) as obj: if not hasattr(obj, 'points'): continue # Append visible points from obj to buffer object # Use .point_selection to get indices for all True values from the selection array all_points = np.vstack((all_points, obj.points[obj.point_visibility])) all_colours = np.vstack((all_colours, obj.point_colours[obj.point_visibility])) if len(all_points) > 3: with proj.new(save_as, Surface, overwrite=True) as surface: surface.points = all_points surface.point_colours = all_colours print("Calculate triangulation of {} points".format(len(all_points))) # for scipy.spatial.Delaunay we want [[x,y],[x,y],etc] x_y_array = all_points[:, :2] # x, y (:2) columns of take all points (:) tri = Delaunay(x_y_array) # Store facet indices: surface.facets = tri.simplices else: print("Not enough points in selected objects to triangulate.")
Creating a hyperbolic paraboloid
This example generates a hyperbolic paraboloid and outputs a representation of it as a PointSet
, EdgeNetwork
(wireframe) and Surface
, using four input boundary points. Smoothness of the result is relative to the number of segments each line is divided by. The example also illustrates a way of subdividing a line into multiple segments.
The example also illustrates how the SDK will handle cases where a list type may be used for convenience instead of a numpy array for providing coordinates and edge arrays.
from mapteksdk.project import Project from mapteksdk.data import Surface, EdgeNetwork, PointSet from scipy.spatial import Delaunay import numpy as np proj = Project() # Connect to default project def subdivide_line(p1, p2, segments): """ Divide a line between p1 and p2 into a number of segments """ x_delta = (p2[0] - p1[0]) / float(segments) y_delta = (p2[1] - p1[1]) / float(segments) z_delta = (p2[2] - p1[2]) / float(segments) points = [] for i in range(1, segments): points.append([p1[0] + i * x_delta, p1[1] + i * y_delta, p1[2] + i * z_delta]) return [p1] + points + [p2] path_prefix = "/surfaces/hyperbolic paraboloid" NW = [0, 35, 17] # NW point SW = [0, 0, 2] # SW point SE = [15, 0, 13] # SE point NE = [15, 35, 5] # NE point segments = 40 # Number of segments to subdivide each line # Create set of points alone the outline of the shape NW_NE = subdivide_line(NW, NE, segments) SW_SE = subdivide_line(SW, SE, segments) NW_SW = subdivide_line(NW, SW, segments) NE_SE = subdivide_line(NE, SE, segments) # Temporary storage for points and edge joins points = [] edges = [] # Temporary storage for all intersection points intersect_points = [] for i in range(len(NW_NE)): # NW -> NE joins to SW -> SE points.append(NW_NE[i]) points.append(SW_SE[i]) edges.append([len(points)-1, len(points)-2]) # NW -> SW joins to NE -> SE points.append(NW_SW[i]) points.append(NE_SE[i]) edges.append([len(points)-1, len(points)-2]) # Generate points for all intersections along these lines intersect_points += subdivide_line(SW_SE[i], NW_NE[i], segments) intersect_points += subdivide_line(NE_SE[i], NW_SW[i], segments) # Store all the bounary and intersection points with proj.new(path_prefix + "_points", PointSet, overwrite=True) as pts: intersect_points += points pts.points = intersect_points # Store wireframe of the shape with proj.new(path_prefix + "_edges", EdgeNetwork, overwrite=True) as lines: lines.points = points lines.edges = edges # Generate a surface of the shape using the points with proj.new(path_prefix + "_surface", Surface, overwrite=True) as sfc: # Convert the list() of points to a numpy array of [X, Y] for the Delaunay function tri = Delaunay(np.array(intersect_points)[:, :2]) sfc.points = intersect_points sfc.facets = tri.simplices