Source code for radiomics.shape

import numpy
import SimpleITK as sitk
from six.moves import range

from radiomics import base, cMatsEnabled, cShape


[docs]class RadiomicsShape(base.RadiomicsFeaturesBase): r""" In this group of features we included descriptors of the three-dimensional size and shape of the tumor region. Let in the following definitions denote :math:`V` the volume (mm\ :sup:`3`) and :math:`A` the surface area of the volume (mm\ :sup:`2`) of interest. """ def __init__(self, inputImage, inputMask, **kwargs): super(RadiomicsShape, self).__init__(inputImage, inputMask, **kwargs) self.pixelSpacing = numpy.array(inputImage.GetSpacing()[::-1]) # Use SimpleITK for some shape features self.logger.debug("Extracting simple ITK shape features") self.lssif = sitk.LabelShapeStatisticsImageFilter() self.lssif.SetComputeFeretDiameter(True) self.lssif.Execute(inputMask) # Pad inputMask to prevent index-out-of-range errors self.logger.debug("Padding the mask with 0s") cpif = sitk.ConstantPadImageFilter() padding = numpy.tile(1, 3) try: cpif.SetPadLowerBound(padding) cpif.SetPadUpperBound(padding) except TypeError: # newer versions of SITK/python want a tuple or list cpif.SetPadLowerBound(padding.tolist()) cpif.SetPadUpperBound(padding.tolist()) self.inputMask = cpif.Execute(self.inputMask) # Reassign self.maskArray using the now-padded self.inputMask and make it binary self.maskArray = (sitk.GetArrayFromImage(self.inputMask) == self.label).astype('int') self.matrixCoordinates = numpy.where(self.maskArray != 0) self.logger.debug('Pre-calculate Volume and Surface Area') # Volume and Surface Area are pre-calculated self.Volume = self.lssif.GetPhysicalSize(self.label) if cMatsEnabled(): self.SurfaceArea = self._calculateCSurfaceArea() else: self.SurfaceArea = self._calculateSurfaceArea() self.logger.debug('Feature class initialized') def _calculateSurfaceArea(self): self.logger.debug("Calculating Surface Area in Python") # define relative locations of the 8 voxels of a sampling cube gridAngles = numpy.array([(0, 0, 0), (0, 0, 1), (0, 1, 1), (0, 1, 0), (1, 0, 0), (1, 0, 1), (1, 1, 1), (1, 1, 0)]) # instantiate lookup tables edgeTable, triTable = self._getMarchingTables() minBounds = numpy.array([numpy.min(self.matrixCoordinates[0]), numpy.min(self.matrixCoordinates[1]), numpy.min(self.matrixCoordinates[2])]) maxBounds = numpy.array([numpy.max(self.matrixCoordinates[0]), numpy.max(self.matrixCoordinates[1]), numpy.max(self.matrixCoordinates[2])]) minBounds = numpy.where(minBounds < 1, 1, minBounds) maxBounds = numpy.where(maxBounds > self.maskArray.shape, self.maskArray.shape, maxBounds) S_A = 0.0 # iterate over all voxels which may border segmentation or are a part of it for v_z in range(minBounds[0] - 1, maxBounds[0] + 1): for v_y in range(minBounds[1] - 1, maxBounds[1] + 1): for v_x in range(minBounds[2] - 1, maxBounds[2] + 1): # indices to corners of current sampling cube gridCell = gridAngles + [v_z, v_y, v_x] # generate lookup index for current cube cube_idx = 0 for p_idx, p in enumerate(gridCell): if self.maskArray[tuple(p)] == 0: cube_idx |= 1 << p_idx # full lookup tables are symmetrical, if cube_idx >= 128, take the XOR, # this allows for lookup tables to be half the size. if cube_idx & 128: cube_idx = cube_idx ^ 0xff # lookup which edges contain vertices and calculate vertices coordinates if edgeTable[cube_idx] == 0: continue vertList = numpy.zeros((12, 3), dtype='float64') if edgeTable[cube_idx] & 1: vertList[0] = self._interpolate(gridCell, 0, 1) if edgeTable[cube_idx] & 2: vertList[1] = self._interpolate(gridCell, 1, 2) if edgeTable[cube_idx] & 4: vertList[2] = self._interpolate(gridCell, 2, 3) if edgeTable[cube_idx] & 8: vertList[3] = self._interpolate(gridCell, 3, 0) if edgeTable[cube_idx] & 16: vertList[4] = self._interpolate(gridCell, 4, 5) if edgeTable[cube_idx] & 32: vertList[5] = self._interpolate(gridCell, 5, 6) if edgeTable[cube_idx] & 64: vertList[6] = self._interpolate(gridCell, 6, 7) if edgeTable[cube_idx] & 128: vertList[7] = self._interpolate(gridCell, 7, 4) if edgeTable[cube_idx] & 256: vertList[8] = self._interpolate(gridCell, 0, 4) if edgeTable[cube_idx] & 512: vertList[9] = self._interpolate(gridCell, 1, 5) if edgeTable[cube_idx] & 1024: vertList[10] = self._interpolate(gridCell, 2, 6) if edgeTable[cube_idx] & 2048: vertList[11] = self._interpolate(gridCell, 3, 7) # calculate triangles for triangle in triTable[cube_idx]: a = vertList[triangle[1]] - vertList[triangle[0]] b = vertList[triangle[2]] - vertList[triangle[0]] c = numpy.cross(a, b) S_A += .5 * numpy.sqrt(numpy.sum(c ** 2)) return S_A def _calculateCSurfaceArea(self): self.logger.debug("Calculating Surface Area in C") return cShape.calculate_surfacearea(self.maskArray, self.pixelSpacing) def _getMaximum2Ddiameter(self, dim): otherDims = tuple(set([0, 1, 2]) - set([dim])) a = numpy.array(list(zip(*self.matrixCoordinates))) maxDiameter = 0 # Check maximum diameter in every slice, retain the overall maximum for i in numpy.unique(a[:, dim]): # Retrieve all indices of mask in current slice plane = a[numpy.where(a[:, dim] == i)] minBounds = numpy.min(plane, 0) maxBounds = numpy.max(plane, 0) # Generate 2 sets of indices: one set of indices in zSlice where at least the x or y component of the index is equal to the # minimum indices in the current slice, and one set of indices where at least one element it is equal to the maximum edgeVoxelsMinCoords = numpy.vstack( [plane[plane[:, otherDims[0]] == minBounds[otherDims[0]]], plane[plane[:, otherDims[1]] == minBounds[otherDims[1]]]]) * self.pixelSpacing edgeVoxelsMaxCoords = numpy.vstack( [plane[plane[:, otherDims[0]] == maxBounds[otherDims[0]]], plane[plane[:, otherDims[1]] == maxBounds[otherDims[1]]]]) * self.pixelSpacing # generate a matrix of distances for every combination of an index in edgeVoxelsMinCoords and edgeVoxelsMaxCoords # By subtraction the distance between the x, y and z components are obtained. The euclidean distance is then calculated: # Sum the squares of dx, dy and dz components and then take the square root; Sqrt( Sum( dx^2 + dy^2 + dz^2 ) ) distances = numpy.sqrt(numpy.sum((edgeVoxelsMaxCoords[:, None] - edgeVoxelsMinCoords[None, :]) ** 2, 2)) tempMax = numpy.max(distances) if tempMax > maxDiameter: maxDiameter = tempMax return maxDiameter
[docs] def getVolumeFeatureValue(self): r""" Calculate the volume of the tumor region in cubic millimeters. """ return self.Volume
[docs] def getSurfaceAreaFeatureValue(self): r""" Calculate the surface area of the tumor region in square millimeters using a marching cubes algorithm. :math:`A = \displaystyle\sum^{N}_{i=1}{\frac{1}{2}|\text{a}_i\text{b}_i \times \text{a}_i\text{c}_i|}` Where: :math:`N` is the number of triangles forming the surface mesh of the volume (ROI) :math:`\text{a}_i\text{b}_i` and :math:`\text{a}_i\text{c}_i` are the edges of the :math:`i^{\text{th}}` triangle formed by points :math:`\text{a}_i`, :math:`\text{b}_i` and :math:`\text{c}_i` """ return (self.SurfaceArea)
[docs] def getSurfaceVolumeRatioFeatureValue(self): r""" Calculate the surface area to volume ratio of the tumor region :math:`surface\ to\ volume\ ratio = \frac{A}{V}` Here, a lower value indicates a more compact (sphere-like) shape. """ return (self.SurfaceArea / self.Volume)
[docs] def getSphericityFeatureValue(self): r""" Calculate the Sphericity of the tumor region. :math:`sphericity = \frac{\sqrt[3]{36 \pi V^2}}{A}` Sphericity is a measure of the roundness of the shape of the tumor region relative to a sphere. It is a dimensionless measure, independent of scale and orientation. The value range is :math:`0 < sphericity \leq 1`, where a value of 1 indicates a perfect sphere (a sphere has the smallest possible surface area for a given volume, compared to other solids). .. note:: This feature is correlated to Compactness 1, Compactness 2 and Spherical Disproportion. In the default parameter file provided in the ``pyradiomics\bin`` folder, Compactness 1 and Compactness 2 are therefore disabled. """ return (36 * numpy.pi * self.Volume ** 2) ** (1.0 / 3.0) / self.SurfaceArea
[docs] def getCompactness1FeatureValue(self): r""" Calculate the compactness (1) of the tumor region. :math:`compactness\ 1 = \frac{V}{\sqrt{\pi A^3}}` Similar to Sphericity, Compactness 1 is a measure of how compact the shape of the tumor is relative to a sphere (most compact). It is therefore correlated to Sphericity and redundant. It is provided here for completeness. The value range is :math:`0 < compactness\ 1 \leq \frac{1}{6 \pi}`, where a value of :math:`\frac{1}{6 \pi}` indicates a perfect sphere. By definition, :math:`compactness\ 1 = \frac{1}{6 \pi}\sqrt{compactness\ 2} = \frac{1}{6 \pi}\sqrt{sphericity^3}`. .. note:: This feature is correlated to Compactness 2, Sphericity and Spherical Disproportion. In the default parameter file provided in the ``pyradiomics\bin`` folder, Compactness 1 and Compactness 2 are therefore disabled. """ return ((self.Volume) / ((self.SurfaceArea) ** (3.0 / 2.0) * numpy.sqrt(numpy.pi)))
[docs] def getCompactness2FeatureValue(self): r""" Calculate the Compactness (2) of the tumor region. :math:`compactness\ 2 = 36 \pi \frac{V^2}{A^3}` Similar to Sphericity and Compactness 1, Compactness 2 is a measure of how compact the shape of the tumor is relative to a sphere (most compact). It is a dimensionless measure, independent of scale and orientation. The value range is :math:`0 < compactness\ 2 \leq 1`, where a value of 1 indicates a perfect sphere. By definition, :math:`compactness\ 2 = (sphericity)^3` .. note:: This feature is correlated to Compactness 1, Sphericity and Spherical Disproportion. In the default parameter file provided in the ``pyradiomics\bin`` folder, Compactness 1 and Compactness 2 are therefore disabled. """ return ((36.0 * numpy.pi) * ((self.Volume) ** 2.0) / ((self.SurfaceArea) ** 3.0))
[docs] def getSphericalDisproportionFeatureValue(self): r""" Calculate the Spherical Disproportion of the tumor region. :math:`spherical\ disproportion = \frac{A}{4\pi R^2} = \frac{A}{\sqrt[3]{36 \pi V^2}}` Where :math:`R` is the radius of a sphere with the same volume as the tumor, and equal to :math:`\sqrt[3]{\frac{3V}{4\pi}}`. Spherical Disproportion is the ratio of the surface area of the tumor region to the surface area of a sphere with the same volume as the tumor region, and by definition, the inverse of Sphericity. Therefore, the value range is :math:`spherical\ disproportion \geq 1`, with a value of 1 indicating a perfect sphere. .. note:: This feature is correlated to Compactness 1, Sphericity and Spherical Disproportion. In the default parameter file provided in the ``pyradiomics\bin`` folder, Compactness 1 and Compactness 2 are therefore disabled. """ return self.SurfaceArea / (36 * numpy.pi * self.Volume ** 2) ** (1.0 / 3.0)
[docs] def getMaximum3DDiameterFeatureValue(self): r""" Calculate the largest pairwise euclidean distance between tumor surface voxels. Also known as Feret Diameter. """ return self.lssif.GetFeretDiameter(self.label)
[docs] def getMaximum2DDiameterSliceFeatureValue(self): r""" Calculate the largest pairwise euclidean distance between tumor surface voxels in the row-column plane. """ return self._getMaximum2Ddiameter(0)
[docs] def getMaximum2DDiameterColumnFeatureValue(self): r""" Calculate the largest pairwise euclidean distance between tumor surface voxels in the row-slice plane. """ return self._getMaximum2Ddiameter(1)
[docs] def getMaximum2DDiameterRowFeatureValue(self): r""" Calculate the largest pairwise euclidean distance between tumor surface voxels in the column-slice plane. """ return self._getMaximum2Ddiameter(2)
[docs] def getElongationFeatureValue(self): r""" Calculate the elongation of the shape, which is defined as: .. math:: Elongation = \frac{\lambda_{\text{longest}}}{\lambda_{\text{intermediate}}} Here, :math:`\lambda_{\text{longest}}` and :math:`\lambda_{\text{intermediate}}` are the lengths of the largest and second largest principal component axes. References: - Andrey P, Kieu K, Kress C, Lehmann G, Tirichine L, Liu Z, et al. Statistical analysis of 3D images detects regular spatial distributions of centromeres and chromocenters in animal and plant nuclei. PLoS Comput Biol. 2010;6:27. """ return self.lssif.GetElongation(self.label)
[docs] def getFlatnessFeatureValue(self): r""" Calculate the flatness of the shape, which is defined as: .. math:: Flatness = \frac{\lambda_{\text{intermediate}}}{\lambda_{\text{shortest}}} Here, :math:`\lambda_{\text{intermediate}}` and :math:`\lambda_{\text{shortest}}` are the lengths of the second largest and smallest principal component axes. References: - Andrey P, Kieu K, Kress C, Lehmann G, Tirichine L, Liu Z, et al. Statistical analysis of 3D images detects regular spatial distributions of centromeres and chromocenters in animal and plant nuclei. PLoS Comput Biol. 2010;6:27. """ return self.lssif.GetFlatness(self.label)
def _interpolate(self, grid, p1, p2): diff = (.5 - self.maskArray[tuple(grid[p1])]) / (self.maskArray[tuple(grid[p2])] - self.maskArray[tuple(grid[p1])]) return (grid[p1] + ((grid[p2] - grid[p1]) * diff)) * self.pixelSpacing def _getMarchingTables(self): edgeTable = [0x0, 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c, 0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00, 0x190, 0x99, 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c, 0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90, 0x230, 0x339, 0x33, 0x13a, 0x636, 0x73f, 0x435, 0x53c, 0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30, 0x3a0, 0x2a9, 0x1a3, 0xaa, 0x7a6, 0x6af, 0x5a5, 0x4ac, 0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0, 0x460, 0x569, 0x663, 0x76a, 0x66, 0x16f, 0x265, 0x36c, 0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60, 0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0xff, 0x3f5, 0x2fc, 0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0, 0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x55, 0x15c, 0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950, 0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0xcc, 0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0] triTable = [[], [[0, 8, 3]], [[0, 1, 9]], [[1, 8, 3], [9, 8, 1]], [[1, 2, 10]], [[0, 8, 3], [1, 2, 10]], [[9, 2, 10], [0, 2, 9]], [[2, 8, 3], [2, 10, 8], [10, 9, 8]], [[3, 11, 2]], [[0, 11, 2], [8, 11, 0]], [[1, 9, 0], [2, 3, 11]], [[1, 11, 2], [1, 9, 11], [9, 8, 11]], [[3, 10, 1], [11, 10, 3]], [[0, 10, 1], [0, 8, 10], [8, 11, 10]], [[3, 9, 0], [3, 11, 9], [11, 10, 9]], [[9, 8, 10], [10, 8, 11]], [[4, 7, 8]], [[4, 3, 0], [7, 3, 4]], [[0, 1, 9], [8, 4, 7]], [[4, 1, 9], [4, 7, 1], [7, 3, 1]], [[1, 2, 10], [8, 4, 7]], [[3, 4, 7], [3, 0, 4], [1, 2, 10]], [[9, 2, 10], [9, 0, 2], [8, 4, 7]], [[2, 10, 9], [2, 9, 7], [2, 7, 3], [7, 9, 4]], [[8, 4, 7], [3, 11, 2]], [[11, 4, 7], [11, 2, 4], [2, 0, 4]], [[9, 0, 1], [8, 4, 7], [2, 3, 11]], [[4, 7, 11], [9, 4, 11], [9, 11, 2], [9, 2, 1]], [[3, 10, 1], [3, 11, 10], [7, 8, 4]], [[1, 11, 10], [1, 4, 11], [1, 0, 4], [7, 11, 4]], [[4, 7, 8], [9, 0, 11], [9, 11, 10], [11, 0, 3]], [[4, 7, 11], [4, 11, 9], [9, 11, 10]], [[9, 5, 4]], [[9, 5, 4], [0, 8, 3]], [[0, 5, 4], [1, 5, 0]], [[8, 5, 4], [8, 3, 5], [3, 1, 5]], [[1, 2, 10], [9, 5, 4]], [[3, 0, 8], [1, 2, 10], [4, 9, 5]], [[5, 2, 10], [5, 4, 2], [4, 0, 2]], [[2, 10, 5], [3, 2, 5], [3, 5, 4], [3, 4, 8]], [[9, 5, 4], [2, 3, 11]], [[0, 11, 2], [0, 8, 11], [4, 9, 5]], [[0, 5, 4], [0, 1, 5], [2, 3, 11]], [[2, 1, 5], [2, 5, 8], [2, 8, 11], [4, 8, 5]], [[10, 3, 11], [10, 1, 3], [9, 5, 4]], [[4, 9, 5], [0, 8, 1], [8, 10, 1], [8, 11, 10]], [[5, 4, 0], [5, 0, 11], [5, 11, 10], [11, 0, 3]], [[5, 4, 8], [5, 8, 10], [10, 8, 11]], [[9, 7, 8], [5, 7, 9]], [[9, 3, 0], [9, 5, 3], [5, 7, 3]], [[0, 7, 8], [0, 1, 7], [1, 5, 7]], [[1, 5, 3], [3, 5, 7]], [[9, 7, 8], [9, 5, 7], [10, 1, 2]], [[10, 1, 2], [9, 5, 0], [5, 3, 0], [5, 7, 3]], [[8, 0, 2], [8, 2, 5], [8, 5, 7], [10, 5, 2]], [[2, 10, 5], [2, 5, 3], [3, 5, 7]], [[7, 9, 5], [7, 8, 9], [3, 11, 2]], [[9, 5, 7], [9, 7, 2], [9, 2, 0], [2, 7, 11]], [[2, 3, 11], [0, 1, 8], [1, 7, 8], [1, 5, 7]], [[11, 2, 1], [11, 1, 7], [7, 1, 5]], [[9, 5, 8], [8, 5, 7], [10, 1, 3], [10, 3, 11]], [[5, 7, 0], [5, 0, 9], [7, 11, 0], [1, 0, 10], [11, 10, 0]], [[11, 10, 0], [11, 0, 3], [10, 5, 0], [8, 0, 7], [5, 7, 0]], [[11, 10, 5], [7, 11, 5]], [[10, 6, 5]], [[0, 8, 3], [5, 10, 6]], [[9, 0, 1], [5, 10, 6]], [[1, 8, 3], [1, 9, 8], [5, 10, 6]], [[1, 6, 5], [2, 6, 1]], [[1, 6, 5], [1, 2, 6], [3, 0, 8]], [[9, 6, 5], [9, 0, 6], [0, 2, 6]], [[5, 9, 8], [5, 8, 2], [5, 2, 6], [3, 2, 8]], [[2, 3, 11], [10, 6, 5]], [[11, 0, 8], [11, 2, 0], [10, 6, 5]], [[0, 1, 9], [2, 3, 11], [5, 10, 6]], [[5, 10, 6], [1, 9, 2], [9, 11, 2], [9, 8, 11]], [[6, 3, 11], [6, 5, 3], [5, 1, 3]], [[0, 8, 11], [0, 11, 5], [0, 5, 1], [5, 11, 6]], [[3, 11, 6], [0, 3, 6], [0, 6, 5], [0, 5, 9]], [[6, 5, 9], [6, 9, 11], [11, 9, 8]], [[5, 10, 6], [4, 7, 8]], [[4, 3, 0], [4, 7, 3], [6, 5, 10]], [[1, 9, 0], [5, 10, 6], [8, 4, 7]], [[10, 6, 5], [1, 9, 7], [1, 7, 3], [7, 9, 4]], [[6, 1, 2], [6, 5, 1], [4, 7, 8]], [[1, 2, 5], [5, 2, 6], [3, 0, 4], [3, 4, 7]], [[8, 4, 7], [9, 0, 5], [0, 6, 5], [0, 2, 6]], [[7, 3, 9], [7, 9, 4], [3, 2, 9], [5, 9, 6], [2, 6, 9]], [[3, 11, 2], [7, 8, 4], [10, 6, 5]], [[5, 10, 6], [4, 7, 2], [4, 2, 0], [2, 7, 11]], [[0, 1, 9], [4, 7, 8], [2, 3, 11], [5, 10, 6]], [[9, 2, 1], [9, 11, 2], [9, 4, 11], [7, 11, 4], [5, 10, 6]], [[8, 4, 7], [3, 11, 5], [3, 5, 1], [5, 11, 6]], [[5, 1, 11], [5, 11, 6], [1, 0, 11], [7, 11, 4], [0, 4, 11]], [[0, 5, 9], [0, 6, 5], [0, 3, 6], [11, 6, 3], [8, 4, 7]], [[6, 5, 9], [6, 9, 11], [4, 7, 9], [7, 11, 9]], [[10, 4, 9], [6, 4, 10]], [[4, 10, 6], [4, 9, 10], [0, 8, 3]], [[10, 0, 1], [10, 6, 0], [6, 4, 0]], [[8, 3, 1], [8, 1, 6], [8, 6, 4], [6, 1, 10]], [[1, 4, 9], [1, 2, 4], [2, 6, 4]], [[3, 0, 8], [1, 2, 9], [2, 4, 9], [2, 6, 4]], [[0, 2, 4], [4, 2, 6]], [[8, 3, 2], [8, 2, 4], [4, 2, 6]], [[10, 4, 9], [10, 6, 4], [11, 2, 3]], [[0, 8, 2], [2, 8, 11], [4, 9, 10], [4, 10, 6]], [[3, 11, 2], [0, 1, 6], [0, 6, 4], [6, 1, 10]], [[6, 4, 1], [6, 1, 10], [4, 8, 1], [2, 1, 11], [8, 11, 1]], [[9, 6, 4], [9, 3, 6], [9, 1, 3], [11, 6, 3]], [[8, 11, 1], [8, 1, 0], [11, 6, 1], [9, 1, 4], [6, 4, 1]], [[3, 11, 6], [3, 6, 0], [0, 6, 4]], [[6, 4, 8], [11, 6, 8]], [[7, 10, 6], [7, 8, 10], [8, 9, 10]], [[0, 7, 3], [0, 10, 7], [0, 9, 10], [6, 7, 10]], [[10, 6, 7], [1, 10, 7], [1, 7, 8], [1, 8, 0]], [[10, 6, 7], [10, 7, 1], [1, 7, 3]], [[1, 2, 6], [1, 6, 8], [1, 8, 9], [8, 6, 7]], [[2, 6, 9], [2, 9, 1], [6, 7, 9], [0, 9, 3], [7, 3, 9]], [[7, 8, 0], [7, 0, 6], [6, 0, 2]], [[7, 3, 2], [6, 7, 2]], [[2, 3, 11], [10, 6, 8], [10, 8, 9], [8, 6, 7]], [[2, 0, 7], [2, 7, 11], [0, 9, 7], [6, 7, 10], [9, 10, 7]], [[1, 8, 0], [1, 7, 8], [1, 10, 7], [6, 7, 10], [2, 3, 11]], [[11, 2, 1], [11, 1, 7], [10, 6, 1], [6, 7, 1]], [[8, 9, 6], [8, 6, 7], [9, 1, 6], [11, 6, 3], [1, 3, 6]], [[0, 9, 1], [11, 6, 7]], [[7, 8, 0], [7, 0, 6], [3, 11, 0], [11, 6, 0]], [[7, 11, 6]]] return edgeTable, triTable