Message Boards Message Boards

UNET image segmentation in stem cells research

POSTED BY: Ali Hashmi
11 Replies

Hi Ali, Martijn and Matteo

Thanks for your very valuable ideas and suggestions!

I have applied your ideas to segment human blast data and found it works excellently. Data can be found [here]. Images can easily be resized such that training works fine.

On the left side we see Human blast cells, in the middle we see the trained masks and on the right side the ground truth.

Segmentation of Images in Transmission Electron Microscopic Cell Recordings

If we want to train large data sets, I found out-of-core training by using a generating function very useful as it is described in the Wolfram Language & System Documentation Center

Many thanks!

POSTED BY: Wolfgang Hitzl

Hi Wolfgang,

I am glad it worked for you. It is very nice dataset btw. Cheers to you

POSTED BY: Ali Hashmi

Hi Wolfgang,

Looks great!!

It might be of interest to the community, i have transformed the notebook into proper paclet. With some demos and documentation. I have extended the networks with some other CONV blocks based on ResNet and others. All methods are fully generalized for 2D and 3D.

UNET

Enjoy!

POSTED BY: Martijn Froeling

Great work and very usefull!

I have generalized your network such that it can take any number of channels and classes. Your case handles one channel and one class. However i did not manage to make the single class output a binary image since the SoftmaxLayer and NetDecoder do not allow to classify one class and i did not find a suitable solution. The SoftmaxLayer with class labeling allows to give error rate feedback during the training.

pool := PoolingLayer[{2, 2}, 2];
upSamp[n_] := DeconvolutionLayer[n, {2, 2}, "Stride" -> {2, 2}]

conv[n_, p_: 0] := NetChain[{If[p == 1, pool, Nothing],
ConvolutionLayer[n, 3, "PaddingSize" -> {1, 1}], 
BatchNormalizationLayer[], Ramp,
ConvolutionLayer[n, 3, "PaddingSize" -> {1, 1}], 
BatchNormalizationLayer[], Ramp
}];

dec[n_] := NetGraph[{
"deconv" -> DeconvolutionLayer[n, {2, 2}, "Stride" -> {2, 2}],
"cat" -> CatenateLayer[],
"conv" -> conv[n]},
{NetPort["Input1"] -> "cat", 
NetPort["Input2"] -> "deconv" -> "cat" -> "conv"}
];

UNET2D[NChan_: 1, Nclass_: 1] := NetGraph[<|
"enc_1" -> conv[64], "enc_2" -> conv[128, 1], 
"enc_3" -> conv[256, 1], "enc_4" -> conv[512, 1], 
"enc_5" -> conv[1024, 1],
"dec_1" -> dec[512], "dec_2" -> dec[256], "dec_3" -> dec[128], 
"dec_4" -> dec[64],
"map" -> {ConvolutionLayer[Nclass, {1, 1}], LogisticSigmoid, 
If[Nclass > 1, TransposeLayer[{1 <-> 3, 1 <-> 2}], Nothing], 
If[Nclass > 1, SoftmaxLayer[], Nothing], 
If[Nclass > 1, Nothing, FlattenLayer[1]]}
|>,
{NetPort["Input"] -> 
"enc_1" -> "enc_2" -> "enc_3" -> "enc_4" -> "enc_5",
{"enc_4", "enc_5"} -> "dec_1", {"enc_3", "dec_1"} -> "dec_2",
{"enc_2", "dec_2"} -> "dec_3", {"enc_1", "dec_3"} -> "dec_4", 
"dec_4" -> "map"},
"Input" -> {NChan, 128, 128}, 
"Output" -> 
If[Nclass > 1, 
NetDecoder[{"Class", "Labels" -> Range[1, Nclass], 
"InputDepth" -> 3}], Automatic]
]

I also made the IuO function a bit faster for large data-sets and it reports the values per calss

ClassIOU[predi_, gti_, class_] := 
 Block[{posN, posP, fn, tp, fp, denom},
  (*find posisions as unitvector*)
  posN = Unitize[predi - class];
  posP = 1 - posN;

  (*get the values*)
  fn = Count[Pick[gti, posN, 1], class];
  tp = Count[Pick[gti, posP, 1], class];
  fp = Total[posP] - tp;

  (*if case does not exist in all data return 1*)
  denom = (tp + fp + fn);
  If[denom == 0, 1., N[tp/denom]]
  ]

IOU[pred_, gt_, nClasses_] := Block[{predf, gtf},
  predf = Flatten[pred];
  gtf = Flatten[gt];
  Table[ClassIOU[predf, gtf, c], {c, nClasses}]
  ]

To split the available data into training, validation and testing data I made this function.

SplitTestData[data_, label_, ratio_: {0.7, .2, .1}] := 
 Block[{allData, train, valid, test, testData, testLabel, s1, s2, s3},
  (*Randomize data*)
  allData = RandomSample[Thread[data -> label]];

  (*split data*)      
  Print["Nuber of Samples in each set: ", Round[ratio Length[allData]]];
  {s1, s2, s3} = Accumulate@Round[ratio Length[allData]];

  (*make training validation and test data*)
  train = allData[[1 ;; s1]];
  valid = allData[[s1 + 1 ;; s2]];
  test = allData[[s2 + 1 ;;]];
  testData = test[[All, 1]];
  testLabel = test[[All, 2]];

  (*define the output*)
  {train, valid, testData, testLabel}
  ]

These are some generated test dataset on which i tested the network

  • single channel - single class

1-1

  • single channel - 2 class

1-2

  • single channel - multi class

1-n

  • multi channel - multi class

n-n

These were the results

  • single channel - single class

case1

  • single channel - 2 classes {1 -> Background, 2 -> segmentation}

case2

  • single channel - multi class {1 -> Background, 2...n -> segmentation}

case3

  • multi channel - multi class {1 -> Background, 2...n -> segmentation}

case4

And finally of course my real application to segment the heart wall from multi modal MRI contrasts. However id do have to admit that my manual annotations could be a bit better such that the network can train more accurately. (Red - Manual annotation, Blue - Trained net, Green - overlap of the two)

real data

Attachments:
POSTED BY: Martijn Froeling

Hi Martijn,

Great stuff generalizing the network !! Could you kindly attach your notebook here with the sections (single class vs. multi-class) arranged separately. Many thanks !

POSTED BY: Ali Hashmi

I have attached the notebook. It also contains the code to generate the test images.

POSTED BY: Martijn Froeling

Great! As a developer in the Wolfram ML team, it's always gratifying to see people doing interesting stuff with what we provide.

There are a couple of comments i'd ike to make about this:

First, you evaluate the final performance using the pixelwise accuracy, but in semantic segmentation there is a more informative measure, namely mean intersection over union (IoU). Geometrically, that corresponds with measuring intersection / union ratio of the "blobs" corresponding to a fixed class in the prediction and ground truth masks, and then averaging those ratios for all classes. In formulas, for a given image:

$$IoU_{c} = \frac{TP_c}{TP_c + FP_c + FN_c}$$ $$IoU = Mean(IoU_c)$$

Where c is a class and TPc, FPc and FN_c are, respectively, the number of true positive, false positive and false negative predictions for class c. The true positives give you the measure of the blob intersections, while the sum gives you the union. A reasonable (but probably not the best) implementation of IoU might be:

classIOU[pred_, gt_, class_] := 
 Block[{positionP, positionN, tp, fp, fn},
  positionP = Position[pred, class];
  positionN = Delete[Range@Length[pred], positionP];
  positionP = Flatten[positionP];
  tp = Count[gt[[positionP]], class];
  fp = Length[positionP] - tp;
  fn = Count[gt[[positionN]], class];
  N[tp/(tp + fp + fn)]
  ]
IOU[pred_, gt_, nClasses_] := Mean@Table[classIOU[pred, gt, c], {c, nClasses}]

This assumes that your data is flattened and your classes are identified with integers starting from 1.

In general, IoU is preferable to pixel accuracy because it makes up for class imbalances in the masks by averaging class-wise accuracies. Suppose, in a 1-D example, that "1" is background and "2" is gastruloid, and your prediction and ground truth masks look like this:

pred = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
groundTruth = {1, 1, 1, 1, 2, 1, 1, 1, 1, 1} 

Pixel-level accuracy would be 90% here, but IoU gives you 45% (90% for background, 0% for gastruloid classes), because you completely failed to segment the gastruloid. Then, in you particular case, looks like you have a good balancing between background and object pixels in your data, so IoU shouldn't be far from pixelwise accuracy.

The second comment is just a technical one: when evaluating the accuracy you run the trained network in a Table, i.e. on each input separately. The framework also supports batch evaluation (or listable, if you want to say it à la WL). In this case, our neural network framework will figure out a suitable parallelization strategy and the computation will be much faster than a serial one. So you could, more efficiently, pre-compute net[data] outside the table and then compare it with the ground truths.

Again, congratulations for your work, the results look very good!

POSTED BY: Ali Hashmi

enter image description here - Congratulations! This post is now a Staff Pick as distinguished by a badge on your profile! Thank you, keep it coming!

POSTED BY: Moderation Team
Posted 6 years ago

Fantastic! What gave you the idea of using UNET for this? Did you try some of the networks on the neural net repository as well? I doubt you'd get meaningful increases in accuracy, I'm just curious!

POSTED BY: Carl Lange

Thanks a lot Carl. Actually I did not know if there are nets available in the repository for segmentation. The repository is very cool btw !

POSTED BY: Ali Hashmi
Reply to this discussion
Community posts can be styled and formatted using the Markdown syntax.
Reply Preview
Attachments
Remove
or Discard

Group Abstract Group Abstract