diff --git a/coclico/metrics/listing.py b/coclico/metrics/listing.py index 246c502..d318c21 100644 --- a/coclico/metrics/listing.py +++ b/coclico/metrics/listing.py @@ -1,5 +1,6 @@ from coclico.malt0.malt0 import MALT0 +from coclico.mobj0.mobj0 import MOBJ0 from coclico.mpap0.mpap0 import MPAP0 from coclico.mpla0.mpla0 import MPLA0 -METRICS = {"mpap0": MPAP0, "mpla0": MPLA0, "malt0": MALT0} +METRICS = {"mpap0": MPAP0, "mpla0": MPLA0, "malt0": MALT0, "mobj0": MOBJ0} diff --git a/coclico/mobj0/__init__.py b/coclico/mobj0/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/coclico/mobj0/mobj0.py b/coclico/mobj0/mobj0.py new file mode 100644 index 0000000..19317f0 --- /dev/null +++ b/coclico/mobj0/mobj0.py @@ -0,0 +1,30 @@ +from pathlib import Path +from typing import Dict, List + +import pandas as pd +from gpao.job import Job + +from coclico.metrics.metric import Metric + + +class MOBJ0(Metric): + """Metric MOBJ0 (for "Métrique par objet 0") + TODO Description + See doc/mobj0.md + """ + + # Pixel size for occupancy map + pixel_size = 0.5 + metric_name = "mobj0" + + def create_metric_intrinsic_one_job(self, name: str, input: Path, output: Path, is_ref: bool): + raise NotImplementedError + + def create_metric_relative_to_ref_jobs( + self, name: str, out_c1: Path, out_ref: Path, output: Path, c1_jobs: List[Job], ref_jobs: List[Job] + ) -> Job: + raise NotImplementedError + + @staticmethod + def compute_note(metric_df: pd.DataFrame, note_config: Dict): + raise NotImplementedError diff --git a/coclico/mobj0/mobj0_intrinsic.py b/coclico/mobj0/mobj0_intrinsic.py new file mode 100644 index 0000000..b3439b5 --- /dev/null +++ b/coclico/mobj0/mobj0_intrinsic.py @@ -0,0 +1,89 @@ +import argparse +import logging +from pathlib import Path + +import geopandas as gpd +import numpy as np +import pandas as pd +import rasterio +from osgeo import gdal +from rasterio.features import shapes as rasterio_shapes +from shapely.geometry import shape as shapely_shape + +import coclico.io +from coclico.metrics.occupancy_map import create_occupancy_map_array, read_las +from coclico.mobj0.mobj0 import MOBJ0 + +gdal.UseExceptions() + + +def create_objects_array(las_file, pixel_size, class_weights): + xs, ys, classifs, crs = read_las(las_file) + + binary_maps, x_min, y_max = create_occupancy_map_array(xs, ys, classifs, pixel_size, class_weights) + + return binary_maps, crs, x_min, y_max + + +def vectorize_occupancy_map(binary_maps, crs, x_min, y_max, pixel_size): + # Create empty dataframe + gdf_list = [] + + for ii, map_layer in enumerate(binary_maps): + shapes_layer = rasterio_shapes( + map_layer, + connectivity=8, + transform=rasterio.transform.from_origin( + x_min - pixel_size / 2, y_max + pixel_size / 2, pixel_size, pixel_size + ), + ) + + geometries = [shapely_shape(shapedict) for shapedict, value in shapes_layer if value != 0] + nb_geometries = len(geometries) + gdf_list.append( + gpd.GeoDataFrame( + {"layer": ii * np.ones(nb_geometries), "geometry": geometries}, + geometry="geometry", + crs=crs, + ) + ) + + gdf = pd.concat(gdf_list) + + return gdf + + +def compute_metric_intrinsic(las_file: Path, config_file: Path, output_geojson: Path, pixel_size: float = 0.5): + config_dict = coclico.io.read_config_file(config_file) + class_weights = config_dict[MOBJ0.metric_name]["weights"] + output_geojson.parent.mkdir(parents=True, exist_ok=True) + obj_array, crs, x_min, y_max = create_objects_array(las_file, pixel_size, class_weights) + polygons_gdf = vectorize_occupancy_map(obj_array, crs, x_min, y_max, pixel_size) + polygons_gdf.to_file(output_geojson) + + +def parse_args(): + parser = argparse.ArgumentParser("Run malt0 intrinsic metric on one tile") + parser.add_argument("-i", "--input-file", type=Path, required=True, help="Path to the LAS file") + parser.add_argument("-o", "--output-geojson", type=Path, required=True, help="Path to the output geojson") + parser.add_argument( + "-c", + "--config-file", + type=Path, + required=True, + help="Coclico configuration file", + ) + parser.add_argument( + "-p", "--pixel-size", type=float, required=True, help="Pixel size of the intermediate occupancy map" + ) + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + logging.basicConfig(format="%(message)s", level=logging.DEBUG) + compute_metric_intrinsic( + las_file=Path(args.input_file), + config_file=args.config_file, + output_geojson=Path(args.output_geojson), + ) diff --git a/coclico/mobj0/mobj0_relative.py b/coclico/mobj0/mobj0_relative.py new file mode 100644 index 0000000..84ac56c --- /dev/null +++ b/coclico/mobj0/mobj0_relative.py @@ -0,0 +1,74 @@ +import argparse +import logging +from pathlib import Path + +import pandas as pd + +from coclico.config import csv_separator +from coclico.io import read_config_file +from coclico.mobj0.mobj0 import MOBJ0 + + +def compute_metric_relative( + c1_dir: Path, ref_dir: Path, occupancy_dir: Path, config_file: str, output_csv: Path, output_csv_tile: Path +): + """Compute metrics that describe the difference between c1 and ref height maps. + The occupancy map is used to mask the pixels for which the difference is computed + + The metrics are: + - mean_diff: the average difference in z between the height maps + - max_diff: the maximum difference in z between the height maps + - std_diff: the standard deviation of the difference in z betweeen the height maps + + These metrics are stored tile by tile and class by class in the output_csv_tile file + These metrics are stored class by class for the whole data in the output_csv file + + Args: + c1_dir (Path): path to the c1 classification directory, + where there are json files with the result of mpap0 intrinsic metric + ref_dir (Path): path to the reference classification directory, + where there are json files with the result of mpap0 intrinsic metric + class_weights (Dict): class weights dict + output_csv (Path): path to output csv file + output_csv_tile (Path): path to output csv file, result by tile + + """ + config_dict = read_config_file(config_file) + class_weights = config_dict[MOBJ0.metric_name]["weights"] + classes = sorted(class_weights.keys()) + classes + csv_data = [] + + df = pd.DataFrame(csv_data) + df.to_csv(output_csv, index=False, sep=csv_separator) + + logging.debug(df.to_markdown()) + raise NotImplementedError + + +def parse_args(): + parser = argparse.ArgumentParser("Run malt0 metric on one tile") + parser.add_argument( + "-i", + "--input-dir", + required=True, + type=Path, + help="Path to the classification directory, \ + where there are tif files with the result of malt0 intrinsic metric (MNx for each class)", + ) + raise NotImplementedError + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + logging.basicConfig(format="%(message)s", level=logging.DEBUG) + compute_metric_relative( + c1_dir=Path(args.input_dir), + ref_dir=Path(args.ref_dir), + occupancy_dir=Path(args.occupancy_dir), + config_file=args.config_file, + output_csv=Path(args.output_csv), + output_csv_tile=Path(args.output_csv_tile), + ) + raise NotImplementedError diff --git a/environment.yml b/environment.yml index 81f31ba..f5c723f 100644 --- a/environment.yml +++ b/environment.yml @@ -9,6 +9,7 @@ dependencies: - laspy - rasterio - pandas + - geopandas - tabulate - requests - pytest diff --git a/test/configs/config_test_metrics.yaml b/test/configs/config_test_metrics.yaml index 5cfe788..be574a5 100644 --- a/test/configs/config_test_metrics.yaml +++ b/test/configs/config_test_metrics.yaml @@ -99,3 +99,9 @@ malt0: max_point: metric: 0.5 note: 0 + +mobj0: + weights: + "6": 2 + "1": 1 + notes: {} \ No newline at end of file diff --git a/test/mobj0/test_mobj0.py b/test/mobj0/test_mobj0.py new file mode 100644 index 0000000..124437f --- /dev/null +++ b/test/mobj0/test_mobj0.py @@ -0,0 +1,28 @@ +import shutil +from pathlib import Path + +import pytest + +import coclico.io as io +from coclico.mobj0.mobj0 import MOBJ0 + +pytestmark = pytest.mark.docker + +TMP_PATH = Path("./tmp/mobj0") +CONFIG_FILE_METRICS = Path("./test/configs/config_test_metrics.yaml") + + +def setup_module(module): + if TMP_PATH.is_dir(): + shutil.rmtree(TMP_PATH) + + +def generate_metric_dataframes(): + raise NotImplementedError + + +def test_compute_note(): + input_df, expected_out = generate_metric_dataframes() + notes_config = io.read_config_file(CONFIG_FILE_METRICS)["mobj0"]["notes"] + out_df = MOBJ0.compute_note(input_df, notes_config) + assert out_df.equals(expected_out) diff --git a/test/mobj0/test_mobj0_intrinsic.py b/test/mobj0/test_mobj0_intrinsic.py new file mode 100644 index 0000000..4d6677c --- /dev/null +++ b/test/mobj0/test_mobj0_intrinsic.py @@ -0,0 +1,54 @@ +import logging +import shutil +import subprocess as sp +from pathlib import Path + +import geopandas as gpd +import pytest + +import coclico.io +from coclico.mobj0 import mobj0_intrinsic + +pytestmark = pytest.mark.docker + +TMP_PATH = Path("./tmp/mobj0_intrinsic") +CONFIG_FILE_METRICS = Path("./test/configs/config_test_metrics.yaml") + + +def setup_module(module): + if TMP_PATH.is_dir(): + shutil.rmtree(TMP_PATH) + + +def test_compute_metric_intrinsic(ensure_test1_data): + las_file = Path("./data/test1/niv1/tile_splitted_2818_32247.laz") + pixel_size = 0.5 + output_geojson = TMP_PATH / "intrinsic" / "unit_test_mpla0_intrinsic.geojson" + config = coclico.io.read_config_file(CONFIG_FILE_METRICS) + nb_layers = len(config["mobj0"]["weights"]) + + mobj0_intrinsic.compute_metric_intrinsic(las_file, CONFIG_FILE_METRICS, output_geojson, pixel_size=pixel_size) + + assert output_geojson.exists() + gdf = gpd.read_file(output_geojson) + logging.debug(gdf.to_markdown()) + assert len(gdf.index) > 0 + assert len(set(gdf["layer"])) == nb_layers + + +def test_run_main(ensure_test1_data): + pixel_size = 0.5 + input_file = Path("./data/test1/niv1/tile_splitted_2818_32247.laz") + output_geojson = TMP_PATH / "intrinsic" / "tile_splitted_2818_32247.geojson" + + cmd = f"""python -m coclico.mobj0.mobj0_intrinsic \ + --input-file {input_file} \ + --output-geojson {output_geojson} \ + --config-file {CONFIG_FILE_METRICS} \ + --pixel-size {pixel_size} + """ + sp.run(cmd, shell=True, check=True) + + logging.info(cmd) + + assert output_geojson.exists() diff --git a/test/mobj0/test_mobj0_relative.py b/test/mobj0/test_mobj0_relative.py new file mode 100644 index 0000000..54516a4 --- /dev/null +++ b/test/mobj0/test_mobj0_relative.py @@ -0,0 +1,22 @@ +import shutil +from pathlib import Path + +import pytest + +pytestmark = pytest.mark.docker + +TMP_PATH = Path("./tmp/mobj0_relative") +CONFIG_FILE_METRICS = Path("./test/configs/config_test_metrics.yaml") + + +def setup_module(module): + if TMP_PATH.is_dir(): + shutil.rmtree(TMP_PATH) + + +def test_compute_metric_relative(ensure_mobj0_data): + raise NotImplementedError + + +def test_run_main(ensure_mobj0_data): + raise NotImplementedError