Skip to content

Commit

Permalink
Merge pull request #317 from michaelschwier/MeasurementReportUtils
Browse files Browse the repository at this point in the history
Util Classes to build Measurement Report and generate JSON
  • Loading branch information
fedorov authored Jan 12, 2018
2 parents f0dbc8f + 9475369 commit 20051ec
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ CMakeLists.txt.user*
__pycache__/
*.py[cod]
*$py.class

# VSCode #
######################
.vscode
7 changes: 7 additions & 0 deletions util/measurementReportUtils/__init__.py
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

47 changes: 47 additions & 0 deletions util/measurementReportUtils/codeSequences.py
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))

27 changes: 27 additions & 0 deletions util/measurementReportUtils/measurementGroup.py
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)




53 changes: 53 additions & 0 deletions util/measurementReportUtils/measurementItem.py
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"
}
74 changes: 74 additions & 0 deletions util/measurementReportUtils/measurementReport.py
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)

0 comments on commit 20051ec

Please sign in to comment.