Beispiel #1
0
    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
Beispiel #2
0
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
Beispiel #3
0
    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)
Beispiel #4
0
    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)
Beispiel #5
0
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)