Beispiel #1
0
    def process(self, args, outputs):
        tree = outputs['tree']
        reconstruction = outputs['reconstruction']

        if not os.path.exists(tree.odm_report): system.mkdir_p(tree.odm_report)

        log.ODM_INFO("Exporting shots.geojson")

        shots_geojson = os.path.join(tree.odm_report, "shots.geojson")
        if not io.file_exists(shots_geojson) or self.rerun():
            # Extract geographical camera shots
            if reconstruction.is_georeferenced():
                shots = get_geojson_shots_from_opensfm(
                    tree.opensfm_reconstruction,
                    utm_srs=reconstruction.get_proj_srs(),
                    utm_offset=reconstruction.georef.utm_offset())
            else:
                # Pseudo geo
                shots = get_geojson_shots_from_opensfm(
                    tree.opensfm_reconstruction,
                    pseudo_geotiff=tree.odm_orthophoto_tif)

            if shots:
                with open(shots_geojson, "w") as fout:
                    fout.write(json.dumps(shots))

                log.ODM_INFO("Wrote %s" % shots_geojson)
            else:
                log.ODM_WARNING("Cannot extract shots")
        else:
            log.ODM_WARNING('Found a valid shots file in: %s' % shots_geojson)

        if args.skip_report:
            # Stop right here
            log.ODM_WARNING("Skipping report generation as requested")
            return

        # Augment OpenSfM stats file with our own stats
        odm_stats_json = os.path.join(tree.odm_report, "stats.json")
        octx = OSFMContext(tree.opensfm)
        osfm_stats_json = octx.path("stats", "stats.json")
        odm_stats = None
        point_cloud_file = None
        views_dimension = None

        if not os.path.exists(odm_stats_json) or self.rerun():
            if os.path.exists(osfm_stats_json):
                with open(osfm_stats_json, 'r') as f:
                    odm_stats = json.loads(f.read())

                # Add point cloud stats
                if os.path.exists(tree.odm_georeferencing_model_laz):
                    point_cloud_file = tree.odm_georeferencing_model_laz
                    views_dimension = "UserData"

                    # pc_info_file should have been generated by cropper
                    pc_info_file = os.path.join(
                        tree.odm_georeferencing,
                        "odm_georeferenced_model.info.json")
                    odm_stats[
                        'point_cloud_statistics'] = generate_point_cloud_stats(
                            tree.odm_georeferencing_model_laz, pc_info_file,
                            self.rerun())
                else:
                    ply_pc = os.path.join(tree.odm_filterpoints,
                                          "point_cloud.ply")
                    if os.path.exists(ply_pc):
                        point_cloud_file = ply_pc
                        views_dimension = "views"

                        pc_info_file = os.path.join(tree.odm_filterpoints,
                                                    "point_cloud.info.json")
                        odm_stats[
                            'point_cloud_statistics'] = generate_point_cloud_stats(
                                ply_pc, pc_info_file, self.rerun())
                    else:
                        log.ODM_WARNING("No point cloud found")

                odm_stats['point_cloud_statistics'][
                    'dense'] = not args.fast_orthophoto

                # Add runtime stats
                total_time = (system.now_raw() -
                              outputs['start_time']).total_seconds()
                odm_stats['odm_processing_statistics'] = {
                    'total_time':
                    total_time,
                    'total_time_human':
                    hms(total_time),
                    'average_gsd':
                    gsd.opensfm_reconstruction_average_gsd(
                        octx.recon_file(),
                        use_all_shots=reconstruction.has_gcp()),
                }

                with open(odm_stats_json, 'w') as f:
                    f.write(json.dumps(odm_stats))
            else:
                log.ODM_WARNING(
                    "Cannot generate report, OpenSfM stats are missing")
        else:
            log.ODM_WARNING("Reading existing stats %s" % odm_stats_json)
            with open(odm_stats_json, 'r') as f:
                odm_stats = json.loads(f.read())

        # Generate overlap diagram
        if odm_stats.get('point_cloud_statistics'
                         ) and point_cloud_file and views_dimension:
            bounds = odm_stats['point_cloud_statistics'].get('stats', {}).get(
                'bbox', {}).get('native', {}).get('bbox')
            if bounds:
                image_target_size = 1400  # pixels
                osfm_stats_dir = os.path.join(tree.opensfm, "stats")
                diagram_tiff = os.path.join(osfm_stats_dir, "overlap.tif")
                diagram_png = os.path.join(osfm_stats_dir, "overlap.png")

                width = bounds.get('maxx') - bounds.get('minx')
                height = bounds.get('maxy') - bounds.get('miny')
                max_dim = max(width, height)
                resolution = float(max_dim) / float(image_target_size)
                radius = resolution * math.sqrt(2)

                # Larger radius for sparse point cloud diagram
                if not odm_stats['point_cloud_statistics']['dense']:
                    radius *= 10

                system.run("pdal translate -i \"{}\" "
                           "-o \"{}\" "
                           "--writer gdal "
                           "--writers.gdal.resolution={} "
                           "--writers.gdal.data_type=uint8_t "
                           "--writers.gdal.dimension={} "
                           "--writers.gdal.output_type=max "
                           "--writers.gdal.radius={} ".format(
                               point_cloud_file, diagram_tiff, resolution,
                               views_dimension, radius))
                report_assets = os.path.abspath(
                    os.path.join(os.path.dirname(__file__),
                                 "../opendm/report"))
                overlap_color_map = os.path.join(report_assets,
                                                 "overlap_color_map.txt")

                bounds_file_path = os.path.join(
                    tree.odm_georeferencing,
                    'odm_georeferenced_model.bounds.gpkg')
                if (args.crop > 0
                        or args.boundary) and os.path.isfile(bounds_file_path):
                    Cropper.crop(bounds_file_path,
                                 diagram_tiff,
                                 get_orthophoto_vars(args),
                                 keep_original=False)

                system.run(
                    "gdaldem color-relief \"{}\" \"{}\" \"{}\" -of PNG -alpha".
                    format(diagram_tiff, overlap_color_map, diagram_png))

                # Copy assets
                for asset in [
                        "overlap_diagram_legend.png", "dsm_gradient.png"
                ]:
                    shutil.copy(os.path.join(report_assets, asset),
                                os.path.join(osfm_stats_dir, asset))

                # Generate previews of ortho/dsm
                if os.path.isfile(tree.odm_orthophoto_tif):
                    osfm_ortho = os.path.join(osfm_stats_dir, "ortho.png")
                    generate_png(tree.odm_orthophoto_tif, osfm_ortho,
                                 image_target_size)

                dems = []
                if args.dsm:
                    dems.append("dsm")
                if args.dtm:
                    dems.append("dtm")

                for dem in dems:
                    dem_file = tree.path("odm_dem", "%s.tif" % dem)
                    if os.path.isfile(dem_file):
                        # Resize first (faster)
                        resized_dem_file = io.related_file_path(
                            dem_file, postfix=".preview")
                        system.run(
                            "gdal_translate -outsize {} 0 \"{}\" \"{}\" --config GDAL_CACHEMAX {}%"
                            .format(image_target_size, dem_file,
                                    resized_dem_file, get_max_memory()))

                        log.ODM_INFO("Computing raster stats for %s" %
                                     resized_dem_file)
                        dem_stats = get_raster_stats(resized_dem_file)
                        if len(dem_stats) > 0:
                            odm_stats[dem + '_statistics'] = dem_stats[0]

                        osfm_dem = os.path.join(osfm_stats_dir, "%s.png" % dem)
                        colored_dem, hillshade_dem, colored_hillshade_dem = generate_colored_hillshade(
                            resized_dem_file)
                        system.run(
                            "gdal_translate -outsize {} 0 -of png \"{}\" \"{}\" --config GDAL_CACHEMAX {}%"
                            .format(image_target_size, colored_hillshade_dem,
                                    osfm_dem, get_max_memory()))
                        for f in [
                                resized_dem_file, colored_dem, hillshade_dem,
                                colored_hillshade_dem
                        ]:
                            if os.path.isfile(f):
                                os.remove(f)
            else:
                log.ODM_WARNING(
                    "Cannot generate overlap diagram, cannot compute point cloud bounds"
                )
        else:
            log.ODM_WARNING(
                "Cannot generate overlap diagram, point cloud stats missing")

        octx.export_report(os.path.join(tree.odm_report, "report.pdf"),
                           odm_stats, self.rerun())
Beispiel #2
0
    def process(self, args, outputs):
        tree = outputs['tree']
        reconstruction = outputs['reconstruction']

        if not os.path.exists(tree.odm_filterpoints):
            system.mkdir_p(tree.odm_filterpoints)

        inputPointCloud = ""

        # check if reconstruction was done before
        if not io.file_exists(tree.filtered_point_cloud) or self.rerun():
            if args.fast_orthophoto:
                inputPointCloud = os.path.join(tree.opensfm,
                                               'reconstruction.ply')
            else:
                inputPointCloud = tree.openmvs_model

            # Check if we need to compute boundary
            if args.auto_boundary:
                if reconstruction.is_georeferenced():
                    if not 'boundary' in outputs:
                        avg_gsd = gsd.opensfm_reconstruction_average_gsd(
                            tree.opensfm_reconstruction)
                        if avg_gsd is not None:
                            outputs['boundary'] = compute_boundary_from_shots(
                                tree.opensfm_reconstruction, avg_gsd * 20,
                                reconstruction.get_proj_offset()
                            )  # 20 is arbitrary
                            if outputs['boundary'] is None:
                                log.ODM_WANING(
                                    "Cannot compute boundary from camera shots"
                                )
                        else:
                            log.ODM_WARNING(
                                "Cannot compute boundary (GSD cannot be estimated)"
                            )
                    else:
                        log.ODM_WARNING(
                            "--auto-boundary set but so is --boundary, will use --boundary"
                        )
                else:
                    log.ODM_WARNING(
                        "Not a georeferenced reconstruction, will ignore --auto-boundary"
                    )

            point_cloud.filter(inputPointCloud,
                               tree.filtered_point_cloud,
                               standard_deviation=args.pc_filter,
                               sample_radius=args.pc_sample,
                               boundary=boundary_offset(
                                   outputs.get('boundary'),
                                   reconstruction.get_proj_offset()),
                               verbose=args.verbose,
                               max_concurrency=args.max_concurrency)

            # Quick check
            info = point_cloud.ply_info(tree.filtered_point_cloud)
            if info["vertex_count"] == 0:
                extra_msg = ''
                if 'boundary' in outputs:
                    extra_msg = '. Also, since you used a boundary setting, make sure that the boundary polygon you specified covers the reconstruction area correctly.'
                raise system.ExitException(
                    "Uh oh! We ended up with an empty point cloud. This means that the reconstruction did not succeed. Have you followed best practices for data acquisition? See https://docs.opendronemap.org/flying/%s"
                    % extra_msg)
        else:
            log.ODM_WARNING('Found a valid point cloud file in: %s' %
                            tree.filtered_point_cloud)

        if args.optimize_disk_space and inputPointCloud:
            if os.path.isfile(inputPointCloud):
                os.remove(inputPointCloud)