AlignTwoPolyDatas

VTKExamples/Cxx/PolyData/AlignTwoPolyDatas


Description

This example shows how to align two vtkPolyData's. Typically, the two datasets are related. For example, aligning a CT head isosurface with an MRI head isosurface of the same patient. Or two steps in a time series of an evolving surface. These cases usually reside in the same coordinate system, and the initial alignment is "close" to the desired results.

Another case is when the two datasets are from the "same" family of objects — for example, running the example with two types of sharks that exist in different coordinate systems.

The algorithm proceeds as follows:

  1. Read the two vtkPolyData's that exist in the example's command line. The first file contains the source vtkPolyData to be aligned with the second file's vtkPolyData called the target. Another naming convention is moving and fixed.

  2. Compute a measure of fit of the two original files. We use the recently added vtkHausdorffDistancePointSetFilter to compute the measure. See Hausdorff Distance.

  3. Align the bounding boxes of the two datasets. Here we use a vtkOBBTree locator to create oriented bounding boxes. See Oriented Bounding Boxes. Use the bounding box corner coordinates to create source and target vtkLandmarkTransform's. vtkTransformPolyData uses this transform to create a new source vtkPolyData. Since the orientations of the bounding boxes may differ, the AlignBoundingBoxes function tries ten different rotations. For each rotation, it computes the Hausdorff distance between the target's OBB corners and the transformed source's OBB corners. Finally, transform the original source using the smallest distance.

  4. Improve the alignment with vtkIterativeClosestPointTransform with a RigidBody transform. Compute the distance metric again.

  5. Using the transform that has the best distance metric, do a final, and display the source and target vtkPolyData's.

Info

The example is run with Grey_Nurse_Shark.stl and shark.ply

Info

You may need to orient the target using vtkTransformPolyDataFilter to get a better fit, for example when using Grey_Nurse_Shark.stl and greatWhite.stl, uncommenting the two rotations in vtkTransform will provide an excellent alignment.

Other Languages

See (Python)

Code

AlignTwoPolyDatas.cxx

#include <vtkFieldData.h>
#include <vtkHausdorffDistancePointSetFilter.h>
#include <vtkIterativeClosestPointTransform.h>
#include <vtkLandmarkTransform.h>
#include <vtkPoints.h>
#include <vtkSmartPointer.h>
#include <vtkTransform.h>
#include <vtkTransformPolyDataFilter.h>

#include <vtkBYUReader.h>
#include <vtkOBBTree.h>
#include <vtkOBJReader.h>
#include <vtkPLYReader.h>
#include <vtkPolyDataReader.h>
#include <vtkSTLReader.h>
#include <vtkSphereSource.h>
#include <vtkXMLPolyDataReader.h>

#include <vtkActor.h>
#include <vtkCamera.h>
#include <vtkDataSetMapper.h>
#include <vtkProperty.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>

#include <vtkNamedColors.h>
#include <vtksys/SystemTools.hxx>

#include <algorithm>
#include <array>
#include <random>
#include <sstream>
#include <string>

namespace {
/**
 * Show the command lime parameters.
 *
 * @param fn: The program name.
 */
std::string ShowUsage(std::string fn);

vtkSmartPointer<vtkPolyData> ReadPolyData(const char* fileName);
void AlignBoundingBoxes(vtkPolyData*, vtkPolyData*);
void BestBoundingBox(std::string axis, vtkPolyData* target, vtkPolyData* source,
                     vtkPolyData* targetLandmarks, vtkPolyData* sourceLandmarks,
                     double& distance, vtkPoints* bestPoints);
} // namespace

int main(int argc, char* argv[])
{
  if (argc != 3)
  {
    std::cout << ShowUsage(argv[0]) << std::endl;
    return EXIT_FAILURE;
  }

  // Vis Pipeline
  auto colors = vtkSmartPointer<vtkNamedColors>::New();

  auto renderer = vtkSmartPointer<vtkRenderer>::New();

  auto renderWindow = vtkSmartPointer<vtkRenderWindow>::New();
  renderWindow->SetSize(640, 480);
  renderWindow->AddRenderer(renderer);

  auto interactor = vtkSmartPointer<vtkRenderWindowInteractor>::New();
  interactor->SetRenderWindow(renderWindow);

  renderer->SetBackground(colors->GetColor3d("sea_green_light").GetData());
  renderer->UseHiddenLineRemovalOn();

  std::cout << "Loading source: " << argv[1] << std::endl;
  auto sourcePolyData = ReadPolyData(argv[1]);

  // Save the source polydata in case the align does not improve
  // segmentation
  auto originalSourcePolyData = vtkSmartPointer<vtkPolyData>::New();
  originalSourcePolyData->DeepCopy(sourcePolyData);

  std::cout << "Loading target: " << argv[2] << std::endl;
  auto targetPolyData = ReadPolyData(argv[2]);

  // If the target orientation is markedly different,
  // you may need to apply a transform to orient the
  // target with the source.
  // For example, when using Grey_Nurse_Shark.stl as the source and
  // greatWhite.stl as the target, you need to uncomment the following
  // two rotations.
  auto trnf = vtkSmartPointer<vtkTransform>::New();
  //trnf->RotateX(90);
  //trnf->RotateY(-90);
  auto tpd = vtkSmartPointer<vtkTransformPolyDataFilter>::New();
  tpd->SetTransform(trnf);
  tpd->SetInputData(targetPolyData);
  tpd->Update();

  auto distance = vtkSmartPointer<vtkHausdorffDistancePointSetFilter>::New();
  distance->SetInputData(0, tpd->GetOutput());
  distance->SetInputData(1, sourcePolyData);
  distance->Update();

  double distanceBeforeAlign = static_cast<vtkPointSet*>(distance->GetOutput(0))
                                   ->GetFieldData()
                                   ->GetArray("HausdorffDistance")
                                   ->GetComponent(0, 0);

  // Get initial alignment using oriented bounding boxes
  AlignBoundingBoxes(sourcePolyData, tpd->GetOutput());

  distance->SetInputData(0, tpd->GetOutput());
  distance->SetInputData(1, sourcePolyData);
  distance->Modified();
  distance->Update();
  double distanceAfterAlign = static_cast<vtkPointSet*>(distance->GetOutput(0))
                                  ->GetFieldData()
                                  ->GetArray("HausdorffDistance")
                                  ->GetComponent(0, 0);

  double bestDistance = std::min(distanceBeforeAlign, distanceAfterAlign);

  if (distanceAfterAlign > distanceBeforeAlign)
  {
    sourcePolyData->DeepCopy(originalSourcePolyData);
  }

  // Refine the alignment using IterativeClosestPoint
  auto icp = vtkSmartPointer<vtkIterativeClosestPointTransform>::New();
  icp->SetSource(sourcePolyData);
  icp->SetTarget(tpd->GetOutput());
  icp->GetLandmarkTransform()->SetModeToRigidBody();
  icp->SetMaximumNumberOfLandmarks(100);
  icp->SetMaximumMeanDistance(.00001);
  icp->SetMaximumNumberOfIterations(500);
  icp->CheckMeanDistanceOn();
  icp->StartByMatchingCentroidsOn();
  icp->Update();

  //  icp->Print(std::cout);

  auto lmTransform = icp->GetLandmarkTransform();
  auto transform = vtkSmartPointer<vtkTransformPolyDataFilter>::New();
  transform->SetInputData(sourcePolyData);
  transform->SetTransform(lmTransform);
  transform->SetTransform(icp);
  transform->Update();

  distance->SetInputData(0, tpd->GetOutput());
  distance->SetInputData(1, transform->GetOutput());
  distance->Update();

  double distanceAfterICP = static_cast<vtkPointSet*>(distance->GetOutput(0))
                                ->GetFieldData()
                                ->GetArray("HausdorffDistance")
                                ->GetComponent(0, 0);

  if (distanceAfterICP < bestDistance)
  {
    bestDistance = distanceAfterICP;
  }

  std::cout << "Distance before, after align, after ICP, min: "
            << distanceBeforeAlign << ", " << distanceAfterAlign << ", "
            << distanceAfterICP << ", " << bestDistance << std::endl;

  // Select
  auto sourceMapper = vtkSmartPointer<vtkDataSetMapper>::New();
  if (bestDistance == distanceBeforeAlign)
  {
    sourceMapper->SetInputData(originalSourcePolyData);
    std::cout << "Using original alignment" << std::endl;
  }
  else if (bestDistance == distanceAfterAlign)
  {
    sourceMapper->SetInputData(sourcePolyData);
    std::cout << "Using alignment by OBB" << std::endl;
  }
  else
  {
    sourceMapper->SetInputConnection(transform->GetOutputPort());
    std::cout << "Using alignment by ICP" << std::endl;
  }
  sourceMapper->ScalarVisibilityOff();

  auto sourceActor = vtkSmartPointer<vtkActor>::New();
  sourceActor->SetMapper(sourceMapper);
  sourceActor->GetProperty()->SetOpacity(.6);
  sourceActor->GetProperty()->SetDiffuseColor(
      colors->GetColor3d("White").GetData());
  renderer->AddActor(sourceActor);

  auto targetMapper = vtkSmartPointer<vtkDataSetMapper>::New();
  targetMapper->SetInputData(tpd->GetOutput());
  targetMapper->ScalarVisibilityOff();

  auto targetActor = vtkSmartPointer<vtkActor>::New();
  targetActor->SetMapper(targetMapper);
  targetActor->GetProperty()->SetDiffuseColor(
      colors->GetColor3d("Tomato").GetData());
  renderer->AddActor(targetActor);

  renderWindow->AddRenderer(renderer);

  renderWindow->Render();
  renderWindow->SetWindowName("AlignTwoPolyDatas");
  renderWindow->Render();

  interactor->Start();

  return EXIT_SUCCESS;
}
namespace {

std::string ShowUsage(std::string fn)
{
  // Remove the folder (if present) then remove the extension in this order
  // since the folder name may contain periods.
  auto last_slash_idx = fn.find_last_of("\\/");
  if (std::string::npos != last_slash_idx)
  {
    fn.erase(0, last_slash_idx + 1);
  }
  auto period_idx = fn.rfind('.');
  if (std::string::npos != period_idx)
  {
    fn.erase(period_idx);
  }
  std::ostringstream os;
  os << "\nusage: " << fn << " src_fn tgt_fn\n\n"
     << "How to align two vtkPolyData's.\n\n"
     << "positional arguments:\n"
     << "  src_fn      The polydata source file name,e.g. "
        "Grey_Nurse_Shark.stl.\n"
     << "  tgt_fn      The polydata target file name, e.g. shark.ply.\n"
     << "\n"
     << std::endl;
  return os.str();
}

vtkSmartPointer<vtkPolyData> ReadPolyData(const char* fileName)
{
  vtkSmartPointer<vtkPolyData> polyData;
  std::string extension =
      vtksys::SystemTools::GetFilenameLastExtension(std::string(fileName));

  // Drop the case of the extension
  std::transform(extension.begin(), extension.end(), extension.begin(),
                 ::tolower);

  if (extension == ".ply")
  {
    auto reader = vtkSmartPointer<vtkPLYReader>::New();
    reader->SetFileName(fileName);
    reader->Update();
    polyData = reader->GetOutput();
  }
  else if (extension == ".vtp")
  {
    auto reader = vtkSmartPointer<vtkXMLPolyDataReader>::New();
    reader->SetFileName(fileName);
    reader->Update();
    polyData = reader->GetOutput();
  }
  else if (extension == ".obj")
  {
    auto reader = vtkSmartPointer<vtkOBJReader>::New();
    reader->SetFileName(fileName);
    reader->Update();
    polyData = reader->GetOutput();
  }
  else if (extension == ".stl")
  {
    auto reader = vtkSmartPointer<vtkSTLReader>::New();
    reader->SetFileName(fileName);
    reader->Update();
    polyData = reader->GetOutput();
  }
  else if (extension == ".vtk")
  {
    auto reader = vtkSmartPointer<vtkPolyDataReader>::New();
    reader->SetFileName(fileName);
    reader->Update();
    polyData = reader->GetOutput();
  }
  else if (extension == ".g")
  {
    auto reader = vtkSmartPointer<vtkBYUReader>::New();
    reader->SetGeometryFileName(fileName);
    reader->Update();
    polyData = reader->GetOutput();
  }
  else
  {
    auto source = vtkSmartPointer<vtkSphereSource>::New();
    source->Update();
    polyData = source->GetOutput();
  }
  return polyData;
}

void AlignBoundingBoxes(vtkPolyData* source, vtkPolyData* target)
{
  // Use OBBTree to create an oriented bounding box for target and source
  auto sourceOBBTree = vtkSmartPointer<vtkOBBTree>::New();
  sourceOBBTree->SetDataSet(source);
  sourceOBBTree->SetMaxLevel(1);
  sourceOBBTree->BuildLocator();

  auto targetOBBTree = vtkSmartPointer<vtkOBBTree>::New();
  targetOBBTree->SetDataSet(target);
  targetOBBTree->SetMaxLevel(1);
  targetOBBTree->BuildLocator();

  auto sourceLandmarks = vtkSmartPointer<vtkPolyData>::New();
  sourceOBBTree->GenerateRepresentation(0, sourceLandmarks);

  auto targetLandmarks = vtkSmartPointer<vtkPolyData>::New();
  targetOBBTree->GenerateRepresentation(0, targetLandmarks);

  auto lmTransform = vtkSmartPointer<vtkLandmarkTransform>::New();
  lmTransform->SetModeToSimilarity();
  lmTransform->SetTargetLandmarks(targetLandmarks->GetPoints());
  // auto lmTransformPD = vtkSmartPointer<vtkTransformPolyDataFilter>::New();
  double bestDistance = VTK_DOUBLE_MAX;
  auto bestPoints = vtkSmartPointer<vtkPoints>::New();
  BestBoundingBox("X", target, source, targetLandmarks, sourceLandmarks,
                  bestDistance, bestPoints);
  BestBoundingBox("Y", target, source, targetLandmarks, sourceLandmarks,
                  bestDistance, bestPoints);
  BestBoundingBox("Z", target, source, targetLandmarks, sourceLandmarks,
                  bestDistance, bestPoints);

  lmTransform->SetSourceLandmarks(bestPoints);
  lmTransform->Modified();

  auto transformPD = vtkSmartPointer<vtkTransformPolyDataFilter>::New();
  transformPD->SetInputData(source);
  transformPD->SetTransform(lmTransform);
  transformPD->Update();

  source->DeepCopy(transformPD->GetOutput());
}
void BestBoundingBox(std::string axis, vtkPolyData* target, vtkPolyData* source,
                     vtkPolyData* targetLandmarks, vtkPolyData* sourceLandmarks,
                     double& bestDistance, vtkPoints* bestPoints)
{
  auto distance = vtkSmartPointer<vtkHausdorffDistancePointSetFilter>::New();
  auto testTransform = vtkSmartPointer<vtkTransform>::New();
  auto testTransformPD = vtkSmartPointer<vtkTransformPolyDataFilter>::New();
  auto lmTransform = vtkSmartPointer<vtkLandmarkTransform>::New();
  auto lmTransformPD = vtkSmartPointer<vtkTransformPolyDataFilter>::New();

  lmTransform->SetModeToSimilarity();
  lmTransform->SetTargetLandmarks(targetLandmarks->GetPoints());

  double sourceCenter[3];
  sourceLandmarks->GetCenter(sourceCenter);

  auto delta = 90.0;
  for (auto i = 0; i < 4; ++i)
  {
    auto angle = delta * i;
    // Rotate about center
    testTransform->Identity();
    testTransform->Translate(sourceCenter[0], sourceCenter[1], sourceCenter[2]);
    if (axis == "X")
    {
      testTransform->RotateX(angle);
    }
    else if (axis == "Y")
    {
      testTransform->RotateY(angle);
    }
    else
    {
      testTransform->RotateZ(angle);
    }
    testTransform->Translate(-sourceCenter[0], -sourceCenter[1],
                             -sourceCenter[2]);

    testTransformPD->SetTransform(testTransform);
    testTransformPD->SetInputData(sourceLandmarks);
    testTransformPD->Update();

    lmTransform->SetSourceLandmarks(testTransformPD->GetOutput()->GetPoints());
    lmTransform->Modified();

    lmTransformPD->SetInputData(source);
    lmTransformPD->SetTransform(lmTransform);
    lmTransformPD->Update();

    distance->SetInputData(0, target);
    distance->SetInputData(1, lmTransformPD->GetOutput());
    distance->Update();

    double testDistance = static_cast<vtkPointSet*>(distance->GetOutput(0))
                              ->GetFieldData()
                              ->GetArray("HausdorffDistance")
                              ->GetComponent(0, 0);
    if (testDistance < bestDistance)
    {
      bestDistance = testDistance;
      bestPoints->DeepCopy(testTransformPD->GetOutput()->GetPoints());
    }
  }
  return;
}

} // namespace

CMakeLists.txt

cmake_minimum_required(VERSION 3.3 FATAL_ERROR)

project(AlignTwoPolyDatas)

find_package(VTK COMPONENTS 
  vtkCommonColor
  vtkCommonCore
  vtkCommonDataModel
  vtkCommonTransforms
  vtkFiltersGeneral
  vtkFiltersModeling
  vtkFiltersSources
  vtkIOGeometry
  vtkIOLegacy
  vtkIOPLY
  vtkIOXML
  vtkInteractionStyle
  vtkRenderingContextOpenGL2
  vtkRenderingCore
  vtkRenderingFreeType
  vtkRenderingGL2PSOpenGL2
  vtkRenderingOpenGL2 QUIET)
if (NOT VTK_FOUND)
  message("Skipping AlignTwoPolyDatas: ${VTK_NOT_FOUND_MESSAGE}")
  return ()
endif()
message (STATUS "VTK_VERSION: ${VTK_VERSION}")
if (VTK_VERSION VERSION_LESS "8.90.0")
  # old system
  include(${VTK_USE_FILE})
  add_executable(AlignTwoPolyDatas MACOSX_BUNDLE AlignTwoPolyDatas.cxx )
  target_link_libraries(AlignTwoPolyDatas PRIVATE ${VTK_LIBRARIES})
else ()
  # include all components
  add_executable(AlignTwoPolyDatas MACOSX_BUNDLE AlignTwoPolyDatas.cxx )
  target_link_libraries(AlignTwoPolyDatas PRIVATE ${VTK_LIBRARIES})
  # vtk_module_autoinit is needed
  vtk_module_autoinit(
    TARGETS AlignTwoPolyDatas
    MODULES ${VTK_LIBRARIES}
    )
endif () 

Download and Build AlignTwoPolyDatas

Click here to download AlignTwoPolyDatas and its CMakeLists.txt file. Once the tarball AlignTwoPolyDatas.tar has been downloaded and extracted,

cd AlignTwoPolyDatas/build 

If VTK is installed:

cmake ..

If VTK is not installed but compiled on your system, you will need to specify the path to your VTK build:

cmake -DVTK_DIR:PATH=/home/me/vtk_build ..

Build the project:

make

and run it:

./AlignTwoPolyDatas

WINDOWS USERS

Be sure to add the VTK bin directory to your path. This will resolve the VTK dll's at run time.