-
Notifications
You must be signed in to change notification settings - Fork 61
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #317 from michaelschwier/MeasurementReportUtils
Util Classes to build Measurement Report and generate JSON
- Loading branch information
Showing
6 changed files
with
212 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,3 +35,7 @@ CMakeLists.txt.user* | |
__pycache__/ | ||
*.py[cod] | ||
*$py.class | ||
|
||
# VSCode # | ||
###################### | ||
.vscode |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# Defining convenience import shortcuts for those classes which are | ||
# supposed to be used publicly | ||
from .measurementReport import MeasurementReport | ||
from .measurementGroup import MeasurementGroup | ||
from .measurementItem import VolumeMeasurementItem, MeanADCMeasurementItem | ||
from .codeSequences import CodeSequence, Finding, FindingSite, ProcedureReported | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
|
||
class CodeSequence(object): | ||
|
||
def __init__(self, codeMeaning, codingSchemeDesignator, codeValue): | ||
self.CodeValue = codeValue | ||
self.CodingSchemeDesignator = codingSchemeDesignator | ||
self.CodeMeaning = codeMeaning | ||
|
||
|
||
class Finding(CodeSequence): | ||
|
||
def __init__(self, segmentedStructure): | ||
if segmentedStructure == "NormalROI_PZ_1": | ||
super().__init__("Normal", "SRT", "G-A460") | ||
elif segmentedStructure == "PeripheralZone": | ||
super().__init__("Entire", "SRT", "R-404A4") | ||
elif segmentedStructure == "TumorROI_PZ_1": | ||
super().__init__("Abnormal", "SRT", "R-42037") | ||
elif segmentedStructure == "WholeGland": | ||
super().__init__("Entire Gland", "SRT", "T-F6078") | ||
else: | ||
raise ValueError("Segmented Structure Type {} is not supported yet. Build your own Finding code sequence using the class CodeSequence".format(segmentedStructure)) | ||
|
||
|
||
class FindingSite(CodeSequence): | ||
|
||
def __init__(self, segmentedStructure): | ||
if segmentedStructure == "NormalROI_PZ_1": | ||
super().__init__("Peripheral zone of the prostate", "SRT", "T-D05E4") | ||
elif segmentedStructure == "PeripheralZone": | ||
super().__init__("Peripheral zone of the prostate", "SRT", "T-D05E4") | ||
elif segmentedStructure == "TumorROI_PZ_1": | ||
super().__init__("Peripheral zone of the prostate", "SRT", "T-D05E4") | ||
elif segmentedStructure == "WholeGland": | ||
super().__init__("Prostate", "SRT", "T-9200B") | ||
else: | ||
raise ValueError("Segmented Structure Type {} is not supported yet. Build your own FindingSite code sequence using the class CodeSequence".format(segmentedStructure)) | ||
|
||
|
||
class ProcedureReported(CodeSequence): | ||
|
||
def __init__(self, codeMeaning): | ||
if codeMeaning == "Multiparametric MRI of prostate": | ||
super().__init__("Multiparametric MRI of prostate", "DCM", "126021") | ||
else: | ||
raise ValueError("Procedure Type {} is not supported yet. Build your own ProcedureReported code sequence using the class CodeSequence".format(codeMeaning)) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
|
||
class MeasurementGroup(object): | ||
""" | ||
Data structure plus convenience methods to create measurment groups following | ||
the required format to be processed by the DCMQI tid1500writer tool. Use this | ||
to populate the Measurements list in :class:`MeasurementReport`. | ||
""" | ||
|
||
def __init__(self, | ||
trackingIdentifier, trackingUniqueIdentifier, referencedSegment, | ||
sourceSeriesInstanceUID, segmentationSOPInstanceUID, | ||
finding, findingSite): | ||
self.TrackingIdentifier = trackingIdentifier | ||
self.TrackingUniqueIdentifier = trackingUniqueIdentifier | ||
self.ReferencedSegment = referencedSegment | ||
self.SourceSeriesForImageSegmentation = sourceSeriesInstanceUID | ||
self.segmentationSOPInstanceUID = segmentationSOPInstanceUID | ||
self.Finding = finding | ||
self.FindingSite = findingSite | ||
self.measurementItems = [] | ||
|
||
def addMeasurementItem(self, measurementItem): | ||
self.measurementItems.append(measurementItem) | ||
|
||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
|
||
class MeasurementItem(object): | ||
|
||
def __init__(self, value): | ||
self.value = self.convertNumericToDcmtkFittingString(value) | ||
|
||
def convertNumericToDcmtkFittingString(self, value): | ||
if isinstance(value, float) or isinstance(value, int): | ||
s = str(value) | ||
if (len(s) <= 16): | ||
return s | ||
elif (s.find(".") >= 0) and (s.find(".") < 15): | ||
return s[:16] | ||
else: | ||
raise ValueError("Value cannot be converted to 16 digits without loosing too much precision!") | ||
else: | ||
raise TypeError("Value to convert is not of type float or int") | ||
|
||
|
||
class VolumeMeasurementItem(MeasurementItem): | ||
|
||
def __init__(self, value): | ||
super().__init__(value) | ||
self.quantity = { | ||
"CodeValue": "G-D705", | ||
"CodingSchemeDesignator": "SRT", | ||
"CodeMeaning": "Volume" | ||
} | ||
self.units = { | ||
"CodeValue": "cm3", | ||
"CodingSchemeDesignator": "UCUM", | ||
"CodeMeaning": "cubic centimeter" | ||
} | ||
|
||
|
||
class MeanADCMeasurementItem(MeasurementItem): | ||
def __init__(self, value): | ||
super().__init__(value) | ||
self.quantity = { | ||
"CodeValue": "113041", | ||
"CodingSchemeDesignator": "DCM", | ||
"CodeMeaning": "Apparent Diffusion Coefficient" | ||
} | ||
self.units = { | ||
"CodeValue": "um2/s", | ||
"CodingSchemeDesignator": "UCUM", | ||
"CodeMeaning": "um2/s" | ||
} | ||
self.derivationModifier = { | ||
"CodeValue": "R-00317", | ||
"CodingSchemeDesignator": "SRT", | ||
"CodeMeaning": "Mean" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import json | ||
|
||
from .measurementGroup import MeasurementGroup | ||
from .measurementItem import MeasurementItem | ||
from .codeSequences import CodeSequence | ||
|
||
class MeasurementReport(object): | ||
""" | ||
Data structure plus convenience methods to create measurment reports following | ||
the required format to be processed by the DCMQI tid1500writer tool (using the | ||
JSON export of this). | ||
""" | ||
|
||
def __init__(self, seriesNumber, compositeContext, dicomSourceFileList, timePoint, | ||
seriesDescription = "Measurements", procedureReported = None): | ||
self.SeriesDescription = str(seriesDescription) | ||
self.SeriesNumber = str(seriesNumber) | ||
self.InstanceNumber = "1" | ||
|
||
self.compositeContext = [compositeContext] | ||
|
||
self.imageLibrary = dicomSourceFileList | ||
|
||
self.observerContext = { | ||
"ObserverType": "PERSON", | ||
"PersonObserverName": "Reader01" | ||
} | ||
|
||
if procedureReported: | ||
self.procedureReported = procedureReported | ||
|
||
self.VerificationFlag = "VERIFIED" | ||
self.CompletionFlag = "COMPLETE" | ||
|
||
self.activitySession = "1" | ||
self.timePoint = str(timePoint) | ||
|
||
self.Measurements = [] | ||
|
||
|
||
def addMeasurementGroup(self, measurementGroup): | ||
self.Measurements.append(measurementGroup) | ||
|
||
|
||
def exportToJson(self, fileName): | ||
with open(fileName, 'w') as fp: | ||
json.dump(self._getAsDict(), fp, indent = 2) | ||
|
||
|
||
def getJsonStr(self): | ||
return json.dumps(self._getAsDict(), indent = 2) | ||
|
||
|
||
def _getAsDict(self): | ||
# This is a bit of a hack to get the "@schema" in there, didn't figure out how to | ||
# do this otherwise with json.dumps. If this wasn't needed I could just dump | ||
# the json directly with my custom encoder. | ||
jsonStr = json.dumps(self, indent = 2, cls = self._MyJSONEncoder) | ||
tempDict = json.loads(jsonStr) | ||
outDict = {} | ||
outDict["@schema"] = "https://raw.githubusercontent.com/qiicr/dcmqi/master/doc/schemas/sr-tid1500-schema.json#" | ||
outDict.update(tempDict) | ||
return outDict | ||
|
||
# Inner private class to define a custom JSON encoder for serializing MeasurmentReport | ||
class _MyJSONEncoder(json.JSONEncoder): | ||
def default(self, obj): | ||
if (isinstance(obj, MeasurementReport) or | ||
isinstance(obj, MeasurementGroup) or | ||
isinstance(obj, MeasurementItem) or | ||
isinstance(obj, CodeSequence)): | ||
return obj.__dict__ | ||
else: | ||
return super(MyEncoder, self).default(obj) |