retuve.hip_us.multiframe.models

All the code for building the different 3D Models and Visualizations is in this file.

  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 the code for building the different 3D Models and Visualizations is in this file.
 17"""
 18
 19import logging
 20from typing import List, Optional, Tuple
 21
 22import numpy as np
 23import open3d as o3d
 24import plotly.graph_objects as go
 25import trimesh
 26from numpy.typing import NDArray
 27from scipy.spatial import Delaunay, _qhull
 28
 29from retuve.classes.seg import SegFrameObjects, SegObject
 30from retuve.hip_us.classes.enums import HipLabelsUS, Side
 31from retuve.hip_us.classes.general import HipDatasUS
 32from retuve.hip_us.metrics.aca import ACA_COLORS, SideZ, Triangle
 33from retuve.hip_us.typing import CoordinatesArray3D, FemoralHeadSphere
 34from retuve.keyphrases.config import Config
 35
 36# Configure logging
 37logging.basicConfig(
 38    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
 39)
 40
 41
 42def ms(
 43    x: float, y: float, z: float, radius: float, resolution: int = 20
 44) -> FemoralHeadSphere:
 45    """
 46    Return the coordinates for plotting a sphere centered at (x,y,z)
 47
 48    :param x: The x-coordinate of the center of the sphere.
 49    :param y: The y-coordinate of the center of the sphere.
 50    :param z: The z-coordinate of the center of the sphere.
 51    :param radius: The radius of the sphere.
 52    :param resolution: The resolution of the sphere.
 53
 54    :return: The coordinates for plotting a sphere.
 55    """
 56    u, v = np.mgrid[
 57        0 : 2 * np.pi : resolution * 2j, 0 : np.pi : resolution * 1j
 58    ]
 59    X = radius * np.cos(u) * np.sin(v) + x
 60    Y = radius * np.sin(u) * np.sin(v) + y
 61    Z = radius * np.cos(v) + z
 62    return (X, Y, Z)
 63
 64
 65def build_3d_visual(
 66    illium_mesh: o3d.geometry.TriangleMesh,
 67    femoral_sphere: FemoralHeadSphere,
 68    avg_normals_data: CoordinatesArray3D,
 69    normals_data: List[Triangle],
 70    cr_points: List[CoordinatesArray3D],
 71    hip_datas: HipDatasUS,
 72    config: Config,
 73) -> go.Figure:
 74    """
 75    Build the 3D visual for the hip US module.
 76
 77    :param illium_mesh: The illium mesh.
 78    :param femoral_sphere: The femoral sphere.
 79    :param avg_normals_data: The average normals data of the ACA Vectors.
 80    :param normals_data: The normals data of the ACA Vectors.
 81    :param cr_points: The CR points.
 82    :param hip_datas: The hip data.
 83    :param config: The configuration object.
 84
 85    :return: The 3D visual.
 86    """
 87
 88    # Get vertices and faces
 89    vertices = np.asarray(illium_mesh.vertices)
 90    faces = np.asarray(illium_mesh.triangles)
 91
 92    com = np.mean(vertices, axis=0)
 93
 94    z_gap = config.hip.z_gap * (200 / len(hip_datas))
 95
 96    # Move the points to the center of mass
 97    vertices = vertices - com
 98
 99    # Create figure
100    fig = go.Figure(
101        data=[
102            go.Mesh3d(
103                x=vertices[:, 0],
104                y=vertices[:, 1],
105                z=vertices[:, 2],
106                i=faces[:, 0],
107                j=faces[:, 1],
108                k=faces[:, 2],
109                color="grey",
110                opacity=0.95,
111            )
112        ]
113    )
114
115    if femoral_sphere is not None:
116        fig.add_trace(
117            go.Surface(
118                x=femoral_sphere[0] - com[0],
119                y=femoral_sphere[1] - com[1],
120                z=femoral_sphere[2] - com[2],
121                opacity=0.2,
122                colorscale=[[0, "blue"], [1, "blue"]],
123                showscale=False,
124            )
125        )
126
127    if cr_points is not None:
128        fig.add_trace(
129            go.Scatter3d(
130                x=[point[0] - com[0] for point in cr_points],
131                y=[point[1] - com[1] for point in cr_points],
132                z=[point[2] - com[2] for point in cr_points],
133                mode="markers",
134                marker=dict(color="black", size=7),
135                showlegend=False,
136            )
137        )
138
139    # Red, Orange, Yellow, Blue, Cyan, Green
140    # [0, 2, 3, 2, 3, 3]
141    # https://github.com/plotly/plotly.js/issues/3613#issuecomment-1750709712
142    scaling_factors = {
143        "red": 0,
144        "orange": 2,
145        "yellow": 3,
146        "blue": 2,
147        "cyan": 3,
148        "lightgreen": 3,
149    }
150
151    for i, color in enumerate(ACA_COLORS.values()):
152        triangles = [
153            triangle for triangle in normals_data if triangle.color == color
154        ]
155        fig.add_trace(
156            go.Cone(
157                x=[triangle.centroid[0] - com[0] for triangle in triangles],
158                y=[triangle.centroid[1] - com[1] for triangle in triangles],
159                z=[triangle.centroid[2] - com[2] for triangle in triangles],
160                u=[triangle.normal[0] for triangle in triangles],
161                v=[triangle.normal[1] for triangle in triangles],
162                w=[triangle.normal[2] for triangle in triangles],
163                colorscale=[
164                    [0, color],
165                    [1, color],
166                ],
167                showscale=False,
168                sizemode="absolute",
169                sizeref=5 - scaling_factors[color],
170                anchor="tail",
171            )
172        )
173
174    for avg_normal, avg_centroid, color in avg_normals_data:
175        fig.add_trace(
176            go.Cone(
177                x=[avg_centroid[0] - com[0]],
178                y=[avg_centroid[1] - com[1]],
179                z=[avg_centroid[2] - com[2]],
180                u=[avg_normal[0]],
181                v=[avg_normal[1]],
182                w=[avg_normal[2]],
183                colorscale=[[0, color], [1, "black"]],
184                showscale=False,
185                sizemode="absolute",
186                sizeref=32,
187                anchor="tail",
188            )
189        )
190
191    illium_landmarks = []
192    fem_landmarks = []
193    for hip in hip_datas:
194        landmark = hip.landmarks
195        if landmark is None:
196            continue
197
198        z_pos = z_gap * hip.frame_no
199        for point in ["left", "right", "apex"]:
200            illium_landmarks.append(list(getattr(landmark, point)) + [z_pos])
201
202        if landmark.point_D is not None:
203            fem_landmarks.extend(
204                [
205                    list(getattr(landmark, attr)) + [z_pos]
206                    for attr in ["point_D", "point_d"]
207                ]
208            )
209
210    for landmarks, color in [
211        (illium_landmarks, "black"),
212        (fem_landmarks, "blue"),
213    ]:
214        fig.add_trace(
215            go.Scatter3d(
216                x=[point[0] - com[0] for point in landmarks],
217                y=[point[1] - com[1] for point in landmarks],
218                z=[point[2] - com[2] for point in landmarks],
219                mode="markers",
220                marker=dict(color=color, size=2),
221                showlegend=False,
222            )
223        )
224
225    # https://plotly.com/python/3d-camera-controls/
226    camera = dict(
227        up=dict(x=0, y=-1, z=0),  # Pointing in the negative y direction
228        eye=dict(
229            x=1.5, y=-1.5, z=-1.5
230        ),  # Position in the positive x, negative y, negative z region
231    )
232
233    fig.update_layout(scene_camera=camera)
234
235    fig.update_layout(scene_dragmode="orbit")
236
237    # add legend for each color
238    for apex_side in [SideZ.LEFT, SideZ.RIGHT]:
239        for third in Side.ALL():
240            fig.add_trace(
241                go.Scatter3d(
242                    x=[(0, 0, 0)],
243                    y=[(0, 0, 0)],
244                    z=[(0, 0, 0)],
245                    mode="markers",
246                    marker=dict(color=ACA_COLORS[apex_side, third]),
247                    name=f"{apex_side} {Side.get_name(third)}",
248                )
249            )
250
251    return fig
252
253
254def get_femoral_sphere(
255    hip_datas: HipDatasUS, config: Config
256) -> Optional[Tuple[FemoralHeadSphere, float]]:
257    """
258    Get the femoral sphere.
259
260    :param hip_datas: The hip data.
261    :param config: The configuration object.
262
263    :return: The femoral sphere.
264    """
265    diameters = []
266
267    z_gap = config.hip.z_gap * (200 / len(hip_datas))
268
269    hips_of_interest = [
270        hip
271        for hip in hip_datas
272        if hip.landmarks and hip.landmarks.point_d is not None
273    ]
274
275    if len(hips_of_interest) != 0:
276        middle_percent = 0.95
277        # Calculate how many elements to skip from both ends
278        skip_count = int((1 - middle_percent) / 2 * len(diameters))
279
280        # Extract the middle 'middle_percent' percentage of elements
281        hips_of_interest = hips_of_interest[
282            skip_count : -skip_count if skip_count != 0 else None
283        ]
284
285        middle_frame = (
286            hips_of_interest[0].frame_no + hips_of_interest[-1].frame_no
287        ) // 2
288        middle_hip = min(
289            hips_of_interest,
290            key=lambda hip: abs(hip.frame_no - middle_frame),
291        )
292
293        diameter = np.linalg.norm(
294            np.array(middle_hip.landmarks.point_D)
295            - np.array(middle_hip.landmarks.point_d)
296        )
297
298        fem_center = (
299            middle_hip.landmarks.point_D[0]
300            + (
301                middle_hip.landmarks.point_d[0]
302                - middle_hip.landmarks.point_D[0]
303            )
304            / 2,
305            middle_hip.landmarks.point_D[1]
306            + (
307                middle_hip.landmarks.point_d[1]
308                - middle_hip.landmarks.point_D[1]
309            )
310            / 2,
311            middle_hip.frame_no * z_gap,
312        )
313
314        radius = (diameter / 2) + (diameter * 0.05)
315
316        femoral_head_sphere = ms(
317            fem_center[0],
318            fem_center[1],
319            fem_center[2],
320            radius,
321            resolution=20,
322        )
323
324        return femoral_head_sphere, radius
325
326    return None, None
327
328
329def get_illium_mesh(
330    hip_datas: HipDatasUS, results: List[SegFrameObjects], config: Config
331) -> Optional[Tuple[o3d.geometry.TriangleMesh, NDArray[np.float64]]]:
332    """
333    Get the illium mesh.
334
335    :param hip_datas: The hip data.
336    :param results: The segmentation results.
337    :param config: The configuration object.
338
339    :return: The illium mesh and the apex points.
340    """
341    illium_pc = []
342    apex_points = []
343
344    even_count = 0
345
346    z_gap = config.hip.z_gap * (200 / len(hip_datas))
347
348    for hip_data, seg_frame_objs in zip(hip_datas, results):
349        illium = [
350            seg_obj
351            for seg_obj in seg_frame_objs
352            if seg_obj.cls == HipLabelsUS.IlliumAndAcetabulum
353        ]
354
355        if (
356            len(illium) != 0
357            and illium[0].midline is not None
358            and hip_data.landmarks
359            and hip_data.landmarks.apex is not None
360        ):
361            illium: SegObject = illium[0]
362
363            # Convert white_points to a convenient shape for vector operations
364            midline_moved = np.array(illium.midline_moved)[
365                :, ::-1
366            ]  # Reverse each point only once
367
368            if even_count % 2 == 0:
369                # Pick points a 10 pixel intervals on x axis
370                chosen_indexs = np.arange(0, midline_moved.shape[0], 10)
371            else:
372                # choose every 10 on a 5 point offset
373                chosen_indexs = np.arange(5, midline_moved.shape[0], 10)
374
375            for points in midline_moved[chosen_indexs]:
376                points = np.append(points, [hip_data.frame_no * z_gap], axis=0)
377                illium_pc.append(points)
378                apex = hip_data.landmarks.apex
379
380                apex_points.append(
381                    [apex[0], apex[1], hip_data.frame_no * z_gap]
382                )
383
384        even_count += 1
385
386    if len(illium_pc) == 0:
387        return None, None
388
389    illium_pc = np.array(illium_pc)
390
391    # use these points to create a surface
392    # Create Open3D mesh
393    illium_pc_o3d = o3d.geometry.PointCloud()
394    illium_pc_o3d.points = o3d.utility.Vector3dVector(illium_pc)
395
396    illium_points_xz = illium_pc[:, [0, 2]]  # Selecting X and Z coordinates
397
398    try:
399        # Apply Delaunay triangulation on the XZ plane
400        tri = Delaunay(illium_points_xz)
401    except _qhull.QhullError:
402        return None, None
403
404    # Reconstruct 3D triangles using original Y-coordinates
405    vertices = illium_pc  # Using original 3D points as vertices
406    triangles = tri.simplices
407
408    # convert to trimesh
409    illium_mesh = trimesh.Trimesh(
410        vertices=np.asarray(o3d.utility.Vector3dVector(vertices)),
411        faces=np.asarray(o3d.utility.Vector3iVector(triangles)),
412    )
413
414    # apply humphrey smoothing
415    illium_mesh = trimesh.smoothing.filter_humphrey(
416        illium_mesh, iterations=20, alpha=0.1, beta=1
417    )
418
419    # convert back to open3d
420    illium_mesh = o3d.geometry.TriangleMesh(
421        vertices=o3d.utility.Vector3dVector(illium_mesh.vertices),
422        triangles=o3d.utility.Vector3iVector(illium_mesh.faces),
423    )
424
425    return illium_mesh, apex_points
426
427
428def circle_radius_at_z(
429    sphere_radius: float, z_center: float, z_input: float
430) -> float:
431    """
432    Calculate the radius of a circle on a sphere at a given Z-coordinate.
433
434    :param sphere_radius: The radius of the sphere.
435    :param z_center: The Z-coordinate of the center of the sphere.
436    :param z_input: The Z-coordinate of the circle.
437
438    :return: The radius of the circle.
439    """
440    delta_z = abs(z_input - z_center)
441    if delta_z > sphere_radius:
442        return 0  # The input Z-coordinate is outside the sphere.
443    return np.sqrt(sphere_radius**2 - delta_z**2)
def ms( x: float, y: float, z: float, radius: float, resolution: int = 20) -> Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray]:
43def ms(
44    x: float, y: float, z: float, radius: float, resolution: int = 20
45) -> FemoralHeadSphere:
46    """
47    Return the coordinates for plotting a sphere centered at (x,y,z)
48
49    :param x: The x-coordinate of the center of the sphere.
50    :param y: The y-coordinate of the center of the sphere.
51    :param z: The z-coordinate of the center of the sphere.
52    :param radius: The radius of the sphere.
53    :param resolution: The resolution of the sphere.
54
55    :return: The coordinates for plotting a sphere.
56    """
57    u, v = np.mgrid[
58        0 : 2 * np.pi : resolution * 2j, 0 : np.pi : resolution * 1j
59    ]
60    X = radius * np.cos(u) * np.sin(v) + x
61    Y = radius * np.sin(u) * np.sin(v) + y
62    Z = radius * np.cos(v) + z
63    return (X, Y, Z)

Return the coordinates for plotting a sphere centered at (x,y,z)

Parameters
  • x: The x-coordinate of the center of the sphere.
  • y: The y-coordinate of the center of the sphere.
  • z: The z-coordinate of the center of the sphere.
  • radius: The radius of the sphere.
  • resolution: The resolution of the sphere.
Returns

The coordinates for plotting a sphere.

def build_3d_visual( illium_mesh: open3d.cpu.pybind.geometry.TriangleMesh, femoral_sphere: Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray], avg_normals_data: numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]], normals_data: List[retuve.hip_us.metrics.aca.Triangle], cr_points: List[numpy.ndarray[Any, numpy.dtype[numpy.float64]]], hip_datas: retuve.hip_us.classes.general.HipDatasUS, config: retuve.keyphrases.config.Config) -> plotly.graph_objs._figure.Figure:
 66def build_3d_visual(
 67    illium_mesh: o3d.geometry.TriangleMesh,
 68    femoral_sphere: FemoralHeadSphere,
 69    avg_normals_data: CoordinatesArray3D,
 70    normals_data: List[Triangle],
 71    cr_points: List[CoordinatesArray3D],
 72    hip_datas: HipDatasUS,
 73    config: Config,
 74) -> go.Figure:
 75    """
 76    Build the 3D visual for the hip US module.
 77
 78    :param illium_mesh: The illium mesh.
 79    :param femoral_sphere: The femoral sphere.
 80    :param avg_normals_data: The average normals data of the ACA Vectors.
 81    :param normals_data: The normals data of the ACA Vectors.
 82    :param cr_points: The CR points.
 83    :param hip_datas: The hip data.
 84    :param config: The configuration object.
 85
 86    :return: The 3D visual.
 87    """
 88
 89    # Get vertices and faces
 90    vertices = np.asarray(illium_mesh.vertices)
 91    faces = np.asarray(illium_mesh.triangles)
 92
 93    com = np.mean(vertices, axis=0)
 94
 95    z_gap = config.hip.z_gap * (200 / len(hip_datas))
 96
 97    # Move the points to the center of mass
 98    vertices = vertices - com
 99
100    # Create figure
101    fig = go.Figure(
102        data=[
103            go.Mesh3d(
104                x=vertices[:, 0],
105                y=vertices[:, 1],
106                z=vertices[:, 2],
107                i=faces[:, 0],
108                j=faces[:, 1],
109                k=faces[:, 2],
110                color="grey",
111                opacity=0.95,
112            )
113        ]
114    )
115
116    if femoral_sphere is not None:
117        fig.add_trace(
118            go.Surface(
119                x=femoral_sphere[0] - com[0],
120                y=femoral_sphere[1] - com[1],
121                z=femoral_sphere[2] - com[2],
122                opacity=0.2,
123                colorscale=[[0, "blue"], [1, "blue"]],
124                showscale=False,
125            )
126        )
127
128    if cr_points is not None:
129        fig.add_trace(
130            go.Scatter3d(
131                x=[point[0] - com[0] for point in cr_points],
132                y=[point[1] - com[1] for point in cr_points],
133                z=[point[2] - com[2] for point in cr_points],
134                mode="markers",
135                marker=dict(color="black", size=7),
136                showlegend=False,
137            )
138        )
139
140    # Red, Orange, Yellow, Blue, Cyan, Green
141    # [0, 2, 3, 2, 3, 3]
142    # https://github.com/plotly/plotly.js/issues/3613#issuecomment-1750709712
143    scaling_factors = {
144        "red": 0,
145        "orange": 2,
146        "yellow": 3,
147        "blue": 2,
148        "cyan": 3,
149        "lightgreen": 3,
150    }
151
152    for i, color in enumerate(ACA_COLORS.values()):
153        triangles = [
154            triangle for triangle in normals_data if triangle.color == color
155        ]
156        fig.add_trace(
157            go.Cone(
158                x=[triangle.centroid[0] - com[0] for triangle in triangles],
159                y=[triangle.centroid[1] - com[1] for triangle in triangles],
160                z=[triangle.centroid[2] - com[2] for triangle in triangles],
161                u=[triangle.normal[0] for triangle in triangles],
162                v=[triangle.normal[1] for triangle in triangles],
163                w=[triangle.normal[2] for triangle in triangles],
164                colorscale=[
165                    [0, color],
166                    [1, color],
167                ],
168                showscale=False,
169                sizemode="absolute",
170                sizeref=5 - scaling_factors[color],
171                anchor="tail",
172            )
173        )
174
175    for avg_normal, avg_centroid, color in avg_normals_data:
176        fig.add_trace(
177            go.Cone(
178                x=[avg_centroid[0] - com[0]],
179                y=[avg_centroid[1] - com[1]],
180                z=[avg_centroid[2] - com[2]],
181                u=[avg_normal[0]],
182                v=[avg_normal[1]],
183                w=[avg_normal[2]],
184                colorscale=[[0, color], [1, "black"]],
185                showscale=False,
186                sizemode="absolute",
187                sizeref=32,
188                anchor="tail",
189            )
190        )
191
192    illium_landmarks = []
193    fem_landmarks = []
194    for hip in hip_datas:
195        landmark = hip.landmarks
196        if landmark is None:
197            continue
198
199        z_pos = z_gap * hip.frame_no
200        for point in ["left", "right", "apex"]:
201            illium_landmarks.append(list(getattr(landmark, point)) + [z_pos])
202
203        if landmark.point_D is not None:
204            fem_landmarks.extend(
205                [
206                    list(getattr(landmark, attr)) + [z_pos]
207                    for attr in ["point_D", "point_d"]
208                ]
209            )
210
211    for landmarks, color in [
212        (illium_landmarks, "black"),
213        (fem_landmarks, "blue"),
214    ]:
215        fig.add_trace(
216            go.Scatter3d(
217                x=[point[0] - com[0] for point in landmarks],
218                y=[point[1] - com[1] for point in landmarks],
219                z=[point[2] - com[2] for point in landmarks],
220                mode="markers",
221                marker=dict(color=color, size=2),
222                showlegend=False,
223            )
224        )
225
226    # https://plotly.com/python/3d-camera-controls/
227    camera = dict(
228        up=dict(x=0, y=-1, z=0),  # Pointing in the negative y direction
229        eye=dict(
230            x=1.5, y=-1.5, z=-1.5
231        ),  # Position in the positive x, negative y, negative z region
232    )
233
234    fig.update_layout(scene_camera=camera)
235
236    fig.update_layout(scene_dragmode="orbit")
237
238    # add legend for each color
239    for apex_side in [SideZ.LEFT, SideZ.RIGHT]:
240        for third in Side.ALL():
241            fig.add_trace(
242                go.Scatter3d(
243                    x=[(0, 0, 0)],
244                    y=[(0, 0, 0)],
245                    z=[(0, 0, 0)],
246                    mode="markers",
247                    marker=dict(color=ACA_COLORS[apex_side, third]),
248                    name=f"{apex_side} {Side.get_name(third)}",
249                )
250            )
251
252    return fig

Build the 3D visual for the hip US module.

Parameters
  • illium_mesh: The illium mesh.
  • femoral_sphere: The femoral sphere.
  • avg_normals_data: The average normals data of the ACA Vectors.
  • normals_data: The normals data of the ACA Vectors.
  • cr_points: The CR points.
  • hip_datas: The hip data.
  • config: The configuration object.
Returns

The 3D visual.

def get_femoral_sphere( hip_datas: retuve.hip_us.classes.general.HipDatasUS, config: retuve.keyphrases.config.Config) -> Optional[Tuple[Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray], float]]:
255def get_femoral_sphere(
256    hip_datas: HipDatasUS, config: Config
257) -> Optional[Tuple[FemoralHeadSphere, float]]:
258    """
259    Get the femoral sphere.
260
261    :param hip_datas: The hip data.
262    :param config: The configuration object.
263
264    :return: The femoral sphere.
265    """
266    diameters = []
267
268    z_gap = config.hip.z_gap * (200 / len(hip_datas))
269
270    hips_of_interest = [
271        hip
272        for hip in hip_datas
273        if hip.landmarks and hip.landmarks.point_d is not None
274    ]
275
276    if len(hips_of_interest) != 0:
277        middle_percent = 0.95
278        # Calculate how many elements to skip from both ends
279        skip_count = int((1 - middle_percent) / 2 * len(diameters))
280
281        # Extract the middle 'middle_percent' percentage of elements
282        hips_of_interest = hips_of_interest[
283            skip_count : -skip_count if skip_count != 0 else None
284        ]
285
286        middle_frame = (
287            hips_of_interest[0].frame_no + hips_of_interest[-1].frame_no
288        ) // 2
289        middle_hip = min(
290            hips_of_interest,
291            key=lambda hip: abs(hip.frame_no - middle_frame),
292        )
293
294        diameter = np.linalg.norm(
295            np.array(middle_hip.landmarks.point_D)
296            - np.array(middle_hip.landmarks.point_d)
297        )
298
299        fem_center = (
300            middle_hip.landmarks.point_D[0]
301            + (
302                middle_hip.landmarks.point_d[0]
303                - middle_hip.landmarks.point_D[0]
304            )
305            / 2,
306            middle_hip.landmarks.point_D[1]
307            + (
308                middle_hip.landmarks.point_d[1]
309                - middle_hip.landmarks.point_D[1]
310            )
311            / 2,
312            middle_hip.frame_no * z_gap,
313        )
314
315        radius = (diameter / 2) + (diameter * 0.05)
316
317        femoral_head_sphere = ms(
318            fem_center[0],
319            fem_center[1],
320            fem_center[2],
321            radius,
322            resolution=20,
323        )
324
325        return femoral_head_sphere, radius
326
327    return None, None

Get the femoral sphere.

Parameters
  • hip_datas: The hip data.
  • config: The configuration object.
Returns

The femoral sphere.

def get_illium_mesh( hip_datas: retuve.hip_us.classes.general.HipDatasUS, results: List[retuve.classes.seg.SegFrameObjects], config: retuve.keyphrases.config.Config) -> Optional[Tuple[open3d.cpu.pybind.geometry.TriangleMesh, numpy.ndarray[Any, numpy.dtype[numpy.float64]]]]:
330def get_illium_mesh(
331    hip_datas: HipDatasUS, results: List[SegFrameObjects], config: Config
332) -> Optional[Tuple[o3d.geometry.TriangleMesh, NDArray[np.float64]]]:
333    """
334    Get the illium mesh.
335
336    :param hip_datas: The hip data.
337    :param results: The segmentation results.
338    :param config: The configuration object.
339
340    :return: The illium mesh and the apex points.
341    """
342    illium_pc = []
343    apex_points = []
344
345    even_count = 0
346
347    z_gap = config.hip.z_gap * (200 / len(hip_datas))
348
349    for hip_data, seg_frame_objs in zip(hip_datas, results):
350        illium = [
351            seg_obj
352            for seg_obj in seg_frame_objs
353            if seg_obj.cls == HipLabelsUS.IlliumAndAcetabulum
354        ]
355
356        if (
357            len(illium) != 0
358            and illium[0].midline is not None
359            and hip_data.landmarks
360            and hip_data.landmarks.apex is not None
361        ):
362            illium: SegObject = illium[0]
363
364            # Convert white_points to a convenient shape for vector operations
365            midline_moved = np.array(illium.midline_moved)[
366                :, ::-1
367            ]  # Reverse each point only once
368
369            if even_count % 2 == 0:
370                # Pick points a 10 pixel intervals on x axis
371                chosen_indexs = np.arange(0, midline_moved.shape[0], 10)
372            else:
373                # choose every 10 on a 5 point offset
374                chosen_indexs = np.arange(5, midline_moved.shape[0], 10)
375
376            for points in midline_moved[chosen_indexs]:
377                points = np.append(points, [hip_data.frame_no * z_gap], axis=0)
378                illium_pc.append(points)
379                apex = hip_data.landmarks.apex
380
381                apex_points.append(
382                    [apex[0], apex[1], hip_data.frame_no * z_gap]
383                )
384
385        even_count += 1
386
387    if len(illium_pc) == 0:
388        return None, None
389
390    illium_pc = np.array(illium_pc)
391
392    # use these points to create a surface
393    # Create Open3D mesh
394    illium_pc_o3d = o3d.geometry.PointCloud()
395    illium_pc_o3d.points = o3d.utility.Vector3dVector(illium_pc)
396
397    illium_points_xz = illium_pc[:, [0, 2]]  # Selecting X and Z coordinates
398
399    try:
400        # Apply Delaunay triangulation on the XZ plane
401        tri = Delaunay(illium_points_xz)
402    except _qhull.QhullError:
403        return None, None
404
405    # Reconstruct 3D triangles using original Y-coordinates
406    vertices = illium_pc  # Using original 3D points as vertices
407    triangles = tri.simplices
408
409    # convert to trimesh
410    illium_mesh = trimesh.Trimesh(
411        vertices=np.asarray(o3d.utility.Vector3dVector(vertices)),
412        faces=np.asarray(o3d.utility.Vector3iVector(triangles)),
413    )
414
415    # apply humphrey smoothing
416    illium_mesh = trimesh.smoothing.filter_humphrey(
417        illium_mesh, iterations=20, alpha=0.1, beta=1
418    )
419
420    # convert back to open3d
421    illium_mesh = o3d.geometry.TriangleMesh(
422        vertices=o3d.utility.Vector3dVector(illium_mesh.vertices),
423        triangles=o3d.utility.Vector3iVector(illium_mesh.faces),
424    )
425
426    return illium_mesh, apex_points

Get the illium mesh.

Parameters
  • hip_datas: The hip data.
  • results: The segmentation results.
  • config: The configuration object.
Returns

The illium mesh and the apex points.

def circle_radius_at_z(sphere_radius: float, z_center: float, z_input: float) -> float:
429def circle_radius_at_z(
430    sphere_radius: float, z_center: float, z_input: float
431) -> float:
432    """
433    Calculate the radius of a circle on a sphere at a given Z-coordinate.
434
435    :param sphere_radius: The radius of the sphere.
436    :param z_center: The Z-coordinate of the center of the sphere.
437    :param z_input: The Z-coordinate of the circle.
438
439    :return: The radius of the circle.
440    """
441    delta_z = abs(z_input - z_center)
442    if delta_z > sphere_radius:
443        return 0  # The input Z-coordinate is outside the sphere.
444    return np.sqrt(sphere_radius**2 - delta_z**2)

Calculate the radius of a circle on a sphere at a given Z-coordinate.

Parameters
  • sphere_radius: The radius of the sphere.
  • z_center: The Z-coordinate of the center of the sphere.
  • z_input: The Z-coordinate of the circle.
Returns

The radius of the circle.