Surfaces

A surface (also referred to as a triangulation), contains points and triangular 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
project = Project() # Connect to default project

path = "/surfaces/tetra"

with project.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 between Surface objects and 00t files, see Built-In 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

project = 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 = project.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 object including primitive counts and extents.

from mapteksdk.project import Project
from mapteksdk.data import Surface

project = Project() # Connect to default project
# Where you stored the surface:
path = "/surfaces/pit_model"
found = project.find_object(path)
if found and found.is_a(Surface):
    with project.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 point set

Converting a Surface object into a PointSet object involves copying the points array, and optionally the point_colours array.

from mapteksdk.project import Project
from mapteksdk.data import Surface, PointSet

project = Project() # Connect to default project
# Where you stored the surface:
path = "/surfaces/pit_model"
output = path + " (points)"
found = project.find_object(path)
if found and found.is_a(Surface):
    with project.read(path) as surface:
        with project.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 edge network

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

project = Project() # Connect to default project
# Where you stored the surface:
path = "/surfaces/pit_model"
output = path + " (wireframe)"
found = project.find_object(path)
if found and found.is_a(Surface):
    with project.read(path) as surface:
        with project.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, translating, or scaling a surface

Rotating, translating, or scaling a Surface can be performed exactly the same way as rotating, translating, or scaling a PointSet. See the following topics for examples:

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__":
    project = 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 = project.find_object(path)
    if found and found.is_a(Surface):
        with project.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 an 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 Maptek object file containing a single surface to apply to the marker. It can be downloaded here: PythonSDK_SampleData_Truck.maptekobj.
More comprehensive examples for importing Maptek object files (.maptekobj) are provided in Built-In Input/Output.

from mapteksdk.project import Project
from mapteksdk.data import io
import trimesh
project = 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
project.rename_object(imported_data, path, overwrite=True)

with project.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 an 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 Surface
import trimesh
project = 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 project.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
project = 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 project.get_selected():
    if item.is_a(Surface):
        with project.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]

Colouring a surface with two-sided colouring

The Surface.back_colour property allows surfaces to be coloured differently on the front and back. The front is coloured using the point or facet colours (depending on which are defined) and the back is coloured uniformly using the back colour. The following example script demonstrates creating a surface with two-sided colouring:

from mapteksdk.project import Project
from mapteksdk.data import Surface
from mapteksdk.operations import open_new_view

if __name__ == "__main__":
    width = 5.0
    height = 2.5
    with Project() as project:
        with project.new("surfaces/two_sided_open_box", Surface) as surface:
            surface.points = [
                [-width, -width, 0], [-width, width, 0],
                [width, width, 0], [width, -width, 0],
                [-width, -width, height], [-width, width, height],
                [width, width, height], [width, -width, height],
            ]
            surface.facets = [
                [1, 2, 0], [2, 3, 0], # Bottom.
                [7, 4, 0], [3, 7, 0], # Front.
                [4, 1, 0], [4, 5, 1], # Left.
                [5, 2, 1], [5, 6, 2], # Back.
                [6, 3, 2], [7, 3, 6]  # Right.
            ]

            surface.facet_colours = [
                [255, 0, 0, 255], [255, 0, 0, 255],
                [0, 255, 0, 255], [0, 255, 0, 255],
                [0, 0, 255, 255], [0, 0, 255, 255],
                [255, 255, 0, 255], [255, 255, 0, 255],
                [255, 0, 255, 255], [255, 0, 255, 255],
            ]

            # Set the back of each facet to be grey.
            surface.back_colour = [127, 127, 127]
            open_new_view(surface.id)

The above script creates the following surface:

Note
  • The back colour does not have an alpha component.

  • The back colour is the same for all facets, regardless of how the surface is coloured on the front.

  • The front and back of a surface is determined by the normals of the facet.

  • If a surface has inconsistent normals, then what is the front and what is the back of a facet will be inconsistent across the surface. This will cause inconsistent colouring on the surface.

  • The fix surface tool (e.g. fix_surface()) can be used to resolve this issue.

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
project = Project() # Connect to default project

# For all surfaces in selection that have selected facets:
for item in project.get_selected():
    if item.is_a(Surface):
        with project.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
project = Project() # Connect to default project

# For all surfaces in selection that have selected facets:
for item in project.get_selected():
    if item.is_a(Surface):
        # Prepare path to store new object:
        new_path = item.path + "_selection"
        with project.edit(item) as surface:
            with project.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.

The Maptek Python SDK supports directly applying colour maps to points, but not edges or facets.

from mapteksdk.project import Project
from mapteksdk.data import Surface
import numpy as np
project = Project() # Connect to default project

# Where you stored the surface:
path = "/surfaces/pit_model"

found = project.find_object(path)
if found and found.is_a(Surface):
    with project.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 such that each facet is 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
project = Project() # Connect to default project

path = "/surfaces/pit_model"
if project.find_object(path):
    with project.read(path) as surface:
       with project.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 creates 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 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

project = Project() # Connect to default project
with project.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
import numpy as np
from scipy.spatial import Delaunay
project = 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 project.get_selected():
    with project.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 project.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
project = 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 project.new(path_prefix + "_points", PointSet, overwrite=True) as pts:
    intersect_points += points
    pts.points = intersect_points
# Store wireframe of the shape
with project.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 project.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