diff --git a/app/otbPatchesExtraction.cxx b/app/otbPatchesExtraction.cxx index a3da71f3563cb176a146af6f4e6dd8f3d86cf171..b00c3a1706ae32973c3f98e96b3acb5b4621939c 100644 --- a/app/otbPatchesExtraction.cxx +++ b/app/otbPatchesExtraction.cxx @@ -102,7 +102,7 @@ public: AddParameter(ParameterType_Int, ss_key_dims_y.str(), ss_desc_dims_y.str()); SetMinimumParameterIntValue (ss_key_dims_y.str(), 1); AddParameter(ParameterType_Float, ss_key_nodata.str(), ss_desc_nodata.str()); - SetDefaultParameterFloat (ss_key_nodata.str(), 0); + MandatoryOff (ss_key_nodata.str()); // Add a new bundle SourceBundle bundle; @@ -132,7 +132,11 @@ public: bundle.m_PatchSize[1] = GetParameterInt(bundle.m_KeyPszY); // No data value - bundle.m_NoDataValue = GetParameterFloat(bundle.m_KeyNoData); + if (HasValue(bundle.m_KeyNoData)) + { + bundle.m_NoDataValue = GetParameterFloat(bundle.m_KeyNoData); + } + } } @@ -169,10 +173,6 @@ public: // Input vector data AddParameter(ParameterType_InputVectorData, "vec", "Positions of the samples (must be in the same projection as input image)"); - // No data parameters - AddParameter(ParameterType_Bool, "usenodata", "Reject samples that have no-data value"); - MandatoryOff ("usenodata"); - // Output label AddParameter(ParameterType_OutputImage, "outlabels", "output labels"); SetDefaultOutputPixelType ("outlabels", ImagePixelType_uint8); @@ -201,14 +201,18 @@ public: SamplerType::Pointer sampler = SamplerType::New(); sampler->SetInputVectorData(GetParameterVectorData("vec")); sampler->SetField(GetParameterAsString("field")); - if (GetParameterInt("usenodata")==1) - { - otbAppLogINFO("Rejecting samples that have at least one no-data value"); - sampler->SetRejectPatchesWithNodata(true); - } + for (auto& bundle: m_Bundles) { - sampler->PushBackInputWithPatchSize(bundle.m_ImageSource.Get(), bundle.m_PatchSize, bundle.m_NoDataValue); + if (HasValue(bundle.m_KeyNoData)) + { + otbAppLogINFO("Rejecting samples that have at least one no-data value"); + sampler->PushBackInputWithPatchSize(bundle.m_ImageSource.Get(), bundle.m_PatchSize, bundle.m_NoDataValue); + } + else + { + sampler->PushBackInputWithPatchSize(bundle.m_ImageSource.Get(), bundle.m_PatchSize); + } } // Run the filter diff --git a/app/otbPatchesSelection.cxx b/app/otbPatchesSelection.cxx index 5d8165a019764a50a5f3301877cb364108c1f92c..68d76221dbf6b7ffbb828a751420a854d58864a7 100644 --- a/app/otbPatchesSelection.cxx +++ b/app/otbPatchesSelection.cxx @@ -1,7 +1,7 @@ /*========================================================================= Copyright (c) 2018-2019 IRSTEA - Copyright (c) 2020-2021 INRAE + Copyright (c) 2020-2022 INRAE This software is distributed WITHOUT ANY WARRANTY; without even @@ -26,11 +26,15 @@ #include "itkNearestNeighborInterpolateImageFunction.h" #include "itkMaskImageFilter.h" -// image utils +// Image utils #include "otbTensorflowCommon.h" #include "otbTensorflowSamplingUtils.h" #include "itkImageRegionConstIteratorWithOnlyIndex.h" +// Math +#include <random> +#include <limits> + // Functor to retrieve nodata template<class TPixel, class OutputPixel> class IsNoData @@ -119,10 +123,7 @@ public: // Input no-data value AddParameter(ParameterType_Float, "nodata", "nodata value"); - MandatoryOn ("nodata"); - SetDefaultParameterFloat ("nodata", 0); - AddParameter(ParameterType_Bool, "nocheck", "If on, no check on the validity of patches is performed"); - MandatoryOff ("nocheck"); + MandatoryOff ("nodata"); // Grid AddParameter(ParameterType_Group, "grid", "grid settings"); @@ -137,7 +138,27 @@ public: // Strategy AddParameter(ParameterType_Choice, "strategy", "Selection strategy for validation/training patches"); - AddChoice("strategy.chessboard", "fifty fifty, like a chess board"); + // Chess board + AddChoice("strategy.chessboard", "Fifty fifty with chess-board-like layout. Only \"outtrain\" and " + "\"outvalid\" output parameters are used."); + // Split + AddChoice("strategy.split", "The traditional training/validation/test split. The \"outtrain\", " + "\"outvalid\" and \"outtest\" output parameters are used."); + AddParameter(ParameterType_Bool, "strategy.split.random", "If false, samples will always be from " + "the same group"); + MandatoryOff ("strategy.split.random"); + AddParameter(ParameterType_Float, "strategy.split.trainprop", "Proportion of training population."); + SetMinimumParameterFloatValue ("strategy.split.trainprop", 0.0); + SetDefaultParameterFloat ("strategy.split.trainprop", 50.0); + AddParameter(ParameterType_Float, "strategy.split.validprop", "Proportion of validation population."); + SetMinimumParameterFloatValue ("strategy.split.validprop", 0.0); + SetDefaultParameterFloat ("strategy.split.validprop", 25.0); + AddParameter(ParameterType_Float, "strategy.split.testprop", "Proportion of test population."); + SetMinimumParameterFloatValue ("strategy.split.testprop", 0.0); + SetDefaultParameterFloat ("strategy.split.testprop", 25.0); + // All + AddChoice("strategy.all", "All locations. Only the \"outtrain\" output parameter is used."); + // Balanced (experimental) AddChoice("strategy.balanced", "you can chose the degree of spatial randomness vs class balance"); AddParameter(ParameterType_Float, "strategy.balanced.sp", "Spatial proportion: between 0 and 1, " "indicating the amount of randomly sampled data in space"); @@ -153,6 +174,9 @@ public: // Output points AddParameter(ParameterType_OutputVectorData, "outtrain", "output set of points (training)"); AddParameter(ParameterType_OutputVectorData, "outvalid", "output set of points (validation)"); + MandatoryOff("outvalid"); + AddParameter(ParameterType_OutputVectorData, "outtest", "output set of points (test)"); + MandatoryOff("outtest"); AddRAMParameter(); @@ -162,14 +186,14 @@ public: { public: SampleBundle(){} - explicit SampleBundle(unsigned int nClasses): dist(DistributionType(nClasses)), id(0), black(true){ + explicit SampleBundle(unsigned int nClasses): dist(DistributionType(nClasses)), id(0), group(true){ (void) point; (void) index; } ~SampleBundle(){} SampleBundle(const SampleBundle & other): dist(other.GetDistribution()), id(other.GetSampleID()), - point(other.GetPosition()), black(other.GetBlack()), index(other.GetIndex()) + point(other.GetPosition()), group(other.GetGroup()), index(other.GetIndex()) {} DistributionType GetDistribution() const @@ -202,14 +226,14 @@ public: return point; } - bool& GetModifiableBlack() + int& GetModifiableGroup() { - return black; + return group; } - bool GetBlack() const + int GetGroup() const { - return black; + return group; } UInt8ImageType::IndexType& GetModifiableIndex() @@ -227,7 +251,7 @@ public: DistributionType dist; unsigned int id; DataNodePointType point; - bool black; + int group; UInt8ImageType::IndexType index; }; @@ -252,9 +276,9 @@ public: UInt8ImageType::Pointer inputImage; bool readInput = true; - if (GetParameterInt("nocheck")==1) + if (!HasValue("nodata")) { - otbAppLogINFO("\"nocheck\" mode is enabled. Input image pixels no-data values will not be checked."); + otbAppLogINFO("No value specified for no-data. Input image pixels no-data values will not be checked."); if (HasValue("mask")) { otbAppLogINFO("Using the provided \"mask\" parameter."); @@ -362,18 +386,19 @@ public: const UInt8ImageType::IndexType & pos, const UInt8ImageType::PointType & geo) { // Black or white - bool black = ((pos[0] + pos[1]) % 2 == 0); + int black = (pos[0] + pos[1]) % 2; bundle.GetModifiableSampleID() = count; bundle.GetModifiablePosition() = geo; - bundle.GetModifiableBlack() = black; + bundle.GetModifiableGroup() = black; bundle.GetModifiableIndex() = pos; count++; } /* - * Samples are placed at regular intervals + * Samples are placed at regular intervals with the same layout as a chessboard, + * in two groups (A: black, B: white) */ void SampleChessboard() { @@ -393,6 +418,75 @@ public: PopulateVectorData(bundles); } + void SetSplitBundle(SampleBundle & bundle, unsigned int & count, + const UInt8ImageType::IndexType & pos, const UInt8ImageType::PointType & geo, + const std::vector<int> & groups) + { + + bundle.GetModifiableGroup() = groups[count]; + bundle.GetModifiableSampleID() = count; + bundle.GetModifiablePosition() = geo; + bundle.GetModifiableIndex() = pos; + count++; + } + + /* + * Samples are split in training/validation/test groups + */ + void SampleSplit(float trp, float vp, float tp) + { + + std::vector<SampleBundle> bundles = AllocateSamples(); + + // Populate groups + unsigned int nbSamples = bundles.size(); + float tot = (trp + vp + tp); + std::vector<float> props = {trp, vp, tp}; + std::vector<float> incs, counts; + for (auto& prop: props) + { + if (prop > 0) + { + incs.push_back(tot / prop); + counts.push_back(.0); + } + else + { + incs.push_back(.0); + counts.push_back((float) nbSamples); + } + } + std::vector<int> groups; + for (unsigned int i = 0; i < nbSamples; i++) + { + // Find the group with the less samples + auto it = std::min_element(std::begin(counts), std::end(counts)); + auto idx = std::distance(std::begin(counts), it); + assert (idx > 0); + // Assign the group number, and update counts + groups.push_back(idx); + counts[idx] += incs[idx]; + } + if (GetParameterInt("strategy.split.random") > 0) + { + // Shuffle groups + auto rng = std::default_random_engine {}; + std::shuffle(std::begin(groups), std::end(groups), rng); + } + + unsigned int count = 0; + auto lambda = [this, &count, &bundles, &groups] + (const UInt8ImageType::IndexType & pos, const UInt8ImageType::PointType & geo) { + SetSplitBundle(bundles[count], count, pos, geo, groups); + }; + + Apply(lambda); + bundles.resize(count); + + // Export training/validation samples + PopulateVectorData(bundles); + } + void SampleBalanced() { @@ -540,14 +634,19 @@ public: // Get data tree DataTreeType::Pointer treeTrain = m_OutVectorDataTrain->GetDataTree(); DataTreeType::Pointer treeValid = m_OutVectorDataValid->GetDataTree(); + DataTreeType::Pointer treeTest = m_OutVectorDataTest->GetDataTree(); DataNodePointer rootTrain = treeTrain->GetRoot()->Get(); DataNodePointer rootValid = treeValid->GetRoot()->Get(); + DataNodePointer rootTest = treeTest->GetRoot()->Get(); DataNodePointer documentTrain = DataNodeType::New(); DataNodePointer documentValid = DataNodeType::New(); + DataNodePointer documentTest = DataNodeType::New(); documentTrain->SetNodeType(DOCUMENT); documentValid->SetNodeType(DOCUMENT); + documentTest->SetNodeType(DOCUMENT); treeTrain->Add(documentTrain, rootTrain); treeValid->Add(documentValid, rootValid); + treeTest->Add(documentTest, rootTest); unsigned int id = 0; for (const auto& sample: samples) @@ -559,16 +658,21 @@ public: id++; // select this sample - if (sample.GetBlack()) + if (sample.GetGroup() == 0) { // Train treeTrain->Add(newDataNode, documentTrain); } - else + else if (sample.GetGroup() == 1) { // Valid treeValid->Add(newDataNode, documentValid); } + else if (sample.GetGroup() == 2) + { + // Test + treeTest->Add(newDataNode, documentTest); + } } } @@ -580,7 +684,12 @@ public: // Compute no-data mask m_NoDataFilter = IsNoDataFilterType::New(); - m_NoDataFilter->GetFunctor().SetNoDataValue(GetParameterFloat("nodata")); + float nodataValue = std::numeric_limits<float>::quiet_NaN(); + if (HasValue("nodata")) + { + nodataValue = GetParameterFloat("nodata"); + } + m_NoDataFilter->GetFunctor().SetNoDataValue(nodataValue); m_NoDataFilter->SetInput(GetParameterFloatVectorImage("in")); m_NoDataFilter->UpdateOutputInformation(); UInt8ImageType::Pointer src = m_NoDataFilter->GetOutput(); @@ -630,14 +739,21 @@ public: // Prepare output vector data m_OutVectorDataTrain = VectorDataType::New(); m_OutVectorDataValid = VectorDataType::New(); + m_OutVectorDataTest = VectorDataType::New(); m_OutVectorDataTrain->SetProjectionRef(m_MorphoFilter->GetOutput()->GetProjectionRef()); m_OutVectorDataValid->SetProjectionRef(m_MorphoFilter->GetOutput()->GetProjectionRef()); + m_OutVectorDataTest->SetProjectionRef(m_MorphoFilter->GetOutput()->GetProjectionRef()); if (GetParameterAsString("strategy") == "chessboard") { otbAppLogINFO("Sampling at regular interval in space (\"Chessboard\" like)"); SampleChessboard(); + + if (HasValue("outtest")) + { + otbAppLogWARNING("The \"outtest\" parameter is unused with the \"chessboard\" sampling strategy.") + } } else if (GetParameterAsString("strategy") == "balanced") { @@ -645,11 +761,45 @@ public: SampleBalanced(); } + else if (GetParameterAsString("strategy") == "split") + { + otbAppLogINFO("Sampling with split strategy (Train/Validation/test)"); + float vp = .0; + float tp = .0; + if (HasValue("outvalid")) + { + vp = GetParameterFloat("strategy.split.validprop"); + } + if (HasValue("outtest")) + { + tp = GetParameterFloat("strategy.split.testprop"); + } + + SampleSplit(GetParameterFloat("strategy.split.trainprop"), vp, tp); + } + else if (GetParameterAsString("strategy") == "all") + { + otbAppLogINFO("Sampling all locations (only \"outtrain\" output parameter will be used"); + + SampleSplit(1.0, .0, .0); + + if (HasValue("outtest") || HasValue("outvalid")) + { + otbAppLogWARNING("The \"outvalid\" and \"outtest\" parameters are unused with the \"all\" sampling strategy.") + } + } otbAppLogINFO( "Writing output samples positions"); SetParameterOutputVectorData("outtrain", m_OutVectorDataTrain); - SetParameterOutputVectorData("outvalid", m_OutVectorDataValid); + if (HasValue("outvalid") && GetParameterAsString("strategy") != "all") + { + SetParameterOutputVectorData("outvalid", m_OutVectorDataValid); + } + if (HasValue("outtest") && GetParameterAsString("strategy") == "split") + { + SetParameterOutputVectorData("outtest", m_OutVectorDataTest); + } } @@ -665,6 +815,7 @@ private: MorphoFilterType::Pointer m_MorphoFilter; VectorDataType::Pointer m_OutVectorDataTrain; VectorDataType::Pointer m_OutVectorDataValid; + VectorDataType::Pointer m_OutVectorDataTest; MaskImageFilterType::Pointer m_MaskImageFilter; }; // end of class diff --git a/include/otbTensorflowSampler.h b/include/otbTensorflowSampler.h index 4fae38e75245ca417c638105379ec7ff7dddf6dd..0252db09565f038468340e7c47b0342dfa4f4685 100644 --- a/include/otbTensorflowSampler.h +++ b/include/otbTensorflowSampler.h @@ -102,13 +102,11 @@ public: /** Set / get image */ virtual void PushBackInputWithPatchSize(const ImageType * input, SizeType & patchSize, InternalPixelType nodataval); + virtual void + PushBackInputWithPatchSize(const ImageType * input, SizeType & patchSize); const ImageType * GetInput(unsigned int index); - /** Set / get no-data related parameters */ - itkSetMacro(RejectPatchesWithNodata, bool); - itkGetMacro(RejectPatchesWithNodata, bool); - /** Do the real work */ virtual void Update(); @@ -144,8 +142,7 @@ private: unsigned long m_NumberOfRejectedSamples; // No data stuff - std::vector<InternalPixelType> m_NoDataValues; - bool m_RejectPatchesWithNodata; + std::map<unsigned int, InternalPixelType> m_NoDataValues; }; // end class diff --git a/include/otbTensorflowSampler.hxx b/include/otbTensorflowSampler.hxx index 77558c7ba08c6dc75ce8ced1d389a537150a68f7..966a37969c43ffdb9b7df32ea21dc8a0c7330dd2 100644 --- a/include/otbTensorflowSampler.hxx +++ b/include/otbTensorflowSampler.hxx @@ -22,7 +22,6 @@ TensorflowSampler<TInputImage, TVectorData>::TensorflowSampler() { m_NumberOfAcceptedSamples = 0; m_NumberOfRejectedSamples = 0; - m_RejectPatchesWithNodata = false; } template <class TInputImage, class TVectorData> @@ -33,7 +32,17 @@ TensorflowSampler<TInputImage, TVectorData>::PushBackInputWithPatchSize(const Im { this->ProcessObject::PushBackInput(const_cast<ImageType *>(input)); m_PatchSizes.push_back(patchSize); - m_NoDataValues.push_back(nodataval); + unsigned int index = m_PatchSizes.size() -1 ; + m_NoDataValues[index] = nodataval; +} + +template <class TInputImage, class TVectorData> +void +TensorflowSampler<TInputImage, TVectorData>::PushBackInputWithPatchSize(const ImageType * input, + SizeType & patchSize) +{ + this->ProcessObject::PushBackInput(const_cast<ImageType *>(input)); + m_PatchSizes.push_back(patchSize); } template <class TInputImage, class TVectorData> @@ -187,8 +196,8 @@ TensorflowSampler<TInputImage, TVectorData>::Update() // If not, reject this sample hasBeenSampled = false; } - // Check if the sampled patch contains a no-data value - if (m_RejectPatchesWithNodata && hasBeenSampled) + // If NoData is provided, check if the sampled patch contains a no-data value + if (m_NoDataValues.count(i) > 0 && hasBeenSampled) { IndexType outIndex; outIndex[0] = 0; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a4260dfe9103806d0ca716022da525da0ee06fca..1b4066913fa6d87382c52f9572fa215e9519f7bb 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -59,6 +59,10 @@ set(PATCHESPOS_01 ${TEMP}/out_train_32.gpkg) set(PATCHESPOS_02 ${TEMP}/out_valid_32.gpkg) set(PATCHESPOS_11 ${TEMP}/out_train_33.gpkg) set(PATCHESPOS_12 ${TEMP}/out_valid_33.gpkg) +set(PATCHESPOS_SPLIT1 vector_train.geojson) +set(PATCHESPOS_SPLIT2 vector_valid.geojson) +set(PATCHESPOS_SPLIT3 vector_test.geojson) +set(PATCHESPOS_ALL vector_all.geojson) # Even patches otb_test_application(NAME PatchesSelectionEven APP PatchesSelection @@ -81,6 +85,43 @@ otb_test_application(NAME PatchesSelectionOdd -outvalid ${PATCHESPOS_12} ) +# Split strategy +otb_test_application(NAME PatchesSelectionSplit + APP PatchesSelection + OPTIONS + -in ${IMAGEPXS} + -grid.step 32 + -grid.psize 32 + -strategy split + -outtrain ${TEMP}/${PATCHESPOS_SPLIT1} + -outvalid ${TEMP}/${PATCHESPOS_SPLIT2} + -outtest ${TEMP}/${PATCHESPOS_SPLIT3} + VALID --compare-ascii ${EPSILON_6} + ${DATADIR}/${PATCHESPOS_SPLIT1} + ${TEMP}/${PATCHESPOS_SPLIT1} + VALID --compare-ascii ${EPSILON_6} + ${DATADIR}/${PATCHESPOS_SPLIT2} + ${TEMP}/${PATCHESPOS_SPLIT2} + VALID --compare-ascii ${EPSILON_6} + ${DATADIR}/${PATCHESPOS_SPLIT3} + ${TEMP}/${PATCHESPOS_SPLIT3} + + ) + +# All strategy +otb_test_application(NAME PatchesSelectionAll + APP PatchesSelection + OPTIONS + -in ${IMAGEPXS} + -grid.step 32 + -grid.psize 32 + -strategy all + -outtrain ${TEMP}/${PATCHESPOS_ALL} + VALID --compare-ascii ${EPSILON_6} + ${DATADIR}/${PATCHESPOS_ALL} + ${TEMP}/${PATCHESPOS_ALL} + ) + #----------- Patches extraction ---------------- # Even patches otb_test_application(NAME PatchesExtractionEven diff --git a/test/data/vector_all.geojson b/test/data/vector_all.geojson new file mode 100644 index 0000000000000000000000000000000000000000..44116919b0893db7e847ac32b43aadb9f5f65e85 --- /dev/null +++ b/test/data/vector_all.geojson @@ -0,0 +1,15 @@ +{ +"type": "FeatureCollection", +"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::2154" } }, +"features": [ +{ "type": "Feature", "properties": { "id": 0 }, "geometry": { "type": "Point", "coordinates": [ 493965.0, 6444396.0 ] } }, +{ "type": "Feature", "properties": { "id": 1 }, "geometry": { "type": "Point", "coordinates": [ 494013.0, 6444396.0 ] } }, +{ "type": "Feature", "properties": { "id": 2 }, "geometry": { "type": "Point", "coordinates": [ 494061.0, 6444396.0 ] } }, +{ "type": "Feature", "properties": { "id": 3 }, "geometry": { "type": "Point", "coordinates": [ 493965.0, 6444348.0 ] } }, +{ "type": "Feature", "properties": { "id": 4 }, "geometry": { "type": "Point", "coordinates": [ 494013.0, 6444348.0 ] } }, +{ "type": "Feature", "properties": { "id": 5 }, "geometry": { "type": "Point", "coordinates": [ 494061.0, 6444348.0 ] } }, +{ "type": "Feature", "properties": { "id": 6 }, "geometry": { "type": "Point", "coordinates": [ 493965.0, 6444300.0 ] } }, +{ "type": "Feature", "properties": { "id": 7 }, "geometry": { "type": "Point", "coordinates": [ 494013.0, 6444300.0 ] } }, +{ "type": "Feature", "properties": { "id": 8 }, "geometry": { "type": "Point", "coordinates": [ 494061.0, 6444300.0 ] } } +] +} diff --git a/test/data/vector_test.geojson b/test/data/vector_test.geojson new file mode 100644 index 0000000000000000000000000000000000000000..0fbd2f0256b879c2e5347282f755790ec845e522 --- /dev/null +++ b/test/data/vector_test.geojson @@ -0,0 +1,8 @@ +{ +"type": "FeatureCollection", +"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::2154" } }, +"features": [ +{ "type": "Feature", "properties": { "id": 2 }, "geometry": { "type": "Point", "coordinates": [ 494061.0, 6444396.0 ] } }, +{ "type": "Feature", "properties": { "id": 6 }, "geometry": { "type": "Point", "coordinates": [ 493965.0, 6444300.0 ] } } +] +} diff --git a/test/data/vector_train.geojson b/test/data/vector_train.geojson new file mode 100644 index 0000000000000000000000000000000000000000..e536388981212ac30ca0d1198e9453afda36cf89 --- /dev/null +++ b/test/data/vector_train.geojson @@ -0,0 +1,11 @@ +{ +"type": "FeatureCollection", +"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::2154" } }, +"features": [ +{ "type": "Feature", "properties": { "id": 0 }, "geometry": { "type": "Point", "coordinates": [ 493965.0, 6444396.0 ] } }, +{ "type": "Feature", "properties": { "id": 3 }, "geometry": { "type": "Point", "coordinates": [ 493965.0, 6444348.0 ] } }, +{ "type": "Feature", "properties": { "id": 4 }, "geometry": { "type": "Point", "coordinates": [ 494013.0, 6444348.0 ] } }, +{ "type": "Feature", "properties": { "id": 7 }, "geometry": { "type": "Point", "coordinates": [ 494013.0, 6444300.0 ] } }, +{ "type": "Feature", "properties": { "id": 8 }, "geometry": { "type": "Point", "coordinates": [ 494061.0, 6444300.0 ] } } +] +} diff --git a/test/data/vector_valid.geojson b/test/data/vector_valid.geojson new file mode 100644 index 0000000000000000000000000000000000000000..3d64b131409939cd43a24ba2b7655f89c2c55b64 --- /dev/null +++ b/test/data/vector_valid.geojson @@ -0,0 +1,8 @@ +{ +"type": "FeatureCollection", +"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::2154" } }, +"features": [ +{ "type": "Feature", "properties": { "id": 1 }, "geometry": { "type": "Point", "coordinates": [ 494013.0, 6444396.0 ] } }, +{ "type": "Feature", "properties": { "id": 5 }, "geometry": { "type": "Point", "coordinates": [ 494061.0, 6444348.0 ] } } +] +}