1.3.64
 
Loading...
Searching...
No Matches
fileIO.cpp
Go to the documentation of this file.
1
16#include "LiDAR.h"
17#include "pugixml.hpp"
18
19using namespace helios;
20using namespace std;
21
22// Helper function to ensure output directory exists
23void ensureOutputDirectoryExists(const char *filename) {
24 std::filesystem::path file_path(filename);
25 std::filesystem::path dir_path = file_path.parent_path();
26
27 if (!dir_path.empty() && !std::filesystem::exists(dir_path)) {
28 std::error_code ec;
29 if (!std::filesystem::create_directories(dir_path, ec)) {
30 helios_runtime_error("ERROR: Could not create output directory '" + dir_path.string() + "': " + ec.message());
31 }
32 }
33}
34
35void LiDARcloud::loadXML(const char *filename) {
36 loadXML(filename, false);
37}
38
39void LiDARcloud::loadXML(const char *filename, bool load_grid_only) {
40
41 if (printmessages) {
42 cout << "Reading XML file: " << filename << "..." << flush;
43 }
44
45 // Check if file exists
46 ifstream f(filename);
47 if (!f.good()) {
48 cerr << "failed.\n";
49 helios_runtime_error("ERROR (LiDARcloud::loadXML): XML file does not exist.");
50 }
51
52 // Using "pugixml" parser. See pugixml.org
53 pugi::xml_document xmldoc;
54
55 // Resolve file path using project-based resolution
56 std::filesystem::path resolved_path = resolveProjectFile(filename);
57 std::string resolved_filename = resolved_path.string();
58
59 // load file
60 pugi::xml_parse_result result = xmldoc.load_file(resolved_filename.c_str());
61
62 // error checking
63 if (!result) {
64 cout << "failed." << endl;
65 cerr << "XML file " << filename << " parsed with errors, attribute value: [" << xmldoc.child("node").attribute("attr").value() << "]\n";
66 helios_runtime_error("ERROR (LiDARcloud::loadXML): Errors were found while parsing XML file. Error description: " + std::string(result.description()));
67 }
68
69 pugi::xml_node helios = xmldoc.child("helios");
70
71 if (helios.empty()) {
72 std::cout << "failed." << std::endl;
73 helios_runtime_error("ERROR (LiDARcloud::loadXML): XML file must have tag '<helios> ... </helios>' bounding all other tags.");
74 }
75
76 //-------------- Scans ---------------//
77
78 uint scan_count = 0; // counter variable for scans
79 size_t total_hits = 0;
80
81 if (load_grid_only == false) {
82
83 // looping over any scans specified in XML file
84 for (pugi::xml_node s = helios.child("scan"); s; s = s.next_sibling("scan")) {
85
86 // ----- scan origin ------//
87 const char *origin_str = s.child_value("origin");
88
89 if (strlen(origin_str) == 0) {
90 cerr << "failed.\n";
91 helios_runtime_error("ERROR (LiDARcloud::loadXML): An origin was not specified for scan #" + std::to_string(scan_count));
92 }
93
94 vec3 origin = string2vec3(origin_str); // note: pugi loads xml data as a character. need to separate it into 3 floats
95
96 // ----- scan size (resolution) ------//
97 const char *size_str = s.child_value("size");
98
99 helios::int2 size;
100 if (strlen(size_str) == 0) {
101 cerr << "failed.\n";
102 helios_runtime_error("ERROR (LiDARcloud::loadXML): A size was not specified for scan #" + std::to_string(scan_count));
103 } else {
104 size = string2int2(size_str); // note: pugi loads xml data as a character. need to separate it into 2 ints
105 }
106 if (size.x <= 0 || size.y <= 0) {
107 cerr << "failed.\n";
108 helios_runtime_error("ERROR (LiDARcloud::loadXML): The scan size must be positive (check scan #" + std::to_string(scan_count) + ").");
109 }
110
111 // ----- scan translation ------//
112 const char *offset_str = s.child_value("translation");
113
114 vec3 translation = make_vec3(0, 0, 0);
115 if (strlen(offset_str) > 0) {
116 translation = string2vec3(offset_str); // note: pugi loads xml data as a character. need to separate it into 3 floats
117 }
118
119 // ----- scan rotation ------//
120 const char *rotation_str = s.child_value("rotation");
121
122 SphericalCoord rotation_sphere(0, 0, 0);
123 if (strlen(rotation_str) > 0) {
124 vec2 rotation = string2vec2(rotation_str); // note: pugi loads xml data as a character. need to separate it into 2 floats
125 rotation = rotation * M_PI / 180.f;
126 rotation_sphere = make_SphericalCoord(rotation.x, rotation.y);
127 }
128
129 // ----- thetaMin ------//
130 const char *thetaMin_str = s.child_value("thetaMin");
131
132 float thetaMin;
133 if (strlen(thetaMin_str) == 0) {
134 // cerr << "WARNING (loadXML): A minimum zenithal scan angle was not specified for scan #" << scan_count << "...assuming thetaMin = 0." << flush;
135 thetaMin = 0.f;
136 } else {
137 thetaMin = atof(thetaMin_str) * M_PI / 180.f;
138 }
139
140 if (thetaMin < 0) {
141 helios_runtime_error("ERROR (LiDARcloud::loadXML): thetaMin cannot be less than 0.");
142 }
143
144 // ----- thetaMax ------//
145 const char *thetaMax_str = s.child_value("thetaMax");
146
147 float thetaMax;
148 if (strlen(thetaMax_str) == 0) {
149 thetaMax = M_PI;
150 } else {
151 thetaMax = atof(thetaMax_str) * M_PI / 180.f;
152 }
153
154 if (thetaMax - 1e-5 > M_PI) {
155 helios_runtime_error("ERROR (LiDARcloud::loadXML): thetaMax cannot be greater than 180 degrees.");
156 }
157
158 // ----- phiMin ------//
159 const char *phiMin_str = s.child_value("phiMin");
160
161 float phiMin;
162 if (strlen(phiMin_str) == 0) {
163 phiMin = 0.f;
164 } else {
165 phiMin = atof(phiMin_str) * M_PI / 180.f;
166 }
167
168 if (phiMin < 0) {
169 helios_runtime_error("ERROR (LiDARcloud::loadXML): phiMin cannot be less than 0.");
170 }
171
172 // ----- phiMax ------//
173 const char *phiMax_str = s.child_value("phiMax");
174
175 float phiMax;
176 if (strlen(phiMax_str) == 0) {
177 phiMax = 2.f * M_PI;
178 } else {
179 phiMax = atof(phiMax_str) * M_PI / 180.f;
180 }
181
182 if (phiMax - 1e-5 > 4.f * M_PI) {
183 helios_runtime_error("ERROR (LiDARcloud::loadXML): phiMax cannot be greater than 720 degrees.");
184 }
185
186 // ----- exitDiameter ------//
187 const char *exitDiameter_str_uc = s.child_value("exitDiameter");
188 const char *exitDiameter_str_lc = s.child_value("exitdiameter");
189
190 float exitDiameter;
191 if (strlen(exitDiameter_str_uc) == 0 && strlen(exitDiameter_str_lc) == 0) {
192 exitDiameter = 0;
193 } else if (strlen(exitDiameter_str_uc) > 0) {
194 exitDiameter = fmax(0, atof(exitDiameter_str_uc));
195 } else {
196 exitDiameter = fmax(0, atof(exitDiameter_str_lc));
197 }
198
199 // ----- beamDivergence ------//
200 const char *beamDivergence_str_uc = s.child_value("beamDivergence");
201 const char *beamDivergence_str_lc = s.child_value("beamdivergence");
202
203 float beamDivergence;
204 if (strlen(beamDivergence_str_uc) == 0 && strlen(beamDivergence_str_lc) == 0) {
205 beamDivergence = 0;
206 } else if (strlen(beamDivergence_str_uc) > 0) {
207 beamDivergence = fmax(0, atof(beamDivergence_str_uc));
208 } else {
209 beamDivergence = fmax(0, atof(beamDivergence_str_lc));
210 }
211
212 // ----- distanceFilter ------//
213 const char *dFilter_str = s.child_value("distanceFilter");
214
215 float distanceFilter = -1;
216 if (strlen(dFilter_str) > 0) {
217 distanceFilter = atof(dFilter_str);
218 }
219
220 // ------ ASCII data file format ------- //
221
222 const char *data_format = s.child_value("ASCII_format");
223
224 std::vector<std::string> column_format;
225 if (strlen(data_format) != 0) {
226
227 std::string tmp;
228
229 std::istringstream stream(data_format);
230 while (stream >> tmp) {
231 column_format.push_back(tmp);
232 }
233 }
234
235 // create a temporary scan object
236 ScanMetadata scan(origin, size.x, thetaMin, thetaMax, size.y, phiMin, phiMax, exitDiameter, beamDivergence, column_format);
237
238 addScan(scan);
239
240 uint scanID = getScanCount() - 1;
241
242 // ----- ASCII data file name ------//
243 std::string data_filename = deblank(s.child_value("filename"));
244
245 if (!data_filename.empty()) {
246
247 char str[100];
248 strcpy(str, "input/"); // first look in the input directory
249 strcat(str, data_filename.c_str());
250 ifstream f(str);
251 if (!f.good()) {
252
253 // if not in input directory, try absolute path
254 strcpy(str, data_filename.c_str());
255 f.open(str);
256
257 if (!f.good()) {
258 cout << "failed.\n";
259 helios_runtime_error("ERROR (LiDARcloud::loadXML): Data file `" + std::string(str) + "' given for scan #" + std::to_string(scan_count) + " does not exist.");
260 }
261 f.close();
262 }
263
264 scan.data_file = str; // set the data file for the scan
265
266 // add hit points to scan if data file was given
267
268 total_hits += loadASCIIFile(scanID, scan.data_file);
269
270 if (translation.magnitude() > 0.f) {
271 coordinateShift(scanID, translation);
272 }
273 if (rotation_sphere.elevation != 0 || rotation_sphere.azimuth != 0) {
274 coordinateRotation(scanID, rotation_sphere);
275 }
276 }
277
278 scan_count++;
279 }
280 }
281
282 //------------ Grids ------------//
283
284 uint cell_count = 0; // counter variable for scans
285
286 // looping over any grids specified in XML file
287 for (pugi::xml_node s = helios.child("grid"); s; s = s.next_sibling("grid")) {
288
289 // ----- grid center ------//
290 const char *center_str = s.child_value("center");
291
292 if (strlen(center_str) == 0) {
293 cerr << "failed.\n";
294 helios_runtime_error("ERROR (LiDARcloud::loadXML): A center was not specified for grid #" + std::to_string(cell_count));
295 }
296
297 vec3 center = string2vec3(center_str); // note: pugi loads xml data as a character. need to separate it into 3 floats
298
299 // ----- grid size ------//
300 const char *gsize_str = s.child_value("size");
301
302 if (strlen(gsize_str) == 0) {
303 cerr << "failed.\n";
304 helios_runtime_error("ERROR (LiDARcloud::loadXML): A size was not specified for grid cell #" + std::to_string(cell_count));
305 }
306
307 vec3 gsize = string2vec3(gsize_str); // note: pugi loads xml data as a character. need to separate it into 3 floats
308
309 if (gsize.x <= 0 || gsize.y <= 0 || gsize.z <= 0) {
310 cerr << "failed.\n";
311 helios_runtime_error("ERROR (LiDARcloud::loadXML): The grid cell size must be positive.");
312 }
313
314 // ----- grid rotation ------//
315 float rotation;
316 const char *grot_str = s.child_value("rotation");
317
318 if (strlen(grot_str) == 0) {
319 rotation = 0; // if no rotation specified, assume = 0
320 } else {
321 rotation = atof(grot_str);
322 }
323
324 // ----- grid cells ------//
325 uint Nx, Ny, Nz;
326
327 const char *Nx_str = s.child_value("Nx");
328
329 if (strlen(Nx_str) == 0) { // If no Nx specified, assume Nx=1;
330 Nx = 1;
331 } else {
332 Nx = atof(Nx_str);
333 }
334 if (Nx <= 0) {
335 cerr << "failed.\n";
336 helios_runtime_error("ERROR (LiDARcloud::loadXML): The number of grid cells must be positive.");
337 }
338
339 const char *Ny_str = s.child_value("Ny");
340
341 if (strlen(Ny_str) == 0) { // If no Ny specified, assume Ny=1;
342 Ny = 1;
343 } else {
344 Ny = atof(Ny_str);
345 }
346 if (Ny <= 0) {
347 cerr << "failed.\n";
348 helios_runtime_error("ERROR (LiDARcloud::loadXML): The number of grid cells must be positive.");
349 }
350
351 const char *Nz_str = s.child_value("Nz");
352
353 if (strlen(Nz_str) == 0) { // If no Nz specified, assume Nz=1;
354 Nz = 1;
355 } else {
356 Nz = atof(Nz_str);
357 }
358 if (Nz <= 0) {
359 cerr << "failed.\n";
360 helios_runtime_error("ERROR (LiDARcloud::loadXML): The number of grid cells must be positive.");
361 }
362
363 int3 gridDivisions = helios::make_int3(Nx, Ny, Nz);
364
365 // add cells to grid
366
367 vec3 gsubsize = make_vec3(float(gsize.x) / float(Nx), float(gsize.y) / float(Ny), float(gsize.z) / float(Nz));
368
369 float x, y, z;
370 uint count = 0;
371 for (int k = 0; k < Nz; k++) {
372 z = -0.5f * float(gsize.z) + (float(k) + 0.5f) * float(gsubsize.z);
373 for (int j = 0; j < Ny; j++) {
374 y = -0.5f * float(gsize.y) + (float(j) + 0.5f) * float(gsubsize.y);
375 for (int i = 0; i < Nx; i++) {
376 x = -0.5f * float(gsize.x) + (float(i) + 0.5f) * float(gsubsize.x);
377
378 vec3 subcenter = make_vec3(x, y, z);
379
380 vec3 subcenter_rot = rotatePoint(subcenter, make_SphericalCoord(0, rotation * M_PI / 180.f));
381
382 if (printmessages) {
383 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 "
384 << gsubsize.z << endl;
385 }
386
387 addGridCell(subcenter + center, center, gsubsize, gsize, rotation * M_PI / 180.f, make_int3(i, j, k), make_int3(Nx, Ny, Nz));
388
389 count++;
390 }
391 }
392 }
393 }
394
395 if (printmessages) {
396
397 cout << "done." << endl;
398
399 cout << "Successfully read " << getScanCount() << " scan(s), which contain " << total_hits << " total hit points." << endl;
400 }
401}
402
403size_t LiDARcloud::loadASCIIFile(uint scanID, const std::string &ASCII_data_file) {
404
405 // Resolve file path using project-based resolution
406 std::filesystem::path resolved_path = resolveProjectFile(ASCII_data_file);
407 std::string resolved_filename = resolved_path.string();
408
409 ifstream datafile(resolved_filename); // open the file
410
411 if (!datafile.is_open()) { // check that file exists
412 helios_runtime_error("ERROR (LiDARcloud::loadASCIIFile): ASCII data file '" + ASCII_data_file + "' does not exist.");
413 }
414
415 if (scanID >= getScanCount()) {
416 helios_runtime_error("ERROR (LiDARcloud::loadASCIIFile): Scan #" + std::to_string(scanID) + " does not exist.");
417 }
418 const ScanMetadata &scan_data = scans.at(scanID);
419
420 vec3 temp_xyz;
421 float temp_zenith, temp_azimuth;
422 RGBcolor temp_rgb;
423 float temp_row, temp_column;
424 double temp_data;
425 std::map<std::string, double> data;
426
427 vector<unsigned int> row, column;
428 std::size_t hit_count = 0;
429 while (datafile.good()) { // loop through file to read scan data
430
431 temp_xyz = make_vec3(-9999, -9999, -9999);
432 temp_rgb = make_RGBcolor(1, 0, 0); // default color: red
433 temp_row = -1;
434 temp_column = -1;
435 temp_zenith = -9999;
436 temp_azimuth = -9999;
437
438 for (uint i = 0; i < scan_data.columnFormat.size(); i++) {
439 if (scan_data.columnFormat.at(i) == "row") {
440 datafile >> temp_row;
441 } else if (scan_data.columnFormat.at(i) == "column") {
442 datafile >> temp_column;
443 } else if (scan_data.columnFormat.at(i) == "zenith") {
444 datafile >> temp_zenith;
445 temp_zenith = deg2rad(temp_zenith);
446 } else if (scan_data.columnFormat.at(i) == "azimuth") {
447 datafile >> temp_azimuth;
448 temp_azimuth = deg2rad(temp_azimuth);
449 } else if (scan_data.columnFormat.at(i) == "zenith_rad") {
450 datafile >> temp_zenith;
451 } else if (scan_data.columnFormat.at(i) == "azimuth_rad") {
452 datafile >> temp_azimuth;
453 } else if (scan_data.columnFormat.at(i) == "x") {
454 datafile >> temp_xyz.x;
455 } else if (scan_data.columnFormat.at(i) == "y") {
456 datafile >> temp_xyz.y;
457 } else if (scan_data.columnFormat.at(i) == "z") {
458 datafile >> temp_xyz.z;
459 } else if (scan_data.columnFormat.at(i) == "r") {
460 datafile >> temp_rgb.r;
461 } else if (scan_data.columnFormat.at(i) == "g") {
462 datafile >> temp_rgb.g;
463 } else if (scan_data.columnFormat.at(i) == "b") {
464 datafile >> temp_rgb.b;
465 } else if (scan_data.columnFormat.at(i) == "r255") {
466 datafile >> temp_rgb.r;
467 temp_rgb.r /= 255.f;
468 } else if (scan_data.columnFormat.at(i) == "g255") {
469 datafile >> temp_rgb.g;
470 temp_rgb.g /= 255.f;
471 } else if (scan_data.columnFormat.at(i) == "b255") {
472 datafile >> temp_rgb.b;
473 temp_rgb.b /= 255.f;
474 } else { // assume that rest is data
475 datafile >> temp_data;
476 data[scan_data.columnFormat.at(i)] = temp_data;
477 }
478 }
479
480 if (!datafile.good()) { // if the whole line was not read successfully, stop
481 if (hit_count == 0) {
482 std::cerr << "WARNING: Something is likely wrong with the data file " << ASCII_data_file << ". Check that the format is consistent with that specified in the XML metadata file." << std::endl;
483 }
484 break;
485 }
486
487 // -- Checks to make sure everything was specified correctly -- //
488
489 // hit point
490 if (temp_xyz.x == -9999) {
491 helios_runtime_error("ERROR (LiDARcloud::loadASCIIFile): x-coordinate not specified for hit point #" + std::to_string(hit_count) + " of scan #" + std::to_string(scanID));
492 } else if (temp_xyz.y == -9999) {
493 helios_runtime_error("ERROR (LiDARcloud::loadASCIIFile): y-coordinate not specified for hit point #" + std::to_string(hit_count) + " of scan #" + std::to_string(scanID));
494 } else if (temp_xyz.z == -9999) {
495 helios_runtime_error("ERROR (LiDARcloud::loadASCIIFile): z-coordinate not specified for hit point #" + std::to_string(hit_count) + " of scan #" + std::to_string(scanID));
496 }
497
498 // direction
499 SphericalCoord temp_direction(1.f, 0.5f * M_PI - temp_zenith, temp_azimuth);
500 if (temp_direction.elevation == -9999 || temp_direction.azimuth == -9999) {
501 temp_direction = cart2sphere(temp_xyz - scan_data.origin);
502 }
503
504 // add hit point to the scan
505 addHitPoint(scanID, temp_xyz, temp_direction, temp_rgb, data);
506
507 hit_count++;
508 }
509
510 datafile.close();
511
512 return hit_count;
513}
514
515void LiDARcloud::exportTriangleNormals(const char *filename) {
516
517 ensureOutputDirectoryExists(filename);
518
519 ofstream file;
520
521 file.open(filename);
522
523 if (!file.is_open()) {
524 helios_runtime_error("ERROR (LiDARcloud::exportTriangleNormals): Could not open file '" + std::string(filename) + "' for writing.");
525 }
526
527 for (std::size_t t = 0; t < triangles.size(); t++) {
528
529 Triangulation tri = triangles.at(t);
530
531 vec3 v0 = tri.vertex0;
532 vec3 v1 = tri.vertex1;
533 vec3 v2 = tri.vertex2;
534
535 vec3 normal = cross(v1 - v0, v2 - v0);
536 normal.normalize();
537
538 file << normal.x << " " << normal.y << " " << normal.z << std::endl;
539 }
540
541 file.close();
542}
543
544void LiDARcloud::exportTriangleNormals(const char *filename, int gridcell) {
545
546 ensureOutputDirectoryExists(filename);
547
548 ofstream file;
549
550 file.open(filename);
551
552 if (!file.is_open()) {
553 helios_runtime_error("ERROR (LiDARcloud::exportTriangleNormals): Could not open file '" + std::string(filename) + "' for writing.");
554 }
555
556 for (std::size_t t = 0; t < triangles.size(); t++) {
557
558 Triangulation tri = triangles.at(t);
559
560 if (tri.gridcell == gridcell) {
561
562 vec3 v0 = tri.vertex0;
563 vec3 v1 = tri.vertex1;
564 vec3 v2 = tri.vertex2;
565
566 vec3 normal = cross(v1 - v0, v2 - v0);
567 normal.normalize();
568
569 file << normal.x << " " << normal.y << " " << normal.z << std::endl;
570 }
571 }
572
573 file.close();
574}
575
576void LiDARcloud::exportTriangleAreas(const char *filename) {
577
578 ensureOutputDirectoryExists(filename);
579
580 ofstream file;
581
582 file.open(filename);
583
584 if (!file.is_open()) {
585 helios_runtime_error("ERROR (LiDARcloud::exportTriangleAreas): Could not open file '" + std::string(filename) + "' for writing.");
586 }
587
588 for (std::size_t t = 0; t < triangles.size(); t++) {
589
590 Triangulation tri = triangles.at(t);
591
592 file << tri.area << std::endl;
593 }
594
595 file.close();
596}
597
598void LiDARcloud::exportTriangleAreas(const char *filename, int gridcell) {
599
600 ensureOutputDirectoryExists(filename);
601
602 ofstream file;
603
604 file.open(filename);
605
606 if (!file.is_open()) {
607 helios_runtime_error("ERROR (LiDARcloud::exportTriangleAreas): Could not open file '" + std::string(filename) + "' for writing.");
608 }
609
610 for (std::size_t t = 0; t < triangles.size(); t++) {
611
612 Triangulation tri = triangles.at(t);
613
614 if (tri.gridcell == gridcell) {
615
616 file << tri.area << std::endl;
617 }
618 }
619
620 file.close();
621}
622
624
625 ensureOutputDirectoryExists(filename);
626
627 std::vector<std::vector<float>> inclinations(getGridCellCount());
628 for (int i = 0; i < getGridCellCount(); i++) {
629 inclinations.at(i).resize(Nbins);
630 }
631 std::vector<float> cell_area(inclinations.size(), 0);
632
633 float db = 0.5f * M_PI / float(Nbins); // bin width
634
635 for (std::size_t t = 0; t < triangles.size(); t++) {
636
637 Triangulation tri = triangles.at(t);
638
639 int cell = tri.gridcell;
640
641 if (cell < 0) {
642 continue;
643 }
644
645 vec3 v0 = tri.vertex0;
646 vec3 v1 = tri.vertex1;
647 vec3 v2 = tri.vertex2;
648
649 vec3 normal = cross(v1 - v0, v2 - v0);
650 normal.normalize();
651
652 float angle = acos_safe(fabs(normal.z));
653
654 float area = tri.area;
655
656 uint bin = floor(angle / db);
657 if (bin >= Nbins) {
658 bin = Nbins - 1;
659 }
660
661 inclinations.at(cell).at(bin) += area;
662
663 cell_area.at(cell) += area;
664 }
665
666 ofstream file;
667
668 file.open(filename);
669
670 if (!file.is_open()) {
671 helios_runtime_error("ERROR (LiDARcloud::exportTriangleInclinationDistribution): Could not open file '" + std::string(filename) + "' for writing.");
672 }
673
674 for (int cell = 0; cell < getGridCellCount(); cell++) {
675 for (int bin = 0; bin < Nbins; bin++) {
676 file << inclinations.at(cell).at(bin) / cell_area.at(cell) << " ";
677 }
678 file << std::endl;
679 }
680
681 file.close();
682}
683
684void LiDARcloud::exportTriangleAzimuthDistribution(const char *filename, uint Nbins) {
685
686 ensureOutputDirectoryExists(filename);
687
688 std::vector<std::vector<float>> azimuths(getGridCellCount());
689 for (int i = 0; i < getGridCellCount(); i++) {
690 azimuths.at(i).resize(Nbins);
691 }
692 std::vector<float> cell_area(azimuths.size(), 0);
693
694 float db = 2 * M_PI / float(Nbins); // bin width
695
696 for (std::size_t t = 0; t < triangles.size(); t++) {
697
698 Triangulation tri = triangles.at(t);
699
700 int cell = tri.gridcell;
701
702 if (cell < 0) {
703 continue;
704 }
705
706 vec3 v0 = tri.vertex0;
707 vec3 v1 = tri.vertex1;
708 vec3 v2 = tri.vertex2;
709
710 vec3 normal = cross(v1 - v0, v2 - v0);
711 normal.normalize();
712 SphericalCoord n_sph = cart2sphere(normal);
713
714 float azimuth = n_sph.azimuth;
715
716 if (normal.z < 0) {
717 azimuth = azimuth + M_PI;
718 if (azimuth > M_PI * 2) {
719 azimuth = azimuth - M_PI * 2;
720 }
721 }
722
723
724 float area = tri.area;
725
726 uint bin = floor(azimuth / db);
727 if (bin >= Nbins) {
728 bin = Nbins - 1;
729 }
730
731 azimuths.at(cell).at(bin) += area;
732 cell_area.at(cell) += area;
733 }
734
735 ofstream file;
736
737 file.open(filename);
738
739 if (!file.is_open()) {
740 helios_runtime_error("ERROR (LiDARcloud::exportTriangleAzimuthDistribution): Could not open file '" + std::string(filename) + "' for writing.");
741 }
742
743 for (int cell = 0; cell < getGridCellCount(); cell++) {
744 for (int bin = 0; bin < Nbins; bin++) {
745 file << azimuths.at(cell).at(bin) / cell_area.at(cell) << " ";
746 }
747 file << std::endl;
748 }
749
750 file.close();
751}
752
753void LiDARcloud::exportLeafAreas(const char *filename) {
754
755 ensureOutputDirectoryExists(filename);
756
757 ofstream file;
758
759 file.open(filename);
760
761 if (!file.is_open()) {
762 helios_runtime_error("ERROR (LiDARcloud::exportLeafAreas): Could not open file '" + std::string(filename) + "' for writing.");
763 }
764
765 for (uint i = 0; i < getGridCellCount(); i++) {
766
767 file << getCellLeafArea(i) << std::endl;
768 }
769
770 file.close();
771}
772
773void LiDARcloud::exportLeafAreaDensities(const char *filename) {
774
775 ensureOutputDirectoryExists(filename);
776
777 ofstream file;
778
779 file.open(filename);
780
781 if (!file.is_open()) {
782 helios_runtime_error("ERROR (LiDARcloud::exportLeafAreaDensities): Could not open file '" + std::string(filename) + "' for writing.");
783 }
784
785 for (uint i = 0; i < getGridCellCount(); i++) {
786
787 file << getCellLeafAreaDensity(i) << std::endl;
788 }
789
790 file.close();
791}
792
793void LiDARcloud::exportGtheta(const char *filename) {
794
795 ensureOutputDirectoryExists(filename);
796
797 ofstream file;
798
799 file.open(filename);
800
801 if (!file.is_open()) {
802 helios_runtime_error("ERROR (LiDARcloud::exportGtheta): Could not open file '" + std::string(filename) + "' for writing.");
803 }
804
805 for (uint i = 0; i < getGridCellCount(); i++) {
806
807 file << getCellGtheta(i) << std::endl;
808 }
809
810 file.close();
811}
812
813void LiDARcloud::exportPointCloud(const char *filename) {
814
815 if (getScanCount() == 1) {
816 exportPointCloud(filename, 0);
817 } else {
818
819 for (int i = 0; i < getScanCount(); i++) {
820
821 std::string filename_a = filename;
822 char scan[20];
823 snprintf(scan, sizeof(scan), "%d", i);
824
825 size_t dotindex = filename_a.find_last_of(".");
826 if (dotindex == filename_a.size() - 1 || filename_a.size() - 1 - dotindex > 4) { // no file extension was provided
827 filename_a = filename_a + "_" + scan;
828 } else { // has file extension
829 std::string ext = filename_a.substr(dotindex, filename_a.size() - 1);
830 filename_a = filename_a.substr(0, dotindex) + "_" + scan + ext;
831 }
832
833 exportPointCloud(filename_a.c_str(), i);
834 }
835 }
836}
837
838
839void LiDARcloud::exportPointCloud(const char *filename, uint scanID) {
840
841 ensureOutputDirectoryExists(filename);
842
843 if (scanID > getScanCount()) {
844 std::cerr << "ERROR (LiDARcloud::exportPointCloud): Cannot export scan " << scanID << " because this scan does not exist." << std::endl;
845 throw 1;
846 }
847
848 ofstream file;
849
850 file.open(filename);
851
852 if (!file.is_open()) {
853 helios_runtime_error("ERROR (LiDARcloud::exportPointCloud): Could not open file '" + std::string(filename) + "' for writing.");
854 }
855
856 std::vector<std::string> hit_data;
857 for (int r = 0; r < getHitCount(); r++) {
858 std::map<std::string, double> data = hits.at(r).data;
859 for (std::map<std::string, double>::iterator iter = data.begin(); iter != data.end(); ++iter) {
860 std::vector<std::string>::iterator it = find(hit_data.begin(), hit_data.end(), iter->first);
861 if (it == hit_data.end()) {
862 hit_data.push_back(iter->first);
863 }
864 }
865 }
866
867 std::vector<std::string> ASCII_format = getScanColumnFormat(scanID);
868
869 if (ASCII_format.size() == 0) {
870 ASCII_format.push_back("x");
871 ASCII_format.push_back("y");
872 ASCII_format.push_back("z");
873 }
874
875 for (int r = 0; r < getHitCount(); r++) {
876
877 if (getHitScanID(r) != scanID) {
878 continue;
879 }
880
881 vec3 xyz = getHitXYZ(r);
882 RGBcolor color = getHitColor(r);
883
884 for (int c = 0; c < ASCII_format.size(); c++) {
885
886 if (ASCII_format.at(c).compare("x") == 0) {
887 file << xyz.x;
888 } else if (ASCII_format.at(c).compare("y") == 0) {
889 file << xyz.y;
890 } else if (ASCII_format.at(c).compare("z") == 0) {
891 file << xyz.z;
892 } else if (ASCII_format.at(c).compare("r") == 0) {
893 file << color.r;
894 } else if (ASCII_format.at(c).compare("g") == 0) {
895 file << color.g;
896 } else if (ASCII_format.at(c).compare("b") == 0) {
897 file << color.b;
898 } else if (ASCII_format.at(c).compare("r255") == 0) {
899 file << round(color.r * 255);
900 } else if (ASCII_format.at(c).compare("g255") == 0) {
901 file << round(color.g * 255);
902 } else if (ASCII_format.at(c).compare("b255") == 0) {
903 file << round(color.b * 255);
904 } else if (ASCII_format.at(c).compare("zenith") == 0) {
905 file << getHitRaydir(r).zenith;
906 } else if (ASCII_format.at(c).compare("azimuth") == 0) {
907 file << getHitRaydir(r).azimuth;
908 } else if (hits.at(r).data.find(ASCII_format.at(c)) != hits.at(r).data.end()) { // hit scalar data
909 file << getHitData(r, ASCII_format.at(c).c_str());
910 } else {
911 file << -9999;
912 }
913
914 if (c < ASCII_format.size() - 1) {
915 file << " ";
916 }
917 }
918
919 file << std::endl;
920 }
921
922 file.close();
923}
924
925void LiDARcloud::exportPointCloudPTX(const char *filename, uint scanID) {
926
927 ensureOutputDirectoryExists(filename);
928
929 if (scanID > getScanCount()) {
930 std::cerr << "ERROR (LiDARcloud::exportPointCloudPTX): Cannot export scan " << scanID << " because this scan does not exist." << std::endl;
931 throw 1;
932 }
933
934 ofstream file;
935
936 file.open(filename);
937
938 if (!file.is_open()) {
939 helios_runtime_error("ERROR (LiDARcloud::exportPointCloudPTX): Could not open file '" + std::string(filename) + "' for writing.");
940 }
941
942 std::vector<std::string> ASCII_format = getScanColumnFormat(scanID);
943
944 uint Nx = getScanSizeTheta(scanID);
945 uint Ny = getScanSizePhi(scanID);
946
947 file << Nx << std::endl;
948 file << Ny << std::endl;
949 file << "0 0 0" << std::endl;
950 file << "1 0 0" << std::endl;
951 file << "0 1 0" << std::endl;
952 file << "0 0 1" << std::endl;
953 file << "1 0 0 0" << std::endl;
954 file << "0 1 0 0" << std::endl;
955 file << "0 0 1 0" << std::endl;
956 file << "0 0 0 1" << std::endl;
957
958 std::vector<std::vector<vec4>> xyzi(Ny);
959 for (int j = 0; j < Ny; j++) {
960 xyzi.at(j).resize(Nx);
961 for (int i = 0; i < Nx; i++) {
962 xyzi.at(j).at(i) = make_vec4(0, 0, 0, 1);
963 }
964 }
965
966 vec3 origin = getScanOrigin(scanID);
967
968 for (int r = 0; r < getHitCount(); r++) {
969
970 if (getHitScanID(r) != scanID) {
971 continue;
972 }
973
974 SphericalCoord raydir = getHitRaydir(r);
975
976 int2 row_column = scans.at(scanID).direction2rc(raydir);
977
978 assert(row_column.x >= 0 && row_column.x < Nx && row_column.y >= 0 && row_column.y < Ny);
979
980 vec3 xyz = getHitXYZ(r);
981
982 if ((xyz - origin).magnitude() >= 1e4) {
983 continue;
984 }
985
986 float intensity = 1.f;
987 if (hits.at(r).data.find("intensity") != hits.at(r).data.end()) {
988 intensity = getHitData(r, "intensity");
989 }
990
991 xyzi.at(row_column.y).at(row_column.x) = make_vec4(xyz.x, xyz.y, xyz.z, intensity);
992 }
993
994 for (int j = 0; j < Ny; j++) {
995 for (int i = 0; i < Nx; i++) {
996 file << xyzi.at(j).at(i).x << " " << xyzi.at(j).at(i).y << " " << xyzi.at(j).at(i).z << " " << xyzi.at(j).at(i).w << std::endl;
997 }
998 }
999
1000 file.close();
1001}
1002
1003std::vector<uint> LiDARcloud::loadTreeQSM(helios::Context *context, const std::string &filename, uint radial_subdivisions, const std::string &texture_file) {
1004 return loadTreeQSM_impl(context, filename, radial_subdivisions, false, texture_file);
1005}
1006
1007std::vector<uint> LiDARcloud::loadTreeQSMColormap(helios::Context *context, const std::string &filename, uint radial_subdivisions, const std::string &colormap_name) {
1008 return loadTreeQSM_impl(context, filename, radial_subdivisions, true, colormap_name);
1009}
1010
1011std::vector<uint> LiDARcloud::loadTreeQSM_impl(helios::Context *context, const std::string &filename, uint radial_subdivisions, bool use_colormap, const std::string &colormap_or_texture) {
1012
1013 if (printmessages) {
1014 if (use_colormap) {
1015 std::cout << "Loading TreeQSM cylinder file with colormap: " << filename << " (colormap: " << colormap_or_texture << ")" << std::endl;
1016 } else {
1017 std::cout << "Loading TreeQSM cylinder file: " << filename << std::endl;
1018 }
1019 }
1020
1021 std::vector<uint> tube_UUIDs;
1022
1023 // Open the file
1024 std::ifstream file(filename);
1025 if (!file.is_open()) {
1026 helios_runtime_error("ERROR (LiDARcloud::loadTreeQSM): Could not open TreeQSM file: " + filename);
1027 }
1028
1029 // Structure to hold cylinder data
1030 struct CylinderData {
1031 float radius;
1032 float length;
1033 helios::vec3 start_point;
1034 helios::vec3 axis_direction;
1035 int parent;
1036 int extension;
1037 int branch_id;
1038 int branch_order;
1039 int position_in_branch;
1040 float mad;
1041 float surf_cov;
1042 int added;
1043 float unmod_radius;
1044 };
1045
1046 std::vector<CylinderData> cylinders;
1047 std::string line;
1048
1049 // Skip the header line
1050 if (!std::getline(file, line)) {
1051 helios_runtime_error("ERROR (LiDARcloud::loadTreeQSM): Empty file or failed to read header: " + filename);
1052 }
1053
1054 // Read cylinder data
1055 while (std::getline(file, line)) {
1056 if (line.empty())
1057 continue;
1058
1059 std::istringstream iss(line);
1060 CylinderData cylinder;
1061
1062 // Parse the tab-separated values
1063 if (!(iss >> cylinder.radius >> cylinder.length >> cylinder.start_point.x >> cylinder.start_point.y >> cylinder.start_point.z >> cylinder.axis_direction.x >> cylinder.axis_direction.y >> cylinder.axis_direction.z >> cylinder.parent >>
1064 cylinder.extension >> cylinder.branch_id >> cylinder.branch_order >> cylinder.position_in_branch >> cylinder.mad >> cylinder.surf_cov >> cylinder.added >> cylinder.unmod_radius)) {
1065 std::cerr << "WARNING (LiDARcloud::loadTreeQSM): Failed to parse line: " << line << std::endl;
1066 continue;
1067 }
1068
1069 cylinders.push_back(cylinder);
1070 }
1071
1072 file.close();
1073
1074 if (printmessages) {
1075 std::cout << "Read " << cylinders.size() << " cylinders from TreeQSM file" << std::endl;
1076 }
1077
1078 // Group cylinders by branch ID
1079 std::map<int, std::vector<CylinderData>> branches;
1080 for (const auto &cylinder: cylinders) {
1081 branches[cylinder.branch_id].push_back(cylinder);
1082 }
1083
1084 if (printmessages) {
1085 std::cout << "Found " << branches.size() << " branches" << std::endl;
1086 }
1087
1088 // Generate the colormap if needed
1089 std::vector<helios::RGBcolor> colormap;
1090 if (use_colormap) {
1091 uint num_colors = std::max(static_cast<uint>(branches.size()), 10u); // At least 10 colors for variety
1092 try {
1093 colormap = context->generateColormap(colormap_or_texture, num_colors);
1094 } catch (const std::exception &e) {
1095 helios_runtime_error("ERROR (LiDARcloud::loadTreeQSM): Invalid colormap name '" + colormap_or_texture + "'. Valid options are: hot, cool, rainbow, lava, parula, gray, green");
1096 }
1097 }
1098
1099 // Create tube objects for each branch
1100 for (const auto &branch_pair: branches) {
1101 int branch_id = branch_pair.first;
1102 const auto &branch_cylinders = branch_pair.second;
1103
1104 if (branch_cylinders.empty())
1105 continue;
1106
1107 // Sort cylinders by position in branch
1108 std::vector<CylinderData> sorted_cylinders = branch_cylinders;
1109 std::sort(sorted_cylinders.begin(), sorted_cylinders.end(), [](const CylinderData &a, const CylinderData &b) { return a.position_in_branch < b.position_in_branch; });
1110
1111 // Create nodes and radii for the tube
1112 std::vector<helios::vec3> nodes;
1113 std::vector<float> radii;
1114
1115 for (const auto &cylinder: sorted_cylinders) {
1116 // Add start point
1117 nodes.push_back(cylinder.start_point);
1118 radii.push_back(cylinder.radius);
1119
1120 // Add end point (start + length * axis_direction)
1121 helios::vec3 end_point = cylinder.start_point + cylinder.length * cylinder.axis_direction;
1122 nodes.push_back(end_point);
1123 radii.push_back(cylinder.radius);
1124 }
1125
1126 // Remove duplicate consecutive nodes (where end of one cylinder = start of next)
1127 std::vector<helios::vec3> final_nodes;
1128 std::vector<float> final_radii;
1129
1130 if (!nodes.empty()) {
1131 final_nodes.push_back(nodes[0]);
1132 final_radii.push_back(radii[0]);
1133
1134 for (size_t i = 1; i < nodes.size(); i++) {
1135 // Check if this node is significantly different from the previous
1136 if ((nodes[i] - final_nodes.back()).magnitude() > 1e-6) {
1137 final_nodes.push_back(nodes[i]);
1138 final_radii.push_back(radii[i]);
1139 }
1140 }
1141 }
1142
1143 // Create the tube object
1144 if (final_nodes.size() >= 2) {
1145 uint tube_UUID;
1146
1147 if (use_colormap) {
1148 // Sample color from colormap based on branch ID
1149 // Use branch_id modulo colormap size for deterministic color selection
1150 int color_index = std::abs(branch_id) % colormap.size();
1151 helios::RGBcolor branch_color = colormap[color_index];
1152
1153 // Create a color vector for all nodes in this branch
1154 std::vector<helios::RGBcolor> tube_colors(final_nodes.size(), branch_color);
1155 tube_UUID = context->addTubeObject(radial_subdivisions, final_nodes, final_radii, tube_colors);
1156
1157 // if( printmessages ){
1158 // std::cout << "Created tube for branch " << branch_id << " with " << final_nodes.size()
1159 // << " nodes, branch_order " << sorted_cylinders[0].branch_order << ", color RGB("
1160 // << branch_color.r << "," << branch_color.g << "," << branch_color.b << ")" << std::endl;
1161 // }
1162 } else {
1163 // Use texture file or solid color
1164 if (colormap_or_texture.empty()) {
1165 // Create a color vector for the tube (red color for all nodes)
1166 std::vector<helios::RGBcolor> tube_colors(final_nodes.size(), helios::RGB::red);
1167 tube_UUID = context->addTubeObject(radial_subdivisions, final_nodes, final_radii, tube_colors);
1168 } else {
1169 tube_UUID = context->addTubeObject(radial_subdivisions, final_nodes, final_radii, colormap_or_texture.c_str());
1170 }
1171
1172 if (printmessages) {
1173 std::cout << "Created tube for branch " << branch_id << " with " << final_nodes.size() << " nodes, branch_order " << sorted_cylinders[0].branch_order << std::endl;
1174 }
1175 }
1176
1177 // Add object data for branch rank (branch_order)
1178 int branch_order = sorted_cylinders[0].branch_order; // All cylinders in a branch should have same order
1179 context->setObjectData(tube_UUID, "branch_order", branch_order);
1180 context->setObjectData(tube_UUID, "branch_id", branch_id);
1181
1182 tube_UUIDs.push_back(tube_UUID);
1183 }
1184 }
1185
1186 if (printmessages) {
1187 if (use_colormap) {
1188 std::cout << "Successfully created " << tube_UUIDs.size() << " tube objects from TreeQSM file using colormap" << std::endl;
1189 } else {
1190 std::cout << "Successfully created " << tube_UUIDs.size() << " tube objects from TreeQSM file" << std::endl;
1191 }
1192 }
1193
1194 return tube_UUIDs;
1195}