Skip to content

Points

Overview

Utility functions for manipulating, filtering, and transforming root point arrays.

Key functions: - join_pts - Combine multiple point arrays - get_all_pts_array - Flatten to 2D array - associate_lateral_to_primary - Map laterals to primary - filter_roots_with_nans - Remove invalid roots

Quick Example

import sleap_roots as sr

series = sr.Series.load("plant", primary_path="primary.slp", lateral_path="lateral.slp")
primary_pts = series.get_primary_points()
lateral_pts_list = series.get_lateral_points()

# Combine all points
all_pts = sr.join_pts([primary_pts] + lateral_pts_list)
print(f"Combined points shape: {all_pts.shape}")

# Filter out roots with NaN
valid_laterals = sr.filter_roots_with_nans(lateral_pts_list)
print(f"Valid laterals: {len(valid_laterals)}/{len(lateral_pts_list)}")

API Reference

join_pts

join_pts

join_pts(
    pts0: ndarray, *args: Optional[ndarray]
) -> List[ndarray]

Join an arbitrary number of points arrays and return them as a list.

Parameters:

Name Type Description Default
pts0 ndarray

The first array of points. Should have shape (instances, nodes, 2) or (nodes, 2).

required
*args Optional[ndarray]

Additional optional arrays of points. Each should have shape (instances, nodes, 2) or (nodes, 2).

()

Returns:

Type Description
List[ndarray]

A list of arrays, each having shape (nodes, 2).

Source code in sleap_roots/points.py
def join_pts(pts0: np.ndarray, *args: Optional[np.ndarray]) -> List[np.ndarray]:
    """Join an arbitrary number of points arrays and return them as a list.

    Args:
        pts0: The first array of points. Should have shape `(instances, nodes, 2)`
            or `(nodes, 2)`.
        *args: Additional optional arrays of points. Each should have shape
            `(instances, nodes, 2)` or `(nodes, 2)`.

    Returns:
        A list of arrays, each having shape `(nodes, 2)`.
    """
    # Initialize an empty list to store the points
    all_pts = []
    # Loop over the input arrays
    for pts in [pts0] + list(args):
        if pts is None:
            continue  # Skip None values

        # If an array has shape `(nodes, 2)`, expand dimensions to `(1, nodes, 2)`
        if pts.ndim == 2 and pts.shape[-1] == 2:
            pts = pts[np.newaxis, :, :]

        # Validate the shape of each array
        if pts.ndim != 3 or pts.shape[-1] != 2:
            raise ValueError(
                "Points should have a shape of `(instances, nodes, 2)` or `(nodes, 2)`."
            )

        # Add the points to the list
        all_pts.extend(list(pts))

    return all_pts

get_all_pts_array

get_all_pts_array

get_all_pts_array(
    pts0: ndarray, *args: Optional[ndarray]
) -> ndarray

Get all landmark points within a given frame as a flat array of coordinates.

Parameters:

Name Type Description Default
pts0 ndarray

The first array of points. Should have shape (instances, nodes, 2) or (nodes, 2).

required
*args Optional[ndarray]

Additional optional arrays of points. Each should have shape (instances, nodes, 2) or (nodes, 2).

()

Returns:

Type Description
ndarray

A 2D array of shape (n_points, 2), containing the coordinates of all extracted

ndarray

points.

Source code in sleap_roots/points.py
def get_all_pts_array(pts0: np.ndarray, *args: Optional[np.ndarray]) -> np.ndarray:
    """Get all landmark points within a given frame as a flat array of coordinates.

    Args:
        pts0: The first array of points. Should have shape `(instances, nodes, 2)`
            or `(nodes, 2)`.
        *args: Additional optional arrays of points. Each should have shape
            `(instances, nodes, 2)` or `(nodes, 2)`.

    Returns:
        A 2D array of shape (n_points, 2), containing the coordinates of all extracted
        points.
    """
    # Initialize an empty list to store the points
    concatenated_pts = []

    # Loop over the input arrays
    for pts in [pts0] + list(args):
        if pts is None:
            continue

        # Check if the array has the right number of dimensions
        if pts.ndim not in [2, 3]:
            raise ValueError("Each input array should be 2D or 3D.")

        # Check if the last dimension of the array has size 2
        # (representing x and y coordinates)
        if pts.shape[-1] != 2:
            raise ValueError(
                "The last dimension should have size 2, representing x and y coordinates."
            )

        # Flatten the array to 2D and append to list
        flat_pts = pts.reshape(-1, 2)
        concatenated_pts.append(flat_pts)

    # Concatenate all points into a single array
    return np.concatenate(concatenated_pts, axis=0)

associate_lateral_to_primary

associate_lateral_to_primary

associate_lateral_to_primary(
    primary_pts: ndarray, lateral_pts: ndarray
) -> dict

Associates each lateral root with the closest primary root.

Parameters:

Name Type Description Default
primary_pts ndarray

A numpy array of primary root points with shape (instances, nodes, 2), where 'instances' is the number of primary roots, 'nodes' is the number of points in each root, and '2' corresponds to the x and y coordinates. Points cannot have NaN values.

required
lateral_pts ndarray

A numpy array of lateral root points with a shape similar to primary_pts, representing the lateral roots. Points cannot have NaN values.

required

Returns:

Name Type Description
dict dict

A dictionary where each key is an index of a primary root (from the primary_pts

dict

array) and each value is a dictionary containing 'primary_points' as the points of

dict

the primary root (1, nodes, 2) and 'lateral_points' as an array of

dict

lateral root points that are closest to that primary root. The shape of

dict

'lateral_points' is (instances, nodes, 2), where instances is the number of

dict

lateral roots associated with the primary root.

Source code in sleap_roots/points.py
def associate_lateral_to_primary(
    primary_pts: np.ndarray, lateral_pts: np.ndarray
) -> dict:
    """Associates each lateral root with the closest primary root.

    Args:
        primary_pts: A numpy array of primary root points with shape
            (instances, nodes, 2), where 'instances' is the number of primary roots,
            'nodes' is the number of points in each root, and '2' corresponds to the x and y
            coordinates. Points cannot have NaN values.
        lateral_pts: A numpy array of lateral root points with a shape similar
            to primary_pts, representing the lateral roots. Points cannot have NaN values.

    Returns:
        dict: A dictionary where each key is an index of a primary root (from the primary_pts
        array) and each value is a dictionary containing 'primary_points' as the points of
        the primary root (1, nodes, 2) and 'lateral_points' as an array of
        lateral root points that are closest to that primary root. The shape of
        'lateral_points' is (instances, nodes, 2), where instances is the number of
        lateral roots associated with the primary root.
    """
    # Basic input validation
    if not isinstance(primary_pts, np.ndarray) or not isinstance(
        lateral_pts, np.ndarray
    ):
        raise ValueError("Both primary_pts and lateral_pts must be numpy arrays.")
    if len(primary_pts.shape) != 3 or len(lateral_pts.shape) != 3:
        raise ValueError("Input arrays must have a shape of (instances, nodes, 2).")
    if primary_pts.shape[2] != 2 or lateral_pts.shape[2] != 2:
        raise ValueError(
            "The last dimension of input arrays must be 2, representing x and y coordinates."
        )

    plant_associations = {}

    # Initialize plant associations dictionary
    for i, primary_root in enumerate(primary_pts):
        if not is_line_valid(primary_root):
            continue  # Skip primary roots containing NaN values
        plant_associations[i] = {
            "primary_points": primary_root,
            "lateral_points": [],
        }

    # Associate each lateral root with the closest primary root
    for lateral_root in lateral_pts:
        if not is_line_valid(lateral_root):
            continue  # Skip lateral roots containing NaN values

        lateral_line = LineString(lateral_root)
        min_distance = float("inf")
        closest_primary_index = None

        for primary_index, primary_data in plant_associations.items():
            primary_root = primary_data["primary_points"]
            try:
                primary_line = LineString(primary_root)
                distance = primary_line.distance(lateral_line)
            except Exception as e:
                print(f"Error computing distance: {e}")
                continue

            if distance < min_distance:
                min_distance = distance
                closest_primary_index = primary_index

        if closest_primary_index is not None:
            plant_associations[closest_primary_index]["lateral_points"].append(
                lateral_root
            )

    # Convert lateral points lists into arrays
    for primary_index, data in plant_associations.items():
        lateral_points_list = data["lateral_points"]
        if lateral_points_list:  # Check if there are any lateral points to convert
            lateral_points_array = np.array(lateral_points_list)
            plant_associations[primary_index]["lateral_points"] = lateral_points_array
        else:
            # Create an array of NaNs if there are no lateral points
            shape = (1, lateral_pts.shape[1], 2)  # Shape of lateral points array
            plant_associations[primary_index]["lateral_points"] = np.full(shape, np.nan)

    return plant_associations

filter_roots_with_nans

filter_roots_with_nans

filter_roots_with_nans(pts: ndarray) -> ndarray

Remove roots with NaN values from an array of root points.

Parameters:

Name Type Description Default
pts ndarray

An array of points representing roots, with shape (instances, nodes, 2), where 'instances' is the number of roots, 'nodes' is the number of points in each root, and '2' corresponds to the x and y coordinates.

required

Returns:

Type Description
ndarray

np.ndarray: An array of shape (instances, nodes, 2) with NaN-containing roots removed. If all roots contain NaN values, an empty array of shape (0, nodes, 2) is returned.

Source code in sleap_roots/points.py
def filter_roots_with_nans(pts: np.ndarray) -> np.ndarray:
    """Remove roots with NaN values from an array of root points.

    Args:
        pts: An array of points representing roots, with shape (instances, nodes, 2),
            where 'instances' is the number of roots, 'nodes' is the number of points in
            each root, and '2' corresponds to the x and y coordinates.

    Returns:
        np.ndarray: An array of shape (instances, nodes, 2) with NaN-containing roots
            removed. If all roots contain NaN values, an empty array of shape
            (0, nodes, 2) is returned.
    """
    if not isinstance(pts, np.ndarray):
        raise TypeError("Input must be a numpy array.")
    if pts.ndim != 3 or pts.shape[2] != 2:
        raise ValueError("Input array must have a shape of (instances, nodes, 2).")

    cleaned_pts = np.array([root for root in pts if not np.isnan(root).any()])

    if cleaned_pts.size == 0:
        return np.empty((0, pts.shape[1], 2))

    return cleaned_pts

  • Series - Loading point data
  • All trait modules use point utilities