在医学影像中,由于CT、MRI等成像设备的数据均以断层影像的形式输出,对其进行轮廓提取和三维重建就成为一个常见的需求。
将一组相互平行的轮廓线,以三角面片连接,形成放样曲面,即为surface from contours问题。这一问题的经典解决方法可参见论文Piecewise-Linear Interpolation between Polygonal Slices(简单贪心)
及approximating complex surfaces by triangulation of contour lines
(动态规划)。
在VTK库中,类vtkVoxelContoursToSurfaceFilter提供了一个基于网格体元的实现。
create surface from contours
vtkVoxelContoursToSurfaceFilter is a filter that takes contours and produces surfaces. There are some restrictions for the contours:
The contours are input as vtkPolyData, with the contours being polys in the vtkPolyData.
The contours lie on XY planes - each contour has a constant Z
The contours are ordered in the polys of the vtkPolyData such that all contours on the first (lowest) XY plane are first, then continuing in order of increasing Z value.
The X, Y and Z coordinates are all integer values.
The desired sampling of the contour data is 1x1x1 - Aspect can be used to control the aspect ratio in the output polygonal dataset.
This filter takes the contours and produces a structured points dataset of signed floating point number indicating distance from a contour. A contouring filter is then applied to generate 3D surfaces from a stack of 2D contour distance slices. This is done in a streaming fashion so as not to use to much memory.
以下为代码注释
// 曲面提取主函数
// Append data sets into single unstructured grid
int vtkVoxelContoursToSurfaceFilter::RequestData(
vtkInformation *vtkNotUsed(request),
vtkInformationVector **inputVector,
vtkInformationVector *outputVector)
{
// get the info objects
vtkInformation *inInfo = inputVector[0]->GetInformationObject(0);
vtkInformation *outInfo = outputVector->GetInformationObject(0);
// get the input and output
vtkPolyData *input = vtkPolyData::SafeDownCast(
inInfo->Get(vtkDataObject::DATA_OBJECT()));
vtkPolyData *output = vtkPolyData::SafeDownCast(
outInfo->Get(vtkDataObject::DATA_OBJECT()));
vtkCellArray *inputPolys = input->GetPolys();
int gridSize[3];
double gridOrigin[3];
double contourBounds[6];
int chunkSize;
int currentSlice, lastSlice, currentIndex;
int i, j;
int numberOfInputCells;
int currentInputCellIndex;
vtkIdType npts = 0;
vtkIdType *pts = 0;
double point1[3], point2[3];
double currentZ;
vtkStructuredPoints *volume;
float *volumePtr, *slicePtr;
vtkContourFilter *contourFilter;
vtkPolyData *contourOutput;
vtkAppendPolyData *appendFilter;
// Get the bounds of the input contours
input->GetBounds( contourBounds );
if (contourBounds[0] > contourBounds[1]) // xmin > xmax
{ // empty input
return 1;
}
// From the bounds, compute the grid size, and origin
//指定网格原点 这里有一个附加平移
//沿xy轴的是因为我们希望数据点落在网格之间
//沿z轴是因为需要一个附加面作为终止结点
gridOrigin[0] = contourBounds[0] - 0.5;
gridOrigin[1] = contourBounds[2] - 0.5;
gridOrigin[2] = contourBounds[4] - 1.0;
//指定网格尺寸 附加数字是差不多的原因
// The difference between the bounds, plus one to account a
// sample on the first and last location, plus one to account
// for the larger grid size ( the 0.5 unit border ) On Z, we
// want to sample exactly on the contours so we don't need to
// add the extra 1, but we have added two extra planes so we
// need another 2.
gridSize[0] = (int) (contourBounds[1] - contourBounds[0] + 2);
gridSize[1] = (int) (contourBounds[3] - contourBounds[2] + 2);
gridSize[2] = (int) (contourBounds[5] - contourBounds[4] + 3);
// How many slices in a chunk? This will later be decremented
// by one to account for the fact that the last slice in the
// previous chuck is copied to the first slice in the next chunk.
// Stay within memory limit. There are 4 bytes per double.
//根据内存限制 计算网格尺寸
chunkSize = this->MemoryLimitInBytes / ( gridSize[0] * gridSize[1] * 4 );
if ( chunkSize > gridSize[2] )
{
chunkSize = gridSize[2];
}
currentSlice = 0;
currentZ = contourBounds[4] - 1.0;
currentIndex = 0;
lastSlice = gridSize[2] - 1;
numberOfInputCells = inputPolys->GetNumberOfCells();
currentInputCellIndex = 0;
//建立网格
volume = vtkStructuredPoints::New();
volume->SetDimensions( gridSize[0], gridSize[1], chunkSize );
volume->SetSpacing( this->Spacing );
volume->AllocateScalars( VTK_FLOAT, 1 );
volumePtr = (float *)(volume->GetPointData()->GetScalars()->GetVoidPointer(0));
contourFilter = vtkContourFilter::New();
contourFilter->SetInputData( volume );
contourFilter->SetNumberOfContours(1);
contourFilter->SetValue( 0, 0.0 );
appendFilter = vtkAppendPolyData::New();
inputPolys->InitTraversal();
inputPolys->GetNextCell( npts, pts );
while ( currentSlice <= lastSlice )
{
// Make sure the origin of the volume is in the right
// place so that the appended polydata all matches up
// nicely.
volume->SetOrigin( gridOrigin[0], gridOrigin[1],
gridOrigin[2] +
this->Spacing[2] * (currentSlice - (currentSlice!=0)) );
for ( i = currentIndex; i < chunkSize; i++ )
{
slicePtr = volumePtr + i * gridSize[0] * gridSize[1];
// Clear out the slice memory - set it all to a large negative
// value indicating no surfaces are nearby, and we assume we
// are outside of any surface
for ( j = 0; j < gridSize[0] * gridSize[1]; j++ )
{
*(slicePtr+j) = -9.99e10;
}
// If we are past the end, don't do anything
if ( currentSlice > lastSlice )
{
continue;
}
this->LineListLength = 0;
// Read in the lines for the contours on this slice
while ( currentInputCellIndex < numberOfInputCells )
{
// Check if we are still on the right z slice
input->GetPoint( pts[0], point1 );
if ( point1[2] != currentZ )
{
break;
}
// This contour is on the right z slice - add the lines
// to our list
for ( j = 0; j < npts; j++ )
{
input->GetPoint( pts[j], point1 );
input->GetPoint( pts[(j+1)%npts], point2 );
//将该层coutour的所有线段加入列表
this->AddLineToLineList( point1[0], point1[1],
point2[0], point2[1] );
}
inputPolys->GetNextCell( npts, pts );
currentInputCellIndex++;
}
// 将加入的所有线段 分别按照端点的x y坐标顺序排序 排序结果存于SortedXList SortedYList变量中供CastLines函数使用
this->SortLineList();
// Cast lines in x and y filling in distance
// 沿x轴和y轴扫描填充volume
this->CastLines( slicePtr, gridOrigin, gridSize, 0 );
this->CastLines( slicePtr, gridOrigin, gridSize, 1 );
// Move on to the next slice
currentSlice++;
currentIndex++;
currentZ += 1.0;
}
//不知道干什么用的 似乎是对数据前后平滑一下
this->PushDistances( volumePtr, gridSize, chunkSize );
// Update the contour filter and grab the output
// Make a new output for it, then grab the output and
// add it to the append filter, then delete the output
// which is ok since it was registered by the appendFilter
contourOutput = vtkPolyData::New();
contourFilter->Update();
contourOutput->ShallowCopy(contourFilter->GetOutput());
appendFilter->AddInputData( contourOutput );
contourOutput->Delete();
if ( currentSlice <= lastSlice )
{
// Copy last slice to first slice
memcpy( volumePtr, volumePtr + (chunkSize-1)*gridSize[0]*gridSize[1],
sizeof(float) * gridSize[0] * gridSize[1] );
// reset currentIndex to 1
currentIndex = 1;
}
}
appendFilter->Update();
// Grab the appended data as the output to this filter
output->SetPoints( appendFilter->GetOutput()->GetPoints() );
output->SetVerts( appendFilter->GetOutput()->GetVerts() );
output->SetLines( appendFilter->GetOutput()->GetLines() );
output->SetPolys( appendFilter->GetOutput()->GetPolys() );
output->SetStrips( appendFilter->GetOutput()->GetStrips() );
output->GetPointData()->PassData(appendFilter->GetOutput()->GetPointData());
contourFilter->Delete();
appendFilter->Delete();
volume->Delete();
return 1;
}