From afdb3ce1763e0e37fb385d31236baefa4de23fcd Mon Sep 17 00:00:00 2001 From: Thiago Franco de Moraes Date: Fri, 9 Mar 2018 10:57:16 -0300 Subject: [PATCH] Open Multiframe dicom (#133) --- invesalius/control.py | 69 ++++++++++++++++++++++++++++++++++++++------------------------------- invesalius/data/imagedata_utils.py | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- invesalius/data/vtk_utils.py | 2 ++ invesalius/gui/dicom_preview_panel.py | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------- invesalius/reader/dicom.py | 22 ++++++++++++++++++---- invesalius/reader/dicom_grouper.py | 10 +++++----- invesalius/reader/dicom_reader.py | 45 +++++++++++++-------------------------------- 7 files changed, 254 insertions(+), 104 deletions(-) diff --git a/invesalius/control.py b/invesalius/control.py index f0e5ee5..a28d22a 100644 --- a/invesalius/control.py +++ b/invesalius/control.py @@ -404,8 +404,9 @@ class Controller(): Publisher.sendMessage('Begin busy cursor') else: #Is None if user canceled the load - self.progress_dialog.Close() - self.progress_dialog = None + if self.progress_dialog is not None: + self.progress_dialog.Close() + self.progress_dialog = None def OnLoadImportPanel(self, evt): patient_series = evt.data @@ -795,47 +796,53 @@ class Controller(): xyspacing = dicom.image.spacing orientation = dicom.image.orientation_label + wl = float(dicom.image.level) + ww = float(dicom.image.window) + if sop_class_uid == '1.2.840.10008.5.1.4.1.1.7': #Secondary Capture Image Storage use_dcmspacing = 1 else: use_dcmspacing = 0 imagedata = None - - sx, sy = size - n_slices = len(filelist) - resolution_percentage = utils.calculate_resizing_tofitmemory(int(sx), int(sy), n_slices, bits/8) - - if resolution_percentage < 1.0 and gui: - re_dialog = dialog.ResizeImageDialog() - re_dialog.SetValue(int(resolution_percentage*100)) - re_dialog_value = re_dialog.ShowModal() - re_dialog.Close() - - if re_dialog_value == wx.ID_OK: - percentage = re_dialog.GetValue() - resolution_percentage = percentage / 100.0 - else: - return - xyspacing = xyspacing[0] / resolution_percentage, xyspacing[1] / resolution_percentage - - - wl = float(dicom.image.level) - ww = float(dicom.image.window) - self.matrix, scalar_range, self.filename = image_utils.dcm2memmap(filelist, size, - orientation, resolution_percentage) + if dicom.image.number_of_frames == 1: + sx, sy = size + n_slices = len(filelist) + resolution_percentage = utils.calculate_resizing_tofitmemory(int(sx), int(sy), n_slices, bits/8) + + if resolution_percentage < 1.0 and gui: + re_dialog = dialog.ResizeImageDialog() + re_dialog.SetValue(int(resolution_percentage*100)) + re_dialog_value = re_dialog.ShowModal() + re_dialog.Close() + + if re_dialog_value == wx.ID_OK: + percentage = re_dialog.GetValue() + resolution_percentage = percentage / 100.0 + else: + return + + xyspacing = xyspacing[0] / resolution_percentage, xyspacing[1] / resolution_percentage + + self.matrix, scalar_range, self.filename = image_utils.dcm2memmap(filelist, size, + orientation, resolution_percentage) + + print xyspacing, zspacing + if orientation == 'AXIAL': + spacing = xyspacing[0], xyspacing[1], zspacing + elif orientation == 'CORONAL': + spacing = xyspacing[0], zspacing, xyspacing[1] + elif orientation == 'SAGITTAL': + spacing = zspacing, xyspacing[1], xyspacing[0] + else: + self.matrix, spacing, scalar_range, self.filename = image_utils.dcmmf2memmap(filelist[0], orientation) self.Slice = sl.Slice() self.Slice.matrix = self.matrix self.Slice.matrix_filename = self.filename - if orientation == 'AXIAL': - self.Slice.spacing = xyspacing[0], xyspacing[1], zspacing - elif orientation == 'CORONAL': - self.Slice.spacing = xyspacing[0], zspacing, xyspacing[1] - elif orientation == 'SAGITTAL': - self.Slice.spacing = zspacing, xyspacing[1], xyspacing[0] + self.Slice.spacing = spacing # 1(a): Fix gantry tilt, if any tilt_value = dicom.acquisition.tilt diff --git a/invesalius/data/imagedata_utils.py b/invesalius/data/imagedata_utils.py index 37e76cc..e3558b9 100644 --- a/invesalius/data/imagedata_utils.py +++ b/invesalius/data/imagedata_utils.py @@ -19,6 +19,7 @@ import math import os +import sys import tempfile import gdcm @@ -35,6 +36,16 @@ from invesalius.data import vtk_utils as vtk_utils import invesalius.reader.bitmap_reader as bitmap_reader import invesalius.utils as utils import invesalius.data.converters as converters + +if sys.platform == 'win32': + try: + import win32api + _has_win32api = True + except ImportError: + _has_win32api = False +else: + _has_win32api = False + # TODO: Test cases which are originally in sagittal/coronal orientation # and have gantry @@ -246,11 +257,79 @@ def ExtractVOI(imagedata,xi,xf,yi,yf,zi,zf): """ voi = vtk.vtkExtractVOI() voi.SetVOI(xi,xf,yi,yf,zi,zf) - voi.SetInput(imagedata) + voi.SetInputData(imagedata) voi.SetSampleRate(1, 1, 1) voi.Update() return voi.GetOutput() + +def create_dicom_thumbnails(filename, window=None, level=None): + rvtk = vtkgdcm.vtkGDCMImageReader() + rvtk.SetFileName(filename) + rvtk.Update() + + img = rvtk.GetOutput() + if window is None or level is None: + _min, _max = img.GetScalarRange() + window = _max - _min + level = _min + window / 2 + + dx, dy, dz = img.GetDimensions() + + if dz > 1: + thumbnail_paths = [] + for i in xrange(dz): + img_slice = ExtractVOI(img, 0, dx-1, 0, dy-1, i, i+1) + + colorer = vtk.vtkImageMapToWindowLevelColors() + colorer.SetInputData(img_slice) + colorer.SetWindow(window) + colorer.SetLevel(level) + colorer.SetOutputFormatToRGB() + colorer.Update() + + resample = vtk.vtkImageResample() + resample.SetInputData(colorer.GetOutput()) + resample.SetAxisMagnificationFactor ( 0, 0.25 ) + resample.SetAxisMagnificationFactor ( 1, 0.25 ) + resample.SetAxisMagnificationFactor ( 2, 1 ) + resample.Update() + + thumbnail_path = tempfile.mktemp() + + write_png = vtk.vtkPNGWriter() + write_png.SetInputData(resample.GetOutput()) + write_png.SetFileName(thumbnail_path) + write_png.Write() + + thumbnail_paths.append(thumbnail_path) + + return thumbnail_paths + else: + colorer = vtk.vtkImageMapToWindowLevelColors() + colorer.SetInputData(img) + colorer.SetWindow(window) + colorer.SetLevel(level) + colorer.SetOutputFormatToRGB() + colorer.Update() + + resample = vtk.vtkImageResample() + resample.SetInputData(colorer.GetOutput()) + resample.SetAxisMagnificationFactor ( 0, 0.25 ) + resample.SetAxisMagnificationFactor ( 1, 0.25 ) + resample.SetAxisMagnificationFactor ( 2, 1 ) + resample.Update() + + thumbnail_path = tempfile.mktemp() + + write_png = vtk.vtkPNGWriter() + write_png.SetInputData(resample.GetOutput()) + write_png.SetFileName(thumbnail_path) + write_png.Write() + + return thumbnail_path + + def CreateImageData(filelist, zspacing, xyspacing,size, bits, use_dcmspacing): message = _("Generating multiplanar visualization...") @@ -587,6 +666,29 @@ def dcm2memmap(files, slice_size, orientation, resolution_percentage): return matrix, scalar_range, temp_file +def dcmmf2memmap(dcm_file, orientation): + r = vtkgdcm.vtkGDCMImageReader() + r.SetFileName(dcm_file) + r.Update() + + temp_file = tempfile.mktemp() + + o = r.GetOutput() + x, y, z = o.GetDimensions() + spacing = o.GetSpacing() + + matrix = numpy.memmap(temp_file, mode='w+', dtype='int16', shape=(z, y, x)) + + d = numpy_support.vtk_to_numpy(o.GetPointData().GetScalars()) + d.shape = z, y, x + matrix[:] = d + matrix.flush() + + scalar_range = matrix.min(), matrix.max() + + return matrix, spacing, scalar_range, temp_file + + def img2memmap(group): """ From a nibabel image data creates a memmap file in the temp folder and diff --git a/invesalius/data/vtk_utils.py b/invesalius/data/vtk_utils.py index 69875c0..a6c2e53 100644 --- a/invesalius/data/vtk_utils.py +++ b/invesalius/data/vtk_utils.py @@ -52,6 +52,8 @@ def ShowProgress(number_of_filters = 1, # when the pipeline is larger than 1, we have to consider this object # percentage + if number_of_filters < 1: + number_of_filters = 1 ratio = (100.0 / number_of_filters) def UpdateProgress(obj, label=""): diff --git a/invesalius/gui/dicom_preview_panel.py b/invesalius/gui/dicom_preview_panel.py index 7679a66..79cf21c 100644 --- a/invesalius/gui/dicom_preview_panel.py +++ b/invesalius/gui/dicom_preview_panel.py @@ -115,7 +115,7 @@ class DicomInfo(object): """ Keep the informations and the image used by preview. """ - def __init__(self, id, dicom, title, subtitle): + def __init__(self, id, dicom, title, subtitle, n=0): self.id = id self.dicom = dicom self.title = title @@ -123,12 +123,15 @@ class DicomInfo(object): self._preview = None self.selected = False self.filename = "" + self._slice = n @property def preview(self): - if not self._preview: - bmp = wx.Bitmap(self.dicom.image.thumbnail_path, wx.BITMAP_TYPE_PNG) + if isinstance(self.dicom.image.thumbnail_path, list): + bmp = wx.Bitmap(self.dicom.image.thumbnail_path[self._slice], wx.BITMAP_TYPE_PNG) + else: + bmp = wx.Bitmap(self.dicom.image.thumbnail_path, wx.BITMAP_TYPE_PNG) self._preview = bmp.ConvertToImage() return self._preview @@ -539,11 +542,22 @@ class DicomPreviewSlice(wx.Panel): dicom_files = group.GetHandSortedList() n = 0 for dicom in dicom_files: - info = DicomInfo(n, dicom, - _("Image %d") % (dicom.image.number), - "%.2f" % (dicom.image.position[2])) - self.files.append(info) - n+=1 + if isinstance(dicom.image.thumbnail_path, list): + _slice = 0 + for thumbnail in dicom.image.thumbnail_path: + print thumbnail + info = DicomInfo(n, dicom, + _("Image %d") % (n), + "%.2f" % (dicom.image.position[2]), _slice) + self.files.append(info) + n+=1 + _slice += 1 + else: + info = DicomInfo(n, dicom, + _("Image %d") % (dicom.image.number), + "%.2f" % (dicom.image.position[2])) + self.files.append(info) + n+=1 scroll_range = len(self.files)/NCOLS if scroll_range * NCOLS < len(self.files): @@ -560,12 +574,23 @@ class DicomPreviewSlice(wx.Panel): dicom_files = group.GetHandSortedList() n = 0 for dicom in dicom_files: - info = DicomInfo(n, dicom, - _("Image %d") % (dicom.image.number), - "%.2f" % (dicom.image.position[2]), - ) - self.files.append(info) - n+=1 + if isinstance(dicom.image.thumbnail_path, list): + _slice = 0 + for thumbnail in dicom.image.thumbnail_path: + print thumbnail + info = DicomInfo(n, dicom, + _("Image %d") % int(n), + "%.2f" % (dicom.image.position[2]), _slice) + self.files.append(info) + n+=1 + _slice += 1 + else: + info = DicomInfo(n, dicom, + _("Image %d") % int(dicom.image.number), + "%.2f" % (dicom.image.position[2]), + ) + self.files.append(info) + n+=1 scroll_range = len(self.files)/NCOLS if scroll_range * NCOLS < len(self.files): @@ -803,14 +828,20 @@ class SingleImagePreview(wx.Panel): def SetDicomGroup(self, group): self.dicom_list = group.GetHandSortedList() self.current_index = 0 - self.nimages = len(self.dicom_list) + if len(self.dicom_list) > 1: + self.nimages = len(self.dicom_list) + else: + self.nimages = self.dicom_list[0].image.number_of_frames # GUI self.slider.SetMax(self.nimages-1) self.slider.SetValue(0) self.ShowSlice() def ShowSlice(self, index = 0): - dicom = self.dicom_list[index] + try: + dicom = self.dicom_list[index] + except IndexError: + dicom = self.dicom_list[0] # UPDATE GUI ## Text related to size @@ -845,28 +876,41 @@ class SingleImagePreview(wx.Panel): dicom.acquisition.time) self.text_acquisition.SetValue(value) - rdicom = vtkgdcm.vtkGDCMImageReader() - if _has_win32api: - rdicom.SetFileName(win32api.GetShortPathName(dicom.image.file).encode(const.FS_ENCODE)) + if isinstance(dicom.image.thumbnail_path, list): + reader = vtk.vtkPNGReader() + if _has_win32api: + reader.SetFileName(win32api.GetShortPathName(dicom.image.thumbnail_path[index]).encode(const.FS_ENCODE)) + else: + reader.SetFileName(dicom.image.thumbnail_path[index]) + reader.Update() + + image = reader.GetOutput() + else: - rdicom.SetFileName(dicom.image.file) - rdicom.Update() - - # ADJUST CONTRAST - window_level = dicom.image.level - window_width = dicom.image.window - colorer = vtk.vtkImageMapToWindowLevelColors() - colorer.SetInputConnection(rdicom.GetOutputPort()) - colorer.SetWindow(float(window_width)) - colorer.SetLevel(float(window_level)) - colorer.Update() + rdicom = vtkgdcm.vtkGDCMImageReader() + if _has_win32api: + rdicom.SetFileName(win32api.GetShortPathName(dicom.image.file).encode(const.FS_ENCODE)) + else: + rdicom.SetFileName(dicom.image.file) + rdicom.Update() + + # ADJUST CONTRAST + window_level = dicom.image.level + window_width = dicom.image.window + colorer = vtk.vtkImageMapToWindowLevelColors() + colorer.SetInputConnection(rdicom.GetOutputPort()) + colorer.SetWindow(float(window_width)) + colorer.SetLevel(float(window_level)) + colorer.Update() + + image = colorer.GetOutput() if self.actor is None: self.actor = vtk.vtkImageActor() self.renderer.AddActor(self.actor) # PLOT IMAGE INTO VIEWER - self.actor.SetInputData(colorer.GetOutput()) + self.actor.SetInputData(image) self.renderer.ResetCamera() self.interactor.Render() diff --git a/invesalius/reader/dicom.py b/invesalius/reader/dicom.py index 843fd94..960b252 100644 --- a/invesalius/reader/dicom.py +++ b/invesalius/reader/dicom.py @@ -1154,8 +1154,20 @@ class Parser(): if (data): return int(data) return "" - - + + def GetNumberOfFrames(self): + """ + Number of frames in a multi-frame image. + + DICOM standard tag (0x0028, 0x0008) was used. + """ + try: + data = self.data_image[str(0x028)][str(0x0008)] + except KeyError: + return 1 + return int(data) + + def GetPatientBirthDate(self): """ Return string containing the patient's birth date using the @@ -1498,11 +1510,11 @@ class Parser(): try: data = self.data_image[str(0x0020)][str(0x0013)] except(KeyError): - return "" + return 0 if (data): return int(data) - return "" + return 0 def GetStudyDescription(self): """ @@ -1954,6 +1966,8 @@ class Image(object): self.bits_allocad = parser._GetBitsAllocated() self.thumbnail_path = parser.thumbnail_path + self.number_of_frames = parser.GetNumberOfFrames() + if (parser.GetImageThickness()): try: spacing.append(parser.GetImageThickness()) diff --git a/invesalius/reader/dicom_grouper.py b/invesalius/reader/dicom_grouper.py index f92d012..6cb297b 100644 --- a/invesalius/reader/dicom_grouper.py +++ b/invesalius/reader/dicom_grouper.py @@ -94,22 +94,22 @@ class DicomGroup: if not self.dicom: self.dicom = dicom - pos = tuple(dicom.image.position) - + pos = tuple(dicom.image.position) + #Case to test: \other\higroma - #condition created, if any dicom with the same + #condition created, if any dicom with the same #position, but 3D, leaving the same series. if not "DERIVED" in dicom.image.type: #if any dicom with the same position if pos not in self.slices_dict.keys(): self.slices_dict[pos] = dicom - self.nslices += 1 + self.nslices += dicom.image.number_of_frames return True else: return False else: self.slices_dict[dicom.image.number] = dicom - self.nslices += 1 + self.nslices += dicom.image.number_of_frames return True def GetList(self): diff --git a/invesalius/reader/dicom_reader.py b/invesalius/reader/dicom_reader.py index 78a17b2..12c3cbc 100644 --- a/invesalius/reader/dicom_reader.py +++ b/invesalius/reader/dicom_reader.py @@ -36,6 +36,8 @@ import invesalius.session as session import glob import invesalius.utils as utils +from invesalius.data import imagedata_utils + import plistlib if sys.platform == 'win32': @@ -187,45 +189,22 @@ class LoadDicom: # -------------- To Create DICOM Thumbnail ----------- - rvtk = vtkgdcm.vtkGDCMImageReader() - if _has_win32api: - print 'dicom', win32api.GetShortPathName(self.filepath) - rvtk.SetFileName(win32api.GetShortPathName(self.filepath).encode(const.FS_ENCODE)) - else: - rvtk.SetFileName(self.filepath) - rvtk.Update() - + try: data = data_dict[str(0x028)][str(0x1050)] level = [float(value) for value in data.split('\\')][0] data = data_dict[str(0x028)][str(0x1051)] window = [float(value) for value in data.split('\\')][0] except(KeyError, ValueError): - level = 300.0 - window = 2000.0 - - colorer = vtk.vtkImageMapToWindowLevelColors() - colorer.SetInputConnection(rvtk.GetOutputPort()) - colorer.SetWindow(float(window)) - colorer.SetLevel(float(level)) - colorer.SetOutputFormatToRGB() - colorer.Update() - - resample = vtk.vtkImageResample() - resample.SetInputConnection(colorer.GetOutputPort()) - resample.SetAxisMagnificationFactor ( 0, 0.25 ) - resample.SetAxisMagnificationFactor ( 1, 0.25 ) - resample.SetAxisMagnificationFactor ( 2, 1 ) - resample.Update() - - thumbnail_path = tempfile.mktemp() - - write_png = vtk.vtkPNGWriter() - write_png.SetInputConnection(resample.GetOutputPort()) - write_png.SetFileName(thumbnail_path) - write_png.Write() - + level = None + window = None + + if _has_win32api: + thumbnail_path = imagedata_utils.create_dicom_thumbnails(win32api.GetShortPathName(self.filepath).encode(const.FS_ENCODE), window, level) + else: + thumbnail_path = imagedata_utils.create_dicom_thumbnails(self.filepath, window, level) + #------ Verify the orientation -------------------------------- img = reader.GetImage() @@ -362,12 +341,14 @@ class ProgressDicomReader: y = yGetDicomGroups(path, recursive) for value_progress in y: + print ">>>>", value_progress if not self.running: break if isinstance(value_progress, tuple): self.UpdateLoadFileProgress(value_progress) else: self.EndLoadFile(value_progress) + self.UpdateLoadFileProgress(None) #Is necessary in the case user cancel #the load, ensure that dicomdialog is closed -- libgit2 0.21.2