garmentiq.landmark.derivation.derive_keypoint_coord

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

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).