From 05af3d3fc09e0e1839d02c6beff651f76a33e002 Mon Sep 17 00:00:00 2001
From: Alvaro Sanchez <alvaro.sanchez@kitware.com>
Date: Wed, 29 Mar 2017 15:30:04 -0400
Subject: [PATCH] Added test for transfer function 2D.

Split functionality in ComputeGradient::Dec into two functions, one
containing all the reusable parts between 1D and 2D transfer functions
(gradient computations) and one containing 1D specific calls.

Changed transfer function type to VTK_FLOAT.
---
 Rendering/Core/vtkVolumeProperty.cxx          |  11 +-
 Rendering/Core/vtkVolumeProperty.h            |   2 +-
 Rendering/Volume/Testing/Cxx/CMakeLists.txt   |   1 +
 .../Testing/Cxx/TestGPURayCastTransfer2D.cxx  | 164 ++++++++++++++++++
 .../Baseline/TestGPURayCastTransfer2D.png.md5 |   1 +
 .../VolumeOpenGL2/shaders/raycasterfs.glsl    |   6 +-
 .../vtkOpenGLGPUVolumeRayCastMapper.cxx       |  83 ++++++---
 .../vtkOpenGLTransferFunction2D.h             |   6 +-
 .../VolumeOpenGL2/vtkVolumeShaderComposer.h   |  46 +++--
 Testing/Data/tooth.nhdr.md5                   |   1 +
 Testing/Data/tooth.raw.md5                    |   1 +
 11 files changed, 269 insertions(+), 53 deletions(-)
 create mode 100644 Rendering/Volume/Testing/Cxx/TestGPURayCastTransfer2D.cxx
 create mode 100644 Rendering/Volume/Testing/Data/Baseline/TestGPURayCastTransfer2D.png.md5
 create mode 100644 Testing/Data/tooth.nhdr.md5
 create mode 100644 Testing/Data/tooth.raw.md5

diff --git a/Rendering/Core/vtkVolumeProperty.cxx b/Rendering/Core/vtkVolumeProperty.cxx
index 9473bc1155..153db73304 100644
--- a/Rendering/Core/vtkVolumeProperty.cxx
+++ b/Rendering/Core/vtkVolumeProperty.cxx
@@ -455,11 +455,14 @@ void vtkVolumeProperty::SetTransferFunction2D(int index, vtkImageData* function)
   if (this->TransferFunction2D[index] != function)
   {
     vtkDataArray* dataArr = function->GetPointData()->GetScalars();
-    if (dataArr->GetNumberOfComponents() != 4 ||
-      dataArr->GetArrayType() != VTK_UNSIGNED_CHAR)
+    if (!dataArr || dataArr->GetNumberOfComponents() != 4 ||
+      dataArr->GetDataType() != VTK_FLOAT)
     {
-      vtkErrorMacro(<< "Invalid type or number of components for a 2D Transfer"
-        " Function, VTK_UNSIGNED_CHAR 4 Components expected!");
+      const int type = dataArr->GetDataType();
+      const int comp = dataArr->GetNumberOfComponents();
+      vtkErrorMacro(<< "Invalid type (" << type << ") or number of components ("
+        << comp << ") for a 2D Transfer Function, VTK_FLOAT 4 Components"
+        " expected!");
       return;
     }
 
diff --git a/Rendering/Core/vtkVolumeProperty.h b/Rendering/Core/vtkVolumeProperty.h
index c0bf4ef521..297e6b633b 100644
--- a/Rendering/Core/vtkVolumeProperty.h
+++ b/Rendering/Core/vtkVolumeProperty.h
@@ -215,7 +215,7 @@ public:
   //@{
   /**
    * Set/Get a 2D transfer function. Volume mappers interpret the x-axis of
-   * of this transfer function scalar value and the y-axis as gradient
+   * of this transfer function as scalar value and the y-axis as gradient
    * magnitude. The value at (X, Y) corresponds to the color and opacity
    * for a salar value of X and a gradient magnitude of Y.
    */
diff --git a/Rendering/Volume/Testing/Cxx/CMakeLists.txt b/Rendering/Volume/Testing/Cxx/CMakeLists.txt
index e24c51332b..d2855a834e 100644
--- a/Rendering/Volume/Testing/Cxx/CMakeLists.txt
+++ b/Rendering/Volume/Testing/Cxx/CMakeLists.txt
@@ -88,6 +88,7 @@ set (VolumeOpenGL2CxxTests
   TestGPURayCastTextureStreaming.cxx
   TestGPURayCastThreeComponentsAdditive.cxx
   TestGPURayCastThreeComponentsIndependent.cxx
+  TestGPURayCastTransfer2D.cxx
   TestGPURayCastTwoComponentsDependent.cxx
   TestGPURayCastTwoComponentsDependentGradient.cxx
   TestGPURayCastTwoComponentsGradient.cxx
diff --git a/Rendering/Volume/Testing/Cxx/TestGPURayCastTransfer2D.cxx b/Rendering/Volume/Testing/Cxx/TestGPURayCastTransfer2D.cxx
new file mode 100644
index 0000000000..f736ccf856
--- /dev/null
+++ b/Rendering/Volume/Testing/Cxx/TestGPURayCastTransfer2D.cxx
@@ -0,0 +1,164 @@
+/*=========================================================================
+
+  Program:   Visualization Toolkit
+  Module:    TestGPURayCastTransfer2D.cxx
+
+  Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+  All rights reserved.
+  See Copyright.txt or http://www.kitware.com/Copyright.htm for details.
+
+     This software is distributed WITHOUT ANY WARRANTY; without even
+     the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+     PURPOSE.  See the above copyright notice for more information.
+
+=========================================================================*/
+/**
+ * Test 2D transfer function support in GPUVolumeRayCastMapper.  The transfer
+ * function is created manually using known value/gradient histogram information
+ * of the test data (tooth.hdr). A filter to create these histograms will be
+ * added in the future.
+ */
+
+#include "vtkCamera.h"
+#include "vtkColorTransferFunction.h"
+#include "vtkGPUVolumeRayCastMapper.h"
+#include "vtkImageData.h"
+#include "vtkInteractorStyleTrackballCamera.h"
+#include "vtkNew.h"
+#include "vtkNrrdReader.h"
+#include "vtkPiecewiseFunction.h"
+#include "vtkPointData.h"
+#include "vtkRegressionTestImage.h"
+#include "vtkRenderer.h"
+#include "vtkRenderWindow.h"
+#include "vtkRenderWindowInteractor.h"
+#include "vtkTestUtilities.h"
+#include "vtkFloatArray.h"
+#include "vtkVolume.h"
+#include "vtkVolumeProperty.h"
+
+
+typedef vtkSmartPointer<vtkImageData> Transfer2DPtr;
+Transfer2DPtr Create2DTransfer()
+{
+  int bins[2] = {256, 256};
+  Transfer2DPtr image = Transfer2DPtr::New();
+  image->SetDimensions(bins[0], bins[1], 1);
+  image->AllocateScalars(VTK_FLOAT, 4);
+  vtkFloatArray* arr = vtkFloatArray::SafeDownCast(
+    image->GetPointData()->GetScalars());
+
+  // Initialize to zero
+  void* dataPtr = arr->GetVoidPointer(0);
+  memset(dataPtr, 0, bins[0] * bins[1] * 4 * sizeof(float));
+
+  // Setting RGBA [1.0, 0,0, 0.05] for a square in the histogram (known)
+  // containing some of the interesting edges (e.g. tooth root).
+  for (int j = 0; j < bins[1]; j++)
+    for (int i = 0; i < bins[0]; i++)
+    {
+      if (i > 130 && i < 190 && j < 50)
+      {
+        double const jFactor = 256.0 / 50;
+
+        vtkIdType const index = bins[0] * j + i;
+        double const red = static_cast<double>(i) / bins[0];
+        double const green = jFactor * static_cast<double>(j) / bins[1];
+        double const blue = jFactor * static_cast<double>(j) / bins[1];
+        double const alpha = 0.25 * jFactor * static_cast<double>(j) / bins[0];
+
+        double color[4] = {red, green, blue, alpha};
+        arr->SetTuple(index, color);
+      }
+    }
+
+  return image;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int TestGPURayCastTransfer2D(int argc, char* argv[])
+{
+  cout << "CTEST_FULL_OUTPUT (Avoid ctest truncation of output)" << endl;
+
+  // Load data
+  char* fname = vtkTestUtilities::ExpandDataFileName(argc, argv,
+    "Data/tooth.nhdr");
+  vtkNew<vtkNrrdReader> reader;
+  reader->SetFileName(fname);
+  reader->Update();
+  delete[] fname;
+
+  vtkNew<vtkVolumeProperty> volumeProperty;
+  volumeProperty->ShadeOn();
+  volumeProperty->SetInterpolationType(VTK_LINEAR_INTERPOLATION);
+
+  vtkDataArray* arr = reader->GetOutput()->GetPointData()->GetScalars();
+  double range[2];
+  arr->GetRange(range);
+
+  // Prepare 1D Transfer Functions
+  vtkNew<vtkColorTransferFunction> ctf;
+  ctf->AddRGBPoint(0, 0.0, 0.0, 0.0);
+  ctf->AddRGBPoint(510, 0.4, 0.4, 1.0);
+  ctf->AddRGBPoint(640, 1.0, 1.0, 1.0);
+  ctf->AddRGBPoint(range[1], 0.9, 0.1, 0.1);
+
+  vtkNew<vtkPiecewiseFunction> pf;
+  pf->AddPoint(0, 0.00);
+  pf->AddPoint(510, 0.00);
+  pf->AddPoint(640, 0.5);
+  pf->AddPoint(range[1], 0.4);
+
+  vtkNew<vtkPiecewiseFunction> gf;
+  gf->AddPoint(0,   0.0);
+  gf->AddPoint(range[1] / 4.0, 1.0);
+
+  volumeProperty->SetScalarOpacity(pf.GetPointer());
+  volumeProperty->SetGradientOpacity(gf.GetPointer());
+  volumeProperty->SetColor(ctf.GetPointer());
+
+  // Prepare 2D Transfer Functions
+  Transfer2DPtr tf2d = Create2DTransfer();
+
+  volumeProperty->SetTransferFunction2D(tf2d);
+
+  // Setup rendering context
+  vtkNew<vtkRenderWindow> renWin;
+  renWin->SetSize(512, 512);
+  renWin->SetMultiSamples(0);
+
+  vtkNew<vtkRenderer> ren;
+  renWin->AddRenderer(ren.GetPointer());
+  ren->SetBackground(0.0, 0.0, 0.0);
+
+  vtkNew<vtkGPUVolumeRayCastMapper> mapper;
+  mapper->SetInputConnection(reader->GetOutputPort());
+  mapper->SetUseJittering(1);
+
+  vtkNew<vtkVolume> volume;
+  volume->SetMapper(mapper.GetPointer());
+  volume->SetProperty(volumeProperty.GetPointer());
+  ren->AddVolume(volume.GetPointer());
+
+  ren->ResetCamera();
+  ren->GetActiveCamera()->Elevation(-90.0);
+  ren->GetActiveCamera()->Zoom(1.4);
+
+  // Interactor
+  vtkNew<vtkRenderWindowInteractor> iren;
+  iren->SetRenderWindow(renWin.GetPointer());
+
+  vtkNew<vtkInteractorStyleTrackballCamera> style;
+  iren->SetInteractorStyle(style.GetPointer());
+
+  renWin->Render();
+
+  int retVal = vtkTesting::Test(argc, argv, renWin.GetPointer(), 90);
+  if (retVal == vtkRegressionTester::DO_INTERACTOR)
+    {
+    iren->Start();
+    }
+
+  return !((retVal == vtkTesting::PASSED) ||
+           (retVal == vtkTesting::DO_INTERACTOR));
+}
diff --git a/Rendering/Volume/Testing/Data/Baseline/TestGPURayCastTransfer2D.png.md5 b/Rendering/Volume/Testing/Data/Baseline/TestGPURayCastTransfer2D.png.md5
new file mode 100644
index 0000000000..ca65ba6dda
--- /dev/null
+++ b/Rendering/Volume/Testing/Data/Baseline/TestGPURayCastTransfer2D.png.md5
@@ -0,0 +1 @@
+528e99e377f471a6d00515fa2af93c82
diff --git a/Rendering/VolumeOpenGL2/shaders/raycasterfs.glsl b/Rendering/VolumeOpenGL2/shaders/raycasterfs.glsl
index 443eea67ec..068f763b95 100644
--- a/Rendering/VolumeOpenGL2/shaders/raycasterfs.glsl
+++ b/Rendering/VolumeOpenGL2/shaders/raycasterfs.glsl
@@ -66,13 +66,15 @@ uniform vec4 in_volume_bias;
 
 //VTK::CompositeMask::Dec
 
+//VTK::GradientCache::Dec
+
 //VTK::ComputeOpacity::Dec
 
 //VTK::ComputeGradient::Dec
 
-//VTK::ComputeLighting::Dec
+//VTK::ComputeGradientOpacity1D::Dec
 
-//VTK::PreComputeGradients::Dec
+//VTK::ComputeLighting::Dec
 
 //VTK::ComputeColor::Dec
 
diff --git a/Rendering/VolumeOpenGL2/vtkOpenGLGPUVolumeRayCastMapper.cxx b/Rendering/VolumeOpenGL2/vtkOpenGLGPUVolumeRayCastMapper.cxx
index 857660f713..52238b7976 100644
--- a/Rendering/VolumeOpenGL2/vtkOpenGLGPUVolumeRayCastMapper.cxx
+++ b/Rendering/VolumeOpenGL2/vtkOpenGLGPUVolumeRayCastMapper.cxx
@@ -1054,7 +1054,7 @@ int vtkOpenGLGPUVolumeRayCastMapper::vtkInternal::
   return 0;
 }
 
-//----------------------------------------------------------------------------
+//------------------------------------------------------------------------------
 void vtkOpenGLGPUVolumeRayCastMapper::vtkInternal::
   UpdateTransferFunction2D(vtkRenderer* ren, vtkVolume* vol,
   unsigned int component)
@@ -2898,9 +2898,15 @@ void vtkOpenGLGPUVolumeRayCastMapper::BuildShader(vtkRenderer* ren,
                            independentComponents),
     true);
 
-
   // Compute methods replacements
   //--------------------------------------------------------------------------
+  fragmentShader = vtkvolume::replace(
+    fragmentShader,
+    "//VTK::ComputeGradient::Dec",
+    vtkvolume::ComputeGradientDeclaration(vol, noOfComponents,
+                                       independentComponents),
+    true);
+
   switch(volumeProperty->GetTransferFunctionMode())
   {
     case vtkVolumeProperty::TF_1D:
@@ -2914,10 +2920,10 @@ void vtkOpenGLGPUVolumeRayCastMapper::BuildShader(vtkRenderer* ren,
 
       fragmentShader = vtkvolume::replace(
         fragmentShader,
-        "//VTK::ComputeGradient::Dec",
-        vtkvolume::ComputeGradientDeclaration(ren, this, vol, noOfComponents,
-                                              independentComponents,
-                                              this->Impl->GradientOpacityTablesMap),
+        "//VTK::ComputeGradientOpacity1D::Dec",
+        vtkvolume::ComputeGradientOpacity1DDecl(vol, noOfComponents,
+                                        independentComponents,
+                                        this->Impl->GradientOpacityTablesMap),
         true);
 
       fragmentShader = vtkvolume::replace(
@@ -2942,8 +2948,8 @@ void vtkOpenGLGPUVolumeRayCastMapper::BuildShader(vtkRenderer* ren,
         true);
 
       fragmentShader = vtkvolume::replace(fragmentShader,
-        "//VTK::PreComputeGradients::Dec",
-        vtkvolume::PreComputeGradientsDec(ren, this, vol, noOfComponents,
+        "//VTK::GradientCache::Dec",
+        vtkvolume::GradientCacheDec(ren, this, vol, noOfComponents,
           independentComponents),
         true);
 
@@ -3341,27 +3347,48 @@ void vtkOpenGLGPUVolumeRayCastMapper::GPURender(vtkRenderer* ren,
   this->ComputeReductionFactor(vol->GetAllocatedRenderTime());
   this->Impl->UpdateSamplingDistance(input, ren, vol);
 
-  // Update the transfer functions
-  if (independentComponents)
-  {
-    for (int i = 0; i < noOfComponents; ++i)
-    {
-      this->Impl->UpdateOpacityTransferFunction(ren, vol, i);
-      this->Impl->UpdateGradientOpacityTransferFunction(ren, vol, i);
-      this->Impl->UpdateColorTransferFunction(ren, vol, i);
-      this->Impl->UpdateTransferFunction2D(ren, vol, i);
-    }
-  }
-  else
+  /// TODO Move this into a function
+  int const transferMode = vol->GetProperty()->GetTransferFunctionMode();
+  switch(transferMode)
   {
-    if (noOfComponents == 2 || noOfComponents == 4)
-    {
-      this->Impl->UpdateOpacityTransferFunction(ren, vol, noOfComponents - 1);
-      this->Impl->UpdateGradientOpacityTransferFunction(ren, vol,
-                                                        noOfComponents - 1);
-      this->Impl->UpdateColorTransferFunction(ren, vol, 0);
-      this->Impl->UpdateTransferFunction2D(ren, vol, 0);
-    }
+    case vtkVolumeProperty::TF_1D:
+      if (independentComponents)
+      {
+        for (int i = 0; i < noOfComponents; ++i)
+        {
+          this->Impl->UpdateOpacityTransferFunction(ren, vol, i);
+          this->Impl->UpdateGradientOpacityTransferFunction(ren, vol, i);
+          this->Impl->UpdateColorTransferFunction(ren, vol, i);
+        }
+      }
+      else
+      {
+        if (noOfComponents == 2 || noOfComponents == 4)
+        {
+          this->Impl->UpdateOpacityTransferFunction(ren, vol, noOfComponents - 1);
+          this->Impl->UpdateGradientOpacityTransferFunction(ren, vol,
+                                                            noOfComponents - 1);
+          this->Impl->UpdateColorTransferFunction(ren, vol, 0);
+        }
+      }
+      break;
+
+    case vtkVolumeProperty::TF_2D:
+      if (independentComponents)
+      {
+        for (int i = 0; i < noOfComponents; ++i)
+        {
+          this->Impl->UpdateTransferFunction2D(ren, vol, i);
+        }
+      }
+      else
+      {
+        if (noOfComponents == 2 || noOfComponents == 4)
+        {
+          this->Impl->UpdateTransferFunction2D(ren, vol, 0);
+        }
+      }
+      break;
   }
 
   // Update noise sampler texture
diff --git a/Rendering/VolumeOpenGL2/vtkOpenGLTransferFunction2D.h b/Rendering/VolumeOpenGL2/vtkOpenGLTransferFunction2D.h
index 7b0f433424..fd22d15959 100644
--- a/Rendering/VolumeOpenGL2/vtkOpenGLTransferFunction2D.h
+++ b/Rendering/VolumeOpenGL2/vtkOpenGLTransferFunction2D.h
@@ -32,8 +32,8 @@
  *
  * Manages the texture fetched by the fragment shader when TransferFunction2D
  * mode is active. Update() assumes the vtkImageData instance used as source
- * is of type VTK_UNSIGNED_CHAR and has 4 components (this is checked in vtkImageData
- * when setting the function).
+ * is of type VTK_FLOAT and has 4 components (vtkVolumeProperty ensures this
+ * is the case when the function is set).
  *
  * \sa vtkVolumeProperty::SetTransferFunction2D
  */
@@ -98,7 +98,7 @@ public:
       this->TextureObject->SetWrapT(vtkTextureObject::ClampToEdge);
       this->TextureObject->SetMagnificationFilter(interpolation);
       this->TextureObject->SetMinificationFilter(interpolation);
-      this->TextureObject->Create2DFromRaw(width, height, 4, VTK_UNSIGNED_CHAR,
+      this->TextureObject->Create2DFromRaw(width, height, 4, VTK_FLOAT,
         data);
       this->LastInterpolation = interpolation;
       this->BuildTime.Modified();
diff --git a/Rendering/VolumeOpenGL2/vtkVolumeShaderComposer.h b/Rendering/VolumeOpenGL2/vtkVolumeShaderComposer.h
index 3dbd64ad1d..18fbf27762 100644
--- a/Rendering/VolumeOpenGL2/vtkVolumeShaderComposer.h
+++ b/Rendering/VolumeOpenGL2/vtkVolumeShaderComposer.h
@@ -424,13 +424,9 @@ namespace vtkvolume
   }
 
   //--------------------------------------------------------------------------
-  std::string ComputeGradientDeclaration(vtkRenderer* vtkNotUsed(ren),
-                                         vtkVolumeMapper* vtkNotUsed(mapper),
-                                         vtkVolume* vol,
-                                         int noOfComponents,
-                                         int independentComponents,
-                                         std::map<int, std::string>
-                                           gradientTableMap)
+  std::string ComputeGradientOpacity1DDecl(vtkVolume* vol,
+                                int noOfComponents, int independentComponents,
+                                std::map<int, std::string> gradientTableMap)
   {
     std::string shaderStr;
     if (vol->GetProperty()->HasGradientOpacity() &&
@@ -479,6 +475,14 @@ namespace vtkvolume
         \n  }");
     }
 
+    return shaderStr;
+  }
+
+  //--------------------------------------------------------------------------
+  std::string ComputeGradientDeclaration(vtkVolume* vol, int noOfComponents,
+                                        int independentComponents)
+  {
+    std::string shaderStr;
     if (vol->GetProperty()->GetShade() &&
         !vol->GetProperty()->HasGradientOpacity())
     {
@@ -592,10 +596,20 @@ namespace vtkvolume
           "  // Compute gradient function only once\n"
           "  vec4 gradient = computeGradient(component);\n");
           break;
-        case vtkVolumeProperty::TF_2D:  shaderStr += std::string(
+        case vtkVolumeProperty::TF_2D:
+          shaderStr += std::string(
           "  // TransferFunction2D is enabled so the gradient for\n"
-          "  // each component has already been cached\n"
-          "  vec4 gradient = g_gradients[component];\n");
+          "  // each component has already been cached\n");
+          if (independentComponents && noOfComponents > 1)
+          {
+            shaderStr +=
+          "  vec4 gradient = g_gradients[component];\n";
+          }
+          else
+          {
+            shaderStr +=
+          "  vec4 gradient = g_gradients;\n";
+          }
           break;
       }
     }
@@ -1001,7 +1015,7 @@ namespace vtkvolume
           "vec4 computeColor(vec4 scalar, float opacity)\n"
           "{\n"
           "  vec4 color = texture2D(" + colorTableMap[0]  + ",\n"
-          "    vec2(scalar.w, g_gradients.w));\n"
+          "    vec2(scalar.w, g_gradients.w));\n" /// TODO Scale all gradients with the actual texture size (?) or is this done already through the normalized magnitude to Range/4?
           "  return computeLighting(color, 0);\n"
           "}\n");
       }
@@ -1213,7 +1227,7 @@ namespace vtkvolume
   }
 
   //--------------------------------------------------------------------------
-  std::string PreComputeGradientsDec(vtkRenderer* vtkNotUsed(ren),
+  std::string GradientCacheDec(vtkRenderer* vtkNotUsed(ren),
                                     vtkVolumeMapper* mapper,
                                     vtkVolume* vtkNotUsed(vol),
                                     int noOfComponents,
@@ -1224,7 +1238,7 @@ namespace vtkvolume
     {
       if (noOfComponents == 1)
       {
-        shader = std::string("uniform vec4 g_gradients;");
+        shader = std::string("vec4 g_gradients;");
       }
       else
       {
@@ -1233,7 +1247,7 @@ namespace vtkvolume
         numss << noOfComponents;
         std::string const num = numss.str();
         shader = std::string(
-          "uniform vec4 g_gradients[" + num  + "];");
+          "vec4 g_gradients[" + num  + "];");
       }
     }
     else
@@ -1242,9 +1256,11 @@ namespace vtkvolume
       // use a look-up-table).
       if (noOfComponents == 2)
       {
-        shader = std::string("uniform vec4 g_gradients;");
+        shader = std::string("vec4 g_gradients;");
       }
     }
+
+    return shader;
   }
 
   //--------------------------------------------------------------------------
diff --git a/Testing/Data/tooth.nhdr.md5 b/Testing/Data/tooth.nhdr.md5
new file mode 100644
index 0000000000..06c44d1663
--- /dev/null
+++ b/Testing/Data/tooth.nhdr.md5
@@ -0,0 +1 @@
+7777ca90d9ee0ce9777fadf56d76fbae
diff --git a/Testing/Data/tooth.raw.md5 b/Testing/Data/tooth.raw.md5
new file mode 100644
index 0000000000..765db12750
--- /dev/null
+++ b/Testing/Data/tooth.raw.md5
@@ -0,0 +1 @@
+3cc1b1d8882ea6b61a4e36c19042a189
-- 
GitLab