1. Add a topocentric to geocoords converter, allowing to convert every points directly.

2. update odm_georeferencing stage to convert and generate ply, laz, and texture model in UTM coordinate system. Some minor change to use pdal python api instead of command line tool
pull/1851/head
Shuo Zhong 2025-04-02 17:47:59 -04:00
rodzic 9e1e2496f4
commit e17c57dba5
5 zmienionych plików z 70 dodań i 98 usunięć

Wyświetl plik

@ -2,32 +2,8 @@ import numpy as np
from numpy import ndarray from numpy import ndarray
from typing import Tuple from typing import Tuple
from pyproj import Proj from pyproj import Proj
import argparse
from opensfm.geo import TopocentricConverter from opensfm.geo import TopocentricConverter
def parse_pdal_args(pdal_args: dict) -> argparse.Namespace:
def validate_arg(name: str, data_type: type):
if name not in pdal_args:
raise ValueError(f"PDAL arguments should contain {name}.")
try:
data_type(pdal_args[name])
except ValueError:
raise ValueError(f"PDAL argument {name} should be of type {data_type}.")
return data_type(pdal_args[name])
return argparse.Namespace(
reflat=validate_arg('reflat', float),
reflon=validate_arg('reflon', float),
refalt=validate_arg('refalt', float),
x_offset=validate_arg('x_offset', float),
y_offset=validate_arg('y_offset', float),
a_srs=validate_arg('a_srs', str)
)
def topocentric_to_georef_pdal(ins, outs):
args = parse_pdal_args(ins)
outs['X'], outs['Y'], outs['Z'] = topocentric_to_georef(args.reflat, args.reflon, args.refalt, args.a_srs, ins['X'], ins['Y'], ins['Z'], args.x_offset, args.y_offset)
return True
def topocentric_to_georef( def topocentric_to_georef(
reflat: float, reflat: float,
reflon: float, reflon: float,
@ -46,4 +22,35 @@ def topocentric_to_georef(
return easting - x_offset, northing - y_offset, alt return easting - x_offset, northing - y_offset, alt
class TopocentricToProj:
def __init__(self, reflat:float, reflon:float, refalt:float, a_srs:str):
self.reference = TopocentricConverter(reflat, reflon, refalt)
self.projection = Proj(a_srs)
def convert_array(self, arr:ndarray, offset_x:float=0, offset_y:float=0):
x, y, z = arr['X'], arr['Y'], arr['Z']
easting, northing, alt = self.convert_points(x, y, z, offset_x, offset_y)
arr['X'] = easting
arr['Y'] = northing
arr['Z'] = alt
return arr
def convert_points(self, x:ndarray, y:ndarray, z:ndarray, offset_x:float=0, offset_y:float=0):
lat, lon, alt = self.reference.to_lla(x, y, z)
easting, northing = self.projection(lon, lat)
return easting - offset_x, northing - offset_y, alt
def convert_obj(self, input_obj:str, output_obj:str, offset_x:float=0, offset_y:float=0):
with open(input_obj, 'r') as fin:
with open(output_obj, 'w') as fout:
lines = fin.readlines()
for line in lines:
if line.startswith("v "):
v = np.fromstring(line.strip()[2:] + " 1", sep=' ', dtype=float)
vt = self.convert_points(v[0], v[1], v[2], offset_x, offset_y)
fout.write("v " + " ".join(map(str, list(vt))) + '\n')
else:
fout.write(line)

Wyświetl plik

@ -631,7 +631,7 @@ class OSFMContext:
def reference(self): def reference(self):
ds = DataSet(self.opensfm_project_path) ds = DataSet(self.opensfm_project_path)
return ds.load_reference_lla() return ds.load_reference()
def name(self): def name(self):
return os.path.basename(os.path.abspath(self.path(".."))) return os.path.basename(os.path.abspath(self.path("..")))

Wyświetl plik

@ -374,7 +374,7 @@ class ODM_Tree(object):
self.odm_25dmeshing_log = os.path.join(self.odm_meshing, 'odm_25dmeshing_log.txt') self.odm_25dmeshing_log = os.path.join(self.odm_meshing, 'odm_25dmeshing_log.txt')
# texturing # texturing
self.odm_textured_model_obj_topo = os.path.join(self.odm_texturing, 'odm_textured_model_topocentric.obj') self.odm_textured_model_obj_topo = 'odm_textured_model_topocentric.obj'
self.odm_textured_model_obj = 'odm_textured_model_geo.obj' self.odm_textured_model_obj = 'odm_textured_model_geo.obj'
self.odm_textured_model_glb = 'odm_textured_model_geo.glb' self.odm_textured_model_glb = 'odm_textured_model_geo.glb'

Wyświetl plik

@ -71,7 +71,6 @@ class ODMMvsTexStage(types.ODM_Stage):
odm_textured_model_obj = os.path.join(r['out_dir'], tree.odm_textured_model_obj_topo) odm_textured_model_obj = os.path.join(r['out_dir'], tree.odm_textured_model_obj_topo)
unaligned_obj = io.related_file_path(odm_textured_model_obj, postfix="_unaligned") unaligned_obj = io.related_file_path(odm_textured_model_obj, postfix="_unaligned")
if not io.file_exists(odm_textured_model_obj) or self.rerun(): if not io.file_exists(odm_textured_model_obj) or self.rerun():
log.ODM_INFO('Writing MVS Textured file in: %s' log.ODM_INFO('Writing MVS Textured file in: %s'
% odm_textured_model_obj) % odm_textured_model_obj)
@ -94,7 +93,7 @@ class ODMMvsTexStage(types.ODM_Stage):
# mvstex definitions # mvstex definitions
kwargs = { kwargs = {
'bin': context.mvstex_path, 'bin': context.mvstex_path,
'out_dir': os.path.join(r['out_dir'], "odm_textured_model_geo"), 'out_dir': os.path.join(r['out_dir'], tree.odm_textured_model_obj_topo.split('.obj')[0]),
'model': r['model'], 'model': r['model'],
'dataTerm': 'gmi', 'dataTerm': 'gmi',
'outlierRemovalType': 'gauss_clamping', 'outlierRemovalType': 'gauss_clamping',

Wyświetl plik

@ -8,7 +8,6 @@ import json
import zipfile import zipfile
import math import math
from collections import OrderedDict from collections import OrderedDict
from numpy import rec
from pyproj import CRS from pyproj import CRS
import pdal import pdal
@ -25,6 +24,7 @@ from opendm.osfm import OSFMContext
from opendm.boundary import as_polygon, export_to_bounds_files from opendm.boundary import as_polygon, export_to_bounds_files
from opendm.align import compute_alignment_matrix, transform_point_cloud, transform_obj from opendm.align import compute_alignment_matrix, transform_point_cloud, transform_obj
from opendm.utils import np_to_json from opendm.utils import np_to_json
from opendm.georef import TopocentricToProj
class ODMGeoreferencingStage(types.ODM_Stage): class ODMGeoreferencingStage(types.ODM_Stage):
def process(self, args, outputs): def process(self, args, outputs):
@ -120,79 +120,50 @@ class ODMGeoreferencingStage(types.ODM_Stage):
# prepare pipeline stage for topocentric to georeferenced conversion # prepare pipeline stage for topocentric to georeferenced conversion
octx = OSFMContext(tree.opensfm) octx = OSFMContext(tree.opensfm)
reference = octx.reference() reference = octx.reference()
pdalargs = { converter = TopocentricToProj(reference.lat, reference.lon, reference.alt, reconstruction.georef.proj4())
'reflat': reference.lat,
'reflon': reference.lon,
'refalt': reference.alt,
'a_srs': reconstruction.georef.proj4(),
'x_offset': reconstruction.georef.utm_east_offset,
'y_offset': reconstruction.georef.utm_north_offset
}
topo_to_georef_def = {
"type": "filters.python",
"module": "opendm.georef",
"function": "topocentric_to_georef_pdal",
"pdalargs": json.dumps(pdalargs)
}
if not io.file_exists(tree.filtered_point_cloud) or self.rerun(): if not io.file_exists(tree.filtered_point_cloud) or self.rerun():
log.ODM_INFO("Georeferecing filtered point cloud") log.ODM_INFO("Georeferecing filtered point cloud")
if reconstruction.is_georeferenced(): if reconstruction.is_georeferenced():
ply_georeferencing_pipeline = [ pipeline = pdal.Reader.ply(tree.filtered_point_cloud_topo).pipeline()
{ pipeline.execute()
"type": "readers.ply", arr = pipeline.arrays[0]
"filename": tree.filtered_point_cloud_topo arr = converter.convert_array(
}, arr,
topo_to_georef_def, reconstruction.georef.utm_east_offset,
{ reconstruction.georef.utm_north_offset
"type": "writers.ply", )
"filename": tree.filtered_point_cloud pipeline = pdal.Writer.ply(tree.filtered_point_cloud).pipeline(arr)
}
]
pipeline = pdal.Pipeline(json.dumps(ply_georeferencing_pipeline))
pipeline.execute() pipeline.execute()
else: else:
shutil.copy(tree.filtered_point_cloud_topo, tree.filtered_point_cloud) shutil.copy(tree.filtered_point_cloud_topo, tree.filtered_point_cloud)
if not io.file_exists(tree.odm_textured_model_geo_obj) or self.rerun(): if not io.file_exists(os.path.join(tree.odm_texturing, tree.odm_textured_model_obj)) or self.rerun():
log.ODM_INFO("Georeferecing textured model") log.ODM_INFO("Georeferecing textured model")
obj_in = os.path.join(tree.odm_texturing, tree.odm_textured_model_obj_topo)
obj_out = os.path.join(tree.odm_texturing, tree.odm_textured_model_obj)
if reconstruction.is_georeferenced(): if reconstruction.is_georeferenced():
obj_georeferencing_pipeline = [ converter.convert_obj(
{ obj_in,
"type": "readers.obj", obj_out,
"filename": tree.odm_textured_model_obj reconstruction.georef.utm_east_offset,
}, reconstruction.georef.utm_north_offset
topo_to_georef_def, )
{
"type": "writers.obj",
"filename": tree.odm_textured_model_geo_obj
}
]
pipeline = pdal.Pipeline(json.dumps(obj_georeferencing_pipeline))
pipeline.execute()
else: else:
shutil.copy(tree.odm_textured_model_obj, tree.odm_textured_model_geo_obj) shutil.copy(obj_in, obj_out)
if not io.file_exists(tree.odm_georeferencing_model_laz) or self.rerun(): if not io.file_exists(tree.odm_georeferencing_model_laz) or self.rerun():
las_georeferencing_pipeline = [ pipeline = pdal.Pipeline()
{ pipeline |= pdal.Reader.ply(tree.filtered_point_cloud)
"type": "readers.las", pipeline |= pdal.Filter.ferry(dimensions="views => UserData")
"filename": tree.filtered_point_cloud
},
{
"type": "filters.ferry",
"dimensions": "views => UserData"
}
]
if reconstruction.is_georeferenced(): if reconstruction.is_georeferenced():
log.ODM_INFO("Georeferencing point cloud") log.ODM_INFO("Georeferencing point cloud")
utmoffset = reconstruction.georef.utm_offset() utmoffset = reconstruction.georef.utm_offset()
las_georeferencing_pipeline.append({ pipeline |= pdal.Filter.transformation(
"type": "filters.transformation", matrix=f"1 0 0 {utmoffset[0]} 0 1 0 {utmoffset[1]} 0 0 1 0 0 0 0 1"
"matrix": f"1 0 0 {utmoffset[0]} 0 1 0 {utmoffset[1]} 0 0 1 0 0 0 0 1" )
})
# Establish appropriate las scale for export # Establish appropriate las scale for export
las_scale = 0.001 las_scale = 0.001
@ -216,7 +187,6 @@ class ODMGeoreferencingStage(types.ODM_Stage):
log.ODM_INFO("No point_cloud_stats.json found. Using default las scale: %s" % las_scale) log.ODM_INFO("No point_cloud_stats.json found. Using default las scale: %s" % las_scale)
las_writer_def = { las_writer_def = {
"type": "writers.las",
"filename": tree.odm_georeferencing_model_laz, "filename": tree.odm_georeferencing_model_laz,
"a_srs": reconstruction.georef.proj4(), "a_srs": reconstruction.georef.proj4(),
"offset_x": utmoffset[0], "offset_x": utmoffset[0],
@ -230,9 +200,6 @@ class ODMGeoreferencingStage(types.ODM_Stage):
if reconstruction.has_gcp() and io.file_exists(gcp_geojson_zip_export_file): if reconstruction.has_gcp() and io.file_exists(gcp_geojson_zip_export_file):
if os.path.getsize(gcp_geojson_zip_export_file) <= 65535: if os.path.getsize(gcp_geojson_zip_export_file) <= 65535:
log.ODM_INFO("Embedding GCP info in point cloud") log.ODM_INFO("Embedding GCP info in point cloud")
params += [
'--writers.las.vlrs="{\\\"filename\\\": \\\"%s\\\", \\\"user_id\\\": \\\"ODM\\\", \\\"record_id\\\": 2, \\\"description\\\": \\\"Ground Control Points (zip)\\\"}"' % gcp_geojson_zip_export_file.replace(os.sep, "/")
]
las_writer_def["vlrs"] = json.dumps( las_writer_def["vlrs"] = json.dumps(
{ {
"filename": gcp_geojson_zip_export_file.replace(os.sep, "/"), "filename": gcp_geojson_zip_export_file.replace(os.sep, "/"),
@ -245,9 +212,10 @@ class ODMGeoreferencingStage(types.ODM_Stage):
else: else:
log.ODM_WARNING("Cannot embed GCP info in point cloud, %s is too large" % gcp_geojson_zip_export_file) log.ODM_WARNING("Cannot embed GCP info in point cloud, %s is too large" % gcp_geojson_zip_export_file)
las_georeferencing_pipeline.append(las_writer_def) pipeline |= pdal.Writer.las(
**las_writer_def
)
pipeline = pdal.Pipeline(json.dumps(las_georeferencing_pipeline))
pipeline.execute() pipeline.execute()
self.update_progress(50) self.update_progress(50)
@ -282,11 +250,9 @@ class ODMGeoreferencingStage(types.ODM_Stage):
export_to_bounds_files(outputs['boundary'], reconstruction.get_proj_srs(), bounds_json, bounds_gpkg) export_to_bounds_files(outputs['boundary'], reconstruction.get_proj_srs(), bounds_json, bounds_gpkg)
else: else:
log.ODM_INFO("Converting point cloud (non-georeferenced)") log.ODM_INFO("Converting point cloud (non-georeferenced)")
las_georeferencing_pipeline.append({ pipeline |= pdal.Writer.las(
"type": "writers.las", tree.odm_georeferencing_model_laz
"filename": tree.odm_georeferencing_model_laz )
})
pipeline = pdal.Pipeline(json.dumps(las_georeferencing_pipeline))
pipeline.execute() pipeline.execute()