3D Point Cloud Files - Developer documentation
Overview
- A point cloud is a set of data points in a 3D coordinate system—commonly known as the XYZ axes. Each point represents a single spatial measurement on the object's surface. Taken together, a point cloud represents the entire external surface of an object.
- To load and view the point cloud file the platform uses a compatible 3D viewer called Potree.
- For filtering, transforming and analyzing the point cloud data the platform uses PDAL - Point Data Abstraction Library and GDAL - translator library for raster and vector geospatial data formats.
Warning
- To use this section functionalities the user must have successfully completed all steps from the Development set-up , especially the part for Waste comparison analysis otherwise it will not work properly.
Upload/Convert Process
Note
- First of all, the following application settings must be stored as the table below
Key |
Value |
Description |
Data Type |
Module |
SupportedPointCloudFileExtensions |
.las, .laz |
Specifies the allowed file extensions for point cloud files. |
Integer (0) |
null |
LegalLandfillPointCloudFileUploads |
Uploads\LegalLandfillUploads\PointCloudUploads |
Legal Landfill PointCloud File Uploads |
Integer (0) |
null |
LegalLandfillPointCloudFileConverts |
Uploads\LegalLandfillUploads\PointCloudConverts |
Legal Landfill PointCloud File Converts |
Integer (0) |
null |
PotreeConverterFilePath |
Should contains the absolute path to potree converter exe (ex. C:\PotreeConverter\PotreeConverter.exe) |
Potree Converter File Path |
Integer (0) |
null |
PdalExePath |
Should contains the absolute path to pdal exe (ex. C:\miniconda3\envs\waste_comparison_env\Library\bin\pdal.exe) |
Pdal exe path |
Integer (0) |
null |
PdalPipelineTemplate |
[ "{INPUT_FILE}", { "filename": "{OUTPUT_FILE}", "gdaldriver": "GTiff", "output_type": "max", "resolution": "0.5", "nodata": -9999, "type": "writers.gdal" } ] |
Pdal Pipeline Template |
Integer (0) |
null |
The uploading process contains several steps:
- Checking supported file formats (.las, .laz).
//check file support
var fileUploadExtension = Path.GetExtension(viewModel.FileUpload.FileName);
ResultDTO resultCheckSuportingFiles = await _legalLandfillPointCloudFileService.CheckSupportingFiles(fileUploadExtension);
if (!resultCheckSuportingFiles.IsSuccess && resultCheckSuportingFiles.HandleError())
{
return ResultDTO.Fail(resultCheckSuportingFiles.ErrMsg!);
}
- Storing the uploaded file in database
//create the file in database
var resultCreateEntity = await _legalLandfillPointCloudFileService.CreateLegalLandfillPointCloudFile(dto);
if (!resultCreateEntity.IsSuccess && resultCreateEntity.HandleError())
{
return ResultDTO.Fail(resultCreateEntity.ErrMsg!);
}
- Storing the uploaded file in folder so it could be available for further usage
//upload the file in folder
var fileName = string.Format("{0}{1}", resultCreateEntity.Data.Id.ToString(), fileUploadExtension);
var filePath = Path.Combine(uploadsFolder, fileName);
var uploadResult = await _legalLandfillPointCloudFileService.UploadFile(viewModel.FileUpload, uploadsFolder, resultCreateEntity.Data.Id.ToString() + fileUploadExtension);
if (!uploadResult.IsSuccess && uploadResult.HandleError())
{
return ResultDTO.Fail("File uploading failed");
}
- Converting and storing the uploaded point cloud file to relevant Potree file format using Potree converter. In the background it transforms the supported/uploaded point cloud file and creates several files on a different location like hierarchy.bin, octree.bin, log.txt, and metadata.json. Later these files are used by the platform so the user can view the potree cloud file/s.
//convert file to potree format
var convertsFolder = Path.Combine(_webHostEnvironment.WebRootPath, pointCloudConvertFolder.Data, resultGetEntity.Data.Id.ToString(), resultCreateEntity.Data.Id.ToString());
var convertResult = await _legalLandfillPointCloudFileService.ConvertToPointCloud(potreeConverterFilePath.Data, uploadResult.Data, convertsFolder, filePath);
if (!convertResult.IsSuccess && convertResult.HandleError())
{
return ResultDTO.Fail("Converting to point cloud failed");
}
//execute command line
var result = await Cli.Wrap(potreeConverterFilePath)
.WithArguments($"{filePath} -o {convertsFolder}")
.WithValidation(CommandResultValidation.None)
.ExecuteAsync();
- Creating and storing a tif file using PDAL for the needs of further waste differential analysis.
//create tif file
var tifFileName = string.Format("{0}{1}", resultCreateEntity.Data.Id.ToString(), "_dsm.tif");
var tifFilePath = Path.Combine(uploadsFolder, tifFileName);
var tiffResult = await _legalLandfillPointCloudFileService.CreateTifFile(pipelineJsonTemplate.Data, pdalAbsPath.Data, filePath, tifFilePath);
if (!tiffResult.IsSuccess)
{
return ResultDTO.Fail("Creating TIF file failed");
}
//execute command line
var res = await Cli.Wrap(pdalAbsPath)
.WithArguments("pipeline --stdin")
.WithStandardInputPipe(PipeSource.FromString(pipelineJson))
.WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync();
View point cloud/s
3D Visualization
- Select some of the previously uploaded point cloud/s to open up in Potree 3D viewer.
//Preview the selected point cloud files - key method
[HttpGet]
[HasAuthClaim(nameof(SD.AuthClaims.PreviewLegalLandfillPointCloudFiles))]
public async Task<IActionResult> Preview(List<string> selectedFiles)
{
if (selectedFiles == null || selectedFiles.Count == 0)
{
var errorPath = _configuration["ErrorViewsPath:Error"];
if (errorPath == null)
{
return BadRequest();
}
return Redirect(errorPath);
}
try
{
List<Guid> depryptedGuids = new List<Guid>();
foreach (var item in selectedFiles)
{
Guid decryptedGuid = GuidEncryptionHelper.DecryptGuid(item);
depryptedGuids.Add(decryptedGuid);
}
List<PreviewViewModel> listVM = new();
foreach (var item in depryptedGuids)
{
var resultGetEntity = await _legalLandfillPointCloudFileService.GetLegalLandfillPointCloudFilesById(item);
if (!resultGetEntity.IsSuccess && resultGetEntity.HandleError())
{
var errorPath = _configuration["ErrorViewsPath:Error404"];
if (errorPath == null)
{
return NotFound();
}
return Redirect(errorPath);
}
if (resultGetEntity.Data == null)
{
var errorPath = _configuration["ErrorViewsPath:Error404"];
if (errorPath == null)
{
return NotFound();
}
return Redirect(errorPath);
}
string? fileNameWithoutExtension = Path.GetFileNameWithoutExtension(resultGetEntity.Data.FileName);
PreviewViewModel vm = new()
{
FileId = resultGetEntity.Data.Id,
LandfillId = resultGetEntity.Data.LegalLandfillId,
FileName = fileNameWithoutExtension ?? resultGetEntity.Data.FileName
};
listVM.Add(vm);
}
return View(listVM);
}
catch (Exception)
{
var errorPath = _configuration["ErrorViewsPath:Error"];
if (errorPath == null)
{
return BadRequest();
}
return Redirect(errorPath);
}
}
//show the selected point cloud in the Potree sidebar
for (let i = 0; i < metadatas.length; i++) {
Potree.loadPointCloud(metadatas[i].FileUrl, metadatas[i].FileName, e => {
let scene = viewer.scene;
let pointcloud = e.pointcloud;
let material = pointcloud.material;
material.size = 1;
material.pointSizeType = Potree.PointSizeType.ADAPTIVE;
material.shape = Potree.PointShape.SQUARE;
scene.addPointCloud(pointcloud);
viewer.fitToScreen();
});
}
- Potree 3D viewer sidebar contains additional options for measuring, clipping, navigating, maintaining the background etc.
//Potree sidebar adjustment
viewer.loadGUI(() => {
viewer.setLanguage('en');
let section = $(`
<h3 id="menu_meta" class="accordion-header ui-widget"><span>Additional actions</span></h3>
<div class="accordion-content ui-widget pv-menu-list"></div>
`);
let content = section.last();
content.html(`
<div class="pv-menu-list">
<a href='@Url.Action("ViewPointCloudFiles", "LegalLandfillsManagement", new { Area = "IntranetPortal" })' class="btn btn-xs m-0 bg-gradient-gray text-white">Back to files list</a>
</div>
`);
section.first().click(() => content.slideToggle());
section.insertAfter($('#menu_about'));
$("#menu_about").hide();
$("#potree_languages").hide();
$("#sidebar_header").hide();
$("#menu_tools").next().show();
viewer.toggleSidebar();
});
Compare Point Clouds
Note
- First of all, the following application settings must be stored as the table below
Key |
Value |
Description |
Data Type |
Module |
PythonExeAbsPath |
Should contains the absolute path to python exe (ex. C:\miniconda3\envs\waste_comparison_env\python.exe) |
Python Exe From Environment Abs Path |
Integer (0) |
null |
GdalCalcAbsPath |
Should contains the absolute path to gdal_calc (ex. C:\miniconda3\envs\waste_comparison_env\Scripts\gdal_calc.py) |
Gdal Calc From Environment Abs Path |
Integer (0) |
null |
OutputDiffFolderPath |
Uploads\LegalLandfillUploads\TempDiffWasteVolumeComparisonUploads |
Output Diff Folder Path |
Integer (0) |
null |
The comparing process contains several steps:
- Select only two point clouds so the application gets the matching tif files for the selected point clouds.
//get selected point cloud files
var resultGetEntities = await _legalLandfillPointCloudFileService.GetFilteredLegalLandfillPointCloudFiles(selectedFiles);
if (!resultGetEntities.IsSuccess && resultGetEntities.HandleError())
{
return ResultDTO<WasteVolumeComparisonDTO>.Fail(resultGetEntities.ErrMsg!);
}
if (resultGetEntities.Data == null || resultGetEntities.Data.Count < 2 || resultGetEntities.Data.Count > 2)
{
return ResultDTO<WasteVolumeComparisonDTO>.Fail("Expected data is null or does not have required number of elements.");
}
//get tif files for the selected point cloud files
var inputA = Path.Combine(webRootPath, firstElement.FilePath, firstElement.Id.ToString() + "_dsm.tif");
var inputB = Path.Combine(webRootPath, secondElement.FilePath, secondElement.Id.ToString() + "_dsm.tif");
- In the background the application always orders the two selected point cloud files by scan date time so the older file is always first.
var orderedList = resultGetEntities.Data.OrderBy(x => x.ScanDateTime).ToList();
- After this procedure creates temporary differential tif file which contains necessary data for calculating the volume difference between two point clouds.
var gdalRes = await Cli.Wrap(pythonExeAbsPath.Data)
.WithArguments($"\"{gdalCalcAbsPath.Data}\" -A \"{inputA}\" -B \"{inputB}\" --outfile=\"{outputDiffFilePath}\" --calc=\"A-B\" --extent=intersect --overwrite")
.WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync();
- The application read the created differential tif file, uses a formula to calculate the data.
//read the file
using (Dataset dataset = Gdal.Open(outputDiffFilePath, Access.GA_ReadOnly))
{
if (dataset == null)
{
return ResultDTO<double?>.Fail("Could not open the TIF file");
}
Band band = dataset.GetRasterBand(1);
double min, max, mean, stdDev;
band.ComputeStatistics(false, out min, out max, out mean, out stdDev, null, null);
string statisticsValidPercent = band.GetMetadataItem("STATISTICS_VALID_PERCENT", null);
string statisticsMean = band.GetMetadataItem("STATISTICS_MEAN", null);
if (string.IsNullOrEmpty(statisticsValidPercent) || string.IsNullOrEmpty(statisticsMean))
{
return ResultDTO<double?>.Fail("Required statistics are missing from the dataset");
}
double statisticsValidPercentValue = double.Parse(statisticsValidPercent);
double statisticsMeanValue = double.Parse(statisticsMean);
int width = dataset.RasterXSize;
int height = dataset.RasterYSize;
double[] geoTransform = new double[6];
dataset.GetGeoTransform(geoTransform);
double pixelWidth = geoTransform[1];
}
//calculating formula
double wasteVolumeDiffrenceFormula = ((width * pixelWidth * (statisticsValidPercentValue / 100)) *
(height * pixelWidth * (statisticsValidPercentValue / 100)) *
statisticsMeanValue) * -1;
- After all, the temporary differential tif file is deleted to free up space and also because it is no longer needed.
File.Delete(outputDiffFilePath);
- For example, with this functionality the user can view the volume difference between two scanned illegal landfills in cubic meters.