134 cout <<
"Reading XML file: " << filename <<
"..." << flush;
138 ifstream f(filename);
145 pugi::xml_document xmldoc;
149 std::string resolved_filename = resolved_path.string();
150 std::filesystem::path xml_parent_dir = resolved_path.parent_path();
153 pugi::xml_parse_result result = xmldoc.load_file(resolved_filename.c_str());
157 cout <<
"failed." << endl;
158 cerr <<
"XML file " << filename <<
" parsed with errors, attribute value: [" << xmldoc.child(
"node").attribute(
"attr").value() <<
"]\n";
159 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): Errors were found while parsing XML file. Error description: " + std::string(result.description()));
162 pugi::xml_node helios = xmldoc.child(
"helios");
164 if (helios.empty()) {
165 std::cout <<
"failed." << std::endl;
166 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): XML file must have tag '<helios> ... </helios>' bounding all other tags.");
172 size_t total_hits = 0;
174 if (load_grid_only ==
false) {
177 for (pugi::xml_node s = helios.child(
"scan"); s; s = s.next_sibling(
"scan")) {
185 const char *origin_str = s.child_value(
"origin");
186 const bool has_static_origin = (strlen(origin_str) != 0);
192 std::string scan_pattern_str =
deblank(s.child_value(
"scanPattern"));
193 if (scan_pattern_str.empty()) {
194 scan_pattern_str =
deblank(s.child_value(
"scanpattern"));
196 std::transform(scan_pattern_str.begin(), scan_pattern_str.end(), scan_pattern_str.begin(), [](
unsigned char ch) { return std::tolower(ch); });
197 const bool spinning_multibeam = (scan_pattern_str ==
"spinning_multibeam" || scan_pattern_str ==
"spinning-multibeam" || scan_pattern_str ==
"spinningmultibeam");
200 const bool risley = (scan_pattern_str ==
"risley" || scan_pattern_str ==
"risley_prism" || scan_pattern_str ==
"risley-prism" || scan_pattern_str ==
"risleyprism");
201 if (!scan_pattern_str.empty() && scan_pattern_str !=
"raster" && !spinning_multibeam && !risley) {
203 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): Unrecognized scanPattern '" + scan_pattern_str +
"' for scan #" + std::to_string(scan_count) +
". Valid values are 'raster', 'spinning_multibeam', and 'risley'.");
210 std::vector<RisleyPrism> risley_prisms;
211 double risley_refractive_index_air = 1.0;
213 const char *air_str = s.child_value(
"refractiveIndexAir");
214 if (strlen(air_str) == 0) {
215 air_str = s.child_value(
"refractiveindexair");
217 if (strlen(air_str) > 0) {
218 risley_refractive_index_air = atof(air_str);
220 for (pugi::xml_node prism_node = s.child(
"prism"); prism_node; prism_node = prism_node.next_sibling(
"prism")) {
224 std::istringstream prism_stream(prism_node.child_value());
225 double wedge_deg, refr_index, rotor_hz;
226 if (!(prism_stream >> wedge_deg >> refr_index >> rotor_hz)) {
228 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): A <prism> of risley scan #" + std::to_string(scan_count) +
229 " must give at least 'wedgeAngle(deg) refractiveIndex rotorRate(Hz)' (an optional fourth value sets the initial phase in degrees).");
231 double phase_deg = 0.0;
232 prism_stream >> phase_deg;
233 risley_prisms.emplace_back(wedge_deg *
M_PI / 180.0, refr_index, rotor_hz * 2.0 *
M_PI, phase_deg *
M_PI / 180.0);
235 if (risley_prisms.empty()) {
237 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): A risley scan (#" + std::to_string(scan_count) +
") requires at least one <prism> child (a Livox-style sensor uses two counter-rotating prisms).");
242 std::vector<float> beamZenithAngles;
243 if (spinning_multibeam) {
244 const char *beam_angles_str = s.child_value(
"beamElevationAngles");
245 if (strlen(beam_angles_str) == 0) {
246 beam_angles_str = s.child_value(
"beamelevationangles");
248 if (strlen(beam_angles_str) == 0) {
250 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): A spinning_multibeam scan (#" + std::to_string(scan_count) +
") requires <beamElevationAngles> (space-separated channel elevation angles, in degrees above the horizon).");
252 std::istringstream beam_stream(beam_angles_str);
254 while (beam_stream >> elev_deg) {
256 beamZenithAngles.push_back(0.5f *
float(
M_PI) - elev_deg *
float(
M_PI) / 180.f);
258 if (beamZenithAngles.empty()) {
260 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): Could not parse any channel angles from <beamElevationAngles> for scan #" + std::to_string(scan_count) +
".");
269 if (spinning_multibeam) {
270 size.
x = int(beamZenithAngles.size());
276 const char *size_str = s.child_value(
"size");
277 if (strlen(size_str) == 0) {
279 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): A size was not specified for scan #" + std::to_string(scan_count));
283 if (size.
x <= 0 || size.
y <= 0) {
285 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): The scan size must be positive (check scan #" + std::to_string(scan_count) +
").");
290 const char *offset_str = s.child_value(
"translation");
293 if (strlen(offset_str) > 0) {
298 const char *rotation_str = s.child_value(
"rotation");
301 if (strlen(rotation_str) > 0) {
303 rotation = rotation *
M_PI / 180.f;
308 const char *thetaMin_str = s.child_value(
"thetaMin");
311 if (strlen(thetaMin_str) == 0) {
315 thetaMin = atof(thetaMin_str) *
M_PI / 180.f;
323 const char *thetaMax_str = s.child_value(
"thetaMax");
326 if (strlen(thetaMax_str) == 0) {
329 thetaMax = atof(thetaMax_str) *
M_PI / 180.f;
332 if (thetaMax - 1e-5 >
M_PI) {
333 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): thetaMax cannot be greater than 180 degrees.");
337 const char *phiMin_str = s.child_value(
"phiMin");
340 if (strlen(phiMin_str) == 0) {
343 phiMin = atof(phiMin_str) *
M_PI / 180.f;
351 const char *phiMax_str = s.child_value(
"phiMax");
354 if (strlen(phiMax_str) == 0) {
357 phiMax = atof(phiMax_str) *
M_PI / 180.f;
360 if (phiMax - 1e-5 > 4.f *
M_PI) {
361 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): phiMax cannot be greater than 720 degrees.");
365 const char *exitDiameter_str_uc = s.child_value(
"exitDiameter");
366 const char *exitDiameter_str_lc = s.child_value(
"exitdiameter");
369 if (strlen(exitDiameter_str_uc) == 0 && strlen(exitDiameter_str_lc) == 0) {
371 }
else if (strlen(exitDiameter_str_uc) > 0) {
372 exitDiameter = fmax(0, atof(exitDiameter_str_uc));
374 exitDiameter = fmax(0, atof(exitDiameter_str_lc));
378 const char *beamDivergence_str_uc = s.child_value(
"beamDivergence");
379 const char *beamDivergence_str_lc = s.child_value(
"beamdivergence");
381 float beamDivergence;
382 if (strlen(beamDivergence_str_uc) == 0 && strlen(beamDivergence_str_lc) == 0) {
384 }
else if (strlen(beamDivergence_str_uc) > 0) {
385 beamDivergence = fmax(0, atof(beamDivergence_str_uc));
387 beamDivergence = fmax(0, atof(beamDivergence_str_lc));
391 const char *rangeNoise_str_uc = s.child_value(
"rangeNoiseStdDev");
392 const char *rangeNoise_str_lc = s.child_value(
"rangenoisestddev");
394 float rangeNoiseStdDev;
395 if (strlen(rangeNoise_str_uc) == 0 && strlen(rangeNoise_str_lc) == 0) {
396 rangeNoiseStdDev = 0;
397 }
else if (strlen(rangeNoise_str_uc) > 0) {
398 rangeNoiseStdDev = fmax(0, atof(rangeNoise_str_uc));
400 rangeNoiseStdDev = fmax(0, atof(rangeNoise_str_lc));
404 const char *angleNoise_str_uc = s.child_value(
"angleNoiseStdDev");
405 const char *angleNoise_str_lc = s.child_value(
"anglenoisestddev");
407 float angleNoiseStdDev;
408 if (strlen(angleNoise_str_uc) == 0 && strlen(angleNoise_str_lc) == 0) {
409 angleNoiseStdDev = 0;
410 }
else if (strlen(angleNoise_str_uc) > 0) {
411 angleNoiseStdDev = fmax(0, atof(angleNoise_str_uc));
413 angleNoiseStdDev = fmax(0, atof(angleNoise_str_lc));
417 const char *scanTilt_str_uc = s.child_value(
"scanTilt");
418 const char *scanTilt_str_lc = s.child_value(
"scantilt");
420 float scanTiltRoll = 0.f;
421 float scanTiltPitch = 0.f;
422 const char *scanTilt_str = (strlen(scanTilt_str_uc) > 0) ? scanTilt_str_uc : scanTilt_str_lc;
423 if (strlen(scanTilt_str) > 0) {
425 scanTilt = scanTilt * float(
M_PI) / 180.f;
426 scanTiltRoll = scanTilt.
x;
427 scanTiltPitch = scanTilt.
y;
431 const char *scanAzimuth_str_uc = s.child_value(
"scanAzimuthOffset");
432 const char *scanAzimuth_str_lc = s.child_value(
"scanazimuthoffset");
434 float scanAzimuthOffset = 0.f;
435 const char *scanAzimuth_str = (strlen(scanAzimuth_str_uc) > 0) ? scanAzimuth_str_uc : scanAzimuth_str_lc;
436 if (strlen(scanAzimuth_str) > 0) {
437 scanAzimuthOffset = atof(scanAzimuth_str) * float(
M_PI) / 180.f;
441 std::string returnMode_str =
deblank(s.child_value(
"returnMode"));
442 if (returnMode_str.empty()) {
443 returnMode_str =
deblank(s.child_value(
"returnmode"));
445 std::transform(returnMode_str.begin(), returnMode_str.end(), returnMode_str.begin(), [](
unsigned char ch) { return std::tolower(ch); });
447 if (returnMode_str ==
"single") {
449 }
else if (!returnMode_str.empty() && returnMode_str !=
"multi") {
451 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): Unrecognized returnMode '" + returnMode_str +
"' for scan #" + std::to_string(scan_count) +
". Valid values are 'multi' and 'single'.");
455 std::string singleSel_str =
deblank(s.child_value(
"singleReturnSelection"));
456 if (singleSel_str.empty()) {
457 singleSel_str =
deblank(s.child_value(
"singlereturnselection"));
459 std::transform(singleSel_str.begin(), singleSel_str.end(), singleSel_str.begin(), [](
unsigned char ch) { return std::tolower(ch); });
461 if (singleSel_str ==
"first") {
463 }
else if (singleSel_str ==
"last") {
465 }
else if (singleSel_str ==
"strongest_plus_last" || singleSel_str ==
"dual") {
467 }
else if (!singleSel_str.empty() && singleSel_str !=
"strongest") {
469 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): Unrecognized singleReturnSelection '" + singleSel_str +
"' for scan #" + std::to_string(scan_count) +
". Valid values are 'strongest', 'first', 'last', and 'strongest_plus_last' (alias 'dual').");
474 const char *maxReturns_str = s.child_value(
"maxReturns");
475 if (strlen(maxReturns_str) == 0) {
476 maxReturns_str = s.child_value(
"maxreturns");
478 if (strlen(maxReturns_str) > 0) {
479 maxReturns = atoi(maxReturns_str);
480 if (maxReturns < 1) {
482 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): maxReturns must be at least 1, but '" + std::string(maxReturns_str) +
"' was given for scan #" + std::to_string(scan_count) +
".");
487 float pulseWidth = 0.f;
488 const char *pulseWidth_str = s.child_value(
"pulseWidth");
489 if (strlen(pulseWidth_str) == 0) {
490 pulseWidth_str = s.child_value(
"pulsewidth");
492 const char *pulseDuration_str = s.child_value(
"pulseDuration");
493 if (strlen(pulseDuration_str) == 0) {
494 pulseDuration_str = s.child_value(
"pulseduration");
496 if (strlen(pulseWidth_str) > 0) {
497 pulseWidth = fmax(0, atof(pulseWidth_str));
498 }
else if (strlen(pulseDuration_str) > 0) {
500 pulseWidth = fmax(0.f,
float(atof(pulseDuration_str)) * 299792458.f * 0.5f);
504 float detectionThreshold = 0.f;
505 const char *detThresh_str = s.child_value(
"detectionThreshold");
506 if (strlen(detThresh_str) == 0) {
507 detThresh_str = s.child_value(
"detectionthreshold");
509 if (strlen(detThresh_str) > 0) {
510 detectionThreshold = fmax(0, atof(detThresh_str));
514 const char *dFilter_str = s.child_value(
"distanceFilter");
517 if (strlen(dFilter_str) > 0) {
523 const char *data_format = s.child_value(
"ASCII_format");
525 std::vector<std::string> column_format;
526 if (strlen(data_format) != 0) {
530 std::istringstream stream(data_format);
531 while (stream >> tmp) {
532 column_format.push_back(tmp);
540 const bool has_perpoint_origin = (std::find(column_format.begin(), column_format.end(),
"origin_x") != column_format.end() && std::find(column_format.begin(), column_format.end(),
"origin_y") != column_format.end() &&
541 std::find(column_format.begin(), column_format.end(),
"origin_z") != column_format.end());
542 const bool has_trajectory_tag = (!s.child(
"trajectory").empty() || strlen(s.child_value(
"trajectoryFile")) != 0 || strlen(s.child_value(
"trajectoryfile")) != 0);
543 if (!has_static_origin && !has_perpoint_origin && !spinning_multibeam && !risley && !has_trajectory_tag) {
546 " has no beam origin. Specify either a static <origin> tag, or per-point origin columns (origin_x origin_y origin_z) in the <ASCII_format> and data file.");
555 float azimuthStep_rad = 0.f;
556 const char *azStep_str = s.child_value(
"azimuthStep");
557 if (strlen(azStep_str) == 0) {
558 azStep_str = s.child_value(
"azimuthstep");
560 if (strlen(azStep_str) > 0) {
561 azimuthStep_rad = float(atof(azStep_str)) * float(
M_PI) / 180.f;
566 const char *prf_str = s.child_value(
"PRF");
567 if (strlen(prf_str) == 0) {
568 prf_str = s.child_value(
"pulseRate");
570 if (strlen(prf_str) == 0) {
571 prf_str = s.child_value(
"pulserate");
573 if (strlen(prf_str) > 0) {
574 PRF = float(atof(prf_str));
579 const char *lever_str = s.child_value(
"leverArm");
580 if (strlen(lever_str) == 0) {
581 lever_str = s.child_value(
"leverarm");
583 if (strlen(lever_str) > 0) {
587 const char *boresight_str = s.child_value(
"boresight");
588 if (strlen(boresight_str) > 0) {
594 bool t0_specified =
false;
595 const char *t0_str = s.child_value(
"t0");
596 if (strlen(t0_str) > 0) {
602 std::vector<double> traj_t;
603 std::vector<vec3> traj_pos;
604 std::vector<vec4> traj_quat;
605 bool has_trajectory =
false;
606 pugi::xml_node traj_node = s.child(
"trajectory");
607 const char *trajFile_str = s.child_value(
"trajectoryFile");
608 if (strlen(trajFile_str) == 0) {
609 trajFile_str = s.child_value(
"trajectoryfile");
611 if (!traj_node.empty()) {
613 std::ostringstream poses;
614 for (pugi::xml_node p = traj_node.child(
"pose"); p; p = p.next_sibling(
"pose")) {
615 poses << p.child_value() <<
"\n";
617 std::istringstream traj_stream(poses.str());
618 parseTrajectoryStream(traj_stream,
"scan #" + std::to_string(scan_count), traj_t, traj_pos, traj_quat);
619 has_trajectory =
true;
620 }
else if (strlen(trajFile_str) > 0) {
622 std::string resolved_traj;
623 std::vector<std::string> candidates;
624 candidates.emplace_back(trajFile_str);
625 if (!xml_parent_dir.empty()) {
626 candidates.push_back((xml_parent_dir / trajFile_str).
string());
628 candidates.push_back(
"input/" + std::string(trajFile_str));
629 for (
const std::string &candidate: candidates) {
630 ifstream tf(candidate);
632 resolved_traj = candidate;
636 if (resolved_traj.empty()) {
638 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): trajectory file `" + std::string(trajFile_str) +
"' given for scan #" + std::to_string(scan_count) +
" does not exist.");
640 ifstream tf(resolved_traj);
641 parseTrajectoryStream(tf,
"scan #" + std::to_string(scan_count), traj_t, traj_pos, traj_quat);
642 has_trajectory =
true;
649 const bool physical_moving_raster = !spinning_multibeam && !risley && has_trajectory && (PRF > 0.f);
657 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): risley scan #" + std::to_string(scan_count) +
" requires a pulse repetition rate given as <PRF> (Hz).");
659 if (!has_trajectory) {
661 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): risley scan #" + std::to_string(scan_count) +
662 " requires a <trajectory> or <trajectoryFile>. For a stationary capture, give two coincident poses with the same position and orientation, separated in time by the acquisition duration.");
665 scanID =
addScanRisley(risley_prisms, risley_refractive_index_air, PRF, traj_t, traj_pos, traj_quat, lever_arm, boresight_rpy, exitDiameter, beamDivergence, rangeNoiseStdDev, angleNoiseStdDev, column_format,
666 t0_specified ? t0 : (traj_t.empty() ? 0.0 : traj_t.front()));
675 }
else if (spinning_multibeam) {
677 if (azimuthStep_rad <= 0.f) {
679 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): spinning_multibeam scan #" + std::to_string(scan_count) +
" requires an azimuth resolution given as <azimuthStep> (degrees per firing step).");
683 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): spinning_multibeam scan #" + std::to_string(scan_count) +
" requires a pulse repetition rate given as <PRF> (Hz).");
685 if (!has_trajectory) {
687 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): spinning_multibeam scan #" + std::to_string(scan_count) +
688 " requires a <trajectory> or <trajectoryFile>. For a stationary spin in place, give two coincident poses with the same position and orientation, separated in time by the acquisition duration.");
691 scanID =
addScanSpinning(beamElevationAnglesRad(beamZenithAngles), azimuthStep_rad, PRF, traj_t, traj_pos, traj_quat, lever_arm, boresight_rpy, exitDiameter, beamDivergence, rangeNoiseStdDev, angleNoiseStdDev, column_format,
692 t0_specified ? t0 : (traj_t.empty() ? 0.0 : traj_t.front()));
701 }
else if (physical_moving_raster) {
703 scanID =
addScanMovingRaster(size.
x, thetaMin, thetaMax, size.
y, phiMin, phiMax, PRF, traj_t, traj_pos, traj_quat, lever_arm, boresight_rpy, exitDiameter, beamDivergence, rangeNoiseStdDev, angleNoiseStdDev, column_format,
704 t0_specified ? t0 : (traj_t.empty() ? 0.0 : traj_t.front()));
715 ScanMetadata scan(origin, size.
x, thetaMin, thetaMax, size.
y, phiMin, phiMax, exitDiameter, beamDivergence, rangeNoiseStdDev, angleNoiseStdDev, column_format, scanTiltRoll, scanTiltPitch, scanAzimuthOffset);
730 std::string data_filename =
deblank(s.child_value(
"filename"));
732 if (!data_filename.empty()) {
738 std::string resolved_data_file;
739 std::vector<std::string> candidates;
740 candidates.push_back(
"input/" + data_filename);
741 candidates.push_back(data_filename);
742 if (!xml_parent_dir.empty()) {
743 candidates.push_back((xml_parent_dir / data_filename).
string());
745 for (
const std::string &candidate: candidates) {
746 ifstream f(candidate);
748 resolved_data_file = candidate;
752 if (resolved_data_file.empty()) {
754 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): Data file `" + data_filename +
"' given for scan #" + std::to_string(scan_count) +
" does not exist.");
757 scans.at(scanID).data_file = resolved_data_file;
761 total_hits +=
loadASCIIFile(scanID, scans.at(scanID).data_file);
780 for (pugi::xml_node s = helios.child(
"grid"); s; s = s.next_sibling(
"grid")) {
783 const char *center_str = s.child_value(
"center");
785 if (strlen(center_str) == 0) {
787 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): A center was not specified for grid #" + std::to_string(cell_count));
793 const char *gsize_str = s.child_value(
"size");
795 if (strlen(gsize_str) == 0) {
797 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): A size was not specified for grid cell #" + std::to_string(cell_count));
802 if (gsize.
x <= 0 || gsize.
y <= 0 || gsize.
z <= 0) {
809 const char *grot_str = s.child_value(
"rotation");
811 if (strlen(grot_str) == 0) {
814 rotation = atof(grot_str);
820 const char *Nx_str = s.child_value(
"Nx");
822 if (strlen(Nx_str) == 0) {
829 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): The number of grid cells must be positive.");
832 const char *Ny_str = s.child_value(
"Ny");
834 if (strlen(Ny_str) == 0) {
841 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): The number of grid cells must be positive.");
844 const char *Nz_str = s.child_value(
"Nz");
846 if (strlen(Nz_str) == 0) {
853 helios_runtime_error(
"ERROR (LiDARcloud::loadXML): The number of grid cells must be positive.");
860 vec3 gsubsize =
make_vec3(
float(gsize.
x) /
float(Nx),
float(gsize.
y) /
float(Ny),
float(gsize.
z) /
float(Nz));
864 for (
int k = 0; k < Nz; k++) {
865 z = -0.5f * float(gsize.
z) + (float(k) + 0.5f) *
float(gsubsize.
z);
866 for (
int j = 0; j < Ny; j++) {
867 y = -0.5f * float(gsize.
y) + (float(j) + 0.5f) *
float(gsubsize.
y);
868 for (
int i = 0; i < Nx; i++) {
869 x = -0.5f * float(gsize.
x) + (float(i) + 0.5f) *
float(gsubsize.
x);
876 cout <<
"Adding grid cell #" << count <<
" with center " << subcenter_rot.
x + center.
x <<
"," << subcenter_rot.
y + center.
y <<
"," << subcenter.
z + center.
z <<
" and size " << gsubsize.
x <<
" x " << gsubsize.
y <<
" x "
877 << gsubsize.
z << endl;
880 addGridCell(subcenter + center, center, gsubsize, gsize, rotation *
M_PI / 180.f,
make_int3(i, j, k),
make_int3(Nx, Ny, Nz));
890 cout <<
"done." << endl;
892 cout <<
"Successfully read " <<
getScanCount() <<
" scan(s), which contain " << total_hits <<
" total hit points." << endl;
1533 ensureOutputDirectoryExists(filename);
1535 std::filesystem::path xml_path(filename);
1536 std::filesystem::path parent_dir = xml_path.parent_path();
1537 std::string stem = xml_path.stem().string();
1539 pugi::xml_document xmldoc;
1540 pugi::xml_node helios_node = xmldoc.append_child(
"helios");
1544 std::string xyz_basename = stem +
"_" + std::to_string(i) +
".xyz";
1545 std::filesystem::path xyz_path = parent_dir / xyz_basename;
1546 std::string xyz_path_str = xyz_path.string();
1552 pugi::xml_node scan_node = helios_node.append_child(
"scan");
1567 if (column_format.empty()) {
1568 column_format = {
"x",
"y",
"z"};
1571 auto append_text_child = [&](
const char *tag,
const std::string &text) {
1572 pugi::xml_node child = scan_node.append_child(tag);
1573 child.append_child(pugi::node_pcdata).set_value(text.c_str());
1579 const bool has_perpoint_origin = (std::find(column_format.begin(), column_format.end(),
"origin_x") != column_format.end() && std::find(column_format.begin(), column_format.end(),
"origin_y") != column_format.end() &&
1580 std::find(column_format.begin(), column_format.end(),
"origin_z") != column_format.end());
1581 if (!has_perpoint_origin) {
1582 std::ostringstream origin_ss;
1583 origin_ss << origin.
x <<
" " << origin.
y <<
" " << origin.
z;
1584 append_text_child(
"origin", origin_ss.str());
1596 append_text_child(
"scanPattern",
"spinning_multibeam");
1597 std::ostringstream elev_ss;
1599 for (
size_t c = 0; c < beam_zenith_angles.size(); c++) {
1603 elev_ss << (0.5f * float(
M_PI) - beam_zenith_angles[c]) * 180.f /
float(
M_PI);
1605 append_text_child(
"beamElevationAngles", elev_ss.str());
1608 }
else if (is_risley) {
1609 append_text_child(
"scanPattern",
"risley");
1617 std::ostringstream prism_ss;
1618 prism_ss << std::setprecision(12) << prism.wedge_angle * 180.0 /
M_PI <<
" " << prism.refractive_index <<
" " << prism.rotor_rate / (2.0 *
M_PI) <<
" " << prism.phase * 180.0 /
M_PI;
1619 append_text_child(
"prism", prism_ss.str());
1621 }
else if (!is_moving_raster) {
1622 std::ostringstream size_ss;
1623 size_ss << Ntheta <<
" " << Nphi;
1624 append_text_child(
"size", size_ss.str());
1630 if (is_moving_raster) {
1631 std::ostringstream size_ss;
1632 size_ss << Ntheta <<
" " << Nphi;
1633 append_text_child(
"size", size_ss.str());
1635 if (!is_spinning && !is_risley) {
1636 append_text_child(
"thetaMin", std::to_string(theta_range.
x * 180.f /
float(
M_PI)));
1637 append_text_child(
"thetaMax", std::to_string(theta_range.
y * 180.f /
float(
M_PI)));
1638 append_text_child(
"phiMin", std::to_string(phi_range.
x * 180.f /
float(
M_PI)));
1639 append_text_child(
"phiMax", std::to_string(phi_range.
y * 180.f /
float(
M_PI)));
1646 if (is_spinning || is_moving_raster || is_risley) {
1651 append_text_child(
"PRF", std::to_string(1.0 / sm.
pulse_period));
1655 append_text_child(
"azimuthStep", std::to_string(360.0 /
double(sm.
steps_per_rev)));
1658 std::ostringstream lever_ss;
1660 append_text_child(
"leverArm", lever_ss.str());
1663 std::ostringstream boresight_ss;
1665 append_text_child(
"boresight", boresight_ss.str());
1668 append_text_child(
"t0", std::to_string(sm.
t0));
1672 std::string traj_basename = stem +
"_" + std::to_string(i) +
"_traj.csv";
1673 std::filesystem::path traj_path = parent_dir / traj_basename;
1674 std::ofstream traj_file(traj_path.string());
1675 if (!traj_file.is_open()) {
1676 helios_runtime_error(
"ERROR (LiDARcloud::exportScans): Could not write trajectory file '" + traj_path.string() +
"'.");
1678 traj_file <<
"# t x y z qx qy qz qw\n";
1679 for (
size_t k = 0; k < sm.
traj_t.size(); k++) {
1682 traj_file << sm.
traj_t.at(k) <<
" " << p.
x <<
" " << p.
y <<
" " << p.
z <<
" " << q.
x <<
" " << q.
y <<
" " << q.
z <<
" " << q.
w <<
"\n";
1685 append_text_child(
"trajectoryFile", traj_basename);
1688 append_text_child(
"exitDiameter", std::to_string(exit_diameter));
1689 append_text_child(
"beamDivergence", std::to_string(beam_divergence));
1690 append_text_child(
"rangeNoiseStdDev", std::to_string(range_noise_stddev));
1691 append_text_child(
"angleNoiseStdDev", std::to_string(angle_noise_stddev));
1695 append_text_child(
"returnMode",
"single");
1698 append_text_child(
"singleReturnSelection",
"first");
1700 append_text_child(
"singleReturnSelection",
"last");
1702 append_text_child(
"singleReturnSelection",
"strongest_plus_last");
1704 append_text_child(
"singleReturnSelection",
"strongest");
1717 if (scan_tilt_roll != 0.f || scan_tilt_pitch != 0.f) {
1718 std::ostringstream tilt_ss;
1719 tilt_ss << scan_tilt_roll * 180.f / float(
M_PI) <<
" " << scan_tilt_pitch * 180.f / float(
M_PI);
1720 append_text_child(
"scanTilt", tilt_ss.str());
1723 if (scan_azimuth_offset != 0.f) {
1724 append_text_child(
"scanAzimuthOffset", std::to_string(scan_azimuth_offset * 180.f /
float(
M_PI)));
1727 std::ostringstream format_ss;
1728 for (
size_t c = 0; c < column_format.size(); c++) {
1732 format_ss << column_format[c];
1734 append_text_child(
"ASCII_format", format_ss.str());
1736 append_text_child(
"filename", xyz_basename);
1739 if (!xmldoc.save_file(filename)) {
1740 helios_runtime_error(
"ERROR (LiDARcloud::exportScans): Could not write XML metadata file '" + std::string(filename) +
"'.");