def _get_full_block(grouped_dicoms): """ Generate a full datablock containing all timepoints """ # For each slice / mosaic create a data volume block data_blocks = [] for index in range(0, len(grouped_dicoms)): logger.info('Creating block %s of %s' % (index + 1, len(grouped_dicoms))) data_blocks.append(_timepoint_to_block(grouped_dicoms[index])) # Add the data_blocks together to one 4d block size_x = numpy.shape(data_blocks[0])[0] size_y = numpy.shape(data_blocks[0])[1] size_z = numpy.shape(data_blocks[0])[2] size_t = len(data_blocks) full_block = numpy.zeros((size_x, size_y, size_z, size_t), dtype=data_blocks[0].dtype) for index in range(0, size_t): if full_block[:, :, :, index].shape != data_blocks[index].shape: logger.warning( 'Missing slices (slice count mismatch between timepoint %s and %s)' % (index - 1, index)) logger.warning( '---------------------------------------------------------') logger.warning(full_block[:, :, :, index].shape) logger.warning(data_blocks[index].shape) logger.warning( '---------------------------------------------------------') raise ConversionError("MISSING_DICOM_FILES") full_block[:, :, :, index] = data_blocks[index] return full_block
def create_affine(sorted_dicoms): """ Function to generate the affine matrix for a dicom series This method was based on (http://nipy.org/nibabel/dicom/dicom_orientation.html) :param sorted_dicoms: list with sorted dicom files """ # Create affine matrix (http://nipy.sourceforge.net/nibabel/dicom/dicom_orientation.html#dicom-slice-affine) image_orient1 = numpy.array(sorted_dicoms[0].ImageOrientationPatient)[0:3] image_orient2 = numpy.array(sorted_dicoms[0].ImageOrientationPatient)[3:6] delta_r = float(sorted_dicoms[0].PixelSpacing[0]) delta_c = float(sorted_dicoms[0].PixelSpacing[1]) image_pos = numpy.array(sorted_dicoms[0].ImagePositionPatient) last_image_pos = numpy.array(sorted_dicoms[-1].ImagePositionPatient) if len(sorted_dicoms) == 1: # Single slice step = [0, 0, -1] else: step = (image_pos - last_image_pos) / (1 - len(sorted_dicoms)) # check if this is actually a volume and not all slices on the same location if numpy.linalg.norm(step) == 0.0: raise ConversionError("NOT_A_VOLUME") affine = numpy.matrix([[-image_orient1[0] * delta_c, -image_orient2[0] * delta_r, -step[0], -image_pos[0]], [-image_orient1[1] * delta_c, -image_orient2[1] * delta_r, -step[1], -image_pos[1]], [image_orient1[2] * delta_c, image_orient2[2] * delta_r, step[2], image_pos[2]], [0, 0, 0, 1]]) return affine
def _assert_explicit_vr(dicom_input): """ Assert that explicit vr is used """ if settings.validate_multiframe_implicit: header = dicom_input[0] if header.file_meta[0x0002, 0x0010].value == '1.2.840.10008.1.2': raise ConversionError('IMPLICIT_VR_ENHANCED_DICOM')
def dicom_to_nifti(dicom_input, output_file): """ This function will convert an anatomical dicom series to a nifti Examples: See unit test :param output_file: filepath to the output nifti :param dicom_input: directory with the dicom files for a single scan, or list of read in dicoms """ if len(dicom_input) <= 0: raise ConversionError('NO_DICOM_FILES_FOUND') # remove duplicate slices based on position and data dicom_input = _remove_duplicate_slices(dicom_input) # remove localizers based on image type dicom_input = _remove_localizers_by_imagetype(dicom_input) if settings.validate_slicecount: # remove_localizers based on image orientation (only valid if slicecount is validated) dicom_input = _remove_localizers_by_orientation(dicom_input) # validate all the dicom files for correct orientations common.validate_slicecount(dicom_input) if settings.validate_orientation: # validate that all slices have the same orientation common.validate_orientation(dicom_input) if settings.validate_orthogonal: # validate that we have an orthogonal image (to detect gantry tilting etc) common.validate_orthogonal(dicom_input) # sort the dicoms dicom_input = common.sort_dicoms(dicom_input) if settings.validate_sliceincrement: # validate that all slices have a consistent slice increment common.validate_sliceincrement(dicom_input) # Get data; originally z,y,x, transposed to x,y,z data = common.get_volume_pixeldata(dicom_input) affine = common.create_affine(dicom_input) # Convert to nifti nii_image = nibabel.Nifti1Image(data, affine) # Set TR and TE if available if Tag(0x0018, 0x0081) in dicom_input[0] and Tag(0x0018, 0x0081) in dicom_input[0]: common.set_tr_te(nii_image, float(dicom_input[0].RepetitionTime), float(dicom_input[0].EchoTime)) # Save to disk if output_file is not None: logger.info('Saving nifti to disk %s' % output_file) nii_image.to_filename(output_file) return {'NII_FILE': output_file, 'NII': nii_image}
def _get_mosaic_type(mosaic): """ Check the extra ascconv headers for the mosaic type based on the slice position We always assume axial in this case the implementation resembles the last lines of documentation in https://www.icts.uiowa.edu/confluence/plugins/viewsource/viewpagesrc.action?pageId=54756326 """ ascconv_headers = _get_asconv_headers(mosaic) try: size = int( re.findall(r'sSliceArray\.lSize\s*=\s*(\d+)', ascconv_headers)[0]) # get the locations of the slices slice_location = [None] * size for index in range(size): axial_result = re.findall( r'sSliceArray\.asSlice\[%s\]\.sPosition\.dTra\s*=\s*([-+]?[0-9]*\.?[0-9]*)' % index, ascconv_headers) if len(axial_result) > 0: axial = float(axial_result[0]) else: axial = 0.0 slice_location[index] = axial # should we invert (https://www.icts.uiowa.edu/confluence/plugins/viewsource/viewpagesrc.action?pageId=54756326) invert = False invert_result = re.findall( r'sSliceArray\.ucImageNumbTra\s*=\s*([-+]?0?x?[0-9]+)', ascconv_headers) if len(invert_result) > 0: invert_value = int(invert_result[0], 16) if invert_value >= 0: invert = True # return the correct slice types if slice_location[0] <= slice_location[1]: if not invert: return MosaicType.ASCENDING else: return MosaicType.DESCENDING else: if not invert: return MosaicType.DESCENDING else: return MosaicType.ASCENDING except: traceback.print_exc() raise ConversionError("MOSAIC_TYPE_NOT_SUPPORTED")
def _get_gdcmconv(): """ Get the full path to gdcmconv. If not found raise error """ gdcmconv_executable = settings.gdcmconv_path if gdcmconv_executable is None: gdcmconv_executable = _which('gdcmconv') if gdcmconv_executable is None: gdcmconv_executable = _which('gdcmconv.exe') if gdcmconv_executable is None: raise ConversionError('GDCMCONV_NOT_FOUND') return gdcmconv_executable
def compress_directory(dicom_directory): """ This function can be used to convert a folder of jpeg compressed images to an uncompressed ones :param dicom_directory: directory of dicom files to compress """ if is_compressed(dicom_directory): return if _which('gdcmconv') is None and _which('gdcmconv.exe') is None: raise ConversionError('GDCMCONV_NOT_FOUND') print('Compressing dicom files in %s' % dicom_directory) for root, _, files in os.walk(dicom_directory): for dicom_file in files: if common.is_dicom_file(os.path.join(root, dicom_file)): compress_dicom(os.path.join(root, dicom_file))
def dicom_series_to_nifti(original_dicom_directory, output_file, reorient_nifti=True): """ Converts dicom single series (see pydicom) to nifty, mimicking SPM Examples: See unit test will return a dictionary containing - the NIFTI under key 'NIFTI' - the NIFTI file path under 'NII_FILE' - the BVAL file path under 'BVAL_FILE' (only for dti) - the BVEC file path under 'BVEC_FILE' (only for dti) IMPORTANT: If no specific sequence type can be found it will default to anatomical and try to convert. You should check that the data you are trying to convert is supported by this code Inspired by http://nipy.sourceforge.net/nibabel/dicom/spm_dicom.html Inspired by http://code.google.com/p/pydicom/source/browse/source/dicom/contrib/pydicom_series.py :param reorient_nifti: if True the nifti affine and data will be updated so the data is stored LAS oriented :param output_file: file path to write to :param original_dicom_directory: directory with the dicom files for a single series/scan """ # copy files so we can can modify without altering the original temp_directory = tempfile.mkdtemp() try: dicom_directory = os.path.join(temp_directory, 'dicom') shutil.copytree(original_dicom_directory, dicom_directory) decompress_directory(dicom_directory) dicom_input = common.read_dicom_directory(dicom_directory) return dicom_array_to_nifti(dicom_input, output_file, reorient_nifti) except AttributeError as exception: reraise(tp=ConversionError, value=ConversionError(str(exception)), tb=sys.exc_info()[2]) finally: # remove the copied data shutil.rmtree(temp_directory)
def decompress_directory(dicom_directory): """ This function can be used to convert a folder of jpeg compressed images to an uncompressed ones :param dicom_directory: directory with dicom files to decompress """ if not is_compressed(dicom_directory): return if settings.gdcmconv_path is None and _which( 'gdcmconv') is None and _which('gdcmconv.exe') is None: raise ConversionError('GDCMCONV_NOT_FOUND') logger.info('Decompressing dicom files in %s' % dicom_directory) for root, _, files in os.walk(dicom_directory): for dicom_file in files: if common.is_dicom_file(os.path.join(root, dicom_file)): decompress_dicom(os.path.join(root, dicom_file))
def _get_first_header(dicom_directory): """ Function to get the first dicom file form a directory and return the header Useful to determine the type of data to convert :param dicom_directory: directory with dicom files """ # looping over all files for root, _, file_names in os.walk(dicom_directory): # go over all the files and try to read the dicom header for file_name in file_names: file_path = os.path.join(root, file_name) # check wither it is a dicom file if not common.is_dicom_file(file_path): continue # read the headers return dicom.read_file(file_path, stop_before_pixels=True) # no dicom files found raise ConversionError('NO_DICOM_FILES_FOUND')
def _singleframe_to_block(grouped_dicoms): """ Generate a full datablock containing all timepoints """ # For each slice / mosaic create a data volume block data_blocks = [] for index in range(0, len(grouped_dicoms)): print('Creating block %s of %s' % (index + 1, len(grouped_dicoms))) current_block = _stack_to_block(grouped_dicoms[index]) current_block = current_block[:, :, :, numpy.newaxis] data_blocks.append(current_block) try: full_block = numpy.concatenate(data_blocks, axis=3) except: traceback.print_exc() raise ConversionError("MISSING_DICOM_FILES") # Apply the rescaling if needed common.apply_scaling(full_block, grouped_dicoms[0][0]) return full_block
def dicom_to_nifti(dicom_input, output_file): """ This function will convert an anatomical dicom series to a nifti Examples: See unit test :param output_file: filepath to the output nifti :param dicom_input: directory with the dicom files for a single scan, or list of read in dicoms """ if len(dicom_input) <= 0: raise ConversionError('NO_DICOM_FILES_FOUND') # remove duplicate slices based on position and data dicom_input = remove_duplicate_slices(dicom_input) # remove localizers based on image type dicom_input = remove_localizers_by_imagetype(dicom_input) # if no dicoms remain we should raise exception if len(dicom_input) < 1: raise ConversionValidationError('TOO_FEW_SLICES/LOCALIZER') if settings.validate_slicecount: common.validate_slicecount(dicom_input) # remove_localizers based on image orientation (only valid if slicecount is validated) dicom_input = remove_localizers_by_orientation(dicom_input) # validate all the dicom files for correct orientations common.validate_slicecount(dicom_input) if settings.validate_orientation: # validate that all slices have the same orientation common.validate_orientation(dicom_input) if settings.validate_orthogonal: # validate that we have an orthogonal image (to detect gantry tilting etc) common.validate_orthogonal(dicom_input) # sort the dicoms dicom_input = common.sort_dicoms(dicom_input) # validate slice increment inconsistent slice_increment_inconsistent = False if settings.validate_slice_increment: # validate that all slices have a consistent slice increment common.validate_slice_increment(dicom_input) elif common.is_slice_increment_inconsistent(dicom_input): slice_increment_inconsistent = True if settings.validate_instance_number: # validate that all slices have a consistent instance_number common.validate_instance_number(dicom_input) # if inconsistent increment and we allow resampling then do the resampling based conversion to maintain the correct geometric shape if slice_increment_inconsistent and settings.resample: nii_image, max_slice_increment = _convert_slice_incement_inconsistencies(dicom_input) # do the normal conversion else: # Get data; originally z,y,x, transposed to x,y,z data = common.get_volume_pixeldata(dicom_input) affine, max_slice_increment = common.create_affine(dicom_input) # Convert to nifti nii_image = nibabel.Nifti1Image(data.squeeze(), affine) # Set TR and TE if available if Tag(0x0018, 0x0080) in dicom_input[0] and Tag(0x0018, 0x0081) in dicom_input[0]: common.set_tr_te(nii_image, float(dicom_input[0].RepetitionTime), float(dicom_input[0].EchoTime)) # Save to disk if output_file is not None: logger.info('Saving nifti to disk %s' % output_file) nii_image.header.set_slope_inter(1, 0) nii_image.header.set_xyzt_units(2) # set units for xyz (leave t as unknown) nii_image.to_filename(output_file) return {'NII_FILE': output_file, 'NII': nii_image, 'MAX_SLICE_INCREMENT': max_slice_increment}