From 0b513e8c796b1218c877c6912449fa218444857b Mon Sep 17 00:00:00 2001 From: Jermiah Joseph Date: Fri, 29 Mar 2024 14:46:17 -0400 Subject: [PATCH 1/4] build: Update Codecov job name --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e8a7020..1c3b36f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -46,7 +46,7 @@ jobs: name: coverage-report path: coverage-report - Codecov: + Code-Coverage: needs: Unit-Tests runs-on: ubuntu-latest steps: From 154daadb7da3edaf7224fb4c75d639fdd2c84cdd Mon Sep 17 00:00:00 2001 From: Jermiah Joseph Date: Fri, 29 Mar 2024 14:51:31 -0400 Subject: [PATCH 2/4] Refactor: DICOM tags module --- src/nbiatoolkit/dicomtags/__init__.py | 24 ++++++++++++++++++++++++ src/nbiatoolkit/dicomtags/tags.py | 3 +-- 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 src/nbiatoolkit/dicomtags/__init__.py diff --git a/src/nbiatoolkit/dicomtags/__init__.py b/src/nbiatoolkit/dicomtags/__init__.py new file mode 100644 index 0000000..84a7a4e --- /dev/null +++ b/src/nbiatoolkit/dicomtags/__init__.py @@ -0,0 +1,24 @@ +from .tags import ( + convert_element_to_int, + convert_int_to_element, + LOOKUP_TAG, + element_VR_lookup, + getSeriesModality, +) + +from .tags import ( + subsetSeriesTags, + getReferencedFrameOfReferenceSequence, + getReferencedSeriesUIDS, +) + +__all__ = [ + "convert_element_to_int", + "convert_int_to_element", + "LOOKUP_TAG", + "element_VR_lookup", + "getSeriesModality", + "subsetSeriesTags", + "getReferencedFrameOfReferenceSequence", + "getReferencedSeriesUIDS", +] diff --git a/src/nbiatoolkit/dicomtags/tags.py b/src/nbiatoolkit/dicomtags/tags.py index 41adb5d..06021d0 100644 --- a/src/nbiatoolkit/dicomtags/tags.py +++ b/src/nbiatoolkit/dicomtags/tags.py @@ -1,8 +1,7 @@ from pydicom.datadict import dictionary_VR from pydicom.datadict import tag_for_keyword -from pydicom._dicom_dict import DicomDictionary import pandas as pd -from typing import Any, Union, List +from typing import List def convert_element_to_int(element_str: str) -> int: From e85819bc9c689c399c716ae2820597880220728f Mon Sep 17 00:00:00 2001 From: Jermiah Joseph Date: Mon, 1 Apr 2024 12:04:43 -0400 Subject: [PATCH 3/4] feat: Add new functions and tests for DICOM sequence tags --- src/nbiatoolkit/dicomtags/tags.py | 141 +++++++++++++++++++++++++++++- tests/test_tags.py | 29 ++++++ 2 files changed, 169 insertions(+), 1 deletion(-) diff --git a/src/nbiatoolkit/dicomtags/tags.py b/src/nbiatoolkit/dicomtags/tags.py index 06021d0..35ba825 100644 --- a/src/nbiatoolkit/dicomtags/tags.py +++ b/src/nbiatoolkit/dicomtags/tags.py @@ -1,3 +1,4 @@ +from math import log from pydicom.datadict import dictionary_VR from pydicom.datadict import tag_for_keyword import pandas as pd @@ -153,6 +154,17 @@ def getSeriesModality(series_tags_df: pd.DataFrame) -> str: def subsetSeriesTags(series_tags_df: pd.DataFrame, element: str) -> pd.DataFrame: """ Subsets a DataFrame containing DICOM series tags based on the start and end elements. + + Args: + series_tags_df (pd.DataFrame): A DataFrame containing DICOM series tags. + element (str): The element to subset the DataFrame. + + Returns: + pd.DataFrame: A DataFrame containing the subset of the series tags. + + Raises: + ValueError: If the element is not found in the series tags. + ValueError: If more than two elements are found in the series tags. """ locs: pd.DataFrame @@ -161,13 +173,31 @@ def subsetSeriesTags(series_tags_df: pd.DataFrame, element: str) -> pd.DataFrame if len(locs) == 0: raise ValueError("Element not found in the series tags.") + if len(locs) == 1: + raise ValueError( + "Only one element found in the series tags. Ensure element is a sequence" + ) + if len(locs) > 2: raise ValueError("More than two elements found in the series tags.") - return series_tags_df.iloc[locs.index[0] : locs.index[1]] + return series_tags_df.iloc[locs.index[0] : locs.index[1] + 1] def getReferencedFrameOfReferenceSequence(series_tags_df: pd.DataFrame) -> pd.DataFrame: + """ + Given a DataFrame containing DICOM series tags, retrieves the ReferencedFrameOfReferenceSequence. + + Args: + series_tags_df (pd.DataFrame): A DataFrame containing DICOM series tags. + + Returns: + pd.DataFrame: A DataFrame containing the ReferencedFrameOfReferenceSequence. + + Raises: + ValueError: If the series is not an RTSTRUCT. + + """ modality = getSeriesModality(series_tags_df=series_tags_df) if modality != "RTSTRUCT": raise ValueError("Series is not an RTSTRUCT.") @@ -219,3 +249,112 @@ def getReferencedSeriesUIDS(series_tags_df: pd.DataFrame) -> List[str]: UIDS: list[str] = value["data"].to_list() return UIDS + + +def getSequenceElement( + sequence_tags_df: pd.DataFrame, element_keyword: str +) -> pd.DataFrame: + """ + Given a DataFrame containing DICOM sequence tags, retrieves the search space + based on the element keyword. + + Args: + sequence_tags_df (pd.DataFrame): A DataFrame containing DICOM sequence tags. + element_keyword (str): The keyword of the element to search for. + + Returns: + pd.DataFrame: A DataFrame containing the search space based on the element keyword. + + Raises: + ValueError: If the element is not found in the sequence tags. + ValueError: If more than two elements are found in the sequence tags. + """ + tag: int = LOOKUP_TAG(keyword=element_keyword) + element: str = convert_int_to_element(combined_int=tag) + + df: pd.DataFrame = subsetSeriesTags( + series_tags_df=sequence_tags_df, element=element + ) + + return df + + +def camel_case_tag(string: str) -> str: + """ + Convert a string to camel case. + + Args: + string (str): The input string to be converted. + + Returns: + str: The camel case string. + + Example: + >>> camel_case_tag("hello world") + 'HelloWorld' + + Note: + This function does not actually convert to camel case to not modify + the tags from the DICOM dictionary. + """ + return "".join(word for word in string.split()) + + +def extract_ROI_info(StructureSetROISequence) -> dict[str, dict[str, str]]: + """ + Extracts ROI information from the StructureSetROISequence. + + Args: + StructureSetROISequence (pandas.DataFrame): A pandas DataFrame representing the StructureSetROISequence. + + Returns: + dict[str, dict[str, str]]: A dictionary containing ROI information, where the key is the ROI number and the value is the ROI information. + + Raises: + ValueError: If ROI Number is not found in the StructureSetROISequence. + """ + + # Initialize an empty dictionary to store ROI information + ROISet: dict[str, dict[str, str]] = {} + + # get the rows where name = " ROI Number" + ROI_indices = StructureSetROISequence[ + StructureSetROISequence["name"] == "ROI Number" + ].index + + if ROI_indices.empty: + raise ValueError("ROI Number not found in the StructureSetROISequence.") + + # Iterate between the indices of the ROI numbers, to extract the ROI information + # add to the dictionary where the key is the ROI number and the value is the ROI information + for i in range(len(ROI_indices) - 1): + ROI_number: str = StructureSetROISequence.loc[ROI_indices[i], "data"] + + ROI_info: pd.DataFrame = StructureSetROISequence.loc[ + ROI_indices[i] + 1 : ROI_indices[i + 1] - 1 + ] + + ROISet[ROI_number] = { + camel_case_tag(string=row["name"]): row["data"] + for _, row in ROI_info.iterrows() + } + + return ROISet + + +# def getRTSTRUCT_ROI_info(seriesUID: str) -> dict[str, dict[str, str]]: +# """ +# Given a SeriesInstanceUID of an RTSTRUCT, retrieves the ROI information. + +# Args: +# seriesUID (str): The SeriesInstanceUID of the RTSTRUCT. + +# Returns: +# dict[str, dict[str, str]]: A dictionary containing the ROI information. +# """ + +# RTSTRUCT_Tags = client.getDICOMTags(seriesUID) + +# StructureSetROISequence = getSequenceElement(sequence_tags_df=RTSTRUCT_Tags, element_keyword="StructureSetROISequence") + +# return extract_ROI_info(StructureSetROISequence) diff --git a/tests/test_tags.py b/tests/test_tags.py index b0c6be8..c3068bb 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -1,3 +1,4 @@ +from pandas import DataFrame import pytest from src.nbiatoolkit import NBIAClient from src.nbiatoolkit.dicomtags.tags import convert_int_to_element @@ -154,3 +155,31 @@ def test_getSeriesModality(RTSTRUCT_Tags): def test_failsubsetSeriesTags(RTSTRUCT_Series): with pytest.raises(KeyError) as e: subsetSeriesTags(RTSTRUCT_Series, "(0008,0060)") + + +def test_extract_ROI_info(RTSTRUCT_Tags): + # tests both getSequenceElement and extract_ROI_info + + StructureSetROISequence: DataFrame = getSequenceElement( + sequence_tags_df=RTSTRUCT_Tags, element_keyword="StructureSetROISequence" + ) + + # make sure that the StructureSetROISequence is not empty + assert ( + not StructureSetROISequence.empty + ), "Expected StructureSetROISequence to not be empty, but got empty" + + ROI_info: dict[str, dict[str, str]] = extract_ROI_info(StructureSetROISequence) + + assert ROI_info is not None, "Expected ROI_info to not be None, but got None" + + # ROI_info should have atleast 29 keys all of which are strings of ints from 1 to 28 + assert len(ROI_info) >= 26, f"Expected atleast 26 keys, but got {len(ROI_info)}" + keys = [int(key) for key in ROI_info.keys()] + + # assert all keys are between 1 and 29 + assert all( + [1 <= key <= 29 for key in keys] + ), "Expected all keys to be between 1 and 28" + + print("All test cases passed!") From ebbfc8529da875200eab554a79817177d9d2ea63 Mon Sep 17 00:00:00 2001 From: Jermiah Joseph Date: Mon, 1 Apr 2024 12:22:42 -0400 Subject: [PATCH 4/4] feat: add refseries query to nbia client --- src/nbiatoolkit/nbia.py | 22 ++++++++++++++++++++++ tests/test_tags.py | 8 ++++++++ 2 files changed, 30 insertions(+) diff --git a/src/nbiatoolkit/nbia.py b/src/nbiatoolkit/nbia.py index 7da54ed..2225ac4 100644 --- a/src/nbiatoolkit/nbia.py +++ b/src/nbiatoolkit/nbia.py @@ -21,6 +21,13 @@ ReturnType, conv_response_list, ) + +from .dicomtags.tags import ( + getReferencedSeriesUIDS, + extract_ROI_info, + getSequenceElement, +) + import pandas as pd import requests from requests.exceptions import JSONDecodeError as JSONDecodeError @@ -615,6 +622,21 @@ def getDICOMTags( return conv_response_list(response, returnType) + def getRefSeriesUIDs( + self, + SeriesInstanceUID: str, + ) -> List[str]: + + tags_df = self.getDICOMTags( + SeriesInstanceUID=SeriesInstanceUID, + return_type=ReturnType.DATAFRAME, + ) + + if type(tags_df) != pd.DataFrame: + raise ValueError("DICOM Tags not df or not found in the response.") + + return getReferencedSeriesUIDS(series_tags_df=tags_df) + def downloadSeries( self, SeriesInstanceUID: Union[str, list], diff --git a/tests/test_tags.py b/tests/test_tags.py index c3068bb..cdf350d 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -183,3 +183,11 @@ def test_extract_ROI_info(RTSTRUCT_Tags): ), "Expected all keys to be between 1 and 28" print("All test cases passed!") + + +def test_getReferencedSeriesUIDS(client, RTSTRUCT_Series): + result = client.getRefSeriesUIDs(RTSTRUCT_Series["SeriesInstanceUID"].values[0]) + + expected = ["1.3.6.1.4.1.14519.5.2.1.133742245714270925254982946723351496764"] + + assert result == expected, f"Expected {expected}, but got {result}"