1 /*
2 * Medical Image Registration ToolKit (MIRTK)
3 *
4 * Copyright 2013-2016 Imperial College London
5 * Copyright 2013-2016 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/IOConfig.h"
24 #include "mirtk/GenericImage.h"
25 #include "mirtk/Transformation.h"
26 #include "mirtk/RegisteredImage.h"
27 #include "mirtk/EnergyMeasure.h"
28 #include "mirtk/SimilarityMeasure.h"
29 #include "mirtk/ImageSimilarity.h"
30 #include "mirtk/HistogramImageSimilarity.h"
31 #include "mirtk/NearestNeighborInterpolateImageFunction.h"
32 #include "mirtk/HomogeneousTransformation.h"
33
34 using namespace mirtk;
35
36
37 // =============================================================================
38 // Help
39 // =============================================================================
40
41 // -----------------------------------------------------------------------------
PrintHelp(const char * name)42 void PrintHelp(const char *name)
43 {
44 cout << "\n";
45 cout << "Usage: " << name << " <target> <source>... [options]\n";
46 cout << "\n";
47 cout << "Description:\n";
48 cout << " Evaluates the (dis-)similarity of two intensity images.\n";
49 cout << "\n";
50 cout << " If more than one source image is given, the (dis-)similarity between\n";
51 cout << " each of these and the target is evaluated. By default, common image\n";
52 cout << " (dis-)similarity metrics are reported. One or more metrics for the\n";
53 cout << " evaluation can be chosen using :option:`-metric`.\n";
54 cout << "\n";
55 cout << " The input source image can either be pre-aligned with the target image\n";
56 cout << " or the output of a previous registration can be specified using\n";
57 cout << " :option:`-dofin`.\n";
58 cout << "\n";
59 cout << "Arguments:\n";
60 cout << " target Target image.\n";
61 cout << " source Source image.\n";
62 cout << "\n";
63 cout << "Optional arguments:\n";
64 cout << " -mask <file> Target image region of interest mask. (default: :option:`-Tp`)\n";
65 cout << " -dofin <file> Source image transformation. (default: Id)\n";
66 cout << " -interp <mode> Interpolation mode. (default: linear with padding)\n";
67 cout << " -metric <sim>... Image (dis-)similarity measure(s):\n";
68 cout << " ";
69 EnergyMeasure m = static_cast<EnergyMeasure>(SIM_Begin + 1);
70 while (m < SIM_End) {
71 if (m > SIM_Begin + 1) cout << ", ";
72 cout << ToString(m);
73 m = static_cast<EnergyMeasure>(m + 1);
74 }
75 cout << "\n";
76 cout << " -p, -padding <value> Common image background value/threshold. (default: NaN)\n";
77 cout << " -Tp <value> Target image background value/threshold. (default: NaN)\n";
78 cout << " -Sp <value> Source image background value/threshold. (default: NaN)\n";
79 cout << " -bins <int> Number of histogram bins. (default: min(dynamic range, 255))\n";
80 cout << " -Tbins <int> Number of target histogram bins. (default: min(dynamic range, 255))\n";
81 cout << " -Sbins <int> Number of source histogram bins. (default: min(dynamic range, 255))\n";
82 cout << " -parzen Smooth histogram samples using cubic B-spline Parzen window. (default: off)\n";
83 cout << " -Rx1 <int> Leftmost target voxel index along x axis.\n";
84 cout << " -Rx2 <int> Rightmost target voxel index along x axis.\n";
85 cout << " -Ry1 <int> Leftmost target voxel index along y axis.\n";
86 cout << " -Ry2 <int> Rightmost target voxel index along y axis.\n";
87 cout << " -Rz1 <int> Leftmost target voxel index along z axis.\n";
88 cout << " -Rz2 <int> Rightmost target voxel index along z axis.\n";
89 cout << "\n";
90 cout << "Output options:\n";
91 cout << " -precision <int> Number of significant digits. (default: 5)\n";
92 cout << " -delim <char> Delimiter for output of multiple metric values. (default: ,)\n";
93 cout << " -table Output in tabular format.\n";
94 cout << " -csv Output as comma separated values table.\n";
95 cout << " -tsv Output as tab separated values table.\n";
96 cout << " -noid Exclude image IDs from output. (default: off)\n";
97 cout << " -fullid Use complete input image file path as ID.\n";
98 cout << " (default: file name without image extension)\n";
99 PrintCommonOptions(cout);
100 cout << endl;
101 }
102
103 // =============================================================================
104 // Auxiliary functions
105 // =============================================================================
106
107 typedef RegisteredImage::DisplacementImageType DisplacementField;
108
109 // -----------------------------------------------------------------------------
DefaultNumberOfBins(const BaseImage *,double min_intensity,double max_intensity)110 int DefaultNumberOfBins(const BaseImage *, double min_intensity, double max_intensity)
111 {
112 int nbins = iround(max_intensity - min_intensity) + 1;
113 if (nbins > 256) nbins = 256;
114 return nbins;
115 }
116
117 // =============================================================================
118 // Main
119 // =============================================================================
120
121 // -----------------------------------------------------------------------------
main(int argc,char ** argv)122 int main(int argc, char **argv)
123 {
124 typedef HistogramImageSimilarity::JointHistogramType JointHistogram;
125 typedef RegisteredImage::VoxelType VoxelType;
126
127 // Check command line
128 REQUIRES_POSARGS(2);
129
130 // Parse positional arguments
131 const char *target_name = POSARG(1);
132 Array<const char *> source_name;
133 source_name.reserve(NUM_POSARGS-1);
134 source_name.push_back(POSARG(2));
135 for (OPTIONAL_POSARGS) {
136 source_name.push_back(POSARG(ARGIDX));
137 }
138
139 // Parse optional arguments
140 Array<SimilarityMeasure> metric;
141 InterpolationMode interp = Interpolation_LinearWithPadding;
142 const char *mask_name = nullptr;
143 const char *dofin_name = nullptr;
144 const char *table_name = nullptr;
145 bool invert = false;
146 double target_padding = NaN;
147 double source_padding = NaN;
148 int target_bins = 0;
149 int source_bins = 0;
150 bool parzen_window = false;
151 bool full_path_id = false;
152 bool image_id_col = true;
153 bool table_header = true;
154 int digits = 5;
155 string delim;
156
157 int i1 = 0, j1 = 0, k1 = 0;
158 int i2 = -1, j2 = -1, k2 = -1;
159
160 for (ALL_OPTIONS) {
161 if (OPTION("-dofin")) dofin_name = ARGUMENT;
162 else if (OPTION("-mask")) mask_name = ARGUMENT;
163 else if (OPTION("-interp")) PARSE_ARGUMENT(interp);
164 else if (OPTION("-Tp")) PARSE_ARGUMENT(target_padding);
165 else if (OPTION("-Sp")) PARSE_ARGUMENT(source_padding);
166 else if (OPTION("-padding") || OPTION("-p")) {
167 PARSE_ARGUMENT(target_padding);
168 source_padding = target_padding;
169 }
170 else if (OPTION("-tbins") || OPTION("-Tbins")) PARSE_ARGUMENT(target_bins);
171 else if (OPTION("-sbins") || OPTION("-Sbins")) PARSE_ARGUMENT(target_bins);
172 else if (OPTION("-bins")) {
173 PARSE_ARGUMENT(target_bins);
174 source_bins = target_bins;
175 }
176 else if (OPTION("-parzen") || OPTION("-parzen-window") || OPTION("-smooth-histogram")) {
177 parzen_window = true;
178 }
179 else if (OPTION("-metric") || OPTION("-measure")) {
180 do {
181 SimilarityMeasure m;
182 PARSE_ARGUMENT(m);
183 metric.push_back(m);
184 } while (HAS_ARGUMENT);
185 }
186 else if (OPTION("-precision")) PARSE_ARGUMENT(digits);
187 else if (OPTION("-delim")) {
188 delim = ARGUMENT;
189 size_t pos;
190 while ((pos = delim.find("\\n")) != string::npos) {
191 delim.replace(pos, 2, "\n");
192 }
193 while ((pos = delim.find("\\t")) != string::npos) {
194 delim.replace(pos, 2, "\t");
195 }
196 }
197 else if (OPTION("-table")) {
198 verbose = -1;
199 if (HAS_ARGUMENT) {
200 table_name = ARGUMENT;
201 }
202 }
203 else if (OPTION("-csv")) verbose = -1, delim = ',';
204 else if (OPTION("-tsv")) verbose = -1, delim = '\t';
205 else if (OPTION("-fullid")) full_path_id = true;
206 else if (OPTION("-Rx1")) PARSE_ARGUMENT(i1);
207 else if (OPTION("-Rx2")) PARSE_ARGUMENT(i2);
208 else if (OPTION("-Ry1")) PARSE_ARGUMENT(j1);
209 else if (OPTION("-Ry2")) PARSE_ARGUMENT(j2);
210 else if (OPTION("-Rz1")) PARSE_ARGUMENT(k1);
211 else if (OPTION("-Rz2")) PARSE_ARGUMENT(k2);
212 else HANDLE_BOOL_OPTION(invert);
213 else HANDLE_BOOLEAN_OPTION("id", image_id_col);
214 else HANDLE_BOOLEAN_OPTION("header", table_header);
215 else HANDLE_COMMON_OR_UNKNOWN_OPTION();
216 }
217
218 if (metric.empty()) {
219 metric.reserve(9);
220 metric.push_back(SIM_SSD);
221 metric.push_back(SIM_PSNR);
222 metric.push_back(SIM_CoVar);
223 metric.push_back(SIM_CC);
224 metric.push_back(SIM_JE);
225 metric.push_back(SIM_MI);
226 metric.push_back(SIM_NMI);
227 metric.push_back(SIM_CR_XY);
228 metric.push_back(SIM_CR_YX);
229 }
230
231 ofstream fout;
232 ostream *out = &cout;
233 if (table_name) {
234 verbose = -1;
235 fout.open(table_name, ios::out);
236 if (!fout) {
237 FatalError("Failed to open output file: " << table_name);
238 }
239 out = &fout;
240 }
241
242 int width = 22;
243 if (verbose > 0) {
244 delim = '\n';
245 for (size_t i = 0; i < metric.size(); ++i) {
246 width = max(width, static_cast<int>(ToPrettyString(metric[i]).length()));
247 }
248 } else if (delim.empty()) {
249 delim = ',';
250 if (verbose >= 0) delim += ' ';
251 }
252
253 // Initialize I/O module
254 InitializeIOLibrary();
255
256 // Input and (transformed) image instances
257 RegisteredImage::InputImageType target_image, source_image;
258 RegisteredImage target, source;
259
260 target.InputImage(&target_image);
261 source.InputImage(&source_image);
262 source.InterpolationMode(interp);
263
264 // Registered images must be updated each time because ImageSimilarity subclasses
265 // are allowed to change the intensities of the registered images. For example,
266 // the NormalizedIntensityCrossCorrelation normalizes the values to [0, 1].
267 target.SelfUpdate(true);
268 source.SelfUpdate(true);
269
270 // Read target image
271 if (verbose > 1) cout << "Reading target image from " << target_name << "...", cout.flush();
272 target_image.Read(target_name);
273 target_image.PutBackgroundValueAsDouble(target_padding, true);
274 if (target_image.T() > 1) {
275 if (verbose > 1) cout << " failed" << endl;
276 FatalError("Target image must be a 2D or 3D image!");
277 }
278 if (verbose > 1) cout << " done" << endl;
279
280 // Target region of interest
281 if (i2 < 0) i2 = target_image.X();
282 if (j2 < 0) j2 = target_image.Y();
283 if (k2 < 0) k2 = target_image.Z();
284
285 ImageAttributes target_region = target_image.Attributes();
286 target_region._x = i2 - i1;
287 target_region._y = j2 - j1;
288 target_region._z = k2 - k1;
289 target_region._xorigin = .0;
290 target_region._yorigin = .0;
291 target_region._zorigin = .0;
292
293 // Adjust origin
294 double x1 = i1, y1 = j1, z1 = k1;
295 target_image.ImageToWorld(x1, y1, z1);
296 double x2 = .0, y2 = .0, z2 = .0;
297 target_region.LatticeToWorld(x2, y2, z2);
298 target_region._xorigin = x1 - x2;
299 target_region._yorigin = y1 - y2;
300 target_region._zorigin = z1 - z2;
301
302 const int nvox = target_region.NumberOfPoints();
303
304 // Read input (and evaluate) source transformation
305 // ATTENTION: MUST be done **before** source.Initialize() is called!
306 UniquePtr<Transformation> dofin;
307 UniquePtr<DisplacementField> disp;
308 if (dofin_name) {
309 if (verbose > 1) cout << "Reading source transformation from " << dofin_name << "...", cout.flush();
310 dofin.reset(Transformation::New(dofin_name));
311 source.Transformation(dofin.get());
312 HomogeneousTransformation *lin;
313 lin = dynamic_cast<HomogeneousTransformation *>(dofin.get());
314 if (invert && lin) {
315 lin->Invert();
316 } else if (!lin && (invert || dofin->RequiresCachingOfDisplacements())) {
317 disp.reset(new DisplacementField(target_region, 3));
318 const double t = source.GetTOrigin();
319 const double t0 = target.GetTOrigin();
320 if (invert) {
321 dofin->InverseDisplacement(*disp, t, t0);
322 } else {
323 dofin->Displacement(*disp, t, t0);
324 }
325 source.ExternalDisplacement(disp.get());
326 }
327 if (verbose > 1) cout << " done" << endl;
328 }
329
330 // Initialize target image
331 if (verbose > 1) cout << "Initializing target image...", cout.flush();
332 target.Initialize(target_region);
333 if (IsNaN(target_padding)) {
334 target.ClearBackgroundValue();
335 } else {
336 target.PutBackgroundValueAsDouble(target_padding);
337 }
338 target.Recompute();
339 if (verbose > 1) cout << " done" << endl;
340
341 if (debug) {
342 target.Write("debug_target.nii.gz");
343 }
344
345 // Target region of interest and image overlap masks
346 BinaryImage mask(target_region), overlap(target_region);
347
348 if (mask_name) {
349 if (verbose > 1) cout << "Reading target mask from " << mask_name << "...", cout.flush();
350 BinaryImage input_mask(mask_name);
351 if (input_mask.T() > 1) {
352 FatalError("Mask image must be a 2D or 3D scalar image!");
353 }
354 if (input_mask.HasSpatialAttributesOf(&target)) {
355 mask.CopyFrom(input_mask);
356 } else {
357 GenericNearestNeighborInterpolateImageFunction<BinaryImage> nn;
358 nn.Input(&input_mask);
359 nn.Initialize();
360 double x, y, z;
361 for (int k = 0; k < target.Z(); ++k)
362 for (int j = 0; j < target.Y(); ++j)
363 for (int i = 0; i < target.X(); ++i) {
364 x = i, y = j, z = k;
365 target.ImageToWorld(x, y, z);
366 mask(i, j, k) = static_cast<BinaryPixel>(nn.Evaluate(x, y, z));
367 }
368 }
369 if (verbose > 1) cout << " done" << endl;
370 } else {
371 const VoxelType *tgt = target.Data();
372 if (IsNaN(target_padding)) {
373 for (int idx = 0; idx < nvox; ++idx, ++tgt) {
374 mask(idx) = (IsNaN(*tgt) ? 0 : 1);
375 }
376 } else {
377 for (int idx = 0; idx < nvox; ++idx, ++tgt) {
378 mask(idx) = (*tgt > target_padding ? 1 : 0);
379 }
380 }
381 }
382 int mask_size = 0;
383 for (int idx = 0; idx < nvox; ++idx) {
384 if (mask(idx) != 0) {
385 mask_size += 1;
386 }
387 }
388 if (verbose > 0 && mask_size == 0) {
389 cerr << "Warning: Target image domain is empty!";
390 if (mask_name) {
391 cerr << " Ensure mask image '" << mask_name << "' overlaps with target image domain.";
392 }
393 cerr << "\n";
394 }
395 target.PutMask(&mask);
396
397 // Target intensity range in region of interest
398 double tmin, tmax;
399 target_image.GetMinMaxAsDouble(tmin, tmax);
400 if (fequal(tmin, tmax)) {
401 FatalError("Target image has homogeneous intensity = " << tmin);
402 }
403
404 // Initialize similarity measures
405 if (verbose > 1) cout << "Initializing similarity measures...", cout.flush();
406 Array<UniquePtr<ImageSimilarity> > sim(metric.size());
407 bool use_shared_histogram = false;
408 JointHistogram samples;
409
410 for (size_t i = 0; i < metric.size(); ++i) {
411 sim[i].reset(ImageSimilarity::New(metric[i], ToString(metric[i]).c_str(), 1.0));
412 sim[i]->Target(&target);
413 sim[i]->Source(&source);
414 sim[i]->Mask(&overlap);
415 sim[i]->Domain(target_region);
416 sim[i]->DivideByInitialValue(false);
417 sim[i]->SkipTargetInitialization(true);
418 sim[i]->SkipSourceInitialization(true);
419 HistogramImageSimilarity *p;
420 p = dynamic_cast<HistogramImageSimilarity *>(sim[i].get());
421 if (p != nullptr) {
422 p->Samples(&samples);
423 p->UseParzenWindow(parzen_window);
424 use_shared_histogram = true;
425 }
426 }
427 if (verbose > 1) cout << " done" << endl;
428
429 // Print table header
430 const string target_id = (full_path_id ? target_name : FileName(target_name));
431
432 if (table_header && verbose < 0) {
433 if (image_id_col) {
434 *out << "Target" << delim << "Source" << delim;
435 }
436 for (size_t i = 0; i < metric.size(); ++i) {
437 if (i > 0) *out << delim;
438 *out << ToString(metric[i]);
439 }
440 *out << endl;
441 }
442
443 // Evaluate similarity of (transformed) source image(s)
444 for (size_t n = 0; n < source_name.size(); ++n) {
445
446 Indent indent;
447 if (verbose > 0 && n > 0) cout << "\n";
448
449 // Read source image
450 if (verbose > 1) {
451 cout << "Reading source image from " << source_name[n] << "...";
452 cout.flush();
453 }
454 source_image.Read(source_name[n]);
455 source_image.PutBackgroundValueAsDouble(source_padding, true);
456 if (verbose > 1) {
457 cout << " done\n";
458 cout << "Initializing source image...";
459 cout.flush();
460 }
461 source.Initialize(target_region);
462 if (IsNaN(source_padding)) {
463 source.ClearBackgroundValue();
464 } else {
465 source.PutBackgroundValueAsDouble(source_padding);
466 }
467 if (verbose > 1) {
468 cout << " done\n";
469 if (source.Transformation()) {
470 cout << "Transforming source image...";
471 }
472 cout.flush();
473 }
474 source.Recompute();
475 if (verbose > 1 && source.Transformation()) {
476 cout << " done\n" << endl;
477 }
478 if (debug) {
479 if (source_name.size() == 1) {
480 source.Write("debug_source.nii.gz");
481 } else {
482 source.Write(("debug_source_" + ToString(n + 1) + ".nii.gz").c_str());
483 }
484 }
485
486 // Print image IDs
487 const string source_id = (full_path_id ? source_name[n] : FileName(source_name[n]));
488
489 if (verbose > 0) {
490 cout << "Similarity of target image " << target_id << " and source image " << source_id << ":\n";
491 ++indent;
492 } else if (image_id_col) {
493 if (verbose == 0) cout << "Target = ";
494 *out << target_id << delim;
495 if (verbose == 0) cout << "Source = ";
496 *out << source_id;
497 }
498 cout.flush();
499
500 // Compute overlap mask
501 overlap = mask;
502 int overlap_size = 0;
503 for (int idx = 0; idx < nvox; ++idx) {
504 if (overlap(idx) != 0 && source.IsForeground(idx)) {
505 overlap_size += 1;
506 } else {
507 overlap(idx) = 0;
508 }
509 }
510 if (verbose > 0 && overlap_size == 0) {
511 cerr << "Warning: Source image '" << source_name[n] << "' has no overlap with the target image domain!\n";
512 }
513
514 // Fill joint histogram
515 if (use_shared_histogram) {
516 double smin, smax;
517 if (n > 0) target.Recompute();
518 source_image.GetMinMaxAsDouble(smin, smax);
519 if (fequal(smin, smax)) {
520 FatalError("Source image has homogeneous intensity = " << smin);
521 }
522 int tbins = target_bins, sbins = source_bins;
523 if (tbins <= 0) tbins = DefaultNumberOfBins(&target_image, tmin, tmax);
524 if (sbins <= 0) sbins = DefaultNumberOfBins(&source_image, smin, smax);
525 double twidth = (tmax - tmin) / tbins;
526 double swidth = (smax - smin) / sbins;
527 samples.Initialize(tmin, tmax, twidth, smin, smax, swidth);
528 const VoxelType *tgt = target.Data();
529 const VoxelType *src = source.Data();
530 for (int idx = 0; idx < nvox; ++idx, ++tgt, ++src) {
531 if (mask(idx)) {
532 samples.Add(samples.ValToBinX(*tgt), samples.ValToBinY(*src));
533 }
534 }
535 if (verbose > 0) {
536 cout.precision(digits);
537 #define Print(name, value) \
538 cout << delim << indent << ToString(name, width, ' ', true) << " = " << value
539 Print("No. of samples", samples.NumberOfSamples());
540 Print("No. of histogram bins", samples.NumberOfBinsX() << " x " << samples.NumberOfBinsY());
541 Print("Size of histogram bins", samples.WidthX() << " x " << samples.WidthY());
542 Print("Mean target intensity", samples.MeanX());
543 Print("Mean source intensity", samples.MeanY());
544 Print("Target image range", "[" << samples.MinX() << ", " << samples.MaxX() << "]");
545 Print("Source image range", "[" << samples.MinY() << ", " << samples.MaxY() << "]");
546 Print("Target image variance", samples.VarianceX());
547 Print("Source image variance", samples.VarianceY());
548 Print("Target image entropy", samples.EntropyX());
549 Print("Source image entropy", samples.EntropyY());
550 #undef Print
551 cout.flush();
552 }
553 }
554
555 // Evaluate similarity measure(s)
556 for (size_t i = 0; i < sim.size(); ++i) {
557 if (image_id_col || i > 0) {
558 *out << delim;
559 }
560 if (verbose > 0) {
561 cout << indent << ToPrettyString(metric[i], width, ' ', true) << " = ";
562 } else if (verbose == 0) {
563 cout << sim[i]->Name() << " = ";
564 }
565 if (verbose > 0) cout.flush();
566 // Initialize similarity measure, possibly changes [min, max] range
567 sim[i]->Initialize();
568 sim[i]->Update();
569 // Evaluate image similarity
570 *out << setprecision(digits) << sim[i]->RawValue();
571 // Reset registered image properties which may be modified by similarity measures
572 target.MinIntensity(NaN);
573 target.MaxIntensity(NaN);
574 source.MinIntensity(NaN);
575 source.MaxIntensity(NaN);
576 if (IsNaN(target_padding)) {
577 target.ClearBackgroundValue();
578 } else {
579 target.PutBackgroundValueAsDouble(target_padding);
580 }
581 if (IsNaN(source_padding)) {
582 source.ClearBackgroundValue();
583 } else {
584 source.PutBackgroundValueAsDouble(source_padding);
585 }
586 if (verbose > 0) cout.flush();
587 }
588
589 *out << endl;
590 }
591
592 // Close output file
593 if (fout.is_open()) {
594 fout.close();
595 }
596 return 0;
597 }
598