Skip to content

Commit c67b3e2

Browse files
Lee Newbergdzenanz
authored andcommitted
BUG: DistinguishersToColors now matches to hematoxylin and eosin
A bug report indicated an exception thrown for `Hematoxylin and Eosin are getting mixed up; failed` when it should not have. Further investigation indicated that an earlier exception should have been thrown for `The image to be normalized could not be processed; does it have white, blue, and pink pixels?`. That was triggered because the same distinguisher was best at suppressing both the color suppressed by hematoxylin and the color suppressed by eosin. The proper behavior is that the distinguishers assigned to these stains should be distinct. The fix has a new approach for matching distinguishers with stains. Specifically, when considering only the 2-dimensional plane that is the colors that are suppressed by hematoxylin and eosin, the angle of each distinguisher suppression vector can be measured in that plane. The assignment of distinguisher to stain is based upon which distinguisher is closer in angle to that stain's axis.
1 parent 517f0f4 commit c67b3e2

File tree

2 files changed

+46
-32
lines changed

2 files changed

+46
-32
lines changed

include/itkStructurePreservingColorNormalizationFilter.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ class StructurePreservingColorNormalizationFilter : public ImageToImageFilter<TI
131131
static constexpr SizeValueType maxNumberOfIterations{ 0 };
132132
/** Select a subset of the pixels if the image has more than this */
133133
static constexpr SizeValueType maxNumberOfRows{ 100000 };
134+
/** Useful constants **/
135+
static constexpr CalcElementType Zero{ 0.0 };
136+
static constexpr CalcElementType One{ 1.0 };
134137
/** Colors at least this fraction as distance as first pass distinguisher are good substitutes for it. */
135138
static constexpr CalcElementType SecondPassDistinguishersThreshold{ 0.90 };
136139
/** Colors that are at least this percentile in brightness are considered bright. */

include/itkStructurePreservingColorNormalizationFilter.hxx

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ StructurePreservingColorNormalizationFilter<TImage>::BeforeThreadedGenerateData(
153153
if (this->ImageToNMF(inIt, m_InputH, m_InputUnstainedPixel) != 0)
154154
{
155155
// we failed
156+
m_Input = nullptr;
156157
itkAssertOrThrowMacro(
157158
m_Input != nullptr,
158159
"The image to be normalized could not be processed; does it have white, blue, and pink pixels?");
@@ -184,7 +185,7 @@ StructurePreservingColorNormalizationFilter<TImage>::BeforeThreadedGenerateData(
184185
m_Reference = referenceImage;
185186
m_ReferenceTimeStamp = referenceImage->GetTimeStamp();
186187

187-
if ((m_InputH * m_ReferenceH.transpose()).determinant() < CalcElementType(0))
188+
if ((m_InputH * m_ReferenceH.transpose()).determinant() < Zero)
188189
{
189190
// Somehow the hematoxylin and eosin rows got swapped in one of
190191
// the input image or reference image. Flip them in referenceH to get
@@ -194,7 +195,7 @@ StructurePreservingColorNormalizationFilter<TImage>::BeforeThreadedGenerateData(
194195
m_ReferenceH.row(0) = referenceHOriginal.row(1);
195196
m_ReferenceH.row(1) = referenceHOriginal.row(0);
196197
}
197-
itkAssertOrThrowMacro((m_InputH * m_ReferenceH.transpose()).determinant() > CalcElementType(0),
198+
itkAssertOrThrowMacro((m_InputH * m_ReferenceH.transpose()).determinant() > Zero,
198199
"Hematoxylin and Eosin are getting mixed up; failed");
199200
}
200201

@@ -245,8 +246,6 @@ StructurePreservingColorNormalizationFilter<TImage>::ImageToNMF(RegionConstItera
245246
}
246247

247248
// Improve matrixH using Virtanen's algorithm
248-
// { std::ostringstream mesg; mesg << "matrixH before VirtanenNMFEuclidean = " << std::endl << matrixH << std::endl;
249-
// std::cout << mesg.str() << std::flush; }
250249
{
251250
CalcMatrixType matrixW; // Could end up large.
252251
this->VirtanenNMFEuclidean(matrixBrightV, matrixW, matrixH);
@@ -255,11 +254,7 @@ StructurePreservingColorNormalizationFilter<TImage>::ImageToNMF(RegionConstItera
255254
// Rescale each row of matrixH so that the
256255
// (100-VeryDarkPercentileLevel) value of each column of matrixW is
257256
// 1.0.
258-
// { std::ostringstream mesg; mesg << "matrixH before NormalizeMatrixH = " << std::endl << matrixH << std::endl;
259-
// std::cout << mesg.str() << std::flush; }
260257
this->NormalizeMatrixH(matrixDarkV, unstainedPixel, matrixH);
261-
// { std::ostringstream mesg; mesg << "matrixH at end = " << std::endl << matrixH << std::endl; std::cout <<
262-
// mesg.str() << std::flush; }
263258

264259
return 0;
265260
}
@@ -289,7 +284,7 @@ StructurePreservingColorNormalizationFilter<TImage>::ImageToMatrix(RegionConstIt
289284
PixelType pixelValue = iter.Get();
290285
for (Eigen::Index color = 0; color < m_NumberOfColors; ++color)
291286
{
292-
matrixV(numberOfRows, color) = pixelValue[color] + CalcElementType(1.0);
287+
matrixV(numberOfRows, color) = pixelValue[color] + One;
293288
}
294289
}
295290
}
@@ -466,7 +461,7 @@ StructurePreservingColorNormalizationFilter<TImage>::SecondPassDistinguishers(
466461
const CalcColVectorType dotProducts{ normV * normV.row(firstPassDistinguisherIndices[distinguisher]).transpose() };
467462
const CalcElementType threshold{ *std::max_element(Self::cbegin(dotProducts), Self::cend(dotProducts)) *
468463
SecondPassDistinguishersThreshold };
469-
CalcRowVectorType cumulative{ CalcRowVectorType::Constant(1, normVStart.cols(), 0.0) };
464+
CalcRowVectorType cumulative{ CalcRowVectorType::Constant(1, normVStart.cols(), Zero) };
470465
SizeValueType numberOfContributions{ 0 };
471466
for (Eigen::Index row = 0; row < dotProducts.size(); ++row)
472467
{
@@ -542,8 +537,6 @@ StructurePreservingColorNormalizationFilter<TImage>::DistinguishersToNMFSeeds(co
542537
return 1; // we failed
543538
}
544539

545-
// { std::ostringstream mesg; mesg << "distinguishers = " << std::endl << distinguishers << std::endl; std::cout <<
546-
// mesg.str() << std::flush; }
547540
unstainedPixel = distinguishers.row(unstainedIndex);
548541
const CalcRowVectorType logUnstained{ unstainedPixel.unaryExpr(CalcUnaryFunctionPointer(std::log)) };
549542
const CalcRowVectorType logHematoxylin{
@@ -556,7 +549,7 @@ StructurePreservingColorNormalizationFilter<TImage>::DistinguishersToNMFSeeds(co
556549
matrixH.row(1) = logEosin;
557550

558551
// If somehow an element of matrixH is negative, set it to zero.
559-
const auto clip = [](const CalcElementType & x) { return std::max(CalcElementType(0.0), x); };
552+
const auto clip = [](const CalcElementType & x) { return std::max(Zero, x); };
560553
matrixH = matrixH.unaryExpr(clip);
561554

562555
return 0;
@@ -582,12 +575,26 @@ StructurePreservingColorNormalizationFilter<TImage>::DistinguishersToColors(Calc
582575
// m_ColorIndexSuppressedByHematoxylin, and the index of the color
583576
// suppressed by eosin is indicated by
584577
// m_ColorIndexSuppressedByEosin.
585-
const CalcColVectorType redValues{ distinguishers.col(m_ColorIndexSuppressedByHematoxylin) };
586-
const CalcElementType * const hematoxylinIt{ std::min_element(Self::cbegin(redValues), Self::cend(redValues)) };
587-
hematoxylinIndex = std::distance(Self::cbegin(redValues), hematoxylinIt);
588-
const CalcColVectorType greenValues{ distinguishers.col(m_ColorIndexSuppressedByEosin) };
589-
const CalcElementType * const eosinIt{ std::min_element(Self::cbegin(greenValues), Self::cend(greenValues)) };
590-
eosinIndex = std::distance(Self::cbegin(greenValues), eosinIt);
578+
std::vector<int> suppressedIndex;
579+
std::vector<CalcRowVectorType> suppressedPixel;
580+
for (int fromRow = 0; fromRow < distinguishers.rows(); ++fromRow)
581+
{
582+
if (fromRow == unstainedIndex)
583+
continue;
584+
suppressedIndex.push_back(fromRow);
585+
suppressedPixel.push_back(distinguishers.row(unstainedIndex) - distinguishers.row(fromRow));
586+
}
587+
if (suppressedPixel[0](m_ColorIndexSuppressedByHematoxylin) * suppressedPixel[1](m_ColorIndexSuppressedByEosin) >
588+
suppressedPixel[0](m_ColorIndexSuppressedByEosin) * suppressedPixel[1](m_ColorIndexSuppressedByHematoxylin))
589+
{
590+
hematoxylinIndex = suppressedIndex[0];
591+
eosinIndex = suppressedIndex[1];
592+
}
593+
else
594+
{
595+
hematoxylinIndex = suppressedIndex[1];
596+
eosinIndex = suppressedIndex[0];
597+
}
591598
}
592599

593600

@@ -597,7 +604,7 @@ StructurePreservingColorNormalizationFilter<TImage>::NormalizeMatrixH(const Calc
597604
const CalcRowVectorType & unstainedPixel,
598605
CalcMatrixType & matrixH) const
599606
{
600-
const CalcColVectorType firstOnes{ CalcColVectorType::Constant(matrixDarkVIn.rows(), 1, 1.0) };
607+
const CalcColVectorType firstOnes{ CalcColVectorType::Constant(matrixDarkVIn.rows(), 1, One) };
601608

602609
// Compute the VeryDarkPercentileLevel percentile of a stain's
603610
// negative(matrixW) column. This a dark value due to its being the
@@ -606,7 +613,7 @@ StructurePreservingColorNormalizationFilter<TImage>::NormalizeMatrixH(const Calc
606613
CalcMatrixType matrixDarkV{ matrixDarkVIn };
607614
matrixDarkV = (firstOnes * logUnstainedCalcPixel) - matrixDarkV.unaryExpr(CalcUnaryFunctionPointer(std::log));
608615

609-
const auto clip = [](const CalcElementType & x) { return std::max(CalcElementType(0.0), x); };
616+
const auto clip = [](const CalcElementType & x) { return std::max(Zero, x); };
610617
CalcMatrixType negativeMatrixW = -(((matrixDarkV * matrixH.transpose()).array() - lambda).unaryExpr(clip).matrix() *
611618
(matrixH * matrixH.transpose()).inverse())
612619
.unaryExpr(clip);
@@ -629,7 +636,7 @@ StructurePreservingColorNormalizationFilter<TImage>::VirtanenNMFEuclidean(const
629636
CalcMatrixType & matrixW,
630637
CalcMatrixType & matrixH)
631638
{
632-
const auto clip = [](const CalcElementType & x) { return std::max(CalcElementType(0.0), x); };
639+
const auto clip = [](const CalcElementType & x) { return std::max(Zero, x); };
633640
matrixW = ((((matrixV * matrixH.transpose()).array() - lambda).unaryExpr(clip) + epsilon2).matrix() *
634641
(matrixH * matrixH.transpose()).inverse())
635642
.unaryExpr(clip);
@@ -664,8 +671,6 @@ StructurePreservingColorNormalizationFilter<TImage>::VirtanenNMFEuclidean(const
664671
previousMatrixW = matrixW;
665672
}
666673
}
667-
// { std::ostringstream mesg; mesg << "Executed = " << loopIter << " iterations of VirtanenNMFEuclidean" << std::endl;
668-
// std::cout << mesg.str() << std::flush; }
669674

670675
// Round off values in the response, so that numbers are quite small
671676
// are set to zero.
@@ -685,15 +690,15 @@ StructurePreservingColorNormalizationFilter<TImage>::VirtanenNMFKLDivergence(con
685690
// the Lasso penalty lambda for matrixW and incorporate the Lagrange
686691
// multipliers to make each row of matrixH have magnitude 1.0.
687692

688-
const auto clip = [](const CalcElementType & x) { return std::max(CalcElementType(0.0), x); };
693+
const auto clip = [](const CalcElementType & x) { return std::max(Zero, x); };
689694
matrixW = ((((matrixV * matrixH.transpose()).array() - lambda).unaryExpr(clip) + epsilon2).matrix() *
690695
(matrixH * matrixH.transpose()).inverse())
691696
.unaryExpr(clip);
692697

693698
// Apply Virtanen's algorithm to iteratively improve matrixW and
694699
// matrixH.
695-
const CalcRowVectorType firstOnes{ CalcRowVectorType::Constant(1, matrixV.rows(), 1.0) };
696-
const CalcColVectorType lastOnes{ CalcColVectorType::Constant(matrixV.cols(), 1, 1.0) };
700+
const CalcRowVectorType firstOnes{ CalcRowVectorType::Constant(1, matrixV.rows(), One) };
701+
const CalcColVectorType lastOnes{ CalcColVectorType::Constant(matrixV.cols(), 1, One) };
697702
CalcMatrixType previousMatrixW{ matrixW };
698703
SizeValueType loopIter{ 0 };
699704
for (; loopIter < maxNumberOfIterations; ++loopIter)
@@ -726,8 +731,6 @@ StructurePreservingColorNormalizationFilter<TImage>::VirtanenNMFKLDivergence(con
726731
previousMatrixW = matrixW;
727732
}
728733
}
729-
// { std::ostringstream mesg; mesg << "Executed = " << loopIter << " iterations of VirtanenNMFKLDivergence" <<
730-
// std::endl; std::cout << mesg.str() << std::flush; }
731734

732735
// Round off values in the response, so that numbers are quite small
733736
// are set to zero.
@@ -770,12 +773,12 @@ StructurePreservingColorNormalizationFilter<TImage>::NMFsToImage(const CalcMatri
770773
// logarithm.
771774
CalcRowVectorType logInputUnstained = inputUnstained.unaryExpr(CalcUnaryFunctionPointer(std::log));
772775
CalcRowVectorType logReferenceUnstained = referenceUnstained.unaryExpr(CalcUnaryFunctionPointer(std::log));
773-
const CalcColVectorType firstOnes{ CalcColVectorType::Constant(numberOfPixels, 1, 1.0) };
776+
const CalcColVectorType firstOnes{ CalcColVectorType::Constant(numberOfPixels, 1, One) };
774777
matrixV = (firstOnes * logInputUnstained) - matrixV.unaryExpr(CalcUnaryFunctionPointer(std::log));
775778

776779
// Switch from inputH to referenceH.
777780
{
778-
const auto clip = [](const CalcElementType & x) { return std::max(CalcElementType(0.0), x); };
781+
const auto clip = [](const CalcElementType & x) { return std::max(Zero, x); };
779782
CalcMatrixType matrixW = (((matrixV * inputH.transpose()).array() - lambda).unaryExpr(clip).matrix() *
780783
(inputH * inputH.transpose()).inverse())
781784
.unaryExpr(clip);
@@ -797,7 +800,7 @@ StructurePreservingColorNormalizationFilter<TImage>::NMFsToImage(const CalcMatri
797800
}
798801
for (Eigen::Index color = 0; color < m_NumberOfColors; ++color)
799802
{
800-
pixelValue[color] = std::max(std::min(matrixV(pixelIndex, color) - CalcElementType(1.0), upperbound), lowerbound);
803+
pixelValue[color] = std::max(std::min(matrixV(pixelIndex, color) - One, upperbound), lowerbound);
801804
}
802805
const PixelType inputPixel = inIt.Get();
803806
for (Eigen::Index dim = m_NumberOfColors; dim < m_NumberOfDimensions; ++dim)
@@ -907,6 +910,14 @@ template <typename TImage>
907910
constexpr typename StructurePreservingColorNormalizationFilter<TImage>::SizeValueType
908911
StructurePreservingColorNormalizationFilter<TImage>::maxNumberOfRows;
909912

913+
template <typename TImage>
914+
constexpr typename StructurePreservingColorNormalizationFilter<TImage>::CalcElementType
915+
StructurePreservingColorNormalizationFilter<TImage>::Zero;
916+
917+
template <typename TImage>
918+
constexpr typename StructurePreservingColorNormalizationFilter<TImage>::CalcElementType
919+
StructurePreservingColorNormalizationFilter<TImage>::One;
920+
910921
template <typename TImage>
911922
constexpr typename StructurePreservingColorNormalizationFilter<TImage>::CalcElementType
912923
StructurePreservingColorNormalizationFilter<TImage>::SecondPassDistinguishersThreshold;

0 commit comments

Comments
 (0)