Skip to content

average_turning_angle

Compute the average turning angle of a line.

The average turning angle measures the mean absolute angular change between consecutive segments of a line. A straight line has an average turning angle of 0, while a line with many sharp turns has a higher value.

Note

This function calculates the absolute angle between consecutive segments using the arctan2 method, which gives angles in the range [0, 180] degrees.

Parameters:

Name Type Description Default
line Union[LineString, MultiLineString]

Input shapely LineString or MultiLineString object.

required

Returns:

Name Type Description
float Optional[float]

The average turning angle in degrees. Returns None if the line has fewer than 3 points (insufficient to calculate any angles).

Source code in src/histolytics/spatial_geom/line_metrics.py
def average_turning_angle(line: Union[LineString, MultiLineString]) -> Optional[float]:
    """Compute the average turning angle of a line.

    The average turning angle measures the mean absolute angular change between
    consecutive segments of a line. A straight line has an average turning angle
    of 0, while a line with many sharp turns has a higher value.

    Note:
        This function calculates the absolute angle between consecutive segments
        using the arctan2 method, which gives angles in the range [0, 180] degrees.

    Parameters:
        line (Union[LineString, MultiLineString]):
            Input shapely LineString or MultiLineString object.

    Returns:
        float:
            The average turning angle in degrees. Returns None if the line has
            fewer than 3 points (insufficient to calculate any angles).
    """
    if isinstance(line, LineString):
        # Extract coordinates
        coords = list(line.coords)

        # Need at least 3 points to calculate angles
        if len(coords) < 3:
            return None

        # Calculate angles between consecutive segments
        angles = []
        for i in range(len(coords) - 2):
            # Get three consecutive points
            p1 = coords[i]
            p2 = coords[i + 1]
            p3 = coords[i + 2]

            # Calculate direction angles of the two segments using arctan2
            # arctan2 gives angle in radians with respect to the positive x-axis
            angle1 = np.arctan2(p2[1] - p1[1], p2[0] - p1[0])
            angle2 = np.arctan2(p3[1] - p2[1], p3[0] - p2[0])

            # Calculate the absolute difference in angles (turning angle)
            # This handles the circular nature of angles
            diff = np.abs(angle2 - angle1)
            if diff > np.pi:
                diff = 2 * np.pi - diff

            # Convert to degrees
            angle_deg = np.degrees(diff)
            angles.append(angle_deg)

        # Return the average turning angle
        return np.mean(angles) if angles else None

    elif isinstance(line, MultiLineString):
        # Combine all angles from individual LineStrings
        all_angles = []

        for geom in line.geoms:
            # Process each LineString
            coords = list(geom.coords)

            if len(coords) < 3:
                continue

            for i in range(len(coords) - 2):
                p1 = coords[i]
                p2 = coords[i + 1]
                p3 = coords[i + 2]

                angle1 = np.arctan2(p2[1] - p1[1], p2[0] - p1[0])
                angle2 = np.arctan2(p3[1] - p2[1], p3[0] - p2[0])

                diff = np.abs(angle2 - angle1)
                if diff > np.pi:
                    diff = 2 * np.pi - diff

                angle_deg = np.degrees(diff)
                all_angles.append(angle_deg)

        # Return the average turning angle across all LineStrings
        return np.mean(all_angles) if all_angles else None

    else:
        return None