garmentiq.landmark.derivation.derive_keypoint_coord

Deriving a landmark coordinate from surrounding landmarks and a mask.

  1"""Deriving a landmark coordinate from surrounding landmarks and a mask."""
  2from typing import Tuple, Optional
  3import numpy as np
  4from .line_intersect import _find_line_line_intersection
  5from .mask_intersect import _get_mask_boundary, _find_line_mask_intersections
  6from .utils import _calculate_line1_vector, _find_closest_point
  7
  8
  9def derive_keypoint_coord(
 10    p1_id: int,
 11    p2_id: int,
 12    p3_id: int,
 13    p4_id: int,
 14    p5_id: int,
 15    direction: str,
 16    landmark_coords: np.array,
 17    np_mask: np.array,
 18    line_length_factor: float = 10000.0,
 19) -> Optional[Tuple[float, float]]:
 20    """
 21    Derives the coordinate of a new keypoint based on intersections of lines defined by
 22    existing predefined keypoints and the segmentation mask boundary.
 23
 24    This function implements a geometric method to derive a new landmark:
 25    1. It uses `p1_id` as a point on the first line.
 26    2. The direction of the first line is derived from `p2_id`, `p3_id`, and `direction`
 27       (parallel or perpendicular to the line formed by p2 and p3).
 28    3. The second line is defined by `p4_id` and `p5_id`.
 29    4. An initial intersection point is found between Line 1 and Line 2.
 30    5. The boundary of the `np_mask` is extracted.
 31    6. Intersections between Line 1 and the `np_mask` boundary are found.
 32    7. The final derived point is the mask intersection point closest to the line-line intersection,
 33       or the line-line intersection itself if no mask intersections are found or an error occurs.
 34
 35    Args:
 36        p1_id (int): ID of the first keypoint used to define Line 1.
 37        p2_id (int): ID of the second keypoint used to define Line 1's direction.
 38        p3_id (int): ID of the third keypoint used to define Line 1's direction.
 39        p4_id (int): ID of the fourth keypoint used to define Line 2.
 40        p5_id (int): ID of the fifth keypoint used to define Line 2.
 41        direction (str): Specifies the direction of Line 1 relative to (p2, p3).
 42                         Can be "parallel" or "perpendicular".
 43        landmark_coords (np.array): NumPy array of all detected landmark coordinates.
 44                                    Shape: (1, N, 2) where N is the total number of landmarks.
 45        np_mask (np.array): NumPy array of the segmentation mask.
 46        line_length_factor (float, optional): Factor to extend lines for intersection calculations.
 47                                              Defaults to 10000.0.
 48
 49    Returns:
 50        Optional[Tuple[float, float]]: The (x, y) coordinates of the derived keypoint, or None if
 51                                      derivation fails (e.g., parallel lines, no valid mask boundary).
 52    """
 53    p1_coord = landmark_coords[:, p1_id - 1, :].reshape(2)
 54    p2_coord = landmark_coords[:, p2_id - 1, :].reshape(2)
 55    p3_coord = landmark_coords[:, p3_id - 1, :].reshape(2)
 56    p4_coord = landmark_coords[:, p4_id - 1, :].reshape(2)
 57    p5_coord = landmark_coords[:, p5_id - 1, :].reshape(2)
 58
 59    # 3. Define Line 1 Vector (v1)
 60    v1 = _calculate_line1_vector(p2_coord, p3_coord, direction)
 61    if v1 is None:
 62        return None  # Error or zero vector detected
 63
 64    # 4. Define Line 2 Vector (v2)
 65    v2 = (p5_coord[0] - p4_coord[0], p5_coord[1] - p4_coord[1])
 66    if np.isclose(v2[0], 0) and np.isclose(v2[1], 0):
 67        print(
 68            f"Warning: Direction vector for Line 2 is zero (p4_id={p4_id} and p5_id={p5_id} likely coincide)."
 69        )
 70        return None  # Cannot define Line 2
 71
 72    # 5. Calculate Line-Line Intersection
 73    line_intersection_point = _find_line_line_intersection(p1_coord, v1, p4_coord, v2)
 74    if line_intersection_point is None:
 75        print(
 76            "Info: Line 1 and Line 2 are parallel or collinear. No unique intersection."
 77        )
 78        return None
 79
 80    # 6. Load Mask Boundary
 81    mask_boundary_geom = _get_mask_boundary(np_mask)
 82    if mask_boundary_geom is None or mask_boundary_geom.is_empty:
 83        print(
 84            "Warning: No valid mask boundary found or mask is empty. Returning line-line intersection."
 85        )
 86        return line_intersection_point
 87
 88    # 7. Find Intersection(s) between Line 1 and Mask Boundary
 89    mask_intersection_points = _find_line_mask_intersections(
 90        p1_coord, v1, mask_boundary_geom, line_length_factor
 91    )
 92
 93    # 8. Determine Final Point
 94    if mask_intersection_points is None:
 95        # An error occurred during intersection calculation
 96        print(
 97            "Warning: Error finding mask intersections. Returning line-line intersection as fallback."
 98        )
 99        return line_intersection_point
100    elif not mask_intersection_points:
101        # No intersection found between line and mask boundary
102        # print("Info: Line 1 does not intersect the mask boundary. Returning line-line intersection.")
103        return line_intersection_point
104    else:
105        # Found intersection(s), find the one closest to the line-line intersection
106        closest_mask_point = _find_closest_point(
107            mask_intersection_points, line_intersection_point
108        )
109        # _find_closest_point should always return a point if the list is not empty
110        return closest_mask_point
def derive_keypoint_coord( p1_id: int, p2_id: int, p3_id: int, p4_id: int, p5_id: int, direction: str, landmark_coords: <built-in function array>, np_mask: <built-in function array>, line_length_factor: float = 10000.0) -> Optional[Tuple[float, float]]:
 10def derive_keypoint_coord(
 11    p1_id: int,
 12    p2_id: int,
 13    p3_id: int,
 14    p4_id: int,
 15    p5_id: int,
 16    direction: str,
 17    landmark_coords: np.array,
 18    np_mask: np.array,
 19    line_length_factor: float = 10000.0,
 20) -> Optional[Tuple[float, float]]:
 21    """
 22    Derives the coordinate of a new keypoint based on intersections of lines defined by
 23    existing predefined keypoints and the segmentation mask boundary.
 24
 25    This function implements a geometric method to derive a new landmark:
 26    1. It uses `p1_id` as a point on the first line.
 27    2. The direction of the first line is derived from `p2_id`, `p3_id`, and `direction`
 28       (parallel or perpendicular to the line formed by p2 and p3).
 29    3. The second line is defined by `p4_id` and `p5_id`.
 30    4. An initial intersection point is found between Line 1 and Line 2.
 31    5. The boundary of the `np_mask` is extracted.
 32    6. Intersections between Line 1 and the `np_mask` boundary are found.
 33    7. The final derived point is the mask intersection point closest to the line-line intersection,
 34       or the line-line intersection itself if no mask intersections are found or an error occurs.
 35
 36    Args:
 37        p1_id (int): ID of the first keypoint used to define Line 1.
 38        p2_id (int): ID of the second keypoint used to define Line 1's direction.
 39        p3_id (int): ID of the third keypoint used to define Line 1's direction.
 40        p4_id (int): ID of the fourth keypoint used to define Line 2.
 41        p5_id (int): ID of the fifth keypoint used to define Line 2.
 42        direction (str): Specifies the direction of Line 1 relative to (p2, p3).
 43                         Can be "parallel" or "perpendicular".
 44        landmark_coords (np.array): NumPy array of all detected landmark coordinates.
 45                                    Shape: (1, N, 2) where N is the total number of landmarks.
 46        np_mask (np.array): NumPy array of the segmentation mask.
 47        line_length_factor (float, optional): Factor to extend lines for intersection calculations.
 48                                              Defaults to 10000.0.
 49
 50    Returns:
 51        Optional[Tuple[float, float]]: The (x, y) coordinates of the derived keypoint, or None if
 52                                      derivation fails (e.g., parallel lines, no valid mask boundary).
 53    """
 54    p1_coord = landmark_coords[:, p1_id - 1, :].reshape(2)
 55    p2_coord = landmark_coords[:, p2_id - 1, :].reshape(2)
 56    p3_coord = landmark_coords[:, p3_id - 1, :].reshape(2)
 57    p4_coord = landmark_coords[:, p4_id - 1, :].reshape(2)
 58    p5_coord = landmark_coords[:, p5_id - 1, :].reshape(2)
 59
 60    # 3. Define Line 1 Vector (v1)
 61    v1 = _calculate_line1_vector(p2_coord, p3_coord, direction)
 62    if v1 is None:
 63        return None  # Error or zero vector detected
 64
 65    # 4. Define Line 2 Vector (v2)
 66    v2 = (p5_coord[0] - p4_coord[0], p5_coord[1] - p4_coord[1])
 67    if np.isclose(v2[0], 0) and np.isclose(v2[1], 0):
 68        print(
 69            f"Warning: Direction vector for Line 2 is zero (p4_id={p4_id} and p5_id={p5_id} likely coincide)."
 70        )
 71        return None  # Cannot define Line 2
 72
 73    # 5. Calculate Line-Line Intersection
 74    line_intersection_point = _find_line_line_intersection(p1_coord, v1, p4_coord, v2)
 75    if line_intersection_point is None:
 76        print(
 77            "Info: Line 1 and Line 2 are parallel or collinear. No unique intersection."
 78        )
 79        return None
 80
 81    # 6. Load Mask Boundary
 82    mask_boundary_geom = _get_mask_boundary(np_mask)
 83    if mask_boundary_geom is None or mask_boundary_geom.is_empty:
 84        print(
 85            "Warning: No valid mask boundary found or mask is empty. Returning line-line intersection."
 86        )
 87        return line_intersection_point
 88
 89    # 7. Find Intersection(s) between Line 1 and Mask Boundary
 90    mask_intersection_points = _find_line_mask_intersections(
 91        p1_coord, v1, mask_boundary_geom, line_length_factor
 92    )
 93
 94    # 8. Determine Final Point
 95    if mask_intersection_points is None:
 96        # An error occurred during intersection calculation
 97        print(
 98            "Warning: Error finding mask intersections. Returning line-line intersection as fallback."
 99        )
100        return line_intersection_point
101    elif not mask_intersection_points:
102        # No intersection found between line and mask boundary
103        # print("Info: Line 1 does not intersect the mask boundary. Returning line-line intersection.")
104        return line_intersection_point
105    else:
106        # Found intersection(s), find the one closest to the line-line intersection
107        closest_mask_point = _find_closest_point(
108            mask_intersection_points, line_intersection_point
109        )
110        # _find_closest_point should always return a point if the list is not empty
111        return closest_mask_point

Derives the coordinate of a new keypoint based on intersections of lines defined by existing predefined keypoints and the segmentation mask boundary.

This function implements a geometric method to derive a new landmark:

  1. It uses p1_id as a point on the first line.
  2. The direction of the first line is derived from p2_id, p3_id, and direction (parallel or perpendicular to the line formed by p2 and p3).
  3. The second line is defined by p4_id and p5_id.
  4. An initial intersection point is found between Line 1 and Line 2.
  5. The boundary of the np_mask is extracted.
  6. Intersections between Line 1 and the np_mask boundary are found.
  7. The final derived point is the mask intersection point closest to the line-line intersection, or the line-line intersection itself if no mask intersections are found or an error occurs.
Arguments:
  • p1_id (int): ID of the first keypoint used to define Line 1.
  • p2_id (int): ID of the second keypoint used to define Line 1's direction.
  • p3_id (int): ID of the third keypoint used to define Line 1's direction.
  • p4_id (int): ID of the fourth keypoint used to define Line 2.
  • p5_id (int): ID of the fifth keypoint used to define Line 2.
  • direction (str): Specifies the direction of Line 1 relative to (p2, p3). Can be "parallel" or "perpendicular".
  • landmark_coords (np.array): NumPy array of all detected landmark coordinates. Shape: (1, N, 2) where N is the total number of landmarks.
  • np_mask (np.array): NumPy array of the segmentation mask.
  • line_length_factor (float, optional): Factor to extend lines for intersection calculations. Defaults to 10000.0.
Returns:

Optional[Tuple[float, float]]: The (x, y) coordinates of the derived keypoint, or None if derivation fails (e.g., parallel lines, no valid mask boundary).