retuve.hip_us.draw
All unique drawing functions for Hip Ultrasound
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""" 16All unique drawing functions for Hip Ultrasound 17""" 18 19import io 20import textwrap 21import time 22from typing import List, Tuple 23 24import cv2 25import numpy as np 26import plotly.graph_objects as go 27from attr import has 28from PIL import Image, ImageOps 29from radstract.data.nifti import NIFTI, convert_images_to_nifti_labels 30 31from retuve.classes.draw import Overlay 32from retuve.classes.seg import SegFrameObjects 33from retuve.draw import (TARGET_SIZE, draw_landmarks, draw_seg, 34 resize_data_for_display) 35from retuve.hip_us.classes.enums import Side 36from retuve.hip_us.classes.general import HipDatasUS, HipDataUS 37from retuve.hip_us.handlers.side import get_side_metainfo 38from retuve.hip_us.metrics.alpha import draw_alpha 39from retuve.hip_us.metrics.coverage import draw_coverage 40from retuve.hip_us.metrics.curvature import draw_curvature 41from retuve.hip_us.multiframe import FemSphere 42from retuve.hip_us.multiframe.models import circle_radius_at_z 43from retuve.keyphrases.config import Config 44from retuve.logs import log_timings, ulogger 45 46 47def draw_fem_head( 48 hip: HipDataUS, fem_sph: FemSphere, overlay: Overlay, z_gap: float 49) -> Overlay: 50 """ 51 Draw the femoral head on the image 52 53 :param hip: The Hip Datas 54 :param fem_sph: The Femoral Sphere 55 :param overlay: The Overlay to draw on 56 57 :return: The Drawn Overlay 58 """ 59 # Get radius at z 60 radius = circle_radius_at_z( 61 fem_sph.radius, fem_sph.center[2], z_gap * hip.frame_no 62 ) 63 64 # draw the circle 65 overlay.draw_circle((fem_sph.center[0], fem_sph.center[1]), radius) 66 67 return overlay 68 69 70def draw_hips_us( 71 hip_datas: HipDatasUS, 72 results: List[SegFrameObjects], 73 fem_sph: FemSphere, 74 config: Config, 75) -> tuple[List[np.ndarray], NIFTI]: 76 """ 77 Draw the hip ultrasound images 78 79 :param hip_datas: The Hip Datas 80 :param results: The Segmentation Results 81 :param fem_sph: The Femoral Sphere 82 :param config: The Config 83 84 :return: The Drawn Images 85 """ 86 draw_timings = [] 87 image_arrays = [] 88 nifti_frames = [] 89 nifti = None 90 91 for i, (hip, seg_frame_objs) in enumerate(zip(hip_datas, results)): 92 start = time.time() 93 94 final_hip, final_seg_frame_objs, final_image = resize_data_for_display( 95 hip, seg_frame_objs 96 ) 97 98 overlay = Overlay( 99 (final_image.shape[0], final_image.shape[1], 3), config 100 ) 101 102 overlay = draw_seg(final_seg_frame_objs, overlay, config) 103 104 overlay = draw_landmarks(final_hip, overlay) 105 106 overlay = draw_alpha(final_hip, overlay, config) 107 overlay = draw_coverage(final_hip, overlay, config) 108 overlay = draw_curvature(final_hip, overlay, config) 109 110 if fem_sph and config.hip.display_fem_guess: 111 overlay = draw_fem_head( 112 final_hip, 113 fem_sph, 114 overlay, 115 config.hip.z_gap, 116 ) 117 118 graf_conf = None 119 if hasattr(hip_datas, "graf_confs"): 120 graf_conf = hip_datas.graf_confs[hip.frame_no] 121 122 overlay, is_graf = draw_other( 123 final_hip, 124 final_seg_frame_objs, 125 hip_datas.graf_frame, 126 overlay, 127 final_image.shape[:2], 128 config, 129 graf_conf, 130 ) 131 132 if config.hip.display_bad_frame_reasons and hasattr( 133 hip_datas, "bad_frame_reasons" 134 ): 135 if hip.frame_no in hip_datas.bad_frame_reasons: 136 overlay.draw_text( 137 hip_datas.bad_frame_reasons[hip.frame_no], 138 final_image.shape[1] // 2, 139 final_image.shape[0] - 100, 140 header="h2", 141 ) 142 143 img = overlay.apply_to_image(final_image) 144 145 if config.seg_export: 146 original_image = seg_frame_objs.img 147 test = overlay.get_nifti_frame( 148 seg_frame_objs, 149 # NOTE(sharpz7) I do not know why this needs to be reversed 150 (original_image.shape[1], original_image.shape[0]), 151 ) 152 nifti_frames.append(test) 153 154 # if its the graf frame, append 5 copies 155 repeats = len(results) // 6 if is_graf else 1 156 for _ in range(repeats): 157 image_arrays.append(img) 158 159 draw_timings.append(time.time() - start) 160 161 start = time.time() 162 if config.seg_export: 163 frames = nifti_frames 164 165 if "Swapped Post and Ant" in hip_datas.recorded_error.errors: 166 frames = nifti_frames[::-1] 167 168 # convert to nifti 169 nifti = convert_images_to_nifti_labels(frames) 170 else: 171 nifti = None 172 173 if (time.time() - start) > 0.1: 174 log_timings([time.time() - start], title="Nifti Conversion Speed:") 175 176 log_timings(draw_timings, title="Drawing Speed:") 177 178 return image_arrays, nifti 179 180 181def draw_other( 182 hip: HipDataUS, 183 seg_frame_objs: SegFrameObjects, 184 graf_frame: int, 185 overlay: Overlay, 186 shape: tuple, 187 config: Config, 188 graf_conf: float = None, 189) -> Tuple[Overlay, bool]: 190 """ 191 Draw the other meta information on the image 192 193 :param hip: The Hip Data 194 :param seg_frame_objs: The Segmentation Frame Objects 195 :param graf_frame: The Graf Frame 196 :param overlay: The Overlay 197 :param config: The Config 198 :param shape: The Shape of the Image 199 200 :return: The Drawn Overlay 201 """ 202 203 if config.hip.display_frame_no: 204 # Draw Frame No 205 overlay.draw_text( 206 f"Frame: {hip.frame_no}", 207 0, 208 0, 209 header="h1", 210 ) 211 212 if graf_conf is not None and config.hip.display_graf_conf: 213 overlay.draw_text( 214 f"Graf Confidence: {graf_conf:.2f}", 215 shape[0] - 100, 216 0, 217 header="h1", 218 ) 219 220 is_graf = hip.frame_no == graf_frame and hip.landmarks is not None 221 222 # Check if graf alpha 223 if is_graf: 224 overlay.draw_text( 225 f"Grafs Frame", 226 int(hip.landmarks.left[0]), 227 int(hip.landmarks.left[1] - 120), 228 header="h2", 229 grafs=True, 230 ) 231 232 # add a border 233 overlay.draw_box( 234 (0, 0, shape[1], shape[0]), 235 grafs=True, 236 ) 237 238 if hip.landmarks and hip.landmarks.right and config.hip.display_side: 239 xl, yl = hip.landmarks.right 240 overlay.draw_text( 241 f"side: {Side.get_name(hip.side)}", 242 int(xl), 243 int(yl), 244 header="h2", 245 ) 246 247 if config.hip.draw_midline: 248 for seg_obj in seg_frame_objs: 249 if seg_obj.midline is not None: 250 overlay.draw_skeleton(seg_obj.midline) 251 252 if seg_obj.midline_moved is not None: 253 overlay.draw_skeleton(seg_obj.midline_moved) 254 255 if config.hip.draw_side_metainfo: 256 closest_illium, mid = get_side_metainfo(hip, seg_frame_objs) 257 # these are points, draw them 258 if closest_illium is not None: 259 overlay.draw_cross(closest_illium) 260 overlay.draw_cross(mid) 261 262 return overlay, is_graf 263 264 265def draw_table(shape: tuple, hip_datas: HipDatasUS) -> np.ndarray: 266 """ 267 Draw the table of the metrics onto an image 268 269 :param shape: The Shape of the Image 270 :param hip_datas: The Hip Datas 271 272 :return: The Image with the Table and any errors 273 """ 274 start = time.time() 275 276 # Create empty image with the specified shape 277 empty_img = np.zeros((shape[1], shape[0], 3), dtype=np.uint8) 278 279 # Find new shape by running 1024 algo 280 shape = ImageOps.contain(Image.fromarray(empty_img), (TARGET_SIZE)).size[ 281 :2 282 ] 283 284 headers = [""] + hip_datas.metrics[0].names() 285 values = [] 286 287 for metrics in reversed(hip_datas.metrics): 288 values.append(metrics.dump()) 289 290 # Rotate and transpose the values list 291 values = list(zip(*values[::-1])) 292 293 # Create a Plotly figure for the table 294 fig = go.Figure( 295 data=[ 296 go.Table( 297 header=dict(values=headers, font=dict(size=18), height=50), 298 cells=dict( 299 values=values, 300 fill=dict(color=["paleturquoise", "white"]), 301 font=dict(size=18), 302 height=50, 303 ), 304 ) 305 ] 306 ) 307 308 MARGIN = 30 309 fig.update_layout( 310 autosize=False, 311 width=shape[1], 312 height=shape[0], 313 margin=dict(l=MARGIN, r=MARGIN, b=MARGIN, t=MARGIN), 314 ) 315 316 # Save the figure as bytes 317 img_bytes = fig.to_image(format="png", width=shape[1], height=shape[0]) 318 319 # Convert image bytes to numpy array 320 data_image = np.array(Image.open(io.BytesIO(img_bytes)).convert("RGB")) 321 322 # Text Wrapping Logic 323 recorded_error_text = str(hip_datas.recorded_error) 324 325 # Set the maximum width for the text box in pixels 326 max_text_width = data_image.shape[1] - 20 # Margin of 10px from both sides 327 font_scale = 0.8 328 font = cv2.FONT_HERSHEY_SIMPLEX 329 font_thickness = 2 330 331 # Estimate the width of each character based on the font 332 (text_width, text_height), _ = cv2.getTextSize( 333 recorded_error_text, font, font_scale, font_thickness 334 ) 335 char_width = ( 336 text_width // len(recorded_error_text) if recorded_error_text else 1 337 ) # Estimate width of one char 338 339 # Wrap the text based on character width and max text width 340 wrap_width = max_text_width // char_width 341 wrapped_text = textwrap.fill(recorded_error_text, width=wrap_width) 342 343 # Split the wrapped text into lines 344 lines = wrapped_text.split("\n") 345 346 # Draw each line of wrapped text 347 y = data_image.shape[0] - 300 # Starting y position 348 line_height = text_height + 10 # Spacing between lines 349 for line in lines: 350 # Put each line of text on the image 351 cv2.putText( 352 data_image, 353 line, 354 (10, y), 355 font, 356 font_scale, 357 (0, 0, 0), 358 font_thickness, 359 ) 360 y += line_height # Move down for the next line 361 362 # Log the time taken to draw the table 363 ulogger.info(f"Table Drawing Time: {time.time() - start:.2f}s") 364 365 return data_image
def
draw_fem_head( hip: retuve.hip_us.classes.general.HipDataUS, fem_sph: retuve.hip_us.multiframe.main.FemSphere, overlay: retuve.classes.draw.Overlay, z_gap: float) -> retuve.classes.draw.Overlay:
48def draw_fem_head( 49 hip: HipDataUS, fem_sph: FemSphere, overlay: Overlay, z_gap: float 50) -> Overlay: 51 """ 52 Draw the femoral head on the image 53 54 :param hip: The Hip Datas 55 :param fem_sph: The Femoral Sphere 56 :param overlay: The Overlay to draw on 57 58 :return: The Drawn Overlay 59 """ 60 # Get radius at z 61 radius = circle_radius_at_z( 62 fem_sph.radius, fem_sph.center[2], z_gap * hip.frame_no 63 ) 64 65 # draw the circle 66 overlay.draw_circle((fem_sph.center[0], fem_sph.center[1]), radius) 67 68 return overlay
Draw the femoral head on the image
Parameters
- hip: The Hip Datas
- fem_sph: The Femoral Sphere
- overlay: The Overlay to draw on
Returns
The Drawn Overlay
def
draw_hips_us( hip_datas: retuve.hip_us.classes.general.HipDatasUS, results: List[retuve.classes.seg.SegFrameObjects], fem_sph: retuve.hip_us.multiframe.main.FemSphere, config: retuve.keyphrases.config.Config) -> tuple[typing.List[numpy.ndarray], radstract.data.nifti.main.NIFTI]:
71def draw_hips_us( 72 hip_datas: HipDatasUS, 73 results: List[SegFrameObjects], 74 fem_sph: FemSphere, 75 config: Config, 76) -> tuple[List[np.ndarray], NIFTI]: 77 """ 78 Draw the hip ultrasound images 79 80 :param hip_datas: The Hip Datas 81 :param results: The Segmentation Results 82 :param fem_sph: The Femoral Sphere 83 :param config: The Config 84 85 :return: The Drawn Images 86 """ 87 draw_timings = [] 88 image_arrays = [] 89 nifti_frames = [] 90 nifti = None 91 92 for i, (hip, seg_frame_objs) in enumerate(zip(hip_datas, results)): 93 start = time.time() 94 95 final_hip, final_seg_frame_objs, final_image = resize_data_for_display( 96 hip, seg_frame_objs 97 ) 98 99 overlay = Overlay( 100 (final_image.shape[0], final_image.shape[1], 3), config 101 ) 102 103 overlay = draw_seg(final_seg_frame_objs, overlay, config) 104 105 overlay = draw_landmarks(final_hip, overlay) 106 107 overlay = draw_alpha(final_hip, overlay, config) 108 overlay = draw_coverage(final_hip, overlay, config) 109 overlay = draw_curvature(final_hip, overlay, config) 110 111 if fem_sph and config.hip.display_fem_guess: 112 overlay = draw_fem_head( 113 final_hip, 114 fem_sph, 115 overlay, 116 config.hip.z_gap, 117 ) 118 119 graf_conf = None 120 if hasattr(hip_datas, "graf_confs"): 121 graf_conf = hip_datas.graf_confs[hip.frame_no] 122 123 overlay, is_graf = draw_other( 124 final_hip, 125 final_seg_frame_objs, 126 hip_datas.graf_frame, 127 overlay, 128 final_image.shape[:2], 129 config, 130 graf_conf, 131 ) 132 133 if config.hip.display_bad_frame_reasons and hasattr( 134 hip_datas, "bad_frame_reasons" 135 ): 136 if hip.frame_no in hip_datas.bad_frame_reasons: 137 overlay.draw_text( 138 hip_datas.bad_frame_reasons[hip.frame_no], 139 final_image.shape[1] // 2, 140 final_image.shape[0] - 100, 141 header="h2", 142 ) 143 144 img = overlay.apply_to_image(final_image) 145 146 if config.seg_export: 147 original_image = seg_frame_objs.img 148 test = overlay.get_nifti_frame( 149 seg_frame_objs, 150 # NOTE(sharpz7) I do not know why this needs to be reversed 151 (original_image.shape[1], original_image.shape[0]), 152 ) 153 nifti_frames.append(test) 154 155 # if its the graf frame, append 5 copies 156 repeats = len(results) // 6 if is_graf else 1 157 for _ in range(repeats): 158 image_arrays.append(img) 159 160 draw_timings.append(time.time() - start) 161 162 start = time.time() 163 if config.seg_export: 164 frames = nifti_frames 165 166 if "Swapped Post and Ant" in hip_datas.recorded_error.errors: 167 frames = nifti_frames[::-1] 168 169 # convert to nifti 170 nifti = convert_images_to_nifti_labels(frames) 171 else: 172 nifti = None 173 174 if (time.time() - start) > 0.1: 175 log_timings([time.time() - start], title="Nifti Conversion Speed:") 176 177 log_timings(draw_timings, title="Drawing Speed:") 178 179 return image_arrays, nifti
Draw the hip ultrasound images
Parameters
- hip_datas: The Hip Datas
- results: The Segmentation Results
- fem_sph: The Femoral Sphere
- config: The Config
Returns
The Drawn Images
def
draw_other( hip: retuve.hip_us.classes.general.HipDataUS, seg_frame_objs: retuve.classes.seg.SegFrameObjects, graf_frame: int, overlay: retuve.classes.draw.Overlay, shape: tuple, config: retuve.keyphrases.config.Config, graf_conf: float = None) -> Tuple[retuve.classes.draw.Overlay, bool]:
182def draw_other( 183 hip: HipDataUS, 184 seg_frame_objs: SegFrameObjects, 185 graf_frame: int, 186 overlay: Overlay, 187 shape: tuple, 188 config: Config, 189 graf_conf: float = None, 190) -> Tuple[Overlay, bool]: 191 """ 192 Draw the other meta information on the image 193 194 :param hip: The Hip Data 195 :param seg_frame_objs: The Segmentation Frame Objects 196 :param graf_frame: The Graf Frame 197 :param overlay: The Overlay 198 :param config: The Config 199 :param shape: The Shape of the Image 200 201 :return: The Drawn Overlay 202 """ 203 204 if config.hip.display_frame_no: 205 # Draw Frame No 206 overlay.draw_text( 207 f"Frame: {hip.frame_no}", 208 0, 209 0, 210 header="h1", 211 ) 212 213 if graf_conf is not None and config.hip.display_graf_conf: 214 overlay.draw_text( 215 f"Graf Confidence: {graf_conf:.2f}", 216 shape[0] - 100, 217 0, 218 header="h1", 219 ) 220 221 is_graf = hip.frame_no == graf_frame and hip.landmarks is not None 222 223 # Check if graf alpha 224 if is_graf: 225 overlay.draw_text( 226 f"Grafs Frame", 227 int(hip.landmarks.left[0]), 228 int(hip.landmarks.left[1] - 120), 229 header="h2", 230 grafs=True, 231 ) 232 233 # add a border 234 overlay.draw_box( 235 (0, 0, shape[1], shape[0]), 236 grafs=True, 237 ) 238 239 if hip.landmarks and hip.landmarks.right and config.hip.display_side: 240 xl, yl = hip.landmarks.right 241 overlay.draw_text( 242 f"side: {Side.get_name(hip.side)}", 243 int(xl), 244 int(yl), 245 header="h2", 246 ) 247 248 if config.hip.draw_midline: 249 for seg_obj in seg_frame_objs: 250 if seg_obj.midline is not None: 251 overlay.draw_skeleton(seg_obj.midline) 252 253 if seg_obj.midline_moved is not None: 254 overlay.draw_skeleton(seg_obj.midline_moved) 255 256 if config.hip.draw_side_metainfo: 257 closest_illium, mid = get_side_metainfo(hip, seg_frame_objs) 258 # these are points, draw them 259 if closest_illium is not None: 260 overlay.draw_cross(closest_illium) 261 overlay.draw_cross(mid) 262 263 return overlay, is_graf
Draw the other meta information on the image
Parameters
- hip: The Hip Data
- seg_frame_objs: The Segmentation Frame Objects
- graf_frame: The Graf Frame
- overlay: The Overlay
- config: The Config
- shape: The Shape of the Image
Returns
The Drawn Overlay
def
draw_table( shape: tuple, hip_datas: retuve.hip_us.classes.general.HipDatasUS) -> numpy.ndarray:
266def draw_table(shape: tuple, hip_datas: HipDatasUS) -> np.ndarray: 267 """ 268 Draw the table of the metrics onto an image 269 270 :param shape: The Shape of the Image 271 :param hip_datas: The Hip Datas 272 273 :return: The Image with the Table and any errors 274 """ 275 start = time.time() 276 277 # Create empty image with the specified shape 278 empty_img = np.zeros((shape[1], shape[0], 3), dtype=np.uint8) 279 280 # Find new shape by running 1024 algo 281 shape = ImageOps.contain(Image.fromarray(empty_img), (TARGET_SIZE)).size[ 282 :2 283 ] 284 285 headers = [""] + hip_datas.metrics[0].names() 286 values = [] 287 288 for metrics in reversed(hip_datas.metrics): 289 values.append(metrics.dump()) 290 291 # Rotate and transpose the values list 292 values = list(zip(*values[::-1])) 293 294 # Create a Plotly figure for the table 295 fig = go.Figure( 296 data=[ 297 go.Table( 298 header=dict(values=headers, font=dict(size=18), height=50), 299 cells=dict( 300 values=values, 301 fill=dict(color=["paleturquoise", "white"]), 302 font=dict(size=18), 303 height=50, 304 ), 305 ) 306 ] 307 ) 308 309 MARGIN = 30 310 fig.update_layout( 311 autosize=False, 312 width=shape[1], 313 height=shape[0], 314 margin=dict(l=MARGIN, r=MARGIN, b=MARGIN, t=MARGIN), 315 ) 316 317 # Save the figure as bytes 318 img_bytes = fig.to_image(format="png", width=shape[1], height=shape[0]) 319 320 # Convert image bytes to numpy array 321 data_image = np.array(Image.open(io.BytesIO(img_bytes)).convert("RGB")) 322 323 # Text Wrapping Logic 324 recorded_error_text = str(hip_datas.recorded_error) 325 326 # Set the maximum width for the text box in pixels 327 max_text_width = data_image.shape[1] - 20 # Margin of 10px from both sides 328 font_scale = 0.8 329 font = cv2.FONT_HERSHEY_SIMPLEX 330 font_thickness = 2 331 332 # Estimate the width of each character based on the font 333 (text_width, text_height), _ = cv2.getTextSize( 334 recorded_error_text, font, font_scale, font_thickness 335 ) 336 char_width = ( 337 text_width // len(recorded_error_text) if recorded_error_text else 1 338 ) # Estimate width of one char 339 340 # Wrap the text based on character width and max text width 341 wrap_width = max_text_width // char_width 342 wrapped_text = textwrap.fill(recorded_error_text, width=wrap_width) 343 344 # Split the wrapped text into lines 345 lines = wrapped_text.split("\n") 346 347 # Draw each line of wrapped text 348 y = data_image.shape[0] - 300 # Starting y position 349 line_height = text_height + 10 # Spacing between lines 350 for line in lines: 351 # Put each line of text on the image 352 cv2.putText( 353 data_image, 354 line, 355 (10, y), 356 font, 357 font_scale, 358 (0, 0, 0), 359 font_thickness, 360 ) 361 y += line_height # Move down for the next line 362 363 # Log the time taken to draw the table 364 ulogger.info(f"Table Drawing Time: {time.time() - start:.2f}s") 365 366 return data_image
Draw the table of the metrics onto an image
Parameters
- shape: The Shape of the Image
- hip_datas: The Hip Datas
Returns
The Image with the Table and any errors