1.3.64
 
Loading...
Searching...
No Matches
LiDAR.cpp
Go to the documentation of this file.
1
16#include "LiDAR.h"
17
18using namespace std;
19using namespace helios;
20
22
23 origin = make_vec3(0, 0, 0);
24 Ntheta = 100;
25 thetaMin = 0;
26 thetaMax = M_PI;
27 Nphi = 200;
28 phiMin = 0;
29 phiMax = 2.f * M_PI;
30 exitDiameter = 0;
32 columnFormat = {"x", "y", "z"};
33
34 data_file = "";
35}
36
37ScanMetadata::ScanMetadata(const vec3 &a_origin, uint a_Ntheta, float a_thetaMin, float a_thetaMax, uint a_Nphi, float a_phiMin, float a_phiMax, float a_exitDiameter, float a_beamDivergence, const vector<string> &a_columnFormat) {
38
39 // Copy arguments into structure variables
40 origin = a_origin;
41 Ntheta = a_Ntheta;
42 thetaMin = a_thetaMin;
43 thetaMax = a_thetaMax;
44 Nphi = a_Nphi;
45 phiMin = a_phiMin;
46 phiMax = a_phiMax;
47 exitDiameter = a_exitDiameter;
48 beamDivergence = a_beamDivergence;
49 columnFormat = a_columnFormat;
50
51 data_file = "";
52}
53
55
56 float zenith = thetaMin + (thetaMax - thetaMin) / float(Ntheta) * float(row);
57 float elevation = 0.5f * M_PI - zenith;
58 float phi = phiMin - (phiMax - phiMin) / float(Nphi) * float(column);
59 return make_SphericalCoord(1, elevation, phi);
60};
61
63
64 float theta = direction.zenith;
65 float phi = direction.azimuth;
66
67 int row = std::round((theta - thetaMin) / (thetaMax - thetaMin) * float(Ntheta));
68 int column = std::round(fabs(phi - phiMin) / (phiMax - phiMin) * float(Nphi));
69
70 if (row <= -1) {
71 row = 0;
72 } else if (row >= Ntheta) {
73 row = Ntheta - 1;
74 }
75 if (column <= -1) {
76 column = 0;
77 } else if (column >= Nphi) {
78 column = Nphi - 1;
79 }
80
81 return helios::make_int2(row, column);
82};
83
85
86 Nhits = 0;
87 hitgridcellcomputed = false;
88 triangulationcomputed = false;
89 printmessages = true;
90 collision_detection = nullptr;
91}
92
94 delete collision_detection;
95}
96
98 printmessages = false;
99}
100
102 if (collision_detection == nullptr) {
103 collision_detection = new CollisionDetection(context);
104 collision_detection->disableMessages();
105 }
106}
107
108void LiDARcloud::performUnifiedRayTracing(helios::Context *context, size_t N, int Npulse, helios::vec3 *ray_origins, helios::vec3 *direction, float *hit_t, float *hit_fnorm, int *hit_ID) {
109 const float miss_distance = 1001.0f;
110
111 size_t total_rays = N * Npulse;
112
113 // Disable automatic BVH rebuilds during batch ray tracing (geometry is static during scan)
114 collision_detection->disableAutomaticBVHRebuilds();
115
116 // Manually ensure BVH is current once for the entire batch
117 collision_detection->buildBVH();
118
119 // Convert LiDAR rays to CollisionDetection format
120 std::vector<CollisionDetection::RayQuery> ray_queries;
121 ray_queries.reserve(total_rays);
122
123 for (size_t i = 0; i < total_rays; i++) {
124 ray_queries.emplace_back(ray_origins[i], direction[i], miss_distance);
125 }
126
127 // Use the collision detection ray casting (this replaces the old CUDA kernels)
128 std::vector<CollisionDetection::HitResult> hit_results = collision_detection->castRays(ray_queries);
129
130 // Re-enable automatic BVH rebuilds for future operations
131 collision_detection->enableAutomaticBVHRebuilds();
132
133 // Convert results back to LiDAR format
134 size_t hit_count = 0;
135 for (size_t i = 0; i < total_rays; i++) {
136 const auto &result = hit_results[i];
137 if (result.hit) {
138 hit_count++;
139 hit_t[i] = result.distance;
140 hit_ID[i] = static_cast<int>(result.primitive_UUID);
141
142 // Calculate dot product for surface normal (approximation)
143 helios::vec3 ray_dir = direction[i];
144 hit_fnorm[i] = ray_dir.x * result.normal.x + ray_dir.y * result.normal.y + ray_dir.z * result.normal.z;
145
146 } else {
147 hit_t[i] = miss_distance;
148 hit_ID[i] = -1;
149 hit_fnorm[i] = 1e6;
150 }
151 }
152}
153
155 printmessages = true;
156}
157
158void LiDARcloud::validateRayDirections() {
159
160 for (uint s = 0; s < getScanCount(); s++) {
161
162 for (int j = 0; j < getScanSizePhi(s); j++) {
163 for (int i = 0; i < getScanSizeTheta(s); i++) {
164 if (getHitIndex(s, i, j) >= 0) {
165 SphericalCoord direction1 = scans.at(s).rc2direction(i, j);
166 SphericalCoord direction2 = cart2sphere(getHitXYZ(getHitIndex(s, i, j)) - getScanOrigin(s));
167 SphericalCoord direction3 = getHitRaydir(getHitIndex(s, i, j));
168
169
170 float err_theta = max(fabs(direction1.zenith - direction2.zenith), fabs(direction1.zenith - direction3.zenith));
171
172 float err_phi = max(fabs(direction1.azimuth - direction2.azimuth), fabs(direction1.azimuth - direction3.azimuth));
173
174 if (err_theta > 1e-6 || err_phi > 1e-6) {
175 helios_runtime_error("ERROR (LiDARcloud::validateRayDirections): validation of ray directions failed.");
176 }
177 }
178 }
179 }
180 }
181}
182
184 return scans.size();
185}
186
188
189 float epsilon = 1e-5;
190
191 if (newscan.thetaMin < 0) {
192 std::cerr << "WARNING (LiDARcloud::addScan): Specified scan minimum zenith angle of " << newscan.thetaMin << " is less than 0. Truncating to 0." << std::endl;
193 newscan.thetaMin = 0;
194 }
195 if (newscan.phiMin < 0) {
196 std::cerr << "WARNING (LiDARcloud::addScan): Specified scan minimum azimuth angle of " << newscan.phiMin << " is less than 0. Truncating to 0." << std::endl;
197 newscan.phiMin = 0;
198 }
199 if (newscan.thetaMax > M_PI + epsilon) {
200 std::cerr << "WARNING (LiDARcloud::addScan): Specified scan maximum zenith angle of " << newscan.thetaMax << " is greater than pi. Setting thetaMin to 0 and truncating thetaMax to pi. Did you mistakenly use degrees instead of radians?"
201 << std::endl;
202 newscan.thetaMax = M_PI;
203 newscan.thetaMin = 0;
204 }
205 if (newscan.phiMax > 4.f * M_PI + epsilon) {
206 std::cerr << "WARNING (LiDARcloud::addScan): Specified scan maximum azimuth angle of " << newscan.phiMax << " is greater than 2pi. Did you mistakenly use degrees instead of radians?" << std::endl;
207 }
208
209 // initialize the hit table to `-1' (all misses)
210 HitTable<int> table;
211 table.resize(newscan.Ntheta, newscan.Nphi, -1);
212 hit_tables.push_back(table);
213
214 scans.emplace_back(newscan);
215
216 return scans.size() - 1;
217}
218
219void LiDARcloud::addHitPoint(uint scanID, const vec3 &xyz, const SphericalCoord &direction) {
220
221 // default color
222 RGBcolor color = make_RGBcolor(1, 0, 0);
223
224 // empty data
225 std::map<std::string, double> data;
226
227 addHitPoint(scanID, xyz, direction, color, data);
228}
229
230void LiDARcloud::addHitPoint(uint scanID, const vec3 &xyz, const SphericalCoord &direction, const map<string, double> &data) {
231
232 // default color
233 RGBcolor color = make_RGBcolor(1, 0, 0);
234
235 addHitPoint(scanID, xyz, direction, color, data);
236}
237
238void LiDARcloud::addHitPoint(uint scanID, const vec3 &xyz, const SphericalCoord &direction, const RGBcolor &color) {
239
240 // empty data
241 std::map<std::string, double> data;
242
243 addHitPoint(scanID, xyz, direction, color, data);
244}
245
246void LiDARcloud::addHitPoint(uint scanID, const vec3 &xyz, const SphericalCoord &direction, const RGBcolor &color, const map<string, double> &data) {
247
248 // error checking
249 if (scanID >= scans.size()) {
250 helios_runtime_error("ERROR (LiDARcloud::addHitPoint): Hit point cannot be added to scan #" + std::to_string(scanID) + " because there have only been " + std::to_string(scans.size()) + " scans added.");
251 }
252
253 ScanMetadata scan = scans.at(scanID);
254 int2 row_column = scan.direction2rc(direction);
255
256 HitPoint hit(scanID, xyz, direction, row_column, color, data);
257
258 hits.push_back(hit);
259}
260
261void LiDARcloud::addHitPoint(uint scanID, const vec3 &xyz, const int2 &row_column, const RGBcolor &color, const map<string, double> &data) {
262
263 ScanMetadata scan = scans.at(scanID);
264 SphericalCoord direction = scan.rc2direction(row_column.x, row_column.y);
265
266 HitPoint hit(scanID, xyz, direction, row_column, color, data);
267
268 hits.push_back(hit);
269}
270
272
273 if (index >= hits.size()) {
274 cerr << "WARNING (deleteHitPoint): Hit point #" << index << " cannot be deleted from the scan because there have only been " << hits.size() << " hit points added." << endl;
275 return;
276 }
277
278 HitPoint hit = hits.at(index);
279
280 int scanID = hit.scanID;
281
282 // erase from vector of hits (use swap-and-pop method)
283 std::swap(hits.at(index), hits.back());
284 hits.pop_back();
285}
286
288 return hits.size();
289}
290
292 if (scanID >= scans.size()) {
293 helios_runtime_error("ERROR (LiDARcloud::getScanOrigin): Cannot get origin of scan #" + std::to_string(scanID) + " because there have only been " + std::to_string(scans.size()) + " scans added.");
294 }
295 return scans.at(scanID).origin;
296}
297
299 if (scanID >= scans.size()) {
300 helios_runtime_error("ERROR (LiDARcloud::getScanSizeTheta): Cannot get theta size for scan #" + std::to_string(scanID) + " because there have only been " + std::to_string(scans.size()) + " scans added.");
301 }
302 return scans.at(scanID).Ntheta;
303}
304
306 if (scanID >= scans.size()) {
307 helios_runtime_error("ERROR (LiDARcloud::getScanSizePhi): Cannot get phi size for scan #" + std::to_string(scanID) + " because there have only been " + std::to_string(scans.size()) + " scans added.");
308 }
309 return scans.at(scanID).Nphi;
310}
311
313 if (scanID >= scans.size()) {
314 helios_runtime_error("ERROR (LiDARcloud::getScanRangeTheta): Cannot get theta range for scan #" + std::to_string(scanID) + " because there have only been " + std::to_string(scans.size()) + " scans added.");
315 }
316 return helios::make_vec2(scans.at(scanID).thetaMin, scans.at(scanID).thetaMax);
317}
318
320 if (scanID >= scans.size()) {
321 helios_runtime_error("ERROR (LiDARcloud::getScanRangePhi): Cannot get phi range for scan #" + std::to_string(scanID) + " because there have only been " + std::to_string(scans.size()) + " scans added.");
322 }
323 return helios::make_vec2(scans.at(scanID).phiMin, scans.at(scanID).phiMax);
324}
325
327 if (scanID >= scans.size()) {
328 helios_runtime_error("ERROR (LiDARcloud::getScanBeamExitDiameter): Cannot get exit diameter for scan #" + std::to_string(scanID) + " because there have only been " + std::to_string(scans.size()) + " scans added.");
329 }
330 return scans.at(scanID).exitDiameter;
331}
332
334 if (scanID >= scans.size()) {
335 helios_runtime_error("ERROR (LiDARcloud::getScanBeamDivergence): Cannot get beam divergence for scan #" + std::to_string(scanID) + " because there have only been " + std::to_string(scans.size()) + " scans added.");
336 }
337 return scans.at(scanID).beamDivergence;
338}
339
340std::vector<std::string> LiDARcloud::getScanColumnFormat(uint scanID) const {
341 if (scanID >= scans.size()) {
342 helios_runtime_error("ERROR (LiDARcloud::getScanColumnFormat): Cannot get column format for scan #" + std::to_string(scanID) + " because there have only been " + std::to_string(scans.size()) + " scans added.");
343 }
344 return scans.at(scanID).columnFormat;
345}
346
348
349 if (index >= hits.size()) {
350 helios_runtime_error("ERROR (LiDARcloud::getHitXYZ): Hit point index out of bounds. Requesting hit #" + std::to_string(index) + " but scan only has " + std::to_string(hits.size()) + " hits.");
351 }
352
353 return hits.at(index).position;
354}
355
357
358 if (index >= hits.size()) {
359 helios_runtime_error("ERROR (LiDARcloud::getHitRaydir): Hit point index out of bounds. Requesting hit #" + std::to_string(index) + " but scan only has " + std::to_string(hits.size()) + " hits.");
360 }
361
362 vec3 direction_cart = getHitXYZ(index) - getScanOrigin(getHitScanID(index));
363 return cart2sphere(direction_cart);
364}
365
366void LiDARcloud::setHitData(uint index, const char *label, double value) {
367
368 if (index >= hits.size()) {
369 helios_runtime_error("ERROR (LiDARcloud::setHitScalarData): Hit point index out of bounds. Tried to set hit #" + std::to_string(index) + " but scan only has " + std::to_string(hits.size()) + " hits.");
370 }
371
372 hits.at(index).data[label] = value;
373}
374
375double LiDARcloud::getHitData(uint index, const char *label) const {
376
377 if (index >= hits.size()) {
378 helios_runtime_error("ERROR (LiDARcloud::getHitData): Hit point index out of bounds. Requesting hit #" + std::to_string(index) + " but scan only has " + std::to_string(hits.size()) + " hits.");
379 }
380
381 std::map<std::string, double> hit_data = hits.at(index).data;
382 if (hit_data.find(label) == hit_data.end()) {
383 helios_runtime_error("ERROR (LiDARcloud::getHitData): Data value ``" + std::string(label) + "'' does not exist.");
384 }
385
386 return hit_data.at(label);
387}
388
389bool LiDARcloud::doesHitDataExist(uint index, const char *label) const {
390
391 if (index >= hits.size()) {
392 return false;
393 }
394
395 std::map<std::string, double> hit_data = hits.at(index).data;
396 if (hit_data.find(label) == hit_data.end()) {
397 return false;
398 } else {
399 return true;
400 }
401}
402
404
405 if (index >= hits.size()) {
406 helios_runtime_error("ERROR (LiDARcloud::getHitColor): Hit point index out of bounds. Requesting hit #" + std::to_string(index) + " but scan only has " + std::to_string(hits.size()) + " hits.");
407 }
408
409 return hits.at(index).color;
410}
411
413
414 if (index >= hits.size()) {
415 helios_runtime_error("ERROR (LiDARcloud::getHitColor): Hit point index out of bounds. Requesting hit #" + std::to_string(index) + " but scan only has " + std::to_string(hits.size()) + " hits.");
416 }
417
418 return hits.at(index).scanID;
419}
420
421int LiDARcloud::getHitIndex(uint scanID, uint row, uint column) const {
422
423 if (scanID >= scans.size()) {
424 helios_runtime_error("ERROR (LiDARcloud::deleteHitPoint): Hit point cannot be deleted from scan #" + std::to_string(scanID) + " because there have only been " + std::to_string(scans.size()) + " scans added.");
425 }
426 if (row >= getScanSizeTheta(scanID)) {
427 helios_runtime_error("ERROR (LiDARcloud::getHitIndex): Row in scan data table out of range.");
428 } else if (column >= getScanSizePhi(scanID)) {
429 helios_runtime_error("ERROR (LiDARcloud::getHitIndex): Column in scan data table out of range.");
430 }
431
432 int hit = hit_tables.at(scanID).get(row, column);
433
434 assert(hit < getScanSizeTheta(scanID) * getScanSizePhi(scanID));
435
436 return hit;
437}
438
440
441 if (index >= hits.size()) {
442 helios_runtime_error("ERROR (LiDARcloud::getHitGridCell): Hit point index out of bounds. Requesting hit #" + std::to_string(index) + " but scan only has " + std::to_string(hits.size()) + " hits.");
443 } else if (hits.at(index).gridcell == -2) {
444 cerr << "WARNING (LiDARcloud::getHitGridCell): hit grid cell for point #" << index << " was never set. Returning a value of `-1'. Did you forget to call calculateHitGridCell[*] first?" << endl;
445 return -1;
446 }
447
448 return hits.at(index).gridcell;
449}
450
451void LiDARcloud::setHitGridCell(uint index, int cell) {
452
453 if (index >= hits.size()) {
454 helios_runtime_error("ERROR (LiDARcloud::setHitGridCell): Hit point index out of bounds. Tried to set hit #" + std::to_string(index) + " but scan only has " + std::to_string(hits.size()) + " hits.");
455 }
456
457 hits.at(index).gridcell = cell;
458}
459
461
462 for (auto &scan: scans) {
463 scan.origin = scan.origin + shift;
464 }
465
466 for (auto &hit: hits) {
467 hit.position = hit.position + shift;
468 }
469}
470
471void LiDARcloud::coordinateShift(uint scanID, const vec3 &shift) {
472
473 if (scanID >= scans.size()) {
474 helios_runtime_error("ERROR (LiDARcloud::coordinateShift): Cannot apply coordinate shift to scan " + std::to_string(scanID) + " because it does not exist.");
475 }
476
477 scans.at(scanID).origin = scans.at(scanID).origin + shift;
478
479 for (auto &hit: hits) {
480 if (hit.scanID == scanID) {
481 hit.position = hit.position + shift;
482 }
483 }
484}
485
487
488 for (auto &scan: scans) {
489 scan.origin = rotatePoint(scan.origin, rotation);
490 }
491
492 for (auto &hit: hits) {
493 hit.position = rotatePoint(hit.position, rotation);
494 hit.direction = cart2sphere(hit.position - scans.at(hit.scanID).origin);
495 }
496}
497
499
500 if (scanID >= scans.size()) {
501 helios_runtime_error("ERROR (LiDARcloud::coordinateRotation): Cannot apply rotation to scan " + std::to_string(scanID) + " because it does not exist.");
502 }
503
504 scans.at(scanID).origin = rotatePoint(scans.at(scanID).origin, rotation);
505
506 for (auto &hit: hits) {
507 if (hit.scanID == scanID) {
508 hit.position = rotatePoint(hit.position, rotation);
509 hit.direction = cart2sphere(hit.position - scans.at(scanID).origin);
510 }
511 }
512}
513
514void LiDARcloud::coordinateRotation(float rotation, const vec3 &line_base, const vec3 &line_direction) {
515
516 for (auto &scan: scans) {
517 scan.origin = rotatePointAboutLine(scan.origin, line_base, line_direction, rotation);
518 }
519
520 for (auto &hit: hits) {
521 hit.position = rotatePointAboutLine(hit.position, line_base, line_direction, rotation);
522 hit.direction = cart2sphere(hit.position - scans.at(hit.scanID).origin);
523 }
524}
525
527 return triangles.size();
528}
529
531 if (index >= triangles.size()) {
532 helios_runtime_error("ERROR (LiDARcloud::getTriangle): Triangle index out of bounds. Tried to get triangle #" + std::to_string(index) + " but point cloud only has " + std::to_string(triangles.size()) + " triangles.");
533 }
534
535 return triangles.at(index);
536}
537
538void LiDARcloud::addHitsToVisualizer(Visualizer *visualizer, uint pointsize) const {
539 addHitsToVisualizer(visualizer, pointsize, "");
540}
541
542void LiDARcloud::addHitsToVisualizer(Visualizer *visualizer, uint pointsize, const RGBcolor &point_color) const {
543
544 if (printmessages && scans.size() == 0) {
545 std::cout << "WARNING (LiDARcloud::addHitsToVisualizer): There are no scans in the point cloud, and thus there is no geometry to add...skipping." << std::endl;
546 return;
547 }
548
549 for (uint i = 0; i < getHitCount(); i++) {
550 vec3 center = getHitXYZ(i);
551
552 visualizer->addPoint(center, point_color, pointsize, Visualizer::COORDINATES_CARTESIAN);
553 }
554}
555
556void LiDARcloud::addHitsToVisualizer(Visualizer *visualizer, uint pointsize, const char *color_value) const {
557
558 if (printmessages && scans.size() == 0) {
559 std::cout << "WARNING (LiDARcloud::addHitsToVisualizer): There are no scans in the point cloud, and thus there is no geometry to add...skipping." << std::endl;
560 return;
561 }
562
563 //-- hit points --//
564 float minval = 1e9;
565 float maxval = -1e9;
566 if (strcmp(color_value, "gridcell") == 0) {
567 minval = 0;
568 maxval = getGridCellCount() - 1;
569 } else if (strcmp(color_value, "") != 0) {
570 for (uint i = 0; i < getHitCount(); i++) {
571 if (doesHitDataExist(i, color_value)) {
572 float data = float(getHitData(i, color_value));
573 if (data < minval) {
574 minval = data;
575 }
576 if (data > maxval) {
577 maxval = data;
578 }
579 }
580 }
581 }
582
583 RGBcolor color;
584 Colormap cmap = visualizer->getCurrentColormap();
585 if (minval != 1e9 && maxval != -1e9) {
586 cmap.setRange(minval, maxval);
587 }
588
589 for (uint i = 0; i < getHitCount(); i++) {
590
591 if (strcmp(color_value, "") == 0) {
592 color = getHitColor(i);
593 } else if (strcmp(color_value, "gridcell") == 0) {
594 if (getHitGridCell(i) < 0) {
595 color = RGB::red;
596 } else {
597 color = cmap.query(getHitGridCell(i));
598 }
599 } else {
600 if (!doesHitDataExist(i, color_value)) {
601 color = RGB::red;
602 } else {
603 float data = float(getHitData(i, color_value));
604 color = cmap.query(data);
605 }
606 }
607
608 vec3 center = getHitXYZ(i);
609
610 visualizer->addPoint(center, color, pointsize, Visualizer::COORDINATES_CARTESIAN);
611 }
612}
613
615
616 if (printmessages && scans.size() == 0) {
617 std::cout << "WARNING (LiDARcloud::addGridToVisualizer): There are no scans in the point cloud, and thus there is no geometry to add...skipping." << std::endl;
618 return;
619 }
620
621 float minval = 1e9;
622 float maxval = -1e9;
623 for (uint i = 0; i < getGridCellCount(); i++) {
624 float data = getCellLeafAreaDensity(i);
625 if (data < minval) {
626 minval = data;
627 }
628 if (data > maxval) {
629 maxval = data;
630 }
631 }
632
633 Colormap cmap = visualizer->getCurrentColormap();
634 if (minval != 1e9 && maxval != -1e9) {
635 cmap.setRange(minval, maxval);
636 }
637
638 vec3 origin;
639 for (uint i = 0; i < getGridCellCount(); i++) {
640
641 if (getCellLeafAreaDensity(i) == 0) {
642 continue;
643 }
644
645 vec3 center = getCellCenter(i);
646
647 vec3 anchor = getCellGlobalAnchor(i);
648
650
651 center = rotatePointAboutLine(center, anchor, make_vec3(0, 0, 1), rotation.azimuth);
652 vec3 size = getCellSize(i);
653
654 RGBAcolor color = make_RGBAcolor(cmap.query(getCellLeafAreaDensity(i)), 0.5);
655
656 visualizer->addVoxelByCenter(center, size, rotation, color, Visualizer::COORDINATES_CARTESIAN);
657
658 origin = origin + center / float(getGridCellCount());
659 }
660
661 vec3 boxmin, boxmax;
662 getHitBoundingBox(boxmin, boxmax);
663
664 float R = 2.f * sqrt(pow(boxmax.x - boxmin.x, 2) + pow(boxmax.y - boxmin.y, 2) + pow(boxmax.z - boxmin.z, 2));
665}
666
668
669 if (printmessages && scans.size() == 0) {
670 std::cout << "WARNING (LiDARcloud::addGeometryToVisualizer): There are no scans in the point cloud, and thus there is no geometry to add...skipping." << std::endl;
671 return;
672 }
673
674 for (uint i = 0; i < triangles.size(); i++) {
675
676 Triangulation tri = triangles.at(i);
677
678 visualizer->addTriangle(tri.vertex0, tri.vertex1, tri.vertex2, tri.color, Visualizer::COORDINATES_CARTESIAN);
679 }
680}
681
682void LiDARcloud::addTrianglesToVisualizer(Visualizer *visualizer, uint gridcell) const {
683
684 if (printmessages && scans.size() == 0) {
685 std::cout << "WARNING (LiDARcloud::addTrianglesToVisualizer): There are no scans in the point cloud, and thus there is no geometry to add...skipping." << std::endl;
686 return;
687 }
688
689 for (uint i = 0; i < triangles.size(); i++) {
690
691 Triangulation tri = triangles.at(i);
692
693 if (tri.gridcell == gridcell) {
694 visualizer->addTriangle(tri.vertex0, tri.vertex1, tri.vertex2, tri.color, Visualizer::COORDINATES_CARTESIAN);
695 }
696 }
697}
698
699void LiDARcloud::addGrid(const vec3 &center, const vec3 &size, const int3 &ndiv, float rotation) {
700 if (size.x <= 0 || size.y <= 0 || size.z <= 0) {
701 cerr << "failed.\n";
702 helios_runtime_error("ERROR (LiDARcloud::addGrid): The grid cell size must be positive.");
703 }
704
705 if (ndiv.x <= 0 || ndiv.y <= 0 || ndiv.z <= 0) {
706 cerr << "failed.\n";
707 helios_runtime_error("ERROR (LiDARcloud::addGrid): The number of grid cells in each direction must be positive.");
708 }
709
710 // add cells to grid
711 vec3 gsubsize = make_vec3(float(size.x) / float(ndiv.x), float(size.y) / float(ndiv.y), float(size.z) / float(ndiv.z));
712
713 float x, y, z;
714 uint count = 0;
715 for (int k = 0; k < ndiv.z; k++) {
716 z = -0.5f * float(size.z) + (float(k) + 0.5f) * float(gsubsize.z);
717 for (int j = 0; j < ndiv.y; j++) {
718 y = -0.5f * float(size.y) + (float(j) + 0.5f) * float(gsubsize.y);
719 for (int i = 0; i < ndiv.x; i++) {
720 x = -0.5f * float(size.x) + (float(i) + 0.5f) * float(gsubsize.x);
721
722 vec3 subcenter = make_vec3(x, y, z);
723
724 vec3 subcenter_rot = rotatePoint(subcenter, make_SphericalCoord(0, rotation * M_PI / 180.f));
725
726 if (printmessages) {
727 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 "
728 << gsubsize.z << endl;
729 }
730
731 addGridCell(subcenter + center, center, gsubsize, size, rotation * M_PI / 180.f, make_int3(i, j, k), ndiv);
732
733 count++;
734 }
735 }
736 }
737}
738
739void LiDARcloud::addGridWireFrametoVisualizer(Visualizer *visualizer, float linewidth_pixels) const {
740
741
742 for (int i = 0; i < getGridCellCount(); i++) {
743 helios::vec3 center = getCellCenter(i);
744 helios::vec3 size = getCellSize(i);
745
746 helios::vec3 boxmin, boxmax;
747 boxmin = make_vec3(center.x - 0.5 * size.x, center.y - 0.5 * size.y, center.z - 0.5 * size.z);
748 boxmax = make_vec3(center.x + 0.5 * size.x, center.y + 0.5 * size.y, center.z + 0.5 * size.z);
749
750 // vertical edges of the cell
751 visualizer->addLine(make_vec3(boxmin.x, boxmin.y, boxmin.z), make_vec3(boxmin.x, boxmin.y, boxmax.z), RGB::black, linewidth_pixels, Visualizer::COORDINATES_CARTESIAN);
752 visualizer->addLine(make_vec3(boxmin.x, boxmax.y, boxmin.z), make_vec3(boxmin.x, boxmax.y, boxmax.z), RGB::black, linewidth_pixels, Visualizer::COORDINATES_CARTESIAN);
753 visualizer->addLine(make_vec3(boxmax.x, boxmin.y, boxmin.z), make_vec3(boxmax.x, boxmin.y, boxmax.z), RGB::black, linewidth_pixels, Visualizer::COORDINATES_CARTESIAN);
754 visualizer->addLine(make_vec3(boxmax.x, boxmax.y, boxmin.z), make_vec3(boxmax.x, boxmax.y, boxmax.z), RGB::black, linewidth_pixels, Visualizer::COORDINATES_CARTESIAN);
755
756 // horizontal top edges
757 visualizer->addLine(make_vec3(boxmin.x, boxmin.y, boxmax.z), make_vec3(boxmin.x, boxmax.y, boxmax.z), RGB::black, linewidth_pixels, Visualizer::COORDINATES_CARTESIAN);
758 visualizer->addLine(make_vec3(boxmin.x, boxmin.y, boxmax.z), make_vec3(boxmax.x, boxmin.y, boxmax.z), RGB::black, linewidth_pixels, Visualizer::COORDINATES_CARTESIAN);
759 visualizer->addLine(make_vec3(boxmax.x, boxmin.y, boxmax.z), make_vec3(boxmax.x, boxmax.y, boxmax.z), RGB::black, linewidth_pixels, Visualizer::COORDINATES_CARTESIAN);
760 visualizer->addLine(make_vec3(boxmin.x, boxmax.y, boxmax.z), make_vec3(boxmax.x, boxmax.y, boxmax.z), RGB::black, linewidth_pixels, Visualizer::COORDINATES_CARTESIAN);
761
762 // horizontal bottom edges
763 visualizer->addLine(make_vec3(boxmin.x, boxmin.y, boxmin.z), make_vec3(boxmin.x, boxmax.y, boxmin.z), RGB::black, linewidth_pixels, Visualizer::COORDINATES_CARTESIAN);
764 visualizer->addLine(make_vec3(boxmin.x, boxmin.y, boxmin.z), make_vec3(boxmax.x, boxmin.y, boxmin.z), RGB::black, linewidth_pixels, Visualizer::COORDINATES_CARTESIAN);
765 visualizer->addLine(make_vec3(boxmax.x, boxmin.y, boxmin.z), make_vec3(boxmax.x, boxmax.y, boxmin.z), RGB::black, linewidth_pixels, Visualizer::COORDINATES_CARTESIAN);
766 visualizer->addLine(make_vec3(boxmin.x, boxmax.y, boxmin.z), make_vec3(boxmax.x, boxmax.y, boxmin.z), RGB::black, linewidth_pixels, Visualizer::COORDINATES_CARTESIAN);
767 }
768}
769
771
772 size_t Ngroups = reconstructed_triangles.size();
773
774 std::vector<helios::RGBcolor> ctable;
775 std::vector<float> clocs;
776
777 ctable.push_back(RGB::violet);
778 ctable.push_back(RGB::blue);
779 ctable.push_back(RGB::green);
780 ctable.push_back(RGB::yellow);
781 ctable.push_back(RGB::orange);
782 ctable.push_back(RGB::red);
783
784 clocs.push_back(0.f);
785 clocs.push_back(0.2f);
786 clocs.push_back(0.4f);
787 clocs.push_back(0.6f);
788 clocs.push_back(0.8f);
789 clocs.push_back(1.f);
790
791 Colormap colormap(ctable, clocs, 100, 0, Ngroups - 1);
792
793 for (size_t g = 0; g < Ngroups; g++) {
794
795 float randi = randu() * (Ngroups - 1);
796 RGBcolor color = colormap.query(randi);
797
798 for (size_t t = 0; t < reconstructed_triangles.at(g).size(); t++) {
799
800 helios::vec3 v0 = reconstructed_triangles.at(g).at(t).vertex0;
801 helios::vec3 v1 = reconstructed_triangles.at(g).at(t).vertex1;
802 helios::vec3 v2 = reconstructed_triangles.at(g).at(t).vertex2;
803
804 // RGBcolor color = reconstructed_triangles.at(g).at(t).color;
805
806 visualizer->addTriangle(v0, v1, v2, color, Visualizer::COORDINATES_CARTESIAN);
807 }
808 }
809
810 Ngroups = reconstructed_alphamasks_center.size();
811
812 for (size_t g = 0; g < Ngroups; g++) {
813
814 visualizer->addRectangleByCenter(reconstructed_alphamasks_center.at(g), reconstructed_alphamasks_size.at(g), reconstructed_alphamasks_rotation.at(g), reconstructed_alphamasks_maskfile.c_str(), Visualizer::COORDINATES_CARTESIAN);
815 }
816}
817
819
820 size_t Ngroups = reconstructed_trunk_triangles.size();
821
822 for (size_t g = 0; g < Ngroups; g++) {
823
824 for (size_t t = 0; t < reconstructed_trunk_triangles.at(g).size(); t++) {
825
826 helios::vec3 v0 = reconstructed_trunk_triangles.at(g).at(t).vertex0;
827 helios::vec3 v1 = reconstructed_trunk_triangles.at(g).at(t).vertex1;
828 helios::vec3 v2 = reconstructed_trunk_triangles.at(g).at(t).vertex2;
829
830 RGBcolor color = reconstructed_trunk_triangles.at(g).at(t).color;
831
832 visualizer->addTriangle(v0, v1, v2, color, Visualizer::COORDINATES_CARTESIAN);
833 }
834 }
835}
836
837void LiDARcloud::addTrunkReconstructionToVisualizer(Visualizer *visualizer, const RGBcolor &trunk_color) const {
838
839 size_t Ngroups = reconstructed_trunk_triangles.size();
840
841 for (size_t g = 0; g < Ngroups; g++) {
842
843 for (size_t t = 0; t < reconstructed_trunk_triangles.at(g).size(); t++) {
844
845 helios::vec3 v0 = reconstructed_trunk_triangles.at(g).at(t).vertex0;
846 helios::vec3 v1 = reconstructed_trunk_triangles.at(g).at(t).vertex1;
847 helios::vec3 v2 = reconstructed_trunk_triangles.at(g).at(t).vertex2;
848
849 visualizer->addTriangle(v0, v1, v2, trunk_color, Visualizer::COORDINATES_CARTESIAN);
850 }
851 }
852}
853
854std::vector<uint> LiDARcloud::addLeafReconstructionToContext(Context *context) const {
856}
857
858std::vector<uint> LiDARcloud::addLeafReconstructionToContext(Context *context, const int2 &subpatches) const {
859
860 std::vector<uint> UUIDs;
861
862 std::vector<uint> UUID_leaf_template;
863 if (subpatches.x > 1 || subpatches.y > 1) {
864 UUID_leaf_template = context->addTile(make_vec3(0, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), subpatches, reconstructed_alphamasks_maskfile.c_str());
865 }
866
867 size_t Ngroups = reconstructed_alphamasks_center.size();
868
869 for (size_t g = 0; g < Ngroups; g++) {
870
871 helios::RGBcolor color = helios::RGB::red;
872
873 uint zone = reconstructed_alphamasks_gridcell.at(g);
874
875 if (reconstructed_alphamasks_size.at(g).x > 0 && reconstructed_alphamasks_size.at(g).y > 0) {
876 std::vector<uint> UUIDs_leaf;
877 if (subpatches.x == 1 && subpatches.y == 1) {
878 UUIDs_leaf.push_back(context->addPatch(reconstructed_alphamasks_center.at(g), reconstructed_alphamasks_size.at(g), reconstructed_alphamasks_rotation.at(g), reconstructed_alphamasks_maskfile.c_str()));
879 } else {
880 UUIDs_leaf = context->copyPrimitive(UUID_leaf_template);
881 context->scalePrimitive(UUIDs_leaf, make_vec3(reconstructed_alphamasks_size.at(g).x, reconstructed_alphamasks_size.at(g).y, 1));
882 context->rotatePrimitive(UUIDs_leaf, -reconstructed_alphamasks_rotation.at(g).elevation, "x");
883 context->rotatePrimitive(UUIDs_leaf, -reconstructed_alphamasks_rotation.at(g).azimuth, "z");
884 context->translatePrimitive(UUIDs_leaf, reconstructed_alphamasks_center.at(g));
885 }
886 context->setPrimitiveData(UUIDs_leaf, "gridCell", zone);
887 uint flag = reconstructed_alphamasks_direct_flag.at(g);
888 context->setPrimitiveData(UUIDs_leaf, "directFlag", flag);
889 UUIDs.insert(UUIDs.end(), UUIDs_leaf.begin(), UUIDs_leaf.end());
890 }
891 }
892
893 context->deletePrimitive(UUID_leaf_template);
894
895 return UUIDs;
896}
897
899
900 std::vector<uint> UUIDs;
901
902 size_t Ngroups = reconstructed_triangles.size();
903
904 for (size_t g = 0; g < Ngroups; g++) {
905
906 int leafGroup = round(context->randu() * (Ngroups - 1));
907
908 for (size_t t = 0; t < reconstructed_triangles.at(g).size(); t++) {
909
910 helios::vec3 v0 = reconstructed_triangles.at(g).at(t).vertex0;
911 helios::vec3 v1 = reconstructed_triangles.at(g).at(t).vertex1;
912 helios::vec3 v2 = reconstructed_triangles.at(g).at(t).vertex2;
913
914 RGBcolor color = reconstructed_triangles.at(g).at(t).color;
915
916 UUIDs.push_back(context->addTriangle(v0, v1, v2, color));
917
918 uint zone = reconstructed_triangles.at(g).at(t).gridcell;
919 context->setPrimitiveData(UUIDs.back(), "gridCell", zone);
920
921 context->setPrimitiveData(UUIDs.back(), "leafGroup", leafGroup);
922 }
923 }
924
925 return UUIDs;
926}
927
928std::vector<uint> LiDARcloud::addTrunkReconstructionToContext(Context *context) const {
929
930 std::vector<uint> UUIDs;
931
932 size_t Ngroups = reconstructed_trunk_triangles.size();
933
934 for (size_t g = 0; g < Ngroups; g++) {
935
936 for (size_t t = 0; t < reconstructed_trunk_triangles.at(g).size(); t++) {
937
938 helios::vec3 v0 = reconstructed_trunk_triangles.at(g).at(t).vertex0;
939 helios::vec3 v1 = reconstructed_trunk_triangles.at(g).at(t).vertex1;
940 helios::vec3 v2 = reconstructed_trunk_triangles.at(g).at(t).vertex2;
941
942 RGBcolor color = reconstructed_trunk_triangles.at(g).at(t).color;
943
944 UUIDs.push_back(context->addTriangle(v0, v1, v2, color));
945 }
946 }
947
948 return UUIDs;
949}
950
952
953 if (printmessages && hits.size() == 0) {
954 std::cout << "WARNING (getHitBoundingBox): There are no hit points in the point cloud, cannot determine bounding box...skipping." << std::endl;
955 return;
956 }
957
958 boxmin = make_vec3(1e6, 1e6, 1e6);
959 boxmax = make_vec3(-1e6, -1e6, -1e6);
960
961 for (std::size_t i = 0; i < hits.size(); i++) {
962
963 vec3 xyz = getHitXYZ(i);
964
965 if (xyz.x < boxmin.x) {
966 boxmin.x = xyz.x;
967 }
968 if (xyz.x > boxmax.x) {
969 boxmax.x = xyz.x;
970 }
971 if (xyz.y < boxmin.y) {
972 boxmin.y = xyz.y;
973 }
974 if (xyz.y > boxmax.y) {
975 boxmax.y = xyz.y;
976 }
977 if (xyz.z < boxmin.z) {
978 boxmin.z = xyz.z;
979 }
980 if (xyz.z > boxmax.z) {
981 boxmax.z = xyz.z;
982 }
983 }
984}
985
987
988 if (printmessages && getGridCellCount() == 0) {
989 std::cout << "WARNING (getGridBoundingBox): There are no grid cells in the point cloud, cannot determine bounding box...skipping." << std::endl;
990 return;
991 }
992
993 boxmin = make_vec3(1e6, 1e6, 1e6);
994 boxmax = make_vec3(-1e6, -1e6, -1e6);
995
996 std::size_t count = 0;
997 for (uint c = 0; c < getGridCellCount(); c++) {
998
999 vec3 center = getCellCenter(c);
1000 vec3 size = getCellSize(c);
1001 vec3 cellanchor = getCellGlobalAnchor(c);
1002 float rotation = getCellRotation(c);
1003
1004 vec3 xyz_min = center - 0.5f * size;
1005 xyz_min = rotatePointAboutLine(xyz_min, cellanchor, make_vec3(0, 0, 1), rotation);
1006 vec3 xyz_max = center + 0.5f * size;
1007 xyz_max = rotatePointAboutLine(xyz_max, cellanchor, make_vec3(0, 0, 1), rotation);
1008
1009 if (xyz_min.x < boxmin.x) {
1010 boxmin.x = xyz_min.x;
1011 }
1012 if (xyz_max.x > boxmax.x) {
1013 boxmax.x = xyz_max.x;
1014 }
1015 if (xyz_min.y < boxmin.y) {
1016 boxmin.y = xyz_min.y;
1017 }
1018 if (xyz_max.y > boxmax.y) {
1019 boxmax.y = xyz_max.y;
1020 }
1021 if (xyz_min.z < boxmin.z) {
1022 boxmin.z = xyz_min.z;
1023 }
1024 if (xyz_max.z > boxmax.z) {
1025 boxmax.z = xyz_max.z;
1026 }
1027 }
1028}
1029
1030void LiDARcloud::distanceFilter(float maxdistance) {
1031
1032 std::size_t delete_count = 0;
1033 for (int i = (getHitCount() - 1); i >= 0; i--) {
1034
1035 vec3 xyz = getHitXYZ(i);
1036 uint scanID = getHitScanID(i);
1037 vec3 r = xyz - getScanOrigin(scanID);
1038
1039 if (r.magnitude() > maxdistance) {
1040 deleteHitPoint(i);
1041 delete_count++;
1042 }
1043 }
1044
1045 if (printmessages) {
1046 std::cout << "Removed " << delete_count << " hit points based on distance filter." << std::endl;
1047 }
1048}
1049
1050void LiDARcloud::reflectanceFilter(float minreflectance) {
1051
1052 std::size_t delete_count = 0;
1053 for (int r = (getHitCount() - 1); r >= 0; r--) {
1054 if (hits.at(r).data.find("reflectance") != hits.at(r).data.end()) {
1055 double R = getHitData(r, "reflectance");
1056 if (R < minreflectance) {
1057 deleteHitPoint(r);
1058 delete_count++;
1059 }
1060 }
1061 }
1062
1063 if (printmessages) {
1064 std::cout << "Removed " << delete_count << " hit points based on reflectance filter." << std::endl;
1065 }
1066}
1067
1068void LiDARcloud::scalarFilter(const char *scalar_field, float threshold, const char *comparator) {
1069
1070 std::size_t delete_count = 0;
1071 for (int r = (getHitCount() - 1); r >= 0; r--) {
1072 if (hits.at(r).data.find(scalar_field) != hits.at(r).data.end()) {
1073 double R = getHitData(r, scalar_field);
1074 if (strcmp(comparator, "<") == 0) {
1075 if (R < threshold) {
1076 deleteHitPoint(r);
1077 delete_count++;
1078 }
1079 } else if (strcmp(comparator, ">") == 0) {
1080 if (R > threshold) {
1081 deleteHitPoint(r);
1082 delete_count++;
1083 }
1084 } else if (strcmp(comparator, "=") == 0) {
1085 if (R == threshold) {
1086 deleteHitPoint(r);
1087 delete_count++;
1088 }
1089 }
1090 }
1091 }
1092
1093 if (printmessages) {
1094 std::cout << "Removed " << delete_count << " hit points based on scalar filter." << std::endl;
1095 }
1096}
1097
1098void LiDARcloud::xyzFilter(float xmin, float xmax, float ymin, float ymax, float zmin, float zmax) {
1099
1100 xyzFilter(xmin, xmax, ymin, ymax, zmin, zmax, true);
1101}
1102
1103void LiDARcloud::xyzFilter(float xmin, float xmax, float ymin, float ymax, float zmin, float zmax, bool deleteOutside) {
1104
1105 if (xmin > xmax || ymin > ymax || zmin > zmax) {
1106 std::cout << "WARNING: at least one minimum value provided is greater than one maximum value. " << std::endl;
1107 }
1108
1109 std::size_t delete_count = 0;
1110
1111 if (deleteOutside) {
1112 for (int i = (getHitCount() - 1); i >= 0; i--) {
1113 vec3 xyz = getHitXYZ(i);
1114
1115 if (xyz.x < xmin || xyz.x > xmax || xyz.y < ymin || xyz.y > ymax || xyz.z < zmin || xyz.z > zmax) {
1116 deleteHitPoint(i);
1117 delete_count++;
1118 }
1119 }
1120 } else {
1121 for (int i = (getHitCount() - 1); i >= 0; i--) {
1122 vec3 xyz = getHitXYZ(i);
1123
1124 if (xyz.x >= xmin && xyz.x < xmax && xyz.y > ymin && xyz.y < ymax && xyz.z > zmin && xyz.z < zmax) {
1125 deleteHitPoint(i);
1126 delete_count++;
1127 }
1128 }
1129 }
1130
1131
1132 if (printmessages) {
1133 std::cout << "Removed " << delete_count << " hit points based on provided bounding box." << std::endl;
1134 }
1135}
1136
1137// bool sortcol0( const std::vector<float>& v0, const std::vector<float>& v1 ){
1138// return v0.at(0)<v1.at(0);
1139// }
1140
1141// bool sortcol1( const std::vector<float>& v0, const std::vector<float>& v1 ){
1142// return v0.at(1)<v1.at(1);
1143// }
1144
1145bool sortcol0(const std::vector<double> &v0, const std::vector<double> &v1) {
1146 return v0.at(0) < v1.at(0);
1147}
1148
1149bool sortcol1(const std::vector<double> &v0, const std::vector<double> &v1) {
1150 return v0.at(1) < v1.at(1);
1151}
1152
1153void LiDARcloud::maxPulseFilter(const char *scalar) {
1154
1155 if (printmessages) {
1156 std::cout << "Filtering point cloud by maximum " << scalar << " per pulse..." << std::flush;
1157 }
1158
1159 std::vector<std::vector<double>> timestamps;
1160 timestamps.resize(getHitCount());
1161
1162 std::size_t delete_count = 0;
1163 for (std::size_t r = 0; r < getHitCount(); r++) {
1164
1165 if (!doesHitDataExist(r, "timestamp")) {
1166 std::cerr << "failed\nERROR (LiDARcloud::maxPulseFilter): Hit point " << r
1167 << " does not have scalar data "
1168 "timestamp"
1169 ", which is required for max pulse filtering. No filtering will be performed."
1170 << std::endl;
1171 return;
1172 } else if (!doesHitDataExist(r, scalar)) {
1173 std::cerr << "failed\nERROR (LiDARcloud::maxPulseFilter): Hit point " << r
1174 << " does not have scalar data "
1175 ""
1176 << scalar
1177 << ""
1178 ". No filtering will be performed."
1179 << std::endl;
1180 return;
1181 }
1182
1183 std::vector<double> v{getHitData(r, "timestamp"), getHitData(r, scalar), double(r)};
1184
1185 timestamps.at(r) = v;
1186 }
1187
1188 std::sort(timestamps.begin(), timestamps.end(), sortcol0);
1189
1190 std::vector<std::vector<double>> isort;
1191 std::vector<int> to_delete;
1192 double time_old = timestamps.at(0).at(0);
1193 for (std::size_t r = 0; r < timestamps.size(); r++) {
1194
1195 if (timestamps.at(r).at(0) != time_old) {
1196
1197 if (isort.size() > 1) {
1198
1199 std::sort(isort.begin(), isort.end(), sortcol1);
1200
1201 for (int i = 0; i < isort.size() - 1; i++) {
1202 to_delete.push_back(int(isort.at(i).at(2)));
1203 }
1204 }
1205
1206 isort.resize(0);
1207 time_old = timestamps.at(r).at(0);
1208 }
1209
1210 isort.push_back(timestamps.at(r));
1211 }
1212
1213 std::sort(to_delete.begin(), to_delete.end());
1214
1215 for (int i = to_delete.size() - 1; i >= 0; i--) {
1216 deleteHitPoint(to_delete.at(i));
1217 }
1218
1219 if (printmessages) {
1220 std::cout << "done." << std::endl;
1221 }
1222}
1223
1224void LiDARcloud::minPulseFilter(const char *scalar) {
1225
1226 if (printmessages) {
1227 std::cout << "Filtering point cloud by minimum " << scalar << " per pulse..." << std::flush;
1228 }
1229
1230 std::vector<std::vector<double>> timestamps;
1231 timestamps.resize(getHitCount());
1232
1233 std::size_t delete_count = 0;
1234 for (std::size_t r = 0; r < getHitCount(); r++) {
1235
1236 if (!doesHitDataExist(r, "timestamp")) {
1237 std::cerr << "failed\nERROR (LiDARcloud::maxPulseFilter): Hit point " << r
1238 << " does not have scalar data "
1239 "timestamp"
1240 ", which is required for max pulse filtering. No filtering will be performed."
1241 << std::endl;
1242 return;
1243 } else if (!doesHitDataExist(r, scalar)) {
1244 std::cerr << "failed\nERROR (LiDARcloud::maxPulseFilter): Hit point " << r
1245 << " does not have scalar data "
1246 ""
1247 << scalar
1248 << ""
1249 ". No filtering will be performed."
1250 << std::endl;
1251 return;
1252 }
1253
1254 std::vector<double> v{getHitData(r, "timestamp"), getHitData(r, scalar), float(r)};
1255
1256 timestamps.at(r) = v;
1257 }
1258
1259 std::sort(timestamps.begin(), timestamps.end(), sortcol0);
1260
1261 std::vector<std::vector<double>> isort;
1262 std::vector<int> to_delete;
1263 double time_old = timestamps.at(0).at(0);
1264 for (std::size_t r = 0; r < timestamps.size(); r++) {
1265
1266 if (timestamps.at(r).at(0) != time_old) {
1267
1268 if (isort.size() > 1) {
1269
1270 std::sort(isort.begin(), isort.end(), sortcol1);
1271
1272 for (int i = 1; i < isort.size(); i++) {
1273 to_delete.push_back(int(isort.at(i).at(2)));
1274 }
1275 }
1276
1277 isort.resize(0);
1278 time_old = timestamps.at(r).at(0);
1279 }
1280
1281 isort.push_back(timestamps.at(r));
1282 }
1283
1284 std::sort(to_delete.begin(), to_delete.end());
1285
1286 for (int i = to_delete.size() - 1; i >= 0; i--) {
1287 deleteHitPoint(to_delete.at(i));
1288 }
1289
1290 if (printmessages) {
1291 std::cout << "done." << std::endl;
1292 }
1293}
1294
1296
1297 if (printmessages) {
1298 std::cout << "Filtering point cloud to only first hits per pulse..." << std::flush;
1299 }
1300
1301 std::vector<float> target_index;
1302 target_index.resize(getHitCount());
1303 int min_tindex = 1;
1304
1305 for (std::size_t r = 0; r < target_index.size(); r++) {
1306
1307 if (!doesHitDataExist(r, "target_index")) {
1308 std::cerr << "failed\nERROR (LiDARcloud::firstHitFilter): Hit point " << r
1309 << " does not have scalar data "
1310 "target_index"
1311 ". No filtering will be performed."
1312 << std::endl;
1313 return;
1314 }
1315
1316 target_index.at(r) = getHitData(r, "target_index");
1317
1318 if (target_index.at(r) == 0) {
1319 min_tindex = 0;
1320 }
1321 }
1322
1323 for (int r = target_index.size() - 1; r >= 0; r--) {
1324
1325 if (target_index.at(r) != min_tindex) {
1326 deleteHitPoint(r);
1327 }
1328 }
1329
1330 if (printmessages) {
1331 std::cout << "done." << std::endl;
1332 }
1333}
1334
1336
1337 if (printmessages) {
1338 std::cout << "Filtering point cloud to only last hits per pulse..." << std::flush;
1339 }
1340
1341 std::vector<float> target_index;
1342 target_index.resize(getHitCount());
1343 int min_tindex = 1;
1344
1345 for (std::size_t r = 0; r < target_index.size(); r++) {
1346
1347 if (!doesHitDataExist(r, "target_index")) {
1348 std::cout << "failed\n";
1349 std::cerr << "ERROR (LiDARcloud::lastHitFilter): Hit point " << r
1350 << " does not have scalar data "
1351 "target_index"
1352 ". No filtering will be performed."
1353 << std::endl;
1354 return;
1355 } else if (!doesHitDataExist(r, "target_count")) {
1356 std::cout << "failed\n";
1357 std::cerr << "ERROR (LiDARcloud::lastHitFilter): Hit point " << r
1358 << " does not have scalar data "
1359 "target_count"
1360 ". No filtering will be performed."
1361 << std::endl;
1362 return;
1363 }
1364
1365 target_index.at(r) = getHitData(r, "target_index");
1366
1367 if (target_index.at(r) == 0) {
1368 min_tindex = 0;
1369 }
1370 }
1371
1372 for (int r = target_index.size() - 1; r >= 0; r--) {
1373
1374 float target_count = getHitData(r, "target_count");
1375
1376 if (target_index.at(r) == target_count - 1 + min_tindex) {
1377 deleteHitPoint(r);
1378 }
1379 }
1380
1381 if (printmessages) {
1382 std::cout << "done." << std::endl;
1383 }
1384}
1385
1386std::vector<helios::vec3> LiDARcloud::gapfillMisses() {
1387 std::vector<helios::vec3> xyz_filled;
1388 for (uint scanID = 0; scanID < getScanCount(); scanID++) {
1389 std::vector<helios::vec3> filled_this_scan = gapfillMisses(scanID, false, false);
1390 xyz_filled.insert(xyz_filled.end(), filled_this_scan.begin(), filled_this_scan.end());
1391 }
1392 return xyz_filled;
1393}
1394
1395std::vector<helios::vec3> LiDARcloud::gapfillMisses(uint scanID) {
1396 return gapfillMisses(scanID, false, false);
1397}
1398
1399std::vector<helios::vec3> LiDARcloud::gapfillMisses(uint scanID, const bool gapfill_grid_only, const bool add_flags) {
1400
1401 // Validate scanID
1402 if (scanID >= getScanCount()) {
1403 helios_runtime_error("ERROR (LiDARcloud::gapfillMisses): Invalid scanID " + std::to_string(scanID) +
1404 ". Only " + std::to_string(getScanCount()) + " scans exist.");
1405 }
1406
1407 if (printmessages) {
1408 std::cout << "Gap filling complete misses in scan " << scanID << "..." << std::flush;
1409 }
1410
1411 float gap_distance = 20000;
1412
1413 helios::vec3 origin = getScanOrigin(scanID);
1414 std::vector<helios::vec3> xyz_filled;
1415
1416 // Populating a hit table for each scan:
1417 // Column 0 - hit index; Column 1 - timestamp; Column 2 - ray zenith; Column 3 - ray azimuth
1418 std::vector<std::vector<double>> hit_table;
1419 for (size_t r = 0; r < getHitCount(); r++) {
1420 if (getHitScanID(r) == scanID) {
1421
1422 if (add_flags) {
1423 // gapfillMisses_code = 0: original points
1424 setHitData(r, "gapfillMisses_code", 0.0);
1425 }
1426
1428
1429 if (!doesHitDataExist(r, "timestamp")) {
1430 helios_runtime_error("ERROR (LiDARcloud::gapfillMisses): Hit " + std::to_string(r) +
1431 " is missing required 'timestamp' data. Cannot perform gap filling.");
1432 }
1433
1434 double timestamp = getHitData(r, "timestamp");
1435 std::vector<double> data;
1436 data.resize(4);
1437 data.at(0) = float(r);
1438 data.at(1) = timestamp;
1439 data.at(2) = raydir.zenith;
1440 data.at(3) = raydir.azimuth;
1441 hit_table.push_back(data);
1442 }
1443 }
1444
1445 // Check for empty scan
1446 if (hit_table.empty()) {
1447 if (printmessages) {
1448 std::cout << "scan has no hits. Skipping gap fill." << std::endl;
1449 }
1450 return xyz_filled; // Return empty vector
1451 }
1452
1453 // sorting, initial dt and dtheta calculations, and determining minimum target index in the scan
1454
1455 // sort the hit table by column 1 (timestamp)
1456 std::sort(hit_table.begin(), hit_table.end(), sortcol1);
1457
1458 int min_tindex = 1;
1459 for (size_t r = 0; r < hit_table.size() - 1; r++) {
1460
1461 // this is to figure out if target indexing uses 0 or 1 offset
1462 if (min_tindex == 1 && doesHitDataExist(hit_table.at(r).at(0), "target_index") && doesHitDataExist(hit_table.at(r).at(0), "target_count")) {
1463 if (getHitData(hit_table.at(r).at(0), "target_index") == 0) {
1464 min_tindex = 0;
1465 }
1466 }
1467 }
1468
1469 // getting rid of points with target index greater than the minimum
1470
1471 int ndup_target = 0;
1472 // create new array without duplicate timestamps
1473 std::vector<std::vector<double>> hit_table_semiclean;
1474 for (size_t r = 0; r < hit_table.size() - 1; r++) {
1475
1476 // only consider first hits
1477 if (doesHitDataExist(hit_table.at(r).at(0), "target_index") && doesHitDataExist(hit_table.at(r).at(0), "target_count")) {
1478 if (getHitData(hit_table.at(r).at(0), "target_index") > min_tindex) {
1479 ndup_target++;
1480 continue;
1481 }
1482 }
1483
1484 hit_table_semiclean.push_back(hit_table.at(r));
1485 }
1486 // Add the last element (loop above stops at size()-1)
1487 if (!hit_table.empty()) {
1488 hit_table_semiclean.push_back(hit_table.back());
1489 }
1490
1491 // re-calculating dt
1492
1493 std::vector<double> dt_semiclean;
1494 dt_semiclean.resize(hit_table_semiclean.size());
1495 for (size_t r = 0; r < hit_table_semiclean.size() - 1; r++) {
1496
1497 dt_semiclean.at(r) = hit_table_semiclean.at(r + 1).at(1) - hit_table_semiclean.at(r).at(1);
1498 // set the hit index of the new array
1499 hit_table_semiclean.at(r).at(0) = r;
1500 }
1501
1502 // checking for duplicate timestamps in the remaining data
1503
1504 int ndup = 0;
1505 // create new array without duplicate timestamps
1506 std::vector<std::vector<double>> hit_table_clean;
1507 for (size_t r = 0; r < hit_table_semiclean.size() - 1; r++) {
1508
1509 // if there are still rows with duplicate timestamps, it probably means there is no "target_index" column, but multiple hits per timestamp are still included
1510 // proceed using this assumption, just get rid of the rows where dt = 0 for simplicity (last hits probably are what remain).
1511 if (dt_semiclean.at(r) == 0) {
1512 ndup++;
1513 continue;
1514 }
1515
1516 hit_table_clean.push_back(hit_table_semiclean.at(r));
1517 }
1518
1519 // recalculate dt and dtheta with only one hit per beam
1520 // and calculate the minimum dt value
1521 std::vector<double> dt_clean;
1522 std::vector<float> dtheta_clean;
1523 dt_clean.resize(hit_table_clean.size());
1524 dtheta_clean.resize(hit_table_clean.size());
1525
1526 double dt_clean_min = 1e6;
1527 for (size_t r = 0; r < hit_table_clean.size() - 1; r++) {
1528
1529 dt_clean.at(r) = hit_table_clean.at(r + 1).at(1) - hit_table_clean.at(r).at(1);
1530 dtheta_clean.at(r) = hit_table_clean.at(r + 1).at(2) - hit_table_clean.at(r).at(2);
1531 // set the hit index of the new array
1532 hit_table_clean.at(r).at(0) = r;
1533
1534 if (dt_clean.at(r) < dt_clean_min) {
1535 dt_clean_min = dt_clean.at(r);
1536 }
1537 }
1538
1539 // configuration of 2D map
1540 // reconfigure hit table into 2D (theta,phi) map
1541 std::vector<std::vector<std::vector<double>>> hit_table2D;
1542
1543 int column = 0;
1544 hit_table2D.resize(1);
1545 for (size_t r = 0; r < hit_table_clean.size() - 1; r++) {
1546
1547 hit_table2D.at(column).push_back(hit_table_clean.at(r));
1548 // for small scans (like the rectangle test case, this needs to change to < 0 or some smaller angle (that is larger than noise))
1549 // if( dtheta_clean.at(r) < 0 ){
1550 // for normal scans, this threshold allows for 10 degrees drops in theta within a given sweep as noise. This can be adjusted as appropriate.
1551 if (dtheta_clean.at(r) < -0.1745329f) {
1552 column++;
1553 hit_table2D.resize(column + 1);
1554 }
1555 }
1556
1557 // calculate average dt and dtheta for subsequent points
1558
1559 // calculate average dt
1560 float dt_avg = 0;
1561 int dt_sum = 0;
1562
1563 // calculate the average dtheta to use for extrapolation
1564 float dtheta_avg = 0;
1565 int dtheta_sum = 0;
1566
1567 for (int j = 0; j < hit_table2D.size(); j++) {
1568 for (int i = 0; i < hit_table2D.at(j).size(); i++) {
1569 int r = int(hit_table2D.at(j).at(i).at(0));
1570 if (dt_clean.at(r) >= dt_clean_min && dt_clean.at(r) < 1.5 * dt_clean_min) {
1571 dt_avg += dt_clean.at(r);
1572 dt_sum++;
1573
1574 // calculate the average dtheta to use for extrapolation
1575 dtheta_avg += dtheta_clean.at(r);
1576 dtheta_sum++;
1577 }
1578 }
1579 }
1580
1581 if (dt_sum == 0 || dtheta_sum == 0) {
1582 if (printmessages) {
1583 std::cout << "insufficient valid hit pairs. Skipping gap fill." << std::endl;
1584 }
1585 return xyz_filled; // Return empty vector
1586 }
1587
1588 dt_avg = dt_avg / float(dt_sum);
1589 // Calculate the average dtheta to use for extrapolation
1590 dtheta_avg = dtheta_avg / float(dtheta_sum);
1591
1592 // Get theta range for grid position calculations (needed early for filled_positions)
1593 helios::vec2 theta_range = getScanRangeTheta(scanID);
1594
1595 // Track which grid positions have been filled (to avoid duplicates)
1596 std::set<std::pair<int, int>> filled_positions;
1597
1598 // Pre-populate with existing hit positions using proper direction2rc conversion
1599 for (size_t r = 0; r < getHitCount(); r++) {
1600 if (getHitScanID(r) == scanID) {
1602 helios::int2 rc = scans.at(scanID).direction2rc(raydir);
1603 filled_positions.insert(std::make_pair(rc.x, rc.y));
1604 }
1605 }
1606
1607 // identify gaps and fill
1608 for (int j = 0; j < hit_table2D.size(); j++) {
1609
1610 if (hit_table2D.at(j).size() > 0) {
1611 for (int i = 0; i < hit_table2D.at(j).size() - 1; i++) {
1612
1613 double dt = hit_table2D.at(j).at(i + 1).at(1) - hit_table2D.at(j).at(i).at(1);
1614
1615 if (dt > 1.5f * dt_clean_min) { // missing hit(s)
1616
1617 // calculate number of missing hits
1618 int Ngap = round(dt / dt_avg) - 1;
1619
1620 // fill missing points
1621 for (int k = 1; k <= Ngap; k++) {
1622
1623 float timestep = hit_table2D.at(j).at(i).at(1) + dt_avg * float(k);
1624
1625 // interpolate theta and phi
1626 float theta = hit_table2D.at(j).at(i).at(2) + (hit_table2D.at(j).at(i + 1).at(2) - hit_table2D.at(j).at(i).at(2)) * float(k) / float(Ngap + 1);
1627 float phi = hit_table2D.at(j).at(i).at(3) + (hit_table2D.at(j).at(i + 1).at(3) - hit_table2D.at(j).at(i).at(3)) * float(k) / float(Ngap + 1);
1628 // Wrap phi to [0, 2π] range
1629 if (phi > 2.f * M_PI) {
1630 phi = phi - 2.f * M_PI;
1631 } else if (phi < 0.f) {
1632 phi = phi + 2.f * M_PI;
1633 }
1634
1635 // Convert to grid indices using proper direction2rc method
1636 helios::SphericalCoord dir_to_check(gap_distance, 0.5 * M_PI - theta, phi);
1637 helios::int2 rc = scans.at(scanID).direction2rc(dir_to_check);
1638 auto grid_key = std::make_pair(rc.x, rc.y);
1639
1640 // Only add if this grid position hasn't been filled yet
1641 if (filled_positions.find(grid_key) == filled_positions.end()) {
1642
1643 helios::SphericalCoord spherical(gap_distance, 0.5 * M_PI - theta, phi);
1644 helios::vec3 xyz = origin + helios::sphere2cart(spherical);
1645 xyz_filled.push_back(xyz);
1646
1647 std::map<std::string, double> data;
1648 data.insert(std::pair<std::string, double>("timestamp", timestep));
1649 data.insert(std::pair<std::string, double>("target_index", min_tindex));
1650 data.insert(std::pair<std::string, double>("nRaysHit", 500));
1651 if (add_flags) {
1652 // gapfillMisses_code = 1: gapfilled points
1653 data.insert(std::pair<std::string, double>("gapfillMisses_code", 1.0));
1654 }
1655 addHitPoint(scanID, xyz, spherical, data);
1656 filled_positions.insert(grid_key); // Mark as filled
1657 }
1658 }
1659 }
1660 }
1661 }
1662 }
1663 uint npointsfilled = xyz_filled.size();
1664
1665 // Get actual grid spacing for proper edge extrapolation (theta_range already declared above)
1666 float grid_dtheta = (theta_range.y - theta_range.x) / float(scans.at(scanID).Ntheta - 1);
1667 float grid_dphi = (scans.at(scanID).phiMax - scans.at(scanID).phiMin) / float(scans.at(scanID).Nphi - 1);
1668
1669 if (gapfill_grid_only == true) {
1670 // instead of extrapolating to the angle ranges given in the xml file, we can extrapolate to the angle range of the voxel grid to save time.
1671 // to do this we loop through the vertices of the voxel grid.
1672 std::vector<helios::vec3> grid_vertices;
1673 helios::vec3 boxmin, boxmax;
1674 getGridBoundingBox(boxmin, boxmax); // axis aligned bounding box of all grid cells
1675 grid_vertices.push_back(boxmin);
1676 grid_vertices.push_back(boxmax);
1677 grid_vertices.push_back(helios::make_vec3(boxmin.x, boxmin.y, boxmax.z));
1678 grid_vertices.push_back(helios::make_vec3(boxmax.x, boxmax.y, boxmin.z));
1679 grid_vertices.push_back(helios::make_vec3(boxmin.x, boxmax.y, boxmin.z));
1680 grid_vertices.push_back(helios::make_vec3(boxmin.x, boxmax.y, boxmax.z));
1681 grid_vertices.push_back(helios::make_vec3(boxmax.x, boxmin.y, boxmin.z));
1682 grid_vertices.push_back(helios::make_vec3(boxmax.x, boxmin.y, boxmax.z));
1683
1684 float max_theta = 0;
1685 float min_theta = M_PI;
1686 float max_phi = 0;
1687 float min_phi = 2 * M_PI;
1688 for (uint gg = 0; gg < grid_vertices.size(); gg++) {
1689 helios::vec3 direction_cart = grid_vertices.at(gg) - getScanOrigin(scanID);
1690 helios::SphericalCoord sc = cart2sphere(direction_cart);
1691 if (sc.azimuth < min_phi) {
1692 min_phi = sc.azimuth;
1693 }
1694
1695 if (sc.azimuth > max_phi) {
1696 max_phi = sc.azimuth;
1697 }
1698
1699 if (sc.zenith < min_theta) {
1700 min_theta = sc.zenith;
1701 }
1702
1703 if (sc.zenith > max_theta) {
1704 max_theta = sc.zenith;
1705 }
1706 }
1707
1708 // if the min or max theta is outside of the values provided in xml, use the xml values
1709 if (min_theta < theta_range.x) {
1710 min_theta = theta_range.x;
1711 }
1712
1713 if (max_theta > theta_range.y) {
1714 max_theta = theta_range.y;
1715 }
1716
1717 theta_range = helios::make_vec2(min_theta, max_theta);
1718 }
1719
1720 // extrapolate missing points
1721 for (int j = 0; j < hit_table2D.size(); j++) {
1722
1723 if (hit_table2D.at(j).size() > 0) {
1724
1725 // upward edge points
1726 if (hit_table2D.at(j).front().at(2) > theta_range.x) {
1727
1728 float dtheta = dtheta_avg;
1729 float theta = hit_table2D.at(j).at(0).at(2) - dtheta;
1730 // just use the last value of phi in the sweep
1731 float phi = hit_table2D.at(j).at(0).at(3);
1732 float timestep = hit_table2D.at(j).at(0).at(1) - dt_avg;
1733 if (dtheta == 0) {
1734 continue;
1735 }
1736
1737 while (theta > theta_range.x) {
1738
1739 // Convert to grid indices using proper direction2rc method
1740 helios::SphericalCoord dir_to_check(gap_distance, 0.5 * M_PI - theta, phi);
1741 helios::int2 rc = scans.at(scanID).direction2rc(dir_to_check);
1742
1743 // Only add if this grid position is actually empty (avoid duplicates)
1744 if (rc.x >= 0 && rc.x < (int)scans.at(scanID).Ntheta &&
1745 rc.y >= 0 && rc.y < (int)scans.at(scanID).Nphi) {
1746
1747 // Check if this grid position has already been filled (avoid duplicates)
1748 auto grid_key = std::make_pair(rc.x, rc.y);
1749 if (filled_positions.find(grid_key) == filled_positions.end()) {
1750
1751 helios::SphericalCoord spherical(gap_distance, 0.5 * M_PI - theta, phi);
1752 helios::vec3 xyz = origin + helios::sphere2cart(spherical);
1753 xyz_filled.push_back(xyz);
1754
1755 std::map<std::string, double> data;
1756 data.insert(std::pair<std::string, double>("timestamp", timestep));
1757 data.insert(std::pair<std::string, double>("target_index", min_tindex));
1758 data.insert(std::pair<std::string, double>("nRaysHit", 500));
1759 if (add_flags) {
1760 // gapfillMisses_code = 3: upward edge points
1761 data.insert(std::pair<std::string, double>("gapfillMisses_code", 3.0));
1762 }
1763
1764 addHitPoint(scanID, xyz, spherical, data);
1765 filled_positions.insert(grid_key); // Mark this position as filled
1766 }
1767 }
1768
1769 theta = theta - dtheta;
1770 timestep = timestep - dt_avg;
1771 }
1772 }
1773
1774 // downward edge points
1775 if (hit_table2D.at(j).back().at(2) < theta_range.y) {
1776
1777 int sz = hit_table2D.at(j).size();
1778 // same concept as above for downward edge points
1779 float dtheta = dtheta_avg;
1780 float theta = hit_table2D.at(j).at(sz - 1).at(2) + dtheta;
1781 float phi = hit_table2D.at(j).at(sz - 1).at(3);
1782 float timestep = hit_table2D.at(j).at(sz - 1).at(1) + dt_avg;
1783 while (theta < theta_range.y) {
1784
1785 // Convert to grid indices using proper direction2rc method
1786 helios::SphericalCoord dir_to_check(gap_distance, 0.5 * M_PI - theta, phi);
1787 helios::int2 rc = scans.at(scanID).direction2rc(dir_to_check);
1788
1789 // Only add if this grid position is actually empty (avoid duplicates)
1790 if (rc.x >= 0 && rc.x < (int)scans.at(scanID).Ntheta &&
1791 rc.y >= 0 && rc.y < (int)scans.at(scanID).Nphi) {
1792
1793 // Check if this grid position has already been filled (avoid duplicates)
1794 auto grid_key = std::make_pair(rc.x, rc.y);
1795 if (filled_positions.find(grid_key) == filled_positions.end()) {
1796
1797 helios::SphericalCoord spherical(gap_distance, 0.5 * M_PI - theta, phi);
1798 helios::vec3 xyz = origin + helios::sphere2cart(spherical);
1799 xyz_filled.push_back(xyz);
1800
1801 std::map<std::string, double> data;
1802 data.insert(std::pair<std::string, double>("timestamp", timestep));
1803 data.insert(std::pair<std::string, double>("target_index", min_tindex));
1804 data.insert(std::pair<std::string, double>("nRaysHit", 500));
1805 if (add_flags) {
1806 // gapfillMisses_code = 2: downward edge points
1807 data.insert(std::pair<std::string, double>("gapfillMisses_code", 2.0));
1808 }
1809
1810 addHitPoint(scanID, xyz, spherical, data);
1811 filled_positions.insert(grid_key); // Mark this position as filled
1812 }
1813 }
1814
1815 theta = theta + dtheta;
1816 timestep = timestep + dt_avg;
1817 }
1818 }
1819 }
1820 }
1821
1822 uint npointsextrapolated = xyz_filled.size() - npointsfilled;
1823
1824 if (printmessages) {
1825 std::cout << "filled " << xyz_filled.size() << " points (" << npointsfilled << " interior, "
1826 << npointsextrapolated << " edge)." << std::endl;
1827 std::cout << " Processed " << hit_table2D.size() << " scan columns" << std::endl;
1828 }
1829 return xyz_filled;
1830}
1831
1832void LiDARcloud::triangulateHitPoints(float Lmax, float max_aspect_ratio) {
1833
1834 if (printmessages && getScanCount() == 0) {
1835 cout << "WARNING (triangulateHitPoints): No scans have been added to the point cloud. Skipping triangulation..." << endl;
1836 return;
1837 } else if (printmessages && getHitCount() == 0) {
1838 cout << "WARNING (triangulateHitPoints): No hit points have been added to the point cloud. Skipping triangulation..." << endl;
1839 return;
1840 }
1841
1842 if (!hitgridcellcomputed) {
1844 }
1845
1846 int Ntriangles = 0;
1847
1848 // For multi-return data, calculate adaptive separation ratio threshold
1849 bool use_adaptive_threshold = isMultiReturnData();
1850 float adaptive_sep_threshold = 0.0f;
1851
1852 if (use_adaptive_threshold) {
1853 if (printmessages) {
1854 std::cout << "Multi-return data detected - calculating adaptive separation ratio threshold..." << std::endl;
1855 }
1856
1857 // First pass: collect separation ratios from all potential triangles
1858 std::vector<float> all_separation_ratios;
1859
1860 for (uint s = 0; s < getScanCount(); s++) {
1861 std::vector<int> Delaunay_inds_pass1;
1862 std::vector<Shx> pts_pass1, pts_copy_pass1;
1863 int count_pass1 = 0;
1864
1865 for (int r = 0; r < getHitCount(); r++) {
1866 if (getHitScanID(r) == s && getHitGridCell(r) >= 0) {
1867 // Auto-filter first returns for multi-return data
1868 if (use_adaptive_threshold) {
1869 if (doesHitDataExist(r, "target_index") && getHitData(r, "target_index") != 0.0) {
1870 continue; // Skip non-first returns
1871 }
1872 }
1873
1874 helios::SphericalCoord direction = getHitRaydir(r);
1875 helios::vec3 direction_cart = getHitXYZ(r) - getScanOrigin(s);
1876 direction = cart2sphere(direction_cart);
1877
1878 Shx pt;
1879 pt.id = count_pass1;
1880 pt.r = direction.zenith;
1881 pt.c = direction.azimuth;
1882 pts_pass1.push_back(pt);
1883 Delaunay_inds_pass1.push_back(r);
1884 count_pass1++;
1885 }
1886 }
1887
1888 if (pts_pass1.size() == 0) continue;
1889
1890 // Handle coordinate wrapping
1891 float h[2] = {0, 0};
1892 for (int r = 0; r < pts_pass1.size(); r++) {
1893 if (pts_pass1.at(r).c < 0.5 * M_PI) h[0] += 1.f;
1894 else if (pts_pass1.at(r).c > 1.5 * M_PI) h[1] += 1.f;
1895 }
1896 h[0] /= float(pts_pass1.size());
1897 h[1] /= float(pts_pass1.size());
1898 if (h[0] + h[1] > 0.4) {
1899 for (int r = 0; r < pts_pass1.size(); r++) {
1900 pts_pass1.at(r).c += M_PI;
1901 if (pts_pass1.at(r).c > 2.f * M_PI) pts_pass1.at(r).c -= 2.f * M_PI;
1902 }
1903 }
1904
1905 std::vector<int> dupes_pass1;
1906 de_duplicate(pts_pass1, dupes_pass1);
1907
1908 std::vector<Triad> triads_pass1;
1909 int success = 0, Ntries = 0;
1910 while (success != 1 && Ntries < 3) {
1911 Ntries++;
1912 success = s_hull_pro(pts_pass1, triads_pass1);
1913 if (success != 1) {
1914 for (int r = 0; r < pts_pass1.size(); r++) {
1915 pts_pass1.at(r).c += 0.25 * M_PI;
1916 if (pts_pass1.at(r).c > 2.f * M_PI) pts_pass1.at(r).c -= 2.f * M_PI;
1917 }
1918 }
1919 }
1920
1921 if (success != 1) continue;
1922
1923 // Collect separation ratios (pre-filter by edge length)
1924 for (int t = 0; t < triads_pass1.size(); t++) {
1925 int ID0 = Delaunay_inds_pass1.at(triads_pass1.at(t).a);
1926 int ID1 = Delaunay_inds_pass1.at(triads_pass1.at(t).b);
1927 int ID2 = Delaunay_inds_pass1.at(triads_pass1.at(t).c);
1928
1929 helios::vec3 v0 = getHitXYZ(ID0);
1930 helios::vec3 v1 = getHitXYZ(ID1);
1931 helios::vec3 v2 = getHitXYZ(ID2);
1935
1936 float L0 = (v0 - v1).magnitude();
1937 float L1 = (v0 - v2).magnitude();
1938 float L2 = (v1 - v2).magnitude();
1939
1940 // Skip triangles that fail edge length filter
1941 if (L0 > Lmax || L1 > Lmax || L2 > Lmax) {
1942 continue;
1943 }
1944
1945 float ang01 = sqrt(pow(r0.zenith - r1.zenith, 2) + pow(r0.azimuth - r1.azimuth, 2));
1946 float ang02 = sqrt(pow(r0.zenith - r2.zenith, 2) + pow(r0.azimuth - r2.azimuth, 2));
1947 float ang12 = sqrt(pow(r1.zenith - r2.zenith, 2) + pow(r1.azimuth - r2.azimuth, 2));
1948
1949 float ratio01 = L0 / (ang01 + 1e-6);
1950 float ratio02 = L1 / (ang02 + 1e-6);
1951 float ratio12 = L2 / (ang12 + 1e-6);
1952 float max_sep_ratio = max(max(ratio01, ratio02), ratio12);
1953
1954 all_separation_ratios.push_back(max_sep_ratio);
1955 }
1956 }
1957
1958 // Calculate 25th percentile and set adaptive threshold
1959 if (!all_separation_ratios.empty()) {
1960 std::sort(all_separation_ratios.begin(), all_separation_ratios.end());
1961 size_t idx_25 = all_separation_ratios.size() / 4;
1962 float percentile_25 = all_separation_ratios[idx_25];
1963 adaptive_sep_threshold = 8.5f * percentile_25;
1964
1965 if (printmessages) {
1966 std::cout << " 25th percentile separation ratio: " << percentile_25 << std::endl;
1967 std::cout << " Adaptive threshold: " << adaptive_sep_threshold << std::endl;
1968 }
1969 }
1970 }
1971
1972 // Second pass: perform triangulation with adaptive filtering
1973 for (uint s = 0; s < getScanCount(); s++) {
1974
1975 std::vector<int> Delaunay_inds;
1976
1977 std::vector<Shx> pts, pts_copy;
1978
1979 int count = 0;
1980 for (int r = 0; r < getHitCount(); r++) {
1981
1982 if (getHitScanID(r) == s && getHitGridCell(r) >= 0) {
1983 // Auto-filter first returns for multi-return data
1984 if (use_adaptive_threshold) {
1985 if (doesHitDataExist(r, "target_index") && getHitData(r, "target_index") != 0.0) {
1986 continue; // Skip non-first returns
1987 }
1988 }
1989
1990 helios::SphericalCoord direction = getHitRaydir(r);
1991
1992 helios::vec3 direction_cart = getHitXYZ(r) - getScanOrigin(s);
1993 direction = cart2sphere(direction_cart);
1994
1995 Shx pt;
1996 pt.id = count;
1997 pt.r = direction.zenith;
1998 pt.c = direction.azimuth;
1999
2000 pts.push_back(pt);
2001
2002 Delaunay_inds.push_back(r);
2003
2004 count++;
2005 }
2006 }
2007
2008 if (pts.size() == 0) {
2009 if (printmessages) {
2010 std::cout << "Scan " << s << " contains no triangles. Skipping this scan..." << std::endl;
2011 }
2012 continue;
2013 }
2014
2015 float h[2] = {0, 0};
2016 for (int r = 0; r < pts.size(); r++) {
2017 if (pts.at(r).c < 0.5 * M_PI) {
2018 h[0] += 1.f;
2019 } else if (pts.at(r).c > 1.5 * M_PI) {
2020 h[1] += 1.f;
2021 }
2022 }
2023 h[0] /= float(pts.size());
2024 h[1] /= float(pts.size());
2025 if (h[0] + h[1] > 0.4) {
2026 if (printmessages) {
2027 std::cout << "Shifting scan " << s << std::endl;
2028 }
2029 for (int r = 0; r < pts.size(); r++) {
2030 pts.at(r).c += M_PI;
2031 if (pts.at(r).c > 2.f * M_PI) {
2032 pts.at(r).c -= 2.f * M_PI;
2033 }
2034 }
2035 }
2036
2037 // Snap coordinates to fixed precision for cross-platform consistency
2038 // This eliminates tiny floating-point differences that cause algorithmic divergence
2039 // Using 1e-6 provides good balance between precision and robustness
2040 const float COORD_SNAP_PRECISION = 1e-6f;
2041 for (auto& pt : pts) {
2042 pt.r = std::round(pt.r / COORD_SNAP_PRECISION) * COORD_SNAP_PRECISION;
2043 pt.c = std::round(pt.c / COORD_SNAP_PRECISION) * COORD_SNAP_PRECISION;
2044 }
2045
2046 std::vector<int> dupes;
2047 int nx = de_duplicate(pts, dupes);
2048 pts_copy = pts;
2049
2050 std::vector<Triad> triads;
2051
2052 if (printmessages) {
2053 std::cout << "starting triangulation for scan " << s << "..." << std::endl;
2054 }
2055
2056 int success = 0;
2057 int Ntries = 0;
2058 while (success != 1 && Ntries < 3) {
2059 Ntries++;
2060
2061 success = s_hull_pro(pts, triads);
2062
2063 if (success != 1) {
2064
2065 // try a 90 degree coordinate shift
2066 if (printmessages) {
2067 std::cout << "Shifting scan " << s << " (try " << Ntries << " of 3)" << std::endl;
2068 }
2069 for (int r = 0; r < pts.size(); r++) {
2070 pts.at(r).c += 0.25 * M_PI;
2071 if (pts.at(r).c > 2.f * M_PI) {
2072 pts.at(r).c -= 2.f * M_PI;
2073 }
2074 }
2075 }
2076 }
2077
2078 if (success != 1) {
2079 if (printmessages) {
2080 std::cout << "FAILED: could not triangulate scan " << s << ". Skipping this scan." << std::endl;
2081 }
2082 continue;
2083 } else if (printmessages) {
2084 std::cout << "finished triangulation" << std::endl;
2085 }
2086
2087 for (int t = 0; t < triads.size(); t++) {
2088
2089 int ID0 = Delaunay_inds.at(triads.at(t).a);
2090 int ID1 = Delaunay_inds.at(triads.at(t).b);
2091 int ID2 = Delaunay_inds.at(triads.at(t).c);
2092
2093 helios::vec3 vertex0 = getHitXYZ(ID0);
2094 helios::SphericalCoord raydir0 = getHitRaydir(ID0);
2095
2096 helios::vec3 vertex1 = getHitXYZ(ID1);
2097 helios::SphericalCoord raydir1 = getHitRaydir(ID1);
2098
2099 helios::vec3 vertex2 = getHitXYZ(ID2);
2100 helios::SphericalCoord raydir2 = getHitRaydir(ID2);
2101
2102 helios::vec3 v;
2103 v = vertex0 - vertex1;
2104 float L0 = v.magnitude();
2105 v = vertex0 - vertex2;
2106 float L1 = v.magnitude();
2107 v = vertex1 - vertex2;
2108 float L2 = v.magnitude();
2109
2110 float aspect_ratio = max(max(L0, L1), L2) / min(min(L0, L1), L2);
2111
2112 // Apply adaptive filtering for multi-return data
2113 bool filtered = (L0 > Lmax || L1 > Lmax || L2 > Lmax);
2114
2115 if (use_adaptive_threshold) {
2116 // Multi-return: use BOTH separation ratio filter AND aspect ratio filter
2117 float ang01 = sqrt(pow(raydir0.zenith - raydir1.zenith, 2) + pow(raydir0.azimuth - raydir1.azimuth, 2));
2118 float ang02 = sqrt(pow(raydir0.zenith - raydir2.zenith, 2) + pow(raydir0.azimuth - raydir2.azimuth, 2));
2119 float ang12 = sqrt(pow(raydir1.zenith - raydir2.zenith, 2) + pow(raydir1.azimuth - raydir2.azimuth, 2));
2120
2121 float ratio01 = L0 / (ang01 + 1e-6);
2122 float ratio02 = L1 / (ang02 + 1e-6);
2123 float ratio12 = L2 / (ang12 + 1e-6);
2124 float max_sep_ratio = max(max(ratio01, ratio02), ratio12);
2125
2126 filtered = filtered || (max_sep_ratio > adaptive_sep_threshold) || (aspect_ratio > max_aspect_ratio);
2127 } else {
2128 // Single-return: use aspect ratio filter
2129 filtered = filtered || (aspect_ratio > max_aspect_ratio);
2130 }
2131
2132 if (filtered) {
2133 continue;
2134 }
2135
2136 int gridcell = getHitGridCell(ID0);
2137
2138 if (printmessages && gridcell == -2) {
2139 cout << "WARNING (triangulateHitPoints): You typically want to define the hit grid cell for all hit points before performing triangulation." << endl;
2140 }
2141
2142 RGBcolor color = make_RGBcolor(0, 0, 0);
2143 color.r = (hits.at(ID0).color.r + hits.at(ID1).color.r + hits.at(ID2).color.r) / 3.f;
2144 color.g = (hits.at(ID0).color.g + hits.at(ID1).color.g + hits.at(ID2).color.g) / 3.f;
2145 color.b = (hits.at(ID0).color.b + hits.at(ID1).color.b + hits.at(ID2).color.b) / 3.f;
2146
2147 Triangulation tri(s, vertex0, vertex1, vertex2, ID0, ID1, ID2, color, gridcell);
2148
2149 if (tri.area != tri.area) {
2150 continue;
2151 }
2152
2153 triangles.push_back(tri);
2154
2155 Ntriangles++;
2156 }
2157 }
2158
2159 triangulationcomputed = true;
2160
2161 if (printmessages) {
2162 cout << "\r ";
2163 cout << "\rTriangulating...formed " << Ntriangles << " total triangles." << endl;
2164 }
2165}
2166
2167void LiDARcloud::triangulateHitPoints(float Lmax, float max_aspect_ratio, const char *scalar_field, float threshold, const char *comparator) {
2168
2169 if (printmessages && getScanCount() == 0) {
2170 cout << "WARNING (triangulateHitPoints): No scans have been added to the point cloud. Skipping triangulation..." << endl;
2171 return;
2172 } else if (printmessages && getHitCount() == 0) {
2173 cout << "WARNING (triangulateHitPoints): No hit points have been added to the point cloud. Skipping triangulation..." << endl;
2174 return;
2175 }
2176
2177 if (!hitgridcellcomputed) {
2179 }
2180
2181 int Ntriangles = 0;
2182
2183 // For multi-return data, calculate adaptive separation ratio threshold
2184 bool use_adaptive_threshold = isMultiReturnData();
2185 float adaptive_sep_threshold = 0.0f;
2186
2187 if (use_adaptive_threshold) {
2188 if (printmessages) {
2189 std::cout << "Multi-return data detected - calculating adaptive separation ratio threshold..." << std::endl;
2190 }
2191
2192 // First pass: collect separation ratios from all potential triangles
2193 std::vector<float> all_separation_ratios;
2194
2195 for (uint s = 0; s < getScanCount(); s++) {
2196 std::vector<int> Delaunay_inds_pass1;
2197 std::vector<Shx> pts_pass1, pts_copy_pass1;
2198 int count_pass1 = 0;
2199
2200 for (int r = 0; r < getHitCount(); r++) {
2201 if (getHitScanID(r) == s && getHitGridCell(r) >= 0) {
2202 helios::SphericalCoord direction = getHitRaydir(r);
2203 helios::vec3 direction_cart = getHitXYZ(r) - getScanOrigin(s);
2204 direction = cart2sphere(direction_cart);
2205
2206 Shx pt;
2207 pt.id = count_pass1;
2208 pt.r = direction.zenith;
2209 pt.c = direction.azimuth;
2210 pts_pass1.push_back(pt);
2211 Delaunay_inds_pass1.push_back(r);
2212 count_pass1++;
2213 }
2214 }
2215
2216 if (pts_pass1.size() == 0) continue;
2217
2218 // Handle coordinate wrapping
2219 float h[2] = {0, 0};
2220 for (int r = 0; r < pts_pass1.size(); r++) {
2221 if (pts_pass1.at(r).c < 0.5 * M_PI) h[0] += 1.f;
2222 else if (pts_pass1.at(r).c > 1.5 * M_PI) h[1] += 1.f;
2223 }
2224 h[0] /= float(pts_pass1.size());
2225 h[1] /= float(pts_pass1.size());
2226 if (h[0] + h[1] > 0.4) {
2227 for (int r = 0; r < pts_pass1.size(); r++) {
2228 pts_pass1.at(r).c += M_PI;
2229 if (pts_pass1.at(r).c > 2.f * M_PI) pts_pass1.at(r).c -= 2.f * M_PI;
2230 }
2231 }
2232
2233 std::vector<int> dupes_pass1;
2234 de_duplicate(pts_pass1, dupes_pass1);
2235
2236 std::vector<Triad> triads_pass1;
2237 int success = 0, Ntries = 0;
2238 while (success != 1 && Ntries < 3) {
2239 Ntries++;
2240 success = s_hull_pro(pts_pass1, triads_pass1);
2241 if (success != 1) {
2242 for (int r = 0; r < pts_pass1.size(); r++) {
2243 pts_pass1.at(r).c += 0.25 * M_PI;
2244 if (pts_pass1.at(r).c > 2.f * M_PI) pts_pass1.at(r).c -= 2.f * M_PI;
2245 }
2246 }
2247 }
2248
2249 if (success != 1) continue;
2250
2251 // Collect separation ratios (pre-filter by edge length)
2252 for (int t = 0; t < triads_pass1.size(); t++) {
2253 int ID0 = Delaunay_inds_pass1.at(triads_pass1.at(t).a);
2254 int ID1 = Delaunay_inds_pass1.at(triads_pass1.at(t).b);
2255 int ID2 = Delaunay_inds_pass1.at(triads_pass1.at(t).c);
2256
2257 helios::vec3 v0 = getHitXYZ(ID0);
2258 helios::vec3 v1 = getHitXYZ(ID1);
2259 helios::vec3 v2 = getHitXYZ(ID2);
2263
2264 float L0 = (v0 - v1).magnitude();
2265 float L1 = (v0 - v2).magnitude();
2266 float L2 = (v1 - v2).magnitude();
2267
2268 // Skip triangles that fail edge length filter
2269 if (L0 > Lmax || L1 > Lmax || L2 > Lmax) {
2270 continue;
2271 }
2272
2273 float ang01 = sqrt(pow(r0.zenith - r1.zenith, 2) + pow(r0.azimuth - r1.azimuth, 2));
2274 float ang02 = sqrt(pow(r0.zenith - r2.zenith, 2) + pow(r0.azimuth - r2.azimuth, 2));
2275 float ang12 = sqrt(pow(r1.zenith - r2.zenith, 2) + pow(r1.azimuth - r2.azimuth, 2));
2276
2277 float ratio01 = L0 / (ang01 + 1e-6);
2278 float ratio02 = L1 / (ang02 + 1e-6);
2279 float ratio12 = L2 / (ang12 + 1e-6);
2280 float max_sep_ratio = max(max(ratio01, ratio02), ratio12);
2281
2282 all_separation_ratios.push_back(max_sep_ratio);
2283 }
2284 }
2285
2286 // Calculate 25th percentile and set adaptive threshold
2287 if (!all_separation_ratios.empty()) {
2288 std::sort(all_separation_ratios.begin(), all_separation_ratios.end());
2289 size_t idx_25 = all_separation_ratios.size() / 4;
2290 float percentile_25 = all_separation_ratios[idx_25];
2291 adaptive_sep_threshold = 8.5f * percentile_25;
2292
2293 if (printmessages) {
2294 std::cout << " 25th percentile separation ratio: " << percentile_25 << std::endl;
2295 std::cout << " Adaptive threshold: " << adaptive_sep_threshold << std::endl;
2296 }
2297 }
2298 }
2299
2300 // Second pass: perform triangulation with adaptive filtering
2301 for (uint s = 0; s < getScanCount(); s++) {
2302
2303 std::vector<int> Delaunay_inds;
2304
2305 std::vector<Shx> pts, pts_copy;
2306
2307 std::size_t delete_count = 0;
2308 int count = 0;
2309
2310 for (int r = 0; r < getHitCount(); r++) {
2311
2312
2313 if (getHitScanID(r) == s && getHitGridCell(r) >= 0) {
2314
2315 if (hits.at(r).data.find(scalar_field) != hits.at(r).data.end()) {
2316 double R = getHitData(r, scalar_field);
2317 if (strcmp(comparator, "<") == 0) {
2318 if (R < threshold) {
2319 delete_count++;
2320 continue;
2321 }
2322 } else if (strcmp(comparator, ">") == 0) {
2323 if (R > threshold) {
2324 delete_count++;
2325 continue;
2326 }
2327 } else if (strcmp(comparator, "=") == 0) {
2328 if (R == threshold) {
2329
2330 delete_count++;
2331 continue;
2332 }
2333 }
2334 }
2335
2336 helios::SphericalCoord direction = getHitRaydir(r);
2337
2338 helios::vec3 direction_cart = getHitXYZ(r) - getScanOrigin(s);
2339 direction = cart2sphere(direction_cart);
2340
2341 Shx pt;
2342 pt.id = count;
2343 pt.r = direction.zenith;
2344 pt.c = direction.azimuth;
2345
2346 pts.push_back(pt);
2347
2348 Delaunay_inds.push_back(r);
2349
2350 count++;
2351 }
2352 }
2353
2354 if (printmessages) {
2355 std::cout << "Scan " << s << " triangulation: " << count << " points used, "
2356 << delete_count << " points filtered out";
2357 if (strlen(scalar_field) > 0) {
2358 std::cout << " (filter: " << scalar_field << " " << comparator << " " << threshold << ")";
2359 }
2360 std::cout << std::endl;
2361 }
2362
2363 if (pts.size() == 0) {
2364 if (printmessages) {
2365 std::cout << "Scan " << s << " contains no triangles. Skipping this scan..." << std::endl;
2366 }
2367 continue;
2368 }
2369
2370 float h[2] = {0, 0};
2371 for (int r = 0; r < pts.size(); r++) {
2372 if (pts.at(r).c < 0.5 * M_PI) {
2373 h[0] += 1.f;
2374 } else if (pts.at(r).c > 1.5 * M_PI) {
2375 h[1] += 1.f;
2376 }
2377 }
2378 h[0] /= float(pts.size());
2379 h[1] /= float(pts.size());
2380 if (h[0] + h[1] > 0.4) {
2381 if (printmessages) {
2382 std::cout << "Shifting scan " << s << std::endl;
2383 }
2384 for (int r = 0; r < pts.size(); r++) {
2385 pts.at(r).c += M_PI;
2386 if (pts.at(r).c > 2.f * M_PI) {
2387 pts.at(r).c -= 2.f * M_PI;
2388 }
2389 }
2390 }
2391
2392 // Snap coordinates to fixed precision for cross-platform consistency
2393 // This eliminates tiny floating-point differences that cause algorithmic divergence
2394 // Using 1e-6 provides good balance between precision and robustness
2395 const float COORD_SNAP_PRECISION = 1e-6f;
2396 for (auto& pt : pts) {
2397 pt.r = std::round(pt.r / COORD_SNAP_PRECISION) * COORD_SNAP_PRECISION;
2398 pt.c = std::round(pt.c / COORD_SNAP_PRECISION) * COORD_SNAP_PRECISION;
2399 }
2400
2401 std::vector<int> dupes;
2402 int nx = de_duplicate(pts, dupes);
2403 pts_copy = pts;
2404
2405 std::vector<Triad> triads;
2406
2407 if (printmessages) {
2408 std::cout << "starting triangulation for scan " << s << "..." << std::endl;
2409 }
2410
2411 int success = 0;
2412 int Ntries = 0;
2413 while (success != 1 && Ntries < 3) {
2414 Ntries++;
2415
2416 success = s_hull_pro(pts, triads);
2417
2418 if (success != 1) {
2419
2420 // try a 90 degree coordinate shift
2421 if (printmessages) {
2422 std::cout << "Shifting scan " << s << " (try " << Ntries << " of 3)" << std::endl;
2423 }
2424 for (int r = 0; r < pts.size(); r++) {
2425 pts.at(r).c += 0.25 * M_PI;
2426 if (pts.at(r).c > 2.f * M_PI) {
2427 pts.at(r).c -= 2.f * M_PI;
2428 }
2429 }
2430 }
2431 }
2432
2433 if (success != 1) {
2434 if (printmessages) {
2435 std::cout << "FAILED: could not triangulate scan " << s << ". Skipping this scan." << std::endl;
2436 }
2437 continue;
2438 } else if (printmessages) {
2439 std::cout << "finished triangulation" << std::endl;
2440 }
2441
2442 for (int t = 0; t < triads.size(); t++) {
2443
2444 int ID0 = Delaunay_inds.at(triads.at(t).a);
2445 int ID1 = Delaunay_inds.at(triads.at(t).b);
2446 int ID2 = Delaunay_inds.at(triads.at(t).c);
2447
2448 helios::vec3 vertex0 = getHitXYZ(ID0);
2449 helios::SphericalCoord raydir0 = getHitRaydir(ID0);
2450
2451 helios::vec3 vertex1 = getHitXYZ(ID1);
2452 helios::SphericalCoord raydir1 = getHitRaydir(ID1);
2453
2454 helios::vec3 vertex2 = getHitXYZ(ID2);
2455 helios::SphericalCoord raydir2 = getHitRaydir(ID2);
2456
2457 helios::vec3 v;
2458 v = vertex0 - vertex1;
2459 float L0 = v.magnitude();
2460 v = vertex0 - vertex2;
2461 float L1 = v.magnitude();
2462 v = vertex1 - vertex2;
2463 float L2 = v.magnitude();
2464
2465 float aspect_ratio = max(max(L0, L1), L2) / min(min(L0, L1), L2);
2466
2467 // Apply adaptive filtering for multi-return data
2468 bool filtered = (L0 > Lmax || L1 > Lmax || L2 > Lmax);
2469
2470 if (use_adaptive_threshold) {
2471 // Multi-return: use BOTH separation ratio filter AND aspect ratio filter
2472 float ang01 = sqrt(pow(raydir0.zenith - raydir1.zenith, 2) + pow(raydir0.azimuth - raydir1.azimuth, 2));
2473 float ang02 = sqrt(pow(raydir0.zenith - raydir2.zenith, 2) + pow(raydir0.azimuth - raydir2.azimuth, 2));
2474 float ang12 = sqrt(pow(raydir1.zenith - raydir2.zenith, 2) + pow(raydir1.azimuth - raydir2.azimuth, 2));
2475
2476 float ratio01 = L0 / (ang01 + 1e-6);
2477 float ratio02 = L1 / (ang02 + 1e-6);
2478 float ratio12 = L2 / (ang12 + 1e-6);
2479 float max_sep_ratio = max(max(ratio01, ratio02), ratio12);
2480
2481 filtered = filtered || (max_sep_ratio > adaptive_sep_threshold) || (aspect_ratio > max_aspect_ratio);
2482 } else {
2483 // Single-return: use aspect ratio filter
2484 filtered = filtered || (aspect_ratio > max_aspect_ratio);
2485 }
2486
2487 if (filtered) {
2488 continue;
2489 }
2490
2491 int gridcell = getHitGridCell(ID0);
2492
2493 if (printmessages && gridcell == -2) {
2494 cout << "WARNING (triangulateHitPoints): You typically want to define the hit grid cell for all hit points before performing triangulation." << endl;
2495 }
2496
2497 RGBcolor color = make_RGBcolor(0, 0, 0);
2498 color.r = (hits.at(ID0).color.r + hits.at(ID1).color.r + hits.at(ID2).color.r) / 3.f;
2499 color.g = (hits.at(ID0).color.g + hits.at(ID1).color.g + hits.at(ID2).color.g) / 3.f;
2500 color.b = (hits.at(ID0).color.b + hits.at(ID1).color.b + hits.at(ID2).color.b) / 3.f;
2501
2502 Triangulation tri(s, vertex0, vertex1, vertex2, ID0, ID1, ID2, color, gridcell);
2503
2504 if (tri.area != tri.area) {
2505 continue;
2506 }
2507
2508 triangles.push_back(tri);
2509
2510 Ntriangles++;
2511 }
2512 }
2513
2514 triangulationcomputed = true;
2515
2516 if (printmessages) {
2517 cout << "\r ";
2518 cout << "\rTriangulating...formed " << Ntriangles << " total triangles." << endl;
2519 }
2520}
2521
2522
2524
2525 if (scans.size() == 0) {
2526 if (printmessages) {
2527 std::cout << "WARNING (addTrianglesToContext): There are no scans in the point cloud, and thus there are no triangles to add...skipping." << std::endl;
2528 }
2529 return;
2530 }
2531
2532 for (std::size_t i = 0; i < getTriangleCount(); i++) {
2533
2534 Triangulation tri = getTriangle(i);
2535
2536 context->addTriangle(tri.vertex0, tri.vertex1, tri.vertex2, tri.color);
2537 }
2538}
2539
2541 return grid_cells.size();
2542}
2543
2544void LiDARcloud::addGridCell(const vec3 &center, const vec3 &size, float rotation) {
2545 addGridCell(center, center, size, size, rotation, make_int3(1, 1, 1), make_int3(1, 1, 1));
2546}
2547
2548void LiDARcloud::addGridCell(const vec3 &center, const vec3 &global_anchor, const vec3 &size, const vec3 &global_size, float rotation, const int3 &global_ijk, const int3 &global_count) {
2549
2550 GridCell newcell(center, global_anchor, size, global_size, rotation, global_ijk, global_count);
2551
2552 grid_cells.push_back(newcell);
2553}
2554
2556
2557 if (index >= getGridCellCount()) {
2558 helios_runtime_error("ERROR (LiDARcloud::getCellCenter): grid cell index out of range. Requested center of cell #" + std::to_string(index) + " but there are only " + std::to_string(getGridCellCount()) + " cells in the grid.");
2559 }
2560
2561 return grid_cells.at(index).center;
2562}
2563
2565
2566 if (index >= getGridCellCount()) {
2567 helios_runtime_error("ERROR (LiDARcloud::getCellGlobalAnchor): grid cell index out of range. Requested anchor of cell #" + std::to_string(index) + " but there are only " + std::to_string(getGridCellCount()) + " cells in the grid.");
2568 }
2569
2570 return grid_cells.at(index).global_anchor;
2571}
2572
2574
2575 if (index >= getGridCellCount()) {
2576 helios_runtime_error("ERROR (LiDARcloud::getCellCenter): grid cell index out of range. Requested size of cell #" + std::to_string(index) + " but there are only " + std::to_string(getGridCellCount()) + " cells in the grid.");
2577 }
2578
2579 return grid_cells.at(index).size;
2580}
2581
2583
2584 if (index >= getGridCellCount()) {
2585 helios_runtime_error("ERROR (LiDARcloud::getCellRotation): grid cell index out of range. Requested rotation of cell #" + std::to_string(index) + " but there are only " + std::to_string(getGridCellCount()) + " cells in the grid.");
2586 }
2587
2588 return grid_cells.at(index).azimuthal_rotation;
2589}
2590
2592
2593 size_t Nprims = context->getPrimitiveCount();
2594
2595 uint Nscans = getScanCount();
2596
2597 uint Ncells = getGridCellCount();
2598
2599 std::vector<float> Gtheta;
2600 Gtheta.resize(Ncells);
2601
2602 std::vector<float> area_sum;
2603 area_sum.resize(Ncells, 0.f);
2604 std::vector<uint> cell_tri_count;
2605 cell_tri_count.resize(Ncells, 0);
2606
2607 std::vector<uint> UUIDs = context->getAllUUIDs();
2608 for (int p = 0; p < UUIDs.size(); p++) {
2609
2610 uint UUID = UUIDs.at(p);
2611
2612 if (context->doesPrimitiveDataExist(UUID, "gridCell")) {
2613
2614 uint gridCell;
2615 context->getPrimitiveData(UUID, "gridCell", gridCell);
2616
2617 std::vector<vec3> vertices = context->getPrimitiveVertices(UUID);
2618 float area = context->getPrimitiveArea(UUID);
2619 vec3 normal = context->getPrimitiveNormal(UUID);
2620
2621 for (int s = 0; s < Nscans; s++) {
2622 vec3 origin = getScanOrigin(s);
2623 vec3 raydir = vertices.front() - origin;
2624 raydir.normalize();
2625
2626 if (area == area) { // in rare cases you can get area=NaN
2627
2628 Gtheta.at(gridCell) += fabs(normal * raydir) * area;
2629
2630 area_sum.at(gridCell) += area;
2631 cell_tri_count.at(gridCell) += 1;
2632 }
2633 }
2634 }
2635 }
2636
2637 for (uint v = 0; v < Ncells; v++) {
2638 if (cell_tri_count[v] > 0) {
2639 Gtheta[v] *= float(cell_tri_count[v]) / (area_sum[v]);
2640 }
2641 }
2642
2643
2644 std::vector<float> output_Gtheta;
2645 output_Gtheta.resize(Ncells, 0.f);
2646
2647 for (int v = 0; v < Ncells; v++) {
2648 output_Gtheta.at(v) = Gtheta.at(v);
2649 if (context->doesPrimitiveDataExist(UUIDs.at(v), "gridCell")) {
2650 context->setPrimitiveData(UUIDs.at(v), "synthetic_Gtheta", Gtheta.at(v));
2651 }
2652 }
2653
2654 return output_Gtheta;
2655}
2656
2657void LiDARcloud::setCellLeafArea(float area, uint index) {
2658
2659 if (index > getGridCellCount()) {
2660 helios_runtime_error("ERROR (LiDARcloud::setCellLeafArea): grid cell index out of range.");
2661 }
2662
2663 grid_cells.at(index).leaf_area = area;
2664}
2665
2667
2668 if (index >= getGridCellCount()) {
2669 helios_runtime_error("ERROR (LiDARcloud::getCellLeafArea): grid cell index out of range. Requested leaf area of cell #" + std::to_string(index) + " but there are only " + std::to_string(getGridCellCount()) + " cells in the grid.");
2670 }
2671
2672 return grid_cells.at(index).leaf_area;
2673}
2674
2676
2677 if (index >= getGridCellCount()) {
2678 helios_runtime_error("ERROR (LiDARcloud::getCellLeafAreaDensity): grid cell index out of range. Requested leaf area density of cell #" + std::to_string(index) + " but there are only " + std::to_string(getGridCellCount()) +
2679 " cells in the grid.");
2680 }
2681
2682 helios::vec3 gridsize = grid_cells.at(index).size;
2683 return grid_cells.at(index).leaf_area / (gridsize.x * gridsize.y * gridsize.z);
2684}
2685
2686void LiDARcloud::setCellGtheta(float Gtheta, uint index) {
2687
2688 if (index > getGridCellCount()) {
2689 helios_runtime_error("ERROR (LiDARcloud::setCellGtheta): grid cell index out of range.");
2690 }
2691
2692 grid_cells.at(index).Gtheta = Gtheta;
2693}
2694
2696
2697 if (index >= getGridCellCount()) {
2698 helios_runtime_error("ERROR (LiDARcloud::getCellGtheta): grid cell index out of range. Requested leaf area of cell #" + std::to_string(index) + " but there are only " + std::to_string(getGridCellCount()) + " cells in the grid.");
2699 }
2700
2701 return grid_cells.at(index).Gtheta;
2702}
2703
2704void LiDARcloud::leafReconstructionFloodfill() {
2705
2706 size_t group_count = 0;
2707 int current_group = 0;
2708
2709 vector<vector<int>> nodes;
2710 nodes.resize(getHitCount());
2711
2712 size_t Ntri = 0;
2713 for (size_t t = 0; t < getTriangleCount(); t++) {
2714
2715 Triangulation tri = getTriangle(t);
2716
2717 if (tri.gridcell >= 0) {
2718
2719 nodes.at(tri.ID0).push_back(t);
2720 nodes.at(tri.ID1).push_back(t);
2721 nodes.at(tri.ID2).push_back(t);
2722
2723 Ntri++;
2724 }
2725 }
2726
2727 std::vector<int> fill_flag;
2728 fill_flag.resize(Ntri);
2729 for (size_t t = 0; t < Ntri; t++) {
2730 fill_flag.at(t) = -1;
2731 }
2732
2733 for (size_t t = 0; t < Ntri; t++) { // looping through all triangles
2734
2735 if (fill_flag.at(t) < 0) {
2736
2737 floodfill(t, triangles, fill_flag, nodes, current_group, 0, 1e3);
2738
2739 current_group++;
2740 }
2741 }
2742
2743 for (size_t t = 0; t < Ntri; t++) { // looping through all triangles
2744
2745 if (fill_flag.at(t) >= 0) {
2746 int fill_group = fill_flag.at(t);
2747
2748 if (fill_group >= reconstructed_triangles.size()) {
2749 reconstructed_triangles.resize(fill_group + 1);
2750 }
2751
2752 reconstructed_triangles.at(fill_group).push_back(triangles.at(t));
2753 }
2754 }
2755}
2756
2757void LiDARcloud::floodfill(size_t t, std::vector<Triangulation> &cloud_triangles, std::vector<int> &fill_flag, std::vector<std::vector<int>> &nodes, int tag, int depth, int maxdepth) {
2758
2759 Triangulation tri = cloud_triangles.at(t);
2760
2761 int verts[3] = {tri.ID0, tri.ID1, tri.ID2};
2762
2763 std::vector<int> connection_list;
2764
2765 for (int i = 0; i < 3; i++) {
2766 std::vector<int> connected_tris = nodes.at(verts[i]);
2767 connection_list.insert(connection_list.begin(), connected_tris.begin(), connected_tris.end());
2768 }
2769
2770 std::sort(connection_list.begin(), connection_list.end());
2771
2772 int count = 0;
2773 for (int tt = 1; tt < connection_list.size(); tt++) {
2774 if (connection_list.at(tt - 1) != connection_list.at(tt)) {
2775
2776 if (count >= 2) {
2777
2778 int index = connection_list.at(tt - 1);
2779
2780 if (fill_flag.at(index) == -1 && index != t) {
2781
2782 fill_flag.at(index) = tag;
2783
2784 if (depth < maxdepth) {
2785 floodfill(index, cloud_triangles, fill_flag, nodes, tag, depth + 1, maxdepth);
2786 }
2787 }
2788 }
2789
2790 count = 1;
2791 } else {
2792 count++;
2793 }
2794 }
2795}
2796
2797void LiDARcloud::leafReconstructionAlphaMask(float minimum_leaf_group_area, float maximum_leaf_group_area, float leaf_aspect_ratio, const char *mask_file) {
2798 leafReconstructionAlphaMask(minimum_leaf_group_area, maximum_leaf_group_area, leaf_aspect_ratio, -1.f, mask_file);
2799}
2800
2801void LiDARcloud::leafReconstructionAlphaMask(float minimum_leaf_group_area, float maximum_leaf_group_area, float leaf_aspect_ratio, float leaf_length_constant, const char *mask_file) {
2802
2803 if (printmessages) {
2804 cout << "Performing alphamask leaf reconstruction..." << flush;
2805 }
2806
2807 if (triangles.size() == 0) {
2808 std::cout << "failed." << std::endl;
2809 helios_runtime_error("ERROR (LiDARcloud::leafReconstructionAlphamask): There are no triangulated points. Either the triangulation failed or 'triangulateHitPoints()' was not called.");
2810 }
2811
2812 std::string file = mask_file;
2813 if (file.substr(file.find_last_of(".") + 1) != "png") {
2814 std::cout << "failed." << std::endl;
2815 helios_runtime_error("ERROR (LiDARcloud::leafReconstructionAlphaMask): Mask data file " + std::string(mask_file) + " must be PNG image format.");
2816 }
2817 std::vector<std::vector<bool>> maskdata = readPNGAlpha(mask_file);
2818 if (maskdata.size() == 0) {
2819 std::cout << "failed." << std::endl;
2820 helios_runtime_error("ERROR (LiDARcloud::leafReconstructionAlphaMask): Could not load mask file " + std::string(mask_file) + ". It contains no data.");
2821 }
2822 int ix = maskdata.front().size();
2823 int jy = maskdata.size();
2824 int2 masksize = make_int2(ix, jy);
2825 uint Atotal = 0;
2826 uint Asolid = 0;
2827 for (uint j = 0; j < masksize.y; j++) {
2828 for (uint i = 0; i < masksize.x; i++) {
2829 Atotal++;
2830 if (maskdata.at(j).at(i)) {
2831 Asolid++;
2832 }
2833 }
2834 }
2835
2836 float solidfraction = float(Asolid) / float(Atotal);
2837
2838 float total_area = 0.f;
2839
2840 std::vector<std::vector<float>> group_areas;
2841 group_areas.resize(getGridCellCount());
2842
2843 reconstructed_alphamasks_maskfile = mask_file;
2844
2845 leafReconstructionFloodfill();
2846
2847 // Filter out small groups by an area threshold
2848
2849 uint group_count = reconstructed_triangles.size();
2850
2851 float group_area_max = 0;
2852
2853 std::vector<bool> group_filter_flag;
2854 group_filter_flag.resize(reconstructed_triangles.size());
2855
2856 for (int group = group_count - 1; group >= 0; group--) {
2857
2858 float garea = 0.f;
2859
2860 for (size_t t = 0; t < reconstructed_triangles.at(group).size(); t++) {
2861
2862 float triangle_area = reconstructed_triangles.at(group).at(t).area;
2863
2864 garea += triangle_area;
2865 }
2866
2867 if (garea < minimum_leaf_group_area || garea > maximum_leaf_group_area) {
2868 group_filter_flag.at(group) = false;
2869 // reconstructed_triangles.erase( reconstructed_triangles.begin()+group );
2870 } else {
2871 group_filter_flag.at(group) = true;
2872 int cell = reconstructed_triangles.at(group).front().gridcell;
2873 group_areas.at(cell).push_back(garea);
2874 }
2875 }
2876
2877 vector<float> Lavg;
2878 Lavg.resize(getGridCellCount(), 0.f);
2879
2880 int Navg = 20;
2881
2882 for (int v = 0; v < getGridCellCount(); v++) {
2883
2884 std::sort(group_areas.at(v).begin(), group_areas.at(v).end());
2885 // std::partial_sort( group_areas.at(v).begin(), group_areas.at(v).begin()+Navg,group_areas.at(v).end(), std::greater<float>() );
2886
2887 if (group_areas.at(v).size() > Navg) {
2888 for (int i = group_areas.at(v).size() - 1; i >= group_areas.at(v).size() - Navg; i--) {
2889 Lavg.at(v) += sqrtf(group_areas.at(v).at(i)) / float(Navg);
2890 }
2891 } else if (group_areas.at(v).size() == 0) {
2892 Lavg.at(v) = 0.05; // NOTE: hard-coded
2893 } else {
2894 for (int i = 0; i < group_areas.at(v).size(); i++) {
2895 Lavg.at(v) += sqrtf(group_areas.at(v).at(i)) / float(group_areas.at(v).size());
2896 }
2897 }
2898
2899 if (printmessages) {
2900 std::cout << "Average leaf length for volume #" << v << " : " << Lavg.at(v) << endl;
2901 }
2902 }
2903
2904 // Form alphamasks
2905
2906 for (int group = 0; group < reconstructed_triangles.size(); group++) {
2907
2908 if (!group_filter_flag.at(group)) {
2909 continue;
2910 }
2911
2912 int cell = reconstructed_triangles.at(group).front().gridcell;
2913
2914 helios::vec3 position = make_vec3(0, 0, 0);
2915 for (int t = 0; t < reconstructed_triangles.at(group).size(); t++) {
2916 position = position + reconstructed_triangles.at(group).at(t).vertex0 / float(reconstructed_triangles.at(group).size());
2917 }
2918
2919 int gind = round(randu() * (reconstructed_triangles.at(group).size() - 1));
2920
2921 reconstructed_alphamasks_center.push_back(position);
2922 float l = Lavg.at(reconstructed_triangles.at(group).front().gridcell) * sqrt(leaf_aspect_ratio / solidfraction);
2923 float w = l / leaf_aspect_ratio;
2924 reconstructed_alphamasks_size.push_back(helios::make_vec2(w, l));
2925 helios::vec3 normal = cross(reconstructed_triangles.at(group).at(gind).vertex1 - reconstructed_triangles.at(group).at(gind).vertex0, reconstructed_triangles.at(group).at(gind).vertex2 - reconstructed_triangles.at(group).at(gind).vertex0);
2926 reconstructed_alphamasks_rotation.push_back(make_SphericalCoord(cart2sphere(normal).zenith, cart2sphere(normal).azimuth));
2927 reconstructed_alphamasks_gridcell.push_back(reconstructed_triangles.at(group).front().gridcell);
2928 reconstructed_alphamasks_direct_flag.push_back(1);
2929 }
2930
2931 if (printmessages) {
2932 cout << "done." << endl;
2933 cout << "Directly reconstructed " << reconstructed_alphamasks_center.size() << " leaf groups." << endl;
2934 }
2935
2936 backfillLeavesAlphaMask(Lavg, leaf_aspect_ratio, solidfraction, group_filter_flag);
2937
2938 for (int group = 0; group < reconstructed_triangles.size(); group++) {
2939
2940 if (!group_filter_flag.at(group)) {
2941 std::swap(reconstructed_triangles.at(group), reconstructed_triangles.back());
2942 reconstructed_triangles.pop_back();
2943 }
2944 }
2945
2946 // reconstructed_triangles.resize(0);
2947}
2948
2949
2950void LiDARcloud::backfillLeavesAlphaMask(const vector<float> &leaf_size, float leaf_aspect_ratio, float solidfraction, const vector<bool> &group_filter_flag) {
2951
2952 if (printmessages) {
2953 cout << "Backfilling leaves..." << endl;
2954 }
2955
2956 unsigned seed = std::chrono::system_clock::now().time_since_epoch().count();
2957 std::minstd_rand0 generator;
2958 generator.seed(seed);
2959 std::normal_distribution<float> randn;
2960
2961 uint Ngroups = reconstructed_triangles.size();
2962
2963 uint Ncells = getGridCellCount();
2964
2965 std::vector<std::vector<uint>> group_gridcell;
2966 group_gridcell.resize(Ncells);
2967
2968 // Calculate the current alphamask leaf area for each grid cell
2969 std::vector<float> leaf_area_current;
2970 leaf_area_current.resize(Ncells);
2971
2972 int cell;
2973 int count = 0;
2974 for (uint g = 0; g < Ngroups; g++) {
2975 if (group_filter_flag.at(g)) {
2976 if (reconstructed_triangles.at(g).size() > 0) {
2977 cell = reconstructed_triangles.at(g).front().gridcell;
2978 leaf_area_current.at(cell) += leaf_size.at(cell) * leaf_size.at(cell) * solidfraction;
2979 group_gridcell.at(cell).push_back(count);
2980 }
2981 count++;
2982 }
2983 }
2984
2985 std::vector<int> deleted_groups;
2986 int backfill_count = 0;
2987
2988 // Get the total theoretical leaf area for each grid cell based on LiDAR scan
2989 for (uint v = 0; v < Ncells; v++) {
2990
2991 float leaf_area_total = getCellLeafArea(v);
2992
2993 float reconstruct_frac = (leaf_area_total - leaf_area_current.at(v)) / leaf_area_total;
2994
2995 if (leaf_area_total == 0 || reconstructed_alphamasks_size.size() == 0) { // no leaves in gridcell
2996 if (printmessages) {
2997 cout << "WARNING: skipping volume #" << v << " because it has no measured leaf area." << endl;
2998 }
2999 continue;
3000 } else if (getTriangleCount() == 0) {
3001 if (printmessages) {
3002 cout << "WARNING: skipping volume #" << v << " because it has no triangles." << endl;
3003 }
3004 continue;
3005 } else if (leaf_area_current.at(v) == 0) { // no directly reconstructed leaves in gridcell
3006
3007 std::vector<SphericalCoord> tri_rots;
3008
3009 size_t Ntri = 0;
3010 for (size_t t = 0; t < getTriangleCount(); t++) {
3011 Triangulation tri = getTriangle(t);
3012 if (tri.gridcell == v) {
3013 helios::vec3 normal = cross(tri.vertex1 - tri.vertex0, tri.vertex2 - tri.vertex0);
3014 tri_rots.push_back(make_SphericalCoord(cart2sphere(normal).zenith, cart2sphere(normal).azimuth));
3015 }
3016 }
3017
3018 while (leaf_area_current.at(v) < leaf_area_total) {
3019
3020 int randi = round(randu() * (tri_rots.size() - 1));
3021
3022 helios::vec3 cellsize = getCellSize(v);
3023 helios::vec3 cellcenter = getCellCenter(v);
3024 float rotation = getCellRotation(v);
3025
3026 helios::vec3 shift = cellcenter + rotatePoint(helios::make_vec3((randu() - 0.5) * cellsize.x, (randu() - 0.5) * cellsize.y, (randu() - 0.5) * cellsize.z), 0, rotation);
3027
3028 reconstructed_alphamasks_center.push_back(shift);
3029 reconstructed_alphamasks_size.push_back(reconstructed_alphamasks_size.front());
3030 reconstructed_alphamasks_rotation.push_back(tri_rots.at(randi));
3031 reconstructed_alphamasks_gridcell.push_back(v);
3032 reconstructed_alphamasks_direct_flag.push_back(0);
3033
3034 leaf_area_current.at(v) += reconstructed_alphamasks_size.back().x * reconstructed_alphamasks_size.back().y * solidfraction;
3035 }
3036
3037
3038 } else if (leaf_area_current.at(v) > leaf_area_total) { // too much leaf area in gridcell
3039
3040 while (leaf_area_current.at(v) > leaf_area_total) {
3041
3042 int randi = round(randu() * (group_gridcell.at(v).size() - 1));
3043
3044 int group_index = group_gridcell.at(v).at(randi);
3045
3046 deleted_groups.push_back(group_index);
3047
3048 leaf_area_current.at(v) -= reconstructed_alphamasks_size.at(group_index).x * reconstructed_alphamasks_size.at(group_index).y * solidfraction;
3049 }
3050
3051 } else { // not enough leaf area in gridcell
3052
3053 while (leaf_area_current.at(v) < leaf_area_total) {
3054
3055 int randi = round(randu() * (group_gridcell.at(v).size() - 1));
3056
3057 int group_index = group_gridcell.at(v).at(randi);
3058
3059 helios::vec3 cellsize = getCellSize(v);
3060 helios::vec3 cellcenter = getCellCenter(v);
3061 float rotation = getCellRotation(v);
3062 helios::vec3 cellanchor = getCellGlobalAnchor(v);
3063
3064 // helios::vec3 shift = reconstructed_alphamasks_center.at(group_index) + helios::make_vec3( 0.45*(randu()-0.5)*cellsize.x, 0.45*(randu()-0.5)*cellsize.y, 0.45*(randu()-0.5)*cellsize.z ); //uniform shift about group
3065 helios::vec3 shift = reconstructed_alphamasks_center.at(group_index) + helios::make_vec3(0.25 * randn(generator) * cellsize.x, 0.25 * randn(generator) * cellsize.y, 0.25 * randn(generator) * cellsize.z); // Gaussian shift about group
3066 // helios::vec3 shift = cellcenter + helios::make_vec3( (randu()-0.5)*cellsize.x, (randu()-0.5)*cellsize.y, (randu()-0.5)*cellsize.z ); //uniform shift within voxel
3067 shift = rotatePointAboutLine(shift, cellanchor, make_vec3(0, 0, 1), rotation);
3068
3069 if (group_index >= reconstructed_alphamasks_center.size()) {
3070 helios_runtime_error("FAILED: " + std::to_string(group_index) + " " + std::to_string(reconstructed_alphamasks_center.size()) + " " + std::to_string(randi));
3071 } else if (reconstructed_alphamasks_gridcell.at(group_index) != v) {
3072 helios_runtime_error("FAILED: selected leaf group is not from this grid cell");
3073 }
3074
3075 reconstructed_alphamasks_center.push_back(shift);
3076 reconstructed_alphamasks_size.push_back(reconstructed_alphamasks_size.at(group_index));
3077 reconstructed_alphamasks_rotation.push_back(reconstructed_alphamasks_rotation.at(group_index));
3078 reconstructed_alphamasks_gridcell.push_back(v);
3079 reconstructed_alphamasks_direct_flag.push_back(0);
3080
3081 leaf_area_current.at(v) += reconstructed_alphamasks_size.at(group_index).x * reconstructed_alphamasks_size.at(group_index).y * solidfraction;
3082
3083 backfill_count++;
3084 }
3085 }
3086 }
3087
3088 for (uint v = 0; v < Ncells; v++) {
3089
3090 float leaf_area_total = getCellLeafArea(v);
3091
3092 float current_area = 0;
3093 for (uint i = 0; i < reconstructed_alphamasks_size.size(); i++) {
3094 if (reconstructed_alphamasks_gridcell.at(i) == v) {
3095 current_area += reconstructed_alphamasks_size.at(i).x * reconstructed_alphamasks_size.at(i).y * solidfraction;
3096 }
3097 }
3098 }
3099
3100 if (printmessages) {
3101 cout << "Backfilled " << backfill_count << " total leaf groups." << endl;
3102 cout << "Deleted " << deleted_groups.size() << " total leaf groups." << endl;
3103 }
3104
3105 for (int i = deleted_groups.size() - 1; i >= 0; i--) {
3106 int group_index = deleted_groups.at(i);
3107 if (group_index >= 0 && group_index < reconstructed_alphamasks_center.size()) {
3108 // use swap-and-pop method
3109 std::swap(reconstructed_alphamasks_center.at(group_index), reconstructed_alphamasks_center.back());
3110 reconstructed_alphamasks_center.pop_back();
3111 std::swap(reconstructed_alphamasks_size.at(group_index), reconstructed_alphamasks_size.back());
3112 reconstructed_alphamasks_size.pop_back();
3113 std::swap(reconstructed_alphamasks_rotation.at(group_index), reconstructed_alphamasks_rotation.back());
3114 reconstructed_alphamasks_rotation.pop_back();
3115 std::swap(reconstructed_alphamasks_gridcell.at(group_index), reconstructed_alphamasks_gridcell.back());
3116 reconstructed_alphamasks_gridcell.pop_back();
3117 std::swap(reconstructed_alphamasks_direct_flag.at(group_index), reconstructed_alphamasks_direct_flag.back());
3118 reconstructed_alphamasks_direct_flag.pop_back();
3119 }
3120 }
3121
3122 if (printmessages) {
3123 cout << "done." << endl;
3124 }
3125}
3126
3127void LiDARcloud::calculateLeafAngleCDF(uint Nbins, std::vector<std::vector<float>> &CDF_theta, std::vector<std::vector<float>> &CDF_phi) {
3128
3129 uint Ncells = getGridCellCount();
3130
3131 std::vector<std::vector<float>> PDF_theta, PDF_phi;
3132 CDF_theta.resize(Ncells);
3133 PDF_theta.resize(Ncells);
3134 CDF_phi.resize(Ncells);
3135 PDF_phi.resize(Ncells);
3136 for (uint v = 0; v < Ncells; v++) {
3137 CDF_theta.at(v).resize(Nbins, 0.f);
3138 PDF_theta.at(v).resize(Nbins, 0.f);
3139 CDF_phi.at(v).resize(Nbins, 0.f);
3140 PDF_phi.at(v).resize(Nbins, 0.f);
3141 }
3142 float db_theta = 0.5 * M_PI / Nbins;
3143 float db_phi = 2.f * M_PI / Nbins;
3144
3145 // calculate PDF from triangulated hit points (not reconstructed triangles)
3146 for (size_t t = 0; t < triangles.size(); t++) {
3147 float triangle_area = triangles.at(t).area;
3148 int gridcell = triangles.at(t).gridcell;
3149
3150 if (gridcell >= 0 && gridcell < (int) Ncells) { // Valid grid cell
3151 helios::vec3 normal = cross(triangles.at(t).vertex1 - triangles.at(t).vertex0, triangles.at(t).vertex2 - triangles.at(t).vertex0);
3152 normal.z = fabs(normal.z); // keep in upper hemisphere
3153
3154 helios::SphericalCoord normal_dir = cart2sphere(normal);
3155
3156 int bin_theta = floor(normal_dir.zenith / db_theta);
3157 if (bin_theta >= Nbins) {
3158 bin_theta = Nbins - 1;
3159 }
3160
3161 int bin_phi = floor(normal_dir.azimuth / db_phi);
3162 if (bin_phi >= Nbins) {
3163 bin_phi = Nbins - 1;
3164 }
3165
3166 PDF_theta.at(gridcell).at(bin_theta) += triangle_area;
3167 PDF_phi.at(gridcell).at(bin_phi) += triangle_area;
3168 }
3169 }
3170
3171 // calculate PDF from CDF
3172 for (uint v = 0; v < Ncells; v++) {
3173 for (uint i = 0; i < Nbins; i++) {
3174 for (uint j = 0; j <= i; j++) {
3175 CDF_theta.at(v).at(i) += PDF_theta.at(v).at(j);
3176 CDF_phi.at(v).at(i) += PDF_phi.at(v).at(j);
3177 }
3178 }
3179 }
3180
3181 // char filename[50];
3182 // std::ofstream file_theta, file_phi;
3183 // for (uint v = 0; v < Ncells; v++) {
3184 // sprintf(filename, "../output/PDF_theta%d.txt", v);
3185 // file_theta.open(filename);
3186 // sprintf(filename, "../output/PDF_phi%d.txt", v);
3187 // file_phi.open(filename);
3188 // for (uint i = 0; i < Nbins; i++) {
3189 // file_theta << PDF_theta.at(v).at(i) << std::endl;
3190 // file_phi << PDF_phi.at(v).at(i) << std::endl;
3191 // }
3192 // file_theta.close();
3193 // file_phi.close();
3194 // }
3195}
3196
3198
3199 // loop through the vertices of the voxel grid.
3200 std::vector<helios::vec3> grid_vertices;
3201 helios::vec3 boxmin, boxmax;
3202 getGridBoundingBox(boxmin, boxmax); // axis aligned bounding box of all grid cells
3203 grid_vertices.push_back(boxmin);
3204 grid_vertices.push_back(boxmax);
3205 grid_vertices.push_back(helios::make_vec3(boxmin.x, boxmin.y, boxmax.z));
3206 grid_vertices.push_back(helios::make_vec3(boxmax.x, boxmax.y, boxmin.z));
3207 grid_vertices.push_back(helios::make_vec3(boxmin.x, boxmax.y, boxmin.z));
3208 grid_vertices.push_back(helios::make_vec3(boxmin.x, boxmax.y, boxmax.z));
3209 grid_vertices.push_back(helios::make_vec3(boxmax.x, boxmin.y, boxmin.z));
3210 grid_vertices.push_back(helios::make_vec3(boxmax.x, boxmin.y, boxmax.z));
3211
3212 float max_theta = 0;
3213 float min_theta = M_PI;
3214 float max_phi = 0;
3215 float min_phi = 2 * M_PI;
3216 for (uint gg = 0; gg < grid_vertices.size(); gg++) {
3217 helios::vec3 direction_cart = grid_vertices.at(gg) - getScanOrigin(source);
3218 helios::SphericalCoord sc = cart2sphere(direction_cart);
3219
3220 std::cout << "azimuth " << sc.azimuth * (180 / M_PI) << ", zenith " << sc.zenith * (180 / M_PI) << std::endl;
3221
3222 if (sc.azimuth < min_phi) {
3223 min_phi = sc.azimuth;
3224 }
3225
3226 if (sc.azimuth > max_phi) {
3227 max_phi = sc.azimuth;
3228 }
3229
3230 if (sc.zenith < min_theta) {
3231 min_theta = sc.zenith;
3232 }
3233
3234 if (sc.zenith > max_theta) {
3235 max_theta = sc.zenith;
3236 }
3237 }
3238
3239 vec2 theta_range = helios::make_vec2(min_theta, max_theta);
3240 vec2 phi_range = helios::make_vec2(min_phi, max_phi);
3241
3242 std::cout << "theta_range = " << theta_range * (180.0 / M_PI) << std::endl;
3243 std::cout << "phi_range = " << phi_range * (180.0 / M_PI) << std::endl;
3244
3245 std::cout << "original # hitpoints = " << getHitCount() << std::endl;
3246
3247 std::cout << "getHitScanID(getHitCount()-1) = " << getHitScanID(getHitCount() - 1) << " getHitScanID(0) = " << getHitScanID(0) << std::endl;
3248
3249 for (int r = (getHitCount() - 1); r >= 0; r--) {
3250 if (getHitScanID(r) == source) {
3252 float this_theta = raydir.zenith;
3253 float this_phi = raydir.azimuth;
3254 double this_phi_d = double(this_phi);
3255 setHitData(r, "beam_azimuth", this_phi_d);
3256 if (this_phi < phi_range.x || this_phi > phi_range.y || this_phi < phi_range.x || this_theta > theta_range.y) {
3257 deleteHitPoint(r);
3258 }
3259 }
3260 }
3261
3262 std::cout << "# hitpoints remaining after crop = " << getHitCount() << std::endl;
3263}
3264
3265// ========== SHARED METHODS FOR GPU AND CD IMPLEMENTATIONS ==========
3266
3267void LiDARcloud::computeGtheta(uint Ncells, uint Nscans,
3268 std::vector<float> &Gtheta, std::vector<float> &Gtheta_bar) {
3269
3270 // Initialize output vectors
3271 Gtheta.resize(Ncells, 0.f);
3272 Gtheta_bar.resize(Ncells, 0.f);
3273
3274 const size_t Ntri = getTriangleCount();
3275
3276 std::vector<float> denom_sum;
3277 denom_sum.resize(Ncells, 0.f);
3278 std::vector<uint> cell_tri_count;
3279 cell_tri_count.resize(Ncells, 0);
3280
3281 // Compute G(theta) for each triangle
3282 for (size_t t = 0; t < Ntri; t++) {
3283
3284 Triangulation tri = getTriangle(t);
3285 int cell = tri.gridcell;
3286
3287 if (cell >= 0 && cell < Ncells) { // triangle is inside a grid cell
3288
3289 helios::vec3 t0 = tri.vertex0;
3290 helios::vec3 t1 = tri.vertex1;
3291 helios::vec3 t2 = tri.vertex2;
3292
3293 helios::vec3 v0 = t1 - t0;
3294 helios::vec3 v1 = t2 - t0;
3295 helios::vec3 v2 = t2 - t1;
3296
3297 float L0 = v0.magnitude();
3298 float L1 = v1.magnitude();
3299 float L2 = v2.magnitude();
3300
3301 // Heron's formula for triangle area
3302 float S = 0.5f * (L0 + L1 + L2);
3303 float area = sqrt(S * (S - L0) * (S - L1) * (S - L2));
3304
3305 helios::vec3 normal = cross(v0, v2);
3306 normal.normalize();
3307
3308 helios::vec3 raydir = t0 - getScanOrigin(tri.scanID);
3309 raydir.normalize();
3310
3311 float theta = fabs(acos_safe(raydir.z));
3312
3313 if (area == area) { // Check for NaN
3314 float normal_dot_ray = fabs(normal * raydir);
3315 Gtheta.at(cell) += normal_dot_ray * area * fabs(sin(theta));
3316 denom_sum.at(cell) += fabs(sin(theta)) * area;
3317 cell_tri_count.at(cell) += 1;
3318 }
3319 }
3320 }
3321
3322 // Normalize by denominator and average over scans
3323 for (uint v = 0; v < Ncells; v++) {
3324 if (cell_tri_count[v] > 0) {
3325 Gtheta[v] = Gtheta[v] / denom_sum[v];
3326 Gtheta_bar[v] += Gtheta[v] / float(Nscans);
3327 }
3328 }
3329}
3330
3331bool LiDARcloud::invertLAD(uint voxel_index, float P, float Gtheta,
3332 const std::vector<float> &dr_samples, int min_voxel_hits,
3333 const helios::vec3 &gridsize, float &leaf_area) {
3334
3335 // Validation checks
3336 if (Gtheta == 0 || Gtheta != Gtheta) { // Check for zero or NaN
3337 leaf_area = 0.0f;
3338 return false;
3339 }
3340
3341 if (dr_samples.size() < min_voxel_hits) {
3342 leaf_area = 0.0f;
3343 return false;
3344 }
3345
3346 // Secant method parameters
3347 float etol = 5e-5f;
3348 uint maxiter = 100;
3349
3350 // Initial guesses
3351 float a = 0.1f;
3352 float h = 0.01f;
3353
3354 // Compute initial error
3355 float mean = 0.f;
3356 for (size_t j = 0; j < dr_samples.size(); j++) {
3357 mean += exp(-a * dr_samples[j] * Gtheta);
3358 }
3359 mean /= float(dr_samples.size());
3360 float error = fabs(mean - P) / P;
3361
3362 float tmp = a;
3363 a = a + h;
3364
3365 // Secant method iteration
3366 uint iter = 0;
3367 float aold, eold;
3368 while (error > etol && iter < maxiter) {
3369
3370 aold = tmp;
3371 eold = error;
3372
3373 mean = 0.f;
3374 for (size_t j = 0; j < dr_samples.size(); j++) {
3375 mean += exp(-a * dr_samples[j] * Gtheta);
3376 }
3377 mean /= float(dr_samples.size());
3378 error = fabs(mean - P) / P;
3379
3380 tmp = a;
3381
3382 if (error == eold) {
3383 break; // No progress
3384 }
3385
3386 // Secant update
3387 a = fabs((aold * error - a * eold) / (error - eold));
3388 iter++;
3389 }
3390
3391 // Calculate mean dr
3392 float dr_bar = 0.0f;
3393 for (size_t i = 0; i < dr_samples.size(); i++) {
3394 dr_bar += dr_samples[i];
3395 }
3396 dr_bar /= float(dr_samples.size());
3397
3398 // Check convergence and use fallback if needed
3399 bool converged = (iter < maxiter - 1 && a == a && a <= 100);
3400 bool used_fallback = false;
3401
3402 if (!converged) {
3403 if (printmessages) {
3404 std::cout << "WARNING: LAD inversion failed for volume #" << voxel_index
3405 << ". Using average dr formulation." << std::endl;
3406 }
3407 a = (1.f - P) / (dr_bar * Gtheta);
3408 used_fallback = true;
3409 }
3410
3411 // Additional constraint for high LAD values
3412 if (a > 5) {
3413 a = fmin((1.f - P) / dr_bar / Gtheta, -log(P) / dr_bar / Gtheta);
3414 }
3415
3416 // Compute final leaf area
3417 leaf_area = a * gridsize.x * gridsize.y * gridsize.z;
3418
3419 return true;
3420}
3421
3422uint LiDARcloud::filterRaysByBoundingBox(const helios::vec3 &scan_origin,
3423 const std::vector<helios::vec3> &ray_endpoints,
3424 const helios::vec3 &bb_center,
3425 const helios::vec3 &bb_size,
3426 std::vector<uint> &filtered_indices) {
3427
3428 filtered_indices.clear();
3429 filtered_indices.reserve(ray_endpoints.size());
3430
3431 // Compute bounding box min/max
3432 float x0 = bb_center.x - 0.5f * bb_size.x;
3433 float x1 = bb_center.x + 0.5f * bb_size.x;
3434 float y0 = bb_center.y - 0.5f * bb_size.y;
3435 float y1 = bb_center.y + 0.5f * bb_size.y;
3436 float z0 = bb_center.z - 0.5f * bb_size.z;
3437 float z1 = bb_center.z + 0.5f * bb_size.z;
3438
3439 // Ray origin
3440 float ox = scan_origin.x;
3441 float oy = scan_origin.y;
3442 float oz = scan_origin.z;
3443
3444 // Test each ray
3445 for (size_t i = 0; i < ray_endpoints.size(); i++) {
3446
3447 // Check if origin is inside bounding box
3448 if (ox >= x0 && ox <= x1 && oy >= y0 && oy <= y1 && oz >= z0 && oz <= z1) {
3449 filtered_indices.push_back(i);
3450 continue;
3451 }
3452
3453 // Compute ray direction
3454 helios::vec3 direction = ray_endpoints[i] - scan_origin;
3455 direction.normalize();
3456
3457 float dx = direction.x;
3458 float dy = direction.y;
3459 float dz = direction.z;
3460
3461 // Slab method for ray-AABB intersection
3462 float tx_min, ty_min, tz_min;
3463 float tx_max, ty_max, tz_max;
3464
3465 // X slab
3466 float a = 1.0f / dx;
3467 if (a >= 0) {
3468 tx_min = (x0 - ox) * a;
3469 tx_max = (x1 - ox) * a;
3470 } else {
3471 tx_min = (x1 - ox) * a;
3472 tx_max = (x0 - ox) * a;
3473 }
3474
3475 // Y slab
3476 float b = 1.0f / dy;
3477 if (b >= 0) {
3478 ty_min = (y0 - oy) * b;
3479 ty_max = (y1 - oy) * b;
3480 } else {
3481 ty_min = (y1 - oy) * b;
3482 ty_max = (y0 - oy) * b;
3483 }
3484
3485 // Z slab
3486 float c = 1.0f / dz;
3487 if (c >= 0) {
3488 tz_min = (z0 - oz) * c;
3489 tz_max = (z1 - oz) * c;
3490 } else {
3491 tz_min = (z1 - oz) * c;
3492 tz_max = (z0 - oz) * c;
3493 }
3494
3495 // Find largest entering t value
3496 float t0 = tx_min;
3497 if (ty_min > t0) t0 = ty_min;
3498 if (tz_min > t0) t0 = tz_min;
3499
3500 // Find smallest exiting t value
3501 float t1 = tx_max;
3502 if (ty_max < t1) t1 = ty_max;
3503 if (tz_max < t1) t1 = tz_max;
3504
3505 // Ray intersects if t0 < t1 and t1 > 0
3506 if (t0 < t1 && t1 > 1e-6f) {
3507 filtered_indices.push_back(i);
3508 }
3509 }
3510
3511 return filtered_indices.size();
3512}
3513
3514void LiDARcloud::calculateVoxelPathLengths(const helios::vec3 &scan_origin,
3515 const std::vector<helios::vec3> &ray_directions,
3516 const std::vector<helios::vec3> &voxel_centers,
3517 const std::vector<helios::vec3> &voxel_sizes,
3518 const std::vector<float> &voxel_rotations,
3519 std::vector<std::vector<float>> &dr_agg,
3520 std::vector<float> &hit_before_agg,
3521 std::vector<float> &hit_after_agg) {
3522
3523 const uint Ncells = voxel_centers.size();
3524
3525 // Initialize output arrays
3526 dr_agg.resize(Ncells);
3527 hit_before_agg.resize(Ncells, 0.0f);
3528 hit_after_agg.resize(Ncells, 0.0f);
3529
3530 // Manual ray-voxel intersection (to track ray indices properly)
3531 // This matches the GPU kernel behavior exactly
3532
3533 for (uint c = 0; c < Ncells; c++) {
3534 helios::vec3 center = voxel_centers[c];
3535 helios::vec3 size = voxel_sizes[c];
3536 float rotation = voxel_rotations[c];
3537
3538 // For each ray, test intersection with this voxel
3539 for (size_t r = 0; r < ray_directions.size(); r++) {
3540
3541 // Transform ray if voxel is rotated
3542 helios::vec3 ray_origin = scan_origin;
3543 helios::vec3 ray_dir = ray_directions[r];
3544
3545 if (fabs(rotation) > 1e-6f) {
3546 // Inverse rotate (following GPU kernel pattern)
3547 helios::vec3 endpoint = scan_origin + ray_directions[r] * 10000.f;
3548 helios::vec3 anchor = center; // Using voxel center as anchor
3549
3550 ray_origin = rotatePointAboutLine(scan_origin - anchor, helios::make_vec3(0, 0, 0), helios::make_vec3(0, 0, 1), -rotation) + anchor;
3551 helios::vec3 transformed_endpoint = rotatePointAboutLine(endpoint - anchor, helios::make_vec3(0, 0, 0), helios::make_vec3(0, 0, 1), -rotation) + anchor;
3552
3553 ray_dir = transformed_endpoint - ray_origin;
3554 ray_dir.normalize();
3555 }
3556
3557 // Ray-AABB intersection (slab method - matches GPU kernel)
3558 helios::vec3 voxel_min = center - size * 0.5f;
3559 helios::vec3 voxel_max = center + size * 0.5f;
3560
3561 float tx_min = (voxel_min.x - ray_origin.x) / ray_dir.x;
3562 float tx_max = (voxel_max.x - ray_origin.x) / ray_dir.x;
3563 if (tx_min > tx_max) std::swap(tx_min, tx_max);
3564
3565 float ty_min = (voxel_min.y - ray_origin.y) / ray_dir.y;
3566 float ty_max = (voxel_max.y - ray_origin.y) / ray_dir.y;
3567 if (ty_min > ty_max) std::swap(ty_min, ty_max);
3568
3569 float tz_min = (voxel_min.z - ray_origin.z) / ray_dir.z;
3570 float tz_max = (voxel_max.z - ray_origin.z) / ray_dir.z;
3571 if (tz_min > tz_max) std::swap(tz_min, tz_max);
3572
3573 float t0 = std::max({tx_min, ty_min, tz_min});
3574 float t1 = std::min({tx_max, ty_max, tz_max});
3575
3576 // Ray intersects voxel if t0 < t1 and t1 > 0
3577 if (t0 < t1 && t1 > 1e-6f) {
3578 // Path length through voxel
3579 float dr = fabs(t1 - t0);
3580 dr_agg[c].push_back(dr);
3581
3582 // Weight for radiative transfer
3583 float weight = 1.0f; // Simplified (Issue 4)
3584 float zenith_weight = sin(acos_safe(ray_dir.z));
3585
3586 // All intersecting rays count toward hit_after
3587 // (GPU kernel only adds to hit_after if hit is within/after voxel,
3588 // but for empty voxels, all rays that pass through count)
3589 hit_after_agg[c] += zenith_weight * weight;
3590 }
3591 }
3592 }
3593}
3594
3596 calculateLeafArea(context, 1);
3597}
3598
3599void LiDARcloud::calculateLeafArea(helios::Context *context, int min_voxel_hits) {
3600
3601 if (printmessages) {
3602 std::cout << "Calculating leaf area (CollisionDetection)..." << std::endl;
3603 }
3604
3605 // Validation checks (same as GPU version)
3606 if (!triangulationcomputed) {
3607 helios_runtime_error("ERROR (LiDARcloud::calculateLeafAreaCD): Triangulation must be performed prior to leaf area calculation. See triangulateHitPoints().");
3608 }
3609
3610 if (!hitgridcellcomputed) {
3612 }
3613
3614 // Initialize CollisionDetection if needed
3616
3617 const uint Nscans = getScanCount();
3618 const uint Ncells = getGridCellCount();
3619
3620 // Auto-detect multi-return data and select appropriate algorithm
3621 const bool use_equal_weighting = isMultiReturnData();
3622
3623 if (printmessages) {
3624 if (use_equal_weighting) {
3625 std::cout << "Multi-return data detected - using beam-based equal weighting algorithm (CD)" << std::endl;
3626
3627 // Check if gap filling has been applied
3628 bool has_gapfilled = false;
3629 for (size_t r = 0; r < getHitCount(); r++) {
3630 if (doesHitDataExist(r, "gapfillMisses_code")) {
3631 has_gapfilled = true;
3632 break;
3633 }
3634 }
3635
3636 if (!has_gapfilled) {
3637 std::cout << "WARNING: Multi-return data detected but gap filling has not been applied." << std::endl;
3638 std::cout << " For best results with multi-return data, call gapfillMisses() before calculateLeafArea()." << std::endl;
3639 }
3640 } else {
3641 std::cout << "Single-return data - using standard weighting algorithm (CD)" << std::endl;
3642 }
3643 }
3644
3645 // Branch to appropriate implementation
3646 if (use_equal_weighting) {
3647 // ============ MULTI-RETURN PATH (Equal Weighting Algorithm - CPU) ============
3648
3649 // Additional arrays for equal weighting P calculation
3650 std::vector<std::vector<float>> P_equal_numerator_array(Ncells);
3651 std::vector<std::vector<float>> P_equal_denominator_array(Ncells);
3652 std::vector<std::vector<float>> dr_array(Ncells);
3653
3654 // Initialize aggregation arrays
3655 std::vector<std::vector<float>> dr_agg;
3656 dr_agg.resize(Ncells);
3657 std::vector<float> Gtheta_bar;
3658 Gtheta_bar.resize(Ncells, 0.f);
3659
3660 // Process each scan
3661 for (uint s = 0; s < Nscans; s++) {
3662
3663 // Collect hits for this scan
3664 std::vector<helios::vec3> this_scan_xyz;
3665 std::vector<uint> this_scan_index;
3666 for (size_t r = 0; r < getHitCount(); r++) {
3667 if (getHitScanID(r) == s) {
3668 this_scan_xyz.push_back(getHitXYZ(r));
3669 this_scan_index.push_back(r);
3670 }
3671 }
3672 size_t Nhits = this_scan_xyz.size();
3673 if (Nhits == 0) continue;
3674
3675 // Group hits by timestamp into beams
3676 BeamGrouping beams = groupHitsByTimestamp(this_scan_index);
3677 uint Nbeams = beams.Nbeams;
3678
3679 helios::vec3 origin = getScanOrigin(s);
3680 float scanner_range = 5000.0f;
3681
3682 // CPU-based voxel intersection with hit_location classification
3683 std::vector<float> dr(Nhits, 0.0f);
3684 std::vector<uint> hit_location(Nhits, 0);
3685
3686 // Process each voxel
3687 for (uint c = 0; c < Ncells; c++) {
3688
3689 helios::vec3 center = getCellCenter(c);
3690 helios::vec3 size = getCellSize(c);
3691 float rotation = getCellRotation(c);
3692
3693 // Reset for this voxel
3694 std::fill(dr.begin(), dr.end(), 0.0f);
3695 std::fill(hit_location.begin(), hit_location.end(), 0);
3696
3697 // Test each hit against this voxel (CPU/OpenMP)
3698 #pragma omp parallel for
3699 for (int i = 0; i < static_cast<int>(Nhits); i++) {
3700 helios::vec3 hit_xyz = this_scan_xyz[i];
3701
3702 // Inverse rotate if needed
3703 if (fabs(rotation) > 1e-6f) {
3704 helios::vec3 anchor = center;
3705 hit_xyz = rotatePointAboutLine(hit_xyz - anchor, helios::make_vec3(0,0,0),
3706 helios::make_vec3(0,0,1), -rotation) + anchor;
3707 }
3708
3709 // Ray from origin to hit
3710 helios::vec3 direction = hit_xyz - origin;
3711 float hit_distance = direction.magnitude();
3712 direction.normalize();
3713
3714 // AABB bounds
3715 helios::vec3 voxel_min = center - size * 0.5f;
3716 helios::vec3 voxel_max = center + size * 0.5f;
3717
3718 // Ray-AABB intersection (slab method)
3719 float tx_min = (voxel_min.x - origin.x) / direction.x;
3720 float tx_max = (voxel_max.x - origin.x) / direction.x;
3721 if (tx_min > tx_max) std::swap(tx_min, tx_max);
3722
3723 float ty_min = (voxel_min.y - origin.y) / direction.y;
3724 float ty_max = (voxel_max.y - origin.y) / direction.y;
3725 if (ty_min > ty_max) std::swap(ty_min, ty_max);
3726
3727 float tz_min = (voxel_min.z - origin.z) / direction.z;
3728 float tz_max = (voxel_max.z - origin.z) / direction.z;
3729 if (tz_min > tz_max) std::swap(tz_min, tz_max);
3730
3731 float t0 = std::max({tx_min, ty_min, tz_min});
3732 float t1 = std::min({tx_max, ty_max, tz_max});
3733
3734 // Classify hit location relative to voxel
3735 if (t0 < t1 && t1 > 1e-6f) {
3736 dr[i] = fabs(t1 - t0);
3737
3738 if (hit_distance >= t0 && hit_distance <= t1) {
3739 hit_location[i] = 2; // Inside voxel
3740 } else if (hit_distance > t1 && hit_distance < scanner_range) {
3741 hit_location[i] = 3; // After voxel (within range)
3742 } else if (hit_distance >= scanner_range) {
3743 hit_location[i] = 4; // Miss (beyond range)
3744 } else if (hit_distance < t0) {
3745 hit_location[i] = 1; // Before voxel
3746 }
3747 }
3748 }
3749
3750 // Beam-level processing
3751 float P_equal_numerator = 0;
3752 float P_equal_denominator = 0;
3753
3754 for (uint k = 0; k < Nbeams; k++) {
3755 float E_before = 0, E_inside = 0, E_after = 0, E_miss = 0;
3756 float drr = 0;
3757 int dr_count = 0; // Count returns with dr > 0
3758
3759 // Count returns in each location for this beam
3760 for (uint j = 0; j < beams.beam_array.at(k).size(); j++) {
3761 uint i = beams.beam_array.at(k).at(j);
3762
3763 if (dr[i] > 0) {
3764 drr += dr[i];
3765 dr_count++;
3766 }
3767
3768 if (hit_location[i] == 1) E_before++;
3769 else if (hit_location[i] == 2) E_inside++;
3770 else if (hit_location[i] == 3) E_after++;
3771 else if (hit_location[i] == 4) E_miss++;
3772 }
3773
3774 // Equal weighting P calculation - simple average per Eq. 7
3775 // P = (1/B_tot) Σ[E_after / (E_inside + E_after)]
3776 if (E_inside != 0 || E_after != 0) {
3777 P_equal_numerator += (E_after / (E_inside + E_after));
3778 P_equal_denominator += 1;
3779 } else if (E_inside == 0 && E_after == 0 && E_before == 0 && E_miss != 0) {
3780 P_equal_numerator += 1;
3781 P_equal_denominator += 1;
3782 }
3783
3784 // Average dr over returns that actually intersect voxel
3785 if (dr_count > 0) {
3786 float drrx = drr / float(dr_count);
3787 dr_array.at(c).push_back(drrx);
3788 }
3789 }
3790
3791 P_equal_numerator_array.at(c).push_back(P_equal_numerator);
3792 P_equal_denominator_array.at(c).push_back(P_equal_denominator);
3793 }
3794 }
3795
3796 // Calculate G(theta) using shared method
3797 std::vector<float> Gtheta;
3798 computeGtheta(Ncells, Nscans, Gtheta, Gtheta_bar);
3799
3800 // LAD inversion with equal weighting P
3801 if (printmessages) {
3802 std::cout << "Inverting to find LAD..." << std::flush;
3803 }
3804
3805 for (uint v = 0; v < Ncells; v++) {
3806 // Calculate P using equal weighting formula
3807 float P = 0.0f;
3808 float P_num_sum = 0.0f, P_denom_sum = 0.0f;
3809 for (uint s = 0; s < P_equal_numerator_array[v].size(); s++) {
3810 P_num_sum += P_equal_numerator_array[v][s];
3811 P_denom_sum += P_equal_denominator_array[v][s];
3812 }
3813 if (P_denom_sum > 0) {
3814 P = P_num_sum / P_denom_sum;
3815 }
3816
3817 // Aggregate dr across all scans
3818 for (uint s = 0; s < dr_array[v].size(); s++) {
3819 if (dr_array[v][s] > 0) {
3820 dr_agg[v].push_back(dr_array[v][s]);
3821 }
3822 }
3823
3824 // Apply min_voxel_hits filtering
3825 if (dr_agg[v].size() < min_voxel_hits) {
3826 setCellLeafArea(0, v);
3827 setCellGtheta(Gtheta[v], v);
3828 continue;
3829 }
3830
3831 // Use shared invertLAD method
3832 float leaf_area = 0.0f;
3833 helios::vec3 gridsize = getCellSize(v);
3834 invertLAD(v, P, Gtheta[v], dr_agg[v], min_voxel_hits, gridsize, leaf_area);
3835
3836 setCellLeafArea(leaf_area, v);
3837 setCellGtheta(Gtheta[v], v);
3838 }
3839
3840 if (printmessages) {
3841 std::cout << "done." << std::endl;
3842 }
3843
3844 return; // Exit after multi-return processing
3845 }
3846
3847 // ============ SINGLE-RETURN PATH (Standard Algorithm) ============
3848 // Existing code continues below unchanged
3849
3850 // Aggregation arrays
3851 std::vector<std::vector<float>> dr_agg;
3852 dr_agg.resize(Ncells);
3853 std::vector<float> hit_before_agg;
3854 hit_before_agg.resize(Ncells, 0);
3855 std::vector<float> hit_after_agg;
3856 hit_after_agg.resize(Ncells, 0);
3857 std::vector<float> hit_inside_agg;
3858 hit_inside_agg.resize(Ncells, 0);
3859 std::vector<float> Gtheta_bar;
3860 Gtheta_bar.resize(Ncells, 0.f);
3861
3862 // Process each scan
3863 for (uint s = 0; s < Nscans; s++) {
3864
3865 const int Nt = getScanSizeTheta(s);
3866 const int Np = getScanSizePhi(s);
3867 const size_t Nmisses = Nt * Np;
3868
3869 const helios::vec3 origin = getScanOrigin(s);
3870
3871 // ----- STAGE B: BOUNDING BOX FILTERING ----- //
3872
3873 // Generate all scan ray endpoints
3874 std::vector<helios::vec3> scan_endpoints;
3875 scan_endpoints.reserve(Nmisses);
3876
3877 for (int j = 0; j < Np; j++) {
3878 for (int i = 0; i < Nt; i++) {
3879 helios::vec3 direction = sphere2cart(scans.at(s).rc2direction(i, j));
3880 helios::vec3 endpoint = origin + direction * 10000.f;
3881 scan_endpoints.push_back(endpoint);
3882 }
3883 }
3884
3885 // Get global bounding box
3886 helios::vec3 gboxmin, gboxmax;
3887 getGridBoundingBox(gboxmin, gboxmax);
3888 helios::vec3 bbcenter = gboxmin + 0.5f * (gboxmax - gboxmin);
3889 helios::vec3 bbsize = gboxmax - gboxmin;
3890
3891 // Filter rays that hit bounding box
3892 std::vector<uint> bb_hit_indices;
3893 uint Nmissesbb = filterRaysByBoundingBox(origin, scan_endpoints, bbcenter, bbsize, bb_hit_indices);
3894
3895 if (Nmissesbb == 0) {
3896 std::cerr << "ERROR (calculateLeafAreaCD): No scan rays passed through grid cells." << std::endl;
3897 for (uint c = 0; c < Ncells; c++) {
3898 setCellLeafArea(0, c);
3899 }
3900 return;
3901 }
3902
3903 // Extract filtered ray directions
3904 std::vector<helios::vec3> missesbb_directions;
3905 missesbb_directions.reserve(Nmissesbb);
3906 for (uint idx : bb_hit_indices) {
3907 helios::vec3 direction = scan_endpoints[idx] - origin;
3908 direction.normalize();
3909 missesbb_directions.push_back(direction);
3910 }
3911
3912 // ----- STAGE C: VOXEL PATH LENGTHS ----- //
3913
3914 // Prepare voxel data arrays
3915 std::vector<helios::vec3> voxel_centers_vec, voxel_sizes_vec;
3916 std::vector<float> voxel_rotations_vec;
3917 voxel_centers_vec.reserve(Ncells);
3918 voxel_sizes_vec.reserve(Ncells);
3919 voxel_rotations_vec.reserve(Ncells);
3920
3921 for (uint c = 0; c < Ncells; c++) {
3922 voxel_centers_vec.push_back(getCellCenter(c));
3923 voxel_sizes_vec.push_back(getCellSize(c));
3924 voxel_rotations_vec.push_back(getCellRotation(c));
3925 }
3926
3927 // Calculate path lengths using CollisionDetection
3928 calculateVoxelPathLengths(origin, missesbb_directions,
3929 voxel_centers_vec, voxel_sizes_vec, voxel_rotations_vec,
3930 dr_agg, hit_before_agg, hit_after_agg);
3931
3932 // FIX: Calculate hit_before for occluded voxels
3933 // For each hit from this scan, determine which voxels it occurred before
3934 for (size_t r = 0; r < getHitCount(); r++) {
3935 if (getHitScanID(r) != s) continue; // Only process hits from this scan
3936
3937 int hit_voxel = getHitGridCell(r);
3938 if (hit_voxel < 0) continue; // Hit not in any grid voxel
3939
3940 helios::vec3 hit_xyz = getHitXYZ(r);
3941 helios::vec3 ray_dir = (hit_xyz - origin);
3942 float hit_distance = ray_dir.magnitude();
3943 ray_dir.normalize();
3944
3945 // For each voxel, check if this hit occurred BEFORE entering that voxel
3946 for (uint c = 0; c < Ncells; c++) {
3947 if (c == hit_voxel) continue; // Hit is in this voxel, not before it
3948
3949 // Check if ray intersects this voxel
3950 helios::vec3 voxel_min = voxel_centers_vec[c] - voxel_sizes_vec[c] * 0.5f;
3951 helios::vec3 voxel_max = voxel_centers_vec[c] + voxel_sizes_vec[c] * 0.5f;
3952
3953 float tx_min = (voxel_min.x - origin.x) / ray_dir.x;
3954 float tx_max = (voxel_max.x - origin.x) / ray_dir.x;
3955 if (tx_min > tx_max) std::swap(tx_min, tx_max);
3956
3957 float ty_min = (voxel_min.y - origin.y) / ray_dir.y;
3958 float ty_max = (voxel_max.y - origin.y) / ray_dir.y;
3959 if (ty_min > ty_max) std::swap(ty_min, ty_max);
3960
3961 float tz_min = (voxel_min.z - origin.z) / ray_dir.z;
3962 float tz_max = (voxel_max.z - origin.z) / ray_dir.z;
3963 if (tz_min > tz_max) std::swap(tz_min, tz_max);
3964
3965 float t_enter = std::max({tx_min, ty_min, tz_min});
3966 float t_exit = std::min({tx_max, ty_max, tz_max});
3967
3968 // Ray intersects this voxel
3969 if (t_enter < t_exit && t_exit > 1e-6f) {
3970 // Check if hit occurred BEFORE entering this voxel
3971 if (hit_distance < t_enter) {
3972 float zenith_weight = sin(acos_safe(ray_dir.z));
3973 hit_before_agg[c] += zenith_weight;
3974 }
3975 }
3976 }
3977 }
3978 }
3979
3980 // ----- STAGE D: HIT CLASSIFICATION ----- //
3981
3982 // Calculate hit_inside for all scans (same as GPU version)
3983 for (size_t r = 0; r < getHitCount(); r++) {
3984 if (getHitGridCell(r) >= 0) {
3985 helios::vec3 direction = getHitXYZ(r) - getScanOrigin(getHitScanID(r));
3986 direction.normalize();
3987 hit_inside_agg.at(getHitGridCell(r)) += sin(acos_safe(direction.z));
3988 }
3989 }
3990
3991 // ----- STAGE E: G(THETA) CALCULATION ----- //
3992
3993 std::vector<float> Gtheta;
3994 computeGtheta(Ncells, Nscans, Gtheta, Gtheta_bar);
3995
3996 // ----- STAGES F, G, H: TRANSMISSION, LAD INVERSION, FINAL LEAF AREA ----- //
3997
3998 if (printmessages) {
3999 std::cout << "Inverting to find LAD..." << std::flush;
4000 }
4001
4002 for (uint v = 0; v < Ncells; v++) {
4003
4004 // Stage F: Calculate transmission probability
4005 float P = 0.0f;
4006 if (hit_after_agg[v] - hit_before_agg[v] > 0) {
4007 P = 1.f - float(hit_inside_agg[v]) / float(hit_after_agg[v] - hit_before_agg[v]);
4008 }
4009
4010 // Stages G & H: LAD inversion and final leaf area
4011 float leaf_area = 0.0f;
4012 helios::vec3 gridsize = getCellSize(v);
4013
4014 invertLAD(v, P, Gtheta[v], dr_agg[v], min_voxel_hits, gridsize, leaf_area);
4015
4016 setCellLeafArea(leaf_area, v);
4017 setCellGtheta(Gtheta[v], v);
4018 }
4019
4020 if (printmessages) {
4021 std::cout << "done." << std::endl;
4022 }
4023}
4024
4025// Helper to find ray index by matching direction (Issue 1 workaround)
4026size_t findRayIndexByDirection(const helios::vec3 &scan_origin,
4027 const helios::vec3 &intersection_point,
4028 const std::vector<helios::vec3> &ray_directions) {
4029 helios::vec3 hit_direction = intersection_point - scan_origin;
4030 hit_direction.normalize();
4031
4032 for (size_t i = 0; i < ray_directions.size(); i++) {
4033 if ((hit_direction - ray_directions[i]).magnitude() < 1e-5f) {
4034 return i;
4035 }
4036 }
4037 return SIZE_MAX; // Not found
4038}
4039
4040// Deprecated wrapper functions for backward compatibility
4044
4045void LiDARcloud::calculateLeafAreaGPU(helios::Context *context, int min_voxel_hits) {
4046 calculateLeafArea(context, min_voxel_hits);
4047}
4048
4050 if (collision_detection != nullptr) {
4051 collision_detection->enableGPUAcceleration();
4052 }
4053}
4054
4056 if (collision_detection != nullptr) {
4057 collision_detection->disableGPUAcceleration();
4058 }
4059}
4060
4062
4063 if (printmessages) {
4064 std::cout << "Grouping hit points by grid cell (CPU)..." << std::flush;
4065 }
4066
4067 const size_t total_hits = getHitCount();
4068 const uint Ncells = getGridCellCount();
4069
4070 if (total_hits == 0) {
4071 std::cout << "WARNING (calculateHitGridCellCD): There are no hits currently in the point cloud. Skipping grid cell binning calculation." << std::endl;
4072 return;
4073 }
4074
4075 // Process each hit point (parallelized with OpenMP)
4076 #pragma omp parallel for schedule(dynamic, 1000)
4077 for (int r = 0; r < static_cast<int>(total_hits); r++) {
4078
4079 helios::vec3 hit_xyz = getHitXYZ(r);
4080 int assigned_cell = -1; // Default: not in any cell
4081
4082 // Test against each voxel
4083 for (uint c = 0; c < Ncells; c++) {
4084
4085 helios::vec3 center = getCellCenter(c);
4087 helios::vec3 size = getCellSize(c);
4088 float rotation = getCellRotation(c);
4089
4090 // Inverse rotate hit point if voxel is rotated
4091 helios::vec3 hit_xyz_rot = hit_xyz;
4092 if (fabs(rotation) > 1e-6f) {
4093 hit_xyz_rot = rotatePointAboutLine(hit_xyz - anchor, helios::make_vec3(0, 0, 0),
4094 helios::make_vec3(0, 0, 1), -rotation) + anchor;
4095 }
4096
4097 // Treat hit as a ray from origin for AABB test (matches GPU kernel)
4098 helios::vec3 origin = helios::make_vec3(0, 0, 0);
4099 helios::vec3 direction = hit_xyz_rot - origin;
4100 direction.normalize();
4101
4102 // AABB bounds
4103 float x0 = center.x - 0.5f * size.x;
4104 float x1 = center.x + 0.5f * size.x;
4105 float y0 = center.y - 0.5f * size.y;
4106 float y1 = center.y + 0.5f * size.y;
4107 float z0 = center.z - 0.5f * size.z;
4108 float z1 = center.z + 0.5f * size.z;
4109
4110 // Slab method for ray-AABB intersection
4111 float tx_min = (x0 - origin.x) / direction.x;
4112 float tx_max = (x1 - origin.x) / direction.x;
4113 if (tx_min > tx_max) std::swap(tx_min, tx_max);
4114
4115 float ty_min = (y0 - origin.y) / direction.y;
4116 float ty_max = (y1 - origin.y) / direction.y;
4117 if (ty_min > ty_max) std::swap(ty_min, ty_max);
4118
4119 float tz_min = (z0 - origin.z) / direction.z;
4120 float tz_max = (z1 - origin.z) / direction.z;
4121 if (tz_min > tz_max) std::swap(tz_min, tz_max);
4122
4123 float t0 = std::max({tx_min, ty_min, tz_min});
4124 float t1 = std::min({tx_max, ty_max, tz_max});
4125
4126 // Check if hit point is inside voxel
4127 if (t0 < t1 && t1 > 1e-6f) {
4128 float T = (hit_xyz_rot - origin).magnitude();
4129 if (T >= t0 && T <= t1) {
4130 assigned_cell = c;
4131 break; // Found the cell, stop searching
4132 }
4133 }
4134 }
4135
4136 // Store result (thread-safe due to unique index per thread)
4137 setHitGridCell(r, assigned_cell);
4138 }
4139
4140 if (printmessages) {
4141 std::cout << "done." << std::endl;
4142 }
4143
4144 hitgridcellcomputed = true;
4145}
4146
4147bool LiDARcloud::isMultiReturnData() const {
4148 // Check if any hit has target_count > 1 (multi-return indicator)
4149 for (size_t r = 0; r < getHitCount(); r++) {
4150 if (doesHitDataExist(r, "target_count")) {
4151 if (getHitData(r, "target_count") > 1) {
4152 // Multi-return data requires timestamp for beam grouping
4153 if (!doesHitDataExist(r, "timestamp")) {
4154 helios_runtime_error("ERROR (isMultiReturnData): Multi-return data detected (target_count > 1) but 'timestamp' field is missing. Cannot group hits into beams.");
4155 }
4156 // Multi-return data requires target_index for triangulation filtering
4157 if (!doesHitDataExist(r, "target_index")) {
4158 helios_runtime_error("ERROR (isMultiReturnData): Multi-return data detected (target_count > 1) but 'target_index' field is missing. Cannot filter first returns for triangulation.");
4159 }
4160 return true;
4161 }
4162 }
4163 }
4164 return false;
4165}
4166
4167LiDARcloud::BeamGrouping LiDARcloud::groupHitsByTimestamp(const std::vector<uint>& scan_indices) const {
4168
4169 BeamGrouping result;
4170
4171 if (scan_indices.empty()) {
4172 result.Nbeams = 0;
4173 return result;
4174 }
4175
4176 // Sort indices by timestamp to group consecutive hits from same beam
4177 std::vector<uint> sorted_indices = scan_indices;
4178 std::sort(sorted_indices.begin(), sorted_indices.end(),
4179 [this](uint a, uint b) {
4180 return this->getHitData(a, "timestamp") < this->getHitData(b, "timestamp");
4181 });
4182
4183 // Count unique beams by counting timestamp changes (now works correctly with sorted data)
4184 double previous_time = -1.0;
4185 result.Nbeams = 0;
4186 for (uint i = 0; i < sorted_indices.size(); i++) {
4187 double current_time = getHitData(sorted_indices[i], "timestamp");
4188 if (current_time != previous_time) {
4189 result.Nbeams++;
4190 previous_time = current_time;
4191 }
4192 }
4193
4194 // Group hits by beam (same timestamp = same beam)
4195 result.beam_array.resize(result.Nbeams);
4196 double previous_beam = getHitData(sorted_indices[0], "timestamp");
4197 uint beam_ID = 0;
4198
4199 for (uint i = 0; i < sorted_indices.size(); i++) {
4200 double current_beam = getHitData(sorted_indices[i], "timestamp");
4201
4202 if (current_beam == previous_beam) {
4203 result.beam_array.at(beam_ID).push_back(sorted_indices[i]);
4204 } else {
4205 beam_ID++;
4206 result.beam_array.at(beam_ID).push_back(sorted_indices[i]);
4207 previous_beam = current_beam;
4208 }
4209 }
4210
4211 return result;
4212}
4213
4215
4216 std::vector<uint> UUIDs_all = context->getAllUUIDs();
4217 const uint N = UUIDs_all.size();
4218 const uint Ncells = getGridCellCount();
4219
4220 // Result: which voxel each primitive belongs to (-1 if none)
4221 std::vector<int> prim_vol(N, -1);
4222
4223 // CPU/OpenMP version of primitive-to-voxel assignment
4224 #pragma omp parallel for
4225 for (int p = 0; p < static_cast<int>(N); p++) {
4226 std::vector<helios::vec3> verts = context->getPrimitiveVertices(UUIDs_all[p]);
4227 helios::vec3 prim_xyz = verts[0]; // Use first vertex
4228
4229 // Test against each voxel (same logic as calculateHitGridCellCD)
4230 for (uint c = 0; c < Ncells; c++) {
4231 helios::vec3 center = getCellCenter(c);
4233 helios::vec3 size = getCellSize(c);
4234 float rotation = getCellRotation(c);
4235
4236 // Inverse rotate primitive position if voxel is rotated
4237 helios::vec3 prim_xyz_rot = prim_xyz;
4238 if (fabs(rotation) > 1e-6f) {
4239 prim_xyz_rot = rotatePointAboutLine(prim_xyz - anchor, helios::make_vec3(0,0,0),
4240 helios::make_vec3(0,0,1), -rotation) + anchor;
4241 }
4242
4243 // Point-in-AABB test (treating as ray from origin for consistency with GPU kernel)
4244 helios::vec3 origin_pt = helios::make_vec3(0, 0, 0);
4245 helios::vec3 direction = prim_xyz_rot - origin_pt;
4246 direction.normalize();
4247
4248 // AABB bounds
4249 float x0 = center.x - 0.5f * size.x;
4250 float x1 = center.x + 0.5f * size.x;
4251 float y0 = center.y - 0.5f * size.y;
4252 float y1 = center.y + 0.5f * size.y;
4253 float z0 = center.z - 0.5f * size.z;
4254 float z1 = center.z + 0.5f * size.z;
4255
4256 // Slab method
4257 float tx_min = (x0 - origin_pt.x) / direction.x;
4258 float tx_max = (x1 - origin_pt.x) / direction.x;
4259 if (tx_min > tx_max) std::swap(tx_min, tx_max);
4260
4261 float ty_min = (y0 - origin_pt.y) / direction.y;
4262 float ty_max = (y1 - origin_pt.y) / direction.y;
4263 if (ty_min > ty_max) std::swap(ty_min, ty_max);
4264
4265 float tz_min = (z0 - origin_pt.z) / direction.z;
4266 float tz_max = (z1 - origin_pt.z) / direction.z;
4267 if (tz_min > tz_max) std::swap(tz_min, tz_max);
4268
4269 float t0 = std::max({tx_min, ty_min, tz_min});
4270 float t1 = std::min({tx_max, ty_max, tz_max});
4271
4272 // Check if primitive is inside voxel
4273 if (t0 < t1 && t1 > 1e-6f) {
4274 float T = (prim_xyz_rot - origin_pt).magnitude();
4275 if (T >= t0 && T <= t1) {
4276 prim_vol[p] = c;
4277 break; // Found the voxel
4278 }
4279 }
4280 }
4281 }
4282
4283 // Sum primitive areas per voxel
4284 std::vector<float> total_area(Ncells, 0.f);
4285 for (size_t p = 0; p < N; p++) {
4286 if (prim_vol[p] >= 0) {
4287 uint gridcell = prim_vol[p];
4288 total_area[gridcell] += context->getPrimitiveArea(UUIDs_all[p]);
4289 context->setPrimitiveData(UUIDs_all[p], "gridCell", gridcell);
4290 }
4291 }
4292
4293 // Store as primitive data
4294 std::vector<float> output_LeafArea(Ncells);
4295 for (int v = 0; v < Ncells; v++) {
4296 output_LeafArea[v] = total_area[v];
4297 if (context->doesPrimitiveDataExist(UUIDs_all[v], "gridCell")) {
4298 context->setPrimitiveData(UUIDs_all[v], "synthetic_leaf_area", total_area[v]);
4299 }
4300 }
4301
4302 return output_LeafArea;
4303}
4304
4306 syntheticScan(context, 1, 0, false, false, true);
4307}
4308
4309void LiDARcloud::syntheticScan(helios::Context *context, bool append) {
4310 syntheticScan(context, 1, 0, false, false, append);
4311}
4312
4313void LiDARcloud::syntheticScan(helios::Context *context, bool scan_grid_only, bool record_misses) {
4314 syntheticScan(context, 1, 0, scan_grid_only, record_misses, true);
4315}
4316
4317void LiDARcloud::syntheticScan(helios::Context *context, bool scan_grid_only, bool record_misses, bool append) {
4318 syntheticScan(context, 1, 0, scan_grid_only, record_misses, append);
4319}
4320
4321void LiDARcloud::syntheticScan(helios::Context *context, int rays_per_pulse, float pulse_distance_threshold) {
4322 syntheticScan(context, rays_per_pulse, pulse_distance_threshold, false, false, true);
4323}
4324
4325void LiDARcloud::syntheticScan(helios::Context *context, int rays_per_pulse, float pulse_distance_threshold, bool append) {
4326 syntheticScan(context, rays_per_pulse, pulse_distance_threshold, false, false, append);
4327}
4328
4329void LiDARcloud::syntheticScan(helios::Context *context, int rays_per_pulse, float pulse_distance_threshold, bool scan_grid_only, bool record_misses) {
4330 syntheticScan(context, rays_per_pulse, pulse_distance_threshold, scan_grid_only, record_misses, true);
4331}
4332
4333void LiDARcloud::syntheticScan(helios::Context *context, int rays_per_pulse, float pulse_distance_threshold, bool scan_grid_only, bool record_misses, bool append) {
4334
4335 // Clear existing hit data if not appending
4336 if (!append) {
4337 hits.clear();
4338 Nhits = 0;
4339 // Reset hit tables for each scan
4340 for (auto &hit_table: hit_tables) {
4341 hit_table.resize(hit_table.Ntheta, hit_table.Nphi, -1);
4342 }
4343 hitgridcellcomputed = false;
4344 triangulationcomputed = false;
4345 }
4346
4347 int Npulse;
4348 if (rays_per_pulse < 1) {
4349 Npulse = 1;
4350 } else {
4351 Npulse = rays_per_pulse;
4352 }
4353
4354 if (printmessages) {
4355 if (Npulse > 1) {
4356 std::cout << "Performing full-waveform synthetic LiDAR scan..." << std::endl;
4357 } else {
4358 std::cout << "Performing discrete return synthetic LiDAR scan..." << std::endl;
4359 }
4360 }
4361
4362 if (getScanCount() == 0) {
4363 std::cout << "WARNING (syntheticScan): No scans added to the point cloud. Exiting.." << std::endl;
4364 return;
4365 }
4366
4367 float miss_distance = 1001.0; // arbitrary distance from scanner for 'miss' points
4368
4369 helios::vec3 bb_center;
4370 helios::vec3 bb_size;
4371
4372 if (scan_grid_only == false) {
4373
4374 // Determine bounding box for Context geometry
4375 helios::vec2 xbounds, ybounds, zbounds;
4376 context->getDomainBoundingBox(xbounds, ybounds, zbounds);
4377 bb_center = helios::make_vec3(xbounds.x + 0.5 * (xbounds.y - xbounds.x), ybounds.x + 0.5 * (ybounds.y - ybounds.x), zbounds.x + 0.5 * (zbounds.y - zbounds.x));
4378 bb_size = helios::make_vec3(xbounds.y - xbounds.x, ybounds.y - ybounds.x, zbounds.y - zbounds.x);
4379
4380 } else {
4381
4382 // Determine bounding box for voxels instead of whole domain
4383 helios::vec3 boxmin, boxmax;
4384 getGridBoundingBox(boxmin, boxmax);
4385 bb_center = helios::make_vec3(boxmin.x + 0.5 * (boxmax.x - boxmin.x), boxmin.y + 0.5 * (boxmax.y - boxmin.y), boxmin.z + 0.5 * (boxmax.z - boxmin.z));
4386 bb_size = helios::make_vec3(boxmax.x - boxmin.x, boxmax.y - boxmin.y, boxmax.z - boxmin.z);
4387 }
4388
4389 // get geometry information and copy to GPU
4390
4391 size_t c = 0;
4392
4393 std::map<std::string, int> textures;
4394 std::map<std::string, helios::int2> texture_size;
4395 std::map<std::string, std::vector<std::vector<bool>>> texture_data;
4396 int tID = 0;
4397
4398 std::vector<uint> UUIDs_all = context->getAllUUIDs();
4399
4400 std::vector<uint> ID_mapping;
4401
4402 //----- PATCHES ----- //
4403
4404 // figure out how many patches
4405 size_t Npatches = 0;
4406 for (int p = 0; p < UUIDs_all.size(); p++) {
4407 if (context->getPrimitiveType(UUIDs_all.at(p)) == helios::PRIMITIVE_TYPE_PATCH) {
4408 Npatches++;
4409 }
4410 }
4411
4412 ID_mapping.resize(Npatches);
4413
4414 helios::vec3 *patch_vertex = (helios::vec3 *) malloc(4 * Npatches * sizeof(helios::vec3)); // allocate host memory
4415 int *patch_textureID = (int *) malloc(Npatches * sizeof(int)); // allocate host memory
4416 helios::vec2 *patch_uv = (helios::vec2 *) malloc(2 * Npatches * sizeof(helios::vec2)); // allocate host memory
4417
4418 c = 0;
4419 for (int p = 0; p < UUIDs_all.size(); p++) {
4420 uint UUID = UUIDs_all.at(p);
4421 if (context->getPrimitiveType(UUID) == helios::PRIMITIVE_TYPE_PATCH) {
4422 std::vector<helios::vec3> verts = context->getPrimitiveVertices(UUID);
4423 patch_vertex[4 * c] = verts.at(0);
4424 patch_vertex[4 * c + 1] = verts.at(1);
4425 patch_vertex[4 * c + 2] = verts.at(2);
4426 patch_vertex[4 * c + 3] = verts.at(3);
4427
4428 ID_mapping.at(c) = UUIDs_all.at(p);
4429
4430 if (!context->getPrimitiveTextureFile(UUID).empty() && context->primitiveTextureHasTransparencyChannel(UUID)) {
4431 std::string tex = context->getPrimitiveTextureFile(UUID);
4432 std::map<std::string, int>::iterator it = textures.find(tex);
4433 if (it != textures.end()) { // texture already exits
4434 patch_textureID[c] = textures.at(tex);
4435 } else { // new texture
4436 patch_textureID[c] = tID;
4437 textures[tex] = tID;
4438 helios::int2 tsize = context->getPrimitiveTextureSize(UUID);
4439 texture_size[tex] = helios::make_int2(tsize.x, tsize.y);
4440 texture_data[tex] = *context->getPrimitiveTextureTransparencyData(UUID);
4441 tID++;
4442 }
4443
4444 std::vector<helios::vec2> uv = context->getPrimitiveTextureUV(UUID);
4445 if (uv.size() == 4) { // custom uv coordinates
4446 patch_uv[2 * c] = uv.at(1);
4447 patch_uv[2 * c + 1] = uv.at(3);
4448 } else { // default uv coordinates
4449 patch_uv[2 * c] = helios::make_vec2(0, 0);
4450 patch_uv[2 * c + 1] = helios::make_vec2(1, 1);
4451 }
4452
4453 } else {
4454 patch_textureID[c] = -1;
4455 }
4456
4457 c++;
4458 }
4459 }
4460
4461 // GPU allocations removed - performUnifiedRayTracing uses CollisionDetection instead
4462
4463 //----- TRIANGLES ----- //
4464
4465 // figure out how many triangles
4466 size_t Ntriangles = 0;
4467 for (int p = 0; p < UUIDs_all.size(); p++) {
4468 if (context->getPrimitiveType(UUIDs_all.at(p)) == helios::PRIMITIVE_TYPE_TRIANGLE) {
4469 Ntriangles++;
4470 }
4471 }
4472
4473 ID_mapping.resize(Npatches + Ntriangles);
4474
4475 helios::vec3 *tri_vertex = (helios::vec3 *) malloc(3 * Ntriangles * sizeof(helios::vec3)); // allocate host memory
4476 int *tri_textureID = (int *) malloc(Ntriangles * sizeof(int)); // allocate host memory
4477 helios::vec2 *tri_uv = (helios::vec2 *) malloc(3 * Ntriangles * sizeof(helios::vec2)); // allocate host memory
4478
4479 c = 0;
4480 for (int p = 0; p < UUIDs_all.size(); p++) {
4481 uint UUID = UUIDs_all.at(p);
4482 if (context->getPrimitiveType(UUID) == helios::PRIMITIVE_TYPE_TRIANGLE) {
4483 std::vector<helios::vec3> verts = context->getPrimitiveVertices(UUID);
4484 tri_vertex[3 * c] = verts.at(0);
4485 tri_vertex[3 * c + 1] = verts.at(1);
4486 tri_vertex[3 * c + 2] = verts.at(2);
4487
4488 ID_mapping.at(Npatches + c) = UUIDs_all.at(p);
4489
4490 if (!context->getPrimitiveTextureFile(UUID).empty() && context->primitiveTextureHasTransparencyChannel(UUID)) {
4491 std::string tex = context->getPrimitiveTextureFile(UUID);
4492 std::map<std::string, int>::iterator it = textures.find(tex);
4493 if (it != textures.end()) { // texture already exits
4494 tri_textureID[c] = textures.at(tex);
4495 } else { // new texture
4496 tri_textureID[c] = tID;
4497 textures[tex] = tID;
4498 helios::int2 tsize = context->getPrimitiveTextureSize(UUID);
4499 texture_size[tex] = helios::make_int2(tsize.x, tsize.y);
4500 texture_data[tex] = *context->getPrimitiveTextureTransparencyData(UUID);
4501 tID++;
4502 }
4503
4504 std::vector<helios::vec2> uv = context->getPrimitiveTextureUV(UUID);
4505 assert(uv.size() == 3);
4506 tri_uv[3 * c] = uv.at(0);
4507 tri_uv[3 * c + 1] = uv.at(1);
4508 tri_uv[3 * c + 2] = uv.at(2);
4509
4510 } else {
4511 tri_textureID[c] = -1;
4512 }
4513
4514 c++;
4515 }
4516 }
4517
4518 // GPU allocations removed - performUnifiedRayTracing uses CollisionDetection instead
4519
4520 // transfer texture data to GPU
4521 const int Ntextures = textures.size();
4522
4523 helios::int2 masksize_max = helios::make_int2(0, 0);
4524 for (std::map<std::string, helios::int2>::iterator it = texture_size.begin(); it != texture_size.end(); ++it) {
4525 if (it->second.x > masksize_max.x) {
4526 masksize_max.x = it->second.x;
4527 }
4528 if (it->second.y > masksize_max.y) {
4529 masksize_max.y = it->second.y;
4530 }
4531 }
4532
4533 bool *maskdata = (bool *) malloc(Ntextures * masksize_max.x * masksize_max.y * sizeof(bool)); // allocate host memory
4534 helios::int2 *masksize = (helios::int2 *) malloc(Ntextures * sizeof(helios::int2)); // allocate host memory
4535
4536 for (std::map<std::string, helios::int2>::iterator it = texture_size.begin(); it != texture_size.end(); ++it) {
4537 std::string texture_file = it->first;
4538
4539 int ID = textures.at(texture_file);
4540
4541 masksize[ID] = it->second;
4542
4543 int ind = 0;
4544 for (int j = 0; j < masksize_max.y; j++) {
4545 for (int i = 0; i < masksize_max.x; i++) {
4546
4547 if (i < texture_size.at(texture_file).x && j < texture_size.at(texture_file).y) {
4548 maskdata[ID * masksize_max.x * masksize_max.y + ind] = texture_data.at(texture_file).at(j).at(i);
4549 } else {
4550 maskdata[ID * masksize_max.x * masksize_max.y + ind] = false;
4551 }
4552 ind++;
4553 }
4554 }
4555 }
4556
4557 // GPU allocations removed - texture data no longer copied to GPU
4558
4559 for (int s = 0; s < getScanCount(); s++) {
4560
4561 helios::vec3 scan_origin = getScanOrigin(s);
4562
4563 int Ntheta = getScanSizeTheta(s);
4564 int Nphi = getScanSizePhi(s);
4565
4566 helios::vec2 thetarange = getScanRangeTheta(s);
4567 float thetamin = thetarange.x;
4568 float thetamax = thetarange.y;
4569 helios::vec2 phirange = getScanRangePhi(s);
4570 float phimin = phirange.x;
4571 float phimax = phirange.y;
4572
4573 std::vector<std::string> column_format = getScanColumnFormat(s);
4574
4575 std::vector<helios::vec3> raydir;
4576 raydir.resize(Ntheta * Nphi);
4577
4578 for (uint j = 0; j < Nphi; j++) {
4579 float phi = phimin + float(j) * (phimax - phimin) / float(Nphi - 1);
4580 for (uint i = 0; i < Ntheta; i++) {
4581 float theta_z = thetamin + float(i) * (thetamax - thetamin) / float(Ntheta - 1);
4582 float theta_elev = 0.5f * M_PI - theta_z;
4583 helios::vec3 dir = sphere2cart(helios::make_SphericalCoord(1.f, theta_elev, phi));
4584 raydir.at(Ntheta * j + i) = dir;
4585 }
4586 }
4587
4588 size_t N = Ntheta * Nphi;
4589
4590 // Bounding box intersection test (CPU version, no CUDA)
4591 std::vector<uint> bb_hit(N, 0);
4592
4593 // Calculate BB bounds once
4594 helios::vec3 bb_min = bb_center - bb_size * 0.5f;
4595 helios::vec3 bb_max = bb_center + bb_size * 0.5f;
4596
4597 // Check if origin is inside bounding box
4598 bool origin_inside_bb = (scan_origin.x >= bb_min.x && scan_origin.x <= bb_max.x &&
4599 scan_origin.y >= bb_min.y && scan_origin.y <= bb_max.y &&
4600 scan_origin.z >= bb_min.z && scan_origin.z <= bb_max.z);
4601
4602 for (size_t r = 0; r < N; r++) {
4603 // If origin inside BB, all rays automatically hit
4604 if (origin_inside_bb) {
4605 bb_hit[r] = 1;
4606 continue;
4607 }
4608
4609 helios::vec3 ray_dir = raydir.at(r);
4610
4611 // AABB ray intersection using slab method
4612 float tx_min, tx_max, ty_min, ty_max, tz_min, tz_max;
4613
4614 float a = 1.0f / ray_dir.x;
4615 if (a >= 0) {
4616 tx_min = (bb_min.x - scan_origin.x) * a;
4617 tx_max = (bb_max.x - scan_origin.x) * a;
4618 } else {
4619 tx_min = (bb_max.x - scan_origin.x) * a;
4620 tx_max = (bb_min.x - scan_origin.x) * a;
4621 }
4622
4623 float b = 1.0f / ray_dir.y;
4624 if (b >= 0) {
4625 ty_min = (bb_min.y - scan_origin.y) * b;
4626 ty_max = (bb_max.y - scan_origin.y) * b;
4627 } else {
4628 ty_min = (bb_max.y - scan_origin.y) * b;
4629 ty_max = (bb_min.y - scan_origin.y) * b;
4630 }
4631
4632 float c = 1.0f / ray_dir.z;
4633 if (c >= 0) {
4634 tz_min = (bb_min.z - scan_origin.z) * c;
4635 tz_max = (bb_max.z - scan_origin.z) * c;
4636 } else {
4637 tz_min = (bb_max.z - scan_origin.z) * c;
4638 tz_max = (bb_min.z - scan_origin.z) * c;
4639 }
4640
4641 // Find largest entering t value
4642 float t0 = tx_min;
4643 if (ty_min > t0) t0 = ty_min;
4644 if (tz_min > t0) t0 = tz_min;
4645
4646 // Find smallest exiting t value
4647 float t1 = tx_max;
4648 if (ty_max < t1) t1 = ty_max;
4649 if (tz_max < t1) t1 = tz_max;
4650
4651 if (t0 < t1 && t1 > 1e-6f) {
4652 bb_hit[r] = 1;
4653 }
4654 }
4655
4656 // determine how many rays hit the bounding box
4657 size_t total_scan_rays = Ntheta * Nphi;
4658 N = 0;
4659 float hit_out = 0;
4660 for (int i = 0; i < total_scan_rays; i++) {
4661 if (bb_hit[i] == 1) {
4662 N++;
4663 helios::SphericalCoord dir = cart2sphere(raydir[i]);
4664 hit_out += sin(dir.zenith);
4665 }
4666 }
4667
4668 // Store base beam directions that hit bounding box
4669 std::vector<helios::vec3> base_directions;
4670 base_directions.reserve(N);
4671 std::vector<helios::int2> pulse_scangrid_ij(N);
4672
4673 int count = 0;
4674 for (int i = 0; i < Ntheta * Nphi; i++) {
4675 if (bb_hit[i] == 1) {
4676
4677 base_directions.push_back(raydir.at(i));
4678
4679 int jj = floor(i / Ntheta);
4680 int ii = i - jj * Ntheta;
4681 pulse_scangrid_ij[count] = helios::make_int2(ii, jj);
4682
4683 count++;
4684 }
4685 // NOTE: Miss recording for rays that don't hit BB removed - those rays can't interact with grid.
4686 // Misses for traced rays are recorded via line 3733 when record_misses=true.
4687 }
4688
4689 // Handle case where no rays hit bounding box
4690 if (N == 0) {
4691 // If record_misses=true, record all rays as misses
4692 if (record_misses) {
4693 float miss_dist = 1001.0f;
4694 for (int i = 0; i < Ntheta * Nphi; i++) {
4695 std::map<std::string, double> data;
4696 data["target_index"] = 0;
4697 data["target_count"] = 1;
4698 data["deviation"] = 0.0;
4699 data["timestamp"] = i;
4700 data["intensity"] = 1.0; // Full miss
4701 data["distance"] = miss_dist;
4702 data["nRaysHit"] = Npulse; // All rays in pulse missed together
4703
4704 helios::vec3 dir = raydir.at(i);
4705 helios::vec3 p = scan_origin + dir * miss_dist;
4706 addHitPoint(s, p, helios::cart2sphere(dir), helios::RGB::red, data);
4707 }
4708 } else if (printmessages) {
4709 std::cout << "WARNING: Synthetic rays did not hit any primitives." << std::endl;
4710 }
4711 continue; // Move to next scan
4712 }
4713
4714 // Allocate host memory for results
4715 float *hit_t = (float *) malloc(N * Npulse * sizeof(float)); // allocate host memory
4716 float *hit_fnorm = (float *) malloc(N * Npulse * sizeof(float)); // allocate host memory
4717 int *hit_ID = (int *) malloc(N * Npulse * sizeof(int)); // allocate host memory
4718
4719 float exit_diameter = getScanBeamExitDiameter(s);
4720 float beam_divergence = getScanBeamDivergence(s);
4721
4722 // Generate N*Npulse perturbed ray directions using beam parameters
4723 helios::vec3 *direction = (helios::vec3 *) malloc(N * Npulse * sizeof(helios::vec3));
4724
4725 for (size_t beam = 0; beam < N; beam++) {
4726 helios::vec3 base_dir = base_directions[beam];
4727
4728 for (int p = 0; p < Npulse; p++) {
4729 if (p == 0 || beam_divergence == 0.0f) {
4730 // First ray OR zero divergence: use nominal direction
4731 // When beam_divergence=0, all rays should be identical to avoid floating-point precision errors
4732 direction[beam * Npulse + p] = base_dir;
4733 } else {
4734 // Subsequent rays with non-zero divergence: apply perturbation
4735 // Sample random angular offset within divergence cone using Context's RNG
4736 float ru = context->randu();
4737 float rv = context->randu();
4738
4739 float theta_offset = beam_divergence * sqrt(ru); // radial angular distance
4740 float phi_offset = 2.0f * M_PI * rv; // azimuthal angle
4741
4742 // Apply small angle perturbation in spherical coordinates
4743 helios::SphericalCoord base_spherical = helios::cart2sphere(base_dir);
4744
4745 // Perturb in elevation space (constructor takes elevation, not zenith)
4746 float new_elevation = base_spherical.elevation + theta_offset * cos(phi_offset);
4747 float new_azimuth = base_spherical.azimuth + theta_offset * sin(phi_offset) / fmax(cos(base_spherical.elevation), 1e-6f);
4748
4749 // Create perturbed direction
4750 helios::vec3 perturbed_dir = helios::sphere2cart(helios::SphericalCoord(1.0f, new_elevation, new_azimuth));
4751 perturbed_dir.normalize();
4752
4753 direction[beam * Npulse + p] = perturbed_dir;
4754 }
4755 }
4756 }
4757
4758 // Generate ray origins based on exit diameter
4759 helios::vec3 *ray_origins = (helios::vec3 *) malloc(N * Npulse * sizeof(helios::vec3));
4760
4761 if (exit_diameter > 0.0f) {
4762 // Finite aperture: distribute ray origins across disk perpendicular to beam direction
4763 float radius = exit_diameter * 0.5f;
4764
4765 for (size_t beam = 0; beam < N; beam++) {
4766 helios::vec3 base_dir = base_directions[beam];
4767
4768 // Construct orthonormal basis {u, v, base_dir} for disk perpendicular to beam
4769 helios::vec3 reference = (fabs(base_dir.z) < 0.9f) ? helios::make_vec3(0, 0, 1) : helios::make_vec3(1, 0, 0);
4770 helios::vec3 u = helios::cross(base_dir, reference);
4771 u.normalize();
4772 helios::vec3 v = helios::cross(base_dir, u); // Already normalized since base_dir and u are orthonormal
4773
4774 for (int p = 0; p < Npulse; p++) {
4775 // Uniform sampling on disk using sqrt transform
4776 float ru = context->randu();
4777 float rv = context->randu();
4778 float r_sample = radius * sqrtf(ru); // sqrt for uniform area distribution
4779 float theta = 2.0f * M_PI * rv;
4780 float x_disk = r_sample * cosf(theta);
4781 float y_disk = r_sample * sinf(theta);
4782
4783 // Transform disk point to world space
4784 helios::vec3 offset = u * x_disk + v * y_disk;
4785 ray_origins[beam * Npulse + p] = scan_origin + offset;
4786 }
4787 }
4788 } else {
4789 // Point source: all rays originate from scan_origin (backward compatibility)
4790 for (size_t i = 0; i < N * Npulse; i++) {
4791 ray_origins[i] = scan_origin;
4792 }
4793 }
4794
4795 // Initialize collision detection for unified ray-tracing
4797
4798 // Use unified ray-tracing engine
4799 performUnifiedRayTracing(context, N, Npulse, ray_origins, direction, hit_t, hit_fnorm, hit_ID);
4800
4801 size_t Nhits = 0;
4802 size_t beams_with_zero_hits = 0;
4803 size_t beams_with_one_hit = 0;
4804 size_t beams_with_multi_hits = 0;
4805
4806 // looping over beams
4807 for (size_t r = 0; r < N; r++) {
4808
4809 std::vector<std::vector<float>> t_pulse;
4810 std::vector<std::vector<float>> t_hit;
4811
4812 // looping over rays in each beam
4813 for (size_t p = 0; p < Npulse; p++) {
4814
4815 float t = hit_t[r * Npulse + p]; // distance to hit (misses t=1001.0)
4816 float i = hit_fnorm[r * Npulse + p]; // dot product between beam direction and primitive normal
4817 float ID = float(hit_ID[r * Npulse + p]); // ID of intersected primitive
4818
4819 if (record_misses || (!record_misses && t < miss_distance)) {
4820 std::vector<float> v{t, i, ID};
4821 t_pulse.push_back(v);
4822 }
4823 }
4824
4825 if (t_pulse.size() == 0) {
4826 // No hits for this beam
4827 beams_with_zero_hits++;
4828 } else if (t_pulse.size() == 1) { // this is discrete-return data, or we only had one hit for this pulse
4829 beams_with_one_hit++;
4830
4831 float distance = t_pulse.front().at(0);
4832 float intensity = t_pulse.front().at(1);
4833 if (distance >= 0.98f * miss_distance) {
4834 intensity = 0.0;
4835 }
4836 float nPulseHit = 1;
4837 float IDmap = t_pulse.front().at(2);
4838
4839 std::vector<float> v{distance, intensity, nPulseHit, IDmap};
4840 t_hit.push_back(v);
4841
4842 } else if (t_pulse.size() > 1) { // more than one hit for this pulse
4843 beams_with_multi_hits++;
4844
4845 std::sort(t_pulse.begin(), t_pulse.end(), [](const std::vector<float> &a, const std::vector<float> &b) {
4846 return a[0] < b[0];
4847 });
4848
4849 float t0 = t_pulse.at(0).at(0);
4850 float d = t_pulse.at(0).at(0);
4851 float f = t_pulse.at(0).at(1);
4852 int count = 1;
4853
4854 // loop over rays in each beam and group into hit points
4855 for (size_t hit = 1; hit <= t_pulse.size(); hit++) {
4856
4857 // if the end has been reached, output the last hitpoint
4858 if (hit == t_pulse.size()) {
4859
4860 float distance = d / float(count);
4861 float intensity = f / float(Npulse);
4862 if (distance >= 0.98f * miss_distance) {
4863 intensity = 0;
4864 }
4865 float nPulseHit = float(count);
4866 float IDmap = t_pulse.at(hit - 1).at(2);
4867
4868 // test for full misses and set those to have intensity = 1
4869 if (nPulseHit == Npulse & distance >= 0.98f * miss_distance) {
4870 std::vector<float> v{distance, 1.0, nPulseHit, IDmap}; // included the ray count here
4871 // Note: the last index of t_pulse (.at(2)) is the object identifier. We don't want object identifiers to be averaged, so we'll assign the hit identifier based on the last ray in the group
4872 t_hit.push_back(v);
4873 } else {
4874 std::vector<float> v{distance, intensity, nPulseHit, IDmap}; // included the ray count here
4875 // Note: the last index of t_pulse (.at(2)) is the object identifier. We don't want object identifiers to be averaged, so we'll assign the hit identifier based on the last ray in the group
4876 t_hit.push_back(v);
4877 }
4878
4879 // else if the current ray is more than the pulse threshold distance from t0, it is part of the next hitpoint so output the previous hitpoint and reset
4880 } else if (t_pulse.at(hit).at(0) - t0 > pulse_distance_threshold) {
4881
4882 float distance = d / float(count);
4883 float intensity = f / float(Npulse);
4884 if (distance >= 0.98f * miss_distance) {
4885 intensity = 0;
4886 }
4887 float nPulseHit = float(count);
4888 float IDmap = t_pulse.at(hit - 1).at(2);
4889
4890 // test for full misses and set those to have intensity = 1
4891 if (nPulseHit == Npulse & distance >= 0.98f * miss_distance) {
4892 std::vector<float> v{distance, 1.0, nPulseHit, IDmap}; // included the ray count here
4893 // Note: the last index of t_pulse (.at(2)) is the object identifier. We don't want object identifiers to be averaged, so we'll assign the hit identifier based on the last ray in the group
4894 t_hit.push_back(v);
4895 } else {
4896 std::vector<float> v{distance, intensity, nPulseHit, IDmap}; // included the ray count here
4897 // Note: the last index of t_pulse (.at(2)) is the object identifier. We don't want object identifiers to be averaged, so we'll assign the hit identifier based on the last ray in the group
4898 t_hit.push_back(v);
4899 }
4900
4901 count = 1;
4902 d = t_pulse.at(hit).at(0);
4903 t0 = t_pulse.at(hit).at(0);
4904 f = t_pulse.at(hit).at(1);
4905
4906 // or else the current ray is less than pulse threshold and is part of current hitpoint; add it to the current hit point and continue on
4907 } else {
4908
4909 count++;
4910 d += t_pulse.at(hit).at(0);
4911 f += t_pulse.at(hit).at(1);
4912 }
4913 }
4914 }
4915
4916 float average = 0;
4917 for (size_t hit = 0; hit < t_hit.size(); hit++) {
4918 average += t_hit.at(hit).at(0) / float(t_hit.size());
4919 }
4920
4921 // Count non-miss returns for proper target_index assignment
4922 int non_miss_count = 0;
4923 for (size_t hit = 0; hit < t_hit.size(); hit++) {
4924 if (t_hit.at(hit).at(0) < 0.98f * 1001.0f) {
4925 non_miss_count++;
4926 }
4927 }
4928
4929 int real_hit_index = 0;
4930 for (size_t hit = 0; hit < t_hit.size(); hit++) {
4931
4932 std::map<std::string, double> data;
4933
4934 // Check if this is a miss point
4935 bool is_miss = (t_hit.at(hit).at(0) >= 0.98f * 1001.0f);
4936
4937 // Assign target_index: misses get 99, real hits get sequential index (0, 1, 2...)
4938 if (is_miss) {
4939 data["target_index"] = 99; // Special value to exclude from triangulation
4940 } else {
4941 data["target_index"] = real_hit_index;
4942 real_hit_index++;
4943 }
4944
4945 data["target_count"] = t_hit.size();
4946 data["deviation"] = fabs(t_hit.at(hit).at(0) - average);
4947 data["timestamp"] = pulse_scangrid_ij.at(r).y * Ntheta + pulse_scangrid_ij.at(r).x;
4948 data["intensity"] = t_hit.at(hit).at(1);
4949 data["distance"] = t_hit.at(hit).at(0);
4950 data["nRaysHit"] = t_hit.at(hit).at(2);
4951
4952 float UUID = t_hit.at(hit).at(3);
4953 if (UUID >= 0 && UUID < ID_mapping.size()) {
4954 UUID = ID_mapping.at(int(t_hit.at(hit).at(3)));
4955 }
4956
4957 helios::RGBcolor color = helios::RGB::red;
4958
4959 if (UUID >= 0 && context->doesPrimitiveExist(uint(UUID))) {
4960
4961 color = context->getPrimitiveColor(uint(UUID));
4962
4963 if (context->doesPrimitiveDataExist(uint(UUID), "object_label") && context->getPrimitiveDataType("object_label") == helios::HELIOS_TYPE_INT) {
4964 int label;
4965 context->getPrimitiveData(uint(UUID), "object_label", label);
4966 data["object_label"] = double(label);
4967 }
4968
4969 if (context->doesPrimitiveDataExist(uint(UUID), "reflectivity_lidar") && context->getPrimitiveDataType("reflectivity_lidar") == helios::HELIOS_TYPE_FLOAT) {
4970 float rho;
4971 context->getPrimitiveData(uint(UUID), "reflectivity_lidar", rho);
4972 data.at("intensity") *= rho;
4973 }
4974 }
4975
4976 // Use base direction for this beam (first ray: r*Npulse+0)
4977 helios::vec3 dir = direction[r * Npulse];
4978 helios::vec3 p = scan_origin + dir * t_hit.at(hit).at(0);
4979 addHitPoint(s, p, helios::cart2sphere(dir), color, data);
4980
4981 Nhits++;
4982 }
4983 }
4984
4985 // No device memory to free
4986 free(ray_origins);
4987 free(direction);
4988 free(hit_t);
4989 free(hit_fnorm);
4990 free(hit_ID);
4991
4992 if (printmessages) {
4993 std::cout << "Created synthetic scan #" << s << " with " << Nhits << " hit points." << std::endl;
4994 }
4995 }
4996
4997 // No device memory to free
4998 free(patch_vertex);
4999 free(patch_textureID);
5000 free(patch_uv);
5001 free(tri_vertex);
5002 free(tri_textureID);
5003 free(tri_uv);
5004 free(maskdata);
5005 free(masksize);
5006}