def create_utm_copy(self, gcp_file_output, filenames=None, rejected_entries=None, include_extras=True): """ Creates a new GCP file from an existing GCP file by optionally including only filenames and reprojecting each point to a UTM CRS. Rejected entries can recorded by passing a list object to rejected_entries. """ if os.path.exists(gcp_file_output): os.remove(gcp_file_output) output = [self.wgs84_utm_zone()] target_srs = location.parse_srs_header(output[0]) transformer = location.transformer(self.srs, target_srs) for entry in self.iter_entries(): if filenames is None or entry.filename in filenames: entry.x, entry.y, entry.z = transformer.TransformPoint( entry.x, entry.y, entry.z) if not include_extras: entry.extras = '' output.append(str(entry)) elif isinstance(rejected_entries, list): rejected_entries.append(entry) with open(gcp_file_output, 'w') as f: f.write('\n'.join(output) + '\n') return gcp_file_output
def load_boundary(boundary_json, reproject_to_proj4=None): if not isinstance(boundary_json, str): boundary_json = json.dumps(boundary_json) with fiona.open(io.BytesIO(boundary_json.encode('utf-8')), 'r') as src: if len(src) != 1: raise IOError("Boundary must have a single polygon (found: %s)" % len(src)) geom = src[0]['geometry'] if geom['type'] != 'Polygon': raise IOError("Boundary must have a polygon feature (found: %s)" % geom['type']) rings = geom['coordinates'] if len(rings) == 0: raise IOError("Boundary geometry has no rings") coords = rings[0] if len(coords) == 0: raise IOError("Boundary geometry has no coordinates") dimensions = len(coords[0]) if reproject_to_proj4 is not None: t = transformer(CRS.from_proj4(fiona.crs.to_string(src.crs)), CRS.from_proj4(reproject_to_proj4)) coords = [t.TransformPoint(*c)[:dimensions] for c in coords] return coords
def make_micmac_copy(self, output_dir, precisionxy=0.01, precisionz=0.01, utm_zone=None): """ Convert this GCP file in a format compatible with MicMac. :param output_dir directory where to save the two MicMac GCP files. The directory must exist. :param utm_zone UTM zone to use for output coordinates (UTM string, PROJ4 or EPSG definition). If one is not specified, the nearest UTM zone will be selected. :param precisionxy horizontal precision of GCP measurements in meters. :param precisionz vertical precision of GCP measurements in meters. """ if not os.path.isdir(output_dir): raise IOError("{} does not exist.".format(output_dir)) if not isinstance(precisionxy, float) and not isinstance( precisionxy, int): raise AssertionError("precisionxy must be a number") if not isinstance(precisionz, float) and not isinstance( precisionz, int): raise AssertionError("precisionz must be a number") gcp_3d_file = os.path.join(output_dir, '3d_gcp.txt') gcp_2d_file = os.path.join(output_dir, '2d_gcp.txt') if os.path.exists(gcp_3d_file): os.remove(gcp_3d_file) if os.path.exists(gcp_2d_file): os.remove(gcp_2d_file) if utm_zone is None: utm_zone = self.wgs84_utm_zone() target_srs = location.parse_srs_header(utm_zone) transformer = location.transformer(self.srs, target_srs) gcps = {} for entry in self.iter_entries(): utm_x, utm_y, utm_z = transformer.TransformPoint( entry.x, entry.y, entry.z) k = "{} {} {}".format(utm_x, utm_y, utm_z) if not k in gcps: gcps[k] = [entry] else: gcps[k].append(entry) with open(gcp_3d_file, 'w') as f3: with open(gcp_2d_file, 'w') as f2: gcp_n = 1 for k in gcps: f3.write("GCP{} {} {} {}\n".format(gcp_n, k, precisionxy, precisionz)) for entry in gcps[k]: f2.write("GCP{} {} {} {}\n".format( gcp_n, entry.filename, entry.px, entry.py)) gcp_n += 1 return (gcp_3d_file, gcp_2d_file)
def process(self, args, outputs): tree = outputs['tree'] reconstruction = outputs['reconstruction'] # Export GCP information if available gcp_export_file = tree.path("odm_georeferencing", "ground_control_points.gpkg") gcp_gml_export_file = tree.path("odm_georeferencing", "ground_control_points.gml") gcp_geojson_export_file = tree.path("odm_georeferencing", "ground_control_points.geojson") if reconstruction.has_gcp() and (not io.file_exists(gcp_export_file) or self.rerun()): octx = OSFMContext(tree.opensfm) gcps = octx.ground_control_points(reconstruction.georef.proj4()) if len(gcps): gcp_schema = { 'geometry': 'Point', 'properties': OrderedDict([ ('id', 'str'), ('observations_count', 'int'), ('observations_list', 'str'), ('error_x', 'float'), ('error_y', 'float'), ('error_z', 'float'), ]) } # Write GeoPackage with fiona.open(gcp_export_file, 'w', driver="GPKG", crs=fiona.crs.from_string(reconstruction.georef.proj4()), schema=gcp_schema) as f: for gcp in gcps: f.write({ 'geometry': { 'type': 'Point', 'coordinates': gcp['coordinates'], }, 'properties': OrderedDict([ ('id', gcp['id']), ('observations_count', len(gcp['observations'])), ('observations_list', ",".join([obs['shot_id'] for obs in gcp['observations']])), ('error_x', gcp['error'][0]), ('error_y', gcp['error'][1]), ('error_z', gcp['error'][2]), ]) }) # Write GML try: system.run('ogr2ogr -of GML "{}" "{}"'.format(gcp_gml_export_file, gcp_export_file)) except Exception as e: log.ODM_WARNING("Cannot generate ground control points GML file: %s" % str(e)) # Write GeoJSON geojson = { 'type': 'FeatureCollection', 'features': [] } from_srs = CRS.from_proj4(reconstruction.georef.proj4()) to_srs = CRS.from_epsg(4326) transformer = location.transformer(from_srs, to_srs) for gcp in gcps: properties = gcp.copy() del properties['coordinates'] geojson['features'].append({ 'type': 'Feature', 'geometry': { 'type': 'Point', 'coordinates': transformer.TransformPoint(*gcp['coordinates']), }, 'properties': properties }) with open(gcp_geojson_export_file, 'w') as f: f.write(json.dumps(geojson, indent=4)) else: log.ODM_WARNING("GCPs could not be loaded for writing to %s" % gcp_export_file) if not io.file_exists(tree.odm_georeferencing_model_laz) or self.rerun(): cmd = ('pdal translate -i "%s" -o \"%s\"' % (tree.filtered_point_cloud, tree.odm_georeferencing_model_laz)) stages = ["ferry"] params = [ '--filters.ferry.dimensions="views => UserData"', '--writers.las.compression="lazip"', ] if reconstruction.is_georeferenced(): log.ODM_INFO("Georeferencing point cloud") stages.append("transformation") params += [ '--filters.transformation.matrix="1 0 0 %s 0 1 0 %s 0 0 1 0 0 0 0 1"' % reconstruction.georef.utm_offset(), '--writers.las.offset_x=%s' % reconstruction.georef.utm_east_offset, '--writers.las.offset_y=%s' % reconstruction.georef.utm_north_offset, '--writers.las.scale_x=0.001', '--writers.las.scale_y=0.001', '--writers.las.scale_z=0.001', '--writers.las.offset_z=0', '--writers.las.a_srs="%s"' % reconstruction.georef.proj4() ] if reconstruction.has_gcp() and io.file_exists(gcp_gml_export_file): log.ODM_INFO("Embedding GCP info in point cloud") params += [ '--writers.las.vlrs="{\\\"filename\\\": \\\"%s\\\", \\\"user_id\\\": \\\"ODM_GCP\\\", \\\"description\\\": \\\"Ground Control Points (GML)\\\"}"' % gcp_gml_export_file.replace(os.sep, "/") ] system.run(cmd + ' ' + ' '.join(stages) + ' ' + ' '.join(params)) self.update_progress(50) if args.crop > 0: log.ODM_INFO("Calculating cropping area and generating bounds shapefile from point cloud") cropper = Cropper(tree.odm_georeferencing, 'odm_georeferenced_model') if args.fast_orthophoto: decimation_step = 4 else: decimation_step = 40 # More aggressive decimation for large datasets if not args.fast_orthophoto: decimation_step *= int(len(reconstruction.photos) / 1000) + 1 decimation_step = min(decimation_step, 95) try: cropper.create_bounds_gpkg(tree.odm_georeferencing_model_laz, args.crop, decimation_step=decimation_step) except: log.ODM_WARNING("Cannot calculate crop bounds! We will skip cropping") args.crop = 0 if 'boundary' in outputs and args.crop == 0: log.ODM_INFO("Using boundary JSON as cropping area") bounds_base, _ = os.path.splitext(tree.odm_georeferencing_model_laz) bounds_json = bounds_base + ".bounds.geojson" bounds_gpkg = bounds_base + ".bounds.gpkg" export_to_bounds_files(outputs['boundary'], reconstruction.get_proj_srs(), bounds_json, bounds_gpkg) else: log.ODM_INFO("Converting point cloud (non-georeferenced)") system.run(cmd + ' ' + ' '.join(stages) + ' ' + ' '.join(params)) point_cloud.post_point_cloud_steps(args, tree, self.rerun()) else: log.ODM_WARNING('Found a valid georeferenced model in: %s' % tree.odm_georeferencing_model_laz) if args.optimize_disk_space and io.file_exists(tree.odm_georeferencing_model_laz) and io.file_exists(tree.filtered_point_cloud): os.remove(tree.filtered_point_cloud)
def get_geojson_shots_from_opensfm(reconstruction_file, geocoords_transformation_file=None, utm_srs=None, pseudo_geotiff=None): """ Extract shots from OpenSfM's reconstruction.json """ # Read transform (if available) if geocoords_transformation_file is not None and utm_srs is not None and os.path.exists( geocoords_transformation_file): geocoords = np.loadtxt(geocoords_transformation_file, usecols=range(4)) pseudo = False elif pseudo_geotiff is not None and os.path.exists(pseudo_geotiff): # pseudogeo transform utm_srs = get_pseudogeo_utm() # the pseudo-georeferencing CRS UL corner is at 0,0 # but our shot coordinates aren't, so we need to offset them raster = gdal.Open(pseudo_geotiff) ulx, xres, _, uly, _, yres = raster.GetGeoTransform() lrx = ulx + (raster.RasterXSize * xres) lry = uly + (raster.RasterYSize * yres) geocoords = np.array( [[1.0 / get_pseudogeo_scale()**2, 0, 0, ulx + lrx / 2.0], [0, 1.0 / get_pseudogeo_scale()**2, 0, uly + lry / 2.0], [0, 0, 1, 0], [0, 0, 0, 1]]) raster = None pseudo = True else: # Can't deal with this return crstrans = transformer(CRS.from_proj4(utm_srs), CRS.from_epsg("4326")) if os.path.exists(reconstruction_file): with open(reconstruction_file, 'r') as fin: reconstructions = json.loads(fin.read()) feats = [] added_shots = {} for recon in reconstructions: cameras = recon.get('cameras', {}) for filename in recon.get('shots', {}): shot = recon['shots'][filename] cam = shot.get('camera') if (not cam in cameras) or (filename in added_shots): continue cam = cameras[cam] Rs, T = geocoords[:3, :3], geocoords[:3, 3] Rs1 = np.linalg.inv(Rs) origin = get_origin(shot) # Translation utm_coords = np.dot(Rs, origin) + T trans_coords = crstrans.TransformPoint( utm_coords[0], utm_coords[1], utm_coords[2]) # Rotation rotation_matrix = get_rotation_matrix( np.array(shot['rotation'])) rotation = matrix_to_rotation(np.dot(rotation_matrix, Rs1)) translation = origin if pseudo else utm_coords feats.append({ 'type': 'Feature', 'properties': { 'filename': filename, 'focal': cam.get( 'focal', cam.get('focal_x') ), # Focal ratio = focal length (mm) / max(sensor_width, sensor_height) (mm) 'width': cam.get('width', 0), 'height': cam.get('height', 0), 'translation': list(translation), 'rotation': list(rotation) }, 'geometry': { 'type': 'Point', 'coordinates': list(trans_coords) } }) added_shots[filename] = True return {'type': 'FeatureCollection', 'features': feats} else: raise RuntimeError("%s does not exist." % reconstruction_file)