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+ }
0 commit comments