preprocess.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. from pathlib import Path
  2. import xmltodict
  3. import argparse
  4. import javabridge
  5. import bioformats
  6. import numpy as np
  7. import nibabel as nib
  8. from PIL import Image, ImageSequence
  9. from scipy import ndimage
  10. from skimage import morphology, filters
  11. from nipype.interfaces.ants.segmentation import N4BiasFieldCorrection
  12. def get_out_paths(out_dir, stem):
  13. out_path_scaled = Path(out_dir, f'{stem}.nii')
  14. out_path_unscaled = Path(out_dir, f'{stem}_us.nii')
  15. return out_path_scaled, out_path_unscaled
  16. def bias_field_correction(image, output, dim=2):
  17. N4BiasFieldCorrection(
  18. input_image=image,
  19. output_image=output,
  20. dimension=dim
  21. ).run()
  22. def image_data_to_nii(pixdim, image_data, shrink, out_dir, file_path, save_unscaled=False):
  23. """
  24. Saves an array of image data as a nifti file, with correct pixel dimensions in
  25. :param save_unscaled: boolean whether to save unscaled, as well as scaled nifti file
  26. :param shrink: factor by which to shrink image dimensions by
  27. :return: location of scaled and possibly unscaled nifti files
  28. """
  29. image_dim = (image_data.shape[0], image_data.shape[1])
  30. scale = 1/shrink
  31. new_dim = (round(image_dim[1] * scale), round(image_dim[0] * scale))
  32. new_arr = np.ndarray(new_dim + (image_data.shape[2],))
  33. for i in range(0,image_data.shape[2]):
  34. cur_channel = image_data[:, :, i]
  35. resized = np.array(Image.fromarray(cur_channel).resize(new_dim)).transpose()
  36. new_arr[:, :, i] = resized
  37. path_scaled, path_unscaled = get_out_paths(out_dir, Path(file_path).stem)
  38. nii_scaled = nib.Nifti1Image(new_arr, np.eye(4))
  39. nii_scaled.header['xyzt_units'] = 3
  40. nii_scaled.header['pixdim'][1:3] = pixdim * shrink, pixdim * shrink
  41. nib.save(nii_scaled, str(path_scaled))
  42. if save_unscaled:
  43. nii_unscaled = nib.Nifti1Image(image_data, np.eye(4))
  44. nii_unscaled.header['xyzt_units'] = 3
  45. nii_unscaled.header['pixdim'][1:3] = pixdim, pixdim
  46. nib.save(nii_unscaled, str(path_unscaled))
  47. print(f'Preprocessed: {path_scaled}\n')
  48. return path_scaled, path_unscaled
  49. def nd2_to_nii(nd2_path, out_dir, series, shrink=10):
  50. """
  51. Wrapper function for image_data_to_nii, for converting nd2 files to nifti
  52. """
  53. javabridge.start_vm(bioformats.JARS)
  54. image = np.array(bioformats.load_image(str(nd2_path), series=series-1, rescale=False))
  55. meta_dict = xmltodict.parse(bioformats.get_omexml_metadata(str(nd2_path)))
  56. vox_size_x = float(meta_dict['OME']['Image'][series-1]['Pixels']['@PhysicalSizeX'])
  57. javabridge.kill_vm()
  58. return image_data_to_nii(vox_size_x, image, shrink, out_dir, nd2_path)
  59. def tiff_to_nii(tif_path, out_dir, pixdim=None, shrink=10):
  60. """
  61. Wrapper function for image_data_to_nii, for converting tiff files to nifti
  62. """
  63. tif_image = Image.open(tif_path)
  64. tif_header = dict(tif_image.tag)
  65. output = np.empty(np.array(tif_image).shape + (0,))
  66. if not pixdim:
  67. pixdim = 10e6/tif_header[282][0][0]
  68. for i, page in enumerate(ImageSequence.Iterator(tif_image)):
  69. page_data = np.expand_dims(np.array(page), 2)
  70. output = np.concatenate((output, page_data), 2)
  71. return image_data_to_nii(pixdim, output, shrink, out_dir, tif_path)
  72. def split_nii_channels(nii_path, out_dir=None, flip=False, mask_index=-1, bias=False):
  73. """
  74. Converts a single multi-channel nifti file to multiple single-channel nifti files, and masks foreground
  75. :param flip: Whether to vertically flip the image, in order to properly align nifti file with template
  76. :param mask_index: index of DAPI stained channel, on which to mask other channels on
  77. :param bias: whether to bias-field correct the image
  78. :return: Location of multiple single-channel nifti files
  79. """
  80. if out_dir is None:
  81. out_dir = nii_path.parent
  82. nii = nib.load(str(nii_path))
  83. nii_data = nii.get_data()
  84. nii_header = nii.header
  85. if mask_index == -1:
  86. mask_index = nii_data.shape[2] - 1
  87. paths = []
  88. for i in range(0, nii_data.shape[2]):
  89. out_path = out_dir / f'im_c{i+1}.nii'
  90. channel_data = nii_data[:, :, i]
  91. if flip:
  92. channel_data = np.flip(channel_data, 1)
  93. if i == mask_index:
  94. channel_data = mask_foreground(channel_data)
  95. new_header = nii_header
  96. new_header['dim'][0] = 2
  97. nii = nib.Nifti1Image(channel_data, np.eye(4), header=new_header)
  98. nib.save(nii, str(out_path))
  99. if i == mask_index and bias:
  100. bias_field_correction(str(out_path), str(out_path))
  101. corrected = nib.load(str(out_path))
  102. corrected_data = corrected.get_data()
  103. corrected_normalized = corrected_data / np.mean(corrected_data[corrected_data != 0])
  104. nii_corrected = nib.Nifti1Image(corrected_normalized, corrected.affine, corrected.header)
  105. nib.save(nii_corrected, str(out_path))
  106. paths.append(out_path)
  107. return paths
  108. def mask_foreground(raw_data):
  109. """
  110. Mask the foreground of an image, using otsu threshold and connected components to remove background noise
  111. """
  112. raw_max = raw_data.max()
  113. raw_data = raw_data / raw_max
  114. blurred_data = ndimage.gaussian_filter(raw_data, 4)
  115. threshold = filters.threshold_otsu(raw_data) / 2
  116. threshold_data = blurred_data > threshold
  117. connected_structure = ndimage.generate_binary_structure(2, 2) # Connects adjacent and diagonal.
  118. padded_comp, padded_nr = ndimage.label(threshold_data, structure=connected_structure)
  119. comps, comps_count = np.unique(padded_comp, return_counts=True)
  120. comps_count, comps = zip(*sorted(zip(comps_count, comps), reverse=True))
  121. two_biggest_cc = ((comps[0], np.average(comps[0])), (comps[1], np.average(comps[1])))
  122. biggest_cc = max(two_biggest_cc, key=lambda a: a[1])[0]
  123. foreground_mask = np.where(padded_comp == biggest_cc, True, False)
  124. closed = morphology.binary_closing(foreground_mask, selem=morphology.square(30))
  125. raw_data = np.where(closed, raw_data, 0)
  126. return raw_data * raw_max
  127. if __name__ == '__main__':
  128. parser = argparse.ArgumentParser()
  129. parser.add_argument("file", help="Location of file to process")
  130. parser.add_argument("dir", help="Directory for preprocessed Files")
  131. parser.add_argument("-s", type=int, help="Series to extract")
  132. parser.add_argument('-b', type=bool, default= True, help='Bias field correct image')
  133. parser.add_argument('--pdim', type=float, help='Pixel dimensions (Retrieved from Tiff file if not set)')
  134. args = parser.parse_args()
  135. filetype = Path(args.file).suffix.lower()
  136. if filetype == '.tif' or filetype == '.tiff':
  137. n_path = tiff_to_nii(Path(args.file), Path(args.dir))
  138. elif filetype == '.nd2':
  139. n_path = nd2_to_nii(Path(args.file), Path(args.dir), args.series)