From 85853d1103dfb32ecbfb0ab62d06af24b392300b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Traizet?= <cedric.traizet@c-s.fr> Date: Wed, 12 Sep 2018 10:47:52 +0200 Subject: [PATCH] ENH : code cleaning --- .../app/otbSmallRegionsMerging.cxx | 68 ++++------ .../otbLabelImageSmallRegionMergingFilter.h | 123 ++++++------------ .../otbLabelImageSmallRegionMergingFilter.hxx | 105 +++------------ 3 files changed, 79 insertions(+), 217 deletions(-) diff --git a/Modules/Applications/AppSegmentation/app/otbSmallRegionsMerging.cxx b/Modules/Applications/AppSegmentation/app/otbSmallRegionsMerging.cxx index 2a5cfc0074..963ab0fc4a 100644 --- a/Modules/Applications/AppSegmentation/app/otbSmallRegionsMerging.cxx +++ b/Modules/Applications/AppSegmentation/app/otbSmallRegionsMerging.cxx @@ -85,9 +85,8 @@ private: SetName("SmallRegionsMerging"); SetDescription("This application performs the third (optional) step of the exact Large-Scale Mean-Shift segmentation workflow [1]."); - SetDocName("Exact Large-Scale Mean-Shift segmentation, step 3 (optional)"); - SetDocLongDescription("Given a segmentation result (can be the out output parameter of the" - " LSMSSegmentation application [2]) and the original image, it will" + SetDocName("Small Region Merging"); + SetDocLongDescription("Given a segmentation result and the original image, it will" " merge segments whose size in pixels is lower than minsize parameter" " with the adjacent segments with the adjacent segment with closest" " radiometry and acceptable size.\n\n" @@ -96,22 +95,11 @@ private: " segments, then all segments of area equal to 2 pixels will be processed," " until segments of area minsize. For large images one can use the" " tilesizex and tilesizey parameters for tile-wise processing, with the" - " guarantees of identical results.\n\n" - "The output of this application can be passed to the" - " LSMSVectorization application [3] to complete the LSMS workflow."); - SetDocLimitations("This application is part of the Large-Scale Mean-Shift segmentation" - " workflow (LSMS) and may not be suited for any other purpose. This" - " application is not compatible with in-memory connection since it does" - " its own internal streaming."); - SetDocAuthors("David Youssefi"); - SetDocSeeAlso( "[1] Michel, J., Youssefi, D., & Grizonnet, M. (2015). Stable" - " mean-shift algorithm and its application to the segmentation of" - " arbitrarily large remote sensing images. IEEE Transactions on" - " Geoscience and Remote Sensing, 53(2), 952-964.\n" - "[2] LSMSegmentation\n" - "[3] LSMSVectorization"); + " guarantees of identical results.\n\n"); + SetDocLimitations("This application is more efficient if the labels are contiguous, starting from 0."); + SetDocAuthors("OTB-Team"); + SetDocSeeAlso( "Segmentation"); AddDocTag(Tags::Segmentation); - AddDocTag("LSMS"); AddParameter(ParameterType_InputImage, "in", "Input image"); SetParameterDescription( "in", "The input image, containing initial spectral signatures corresponding to the segmented image (inseg)." ); @@ -123,9 +111,9 @@ private: SetDefaultOutputPixelType("out",ImagePixelType_uint32); AddParameter(ParameterType_Int, "minsize", "Minimum Segment Size"); - SetParameterDescription("minsize", "Minimum Segment Size. If, after the segmentation, a segment is of size lower than this criterion, the segment is merged with the segment that has the closest sepctral signature."); + SetParameterDescription("minsize", "Minimum Segment Size. If, after the segmentation, a segment is of size strictly lower than this criterion, the segment is merged with the segment that has the closest sepctral signature."); SetDefaultParameterInt("minsize", 50); - SetMinimumParameterIntValue("minsize", 0); + SetMinimumParameterIntValue("minsize", 1); MandatoryOff("minsize"); AddRAMParameter(); @@ -135,8 +123,6 @@ private: SetDocExampleParameterValue("inseg","segmentation.tif"); SetDocExampleParameterValue("out","merged.tif"); SetDocExampleParameterValue("minsize","20"); - SetDocExampleParameterValue("tilesizex","256"); - SetDocExampleParameterValue("tilesizey","256"); SetOfficialDocLink(); } @@ -163,52 +149,43 @@ private: AddProcess(labelStatsFilter->GetStreamer() , "Computing stats on input image ..."); labelStatsFilter->Update(); - // Merge small segments - auto regionMergingFilter = LabelImageSmallRegionMergingFilterType::New(); - regionMergingFilter->SetInputLabelImage( labelIn ); - regionMergingFilter->SetInputSpectralImage( imageIn ); - + // Convert Map to Vector auto labelPopulationMap = labelStatsFilter->GetLabelPopulationMap(); std::vector<double> labelPopulation; - for (int i =0; i <= labelPopulationMap.rbegin()->first; i++) + for (unsigned int i =0; i <= labelPopulationMap.rbegin()->first; i++) { labelPopulation.push_back(labelPopulationMap[i]); } auto meanValueMap = labelStatsFilter->GetMeanValueMap(); std::vector<itk::VariableLengthVector<double> > meanValues; - for (int i =0; i <= meanValueMap.rbegin()->first; i++) + for (unsigned int i =0; i <= meanValueMap.rbegin()->first; i++) { meanValues.push_back(meanValueMap[i]); } - //regionMergingFilter->SetLabelPopulation( labelStatsFilter->GetLabelPopulationMap() ); + // Merge small segments + auto regionMergingFilter = LabelImageSmallRegionMergingFilterType::New(); + regionMergingFilter->SetInputLabelImage( labelIn ); regionMergingFilter->SetLabelPopulation( labelPopulation ); regionMergingFilter->SetLabelStatistic( meanValues ); - clock_t tic2 = clock(); + for (unsigned int size = 1 ; size < minSize ; size++) { regionMergingFilter->SetSize( size ); regionMergingFilter->Update(); } - clock_t toc2 = clock(); - std::cout <<"Elapsed timeaazaed : "<<(double)(toc2 - tic2) / CLOCKS_PER_SEC<<" seconds" << std::endl; + //Relabelling auto changeLabelFilter = ChangeLabelImageFilterType::New(); changeLabelFilter->SetInput(labelIn); - auto correspondanceMap = regionMergingFilter->GetCorrespondanceMap(); - /*for (auto correspondance : correspondanceMap) - { - if (correspondance.first != correspondance.second) - { - changeLabelFilter->SetChange(correspondance.first, correspondance.second); - } - }*/ //TODO - for(int i = 0; i<correspondanceMap.size(); ++i) + auto LUT = regionMergingFilter->GetLUT(); + + for(unsigned int i = 0; i<LUT.size(); ++i) { - if(i!=correspondanceMap[i]) + if(i!=LUT[i]) { - std::cout << i << " " << correspondanceMap[i] << std::endl; - changeLabelFilter->SetChange(i,correspondanceMap[i]); + std::cout << i << " " << LUT[i] << std::endl; + changeLabelFilter->SetChange(i,LUT[i]); } } SetParameterOutputImage("out", changeLabelFilter->GetOutput()); @@ -217,6 +194,7 @@ private: otbAppLogINFO(<<"Elapsed time: "<<(double)(toc - tic) / CLOCKS_PER_SEC<<" seconds"); } + }; } } diff --git a/Modules/Segmentation/Conversion/include/otbLabelImageSmallRegionMergingFilter.h b/Modules/Segmentation/Conversion/include/otbLabelImageSmallRegionMergingFilter.h index 69e3850099..f6d21575a3 100644 --- a/Modules/Segmentation/Conversion/include/otbLabelImageSmallRegionMergingFilter.h +++ b/Modules/Segmentation/Conversion/include/otbLabelImageSmallRegionMergingFilter.h @@ -21,19 +21,13 @@ #ifndef otbLabelImageSmallRegionMergingFilter_h #define otbLabelImageSmallRegionMergingFilter_h -#include "otbImage.h" -#include "otbVectorImage.h" -#include "itkImageToImageFilter.h" - #include "otbPersistentImageFilter.h" #include "otbPersistentFilterStreamingDecorator.h" -#include <set> namespace otb { /** \class PersistentLabelImageSmallRegionMergingFilter - * * * This class merges regions in the input label image according to the input * image of spectral values and the RangeBandwidth parameter. @@ -73,92 +67,64 @@ public: typedef TInputLabelImage InputLabelImageType; typedef typename InputLabelImageType::PixelType InputLabelType; - typedef TInputSpectralImage InputSpectralImageType; - typedef typename TInputSpectralImage::PixelType SpectralPixelType; - typedef itk::VariableLengthVector<double> RealVectorPixelType; - //typedef std::map<InputLabelType, double> LabelPopulationMapType; - typedef std::vector<double> LabelPopulationMapType; - typedef std::map<InputLabelType, std::set<InputLabelType> > NeigboursMapType; - //typedef std::map<InputLabelType, RealVectorPixelType > LabelStatisticMapType; - typedef std::vector<RealVectorPixelType > LabelStatisticMapType; - - - //typedef std::map<InputLabelType, InputLabelType> CorrespondanceMapType; - typedef std::vector<double> CorrespondanceMapType; - - - - /** Sets the input image where the value of a pixel is the region id */ - void SetInputLabelImage( const InputLabelImageType * labelImage); - /** Sets the input image representing spectral values */ - void SetInputSpectralImage( const InputSpectralImageType * spectralImage); - /** Returns input label image */ - InputLabelImageType * GetInputLabelImage(); - /** Returns input spectral image */ - InputSpectralImageType * GetInputSpectralImage(); + typedef std::vector<RealVectorPixelType > LabelStatisticType; + typedef std::vector<double> LabelPopulationType; + typedef std::vector<InputLabelType> LUTType; /** Set/Get size of polygon to be merged */ itkGetMacro(Size , unsigned int); itkSetMacro(Size , unsigned int); - /** Set/Get the Label population map and initialize the correspondance map*/ - /*void SetLabelPopulation( LabelPopulationMapType const & labelPopulation ) - { - m_LabelPopulation = labelPopulation; - // Initialize m_CorrespondingMap to the identity (i.e. m[label] = label) - for (auto label : labelPopulation) - { - m_CorrespondanceMap[ label.first ] = label.first; - } - } - */ - void SetLabelPopulation( LabelPopulationMapType const & labelPopulation ) + /** Set the Label population and initialize the LUT */ + void SetLabelPopulation( LabelPopulationType const & labelPopulation ) { m_LabelPopulation = labelPopulation; // Initialize m_CorrespondingMap to the identity (i.e. m[label] = label) - m_CorrespondanceMap.resize( labelPopulation.size() ); - for (int i =0; i <labelPopulation.size(); i++) + m_LUT.resize( labelPopulation.size() ); + for (unsigned int i =0; i <labelPopulation.size(); i++) { - m_CorrespondanceMap[ i ] = i; + m_LUT[ i ] = i; } } - LabelPopulationMapType const & GetLabelPopulation() const + /** Get the Label population */ + LabelPopulationType const & GetLabelPopulation() const { return m_LabelPopulation; } - void SetLabelStatistic( LabelStatisticMapType const & labelStatistic ) + /** Set the label statistic */ + void SetLabelStatistic( LabelStatisticType const & labelStatistic ) { m_LabelStatistic = labelStatistic; } - LabelStatisticMapType const & GetLabelStatistic() const + /** Get the label statistic */ + LabelStatisticType const & GetLabelStatistic() const { return m_LabelStatistic; } - CorrespondanceMapType const & GetCorrespondanceMap() const + /** Get the LUT */ + LUTType const & GetLUT() const { - return m_CorrespondanceMap; + return m_LUT; } virtual void Reset(void); virtual void Synthetize(void); protected: - //void EnlargeOutputRequestedRegion( itk::DataObject *output ) override; - void GenerateOutputInformation(void) override; void ThreadedGenerateData(const RegionType& outputRegionForThread, itk::ThreadIdType threadId) override; - // Use m_CorrespondanceMap recurively to find the label corresponding to the input label + // Use m_LUT recurively to find the label corresponding to the input label InputLabelType FindCorrespondingLabel( InputLabelType label); /** Constructor */ @@ -175,15 +141,18 @@ private: void operator =(const Self&) = delete; unsigned int m_Size; - LabelPopulationMapType m_LabelPopulation; - LabelStatisticMapType m_LabelStatistic; + /** Vector containing at position i the population of the segment labelled i */ + LabelPopulationType m_LabelPopulation; + + /** Vector containing at position i the population of mean of element of the segment labelled i*/ + LabelStatisticType m_LabelStatistic; - // Neigbours maps for each thread + /** Neigbours maps for each thread */ std::vector <NeigboursMapType > m_NeighboursMapsTmp; - CorrespondanceMapType m_CorrespondanceMap; - //NeigboursMapType m_NeighboursMap; + /** LUT giving correspondance between labels in the original segmentation and the merged labels */ + LUTType m_LUT; }; /** \class LabelImageSmallRegionMergingFilter @@ -218,75 +187,61 @@ public: typedef PersistentLabelImageSmallRegionMergingFilter<TInputLabelImage, TInputSpectralImage> PersistentFilterType; typedef typename PersistentFilterType::InputLabelImageType InputLabelImageType; - typedef typename PersistentFilterType::InputSpectralImageType InputSpectralImageType; - typedef typename PersistentFilterType::LabelPopulationMapType LabelPopulationMapType; - typedef typename PersistentFilterType::LabelStatisticMapType LabelStatisticMapType; - typedef typename PersistentFilterType::CorrespondanceMapType CorrespondanceMapType; + typedef typename PersistentFilterType::LabelPopulationType LabelPopulationType; + typedef typename PersistentFilterType::LabelStatisticType LabelStatisticType; + typedef typename PersistentFilterType::LUTType LUTType; /** Sets the input image where the value of a pixel is the region id */ void SetInputLabelImage( const InputLabelImageType * labelImage) { - this->GetFilter()->SetInputLabelImage( labelImage ); - } - - - /** Sets the input image representing spectral values */ - void SetInputSpectralImage( const InputSpectralImageType * spectralImage) - { - this->GetFilter()->SetInputSpectralImage( spectralImage ); + this->GetFilter()->SetInput( labelImage ); } /** Returns input label image */ InputLabelImageType * GetInputLabelImage() { - return this->GetFilter()->GetInputLabelImage(); - } - - /** Returns input spectral image */ - InputSpectralImageType * GetInputSpectralImage() - { - return this->GetFilter()->GetInputSpectralImage(); + return this->GetFilter()->GetInput(); } - /** Set size of polygon to be merged */ + /** Set size of segments to be merged */ void SetSize(unsigned int size) { this->GetFilter()->SetSize( size ); } - /** Get size of polygon to be merged */ + /** Get size of segments to be merged */ unsigned int GetSize() { return this->GetFilter()->GetSize(); } /** Set the Label population map */ - void SetLabelPopulation( LabelPopulationMapType const & labelPopulation ) + void SetLabelPopulation( LabelPopulationType const & labelPopulation ) { this->GetFilter()->SetLabelPopulation( labelPopulation ); } /** Get the Label population map */ - LabelPopulationMapType const & GetLabelPopulation( ) const + LabelPopulationType const & GetLabelPopulation( ) const { return this->GetFilter()->GetLabelPopulation(); } /** Set the Label statistic map */ - void SetLabelStatistic( LabelStatisticMapType const & labelStatistic ) + void SetLabelStatistic( LabelStatisticType const & labelStatistic ) { this->GetFilter()->SetLabelStatistic( labelStatistic ); } /** Get the Label statistic map */ - LabelStatisticMapType const & GetLabelStatistic( ) const + LabelStatisticType const & GetLabelStatistic( ) const { return this->GetFilter()->GetLabelStatistic(); } - CorrespondanceMapType const & GetCorrespondanceMap() const + LUTType const & GetLUT() const { - return this->GetFilter()->GetCorrespondanceMap(); + return this->GetFilter()->GetLUT(); } diff --git a/Modules/Segmentation/Conversion/include/otbLabelImageSmallRegionMergingFilter.hxx b/Modules/Segmentation/Conversion/include/otbLabelImageSmallRegionMergingFilter.hxx index 6c7a273b48..0b8aba913d 100644 --- a/Modules/Segmentation/Conversion/include/otbLabelImageSmallRegionMergingFilter.hxx +++ b/Modules/Segmentation/Conversion/include/otbLabelImageSmallRegionMergingFilter.hxx @@ -36,45 +36,10 @@ PersistentLabelImageSmallRegionMergingFilter<TInputLabelImage, TInputSpectralIma { } -template <class TInputLabelImage, class TInputSpectralImage> -void -PersistentLabelImageSmallRegionMergingFilter<TInputLabelImage, TInputSpectralImage> -::SetInputLabelImage( const TInputLabelImage * labelImage) -{ - // Process object is not const-correct so the const casting is required. - this->SetNthInput(0, const_cast<TInputLabelImage *>( labelImage )); -} - -template <class TInputLabelImage, class TInputSpectralImage> -void -PersistentLabelImageSmallRegionMergingFilter<TInputLabelImage, TInputSpectralImage> -::SetInputSpectralImage( const TInputSpectralImage * spectralImage) -{ - // Process object is not const-correct so the const casting is required. - this->SetNthInput(1, const_cast<TInputSpectralImage *>( spectralImage )); -} - -template <class TInputLabelImage, class TInputSpectralImage> -TInputLabelImage * -PersistentLabelImageSmallRegionMergingFilter<TInputLabelImage, TInputSpectralImage> -::GetInputLabelImage() -{ - return dynamic_cast<TInputLabelImage*>(itk::ProcessObject::GetInput(0)); -} - -template <class TInputLabelImage, class TInputSpectralImage> -TInputSpectralImage * -PersistentLabelImageSmallRegionMergingFilter<TInputLabelImage, TInputSpectralImage> -::GetInputSpectralImage() -{ - return dynamic_cast<TInputSpectralImage*>(itk::ProcessObject::GetInput(1)); -} - template <class TInputLabelImage, class TInputSpectralImage> PersistentLabelImageSmallRegionMergingFilter<TInputLabelImage, TInputSpectralImage> ::~PersistentLabelImageSmallRegionMergingFilter() { - } @@ -93,7 +58,6 @@ void PersistentLabelImageSmallRegionMergingFilter<TInputLabelImage, TInputSpectralImage> ::Synthetize() { - clock_t tic = clock(); NeigboursMapType neighboursMap; // Merge the neighbours maps from all threads for( unsigned int threadId = 0; threadId < this->GetNumberOfThreads(); threadId++) @@ -118,11 +82,10 @@ PersistentLabelImageSmallRegionMergingFilter<TInputLabelImage, TInputSpectralIma auto statsNeighbour = m_LabelStatistic[ neighbour ]; assert( statsLabel.Size() == statsNeighbour.Size() ); double distance = 0; - for (int i = 0 ; i < statsLabel.Size(); i++) + for (unsigned int i = 0 ; i < statsLabel.Size(); i++) { distance += pow( statsLabel[i] - statsNeighbour[i] , 2); } - //std::cout << label << " " << neighbour << " " << distance << " " << m_LabelStatistic[ neighbour ] <<std::endl; if (distance < proximity) { proximity = distance; @@ -133,51 +96,40 @@ PersistentLabelImageSmallRegionMergingFilter<TInputLabelImage, TInputSpectralIma auto curLabelLUT = label; auto adjLabelLUT = closestNeighbour; std::cout << label << " " << closestNeighbour; - while(m_CorrespondanceMap[curLabelLUT] != curLabelLUT) + while(m_LUT[curLabelLUT] != curLabelLUT) { - curLabelLUT = m_CorrespondanceMap[curLabelLUT]; + curLabelLUT = m_LUT[curLabelLUT]; } - while(m_CorrespondanceMap[adjLabelLUT] != adjLabelLUT) + while(m_LUT[adjLabelLUT] != adjLabelLUT) { - adjLabelLUT = m_CorrespondanceMap[adjLabelLUT]; + adjLabelLUT = m_LUT[adjLabelLUT]; } if(curLabelLUT < adjLabelLUT) { - m_CorrespondanceMap[adjLabelLUT] = curLabelLUT; + m_LUT[adjLabelLUT] = curLabelLUT; } else { - m_CorrespondanceMap[m_CorrespondanceMap[curLabelLUT]] = adjLabelLUT; - m_CorrespondanceMap[curLabelLUT] = adjLabelLUT; + m_LUT[m_LUT[curLabelLUT]] = adjLabelLUT; + m_LUT[curLabelLUT] = adjLabelLUT; } - - - - - //m_CorrespondanceMap[label] = closestNeighbour; - - // Update Stats - /*m_LabelStatistic[closestNeighbour] = (m_LabelStatistic[closestNeighbour]*m_LabelPopulation[closestNeighbour] + - m_LabelStatistic[label]*m_LabelPopulation[label] ) / (m_LabelPopulation[label]+m_LabelPopulation[closestNeighbour]); - m_LabelPopulation[closestNeighbour] += m_LabelPopulation[label];*/ - } - for(InputLabelType label = 0; label < m_CorrespondanceMap.size(); ++label) + for(InputLabelType label = 0; label < m_LUT.size(); ++label) { InputLabelType can = label; - while(m_CorrespondanceMap[can] != can) + while(m_LUT[can] != can) { - can = m_CorrespondanceMap[can]; + can = m_LUT[can]; } - m_CorrespondanceMap[label] = can; + m_LUT[label] = can; } - for(InputLabelType label = 0; label < m_CorrespondanceMap.size(); ++label) + for(InputLabelType label = 0; label < m_LUT.size(); ++label) { - InputLabelType correspondingLabel = m_CorrespondanceMap[label]; + InputLabelType correspondingLabel = m_LUT[label]; if((m_LabelPopulation[label]!=0) && (correspondingLabel != label)) { @@ -188,16 +140,6 @@ PersistentLabelImageSmallRegionMergingFilter<TInputLabelImage, TInputSpectralIma } } - /* - // We have to update corresponding label to propagate the correspondance between the labels. - for (auto & corres : m_CorrespondanceMap) - { - corres = FindCorrespondingLabel(corres); - // corres.second = FindCorrespondingLabel(corres.second); //TODO - }*/ - clock_t toc = clock(); - - std::cout << "Synthetize : " << (double)(toc - tic) / CLOCKS_PER_SEC << std::endl; } template <class TInputLabelImage, class TInputSpectralImage> @@ -206,11 +148,11 @@ PersistentLabelImageSmallRegionMergingFilter<TInputLabelImage, TInputSpectralIma ::FindCorrespondingLabel( typename PersistentLabelImageSmallRegionMergingFilter<TInputLabelImage, TInputSpectralImage> ::InputLabelType label) { - auto correspondingLabel = m_CorrespondanceMap[label]; + auto correspondingLabel = m_LUT[label]; while (label != correspondingLabel) { label = correspondingLabel; - correspondingLabel = m_CorrespondanceMap[correspondingLabel]; + correspondingLabel = m_LUT[correspondingLabel]; } return correspondingLabel; } @@ -230,17 +172,13 @@ void PersistentLabelImageSmallRegionMergingFilter<TInputLabelImage, TInputSpectralImage> ::ThreadedGenerateData(const RegionType& outputRegionForThread, itk::ThreadIdType threadId ) { - - clock_t tic = clock(); using IteratorType = itk::ImageRegionConstIterator< TInputLabelImage >; using NeighborhoodIteratorType = itk::ConstShapedNeighborhoodIterator< TInputLabelImage >; - typename NeighborhoodIteratorType::RadiusType radius; radius.Fill(1); - auto labelImage = this->GetInputLabelImage(); - + auto labelImage = this->GetInput(); IteratorType it(labelImage, outputRegionForThread); NeighborhoodIteratorType itN(radius, labelImage, outputRegionForThread); @@ -260,25 +198,16 @@ PersistentLabelImageSmallRegionMergingFilter<TInputLabelImage, TInputSpectralIma assert( !itN.IsAtEnd() ); int currentLabel = FindCorrespondingLabel(it.Get()); - //if ( it.Get() == m_Size ) if ( m_LabelPopulation[currentLabel] == m_Size ) { for (auto ci = itN.Begin() ; !ci.IsAtEnd(); ci++) { - //int neighbourLabel = ci.Get(); int neighbourLabel = FindCorrespondingLabel(ci.Get() ); - //if (neighbourLabel != it.Get() && m_LabelPopulation[neighbourLabel] > m_Size) if (neighbourLabel != currentLabel) m_NeighboursMapsTmp[threadId][ currentLabel ].insert( neighbourLabel ); } } } - - clock_t toc = clock(); - - if (threadId==0) - std::cout << threadId << " " << this->GetNumberOfThreads() << " Elapsed time : " << (double)(toc - tic) / CLOCKS_PER_SEC << std::endl; - } template <class TInputLabelImage, class TInputSpectralImage> -- GitLab