Take the 2-minute tour ×
Mathematica Stack Exchange is a question and answer site for users of Mathematica. It's 100% free, no registration required.

I currently work on an image segmentation scenario to detect cell nuclei in 3D using Mathematica. The data I have are image stacks in the TIFF file format. I would like to keep the sequence of image processing steps as small and simple as possible, since I expect the segmentation pipeline to work for several image stacks. I uploaded an example image stack containing several cell nuclei objects. Here is a screenshot of the image stack after creating an Image3D object out of it.

smallCropped = Image3D[Import["...\\smallExample.tif"], 
Axes -> True, AxesLabel -> {"X", "Y", "Z"}]

raw image stack as Image3D object

The image shows about 40-50 cell nuclei objects. Note that due to a different spacings in the z-dimension the objects appear flattened. However, this should not have any influence on the segmentation itself. If you have a look at the data you will find out that the signal to noise ratio (SNR) is very high. The segmentation of the cell nuclei objects should therefore be feasible. Here is a GIF animation of the image stack moving through the stack in the z-dimension:

enter image description here

I applied what seems to me as the most straight forward approach to segmentation in Mathematica (Binarize and MorphologicalComponents):

Show[#, ImageSize -> 512] & /@ {binary = Binarize@smallCropped, 
  Image3D[Colorize@MorphologicalComponents@binary]}

enter image description here

So far so good. As you can see from the colorized components matrix on the left, several cell nuclei objects are assigned the same object label (e.g. the large pink object). Hence, preprocessing steps and a more sophisticated segmentation procedure seem to be required.

After going through many types of image filtering and segmentation in Mathematica I got lost at some point and ended up plugging together various different filtering and segmentation steps. In most of the cases, the more I tried the worse the results got ;) I know that there is no golden rule for segmentation in image processing but the task of correctly segmenting the objects in this scenario seems to me not that difficult and I wonder if there exists a nice and easy way.

The WatershedComponents method tries to separate connected objects. Since this method currently only works in 2D in Mathematica I implemented a method that first segments all the images of the stack slice by slice using a combination of GradientFilter, WatershedComponents, Binarize, MaxDetect and DistanceTransform. I then tried to merge the results from this 2D segmentation to end up with a 3D components matrix. I didn't put the code here but you can see the output of these two steps below (left: 2D WatershedComponents result, right: 3D result after merging).

compsMatrix = 
  SelectComponents[
     WatershedComponents[GradientFilter[#, 1], 
      MaxDetect[DistanceTransform@Binarize@ImageAdjust@#], 
      Method -> {"MinimumSaliency", .3}], "Count", 
     10 < # < 2000 &] & /@ Image3DSlices[Binarize@smallCropped]

enter image description here

This is where I am at so far. The segmentation accuracy and object separation still looks poor to me and I am curious what steps might improve the results. My questions are:

1. What preprocessing steps (e.g. filters) built in Mathematica might be suitable to increase the segmentation accuracy in this case? Is there a combination of standard filters that might work here?

2. How could I improve the separation of objects in 3D?

3. What about WatershedComponents for Image3D objects?

Edit: response comment on Erosion A suggestion in the comments was to use Erosion. I know that erosion helps to separate objects.however, in this scenario it did not help and I would lose much information about the objects.

Edit: response to answer by UDB

Thanks to the nice approach proposed by UDB I could go on with the 3D segmentation problem and try the approach with my data. At first the results looked very promising but when taking a closer look, I have to say that the solution proposed does not provide a satisfying result. I have checked the results for the test image stack provided above I then used the functions proposed by UDB:

distcompiled = 
  Compile[{{dist, _Integer, 3}}, 
   Module[{dimi, dimj, dimk, disttab, i, j, k, 
     ii}, {dimi, dimj, dimk} = Dimensions[dist];
    disttab = Table[(i - ii)^2, {ii, dimi}, {i, dimi}];
    Table[
     Min@Table[disttab[[i, ii]] + dist[[ii, j, k]], {ii, dimi}], {i, 
      dimi}, {j, dimj}, {k, dimk}]], CompilationTarget -> "C"];

Options[EuclideanDistanceTransform3D] = {Padding -> 1};
EuclideanDistanceTransform3D[im3d_Image3D, OptionsPattern[]] := 
  Module[{dist3d, i, j, k, ii, dimi, dimj, dimk}, 
   dist3d = 
    Developer`ToPackedArray@
     ParallelMap[
      Round[ImageData[
          DistanceTransform[#, DistanceFunction -> EuclideanDistance, 
           Padding -> OptionValue[Padding]]]^2] &, 
      Image3DSlices[
       Image3D[ArrayPad[ImageData[im3d, "Bit"], 1, 
         Padding -> OptionValue[Padding]]]]];
   If[OptionValue[Padding] != 0, 
    dist3d = 
      Developer`ToPackedArray@
       ParallelMap[
        If[Times @@ Dimensions[#] == Total[Flatten[#]], # + 
           1073741822, #] &, dist3d, {1}];];
   dist3d = Developer`ToPackedArray@distcompiled[dist3d];
   dist3d = ArrayPad[dist3d, -1];
   Image3D[Sqrt@dist3d, "Real"]];

As you can see, I changed the CompilationTarget to "C" and added a missing comma after the local variable disttab in the distcompiled function. I then packed the function together and gave it a name:

splitSegmentation3D[img3d_Image3D] := 
 Function[{image}, 
   Image3D[Colorize[
     MorphologicalComponents[
      ImageMultiply[
       Binarize[
        LaplacianGaussianFilter[
         EuclideanDistanceTransform3D[
          ColorNegate[
           Binarize[
            ImageMultiply[MaxDetect[#], #] &[
             EuclideanDistanceTransform3D[image]], 5]]], 1]], 
       image]]]]][img3d]

As proposed I set the threshold for accepted maxima to 5 in this function. For binarization I use the proposed TotalVariationFilter as pre-processing step and then do a simple Binarize. I then do the last of the pipeline using the function splitSegmentation3D:

{smallCropped = Image3D[Import["...\\smallExample.tif"],
prepro = TotalVariationFilter[smallCropped],
bin = Binarize[prepro],
comps = splitSegmentation3D[Binarize[prepro]]}

enter image description here

The components image looks just like the components image in the answer except for the colors...good. But when taking a closer look at the segmentation by slicing through the images in z-dimension, you see the details. I put the results of the complete image processing chain next to each other (from left to right: original, preprocessed, binary, components image):

enter image description here

I see multiple problems with this approach. First, using TotalVariationFilter as preprocessing step also blurs the image which results in many connected objects. Second, the splitting performs heavy over segmentation resulting in splitted objects. Third, many objects seem to be rounded by the segmentation. I think this results from the EuclideanDistanceTransform3D in combination with ImageMultiply.

I don't think that a perfect segmentation is possible on my data, but it should work out for at least some of the objects.

share|improve this question
    
I get the feeling you really want to compensate for the flattened dimension. Once this is taken into account, you might use Erosion to "shrink" cells before MorphologicalComponents, and then somehow use the partitioning again on original data. –  kirma Sep 9 '13 at 20:04
    
I don't think that the z-dimension spacing plays a crucial role if I understand your comment right. It is just that the objects appear to be a little more flat. But this should not change the segmentation. About Erosion: I know about morphological operations, but as far as I can tell it doesn't help with the separation if the objects overlap a little more. –  g3kk0 Sep 9 '13 at 20:53
    
The lack of definition in the vertical scale prevents the use of simple erosion techniques in order to separate the nuclei without losing a lot of info. Are you able to get more vertical samples density? –  belisarius Sep 16 '13 at 13:09
    
@belisarius: No. There is currently no possibility to increase the sample density in z. It is due to the optical properties of the microscope the images are aquired with. I am thinking about using something like Watershed segmentation in 3D to cope with the problem. But since there is no 3D implementation in MMA I will have to implement it or use another software. –  g3kk0 Sep 16 '13 at 13:31
1  
You can correct for the flattening of the vertical range using BoxRatios. –  Sjoerd C. de Vries Sep 18 '13 at 21:42

2 Answers 2

up vote 35 down vote accepted
+100

Introduction

I propose a solution to this object separation problem, which simply was inspired by one of the application examples of DistanceTransform. It uses a simple method to split virtually connected (blurred) objects in 3D. A LoG filter (Marr-Hildreth operator) is detecting edges in the Euclidean distance transform by this doing a tesselation required to split the abovementioned blurred objects. Since the DistanceTransform function presently is lacking 3D functionality, an own implementation of Euclidean distance transform is provided.

Using this method you might end up with this label image:

Mathematica graphics

But one step after another. This is our test data set for the moment:

img3d = Image3D[
  ArrayPad[DiskMatrix[{3, 4, 5}], {{5, 10}, {20, 15}, {25, 30}}] + 
   ArrayPad[DiskMatrix[{5, 6, 3}], {{3, 8}, {18, 13}, {33, 26}}]]

Mathematica graphics

A self-made 3D distance transform

However, we still have to have a 3D distance transform. I will provide a poor man's solution of a 3D Euclidean distance transform, utilizing the built-in 2D DistanceTransform and adding some stuff according to algorithm 1 of the 1994 Pattern Recognition paper by Saito and Toriwaki entitled "New algorithm for Euclidean distance transformation of an n-dimensional digitized picture with applications", vol. 27, no. 11, pp. 1551-1565.

The following code provides an auxiliary function later on used to do the Euclidean distance measurements in the third dimension. It receives a 3D matrix dist consisting of a stack of 2D distance transformed images, whereas the squared Euclidean distances are assumed here. Along the stack direction, for each voxel it searches the minimum sum of the squared distance and the provided intermediate result:

distcompiled = Compile[{{dist, _Integer, 3}},
   Module[{dimi, dimj, dimk, disttab, i, j, k, ii},
    {dimi, dimj, dimk} = Dimensions[dist];
    disttab = Table[(i - ii)^2, {ii, dimi}, {i, dimi}];
    Table[
     Min[disttab[[i, All]] + dist[[All, j, k]]],
      {i, dimi}, {j, dimj}, {k, dimk}]
    ],
   CompilationTarget -> "WVM"
   (*this might be set to "C" if a compiler is installed*)
   ];

What still has to be defined is the function EuclideanDistanceTransform3D itself:

Options[EuclideanDistanceTransform3D] = {Padding -> 1};
EuclideanDistanceTransform3D[im3d_Image3D, OptionsPattern[]] := 
  Module[{dist3d, i, j, k, ii, dimi, dimj, dimk},

   dist3d = 
    Developer`ToPackedArray@
     Map[Round[
        ImageData[
          DistanceTransform[#, DistanceFunction -> EuclideanDistance, 
           Padding -> OptionValue[Padding]]]^2] &, 
      Image3DSlices[
       Image3D[ArrayPad[ImageData[im3d, "Bit"], 1, 
         Padding -> OptionValue[Padding]]]]];

   If[OptionValue[Padding] != 0,
    dist3d = 
      Developer`ToPackedArray@
       Map[If[Times @@ Dimensions[#] == Total[Flatten[#]], # + 
           1073741822, #] &, dist3d, {1}];
    ];

   dist3d = Developer`ToPackedArray@distcompiled[dist3d];

   dist3d = ArrayPad[dist3d, -1];

   Image3D[Sqrt@dist3d, "Real"]
   ];

This function assumes a binary Image3D handed over using the variable im3d.

The first part of the function does nothing but splitting the 3D stack into a sequence of 2D images and mapping them into the DistanceTransform function, wheras the result is re-squared. A one-voxel padding is applied using the Padding option provided.

The second part is circumventing the behavior of DistanceTransform when receiving images only consisting of 1's, in such a case it again returns an image purely filled with 1's, however, it should be full with Infinity entries, since there was not any 0. So we add $$2^{30}-2=1073741822$$ in such cases, by this ensuring that $$2^{31}-1$$ is never being exceeded as long as $$2^{15}=32768$$ is taken as largest extent in either of the volume dimensions: $$2^{30}-1+(2^{15})^2=2^{31}-1=2147483647$$.

In the third part we just use the auxiliary function already defined above.

In the fourth part the padding made in the beginning is being eliminated.

In the last part an Image3D is being assembled as return value, whereas for all elements of dist3d the square root is extracted, since we want to provide the Euclidean distances.

Now we have this EuclideanDistanceTransform3D.

A first test

Let us make a first test:

EuclideanDistanceTransform3D[img3d] // ImageAdjust

Mathematica graphics

Well, we are interested in those voxels indicating the local maxima:

MaxDetect[EuclideanDistanceTransform3D[img3d]]

Mathematica graphics

We might want to exclude some local maxima with not exceeding some certain distance values (3 in this example; this makes no difference to the result above for this test data set):

Binarize[ImageMultiply[MaxDetect[#], #] &[EuclideanDistanceTransform3D[img3d]], 3]

Mathematica graphics

Using these local maxima voxels, we apply another EuclideanDistanceTransform3D to measure the distances from them (we need to negate the maxima voxel intermediate result):

EuclideanDistanceTransform3D[
  ColorNegate[
   MaxDetect[EuclideanDistanceTransform3D[img3d]]]] // ImageAdjust

Mathematica graphics

What follows is the detection of 3D edges inside this second distance transform using an LoG filter:

LaplacianGaussianFilter[
  EuclideanDistanceTransform3D[
   ColorNegate[MaxDetect[EuclideanDistanceTransform3D[img3d]]]], 
  1] // ImageAdjust

Mathematica graphics

By binarizing the latter we will obtain a template to tesselate our data set, so objects might be split as intended. Here is the full image processing chain:

Function[{img3d},
  Image3D[
   Colorize[
    MorphologicalComponents[
     ImageMultiply[
      Binarize[
       LaplacianGaussianFilter[
        EuclideanDistanceTransform3D[
         ColorNegate[
          Binarize[
           ImageMultiply[
              MaxDetect[#], #
              ] &[EuclideanDistanceTransform3D[img3d]], 
           3(*allow only maxima with distance values > 3*)
           ]
          ]
         ], 1(*LoG filter kernel size*)
        ]
       ], img3d
      ]
     ]
    ]
   ]
  ][img3d]

Mathematica graphics

A second test

To apply this method to the smallCropped data set provided above, I recommend to do an edge-preserving smoothing before binarization:

img3d = Binarize[TotalVariationFilter[smallCropped]]

Mathematica graphics

What simply has to be done here is to apply the same image processing chain given above, whereas the minimum threshold for accepted maxima was set to 5 here instead of 3 (as for the synthetic example).

Mathematica graphics

Please compare this output with the original data set:

Mathematica graphics

We are done.

Problems I still see here may be mainly caused due to the non-convexity of a certain number of objects in this data set.

Parallelized version of Euclidean 3D distance transform

I am happy to show you a parallelized version of my self-made 3D distance transform. Again, we use a compiled auxiliary function to treat the 3rd dimension, but this time with two option set: RuntimeAttributes -> {Listable}, Parallelization -> True, so we later on will be able to hand over a series of 3D lists, and we have enabled the execution of these series in parallel:

distcompiledparallelized = Compile[{{dist, _Integer, 3}},
   Module[{dimi, dimj, dimk, disttab, i, j, k, ii},
    {dimi, dimj, dimk} = Dimensions[dist];
    disttab = Table[(i - ii)^2, {ii, dimi}, {i, dimi}];
    Table[Min[disttab[[i, All]] + dist[[All, j, k]]], {i, dimi}, {j, dimj}, {k, dimk}]],
   CompilationTarget -> "C",
   (*this should be set to "WVM" if a compiler is not installed*)
   RuntimeAttributes -> {Listable}, Parallelization -> True
   ];

Even though Wolfram virtual machine does a good job here, we have turned on C compilation.

Now we come to the function itself:

Options[EuclideanDistanceTransform3Dparallelized] = {Padding -> 1};
EuclideanDistanceTransform3Dparallelized[im3d_Image3D, 
   OptionsPattern[]] := 
  Module[{dist3d, forwardtransposition, backwardtransposition, 
    addmargin},

   backwardtransposition = Ordering[Reverse[ImageDimensions[im3d]]];
   forwardtransposition = Ordering[backwardtransposition];

   dist3d = 
    Developer`ToPackedArray@
     Map[Round[
        ImageData[
          DistanceTransform[#, DistanceFunction -> EuclideanDistance, 
           Padding -> OptionValue[Padding]]]^2] &, 
      Image3DSlices[
       Image3D[ArrayPad[
         Transpose[ImageData[im3d, "Bit"], forwardtransposition], 1, 
         Padding -> OptionValue[Padding]]]]];

   If[OptionValue[Padding] != 0,
    dist3d = 
      Developer`ToPackedArray@
       Map[If[Times @@ Dimensions[#] == Total[Flatten[#]], # + 
           1073741822, #] &, dist3d, {1}];
    ];

   addmargin = 
    Ceiling[#1/#2]*#2 - #1 &[Dimensions[dist3d][[3]], $ProcessorCount];

   dist3d = ArrayPad[dist3d, {{0, 0}, {0, 0}, {0, addmargin}}];

   dist3d = 
    Function[{len, procs}, 
      dist3d[[All, 
         All, # ;; Min[# + Ceiling[len/procs] - 1, len]]] & /@ 
       Range[1, len, Ceiling[len/procs]]][
     Last[Dimensions[dist3d]], $ProcessorCount];

   dist3d = Developer`ToPackedArray@distcompiledparallelized[dist3d];

   dist3d = Join[Sequence @@ dist3d, 3];

   dist3d = ArrayPad[dist3d, {{-1, -1}, {-1, -1}, {-1, -addmargin - 1}}];

   dist3d = Transpose[dist3d, backwardtransposition];

   Image3D[Sqrt@N@dist3d, "Real"]
   ];

First of all we make sure that the 3D data will be transposed in a way that the extent of the 1st coordinate is smaller than the 2nd and 3rd, by this generally minimizing the computational load of distcompiledparallelized called later on. To manage this, we determine appropriate lists forwardtransposition and backwardtransposition for the respective Transpose calls later on.

Then we do the processing of the series of 2D images as done in the initial version.

Once this was done, we do some preparations for the parallelization. We have to make sure that all 3D sublists we want to hand over to distcompiledparallelized have the same dimensions. However, depending on the im3d dimensions and the $ProcessorCount this requires some padding. We do this padding solely at the upper margin of the 3rd dimension.

Once we have done the padding, we continue with equally splitting dist3d depending on both its 3rd dimension and the $ProcessorCount, so distcompiledparallelized can be called.

What remains to be done is some maintenance: Re-joining the dist3d, removing the margin (together with the overall padding), and undoing the transposition.

The last very effective way for speeing up is to numericalize dist3d before drawing the square root ...

A further approach to the object separation problem

Looking on the smallCropped data set one might get the feeling that its height requires stretching in order to let the cells appear as spheres (anisotropy correction o f course will have some influence to our distance based approach), we may assume a factor of approx. 3:

smallCroppedinterpolationfunction = 
  ListInterpolation[ImageData[smallCropped, "Real"], Method -> "Spline"];
smallCroppedinterpolated = 
  ParallelTable[
   smallCroppedinterpolationfunction[i, j, k], {i, 1, 
    Dimensions[ImageData[smallCropped]][[1]], 0.33}, {j, 1, 
    Dimensions[ImageData[smallCropped]][[2]]}, {k, 1, 
    Dimensions[ImageData[smallCropped]][[3]]}];
smallCroppedstreched = Image3D[smallCroppedinterpolated, "Real"]

Mathematica graphics

Then we again should do a TV filtering, but for fluorescence data we better should assume a Poisson noise:

img3d = Binarize[
  TotalVariationFilter[smallCroppedstreched, Method -> "Poisson", MaxIterations -> 100]
]

Without option specification (or by setting Method -> "Cluster") Binarize takes "Otsu's method" doing some minimization of the intra-class variances.

The distance transformed img3d then looks like this:

EuclideanDistanceTransform3Dparallelized[img3d] // ImageAdjust

Mathematica graphics

Having detected the maxima inside this distance transform (and selecting those exceeding a certain minimum), we can look how both the distance field between these maxima and the tesselation look like:

EuclideanDistanceTransform3Dparallelized[ColorNegate[
   Binarize[
    ImageMultiply[MaxDetect[#], #] &[
     EuclideanDistanceTransform3Dparallelized[img3d]], 
    15]]] // ImageAdjust

Mathematica graphics

LaplacianGaussianFilter[
  EuclideanDistanceTransform3Dparallelized[ColorNegate[
    Binarize[
     ImageMultiply[MaxDetect[#], #] &[
      EuclideanDistanceTransform3Dparallelized[img3d]], 15]]], 
  3] // ImageAdjust

Mathematica graphics

So, here is the code doing all the steps:

Function[{im3d},
  Image3D[
   Colorize[
    MorphologicalComponents[
     ImageMultiply[
      Binarize[
       LaplacianGaussianFilter[(*Marr-Hildreth operator*)
        EuclideanDistanceTransform3Dparallelized[
         ColorNegate[
          Binarize[
           ImageMultiply[
              MaxDetect[#], #
              ] &
            [EuclideanDistanceTransform3Dparallelized[im3d]], 
           15(*allow only maxima with distance values > 15*)
           ]
          ]
         ], 3(*LoG filter kernel size*)
        ]
       ], im3d
      ]
     ]
    ]
   ]
  ][img3d]

Mathematica graphics

You may complain that small cells are missing, well, I did my best (mixed-sized cells in an object separation problem make things sophisticated), and you may play with the two parameters, i.e. the LoG filter kernel size and the minimum maxima threshold. I personally think that it should not be considered an artifact that almost all cells touching the data set margins have been canceled.

Hope you enjoy the parallelized Euclidean 3D distance transform. It now can easily handle large volumes (not exceeding 32768 for all dimensions). If there is anybody who is inspired to extend my self-made 3D distance transform approach to ManhattanDistance and ChessboardDistance, please feel free to do so...

Addition: Extended 3D Distance Transform

I am happy to introduce my extended parallelized solution DistanceTransform3D for Image3D data, in analogy to the kernel function DistanceFunction presently only processing Image data, it now includes these distances: EuclideanDistance, ManhattanDistance, and ChessboardDistance.

We utilize three different compiled auxiliary functions:

eucldistcompiledparallelized = Compile[{{dist, _Integer, 3}},
   Module[{dimi, dimj, dimk, disttab, i, j, k, ii},
    {dimi, dimj, dimk} = Dimensions[dist];
    disttab = Table[(i - ii)^2, {ii, dimi}, {i, dimi}];
    Table[
     Min[disttab[[i, All]] + dist[[All, j, k]]], {i, dimi}, {j, 
      dimj}, {k, dimk}]
    ],
   CompilationTarget -> "C", (*should be set to "WVM" if no compiler is installed*)
   RuntimeAttributes -> {Listable}, Parallelization -> True
   ];

manhdistcompiledparallelized = Compile[{{dist, _Integer, 3}},
   Module[{dimi, dimj, dimk, disttab, i, j, k, ii},
    {dimi, dimj, dimk} = Dimensions[dist];
    disttab = Table[Abs[(i - ii)], {ii, dimi}, {i, dimi}];
    Table[
     Min[disttab[[i, All]] + dist[[All, j, k]]], {i, dimi}, {j, 
      dimj}, {k, dimk}]
    ],
   CompilationTarget -> "C", (*should be set to "WVM" if no compiler is installed*)
   RuntimeAttributes -> {Listable}, Parallelization -> True
   ];

chessdistcompiledparallelized = Compile[{{dist, _Integer, 3}},
   Module[{dimi, dimj, dimk, disttab, i, j, k, ii},
    {dimi, dimj, dimk} = Dimensions[dist];
    disttab = Table[Abs[(i - ii)], {ii, dimi}, {i, dimi}];
    Table[
     Min@Map[Max[#] &, 
       Transpose[{disttab[[i, All]], dist[[All, j, k]]}]], {i, 
      dimi}, {j, dimj}, {k, dimk}]
    ],
   CompilationTarget -> "C", (*should be set to "WVM" if no compiler is installed*)
   RuntimeAttributes -> {Listable}, Parallelization -> True
   ];

The 3D distance transform reads now:

Options[DistanceTransform3D] = {Padding -> 1, DistanceFunction -> EuclideanDistance};
DistanceTransform3D[im3d_Image3D, thresh_Real: 0., OptionsPattern[]] :=
   Module[{dist3d, forwardtransposition, backwardtransposition, addmargin},

   backwardtransposition = Ordering[Reverse[ImageDimensions[im3d]]];
   forwardtransposition = Ordering[backwardtransposition];

   Switch[OptionValue[DistanceFunction],
    EuclideanDistance,
    dist3d = 
      Developer`ToPackedArray@
       Map[Round[
          ImageData[
            DistanceTransform[#, 
             DistanceFunction -> EuclideanDistance, 
             Padding -> OptionValue[Padding]]]^2] &, 
        Image3DSlices[
         Image3D[ArrayPad[
           Transpose[ImageData[Binarize[im3d, thresh], "Bit"], 
            forwardtransposition], 1, 
           Padding -> OptionValue[Padding]]]]];
    ,
    _,
    dist3d = 
      Developer`ToPackedArray@
       Map[ImageData[
          DistanceTransform[#, 
           DistanceFunction -> OptionValue[DistanceFunction], 
           Padding -> OptionValue[Padding]]] &, 
        Image3DSlices[
         Image3D[ArrayPad[
           Transpose[ImageData[Binarize[im3d, thresh], "Bit"], 
            forwardtransposition], 1, 
           Padding -> OptionValue[Padding]]]]];
    ];

   If[OptionValue[Padding] != 0,
    dist3d = 
      Developer`ToPackedArray@
       Map[If[Times @@ Dimensions[#] == Total[Flatten[#]], # + 
           1073741822, #] &, dist3d, {1}];
    ];

   addmargin = Ceiling[#1/#2]*#2 - #1 &[Dimensions[dist3d][[3]], $ProcessorCount];

   dist3d = ArrayPad[dist3d, {{0, 0}, {0, 0}, {0, addmargin}}];

   dist3d = 
    Developer`ToPackedArray@
     Function[{len, procs}, 
       dist3d[[All, 
          All, # ;; Min[# + Ceiling[len/procs] - 1, len]]] & /@ 
        Range[1, len, Ceiling[len/procs]]][
      Last[Dimensions[dist3d]], $ProcessorCount];

   Switch[OptionValue[DistanceFunction],
    EuclideanDistance,
    dist3d = Developer`ToPackedArray@eucldistcompiledparallelized[dist3d];
    ,
    ManhattanDistance,
    dist3d = Developer`ToPackedArray@manhdistcompiledparallelized[dist3d];
    ,
    ChessboardDistance,
    dist3d = Developer`ToPackedArray@chessdistcompiledparallelized[dist3d];
    ];

   dist3d = Join[Sequence @@ dist3d, 3];

   dist3d = ArrayPad[dist3d, {{-1, -1}, {-1, -1}, {-1, -addmargin - 1}}];

   dist3d = Transpose[dist3d, backwardtransposition];

   Switch[OptionValue[DistanceFunction],
    EuclideanDistance,
    Return[Image3D[Sqrt@N@dist3d, "Real"]];
    ,
    _,
    Return[Image3D[dist3d, "Bit16"]];
    ];
   ];

We generate a synthetic data set:

SeedRandom[2405];
img3d = Image3D[Table[If[RandomReal[] > N[1/1000], 1, 0], {10}, {20}, {15}]];

We do a short performance test:

eucldist3d = DistanceTransform3D[img3d, DistanceFunction -> EuclideanDistance];
manhdist3d = DistanceTransform3D[img3d, DistanceFunction -> ManhattanDistance];
chessdist3d = DistanceTransform3D[img3d, DistanceFunction -> ChessboardDistance];

Here we have the results:

ImageAdjust[ColorNegate[#]]& /@ {img3d, eucldist3d, manhdist3d, chessdist3d}

test data set Euclidean distance Manhattan distance Chessboard distance

Hope one day the same 3D functionality will be included in the built-in DistanceTransform.

share|improve this answer
10  
That's one hell of a first answer! +1 –  ssch Sep 25 '13 at 15:47
    
Perfect answer! Thanks very much! I can't wait to try it on my data ;) –  g3kk0 Sep 25 '13 at 16:10
    
The non-convexity of certain objects could indeed be a problem which might be fixed by merging some of the objects back together after they have been split by your proposed method. I will try the processing chain on my data and keep you informed. –  g3kk0 Sep 25 '13 at 16:57
    
There is a comma missing in the function distcompiled after disttab. –  g3kk0 Sep 26 '13 at 14:33
1  
Thanks, have corrected this missing comma, and also have improved the return value computation in distcompiled(could easily get rid of one of Table functions). –  UDB Sep 26 '13 at 22:00

In this approach the main idea is to pre-process the images rather than processing 3D Image. Lets look at some properties of the images.

im = Import["...\\smallExample.tif"]

(*Resize it for speeding up the process*)
img = ImageResize[#, 50] & /@ im;

Lets look at image 20 (others are similar) to see it's histogram. 0-0.05 shows us the Black background. The brightest color is about 0.5-0.55.

ImageHistogram[img[[20]], Frame -> False, Axes -> True, 
 PlotRange -> Automatic, Ticks -> {Range[0, .6, .05]}, 
 FrameTicks -> {Automatic, Automatic}]

Histogram of the image 20

The main idea towards the solution is to use the ColorFunction to pre-process the images. Blend linearly interpolates between colors. It can be used in 2D or 3D. In the sample images the dark grey tones means the objects below parts under the cross section planes and the brighter the parts closer to plane. (Side note binarizing early in the pre-processing, we loose a lot of information.) Ok, lets see what we can do with ColorFunction and Blend. We use the ranges defined in histogram. We can combine or use varying ranges best suits to our purpose.

(*Define the Blended Color Function*)
cf = (Blend[{{0.0, \!\(\*
 TagBox[ColorSetterBox[RGBColor[0., 0., 0.], "Swatch"], Setting]\)}, {0.05, \!\(\*
 TagBox[ColorSetterBox[RGBColor[0., 0., 0.], "Swatch"], Setting]\)}, {0.09, \!\(\*
 TagBox[ColorSetterBox[RGBColor[0.27450980392156865`, 0.6039215686274509, 0.22745098039215686`], "Swatch"], Setting]\)}, {0.27, \!\(\*
 TagBox[ColorSetterBox[RGBColor[0.5843137254901961, 0.30196078431372547`, 0.30196078431372547`], "Swatch"], Setting]\)}, {0.3, \!\(\*
 TagBox[ColorSetterBox[RGBColor[0.4117647058823529, 0.20392156862745098`, 0.20392156862745098`], "Swatch"], Setting]\)}, {0.4, \!\(\*
 TagBox[ColorSetterBox[RGBColor[0.5686274509803921, 0.5372549019607843, 0.], "Swatch"], Setting]\)}, {1., \!\(\*
 TagBox[ColorSetterBox[RGBColor[1., 1., 1.], "Swatch"], Setting]\)}}, #1] &);

(*Reconstruct the image with new colors*)
ii = ((Raster[ImageData[#], ColorFunction -> cf] // Image)) & /@ img

Blended images

From the images the we can see that green colors are in fact not on the cross section plane. The brownish and yellow colors are. By playing color ranges one can isolate the underneath part of the objects. With out knowing the actual shape of the objects it is hard to correct or preprocess further the images. From this point on one can use MeanShiftFilter, BilateralFilter or other relevant ones.

Corrected ranges of blended image

Corrected ranges of blended image

 (*Constructing 3d image*)
 im = Image3D[
  ii, "Byte",
  Background -> Black,
  BoxRatios -> {1, 1, 1},
  ImageSize -> 200,
  (*ColorFunction\[Rule]cf2,*)
  Method -> {"InterpolateValues" -> True,
  "SampleLayers" -> 200,
  "FixedSampleDirection" -> True}
 ]
(*Find the Components*)
Colorize@MorphologicalComponents@(Binarize[im, .32] )

The final image

Final image 3d

The results can be further improved by using other filters in 2d like FillingTransform etc.

share|improve this answer
    
Nice approach, thanks! I will try your approach on the data and keep you informed. –  g3kk0 Sep 30 '13 at 13:38

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.