1 /*
2  * Medical Image Registration ToolKit (MIRTK)
3  *
4  * Copyright 2013-2017 Imperial College London
5  * Copyright 2013-2017 Andreas Schuh
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *     http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 
20 #include "mirtk/Common.h"
21 #include "mirtk/Options.h"
22 
23 #include "mirtk/PointSetIO.h"
24 #include "mirtk/PointSetUtils.h"
25 #include "mirtk/DilatePointData.h"
26 #include "mirtk/ErodePointData.h"
27 #include "mirtk/EdgeTable.h"
28 #include "mirtk/EdgeConnectivity.h"
29 
30 #include "mirtk/Vtk.h"
31 #include "mirtk/VtkMath.h"
32 
33 #include "vtkPointSet.h"
34 #include "vtkPolyData.h"
35 #include "vtkPointData.h"
36 #include "vtkFloatArray.h"
37 #include "vtkCellLocator.h"
38 #include "vtkPolyDataNormals.h"
39 #include "vtkModifiedBSPTree.h"
40 #include "vtkKdTreePointLocator.h"
41 
42 using namespace mirtk;
43 
44 
45 // =============================================================================
46 // Help
47 // =============================================================================
48 
49 // -----------------------------------------------------------------------------
PrintHelp(const char * name)50 void PrintHelp(const char *name)
51 {
52   cout << "\n";
53   cout << "Usage: " << name << " <target> <source> [<output>] [options]\n";
54   cout << "\n";
55   cout << "Description:\n";
56   cout << "  Evaluate distance between two given point sets. With increased verbosity (see :option:`-v`),\n";
57   cout << "  the mean and standard deviation of the measured distances  (verbosity level >=1) and the\n";
58   cout << "  individual distance for each target point is reported (verbosity level >=2).\n";
59   cout << "\n";
60   cout << "Arguments:\n";
61   cout << "  target   Target point set/surface.\n";
62   cout << "  source   Source point set/surface.\n";
63   cout << "  output   Output point set/surface with point data array of measured distances. (default: none)\n";
64   cout << "\n";
65   cout << "Optional arguments:\n";
66   cout << "  -mask-name <name>\n";
67   cout << "      Name of input mask. (default: none)\n";
68   cout << "  -mask-erosion <n>\n";
69   cout << "      Number of iterations of mask erosions. (default: 0)\n";
70   cout << "  -mask-dilation <n>\n";
71   cout << "      Number of iterations of mask dilations. (default: 0)\n";
72   cout << "  -pad-value, -padding-value <value>\n";
73   cout << "      Floating point value for points with zero mask value. (default: NaN)\n";
74   cout << "  -undef-value, -undefined-value <value>\n";
75   cout << "      Value for undefined distance measures, e.g., normal does not intersect source. (default: NaN)\n";
76   cout << "  -nan-value <value>\n";
77   cout << "      Set both :option:`-pad-value` and :option:`-undef-value` to the specified value.\n";
78   cout << "  -dist-name, -name, -array <name>\n";
79   cout << "      Name of output point data array or column header. (default: Distance)\n";
80   cout << "  -digits, -precision <n>\n";
81   cout << "      Number of digits after the decimal point to print/write. (default: 5)\n";
82   cout << "  -table [<name>|stdout|cout|print]\n";
83   cout << "      Write statistics of distance measure in table format to named output file.\n";
84   cout << "      When <name> is 'stdout', 'cout',  or 'print', print table to standard output stream. (default: off)\n";
85   cout << "  -[no]append\n";
86   cout << "      Whether to append row to existing :option:`-table` file.\n";
87   cout << "      If table file exists, skip header and write measurements only. (default: off)\n";
88   cout << "  -[no]header\n";
89   cout << "      When :option:`-table` given, write table header before row of measurements. (default: on)\n";
90   cout << "  -delim, -delimiter, -sep, -separator <str>\n";
91   cout << "      Column separator used for table of measured point distances. (default: \\t)\n";
92   cout << "  -point\n";
93   cout << "      Evaluate minimum distance to closest point. (default for point clouds)\n";
94   cout << "  -cell\n";
95   cout << "      Evaluate minimum distance to closest surface point. (default for surface meshes)\n";
96   cout << "  -normal\n";
97   cout << "      Evaluate minimum distance along surface normal.\n";
98   cout << "  -index\n";
99   cout << "      Evaluate distance between points with identical index, e.g., pairs of fiducial markers.\n";
100   cout << "  -hausdorff\n";
101   cout << "      Report Hausdorff distance. (default: off)\n";
102   PrintStandardOptions(cout);
103   cout << endl;
104 }
105 
106 // =============================================================================
107 // Auxiliaries
108 // =============================================================================
109 
110 // -----------------------------------------------------------------------------
111 /// Evaluate distance of target to closest source points
EvaluateClosestPointDistance(vtkPointSet * target,vtkPointSet * source,vtkDataArray * distance,vtkDataArray * mask=nullptr,double pad_value=NaN,double nan_value=NaN)112 void EvaluateClosestPointDistance(vtkPointSet *target, vtkPointSet *source, vtkDataArray *distance,
113                                   vtkDataArray *mask = nullptr, double pad_value = NaN, double nan_value = NaN)
114 {
115   vtkIdType otherPtId;
116   double    a[3], b[3], dist;
117 
118   vtkNew<vtkKdTreePointLocator> locator;
119   locator->SetDataSet(source);
120   locator->BuildLocator();
121 
122   for (vtkIdType ptId = 0; ptId < target->GetNumberOfPoints(); ++ptId) {
123     if (!mask || mask->GetComponent(ptId, 0) != 0) {
124       target->GetPoint(ptId, a);
125       otherPtId = locator->FindClosestPoint(a);
126       source->GetPoint(otherPtId, b);
127       dist = sqrt(vtkMath::Distance2BetweenPoints(a, b));
128       distance->SetComponent(ptId, 0, IsNaN(dist) ? nan_value : dist);
129     } else {
130       distance->SetComponent(ptId, 0, pad_value);
131     }
132   }
133 }
134 
135 // -----------------------------------------------------------------------------
136 /// Evaluate distance of target to closest source cell point
EvaluateClosestCellDistance(vtkPointSet * target,vtkPointSet * source,vtkDataArray * distance,vtkDataArray * mask=nullptr,double pad_value=NaN,double nan_value=NaN)137 void EvaluateClosestCellDistance(vtkPointSet *target, vtkPointSet *source, vtkDataArray *distance,
138                                  vtkDataArray *mask = nullptr, double pad_value = NaN, double nan_value = NaN)
139 {
140   int       subId;
141   vtkIdType cellId;
142   double    a[3], b[3], dist;
143 
144   vtkNew<vtkCellLocator> locator;
145   locator->SetDataSet(source);
146   locator->BuildLocator();
147 
148   for (vtkIdType ptId = 0; ptId < target->GetNumberOfPoints(); ++ptId) {
149     if (!mask || mask->GetComponent(ptId, 0) != 0) {
150       target->GetPoint(ptId, a);
151       locator->FindClosestPoint(a, b, cellId, subId, dist);
152       dist = sqrt(dist);
153       distance->SetComponent(ptId, 0, IsNaN(dist) ? nan_value : dist);
154     } else {
155       distance->SetComponent(ptId, 0, pad_value);
156     }
157   }
158 }
159 
160 // -----------------------------------------------------------------------------
161 /// Evaluate distance of corresponding points
EvaluateCorrespondingPointDistance(vtkPointSet * target,vtkPointSet * source,vtkDataArray * distance,vtkDataArray * mask=nullptr,double pad_value=NaN,double nan_value=NaN)162 void EvaluateCorrespondingPointDistance(vtkPointSet *target, vtkPointSet *source, vtkDataArray *distance,
163                                         vtkDataArray *mask = nullptr, double pad_value = NaN, double nan_value = NaN)
164 {
165   double a[3], b[3], dist;
166 
167   if (target->GetNumberOfPoints() != source->GetNumberOfPoints()) {
168     Throw(ERR_InvalidArgument, __FUNCTION__, "Point sets must have equal number of points!");
169   }
170   if (mask && mask->GetNumberOfTuples() != target->GetNumberOfPoints()) {
171     Throw(ERR_InvalidArgument, __FUNCTION__, "Mask array must have one tuple for each target point!");
172   }
173 
174   for (vtkIdType ptId = 0; ptId < target->GetNumberOfPoints(); ++ptId) {
175     if (!mask || mask->GetComponent(ptId, 0) != 0) {
176       target->GetPoint(ptId, a);
177       source->GetPoint(ptId, b);
178       dist = sqrt(vtkMath::Distance2BetweenPoints(a, b));
179       distance->SetComponent(ptId, 0, IsNaN(dist) ? nan_value : dist);
180     } else {
181       distance->SetComponent(ptId, 0, pad_value);
182     }
183   }
184 }
185 
186 // -----------------------------------------------------------------------------
187 /// Evaluate distance of target to closest source cell point in normal direction
EvaluateNormalDistance(vtkPointSet * target,vtkPointSet * source,vtkDataArray * distance,vtkDataArray * mask=nullptr,double pad_value=NaN,double nan_value=NaN)188 void EvaluateNormalDistance(vtkPointSet *target, vtkPointSet *source, vtkDataArray *distance,
189                             vtkDataArray *mask = nullptr, double pad_value = NaN, double nan_value = NaN)
190 {
191   vtkPolyData *surface = vtkPolyData::SafeDownCast(target);
192   if (surface == nullptr) {
193     Throw(ERR_InvalidArgument, __FUNCTION__, "Cannot compute distance along normal for non-polygonal point set!");
194   }
195 
196   vtkNew<vtkPolyDataNormals> calc_normals;
197   calc_normals->ComputeCellNormalsOff();
198   calc_normals->ComputePointNormalsOn();
199   calc_normals->AutoOrientNormalsOn();
200   calc_normals->SplittingOff();
201   SetVTKInput(calc_normals, surface);
202   calc_normals->Update();
203 
204   vtkSmartPointer<vtkDataArray> normals;
205   normals = calc_normals->GetOutput()->GetPointData()->GetNormals();
206 
207   int    subId;
208   double a[3], b[3], n[3], x[3], t, pcoords[3], dist;
209 
210   const double max_dist = 1000.0;
211 
212   vtkNew<vtkModifiedBSPTree> locator;
213   locator->SetDataSet(source);
214   locator->BuildLocator();
215 
216   for (vtkIdType ptId = 0; ptId < target->GetNumberOfPoints(); ++ptId) {
217     if (!mask || mask->GetComponent(ptId, 0) != 0) {
218       dist = inf;
219       target->GetPoint(ptId, a);
220       normals->GetTuple(ptId, n);
221       b[0] = a[0] + max_dist * n[0];
222       b[1] = a[1] + max_dist * n[1];
223       b[2] = a[2] + max_dist * n[2];
224       if (locator->IntersectWithLine(a, b, .0, t, x, pcoords, subId)) {
225         dist = sqrt(vtkMath::Distance2BetweenPoints(a, x));
226       }
227       b[0] = a[0] - max_dist * n[0];
228       b[1] = a[1] - max_dist * n[1];
229       b[2] = a[2] - max_dist * n[2];
230       if (locator->IntersectWithLine(a, b, .0, t, x, pcoords, subId)) {
231         dist = min(dist, sqrt(vtkMath::Distance2BetweenPoints(a, x)));
232       }
233       if (IsInf(dist)) dist = nan_value;
234     } else {
235       dist = pad_value;
236     }
237     distance->SetComponent(ptId, 0, dist);
238   }
239 }
240 
241 // =============================================================================
242 // Main
243 // =============================================================================
244 
245 // -----------------------------------------------------------------------------
main(int argc,char * argv[])246 int main(int argc, char *argv[])
247 {
248   // Parse arguments
249   REQUIRES_POSARGS(2);
250 
251   const char *target_name = POSARG(1);
252   const char *source_name = POSARG(2);
253   const char *output_name = nullptr;
254 
255   if (NUM_POSARGS == 3) {
256     output_name = POSARG(3);
257   } else if (NUM_POSARGS > 3) {
258     PrintHelp(EXECNAME);
259     exit(1);
260   }
261 
262   enum DistanceType
263   {
264     Default,
265     ClosestPoint,
266     ClosestCell,
267     AlongNormal,
268     CorrespondingPoint
269   };
270 
271   enum MaskOperation
272   {
273     DilateMask,
274     ErodeMask
275   };
276 
277   const char  *array_name     = "Distance";
278   const char  *separator      = nullptr;
279   const char  *table_name     = nullptr;
280   bool         table_header   = true;
281   bool         table_append   = false;
282   int          table_digits   = 5;
283   DistanceType dist_type      = Default;
284   bool         eval_hausdorff = false;
285   FileOption   fopt           = FO_Default;
286   double       nan_value      = NaN;
287   double       pad_value      = NaN;
288   const char  *mask_name      = nullptr;
289   Array<Pair<MaskOperation, int> > mask_ops;
290 
291   for (ALL_OPTIONS) {
292     if (OPTION("-dist-name") || OPTION("-array") || OPTION("-name")) {
293       array_name = ARGUMENT;
294     }
295     else if (OPTION("-mask-name")) {
296       mask_name = ARGUMENT;
297     }
298     else if (OPTION("-mask-erosion")) {
299       int n;
300       PARSE_ARGUMENT(n);
301       if (n > 0) {
302         mask_ops.push_back(MakePair(ErodeMask, n));
303       }
304     }
305     else if (OPTION("-mask-dilation")) {
306       int n;
307       PARSE_ARGUMENT(n);
308       if (n > 0) {
309         mask_ops.push_back(MakePair(DilateMask, n));
310       }
311     }
312     else if (OPTION("-undefined-value") || OPTION("-undef-value")) {
313       PARSE_ARGUMENT(nan_value);
314     }
315     else if (OPTION("-padding-value") || OPTION("-pad-value")) {
316       PARSE_ARGUMENT(pad_value);
317     }
318     else if (OPTION("-nan-value")) {
319       PARSE_ARGUMENT(nan_value);
320       pad_value = nan_value;
321     }
322     else if (OPTION("-point"))  dist_type = ClosestPoint;
323     else if (OPTION("-cell"))   dist_type = ClosestCell;
324     else if (OPTION("-normal")) dist_type = AlongNormal;
325     else if (OPTION("-index"))  dist_type = CorrespondingPoint;
326     else if (OPTION("-hausdorff")) eval_hausdorff = true;
327     else if (OPTION("-sep") || OPTION("-separator") || OPTION("-delimiter") || OPTION("-delim")) {
328       separator = ARGUMENT;
329       if (strcmp(separator, "\\t") == 0) separator = "\t";
330     }
331     else if (OPTION("-table")) {
332       if (HAS_ARGUMENT) {
333         table_name = ARGUMENT;
334       } else {
335         table_name = "stdout";
336       }
337     }
338     else if (OPTION("-digits") || OPTION("-precision")) {
339       PARSE_ARGUMENT(table_digits);
340     }
341     else HANDLE_BOOLEAN_OPTION("header", table_header);
342     else HANDLE_BOOLEAN_OPTION("append", table_append);
343     else HANDLE_POINTSETIO_OPTION(fopt);
344     else HANDLE_COMMON_OR_UNKNOWN_OPTION();
345   }
346   if (!mask_name && !mask_ops.empty()) {
347     Warning("Ignoring -mask-* options because no -mask-name is specified.");
348   }
349   if (table_name) {
350     const auto table_lname = ToLower(table_name);
351     if (table_lname == "" || table_lname == "cout" || table_lname == "stdout" || table_lname == "print") {
352       table_name = "stdout";
353     } else if (!separator) {
354       const auto ext = Extension(table_lname, EXT_Last);
355       if      (ext == ".csv") separator = ",";
356       else if (ext == ".tsv") separator = "\t";
357       else                    separator = " ";
358     }
359   }
360   if (!separator) separator = "\t";
361 
362   // read input point sets
363   FileOption source_fopt;
364   vtkSmartPointer<vtkPointSet> target, source;
365   target = ReadPointSet(target_name);
366   source = ReadPointSet(source_name, source_fopt);
367   if (fopt == FO_Default) fopt = source_fopt;
368 
369   if (dist_type == Default) {
370     if (target->GetNumberOfCells() > 0) dist_type = ClosestCell;
371     else                                dist_type = ClosestPoint;
372   }
373   if (dist_type == ClosestCell) {
374     if (source->GetNumberOfCells() == 0) {
375       FatalError("Cannot evaluate -cell distance when source has no cells");
376     }
377     if (eval_hausdorff && target->GetNumberOfCells() == 0) {
378       FatalError("Cannot evaluate -cell -hausdorff distance when target has no cells");
379     }
380   }
381   if (eval_hausdorff && dist_type != ClosestPoint && dist_type != ClosestCell) {
382     FatalError("Hausdorff distance currenly only implemented for -point or -cell distance");
383   }
384 
385   // check mask array
386   vtkSmartPointer<vtkDataArray> mask;
387   if (mask_name) {
388     mask = GetArrayByCaseInsensitiveName(target->GetPointData(), mask_name);
389     if (!mask) {
390       FatalError("Input target point set does not have a point data array named '" << mask_name << "'");
391     }
392     if (!mask_ops.empty()) {
393       vtkPolyData *surface = vtkPolyData::SafeDownCast(target);
394       if (!surface) {
395         FatalError("Morphological mask operations only supported for surfaces");
396       }
397       SharedPtr<EdgeTable> edgeTable(new EdgeTable(surface));
398       SharedPtr<EdgeConnectivity> neighbors(new EdgeConnectivity(surface, 1, edgeTable.get()));
399       for (const auto op : mask_ops) {
400         if (op.second > 0) {
401           if (op.first == DilateMask) {
402             DilatePointData dilate;
403             dilate.Input(surface);
404             dilate.InputData(mask);
405             dilate.EdgeTable(edgeTable);
406             dilate.Neighbors(neighbors);
407             dilate.Iterations(op.second);
408             dilate.Run();
409             mask = dilate.OutputData();
410           } else if (op.first == ErodeMask) {
411             ErodePointData erode;
412             erode.Input(surface);
413             erode.InputData(mask);
414             erode.EdgeTable(edgeTable);
415             erode.Neighbors(neighbors);
416             erode.Iterations(op.second);
417             erode.Run();
418             mask = erode.OutputData();
419           } else {
420             Throw(ERR_LogicError, EXECNAME, "Unknown mask operation: ", op.first);
421           }
422         }
423       }
424     }
425   }
426 
427   // find closest source points
428   int       subId;
429   vtkIdType otherPtId, cellId;
430   double    a[3], b[3], dist;
431 
432   vtkSmartPointer<vtkFloatArray> distance;
433   distance = vtkSmartPointer<vtkFloatArray>::New();
434   distance->SetName(array_name);
435   distance->SetNumberOfComponents(1);
436   distance->SetNumberOfTuples(target->GetNumberOfPoints());
437   target->GetPointData()->AddArray(distance);
438 
439   switch (dist_type) {
440     case Default: // never the case
441     case ClosestPoint:       EvaluateClosestPointDistance      (target, source, distance, mask, pad_value, nan_value); break;
442     case ClosestCell:        EvaluateClosestCellDistance       (target, source, distance, mask, pad_value, nan_value); break;
443     case AlongNormal:        EvaluateNormalDistance            (target, source, distance, mask, pad_value, nan_value); break;
444     case CorrespondingPoint: EvaluateCorrespondingPointDistance(target, source, distance, mask, pad_value, nan_value); break;
445   }
446 
447   // calculate min/max and average distance
448   Array<double> dists;
449   dists.reserve(target->GetNumberOfPoints());
450   for (vtkIdType ptId = 0; ptId < target->GetNumberOfPoints(); ++ptId) {
451     if (!mask || mask->GetComponent(ptId, 0) != 0.) {
452       dist = distance->GetComponent(ptId, 0);
453       if (!IsNaN(dist) && !IsInf(dist)) {
454         dists.push_back(dist);
455       }
456     }
457   }
458   if (dists.empty()) {
459     Warning("No. of valid target point distance measures = 0");
460   }
461   const auto range = MinMaxElement(dists);
462 
463   // calculate Hausdorff distance
464   double hausdorff_dist = range.second;
465   if (eval_hausdorff) {
466     if (dist_type == ClosestCell) {
467 
468       vtkNew<vtkCellLocator> locator;
469       locator->SetDataSet(target);
470       locator->BuildLocator();
471       vtkNew<vtkIdList> ptIds;
472 
473       for (vtkIdType ptId = 0; ptId < target->GetNumberOfPoints(); ++ptId) {
474         source->GetPoint(ptId, a);
475         locator->FindClosestPoint(a, b, cellId, subId, dist);
476         dist = sqrt(dist);
477         if (mask) {
478           target->GetCellPoints(cellId, ptIds.GetPointer());
479           for (vtkIdType i = 0; i < ptIds->GetNumberOfIds(); ++i) {
480             if (mask->GetComponent(ptIds->GetId(i), 0) != 0.) {
481               if (dist > hausdorff_dist) hausdorff_dist = dist;
482               break;
483             }
484           }
485         } else {
486           if (dist > hausdorff_dist) hausdorff_dist = dist;
487         }
488       }
489 
490     } else {
491 
492       vtkNew<vtkKdTreePointLocator> locator;
493       locator->SetDataSet(target);
494       locator->BuildLocator();
495 
496       for (vtkIdType ptId = 0; ptId < target->GetNumberOfPoints(); ++ptId) {
497         source->GetPoint(ptId, a);
498         otherPtId = locator->FindClosestPoint(a);
499         if (!mask || mask->GetComponent(otherPtId, 0) != 0.) {
500           source->GetPoint(otherPtId, b);
501           dist = sqrt(vtkMath::Distance2BetweenPoints(a, b));
502           if (dist > hausdorff_dist) hausdorff_dist = dist;
503         }
504       }
505 
506     }
507   }
508 
509   // write output
510   if (output_name) {
511     if (!WritePointSet(output_name, target, fopt)) {
512       FatalError("Failed to write output point set to " << output_name);
513     }
514   }
515 
516   // print distances and summary
517   if (verbose > 1) {
518     cout << "ID" << separator << array_name << "\n";
519     for (vtkIdType ptId = 0; ptId < target->GetNumberOfPoints(); ++ptId) {
520       cout << (ptId + 1) << separator << distance->GetComponent(ptId, 0) << "\n";
521     }
522     cout << "\n";
523   }
524   if (verbose || !output_name || table_name) {
525     const auto mean = Accumulate(dists) / dists.size();
526     const auto median = MedianValue(dists);
527     if (table_name) {
528       ofstream ofs;
529       ostream *os = nullptr;
530       if (strcmp(table_name, "stdout") == 0) {
531         os = &cout;
532       } else {
533         if (table_append) {
534           ifstream ifs(table_name);
535           if (ifs.is_open()) {
536             table_header = false;
537             ifs.close();
538           } else {
539             table_append = false;
540           }
541         }
542         ofs.open(table_name, table_append ? ostream::app : ostream::out);
543         if (!ofs.is_open()) {
544           FatalError("Failed to create/open output table file: " << table_name);
545         }
546         os = &ofs;
547       }
548       if (table_header) {
549         (*os) << "min" << separator << "max" << separator << "mean" << separator << "median";
550         if (eval_hausdorff) {
551           (*os) << separator << "hausdorff";
552         }
553         (*os) << "\n";
554       }
555       fixed(*os);
556       os->precision(table_digits);
557       (*os) << range.first << separator << range.second << separator << mean << separator << median;
558       if (eval_hausdorff) {
559         (*os) << separator << hausdorff_dist;
560       }
561       (*os) << "\n";
562       ofs.close();
563     }
564     if (!table_name && (verbose || !output_name)) {
565       fixed(cout);
566       cout.precision(table_digits);
567       cout << "Minimum distance   = " << range.first << "\n";
568       cout << "Maximum distance   = " << range.second << "\n";
569       cout << "Average distance   = " << mean << "\n";
570       cout << "Median distance    = " << median << "\n";
571     }
572   }
573   if (!table_name && eval_hausdorff) {
574     cout << "Hausdorff distance = " << hausdorff_dist << "\n";
575   }
576 
577   return 0;
578 }
579