Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions examples/anthropogenic-activities/config_trne.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ generate_tilesets.py:
working_directory: .
output_folder: output/trne/
datasets:
aoi_tiles: output/trne/tiles.geojson
ground_truth_labels: output/trne/labels.geojson
aoi_tiles: output/trne/tiles.gpkg
ground_truth_labels: output/trne/labels.gpkg
add_fp_labels: # Uncomment if FP shapefile exists in prepare_data.py
fp_labels: output/trne/FP.geojson
fp_labels: output/trne/FP_labels.gpkg
frac_trn: 0.7 # fraction of fp tiles to add to the trn dataset, then the remaining tiles will be split in 2 and added to tst and val datasets
image_source:
type: XYZ # supported values: 1. MIL = Map Image Layer 2. WMS 3. XYZ 4. FOLDER
Expand Down Expand Up @@ -82,7 +82,7 @@ assess_detections.py:
working_directory: output/trne
output_folder: assessment
datasets:
ground_truth_labels: labels.geojson
ground_truth_labels: labels.gpkg
split_aoi_tiles: split_aoi_tiles.geojson # aoi = Area of Interest
categories: category_ids.json
detections:
Expand Down
76 changes: 3 additions & 73 deletions examples/anthropogenic-activities/merge_detections.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import pandas as pd

sys.path.insert(1, '../..')
from helpers.functions_for_examples import get_categories
from helpers.functions_for_examples import get_categories, merge_adjacent_detections, read_dets_and_aoi
import helpers.misc as misc

from loguru import logger
Expand Down Expand Up @@ -62,87 +62,17 @@
logger.success(f"Done! All files already exist in folder {OUTPUT_DIR}. Exiting.")
sys.exit(0)

logger.info("Loading split AoI tiles as a GeoPandas DataFrame...")
tiles_gdf = gpd.read_file('split_aoi_tiles.geojson')
tiles_gdf = tiles_gdf.to_crs(2056)
if 'year_tile' in tiles_gdf.keys():
tiles_gdf['year_tile'] = tiles_gdf.year_tile.astype(int)
logger.success(f"Done! {len(tiles_gdf)} features were found.")

logger.info("Loading detections as a GeoPandas DataFrame...")

detections_gdf = gpd.GeoDataFrame()

for dataset, dets_file in DETECTION_FILES.items():
detections_ds_gdf = gpd.read_file(dets_file)
detections_ds_gdf[f'dataset'] = dataset
detections_gdf = pd.concat([detections_gdf, detections_ds_gdf], axis=0, ignore_index=True)
detections_gdf = detections_gdf.to_crs(2056)
detections_gdf['area'] = detections_gdf.area
detections_gdf['det_id'] = detections_gdf.index
if 'year_det' in detections_gdf.keys():
detections_gdf['year_det'] = detections_gdf.year_det.astype(int)
logger.success(f"Done! {len(detections_gdf)} features were found.")
tiles_gdf, detections_gdf = read_dets_and_aoi(DETECTION_FILES)

# Merge features
logger.info(f"Merge adjacent polygons overlapping tiles with a buffer of {DISTANCE} m...")
detections_all_years_gdf = gpd.GeoDataFrame()

# Process detection by year
for year in detections_gdf.year_det.unique():
detections_by_year_gdf = detections_gdf[detections_gdf['year_det']==year]

detections_buffer_gdf = detections_by_year_gdf.copy()
detections_buffer_gdf['geometry'] = detections_by_year_gdf.geometry.buffer(DISTANCE, resolution=2)

# Saves the id of polygons contained entirely within the tile (no merging with adjacent tiles), to avoid merging them if they are at a distance of less than thd
detections_tiles_join_gdf = gpd.sjoin(tiles_gdf, detections_buffer_gdf, how='left', predicate='contains')
remove_det_list = detections_tiles_join_gdf.det_id.unique().tolist()

detections_within_tiles_gdf = detections_by_year_gdf[detections_by_year_gdf.det_id.isin(remove_det_list)].drop_duplicates(subset=['det_id'], ignore_index=True)
detections_overlap_tiles_gdf = detections_by_year_gdf[~detections_by_year_gdf.det_id.isin(remove_det_list)].drop_duplicates(subset=['det_id'], ignore_index=True)

# Merge polygons within the thd distance
detections_overlap_tiles_gdf.loc[:, 'geometry'] = detections_overlap_tiles_gdf.buffer(DISTANCE, resolution=2)
detections_dissolve_gdf = detections_overlap_tiles_gdf[['det_id', 'geometry']].dissolve(as_index=False)
detections_merge_gdf = detections_dissolve_gdf.explode(ignore_index=True)
del detections_dissolve_gdf, detections_overlap_tiles_gdf

if detections_merge_gdf.isnull().values.any():
detections_merge_gdf = gpd.GeoDataFrame()
else:
detections_merge_gdf.geometry = detections_merge_gdf.buffer(-DISTANCE, resolution=2)

# Spatially join merged detection with raw ones to retrieve relevant information (score, area,...)
detections_merge_gdf['index_merge'] = detections_merge_gdf.index
detections_join_gdf = gpd.sjoin(detections_merge_gdf, detections_by_year_gdf, how='inner', predicate='intersects')

det_class_all = []
det_score_all = []

for id in detections_merge_gdf.index_merge.unique():
detections_by_year_gdf = detections_join_gdf.copy()
detections_by_year_gdf = detections_by_year_gdf[(detections_by_year_gdf['index_merge']==id)]
detections_by_year_gdf.rename(columns={'score_left': 'score'}, inplace=True)
det_score_all.append(detections_by_year_gdf['score'].mean())
detections_by_year_gdf = detections_by_year_gdf.dissolve(by='det_class', aggfunc='sum', as_index=False)
# Keep class of largest det
if len(detections_by_year_gdf) > 0:
detections_by_year_gdf['det_class'] = detections_by_year_gdf.loc[detections_by_year_gdf['area'] == detections_by_year_gdf['area'].max(),
'det_class'].iloc[0]
det_class = detections_by_year_gdf['det_class'].drop_duplicates().tolist()
else:
det_class = [0]
det_class_all.append(det_class[0])

detections_merge_gdf['det_class'] = det_class_all
detections_merge_gdf['score'] = det_score_all

complete_merge_dets_gdf = pd.merge(detections_merge_gdf, detections_join_gdf[['index_merge', 'year_det'] + ([] if 'dataset' in detections_merge_gdf.columns else ['dataset'])], on='index_merge')
complete_merge_dets_gdf, detections_within_tiles_gdf = merge_adjacent_detections(detections_gdf, tiles_gdf, year, DISTANCE)
detections_all_years_gdf = pd.concat([detections_all_years_gdf, complete_merge_dets_gdf, detections_within_tiles_gdf], ignore_index=True)

del complete_merge_dets_gdf, detections_merge_gdf, detections_by_year_gdf, detections_within_tiles_gdf, detections_join_gdf

# get classe ids
CATEGORIES = os.path.join('category_ids.json')
categories_info_df, _ = get_categories(CATEGORIES)
Expand Down
29 changes: 5 additions & 24 deletions examples/anthropogenic-activities/prepare_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,32 +59,13 @@
if not os.path.exists(OUTPUT_DIR):
os.makedirs(OUTPUT_DIR)

written_files = []

gt_labels_4326_gdf = ffe.prepare_labels(SHPFILE, CATEGORY, supercategory=SUPERCATEGORY)

label_filepath = os.path.join(OUTPUT_DIR, 'labels.geojson')
gt_labels_4326_gdf.to_file(label_filepath, driver='GeoJSON')
written_files.append(label_filepath)
logger.success(f"Done! A file was written: {label_filepath}")
gt_labels_4326_gdf, written_files = ffe.prepare_labels(SHPFILE, CATEGORY, supercategory=SUPERCATEGORY, output_dir=OUTPUT_DIR)

tiles_4326_all_gdf, tmp_written_files = ffe.format_all_tiles(
FP_SHPFILE, os.path.join(OUTPUT_DIR, 'FP.geojson'), EPT_SHPFILE, ept_data_type=EPT, ept_year=EPT_YEAR, labels_4326_gdf=gt_labels_4326_gdf,
category=CATEGORY, supercategory=SUPERCATEGORY, zoom_level=ZOOM_LEVEL
_, tmp_written_files = ffe.format_all_tiles(
FP_SHPFILE, EPT_SHPFILE, ept_data_type=EPT, ept_year=EPT_YEAR, labels_4326_gdf=gt_labels_4326_gdf,
category=CATEGORY, supercategory=SUPERCATEGORY, zoom_level=ZOOM_LEVEL, output_dir=OUTPUT_DIR
)

# Save tile shapefile
tile_filepath = os.path.join(OUTPUT_DIR, 'tiles.geojson')
if tiles_4326_all_gdf.empty:
logger.warning('No tile generated for the designated area.')
tile_filepath = os.path.join(OUTPUT_DIR, 'area_without_tiles.gpkg')
gt_labels_4326_gdf.to_file(tile_filepath)
written_files.append(tile_filepath)
else:
logger.info("Export tiles to GeoJSON (EPSG:4326)...")
tiles_4326_all_gdf.to_file(tile_filepath, driver='GeoJSON')
written_files.append(tile_filepath)
logger.success(f"Done! A file was written: {tile_filepath}")
written_files.extend(tmp_written_files)

print()
logger.info("The following files were written. Let's check them out!")
Expand Down
8 changes: 5 additions & 3 deletions examples/mineral-extract-sites-detection/README.md
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ It consists of the following elements:
- ready-to-use configuration files:
- `config_trne.yaml`;
- `config_det.yaml`;
- `detectron2_config_dqry.yaml`.
- `detectron2_config.yaml`.
- Input data in the `data` subfolder:
- MES **labels** issued from the [swissTLM3D](https://www.swisstopo.admin.ch/fr/geodata/landscape/tlm3d.html) product, revised and synchronized with the 2020 [SWISSIMAGE](https://www.swisstopo.admin.ch/fr/geodata/images/ortho/swissimage10.html) orthophotos;
- the delimitation of the **Area of Interest (AoI)**;
- the Swiss DEM raster is too large to be saved on this platform but can be downloaded from this [link](https://github.com/lukasmartinelli/swissdem) using the [EPSG:4326](https://epsg.io/4326) coordinate reference system. The raster must be re-projected to [EPSG:2056](https://epsg.io/2056), renamed as `switzerland_dem_EPSG2056.tif` and located in the **DEM** subfolder. This procedure is managed by running the bash script `get_dem.sh`.
- the Swiss DEM raster can be downloaded from this [link](https://github.com/lukasmartinelli/swissdem) using the [EPSG:4326](https://epsg.io/4326) coordinate reference system. The raster must be re-projected to [EPSG:2056](https://epsg.io/2056), renamed as `switzerland_dem_EPSG2056.tif` and located in the **DEM** subfolder. This procedure is managed by running the bash script `get_dem.sh`.
- A data preparation script (`prepare_data.py`) producing the files to be used as input to the `generate_tilesets` stage.
- A post-processing script (`filter_detections.py`) which filters detections according to their confidence score, altitude and area. The script also identifies and merges groups of nearby polygons.
- Post-processing scripts (`merge_detections.py` and `filter_detections.py`) which merge adjacent polygons and filter detections according to their confidence score, altitude and area respectively.

The workflow can be run end-to-end by issuing the following list of commands, from the root folder of this GitHub repository:

Expand All @@ -25,9 +25,11 @@ nobody@<id>:/app# stdl-objdet generate_tilesets config_trne.yaml
nobody@<id>:/app# stdl-objdet train_model config_trne.yaml
nobody@<id>:/app# stdl-objdet make_detections config_trne.yaml
nobody@<id>:/app# stdl-objdet assess_detections config_trne.yaml
nobody@<id>:/app# python merge_detections.py config_trne.yaml
nobody@<id>:/app# python prepare_data.py config_det.yaml
nobody@<id>:/app# stdl-objdet generate_tilesets config_det.yaml
nobody@<id>:/app# stdl-objdet make_detections config_det.yaml
nobody@<id>:/app# python merge_detections.py config_det.yaml
nobody@<id>:/app# bash get_dem.sh
nobody@<id>:/app# python filter_detections.py config_det.yaml
nobody@<id>:/app# exit
Expand Down
58 changes: 33 additions & 25 deletions examples/mineral-extract-sites-detection/config_det.yaml
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,49 +1,50 @@
###################################
####### Inference detection #######
# Automatic detection of Quarries and Mineral Extraction Sites (MES) in images
##### Detection by inference #####
# Automatic detection of Mineral Extraction Sites (MES) in aerial images

# 1-Produce tile geometries based on the AoI extent and zoom level
# Produce tile geometries based on the AoI extent and zoom level
prepare_data.py:
srs: "EPSG:2056" # Projection of the input file
datasets:
shapefile: ./data/AoI/AoI_2020.shp
output_folder: ./output/det/
zoom_level: 16

# 2-Fetch of tiles (online server) and split into 3 datasets: train, test, validation
# Fetch of tiles (online server) and split into 3 datasets: train, test, validation
generate_tilesets.py:
debug_mode:
enable: False # sample of tiles
nb_tiles_max: 5000
working_directory: output
working_directory: ./output/
datasets:
aoi_tiles: det/tiles.geojson
aoi_tiles: det/tiles.gpkg
image_source:
type: XYZ # supported values: 1. MIL = Map Image Layer 2. WMS 3. XYZ 4. FOLDER
location: https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swissimage-product/default/2020/3857/{z}/{x}/{y}.jpeg
type: XYZ # supported values: 1. MIL = Map Image Layer 2. WMS 3. XYZ 4. FOLDER
year: 2020 # supported values: 1. multi-year (tiles of different year), 2. <year> (i.e. 2020)
location: https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swissimage-product/default/{year}/3857/{z}/{x}/{y}.jpeg
output_folder: det/
tile_size: 256 # per side, in pixels
overwrite: False
n_jobs: 10
seed: 42
COCO_metadata:
year: 2021
version: 1.0
description: Swiss Image Hinterground w/ Quarries and Mineral Exploitation Sites detection
description: Swissimage background with Mineral Exploitation Site labels
contributor: swisstopo
url: https://swisstopo.ch
license:
name: Unknown
url:
name: unknown
url: unknown
categories_file: trne/category_ids.json

# 3-Object detection by inference with the optimised trained model
# Object detection by inference with the optimised trained model
make_detections.py:
working_directory: ./output/det/
log_subfolder: logs
sample_tagged_img_subfolder: sample_tagged_images
COCO_files: # relative paths, w/ respect to the working_folder
COCO_files: # relative paths, with respect to the working_folder
oth: COCO_oth.json
detectron2_config_file: ../../detectron2_config_dqry.yaml # path relative to the working_folder
detectron2_config_file: ../../detectron2_config.yaml # path relative to the working_folder
model_weights:
pth_file: ../trne/logs/model_final.pth # trained model minimising the validation loss curve, monitor the training process via tensorboard (tensorboard --logdir </logs>)
image_metadata_json: img_metadata.json
Expand All @@ -53,14 +54,21 @@ make_detections.py:
score_lower_threshold: 0.3
remove_det_overlap: False # if several detections overlap (IoU > 0.5), only the one with the highest confidence score is retained. Not recommended for use with a single class.

# 4-Filtering and merging detection polygons
# Assess the final results
merge_detections.py:
working_directory: ./output/det/
detections:
oth: oth_detections_at_0dot3_threshold.gpkg
distance: 10 # m, distance use as a buffer to merge close polygons (likely to belong to the same object) together
iou_threshold: 0.1
score_threshold: 0.3 # choose a value

# Filtering and merging detection polygons
filter_detections.py:
year: 2020
detections: ./output/det/oth_detections_at_0dot3_threshold.gpkg
shapefile: ./data/AoI/AoI_2020.shp
dem: ./data/DEM/switzerland_dem_EPSG2056.tif
elevation: 1200.0 # m, altitude threshold
score: 0.95 # detection score (from 0 to 1) provided by detectron2
distance: 10 # m, distance use as a buffer to merge close polygons (likely to belong to the same object) together
area: 5000.0 # m2, area threshold under which polygons are discarded
output: ./output/det/oth_detections_at_0dot3_threshold_year-{year}_score-{score}_area-{area}_elevation-{elevation}_distance-{distance}.geojson
working_directory: ./data
detections: ../output/det/merged_detections_at_0dot3_threshold.gpkg
aoi: ./AoI/AoI_2020.shp
dem: ./DEM/switzerland_dem_EPSG2056.tif
elevation_threshold: 1200 # m, altitude threshold, optional
score_threshold: 0.95 # detection score (from 0 to 1) provided by detectron2, optional
area_threshold: 5000 # m2, area threshold under which polygons are discarded, optional
Loading