retuve.hip_xray.metrics.ace

Metric: Acentabular Index (ACE Index)

  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: Acentabular Index (ACE Index)
 17"""
 18
 19from typing import Literal
 20
 21import numpy as np
 22from radstract.math import smart_find_intersection
 23
 24from retuve.draw import Overlay
 25from retuve.hip_xray.classes import HipDataXray, LandmarksXRay
 26from retuve.keyphrases.config import Config
 27
 28
 29def find_ace(landmarks: LandmarksXRay) -> tuple[float, float]:
 30    """
 31    Calculate the Acentabular Index (ACE Index) for both hips.
 32
 33    :param landmarks: The Landmarks of the Hip
 34    :return: The ACE Index for both hips
 35    """
 36    intersection_left = smart_find_intersection(
 37        landmarks.pel_l_o, landmarks.pel_l_i, landmarks.fem_l, landmarks.fem_r
 38    )
 39    intersection_right = smart_find_intersection(
 40        landmarks.pel_r_o, landmarks.pel_r_i, landmarks.fem_l, landmarks.fem_r
 41    )
 42
 43    A, B, C = (
 44        np.array(landmarks.pel_l_o),
 45        np.array(landmarks.pel_l_i),
 46        np.array(intersection_right),
 47    )
 48    AB = np.linalg.norm(A - B)
 49    BC = np.linalg.norm(B - C)
 50    AC = np.linalg.norm(A - C)
 51
 52    ace_index_left = np.arccos((BC**2 + AB**2 - AC**2) / (2 * BC * AB))
 53
 54    ace_index_left = round(180 - np.degrees(ace_index_left), 1)
 55
 56    A, B, C = (
 57        np.array(landmarks.pel_r_o),
 58        np.array(landmarks.pel_r_i),
 59        np.array(intersection_left),
 60    )
 61    AB = np.linalg.norm(A - B)
 62    BC = np.linalg.norm(B - C)
 63    AC = np.linalg.norm(A - C)
 64
 65    ace_index_right = np.arccos((BC**2 + AB**2 - AC**2) / (2 * BC * AB))
 66
 67    ace_index_right = round(180 - np.degrees(ace_index_right), 1)
 68
 69    if not (5 < ace_index_left < 50):
 70        ace_index_left = 0
 71
 72    if not (5 < ace_index_right < 50):
 73        ace_index_right = 0
 74
 75    return ace_index_left, ace_index_right
 76
 77
 78def extend_line(
 79    p1: list,
 80    p2: list,
 81    scale: float = 1.2,
 82    direction: Literal["both", "up", "down"] = "both",
 83):
 84    """
 85    Extend the line from p1 to p2 by a scale factor (default 10%)
 86    in specified direction ('up', 'down', 'both').
 87
 88    :param p1: The first point of the line
 89    :param p2: The second point of the line
 90    :param scale: The scale factor
 91    :param direction: The direction to extend the lines
 92    """
 93    direction_vector = np.array(p2) - np.array(p1)
 94    left_point = (
 95        np.array(p1) - direction_vector * (scale - 1)
 96        if direction in ["up", "both"]
 97        else np.array(p1)
 98    )
 99    right_point = (
100        np.array(p2) + direction_vector * (scale - 1)
101        if direction in ["down", "both"]
102        else np.array(p2)
103    )
104
105    return left_point, right_point
106
107
108def draw_ace(hip: HipDataXray, overlay: Overlay, config: Config):
109    """
110    Draw the Acentabular Index (ACE Index) on the X-Ray.
111
112    :param hip: The Hip Data
113    :param overlay: The Overlay
114    :param config: The Config
115
116    :return: The Drawn Overlay
117    """
118    landmarks = hip.landmarks
119
120    if not landmarks.fem_l:
121        return overlay
122
123    # Intersect and extend lines
124    intersection_left = smart_find_intersection(
125        landmarks.pel_l_o, landmarks.pel_l_i, landmarks.fem_l, landmarks.fem_r
126    )
127    intersection_right = smart_find_intersection(
128        landmarks.pel_r_o, landmarks.pel_r_i, landmarks.fem_l, landmarks.fem_r
129    )
130
131    new_H_line = extend_line(landmarks.fem_l, landmarks.fem_r, scale=1.2)
132    new_A_line = extend_line(
133        landmarks.pel_l_o, intersection_left, scale=1.3, direction="up"
134    )
135    new_B_line = extend_line(
136        landmarks.pel_r_o, intersection_right, scale=1.3, direction="up"
137    )
138
139    overlay.draw_lines([new_H_line, new_A_line, new_B_line])
140
141    # Draw ace_index_left and ace_index_right
142    overlay.draw_text(
143        f"ACE Index: {hip.metrics[0].value}",
144        landmarks.fem_l[0] - 150,
145        (landmarks.fem_l[1] + 10),
146    )
147
148    overlay.draw_text(
149        f"ACE Index: {hip.metrics[1].value}",
150        landmarks.fem_r[0] - 100,
151        (landmarks.fem_r[1] + 10),
152    )
153
154    return overlay
def find_ace(landmarks: retuve.hip_xray.classes.LandmarksXRay) -> tuple[float, float]:
30def find_ace(landmarks: LandmarksXRay) -> tuple[float, float]:
31    """
32    Calculate the Acentabular Index (ACE Index) for both hips.
33
34    :param landmarks: The Landmarks of the Hip
35    :return: The ACE Index for both hips
36    """
37    intersection_left = smart_find_intersection(
38        landmarks.pel_l_o, landmarks.pel_l_i, landmarks.fem_l, landmarks.fem_r
39    )
40    intersection_right = smart_find_intersection(
41        landmarks.pel_r_o, landmarks.pel_r_i, landmarks.fem_l, landmarks.fem_r
42    )
43
44    A, B, C = (
45        np.array(landmarks.pel_l_o),
46        np.array(landmarks.pel_l_i),
47        np.array(intersection_right),
48    )
49    AB = np.linalg.norm(A - B)
50    BC = np.linalg.norm(B - C)
51    AC = np.linalg.norm(A - C)
52
53    ace_index_left = np.arccos((BC**2 + AB**2 - AC**2) / (2 * BC * AB))
54
55    ace_index_left = round(180 - np.degrees(ace_index_left), 1)
56
57    A, B, C = (
58        np.array(landmarks.pel_r_o),
59        np.array(landmarks.pel_r_i),
60        np.array(intersection_left),
61    )
62    AB = np.linalg.norm(A - B)
63    BC = np.linalg.norm(B - C)
64    AC = np.linalg.norm(A - C)
65
66    ace_index_right = np.arccos((BC**2 + AB**2 - AC**2) / (2 * BC * AB))
67
68    ace_index_right = round(180 - np.degrees(ace_index_right), 1)
69
70    if not (5 < ace_index_left < 50):
71        ace_index_left = 0
72
73    if not (5 < ace_index_right < 50):
74        ace_index_right = 0
75
76    return ace_index_left, ace_index_right

Calculate the Acentabular Index (ACE Index) for both hips.

Parameters
  • landmarks: The Landmarks of the Hip
Returns

The ACE Index for both hips

def extend_line( p1: list, p2: list, scale: float = 1.2, direction: Literal['both', 'up', 'down'] = 'both'):
 79def extend_line(
 80    p1: list,
 81    p2: list,
 82    scale: float = 1.2,
 83    direction: Literal["both", "up", "down"] = "both",
 84):
 85    """
 86    Extend the line from p1 to p2 by a scale factor (default 10%)
 87    in specified direction ('up', 'down', 'both').
 88
 89    :param p1: The first point of the line
 90    :param p2: The second point of the line
 91    :param scale: The scale factor
 92    :param direction: The direction to extend the lines
 93    """
 94    direction_vector = np.array(p2) - np.array(p1)
 95    left_point = (
 96        np.array(p1) - direction_vector * (scale - 1)
 97        if direction in ["up", "both"]
 98        else np.array(p1)
 99    )
100    right_point = (
101        np.array(p2) + direction_vector * (scale - 1)
102        if direction in ["down", "both"]
103        else np.array(p2)
104    )
105
106    return left_point, right_point

Extend the line from p1 to p2 by a scale factor (default 10%) in specified direction ('up', 'down', 'both').

Parameters
  • p1: The first point of the line
  • p2: The second point of the line
  • scale: The scale factor
  • direction: The direction to extend the lines
109def draw_ace(hip: HipDataXray, overlay: Overlay, config: Config):
110    """
111    Draw the Acentabular Index (ACE Index) on the X-Ray.
112
113    :param hip: The Hip Data
114    :param overlay: The Overlay
115    :param config: The Config
116
117    :return: The Drawn Overlay
118    """
119    landmarks = hip.landmarks
120
121    if not landmarks.fem_l:
122        return overlay
123
124    # Intersect and extend lines
125    intersection_left = smart_find_intersection(
126        landmarks.pel_l_o, landmarks.pel_l_i, landmarks.fem_l, landmarks.fem_r
127    )
128    intersection_right = smart_find_intersection(
129        landmarks.pel_r_o, landmarks.pel_r_i, landmarks.fem_l, landmarks.fem_r
130    )
131
132    new_H_line = extend_line(landmarks.fem_l, landmarks.fem_r, scale=1.2)
133    new_A_line = extend_line(
134        landmarks.pel_l_o, intersection_left, scale=1.3, direction="up"
135    )
136    new_B_line = extend_line(
137        landmarks.pel_r_o, intersection_right, scale=1.3, direction="up"
138    )
139
140    overlay.draw_lines([new_H_line, new_A_line, new_B_line])
141
142    # Draw ace_index_left and ace_index_right
143    overlay.draw_text(
144        f"ACE Index: {hip.metrics[0].value}",
145        landmarks.fem_l[0] - 150,
146        (landmarks.fem_l[1] + 10),
147    )
148
149    overlay.draw_text(
150        f"ACE Index: {hip.metrics[1].value}",
151        landmarks.fem_r[0] - 100,
152        (landmarks.fem_r[1] + 10),
153    )
154
155    return overlay

Draw the Acentabular Index (ACE Index) on the X-Ray.

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

The Drawn Overlay