src/tools/lvr2_registration/Main.cpp
Go to the documentation of this file.
1 
35 #include "lvr2/io/ModelFactory.hpp"
36 #include "lvr2/io/IOUtils.hpp"
38 #include "lvr2/io/HDF5IO.hpp"
40 #include "lvr2/io/hdf5/ArrayIO.hpp"
46 
47 
48 #include <boost/program_options.hpp>
49 #include <boost/filesystem.hpp>
50 #include <boost/optional.hpp>
51 #include <iostream>
52 #include <chrono>
53 #include <fstream>
54 
55 using namespace lvr2;
56 using namespace std;
57 using boost::filesystem::path;
58 
59 string format_name(const string& format, int index)
60 {
61  size_t size = snprintf(nullptr, 0, format.c_str(), index) + 1; // Extra space for '\0'
62  char buff[size];
63  snprintf(buff, size, format.c_str(), index);
64  return string(buff);
65 }
66 
67 
68 string map_format(const string& format)
69 {
70  if (format == "uos")
71  {
72  return "scan%03i.3d";
73  }
74  else if (format == "riegl_txt")
75  {
76  return "scan%03i.txt";
77  }
78  else if (format == "ply")
79  {
80  return "scan%03i.ply";
81  }
82  else
83  {
84  throw boost::program_options::error(string("Unknown Output format: ") + format);
85  }
86 }
87 
88 int main(int argc, char** argv)
89 {
90  // =============== parse options ===============
92 
93  path dir;
94  int start = -1;
95  int end = -1;
96  string format = "uos";
97  string pose_format = "pose";
98  bool isHDF = false;
99 
100  bool write_scans = false;
101  string output_format;
102  bool write_pose = false;
103  string output_pose_format;
104  bool no_frames = false;
105  path output_dir;
106 
107  bool help;
108 
109  try
110  {
111  using namespace boost::program_options;
112 
113  options_description general_options("General Options");
114  options_description icp_options("ICP Options");
115  options_description loopclosing_options("Loopclosing Options");
116 
117  general_options.add_options()
118  ("start,s", value<int>(&start)->default_value(start),
119  "The first Scan to process.\n"
120  "-1 (default): Search for first Scan.")
121 
122  ("end,e", value<int>(&end)->default_value(end),
123  "The last Scan to process.\n"
124  "-1 (default): Continue until no more Scans found.")
125 
126  ("format,f", value<string>(&format)->default_value(format),
127  "The format of the Scans in <dir>.\n"
128  "This can be a predefined Format (uos, ply, riegl_txt), or a printf Format String like \"scan%03i.3d\",\n"
129  "containing one %i or %d that will be replaced with the Scan Index.")
130 
131  ("pose-format", value<string>(&pose_format)->default_value(pose_format),
132  "The File extension of the Pose files.\n"
133  "Currently supported are: pose, dat, frames.")
134 
135  ("reduction,r", value<double>(&options.reduction)->default_value(options.reduction),
136  "The Voxel size for Octree based reduction.\n"
137  "-1 (default): No reduction.")
138 
139  ("min,m", value<double>(&options.minDistance)->default_value(options.minDistance),
140  "Ignore all Points closer than <value> to the origin of the Scan.\n"
141  "-1 (default): No filter.")
142 
143  ("max,M", value<double>(&options.maxDistance)->default_value(options.maxDistance),
144  "Ignore all Points farther away than <value> from the origin of the Scan.\n"
145  "-1 (default): No filter.")
146 
147  ("trustPose,p", bool_switch(&options.trustPose),
148  "Use the unmodified Pose for ICP. Useful for GPS Poses or unordered Scans.\n"
149  "false (default): Apply the relative refinement of previous Scans.")
150 
151  ("metascan", bool_switch(&options.metascan),
152  "Match Scans to the combined Pointcloud of all previous Scans instead of just the last Scan.")
153 
154  ("noFrames,F", bool_switch(&no_frames),
155  "Don't write \".frames\" files.")
156 
157  ("writePose,w", value<string>(&output_pose_format)->implicit_value("<pose-format>"),
158  "Write Poses to directory specified by --output.")
159 
160  ("writeScans,W", value<string>(&output_format)->implicit_value("<format>"),
161  "Write Scans to directory specified by --output.")
162 
163  ("output,o", value<path>(&output_dir),
164  "Changes output directory of --writePose and --writeScans. Does not affect \".frames\" files\n"
165  "default: <dir>/output.")
166 
167  ("verbose,v", bool_switch(&options.verbose),
168  "Show more detailed output. Useful for fine-tuning Parameters or debugging.")
169 
170  ("hdf,H", bool_switch(&options.useHDF),
171  "Opens the given hdf5 file. Then registrates all scans in '/raw/scans/'\nthat are named after the scheme: 'position_00001' where '1' is the scans number.\nAfter registration the calculated poses are written to the finalPose dataset in the hdf5 file.\n")
172 
173  ("help,h", bool_switch(&help),
174  "Print this help. Seriously how are you reading this if you don't know the --help Option?")
175  ;
176 
177  icp_options.add_options()
178  ("icpIterations,i", value<int>(&options.icpIterations)->default_value(options.icpIterations),
179  "Number of iterations for ICP.\n"
180  "ICP should ideally converge before this number is met, but this number places an upper Bound on calculation time.")
181 
182  ("icpMaxDistance,d", value<double>(&options.icpMaxDistance)->default_value(options.icpMaxDistance),
183  "The maximum distance between two points during ICP.")
184 
185  ("maxLeafSize", value<int>(&options.maxLeafSize)->default_value(options.maxLeafSize),
186  "The maximum number of Points in a Leaf of a KDTree.")
187 
188  ("epsilon", value<double>(&options.epsilon)->default_value(options.epsilon),
189  "The epsilon difference between ICP-errors for the stop criterion of ICP.")
190  ;
191 
192  loopclosing_options.add_options()
193  ("loopClosing,L", bool_switch(&options.doLoopClosing),
194  "Use simple Loopclosing.\n"
195  "At least one of -L and -G must be specified for Loopclosing to take place.")
196 
197  ("graphSlam,G", bool_switch(&options.doGraphSLAM),
198  "Use complex Loop Closing with GraphSLAM.\n"
199  "At least one of -L and -G must be specified for Loopclosing to take place.")
200 
201  ("closeLoopDistance,c", value<double>(&options.closeLoopDistance)->default_value(options.closeLoopDistance),
202  "The maximum distance between two poses to consider a closed loop in Loopclosing or an Edge in the GraphSLAM Graph.\n"
203  "Mutually exclusive to --closeLoopPairs.")
204 
205  ("closeLoopPairs,C", value<int>(&options.closeLoopPairs)->default_value(options.closeLoopPairs),
206  "The minimum pair overlap between two poses to consider a closed loop in Loopclosing or an Edge in the GraphSLAM Graph.\n"
207  "Mutually exclusive to --closeLoopDistance.\n"
208  "Pairs are judged using slamMaxDistance.\n"
209  "-1 (default): use --closeLoopDistance instead.")
210 
211  ("loopSize,l", value<int>(&options.loopSize)->default_value(options.loopSize),
212  "The minimum number of Scans to be considered a Loop to prevent Loopclosing from triggering on adjacent Scans.\n"
213  "Also used in GraphSLAM when considering other Scans for Edges\n"
214  "For Loopclosing, this value needs to be at least 6, for GraphSLAM at least 1.")
215 
216  ("slamIterations,I", value<int>(&options.slamIterations)->default_value(options.slamIterations),
217  "Number of iterations for SLAM.")
218 
219  ("slamMaxDistance,D", value<double>(&options.slamMaxDistance)->default_value(options.slamMaxDistance),
220  "The maximum distance between two points during SLAM.")
221 
222  ("slamEpsilon", value<double>(&options.slamEpsilon)->default_value(options.slamEpsilon),
223  "The epsilon difference of SLAM corrections for the stop criterion of SLAM.")
224  ;
225 
226  options_description hidden_options("hidden_options");
227  hidden_options.add_options()
228  ("dir", value<path>(&dir))
229  ;
230 
231  positional_options_description pos;
232  pos.add("dir", 1);
233 
234  options_description all_options("options");
235  all_options.add(general_options).add(icp_options).add(loopclosing_options).add(hidden_options);
236 
237  variables_map variables;
238  store(command_line_parser(argc, argv).options(all_options).positional(pos).run(), variables);
239  notify(variables);
240 
241  if (help)
242  {
243  cout << "The Scan Registration Tool" << endl;
244  cout << "Usage: " << endl;
245  cout << "\tlvr2_registration [OPTIONS] <dir>" << endl;
246  cout << endl;
247  general_options.print(cout);
248  cout << endl;
249  icp_options.print(cout);
250  cout << endl;
251  loopclosing_options.print(cout);
252  cout << endl;
253  cout << "<dir> is the directory to search scans in" << endl;
254  return EXIT_SUCCESS;
255  }
256 
257  if (variables.count("dir") != 1)
258  {
259  throw error("Missing <dir> Parameter");
260  }
261 
262  if (variables.count("output") == 0)
263  {
264  output_dir = dir / "output";
265  }
266 
267  if (format.find('%') == string::npos)
268  {
269  format = map_format(format);
270  }
271 
272  if (variables.count("writePose") == 1)
273  {
274  write_pose = true;
275  if (output_pose_format[0] == '<')
276  {
277  output_pose_format = pose_format;
278  }
279  }
280  if (variables.count("writeScans") == 1)
281  {
282  write_scans = true;
283  if (output_format[0] == '<')
284  {
285  output_format = format;
286  }
287  else if (output_format.find('%') == string::npos)
288  {
289  format = map_format(format);
290  }
291  }
292 
293  options.createFrames = !no_frames;
294  }
295  catch (const boost::program_options::error& ex)
296  {
297  std::cerr << ex.what() << endl;
298  std::cerr << endl;
299  std::cerr << "Use '--help' to see the list of possible options" << endl;
300  return EXIT_FAILURE;
301  }
302 
303  using HDF5PCIO = lvr2::Hdf5IO<
309  // contains the hdf5
310  std::shared_ptr<HDF5PCIO> h5_ptr(new HDF5PCIO());
311 
312  if(options.useHDF)
313  {
314  cout << "HDF5 is used!" << endl;
315  ifstream f(dir.c_str());
316  if (f.good())
317  {
318  // create boost::fileystem::path to hdf file location
319  boost::filesystem::path pathToHDF(dir.c_str());
320  h5_ptr->open(pathToHDF.string());
321  }
322  else
323  {
324  cerr << "The given HDF5 file could not be opened! Oben" << endl;
325  return EXIT_FAILURE;
326  }
327  }
328 
329  // =============== search scans ===============
330  if (start == -1)
331  {
332  if (!options.useHDF)
333  {
334  for (int i = 0; i < 100; i++)
335  {
336  path file = dir / format_name(format, i);
337  if (exists(file))
338  {
339  start = i;
340  cout << "First scan: " << file.filename() << endl;
341  break;
342  }
343  }
344  if (start == -1)
345  {
346  cerr << "Could not find a starting scan. are you using the right format?" << endl;
347  return EXIT_FAILURE;
348  }
349  }
350  }
351  if (!options.useHDF)
352  {
353  // make sure all scan and pose files are in the directory
354  for (int i = start; end == -1 || i <= end; i++)
355  {
356  path file = dir / format_name(format, i);
357  if (!exists(file))
358  {
359  if (end != -1 || i == start)
360  {
361  cerr << "Missing scan " << file.filename() << endl;
362  return EXIT_FAILURE;
363  }
364  end = i - 1;
365  cout << "Last scan: \"" << format_name(format, end) << '"' << endl;
366  break;
367  }
368  file.replace_extension(pose_format);
369  if (!exists(file))
370  {
371  cerr << "Missing pose file " << file.filename() << endl;
372  return EXIT_FAILURE;
373  }
374  }
375  }
376 
377  SLAMAlign align(options);
378  vector<SLAMScanPtr> scans;
379 
380  // DEBUG
381  ScanProjectEditMark proj;
382  proj.project = ScanProjectPtr(new ScanProject);
383 
384  int count = end - start + 1;
385 
386  vector<string> numOfScansInHDF;
387  if(h5_ptr->m_hdf5_file)
388  {
389  HighFive::Group hfscans = hdf5util::getGroup(h5_ptr->m_hdf5_file, "raw/scans");
390  numOfScansInHDF = hfscans.listObjectNames();
391  }
392 
393  vector<lvr2::ScanPtr> rawScans;
394  if (options.useHDF)
395  {
396  for (int i = 0; i < numOfScansInHDF.size(); i++)
397  {
398  // create a scan object for each scan in hdf
399  ScanPtr tempScan(new Scan());
400  size_t six;
401  size_t pointsNum;
402  boost::shared_array<float> bb_array = h5_ptr->loadArray<float>("raw/scans/" + numOfScansInHDF[i], "boundingBox", six);
403  BoundingBox<BaseVector<float>> bb(BaseVector<float>(bb_array[0], bb_array[1], bb_array[2]),
404  BaseVector<float>(bb_array[3], bb_array[4], bb_array[5]));
405  // bounding box transfered to object
406  tempScan->boundingBox = bb;
407 
408  boost::shared_array<float> fov_array = h5_ptr->loadArray<float>("raw/scans/" + numOfScansInHDF[i], "fov", six);
409  // fov transfered to object
410 
411  // TODO: min and max angles from new structure
412 
413  boost::shared_array<float> res_array = h5_ptr->loadArray<float>("raw/scans/" + numOfScansInHDF[i], "resolution", six);
414  // resolution transfered
415  tempScan->hResolution = res_array[0];
416  tempScan->vResolution = res_array[1];
417  // point cloud transfered
418  boost::shared_array<float> point_array = h5_ptr->loadArray<float>("raw/scans/"+ numOfScansInHDF[i], "points", pointsNum);
419  // important because x, y, z coords
420  pointsNum = pointsNum / 3;
421  PointBufferPtr pointPointer = PointBufferPtr(new PointBuffer(point_array, pointsNum));
422  tempScan->points = pointPointer;
423  // tempScan->m_points = h5_ptr->loadPointCloud("raw/scans/" + numOfScansInHDF[i]);
424  tempScan->pointsLoaded = true;
425  // pose transfered
426  tempScan->poseEstimation = h5_ptr->loadMatrix<Transformd>("raw/scans/" + numOfScansInHDF[i], "initialPose").get();
427  tempScan->positionNumber = i;
428 
429  tempScan->scanRoot = "raw/scans/" + numOfScansInHDF[i];
430 
431 
432  // sets the finalPose to the identiy matrix
433  tempScan->registration = Transformd::Identity();
434 
435 
436  // DEBUG
437  ScanPosition pos;
438  pos.scans.push_back(tempScan);
439  proj.project->positions.push_back(std::make_shared<ScanPosition>(pos));
440  proj.changed.push_back(false);
441 
442  SLAMScanPtr slamScan = SLAMScanPtr(new SLAMScanWrapper(tempScan));
443 
444  // scans.push_back(slamScan);
445  // align.addScan(slamScan);
446  }
447  // DEBUG
448 
449  cout << "vor Pipe Konstruktor" << endl;
450  ScanProjectEditMarkPtr projPtr = make_shared<ScanProjectEditMark>(proj);
451  RegistrationPipeline pipe(&options, projPtr);
452  pipe.doRegistration();
453  cout << "Nach doRegistration" << endl;
454  for (size_t i = 0; i < projPtr->changed.size(); i++)
455  {
456  cout << "Reconstruct indivcator ans Stelle: " << i << " ist: " << projPtr->changed.at(i)<< endl;
457  }
458 
459  cout << "Eine Pose aus dem Project:" << endl << projPtr->project->positions.at(1)->scans[0]->registration << endl;
460  }
461  else
462  {
463  // case for not using HDF5
464  // TODO: change to ScanDirectoryParser once that is done
465 
466  for (int i = 0; i < count; i++)
467  {
468  path file = dir / format_name(format, start + i);
469  auto model = ModelFactory::readModel(file.string());
470 
471  if (!model)
472  {
473  cerr << "Unable to read Model from: " << file.string() << endl;
474  return EXIT_FAILURE;
475  }
476  if (!model->m_pointCloud)
477  {
478  cerr << "file does not contain Points: " << file.string() << endl;
479  return EXIT_FAILURE;
480  }
481 
482  file.replace_extension(pose_format);
483  Transformd pose = getTransformationFromFile<double>(file);
484 
485  ScanPtr scan = ScanPtr(new Scan());
486  scan->points = model->m_pointCloud;
487  scan->poseEstimation = pose;
488 
489  SLAMScanPtr slamScan = SLAMScanPtr(new SLAMScanWrapper(scan));
490  scans.push_back(slamScan);
491  align.addScan(slamScan);
492  }
493  }
494 
495 
496  auto start_time = chrono::steady_clock::now();
497 
498  align.finish();
499 
500  auto required_time = chrono::steady_clock::now() - start_time;
501  cout << "SLAM finished in " << required_time.count() / 1e9 << " seconds" << endl;
502 
503  if (write_pose || write_scans)
504  {
505  create_directories(output_dir);
506  }
507 
508  path file;
509 
510  if (options.useHDF)
511  {
512  // write poses to hdf
513  for(int i = 0; i < scans.size(); i++)
514  {
515  Transformd pose = scans[i]->pose();
516  cout << "Main:Pose Scan Nummer " << i << endl << pose << endl;
517  // The pose needs to be transposed before writing to hdf,
518  // because the lvr2_viewer expects finalPose in hdf transposed this way.
519  // The initial pose is saved NOT transposed in HDF
520  pose.transposeInPlace();
521  h5_ptr->MatrixIO::save("raw/scans/" + numOfScansInHDF[i], "finalPose", pose);
522  }
523  }
524 
525  for (int i = 0; i < count; i++)
526  {
527  auto& scan = scans[i];
528 
529  if (!no_frames)
530  {
531  file = dir / format_name(format, start + i);
532  file.replace_extension("frames");
533 
534  scan->writeFrames(file.string());
535  }
536 
537  if (write_pose)
538  {
539  file = output_dir / format_name(write_scans ? output_format : format, start + i);
540  file.replace_extension(output_pose_format);
541  ofstream out(file.string());
542 
543  auto pose = scan->pose();
544  for (int y = 0; y < 4; y++)
545  {
546  for (int x = 0; x < 4; x++)
547  {
548  out << pose(y, x);
549  if (x < 3)
550  {
551  out << " ";
552  }
553  }
554  out << endl;
555  }
556  }
557 
558  if (write_scans)
559  {
560  file = output_dir / format_name(output_format, start + i);
561 
562  size_t n = scan->numPoints();
563 
564  auto model = make_shared<Model>();
565  auto pointCloud = make_shared<PointBuffer>();
566  floatArr points = floatArr(new float[n * 3]);
567 
568  #pragma omp parallel for schedule(static)
569  for (size_t i = 0; i < n; i++)
570  {
571  auto point = scan->rawPoint(i);
572  points[i * 3] = point[0];
573  points[i * 3 + 1] = point[1];
574  points[i * 3 + 2] = point[2];
575  }
576 
577  pointCloud->setPointArray(points, n);
578  model->m_pointCloud = pointCloud;
579  ModelFactory::saveModel(model, file.string());
580  }
581  }
582  return EXIT_SUCCESS;
583 }
lvr2::floatArr
boost::shared_array< float > floatArr
Definition: DataStruct.hpp:133
lvr2::SLAMScanWrapper
A Wrapper around Scan to allow for SLAM usage.
Definition: SLAMScanWrapper.hpp:65
lvr2::ModelFactory::saveModel
static void saveModel(ModelPtr m, std::string file)
Definition: ModelFactory.cpp:225
lvr2::ScanProjectEditMarkPtr
std::shared_ptr< ScanProjectEditMark > ScanProjectEditMarkPtr
Definition: ScanTypes.hpp:357
lvr2::ScanProjectEditMark
Definition: ScanTypes.hpp:350
lvr2::Hdf5IO
Manager Class for all Hdf5IO components located in hdf5 directory.
Definition: HDF5FeatureBase.hpp:38
PointCloudIO.hpp
lvr2::BaseVector< float >
main
int main(int argc, char **argv)
Definition: src/tools/lvr2_registration/Main.cpp:88
HDF5IO.hpp
MatrixIO.hpp
SLAMAlign.hpp
lvr2::Transformd
Transform< double > Transformd
4x4 double precision transformation matrix
Definition: MatrixTypes.hpp:71
lvr2::PointBufferPtr
std::shared_ptr< PointBuffer > PointBufferPtr
Definition: PointBuffer.hpp:130
lvr2::ScanProjectPtr
std::shared_ptr< ScanProject > ScanProjectPtr
Definition: ScanTypes.hpp:344
lvr2::PointBuffer
A class to handle point information with an arbitrarily large number of attribute channels....
Definition: PointBuffer.hpp:51
IOUtils.hpp
lvr2::ScanProjectEditMark::changed
std::vector< bool > changed
True if scan pose has been changed, one bool for each scan position.
Definition: ScanTypes.hpp:357
VariantChannelIO.hpp
lvr2::Scan
Definition: ScanTypes.hpp:24
RegistrationPipeline.hpp
options
const kaboom::Options * options
Definition: src/tools/lvr2_kaboom/Main.cpp:45
lvr2::ScanPosition
Definition: ScanTypes.hpp:276
lvr2::hdf5features::VariantChannelIO
Hdf5IO Feature for handling VariantChannel related IO.
Definition: hdf5/VariantChannelIO.hpp:59
HighFive::Group
Definition: H5Group.hpp:20
lvr2::SLAMScanPtr
std::shared_ptr< SLAMScanWrapper > SLAMScanPtr
Definition: SLAMScanWrapper.hpp:221
RegistrationPipeline::doRegistration
void doRegistration()
Starts the registration.
Definition: RegistrationPipeline.cpp:120
lvr2::hdf5features::PointCloudIO
Hdf5IO Feature for handling PointBuffer related IO.
Definition: hdf5/PointCloudIO.hpp:47
lvr2::ScanPosition::scans
std::vector< ScanPtr > scans
Definition: ScanTypes.hpp:287
lvr2::ScanProject
Definition: ScanTypes.hpp:322
ChannelIO.hpp
HDF5FeatureBase.hpp
lvr2::BoundingBox
A dynamic bounding box class.
Definition: BoundingBox.hpp:49
argc
int argc
Definition: tests_high_five_parallel.cpp:27
file
FILE * file
Definition: arithmeticencoder.cpp:77
lvr2::SLAMAlign
A class to run SLAM on Scans.
Definition: SLAMAlign.hpp:48
std
Definition: HalfEdge.hpp:124
lvr2
Definition: BaseBufferManipulators.hpp:39
kfusion::cuda::error
KF_EXPORTS void error(const char *error_string, const char *file, const int line, const char *func="")
Error handler. All GPU functions from this subsystem call the function to report an error....
Definition: device_memory.cpp:7
lvr2::ModelFactory::readModel
static ModelPtr readModel(std::string filename)
Definition: ModelFactory.cpp:65
ModelFactory.hpp
lvr2::ScanProjectEditMark::project
ScanProjectPtr project
Definition: ScanTypes.hpp:355
map_format
string map_format(const string &format)
Definition: src/tools/lvr2_registration/Main.cpp:68
lvr2::SLAMAlign::finish
void finish()
Indicates that no new Scans will be added.
Definition: SLAMAlign.cpp:369
lvr2::SLAMOptions
A struct to configure SLAMAlign.
Definition: SLAMOptions.hpp:45
lvr2::SLAMAlign::addScan
void addScan(const SLAMScanPtr &scan, bool match=false)
Adds a new Scan to the SLAM instance.
Definition: SLAMAlign.cpp:77
format_name
string format_name(const string &format, int index)
Definition: src/tools/lvr2_registration/Main.cpp:59
RegistrationPipeline
Definition: RegistrationPipeline.hpp:45
lvr2::hdf5features::ArrayIO
Definition: hdf5/ArrayIO.hpp:13
argv
char ** argv
Definition: tests_high_five_parallel.cpp:28
ArrayIO.hpp
lvr2::hdf5features::ChannelIO
Definition: hdf5/ChannelIO.hpp:20
lvr2::hdf5features::MatrixIO
Definition: hdf5/MatrixIO.hpp:16
HighFive::NodeTraits::listObjectNames
std::vector< std::string > listObjectNames() const
list all leaf objects name of the node / group
Definition: H5Node_traits_misc.hpp:152
lvr2::ScanPtr
std::shared_ptr< Scan > ScanPtr
Shared pointer to scans.
Definition: ScanTypes.hpp:98
lvr2::hdf5util::getGroup
HighFive::Group getGroup(std::shared_ptr< HighFive::File > hdf5_file, const std::string &groupName, bool create=true)
Definition: Hdf5Util.cpp:58


lvr2
Author(s): Thomas Wiemann , Sebastian Pütz , Alexander Mock , Lars Kiesow , Lukas Kalbertodt , Tristan Igelbrink , Johan M. von Behren , Dominik Feldschnieders , Alexander Löhr
autogenerated on Wed Mar 2 2022 00:37:24