|
import vtk
|
|
import numpy as np
|
|
|
|
|
|
def CreateLut():
|
|
colors = vtk.vtkNamedColors()
|
|
|
|
colorLut = vtk.vtkLookupTable()
|
|
colorLut.SetNumberOfColors(17)
|
|
colorLut.SetTableRange(0, 16)
|
|
colorLut.Build()
|
|
|
|
colorLut.SetTableValue(0, 0, 0, 0, 0)
|
|
colorLut.SetTableValue(1, colors.GetColor4d("salmon")) # blood
|
|
colorLut.SetTableValue(2, colors.GetColor4d("beige")) # brain
|
|
colorLut.SetTableValue(3, colors.GetColor4d("orange")) # duodenum
|
|
colorLut.SetTableValue(4, colors.GetColor4d("misty_rose")) # eye_retina
|
|
colorLut.SetTableValue(5, colors.GetColor4d("white")) # eye_white
|
|
colorLut.SetTableValue(6, colors.GetColor4d("tomato")) # heart
|
|
colorLut.SetTableValue(7, colors.GetColor4d("raspberry")) # ileum
|
|
colorLut.SetTableValue(8, colors.GetColor4d("banana")) # kidney
|
|
colorLut.SetTableValue(9, colors.GetColor4d("peru")) # l_intestine
|
|
colorLut.SetTableValue(10, colors.GetColor4d("pink")) # liver
|
|
colorLut.SetTableValue(11, colors.GetColor4d("powder_blue")) # lung
|
|
colorLut.SetTableValue(12, colors.GetColor4d("carrot")) # nerve
|
|
colorLut.SetTableValue(13, colors.GetColor4d("wheat")) # skeleton
|
|
colorLut.SetTableValue(14, colors.GetColor4d("violet")) # spleen
|
|
colorLut.SetTableValue(15, colors.GetColor4d("plum")) # stomach
|
|
|
|
return colorLut
|
|
|
|
|
|
def CreateTissueMap():
|
|
tissueMap = dict()
|
|
tissueMap["blood"] = 1
|
|
tissueMap["brain"] = 2
|
|
tissueMap["duodenum"] = 3
|
|
tissueMap["eyeRetina"] = 4
|
|
tissueMap["eyeWhite"] = 5
|
|
tissueMap["heart"] = 6
|
|
tissueMap["ileum"] = 7
|
|
tissueMap["kidney"] = 8
|
|
tissueMap["intestine"] = 9
|
|
tissueMap["liver"] = 10
|
|
tissueMap["lung"] = 11
|
|
tissueMap["nerve"] = 12
|
|
tissueMap["skeleton"] = 13
|
|
tissueMap["spleen"] = 14
|
|
tissueMap["stomach"] = 15
|
|
|
|
return tissueMap
|
|
|
|
|
|
tissueMap = CreateTissueMap()
|
|
colorLut = CreateLut()
|
|
|
|
|
|
# Generate a 3D mesh with marching cubes algorithm
|
|
def CreateTissue(
|
|
image_producer: vtk.vtkAlgorithm, ThrIn, ThrOut, color="skeleton", isoValue=127.5
|
|
):
|
|
selectTissue = vtk.vtkImageThreshold()
|
|
selectTissue.ThresholdBetween(ThrIn, ThrOut)
|
|
selectTissue.ReplaceInOn()
|
|
selectTissue.SetInValue(255)
|
|
selectTissue.ReplaceOutOn()
|
|
selectTissue.SetOutValue(0)
|
|
# selectTissue.SetInputData(imageData)
|
|
selectTissue.SetInputConnection(image_producer.GetOutputPort())
|
|
# selectTissue.Update()
|
|
|
|
gaussianRadius = 5
|
|
gaussianStandardDeviation = 2.0
|
|
gaussian = vtk.vtkImageGaussianSmooth()
|
|
gaussian.SetStandardDeviations(
|
|
gaussianStandardDeviation, gaussianStandardDeviation, gaussianStandardDeviation
|
|
)
|
|
gaussian.SetRadiusFactors(gaussianRadius, gaussianRadius, gaussianRadius)
|
|
gaussian.SetInputConnection(selectTissue.GetOutputPort())
|
|
|
|
# isoValue = 127.5
|
|
mcubes = vtk.vtkMarchingCubes()
|
|
mcubes.SetInputConnection(gaussian.GetOutputPort())
|
|
mcubes.ComputeScalarsOff()
|
|
mcubes.ComputeGradientsOff()
|
|
mcubes.ComputeNormalsOff()
|
|
mcubes.SetValue(0, isoValue)
|
|
|
|
smoothingIterations = 5
|
|
passBand = 0.001
|
|
featureAngle = 60.0
|
|
smoother = vtk.vtkWindowedSincPolyDataFilter()
|
|
smoother.SetInputConnection(mcubes.GetOutputPort())
|
|
smoother.SetNumberOfIterations(smoothingIterations)
|
|
smoother.BoundarySmoothingOff()
|
|
smoother.FeatureEdgeSmoothingOff()
|
|
smoother.SetFeatureAngle(featureAngle)
|
|
smoother.SetPassBand(passBand)
|
|
smoother.NonManifoldSmoothingOn()
|
|
smoother.NormalizeCoordinatesOn()
|
|
smoother.Update()
|
|
|
|
normals = vtk.vtkPolyDataNormals()
|
|
normals.SetInputConnection(smoother.GetOutputPort())
|
|
normals.SetFeatureAngle(featureAngle)
|
|
|
|
stripper = vtk.vtkStripper()
|
|
stripper.SetInputConnection(normals.GetOutputPort())
|
|
|
|
mapper = vtk.vtkPolyDataMapper()
|
|
mapper.SetInputConnection(stripper.GetOutputPort())
|
|
|
|
actor = vtk.vtkActor()
|
|
actor.SetMapper(mapper)
|
|
actor.GetProperty().SetColor(colorLut.GetTableValue(tissueMap[color])[:3])
|
|
actor.GetProperty().SetSpecular(0.5)
|
|
actor.GetProperty().SetSpecularPower(10)
|
|
|
|
return actor
|
|
|
|
|
|
import pathlib
|
|
import pydicom
|
|
from typing import List
|
|
|
|
|
|
def load_scan(dicom_dir: str) -> List[pydicom.FileDataset]:
|
|
slices = [pydicom.dcmread(f) for f in pathlib.Path(dicom_dir).glob("*.dcm")]
|
|
print(f"file count: {len(slices)}")
|
|
|
|
# skip files with no SliceLocation or InstanceNumber(eg scout views)
|
|
slices = list(filter(lambda s: hasattr(s, "SliceLocation"), slices))
|
|
|
|
print(f"file count with SliceLocation: {len(slices)}")
|
|
slices = sorted(slices, key=lambda s: s.SliceLocation)
|
|
|
|
return slices
|
|
|
|
|
|
def get_pixels_hu(slices: List[pydicom.FileDataset]) -> np.ndarray:
|
|
# s.pixel_array: 2D array with HW format
|
|
# img3d: 3D array with NHW format
|
|
img3d = np.stack([s.pixel_array for s in slices])
|
|
|
|
# Convert to int16 (from sometimes int16),
|
|
# should be possible as values should always be low enough (<32k)
|
|
img3d = img3d.astype(np.int16)
|
|
|
|
# Set outside-of-scan pixels to 1
|
|
# The intercept is usually -1024, so air is approximately 0
|
|
# img3d[img3d == -1000] = 0
|
|
img3d[img3d == -2000] = 0
|
|
|
|
# Convert to Hounsfield units (HU)
|
|
|
|
# Operate on whole array
|
|
intercept = slices[0].RescaleIntercept
|
|
slope = slices[0].RescaleSlope
|
|
|
|
if slope != 1:
|
|
img3d = slope * img3d.astype(np.float64)
|
|
img3d = img3d.astype(np.int16)
|
|
|
|
img3d += np.int16(intercept)
|
|
|
|
# # Operate on individual array
|
|
# for n in range(len(slices)):
|
|
# intercept = slices[n].RescaleIntercept
|
|
# slope = slices[n].RescaleSlope
|
|
|
|
# if slope != 1:
|
|
# img3d[n] = slope * img3d[n].astype(np.float64)
|
|
# img3d[n] = img3d[n].astype(np.int16)
|
|
|
|
# img3d[n] += np.int16(intercept)
|
|
|
|
return np.array(img3d, dtype=np.int16)
|
|
|
|
|
|
def get_pixels(scans: List[pydicom.FileDataset]) -> np.ndarray:
|
|
# s.pixel_array: 2D array with HW format
|
|
# img3d: 3D array with NHW format
|
|
img3d = np.stack([s.pixel_array for s in scans])
|
|
|
|
return img3d
|
|
|
|
|
|
def plot_stack_sample(stack, rows=6, cols=6, start_with=10, show_every=3):
|
|
import matplotlib.pyplot as plt
|
|
|
|
fig, ax = plt.subplots(rows, cols, figsize=[12, 12])
|
|
for i in range(rows * cols):
|
|
ind = start_with + i * show_every
|
|
ax[int(i / rows), int(i % rows)].set_title("slice %d" % ind)
|
|
ax[int(i / rows), int(i % rows)].imshow(stack[ind], cmap="gray")
|
|
ax[int(i / rows), int(i % rows)].axis("off")
|
|
plt.show()
|
|
|
|
|
|
def resample(image, scan, new_spacing=[1, 1, 1]):
|
|
import scipy.ndimage
|
|
|
|
# Determine current pixel spacing
|
|
spacing = np.array(
|
|
[
|
|
float(scan[0].SliceThickness),
|
|
scan[0].PixelSpacing[0],
|
|
scan[0].PixelSpacing[1],
|
|
]
|
|
)
|
|
|
|
resize_factor = spacing / new_spacing
|
|
new_real_shape = image.shape * resize_factor
|
|
new_shape = np.round(new_real_shape)
|
|
real_resize_factor = new_shape / image.shape
|
|
new_spacing = spacing / real_resize_factor
|
|
|
|
image = scipy.ndimage.interpolation.zoom(image, real_resize_factor)
|
|
|
|
return image, new_spacing
|