retuve.hip_us.metrics.coverage
Metric: Coverage
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: Coverage 17""" 18 19import numpy as np 20from networkx import diameter 21from radstract.math import smart_find_intersection 22 23from retuve.classes.draw import Overlay 24from retuve.classes.seg import SegObject 25from retuve.hip_us.classes.general import HipDataUS, LandmarksUS 26from retuve.keyphrases.config import Config 27from retuve.keyphrases.enums import MetricUS 28 29FEM_HEAD_SCALE_FACTOR = 0.18 30 31 32def find_cov_landmarks( 33 femoral: SegObject, landmarks: LandmarksUS, config: Config 34) -> LandmarksUS: 35 """ 36 Algorithm to find the landmarks for the Coverage metric. 37 38 :param femoral: SegObject: The femoral SegObject. 39 :param landmarks: LandmarksUS: The landmarks object. 40 :param config: Config: The Config 41 42 :return: LandmarksUS: The updated landmarks object. 43 """ 44 if not ( 45 femoral 46 and any(m in config.hip.measurements for m in MetricUS.ALL()) 47 and femoral.points is not None 48 and landmarks.apex is not None 49 ): 50 return landmarks 51 52 points = femoral.points 53 54 # Find the most right and left points 55 most_right_point = max(points, key=lambda x: x[0]) 56 most_left_point = min(points, key=lambda x: x[0]) 57 58 top_most_point = min(points, key=lambda x: x[1]) 59 bottom_most_point = max(points, key=lambda x: x[1]) 60 61 # find diameter using right-left and top-bottom 62 diameter_1 = abs(most_right_point[0] - most_left_point[0]) 63 diameter_2 = abs(bottom_most_point[1] - top_most_point[1]) 64 65 if diameter_1 == 0 or diameter_2 == 0: 66 return landmarks 67 68 # Reject frames with a non-circular femoral head 69 if abs((diameter_1 - diameter_2) / diameter_1) > 0.35: 70 return landmarks 71 72 diameter = diameter_2 73 74 # take half of the diameter 75 radius = diameter / 2 76 77 center = ( 78 int( 79 (abs(most_left_point[0] - most_right_point[0]) / 2) 80 + most_left_point[0] 81 ), 82 int(top_most_point[1] + radius), 83 ) 84 85 # find the line from landmarks left to apex 86 m = (landmarks.apex[1] - landmarks.left[1]) / ( 87 landmarks.apex[0] - landmarks.left[0] 88 ) 89 90 radius = radius + radius * config.hip.fem_extention 91 92 if m == 0: 93 point_above = (center[0], center[1] + radius) 94 point_below = (center[0], center[1] - radius) 95 else: 96 m_orth = -1 / m 97 # equation of orthogonal line: y = m_orth * (x - center[0]) + center[1] 98 # solve for x using the circle equation (x-center[0])**2 + (y-center[1])**2 = radius**2 99 100 # break the radius into two parts, based on the 101 # sin and cos of the angle between the m_orth and the x-axis 102 angle = np.degrees(np.arctan(m_orth)) 103 104 # break the radius into two parts, based on the 105 # sin and cos of the angle between the m_orth and the x-axis 106 radius_x = radius * np.cos(np.radians(angle)) 107 radius_y = radius * np.sin(np.radians(angle)) 108 109 point_above = ( 110 int(center[0] + radius_x), 111 int(center[1] + radius_y), 112 ) 113 114 point_below = ( 115 int(center[0] - radius_x), 116 int(center[1] - radius_y), 117 ) 118 119 # ensure that point_above and point_below are the right way round 120 # (Larger y represents down) 121 if point_above[1] > point_below[1]: 122 point_above, point_below = point_below, point_above 123 124 mid_cov_point = smart_find_intersection( 125 landmarks.apex, 126 landmarks.left, 127 point_above, 128 point_below, 129 ) 130 131 landmarks.point_d = point_above 132 landmarks.point_D = point_below 133 landmarks.mid_cov_point = mid_cov_point 134 135 return landmarks 136 137 138def find_coverage(landmarks: LandmarksUS) -> float: 139 """ 140 Calculate the Coverage metric. 141 142 :param landmarks: LandmarksUS: The landmarks object. 143 144 :return: float: The Coverage metric. 145 """ 146 if not ( 147 landmarks 148 and landmarks.mid_cov_point 149 and landmarks.point_D 150 and landmarks.point_d 151 ): 152 return 0 153 154 coverage = abs(landmarks.mid_cov_point[1] - landmarks.point_D[1]) / abs( 155 landmarks.point_D[1] - landmarks.point_d[1] 156 ) 157 158 # if the mid_point is above the point_D, then the coverage is 0 159 if landmarks.mid_cov_point[1] > landmarks.point_D[1]: 160 coverage = 0 161 162 return round(coverage, 3) 163 164 165def draw_coverage(hip: HipDataUS, overlay: Overlay, config: Config) -> Overlay: 166 """ 167 Draw the Coverage metric on the Overlay. 168 169 :param hip: HipDataUS: The Hip Data. 170 :param overlay: Overlay: The Overlay. 171 :param config: Config: The Config. 172 173 :return: Overlay: The updated Overlay. 174 """ 175 coverage = hip.get_metric(MetricUS.COVERAGE) 176 if coverage != 0: 177 overlay.draw_lines( 178 ((hip.landmarks.point_D, hip.landmarks.point_d),), 179 ) 180 181 xl, yl = hip.landmarks.mid_cov_point 182 183 if config.visuals.display_full_metric_names: 184 title = "Coverage" 185 else: 186 title = "cov" 187 188 overlay.draw_text( 189 f"{title}: {coverage:.2f}", 190 int(xl + 10), 191 int(yl - 30), 192 header="h2", 193 ) 194 195 return overlay 196 197 198def bad_coverage(hip: HipDataUS) -> bool: 199 """ 200 Check if the Coverage is bad. 201 202 :param hip: HipDataUS: The Hip Data. 203 204 :return: bool: True if the Coverage is bad. 205 """ 206 207 if ( 208 hip.get_metric(MetricUS.COVERAGE) < 0 209 or hip.get_metric(MetricUS.COVERAGE) > 1 210 ): 211 return True 212 213 return False
FEM_HEAD_SCALE_FACTOR =
0.18
def
find_cov_landmarks( femoral: retuve.classes.seg.SegObject, landmarks: retuve.hip_us.classes.general.LandmarksUS, config: retuve.keyphrases.config.Config) -> retuve.hip_us.classes.general.LandmarksUS:
33def find_cov_landmarks( 34 femoral: SegObject, landmarks: LandmarksUS, config: Config 35) -> LandmarksUS: 36 """ 37 Algorithm to find the landmarks for the Coverage metric. 38 39 :param femoral: SegObject: The femoral 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 femoral 47 and any(m in config.hip.measurements for m in MetricUS.ALL()) 48 and femoral.points is not None 49 and landmarks.apex is not None 50 ): 51 return landmarks 52 53 points = femoral.points 54 55 # Find the most right and left points 56 most_right_point = max(points, key=lambda x: x[0]) 57 most_left_point = min(points, key=lambda x: x[0]) 58 59 top_most_point = min(points, key=lambda x: x[1]) 60 bottom_most_point = max(points, key=lambda x: x[1]) 61 62 # find diameter using right-left and top-bottom 63 diameter_1 = abs(most_right_point[0] - most_left_point[0]) 64 diameter_2 = abs(bottom_most_point[1] - top_most_point[1]) 65 66 if diameter_1 == 0 or diameter_2 == 0: 67 return landmarks 68 69 # Reject frames with a non-circular femoral head 70 if abs((diameter_1 - diameter_2) / diameter_1) > 0.35: 71 return landmarks 72 73 diameter = diameter_2 74 75 # take half of the diameter 76 radius = diameter / 2 77 78 center = ( 79 int( 80 (abs(most_left_point[0] - most_right_point[0]) / 2) 81 + most_left_point[0] 82 ), 83 int(top_most_point[1] + radius), 84 ) 85 86 # find the line from landmarks left to apex 87 m = (landmarks.apex[1] - landmarks.left[1]) / ( 88 landmarks.apex[0] - landmarks.left[0] 89 ) 90 91 radius = radius + radius * config.hip.fem_extention 92 93 if m == 0: 94 point_above = (center[0], center[1] + radius) 95 point_below = (center[0], center[1] - radius) 96 else: 97 m_orth = -1 / m 98 # equation of orthogonal line: y = m_orth * (x - center[0]) + center[1] 99 # solve for x using the circle equation (x-center[0])**2 + (y-center[1])**2 = radius**2 100 101 # break the radius into two parts, based on the 102 # sin and cos of the angle between the m_orth and the x-axis 103 angle = np.degrees(np.arctan(m_orth)) 104 105 # break the radius into two parts, based on the 106 # sin and cos of the angle between the m_orth and the x-axis 107 radius_x = radius * np.cos(np.radians(angle)) 108 radius_y = radius * np.sin(np.radians(angle)) 109 110 point_above = ( 111 int(center[0] + radius_x), 112 int(center[1] + radius_y), 113 ) 114 115 point_below = ( 116 int(center[0] - radius_x), 117 int(center[1] - radius_y), 118 ) 119 120 # ensure that point_above and point_below are the right way round 121 # (Larger y represents down) 122 if point_above[1] > point_below[1]: 123 point_above, point_below = point_below, point_above 124 125 mid_cov_point = smart_find_intersection( 126 landmarks.apex, 127 landmarks.left, 128 point_above, 129 point_below, 130 ) 131 132 landmarks.point_d = point_above 133 landmarks.point_D = point_below 134 landmarks.mid_cov_point = mid_cov_point 135 136 return landmarks
Algorithm to find the landmarks for the Coverage metric.
Parameters
- femoral: SegObject: The femoral SegObject.
- landmarks: LandmarksUS: The landmarks object.
- config: Config: The Config
Returns
LandmarksUS: The updated landmarks object.
139def find_coverage(landmarks: LandmarksUS) -> float: 140 """ 141 Calculate the Coverage metric. 142 143 :param landmarks: LandmarksUS: The landmarks object. 144 145 :return: float: The Coverage metric. 146 """ 147 if not ( 148 landmarks 149 and landmarks.mid_cov_point 150 and landmarks.point_D 151 and landmarks.point_d 152 ): 153 return 0 154 155 coverage = abs(landmarks.mid_cov_point[1] - landmarks.point_D[1]) / abs( 156 landmarks.point_D[1] - landmarks.point_d[1] 157 ) 158 159 # if the mid_point is above the point_D, then the coverage is 0 160 if landmarks.mid_cov_point[1] > landmarks.point_D[1]: 161 coverage = 0 162 163 return round(coverage, 3)
Calculate the Coverage metric.
Parameters
- landmarks: LandmarksUS: The landmarks object.
Returns
float: The Coverage metric.
def
draw_coverage( hip: retuve.hip_us.classes.general.HipDataUS, overlay: retuve.classes.draw.Overlay, config: retuve.keyphrases.config.Config) -> retuve.classes.draw.Overlay:
166def draw_coverage(hip: HipDataUS, overlay: Overlay, config: Config) -> Overlay: 167 """ 168 Draw the Coverage metric on the Overlay. 169 170 :param hip: HipDataUS: The Hip Data. 171 :param overlay: Overlay: The Overlay. 172 :param config: Config: The Config. 173 174 :return: Overlay: The updated Overlay. 175 """ 176 coverage = hip.get_metric(MetricUS.COVERAGE) 177 if coverage != 0: 178 overlay.draw_lines( 179 ((hip.landmarks.point_D, hip.landmarks.point_d),), 180 ) 181 182 xl, yl = hip.landmarks.mid_cov_point 183 184 if config.visuals.display_full_metric_names: 185 title = "Coverage" 186 else: 187 title = "cov" 188 189 overlay.draw_text( 190 f"{title}: {coverage:.2f}", 191 int(xl + 10), 192 int(yl - 30), 193 header="h2", 194 ) 195 196 return overlay
Draw the Coverage metric on the Overlay.
Parameters
- hip: HipDataUS: The Hip Data.
- overlay: Overlay: The Overlay.
- config: Config: The Config.
Returns
Overlay: The updated Overlay.
199def bad_coverage(hip: HipDataUS) -> bool: 200 """ 201 Check if the Coverage is bad. 202 203 :param hip: HipDataUS: The Hip Data. 204 205 :return: bool: True if the Coverage is bad. 206 """ 207 208 if ( 209 hip.get_metric(MetricUS.COVERAGE) < 0 210 or hip.get_metric(MetricUS.COVERAGE) > 1 211 ): 212 return True 213 214 return False
Check if the Coverage is bad.
Parameters
- hip: HipDataUS: The Hip Data.
Returns
bool: True if the Coverage is bad.