2018-06-12 21:36:24 +00:00
|
|
|
from __future__ import absolute_import
|
2021-06-02 19:38:03 +00:00
|
|
|
import os, shutil, sys, struct, random, math, platform
|
2018-06-10 18:57:16 +00:00
|
|
|
from opendm.dem import commands
|
|
|
|
from opendm import system
|
|
|
|
from opendm import log
|
2018-07-03 15:24:47 +00:00
|
|
|
from opendm import context
|
2021-06-21 14:33:42 +00:00
|
|
|
from opendm import concurrency
|
2020-09-11 16:34:38 +00:00
|
|
|
from scipy import signal
|
2018-06-18 13:54:09 +00:00
|
|
|
import numpy as np
|
2018-06-10 18:57:16 +00:00
|
|
|
|
2019-06-08 23:28:58 +00:00
|
|
|
def create_25dmesh(inPointCloud, outMesh, dsm_radius=0.07, dsm_resolution=0.05, depth=8, samples=1, maxVertexCount=100000, verbose=False, available_cores=None, method='gridded', smooth_dsm=True):
|
2018-06-10 18:57:16 +00:00
|
|
|
# Create DSM from point cloud
|
|
|
|
|
|
|
|
# Create temporary directory
|
|
|
|
mesh_directory = os.path.dirname(outMesh)
|
|
|
|
tmp_directory = os.path.join(mesh_directory, 'tmp')
|
2018-10-07 21:55:10 +00:00
|
|
|
if os.path.exists(tmp_directory):
|
|
|
|
shutil.rmtree(tmp_directory)
|
2018-06-10 18:57:16 +00:00
|
|
|
os.mkdir(tmp_directory)
|
|
|
|
log.ODM_INFO('Created temporary directory: %s' % tmp_directory)
|
|
|
|
|
2018-10-17 15:35:18 +00:00
|
|
|
radius_steps = [dsm_radius]
|
2022-03-03 20:10:31 +00:00
|
|
|
for _ in range(2):
|
|
|
|
radius_steps.append(radius_steps[-1] * 2) # 2 is arbitrary
|
2018-06-10 18:57:16 +00:00
|
|
|
|
|
|
|
log.ODM_INFO('Creating DSM for 2.5D mesh')
|
|
|
|
|
2019-04-11 20:29:53 +00:00
|
|
|
commands.create_dem(
|
|
|
|
inPointCloud,
|
2018-06-10 18:57:16 +00:00
|
|
|
'mesh_dsm',
|
2019-04-11 20:29:53 +00:00
|
|
|
output_type='max',
|
2020-09-11 15:05:34 +00:00
|
|
|
radiuses=list(map(str, radius_steps)),
|
2018-06-10 18:57:16 +00:00
|
|
|
gapfill=True,
|
|
|
|
outdir=tmp_directory,
|
|
|
|
resolution=dsm_resolution,
|
2018-07-03 17:01:18 +00:00
|
|
|
verbose=verbose,
|
2019-06-08 23:28:58 +00:00
|
|
|
max_workers=available_cores,
|
|
|
|
apply_smoothing=smooth_dsm
|
2018-06-10 18:57:16 +00:00
|
|
|
)
|
|
|
|
|
2018-10-28 23:57:08 +00:00
|
|
|
if method == 'gridded':
|
2020-08-31 17:07:08 +00:00
|
|
|
mesh = dem_to_mesh_gridded(os.path.join(tmp_directory, 'mesh_dsm.tif'), outMesh, maxVertexCount, verbose, maxConcurrency=max(1, available_cores))
|
2018-10-28 23:57:08 +00:00
|
|
|
elif method == 'poisson':
|
|
|
|
dsm_points = dem_to_points(os.path.join(tmp_directory, 'mesh_dsm.tif'), os.path.join(tmp_directory, 'dsm_points.ply'), verbose)
|
|
|
|
mesh = screened_poisson_reconstruction(dsm_points, outMesh, depth=depth,
|
|
|
|
samples=samples,
|
|
|
|
maxVertexCount=maxVertexCount,
|
2019-05-31 19:19:59 +00:00
|
|
|
threads=max(1, available_cores - 1), # poissonrecon can get stuck on some machines if --threads == all cores
|
2018-10-28 23:57:08 +00:00
|
|
|
verbose=verbose)
|
|
|
|
else:
|
|
|
|
raise 'Not a valid method: ' + method
|
2018-07-06 14:01:33 +00:00
|
|
|
|
2018-06-10 18:57:16 +00:00
|
|
|
# Cleanup tmp
|
2018-10-17 17:14:03 +00:00
|
|
|
if os.path.exists(tmp_directory):
|
|
|
|
shutil.rmtree(tmp_directory)
|
2018-06-10 18:57:16 +00:00
|
|
|
|
|
|
|
return mesh
|
|
|
|
|
2018-10-28 23:57:08 +00:00
|
|
|
|
|
|
|
def dem_to_points(inGeotiff, outPointCloud, verbose=False):
|
|
|
|
log.ODM_INFO('Sampling points from DSM: %s' % inGeotiff)
|
|
|
|
|
|
|
|
kwargs = {
|
|
|
|
'bin': context.dem2points_path,
|
|
|
|
'outfile': outPointCloud,
|
|
|
|
'infile': inGeotiff,
|
|
|
|
'verbose': '-verbose' if verbose else ''
|
|
|
|
}
|
|
|
|
|
2021-05-17 17:25:52 +00:00
|
|
|
system.run('"{bin}" -inputFile "{infile}" '
|
|
|
|
'-outputFile "{outfile}" '
|
2018-10-28 23:57:08 +00:00
|
|
|
'-skirtHeightThreshold 1.5 '
|
|
|
|
'-skirtIncrements 0.2 '
|
|
|
|
'-skirtHeightCap 100 '
|
|
|
|
' {verbose} '.format(**kwargs))
|
|
|
|
|
|
|
|
return outPointCloud
|
|
|
|
|
|
|
|
|
2020-08-31 17:07:08 +00:00
|
|
|
def dem_to_mesh_gridded(inGeotiff, outMesh, maxVertexCount, verbose=False, maxConcurrency=1):
|
2018-10-07 21:55:10 +00:00
|
|
|
log.ODM_INFO('Creating mesh from DSM: %s' % inGeotiff)
|
2018-06-10 18:57:16 +00:00
|
|
|
|
2019-04-23 16:05:05 +00:00
|
|
|
mesh_path, mesh_filename = os.path.split(outMesh)
|
|
|
|
# mesh_path = path/to
|
|
|
|
# mesh_filename = odm_mesh.ply
|
|
|
|
|
|
|
|
basename, ext = os.path.splitext(mesh_filename)
|
|
|
|
# basename = odm_mesh
|
|
|
|
# ext = .ply
|
|
|
|
|
|
|
|
outMeshDirty = os.path.join(mesh_path, "{}.dirty{}".format(basename, ext))
|
|
|
|
|
2020-08-31 17:07:08 +00:00
|
|
|
# This should work without issues most of the times,
|
|
|
|
# but just in case we lower maxConcurrency if it fails.
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
kwargs = {
|
|
|
|
'bin': context.dem2mesh_path,
|
|
|
|
'outfile': outMeshDirty,
|
|
|
|
'infile': inGeotiff,
|
|
|
|
'maxVertexCount': maxVertexCount,
|
|
|
|
'maxConcurrency': maxConcurrency,
|
|
|
|
'verbose': '-verbose' if verbose else ''
|
|
|
|
}
|
2021-05-17 17:25:52 +00:00
|
|
|
system.run('"{bin}" -inputFile "{infile}" '
|
|
|
|
'-outputFile "{outfile}" '
|
2020-08-31 17:07:08 +00:00
|
|
|
'-maxTileLength 2000 '
|
|
|
|
'-maxVertexCount {maxVertexCount} '
|
|
|
|
'-maxConcurrency {maxConcurrency} '
|
2022-02-14 02:59:01 +00:00
|
|
|
'-edgeSwapThreshold 0.15 '
|
2020-08-31 17:07:08 +00:00
|
|
|
' {verbose} '.format(**kwargs))
|
|
|
|
break
|
|
|
|
except Exception as e:
|
|
|
|
maxConcurrency = math.floor(maxConcurrency / 2)
|
|
|
|
if maxConcurrency >= 1:
|
|
|
|
log.ODM_WARNING("dem2mesh failed, retrying with lower concurrency (%s) in case this is a memory issue" % maxConcurrency)
|
|
|
|
else:
|
|
|
|
raise e
|
2018-06-10 18:57:16 +00:00
|
|
|
|
|
|
|
|
2019-04-23 16:05:05 +00:00
|
|
|
# Cleanup and reduce vertex count if necessary
|
|
|
|
# (as dem2mesh cannot guarantee that we'll have the target vertex count)
|
|
|
|
cleanupArgs = {
|
2021-04-26 15:12:33 +00:00
|
|
|
'reconstructmesh': context.omvs_reconstructmesh_path,
|
2019-04-23 16:05:05 +00:00
|
|
|
'outfile': outMesh,
|
|
|
|
'infile': outMeshDirty,
|
2021-05-03 16:38:36 +00:00
|
|
|
'max_faces': maxVertexCount * 2
|
2019-04-23 16:05:05 +00:00
|
|
|
}
|
|
|
|
|
2021-05-17 17:25:52 +00:00
|
|
|
system.run('"{reconstructmesh}" -i "{infile}" '
|
2021-04-26 15:12:33 +00:00
|
|
|
'-o "{outfile}" '
|
2021-10-12 20:43:42 +00:00
|
|
|
'--remove-spikes 0 --remove-spurious 0 --smooth 0 '
|
2021-12-03 16:08:56 +00:00
|
|
|
'--target-face-num {max_faces} -v 0'.format(**cleanupArgs))
|
2019-04-23 16:05:05 +00:00
|
|
|
|
|
|
|
# Delete intermediate results
|
|
|
|
os.remove(outMeshDirty)
|
|
|
|
|
|
|
|
return outMesh
|
2018-06-10 18:57:16 +00:00
|
|
|
|
|
|
|
|
2018-07-06 14:01:33 +00:00
|
|
|
def screened_poisson_reconstruction(inPointCloud, outMesh, depth = 8, samples = 1, maxVertexCount=100000, pointWeight=4, threads=context.num_cores, verbose=False):
|
2018-07-03 15:24:47 +00:00
|
|
|
|
|
|
|
mesh_path, mesh_filename = os.path.split(outMesh)
|
|
|
|
# mesh_path = path/to
|
|
|
|
# mesh_filename = odm_mesh.ply
|
|
|
|
|
|
|
|
basename, ext = os.path.splitext(mesh_filename)
|
|
|
|
# basename = odm_mesh
|
|
|
|
# ext = .ply
|
|
|
|
|
|
|
|
outMeshDirty = os.path.join(mesh_path, "{}.dirty{}".format(basename, ext))
|
2021-06-21 14:33:42 +00:00
|
|
|
if os.path.isfile(outMeshDirty):
|
|
|
|
os.remove(outMeshDirty)
|
2021-06-02 19:38:03 +00:00
|
|
|
|
|
|
|
# Since PoissonRecon has some kind of a race condition on ppc64el, and this helps...
|
|
|
|
if platform.machine() == 'ppc64le':
|
|
|
|
log.ODM_WARNING("ppc64le platform detected, forcing single-threaded operation for PoissonRecon")
|
|
|
|
threads = 1
|
2018-06-10 18:57:16 +00:00
|
|
|
|
2021-06-21 14:33:42 +00:00
|
|
|
while True:
|
|
|
|
poissonReconArgs = {
|
|
|
|
'bin': context.poisson_recon_path,
|
|
|
|
'outfile': outMeshDirty,
|
|
|
|
'infile': inPointCloud,
|
|
|
|
'depth': depth,
|
|
|
|
'samples': samples,
|
|
|
|
'pointWeight': pointWeight,
|
|
|
|
'threads': int(threads),
|
|
|
|
'verbose': '--verbose' if verbose else ''
|
|
|
|
}
|
|
|
|
|
|
|
|
# Run PoissonRecon
|
2021-06-22 16:23:31 +00:00
|
|
|
try:
|
|
|
|
system.run('"{bin}" --in "{infile}" '
|
|
|
|
'--out "{outfile}" '
|
|
|
|
'--depth {depth} '
|
|
|
|
'--pointWeight {pointWeight} '
|
|
|
|
'--samplesPerNode {samples} '
|
|
|
|
'--threads {threads} '
|
|
|
|
'--bType 2 '
|
|
|
|
'--linearFit '
|
|
|
|
'{verbose}'.format(**poissonReconArgs))
|
|
|
|
except Exception as e:
|
|
|
|
log.ODM_WARNING(str(e))
|
|
|
|
|
2021-06-21 14:33:42 +00:00
|
|
|
if os.path.isfile(outMeshDirty):
|
|
|
|
break # Done!
|
|
|
|
else:
|
|
|
|
|
|
|
|
# PoissonRecon will sometimes fail due to race conditions
|
|
|
|
# on certain machines, especially on Windows
|
|
|
|
threads //= 2
|
|
|
|
|
|
|
|
if threads < 1:
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
log.ODM_WARNING("PoissonRecon failed with %s threads, let's retry with %s..." % (threads, threads // 2))
|
|
|
|
|
2018-07-03 15:24:47 +00:00
|
|
|
|
|
|
|
# Cleanup and reduce vertex count if necessary
|
|
|
|
cleanupArgs = {
|
2021-04-26 15:12:33 +00:00
|
|
|
'reconstructmesh': context.omvs_reconstructmesh_path,
|
2018-07-03 15:24:47 +00:00
|
|
|
'outfile': outMesh,
|
2021-05-17 17:25:52 +00:00
|
|
|
'infile':outMeshDirty,
|
2021-05-03 16:38:36 +00:00
|
|
|
'max_faces': maxVertexCount * 2
|
2018-07-03 15:24:47 +00:00
|
|
|
}
|
|
|
|
|
2021-05-17 17:25:52 +00:00
|
|
|
system.run('"{reconstructmesh}" -i "{infile}" '
|
2021-04-26 15:12:33 +00:00
|
|
|
'-o "{outfile}" '
|
2021-06-09 17:39:26 +00:00
|
|
|
'--remove-spikes 0 --remove-spurious 20 --smooth 0 '
|
2021-12-03 16:08:56 +00:00
|
|
|
'--target-face-num {max_faces} -v 0'.format(**cleanupArgs))
|
2018-07-03 15:24:47 +00:00
|
|
|
|
|
|
|
# Delete intermediate results
|
|
|
|
os.remove(outMeshDirty)
|
2018-06-10 18:57:16 +00:00
|
|
|
|
|
|
|
return outMesh
|