Skip to content

Commit 082e5ce

Browse files
authored
Algorithm to filter noise (#75)
* filter noise tool * new function to store results of tests in output folder in tests/data * test cases * use utility functions * update tests
1 parent bf1d3f4 commit 082e5ce

File tree

5 files changed

+384
-0
lines changed

5 files changed

+384
-0
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ add_executable(pdal_wrench
1919
src/classify_ground.cpp
2020
src/clip.cpp
2121
src/density.cpp
22+
src/filter_noise.cpp
2223
src/info.cpp
2324
src/merge.cpp
2425
src/thin.cpp

src/alg.hpp

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,44 @@ struct ClassifyGround : public Alg
369369
virtual void finalize(std::vector<std::unique_ptr<PipelineManager>>& pipelines) override;
370370
};
371371

372+
373+
struct FilterNoise: public Alg
374+
{
375+
376+
FilterNoise() { isStreaming = false; }
377+
378+
std::vector<std::string> tileOutputFiles;
379+
380+
// parameters from the user
381+
std::string outputFile;
382+
std::string outputFormat; // las / laz / copc
383+
std::string algorithm = "statistical"; // "statistical" or "radius"
384+
385+
// radius params
386+
double radiusMinK = 2;
387+
double radiusRadius = 1.0;
388+
389+
// statistical params
390+
int statisticalMeanK = 8;
391+
double statisticalMultiplier = 2.0;
392+
393+
// args - initialized in addArgs()
394+
pdal::Arg* argOutput = nullptr;
395+
pdal::Arg* argOutputFormat = nullptr;
396+
pdal::Arg* argAlgorithm = nullptr;
397+
pdal::Arg* argRadiusMinK = nullptr;
398+
pdal::Arg* argRadiusRadius = nullptr;
399+
pdal::Arg* argStatisticalMeanK = nullptr;
400+
pdal::Arg* argStatisticalMultiplier = nullptr;
401+
402+
// impl
403+
virtual void addArgs() override;
404+
virtual bool checkArgs() override;
405+
virtual void preparePipelines(std::vector<std::unique_ptr<PipelineManager>>& pipelines) override;
406+
virtual void finalize(std::vector<std::unique_ptr<PipelineManager>>& pipelines) override;
407+
};
408+
409+
372410
struct HeightAboveGround : public Alg
373411
{
374412
HeightAboveGround() { isStreaming = false; }

src/filter_noise.cpp

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/*****************************************************************************
2+
* Copyright (c) 2025, Lutra Consulting Ltd. and Hobu, Inc. *
3+
* *
4+
* All rights reserved. *
5+
* *
6+
* This program is free software; you can redistribute it and/or modify *
7+
* it under the terms of the GNU General Public License as published by *
8+
* the Free Software Foundation; either version 3 of the License, or *
9+
* (at your option) any later version. *
10+
* *
11+
****************************************************************************/
12+
13+
#include <iostream>
14+
#include <filesystem>
15+
#include <thread>
16+
17+
#include <pdal/PipelineManager.hpp>
18+
#include <pdal/Stage.hpp>
19+
#include <pdal/util/ProgramArgs.hpp>
20+
#include <pdal/pdal_types.hpp>
21+
#include <pdal/Polygon.hpp>
22+
#include <pdal/PipelineWriter.hpp>
23+
24+
#include <gdal_utils.h>
25+
26+
#include "utils.hpp"
27+
#include "alg.hpp"
28+
#include "vpc.hpp"
29+
30+
using namespace pdal;
31+
32+
namespace fs = std::filesystem;
33+
34+
void FilterNoise::addArgs()
35+
{
36+
argOutput = &programArgs.add("output,o", "Output point cloud file", outputFile);
37+
argOutputFormat = &programArgs.add("output-format", "Output format (las/laz/copc)", outputFormat);
38+
39+
argAlgorithm = &programArgs.add("algorithm", "Noise filtering algorithm to use: statistical or radius.", algorithm, "statistical");
40+
41+
// radius args
42+
argRadiusMinK = &programArgs.add("radius-min-k", "Minimum number of neighbors in radius (radius algorithm only).", radiusMinK, 2.0);
43+
argRadiusRadius = &programArgs.add("radius-radius", "Radius (radius method only).", radiusRadius, 1.0);
44+
45+
// statistical args
46+
argStatisticalMeanK = &programArgs.add("statistical-mean-k", "Mean number of neighbors (statistical method only)", statisticalMeanK, 8);
47+
argStatisticalMultiplier = &programArgs.add("statistical-multiplier", "Standard deviation threshold (statistical method only).", statisticalMultiplier, 2.0);
48+
}
49+
50+
bool FilterNoise::checkArgs()
51+
{
52+
if (!argOutput->set())
53+
{
54+
std::cerr << "missing output" << std::endl;
55+
return false;
56+
}
57+
58+
if (argOutputFormat->set())
59+
{
60+
if (outputFormat != "las" && outputFormat != "laz" && outputFormat != "copc")
61+
{
62+
std::cerr << "unknown output format: " << outputFormat << std::endl;
63+
return false;
64+
}
65+
}
66+
else
67+
outputFormat = "las"; // uncompressed by default
68+
69+
if (!argAlgorithm->set())
70+
{
71+
std::cerr << "missing algorithm" << std::endl;
72+
return false;
73+
}
74+
else
75+
{
76+
if (!(algorithm == "statistical" || algorithm == "radius"))
77+
{
78+
std::cerr << "unknown algorithm: " << algorithm << std::endl;
79+
return false;
80+
}
81+
}
82+
83+
if (algorithm == "radius" && (argStatisticalMeanK->set() || argStatisticalMultiplier->set()))
84+
{
85+
std::cerr << "statistical- arguments are not supported with radius algorithm" << std::endl;
86+
return false;
87+
}
88+
89+
if (algorithm == "statistical" && (argRadiusMinK->set() || argRadiusRadius->set()))
90+
{
91+
std::cerr << "radius- arguments are not supported with statistical algorithm" << std::endl;
92+
return false;
93+
}
94+
95+
return true;
96+
}
97+
98+
99+
static std::unique_ptr<PipelineManager> pipeline(ParallelJobInfo *tile, pdal::Options &noiseFilterOptions)
100+
{
101+
std::unique_ptr<PipelineManager> manager( new PipelineManager );
102+
103+
Stage& r = makeReader(manager.get(), tile->inputFilenames[0]);
104+
105+
Stage *last = &r;
106+
107+
// filtering
108+
if (!tile->filterBounds.empty())
109+
{
110+
Options filter_opts;
111+
filter_opts.add(pdal::Option("bounds", tile->filterBounds));
112+
113+
if (readerSupportsBounds(r))
114+
{
115+
// Reader of the format can do the filtering - use that whenever possible!
116+
r.addOptions(filter_opts);
117+
}
118+
else
119+
{
120+
// Reader can't do the filtering - do it with a filter
121+
last = &manager->makeFilter("filters.crop", *last, filter_opts);
122+
}
123+
}
124+
125+
if (!tile->filterExpression.empty())
126+
{
127+
Options filter_opts;
128+
filter_opts.add(pdal::Option("expression", tile->filterExpression));
129+
last = &manager->makeFilter("filters.expression", *last, filter_opts);
130+
}
131+
132+
last = &manager->makeFilter("filters.outlier", *last, noiseFilterOptions);
133+
134+
makeWriter(manager.get(), tile->outputFilename, last);
135+
136+
return manager;
137+
}
138+
139+
void FilterNoise::preparePipelines(std::vector<std::unique_ptr<PipelineManager>>& pipelines)
140+
{
141+
pdal::Options noiseFilterOptions;
142+
noiseFilterOptions.add(pdal::Option("method", algorithm));
143+
144+
if (algorithm == "radius")
145+
{
146+
noiseFilterOptions.add(pdal::Option("min_k", radiusMinK));
147+
noiseFilterOptions.add(pdal::Option("radius", radiusRadius));
148+
}
149+
else if (algorithm == "statistical")
150+
{
151+
noiseFilterOptions.add(pdal::Option("mean_k", statisticalMeanK));
152+
noiseFilterOptions.add(pdal::Option("multiplier", statisticalMultiplier));
153+
}
154+
155+
if (ends_with(inputFile, ".vpc"))
156+
{
157+
// for /tmp/hello.vpc we will use /tmp/hello dir for all results
158+
fs::path outputParentDir = fs::path(outputFile).parent_path();
159+
fs::path outputSubdir = outputParentDir / fs::path(outputFile).stem();
160+
fs::create_directories(outputSubdir);
161+
162+
// VPC handling
163+
VirtualPointCloud vpc;
164+
if (!vpc.read(inputFile))
165+
return;
166+
167+
for (const VirtualPointCloud::File& f : vpc.files)
168+
{
169+
ParallelJobInfo tile(ParallelJobInfo::FileBased, BOX2D(), filterExpression, filterBounds);
170+
tile.inputFilenames.push_back(f.filename);
171+
172+
// for input file /x/y/z.las that goes to /tmp/hello.vpc,
173+
// individual output file will be called /tmp/hello/z.las
174+
fs::path inputBasename = fileStem(f.filename);
175+
176+
if (!ends_with(outputFile, ".vpc"))
177+
tile.outputFilename = (outputSubdir / inputBasename).string() + ".las";
178+
else
179+
tile.outputFilename = (outputSubdir / inputBasename).string() + "." + outputFormat;
180+
181+
tileOutputFiles.push_back(tile.outputFilename);
182+
183+
pipelines.push_back(pipeline(&tile, noiseFilterOptions));
184+
}
185+
}
186+
else
187+
{
188+
ParallelJobInfo tile(ParallelJobInfo::Single, BOX2D(), filterExpression, filterBounds);
189+
tile.inputFilenames.push_back(inputFile);
190+
tile.outputFilename = outputFile;
191+
192+
pipelines.push_back(pipeline(&tile, noiseFilterOptions));
193+
}
194+
}
195+
196+
void FilterNoise::finalize(std::vector<std::unique_ptr<PipelineManager>>&)
197+
{
198+
if (tileOutputFiles.empty())
199+
return;
200+
201+
buildOutput(outputFile, tileOutputFiles);
202+
}

src/main.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,11 @@ int main(int argc, char* argv[])
132132
{
133133
runTile(args);
134134
}
135+
else if (cmd == "filter_noise")
136+
{
137+
FilterNoise filterNoise;
138+
runAlg(args, filterNoise);
139+
}
135140
else if (cmd == "classify_ground")
136141
{
137142
ClassifyGround classifyGround;

0 commit comments

Comments
 (0)