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