From 3b504bf15b3c5ca0a1e87b18a431a66f21227200 Mon Sep 17 00:00:00 2001 From: Steve Pieper Date: Wed, 8 Nov 2023 18:50:53 -0500 Subject: [PATCH] ENH: dicom url retries and code cleanup Catch if the returned data is not as expected and if so do a retry. Based on testing with a Google DICOMweb store about 5-10 request were incomplete on a several hundred slice dataset but no error code was reported, just truncated data. There's no clear reason to why these failures occur, but retrying implemented here has always resulted in good data during testing so far. --- .../DICOMPlugins/DICOMScalarVolumePlugin.py | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/Modules/Scripted/DICOMPlugins/DICOMScalarVolumePlugin.py b/Modules/Scripted/DICOMPlugins/DICOMScalarVolumePlugin.py index 9b363955db0..68b210b03a2 100644 --- a/Modules/Scripted/DICOMPlugins/DICOMScalarVolumePlugin.py +++ b/Modules/Scripted/DICOMPlugins/DICOMScalarVolumePlugin.py @@ -35,7 +35,7 @@ def __init__(self, epsilon=0.01): self.acquisitionModeling = None self.defaultStudyID = 'SLICER10001' # TODO: What should be the new study ID? self.urlHandlerInterval = 300 # ms - self.urlHandlerTimeout = 120 # seconds + self.urlHandlerTimeout = 60 # seconds self.tags['sopClassUID'] = "0008,0016" self.tags['photometricInterpretation'] = "0028,0004" @@ -409,7 +409,6 @@ def loadFilesWithSeriesReader(self, imageIOName, files, name, grayscale=True): logging.info("Loading with imageIOName: %s" % imageIOName) reader.Update() - slicer.modules.reader = reader if reader.GetErrorCode() != vtk.vtkErrorCode.NoError: errorStrings = (imageIOName, vtk.vtkErrorCode.GetStringFromErrorCode(reader.GetErrorCode())) logging.error("Could not read scalar volume using %s approach. Error is: %s" % errorStrings) @@ -431,9 +430,6 @@ def loadFilesWithSeriesReader(self, imageIOName, files, name, grayscale=True): volumeNode.SetRASToIJKMatrix(reader.GetRasToIjkMatrix()) volumeNode.CreateDefaultDisplayNodes() - slicer.modules.DICOMInstance.reader = reader - slicer.modules.DICOMInstance.imageChangeInformation = imageChangeInformation - return volumeNode def setVolumeNodeProperties(self, volumeNode, loadable): @@ -529,8 +525,7 @@ def _processURLHandler(self, urlHandler, volumeNode, urls): framesByURL = urlHandler.getFrames() volumeArray = slicer.util.arrayFromVolume(volumeNode) elapsedTime = time.time() - self._urlHandlerStartTime - msg = _("{framesThisLoad} of {frames} in {elapsedTime:.3g}").format(framesThisLoad=len(framesByURL), frames=volumeArray.shape[0], elapsedTime=elapsedTime) - print(msg) # TODO + msg = _("{framesThisLoad} of {frames} in {elapsedTime:.3f}").format(framesThisLoad=len(framesByURL), frames=volumeArray.shape[0], elapsedTime=elapsedTime) logging.info(msg) if len(framesByURL) > 0: for url,frame in framesByURL.items(): @@ -539,8 +534,20 @@ def _processURLHandler(self, urlHandler, volumeNode, urls): rescaleIntercept = float(rescaleInterceptValue) if rescaleInterceptValue != "" else 0. rescaleSlope = float(rescaleSlopeValue) if rescaleSlopeValue != "" else 1. sliceIndex = urls.index(url) - volumeArray[sliceIndex] = rescaleIntercept + rescaleSlope * frame + try: + volumeArray[sliceIndex] = rescaleIntercept + rescaleSlope * frame.reshape(volumeArray[sliceIndex].shape) + except ValueError: + logging.error(f"Incorrect frame size {frame.shape} not {volumeArray[sliceIndex].shape}") + logging.error(f"retrying {url}") + urlHandler.startRequest([url]) slicer.util.arrayFromVolumeModified(volumeNode) + displayNode = volumeNode.GetDisplayNode() + if displayNode.GetNumberOfWindowLevelPresets() > 0: + displayNode.SetWindowLevelFromPreset(0) + else: + displayNode.AutoWindowLevelOff() + displayNode.AutoWindowLevelOn() + if not urlHandler.requestFinished(): if elapsedTime > self.urlHandlerTimeout: logging.error("Download timeout") @@ -550,8 +557,8 @@ def _processURLHandler(self, urlHandler, volumeNode, urls): qt.QTimer.singleShot(self.urlHandlerInterval, callback) else: rate = (volumeArray.size * volumeArray.itemsize) / elapsedTime / 1024. / 1024. - logging.info(f"Download time {elapsedTime:.3g} at {rate:.2g} mebibytes/second") # TODO i18n - print(f"Download time {elapsedTime:.3g} at {rate:.2g} mebibytes/second") + msg = "Download time {elapsedTime:.2f} at {rate:.2f} mebibytes/second".format(elapsedTime=elapsedTime, rate=rate) + logging.info(msg) def load(self, loadable, readerApproach=None): """Load the select as a scalar volume using desired approach @@ -564,6 +571,8 @@ def load(self, loadable, readerApproach=None): # - data comes from a URL, so parse metadata from dicom to slicer conventions # - create a placeholder volumeNode that can be populated by the downloader # - rely on files having already been sorted during examine step + if not hasattr(slicer.modules, "dicomURLHandlers"): + slicer.modules.dicomURLHandlers = {} if urlScheme in slicer.modules.dicomURLHandlers: # first, initiate the request urlHandler = slicer.modules.dicomURLHandlers[urlScheme]