dsets.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import copy
  2. import csv
  3. import functools
  4. import glob
  5. import os
  6. import random
  7. import SimpleITK as sitk
  8. import numpy as np
  9. import torch
  10. import torch.cuda
  11. from torch.utils.data import Dataset
  12. from util.disk import getCache
  13. from util.util import XyzTuple, xyz2irc
  14. from util.logconf import logging
  15. log = logging.getLogger(__name__)
  16. # log.setLevel(logging.WARN)
  17. log.setLevel(logging.INFO)
  18. log.setLevel(logging.DEBUG)
  19. raw_cache = getCache('part2ch10_raw')
  20. @functools.lru_cache(1)
  21. def getNoduleInfoList(requireDataOnDisk_bool=True):
  22. # We construct a set with all series_uids that are present on disk.
  23. # This will let us use the data, even if we haven't downloaded all of
  24. # the subsets yet.
  25. mhd_list = glob.glob('data/luna/subset*/*.mhd')
  26. dataPresentOnDisk_set = {os.path.split(p)[-1][:-4] for p in mhd_list}
  27. diameter_dict = {}
  28. with open('data/luna/annotations.csv', "r") as f:
  29. for row in list(csv.reader(f))[1:]:
  30. series_uid = row[0]
  31. annotationCenter_xyz = tuple([float(x) for x in row[1:4]])
  32. annotationDiameter_mm = float(row[4])
  33. diameter_dict.setdefault(series_uid, []).append((annotationCenter_xyz, annotationDiameter_mm))
  34. noduleInfo_list = []
  35. with open('data/luna/candidates.csv', "r") as f:
  36. for row in list(csv.reader(f))[1:]:
  37. series_uid = row[0]
  38. if series_uid not in dataPresentOnDisk_set and requireDataOnDisk_bool:
  39. continue
  40. isMalignant_bool = bool(int(row[4]))
  41. candidateCenter_xyz = tuple([float(x) for x in row[1:4]])
  42. candidateDiameter_mm = 0.0
  43. for annotationCenter_xyz, annotationDiameter_mm in diameter_dict.get(series_uid, []):
  44. for i in range(3):
  45. delta_mm = abs(candidateCenter_xyz[i] - annotationCenter_xyz[i])
  46. if delta_mm > annotationDiameter_mm / 4:
  47. break
  48. else:
  49. candidateDiameter_mm = annotationDiameter_mm
  50. break
  51. noduleInfo_list.append((isMalignant_bool, candidateDiameter_mm, series_uid, candidateCenter_xyz))
  52. noduleInfo_list.sort(reverse=True)
  53. return noduleInfo_list
  54. class Ct(object):
  55. def __init__(self, series_uid):
  56. mhd_path = glob.glob('data/luna/subset*/{}.mhd'.format(series_uid))[0]
  57. ct_mhd = sitk.ReadImage(mhd_path)
  58. ct_ary = np.array(sitk.GetArrayFromImage(ct_mhd), dtype=np.float32)
  59. # CTs are natively expressed in https://en.wikipedia.org/wiki/Hounsfield_scale
  60. # HU are scaled oddly, with 0 g/cc (air, approximately) being -1000 and 1 g/cc (water) being 0.
  61. # This converts HU to g/cc.
  62. ct_ary += 1000
  63. ct_ary /= 1000
  64. # This gets rid of negative density stuff used to indicate out-of-FOV
  65. ct_ary[ct_ary < 0] = 0
  66. # This nukes any weird hotspots and clamps bone down
  67. ct_ary[ct_ary > 2] = 2
  68. self.series_uid = series_uid
  69. self.ary = ct_ary
  70. self.origin_xyz = XyzTuple(*ct_mhd.GetOrigin())
  71. self.vxSize_xyz = XyzTuple(*ct_mhd.GetSpacing())
  72. self.direction_tup = tuple(int(round(x)) for x in ct_mhd.GetDirection())
  73. def getRawNodule(self, center_xyz, width_irc):
  74. center_irc = xyz2irc(center_xyz, self.origin_xyz, self.vxSize_xyz, self.direction_tup)
  75. slice_list = []
  76. for axis, center_val in enumerate(center_irc):
  77. start_ndx = int(round(center_val - width_irc[axis]/2))
  78. end_ndx = int(start_ndx + width_irc[axis])
  79. assert center_val >= 0 and center_val < self.ary.shape[axis], repr([self.series_uid, center_xyz, self.origin_xyz, self.vxSize_xyz, center_irc, axis])
  80. if start_ndx < 0:
  81. # log.warning("Crop outside of CT array: {} {}, center:{} shape:{} width:{}".format(
  82. # self.series_uid, center_xyz, center_irc, self.ary.shape, width_irc))
  83. start_ndx = 0
  84. end_ndx = int(width_irc[axis])
  85. if end_ndx > self.ary.shape[axis]:
  86. # log.warning("Crop outside of CT array: {} {}, center:{} shape:{} width:{}".format(
  87. # self.series_uid, center_xyz, center_irc, self.ary.shape, width_irc))
  88. end_ndx = self.ary.shape[axis]
  89. start_ndx = int(self.ary.shape[axis] - width_irc[axis])
  90. slice_list.append(slice(start_ndx, end_ndx))
  91. ct_chunk = self.ary[slice_list]
  92. return ct_chunk, center_irc
  93. @functools.lru_cache(1, typed=True)
  94. def getCt(series_uid):
  95. return Ct(series_uid)
  96. @raw_cache.memoize(typed=True)
  97. def getCtRawNodule(series_uid, center_xyz, width_irc):
  98. ct = getCt(series_uid)
  99. ct_chunk, center_irc = ct.getRawNodule(center_xyz, width_irc)
  100. return ct_chunk, center_irc
  101. class LunaDataset(Dataset):
  102. def __init__(self,
  103. test_stride=0,
  104. isTestSet_bool=None,
  105. series_uid=None,
  106. sortby_str='random',
  107. ratio_int=0,
  108. ):
  109. self.ratio_int = ratio_int
  110. self.noduleInfo_list = copy.copy(getNoduleInfoList())
  111. if series_uid:
  112. self.noduleInfo_list = [x for x in self.noduleInfo_list if x[2] == series_uid]
  113. if test_stride > 1:
  114. if isTestSet_bool:
  115. self.noduleInfo_list = self.noduleInfo_list[::test_stride]
  116. else:
  117. del self.noduleInfo_list[::test_stride]
  118. if sortby_str == 'random':
  119. random.shuffle(self.noduleInfo_list)
  120. elif sortby_str == 'series_uid':
  121. self.noduleInfo_list.sort(key=lambda x: (x[2], x[3])) # sorting by series_uid, center_xyz)
  122. elif sortby_str == 'malignancy_size':
  123. pass
  124. else:
  125. raise Exception("Unknown sort: " + repr(sortby_str))
  126. self.benignIndex_list = [i for i, x in enumerate(self.noduleInfo_list) if not x[0]]
  127. self.malignantIndex_list = [i for i, x in enumerate(self.noduleInfo_list) if x[0]]
  128. log.info("{!r}: {} {} samples, {} ben, {} mal, {} ratio".format(
  129. self,
  130. len(self.noduleInfo_list),
  131. "testing" if isTestSet_bool else "training",
  132. len(self.benignIndex_list),
  133. len(self.malignantIndex_list),
  134. '{}:1'.format(self.ratio_int) if self.ratio_int else 'unbalanced'
  135. ))
  136. def shuffleSamples(self):
  137. if self.ratio_int:
  138. random.shuffle(self.benignIndex_list)
  139. random.shuffle(self.malignantIndex_list)
  140. def __len__(self):
  141. if self.ratio_int:
  142. return 100000
  143. else:
  144. return len(self.noduleInfo_list)
  145. def __getitem__(self, ndx):
  146. if self.ratio_int:
  147. malignant_ndx = ndx // (self.ratio_int + 1)
  148. if ndx % (self.ratio_int + 1):
  149. benign_ndx = ndx - 1 - malignant_ndx
  150. nodule_ndx = self.benignIndex_list[benign_ndx % len(self.benignIndex_list)]
  151. else:
  152. nodule_ndx = self.malignantIndex_list[malignant_ndx % len(self.malignantIndex_list)]
  153. else:
  154. nodule_ndx = ndx
  155. isMalignant_bool, _diameter_mm, series_uid, center_xyz = self.noduleInfo_list[nodule_ndx]
  156. nodule_ary, center_irc = getCtRawNodule(series_uid, center_xyz, (32, 32, 32))
  157. nodule_tensor = torch.from_numpy(nodule_ary)
  158. nodule_tensor = nodule_tensor.unsqueeze(0)
  159. malignant_tensor = torch.tensor([isMalignant_bool], dtype=torch.float32)
  160. return nodule_tensor, malignant_tensor, series_uid, center_irc