Skip to content

Commit

Permalink
series_selector op numerical type cleanup; fixed STL op to change tem…
Browse files Browse the repository at this point in the history
…p_folder declare location per pytype check

Signed-off-by: bluna301 <luna.bryanr@gmail.com>
  • Loading branch information
bluna301 committed Sep 6, 2024
1 parent c45e902 commit d064aff
Show file tree
Hide file tree
Showing 2 changed files with 29 additions and 16 deletions.
42 changes: 27 additions & 15 deletions monai/deploy/operators/dicom_series_selector_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class DICOMSeriesSelectorOperator(Operator):
1. attribute "selections" value is a list of selections
2. each selection has a "name", and its "conditions" value is a list of matching criteria
3. each condition uses the implicit equal operator; in addition, the following are supported:
- regex and range matching for numerical types
- regex and range matching for float and int types
- regex matching for str type
- union matching for set type
4. DICOM attribute keywords are used, and only for those defined as DICOMStudy and DICOMSeries properties
Expand Down Expand Up @@ -81,14 +81,22 @@ class DICOMSeriesSelectorOperator(Operator):
}
"""

def __init__(self, fragment: Fragment, *args, rules: str = "", all_matched: bool = False, sort_by_sop_instance_count: bool = False, **kwargs) -> None:
def __init__(
self,
fragment: Fragment,
*args,
rules: str = "",
all_matched: bool = False,
sort_by_sop_instance_count: bool = False,
**kwargs,
) -> None:
"""Instantiate an instance.
Args:
fragment (Fragment): An instance of the Application class which is derived from Fragment.
rules (Text): Selection rules in JSON string.
all_matched (bool): Gets all matched series in a study. Defaults to False for first match only.
sort_by_sop_instance_count (bool): If all_matched = True and multiple series are matched, sorts matched series in
sort_by_sop_instance_count (bool): If all_matched = True and multiple series are matched, sorts the matched series in
descending SOP instance count. Defaults to False for no sorting.
"""

Expand All @@ -115,24 +123,28 @@ def compute(self, op_input, op_output, context):

dicom_study_list = op_input.receive(self.input_name_study_list)
selection_rules = self._load_rules() if self._rules_json_str else None
study_selected_series = self.filter(selection_rules, dicom_study_list, self._all_matched, self._sort_by_sop_instance_count)
study_selected_series = self.filter(
selection_rules, dicom_study_list, self._all_matched, self._sort_by_sop_instance_count
)
op_output.emit(study_selected_series, self.output_name_selected_series)

def filter(self, selection_rules, dicom_study_list, all_matched: bool = False, sort_by_sop_instance_count: bool = False) -> List[StudySelectedSeries]:
def filter(
self, selection_rules, dicom_study_list, all_matched: bool = False, sort_by_sop_instance_count: bool = False
) -> List[StudySelectedSeries]:
"""Selects the series with the given matching rules.
If rules object is None, all series will be returned with series instance UID as the selection name.
Supported matching logic:
Number: exact matching, range matching (if a list with two numerical elements is provided), and regex matching
Float + Int: exact matching, range matching (if a list with two numerical elements is provided), and regex matching
String: matches case insensitive, if fails then tries RegEx search
String array (set): matches as subset, case insensitive
Args:
selection_rules (object): JSON object containing the matching rules.
dicom_study_list (list): A list of DICOMStudiy objects.
all_matched (bool): Gets all matched series in a study. Defaults to False for first match only.
sort_by_sop_instance_count (bool): If all_matched = True and multiple series are matched, sorts matched series in
sort_by_sop_instance_count (bool): If all_matched = True and multiple series are matched, sorts the matched series in
descending SOP instance count. Defaults to False for no sorting.
Returns:
Expand Down Expand Up @@ -202,13 +214,15 @@ def _select_all_series(self, dicom_study_list: List[DICOMStudy]) -> List[StudySe
study_selected_series_list.append(study_selected_series)
return study_selected_series_list

def _select_series(self, attributes: dict, study: DICOMStudy, all_matched=False, sort_by_sop_instance_count=False) -> List[DICOMSeries]:
def _select_series(
self, attributes: dict, study: DICOMStudy, all_matched=False, sort_by_sop_instance_count=False
) -> List[DICOMSeries]:
"""Finds series whose attributes match the given attributes.
Args:
attributes (dict): Dictionary of attributes for matching
all_matched (bool): Gets all matched series in a study. Defaults to False for first match only.
sort_by_sop_instance_count (bool): If all_matched = True and multiple series are matched, sorts matched series in
sort_by_sop_instance_count (bool): If all_matched = True and multiple series are matched, sorts the matched series in
descending SOP instance count. Defaults to False for no sorting.
Returns:
Expand Down Expand Up @@ -255,14 +269,14 @@ def _select_series(self, attributes: dict, study: DICOMStudy, all_matched=False,

if not attr_value:
matched = False
elif isinstance(attr_value, numbers.Number):
elif isinstance(attr_value, float) or isinstance(attr_value, int):
# range matching
if isinstance(value_to_match, list) and len(value_to_match) == 2:
lower_bound, upper_bound = map(float, value_to_match)
matched = lower_bound <= attr_value <= upper_bound
# RegEx matching
elif isinstance(value_to_match, str):
matched = re.fullmatch(value_to_match, str(attr_value))
matched = bool(re.fullmatch(value_to_match, str(attr_value)))
# exact matching
else:
matched = value_to_match == attr_value
Expand Down Expand Up @@ -300,7 +314,7 @@ def _select_series(self, attributes: dict, study: DICOMStudy, all_matched=False,
if sort_by_sop_instance_count and len(found_series) > 1:
# sort series in descending SOP instance count
found_series.sort(key=lambda x: len(x.get_sop_instances()), reverse=True)

return found_series

@staticmethod
Expand Down Expand Up @@ -335,8 +349,6 @@ def test():
sample_selection_rule = json_loads(Sample_Rules_Text)
print(f"Selection rules in JSON:\n{sample_selection_rule}")
study_selected_seriee_list = selector.filter(sample_selection_rule, study_list)
# # multiple series match testing:
# study_selected_seriee_list = selector.filter(sample_selection_rule, study_list, all_matched=False, sort_by_sop_instance_count=False)

for sss_obj in study_selected_seriee_list:
_print_instance_properties(sss_obj, pre_fix="", print_val=False)
Expand Down Expand Up @@ -403,4 +415,4 @@ def test():
"""

if __name__ == "__main__":
test()
test()
3 changes: 2 additions & 1 deletion monai/deploy/operators/stl_conversion_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ def convert(
if isinstance(output_file, Path):
output_file.parent.mkdir(parents=True, exist_ok=True)

temp_folder = tempfile.mkdtemp()

s_image = self.SpatialImage(image)
nda = s_image.image_array
self._logger.info(f"Image ndarray shape:{nda.shape}")
Expand Down Expand Up @@ -231,7 +233,6 @@ def convert(

# Write out the STL file, and then load into trimesh
try:
temp_folder = tempfile.mkdtemp()
raw_stl_filename = os.path.join(temp_folder, "temp.stl")
STLConverter.write_stl(verts, faces, raw_stl_filename)
mesh_data = trimesh.load(raw_stl_filename)
Expand Down

0 comments on commit d064aff

Please sign in to comment.