Merge pull request #1068 from pierotofy/pseudogeo

Pseudo-georeferencing for datasets missing GPS/GCP information, AKA putting bananas on the map
pull/1073/head
Piero Toffanin 2020-02-04 21:46:19 -06:00 zatwierdzone przez GitHub
commit ed551d6218
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
7 zmienionych plików z 102 dodań i 27 usunięć

Wyświetl plik

@ -21,7 +21,7 @@ def rounded_gsd(reconstruction_json, default_value=None, ndigits=0, ignore_gsd=F
return default_value
def image_max_size(photos, target_resolution, reconstruction_json, gsd_error_estimate = 0.5, ignore_gsd=False):
def image_max_size(photos, target_resolution, reconstruction_json, gsd_error_estimate = 0.5, ignore_gsd=False, has_gcp=False):
"""
:param photos images database
:param target_resolution resolution the user wants have in cm / pixel
@ -36,7 +36,7 @@ def image_max_size(photos, target_resolution, reconstruction_json, gsd_error_est
if ignore_gsd:
isf = 1.0
else:
isf = image_scale_factor(target_resolution, reconstruction_json, gsd_error_estimate)
isf = image_scale_factor(target_resolution, reconstruction_json, gsd_error_estimate, has_gcp=has_gcp)
for p in photos:
max_width = max(p.width, max_width)
@ -44,7 +44,7 @@ def image_max_size(photos, target_resolution, reconstruction_json, gsd_error_est
return int(math.ceil(max(max_width, max_height) * isf))
def image_scale_factor(target_resolution, reconstruction_json, gsd_error_estimate = 0.5):
def image_scale_factor(target_resolution, reconstruction_json, gsd_error_estimate = 0.5, has_gcp=False):
"""
:param target_resolution resolution the user wants have in cm / pixel
:param reconstruction_json path to OpenSfM's reconstruction.json
@ -52,7 +52,7 @@ def image_scale_factor(target_resolution, reconstruction_json, gsd_error_estimat
:return A down-scale (<= 1) value to apply to images to achieve the target resolution by comparing the current GSD of the reconstruction.
If a GSD cannot be computed, it just returns 1. Returned scale values are never higher than 1.
"""
gsd = opensfm_reconstruction_average_gsd(reconstruction_json)
gsd = opensfm_reconstruction_average_gsd(reconstruction_json, use_all_shots=has_gcp)
if gsd is not None and target_resolution > 0:
gsd = gsd * (1 + gsd_error_estimate)
@ -61,7 +61,7 @@ def image_scale_factor(target_resolution, reconstruction_json, gsd_error_estimat
return 1
def cap_resolution(resolution, reconstruction_json, gsd_error_estimate = 0.1, ignore_gsd=False):
def cap_resolution(resolution, reconstruction_json, gsd_error_estimate = 0.1, ignore_gsd=False, ignore_resolution=False, has_gcp=False):
"""
:param resolution resolution in cm / pixel
:param reconstruction_json path to OpenSfM's reconstruction.json
@ -73,11 +73,11 @@ def cap_resolution(resolution, reconstruction_json, gsd_error_estimate = 0.1, ig
if ignore_gsd:
return resolution
gsd = opensfm_reconstruction_average_gsd(reconstruction_json)
gsd = opensfm_reconstruction_average_gsd(reconstruction_json, use_all_shots=has_gcp or ignore_resolution)
if gsd is not None:
gsd = gsd * (1 - gsd_error_estimate)
if gsd > resolution:
if gsd > resolution or ignore_resolution:
log.ODM_WARNING('Maximum resolution set to GSD - {}% ({} cm / pixel, requested resolution was {} cm / pixel)'.format(gsd_error_estimate * 100, round(gsd, 2), round(resolution, 2)))
return gsd
else:
@ -88,7 +88,7 @@ def cap_resolution(resolution, reconstruction_json, gsd_error_estimate = 0.1, ig
@lru_cache(maxsize=None)
def opensfm_reconstruction_average_gsd(reconstruction_json):
def opensfm_reconstruction_average_gsd(reconstruction_json, use_all_shots=False):
"""
Computes the average Ground Sampling Distance of an OpenSfM reconstruction.
:param reconstruction_json path to OpenSfM's reconstruction.json
@ -114,9 +114,8 @@ def opensfm_reconstruction_average_gsd(reconstruction_json):
gsds = []
for shotImage in reconstruction['shots']:
shot = reconstruction['shots'][shotImage]
if shot['gps_dop'] < 999999:
if use_all_shots or shot['gps_dop'] < 999999:
camera = reconstruction['cameras'][shot['camera']]
shot_height = shot['translation'][2]
focal_ratio = camera.get('focal', camera.get('focal_x'))
if not focal_ratio:

Wyświetl plik

@ -0,0 +1,23 @@
import osr
import gdal
from gdalconst import GA_Update
from opendm import io
from opendm import log
def add_pseudo_georeferencing(geotiff, scale=1.0):
if not io.file_exists(geotiff):
log.ODM_WARNING("Cannot add pseudo georeferencing, %s does not exist" % geotiff)
return
try:
log.ODM_INFO("Adding pseudo georeferencing (raster should show up at the equator) to %s" % geotiff)
dst_ds = gdal.Open(geotiff, GA_Update)
srs = osr.SpatialReference()
srs.ImportFromProj4('+proj=utm +zone=30 +ellps=WGS84 +datum=WGS84 +units=m +no_defs')
dst_ds.SetProjection( srs.ExportToWkt() )
dst_ds.SetGeoTransform( [ 0.0, scale, 0.0, 0.0, 0.0, -scale ] )
dst_ds = None
except Exception as e:
log.ODM_WARNING("Cannot add psuedo georeferencing to %s (%s), skipping..." % (geotiff, str(e)))

Wyświetl plik

@ -190,6 +190,9 @@ class ODM_Reconstruction(object):
def is_georeferenced(self):
return self.georef is not None
def has_gcp(self):
return self.is_georeferenced() and self.gcp is not None
def georeference_with_gcp(self, gcp_file, output_coords_file, output_gcp_file, rerun=False):
if not io.file_exists(output_coords_file) or not io.file_exists(output_gcp_file) or rerun:
gcp = GCPFile(gcp_file)
@ -238,7 +241,7 @@ class ODM_Reconstruction(object):
self.georef = ODM_GeoRef.FromCoordsFile(output_coords_file)
except:
log.ODM_WARNING('Could not generate coordinates file. An orthophoto will not be generated.')
log.ODM_WARNING('Could not generate coordinates file. The orthophoto will not be georeferenced.')
self.gcp = GCPFile(None)
return self.georef

Wyświetl plik

@ -9,28 +9,55 @@ from opendm import types
from opendm import gsd
from opendm.dem import commands, utils
from opendm.cropper import Cropper
from opendm import pseudogeo
class ODMDEMStage(types.ODM_Stage):
def process(self, args, outputs):
tree = outputs['tree']
las_model_found = io.file_exists(tree.odm_georeferencing_model_laz)
reconstruction = outputs['reconstruction']
dem_input = tree.odm_georeferencing_model_laz
pc_model_found = io.file_exists(dem_input)
ignore_resolution = False
pseudo_georeference = False
if not reconstruction.is_georeferenced():
# Special case to clear previous run point cloud
# (NodeODM will generate a fake georeferenced laz during postprocessing
# with non-georeferenced datasets). odm_georeferencing_model_laz should
# not be here! Perhaps we should improve this.
if io.file_exists(tree.odm_georeferencing_model_laz) and self.rerun():
os.remove(tree.odm_georeferencing_model_laz)
log.ODM_WARNING("Not georeferenced, using ungeoreferenced point cloud...")
dem_input = tree.path("odm_filterpoints", "point_cloud.ply")
pc_model_found = io.file_exists(dem_input)
ignore_resolution = True
pseudo_georeference = True
resolution = gsd.cap_resolution(args.dem_resolution, tree.opensfm_reconstruction,
gsd_error_estimate=-3,
ignore_gsd=args.ignore_gsd,
ignore_resolution=ignore_resolution,
has_gcp=reconstruction.has_gcp())
log.ODM_INFO('Classify: ' + str(args.pc_classify))
log.ODM_INFO('Create DSM: ' + str(args.dsm))
log.ODM_INFO('Create DTM: ' + str(args.dtm))
log.ODM_INFO('DEM input file {0} found: {1}'.format(tree.odm_georeferencing_model_laz, str(las_model_found)))
log.ODM_INFO('DEM input file {0} found: {1}'.format(dem_input, str(pc_model_found)))
# define paths and create working directories
odm_dem_root = tree.path('odm_dem')
if not io.dir_exists(odm_dem_root):
system.mkdir_p(odm_dem_root)
if args.pc_classify and las_model_found:
if args.pc_classify and pc_model_found:
pc_classify_marker = os.path.join(odm_dem_root, 'pc_classify_done.txt')
if not io.file_exists(pc_classify_marker) or self.rerun():
log.ODM_INFO("Classifying {} using Simple Morphological Filter".format(tree.odm_georeferencing_model_laz))
commands.classify(tree.odm_georeferencing_model_laz,
log.ODM_INFO("Classifying {} using Simple Morphological Filter".format(dem_input))
commands.classify(dem_input,
args.smrf_scalar,
args.smrf_slope,
args.smrf_threshold,
@ -49,7 +76,7 @@ class ODMDEMStage(types.ODM_Stage):
self.update_progress(progress)
# Do we need to process anything here?
if (args.dsm or args.dtm) and las_model_found:
if (args.dsm or args.dtm) and pc_model_found:
dsm_output_filename = os.path.join(odm_dem_root, 'dsm.tif')
dtm_output_filename = os.path.join(odm_dem_root, 'dtm.tif')
@ -61,15 +88,14 @@ class ODMDEMStage(types.ODM_Stage):
if args.dsm or (args.dtm and args.dem_euclidean_map): products.append('dsm')
if args.dtm: products.append('dtm')
resolution = gsd.cap_resolution(args.dem_resolution, tree.opensfm_reconstruction, gsd_error_estimate=-3, ignore_gsd=args.ignore_gsd)
radius_steps = [(resolution / 100.0) / 2.0]
for _ in range(args.dem_gapfill_steps - 1):
radius_steps.append(radius_steps[-1] * 2) # 2 is arbitrary, maybe there's a better value?
for product in products:
commands.create_dem(
tree.odm_georeferencing_model_laz,
dem_input,
product,
output_type='idw' if product == 'dtm' else 'max',
radiuses=map(str, radius_steps),
@ -99,6 +125,10 @@ class ODMDEMStage(types.ODM_Stage):
commands.compute_euclidean_map(unfilled_dem_path,
io.related_file_path(dem_geotiff_path, postfix=".euclideand"),
overwrite=True)
if pseudo_georeference:
# 0.1 is arbitrary
pseudogeo.add_pseudo_georeferencing(dem_geotiff_path, 0.1)
progress += 30
self.update_progress(progress)

Wyświetl plik

@ -42,7 +42,10 @@ class ODMeshingStage(types.ODM_Stage):
if not io.file_exists(tree.odm_25dmesh) or self.rerun():
log.ODM_INFO('Writing ODM 2.5D Mesh file in: %s' % tree.odm_25dmesh)
ortho_resolution = gsd.cap_resolution(args.orthophoto_resolution, tree.opensfm_reconstruction, ignore_gsd=args.ignore_gsd) / 100.0
ortho_resolution = gsd.cap_resolution(args.orthophoto_resolution, tree.opensfm_reconstruction,
ignore_gsd=args.ignore_gsd,
ignore_resolution=not reconstruction.is_georeferenced(),
has_gcp=reconstruction.has_gcp()) / 100.0
dsm_multiplier = max(1.0, gsd.rounded_gsd(tree.opensfm_reconstruction, default_value=4, ndigits=3, ignore_gsd=args.ignore_gsd))

Wyświetl plik

@ -10,6 +10,7 @@ from opendm import orthophoto
from opendm.concurrency import get_max_memory
from opendm.cutline import compute_cutline
from pipes import quote
from opendm import pseudogeo
class ODMOrthoPhotoStage(types.ODM_Stage):
def process(self, args, outputs):
@ -21,6 +22,18 @@ class ODMOrthoPhotoStage(types.ODM_Stage):
system.mkdir_p(tree.odm_orthophoto)
if not io.file_exists(tree.odm_orthophoto_tif) or self.rerun():
gsd_error_estimate = 0.1
ignore_resolution = False
if not reconstruction.is_georeferenced():
# Match DEMs
gsd_error_estimate = -3
ignore_resolution = True
resolution = 1.0 / (gsd.cap_resolution(args.orthophoto_resolution, tree.opensfm_reconstruction,
gsd_error_estimate=gsd_error_estimate,
ignore_gsd=args.ignore_gsd,
ignore_resolution=ignore_resolution,
has_gcp=reconstruction.has_gcp()) / 100.0)
# odm_orthophoto definitions
kwargs = {
@ -28,7 +41,7 @@ class ODMOrthoPhotoStage(types.ODM_Stage):
'log': tree.odm_orthophoto_log,
'ortho': tree.odm_orthophoto_render,
'corners': tree.odm_orthophoto_corners,
'res': 1.0 / (gsd.cap_resolution(args.orthophoto_resolution, tree.opensfm_reconstruction, ignore_gsd=args.ignore_gsd) / 100.0),
'res': resolution,
'bands': '',
'verbose': verbose
}
@ -43,7 +56,7 @@ class ODMOrthoPhotoStage(types.ODM_Stage):
if io.file_exists(odm_georeferencing_model_txt_geo_file):
reconstruction.georef.extract_offsets(odm_georeferencing_model_txt_geo_file)
else:
log.ODM_WARNING('Cannot read UTM offset from {}. An orthophoto will not be generated.'.format(odm_georeferencing_model_txt_geo_file))
log.ODM_WARNING('Cannot read UTM offset from {}.'.format(odm_georeferencing_model_txt_geo_file))
models = []
@ -146,8 +159,12 @@ class ODMOrthoPhotoStage(types.ODM_Stage):
geotiffcreated = True
if not geotiffcreated:
log.ODM_WARNING('No geo-referenced orthophoto created due '
'to missing geo-referencing or corner coordinates.')
if io.file_exists(tree.odm_orthophoto_render):
# 0.1 is arbitrary
pseudogeo.add_pseudo_georeferencing(tree.odm_orthophoto_render, 0.1)
log.ODM_INFO("Renaming %s --> %s" % (tree.odm_orthophoto_render, tree.odm_orthophoto_tif))
os.rename(tree.odm_orthophoto_render, tree.odm_orthophoto_tif)
else:
log.ODM_WARNING("Could not generate an orthophoto (it did not render)")
else:
log.ODM_WARNING('Found a valid orthophoto in: %s' % tree.odm_orthophoto_tif)

Wyświetl plik

@ -48,7 +48,7 @@ class ODMOpenSfMStage(types.ODM_Stage):
# Make sure it's capped by the depthmap-resolution arg,
# since the undistorted images are used for MVS
outputs['undist_image_max_size'] = max(
gsd.image_max_size(photos, args.orthophoto_resolution, tree.opensfm_reconstruction, ignore_gsd=args.ignore_gsd),
gsd.image_max_size(photos, args.orthophoto_resolution, tree.opensfm_reconstruction, ignore_gsd=args.ignore_gsd, has_gcp=reconstruction.has_gcp()),
args.depthmap_resolution
)