retuve.hip_us.modes.seg

All code relating to going from segmentation results to landmarks 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 code relating to going from segmentation results to landmarks is in this file.
 17"""
 18
 19import copy
 20import time
 21from typing import List
 22
 23import cv2
 24import numpy as np
 25from skimage.morphology import skeletonize
 26
 27from retuve.classes.seg import (
 28    MidLine,
 29    NDArrayImg_NxNx3_AllWhite,
 30    SegFrameObjects,
 31    SegObject,
 32)
 33from retuve.hip_us.classes.enums import HipLabelsUS
 34from retuve.hip_us.classes.general import LandmarksUS
 35from retuve.hip_us.handlers.segs import remove_bad_objs
 36from retuve.hip_us.metrics.alpha import find_alpha_landmarks
 37from retuve.hip_us.metrics.coverage import find_cov_landmarks
 38from retuve.keyphrases.config import Config
 39from retuve.keyphrases.enums import MidLineMove
 40from retuve.logs import log_timings
 41from retuve.utils import find_midline_extremes
 42
 43
 44def get_midlines(
 45    mask: NDArrayImg_NxNx3_AllWhite, config: Config, color: int = 255
 46) -> MidLine:
 47    """
 48    Get the midline of the illium mask. This is used to '
 49    find the landmarks for the alpha angle.
 50
 51    :param mask: The mask of the illium.
 52    :param config: The configuration object.
 53    :param color: The color of the midline.
 54
 55    :return: The midline of the illium.
 56    """
 57
 58    mask = cv2.cvtColor(mask, cv2.COLOR_RGB2GRAY)
 59    # Takes bool as input and returns bool as output
 60    midline = skeletonize(mask > 0)
 61    midline = midline.astype(np.uint8) * color
 62
 63    midline_moved = midline
 64
 65    # move the skeleton up and right
 66    if config.hip.midline_move_method == MidLineMove.BASIC:
 67        height, width = midline_moved.shape
 68        midline_moved = np.roll(midline_moved, -1 * int(height / 51), axis=0)
 69        midline_moved = np.roll(midline_moved, int(width / 86), axis=1)
 70
 71    for midl in [midline_moved, midline]:
 72        left_most, right_most = find_midline_extremes(
 73            np.column_stack(np.where(midl == color))
 74        )
 75        if left_most is not None and right_most is not None:
 76            width = right_most[1] - left_most[1]
 77
 78            new_left_bound = max(int(left_most[1] + width * 0.01), 4)
 79            new_right_bound = max(int(right_most[1] - width * 0.01), 4)
 80
 81            # Apply the boundary conditions to remove edges
 82            midl[:, :new_left_bound] = 0
 83            midl[:, new_right_bound:] = 0
 84
 85    midline = np.column_stack(np.where(midline == color))
 86    midline_moved = np.column_stack(np.where(midline_moved == color))
 87
 88    return midline, midline_moved
 89
 90
 91def segs_2_landmarks_us(
 92    results: List[SegFrameObjects], config: Config
 93) -> List[LandmarksUS]:
 94    """
 95    Converts a list of segmentation results to a list of landmarks.
 96
 97    :param results: A list of segmentation results.
 98    :param config: The configuration object.
 99
100    :return: A list of landmarks.
101    """
102    hip_landmarks = []
103    timings = []
104    hip_objs_list = []
105    fem_head_ilium_wrong_way_round = 0
106    all_rejection_reasons = []
107
108    for seg_frame_objs in results:
109
110        if all(seg_obj.empty for seg_obj in seg_frame_objs):
111            hip_objs_list.append(None)
112            all_rejection_reasons.append([])
113            continue
114
115        hip_objs = {
116            HipLabelsUS.IlliumAndAcetabulum: [],
117            HipLabelsUS.FemoralHead: [],
118            HipLabelsUS.OsIchium: [],
119        }
120
121        for seg_obj in seg_frame_objs:
122            hip_objs[seg_obj.cls].append(seg_obj)
123
124        hip_objs, wrong_way_round, rejection_reasons = remove_bad_objs(
125            hip_objs, seg_frame_objs.img
126        )
127        if wrong_way_round:
128            fem_head_ilium_wrong_way_round += 1
129
130        hip_objs_list.append(hip_objs)
131        all_rejection_reasons.append(rejection_reasons)
132
133    if fem_head_ilium_wrong_way_round > 5:
134        # Flip images
135        for seg_frame_objs in results:
136            seg_frame_objs.img = cv2.flip(seg_frame_objs.img, 1)
137
138        for hip_objs in hip_objs_list:
139            if hip_objs is None:
140                continue
141
142            for seg_obj in hip_objs.values():
143                seg_obj.flip_horizontally(results[0].img.shape[1])
144
145            ilium = hip_objs.get(HipLabelsUS.IlliumAndAcetabulum, None)
146
147            if (
148                ilium is not None
149                and ilium.box is not None
150                and ilium.box[0] > results[0].img.shape[1] / 2
151            ):
152                hip_objs[HipLabelsUS.IlliumAndAcetabulum] = SegObject(
153                    empty=True
154                )
155
156    for seg_frame_objs, hip_objs in zip(results, hip_objs_list):
157        start = time.time()
158
159        landmarks = LandmarksUS()
160        if hip_objs is None:
161            hip_landmarks.append(landmarks)
162            continue
163
164        # set the seg_frame_objs to only have the good seg_objs
165        seg_frame_objs.seg_objects = [
166            obj for obj in list(hip_objs.values()) if obj is not None
167        ]
168
169        illium = hip_objs.get(HipLabelsUS.IlliumAndAcetabulum, None)
170        femoral = hip_objs.get(HipLabelsUS.FemoralHead, None)
171
172        landmarks = find_alpha_landmarks(illium, landmarks, config)
173        landmarks = find_cov_landmarks(femoral, landmarks, config)
174
175        hip_landmarks.append(landmarks)
176        timings.append(time.time() - start)
177
178    log_timings(timings, title="Seg->Landmarking Speed:")
179    return hip_landmarks, all_rejection_reasons
180
181
182def pre_process_segs_us(
183    results: List[SegFrameObjects], config: Config
184) -> tuple[List[SegFrameObjects], tuple]:
185    """
186    Pre-processes the segmentation results for the hip US module.
187
188    :param results: A list of segmentation results.
189    :param config: The configuration object.
190
191    :return: A tuple containing the pre-processed
192             segmentation results and the shape of the image.
193    """
194    timings = []
195    for frame_seg_objs in results:
196        start = time.time()
197        for seg_object in frame_seg_objs:
198            if (
199                seg_object.empty
200                or seg_object.cls != HipLabelsUS.IlliumAndAcetabulum
201            ):
202                continue
203
204            seg_object.midline, seg_object.midline_moved = get_midlines(
205                seg_object.mask, config
206            )
207
208        timings.append(time.time() - start)
209
210    log_timings(timings, title="Skeletonization Speed:")
211
212    img = results[0].img
213    shape = img.shape
214
215    return results, shape
def get_midlines( mask: typing.Annotated[numpy.ndarray[typing.Any, numpy.dtype[~DType]], typing.Literal['N', 3]], config: retuve.keyphrases.config.Config, color: int = 255) -> List[Tuple[int, int]]:
45def get_midlines(
46    mask: NDArrayImg_NxNx3_AllWhite, config: Config, color: int = 255
47) -> MidLine:
48    """
49    Get the midline of the illium mask. This is used to '
50    find the landmarks for the alpha angle.
51
52    :param mask: The mask of the illium.
53    :param config: The configuration object.
54    :param color: The color of the midline.
55
56    :return: The midline of the illium.
57    """
58
59    mask = cv2.cvtColor(mask, cv2.COLOR_RGB2GRAY)
60    # Takes bool as input and returns bool as output
61    midline = skeletonize(mask > 0)
62    midline = midline.astype(np.uint8) * color
63
64    midline_moved = midline
65
66    # move the skeleton up and right
67    if config.hip.midline_move_method == MidLineMove.BASIC:
68        height, width = midline_moved.shape
69        midline_moved = np.roll(midline_moved, -1 * int(height / 51), axis=0)
70        midline_moved = np.roll(midline_moved, int(width / 86), axis=1)
71
72    for midl in [midline_moved, midline]:
73        left_most, right_most = find_midline_extremes(
74            np.column_stack(np.where(midl == color))
75        )
76        if left_most is not None and right_most is not None:
77            width = right_most[1] - left_most[1]
78
79            new_left_bound = max(int(left_most[1] + width * 0.01), 4)
80            new_right_bound = max(int(right_most[1] - width * 0.01), 4)
81
82            # Apply the boundary conditions to remove edges
83            midl[:, :new_left_bound] = 0
84            midl[:, new_right_bound:] = 0
85
86    midline = np.column_stack(np.where(midline == color))
87    midline_moved = np.column_stack(np.where(midline_moved == color))
88
89    return midline, midline_moved

Get the midline of the illium mask. This is used to ' find the landmarks for the alpha angle.

Parameters
  • mask: The mask of the illium.
  • config: The configuration object.
  • color: The color of the midline.
Returns

The midline of the illium.

def segs_2_landmarks_us( results: List[retuve.classes.seg.SegFrameObjects], config: retuve.keyphrases.config.Config) -> List[retuve.hip_us.classes.general.LandmarksUS]:
 92def segs_2_landmarks_us(
 93    results: List[SegFrameObjects], config: Config
 94) -> List[LandmarksUS]:
 95    """
 96    Converts a list of segmentation results to a list of landmarks.
 97
 98    :param results: A list of segmentation results.
 99    :param config: The configuration object.
100
101    :return: A list of landmarks.
102    """
103    hip_landmarks = []
104    timings = []
105    hip_objs_list = []
106    fem_head_ilium_wrong_way_round = 0
107    all_rejection_reasons = []
108
109    for seg_frame_objs in results:
110
111        if all(seg_obj.empty for seg_obj in seg_frame_objs):
112            hip_objs_list.append(None)
113            all_rejection_reasons.append([])
114            continue
115
116        hip_objs = {
117            HipLabelsUS.IlliumAndAcetabulum: [],
118            HipLabelsUS.FemoralHead: [],
119            HipLabelsUS.OsIchium: [],
120        }
121
122        for seg_obj in seg_frame_objs:
123            hip_objs[seg_obj.cls].append(seg_obj)
124
125        hip_objs, wrong_way_round, rejection_reasons = remove_bad_objs(
126            hip_objs, seg_frame_objs.img
127        )
128        if wrong_way_round:
129            fem_head_ilium_wrong_way_round += 1
130
131        hip_objs_list.append(hip_objs)
132        all_rejection_reasons.append(rejection_reasons)
133
134    if fem_head_ilium_wrong_way_round > 5:
135        # Flip images
136        for seg_frame_objs in results:
137            seg_frame_objs.img = cv2.flip(seg_frame_objs.img, 1)
138
139        for hip_objs in hip_objs_list:
140            if hip_objs is None:
141                continue
142
143            for seg_obj in hip_objs.values():
144                seg_obj.flip_horizontally(results[0].img.shape[1])
145
146            ilium = hip_objs.get(HipLabelsUS.IlliumAndAcetabulum, None)
147
148            if (
149                ilium is not None
150                and ilium.box is not None
151                and ilium.box[0] > results[0].img.shape[1] / 2
152            ):
153                hip_objs[HipLabelsUS.IlliumAndAcetabulum] = SegObject(
154                    empty=True
155                )
156
157    for seg_frame_objs, hip_objs in zip(results, hip_objs_list):
158        start = time.time()
159
160        landmarks = LandmarksUS()
161        if hip_objs is None:
162            hip_landmarks.append(landmarks)
163            continue
164
165        # set the seg_frame_objs to only have the good seg_objs
166        seg_frame_objs.seg_objects = [
167            obj for obj in list(hip_objs.values()) if obj is not None
168        ]
169
170        illium = hip_objs.get(HipLabelsUS.IlliumAndAcetabulum, None)
171        femoral = hip_objs.get(HipLabelsUS.FemoralHead, None)
172
173        landmarks = find_alpha_landmarks(illium, landmarks, config)
174        landmarks = find_cov_landmarks(femoral, landmarks, config)
175
176        hip_landmarks.append(landmarks)
177        timings.append(time.time() - start)
178
179    log_timings(timings, title="Seg->Landmarking Speed:")
180    return hip_landmarks, all_rejection_reasons

Converts a list of segmentation results to a list of landmarks.

Parameters
  • results: A list of segmentation results.
  • config: The configuration object.
Returns

A list of landmarks.

def pre_process_segs_us( results: List[retuve.classes.seg.SegFrameObjects], config: retuve.keyphrases.config.Config) -> tuple[typing.List[retuve.classes.seg.SegFrameObjects], tuple]:
183def pre_process_segs_us(
184    results: List[SegFrameObjects], config: Config
185) -> tuple[List[SegFrameObjects], tuple]:
186    """
187    Pre-processes the segmentation results for the hip US module.
188
189    :param results: A list of segmentation results.
190    :param config: The configuration object.
191
192    :return: A tuple containing the pre-processed
193             segmentation results and the shape of the image.
194    """
195    timings = []
196    for frame_seg_objs in results:
197        start = time.time()
198        for seg_object in frame_seg_objs:
199            if (
200                seg_object.empty
201                or seg_object.cls != HipLabelsUS.IlliumAndAcetabulum
202            ):
203                continue
204
205            seg_object.midline, seg_object.midline_moved = get_midlines(
206                seg_object.mask, config
207            )
208
209        timings.append(time.time() - start)
210
211    log_timings(timings, title="Skeletonization Speed:")
212
213    img = results[0].img
214    shape = img.shape
215
216    return results, shape

Pre-processes the segmentation results for the hip US module.

Parameters
  • results: A list of segmentation results.
  • config: The configuration object.
Returns

A tuple containing the pre-processed segmentation results and the shape of the image.