Skip to content

Commit 20051ec

Browse files
authored
Merge pull request #317 from michaelschwier/MeasurementReportUtils
Util Classes to build Measurement Report and generate JSON
2 parents f0dbc8f + 9475369 commit 20051ec

File tree

6 files changed

+212
-0
lines changed

6 files changed

+212
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,7 @@ CMakeLists.txt.user*
3535
__pycache__/
3636
*.py[cod]
3737
*$py.class
38+
39+
# VSCode #
40+
######################
41+
.vscode
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Defining convenience import shortcuts for those classes which are
2+
# supposed to be used publicly
3+
from .measurementReport import MeasurementReport
4+
from .measurementGroup import MeasurementGroup
5+
from .measurementItem import VolumeMeasurementItem, MeanADCMeasurementItem
6+
from .codeSequences import CodeSequence, Finding, FindingSite, ProcedureReported
7+
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
2+
class CodeSequence(object):
3+
4+
def __init__(self, codeMeaning, codingSchemeDesignator, codeValue):
5+
self.CodeValue = codeValue
6+
self.CodingSchemeDesignator = codingSchemeDesignator
7+
self.CodeMeaning = codeMeaning
8+
9+
10+
class Finding(CodeSequence):
11+
12+
def __init__(self, segmentedStructure):
13+
if segmentedStructure == "NormalROI_PZ_1":
14+
super().__init__("Normal", "SRT", "G-A460")
15+
elif segmentedStructure == "PeripheralZone":
16+
super().__init__("Entire", "SRT", "R-404A4")
17+
elif segmentedStructure == "TumorROI_PZ_1":
18+
super().__init__("Abnormal", "SRT", "R-42037")
19+
elif segmentedStructure == "WholeGland":
20+
super().__init__("Entire Gland", "SRT", "T-F6078")
21+
else:
22+
raise ValueError("Segmented Structure Type {} is not supported yet. Build your own Finding code sequence using the class CodeSequence".format(segmentedStructure))
23+
24+
25+
class FindingSite(CodeSequence):
26+
27+
def __init__(self, segmentedStructure):
28+
if segmentedStructure == "NormalROI_PZ_1":
29+
super().__init__("Peripheral zone of the prostate", "SRT", "T-D05E4")
30+
elif segmentedStructure == "PeripheralZone":
31+
super().__init__("Peripheral zone of the prostate", "SRT", "T-D05E4")
32+
elif segmentedStructure == "TumorROI_PZ_1":
33+
super().__init__("Peripheral zone of the prostate", "SRT", "T-D05E4")
34+
elif segmentedStructure == "WholeGland":
35+
super().__init__("Prostate", "SRT", "T-9200B")
36+
else:
37+
raise ValueError("Segmented Structure Type {} is not supported yet. Build your own FindingSite code sequence using the class CodeSequence".format(segmentedStructure))
38+
39+
40+
class ProcedureReported(CodeSequence):
41+
42+
def __init__(self, codeMeaning):
43+
if codeMeaning == "Multiparametric MRI of prostate":
44+
super().__init__("Multiparametric MRI of prostate", "DCM", "126021")
45+
else:
46+
raise ValueError("Procedure Type {} is not supported yet. Build your own ProcedureReported code sequence using the class CodeSequence".format(codeMeaning))
47+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
2+
class MeasurementGroup(object):
3+
"""
4+
Data structure plus convenience methods to create measurment groups following
5+
the required format to be processed by the DCMQI tid1500writer tool. Use this
6+
to populate the Measurements list in :class:`MeasurementReport`.
7+
"""
8+
9+
def __init__(self,
10+
trackingIdentifier, trackingUniqueIdentifier, referencedSegment,
11+
sourceSeriesInstanceUID, segmentationSOPInstanceUID,
12+
finding, findingSite):
13+
self.TrackingIdentifier = trackingIdentifier
14+
self.TrackingUniqueIdentifier = trackingUniqueIdentifier
15+
self.ReferencedSegment = referencedSegment
16+
self.SourceSeriesForImageSegmentation = sourceSeriesInstanceUID
17+
self.segmentationSOPInstanceUID = segmentationSOPInstanceUID
18+
self.Finding = finding
19+
self.FindingSite = findingSite
20+
self.measurementItems = []
21+
22+
def addMeasurementItem(self, measurementItem):
23+
self.measurementItems.append(measurementItem)
24+
25+
26+
27+
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
2+
class MeasurementItem(object):
3+
4+
def __init__(self, value):
5+
self.value = self.convertNumericToDcmtkFittingString(value)
6+
7+
def convertNumericToDcmtkFittingString(self, value):
8+
if isinstance(value, float) or isinstance(value, int):
9+
s = str(value)
10+
if (len(s) <= 16):
11+
return s
12+
elif (s.find(".") >= 0) and (s.find(".") < 15):
13+
return s[:16]
14+
else:
15+
raise ValueError("Value cannot be converted to 16 digits without loosing too much precision!")
16+
else:
17+
raise TypeError("Value to convert is not of type float or int")
18+
19+
20+
class VolumeMeasurementItem(MeasurementItem):
21+
22+
def __init__(self, value):
23+
super().__init__(value)
24+
self.quantity = {
25+
"CodeValue": "G-D705",
26+
"CodingSchemeDesignator": "SRT",
27+
"CodeMeaning": "Volume"
28+
}
29+
self.units = {
30+
"CodeValue": "cm3",
31+
"CodingSchemeDesignator": "UCUM",
32+
"CodeMeaning": "cubic centimeter"
33+
}
34+
35+
36+
class MeanADCMeasurementItem(MeasurementItem):
37+
def __init__(self, value):
38+
super().__init__(value)
39+
self.quantity = {
40+
"CodeValue": "113041",
41+
"CodingSchemeDesignator": "DCM",
42+
"CodeMeaning": "Apparent Diffusion Coefficient"
43+
}
44+
self.units = {
45+
"CodeValue": "um2/s",
46+
"CodingSchemeDesignator": "UCUM",
47+
"CodeMeaning": "um2/s"
48+
}
49+
self.derivationModifier = {
50+
"CodeValue": "R-00317",
51+
"CodingSchemeDesignator": "SRT",
52+
"CodeMeaning": "Mean"
53+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import json
2+
3+
from .measurementGroup import MeasurementGroup
4+
from .measurementItem import MeasurementItem
5+
from .codeSequences import CodeSequence
6+
7+
class MeasurementReport(object):
8+
"""
9+
Data structure plus convenience methods to create measurment reports following
10+
the required format to be processed by the DCMQI tid1500writer tool (using the
11+
JSON export of this).
12+
"""
13+
14+
def __init__(self, seriesNumber, compositeContext, dicomSourceFileList, timePoint,
15+
seriesDescription = "Measurements", procedureReported = None):
16+
self.SeriesDescription = str(seriesDescription)
17+
self.SeriesNumber = str(seriesNumber)
18+
self.InstanceNumber = "1"
19+
20+
self.compositeContext = [compositeContext]
21+
22+
self.imageLibrary = dicomSourceFileList
23+
24+
self.observerContext = {
25+
"ObserverType": "PERSON",
26+
"PersonObserverName": "Reader01"
27+
}
28+
29+
if procedureReported:
30+
self.procedureReported = procedureReported
31+
32+
self.VerificationFlag = "VERIFIED"
33+
self.CompletionFlag = "COMPLETE"
34+
35+
self.activitySession = "1"
36+
self.timePoint = str(timePoint)
37+
38+
self.Measurements = []
39+
40+
41+
def addMeasurementGroup(self, measurementGroup):
42+
self.Measurements.append(measurementGroup)
43+
44+
45+
def exportToJson(self, fileName):
46+
with open(fileName, 'w') as fp:
47+
json.dump(self._getAsDict(), fp, indent = 2)
48+
49+
50+
def getJsonStr(self):
51+
return json.dumps(self._getAsDict(), indent = 2)
52+
53+
54+
def _getAsDict(self):
55+
# This is a bit of a hack to get the "@schema" in there, didn't figure out how to
56+
# do this otherwise with json.dumps. If this wasn't needed I could just dump
57+
# the json directly with my custom encoder.
58+
jsonStr = json.dumps(self, indent = 2, cls = self._MyJSONEncoder)
59+
tempDict = json.loads(jsonStr)
60+
outDict = {}
61+
outDict["@schema"] = "https://raw.githubusercontent.com/qiicr/dcmqi/master/doc/schemas/sr-tid1500-schema.json#"
62+
outDict.update(tempDict)
63+
return outDict
64+
65+
# Inner private class to define a custom JSON encoder for serializing MeasurmentReport
66+
class _MyJSONEncoder(json.JSONEncoder):
67+
def default(self, obj):
68+
if (isinstance(obj, MeasurementReport) or
69+
isinstance(obj, MeasurementGroup) or
70+
isinstance(obj, MeasurementItem) or
71+
isinstance(obj, CodeSequence)):
72+
return obj.__dict__
73+
else:
74+
return super(MyEncoder, self).default(obj)

0 commit comments

Comments
 (0)