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