retuve.hip_us.metrics.alpha

Metric: Alpha Angle

  1# Copyright 2024 Adam McArthur
  2#
  3# Licensed under the Apache License, Version 2.0 (the "License");
  4# you may not use this file except in compliance with the License.
  5# You may obtain a copy of the License at
  6#
  7#     http://www.apache.org/licenses/LICENSE-2.0
  8#
  9# Unless required by applicable law or agreed to in writing, software
 10# distributed under the License is distributed on an "AS IS" BASIS,
 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12# See the License for the specific language governing permissions and
 13# limitations under the License.
 14
 15"""
 16Metric: Alpha Angle
 17
 18"""
 19
 20import numpy as np
 21
 22from retuve.classes.draw import Overlay
 23from retuve.classes.seg import SegObject
 24from retuve.hip_us.classes.general import HipDataUS, LandmarksUS
 25from retuve.keyphrases.config import Config
 26from retuve.keyphrases.enums import MetricUS
 27from retuve.utils import find_midline_extremes
 28
 29# To move the apex point left or right
 30APEX_RIGHT_FACTOR = 0
 31
 32
 33def find_alpha_landmarks(
 34    illium: SegObject, landmarks: LandmarksUS, config: Config
 35) -> LandmarksUS:
 36    """
 37    Algorithm to find the landmarks for the Alpha Angle.
 38
 39    :param illium: SegObject: The Illium SegObject.
 40    :param landmarks: LandmarksUS: The landmarks object.
 41    :param config: Config: The Config
 42
 43    :return: LandmarksUS: The updated landmarks object.
 44    """
 45    if not (
 46        illium
 47        and any(m in config.hip.measurements for m in MetricUS.ALL())
 48        and illium.mask is not None
 49    ):
 50        return landmarks
 51
 52    left_most, right_most = find_midline_extremes(illium.midline_moved)
 53
 54    if (
 55        right_most is None
 56        or left_most is None
 57        or (right_most[1] - left_most[1]) == 0
 58    ):
 59        return landmarks
 60
 61    # get the equation for the line between the two extreme points
 62    # y = mx + b
 63    m = (right_most[0] - left_most[0]) / (right_most[1] - left_most[1])
 64    b = left_most[0] - m * left_most[1]
 65
 66    # if m is 0, return landmarks
 67    if m == 0:
 68        return landmarks
 69
 70    # find the height
 71
 72    # find the orthogonal line to the line between the two extreme points
 73    # y = -1/m * x + b
 74    m_orth = -1 / m
 75    # for each point along the line, find the distance between that point and the
 76
 77    points_on_line = np.array(
 78        [
 79            [x, m * x + b]
 80            for x in range(int(left_most[1]), int(right_most[1]), 1)
 81        ]
 82    )
 83
 84    # Convert white_points to a convenient shape for vector operations
 85    midline_moved = np.array(illium.midline_moved)[
 86        :, ::-1
 87    ]  # Reverse each point only once
 88
 89    # Create an array for b_orth values for each point in points_on_line
 90    b_orth_array = points_on_line[:, 1] - m_orth * points_on_line[:, 0]
 91
 92    # Calculate y_values on the orthogonal line for each point in points_on_line
 93    y_values_orth_line = (
 94        m_orth * midline_moved[:, 0] + b_orth_array[:, np.newaxis]
 95    )
 96
 97    # Check which white points are close to each orthogonal line
 98    close_points = np.isclose(
 99        y_values_orth_line, midline_moved[:, 1], atol=0.8
100    )
101
102    # Calculate distances for all point pairs
103    distances = np.linalg.norm(
104        points_on_line[:, np.newaxis, :] - midline_moved, axis=2
105    )
106
107    # Mask distances with close_points to consider only relevant distances
108    masked_distances = np.where(close_points, distances, 0)
109
110    # Find the maximum distance and the corresponding points
111    max_distance = np.max(masked_distances)
112    max_distance = np.max(masked_distances)
113    if max_distance > 0:
114        point_index, apex_point_index = np.unravel_index(
115            np.argmax(masked_distances), masked_distances.shape
116        )
117        best_point = tuple(points_on_line[point_index])
118        # Check APEX_RIGHT_FACTOR is within bounds
119        if apex_point_index + APEX_RIGHT_FACTOR < len(midline_moved):
120            factor = APEX_RIGHT_FACTOR
121        else:
122            # max possible factor
123            factor = len(midline_moved) - apex_point_index - 1
124
125        best_apex_point = tuple(midline_moved[apex_point_index + factor])
126
127        left_most, right_most = find_midline_extremes(illium.midline_moved)
128        if right_most is None or left_most is None:
129            return landmarks
130
131        left_most, right_most = tuple(reversed(left_most)), tuple(
132            reversed(right_most)
133        )
134        mid_x = (left_most[0] + right_most[0]) / 2
135        if (
136            (best_apex_point[1] < best_point[1])  # apex above line
137            and right_most[0] > best_apex_point[0]  # apex left of right point
138            and best_apex_point[0] > mid_x  # mid_x left of apex
139        ):
140            # re-calculate the left and right points from original midline
141
142            landmarks.left = left_most
143            landmarks.right = right_most
144            landmarks.apex = best_apex_point
145
146    # reversed because cv2 uses (y, x)??
147    return landmarks
148
149
150def find_alpha_angle(points: LandmarksUS) -> float:
151    """
152    Calculate the Alpha Angle.
153
154    :param points: LandmarksUS: The landmarks object.
155
156    :return: float: The Alpha Angle.
157    """
158    if not (points and points.left and points.apex and points.right):
159        return 0
160
161    # find angle ABC of points
162    A, B, C = (
163        np.array(points.left),
164        np.array(points.apex),
165        np.array(points.right),
166    )
167    AB = np.linalg.norm(A - B)
168    BC = np.linalg.norm(B - C)
169    AC = np.linalg.norm(A - C)
170    angle = np.arccos((BC**2 + AB**2 - AC**2) / (2 * BC * AB))
171
172    angle = np.degrees(angle)
173    angle = round((180 - angle), 1)
174
175    return round(angle, 2)
176
177
178def draw_alpha(hip: HipDataUS, overlay: Overlay, config: Config) -> Overlay:
179    """
180    Draw the Alpha Angle on the Overlay.
181
182    :param hip: HipDataUS: The Hip Data.
183    :param overlay: Overlay: The Overlay.
184    :param config: Config: The Config.
185
186    :return: Overlay: The updated Overlay.
187    """
188    alpha = hip.get_metric(MetricUS.ALPHA)
189    if alpha != 0:
190        m1 = (hip.landmarks.apex[1] - hip.landmarks.left[1]) / (
191            hip.landmarks.apex[0] - hip.landmarks.left[0]
192        )
193        m2 = (hip.landmarks.right[1] - hip.landmarks.apex[1]) / (
194            hip.landmarks.right[0] - hip.landmarks.apex[0]
195        )
196
197        b1 = hip.landmarks.apex[1] - m1 * hip.landmarks.apex[0]
198        b2 = hip.landmarks.apex[1] - m2 * hip.landmarks.apex[0]
199
200        # x2 to account for difference in gradient
201        new_right_apex = (
202            hip.landmarks.apex[0] + 350,
203            m1 * (hip.landmarks.apex[0] + 350) + b1,
204        )
205        new_right_landmark = (
206            hip.landmarks.right[0],
207            m2 * (hip.landmarks.right[0]) + b2,
208        )
209
210        overlay.draw_lines(
211            (
212                (hip.landmarks.left, new_right_apex),
213                (hip.landmarks.apex, new_right_landmark),
214            ),
215        )
216
217        if config.visuals.display_full_metric_names:
218            title = "Alpha Angle"
219        else:
220            title = "a"
221
222        overlay.draw_text(
223            f"{title}: {alpha}",
224            int(hip.landmarks.left[0]),
225            int(hip.landmarks.left[1] - 40),
226            header="h2",
227        )
228
229    return overlay
230
231
232def bad_alpha(hip: HipDataUS) -> bool:
233    """
234    Check if the Alpha Angle is bad.
235
236    :param hip: HipDataUS: The Hip Data.
237
238    :return: bool: True if the Alpha Angle is bad.
239    """
240
241    if (
242        hip.get_metric(MetricUS.ALPHA) < 20
243        or hip.get_metric(MetricUS.ALPHA) > 100
244    ):
245        return True
246
247    return False
APEX_RIGHT_FACTOR = 0
 34def find_alpha_landmarks(
 35    illium: SegObject, landmarks: LandmarksUS, config: Config
 36) -> LandmarksUS:
 37    """
 38    Algorithm to find the landmarks for the Alpha Angle.
 39
 40    :param illium: SegObject: The Illium SegObject.
 41    :param landmarks: LandmarksUS: The landmarks object.
 42    :param config: Config: The Config
 43
 44    :return: LandmarksUS: The updated landmarks object.
 45    """
 46    if not (
 47        illium
 48        and any(m in config.hip.measurements for m in MetricUS.ALL())
 49        and illium.mask is not None
 50    ):
 51        return landmarks
 52
 53    left_most, right_most = find_midline_extremes(illium.midline_moved)
 54
 55    if (
 56        right_most is None
 57        or left_most is None
 58        or (right_most[1] - left_most[1]) == 0
 59    ):
 60        return landmarks
 61
 62    # get the equation for the line between the two extreme points
 63    # y = mx + b
 64    m = (right_most[0] - left_most[0]) / (right_most[1] - left_most[1])
 65    b = left_most[0] - m * left_most[1]
 66
 67    # if m is 0, return landmarks
 68    if m == 0:
 69        return landmarks
 70
 71    # find the height
 72
 73    # find the orthogonal line to the line between the two extreme points
 74    # y = -1/m * x + b
 75    m_orth = -1 / m
 76    # for each point along the line, find the distance between that point and the
 77
 78    points_on_line = np.array(
 79        [
 80            [x, m * x + b]
 81            for x in range(int(left_most[1]), int(right_most[1]), 1)
 82        ]
 83    )
 84
 85    # Convert white_points to a convenient shape for vector operations
 86    midline_moved = np.array(illium.midline_moved)[
 87        :, ::-1
 88    ]  # Reverse each point only once
 89
 90    # Create an array for b_orth values for each point in points_on_line
 91    b_orth_array = points_on_line[:, 1] - m_orth * points_on_line[:, 0]
 92
 93    # Calculate y_values on the orthogonal line for each point in points_on_line
 94    y_values_orth_line = (
 95        m_orth * midline_moved[:, 0] + b_orth_array[:, np.newaxis]
 96    )
 97
 98    # Check which white points are close to each orthogonal line
 99    close_points = np.isclose(
100        y_values_orth_line, midline_moved[:, 1], atol=0.8
101    )
102
103    # Calculate distances for all point pairs
104    distances = np.linalg.norm(
105        points_on_line[:, np.newaxis, :] - midline_moved, axis=2
106    )
107
108    # Mask distances with close_points to consider only relevant distances
109    masked_distances = np.where(close_points, distances, 0)
110
111    # Find the maximum distance and the corresponding points
112    max_distance = np.max(masked_distances)
113    max_distance = np.max(masked_distances)
114    if max_distance > 0:
115        point_index, apex_point_index = np.unravel_index(
116            np.argmax(masked_distances), masked_distances.shape
117        )
118        best_point = tuple(points_on_line[point_index])
119        # Check APEX_RIGHT_FACTOR is within bounds
120        if apex_point_index + APEX_RIGHT_FACTOR < len(midline_moved):
121            factor = APEX_RIGHT_FACTOR
122        else:
123            # max possible factor
124            factor = len(midline_moved) - apex_point_index - 1
125
126        best_apex_point = tuple(midline_moved[apex_point_index + factor])
127
128        left_most, right_most = find_midline_extremes(illium.midline_moved)
129        if right_most is None or left_most is None:
130            return landmarks
131
132        left_most, right_most = tuple(reversed(left_most)), tuple(
133            reversed(right_most)
134        )
135        mid_x = (left_most[0] + right_most[0]) / 2
136        if (
137            (best_apex_point[1] < best_point[1])  # apex above line
138            and right_most[0] > best_apex_point[0]  # apex left of right point
139            and best_apex_point[0] > mid_x  # mid_x left of apex
140        ):
141            # re-calculate the left and right points from original midline
142
143            landmarks.left = left_most
144            landmarks.right = right_most
145            landmarks.apex = best_apex_point
146
147    # reversed because cv2 uses (y, x)??
148    return landmarks

Algorithm to find the landmarks for the Alpha Angle.

Parameters
  • illium: SegObject: The Illium SegObject.
  • landmarks: LandmarksUS: The landmarks object.
  • config: Config: The Config
Returns

LandmarksUS: The updated landmarks object.

def find_alpha_angle(points: retuve.hip_us.classes.general.LandmarksUS) -> float:
151def find_alpha_angle(points: LandmarksUS) -> float:
152    """
153    Calculate the Alpha Angle.
154
155    :param points: LandmarksUS: The landmarks object.
156
157    :return: float: The Alpha Angle.
158    """
159    if not (points and points.left and points.apex and points.right):
160        return 0
161
162    # find angle ABC of points
163    A, B, C = (
164        np.array(points.left),
165        np.array(points.apex),
166        np.array(points.right),
167    )
168    AB = np.linalg.norm(A - B)
169    BC = np.linalg.norm(B - C)
170    AC = np.linalg.norm(A - C)
171    angle = np.arccos((BC**2 + AB**2 - AC**2) / (2 * BC * AB))
172
173    angle = np.degrees(angle)
174    angle = round((180 - angle), 1)
175
176    return round(angle, 2)

Calculate the Alpha Angle.

Parameters
  • points: LandmarksUS: The landmarks object.
Returns

float: The Alpha Angle.

179def draw_alpha(hip: HipDataUS, overlay: Overlay, config: Config) -> Overlay:
180    """
181    Draw the Alpha Angle on the Overlay.
182
183    :param hip: HipDataUS: The Hip Data.
184    :param overlay: Overlay: The Overlay.
185    :param config: Config: The Config.
186
187    :return: Overlay: The updated Overlay.
188    """
189    alpha = hip.get_metric(MetricUS.ALPHA)
190    if alpha != 0:
191        m1 = (hip.landmarks.apex[1] - hip.landmarks.left[1]) / (
192            hip.landmarks.apex[0] - hip.landmarks.left[0]
193        )
194        m2 = (hip.landmarks.right[1] - hip.landmarks.apex[1]) / (
195            hip.landmarks.right[0] - hip.landmarks.apex[0]
196        )
197
198        b1 = hip.landmarks.apex[1] - m1 * hip.landmarks.apex[0]
199        b2 = hip.landmarks.apex[1] - m2 * hip.landmarks.apex[0]
200
201        # x2 to account for difference in gradient
202        new_right_apex = (
203            hip.landmarks.apex[0] + 350,
204            m1 * (hip.landmarks.apex[0] + 350) + b1,
205        )
206        new_right_landmark = (
207            hip.landmarks.right[0],
208            m2 * (hip.landmarks.right[0]) + b2,
209        )
210
211        overlay.draw_lines(
212            (
213                (hip.landmarks.left, new_right_apex),
214                (hip.landmarks.apex, new_right_landmark),
215            ),
216        )
217
218        if config.visuals.display_full_metric_names:
219            title = "Alpha Angle"
220        else:
221            title = "a"
222
223        overlay.draw_text(
224            f"{title}: {alpha}",
225            int(hip.landmarks.left[0]),
226            int(hip.landmarks.left[1] - 40),
227            header="h2",
228        )
229
230    return overlay

Draw the Alpha Angle on the Overlay.

Parameters
  • hip: HipDataUS: The Hip Data.
  • overlay: Overlay: The Overlay.
  • config: Config: The Config.
Returns

Overlay: The updated Overlay.

def bad_alpha(hip: retuve.hip_us.classes.general.HipDataUS) -> bool:
233def bad_alpha(hip: HipDataUS) -> bool:
234    """
235    Check if the Alpha Angle is bad.
236
237    :param hip: HipDataUS: The Hip Data.
238
239    :return: bool: True if the Alpha Angle is bad.
240    """
241
242    if (
243        hip.get_metric(MetricUS.ALPHA) < 20
244        or hip.get_metric(MetricUS.ALPHA) > 100
245    ):
246        return True
247
248    return False

Check if the Alpha Angle is bad.

Parameters
  • hip: HipDataUS: The Hip Data.
Returns

bool: True if the Alpha Angle is bad.