diff --git a/Modules/Classification/CLActiveLearning/include/mitkActiveLearningInteractor.h b/Modules/Classification/CLActiveLearning/include/mitkActiveLearningInteractor.h index 3875cc63ea..fd0abac803 100644 --- a/Modules/Classification/CLActiveLearning/include/mitkActiveLearningInteractor.h +++ b/Modules/Classification/CLActiveLearning/include/mitkActiveLearningInteractor.h @@ -1,60 +1,62 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef mitkActiveLearningInteractor_h #define mitkActiveLearningInteractor_h #include #include #include #include namespace mitk { - class MITKCLACTIVELEARNING_EXPORT ActiveLearningInteractor : public DataInteractor - { +class MITKCLACTIVELEARNING_EXPORT ActiveLearningInteractor : public DataInteractor +{ + +public: - public: + typedef mitk::LabelSetImage::PixelType AnnotationPixelType; - mitkClassMacro(ActiveLearningInteractor, DataInteractor) - itkFactorylessNewMacro(Self) + mitkClassMacro(ActiveLearningInteractor, DataInteractor) + itkFactorylessNewMacro(Self) - void SetPaintingPixelValue(mitk::LabelSetImage::PixelType value){m_PaintingPixelValue = value;} + void SetPaintingPixelValue(AnnotationPixelType value){m_PaintingPixelValue = value;} - bool IsUsed(){return m_Used;} + bool IsUsed(){return m_Used;} - private: +private: - ActiveLearningInteractor(); - ~ActiveLearningInteractor(); + ActiveLearningInteractor(); + ~ActiveLearningInteractor(); - void ConnectActionsAndFunctions() override; - void DataNodeChanged() override; + void ConnectActionsAndFunctions() override; + void DataNodeChanged() override; - void Paint(mitk::StateMachineAction* action, mitk::InteractionEvent* event); + void Paint(mitk::StateMachineAction* action, mitk::InteractionEvent* event); - void PaintInterpolate(mitk::StateMachineAction* action, mitk::InteractionEvent* event); + void PaintInterpolate(mitk::StateMachineAction* action, mitk::InteractionEvent* event); - itk::Index<3> m_LastPixelIndex; - mitk::LabelSetImage::PixelType m_PaintingPixelValue; - bool m_Used; + itk::Index<3> m_LastPixelIndex; + AnnotationPixelType m_PaintingPixelValue; + bool m_Used; - }; +}; } #endif diff --git a/Modules/Classification/CLActiveLearning/src/mitkActiveLearningInteractor.cpp b/Modules/Classification/CLActiveLearning/src/mitkActiveLearningInteractor.cpp index e17131484f..badb2ea81d 100644 --- a/Modules/Classification/CLActiveLearning/src/mitkActiveLearningInteractor.cpp +++ b/Modules/Classification/CLActiveLearning/src/mitkActiveLearningInteractor.cpp @@ -1,208 +1,217 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #include #include #include #include // Helper function to get an image from a data node. -static mitk::LabelSetImage::Pointer GetImage(mitk::DataNode::Pointer dataNode) +static mitk::Image::Pointer GetImage(mitk::DataNode::Pointer dataNode) { if (dataNode.IsNull()) mitkThrow(); - mitk::LabelSetImage::Pointer image = dynamic_cast(dataNode->GetData()); + mitk::Image::Pointer image = dynamic_cast(dataNode->GetData()); if (image.IsNull()) mitkThrow(); return image; } // Helper function to get a geometry of an image for a specific time step. -static mitk::BaseGeometry::Pointer GetGeometry(mitk::LabelSetImage::Pointer image, unsigned int timeStep) +static mitk::BaseGeometry::Pointer GetGeometry(mitk::Image* image, unsigned int timeStep) { + if (image == nullptr) + mitkThrow(); + mitk::TimeGeometry::Pointer timeGeometry = image->GetTimeGeometry(); if (timeGeometry.IsNull()) mitkThrow(); auto geometry = timeGeometry->GetGeometryForTimeStep(timeStep); if (geometry.IsNull()) mitkThrow(); return geometry; } static std::vector> InterpolateIndices(itk::Index<3> startIndex, itk::Index<3> endIndex, mitk::BaseGeometry* geometry) { + if (geometry == nullptr) + mitkThrow(); + std::vector> resultIndices; mitk::Point3D startPoint; mitk::Point3D endPoint; geometry->IndexToWorld(startIndex, startPoint); geometry->IndexToWorld(endIndex, endPoint); + itk::Index<3> indexDelta; int indexDeltaInc[3]; for (int i=0; i<3; i++) { indexDelta[i] = endIndex[i] - startIndex[i]; indexDeltaInc[i] = (indexDelta[i] > 0) ? 1 : (indexDelta[i] < 0) ? -1 : 0; } int argm[3] = {0, 1, 2}; if (abs(indexDelta[1]) > abs(indexDelta[0])) { argm[0] = 1; argm[1] = 0; } if (abs(indexDelta[2]) > abs(indexDelta[argm[1]])) { argm[2] = argm[1]; argm[1] = 2; } if (abs(indexDelta[2]) > abs(indexDelta[argm[0]])) { argm[1] = argm[0]; argm[0] = 2; } double slopes[2]; slopes[0] = (endPoint[argm[1]] - startPoint[argm[1]]) / (endPoint[argm[0]] - startPoint[argm[0]]); slopes[1] = (endPoint[argm[2]] - startPoint[argm[2]]) / sqrt((endPoint[argm[1]] - startPoint[argm[1]]) * (endPoint[argm[1]] - startPoint[argm[1]]) + (endPoint[argm[0]] - startPoint[argm[0]]) * (endPoint[argm[0]] - startPoint[argm[0]])); itk::Index<3> currentIndex = startIndex; mitk::Point3D currentPoint = startPoint; while (currentIndex != endIndex) { currentIndex[argm[0]] += indexDeltaInc[argm[0]]; geometry->IndexToWorld(currentIndex, currentPoint); currentPoint[argm[1]] = startPoint[argm[1]] + slopes[0] * (currentPoint[argm[0]] - startPoint[argm[0]]); currentPoint[argm[2]] = startPoint[argm[2]] + slopes[1] * sqrt((currentPoint[argm[1]] - startPoint[argm[1]]) * (currentPoint[argm[1]] - startPoint[argm[1]]) + (currentPoint[argm[0]] - startPoint[argm[0]]) * (currentPoint[argm[0]] - startPoint[argm[0]])); geometry->WorldToIndex(currentPoint, currentIndex); resultIndices.push_back(currentIndex); } return resultIndices; } mitk::ActiveLearningInteractor::ActiveLearningInteractor() : m_PaintingPixelValue(0) { } mitk::ActiveLearningInteractor::~ActiveLearningInteractor() { } void mitk::ActiveLearningInteractor::ConnectActionsAndFunctions() { CONNECT_FUNCTION("paint", Paint) CONNECT_FUNCTION("paint_interpolate", PaintInterpolate) } void mitk::ActiveLearningInteractor::DataNodeChanged() { this->ResetToStartState(); } void mitk::ActiveLearningInteractor::Paint(mitk::StateMachineAction* /*action*/, mitk::InteractionEvent* event) { + if (m_PaintingPixelValue == -1) return; + try { auto renderer = event->GetSender(); auto image = GetImage(this->GetDataNode()); auto timeStep = renderer->GetTimeStep(); auto geometry = GetGeometry(image, timeStep); auto positionEvent = dynamic_cast(event); auto position = positionEvent->GetPositionInWorld(); if (!geometry->IsInside(position)) return; // Okay, we're safe. Convert the mouse position to the index of the pixel // we're pointing at. itk::Index<3> index; geometry->WorldToIndex<3>(position, index); // We don't need to paint over and over again while moving the mouse // pointer inside the same pixel. That's especially relevant when operating // on zoomed images. if (index != m_LastPixelIndex) { - mitk::ImagePixelWriteAccessor writeAccessor(image.GetPointer(), image->GetVolumeData(timeStep)); + mitk::ImagePixelWriteAccessor writeAccessor(image, image->GetVolumeData(timeStep)); writeAccessor.SetPixelByIndexSafe(index, m_PaintingPixelValue); image->Modified(); this->GetDataNode()->Modified(); -// mitk::RenderingManager::GetInstance()->RequestUpdate(positionEvent->GetSender()->GetRenderWindow()); mitk::RenderingManager::GetInstance()->RequestUpdateAll(); m_LastPixelIndex = index; m_Used = true; } } catch (...) { return; } } void mitk::ActiveLearningInteractor::PaintInterpolate(mitk::StateMachineAction* /*action*/, mitk::InteractionEvent* event) { + if (m_PaintingPixelValue == -1) return; + try { auto renderer = event->GetSender(); auto image = GetImage(this->GetDataNode()); auto timeStep = renderer->GetTimeStep(); auto geometry = GetGeometry(image, timeStep); auto positionEvent = dynamic_cast(event); auto position = positionEvent->GetPositionInWorld(); if (!geometry->IsInside(position)) return; // Okay, we're safe. Convert the mouse position to the index of the pixel // we're pointing at. itk::Index<3> index; geometry->WorldToIndex<3>(position, index); // We don't need to paint over and over again while moving the mouse // pointer inside the same pixel. That's especially relevant when operating // on zoomed images. if (index != m_LastPixelIndex) { // And finally... - mitk::ImagePixelWriteAccessor writeAccessor(image.GetPointer(), image->GetVolumeData(timeStep)); + mitk::ImagePixelWriteAccessor writeAccessor(image, image->GetVolumeData(timeStep)); // Paint all points between current and last pixel auto indices = InterpolateIndices(m_LastPixelIndex, index, geometry); for (auto i : indices) { writeAccessor.SetPixelByIndexSafe(i, m_PaintingPixelValue); } image->Modified(); this->GetDataNode()->Modified(); -// mitk::RenderingManager::GetInstance()->RequestUpdate(positionEvent->GetSender()->GetRenderWindow()); mitk::RenderingManager::GetInstance()->RequestUpdateAll(); m_LastPixelIndex = index; m_Used = true; } } catch (...) { return; } } diff --git a/Plugins/org.mitk.gui.qt.activelearning/src/internal/QmitkActiveLearning.cpp b/Plugins/org.mitk.gui.qt.activelearning/src/internal/QmitkActiveLearning.cpp index f577dccd95..eb2ceffb26 100644 --- a/Plugins/org.mitk.gui.qt.activelearning/src/internal/QmitkActiveLearning.cpp +++ b/Plugins/org.mitk.gui.qt.activelearning/src/internal/QmitkActiveLearning.cpp @@ -1,982 +1,932 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ // Blueberry #include #include // Qt #include #include #include #include // Qmitk #include "QmitkActiveLearning.h" // MITK #include #include #include #include #include #include #include #include #include #include +#include +#include +#include // ITK #include #include #include #include #include #include #include #include typedef double FeaturePixelType; // Returns true if list has at least one entry and all entries are valid mitk::Images, otherwise false static bool SelectionAllImages(const QList& nodes) { if (nodes.empty()) { return false; } for (const auto& node : nodes) { if(!(node.IsNotNull() && dynamic_cast(node->GetData()) != nullptr)) return false; } return true; } // QColor to mitk::Color static mitk::Color QColorToMitkColor(const QColor& qcolor) { mitk::Color color; color.SetRed((float)qcolor.red() / 255); color.SetGreen((float)qcolor.green() / 255); color.SetBlue((float)qcolor.blue() / 255); return color; } // For debugging static void PrintAllLabels(mitk::LabelSetImage* image) { for (auto it=image->GetActiveLabelSet()->IteratorBegin(); it!=image->GetActiveLabelSet()->IteratorConstEnd(); ++it) { MITK_INFO << "Key: " << it->first << " - Name: " << it->second->GetName() << " - Value: " << it->second->GetValue() << " - Color: " << it->second->GetColor(); } } // Make values of labels a consistent range static void FillLabelValues(mitk::LabelSetImage* image) { int value(0); for (auto it=image->GetActiveLabelSet()->IteratorBegin(); it!=image->GetActiveLabelSet()->IteratorConstEnd(); ++it) { it->second->SetValue(value); value++; } image->GetActiveLabelSet()->SetActiveLabel(0); } // Fill image with zeros static void FillWithZeros(mitk::Image* image) { unsigned int size = image->GetPixelType().GetSize(); for (unsigned int i=0; iGetDimension(); i++) { size *= image->GetDimension(i); } for (unsigned int t=0; tGetTimeSteps(); t++) { mitk::ImageWriteAccessor accessor(image, image->GetVolumeData(0)); memset(accessor.GetData(), 0, size); } } template static Eigen::Matrix Transform(const std::vector images) { // Find size for output matrix [number of voxels, number of feature images] unsigned int size = images[0]->GetDimension(0); for (unsigned int i=1; i<3; ++i) { size *= images[0]->GetDimension(i); } Eigen::Matrix outputMatrix(size, images.size()); int j = 0; for (auto image : images) { int i = 0; typename itk::Image::Pointer imageItk; mitk::CastToItkImage(image, imageItk); auto it = itk::ImageRegionConstIterator>(imageItk, imageItk->GetLargestPossibleRegion()); while (!it.IsAtEnd()) { outputMatrix(i, j) = it.Get(); ++it; } ++j; } return outputMatrix; } template -static mitk::Image::Pointer Transform(const Eigen::Matrix &inputMatrix, mitk::Image::Pointer referenceImage) +static mitk::Image::Pointer Transform(const Eigen::Matrix &inputMatrix, const mitk::Image::Pointer referenceImage) { typename itk::Image::Pointer imageItk; auto outputImage = mitk::Image::New(); outputImage->Initialize(referenceImage); mitk::CastToItkImage(outputImage, imageItk); auto it = itk::ImageRegionIterator>(imageItk, imageItk->GetLargestPossibleRegion()); int i = 0; while (!it.IsAtEnd()) { it.Set(inputMatrix(i, 0)); + labelCounts[inputMatrix(i, 0)] += 1; ++it; ++i; } - int s = 5; - for (int i=0; iGetPixel({i, j, k}) << " "; - } - MITK_INFO << ss.str(); - } - } - outputImage = mitk::GrabItkImageMemory(imageItk); - - s = 5; - for (int i=0; iGetPixelValueByIndex({i, j, k}) << " "; - } - MITK_INFO << ss.str(); - } - } - return outputImage; } /* ================================================================== * FEATURES * =============================================================== */ template static void GaussianSmoothing(const itk::Image* inputImage, const double sigma, mitk::Image::Pointer outputImage) { typedef itk::Image ImageType; typedef itk::Image FeatureImageType; auto filter = itk::DiscreteGaussianImageFilter::New(); filter->SetInput(inputImage); filter->SetVariance(sigma*sigma); filter->Update(); mitk::GrabItkImageMemory(filter->GetOutput(), outputImage); } template static void GaussianGradientMagnitude(const itk::Image* inputImage, const double sigma, mitk::Image::Pointer outputImage) { typedef itk::Image ImageType; typedef itk::Image FeatureImageType; auto filter = itk::GradientMagnitudeRecursiveGaussianImageFilter::New(); filter->SetInput(inputImage); filter->SetSigma(sigma); filter->Update(); mitk::GrabItkImageMemory(filter->GetOutput(), outputImage); } template static void LaplacianOfGaussian(const itk::Image* inputImage, const double sigma, mitk::Image::Pointer outputImage) { typedef itk::Image ImageType; typedef itk::Image FeatureImageType; auto filter = itk::LaplacianRecursiveGaussianImageFilter::New(); filter->SetInput(inputImage); filter->SetSigma(sigma); filter->Update(); mitk::GrabItkImageMemory(filter->GetOutput(), outputImage); } //template //static void StructureTensorEigenvalues(const itk::Image* inputImage, float sigma, std::vector outputImages) //{ // typedef itk::Image ImageType; // auto filter = itk::StructureTensorEigenvalueImageFilter::New(); // filter->SetInput(inputImage); // filter->SetInnerScale(sigma); // filter->SetOuterScale(sigma); // filter->Update(); // for (unsigned int i=0; iGetNumberOfOutputs(); i++) // { // mitk::GrabItkImageMemory(filter->GetOutput(i), outputImages[i]); // } //} //template //static void HessianEigenvalues(const itk::Image* inputImage, float sigma, std::vector outputImages) //{ // typedef itk::Image ImageType; // typedef itk::Image, imageDimension> TensorImageType; // auto filter = itk::HessianRecursiveGaussianImageFilter::New(); // ImageType::Pointer o1, o2, o3; // o1->Allocate(); // o2->Allocate(); // o3->Allocate(); // filter->SetInput(inputImage); // filter->SetSigma(sigma); // filter->Update(); // TensorImageType::Pointer tensorImage = filter->GetOutput(); // itk::ImageRegionIterator tensorIt(tensorImage, tensorImage->GetLargestPossibleRegion()); // itk::ImageRegionIterator o1It(o1, o1->GetLargestPossibleRegion()); // itk::ImageRegionIterator o2It(o2, o2->GetLargestPossibleRegion()); // itk::ImageRegionIterator o3It(o3, o3->GetLargestPossibleRegion()); // while (!tensorIt.IsAtEnd()) // { // itk::SymmetricSecondRankTensor::EigenValue // for (unsigned int i=0; iGetNumberOfOutputs(); i++) // { // mitk::GrabItkImageMemory(filter->GetOutput(i), outputImages[i]); // } //} /* ================================================================== * PUBLIC SLOTS * =============================================================== */ void ActiveLearning::Initialize() { // Get selected nodes and check again if these are all images m_Nodes = this->GetDataManagerSelection(); if (!SelectionAllImages(m_Nodes)) return; m_Controls.m_InitializePushButton->setDisabled(true); // Set names to the label (again) QString nameList = QString::fromStdString(m_Nodes[0]->GetName()); if (m_Nodes.length() >= 2) { for (int i=1; i"); nameList += QString::fromStdString(m_Nodes[i]->GetName()); } } m_Controls.m_InitializeLabel->setText(nameList); // ======================================= // PREDICTION IMAGE // ======================================= m_PredictionImage = mitk::Image::New(); try { mitk::Image::Pointer referenceImage = dynamic_cast(m_Nodes[0]->GetData()); m_PredictionImage->Initialize(referenceImage); } catch (mitk::Exception& e) { MITK_ERROR << "Exception caught: " << e.GetDescription(); QMessageBox::information(m_Parent, "Error", "Could not initialize prediction image"); return; } FillWithZeros(m_PredictionImage); m_PredictionNode = mitk::DataNode::New(); m_PredictionNode->SetData(m_PredictionImage); m_PredictionNode->SetName("Predictions"); - m_PredictionNode->SetColor(0., 0., 0.); + m_PredictionNode->SetColor(1., 1. ,1.); + m_PredictionNode->SetBoolProperty("binary", false); + m_PredictionNode->SetProperty("opacity", mitk::FloatProperty::New(0.0f)); // m_PredictionNode->SetBoolProperty("helper object", true); -// m_PredictionImage->GetExteriorLabel()->SetProperty("name.parent", mitk::StringProperty::New(nodes[0]->GetName().c_str())); -// m_PredictionImage->GetExteriorLabel()->SetProperty("name.image", mitk::StringProperty::New("Predictions")); this->GetDataStorage()->Add(m_PredictionNode, m_Nodes[0]); // ======================================= // SEGMENTATION IMAGE // ======================================= - m_SegmentationImage = mitk::LabelSetImage::New(); + m_SegmentationImage = mitk::Image::New(); try { - auto referenceImage = dynamic_cast(m_Nodes[0]->GetData()); - m_SegmentationImage->Initialize(referenceImage); + mitk::Image::Pointer referenceImage = dynamic_cast(m_Nodes[0]->GetData()); + m_SegmentationImage->Initialize(mitk::MakeScalarPixelType(), *(referenceImage->GetTimeGeometry()->Clone())); } catch (mitk::Exception& e) { MITK_ERROR << "Exception caught: " << e.GetDescription(); QMessageBox::information(m_Parent, "Error", "Could not initialize segmentation image"); return; } FillWithZeros(m_SegmentationImage); m_SegmentationNode = mitk::DataNode::New(); m_SegmentationNode->SetData(m_SegmentationImage); m_SegmentationNode->SetName("Segmentation"); - m_SegmentationNode->SetColor(0., 0., 0.); + m_SegmentationNode->SetColor(1., 1., 1.); + m_SegmentationNode->SetBoolProperty("binary", false); // m_PredictionNode->SetBoolProperty("helper object", true); - m_SegmentationImage->GetExteriorLabel()->SetProperty("name.parent", mitk::StringProperty::New(m_Nodes[0]->GetName().c_str())); - m_SegmentationImage->GetExteriorLabel()->SetProperty("name.image", mitk::StringProperty::New("Segmentation")); this->GetDataStorage()->Add(m_SegmentationNode, m_Nodes[0]); // ======================================= // ANNOTATION IMAGE // ======================================= - m_AnnotationImage = mitk::LabelSetImage::New(); + m_AnnotationImage = mitk::Image::New(); try { - auto referenceImage = dynamic_cast(m_Nodes[0]->GetData()); - m_AnnotationImage->Initialize(referenceImage); + mitk::Image::Pointer referenceImage = dynamic_cast(m_Nodes[0]->GetData()); + m_AnnotationImage->Initialize(mitk::MakeScalarPixelType(), *(referenceImage->GetTimeGeometry()->Clone())); } catch (mitk::Exception& e) { MITK_ERROR << "Exception caught: " << e.GetDescription(); QMessageBox::information(m_Parent, "Error", "Could not initialize annotation image"); return; } FillWithZeros(m_AnnotationImage); m_AnnotationNode = mitk::DataNode::New(); m_AnnotationNode->SetData(m_AnnotationImage); m_AnnotationNode->SetName("Labels"); - m_AnnotationNode->SetColor(0., 0., 0.); + m_AnnotationNode->SetColor(1., 1., 1.); + m_AnnotationNode->SetBoolProperty("binary", false); + m_AnnotationNode->SetProperty("opacity", mitk::FloatProperty::New(1.0f)); // m_AnnotationNode->SetBoolProperty("helper object", true); - m_AnnotationImage->GetExteriorLabel()->SetProperty("name.parent", mitk::StringProperty::New(m_Nodes[0]->GetName().c_str())); - m_AnnotationImage->GetExteriorLabel()->SetProperty("name.image", mitk::StringProperty::New("Labels")); this->GetDataStorage()->Add(m_AnnotationNode, m_Nodes[0]); // Convert input images to FeaturePixelType for (auto node : m_Nodes) { mitk::Image::Pointer image = dynamic_cast(node->GetData()); auto itkImage = itk::Image::New(); mitk::CastToItkImage(image, itkImage); image = mitk::GrabItkImageMemory(itkImage); node->SetData(image); } // Calculate features for (const auto node : m_Nodes) { mitk::Image::Pointer currentImage = dynamic_cast(node->GetData()); QFuture>> future; future = QtConcurrent::run(this, &ActiveLearning::CalculateFeatures, currentImage); auto futureWatcher = new QFutureWatcher>>(); futureWatcher->setFuture(future); connect(futureWatcher, SIGNAL(finished()), this, SLOT(OnInitializationFinished())); m_FeatureCalculationWatchers.push_back(futureWatcher); } // Interactor auto activeLearningLib = us::ModuleRegistry::GetModule("MitkCLActiveLearning"); m_Interactor = mitk::ActiveLearningInteractor::New(); m_Interactor->LoadStateMachine("Paint.xml", activeLearningLib); m_Interactor->SetEventConfig("PaintConfig.xml", activeLearningLib); m_Interactor->SetDataNode(m_AnnotationNode); // Classifier m_Classifier = mitk::VigraRandomForestClassifier::New(); m_Classifier->SetTreeCount(m_NumberOfTrees); m_Classifier->SetMaximumTreeDepth(m_MaximumTreeDepth); // Automatically add first label OnAddLabelPushButtonClicked(); m_Active = true; } /* ================================================================== * PUBLIC * =============================================================== */ ActiveLearning::ActiveLearning() : m_Parent(nullptr), m_AnnotationImage(nullptr), m_AnnotationNode(nullptr), m_PredictionImage(nullptr), m_PredictionNode(nullptr), m_SegmentationImage(nullptr), m_SegmentationNode(nullptr), m_Active(false), m_NumberOfTrees(50), m_MaximumTreeDepth(10), m_PredictionMatrix(nullptr) { } ActiveLearning::~ActiveLearning() { } void ActiveLearning::CreateQtPartControl( QWidget *parent ) { m_Controls.setupUi(parent); m_Parent = parent; // Label model m_LabelListModel = new QStandardItemModel(0, 3, this); m_Controls.m_LabelTableView->setModel(m_LabelListModel); m_Controls.m_LabelTableView->horizontalHeader()->setDefaultSectionSize(20); m_Controls.m_LabelTableView->verticalHeader()->setDefaultSectionSize(20); NotEditableDelegate* itemDelegate = new NotEditableDelegate(parent); m_Controls.m_LabelTableView->setItemDelegateForColumn(1, itemDelegate); connect(m_Controls.m_LabelTableView, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(OnColorIconDoubleClicked(QModelIndex))); connect(m_Controls.m_LabelTableView->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(OnLabelListSelectionChanged(QItemSelection, QItemSelection))); connect(m_LabelListModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(OnLabelNameChanged(QModelIndex, QModelIndex))); // Buttons connect(m_Controls.m_InitializePushButton, SIGNAL(clicked()), this, SLOT(Initialize())); connect(m_Controls.m_AddLabelPushButton, SIGNAL(clicked()), this, SLOT(OnAddLabelPushButtonClicked())); connect(m_Controls.m_RemoveLabelPushButton, SIGNAL(clicked()), this, SLOT(OnRemoveLabelPushButtonClicked())); connect(m_Controls.m_PaintToolButton, SIGNAL(clicked()), this, SLOT(OnPaintToolButtonClicked())); connect(m_Controls.m_EraseToolButton, SIGNAL(clicked()), this, SLOT(OnEraseToolButtonClicked())); connect(m_Controls.m_UpdatePredictionsPushButton, SIGNAL(clicked()), this, SLOT(OnUpdatePredictionsPushButtonClicked())); // Set start configuration m_Controls.m_LabelControlsFrame->setVisible(false); SetInitializeReady(false); } void ActiveLearning::ResetLabels() { - // Remove all labels but the first - for (auto it=m_AnnotationImage->GetActiveLabelSet()->IteratorBegin(); it!=m_AnnotationImage->GetActiveLabelSet()->IteratorConstEnd(); it++) - { - if (it->first != 0) - { - m_AnnotationImage->GetActiveLabelSet()->RemoveLabel(it->first); - } - } - - // Fill with labels from list for (int i=0; irowCount(); i++) { - QString name = m_LabelListModel->item(i, 2)->text(); m_LabelListModel->item(i, 1)->setText(QString::number(i + 1)); - QColor color = m_LabelListModel->item(i)->background().color(); - m_AnnotationImage->GetActiveLabelSet()->AddLabel(name.toStdString(), QColorToMitkColor(color)); } } std::vector > ActiveLearning::CalculateFeatures(const mitk::Image::Pointer inputImage) { std::vector> result; // TODO: Get features from preference page std::vector sigmas = {0.7, 1.6}; for (auto sigma : sigmas) { std::stringstream ss; auto gaussImage = mitk::Image::New(); AccessByItk_n(inputImage, GaussianSmoothing, (sigma, gaussImage)); ss << "GaussianSmoothing (" << std::fixed << std::setprecision(2) << sigma << ")"; result.push_back(std::pair(gaussImage, ss.str())); ss.str(""); auto gradMagImage = mitk::Image::New(); AccessByItk_n(inputImage, GaussianGradientMagnitude, (sigma, gradMagImage)); ss << "GaussianGradientMagnitude (" << std::fixed << std::setprecision(2) << sigma << ")"; result.push_back(std::pair(gradMagImage, ss.str())); ss.str(""); auto logImage = mitk::Image::New(); AccessByItk_n(inputImage, LaplacianOfGaussian, (sigma, logImage)); ss << "LaplacianOfGaussian (" << std::fixed << std::setprecision(2) << sigma << ")"; result.push_back(std::pair(logImage, ss.str())); ss.str(""); // auto structImage1 = mitk::Image::New(); // auto structImage2 = mitk::Image::New(); // auto structImage3 = mitk::Image::New(); // std::vector structImages = {structImage1, structImage2, structImage3}; // AccessByItk_n(inputImage, StructureTensorEigenvalues, (sigma, structImages)); // ss << "StructureTensorEigenvalue1 (" << std::fixed << std::setprecision(2) << sigma << ")"; // result.push_back(std::pair(structImage1, ss.str())); // ss.str(""); // ss << "StructureTensorEigenvalue2 (" << std::fixed << std::setprecision(2) << sigma << ")"; // result.push_back(std::pair(structImage2, ss.str())); // ss.str(""); // if (inputImage->GetDimension() == 3) // { // ss << "StructureTensorEigenvalue3 (" << std::fixed << std::setprecision(2) << sigma << ")"; // result.push_back(std::pair(structImage3, ss.str())); // ss.str(""); // } // auto hessianImage1 = mitk::Image::New(); // auto hessianImage2 = mitk::Image::New(); // auto hessianImage3 = mitk::Image::New(); // std::vector hessianImages = {hessianImage1, hessianImage2, hessianImage3}; // AccessByItk_n(inputImage, HessianEigenvalues, (sigma, hessianImages)); // ss << "HessianEigenvalue1 (" << std::fixed << std::setprecision(2) << sigma << ")"; // result.push_back(std::pair(hessianImage1, ss.str())); // ss.str(""); // ss << "HessianEigenvalue2 (" << std::fixed << std::setprecision(2) << sigma << ")"; // result.push_back(std::pair(hessianImage2, ss.str())); // ss.str(""); // if (inputImage->GetDimension() == 3) // { // ss << "HessianEigenvalue3 (" << std::fixed << std::setprecision(2) << sigma << ")"; // result.push_back(std::pair(hessianImage3, ss.str())); // ss.str(""); // } } return result; } -std::pair, std::shared_ptr> ActiveLearning::GetTrainingData(const mitk::LabelSetImage::Pointer annotationImage, const std::vector featureImageVector) +std::pair, std::shared_ptr> ActiveLearning::GetTrainingData(const mitk::Image::Pointer annotationImage, const std::vector featureImageVector) { // Get indices and labels std::vector> indices; std::vector labels; - itk::Image::Pointer annotationImageItk; + itk::Image::Pointer annotationImageItk; mitk::CastToItkImage(annotationImage, annotationImageItk); - itk::ImageRegionIteratorWithIndex> it(annotationImageItk, annotationImageItk->GetLargestPossibleRegion()); + itk::ImageRegionIteratorWithIndex> it(annotationImageItk, annotationImageItk->GetLargestPossibleRegion()); while (!it.IsAtEnd()) { if (it.Get() != 0) { indices.push_back(it.GetIndex()); labels.push_back(it.Get()); + labelCounts[it.Get()] += 1; } ++it; } Eigen::MatrixXd trainingData(indices.size(), featureImageVector.size()); Eigen::VectorXi trainingLabels = Eigen::VectorXi::Map(labels.data(), labels.size()); int j = 0; for (mitk::Image::Pointer feature : featureImageVector) { int i = 0; mitk::ImagePixelReadAccessor access(feature, feature->GetVolumeData()); for (auto index : indices) { trainingData(i, j) = access.GetPixelByIndexSafe(index); i++; } j++; } auto trainingLabelsPtr = std::make_shared(trainingLabels); auto trainingDataPtr = std::make_shared(trainingData); std::pair, std::shared_ptr> result = std::make_pair(trainingLabelsPtr, trainingDataPtr); return result; } +void ActiveLearning::UpdateLookupTables() +{ + // Create new lookup table from list + auto lut = vtkSmartPointer::New(); + AnnotationPixelType lim = std::numeric_limits::max(); + lut->SetNumberOfTableValues(lim); + lut->SetTableRange(0, lim); + for (AnnotationPixelType i=0; iSetTableValue(i, 0.0, 0.0, 0.0, 0.0); + } + for (int j=0; jrowCount(); ++j) + { + int value = m_LabelListModel->item(j, 1)->text().toInt(); + const QColor color = m_LabelListModel->item(j, 0)->background().color(); + lut->SetTableValue(value, color.redF(), color.greenF(), color.blueF(), 1.0); + } + + auto lutMitk = mitk::LookupTable::New(); + lutMitk->SetVtkLookupTable(lut); + + // Set to annotation image and segmentation image + auto * lut_prop = dynamic_cast(m_AnnotationNode->GetProperty("LookupTable")); + lut_prop->SetLookupTable(lutMitk); + m_AnnotationNode->SetProperty("Image Rendering.Mode", mitk::RenderingModeProperty::New(mitk::RenderingModeProperty::LOOKUPTABLE_COLOR)); + lut_prop = dynamic_cast(m_SegmentationNode->GetProperty("LookupTable")); + lut_prop->SetLookupTable(lutMitk); + m_SegmentationNode->SetProperty("Image Rendering.Mode", mitk::RenderingModeProperty::New(mitk::RenderingModeProperty::LOOKUPTABLE_COLOR)); +} + const std::string ActiveLearning::VIEW_ID = "org.mitk.views.activelearning"; /* ================================================================== * PROTECTED SLOTS * =============================================================== */ void ActiveLearning::OnAddLabelPushButtonClicked() { - QString labelName = QString("Label ") + QString::number(m_AnnotationImage->GetActiveLabelSet()->ReverseIteratorBegin()->first + 1); + QString labelName = QString("Label ") + QString::number(m_LabelListModel->rowCount() + 1); QColor labelColor = Qt::GlobalColor(m_LabelListModel->rowCount() % 12 + 7); // We only want Qt default colors 7 to 18 // Create icon QStandardItem* colorSquare = new QStandardItem; colorSquare->setBackground(labelColor); colorSquare->setEditable(false); QPixmap colorPixmap(20, 20); colorPixmap.fill(labelColor); colorSquare->setIcon(QIcon(colorPixmap)); // Key is the highest existing key + 1 - int value = (int)m_AnnotationImage->GetActiveLabelSet()->ReverseIteratorBegin()->first + 1; - QStandardItem* valueItem = new QStandardItem(); + int value = 1; + if (m_LabelListModel->rowCount() >= 1) + { + value = m_LabelListModel->item(m_LabelListModel->rowCount() - 1, 1)->text().toInt() + 1; + } + QStandardItem* valueItem = new QStandardItem; valueItem->setText(QString::number(value)); // Create label item QStandardItem* label = new QStandardItem(labelName); - // Add to image - m_AnnotationImage->GetActiveLabelSet()->AddLabel(labelName.toStdString(), QColorToMitkColor(labelColor)); - PrintAllLabels(m_AnnotationImage); - // Make list and insert QList list; list.append(colorSquare); list.append(valueItem); list.append(label); m_LabelListModel->appendRow(list); // If this is the first label, we activate the paint button // We also have to set the data node color for this one, because for 1 values that color seems to define the rendered color if (m_LabelListModel->rowCount() == 1) { OnPaintToolButtonClicked(); - m_AnnotationNode->SetColor(QColorToMitkColor(labelColor)); } - // Select newly added label - m_Controls.m_LabelTableView->selectRow(m_LabelListModel->rowCount() - 1); + // Update colors + UpdateLookupTables(); } void ActiveLearning::OnRemoveLabelPushButtonClicked() { QItemSelectionModel* selection = m_Controls.m_LabelTableView->selectionModel(); if (selection->hasSelection()) { unsigned int removeIndex = selection->selectedRows().first().row(); QString removeMessage = QString("Remove label '") + m_LabelListModel->item(removeIndex, 2)->text() + QString("'?"); QMessageBox::StandardButton removeReply; removeReply = QMessageBox::question(m_Parent, "Remove Label", removeMessage, QMessageBox::Yes | QMessageBox::No); if (removeReply == QMessageBox::Yes) { - // if there are no annotations, reset labels - if (m_Interactor->IsUsed()) - { - std::vector labels; - labels.push_back(m_LabelListModel->item(removeIndex, 1)->text().toInt()); - m_AnnotationImage->RemoveLabels(labels); - m_LabelListModel->removeRow(removeIndex); - } - else - { - m_LabelListModel->removeRow(removeIndex); + m_LabelListModel->removeRow(removeIndex); + if (!m_Interactor->IsUsed()) ResetLabels(); - } - PrintAllLabels(m_AnnotationImage); } } } void ActiveLearning::OnPaintToolButtonClicked() { m_Controls.m_PaintToolButton->setChecked(true); QItemSelectionModel* selection = m_Controls.m_LabelTableView->selectionModel(); int row(0); if (selection->hasSelection()) { row = selection->selectedRows().first().row(); } else { m_Controls.m_LabelTableView->selectRow(0); } m_Interactor->SetPaintingPixelValue(m_LabelListModel->item(row, 1)->text().toInt()); - m_AnnotationImage->GetActiveLabelSet()->SetActiveLabel(m_LabelListModel->item(row, 1)->text().toInt()); } void ActiveLearning::OnEraseToolButtonClicked() { m_Controls.m_EraseToolButton->setChecked(true); m_Interactor->SetPaintingPixelValue(0); - m_AnnotationImage->GetActiveLabelSet()->SetActiveLabel(0); } void ActiveLearning::OnActivateGuidancePushButtonToggled(bool toggled) { } void ActiveLearning::OnSaveSegmentationPushButtonClicked() { } void ActiveLearning::OnSavePredictionsPushButtonClicked() { } void ActiveLearning::OnExportSegmentationPushButtonClicked() { } void ActiveLearning::OnExportPredictionsPushButtonClicked() { } void ActiveLearning::OnColorIconDoubleClicked(const QModelIndex& index) { // Check if click is really from color icon if (index.column() != 0) { return; } else { // Color change dialog QColor setColor = QColorDialog::getColor(m_LabelListModel->itemFromIndex(index)->background().color(), m_Parent, "Select Label Color"); if (setColor.isValid()) { m_LabelListModel->itemFromIndex(index)->setBackground(setColor); QPixmap colorPixmap(20, 20); colorPixmap.fill(setColor); m_LabelListModel->itemFromIndex(index)->setIcon(QIcon(colorPixmap)); - - // Set color on label - m_AnnotationImage->GetActiveLabelSet()->GetLabel(m_LabelListModel->item(index.row(), 1)->text().toInt())->SetColor(QColorToMitkColor(setColor)); - m_AnnotationImage->GetActiveLabelSet()->UpdateLookupTable(m_LabelListModel->item(index.row(), 1)->text().toInt()); - PrintAllLabels(m_AnnotationImage); - - // If this is the label with value 1 we have to change the data node color - if (m_LabelListModel->item(index.row(), 1)->text().toInt() == 1) - { - m_AnnotationNode->SetColor(QColorToMitkColor(setColor)); - } + UpdateLookupTables(); } } } void ActiveLearning::OnLabelListSelectionChanged(const QItemSelection& selected, const QItemSelection& /*deselected*/) { if (selected.empty()) return; + if (m_Controls.m_EraseToolButton->isChecked()) return; + // This assumes that only one item can be selected (single selection table view) try { int labelValue = m_LabelListModel->item(selected.indexes()[0].row(), 1)->text().toInt(); m_Interactor->SetPaintingPixelValue(labelValue); - m_AnnotationImage->GetActiveLabelSet()->SetActiveLabel(labelValue); } catch (...) { - m_Interactor->SetPaintingPixelValue(0); - m_AnnotationImage->GetActiveLabelSet()->SetActiveLabel(0); + m_Interactor->SetPaintingPixelValue(-1); } } void ActiveLearning::OnLabelNameChanged(const QModelIndex& topLeft, const QModelIndex& /*bottomRight*/) { - auto item = m_LabelListModel->itemFromIndex(topLeft); - if (item->column() != 2) return; - m_AnnotationImage->GetActiveLabelSet()->GetLabel(m_LabelListModel->item(item->row(), 1)->text().toInt())->SetName(item->text().toStdString()); + } void ActiveLearning::OnInitializationFinished() { // Check if all futures are finished for (auto watcher : m_FeatureCalculationWatchers) { if (watcher->isFinished() == false) {return;} } // Empty feature vector m_FeatureImageVector.clear(); // Insert features into feature vector and data storage for (unsigned int i=0; iresult(); for (unsigned int j=0; jSetData(result[j].first); node->SetName(result[j].second); node->SetBoolProperty("helper object", true); node->SetVisibility(false); this->GetDataStorage()->Add(node, m_Nodes[i]); } } // Show controls m_Controls.m_LabelControlsFrame->setVisible(true); m_Controls.m_InitializePushButton->setHidden(true); MITK_INFO << "Features: " << m_FeatureImageVector.size(); // Delete watchers for (auto watcher : m_FeatureCalculationWatchers) { delete watcher; } m_FeatureCalculationWatchers.clear(); } void ActiveLearning::OnUpdatePredictionsPushButtonClicked() { if (m_PredictionMatrix == nullptr) { auto mat = Transform(m_FeatureImageVector); m_PredictionMatrix = std::make_shared(mat); } MITK_INFO << "Made test data"; auto training = GetTrainingData(m_AnnotationImage, m_FeatureImageVector); MITK_INFO << "Got training data"; m_Classifier->Train(*training.second, *training.first); MITK_INFO << "Trained"; Eigen::MatrixXi prediction = m_Classifier->Predict(*m_PredictionMatrix); MITK_INFO << "Predicted"; mitk::Image::Pointer referenceImage = dynamic_cast(m_Nodes[0]->GetData()); - mitk::Image::Pointer newImage = Transform(prediction, referenceImage); - auto newSegmentation = mitk::LabelSetImage::New(); - newSegmentation->InitializeByLabeledImage(newImage); - m_SegmentationImage = newSegmentation; + m_SegmentationImage = Transform(prediction, referenceImage); + m_SegmentationImage->Modified(); + m_SegmentationNode->SetData(m_SegmentationImage); m_SegmentationNode->Modified(); - int s = 5; - for (int i=0; iGetLayerImage(m_SegmentationImage->GetActiveLayer())->GetPixelValueByIndex({i, j, k}) << " "; - } - MITK_INFO << ss.str(); - } - } + UpdateLookupTables(); this->RequestRenderWindowUpdate(); } /* ================================================================== * PROTECTED * =============================================================== */ void ActiveLearning::OnSelectionChanged(berry::IWorkbenchPart::Pointer /*source*/, const QList& nodes) { if (!SelectionAllImages(nodes)) { SetInitializeReady(false); return; } if (nodes.length() >= 2) { // First selection is the reference (could be any other) mitk::Image::Pointer referenceImage = dynamic_cast(nodes[0]->GetData()); mitk::BaseGeometry* referenceGeometry = referenceImage->GetTimeGeometry()->GetGeometryForTimeStep(0); // Adjust for multiple timesteps for (int i=1; i(nodes[i]->GetData()); mitk::BaseGeometry* currentGeometry = currentImage->GetTimeGeometry()->GetGeometryForTimeStep(0); // Adjust for multiple timesteps if (!mitk::Equal(*currentGeometry, *referenceGeometry, mitk::eps, true)) { SetInitializeReady(false); return; } } } // All nodes have the same geometry, allow init SetInitializeReady(true); } void ActiveLearning::SetFocus() { } /* ================================================================== * PRIVATE * =============================================================== */ void ActiveLearning::SetInitializeReady(bool ready) { if (ready) { // get selection, check again just to be sure auto nodes = this->GetDataManagerSelection(); if (!SelectionAllImages(nodes)) return; m_Controls.m_InitializePushButton->setEnabled(true); if (!m_Active) { QString nameList = QString::fromStdString(nodes[0]->GetName()); if (nodes.length() >= 2) { for (int i=1; i"); nameList += QString::fromStdString(nodes[i]->GetName()); } } m_Controls.m_InitializeLabel->setText(nameList); } } else { m_Controls.m_InitializePushButton->setDisabled(true); if (!m_Active) { m_Controls.m_InitializeLabel->setText("Selected images must have matching geometries"); } } } diff --git a/Plugins/org.mitk.gui.qt.activelearning/src/internal/QmitkActiveLearning.h b/Plugins/org.mitk.gui.qt.activelearning/src/internal/QmitkActiveLearning.h index 010a04dd14..7af55921cf 100644 --- a/Plugins/org.mitk.gui.qt.activelearning/src/internal/QmitkActiveLearning.h +++ b/Plugins/org.mitk.gui.qt.activelearning/src/internal/QmitkActiveLearning.h @@ -1,153 +1,157 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef ActiveLearning_h #define ActiveLearning_h #include #include #include "ui_QmitkActiveLearningControls.h" // Qt #include #include #include #include // MITK #include #include #include #include /** \brief ActiveLearning \warning This class is not yet documented. Use "git blame" and ask the author to provide basic documentation. \sa QmitkAbstractView \ingroup ${plugin_target}_internal */ // Just a helper class class NotEditableDelegate : public QItemDelegate { Q_OBJECT public: explicit NotEditableDelegate(QObject* parent = nullptr) : QItemDelegate(parent) {} protected: QWidget* createEditor(QWidget*, const QStyleOptionViewItem&, const QModelIndex&) const {return Q_NULLPTR;} }; class ActiveLearning : public QmitkAbstractView { Q_OBJECT public slots: void Initialize(); public: + typedef mitk::LabelSetImage::PixelType AnnotationPixelType; + ActiveLearning(); ~ActiveLearning(); void CreateQtPartControl(QWidget *parent) override; std::vector> CalculateFeatures(const mitk::Image::Pointer inputImage); - std::pair, std::shared_ptr > GetTrainingData(const mitk::LabelSetImage::Pointer annotationImage, const std::vector featureImageVector); + std::pair, std::shared_ptr > GetTrainingData(const mitk::Image::Pointer annotationImage, const std::vector featureImageVector); + + void UpdateLookupTables(); static const std::string VIEW_ID; protected slots: void OnAddLabelPushButtonClicked(); void OnRemoveLabelPushButtonClicked(); void OnPaintToolButtonClicked(); void OnEraseToolButtonClicked(); void OnActivateGuidancePushButtonToggled(bool toggled); void OnSaveSegmentationPushButtonClicked(); void OnSavePredictionsPushButtonClicked(); void OnExportSegmentationPushButtonClicked(); void OnExportPredictionsPushButtonClicked(); void OnColorIconDoubleClicked(const QModelIndex& index); void OnLabelListSelectionChanged(const QItemSelection& selected, const QItemSelection& /*deselected*/); void OnLabelNameChanged(const QModelIndex& topLeft, const QModelIndex& /*bottomRight*/); void OnUpdatePredictionsPushButtonClicked(); void OnInitializationFinished(); protected: void OnSelectionChanged(berry::IWorkbenchPart::Pointer /*source*/, const QList& nodes) override; void SetFocus() override; void ResetLabels(); void SetInitializeReady(bool ready); Ui::ActiveLearningControls m_Controls; QWidget* m_Parent; - mitk::LabelSetImage::Pointer m_AnnotationImage; + mitk::Image::Pointer m_AnnotationImage; mitk::DataNode::Pointer m_AnnotationNode; mitk::Image::Pointer m_PredictionImage; mitk::DataNode::Pointer m_PredictionNode; - mitk::LabelSetImage::Pointer m_SegmentationImage; + mitk::Image::Pointer m_SegmentationImage; mitk::DataNode::Pointer m_SegmentationNode; std::vector m_FeatureImageVector; std::vector>>*> m_FeatureCalculationWatchers; QStandardItemModel* m_LabelListModel; mitk::ActiveLearningInteractor::Pointer m_Interactor; QList m_Nodes; mitk::VigraRandomForestClassifier::Pointer m_Classifier; private: bool m_Active; int m_NumberOfTrees; int m_MaximumTreeDepth; std::shared_ptr m_PredictionMatrix; }; #endif // ActiveLearning_h