Colour Maps

Colour maps, also referred to as legends in the software, associate colours with numeric ranges or string values. Colour maps can be applied to objects of most spatial data types to highlight physical trends or attributes. You can use the Maptek Python SDK to access legends created in the software, or programmatically create colour maps that can be used in scripts, or made available to users of the software. An example of doing this is provided in the Create facet primitive attributes example.

Colour map examples

Creating a string colour map

In the following code, we:

  • Create a string colour map

  • Create a dense block model

  • Apply the string colour map to the dense block model using block attributes

"""Example 1: Colouring a dense block model using a string colour map."""

from mapteksdk.project import Project
from mapteksdk.data import StringColourMap, DenseBlockModel

project = Project()

with project.new_or_edit("legends/string_colour_map", StringColourMap) as string_map:
  # The names associated with the colours in the colour map. Note that these names are case-sensitive.
  # ie: red is valid, but Red is an invalid.
  string_map.legend = ["red", "green", "blue", "yellow", "cyan", "magenta", "grey", "white"]
  string_map.colours = [[255, 0, 0], # red
                     [0, 255, 0], # green
                     [0, 0, 255], # blue
                     [255, 255, 0], # yellow
                     [0, 255, 255], # cyan
                     [255, 0, 255], # magenta
                     [100, 100, 100], # black
                     [255, 255, 255]] # gray

  # cutoff specifies how to colour invalid values. In this case invalid values will be coloured
  # transparent.
  string_map.cutoff = [0, 0, 0, 0]

# Now use the colour map to colour a block model.
with project.new("blockmodels/coloured_block_model", DenseBlockModel(
    x_res=1, y_res=1, z_res=1, x_count=3, y_count=3, z_count=1
    )) as coloured_model:
  # First a block attribute must be created to colour by.
  # Note that the block in the middle has an invalid value so will be invisible.
  coloured_model.block_attributes["colours"] = [
    "red", "green", "blue",
    "yellow", "invalid", "cyan",
    "magenta", "grey", "white",
    ]
  # Now that the attribute has been added, we can assign the colour map to it.
  coloured_model.block_attributes.set_colour_map("colours", string_map)

If you run this code, you will create a string colour map and a coloured dense block model that look like the following:

Creating a numeric colour map

In the following code, we:

  • Create a solid numeric colour map

  • Create a dense block model

  • Apply the solid numeric colour map to the dense block model

"""Example 2: Colouring a dense block model using a solid numeric colour map.

There are two types of numeric colour map - solid and interpolated. They are
differentiated by the interpolated property of the colour map.

"""
from mapteksdk.project import Project
from mapteksdk.data import NumericColourMap, DenseBlockModel

project = Project()

with project.new_or_edit("legends/numeric_map_exp", NumericColourMap) as solid_map:
  solid_map.interpolated = False
  # For non-interpolated (solid) colour maps there is one less colour than range.
  solid_map.ranges = [0, 10, 20, 30, 40, 50, 60, 70]
  solid_map.colours = [[255, 0, 0], # 0 < value <= 10 are coloured this colour (Red).
                       [0, 255, 0], # 10 < value <= 20 are coloured this colour (Green).
                       [0, 0, 255], # 20 < value < 30 are coloured this colour (Blue).
                       [255,50,100], # 30
                       [250,255,40], # 40
                       [100, 30, 255], #50
                       [45, 45, 45],  #60
                      ]

  # Colours below zero are coloured this colour. In this case it is semi-transparent red.
  solid_map.lower_cutoff = [255, 0, 0, 100]

  # Colours greater than the highest range, in this case it is semi-transparent blue.
  solid_map.upper_cutoff = [0, 0, 255, 100]

with project.new("blockmodels/dense_solid_colours", DenseBlockModel(
    x_count=5, y_count=1, z_count=1, x_res=1, y_res=1, z_res=1
    ), overwrite=True) as solid_model:
  solid_model.block_attributes["solid_colour"] = [-5, 5, 15, 25, 35]
  solid_model.block_attributes.set_colour_map("solid_colour", solid_map)

If you run the following code, you will create a solid numeric colour map and dense block model that look like this:

Applying a colour map to an object

In the following code, we:

  • Create an interpolated numeric colour map

  • Create a dense block model

  • Apply the interpolated numeric colour map to the dense block model

"""Example 4: Colouring a block model using a numeric colour map."""

from mapteksdk.project import Project
from mapteksdk.data import NumericColourMap, DenseBlockModel

project = Project()

with project.new_or_edit("legends/interpolated_map", NumericColourMap) as interpolated_map:
  interpolated_map.interpolated = True
  # For interpolated colour maps the number of colours is equal to the number of ranges.
  interpolated_map.ranges = [0, 10, 20]
  interpolated_map.colours = [[255, 0, 0],
                              # Colours between 0 and 10 will smoothly transition from the colour above (red)
                              # to the colour below (green)
                              [0, 255, 0],
                              # Colours between 10 and 20 will smoothly transition from the colour above (green)
                              # to the colour below (blue)
                              [0, 0, 255],
                             ]

  # Colours below zero are coloured this colour. In this case it is semi-transparent red.
  interpolated_map.lower_cutoff = [255, 0, 0, 100]

  # Colours greater than the highest range, in this case it is semi-transparent blue.
  interpolated_map.upper_cutoff = [0, 0, 255, 100]

# Use the colour map to colour a blockmodel.
with project.new("blockmodels/dense_interpolated_colours", DenseBlockModel(
    x_count=11, y_count=1, z_count=1, x_res=1, y_res=1, z_res=1
    ), overwrite=True) as interpolated_model:
  # Create a block attribute to colour by.
  # From left to right when viewing from above:
  # The first block's value is -1 which is below the minimum so is coloured by lower_offcut.
  # As the value increases from 2 to 10 the blocks get progressively more green.
  # As the value increases from 10 to 18 the blocks get progressively more blue.
  # And finally the last block's value is 22 which is above the maximum so is coloured by upper_offcut.
  interpolated_model.block_attributes["interpolated_colour"] = [-1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 22]
  interpolated_model.block_attributes.set_colour_map("interpolated_colour", interpolated_map)

If you run the following code, you will create an interpolated NumericColourMap and DenseBlockModel that look like this:

Getting colours from a NumericColourMap

Consider the following colour map:

It transitions from red at -20 to yellow at -10, then to green at 0, then to cyan at 10 and finally to blue at 20. But given this, how can you know what colour it would give at 19.5? Or at 0.55? How does the application determine what colour should be given a specific value?

Rather than worry about the specific mathematics of colour interpolation, it is significantly easier to use the NumericColourMap.colours_for() function as demonstrated in this snippet:

def colours_for(project: Project, colour_map_id: ObjectID[NumericColourMap]):
    with project.read(colour_map_id) as colour_map:
        # It accepts a list of values to allow for multiple to be queried at once.
        values = [19.5, 0.55]
        results = colour_map.colours_for([19.5, 0.55])
        for value, colour in zip(values, results):
            print(f"{value} : {colour}")

Output with the colour map shown above:

19.5 : [  0  12 255 255]
0.55 : [  0 255  14 255]

The NumericColourMap.colours_for() function accepts a sequence of numbers and returns the corresponding colours from the ColourMap, taking into account the interpolation and the upper and lower cutoff.

This can be useful when exporting to a format that doesn’t support colour maps or legends.

Important:  If the colour map has a unit (e.g. radians, degrees, metres, or yards) then the values passed to colours_for() are assumed to be in SI units (radians for angles, metres for distance, etc.) rather than the displayed unit.

Example: reading point colours using the associated colour map

The following snippet demonstrates how to determine what the point colours would be if an object was coloured by the given point attribute:

colour_map.colours_for(
    data_object.point_attributes[attribute_name]
)

Given the following:

the following script makes use of the above code snippet to extract coloured points from the picked object using the colour map if the object is coloured using a colour map and the point colours array if it is not coloured by a colour map.

import os


import numpy as np
from mapteksdk.project import Project, OverwriteMode
from mapteksdk.data import PointSet, NumericColourMap, StringColourMap, ObjectID
from mapteksdk.operations import (
    object_pick,
    active_view,
    show_message,
    show_toast_notification,
    Severity,
)


SCRIPT_NAME = os.path.basename(__file__)




def get_point_colours_from_colour_map(project: Project, oid: ObjectID) -> np.ndarray:
    """Read the point colours of the object.


    Unlike accessing the point colours directly, this will return correct
    values even for objects which are coloured by colour maps.


    Parameters
    ----------
    project
      Project to use to read the object.
    oid
      ObjectID of the object to read the colours of.


    Returns
    -------
    np.ndarray
      Array of the colours for each point.


    Raises
    ------
    ValueError
      If the object does not have point colours, or is using an unsupported
      colour map.
    """
    with project.read(oid) as data_object:
        colour_map_id = data_object.get_colour_map()
        if colour_map_id is None:
            # The object is not coloured via a colour map.
            try:
                return data_object.point_colours
            except AttributeError:
                raise ValueError("The object does not have point colours.") from None
        colour_map_attribute = None
        try:
            colour_map_attribute = data_object.point_attributes.colour_map_attribute
        except AttributeError:
            # The point_attributes property was missing.
            raise ValueError("The object does not have point colours.") from None
        if colour_map_attribute is None:
            # The object has a colour map, but it is not associated with the
            # point colours.
            try:
                return data_object.point_colours
            except AttributeError:
                raise ValueError("The object does not have point colours.") from None
        with project.read(data_object.get_colour_map()) as colour_map:
            if isinstance(colour_map, (NumericColourMap, StringColourMap)):
                return colour_map.colours_for(
                    data_object.point_attributes[colour_map_attribute]
                )
            raise ValueError(f"Unsupported colour map type: {colour_map.id.type_name}")




def extract_coloured_points(
    project: Project, object_id: ObjectID
) -> ObjectID[PointSet]:
    """Extract coloured points from another object.


    This will read the point colours from numeric colour maps associated with
    an object if one is available.


    Parameters
    ----------
    project
      Project to use to read the existing object and create the new object.
    object_id
      Existing object to read the points and point colours from.


    Raises
    ------
    ValueError
      If the existing object does not have points.
    """
    baked_point_colours = get_point_colours_from_colour_map(project, object_id)
    with project.new(
        f"{object_id.path}_coloured_points",
        PointSet,
        overwrite=OverwriteMode.UNIQUE_NAME,
    ) as point_set:
        with project.read(object_id) as read_object:
            point_set.points = read_object.points
            point_set.point_colours = baked_point_colours
    return point_set.id




def main(project: Project):
    """Allow the user to pick an object and then extract coloured points.


    Parameters
    ----------
    project
      Project to use for the object pick and creating or reading objects.
    """
    oid = object_pick(label="Pick an object to extract coloured points from.")
    view = active_view()
    result_oid = extract_coloured_points(project, oid)
    # Remove the picked object and replace it with the extracted points.
    if view is not None:
        view.add_object(result_oid)
        view.remove_object(oid)




if __name__ == "__main__":
    with Project() as main_project:
        try:
            with main_project.undo():
                main(main_project)
                show_toast_notification(SCRIPT_NAME, "Completed")
        except Exception as error:
            message = f"{type(error).__name__}: {str(error)}"
            show_message(SCRIPT_NAME, message, Severity.ERROR)
            raise

The following animation demonstrates running the above script on a simple cube coloured via its point colours:

Example: baked colour by facet attribute

The following snippet demonstrates how to colour a surface via a facet attribute using a numeric colour map without associating that colour map with the object:

surface.facet_colours = colour_map.colours_for(
    data_object.facet_attributes[attribute_name])

The following script uses this to colour a surface via a facet area attribute which the script also calculates:

import os


import numpy as np
from mapteksdk.project import Project, OverwriteMode
from mapteksdk.data import PointSet, NumericColourMap, StringColourMap, ObjectID
from mapteksdk.operations import (
    object_pick,
    active_view,
    show_message,
    show_toast_notification,
    Severity,
)


SCRIPT_NAME = os.path.basename(__file__)




def get_point_colours_from_colour_map(project: Project, oid: ObjectID) -> np.ndarray:
    """Read the point colours of the object.


    Unlike accessing the point colours directly, this will return correct
    values even for objects which are coloured by colour maps.


    Parameters
    ----------
    project
      Project to use to read the object.
    oid
      ObjectID of the object to read the colours of.


    Returns
    -------
    np.ndarray
      Array of the colours for each point.


    Raises
    ------
    ValueError
      If the object does not have point colours, or is using an unsupported
      colour map.
    """
    with project.read(oid) as data_object:
        colour_map_id = data_object.get_colour_map()
        if colour_map_id is None:
            # The object is not coloured via a colour map.
            try:
                return data_object.point_colours
            except AttributeError:
                raise ValueError("The object does not have point colours.") from None
        colour_map_attribute = None
        try:
            colour_map_attribute = data_object.point_attributes.colour_map_attribute
        except AttributeError:
            # The point_attributes property was missing.
            raise ValueError("The object does not have point colours.") from None
        if colour_map_attribute is None:
            # The object has a colour map, but it is not associated with the
            # point colours.
            try:
                return data_object.point_colours
            except AttributeError:
                raise ValueError("The object does not have point colours.") from None
        with project.read(data_object.get_colour_map()) as colour_map:
            if isinstance(colour_map, (NumericColourMap, StringColourMap)):
                return colour_map.colours_for(
                    data_object.point_attributes[colour_map_attribute]
                )
            raise ValueError(f"Unsupported colour map type: {colour_map.id.type_name}")




def extract_coloured_points(
    project: Project, object_id: ObjectID
) -> ObjectID[PointSet]:
    """Extract coloured points from another object.


    This will read the point colours from numeric colour maps associated with
    an object if one is available.


    Parameters
    ----------
    project
      Project to use to read the existing object and create the new object.
    object_id
      Existing object to read the points and point colours from.


    Raises
    ------
    ValueError
      If the existing object does not have points.
    """
    baked_point_colours = get_point_colours_from_colour_map(project, object_id)
    with project.new(
        f"{object_id.path}_coloured_points",
        PointSet,
        overwrite=OverwriteMode.UNIQUE_NAME,
    ) as point_set:
        with project.read(object_id) as read_object:
            point_set.points = read_object.points
            point_set.point_colours = baked_point_colours
    return point_set.id




def main(project: Project):
    """Allow the user to pick an object and then extract coloured points.


    Parameters
    ----------
    project
      Project to use for the object pick and creating or reading objects.
    """
    oid = object_pick(label="Pick an object to extract coloured points from.")
    view = active_view()
    result_oid = extract_coloured_points(project, oid)
    # Remove the picked object and replace it with the extracted points.
    if view is not None:
        view.add_object(result_oid)
        view.remove_object(oid)




if __name__ == "__main__":
    with Project() as main_project:
        try:
            with main_project.undo():
                main(main_project)
                show_toast_notification(SCRIPT_NAME, "Completed")
        except Exception as error:
            message = f"{type(error).__name__}: {str(error)}"
            show_message(SCRIPT_NAME, message, Severity.ERROR)
            raise

The following animation demonstrates the above script in action using the truck surface available on the Markers page.

The small facets in areas with a lot of detail are coloured red, whereas the large facets in areas of lower detail are coloured orange or green. The largest facets (not shown in the gif as they are on the bottom of the truck) are coloured blue.

Accessing a colour map like a dictionary

The colours of string colour maps can be accessed as if the object was a dictionary of colours. The script in the following example uses this property to create a colour map in a more readable way:

from mapteksdk.project import Project
from mapteksdk.data import StringColourMap
 
if __name__ == "__main__":
  with Project() as project:
    with project.new("legends/dictionary_map", StringColourMap) as colour_map:
      colour_map["red"] = [255, 0, 0, 255]
      colour_map["green"] = [0, 255, 0, 255]
      colour_map["blue"] = [0, 0, 255, 255]
      colour_map["yellow"] = [255, 255, 0, 255]
      colour_map["magenta"] = [255, 0, 255, 255]
      colour_map["cyan"] = [0, 255, 255, 255]
      # The cut off colour is transparent grey.
      colour_map.cutoff = [100, 100, 100, 100]

The script above creates the following colour map in the application:

String colour maps support the full dictionary interface. Thus they support the following operations:

# This script is a fragment and cannot run on its own.
# Get the colour associated with red.
# This would raise an error if red is not in the colour map.
red = colour_map["red"]
 
# The get function will return the cut off if the colour doesn't exist.
orange = colour_map.get("orange")
 
# Del can be used to delete colours from the map.
del colour_map["red"]
 
# It is also possible to check if a key exists in the colour map.
if "red" in colour_map:
    print("No red in this colour map...")

Getting many colours from a string colour map

Treating a string colour map like a dictionary is useful for getting a single colour, however if you need to get a large number of colours from the colour map it is usually more efficient to use the colours_for() function instead. For example, given a string colour map and a block property with string properties, to get an array containing the colours which would be used for each block if the block model was coloured by that property using the given colour map:

model.block_colours = string_colour_map.colours_for(model.block_attributes["geocode"])

The script below uses the above to set the block colours of a dense block model:

from mapteksdk.project import Project, OverwriteMode
from mapteksdk.data import DenseBlockModel, StringColourMap
from mapteksdk.operations import open_new_view


creation_parameters = {
    "row_count": 2,
    "col_count": 2,
    "slice_count": 2,
    "row_res": 0.5,
    "col_res": 0.5,
    "slice_res": 0.5,
}


ROCK = "ROCK"
ORE = "ORE"
RICH_ORE = "RICH_ORE"


with Project() as project:
    with project.new(
        "blockmodels/baked_colours",
        DenseBlockModel(**creation_parameters),
        overwrite=OverwriteMode.UNIQUE_NAME,
    ) as model, project.new(None, StringColourMap) as colour_map:
        colour_map.legend = [ROCK, ORE, RICH_ORE]
        colour_map.colours = [[100, 100, 100, 255]]
        colour_map[ROCK] = [100, 100, 100, 255]
        colour_map[ORE] = [25, 25, 200, 255]
        colour_map[RICH_ORE] = [10, 200, 200, 255]


        model.block_attributes["geocode"] = [
            ORE,
            RICH_ORE,
            ORE,
            ORE,
            ROCK,
            ROCK,
            ROCK,
            ROCK,
        ]
        # :NOTE: model.block_attributes.set_colour_map("geocode", colour_map.id)
        # might be preferable in this case because that would cause the colours
        # to update when the block attribute is updated.
        model.block_colours = colour_map.colours_for(model.block_attributes["geocode"])
        print(model.block_colours)


    open_new_view([model.id])

The print statement prints out the block colours array generated by the StringColourMap.colours_for() function:

[[ 25  25 200 255]
[ 10 200 200 255]
[ 25  25 200 255]
[ 25  25 200 255]
[100 100 100 255]
[100 100 100 255]
[100 100 100 255]
[100 100 100 255]]

This shows the actual colours used for each block as read from the colour map (information which would not be available if the script were to use model.block_attributes.set_colour_map("geocode", colour_map)). The block model itself is shown in the screenshot below:

Colour map case sensitivity

The following script has a problem:

from mapteksdk.project import Project
from mapteksdk.data import StringColourMap, DenseBlockModel, ObjectID
from mapteksdk.operations import open_new_view

def create_dense_block_model_with_attribute(
        path: str,
        target_project: Project) -> ObjectID[DenseBlockModel]:
    """Creates a dense block model with the 'Type' block attribute.

    This block attribute contains strings that have differing casing.

    Parameters
    ----------
    path
        Path to create the block model at.
    target_project
        Project to use to create the block model.

    Returns
    -------
    ObjectID
        Object ID of the newly created block model.
    """
    with target_project.new(
            path, DenseBlockModel(
                row_count=3, col_count=4, slice_count=1,
                row_res=0.5, col_res=0.5, slice_res=1.5
            )) as model:
        model.block_attributes["Type"] = [
            "IRON", "Iron", "iron", "IRon",
            "COPPER", "Copper", "copper", "COpper",
            "COBALT", "Cobalt", "cobalt", "CoBalt"
        ]
        return model.id

if __name__ == "__main__":
    with Project() as project:
        block_id = create_dense_block_model_with_attribute(
            "block models/examples/case_sensitive",
            project)
        with project.edit(block_id) as edit_model:
            with project.new(None, StringColourMap) as colour_map:
                colour_map.case_sensitive = True
                colour_map["IRON"] = [255, 0, 0, 255]
                colour_map["copper"] = [0, 255, 0, 255]
                colour_map["Cobalt"] = [0, 0, 255, 255]
                colour_map.cutoff = [100, 100, 100, 255]
            edit_model.block_attributes.set_colour_map("Type", colour_map)
        open_new_view([block_id])

This script creates the following block model:

\

The blocks in the bottom row should be all red, the blocks in the middle row should be all green and the blocks in the top row should be all blue. But instead most of the blocks are grey, indicating the block attribute value is not in the colour map.

The cause is quite clear by comparing the keys in the colour map to the block attribute values.

The colour map contains the following keys:

Key Colour
IRON Red
copper Green
Cobalt Blue

and the block model contains the following values:

COBALT Cobalt cobalt CoBalt
COPPER Copper copper COpper
IRON Iron iron IRon

Only three of the twelve values for the block attribute match the casing of the attribute in the colour map, so only three are coloured using the colour map. The rest are coloured using the cut-off colour. Why? Because the colour map is case sensitive. It does not consider the strings “IRON” and “Iron” to be the same. This is a common problem for drillhole databases, especially when strings are entered manually by many different people.

To cause a colour map to be case insensitive, the script needs to set the StringColourMap.case_sensitive flag to False. Below is the same script, but with the case sensitive flag set to False.

from mapteksdk.project import Project
from mapteksdk.data import StringColourMap, DenseBlockModel, ObjectID
from mapteksdk.operations import open_new_view

def create_dense_block_model_with_attribute(
        path: str,
        target_project: Project) -> ObjectID[DenseBlockModel]:
    """Creates a dense block model with the 'Type' block attribute.

    This block attribute contains strings that have differing casing.

    Parameters
    ----------
    path
        Path to create the block model at.
    target_project
        Project to use to create the block model.

    Returns
    -------
    ObjectID
        Object ID of the newly created block model.
    """
    with target_project.new(
            path, DenseBlockModel(
                row_count=3, col_count=4, slice_count=1,
                row_res=0.5, col_res=0.5, slice_res=1.5
            )) as model:
        model.block_attributes["Type"] = [
            "IRON", "Iron", "iron", "IRon",
            "COPPER", "Copper", "copper", "COpper",
            "COBALT", "Cobalt", "cobalt", "CoBalt"
        ]
        return model.id

if __name__ == "__main__":
    with Project() as project:
        block_id = create_dense_block_model_with_attribute(
            "block models/examples/case_insensitive",
            project)
        with project.edit(block_id) as edit_model:
            with project.new(None, StringColourMap) as colour_map:
                colour_map.case_sensitive = False
                colour_map["IRON"] = [255, 0, 0, 255]
                colour_map["copper"] = [0, 255, 0, 255]
                colour_map["Cobalt"] = [0, 0, 255, 255]
                colour_map.cutoff = [100, 100, 100, 255]
            edit_model.block_attributes.set_colour_map("Type", colour_map)
        open_new_view([block_id])

This script creates the following block model:

Because the colour map is case insensitive, the block model is coloured correctly regardless of the casing of the strings.

Note
  • Data containing inconsistent casing is common when data is entered manually.

  • Some people will prefer different casing (e.g. “IRON”, “iron”, “Iron”)

  • There is also the possibility of typos due to holding down the Shift key too long (“IRon”).

  • An alternative to making the colour map case sensitive is to fix the data using a script that makes all of the values consistently cased.

  • A case sensitive colour map can have different colours for different cases of the same key (e.g. “iron”, “Iron” and “IRON” could be coloured differently).

  • A case-insensitive colour map considers all keys that only differ by case to be identical (e.g. “iron”, “Iron”, “IRON” are all considered the same key).