From bd0bcaf5de74bb60f8dca0a401e722303158eadb Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Mon, 13 Mar 2023 15:55:38 +0100 Subject: [PATCH 001/108] Add GitHub action for SonarQube --- .github/workflows/analyze.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/analyze.yml diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml new file mode 100644 index 0000000..6874fb1 --- /dev/null +++ b/.github/workflows/analyze.yml @@ -0,0 +1,29 @@ +name: Analyze + +on: + push: + branches: + - '*' + pull_request: + types: [opened, synchronize, reopened] + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - uses: sonarsource/sonarqube-scan-action@master + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + # If you wish to fail your job when the Quality Gate is red, uncomment the + # following lines. This would typically be used to fail a deployment. + # We do not recommend to use this in a pull request. Prefer using pull request + # decoration instead. + # - uses: sonarsource/sonarqube-quality-gate-action@master + # timeout-minutes: 5 + # env: + # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} \ No newline at end of file From 080780a9bf1f38d9479dc8657ab88445acd793a9 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Mon, 13 Mar 2023 16:14:53 +0100 Subject: [PATCH 002/108] Add sonar-project.properties --- sonar-project.properties | 1 + 1 file changed, 1 insertion(+) create mode 100644 sonar-project.properties diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..b1726e5 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1 @@ +sonar.projectKey=swiss-territorial-data-lab_object-detector_AYZ4zWIzr7JdaaSXwexc \ No newline at end of file From 51948a57a13b0a434284779051b2855e82793abc Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Tue, 14 Mar 2023 15:36:41 +0100 Subject: [PATCH 003/108] Remove unused variable --- scripts/generate_tilesets.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/scripts/generate_tilesets.py b/scripts/generate_tilesets.py index 5cbe2ec..37ce5d9 100644 --- a/scripts/generate_tilesets.py +++ b/scripts/generate_tilesets.py @@ -75,7 +75,7 @@ def get_COCO_image_and_segmentations(tile, labels, COCO_license_id, output_dir): try: assert(min(segmentation) >= 0) assert(max(segmentation) <= min(COCO_image['width'], COCO_image['height'])) - except Exception as e: + except: raise Exception(f"Label boundaries exceed this tile size! Tile ID = {_tile['id']}") segmentations.append(segmentation) @@ -99,11 +99,6 @@ def check_aoi_tiles(aoi_tiles_gdf): except: raise Exception("IDs do not seem to be well-formatted. Here's how they must look like: (, , ), e.g. (, , ).") - if not aoi_tiles_gdf['id'].str.startswith('(').all(): - aoi_tiles_gdf['id']='('+aoi_tiles_gdf['id'] - if not aoi_tiles_gdf['id'].str.endswith(')').all(): - aoi_tiles_gdf['id']=aoi_tiles_gdf['id']+')' - return From 918118acb7c57997e4e66e965e2eda57c8d3d225 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Tue, 14 Mar 2023 15:39:20 +0100 Subject: [PATCH 004/108] Enable server certificate validation on SSL/TLS connection --- helpers/MIL.py | 2 +- helpers/WMS.py | 2 +- helpers/XYZ.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/helpers/MIL.py b/helpers/MIL.py index d7db78b..e88c5b0 100644 --- a/helpers/MIL.py +++ b/helpers/MIL.py @@ -79,7 +79,7 @@ def get_geotiff(MIL_url, bbox, width, height, filename, imageSR="3857", bboxSR=" #params = {'bbox': bbox, 'format': 'tif', 'size': f'{width},{height}', 'f': 'pjson', 'imageSR': imageSR, 'bboxSR': bboxSR} - r = requests.post(MIL_url + '/export', data=params, verify=False, timeout=30) + r = requests.post(MIL_url + '/export', data=params, timeout=30) if r.status_code == 200: diff --git a/helpers/WMS.py b/helpers/WMS.py index 9bf84aa..a658df7 100644 --- a/helpers/WMS.py +++ b/helpers/WMS.py @@ -84,7 +84,7 @@ def get_geotiff(WMS_url, layers, bbox, width, height, filename, srs="EPSG:3857", } } - r = requests.get(WMS_url, params=params, allow_redirects=True, verify=False) + r = requests.get(WMS_url, params=params, allow_redirects=True) if r.status_code == 200: diff --git a/helpers/XYZ.py b/helpers/XYZ.py index f43a80d..d2f8b77 100644 --- a/helpers/XYZ.py +++ b/helpers/XYZ.py @@ -71,7 +71,7 @@ def get_geotiff(XYZ_url, bbox, xyz, filename, save_metadata=False, overwrite=Tru xmin, ymin, xmax, ymax = [float(x) for x in bbox.split(',')] - r = requests.get(XYZ_url_completed, allow_redirects=True, verify=False) + r = requests.get(XYZ_url_completed, allow_redirects=True) if r.status_code == 200: From 880a0f20408e6dae06d4bcdc2e2446055392ed71 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Wed, 24 May 2023 09:43:22 +0000 Subject: [PATCH 005/108] Define DEMO_MSG constant in order to avoid duplication --- scripts/generate_tilesets.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/scripts/generate_tilesets.py b/scripts/generate_tilesets.py index 37ce5d9..a7e89a5 100644 --- a/scripts/generate_tilesets.py +++ b/scripts/generate_tilesets.py @@ -33,6 +33,8 @@ logging.config.fileConfig('logging.conf') logger = logging.getLogger('root') +DONE_MSG = "...done." + def read_img_metadata(md_file, all_img_path): img_path = os.path.join(all_img_path, md_file.replace('json', 'tif')) @@ -171,24 +173,24 @@ def check_aoi_tiles(aoi_tiles_gdf): logger.info("Loading AoI tiles as a GeoPandas DataFrame...") aoi_tiles_gdf = gpd.read_file(AOI_TILES_GEOJSON) - logger.info(f"...done. {len(aoi_tiles_gdf)} records were found.") + logger.info(f"{DONE_MSG} {len(aoi_tiles_gdf)} records were found.") logger.info("Checking whether AoI tiles are consistent and well-formatted...") try: check_aoi_tiles(aoi_tiles_gdf) except Exception as e: logger.critical(f"AoI tiles check failed. Exception: {e}") sys.exit(1) - logger.info(f"...done.") + logger.info(DONE_MSG) if GT_LABELS_GEOJSON: logger.info("Loading Ground Truth Labels as a GeoPandas DataFrame...") gt_labels_gdf = gpd.read_file(GT_LABELS_GEOJSON) - logger.info(f"...done. {len(gt_labels_gdf)} records were found.") + logger.info(f"{DONE_MSG} {len(gt_labels_gdf)} records were found.") if OTH_LABELS_GEOJSON: logger.info("Loading Other Labels as a GeoPandas DataFrame...") oth_labels_gdf = gpd.read_file(OTH_LABELS_GEOJSON) - logger.info(f"...done. {len(oth_labels_gdf)} records were found.") + logger.info(f"{DONE_MSG} {len(oth_labels_gdf)} records were found.") logger.info("Generating the list of tasks to be executed (one task per tile)...") @@ -293,7 +295,7 @@ def check_aoi_tiles(aoi_tiles_gdf): logger.critical(f'Web Service of type "{ORTHO_WS_TYPE}" are not yet supported. Exiting.') sys.exit(1) - logger.info("...done.") + logger.info(DONE_MSG) logger.info(f"Executing tasks, {N_JOBS} at a time...") job_outcome = Parallel(n_jobs=N_JOBS, backend="loky")( @@ -308,7 +310,7 @@ def check_aoi_tiles(aoi_tiles_gdf): logger.warning('Failed task: ', job) if all_tiles_were_downloaded: - logger.info("...done.") + logger.info(DONE_MSG) else: logger.critical("Some tiles were not downloaded. Please try to run this script again.") sys.exit(1) @@ -329,7 +331,7 @@ def check_aoi_tiles(aoi_tiles_gdf): json.dump(img_metadata_dict, fp) written_files.append(IMG_METADATA_FILE) - logger.info(f"...done. A file was written: {IMG_METADATA_FILE}") + logger.info(f"{DONE_MSG} A file was written: {IMG_METADATA_FILE}") # ------ Training/validation/test/other dataset generation @@ -404,7 +406,7 @@ def check_aoi_tiles(aoi_tiles_gdf): except Exception as e: logger.error(e) written_files.append(SPLIT_AOI_TILES_GEOJSON) - logger.info(f'...done. A file was written {SPLIT_AOI_TILES_GEOJSON}') + logger.info(f'{DONE_MSG} A file was written {SPLIT_AOI_TILES_GEOJSON}') img_md_df = pd.DataFrame.from_dict(img_metadata_dict, orient='index') img_md_df.reset_index(inplace=True) @@ -491,7 +493,7 @@ def check_aoi_tiles(aoi_tiles_gdf): toc = time.time() - logger.info("...done.") + logger.info(DONE_MSG) logger.info("You can now open a Linux shell and type the following command in order to create a .tar.gz archive including images and COCO annotations:") if GT_LABELS_GEOJSON: From 7ebf10a72dfb248d3d4fb4624e246565418ea732 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Wed, 24 May 2023 10:06:19 +0000 Subject: [PATCH 006/108] Introduce LabelOverflowException --- scripts/generate_tilesets.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/scripts/generate_tilesets.py b/scripts/generate_tilesets.py index a7e89a5..7e52e1e 100644 --- a/scripts/generate_tilesets.py +++ b/scripts/generate_tilesets.py @@ -35,6 +35,10 @@ DONE_MSG = "...done." +class LabelOverflowException(Exception): + "Raised when a label exceeds the tile size" + pass + def read_img_metadata(md_file, all_img_path): img_path = os.path.join(all_img_path, md_file.replace('json', 'tif')) @@ -77,8 +81,8 @@ def get_COCO_image_and_segmentations(tile, labels, COCO_license_id, output_dir): try: assert(min(segmentation) >= 0) assert(max(segmentation) <= min(COCO_image['width'], COCO_image['height'])) - except: - raise Exception(f"Label boundaries exceed this tile size! Tile ID = {_tile['id']}") + except AssertionError: + raise LabelOverflowException(f"Label boundaries exceed tile size - Tile ID = {_tile['id']}") segmentations.append(segmentation) @@ -467,10 +471,14 @@ def check_aoi_tiles(aoi_tiles_gdf): tiles_iterator = tmp_tiles_gdf.sort_index().iterrows() - results = Parallel(n_jobs=N_JOBS, backend="loky") \ - (delayed(get_COCO_image_and_segmentations) \ - (tile, labels_gdf, coco_license_id, OUTPUT_DIR) \ - for tile in tqdm( tiles_iterator, total=len(tmp_tiles_gdf) )) + try: + results = Parallel(n_jobs=N_JOBS, backend="loky") \ + (delayed(get_COCO_image_and_segmentations) \ + (tile, labels_gdf, coco_license_id, OUTPUT_DIR) \ + for tile in tqdm( tiles_iterator, total=len(tmp_tiles_gdf) )) + except Exception as e: + logger.critical(f"Tile generation failed. Exception: {e}") + sys.exit(1) for result in results: coco_image, segmentations = result From ae790297d85bcb99722d045ede1a56ec30a135bd Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Wed, 24 May 2023 15:10:33 +0000 Subject: [PATCH 007/108] ACheck and extract x, y, z coordinates from tile IDs at once --- helpers/MIL.py | 36 +++++++++------------ helpers/WMS.py | 17 +++------- helpers/XYZ.py | 17 +++------- helpers/misc.py | 16 --------- scripts/generate_tilesets.py | 63 +++++++++++++++++++++++++----------- 5 files changed, 70 insertions(+), 79 deletions(-) diff --git a/helpers/MIL.py b/helpers/MIL.py index e88c5b0..abc31cd 100644 --- a/helpers/MIL.py +++ b/helpers/MIL.py @@ -23,9 +23,9 @@ try: try: - from helpers.misc import reformat_xyz, image_metadata_to_world_file, bounds_to_bbox - except: - from misc import reformat_xyz, image_metadata_to_world_file, bounds_to_bbox + from helpers.misc import image_metadata_to_world_file, bounds_to_bbox + except ModuleNotFoundError: + from misc import image_metadata_to_world_file, bounds_to_bbox except Exception as e: logger.error(f"Could not import some dependencies. Exception: {e}") sys.exit(1) @@ -116,27 +116,21 @@ def get_job_dict(tiles_gdf, MIL_url, width, height, img_path, imageSR, save_meta job_dict = {} - #print('Computing xyz...') - gdf = tiles_gdf.apply(reformat_xyz, axis=1) - gdf.crs = tiles_gdf.crs - #print('...done.') + for tile in tqdm(tiles_gdf.itertuples(), total=len(tiles_gdf)): - for tile in tqdm(gdf.itertuples(), total=len(gdf)): - - x, y, z = tile.xyz - - img_filename = os.path.join(img_path, f'{z}_{x}_{y}.tif') + img_filename = os.path.join(img_path, f'{tile.z}_{tile.x}_{tile.y}.tif') bbox = bounds_to_bbox(tile.geometry.bounds) - job_dict[img_filename] = {'MIL_url': MIL_url, - 'bbox': bbox, - 'width': width, - 'height': height, - 'filename': img_filename, - 'imageSR': imageSR, - 'bboxSR': gdf.crs.to_epsg(), - 'save_metadata': save_metadata, - 'overwrite': overwrite + job_dict[img_filename] = { + 'MIL_url': MIL_url, + 'bbox': bbox, + 'width': width, + 'height': height, + 'filename': img_filename, + 'imageSR': imageSR, + 'bboxSR': tiles_gdf.crs.to_epsg(), + 'save_metadata': save_metadata, + 'overwrite': overwrite } return job_dict diff --git a/helpers/WMS.py b/helpers/WMS.py index a658df7..a9cce57 100644 --- a/helpers/WMS.py +++ b/helpers/WMS.py @@ -24,9 +24,9 @@ try: try: - from helpers.misc import reformat_xyz, image_metadata_to_world_file, bounds_to_bbox - except: - from misc import reformat_xyz, image_metadata_to_world_file, bounds_to_bbox + from helpers.misc import image_metadata_to_world_file, bounds_to_bbox + except ModuleNotFoundError: + from misc import image_metadata_to_world_file, bounds_to_bbox except Exception as e: logger.error(f"Could not import some dependencies. Exception: {e}") sys.exit(1) @@ -121,16 +121,9 @@ def get_job_dict(tiles_gdf, WMS_url, layers, width, height, img_path, srs, save_ job_dict = {} - #print('Computing xyz...') - gdf = tiles_gdf.apply(reformat_xyz, axis=1) - gdf.crs = tiles_gdf.crs - #print('...done.') + for tile in tqdm(tiles_gdf.itertuples(), total=len(tiles_gdf)): - for tile in tqdm(gdf.itertuples(), total=len(gdf)): - - x, y, z = tile.xyz - - img_filename = os.path.join(img_path, f'{z}_{x}_{y}.tif') + img_filename = os.path.join(img_path, f'{tile.z}_{tile.x}_{tile.y}.tif') bbox = bounds_to_bbox(tile.geometry.bounds) job_dict[img_filename] = { diff --git a/helpers/XYZ.py b/helpers/XYZ.py index d2f8b77..a11c9ac 100644 --- a/helpers/XYZ.py +++ b/helpers/XYZ.py @@ -18,9 +18,9 @@ try: try: - from helpers.misc import reformat_xyz, image_metadata_to_world_file, bounds_to_bbox - except: - from misc import reformat_xyz, image_metadata_to_world_file, bounds_to_bbox + from helpers.misc import image_metadata_to_world_file, bounds_to_bbox + except ModuleNotFoundError: + from misc import image_metadata_to_world_file, bounds_to_bbox except Exception as e: logger.error(f"Could not import some dependencies. Exception: {e}") sys.exit(1) @@ -130,16 +130,9 @@ def get_job_dict(tiles_gdf, XYZ_url, img_path, save_metadata=False, overwrite=Tr job_dict = {} - #print('Computing xyz...') - gdf = tiles_gdf.apply(reformat_xyz, axis=1) - gdf.crs = tiles_gdf.crs - #print('...done.') + for tile in tqdm(tiles_gdf.itertuples(), total=len(tiles_gdf)): - for tile in tqdm(gdf.itertuples(), total=len(gdf)): - - x, y, z = tile.xyz - - img_filename = os.path.join(img_path, f'{z}_{x}_{y}.tif') + img_filename = os.path.join(img_path, f'{tile.z}_{tile.x}_{tile.y}.tif') bbox = bounds_to_bbox(tile.geometry.bounds) job_dict[img_filename] = { diff --git a/helpers/misc.py b/helpers/misc.py index 095f204..d9c7dc2 100644 --- a/helpers/misc.py +++ b/helpers/misc.py @@ -169,22 +169,6 @@ def image_metadata_to_affine_transform(image_metadata): return affine -def reformat_xyz(row): - """ - convert 'id' string to list of ints for z,x,y - """ - x, y, z = row['id'].lstrip('(,)').rstrip('(,)').split(',') - - # check whether x, y, z are ints - assert str(int(x)) == str(x).strip(' ') - assert str(int(y)) == str(y).strip(' ') - assert str(int(z)) == str(z).strip(' ') - - row['xyz'] = [int(x), int(y), int(z)] - - return row - - def bounds_to_bbox(bounds): xmin = bounds[0] diff --git a/scripts/generate_tilesets.py b/scripts/generate_tilesets.py index 7e52e1e..ed98b69 100644 --- a/scripts/generate_tilesets.py +++ b/scripts/generate_tilesets.py @@ -40,6 +40,21 @@ class LabelOverflowException(Exception): pass +class MissingIdException(Exception): + "Raised when tiles are lacking IDs" + pass + + +class TileDuplicationException(Exception): + "Raised when the 'id' column contains duplicates" + pass + + +class BadTileIdException(Exception): + "Raised when tile IDs cannot be parsed into X, Y, Z" + pass + + def read_img_metadata(md_file, all_img_path): img_path = os.path.join(all_img_path, md_file.replace('json', 'tif')) @@ -89,24 +104,35 @@ def get_COCO_image_and_segmentations(tile, labels, COCO_license_id, output_dir): return (COCO_image, segmentations) -def check_aoi_tiles(aoi_tiles_gdf): - ''' - Check that the id of the AoI tile is exists and will be accepted by the function reformat_xyz - The format should be "(, , )" or ", , " - ''' +def extract_xyz(aoi_tiles_gdf): + def _id_to_xyz(row): + """ + convert 'id' string to list of ints for x,y,z + """ + + try: + x, y, z = row['id'].lstrip('(,)').rstrip('(,)').split(',') + except ValueError: + raise ValueError(f"Could not extract x, y, z from tile ID {row['id']}.") + + # check whether x, y, z are ints + assert str(int(x)) == str(x).strip(' '), "tile x coordinate is not actually integer" + assert str(int(y)) == str(y).strip(' '), "tile y coordinate is not actually integer" + assert str(int(z)) == str(z).strip(' '), "tile z coordinate is not actually integer" + + row['x'] = int(x) + row['y'] = int(y) + row['z'] = int(z) + + return row + if 'id' not in aoi_tiles_gdf.columns.to_list(): - raise Exception("No 'id' column was found in the AoI tiles dataset.") + raise MissingIdException("No 'id' column was found in the AoI tiles dataset.") if len(aoi_tiles_gdf[aoi_tiles_gdf.id.duplicated()]) > 0: - raise Exception("The 'id' column in the AoI tiles dataset should not contain any duplicate.") + raise TileDuplicationException("The 'id' column in the AoI tiles dataset should not contain any duplicate.") - try: - aoi_tiles_gdf.apply(misc.reformat_xyz, axis=1) - except: - raise Exception("IDs do not seem to be well-formatted. Here's how they must look like: (, , ), e.g. (, , ).") - - return - + return aoi_tiles_gdf.apply(_id_to_xyz, axis=1) if __name__ == "__main__": @@ -178,11 +204,12 @@ def check_aoi_tiles(aoi_tiles_gdf): logger.info("Loading AoI tiles as a GeoPandas DataFrame...") aoi_tiles_gdf = gpd.read_file(AOI_TILES_GEOJSON) logger.info(f"{DONE_MSG} {len(aoi_tiles_gdf)} records were found.") - logger.info("Checking whether AoI tiles are consistent and well-formatted...") + + logger.info("Extracting tile coordinates (x, y, z) from tile IDs...") try: - check_aoi_tiles(aoi_tiles_gdf) + aoi_tiles_gdf = extract_xyz(aoi_tiles_gdf) except Exception as e: - logger.critical(f"AoI tiles check failed. Exception: {e}") + logger.critical(f"[...] Exception: {e}") sys.exit(1) logger.info(DONE_MSG) @@ -296,7 +323,7 @@ def check_aoi_tiles(aoi_tiles_gdf): image_getter = XYZ.get_geotiff else: - logger.critical(f'Web Service of type "{ORTHO_WS_TYPE}" are not yet supported. Exiting.') + logger.critical(f'Web Services of type "{ORTHO_WS_TYPE}" are not supported. Exiting.') sys.exit(1) logger.info(DONE_MSG) From 507ae22665035acfd6f5acee6c19b998920e5ba5 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Wed, 24 May 2023 15:18:57 +0000 Subject: [PATCH 008/108] Do not ignore insecure request warnings --- helpers/MIL.py | 3 --- helpers/WMS.py | 3 --- helpers/XYZ.py | 3 --- 3 files changed, 9 deletions(-) diff --git a/helpers/MIL.py b/helpers/MIL.py index abc31cd..9cd787b 100644 --- a/helpers/MIL.py +++ b/helpers/MIL.py @@ -11,9 +11,6 @@ logging.config.fileConfig('logging.conf') logger = logging.getLogger('MIL') -from requests.packages.urllib3.exceptions import InsecureRequestWarning -requests.packages.urllib3.disable_warnings(InsecureRequestWarning) - from rasterio.transform import from_bounds from rasterio import rasterio, features from osgeo import gdal diff --git a/helpers/WMS.py b/helpers/WMS.py index a9cce57..28c6dfa 100644 --- a/helpers/WMS.py +++ b/helpers/WMS.py @@ -11,9 +11,6 @@ logging.config.fileConfig('logging.conf') logger = logging.getLogger('WMS') -from requests.packages.urllib3.exceptions import InsecureRequestWarning -requests.packages.urllib3.disable_warnings(InsecureRequestWarning) - from rasterio.transform import from_bounds from rasterio import rasterio, features from osgeo import gdal diff --git a/helpers/XYZ.py b/helpers/XYZ.py index a11c9ac..2c932d5 100644 --- a/helpers/XYZ.py +++ b/helpers/XYZ.py @@ -10,9 +10,6 @@ logging.config.fileConfig('logging.conf') logger = logging.getLogger('XYZ') -from requests.packages.urllib3.exceptions import InsecureRequestWarning -requests.packages.urllib3.disable_warnings(InsecureRequestWarning) - from osgeo import gdal from tqdm import tqdm From 185447352922da660f2665b5aa69f65a932cb22a Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Wed, 24 May 2023 15:26:49 +0000 Subject: [PATCH 009/108] Remove unused imports --- helpers/MIL.py | 5 ----- helpers/WMS.py | 6 ------ helpers/misc.py | 8 ++------ 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/helpers/MIL.py b/helpers/MIL.py index 9cd787b..7031ce8 100644 --- a/helpers/MIL.py +++ b/helpers/MIL.py @@ -4,18 +4,13 @@ import os, sys import json import requests -import pyproj import logging import logging.config logging.config.fileConfig('logging.conf') logger = logging.getLogger('MIL') -from rasterio.transform import from_bounds -from rasterio import rasterio, features from osgeo import gdal -from shapely.geometry import box -from shapely.affinity import affine_transform from tqdm import tqdm try: diff --git a/helpers/WMS.py b/helpers/WMS.py index 28c6dfa..6f1daa5 100644 --- a/helpers/WMS.py +++ b/helpers/WMS.py @@ -4,19 +4,13 @@ import os, sys import json import requests -import pyproj import logging import logging.config logging.config.fileConfig('logging.conf') logger = logging.getLogger('WMS') -from rasterio.transform import from_bounds -from rasterio import rasterio, features from osgeo import gdal -from shapely.geometry import box -from shapely.affinity import affine_transform - from tqdm import tqdm try: diff --git a/helpers/misc.py b/helpers/misc.py index d9c7dc2..4883b99 100644 --- a/helpers/misc.py +++ b/helpers/misc.py @@ -4,14 +4,10 @@ import warnings warnings.simplefilter(action='ignore', category=FutureWarning) -import os, sys -import pandas as pd +import os import geopandas as gpd -import numpy as np -from shapely.affinity import affine_transform, scale -from shapely.geometry import box -from rasterio import rasterio, features +from shapely.affinity import scale from rasterio.transform import from_bounds From 35e7ae2ffe5600f829bfa5d8bd97224031a48e18 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Wed, 24 May 2023 16:07:49 +0000 Subject: [PATCH 010/108] Introduce and handle custom exceptions for the COCO module --- helpers/COCO.py | 52 +++++++++++++++++++----------------- scripts/generate_tilesets.py | 16 ++++++++--- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/helpers/COCO.py b/helpers/COCO.py index 19b03ee..1a53faa 100644 --- a/helpers/COCO.py +++ b/helpers/COCO.py @@ -1,10 +1,11 @@ #!/bin/python # -*- coding: utf-8 -*- -import os +import os, sys import json import numpy as np import logging + from datetime import datetime, date from PIL import Image @@ -12,6 +13,21 @@ pil_logger.setLevel(logging.INFO) +class MissingImageIdException(Exception): + "Raised when an annotation is lacking the image ID field" + pass + + +class MissingCategoryIdException(Exception): + "Raised when an annotation is lacking the category ID field" + pass + + +class LicenseIdNotFoundException(Exception): + "Raised when a given license ID is not found" + pass + + class COCO: # cf. http://cocodataset.org/#format-data @@ -102,15 +118,11 @@ def annotation(self, def insert_annotation(self, the_annotation): # let's perform some checks... - try: - self._images_dict[the_annotation['image_id']] - except: - raise Exception("inexistent image id") + if 'image_id' not in the_annotation.keys(): + raise MissingImageIdException(f"Missing image ID = {the_annotation['image_id']}") - try: - self._categories_dict[the_annotation['category_id']] - except: - raise Exception("inexistent category id") + if 'category_id' not in the_annotation.keys(): + raise MissingCategoryIdException(f"Missing category ID = {the_annotation['category_id']}") if 'id' not in the_annotation: the_annotation['id'] = len(self.annotations) + 1 @@ -207,10 +219,8 @@ def image(self, def insert_image(self, the_image): # check whether the license_id is valid - try: - self._licenses_dict[the_image['license']] - except: - raise Exception("inexistent license id") + if the_image['license'] not in self._licenses_dict.keys(): + raise LicenseIdNotFoundException(f"License ID = {the_image['license']} not found.") if 'id' not in the_image: the_image['id'] = len(self.images)+1 @@ -287,21 +297,13 @@ def __repr__(self): coco.insert_category(cat) try: - ann = coco.annotation(1, 1, segmentation, 0) + ann = coco.annotation(the_image_id=1, the_category_id=1, the_segmentation=segmentation, the_iscrowd=0,the_annotation_id=0) coco.insert_annotation(ann) except Exception as e: - print(e) - - # img = coco.image('output/images-256', 'trn18_135553_92964.tif', 1) - # coco.insert_image(img) - - # try: - # img = coco.image('output/images-256', 'trn18_135553_92964.tif', 999) - # coco.insert_image(img) - # except Exception as e: - # print(e) + print(f"Failed to insert annotation. Exception: {e}") + sys.exit(1) - ann = coco.annotation(1, 1, segmentation, 0, 123) + ann = coco.annotation(the_image_id=1, the_category_id=1, the_segmentation=segmentation, the_iscrowd=0,the_annotation_id=123) coco.insert_annotation(ann) pprint(coco.to_json()) \ No newline at end of file diff --git a/scripts/generate_tilesets.py b/scripts/generate_tilesets.py index ed98b69..7a2b30c 100644 --- a/scripts/generate_tilesets.py +++ b/scripts/generate_tilesets.py @@ -491,7 +491,6 @@ def _id_to_xyz(row): coco_category_id = coco.insert_category(coco_category) tmp_tiles_gdf = split_aoi_tiles_with_img_md_gdf[split_aoi_tiles_with_img_md_gdf.dataset == dataset].dropna() - #tmp_tiles_gdf = tmp_tiles_gdf.to_crs(epsg=3857) if len(labels_gdf) > 0: assert(labels_gdf.crs == tmp_tiles_gdf.crs) @@ -509,7 +508,12 @@ def _id_to_xyz(row): for result in results: coco_image, segmentations = result - coco_image_id = coco.insert_image(coco_image) + + try: + coco_image_id = coco.insert_image(coco_image) + except Exception as e: + logger.critical(f"Could not insert image into the COCO data structure. Exception: {e}") + sys.exit(1) for segmentation in segmentations: @@ -519,11 +523,17 @@ def _id_to_xyz(row): the_iscrowd=0 ) - coco.insert_annotation(coco_annotation) + try: + coco.insert_annotation(coco_annotation) + except Exception as e: + logger.critical(f"Could not insert annotation into the COCO data structure. Exception: {e}") + sys.exit(1) COCO_file = os.path.join(OUTPUT_DIR, f'COCO_{dataset}.json') + with open(COCO_file, 'w') as fp: json.dump(coco.to_json(), fp) + written_files.append(COCO_file) From ea530c82d225d182efad761837f3094756bfde10 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Wed, 24 May 2023 16:12:40 +0000 Subject: [PATCH 011/108] Remove unexpected argument in logger.warning call --- scripts/generate_tilesets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/generate_tilesets.py b/scripts/generate_tilesets.py index 7a2b30c..6d2889c 100644 --- a/scripts/generate_tilesets.py +++ b/scripts/generate_tilesets.py @@ -338,7 +338,7 @@ def _id_to_xyz(row): for job in job_dict.keys(): if not os.path.isfile(job) or not os.path.isfile(job.replace('.tif', '.json')): all_tiles_were_downloaded = False - logger.warning('Failed task: ', job) + logger.warning(f"Failed job: {job}") if all_tiles_were_downloaded: logger.info(DONE_MSG) From c152550ae4c42f2fb1e0a5e6438bffdbda1a0410 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Wed, 24 May 2023 19:19:47 +0000 Subject: [PATCH 012/108] Use specific exception instead of a generic one. Rename variables starting with the_ --- helpers/misc.py | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/helpers/misc.py b/helpers/misc.py index 4883b99..574c610 100644 --- a/helpers/misc.py +++ b/helpers/misc.py @@ -45,7 +45,7 @@ def img_md_record_to_tile_id(img_md_record): def make_hard_link(row): if not os.path.isfile(row.img_file): - raise Exception('File not found.') + raise FileNotFoundError(row.img_file) src_file = row.img_file dst_file = src_file.replace('all', row.dataset) @@ -94,7 +94,6 @@ def get_metrics(tp_gdf, fp_gdf, fn_gdf): TP = len(tp_gdf) FP = len(fp_gdf) FN = len(fn_gdf) - #print(TP, FP, FN) if TP == 0: return 0, 0, 0 @@ -106,41 +105,37 @@ def get_metrics(tp_gdf, fp_gdf, fn_gdf): return precision, recall, f1 -def get_fractional_sets(the_preds_gdf, the_labels_gdf): +def get_fractional_sets(preds_gdf, labels_gdf): - preds_gdf = the_preds_gdf.copy() - labels_gdf = the_labels_gdf.copy() + _preds_gdf = preds_gdf.copy() + _labels_gdf = labels_gdf.copy() - if len(labels_gdf) == 0: - fp_gdf = preds_gdf.copy() + if len(_labels_gdf) == 0: + fp_gdf = _preds_gdf.copy() tp_gdf = gpd.GeoDataFrame() fn_gdf = gpd.GeoDataFrame() return tp_gdf, fp_gdf, fn_gdf - try: - assert(preds_gdf.crs == labels_gdf.crs), f"CRS Mismatch: predictions' CRS = {preds_gdf.crs}, labels' CRS = {labels_gdf.crs}" - except Exception as e: - raise Exception(e) - + assert(_preds_gdf.crs == _labels_gdf.crs), f"CRS Mismatch: predictions' CRS = {_preds_gdf.crs}, labels' CRS = {_labels_gdf.crs}" # we add a dummy column to the labels dataset, which should not exist in predictions too; # this allows us to distinguish matching from non-matching predictions - labels_gdf['dummy_id'] = labels_gdf.index + _labels_gdf['dummy_id'] = _labels_gdf.index # TRUE POSITIVES - left_join = gpd.sjoin(preds_gdf, labels_gdf, how='left', predicate='intersects', lsuffix='left', rsuffix='right') + left_join = gpd.sjoin(_preds_gdf, _labels_gdf, how='left', predicate='intersects', lsuffix='left', rsuffix='right') tp_gdf = left_join[left_join.dummy_id.notnull()].copy() tp_gdf.drop_duplicates(subset=['dummy_id', 'tile_id'], inplace=True) tp_gdf.drop(columns=['dummy_id'], inplace=True) - # FALSE POSITIVES -> potentially "new" swimming pools + # FALSE POSITIVES fp_gdf = left_join[left_join.dummy_id.isna()].copy() assert(len(fp_gdf[fp_gdf.duplicated()]) == 0) fp_gdf.drop(columns=['dummy_id'], inplace=True) - # FALSE NEGATIVES -> potentially, objects that are not actual swimming pools! - right_join = gpd.sjoin(preds_gdf, labels_gdf, how='right', predicate='intersects', lsuffix='left', rsuffix='right') + # FALSE NEGATIVES + right_join = gpd.sjoin(_preds_gdf, _labels_gdf, how='right', predicate='intersects', lsuffix='left', rsuffix='right') fn_gdf = right_join[right_join.score.isna()].copy() fn_gdf.drop_duplicates(subset=['dummy_id', 'tile_id'], inplace=True) From 5564576e1271cbe802200f97e3c799d340ffab1a Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Wed, 24 May 2023 19:20:44 +0000 Subject: [PATCH 013/108] Introduce and handle custom exceptions. Rename variables starting with "the_" --- helpers/COCO.py | 175 ++++++++++++++++++----------------- scripts/generate_tilesets.py | 25 +++-- 2 files changed, 101 insertions(+), 99 deletions(-) diff --git a/helpers/COCO.py b/helpers/COCO.py index 1a53faa..01f744a 100644 --- a/helpers/COCO.py +++ b/helpers/COCO.py @@ -48,22 +48,22 @@ def __init__(self): return None def set_info(self, - the_year: int, - the_version: str, - the_description: str, - the_contributor: str, - the_url: str, - the_date_created: datetime=None): + year: int, + version: str, + description: str, + contributor: str, + url: str, + date_created: datetime=None): - if the_date_created == None: - the_date_created = date.today() + if date_created == None: + date_created = date.today() - info = {"year": the_year, - "version": the_version, - "description": the_description, - "contributor": the_contributor, - "url": the_url, - "date_created": the_date_created, + info = {"year": year, + "version": version, + "description": description, + "contributor": contributor, + "url": url, + "date_created": date_created, } self.info = info @@ -72,23 +72,23 @@ def set_info(self, def annotation(self, - the_image_id: int, - the_category_id: int, - the_segmentation: list, - the_iscrowd: int, - the_annotation_id: int=None): + image_id: int, + category_id: int, + segmentation: list, + iscrowd: int, + annotation_id: int=None): _annotation = { - "image_id": the_image_id, - "category_id": the_category_id, - "segmentation": the_segmentation, - #"area": the_area, - #"bbox": the_bbox, #[x,y,width,height], - "iscrowd": the_iscrowd, + "image_id": image_id, + "category_id": category_id, + "segmentation": segmentation, + #"area": area, + #"bbox": bbox, #[x,y,width,height], + "iscrowd": iscrowd, } - if the_annotation_id != None: - _annotation['id'] = the_annotation_id + if annotation_id != None: + _annotation['id'] = annotation_id # init _annotation['area'] = 0 @@ -97,7 +97,7 @@ def annotation(self, ymin = np.inf ymax = -np.inf - for seg in the_segmentation: + for seg in segmentation: xx = [x for idx, x in enumerate(seg) if idx % 2 == 0] yy = [x for idx, x in enumerate(seg) if idx % 2 == 1] @@ -115,100 +115,105 @@ def annotation(self, return _annotation - def insert_annotation(self, the_annotation): + def insert_annotation(self, annotation): # let's perform some checks... - if 'image_id' not in the_annotation.keys(): - raise MissingImageIdException(f"Missing image ID = {the_annotation['image_id']}") + if 'image_id' not in annotation.keys(): + raise MissingImageIdException(f"Missing image ID = {annotation['image_id']}") - if 'category_id' not in the_annotation.keys(): - raise MissingCategoryIdException(f"Missing category ID = {the_annotation['category_id']}") + if 'category_id' not in annotation.keys(): + raise MissingCategoryIdException(f"Missing category ID = {annotation['category_id']}") - if 'id' not in the_annotation: - the_annotation['id'] = len(self.annotations) + 1 + if 'id' not in annotation: + annotation['id'] = len(self.annotations) + 1 - self.annotations.append(the_annotation) + self.annotations.append(annotation) - self._annotations_dict[the_annotation['id']] = the_annotation + self._annotations_dict[annotation['id']] = annotation - return the_annotation['id'] + return annotation['id'] - def license(self, the_name: str, the_url: str, the_id: int=None): + def license(self, name: str, url: str, id: int=None): _license = { - "name": the_name, - "url": the_url + "name": name, + "url": url } - if the_id != None: - _license['id'] = the_id + if id != None: + _license['id'] = id return _license - def insert_license(self, the_license): + def insert_license(self, license): - if 'id' not in the_license: - the_license['id'] = len(self.licenses) + 1 + if 'id' not in license: + license['id'] = len(self.licenses) + 1 - self.licenses.append(the_license) - self._licenses_dict[the_license['id']] = the_license + self.licenses.append(license) + self._licenses_dict[license['id']] = license - return the_license['id'] + return license['id'] - def category(self, the_name: str, the_supercategory: str, the_id: int=None): + def category(self, name: str, supercategory: str, id: int=None): _category = { - "name": the_name, - "supercategory": the_supercategory + "name": name, + "supercategory": supercategory } - if the_id != None: - _category['id'] = the_id + if id != None: + _category['id'] = id return _category - def insert_category(self, the_category): + def insert_category(self, category): - if 'id' not in the_category: - the_category['id'] = len(self.categories) + 1 + if 'id' not in category: + category['id'] = len(self.categories) + 1 - self.categories.append(the_category) - self._categories_dict[the_category['id']] = the_category + self.categories.append(category) + self._categories_dict[category['id']] = category - return the_category['id'] + return category['id'] def image(self, - the_path: str, - the_filename: str, - the_license_id: int, - the_id: int=None, - the_date_captured: datetime=None, - the_flickr_url: str=None, - the_coco_url: str=None): + path: str, + filename: str, + license_id: int, + id: int=None, + date_captured: datetime=None, + flickr_url: str=None, + coco_url: str=None): - full_filename = os.path.join(the_path, the_filename) + full_filename = os.path.join(path, filename) img = Image.open(full_filename) # this was checked to be faster than skimage and rasterio width, height = img.size image = { "width": width, "height": height, - "file_name": the_filename, - "license": the_license_id + "file_name": filename, + "license": license_id } - for el in ['id', 'flickr_url', 'coco_url']: - if eval('the_' + el) != None: - image[el] = eval('the_' + el) + if id != None: + image['id'] = id - if the_date_captured != None: - image['date_captured'] = the_date_captured + if flickr_url != None: + image['flickr_url'] = flickr_url + + if coco_url != None: + image['coco_url'] = coco_url + + if date_captured != None: + image['date_captured'] = date_captured else: dc = os.stat(full_filename).st_ctime image['date_captured'] = datetime.utcfromtimestamp(dc) @@ -216,19 +221,19 @@ def image(self, return image - def insert_image(self, the_image): + def insert_image(self, image): # check whether the license_id is valid - if the_image['license'] not in self._licenses_dict.keys(): - raise LicenseIdNotFoundException(f"License ID = {the_image['license']} not found.") + if image['license'] not in self._licenses_dict.keys(): + raise LicenseIdNotFoundException(f"License ID = {image['license']} not found.") - if 'id' not in the_image: - the_image['id'] = len(self.images)+1 + if 'id' not in image: + image['id'] = len(self.images)+1 - self.images.append(the_image) - self._images_dict[the_image['id']] = the_image + self.images.append(image) + self._images_dict[image['id']] = image - return the_image['id'] + return image['id'] def to_json(self): @@ -297,13 +302,13 @@ def __repr__(self): coco.insert_category(cat) try: - ann = coco.annotation(the_image_id=1, the_category_id=1, the_segmentation=segmentation, the_iscrowd=0,the_annotation_id=0) + ann = coco.annotation(image_id=1, category_id=1, segmentation=segmentation, iscrowd=0, annotation_id=0) coco.insert_annotation(ann) except Exception as e: print(f"Failed to insert annotation. Exception: {e}") sys.exit(1) - ann = coco.annotation(the_image_id=1, the_category_id=1, the_segmentation=segmentation, the_iscrowd=0,the_annotation_id=123) + ann = coco.annotation(image_id=1, category_id=1, segmentation=segmentation, iscrowd=0, annotation_id=123) coco.insert_annotation(ann) pprint(coco.to_json()) \ No newline at end of file diff --git a/scripts/generate_tilesets.py b/scripts/generate_tilesets.py index 6d2889c..772e132 100644 --- a/scripts/generate_tilesets.py +++ b/scripts/generate_tilesets.py @@ -81,11 +81,6 @@ def get_COCO_image_and_segmentations(tile, labels, COCO_license_id, output_dir): # note the .explode() which turns Multipolygon into Polygons clipped_labels_gdf = gpd.clip(labels, _tile['geometry']).explode() - #try: - # assert( len(clipped_labels_gdf) > 0 ) - #except: - # raise Exception(f'No labels found within this tile! Tile ID = {tile.id}') - for label in clipped_labels_gdf.itertuples(): scaled_poly = misc.scale_polygon(label.geometry, xmin, ymin, xmax, ymax, COCO_image['width'], COCO_image['height']) @@ -477,17 +472,17 @@ def _id_to_xyz(row): logger.info(f'Generating COCO annotations for the {dataset} dataset...') coco = COCO.COCO() - coco.set_info(the_year=COCO_YEAR, - the_version=COCO_VERSION, - the_description=f"{COCO_DESCRIPTION} - {dataset} dataset", - the_contributor=COCO_CONTRIBUTOR, - the_url=COCO_URL) + coco.set_info(year=COCO_YEAR, + version=COCO_VERSION, + description=f"{COCO_DESCRIPTION} - {dataset} dataset", + contributor=COCO_CONTRIBUTOR, + url=COCO_URL) - coco_license = coco.license(the_name=COCO_LICENSE_NAME, the_url=COCO_LICENSE_URL) + coco_license = coco.license(name=COCO_LICENSE_NAME, url=COCO_LICENSE_URL) coco_license_id = coco.insert_license(coco_license) # TODO: read (super)category from the labels datataset - coco_category = coco.category(the_name=COCO_CATEGORY_NAME, the_supercategory=COCO_CATEGORY_SUPERCATEGORY) + coco_category = coco.category(name=COCO_CATEGORY_NAME, supercategory=COCO_CATEGORY_SUPERCATEGORY) coco_category_id = coco.insert_category(coco_category) tmp_tiles_gdf = split_aoi_tiles_with_img_md_gdf[split_aoi_tiles_with_img_md_gdf.dataset == dataset].dropna() @@ -507,6 +502,7 @@ def _id_to_xyz(row): sys.exit(1) for result in results: + coco_image, segmentations = result try: @@ -517,10 +513,11 @@ def _id_to_xyz(row): for segmentation in segmentations: - coco_annotation = coco.annotation(coco_image_id, + coco_annotation = coco.annotation( + coco_image_id, coco_category_id, [segmentation], - the_iscrowd=0 + iscrowd=0 ) try: From 3b7114f9ffd533c8feb0ee07164ee978a2eb1417 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Wed, 24 May 2023 19:25:09 +0000 Subject: [PATCH 014/108] Rename _PolyArea method as _compute_polygon_area --- helpers/COCO.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpers/COCO.py b/helpers/COCO.py index 01f744a..b7b65a3 100644 --- a/helpers/COCO.py +++ b/helpers/COCO.py @@ -108,7 +108,7 @@ def annotation(self, ymin = np.min([ymin, np.min(yy)]) ymax = np.max([ymax, np.max(yy)]) - _annotation['area'] += self._PolyArea(xx, yy) + _annotation['area'] += self._compute_polygon_area(xx, yy) _annotation['bbox'] = [xmin, ymin, xmax-xmin, ymax-ymin] @@ -248,7 +248,7 @@ def to_json(self): return json.loads(json.dumps(out, default=self._default)) # cf. https://stackoverflow.com/questions/24467972/calculate-area-of-polygon-given-x-y-coordinates - def _PolyArea(self, x, y): + def _compute_polygon_area(self, x, y): return 0.5*np.abs(np.dot(x,np.roll(y,1))-np.dot(y,np.roll(x,1))) From 0e95411afc54e0c1d2d3e5c72f56b9b8c3db3f54 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Wed, 24 May 2023 19:30:49 +0000 Subject: [PATCH 015/108] Remove TODO messages --- examples/swimming-pool-detection/GE/prepare_data.py | 5 +---- examples/swimming-pool-detection/NE/prepare_data.py | 2 -- helpers/misc.py | 1 - scripts/assess_predictions.py | 1 - scripts/generate_tilesets.py | 2 -- 5 files changed, 1 insertion(+), 10 deletions(-) diff --git a/examples/swimming-pool-detection/GE/prepare_data.py b/examples/swimming-pool-detection/GE/prepare_data.py index f9f7e0e..0b88a23 100644 --- a/examples/swimming-pool-detection/GE/prepare_data.py +++ b/examples/swimming-pool-detection/GE/prepare_data.py @@ -22,7 +22,6 @@ if __name__ == "__main__": - tic = time.time() logger.info('Starting...') @@ -35,7 +34,6 @@ with open(args.config_file) as fp: cfg = yaml.load(fp, Loader=yaml.FullLoader)[os.path.basename(__file__)] - # TODO: check whether the configuration file contains the required information OUTPUT_DIR = cfg['output_folder'] LAKES_SHPFILE = cfg['datasets']['lakes_shapefile'] PARCELS_SHPFILE = cfg['datasets']['parcels_shapefile'] @@ -49,7 +47,7 @@ written_files = [] - # ------ Down(loading) datasets + # ------ (Down)loading datasets dataset_dict = {} @@ -68,7 +66,6 @@ written_files.append(shpfile_path) logger.info(f"...done. A file was written: {shpfile_path}") - # TODO: check file integrity (ex.: md5sum) logger.info(f"Loading the {dataset} dataset as a GeoPandas DataFrame...") dataset_dict[dataset] = gpd.read_file(f'zip://{shpfile_path}') logger.info(f"...done. {len(dataset_dict[dataset])} records were found.") diff --git a/examples/swimming-pool-detection/NE/prepare_data.py b/examples/swimming-pool-detection/NE/prepare_data.py index 795488c..567f21f 100644 --- a/examples/swimming-pool-detection/NE/prepare_data.py +++ b/examples/swimming-pool-detection/NE/prepare_data.py @@ -35,7 +35,6 @@ with open(args.config_file) as fp: cfg = yaml.load(fp, Loader=yaml.FullLoader)[os.path.basename(__file__)] - # TODO: check whether the configuration file contains the required information OUTPUT_DIR = cfg['output_folder'] # sectors GROUND_TRUTH_SECTORS_SHPFILE = cfg['datasets']['ground_truth_sectors_shapefile'] @@ -64,7 +63,6 @@ shpfile = eval(f'{dataset.upper()}_SHPFILE')#.split('/')[-1] - # TODO: check file integrity (ex.: md5sum) logger.info(f"Loading the {dataset} dataset as a GeoPandas DataFrame...") dataset_dict[dataset] = gpd.read_file(f'{shpfile}') logger.info(f"...done. {len(dataset_dict[dataset])} records were found.") diff --git a/helpers/misc.py b/helpers/misc.py index 574c610..11b7a50 100644 --- a/helpers/misc.py +++ b/helpers/misc.py @@ -20,7 +20,6 @@ def scale_polygon(shapely_polygon, xmin, ymin, xmax, ymax, width, height): xx, yy = shapely_polygon.exterior.coords.xy - # TODO: vectorize! scaled_polygon = [scale_point(x, y, xmin, ymin, xmax, ymax, width, height) for x, y in zip(xx, yy)] return scaled_polygon diff --git a/scripts/assess_predictions.py b/scripts/assess_predictions.py index e2a5eb9..59979be 100644 --- a/scripts/assess_predictions.py +++ b/scripts/assess_predictions.py @@ -43,7 +43,6 @@ with open(args.config_file) as fp: cfg = yaml.load(fp, Loader=yaml.FullLoader)[os.path.basename(__file__)] - # TODO: check whether the configuration file contains the required information OUTPUT_DIR = cfg['output_folder'] IMG_METADATA_FILE = cfg['datasets']['image_metadata_json'] PREDICTION_FILES = cfg['datasets']['predictions'] diff --git a/scripts/generate_tilesets.py b/scripts/generate_tilesets.py index 772e132..88e2a2f 100644 --- a/scripts/generate_tilesets.py +++ b/scripts/generate_tilesets.py @@ -145,7 +145,6 @@ def _id_to_xyz(row): with open(args.config_file) as fp: cfg = yaml.load(fp, Loader=yaml.FullLoader)[os.path.basename(__file__)] - # TODO: check whether the configuration file contains the required information DEBUG_MODE = cfg['debug_mode'] OUTPUT_DIR = cfg['output_folder'] @@ -481,7 +480,6 @@ def _id_to_xyz(row): coco_license = coco.license(name=COCO_LICENSE_NAME, url=COCO_LICENSE_URL) coco_license_id = coco.insert_license(coco_license) - # TODO: read (super)category from the labels datataset coco_category = coco.category(name=COCO_CATEGORY_NAME, supercategory=COCO_CATEGORY_SUPERCATEGORY) coco_category_id = coco.insert_category(coco_category) From fcc0f3fc49e78c66f9e05025f2edad8d8ce20a45 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Wed, 24 May 2023 19:36:12 +0000 Subject: [PATCH 016/108] Introduce BadFileExtensionException and UnsupportedImageFormatException --- helpers/MIL.py | 8 +++----- helpers/XYZ.py | 13 +++++++++---- helpers/misc.py | 5 +++++ 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/helpers/MIL.py b/helpers/MIL.py index 7031ce8..ed0222b 100644 --- a/helpers/MIL.py +++ b/helpers/MIL.py @@ -15,9 +15,9 @@ try: try: - from helpers.misc import image_metadata_to_world_file, bounds_to_bbox + from helpers.misc import image_metadata_to_world_file, bounds_to_bbox, BadFileExtensionException except ModuleNotFoundError: - from misc import image_metadata_to_world_file, bounds_to_bbox + from misc import image_metadata_to_world_file, bounds_to_bbox, BadFileExtensionException except Exception as e: logger.error(f"Could not import some dependencies. Exception: {e}") sys.exit(1) @@ -29,7 +29,7 @@ def get_geotiff(MIL_url, bbox, width, height, filename, imageSR="3857", bboxSR=" """ if not filename.endswith('.tif'): - raise Exception("Filename must end with .tif") + raise BadFileExtensionException("Filename must end with .tif") png_filename = filename.replace('.tif', '_.png') pgw_filename = filename.replace('.tif', '_.pgw') @@ -68,8 +68,6 @@ def get_geotiff(MIL_url, bbox, width, height, filename, imageSR="3857", bboxSR=" } } } - - #params = {'bbox': bbox, 'format': 'tif', 'size': f'{width},{height}', 'f': 'pjson', 'imageSR': imageSR, 'bboxSR': bboxSR} r = requests.post(MIL_url + '/export', data=params, timeout=30) diff --git a/helpers/XYZ.py b/helpers/XYZ.py index 2c932d5..240784c 100644 --- a/helpers/XYZ.py +++ b/helpers/XYZ.py @@ -15,14 +15,19 @@ try: try: - from helpers.misc import image_metadata_to_world_file, bounds_to_bbox + from helpers.misc import image_metadata_to_world_file, bounds_to_bbox, BadFileExtensionException except ModuleNotFoundError: - from misc import image_metadata_to_world_file, bounds_to_bbox + from misc import image_metadata_to_world_file, bounds_to_bbox, BadFileExtensionException except Exception as e: logger.error(f"Could not import some dependencies. Exception: {e}") sys.exit(1) +class UnsupportedImageFormatException(Exception): + "Raised when the detected image format is not supported" + pass + + def detect_img_format(url): lower_url = url.lower() @@ -43,12 +48,12 @@ def get_geotiff(XYZ_url, bbox, xyz, filename, save_metadata=False, overwrite=Tru """ if not filename.endswith('.tif'): - raise Exception("Filename must end with .tif") + raise BadFileExtensionException("Filename must end with .tif") img_format = detect_img_format(XYZ_url) if not img_format: - raise Exception("Unsupported image format") + raise UnsupportedImageFormatException("Unsupported image format") img_filename = filename.replace('.tif', f'_.{img_format}') wld_filename = filename.replace('.tif', '_.wld') # world file diff --git a/helpers/misc.py b/helpers/misc.py index 11b7a50..8a2849f 100644 --- a/helpers/misc.py +++ b/helpers/misc.py @@ -11,6 +11,11 @@ from rasterio.transform import from_bounds +class BadFileExtensionException(Exception): + "Raised when the file extension is different from the expected one" + pass + + def scale_point(x, y, xmin, ymin, xmax, ymax, width, height): return (x-xmin)/(xmax-xmin)*(width), (ymax-y)/(ymax-ymin)*(height) From 52d0c7fb0efb3096a884b77223f2c9f0f05b742d Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Wed, 24 May 2023 19:56:27 +0000 Subject: [PATCH 017/108] Use custom exception "BadFileExtensionException" --- helpers/WMS.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helpers/WMS.py b/helpers/WMS.py index 6f1daa5..c020c99 100644 --- a/helpers/WMS.py +++ b/helpers/WMS.py @@ -15,9 +15,9 @@ try: try: - from helpers.misc import image_metadata_to_world_file, bounds_to_bbox + from helpers.misc import image_metadata_to_world_file, bounds_to_bbox, BadFileExtensionException except ModuleNotFoundError: - from misc import image_metadata_to_world_file, bounds_to_bbox + from misc import image_metadata_to_world_file, bounds_to_bbox, BadFileExtensionException except Exception as e: logger.error(f"Could not import some dependencies. Exception: {e}") sys.exit(1) @@ -29,7 +29,7 @@ def get_geotiff(WMS_url, layers, bbox, width, height, filename, srs="EPSG:3857", """ if not filename.endswith('.tif'): - raise Exception("Filename must end with .tif") + raise BadFileExtensionException("Filename must end with .tif") png_filename = filename.replace('.tif', '_.png') pgw_filename = filename.replace('.tif', '_.pgw') From 6c1a6a01b2428e1cdfd6a99a9a834a46e7c166d4 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Wed, 24 May 2023 20:06:07 +0000 Subject: [PATCH 018/108] Rename variables in order to comply with Python naming conventions --- helpers/MIL.py | 20 ++++++++++---------- helpers/WMS.py | 8 ++++---- helpers/XYZ.py | 12 ++++++------ scripts/generate_tilesets.py | 18 +++++++++--------- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/helpers/MIL.py b/helpers/MIL.py index ed0222b..6450e97 100644 --- a/helpers/MIL.py +++ b/helpers/MIL.py @@ -23,7 +23,7 @@ sys.exit(1) -def get_geotiff(MIL_url, bbox, width, height, filename, imageSR="3857", bboxSR="3857", save_metadata=False, overwrite=True): +def get_geotiff(mil_url, bbox, width, height, filename, image_sr="3857", bbox_sr="3857", save_metadata=False, overwrite=True): """ by default, bbox must be in EPSG:3857 """ @@ -48,8 +48,8 @@ def get_geotiff(MIL_url, bbox, width, height, filename, imageSR="3857", bboxSR=" format='png', size=f'{width},{height}', f='image', - imageSR=imageSR, - bboxSR=bboxSR, + imageSR=image_sr, + bboxSR=bbox_sr, transparent=False ) @@ -64,12 +64,12 @@ def get_geotiff(MIL_url, bbox, width, height, filename, imageSR="3857", bboxSR=" "xmax": xmax, "ymax": ymax, 'spatialReference': { - 'latestWkid': bboxSR + 'latestWkid': bbox_sr } } } - r = requests.post(MIL_url + '/export', data=params, timeout=30) + r = requests.post(mil_url + '/export', data=params, timeout=30) if r.status_code == 200: @@ -87,7 +87,7 @@ def get_geotiff(MIL_url, bbox, width, height, filename, imageSR="3857", bboxSR=" try: src_ds = gdal.Open(png_filename) - gdal.Translate(geotiff_filename, src_ds, options=f'-of GTiff -a_srs EPSG:{imageSR}') + gdal.Translate(geotiff_filename, src_ds, options=f'-of GTiff -a_srs EPSG:{image_sr}') src_ds = None except Exception as e: logger.warning(f"Exception in the 'get_geotiff' function: {e}") @@ -102,7 +102,7 @@ def get_geotiff(MIL_url, bbox, width, height, filename, imageSR="3857", bboxSR=" return {} -def get_job_dict(tiles_gdf, MIL_url, width, height, img_path, imageSR, save_metadata=False, overwrite=True): +def get_job_dict(tiles_gdf, mil_url, width, height, img_path, image_sr, save_metadata=False, overwrite=True): job_dict = {} @@ -112,13 +112,13 @@ def get_job_dict(tiles_gdf, MIL_url, width, height, img_path, imageSR, save_meta bbox = bounds_to_bbox(tile.geometry.bounds) job_dict[img_filename] = { - 'MIL_url': MIL_url, + 'mil_url': mil_url, 'bbox': bbox, 'width': width, 'height': height, 'filename': img_filename, - 'imageSR': imageSR, - 'bboxSR': tiles_gdf.crs.to_epsg(), + 'image_sr': image_sr, + 'bbox_sr': tiles_gdf.crs.to_epsg(), 'save_metadata': save_metadata, 'overwrite': overwrite } diff --git a/helpers/WMS.py b/helpers/WMS.py index c020c99..bf3659d 100644 --- a/helpers/WMS.py +++ b/helpers/WMS.py @@ -23,7 +23,7 @@ sys.exit(1) -def get_geotiff(WMS_url, layers, bbox, width, height, filename, srs="EPSG:3857", save_metadata=False, overwrite=True): +def get_geotiff(wms_url, layers, bbox, width, height, filename, srs="EPSG:3857", save_metadata=False, overwrite=True): """ ... """ @@ -75,7 +75,7 @@ def get_geotiff(WMS_url, layers, bbox, width, height, filename, srs="EPSG:3857", } } - r = requests.get(WMS_url, params=params, allow_redirects=True) + r = requests.get(wms_url, params=params, allow_redirects=True) if r.status_code == 200: @@ -108,7 +108,7 @@ def get_geotiff(WMS_url, layers, bbox, width, height, filename, srs="EPSG:3857", return {} -def get_job_dict(tiles_gdf, WMS_url, layers, width, height, img_path, srs, save_metadata=False, overwrite=True): +def get_job_dict(tiles_gdf, wms_url, layers, width, height, img_path, srs, save_metadata=False, overwrite=True): job_dict = {} @@ -118,7 +118,7 @@ def get_job_dict(tiles_gdf, WMS_url, layers, width, height, img_path, srs, save_ bbox = bounds_to_bbox(tile.geometry.bounds) job_dict[img_filename] = { - 'WMS_url': WMS_url, + 'wms_url': wms_url, 'layers': layers, 'bbox': bbox, 'width': width, diff --git a/helpers/XYZ.py b/helpers/XYZ.py index 240784c..831d869 100644 --- a/helpers/XYZ.py +++ b/helpers/XYZ.py @@ -42,7 +42,7 @@ def detect_img_format(url): return None -def get_geotiff(XYZ_url, bbox, xyz, filename, save_metadata=False, overwrite=True): +def get_geotiff(xyz_url, bbox, xyz, filename, save_metadata=False, overwrite=True): """ ... """ @@ -50,7 +50,7 @@ def get_geotiff(XYZ_url, bbox, xyz, filename, save_metadata=False, overwrite=Tru if not filename.endswith('.tif'): raise BadFileExtensionException("Filename must end with .tif") - img_format = detect_img_format(XYZ_url) + img_format = detect_img_format(xyz_url) if not img_format: raise UnsupportedImageFormatException("Unsupported image format") @@ -69,11 +69,11 @@ def get_geotiff(XYZ_url, bbox, xyz, filename, save_metadata=False, overwrite=Tru x, y, z = xyz - XYZ_url_completed = XYZ_url.replace('{z}', str(z)) .replace('{x}', str(x)).replace('{y}', str(y)) + xyz_url_completed = xyz_url.replace('{z}', str(z)) .replace('{x}', str(x)).replace('{y}', str(y)) xmin, ymin, xmax, ymax = [float(x) for x in bbox.split(',')] - r = requests.get(XYZ_url_completed, allow_redirects=True) + r = requests.get(xyz_url_completed, allow_redirects=True) if r.status_code == 200: @@ -128,7 +128,7 @@ def get_geotiff(XYZ_url, bbox, xyz, filename, save_metadata=False, overwrite=Tru -def get_job_dict(tiles_gdf, XYZ_url, img_path, save_metadata=False, overwrite=True): +def get_job_dict(tiles_gdf, xyz_url, img_path, save_metadata=False, overwrite=True): job_dict = {} @@ -138,7 +138,7 @@ def get_job_dict(tiles_gdf, XYZ_url, img_path, save_metadata=False, overwrite=Tr bbox = bounds_to_bbox(tile.geometry.bounds) job_dict[img_filename] = { - 'XYZ_url': XYZ_url, + 'xyz_url': xyz_url, 'bbox': bbox, 'xyz': tile.xyz, 'filename': img_filename, diff --git a/scripts/generate_tilesets.py b/scripts/generate_tilesets.py index 88e2a2f..4a3b646 100644 --- a/scripts/generate_tilesets.py +++ b/scripts/generate_tilesets.py @@ -62,7 +62,7 @@ def read_img_metadata(md_file, all_img_path): return {img_path: json.load(fp)} -def get_COCO_image_and_segmentations(tile, labels, COCO_license_id, output_dir): +def get_coco_image_and_segmentations(tile, labels, coco_license_id, output_dir): _id, _tile = tile @@ -71,7 +71,7 @@ def get_COCO_image_and_segmentations(tile, labels, COCO_license_id, output_dir): this_tile_dirname = os.path.relpath(_tile['img_file'].replace('all', _tile['dataset']), output_dir) this_tile_dirname = this_tile_dirname.replace('\\', '/') # should the dirname be generated from Windows - COCO_image = coco_obj.image(output_dir, this_tile_dirname, COCO_license_id) + coco_image = coco_obj.image(output_dir, this_tile_dirname, coco_license_id) segmentations = [] if len(labels) > 0: @@ -83,20 +83,20 @@ def get_COCO_image_and_segmentations(tile, labels, COCO_license_id, output_dir): for label in clipped_labels_gdf.itertuples(): scaled_poly = misc.scale_polygon(label.geometry, xmin, ymin, xmax, ymax, - COCO_image['width'], COCO_image['height']) + coco_image['width'], coco_image['height']) scaled_poly = scaled_poly[:-1] # let's remove the last point segmentation = misc.my_unpack(scaled_poly) try: assert(min(segmentation) >= 0) - assert(max(segmentation) <= min(COCO_image['width'], COCO_image['height'])) + assert(max(segmentation) <= min(coco_image['width'], coco_image['height'])) except AssertionError: raise LabelOverflowException(f"Label boundaries exceed tile size - Tile ID = {_tile['id']}") segmentations.append(segmentation) - return (COCO_image, segmentations) + return (coco_image, segmentations) def extract_xyz(aoi_tiles_gdf): @@ -273,11 +273,11 @@ def _id_to_xyz(row): job_dict = MIL.get_job_dict( tiles_gdf=aoi_tiles_gdf.to_crs(ORTHO_WS_SRS), # <- note the reprojection - MIL_url=ORTHO_WS_URL, + mil_url=ORTHO_WS_URL, width=TILE_SIZE, height=TILE_SIZE, img_path=ALL_IMG_PATH, - imageSR=ORTHO_WS_SRS.split(":")[1], + image_sr=ORTHO_WS_SRS.split(":")[1], save_metadata=SAVE_METADATA, overwrite=OVERWRITE ) @@ -290,7 +290,7 @@ def _id_to_xyz(row): job_dict = WMS.get_job_dict( tiles_gdf=aoi_tiles_gdf.to_crs(ORTHO_WS_SRS), # <- note the reprojection - WMS_url=ORTHO_WS_URL, + wms_url=ORTHO_WS_URL, layers=ORTHO_WS_LAYERS, width=TILE_SIZE, height=TILE_SIZE, @@ -492,7 +492,7 @@ def _id_to_xyz(row): try: results = Parallel(n_jobs=N_JOBS, backend="loky") \ - (delayed(get_COCO_image_and_segmentations) \ + (delayed(get_coco_image_and_segmentations) \ (tile, labels_gdf, coco_license_id, OUTPUT_DIR) \ for tile in tqdm( tiles_iterator, total=len(tmp_tiles_gdf) )) except Exception as e: From 42a47825d8b692d3ac983742127e8b40a80fb5ba Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Wed, 24 May 2023 20:07:07 +0000 Subject: [PATCH 019/108] Remove useless f-strings --- examples/swimming-pool-detection/GE/prepare_data.py | 4 ++-- examples/swimming-pool-detection/NE/prepare_data.py | 4 ++-- helpers/XYZ.py | 2 +- scripts/assess_predictions.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/swimming-pool-detection/GE/prepare_data.py b/examples/swimming-pool-detection/GE/prepare_data.py index 0b88a23..42784d5 100644 --- a/examples/swimming-pool-detection/GE/prepare_data.py +++ b/examples/swimming-pool-detection/GE/prepare_data.py @@ -146,8 +146,8 @@ logger.critical(e) sys.exit(1) - GT_LABELS_GEOJSON = os.path.join(OUTPUT_DIR, f'ground_truth_labels.geojson') - OTH_LABELS_GEOJSON = os.path.join(OUTPUT_DIR, f'other_labels.geojson') + GT_LABELS_GEOJSON = os.path.join(OUTPUT_DIR, 'ground_truth_labels.geojson') + OTH_LABELS_GEOJSON = os.path.join(OUTPUT_DIR, 'other_labels.geojson') GT_labels_gdf.to_crs(epsg=4326).to_file(GT_LABELS_GEOJSON, driver='GeoJSON') written_files.append(GT_LABELS_GEOJSON) diff --git a/examples/swimming-pool-detection/NE/prepare_data.py b/examples/swimming-pool-detection/NE/prepare_data.py index 567f21f..548ccd4 100644 --- a/examples/swimming-pool-detection/NE/prepare_data.py +++ b/examples/swimming-pool-detection/NE/prepare_data.py @@ -103,8 +103,8 @@ # ------ Exporting labels to GeoJSON - GT_LABELS_GEOJSON = os.path.join(OUTPUT_DIR, f'ground_truth_labels.geojson') - OTH_LABELS_GEOJSON = os.path.join(OUTPUT_DIR, f'other_labels.geojson') + GT_LABELS_GEOJSON = os.path.join(OUTPUT_DIR, 'ground_truth_labels.geojson') + OTH_LABELS_GEOJSON = os.path.join(OUTPUT_DIR, 'other_labels.geojson') dataset_dict['ground_truth_swimming_pools'].to_crs(epsg=4326).to_file(GT_LABELS_GEOJSON, driver='GeoJSON') written_files.append(GT_LABELS_GEOJSON) diff --git a/helpers/XYZ.py b/helpers/XYZ.py index 831d869..bfaf7fe 100644 --- a/helpers/XYZ.py +++ b/helpers/XYZ.py @@ -112,7 +112,7 @@ def get_geotiff(xyz_url, bbox, xyz, filename, save_metadata=False, overwrite=Tru try: src_ds = gdal.Open(img_filename) # NOTE: EPSG:3857 is hard-coded - gdal.Translate(geotiff_filename, src_ds, options=f'-of GTiff -a_srs EPSG:3857') + gdal.Translate(geotiff_filename, src_ds, options='-of GTiff -a_srs EPSG:3857') src_ds = None except Exception as e: logger.warning(f"Exception in the 'get_geotiff' function: {e}") diff --git a/scripts/assess_predictions.py b/scripts/assess_predictions.py index 59979be..608ba85 100644 --- a/scripts/assess_predictions.py +++ b/scripts/assess_predictions.py @@ -284,7 +284,7 @@ tagged_preds_gdf_dict[x] for x in metrics.keys() ]) - file_to_write = os.path.join(OUTPUT_DIR, f'tagged_predictions.gpkg') + file_to_write = os.path.join(OUTPUT_DIR, 'tagged_predictions.gpkg') tagged_preds_gdf[['geometry', 'score', 'tag', 'dataset']].to_file(file_to_write, driver='GPKG', index=False) written_files.append(file_to_write) From 925c2791aa707cf7cf26b0884c3e949c79b195aa Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Wed, 24 May 2023 20:11:31 +0000 Subject: [PATCH 020/108] Add SCATTER_PLOT_MODE constant --- scripts/assess_predictions.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/assess_predictions.py b/scripts/assess_predictions.py index 608ba85..ca83c47 100644 --- a/scripts/assess_predictions.py +++ b/scripts/assess_predictions.py @@ -28,6 +28,8 @@ logging.config.fileConfig('logging.conf') logger = logging.getLogger('root') +SCATTER_PLOT_MODE = 'markers+lines' + if __name__ == '__main__': @@ -189,7 +191,7 @@ go.Scatter( x=metrics_df_dict[dataset]['recall'], y=metrics_df_dict[dataset]['precision'], - mode='markers+lines', + mode=SCATTER_PLOT_MODE, text=metrics_df_dict[dataset]['threshold'], name=dataset ) @@ -217,7 +219,7 @@ go.Scatter( x=metrics_df_dict[dataset]['threshold'], y=metrics_df_dict[dataset][y], - mode='markers+lines', + mode=SCATTER_PLOT_MODE, name=y ) ) @@ -239,7 +241,7 @@ go.Scatter( x=metrics_df_dict[dataset]['threshold'], y=metrics_df_dict[dataset][y], - mode='markers+lines', + mode=SCATTER_PLOT_MODE, name=y ) ) From af5110935c6d0cbd0cafe24bba227128ef3fb519 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Wed, 24 May 2023 20:47:13 +0000 Subject: [PATCH 021/108] Update requirements.in and requirements.txt in order to faddress Dependabot vulnerability alerts --- requirements.in | 9 +++++++-- requirements.txt | 35 ++++++++++++++++++++++------------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/requirements.in b/requirements.in index 87ff50f..8985e8b 100644 --- a/requirements.in +++ b/requirements.in @@ -6,10 +6,10 @@ GDAL==3.0.4 rtree geopandas joblib -pillow +pillow>=9.3.0 pyyaml rasterio -requests +requests>=2.31.0 supermercado tqdm opencv-python @@ -19,3 +19,8 @@ torchvision @ https://download.pytorch.org/whl/cu113/torchvision-0.11.3%2Bcu113- detectron2 @ https://dl.fbaipublicfiles.com/detectron2/wheels/cu113/torch1.10/detectron2-0.6%2Bcu113-cp38-cp38-linux_x86_64.whl plotly rdp +Werkzeug>=2.2.3 +future>=0.18.3 +wheel>=0.38.1 +oauthlib>=3.2.2 +certifi>=2022.12.07 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7cb4579..fce95d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # -# This file is autogenerated by pip-compile with python 3.8 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: # # pip-compile requirements.in # @@ -24,8 +24,9 @@ black==21.4b2 # via detectron2 cachetools==5.2.0 # via google-auth -certifi==2022.9.24 +certifi==2023.5.7 # via + # -r requirements.in # fiona # pyproj # rasterio @@ -63,8 +64,10 @@ fiona==1.8.21 # via geopandas fonttools==4.37.4 # via matplotlib -future==0.18.2 - # via detectron2 +future==0.18.3 + # via + # -r requirements.in + # detectron2 fvcore==0.1.5.post20220512 # via detectron2 gdal==3.0.4 @@ -123,8 +126,10 @@ numpy==1.23.3 # supermercado # tensorboard # torchvision -oauthlib==3.2.1 - # via requests-oauthlib +oauthlib==3.2.2 + # via + # -r requirements.in + # requests-oauthlib omegaconf==2.2.3 # via # detectron2 @@ -140,7 +145,7 @@ pandas==1.5.0 # via geopandas pathspec==0.10.1 # via black -pillow==9.2.0 +pillow==9.5.0 # via # -r requirements.in # detectron2 @@ -191,7 +196,7 @@ rdp==0.8 # via -r requirements.in regex==2022.9.13 # via black -requests==2.28.1 +requests==2.31.0 # via # -r requirements.in # requests-oauthlib @@ -249,10 +254,14 @@ typing-extensions==4.4.0 # via torch urllib3==1.26.12 # via requests -werkzeug==2.2.2 - # via tensorboard -wheel==0.37.1 - # via tensorboard +werkzeug==2.3.4 + # via + # -r requirements.in + # tensorboard +wheel==0.40.0 + # via + # -r requirements.in + # tensorboard yacs==0.1.8 # via # detectron2 From b17d22de6cb3fc358cbc8907dbee070aed3ad87a Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Wed, 24 May 2023 21:09:10 +0000 Subject: [PATCH 022/108] Remove extra blank space. Use "md_filename" variable instead of equivalent expression --- helpers/XYZ.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpers/XYZ.py b/helpers/XYZ.py index bfaf7fe..e7b0ccc 100644 --- a/helpers/XYZ.py +++ b/helpers/XYZ.py @@ -61,7 +61,7 @@ def get_geotiff(xyz_url, bbox, xyz, filename, save_metadata=False, overwrite=Tru geotiff_filename = filename if save_metadata: - if not overwrite and os.path.isfile(geotiff_filename) and os.path.isfile(geotiff_filename.replace('.tif', '.json')): + if not overwrite and os.path.isfile(geotiff_filename) and os.path.isfile(md_filename): return None else: if not overwrite and os.path.isfile(geotiff_filename): @@ -69,7 +69,7 @@ def get_geotiff(xyz_url, bbox, xyz, filename, save_metadata=False, overwrite=Tru x, y, z = xyz - xyz_url_completed = xyz_url.replace('{z}', str(z)) .replace('{x}', str(x)).replace('{y}', str(y)) + xyz_url_completed = xyz_url.replace('{z}', str(z)).replace('{x}', str(x)).replace('{y}', str(y)) xmin, ymin, xmax, ymax = [float(x) for x in bbox.split(',')] From fb494674ae1bdb52c9ca97d41baf65e693815d0c Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Thu, 25 May 2023 06:54:54 +0000 Subject: [PATCH 023/108] Ignore UserWarning, cf. https://github.com/facebookresearch/detectron2/issues/3983 --- scripts/make_predictions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/make_predictions.py b/scripts/make_predictions.py index c78b33e..7640ed0 100644 --- a/scripts/make_predictions.py +++ b/scripts/make_predictions.py @@ -1,6 +1,9 @@ #!/usr/bin/env python # coding: utf-8 +import warnings +warnings.simplefilter(action='ignore', category=UserWarning) + import os, sys import argparse import json, yaml From ab66817ae469480503eb5676b49c75fff605628a Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Thu, 25 May 2023 06:56:01 +0000 Subject: [PATCH 024/108] Remove useless imports --- scripts/make_predictions.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/scripts/make_predictions.py b/scripts/make_predictions.py index 7640ed0..554b1fc 100644 --- a/scripts/make_predictions.py +++ b/scripts/make_predictions.py @@ -12,8 +12,6 @@ import logging, logging.config import geopandas as gpd -import torch - from tqdm import tqdm from detectron2.utils.logger import setup_logger @@ -32,11 +30,9 @@ parent_dir = current_dir[:current_dir.rfind(os.path.sep)] sys.path.insert(0, parent_dir) -from helpers.detectron2 import LossEvalHook, CocoTrainer from helpers.detectron2 import detectron2preds_to_features from helpers.misc import image_metadata_to_affine_transform - logging.config.fileConfig('logging.conf') logger = logging.getLogger('root') From 6e2d3b53292e953cec6cfc54ececb1186b3cb3de Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Thu, 25 May 2023 07:05:15 +0000 Subject: [PATCH 025/108] Remove commented-out line --- scripts/make_predictions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/make_predictions.py b/scripts/make_predictions.py index 554b1fc..8164e88 100644 --- a/scripts/make_predictions.py +++ b/scripts/make_predictions.py @@ -138,7 +138,6 @@ crs = _crs transform = image_metadata_to_affine_transform(im_md) - #predictions[d['file_name']] = dt2predictions_to_list(outputs) this_image_feats = detectron2preds_to_features(outputs, crs, transform, RDP_SIMPLIFICATION_ENABLED, RDP_SIMPLIFICATION_EPSILON) all_feats += this_image_feats From c645f3db9b15b6176cc09b6ab8bd9400dd3617eb Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Thu, 25 May 2023 07:51:10 +0000 Subject: [PATCH 026/108] Better handle constants (e.g. DONE_MSG) --- helpers/constants.py | 2 ++ helpers/misc.py | 23 +++++++++++++++++++++-- scripts/assess_predictions.py | 12 +++++------- scripts/generate_tilesets.py | 2 +- 4 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 helpers/constants.py diff --git a/helpers/constants.py b/helpers/constants.py new file mode 100644 index 0000000..b05ffb5 --- /dev/null +++ b/helpers/constants.py @@ -0,0 +1,2 @@ +DONE_MSG = "...done." +SCATTER_PLOT_MODE = 'markers+lines' \ No newline at end of file diff --git a/helpers/misc.py b/helpers/misc.py index 8a2849f..5a3fc96 100644 --- a/helpers/misc.py +++ b/helpers/misc.py @@ -4,13 +4,17 @@ import warnings warnings.simplefilter(action='ignore', category=FutureWarning) -import os +import os, sys import geopandas as gpd from shapely.affinity import scale from rasterio.transform import from_bounds +DONE_MSG = "...done." +SCATTER_PLOT_MODE = 'markers+lines' + + class BadFileExtensionException(Exception): "Raised when the file extension is different from the expected one" pass @@ -201,4 +205,19 @@ def image_metadata_to_world_file(image_metadata): c += a/2.0 # <- IMPORTANT f += e/2.0 # <- IMPORTANT - return "\n".join([str(a), str(d), str(b), str(e), str(c), str(f)+"\n"]) \ No newline at end of file + return "\n".join([str(a), str(d), str(b), str(e), str(c), str(f)+"\n"]) + + +def format_logger(logger): + + logger.remove() + logger.add(sys.stdout, format="{time:YYYY-MM-DD HH:mm:ss} - {level} - {message}", + level="INFO", filter=lambda record: record["level"].no < 25) + logger.add(sys.stdout, format="{time:YYYY-MM-DD HH:mm:ss} - {level} - {message}", + level="SUCCESS", filter=lambda record: record["level"].no < 30) + logger.add(sys.stdout, format="{time:YYYY-MM-DD HH:mm:ss} - {level} - {message}", + level="WARNING", filter=lambda record: record["level"].no < 40) + logger.add(sys.stderr, format="{time:YYYY-MM-DD HH:mm:ss} - {level} - {message}", + level="ERROR") + + return logger \ No newline at end of file diff --git a/scripts/assess_predictions.py b/scripts/assess_predictions.py index ca83c47..83773d5 100644 --- a/scripts/assess_predictions.py +++ b/scripts/assess_predictions.py @@ -15,7 +15,6 @@ import plotly.graph_objects as go from tqdm import tqdm - # the following lines allow us to import modules from within this file's parent folder from inspect import getsourcefile current_path = os.path.abspath(getsourcefile(lambda:0)) @@ -24,12 +23,11 @@ sys.path.insert(0, parent_dir) from helpers import misc +from helpers.constants import DONE_MSG, SCATTER_PLOT_MODE logging.config.fileConfig('logging.conf') logger = logging.getLogger('root') -SCATTER_PLOT_MODE = 'markers+lines' - if __name__ == '__main__': @@ -69,17 +67,17 @@ logger.info("Loading split AoI tiles as a GeoPandas DataFrame...") split_aoi_tiles_gdf = gpd.read_file(SPLIT_AOI_TILES_GEOJSON) - logger.info(f"...done. {len(split_aoi_tiles_gdf)} records were found.") + logger.info(f"{DONE_MSG} {len(split_aoi_tiles_gdf)} records were found.") if GT_LABELS_GEOJSON: logger.info("Loading Ground Truth Labels as a GeoPandas DataFrame...") gt_labels_gdf = gpd.read_file(GT_LABELS_GEOJSON) - logger.info(f"...done. {len(gt_labels_gdf)} records were found.") + logger.info(f"{DONE_MSG} {len(gt_labels_gdf)} records were found.") if OTH_LABELS_GEOJSON: logger.info("Loading Other Labels as a GeoPandas DataFrame...") oth_labels_gdf = gpd.read_file(OTH_LABELS_GEOJSON) - logger.info(f"...done. {len(oth_labels_gdf)} records were found.") + logger.info(f"{DONE_MSG} {len(oth_labels_gdf)} records were found.") if GT_LABELS_GEOJSON and OTH_LABELS_GEOJSON: labels_gdf = pd.concat([ @@ -111,7 +109,7 @@ written_files.append(file_to_write) - logging.info(f"...done. Elapsed time = {(time.time()-tic):.2f} seconds.") + logging.info(f"{DONE_MSG} Elapsed time = {(time.time()-tic):.2f} seconds.") # ------ Loading image metadata diff --git a/scripts/generate_tilesets.py b/scripts/generate_tilesets.py index 4a3b646..2dc4452 100644 --- a/scripts/generate_tilesets.py +++ b/scripts/generate_tilesets.py @@ -29,11 +29,11 @@ from helpers import XYZ # XYZ link connection from helpers import COCO from helpers import misc +from helpers.constants import DONE_MSG logging.config.fileConfig('logging.conf') logger = logging.getLogger('root') -DONE_MSG = "...done." class LabelOverflowException(Exception): "Raised when a label exceeds the tile size" From 7b82c40b8c9574874f06af87f7bb2e6fce385a32 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Thu, 25 May 2023 07:54:03 +0000 Subject: [PATCH 027/108] Remove useless imports --- examples/swimming-pool-detection/GE/prepare_data.py | 5 +---- examples/swimming-pool-detection/NE/prepare_data.py | 6 +----- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/examples/swimming-pool-detection/GE/prepare_data.py b/examples/swimming-pool-detection/GE/prepare_data.py index 42784d5..6232abe 100644 --- a/examples/swimming-pool-detection/GE/prepare_data.py +++ b/examples/swimming-pool-detection/GE/prepare_data.py @@ -6,13 +6,10 @@ import time import argparse import yaml -import os, sys, inspect +import os, sys import requests import geopandas as gpd import pandas as pd -import json - -from tqdm import tqdm # the following allows us to import modules from within this file's parent folder sys.path.insert(0, '.') diff --git a/examples/swimming-pool-detection/NE/prepare_data.py b/examples/swimming-pool-detection/NE/prepare_data.py index 548ccd4..17fabc1 100644 --- a/examples/swimming-pool-detection/NE/prepare_data.py +++ b/examples/swimming-pool-detection/NE/prepare_data.py @@ -6,13 +6,9 @@ import time import argparse import yaml -import os, sys, inspect -import requests +import os, sys import geopandas as gpd import pandas as pd -import json - -from tqdm import tqdm # the following allows us to import modules from within this file's parent folder sys.path.insert(0, '.') From 1bbb612dc18ec5b838bfc220b9cf2f2d0c7d3404 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Thu, 25 May 2023 08:18:43 +0000 Subject: [PATCH 028/108] Use loguru instead of logging --- .../swimming-pool-detection/GE/logging.conf | 23 ---------------- .../GE/prepare_data.py | 15 ++++------- .../swimming-pool-detection/NE/logging.conf | 23 ---------------- .../NE/prepare_data.py | 13 +++------- helpers/MIL.py | 13 +++++----- helpers/WMS.py | 13 +++++----- helpers/XYZ.py | 13 +++++----- helpers/misc.py | 4 --- logging.conf | 23 ---------------- requirements.in | 3 ++- requirements.txt | 2 ++ scripts/assess_predictions.py | 19 ++++++-------- scripts/generate_tilesets.py | 26 +++++++++---------- scripts/make_predictions.py | 14 +++++----- scripts/train_model.py | 14 +++++----- 15 files changed, 65 insertions(+), 153 deletions(-) delete mode 100644 examples/swimming-pool-detection/GE/logging.conf delete mode 100644 examples/swimming-pool-detection/NE/logging.conf delete mode 100644 logging.conf diff --git a/examples/swimming-pool-detection/GE/logging.conf b/examples/swimming-pool-detection/GE/logging.conf deleted file mode 100644 index 9e26be7..0000000 --- a/examples/swimming-pool-detection/GE/logging.conf +++ /dev/null @@ -1,23 +0,0 @@ -[loggers] -keys=root - -[handlers] -keys=consoleHandler - -[formatters] -keys=simpleFormatter - -[logger_root] -level=DEBUG -handlers=consoleHandler - - -[handler_consoleHandler] -class=StreamHandler -level=INFO -formatter=simpleFormatter -args=(sys.stdout,) - -[formatter_simpleFormatter] -format=%(asctime)s - %(name)s - %(levelname)s - %(message)s -datefmt= \ No newline at end of file diff --git a/examples/swimming-pool-detection/GE/prepare_data.py b/examples/swimming-pool-detection/GE/prepare_data.py index 6232abe..3120978 100644 --- a/examples/swimming-pool-detection/GE/prepare_data.py +++ b/examples/swimming-pool-detection/GE/prepare_data.py @@ -1,8 +1,6 @@ #!/bin/python # -*- coding: utf-8 -*- -import logging -import logging.config import time import argparse import yaml @@ -11,11 +9,8 @@ import geopandas as gpd import pandas as pd -# the following allows us to import modules from within this file's parent folder -sys.path.insert(0, '.') +from loguru import logger -logging.config.fileConfig('logging.conf') -logger = logging.getLogger('root') if __name__ == "__main__": @@ -61,11 +56,11 @@ f.write(r.content) written_files.append(shpfile_path) - logger.info(f"...done. A file was written: {shpfile_path}") + logger.success(f"...done. A file was written: {shpfile_path}") logger.info(f"Loading the {dataset} dataset as a GeoPandas DataFrame...") dataset_dict[dataset] = gpd.read_file(f'zip://{shpfile_path}') - logger.info(f"...done. {len(dataset_dict[dataset])} records were found.") + logger.success(f"...done. {len(dataset_dict[dataset])} records were found.") # ------ Computing the Area of Interest (AOI) = cadastral parcels - Léman lake @@ -89,7 +84,7 @@ PARCELS_GEOJSON_FILE = os.path.join(OUTPUT_DIR, 'parcels.geojson') p_gdf[['geometry']].to_crs(epsg=4326).to_file(PARCELS_GEOJSON_FILE, driver='GeoJSON') written_files.append(PARCELS_GEOJSON_FILE) - logger.info(f"...done. The {PARCELS_GEOJSON_FILE} was written.") + logger.success(f"...done. The {PARCELS_GEOJSON_FILE} was written.") print() logger.warning(f"You should now open a Linux shell and run the following command from the working directory (./{OUTPUT_DIR}), then run this script again:") @@ -158,6 +153,6 @@ print() toc = time.time() - logger.info(f"Nothing left to be done: exiting. Elapsed time: {(toc-tic):.2f} seconds") + logger.success(f"Nothing left to be done: exiting. Elapsed time: {(toc-tic):.2f} seconds") sys.stderr.flush() \ No newline at end of file diff --git a/examples/swimming-pool-detection/NE/logging.conf b/examples/swimming-pool-detection/NE/logging.conf deleted file mode 100644 index 9e26be7..0000000 --- a/examples/swimming-pool-detection/NE/logging.conf +++ /dev/null @@ -1,23 +0,0 @@ -[loggers] -keys=root - -[handlers] -keys=consoleHandler - -[formatters] -keys=simpleFormatter - -[logger_root] -level=DEBUG -handlers=consoleHandler - - -[handler_consoleHandler] -class=StreamHandler -level=INFO -formatter=simpleFormatter -args=(sys.stdout,) - -[formatter_simpleFormatter] -format=%(asctime)s - %(name)s - %(levelname)s - %(message)s -datefmt= \ No newline at end of file diff --git a/examples/swimming-pool-detection/NE/prepare_data.py b/examples/swimming-pool-detection/NE/prepare_data.py index 17fabc1..bd725ef 100644 --- a/examples/swimming-pool-detection/NE/prepare_data.py +++ b/examples/swimming-pool-detection/NE/prepare_data.py @@ -1,8 +1,6 @@ #!/bin/python # -*- coding: utf-8 -*- -import logging -import logging.config import time import argparse import yaml @@ -10,11 +8,8 @@ import geopandas as gpd import pandas as pd -# the following allows us to import modules from within this file's parent folder -sys.path.insert(0, '.') +from loguru import logger -logging.config.fileConfig('logging.conf') -logger = logging.getLogger('root') if __name__ == "__main__": @@ -61,7 +56,7 @@ logger.info(f"Loading the {dataset} dataset as a GeoPandas DataFrame...") dataset_dict[dataset] = gpd.read_file(f'{shpfile}') - logger.info(f"...done. {len(dataset_dict[dataset])} records were found.") + logger.success(f"...done. {len(dataset_dict[dataset])} records were found.") # ------ Computing the Area of Interest (AOI) @@ -91,7 +86,7 @@ else: logger.info("Loading AoI tiles as a GeoPandas DataFrame...") aoi_tiles_gdf = gpd.read_file(AOI_TILES_GEOJSON) - logger.info(f"...done. {len(aoi_tiles_gdf)} records were found.") + logger.success(f"...done. {len(aoi_tiles_gdf)} records were found.") assert ( len(aoi_tiles_gdf.drop_duplicates(subset='id')) == len(aoi_tiles_gdf) ) # make sure there are no duplicates @@ -114,7 +109,7 @@ print() toc = time.time() - logger.info(f"Nothing left to be done: exiting. Elapsed time: {(toc-tic):.2f} seconds") + logger.success(f"Nothing left to be done: exiting. Elapsed time: {(toc-tic):.2f} seconds") sys.stderr.flush() diff --git a/helpers/MIL.py b/helpers/MIL.py index 6450e97..3780223 100644 --- a/helpers/MIL.py +++ b/helpers/MIL.py @@ -4,25 +4,24 @@ import os, sys import json import requests -import logging -import logging.config - -logging.config.fileConfig('logging.conf') -logger = logging.getLogger('MIL') from osgeo import gdal from tqdm import tqdm +from loguru import logger try: try: - from helpers.misc import image_metadata_to_world_file, bounds_to_bbox, BadFileExtensionException + from helpers.misc import image_metadata_to_world_file, bounds_to_bbox, format_logger, BadFileExtensionException except ModuleNotFoundError: - from misc import image_metadata_to_world_file, bounds_to_bbox, BadFileExtensionException + from misc import image_metadata_to_world_file, bounds_to_bbox, format_logger, BadFileExtensionException except Exception as e: logger.error(f"Could not import some dependencies. Exception: {e}") sys.exit(1) +logger = format_logger(logger) + + def get_geotiff(mil_url, bbox, width, height, filename, image_sr="3857", bbox_sr="3857", save_metadata=False, overwrite=True): """ by default, bbox must be in EPSG:3857 diff --git a/helpers/WMS.py b/helpers/WMS.py index bf3659d..a28f195 100644 --- a/helpers/WMS.py +++ b/helpers/WMS.py @@ -4,25 +4,24 @@ import os, sys import json import requests -import logging -import logging.config - -logging.config.fileConfig('logging.conf') -logger = logging.getLogger('WMS') from osgeo import gdal from tqdm import tqdm +from loguru import logger try: try: - from helpers.misc import image_metadata_to_world_file, bounds_to_bbox, BadFileExtensionException + from helpers.misc import image_metadata_to_world_file, bounds_to_bbox, format_logger, BadFileExtensionException except ModuleNotFoundError: - from misc import image_metadata_to_world_file, bounds_to_bbox, BadFileExtensionException + from misc import image_metadata_to_world_file, bounds_to_bbox, format_logger, BadFileExtensionException except Exception as e: logger.error(f"Could not import some dependencies. Exception: {e}") sys.exit(1) +logger = format_logger(logger) + + def get_geotiff(wms_url, layers, bbox, width, height, filename, srs="EPSG:3857", save_metadata=False, overwrite=True): """ ... diff --git a/helpers/XYZ.py b/helpers/XYZ.py index e7b0ccc..64f2990 100644 --- a/helpers/XYZ.py +++ b/helpers/XYZ.py @@ -4,25 +4,24 @@ import os, sys import json import requests -import logging -import logging.config - -logging.config.fileConfig('logging.conf') -logger = logging.getLogger('XYZ') from osgeo import gdal from tqdm import tqdm +from loguru import logger try: try: - from helpers.misc import image_metadata_to_world_file, bounds_to_bbox, BadFileExtensionException + from helpers.misc import image_metadata_to_world_file, bounds_to_bbox, format_logger, BadFileExtensionException except ModuleNotFoundError: - from misc import image_metadata_to_world_file, bounds_to_bbox, BadFileExtensionException + from misc import image_metadata_to_world_file, bounds_to_bbox, format_logger, BadFileExtensionException except Exception as e: logger.error(f"Could not import some dependencies. Exception: {e}") sys.exit(1) +logger = format_logger(logger) + + class UnsupportedImageFormatException(Exception): "Raised when the detected image format is not supported" pass diff --git a/helpers/misc.py b/helpers/misc.py index 5a3fc96..04274de 100644 --- a/helpers/misc.py +++ b/helpers/misc.py @@ -11,10 +11,6 @@ from rasterio.transform import from_bounds -DONE_MSG = "...done." -SCATTER_PLOT_MODE = 'markers+lines' - - class BadFileExtensionException(Exception): "Raised when the file extension is different from the expected one" pass diff --git a/logging.conf b/logging.conf deleted file mode 100644 index 9e26be7..0000000 --- a/logging.conf +++ /dev/null @@ -1,23 +0,0 @@ -[loggers] -keys=root - -[handlers] -keys=consoleHandler - -[formatters] -keys=simpleFormatter - -[logger_root] -level=DEBUG -handlers=consoleHandler - - -[handler_consoleHandler] -class=StreamHandler -level=INFO -formatter=simpleFormatter -args=(sys.stdout,) - -[formatter_simpleFormatter] -format=%(asctime)s - %(name)s - %(levelname)s - %(message)s -datefmt= \ No newline at end of file diff --git a/requirements.in b/requirements.in index 8985e8b..73fd07a 100644 --- a/requirements.in +++ b/requirements.in @@ -23,4 +23,5 @@ Werkzeug>=2.2.3 future>=0.18.3 wheel>=0.38.1 oauthlib>=3.2.2 -certifi>=2022.12.07 \ No newline at end of file +certifi>=2022.12.07 +loguru \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index fce95d9..364817e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -98,6 +98,8 @@ joblib==1.2.0 # via -r requirements.in kiwisolver==1.4.4 # via matplotlib +loguru==0.7.0 + # via -r requirements.in markdown==3.4.1 # via tensorboard markupsafe==2.1.1 diff --git a/scripts/assess_predictions.py b/scripts/assess_predictions.py index 83773d5..cee3968 100644 --- a/scripts/assess_predictions.py +++ b/scripts/assess_predictions.py @@ -1,13 +1,10 @@ #!/bin/python # -*- coding: utf-8 -*- -import logging -import logging.config import time import argparse import yaml import os, sys -import pickle import json import geopandas as gpd import pandas as pd @@ -25,8 +22,8 @@ from helpers import misc from helpers.constants import DONE_MSG, SCATTER_PLOT_MODE -logging.config.fileConfig('logging.conf') -logger = logging.getLogger('root') +from loguru import logger +logger = misc.format_logger(logger) if __name__ == '__main__': @@ -67,17 +64,17 @@ logger.info("Loading split AoI tiles as a GeoPandas DataFrame...") split_aoi_tiles_gdf = gpd.read_file(SPLIT_AOI_TILES_GEOJSON) - logger.info(f"{DONE_MSG} {len(split_aoi_tiles_gdf)} records were found.") + logger.success(f"{DONE_MSG} {len(split_aoi_tiles_gdf)} records were found.") if GT_LABELS_GEOJSON: logger.info("Loading Ground Truth Labels as a GeoPandas DataFrame...") gt_labels_gdf = gpd.read_file(GT_LABELS_GEOJSON) - logger.info(f"{DONE_MSG} {len(gt_labels_gdf)} records were found.") + logger.success(f"{DONE_MSG} {len(gt_labels_gdf)} records were found.") if OTH_LABELS_GEOJSON: logger.info("Loading Other Labels as a GeoPandas DataFrame...") oth_labels_gdf = gpd.read_file(OTH_LABELS_GEOJSON) - logger.info(f"{DONE_MSG} {len(oth_labels_gdf)} records were found.") + logger.success(f"{DONE_MSG} {len(oth_labels_gdf)} records were found.") if GT_LABELS_GEOJSON and OTH_LABELS_GEOJSON: labels_gdf = pd.concat([ @@ -93,7 +90,7 @@ if len(labels_gdf)>0: - logging.info("Clipping labels...") + logger.info("Clipping labels...") tic = time.time() assert(labels_gdf.crs == split_aoi_tiles_gdf.crs) @@ -109,7 +106,7 @@ written_files.append(file_to_write) - logging.info(f"{DONE_MSG} Elapsed time = {(time.time()-tic):.2f} seconds.") + logger.success(f"{DONE_MSG} Elapsed time = {(time.time()-tic):.2f} seconds.") # ------ Loading image metadata @@ -298,6 +295,6 @@ print() toc = time.time() - logger.info(f"Nothing left to be done: exiting. Elapsed time: {(toc-tic):.2f} seconds") + logger.success(f"Nothing left to be done: exiting. Elapsed time: {(toc-tic):.2f} seconds") sys.stderr.flush() \ No newline at end of file diff --git a/scripts/generate_tilesets.py b/scripts/generate_tilesets.py index 2dc4452..92052b3 100644 --- a/scripts/generate_tilesets.py +++ b/scripts/generate_tilesets.py @@ -4,8 +4,6 @@ import warnings warnings.simplefilter(action='ignore', category=FutureWarning) -import logging -import logging.config import time import argparse import yaml @@ -31,8 +29,8 @@ from helpers import misc from helpers.constants import DONE_MSG -logging.config.fileConfig('logging.conf') -logger = logging.getLogger('root') +from loguru import logger +logger = misc.format_logger(logger) class LabelOverflowException(Exception): @@ -197,7 +195,7 @@ def _id_to_xyz(row): logger.info("Loading AoI tiles as a GeoPandas DataFrame...") aoi_tiles_gdf = gpd.read_file(AOI_TILES_GEOJSON) - logger.info(f"{DONE_MSG} {len(aoi_tiles_gdf)} records were found.") + logger.success(f"{DONE_MSG} {len(aoi_tiles_gdf)} records were found.") logger.info("Extracting tile coordinates (x, y, z) from tile IDs...") try: @@ -205,17 +203,17 @@ def _id_to_xyz(row): except Exception as e: logger.critical(f"[...] Exception: {e}") sys.exit(1) - logger.info(DONE_MSG) + logger.success(DONE_MSG) if GT_LABELS_GEOJSON: logger.info("Loading Ground Truth Labels as a GeoPandas DataFrame...") gt_labels_gdf = gpd.read_file(GT_LABELS_GEOJSON) - logger.info(f"{DONE_MSG} {len(gt_labels_gdf)} records were found.") + logger.success(f"{DONE_MSG} {len(gt_labels_gdf)} records were found.") if OTH_LABELS_GEOJSON: logger.info("Loading Other Labels as a GeoPandas DataFrame...") oth_labels_gdf = gpd.read_file(OTH_LABELS_GEOJSON) - logger.info(f"{DONE_MSG} {len(oth_labels_gdf)} records were found.") + logger.success(f"{DONE_MSG} {len(oth_labels_gdf)} records were found.") logger.info("Generating the list of tasks to be executed (one task per tile)...") @@ -320,7 +318,7 @@ def _id_to_xyz(row): logger.critical(f'Web Services of type "{ORTHO_WS_TYPE}" are not supported. Exiting.') sys.exit(1) - logger.info(DONE_MSG) + logger.success(DONE_MSG) logger.info(f"Executing tasks, {N_JOBS} at a time...") job_outcome = Parallel(n_jobs=N_JOBS, backend="loky")( @@ -335,7 +333,7 @@ def _id_to_xyz(row): logger.warning(f"Failed job: {job}") if all_tiles_were_downloaded: - logger.info(DONE_MSG) + logger.success(DONE_MSG) else: logger.critical("Some tiles were not downloaded. Please try to run this script again.") sys.exit(1) @@ -356,7 +354,7 @@ def _id_to_xyz(row): json.dump(img_metadata_dict, fp) written_files.append(IMG_METADATA_FILE) - logger.info(f"{DONE_MSG} A file was written: {IMG_METADATA_FILE}") + logger.success(f"{DONE_MSG} A file was written: {IMG_METADATA_FILE}") # ------ Training/validation/test/other dataset generation @@ -431,7 +429,7 @@ def _id_to_xyz(row): except Exception as e: logger.error(e) written_files.append(SPLIT_AOI_TILES_GEOJSON) - logger.info(f'{DONE_MSG} A file was written {SPLIT_AOI_TILES_GEOJSON}') + logger.success(f'{DONE_MSG} A file was written {SPLIT_AOI_TILES_GEOJSON}') img_md_df = pd.DataFrame.from_dict(img_metadata_dict, orient='index') img_md_df.reset_index(inplace=True) @@ -533,7 +531,7 @@ def _id_to_xyz(row): toc = time.time() - logger.info(DONE_MSG) + logger.success(DONE_MSG) logger.info("You can now open a Linux shell and type the following command in order to create a .tar.gz archive including images and COCO annotations:") if GT_LABELS_GEOJSON: @@ -554,6 +552,6 @@ def _id_to_xyz(row): print() toc = time.time() - logger.info(f"Nothing left to be done: exiting. Elapsed time: {(toc-tic):.2f} seconds") + logger.success(f"Nothing left to be done: exiting. Elapsed time: {(toc-tic):.2f} seconds") sys.stderr.flush() diff --git a/scripts/make_predictions.py b/scripts/make_predictions.py index 8164e88..f80d668 100644 --- a/scripts/make_predictions.py +++ b/scripts/make_predictions.py @@ -9,7 +9,6 @@ import json, yaml import cv2 import time -import logging, logging.config import geopandas as gpd from tqdm import tqdm @@ -31,10 +30,11 @@ sys.path.insert(0, parent_dir) from helpers.detectron2 import detectron2preds_to_features -from helpers.misc import image_metadata_to_affine_transform +from helpers.misc import image_metadata_to_affine_transform, format_logger +from helpers.constants import DONE_MSG -logging.config.fileConfig('logging.conf') -logger = logging.getLogger('root') +from loguru import logger +logger = format_logger(logger) if __name__ == "__main__": @@ -148,7 +148,7 @@ gdf.to_file(prediction_filename, driver='GPKG', index=False) written_files.append(os.path.join(WORKING_DIR, prediction_filename)) - logger.info('...done.') + logger.success(DONE_MSG) logger.info("Let's tag some sample images...") for d in DatasetCatalog.get(dataset)[0:min(len(DatasetCatalog.get(dataset)), 10)]: @@ -164,7 +164,7 @@ v = v.draw_instance_predictions(outputs["instances"].to("cpu")) cv2.imwrite(os.path.join(SAMPLE_TAGGED_IMG_SUBDIR, output_filename), v.get_image()[:, :, ::-1]) written_files.append( os.path.join(WORKING_DIR, os.path.join(SAMPLE_TAGGED_IMG_SUBDIR, output_filename)) ) - logger.info('...done.') + logger.success(DONE_MSG) # ------ wrap-up @@ -177,7 +177,7 @@ print() toc = time.time() - logger.info(f"Nothing left to be done: exiting. Elapsed time: {(toc-tic):.2f} seconds") + logger.success(f"Nothing left to be done: exiting. Elapsed time: {(toc-tic):.2f} seconds") sys.stderr.flush() diff --git a/scripts/train_model.py b/scripts/train_model.py index d28f03a..365497e 100644 --- a/scripts/train_model.py +++ b/scripts/train_model.py @@ -1,13 +1,11 @@ #!/usr/bin/env python # coding: utf-8 - import argparse import yaml import os, sys import cv2 import time -import logging, logging.config from detectron2.utils.logger import setup_logger setup_logger() @@ -26,10 +24,12 @@ parent_dir = current_dir[:current_dir.rfind(os.path.sep)] sys.path.insert(0, parent_dir) -from helpers.detectron2 import LossEvalHook, CocoTrainer +from helpers.detectron2 import CocoTrainer +from helpers.misc import format_logger +from helpers.constants import DONE_MSG -logging.config.fileConfig('logging.conf') -logger = logging.getLogger('root') +from loguru import logger +logger = format_logger(logger) if __name__ == "__main__": @@ -153,7 +153,7 @@ cv2.imwrite(os.path.join(SAMPLE_TAGGED_IMG_SUBDIR, output_filename), v.get_image()[:, :, ::-1]) written_files.append( os.path.join(WORKING_DIR, os.path.join(SAMPLE_TAGGED_IMG_SUBDIR, output_filename)) ) - logger.info("...done.") + logger.success(DONE_MSG) # ------ wrap-up @@ -166,7 +166,7 @@ print() toc = time.time() - logger.info(f"Nothing left to be done: exiting. Elapsed time: {(toc-tic):.2f} seconds") + logger.success(f"Nothing left to be done: exiting. Elapsed time: {(toc-tic):.2f} seconds") sys.stderr.flush() From 2e6da72ae20621b3d3b4dbef51713f400065d6e9 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Fri, 28 Jul 2023 15:01:29 +0000 Subject: [PATCH 029/108] Fix XYZ connector --- helpers/XYZ.py | 2 +- scripts/generate_tilesets.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/helpers/XYZ.py b/helpers/XYZ.py index 64f2990..292d1a9 100644 --- a/helpers/XYZ.py +++ b/helpers/XYZ.py @@ -139,7 +139,7 @@ def get_job_dict(tiles_gdf, xyz_url, img_path, save_metadata=False, overwrite=Tr job_dict[img_filename] = { 'xyz_url': xyz_url, 'bbox': bbox, - 'xyz': tile.xyz, + 'xyz': (tile.x, tile.y, tile.z), 'filename': img_filename, 'save_metadata': save_metadata, 'overwrite': overwrite diff --git a/scripts/generate_tilesets.py b/scripts/generate_tilesets.py index 92052b3..1fde333 100644 --- a/scripts/generate_tilesets.py +++ b/scripts/generate_tilesets.py @@ -306,7 +306,7 @@ def _id_to_xyz(row): job_dict = XYZ.get_job_dict( tiles_gdf=aoi_tiles_gdf.to_crs(ORTHO_WS_SRS), # <- note the reprojection - XYZ_url=ORTHO_WS_URL, + xyz_url=ORTHO_WS_URL, img_path=ALL_IMG_PATH, save_metadata=SAVE_METADATA, overwrite=OVERWRITE From 9360c3618a9bafede7b1e62ff3b1ee312d05a159 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Fri, 4 Aug 2023 08:24:15 +0000 Subject: [PATCH 030/108] Add setup.py and adapt scripts to be used as console scripts --- scripts/__init__.py | 0 scripts/assess_predictions.py | 9 +++++++-- scripts/generate_tilesets.py | 8 ++++++-- scripts/make_predictions.py | 5 ++++- scripts/train_model.py | 7 ++++++- setup.py | 23 +++++++++++++++++++++++ 6 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 scripts/__init__.py create mode 100644 setup.py diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/assess_predictions.py b/scripts/assess_predictions.py index cee3968..d053999 100644 --- a/scripts/assess_predictions.py +++ b/scripts/assess_predictions.py @@ -26,7 +26,7 @@ logger = misc.format_logger(logger) -if __name__ == '__main__': +def main(): tic = time.time() logger.info('Starting...') @@ -297,4 +297,9 @@ toc = time.time() logger.success(f"Nothing left to be done: exiting. Elapsed time: {(toc-tic):.2f} seconds") - sys.stderr.flush() \ No newline at end of file + sys.stderr.flush() + + +if __name__ == "__main__": + + main() \ No newline at end of file diff --git a/scripts/generate_tilesets.py b/scripts/generate_tilesets.py index 1fde333..bf3746f 100644 --- a/scripts/generate_tilesets.py +++ b/scripts/generate_tilesets.py @@ -128,8 +128,7 @@ def _id_to_xyz(row): return aoi_tiles_gdf.apply(_id_to_xyz, axis=1) -if __name__ == "__main__": - +def main(): tic = time.time() logger.info('Starting...') @@ -555,3 +554,8 @@ def _id_to_xyz(row): logger.success(f"Nothing left to be done: exiting. Elapsed time: {(toc-tic):.2f} seconds") sys.stderr.flush() + + +if __name__ == "__main__": + + main() \ No newline at end of file diff --git a/scripts/make_predictions.py b/scripts/make_predictions.py index f80d668..8e6d66e 100644 --- a/scripts/make_predictions.py +++ b/scripts/make_predictions.py @@ -37,7 +37,7 @@ logger = format_logger(logger) -if __name__ == "__main__": +def main(): tic = time.time() logger.info('Starting...') @@ -182,3 +182,6 @@ sys.stderr.flush() +if __name__ == "__main__": + + main() \ No newline at end of file diff --git a/scripts/train_model.py b/scripts/train_model.py index 365497e..54a452f 100644 --- a/scripts/train_model.py +++ b/scripts/train_model.py @@ -32,7 +32,7 @@ logger = format_logger(logger) -if __name__ == "__main__": +def main(): tic = time.time() logger.info('Starting...') @@ -171,3 +171,8 @@ sys.stderr.flush() +if __name__ == "__main__": + + main() + + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..86e06a0 --- /dev/null +++ b/setup.py @@ -0,0 +1,23 @@ +from setuptools import setup + +with open('requirements.txt') as f: + requirements = f.read().splitlines() + +setup( + name='stdl-object-detector', + version='latest', + description='A suite of Python scripts allowing the end-user to use Deep Learning to detect objects in georeferenced raster images.', + author='Swiss Territorial Data Lab (STDL)', + author_email='info@stdl.ch', + python_requires=">=3.8", + license="MIT license", + entry_points = { + 'console_scripts': [ + 'generate_tilesets=scripts.generate_tilesets:main', + 'train_model=scripts.train_model:main', + 'make_predictions=scripts.make_predictions:main', + 'assess_predictions=scripts.assess_predictions:main', + ] + }, + install_requires=requirements +) \ No newline at end of file From bd9f95dd6dceba462a012ed0dd4380a347f66a6c Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Fri, 4 Aug 2023 17:17:36 +0000 Subject: [PATCH 031/108] Update setup.py --- setup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 86e06a0..59ba70e 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup +from setuptools import setup, find_packages with open('requirements.txt') as f: requirements = f.read().splitlines() @@ -19,5 +19,7 @@ 'assess_predictions=scripts.assess_predictions:main', ] }, - install_requires=requirements + install_requires=requirements, + packages=find_packages() + #package_dir = {'scripts': 'scripts'} ) \ No newline at end of file From bb32f120fbf315d08de4313ca90599f036762d42 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Mon, 7 Aug 2023 12:12:48 +0000 Subject: [PATCH 032/108] Remove commented out line --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 59ba70e..45e3b5e 100644 --- a/setup.py +++ b/setup.py @@ -21,5 +21,4 @@ }, install_requires=requirements, packages=find_packages() - #package_dir = {'scripts': 'scripts'} ) \ No newline at end of file From 38460638ade5edfe5b1bf442b10ec88426ecc536 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Mon, 14 Aug 2023 09:09:07 +0000 Subject: [PATCH 033/108] Make version number compliant with PEP 440 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 45e3b5e..cb20b7e 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='stdl-object-detector', - version='latest', + version='1.0rc1.dev1', description='A suite of Python scripts allowing the end-user to use Deep Learning to detect objects in georeferenced raster images.', author='Swiss Territorial Data Lab (STDL)', author_email='info@stdl.ch', From 45fb8112fafa5ecb9771fa8cbf2bb4af4aaf64bd Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Wed, 16 Aug 2023 15:31:51 +0000 Subject: [PATCH 034/108] Add CLI with tasks as subcommands --- scripts/assess_predictions.py | 18 +++++++------ scripts/cli.py | 49 +++++++++++++++++++++++++++++++++++ scripts/generate_tilesets.py | 18 +++++++------ scripts/make_predictions.py | 22 +++++++++------- scripts/train_model.py | 23 ++++++++-------- setup.py | 5 +--- 6 files changed, 94 insertions(+), 41 deletions(-) create mode 100644 scripts/cli.py diff --git a/scripts/assess_predictions.py b/scripts/assess_predictions.py index d053999..3df3e87 100644 --- a/scripts/assess_predictions.py +++ b/scripts/assess_predictions.py @@ -26,18 +26,14 @@ logger = misc.format_logger(logger) -def main(): +def main(cfg_file_path): tic = time.time() logger.info('Starting...') - parser = argparse.ArgumentParser(description="This script assesses the quality of predictions with respect to ground-truth/other labels.") - parser.add_argument('config_file', type=str, help='a YAML config file') - args = parser.parse_args() + logger.info(f"Using {cfg_file_path} as config file.") - logger.info(f"Using {args.config_file} as config file.") - - with open(args.config_file) as fp: + with open(cfg_file_path) as fp: cfg = yaml.load(fp, Loader=yaml.FullLoader)[os.path.basename(__file__)] OUTPUT_DIR = cfg['output_folder'] @@ -302,4 +298,10 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + parser = argparse.ArgumentParser(description="This script assesses the quality of predictions with respect to ground-truth/other labels.") + parser.add_argument('config_file', type=str, help='a YAML config file') + args = parser.parse_args() + + main(args.config_file) + + \ No newline at end of file diff --git a/scripts/cli.py b/scripts/cli.py new file mode 100644 index 0000000..e675dd3 --- /dev/null +++ b/scripts/cli.py @@ -0,0 +1,49 @@ +# see https://realpython.com/command-line-interfaces-python-argparse/#adding-subcommands-to-your-clis + +import argparse +from scripts.generate_tilesets import main as generate_tilesets +from scripts.train_model import main as train_model +from scripts.make_predictions import main as make_predictions +from scripts.assess_predictions import main as assess_predictions + + +def main(): + + global_parser = argparse.ArgumentParser(prog="stdl-objdet") + + subparsers = global_parser.add_subparsers( + title="tasks", help="the various tasks which can be performed by the STDL Object Detector" + ) + + arg_template = { + "dest": "operands", + "type": str, + "nargs": 1, + "metavar": "", + "help": "configuration file", + } + + add_parser = subparsers.add_parser("generate_tilesets", help="This script generates COCO-annotated training/validation/test/other datasets for object detection tasks.") + add_parser.add_argument(**arg_template) + add_parser.set_defaults(func=generate_tilesets) + + add_parser = subparsers.add_parser("train_model", help="This script trains a predictive model.") + add_parser.add_argument(**arg_template) + add_parser.set_defaults(func=train_model) + + add_parser = subparsers.add_parser("make_predictions", help="This script makes predictions, using a previously trained model.") + add_parser.add_argument(**arg_template) + add_parser.set_defaults(func=make_predictions) + + add_parser = subparsers.add_parser("assess_predictions", help="This script assesses the quality of predictions with respect to ground-truth/other labels.") + add_parser.add_argument(**arg_template) + add_parser.set_defaults(func=assess_predictions) + + args = global_parser.parse_args() + + args.func(*args.operands) + + +if __name__ == "__main__": + + main() \ No newline at end of file diff --git a/scripts/generate_tilesets.py b/scripts/generate_tilesets.py index bf3746f..cc62e9d 100644 --- a/scripts/generate_tilesets.py +++ b/scripts/generate_tilesets.py @@ -128,18 +128,14 @@ def _id_to_xyz(row): return aoi_tiles_gdf.apply(_id_to_xyz, axis=1) -def main(): +def main(cfg_file_path): tic = time.time() logger.info('Starting...') - parser = argparse.ArgumentParser(description="This script generates COCO-annotated training/validation/test/other datasets for object detection tasks.") - parser.add_argument('config_file', type=str, help='a YAML config file') - args = parser.parse_args() + logger.info(f"Using {cfg_file_path} as config file.") - logger.info(f"Using {args.config_file} as config file.") - - with open(args.config_file) as fp: + with open(cfg_file_path) as fp: cfg = yaml.load(fp, Loader=yaml.FullLoader)[os.path.basename(__file__)] DEBUG_MODE = cfg['debug_mode'] @@ -558,4 +554,10 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + parser = argparse.ArgumentParser(description="This script generates COCO-annotated training/validation/test/other datasets for object detection tasks.") + parser.add_argument('config_file', type=str, help='a YAML config file') + args = parser.parse_args() + + main(args.config_file) + + \ No newline at end of file diff --git a/scripts/make_predictions.py b/scripts/make_predictions.py index 8e6d66e..91d80ac 100644 --- a/scripts/make_predictions.py +++ b/scripts/make_predictions.py @@ -37,18 +37,14 @@ logger = format_logger(logger) -def main(): - +def main(cfg_file_path): + tic = time.time() logger.info('Starting...') - parser = argparse.ArgumentParser(description="This script makes predictions, using a previously trained model.") - parser.add_argument('config_file', type=str, help='a YAML config file') - args = parser.parse_args() - - logger.info(f"Using {args.config_file} as config file.") + logger.info(f"Using {cfg_file_path} as config file.") - with open(args.config_file) as fp: + with open(cfg_file_path) as fp: cfg = yaml.load(fp, Loader=yaml.FullLoader)[os.path.basename(__file__)] # ---- parse config file @@ -181,7 +177,13 @@ def main(): sys.stderr.flush() - + if __name__ == "__main__": + + parser = argparse.ArgumentParser(description="This script makes predictions, using a previously trained model.") + parser.add_argument('config_file', type=str, help='a YAML config file') + args = parser.parse_args() + + main(args.config_file) - main() \ No newline at end of file + \ No newline at end of file diff --git a/scripts/train_model.py b/scripts/train_model.py index 54a452f..9e2ac37 100644 --- a/scripts/train_model.py +++ b/scripts/train_model.py @@ -32,18 +32,14 @@ logger = format_logger(logger) -def main(): - +def main(cfg_file_path): + tic = time.time() logger.info('Starting...') - parser = argparse.ArgumentParser(description="This script trains a predictive models.") - parser.add_argument('config_file', type=str, help='a YAML config file') - args = parser.parse_args() - - logger.info(f"Using {args.config_file} as config file.") - - with open(args.config_file) as fp: + logger.info(f"Using {cfg_file_path} as config file.") + + with open(cfg_file_path) as fp: cfg = yaml.load(fp, Loader=yaml.FullLoader)[os.path.basename(__file__)] # ---- parse config file @@ -169,10 +165,15 @@ def main(): logger.success(f"Nothing left to be done: exiting. Elapsed time: {(toc-tic):.2f} seconds") sys.stderr.flush() - + if __name__ == "__main__": - main() + parser = argparse.ArgumentParser(description="This script trains a predictive model.") + parser.add_argument('config_file', type=str, help='a YAML config file') + args = parser.parse_args() + + main(args.config_file) + diff --git a/setup.py b/setup.py index cb20b7e..1eae9d8 100644 --- a/setup.py +++ b/setup.py @@ -13,10 +13,7 @@ license="MIT license", entry_points = { 'console_scripts': [ - 'generate_tilesets=scripts.generate_tilesets:main', - 'train_model=scripts.train_model:main', - 'make_predictions=scripts.make_predictions:main', - 'assess_predictions=scripts.assess_predictions:main', + 'stdl-objdet=scripts.cli:main' ] }, install_requires=requirements, From 2089b6ebd7fa49ad52005b368ff1dc4b201d922d Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Wed, 30 Aug 2023 13:53:29 +0000 Subject: [PATCH 035/108] Prevent the CLI from raising an exception in case no argument is passed --- scripts/cli.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/cli.py b/scripts/cli.py index e675dd3..e132406 100644 --- a/scripts/cli.py +++ b/scripts/cli.py @@ -1,5 +1,5 @@ # see https://realpython.com/command-line-interfaces-python-argparse/#adding-subcommands-to-your-clis - +import sys import argparse from scripts.generate_tilesets import main as generate_tilesets from scripts.train_model import main as train_model @@ -39,7 +39,8 @@ def main(): add_parser.add_argument(**arg_template) add_parser.set_defaults(func=assess_predictions) - args = global_parser.parse_args() + # https://stackoverflow.com/a/47440202 + args = global_parser.parse_args(args=None if sys.argv[1:] else ['--help']) args.func(*args.operands) From ed1fe423a9e70ac678ae45e863506343b3c92b41 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Thu, 31 Aug 2023 08:27:51 +0000 Subject: [PATCH 036/108] Add detectron2 config file --- .../detectron2_config_dqry.yaml | 326 ++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 examples/quarry-detection/detectron2_config_dqry.yaml diff --git a/examples/quarry-detection/detectron2_config_dqry.yaml b/examples/quarry-detection/detectron2_config_dqry.yaml new file mode 100644 index 0000000..08374ee --- /dev/null +++ b/examples/quarry-detection/detectron2_config_dqry.yaml @@ -0,0 +1,326 @@ +CUDNN_BENCHMARK: false +DATALOADER: + ASPECT_RATIO_GROUPING: true + FILTER_EMPTY_ANNOTATIONS: false + NUM_WORKERS: 4 + REPEAT_THRESHOLD: 0.0 + SAMPLER_TRAIN: TrainingSampler +DATASETS: + PRECOMPUTED_PROPOSAL_TOPK_TEST: 1000 + PRECOMPUTED_PROPOSAL_TOPK_TRAIN: 2000 + PROPOSAL_FILES_TEST: [] + PROPOSAL_FILES_TRAIN: [] + TEST: + - val_dataset + TRAIN: + - trn_dataset +GLOBAL: + HACK: 1.0 +INPUT: + CROP: + ENABLED: false + SIZE: + - 0.9 + - 0.9 + TYPE: relative_range + FORMAT: RGB + MASK_FORMAT: polygon + MAX_SIZE_TEST: 1333 + MAX_SIZE_TRAIN: 1333 + MIN_SIZE_TEST: 800 + MIN_SIZE_TRAIN: + - 640 + - 672 + - 704 + - 736 + - 768 + - 800 + MIN_SIZE_TRAIN_SAMPLING: choice +MODEL: + ANCHOR_GENERATOR: + ANGLES: + - - -90 + - 0 + - 90 + ASPECT_RATIOS: + - - 0.5 + - 1.0 + - 2.0 + NAME: DefaultAnchorGenerator + OFFSET: 0.0 + SIZES: + - - 32 + - - 64 + - - 128 + - - 256 + - - 512 + BACKBONE: + FREEZE_AT: 2 + NAME: build_resnet_fpn_backbone + DEVICE: cuda + FPN: + FUSE_TYPE: sum + IN_FEATURES: + - res2 + - res3 + - res4 + - res5 + NORM: '' + OUT_CHANNELS: 256 + KEYPOINT_ON: false + LOAD_PROPOSALS: false + MASK_ON: true + META_ARCHITECTURE: GeneralizedRCNN + PANOPTIC_FPN: + COMBINE: + ENABLED: true + INSTANCES_CONFIDENCE_THRESH: 0.5 + OVERLAP_THRESH: 0.5 + STUFF_AREA_LIMIT: 4096 + INSTANCE_LOSS_WEIGHT: 1.0 + PIXEL_MEAN: + - 103.53 + - 116.28 + - 123.675 + PIXEL_STD: + - 1.0 + - 1.0 + - 1.0 + PROPOSAL_GENERATOR: + MIN_SIZE: 0 + NAME: RPN + RESNETS: + DEFORM_MODULATED: false + DEFORM_NUM_GROUPS: 1 + DEFORM_ON_PER_STAGE: + - false + - false + - false + - false + DEPTH: 50 + NORM: FrozenBN + NUM_GROUPS: 1 + OUT_FEATURES: + - res2 + - res3 + - res4 + - res5 + RES2_OUT_CHANNELS: 256 + RES5_DILATION: 1 + STEM_OUT_CHANNELS: 64 + STRIDE_IN_1X1: true + WIDTH_PER_GROUP: 64 + RETINANET: + BBOX_REG_WEIGHTS: + - 1.0 + - 1.0 + - 1.0 + - 1.0 + FOCAL_LOSS_ALPHA: 0.25 + FOCAL_LOSS_GAMMA: 2.0 + IN_FEATURES: + - p3 + - p4 + - p5 + - p6 + - p7 + IOU_LABELS: + - 0 + - -1 + - 1 + IOU_THRESHOLDS: + - 0.4 + - 0.5 + NMS_THRESH_TEST: 0.5 + NUM_CLASSES: 80 + NUM_CONVS: 4 + PRIOR_PROB: 0.01 + SCORE_THRESH_TEST: 0.05 + SMOOTH_L1_LOSS_BETA: 0.1 + TOPK_CANDIDATES_TEST: 1000 + ROI_BOX_CASCADE_HEAD: + BBOX_REG_WEIGHTS: + - - 10.0 + - 10.0 + - 5.0 + - 5.0 + - - 20.0 + - 20.0 + - 10.0 + - 10.0 + - - 30.0 + - 30.0 + - 15.0 + - 15.0 + IOUS: + - 0.5 + - 0.6 + - 0.7 + ROI_BOX_HEAD: + BBOX_REG_WEIGHTS: + - 10.0 + - 10.0 + - 5.0 + - 5.0 + CLS_AGNOSTIC_BBOX_REG: false + CONV_DIM: 256 + FC_DIM: 1024 + NAME: FastRCNNConvFCHead + NORM: '' + NUM_CONV: 0 + NUM_FC: 2 + POOLER_RESOLUTION: 7 + POOLER_SAMPLING_RATIO: 0 + POOLER_TYPE: ROIAlignV2 + SMOOTH_L1_BETA: 0.0 + TRAIN_ON_PRED_BOXES: false + ROI_HEADS: + BATCH_SIZE_PER_IMAGE: 1024 + IN_FEATURES: + - p2 + - p3 + - p4 + - p5 + IOU_LABELS: + - 0 + - 1 + IOU_THRESHOLDS: + - 0.5 + NAME: StandardROIHeads + NMS_THRESH_TEST: 0.5 + NUM_CLASSES: 1 + POSITIVE_FRACTION: 0.25 + PROPOSAL_APPEND_GT: true + SCORE_THRESH_TEST: 0.05 + ROI_KEYPOINT_HEAD: + CONV_DIMS: + - 512 + - 512 + - 512 + - 512 + - 512 + - 512 + - 512 + - 512 + LOSS_WEIGHT: 1.0 + MIN_KEYPOINTS_PER_IMAGE: 1 + NAME: KRCNNConvDeconvUpsampleHead + NORMALIZE_LOSS_BY_VISIBLE_KEYPOINTS: true + NUM_KEYPOINTS: 17 + POOLER_RESOLUTION: 14 + POOLER_SAMPLING_RATIO: 0 + POOLER_TYPE: ROIAlignV2 + ROI_MASK_HEAD: + CLS_AGNOSTIC_MASK: false + CONV_DIM: 256 + NAME: MaskRCNNConvUpsampleHead + NORM: '' + NUM_CONV: 4 + POOLER_RESOLUTION: 14 + POOLER_SAMPLING_RATIO: 0 + POOLER_TYPE: ROIAlignV2 + RPN: + BATCH_SIZE_PER_IMAGE: 256 + BBOX_REG_WEIGHTS: + - 1.0 + - 1.0 + - 1.0 + - 1.0 + BOUNDARY_THRESH: -1 + HEAD_NAME: StandardRPNHead + IN_FEATURES: + - p2 + - p3 + - p4 + - p5 + - p6 + IOU_LABELS: + - 0 + - -1 + - 1 + IOU_THRESHOLDS: + - 0.3 + - 0.7 + LOSS_WEIGHT: 1.0 + NMS_THRESH: 0.7 + POSITIVE_FRACTION: 0.5 + POST_NMS_TOPK_TEST: 1000 + POST_NMS_TOPK_TRAIN: 1000 + PRE_NMS_TOPK_TEST: 1000 + PRE_NMS_TOPK_TRAIN: 2000 + SMOOTH_L1_BETA: 0.0 + SEM_SEG_HEAD: + COMMON_STRIDE: 4 + CONVS_DIM: 128 + IGNORE_VALUE: 255 + IN_FEATURES: + - p2 + - p3 + - p4 + - p5 + LOSS_WEIGHT: 1.0 + NAME: SemSegFPNHead + NORM: GN + NUM_CLASSES: 54 + WEIGHTS: https://dl.fbaipublicfiles.com/detectron2/COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x/137260431/model_final_a54504.pkl +OUTPUT_DIR: logs +SEED: 42 +SOLVER: + BASE_LR: 0.005 + BIAS_LR_FACTOR: 1.0 + CHECKPOINT_PERIOD: 1000 + CLIP_GRADIENTS: + CLIP_TYPE: value + CLIP_VALUE: 1.0 + ENABLED: false + NORM_TYPE: 2.0 + GAMMA: 0.8 + IMS_PER_BATCH: 2 + LR_SCHEDULER_NAME: WarmupMultiStepLR + MAX_ITER: 7000 + MOMENTUM: 0.9 + NESTEROV: false + STEPS: + - 500 + - 1000 + - 1500 + - 2000 + - 2500 + - 3000 + - 3500 + - 4000 + - 4500 + - 5000 + - 5500 + - 6000 + - 6500 + WARMUP_FACTOR: 0.001 + WARMUP_ITERS: 200 + WARMUP_METHOD: linear + WEIGHT_DECAY: 0.0001 + WEIGHT_DECAY_BIAS: 0.0001 + WEIGHT_DECAY_NORM: 0.0 +TEST: + AUG: + ENABLED: false + FLIP: true + MAX_SIZE: 4000 + MIN_SIZES: + - 400 + - 500 + - 600 + - 700 + - 800 + - 900 + - 1000 + - 1100 + - 1200 + DETECTIONS_PER_IMAGE: 100 + EVAL_PERIOD: 200 + EXPECTED_RESULTS: [] + KEYPOINT_OKS_SIGMAS: [] + PRECISE_BN: + ENABLED: false + NUM_ITER: 200 +VERSION: 2 +VIS_PERIOD: 0 \ No newline at end of file From bad71e3674ac6a13b6c2f7ccf388d99a78d99aea Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Thu, 31 Aug 2023 08:34:03 +0000 Subject: [PATCH 037/108] Add trne config file --- examples/quarry-detection/config-trne.yaml | 81 ++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 examples/quarry-detection/config-trne.yaml diff --git a/examples/quarry-detection/config-trne.yaml b/examples/quarry-detection/config-trne.yaml new file mode 100644 index 0000000..25a9da3 --- /dev/null +++ b/examples/quarry-detection/config-trne.yaml @@ -0,0 +1,81 @@ +############################################# +####### Model training and evaluation ####### +# Automatic detection of mineral extraction sites in images with a provided ground truth + +# 1-Prepare the tiles geometry according to the AOI and zoom level +prepare_data.py: + srs: "EPSG:2056" + datasets: + labels_shapefile: ../input/input-trne/tlm-hr-trn-topo.shp + output_folder: ../output/output-trne + zoom_level: 16 #z, keep between 15 and 18 + +# 2-Request tiles according to the provided AOI and tiles parameters and split tiles into 3 datasets: train, test, validation +generate_tilesets.py: + debug_mode: False + datasets: + aoi_tiles_geojson: ../output/output-trne/tiles.geojson + ground_truth_labels_geojson: ../output/output-trne/labels.geojson + orthophotos_web_service: + type: XYZ # supported values: 1. MIL = Map Image Layer 2. WMS 3. XYZ + url: https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swissimage-product/default/2020/3857/{z}/{x}/{y}.jpeg + output_folder: ../output/output-trne + tile_size: 256 # per side, in pixels + overwrite: False + n_jobs: 10 + COCO_metadata: + year: 2021 + version: 1.0 + description: Swiss Image Hinterground w/ Quarry and exploitation site detection + contributor: swisstopo + url: https://swisstopo.ch + license: + name: Unknown + url: + category: + name: "Quarry" + supercategory: "Land usage" + +# 3-Train the model with the detectron2 algorithm +# Monitor the training process via tensorboard (tensorboard --logdir ). Choice of the optimized model: minimisation of the validation loss curve +train_model.py: + working_folder: ../output/output-trne + log_subfolder: logs + sample_tagged_img_subfolder: sample_tagged_images + COCO_files: # relative paths, w/ respect to the working_folder + trn: COCO_trn.json + val: COCO_val.json + tst: COCO_tst.json + detectron2_config_file: '../../config/detectron2_config_dqry.yaml' # path relative to the working_folder + model_weights: + model_zoo_checkpoint_url: "COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x.yaml" + +# 4-Perform the object detection based on the optimized trained model +make_predictions.py: + working_folder: ../output/output-trne + log_subfolder: logs + sample_tagged_img_subfolder: sample_tagged_images + COCO_files: # relative paths, w/ respect to the working_folder + trn: COCO_trn.json + val: COCO_val.json + tst: COCO_tst.json + detectron2_config_file: '../../config/detectron2_config_dqry.yaml' # path relative to the working_folder + model_weights: + pth_file: './z16/replicate_3/logs/model_0002999.pth' + image_metadata_json: '../output/output-trne/img_metadata.json' + rdp_simplification: # rdp = Ramer-Douglas-Peucker + enabled: true + epsilon: 2.0 # cf. https://rdp.readthedocs.io/en/latest/ + score_lower_threshold: 0.05 + +# 5-Evaluate the quality of the prediction for the different datasets with metrics calculation +assess_predictions.py: + datasets: + ground_truth_labels_geojson: ../output/output-trne/labels.geojson + image_metadata_json: ../output/output-trne/img_metadata.json + split_aoi_tiles_geojson: ../output/output-trne/split_aoi_tiles.geojson # aoi = Area of Interest + predictions: + trn: ../output/output-trne/trn_predictions_at_0dot05_threshold.gpkg + val: ../output/output-trne/val_predictions_at_0dot05_threshold.gpkg + tst: ../output/output-trne/tst_predictions_at_0dot05_threshold.gpkg + output_folder: ../output/output-trne \ No newline at end of file From ffe65967257c49b2f28edbebf63567f09cd9f95d Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Thu, 31 Aug 2023 08:37:36 +0000 Subject: [PATCH 038/108] Add config-prd file --- examples/quarry-detection/config-prd.yaml | 64 ++++++++++++++++++++++ examples/quarry-detection/config-trne.yaml | 2 +- 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 examples/quarry-detection/config-prd.yaml diff --git a/examples/quarry-detection/config-prd.yaml b/examples/quarry-detection/config-prd.yaml new file mode 100644 index 0000000..f73730c --- /dev/null +++ b/examples/quarry-detection/config-prd.yaml @@ -0,0 +1,64 @@ +################################### +####### Inference detection ####### +# Automatic detection of mineral extraction sites in images + +# 1-Prepare the tiles geometry according to the AOI and zoom level +prepare_data.py: + srs: "EPSG:2056" # Projection of the input file + datasets: + labels_shapefile: ../input/input-prd/swissimage_footprint_2019.shp + output_folder: ../output/output-prd + zoom_level: 16 #z, keep between 15 and 18 + +# 2-Request tiles according to the provided AOI and tiles parameters +generate_tilesets.py: + debug_mode: False #reduced amount of tiles + datasets: + aoi_tiles_geojson: ../output/output-prd/tiles.geojson + orthophotos_web_service: + type: XYZ # supported values: 1. MIL = Map Image Layer 2. WMS 3. XYZ + url: https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swissimage-product/default/2019/3857/{z}/{x}/{y}.jpeg + output_folder: ../output/output-prd + tile_size: 256 # per side, in pixels + overwrite: True + n_jobs: 10 + COCO_metadata: + year: 2021 + version: 1.0 + description: Swiss Image Hinterground w/ Quarry and exploitation site detection + contributor: swisstopo + url: https://swisstopo.ch + license: + name: Unknown + url: + category: + name: "Quarry" + supercategory: "Land usage" + +# 3-Perform the object detection based on the optimized trained model +make_predictions.py: + working_folder: ../output/output-prd + log_subfolder: logs + sample_tagged_img_subfolder: sample_tagged_images + COCO_files: # relative paths, w/ respect to the working_folder + oth: COCO_oth.json + detectron2_config_file: '../../config/detectron2_config_dqry.yaml' # path relative to the working_folder + model_weights: + pth_file: '../../input/input-prd/logs/z16/replicate_3/model_0002999.pth' #!!!Chose the optimized trained model, i.e. the one minimizing the validation loss curve + image_metadata_json: '../output/output-prd/img_metadata.json' + rdp_simplification: # rdp = Ramer-Douglas-Peucker + enabled: True + epsilon: 2.0 # cf. https://rdp.readthedocs.io/en/latest/ + score_lower_threshold: 0.3 + +# 4-Filtering and merging prediction polygons to improve results +prediction_filter.py: + year: 2020 + input: ../output/output-prd/oth_predictions_at_0dot3_threshold.gpkg + labels_shapefile: ../input/input-prd/swissimage_footprint_2019.shp + dem: ../input/input-prd/switzerland_dem_EPSG2056.tif + elevation: 1200.0 #m, altitude threshold. Dectection above the threshold are discarded. Unlikely to observe quarry above and avoid flase detection of rock outcrop and snow . Default: 1200 + score: 0.50 #prediction score (reliability of the detection) provided by detectron2. The value can be varied from 0 to 1. Default: 0.95 + distance: 10 #m, distance use as a buffer to merge close polygons (likely to belong to the same quarry) together. Default: 10 + area: 5000.0 #m2, area threshold under which polygons are discarded (unlikely to observe quarry sites under this surface). Default: 5000.0 + output: ../output/output-prd/oth_prediction_at_0dot3_threshold_year-{year}_score-{score}_area-{area}_elevation-{elevation}_distance-{distance}.geojson \ No newline at end of file diff --git a/examples/quarry-detection/config-trne.yaml b/examples/quarry-detection/config-trne.yaml index 25a9da3..347a04b 100644 --- a/examples/quarry-detection/config-trne.yaml +++ b/examples/quarry-detection/config-trne.yaml @@ -1,6 +1,6 @@ ############################################# ####### Model training and evaluation ####### -# Automatic detection of mineral extraction sites in images with a provided ground truth +# Training of automatic detection of mineral extraction sites in images with a provided ground truth # 1-Prepare the tiles geometry according to the AOI and zoom level prepare_data.py: From a331de65df7ada93abc11127a2b03a43e1403b29 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Thu, 31 Aug 2023 08:39:05 +0000 Subject: [PATCH 039/108] Add logging.conf file --- examples/quarry-detection/logging.conf | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 examples/quarry-detection/logging.conf diff --git a/examples/quarry-detection/logging.conf b/examples/quarry-detection/logging.conf new file mode 100644 index 0000000..9e26be7 --- /dev/null +++ b/examples/quarry-detection/logging.conf @@ -0,0 +1,23 @@ +[loggers] +keys=root + +[handlers] +keys=consoleHandler + +[formatters] +keys=simpleFormatter + +[logger_root] +level=DEBUG +handlers=consoleHandler + + +[handler_consoleHandler] +class=StreamHandler +level=INFO +formatter=simpleFormatter +args=(sys.stdout,) + +[formatter_simpleFormatter] +format=%(asctime)s - %(name)s - %(levelname)s - %(message)s +datefmt= \ No newline at end of file From fdcfa51d9c596b5068f66cea1da98673e2644d31 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Thu, 31 Aug 2023 08:42:30 +0000 Subject: [PATCH 040/108] Add prepare_data.py --- examples/quarry-detection/prepare_data.py | 133 ++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 examples/quarry-detection/prepare_data.py diff --git a/examples/quarry-detection/prepare_data.py b/examples/quarry-detection/prepare_data.py new file mode 100644 index 0000000..8bc6b63 --- /dev/null +++ b/examples/quarry-detection/prepare_data.py @@ -0,0 +1,133 @@ +#!/bin/python +# -*- coding: utf-8 -*- + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +import time +import argparse +import yaml +import os, sys +import geopandas as gpd +import pandas as pd +import morecantile +import re + +from tqdm import tqdm +from loguru import logger + +# the following allows us to import modules from within this file's parent folder +sys.path.insert(0, '.') + +logger.remove() +logger.add(sys.stderr, format="{time:YYYY-MM-DD HH:mm:ss} - {level} - {message}", level="INFO") + +if __name__ == "__main__": + + # Start chronometer + tic = time.time() + logger.info('Starting...') + + # Argument and parameter specification + parser = argparse.ArgumentParser(description="The script prepares dataset to process the quarries detection project (STDL.proj-dqry)") + parser.add_argument('config_file', type=str, help='Framework configuration file') + args = parser.parse_args() + + logger.info(f"Using {args.config_file} as config file.") + + with open(args.config_file) as fp: + cfg = yaml.load(fp, Loader=yaml.FullLoader)[os.path.basename(__file__)] + + # Load input parameters + OUTPUT_DIR = cfg['output_folder'] + LABELS_SHPFILE = cfg['datasets']['labels_shapefile'] + ZOOM_LEVEL = cfg['zoom_level'] + + # Create an output directory in case it doesn't exist + if not os.path.exists(OUTPUT_DIR): + os.makedirs(OUTPUT_DIR) + + # Prepare the tiles + written_files = [] + + ## Convert datasets shapefiles into geojson format + logger.info('Convert labels shapefile into GeoJSON format (EPSG:4326)...') + labels = gpd.read_file(LABELS_SHPFILE) + labels_4326 = labels.to_crs(epsg=4326) + + nb_labels = len(labels) + logger.info('There is/are ' + str(nb_labels) + ' polygon(s) in ' + LABELS_SHPFILE) + + feature = 'labels.geojson' + feature_path = os.path.join(OUTPUT_DIR, feature) + labels_4326.to_file(feature_path, driver='GeoJSON') + written_files.append(feature_path) + logger.info(f"...done. A file was written: {feature_path}") + + logger.info('Creating tiles for the Area of Interest (AOI)...') + + # Grid definition + tms = morecantile.tms.get("WebMercatorQuad") # epsg:3857 + + # New gpd with only labels geometric info (minx, miny, maxx, maxy) + logger.info('- Get geometric boundaries of the label(s)') + boundary = labels_4326.bounds + + # Iterate on geometric coordinates to defined tiles for a given label at a given zoom level + # A gpd is created for each label and are then concatenate into a single gpd + logger.info('- Compute tiles for each label(s) geometry') + tiles_3857_all = [] + for row in range(len(boundary)): + coords = (boundary.iloc[row,0],boundary.iloc[row,1],boundary.iloc[row,2],boundary.iloc[row,3]) + tiles_3857 = gpd.GeoDataFrame.from_features([tms.feature(x, projected=True) for x in tqdm(tms.tiles(*coords, zooms=[ZOOM_LEVEL]))]) + tiles_3857.set_crs(epsg=3857, inplace=True) + tiles_3857_all.append(tiles_3857) + tiles_3857_aoi = gpd.GeoDataFrame(pd.concat(tiles_3857_all, ignore_index=True) ) + + # Remove unrelevant tiles and reorganized the data set: + logger.info('- Remove duplicated tiles and tiles that are not intersecting labels') + + # - Keep only tiles that are intersecting the label + labels_3857=labels_4326.to_crs(epsg=3857) + labels_3857.rename(columns={'FID': 'id_aoi'},inplace=True) + tiles_aoi=gpd.sjoin(tiles_3857_aoi, labels_3857, how='inner') + + # - Remove duplicated tiles + if nb_labels > 1: + tiles_aoi.drop_duplicates('title', inplace=True) + + # - Remove useless columns, reinitilize feature id and redifined it according to xyz format + logger.info('- Format feature id and reorganise data set') + tiles_aoi.drop(tiles_aoi.columns.difference(['geometry','id','title']), axis=1, inplace=True) + tiles_aoi.reset_index(drop=True, inplace=True) + + # Format the xyz parameters and filled in the attributes columns + xyz = [] + for idx in tiles_aoi.index: + xyz.append([re.sub('\D','',coor) for coor in tiles_aoi.loc[idx,'title'].split(',')]) + tiles_aoi['id'] = [f'({x}, {y}, {z})' for x, y, z in xyz] + tiles_aoi = tiles_aoi[['geometry', 'title', 'id']] + + nb_tiles = len(tiles_aoi) + logger.info('There was/were ' + str(nb_tiles) + ' tiles(s) created') + + # Convert datasets shapefiles into geojson format + logger.info('Convert tiles shapefile into GeoJSON format (EPSG:4326)...') + feature = 'tiles.geojson' + feature_path = os.path.join(OUTPUT_DIR, feature) + tiles_4326=tiles_aoi.to_crs(epsg=4326) + tiles_4326.to_file(feature_path, driver='GeoJSON') + written_files.append(feature_path) + logger.info(f"...done. A file was written: {feature_path}") + + print() + logger.info("The following files were written. Let's check them out!") + for written_file in written_files: + logger.info(written_file) + print() + + # Stop chronometer + toc = time.time() + logger.info(f"Nothing left to be done: exiting. Elapsed time: {(toc-tic):.2f} seconds") + + sys.stderr.flush() \ No newline at end of file From 631b08e7baed53efbee1d933fea563f5fe37253b Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Thu, 31 Aug 2023 08:53:34 +0000 Subject: [PATCH 041/108] Add prediction_filter.py script --- .../quarry-detection/prediction_filter.py | 155 ++++++++++++++++++ examples/quarry-detection/prepare_data.py | 21 ++- 2 files changed, 167 insertions(+), 9 deletions(-) create mode 100644 examples/quarry-detection/prediction_filter.py diff --git a/examples/quarry-detection/prediction_filter.py b/examples/quarry-detection/prediction_filter.py new file mode 100644 index 0000000..3ae6151 --- /dev/null +++ b/examples/quarry-detection/prediction_filter.py @@ -0,0 +1,155 @@ +#!/bin/python +# -*- coding: utf-8 -*- + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +import os +import sys +import inspect +import time +import argparse +import yaml +from loguru import logger + +import geopandas as gpd +import pandas as pd +import rasterio +from sklearn.cluster import KMeans + + +# the following allows us to import modules from within this file's parent folder +sys.path.insert(0, '.') + +logger.remove() +logger.add(sys.stderr, format="{time:YYYY-MM-DD HH:mm:ss} - {level} - {message}", level="INFO") + + +if __name__ == "__main__": + + # Chronometer + tic = time.time() + logger.info('Starting...') + + # argument parser + parser = argparse.ArgumentParser(description="The script filters the detection of potential Mineral Extraction Sites obtained with the object-detector scripts") + parser.add_argument('config_file', type=str, help='input geojson path') + args = parser.parse_args() + + logger.info(f"Using {args.config_file} as config file.") + + with open(args.config_file) as fp: + cfg = yaml.load(fp, Loader=yaml.FullLoader)[os.path.basename(__file__)] + + # Load input parameters + YEAR = cfg['year'] + INPUT = cfg['input'] + LABELS_SHPFILE = cfg['labels_shapefile'] + DEM = cfg['dem'] + SCORE = cfg['score'] + AREA = cfg['area'] + ELEVATION = cfg['elevation'] + DISTANCE = cfg['distance'] + OUTPUT = cfg['output'] + + written_files = [] + + # Convert input detection to a geo dataframe + aoi = gpd.read_file(LABELS_SHPFILE) + aoi = aoi.to_crs(epsg=2056) + + input = gpd.read_file(INPUT) + input = input.to_crs(2056) + total = len(input) + logger.info(f"Total input = {total}") + + # Discard polygons detected above the threshold elevalation and 0 m + r = rasterio.open(DEM) + row, col = r.index(input.centroid.x, input.centroid.y) + values = r.read(1)[row, col] + input['elev'] = values + input = input[input.elev < ELEVATION] + row, col = r.index(input.centroid.x, input.centroid.y) + values = r.read(1)[row, col] + input['elev'] = values + + input = input[input.elev != 0] + te = len(input) + logger.info(f"{str(total - te)} predictions removed by elevation threshold: {str(ELEVATION)}") + + # Centroid of every prediction polygon + centroids = gpd.GeoDataFrame() + centroids.geometry = input.representative_point() + + # KMeans Unsupervised Learning + centroids = pd.DataFrame({'x': centroids.geometry.x, 'y': centroids.geometry.y}) + k = int((len(input)/3) + 1) + cluster = KMeans(n_clusters=k, algorithm='auto', random_state=1) + model = cluster.fit(centroids) + labels = model.predict(centroids) + logger.info(f"KMeans algorithm computed with k = {str(k)}") + + # Dissolve and Aggregate (keep the max value of aggregate attributes) + input['cluster'] = labels + + input = input.dissolve(by='cluster', aggfunc='max') + total = len(input) + + # Filter dataframe by score value + input = input[input['score'] > SCORE] + sc = len(input) + logger.info(f"{str(total - sc)} predictions removed by score threshold: {str(SCORE)}") + + # Clip prediction to AOI + input = gpd.clip(input, aoi) + + # Create empty data frame + geo_merge = gpd.GeoDataFrame() + # Merge close labels using buffer and unions + geo_merge = input.buffer(+DISTANCE, resolution = 2) + geo_merge = geo_merge.geometry.unary_union + geo_merge = gpd.GeoDataFrame(geometry=[geo_merge], crs = input.crs) + geo_merge = geo_merge.explode(index_parts=True).reset_index(drop=True) + geo_merge = geo_merge.buffer(-DISTANCE, resolution=2) + + td = len(geo_merge) + logger.info(f"{str(sc - td)} difference to clustered predictions after union (distance {str(DISTANCE)})") + + # Discard polygons with area under the threshold + geo_merge = geo_merge[geo_merge.area > AREA] + ta = len(geo_merge) + logger.info(f"{str(td - ta)} difference to clustered predictions after union (distance {str(AREA)})") + + # Preparation of a geo df + data = {'id': geo_merge.index,'area': geo_merge.area, 'centroid_x': geo_merge.centroid.x, 'centroid_y': geo_merge.centroid.y, 'geometry': geo_merge} + geo_tmp = gpd.GeoDataFrame(data, crs=input.crs) + + # Get the averaged prediction score of the merged polygons + intersection = gpd.sjoin(geo_tmp, input, how='inner') + intersection['id'] = intersection.index + score_final=intersection.groupby(['id']).mean(numeric_only=True) + # Formatting the final geo df + data = {'id_feature': geo_merge.index,'score': score_final['score'] , 'area': geo_merge.area, 'centroid_x': geo_merge.centroid.x, 'centroid_y': geo_merge.centroid.y, 'geometry': geo_merge} + geo_final = gpd.GeoDataFrame(data, crs=input.crs) + logger.info(f"{len(geo_final)} predictions remaining") + + # Format the ooutput name of the filtered prediction + feature = OUTPUT.replace('{score}', str(SCORE)).replace('0.', '0dot') \ + .replace('{year}', str(int(YEAR)))\ + .replace('{area}', str(int(AREA)))\ + .replace('{elevation}', str(int(ELEVATION))) \ + .replace('{distance}', str(int(DISTANCE))) + geo_final.to_file(feature, driver='GeoJSON') + + written_files.append(feature) + logger.info(f"...done. A file was written: {feature}") + + logger.info("The following files were written. Let's check them out!") + for written_file in written_files: + logger.info(written_file) + + # Stop chronometer + toc = time.time() + logger.info(f"Nothing left to be done: exiting. Elapsed time: {(toc-tic):.2f} seconds") + + sys.stderr.flush() \ No newline at end of file diff --git a/examples/quarry-detection/prepare_data.py b/examples/quarry-detection/prepare_data.py index 8bc6b63..7d1c09e 100644 --- a/examples/quarry-detection/prepare_data.py +++ b/examples/quarry-detection/prepare_data.py @@ -4,24 +4,27 @@ # This source code is licensed under the license found in the # LICENSE file in the root directory of this source tree. +import os +import sys import time import argparse import yaml -import os, sys -import geopandas as gpd -import pandas as pd -import morecantile import re - from tqdm import tqdm from loguru import logger +import geopandas as gpd +import morecantile +import pandas as pd + + # the following allows us to import modules from within this file's parent folder sys.path.insert(0, '.') logger.remove() logger.add(sys.stderr, format="{time:YYYY-MM-DD HH:mm:ss} - {level} - {message}", level="INFO") + if __name__ == "__main__": # Start chronometer @@ -29,7 +32,7 @@ logger.info('Starting...') # Argument and parameter specification - parser = argparse.ArgumentParser(description="The script prepares dataset to process the quarries detection project (STDL.proj-dqry)") + parser = argparse.ArgumentParser(description="The script prepares the Mineral Extraction Sites dataset to be processed by the object-detector scripts") parser.add_argument('config_file', type=str, help='Framework configuration file') args = parser.parse_args() @@ -88,9 +91,9 @@ logger.info('- Remove duplicated tiles and tiles that are not intersecting labels') # - Keep only tiles that are intersecting the label - labels_3857=labels_4326.to_crs(epsg=3857) + labels_3857 = labels_4326.to_crs(epsg=3857) labels_3857.rename(columns={'FID': 'id_aoi'},inplace=True) - tiles_aoi=gpd.sjoin(tiles_3857_aoi, labels_3857, how='inner') + tiles_aoi = gpd.sjoin(tiles_3857_aoi, labels_3857, how='inner') # - Remove duplicated tiles if nb_labels > 1: @@ -115,7 +118,7 @@ logger.info('Convert tiles shapefile into GeoJSON format (EPSG:4326)...') feature = 'tiles.geojson' feature_path = os.path.join(OUTPUT_DIR, feature) - tiles_4326=tiles_aoi.to_crs(epsg=4326) + tiles_4326 = tiles_aoi.to_crs(epsg=4326) tiles_4326.to_file(feature_path, driver='GeoJSON') written_files.append(feature_path) logger.info(f"...done. A file was written: {feature_path}") From 1a7a83132e3df0e331be35785bdc1c846f1c47fd Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Thu, 31 Aug 2023 09:22:28 +0000 Subject: [PATCH 042/108] Adapt paths in config files --- examples/quarry-detection/config-prd.yaml | 28 ++++++++-------- examples/quarry-detection/config-trne.yaml | 38 +++++++++++----------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/examples/quarry-detection/config-prd.yaml b/examples/quarry-detection/config-prd.yaml index f73730c..eb8764e 100644 --- a/examples/quarry-detection/config-prd.yaml +++ b/examples/quarry-detection/config-prd.yaml @@ -6,19 +6,19 @@ prepare_data.py: srs: "EPSG:2056" # Projection of the input file datasets: - labels_shapefile: ../input/input-prd/swissimage_footprint_2019.shp - output_folder: ../output/output-prd + labels_shapefile: ./input/input-prd/swissimage_footprint_2021.shp + output_folder: ./output/output-prd zoom_level: 16 #z, keep between 15 and 18 # 2-Request tiles according to the provided AOI and tiles parameters generate_tilesets.py: debug_mode: False #reduced amount of tiles datasets: - aoi_tiles_geojson: ../output/output-prd/tiles.geojson + aoi_tiles_geojson: ./output/output-prd/tiles.geojson orthophotos_web_service: type: XYZ # supported values: 1. MIL = Map Image Layer 2. WMS 3. XYZ url: https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swissimage-product/default/2019/3857/{z}/{x}/{y}.jpeg - output_folder: ../output/output-prd + output_folder: ./output/output-prd tile_size: 256 # per side, in pixels overwrite: True n_jobs: 10 @@ -37,15 +37,15 @@ generate_tilesets.py: # 3-Perform the object detection based on the optimized trained model make_predictions.py: - working_folder: ../output/output-prd + working_folder: ./output/output-prd log_subfolder: logs sample_tagged_img_subfolder: sample_tagged_images COCO_files: # relative paths, w/ respect to the working_folder oth: COCO_oth.json - detectron2_config_file: '../../config/detectron2_config_dqry.yaml' # path relative to the working_folder + detectron2_config_file: 'detectron2_config_dqry.yaml' # path relative to the working_folder model_weights: - pth_file: '../../input/input-prd/logs/z16/replicate_3/model_0002999.pth' #!!!Chose the optimized trained model, i.e. the one minimizing the validation loss curve - image_metadata_json: '../output/output-prd/img_metadata.json' + pth_file: '..output/output-trne/logs/model_0002999.pth' #!!!Chose the optimized trained model, i.e. the one minimizing the validation loss curve + image_metadata_json: './output/output-prd/img_metadata.json' rdp_simplification: # rdp = Ramer-Douglas-Peucker enabled: True epsilon: 2.0 # cf. https://rdp.readthedocs.io/en/latest/ @@ -53,12 +53,12 @@ make_predictions.py: # 4-Filtering and merging prediction polygons to improve results prediction_filter.py: - year: 2020 - input: ../output/output-prd/oth_predictions_at_0dot3_threshold.gpkg - labels_shapefile: ../input/input-prd/swissimage_footprint_2019.shp - dem: ../input/input-prd/switzerland_dem_EPSG2056.tif + year: 2021 + input: ./output/output-prd/oth_predictions_at_0dot3_threshold.gpkg + labels_shapefile: ./input/input-prd/swissimage_footprint_2019.shp + dem: ./input/input-prd/switzerland_dem_EPSG2056.tif elevation: 1200.0 #m, altitude threshold. Dectection above the threshold are discarded. Unlikely to observe quarry above and avoid flase detection of rock outcrop and snow . Default: 1200 - score: 0.50 #prediction score (reliability of the detection) provided by detectron2. The value can be varied from 0 to 1. Default: 0.95 + score: 0.95 #prediction score (reliability of the detection) provided by detectron2. The value can be varied from 0 to 1. Default: 0.95 distance: 10 #m, distance use as a buffer to merge close polygons (likely to belong to the same quarry) together. Default: 10 area: 5000.0 #m2, area threshold under which polygons are discarded (unlikely to observe quarry sites under this surface). Default: 5000.0 - output: ../output/output-prd/oth_prediction_at_0dot3_threshold_year-{year}_score-{score}_area-{area}_elevation-{elevation}_distance-{distance}.geojson \ No newline at end of file + output: ./output/output-prd/oth_prediction_at_0dot3_threshold_year-{year}_score-{score}_area-{area}_elevation-{elevation}_distance-{distance}.geojson \ No newline at end of file diff --git a/examples/quarry-detection/config-trne.yaml b/examples/quarry-detection/config-trne.yaml index 347a04b..9aca38b 100644 --- a/examples/quarry-detection/config-trne.yaml +++ b/examples/quarry-detection/config-trne.yaml @@ -6,20 +6,20 @@ prepare_data.py: srs: "EPSG:2056" datasets: - labels_shapefile: ../input/input-trne/tlm-hr-trn-topo.shp - output_folder: ../output/output-trne - zoom_level: 16 #z, keep between 15 and 18 + labels_shapefile: ./data/labels/tlm-hr-trn-topo.shp + output_folder: ./output/output-trne + zoom_level: 16 # z, keep between 15 and 18 # 2-Request tiles according to the provided AOI and tiles parameters and split tiles into 3 datasets: train, test, validation generate_tilesets.py: debug_mode: False datasets: - aoi_tiles_geojson: ../output/output-trne/tiles.geojson - ground_truth_labels_geojson: ../output/output-trne/labels.geojson + aoi_tiles_geojson: ./output/output-trne/tiles.geojson + ground_truth_labels_geojson: ./output/output-trne/labels.geojson orthophotos_web_service: type: XYZ # supported values: 1. MIL = Map Image Layer 2. WMS 3. XYZ url: https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swissimage-product/default/2020/3857/{z}/{x}/{y}.jpeg - output_folder: ../output/output-trne + output_folder: ./output/output-trne tile_size: 256 # per side, in pixels overwrite: False n_jobs: 10 @@ -39,30 +39,30 @@ generate_tilesets.py: # 3-Train the model with the detectron2 algorithm # Monitor the training process via tensorboard (tensorboard --logdir ). Choice of the optimized model: minimisation of the validation loss curve train_model.py: - working_folder: ../output/output-trne + working_folder: ./output/output-trne log_subfolder: logs sample_tagged_img_subfolder: sample_tagged_images COCO_files: # relative paths, w/ respect to the working_folder trn: COCO_trn.json val: COCO_val.json tst: COCO_tst.json - detectron2_config_file: '../../config/detectron2_config_dqry.yaml' # path relative to the working_folder + detectron2_config_file: 'detectron2_config_dqry.yaml' # path relative to the working_folder model_weights: model_zoo_checkpoint_url: "COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x.yaml" # 4-Perform the object detection based on the optimized trained model make_predictions.py: - working_folder: ../output/output-trne + working_folder: ./output/output-trne log_subfolder: logs sample_tagged_img_subfolder: sample_tagged_images COCO_files: # relative paths, w/ respect to the working_folder trn: COCO_trn.json val: COCO_val.json tst: COCO_tst.json - detectron2_config_file: '../../config/detectron2_config_dqry.yaml' # path relative to the working_folder + detectron2_config_file: 'detectron2_config_dqry.yaml' # path relative to the working_folder model_weights: - pth_file: './z16/replicate_3/logs/model_0002999.pth' - image_metadata_json: '../output/output-trne/img_metadata.json' + pth_file: './logs/model_0002999.pth' + image_metadata_json: './output/output-trne/img_metadata.json' rdp_simplification: # rdp = Ramer-Douglas-Peucker enabled: true epsilon: 2.0 # cf. https://rdp.readthedocs.io/en/latest/ @@ -71,11 +71,11 @@ make_predictions.py: # 5-Evaluate the quality of the prediction for the different datasets with metrics calculation assess_predictions.py: datasets: - ground_truth_labels_geojson: ../output/output-trne/labels.geojson - image_metadata_json: ../output/output-trne/img_metadata.json - split_aoi_tiles_geojson: ../output/output-trne/split_aoi_tiles.geojson # aoi = Area of Interest + ground_truth_labels_geojson: ./output/output-trne/labels.geojson + image_metadata_json: ./output/output-trne/img_metadata.json + split_aoi_tiles_geojson: ./output/output-trne/split_aoi_tiles.geojson # aoi = Area of Interest predictions: - trn: ../output/output-trne/trn_predictions_at_0dot05_threshold.gpkg - val: ../output/output-trne/val_predictions_at_0dot05_threshold.gpkg - tst: ../output/output-trne/tst_predictions_at_0dot05_threshold.gpkg - output_folder: ../output/output-trne \ No newline at end of file + trn: ./output/output-trne/trn_predictions_at_0dot05_threshold.gpkg + val: ./output/output-trne/val_predictions_at_0dot05_threshold.gpkg + tst: ./output/output-trne/tst_predictions_at_0dot05_threshold.gpkg + output_folder: ./output/output-trne \ No newline at end of file From 1a26ef8b99a1d056a66186fbd9ff553313deaf0d Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Thu, 31 Aug 2023 09:23:02 +0000 Subject: [PATCH 043/108] Add morecantile to requirements --- requirements.in | 19 +++--- requirements.txt | 152 ++++++++++++++++++++++++++--------------------- 2 files changed, 95 insertions(+), 76 deletions(-) diff --git a/requirements.in b/requirements.in index 73fd07a..58d2e27 100644 --- a/requirements.in +++ b/requirements.in @@ -3,25 +3,26 @@ # $ sudo apt-get install -y python3-gdal gdal-bin libgdal-dev gcc g++ python3.8-dev # -------------------------------------------------------------------------------------- GDAL==3.0.4 -rtree +certifi>=2022.12.07 +future>=0.18.3 geopandas joblib +loguru +morecantile +oauthlib>=3.2.2 +opencv-python pillow>=9.3.0 +plotly pyyaml rasterio +rdp requests>=2.31.0 +rtree supermercado tqdm -opencv-python # cf. https://pytorch.org/get-started/locally/ torch @ https://download.pytorch.org/whl/cu113/torch-1.10.2%2Bcu113-cp38-cp38-linux_x86_64.whl torchvision @ https://download.pytorch.org/whl/cu113/torchvision-0.11.3%2Bcu113-cp38-cp38-linux_x86_64.whl detectron2 @ https://dl.fbaipublicfiles.com/detectron2/wheels/cu113/torch1.10/detectron2-0.6%2Bcu113-cp38-cp38-linux_x86_64.whl -plotly -rdp Werkzeug>=2.2.3 -future>=0.18.3 -wheel>=0.38.1 -oauthlib>=3.2.2 -certifi>=2022.12.07 -loguru \ No newline at end of file +wheel>=0.38.1 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 364817e..2b3a854 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,36 +4,39 @@ # # pip-compile requirements.in # -absl-py==1.2.0 +absl-py==1.4.0 # via tensorboard -affine==2.3.1 +affine==2.4.0 # via # rasterio # supermercado +annotated-types==0.5.0 + # via pydantic antlr4-python3-runtime==4.9.3 # via # hydra-core # omegaconf appdirs==1.4.4 # via black -attrs==22.1.0 +attrs==23.1.0 # via # fiona + # morecantile # rasterio black==21.4b2 # via detectron2 -cachetools==5.2.0 +cachetools==5.3.1 # via google-auth -certifi==2023.5.7 +certifi==2023.7.22 # via # -r requirements.in # fiona # pyproj # rasterio # requests -charset-normalizer==2.1.1 +charset-normalizer==3.2.0 # via requests -click==8.1.3 +click==8.1.7 # via # black # click-plugins @@ -52,69 +55,73 @@ cligj==0.7.2 # fiona # rasterio # supermercado -cloudpickle==2.2.0 +cloudpickle==2.2.1 # via detectron2 -contourpy==1.0.5 +contourpy==1.1.0 # via matplotlib cycler==0.11.0 # via matplotlib detectron2 @ https://dl.fbaipublicfiles.com/detectron2/wheels/cu113/torch1.10/detectron2-0.6%2Bcu113-cp38-cp38-linux_x86_64.whl # via -r requirements.in -fiona==1.8.21 +fiona==1.9.4.post1 # via geopandas -fonttools==4.37.4 +fonttools==4.42.1 # via matplotlib future==0.18.3 # via # -r requirements.in # detectron2 -fvcore==0.1.5.post20220512 +fvcore==0.1.5.post20221221 # via detectron2 gdal==3.0.4 # via -r requirements.in -geopandas==0.11.1 +geopandas==0.13.2 # via -r requirements.in -google-auth==2.12.0 +google-auth==2.22.0 # via # google-auth-oauthlib # tensorboard -google-auth-oauthlib==0.4.6 +google-auth-oauthlib==1.0.0 # via tensorboard -grpcio==1.49.1 +grpcio==1.57.0 # via tensorboard -hydra-core==1.2.0 +hydra-core==1.3.2 # via detectron2 idna==3.4 # via requests -importlib-metadata==5.0.0 - # via markdown -importlib-resources==5.10.0 - # via hydra-core +importlib-metadata==6.8.0 + # via + # fiona + # markdown +importlib-resources==6.0.1 + # via + # hydra-core + # matplotlib iopath==0.1.9 # via # detectron2 # fvcore -joblib==1.2.0 +joblib==1.3.2 # via -r requirements.in -kiwisolver==1.4.4 +kiwisolver==1.4.5 # via matplotlib loguru==0.7.0 # via -r requirements.in -markdown==3.4.1 +markdown==3.4.4 # via tensorboard -markupsafe==2.1.1 +markupsafe==2.1.3 # via werkzeug -matplotlib==3.6.1 +matplotlib==3.7.2 # via # detectron2 # pycocotools mercantile==1.2.1 # via supermercado -munch==2.5.0 - # via fiona -mypy-extensions==0.4.3 +morecantile==5.0.0 + # via -r requirements.in +mypy-extensions==1.0.0 # via black -numpy==1.23.3 +numpy==1.24.4 # via # contourpy # fvcore @@ -124,6 +131,7 @@ numpy==1.23.3 # pycocotools # rasterio # rdp + # shapely # snuggs # supermercado # tensorboard @@ -132,71 +140,77 @@ oauthlib==3.2.2 # via # -r requirements.in # requests-oauthlib -omegaconf==2.2.3 +omegaconf==2.3.0 # via # detectron2 # hydra-core -opencv-python==4.6.0.66 +opencv-python==4.8.0.76 # via -r requirements.in -packaging==21.3 +packaging==23.1 # via # geopandas # hydra-core # matplotlib -pandas==1.5.0 + # plotly +pandas==2.0.3 # via geopandas -pathspec==0.10.1 +pathspec==0.11.2 # via black -pillow==9.5.0 +pillow==10.0.0 # via # -r requirements.in # detectron2 # fvcore # matplotlib # torchvision -plotly==5.10.0 +plotly==5.16.1 # via -r requirements.in -portalocker==2.5.1 +portalocker==2.7.0 # via iopath -protobuf==3.19.6 +protobuf==4.24.2 # via tensorboard -pyasn1==0.4.8 +pyasn1==0.5.0 # via # pyasn1-modules # rsa -pyasn1-modules==0.2.8 +pyasn1-modules==0.3.0 # via google-auth -pycocotools==2.0.5 +pycocotools==2.0.7 # via detectron2 +pydantic==2.3.0 + # via morecantile +pydantic-core==2.6.3 + # via pydantic pydot==1.4.2 # via detectron2 pyparsing==3.0.9 # via # matplotlib - # packaging # pydot # snuggs -pyproj==3.4.0 - # via geopandas +pyproj==3.5.0 + # via + # geopandas + # morecantile python-dateutil==2.8.2 # via # matplotlib # pandas -pytz==2022.4 +pytz==2023.3 # via pandas -pyyaml==6.0 +pyyaml==6.0.1 # via # -r requirements.in # fvcore # omegaconf # yacs -rasterio==1.3.2 +rasterio==1.3.8 # via # -r requirements.in # supermercado rdp==0.8 # via -r requirements.in -regex==2022.9.13 +regex==2023.8.8 # via black requests==2.31.0 # via @@ -207,16 +221,14 @@ requests-oauthlib==1.3.1 # via google-auth-oauthlib rsa==4.9 # via google-auth -rtree==1.0.0 +rtree==1.0.1 # via -r requirements.in -shapely==1.8.4 +shapely==2.0.1 # via geopandas six==1.16.0 # via # fiona # google-auth - # grpcio - # munch # python-dateutil snuggs==1.4.7 # via rasterio @@ -226,15 +238,13 @@ tabulate==0.9.0 # via # detectron2 # fvcore -tenacity==8.1.0 +tenacity==8.2.3 # via plotly -tensorboard==2.10.1 +tensorboard==2.14.0 # via detectron2 -tensorboard-data-server==0.6.1 - # via tensorboard -tensorboard-plugin-wit==1.8.1 +tensorboard-data-server==0.7.1 # via tensorboard -termcolor==2.0.1 +termcolor==2.3.0 # via # detectron2 # fvcore @@ -246,21 +256,29 @@ torch @ https://download.pytorch.org/whl/cu113/torch-1.10.2%2Bcu113-cp38-cp38-li # torchvision torchvision @ https://download.pytorch.org/whl/cu113/torchvision-0.11.3%2Bcu113-cp38-cp38-linux_x86_64.whl # via -r requirements.in -tqdm==4.64.1 +tqdm==4.66.1 # via # -r requirements.in # detectron2 # fvcore # iopath -typing-extensions==4.4.0 - # via torch -urllib3==1.26.12 - # via requests -werkzeug==2.3.4 +typing-extensions==4.7.1 + # via + # annotated-types + # pydantic + # pydantic-core + # torch +tzdata==2023.3 + # via pandas +urllib3==1.26.16 + # via + # google-auth + # requests +werkzeug==2.3.7 # via # -r requirements.in # tensorboard -wheel==0.40.0 +wheel==0.41.2 # via # -r requirements.in # tensorboard @@ -268,7 +286,7 @@ yacs==0.1.8 # via # detectron2 # fvcore -zipp==3.9.0 +zipp==3.16.2 # via # importlib-metadata # importlib-resources From 13c13d7ecb9b8fa4a1c1388be22c189ae7bfd0e7 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Thu, 31 Aug 2023 09:24:03 +0000 Subject: [PATCH 044/108] Add input data (label shp and AOI 2021 shp --- .../data/AOI/swissimage_footprint_2021.dbf | Bin 0 -> 2129 bytes .../data/AOI/swissimage_footprint_2021.prj | 1 + .../data/AOI/swissimage_footprint_2021.shp | Bin 0 -> 19068 bytes .../data/AOI/swissimage_footprint_2021.shx | Bin 0 -> 108 bytes .../data/labels/tlm-hr-trn-topo.dbf | Bin 0 -> 2992 bytes .../data/labels/tlm-hr-trn-topo.prj | 1 + .../data/labels/tlm-hr-trn-topo.shp | Bin 0 -> 651796 bytes .../data/labels/tlm-hr-trn-topo.shx | Bin 0 -> 2228 bytes 8 files changed, 2 insertions(+) create mode 100755 examples/quarry-detection/data/AOI/swissimage_footprint_2021.dbf create mode 100755 examples/quarry-detection/data/AOI/swissimage_footprint_2021.prj create mode 100755 examples/quarry-detection/data/AOI/swissimage_footprint_2021.shp create mode 100755 examples/quarry-detection/data/AOI/swissimage_footprint_2021.shx create mode 100755 examples/quarry-detection/data/labels/tlm-hr-trn-topo.dbf create mode 100755 examples/quarry-detection/data/labels/tlm-hr-trn-topo.prj create mode 100755 examples/quarry-detection/data/labels/tlm-hr-trn-topo.shp create mode 100755 examples/quarry-detection/data/labels/tlm-hr-trn-topo.shx diff --git a/examples/quarry-detection/data/AOI/swissimage_footprint_2021.dbf b/examples/quarry-detection/data/AOI/swissimage_footprint_2021.dbf new file mode 100755 index 0000000000000000000000000000000000000000..f3fb296081734105531f917596e91e25dd23c3af GIT binary patch literal 2129 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 literal 0 HcmV?d00001 diff --git a/examples/quarry-detection/data/AOI/swissimage_footprint_2021.shx b/examples/quarry-detection/data/AOI/swissimage_footprint_2021.shx new file mode 100755 index 0000000000000000000000000000000000000000..39eee76c9ffbe5ce3965de9cf4c27efbc3a5e602 GIT binary patch literal 108 zcmZQzQ0HR64$NLKGcd3M9o literal 0 HcmV?d00001 diff --git a/examples/quarry-detection/data/labels/tlm-hr-trn-topo.prj b/examples/quarry-detection/data/labels/tlm-hr-trn-topo.prj new file mode 100755 index 0000000..55e14b1 --- /dev/null +++ b/examples/quarry-detection/data/labels/tlm-hr-trn-topo.prj @@ -0,0 +1 @@ +PROJCS["CH1903+_LV95",GEOGCS["GCS_CH1903+",DATUM["D_CH1903+",SPHEROID["Bessel_1841",6377397.155,299.1528128]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Hotine_Oblique_Mercator_Azimuth_Center"],PARAMETER["False_Easting",2600000.0],PARAMETER["False_Northing",1200000.0],PARAMETER["Scale_Factor",1.0],PARAMETER["Azimuth",90.0],PARAMETER["Longitude_Of_Center",7.43958333333333],PARAMETER["Latitude_Of_Center",46.9524055555556],UNIT["Meter",1.0]] \ No newline at end of file diff --git a/examples/quarry-detection/data/labels/tlm-hr-trn-topo.shp b/examples/quarry-detection/data/labels/tlm-hr-trn-topo.shp new file mode 100755 index 0000000000000000000000000000000000000000..5923290c5e14686d84d866588f28b3fd674e1331 GIT binary patch literal 651796 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


*P%WpF8XcL@1xv{{+;GK*zc&0Azc6NF4%MA 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�nB{k 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

dQwk>pfRGz}Qd89dBI!csq}0 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%0A@MKJGk_FD{N5C6IpX8*!UpP%@|o?l?> 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 literal 0 HcmV?d00001 diff --git a/examples/quarry-detection/data/labels/tlm-hr-trn-topo.shx b/examples/quarry-detection/data/labels/tlm-hr-trn-topo.shx new file mode 100755 index 0000000000000000000000000000000000000000..53c8d4cd111fed8625f7c475571c3ca080e62a30 GIT binary patch literal 2228 zcmZvedrX#P7{;&Xzy~3sp*Ut`4v9=niHywDM2Xa#nGuwkaz zv^FPoBiEMvz|8}pvFXD~*UrrOA|<`F{(9l`*vHc!+H^Gj-8~Pc|Jyz~`t+89l1-!k z_5Wj2CGYtiE=5k2x@?qeu@u{f*ib1h8^1-0A0^$EBlW3}Oo7xlj`&e2@dqh%LAtLI zJ6THUExA+DgV(4#E5!KSjO#F7~G4R|fY0NR^GG{`Fc`Kz!>xd7Mp03C4 zm!4h5+`iI`D(00+FT&I8k#cW<7bE4<7qLrvwJzLO@GEs!q}So)nxr>s$s>31PVl!& zOLN0~mZwS4qovhF_{7%^2+v=?oBk2f2jGW#O69TmDN@B4cEcWOs59{07T~l?pG}i) zPmt;sQNKiL=m_&@qAmiQujbHSEgkO_=5z9gFn)6)KKj1Jcj)^r(2uU~>%nc1E|xH_ zT>1?hnJmyu(^e4`K%|Q4@B*>&B z;%CXEvPZsy83wNi=8kNn|D4R2U~Y~~CiMpYiB|G4G81pmH&^DVJp2@ytTO6fl6f96 z=rGfX+i@~4>?D3%Ciflc(D6!RxNh#?aC|=XoCmXjJw$+4NIcM8RF6;UZR!I)E2d!! zWlGRz56G-$p4lL?7QT^{G8>6^A-{PFd>6`md>H)2GBp#Z+b*-ML+aWtv-cWx=sp+= zUXsk=H0s#Xv0LD_$b8ev-GRr2Y|g_DnV+KR2fwWr+{-eTE2wXixmpsgZ(qR2qwab# z`I|C-BvL6?jL|KaV+Pv0T9vaCblf{(MijXgGWm@VQ= z<1*qca&7R{DY>7=;FJ0V-CfXkqX?%-t^+^V=U-*$+|N-Dcqhq+g8mr!sCE1ucu794 zgMGEj_r8X{;~e2e@+tCth=-cw@4=70Q+`M*cs=D)ZxPRte}w%+R?4R{oOi{K4f=M; zXTC*UhWz-zN79qQo(knBWpy6FgN-cob9{Dqa!3F=~S>omLtwsDNg3jyp H@X7cGg-x6S literal 0 HcmV?d00001 From 1d450223ff2b8f9adc99910f1a432c9feed92f7f Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Thu, 31 Aug 2023 13:14:30 +0000 Subject: [PATCH 045/108] Running version of the quarry example with packages in requirements.txt --- .gitignore | 1 + examples/quarry-detection/config-prd.yaml | 34 +- examples/quarry-detection/config-trne.yaml | 19 +- .../quarry-detection/data/AOI/AOI_2020.cpg | 1 + .../quarry-detection/data/AOI/AOI_2020.dbf | Bin 0 -> 77 bytes ...simage_footprint_2021.prj => AOI_2020.prj} | 0 .../quarry-detection/data/AOI/AOI_2020.shp | Bin 0 -> 236 bytes .../quarry-detection/data/AOI/AOI_2020.shx | Bin 0 -> 108 bytes .../data/AOI/swissimage_footprint_2021.dbf | Bin 2129 -> 0 bytes .../data/AOI/swissimage_footprint_2021.shp | Bin 19068 -> 0 bytes .../data/AOI/swissimage_footprint_2021.shx | Bin 108 -> 0 bytes .../detectron2_config_dqry.yaml | 10 +- requirements.in | 3 +- requirements.txt | 358 +++++++++++------- 14 files changed, 261 insertions(+), 165 deletions(-) create mode 100644 examples/quarry-detection/data/AOI/AOI_2020.cpg create mode 100644 examples/quarry-detection/data/AOI/AOI_2020.dbf rename examples/quarry-detection/data/AOI/{swissimage_footprint_2021.prj => AOI_2020.prj} (100%) mode change 100755 => 100644 create mode 100644 examples/quarry-detection/data/AOI/AOI_2020.shp create mode 100644 examples/quarry-detection/data/AOI/AOI_2020.shx delete mode 100755 examples/quarry-detection/data/AOI/swissimage_footprint_2021.dbf delete mode 100755 examples/quarry-detection/data/AOI/swissimage_footprint_2021.shp delete mode 100755 examples/quarry-detection/data/AOI/swissimage_footprint_2021.shx diff --git a/.gitignore b/.gitignore index 8fa2469..1951d82 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ output* +DEM* cache # Byte-compiled / optimized / DLL files diff --git a/examples/quarry-detection/config-prd.yaml b/examples/quarry-detection/config-prd.yaml index eb8764e..02edd8b 100644 --- a/examples/quarry-detection/config-prd.yaml +++ b/examples/quarry-detection/config-prd.yaml @@ -1,23 +1,23 @@ ################################### ####### Inference detection ####### -# Automatic detection of mineral extraction sites in images +# Automatic detection of Quarries and Mineral Extraction Sites (MES) in images -# 1-Prepare the tiles geometry according to the AOI and zoom level +# 1-Produce tiles geometry according to the AOI extent and zoom level prepare_data.py: srs: "EPSG:2056" # Projection of the input file datasets: - labels_shapefile: ./input/input-prd/swissimage_footprint_2021.shp + labels_shapefile: ./data/AOI/AOI_2020.shp output_folder: ./output/output-prd - zoom_level: 16 #z, keep between 15 and 18 + zoom_level: 16 -# 2-Request tiles according to the provided AOI and tiles parameters +# 2-Fetch of tiles (online server) and split into 3 datasets: train, test, validation generate_tilesets.py: - debug_mode: False #reduced amount of tiles + debug_mode: False # sample of tiles datasets: aoi_tiles_geojson: ./output/output-prd/tiles.geojson orthophotos_web_service: type: XYZ # supported values: 1. MIL = Map Image Layer 2. WMS 3. XYZ - url: https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swissimage-product/default/2019/3857/{z}/{x}/{y}.jpeg + url: https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swissimage-product/default/2020/3857/{z}/{x}/{y}.jpeg output_folder: ./output/output-prd tile_size: 256 # per side, in pixels overwrite: True @@ -25,7 +25,7 @@ generate_tilesets.py: COCO_metadata: year: 2021 version: 1.0 - description: Swiss Image Hinterground w/ Quarry and exploitation site detection + description: Swiss Image Hinterground w/ Quarries and Mineral Exploitation Sites detection contributor: swisstopo url: https://swisstopo.ch license: @@ -42,9 +42,9 @@ make_predictions.py: sample_tagged_img_subfolder: sample_tagged_images COCO_files: # relative paths, w/ 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_dqry.yaml' # path relative to the working_folder model_weights: - pth_file: '..output/output-trne/logs/model_0002999.pth' #!!!Chose the optimized trained model, i.e. the one minimizing the validation loss curve + pth_file: '../output-trne/logs/model_0002999.pth' # trained model minimizing the validation loss curve, monitor the training process via tensorboard (tensorboard --logdir ) image_metadata_json: './output/output-prd/img_metadata.json' rdp_simplification: # rdp = Ramer-Douglas-Peucker enabled: True @@ -53,12 +53,12 @@ make_predictions.py: # 4-Filtering and merging prediction polygons to improve results prediction_filter.py: - year: 2021 + year: 2020 input: ./output/output-prd/oth_predictions_at_0dot3_threshold.gpkg - labels_shapefile: ./input/input-prd/swissimage_footprint_2019.shp - dem: ./input/input-prd/switzerland_dem_EPSG2056.tif - elevation: 1200.0 #m, altitude threshold. Dectection above the threshold are discarded. Unlikely to observe quarry above and avoid flase detection of rock outcrop and snow . Default: 1200 - score: 0.95 #prediction score (reliability of the detection) provided by detectron2. The value can be varied from 0 to 1. Default: 0.95 - distance: 10 #m, distance use as a buffer to merge close polygons (likely to belong to the same quarry) together. Default: 10 - area: 5000.0 #m2, area threshold under which polygons are discarded (unlikely to observe quarry sites under this surface). Default: 5000.0 + labels_shapefile: ./data/AOI/AOI_2020.shp + dem: ./data/DEM/switzerland_dem_EPSG2056.tif + elevation: 1200.0 # m, altitude threshold + score: 0.95 # prediction 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/output-prd/oth_prediction_at_0dot3_threshold_year-{year}_score-{score}_area-{area}_elevation-{elevation}_distance-{distance}.geojson \ No newline at end of file diff --git a/examples/quarry-detection/config-trne.yaml b/examples/quarry-detection/config-trne.yaml index 9aca38b..e4bac49 100644 --- a/examples/quarry-detection/config-trne.yaml +++ b/examples/quarry-detection/config-trne.yaml @@ -1,18 +1,18 @@ ############################################# ####### Model training and evaluation ####### -# Training of automatic detection of mineral extraction sites in images with a provided ground truth +# Training of automatic detection of Quarries and Mineral Extraction Sites (MES) in images with a provided ground truth -# 1-Prepare the tiles geometry according to the AOI and zoom level +# 1-Produce tiles geometry according to the AOI extent and zoom level prepare_data.py: srs: "EPSG:2056" datasets: labels_shapefile: ./data/labels/tlm-hr-trn-topo.shp output_folder: ./output/output-trne - zoom_level: 16 # z, keep between 15 and 18 + zoom_level: 16 -# 2-Request tiles according to the provided AOI and tiles parameters and split tiles into 3 datasets: train, test, validation +# 2-Fetch of tiles (online server) and split into 3 datasets: train, test, validation generate_tilesets.py: - debug_mode: False + debug_mode: False # sample of tiles datasets: aoi_tiles_geojson: ./output/output-trne/tiles.geojson ground_truth_labels_geojson: ./output/output-trne/labels.geojson @@ -26,7 +26,7 @@ generate_tilesets.py: COCO_metadata: year: 2021 version: 1.0 - description: Swiss Image Hinterground w/ Quarry and exploitation site detection + description: Swiss Image Hinterground w/ Quarries and Mineral Exploitation Sites detection contributor: swisstopo url: https://swisstopo.ch license: @@ -37,7 +37,6 @@ generate_tilesets.py: supercategory: "Land usage" # 3-Train the model with the detectron2 algorithm -# Monitor the training process via tensorboard (tensorboard --logdir ). Choice of the optimized model: minimisation of the validation loss curve train_model.py: working_folder: ./output/output-trne log_subfolder: logs @@ -46,7 +45,7 @@ train_model.py: trn: COCO_trn.json val: COCO_val.json tst: COCO_tst.json - detectron2_config_file: 'detectron2_config_dqry.yaml' # path relative to the working_folder + detectron2_config_file: '../../detectron2_config_dqry.yaml' # path relative to the working_folder model_weights: model_zoo_checkpoint_url: "COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x.yaml" @@ -59,9 +58,9 @@ make_predictions.py: trn: COCO_trn.json val: COCO_val.json tst: COCO_tst.json - detectron2_config_file: 'detectron2_config_dqry.yaml' # path relative to the working_folder + detectron2_config_file: '../../detectron2_config_dqry.yaml' # path relative to the working_folder model_weights: - pth_file: './logs/model_0002999.pth' + pth_file: './logs/model_0002999.pth' # trained model minimizing the validation loss curve, monitor the training process via tensorboard (tensorboard --logdir ) image_metadata_json: './output/output-trne/img_metadata.json' rdp_simplification: # rdp = Ramer-Douglas-Peucker enabled: true diff --git a/examples/quarry-detection/data/AOI/AOI_2020.cpg b/examples/quarry-detection/data/AOI/AOI_2020.cpg new file mode 100644 index 0000000..3ad133c --- /dev/null +++ b/examples/quarry-detection/data/AOI/AOI_2020.cpg @@ -0,0 +1 @@ +UTF-8 \ No newline at end of file diff --git a/examples/quarry-detection/data/AOI/AOI_2020.dbf b/examples/quarry-detection/data/AOI/AOI_2020.dbf new file mode 100644 index 0000000000000000000000000000000000000000..56a141c4a42308d4c7c1f1b451ca86a07802fb56 GIT binary patch literal 77 mcmZRs=8$J(U|?`$;0BVIATtFn<_BVN!MP9yuL2wxN&x_WT?4QH literal 0 HcmV?d00001 diff --git a/examples/quarry-detection/data/AOI/swissimage_footprint_2021.prj b/examples/quarry-detection/data/AOI/AOI_2020.prj old mode 100755 new mode 100644 similarity index 100% rename from examples/quarry-detection/data/AOI/swissimage_footprint_2021.prj rename to examples/quarry-detection/data/AOI/AOI_2020.prj diff --git a/examples/quarry-detection/data/AOI/AOI_2020.shp b/examples/quarry-detection/data/AOI/AOI_2020.shp new file mode 100644 index 0000000000000000000000000000000000000000..5c98184a979a068e761b0feb50253f7f82b62228 GIT binary patch literal 236 zcmZQzQ0HR64$59IGcd3M<>Y?4ACmEOc1(PzaesSmqOasJ3H1*neQ24ZRChu zJBlbHkmo>*Ss;ZVbszw<*NM}Af=8F3Y?4ACmEOc1(PzaesSmqOasJ3H1*neQ24ZRChu LJBp|gkmmpZolXz~ literal 0 HcmV?d00001 diff --git a/examples/quarry-detection/data/AOI/swissimage_footprint_2021.dbf b/examples/quarry-detection/data/AOI/swissimage_footprint_2021.dbf deleted file mode 100755 index f3fb296081734105531f917596e91e25dd23c3af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2129 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 diff --git a/examples/quarry-detection/data/AOI/swissimage_footprint_2021.shx b/examples/quarry-detection/data/AOI/swissimage_footprint_2021.shx deleted file mode 100755 index 39eee76c9ffbe5ce3965de9cf4c27efbc3a5e602..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 108 zcmZQzQ0HR64$NLKGcd3M=3.2.2 opencv-python -pillow>=9.3.0 +pillow==9.3.0 plotly pyyaml rasterio rdp requests>=2.31.0 rtree +scikit-learn supermercado tqdm # cf. https://pytorch.org/get-started/locally/ diff --git a/requirements.txt b/requirements.txt index 2b3a854..d681924 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,39 +4,51 @@ # # pip-compile requirements.in # -absl-py==1.4.0 - # via tensorboard -affine==2.4.0 +absl-py==1.2.0 + # via + # stdl-object-detector + # tensorboard +affine==2.3.1 # via # rasterio + # stdl-object-detector # supermercado -annotated-types==0.5.0 - # via pydantic antlr4-python3-runtime==4.9.3 # via # hydra-core # omegaconf + # stdl-object-detector appdirs==1.4.4 - # via black -attrs==23.1.0 + # via + # black + # stdl-object-detector +attrs==22.1.0 # via # fiona # morecantile # rasterio + # stdl-object-detector black==21.4b2 - # via detectron2 -cachetools==5.3.1 - # via google-auth -certifi==2023.7.22 # via - # -r requirements.in + # detectron2 + # stdl-object-detector +cachetools==5.2.0 + # via + # google-auth + # morecantile + # stdl-object-detector +certifi==2023.5.7 + # via # fiona # pyproj # rasterio # requests -charset-normalizer==3.2.0 - # via requests -click==8.1.7 + # stdl-object-detector +charset-normalizer==2.1.1 + # via + # requests + # stdl-object-detector +click==8.1.3 # via # black # click-plugins @@ -44,84 +56,124 @@ click==8.1.7 # fiona # mercantile # rasterio + # stdl-object-detector # supermercado click-plugins==1.1.1 # via # fiona # rasterio + # stdl-object-detector # supermercado cligj==0.7.2 # via # fiona # rasterio + # stdl-object-detector # supermercado -cloudpickle==2.2.1 - # via detectron2 -contourpy==1.1.0 - # via matplotlib +cloudpickle==2.2.0 + # via + # detectron2 + # stdl-object-detector +contourpy==1.0.5 + # via + # matplotlib + # stdl-object-detector cycler==0.11.0 - # via matplotlib + # via + # matplotlib + # stdl-object-detector detectron2 @ https://dl.fbaipublicfiles.com/detectron2/wheels/cu113/torch1.10/detectron2-0.6%2Bcu113-cp38-cp38-linux_x86_64.whl - # via -r requirements.in -fiona==1.9.4.post1 - # via geopandas -fonttools==4.42.1 - # via matplotlib + # via stdl-object-detector +fiona==1.8.21 + # via + # geopandas + # stdl-object-detector +fonttools==4.37.4 + # via + # matplotlib + # stdl-object-detector future==0.18.3 # via - # -r requirements.in # detectron2 -fvcore==0.1.5.post20221221 - # via detectron2 + # stdl-object-detector +fvcore==0.1.5.post20220512 + # via + # detectron2 + # stdl-object-detector gdal==3.0.4 - # via -r requirements.in -geopandas==0.13.2 - # via -r requirements.in -google-auth==2.22.0 + # via stdl-object-detector +geopandas==0.11.1 + # via stdl-object-detector +google-auth==2.12.0 # via # google-auth-oauthlib + # stdl-object-detector + # tensorboard +google-auth-oauthlib==0.4.6 + # via + # stdl-object-detector + # tensorboard +grpcio==1.49.1 + # via + # stdl-object-detector # tensorboard -google-auth-oauthlib==1.0.0 - # via tensorboard -grpcio==1.57.0 - # via tensorboard -hydra-core==1.3.2 - # via detectron2 +hydra-core==1.2.0 + # via + # detectron2 + # stdl-object-detector idna==3.4 - # via requests -importlib-metadata==6.8.0 # via - # fiona + # requests + # stdl-object-detector +importlib-metadata==5.0.0 + # via # markdown -importlib-resources==6.0.1 + # stdl-object-detector +importlib-resources==5.10.0 # via # hydra-core - # matplotlib + # stdl-object-detector iopath==0.1.9 # via # detectron2 # fvcore -joblib==1.3.2 - # via -r requirements.in -kiwisolver==1.4.5 - # via matplotlib + # stdl-object-detector +joblib==1.2.0 + # via stdl-object-detector +kiwisolver==1.4.4 + # via + # matplotlib + # stdl-object-detector loguru==0.7.0 - # via -r requirements.in -markdown==3.4.4 - # via tensorboard -markupsafe==2.1.3 - # via werkzeug -matplotlib==3.7.2 + # via stdl-object-detector +markdown==3.4.1 + # via + # stdl-object-detector + # tensorboard +markupsafe==2.1.1 + # via + # stdl-object-detector + # werkzeug +matplotlib==3.6.1 # via # detectron2 # pycocotools + # stdl-object-detector mercantile==1.2.1 - # via supermercado -morecantile==5.0.0 + # via + # stdl-object-detector + # supermercado +morecantile==4.3.0 # via -r requirements.in -mypy-extensions==1.0.0 - # via black -numpy==1.24.4 +munch==2.5.0 + # via + # fiona + # stdl-object-detector +mypy-extensions==0.4.3 + # via + # black + # stdl-object-detector +numpy==1.23.3 # via # contourpy # fvcore @@ -131,165 +183,215 @@ numpy==1.24.4 # pycocotools # rasterio # rdp - # shapely # snuggs + # stdl-object-detector # supermercado # tensorboard # torchvision oauthlib==3.2.2 # via - # -r requirements.in # requests-oauthlib -omegaconf==2.3.0 + # stdl-object-detector +omegaconf==2.2.3 # via # detectron2 # hydra-core -opencv-python==4.8.0.76 - # via -r requirements.in -packaging==23.1 + # stdl-object-detector +opencv-python==4.6.0.66 + # via stdl-object-detector +packaging==21.3 # via # geopandas # hydra-core # matplotlib - # plotly -pandas==2.0.3 - # via geopandas -pathspec==0.11.2 - # via black -pillow==10.0.0 + # stdl-object-detector +pandas==1.5.0 + # via + # geopandas + # stdl-object-detector +pathspec==0.10.1 + # via + # black + # stdl-object-detector +pillow==9.5.0 # via - # -r requirements.in # detectron2 # fvcore # matplotlib + # stdl-object-detector # torchvision -plotly==5.16.1 - # via -r requirements.in -portalocker==2.7.0 - # via iopath -protobuf==4.24.2 - # via tensorboard -pyasn1==0.5.0 +plotly==5.10.0 + # via stdl-object-detector +portalocker==2.5.1 + # via + # iopath + # stdl-object-detector +protobuf==3.19.6 + # via + # stdl-object-detector + # tensorboard +pyasn1==0.4.8 # via # pyasn1-modules # rsa -pyasn1-modules==0.3.0 - # via google-auth -pycocotools==2.0.7 - # via detectron2 -pydantic==2.3.0 + # stdl-object-detector +pyasn1-modules==0.2.8 + # via + # google-auth + # stdl-object-detector +pycocotools==2.0.5 + # via + # detectron2 + # stdl-object-detector +pydantic==1.10.12 # via morecantile -pydantic-core==2.6.3 - # via pydantic pydot==1.4.2 - # via detectron2 + # via + # detectron2 + # stdl-object-detector pyparsing==3.0.9 # via # matplotlib + # packaging # pydot # snuggs -pyproj==3.5.0 + # stdl-object-detector +pyproj==3.4.0 # via # geopandas # morecantile + # stdl-object-detector python-dateutil==2.8.2 # via # matplotlib # pandas -pytz==2023.3 - # via pandas -pyyaml==6.0.1 + # stdl-object-detector +pytz==2022.4 + # via + # pandas + # stdl-object-detector +pyyaml==6.0 # via - # -r requirements.in # fvcore # omegaconf + # stdl-object-detector # yacs -rasterio==1.3.8 +rasterio==1.3.2 # via - # -r requirements.in + # stdl-object-detector # supermercado rdp==0.8 - # via -r requirements.in -regex==2023.8.8 - # via black + # via stdl-object-detector +regex==2022.9.13 + # via + # black + # stdl-object-detector requests==2.31.0 # via - # -r requirements.in # requests-oauthlib + # stdl-object-detector # tensorboard requests-oauthlib==1.3.1 - # via google-auth-oauthlib + # via + # google-auth-oauthlib + # stdl-object-detector rsa==4.9 - # via google-auth -rtree==1.0.1 + # via + # google-auth + # stdl-object-detector +rtree==1.0.0 + # via stdl-object-detector +scikit-learn==0.24.2 # via -r requirements.in -shapely==2.0.1 - # via geopandas +shapely==1.8.4 + # via + # geopandas + # stdl-object-detector six==1.16.0 # via # fiona # google-auth + # grpcio + # munch # python-dateutil + # stdl-object-detector snuggs==1.4.7 - # via rasterio -supermercado==0.2.0 + # via + # rasterio + # stdl-object-detector +stdl-object-detector @ git+https://github.com/swiss-territorial-data-lab/object-detector.git@ac/add-setuptools # via -r requirements.in +supermercado==0.2.0 + # via stdl-object-detector tabulate==0.9.0 # via # detectron2 # fvcore -tenacity==8.2.3 - # via plotly -tensorboard==2.14.0 - # via detectron2 -tensorboard-data-server==0.7.1 - # via tensorboard -termcolor==2.3.0 + # stdl-object-detector +tenacity==8.1.0 + # via + # plotly + # stdl-object-detector +tensorboard==2.10.1 + # via + # detectron2 + # stdl-object-detector +tensorboard-data-server==0.6.1 + # via + # stdl-object-detector + # tensorboard +tensorboard-plugin-wit==1.8.1 + # via + # stdl-object-detector + # tensorboard +termcolor==2.0.1 # via # detectron2 # fvcore + # stdl-object-detector toml==0.10.2 - # via black + # via + # black + # stdl-object-detector torch @ https://download.pytorch.org/whl/cu113/torch-1.10.2%2Bcu113-cp38-cp38-linux_x86_64.whl # via - # -r requirements.in + # stdl-object-detector # torchvision torchvision @ https://download.pytorch.org/whl/cu113/torchvision-0.11.3%2Bcu113-cp38-cp38-linux_x86_64.whl - # via -r requirements.in -tqdm==4.66.1 + # via stdl-object-detector +tqdm==4.64.1 # via - # -r requirements.in # detectron2 # fvcore # iopath -typing-extensions==4.7.1 + # stdl-object-detector +typing-extensions==4.4.0 # via - # annotated-types # pydantic - # pydantic-core + # stdl-object-detector # torch -tzdata==2023.3 - # via pandas -urllib3==1.26.16 +urllib3==1.26.12 # via - # google-auth # requests -werkzeug==2.3.7 + # stdl-object-detector +werkzeug==2.3.4 # via - # -r requirements.in + # stdl-object-detector # tensorboard -wheel==0.41.2 +wheel==0.40.0 # via - # -r requirements.in + # stdl-object-detector # tensorboard yacs==0.1.8 # via # detectron2 # fvcore -zipp==3.16.2 + # stdl-object-detector +zipp==3.9.0 # via # importlib-metadata # importlib-resources + # stdl-object-detector # The following packages are considered to be unsafe in a requirements file: # setuptools From f9537d2a41a88bb7798dfac14ff1fe38f77dcbe5 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Thu, 31 Aug 2023 15:09:21 +0000 Subject: [PATCH 046/108] Correct packages version in requirements.in --- examples/quarry-detection/prediction_filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/quarry-detection/prediction_filter.py b/examples/quarry-detection/prediction_filter.py index 3ae6151..3c297e5 100644 --- a/examples/quarry-detection/prediction_filter.py +++ b/examples/quarry-detection/prediction_filter.py @@ -75,7 +75,7 @@ input = input[input.elev != 0] te = len(input) - logger.info(f"{str(total - te)} predictions removed by elevation threshold: {str(ELEVATION)}") + logger.info(f"{str(total - te)} predictions removed by elevation threshold: {str(ELEVATION)} m") # Centroid of every prediction polygon centroids = gpd.GeoDataFrame() From 1776ba7c2c3922ca36367840268f2fc4a6b47b34 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Thu, 31 Aug 2023 15:10:57 +0000 Subject: [PATCH 047/108] Change name of prediction filter script --- examples/quarry-detection/config-prd.yaml | 2 +- .../{prediction_filter.py => filter_prediction.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename examples/quarry-detection/{prediction_filter.py => filter_prediction.py} (100%) diff --git a/examples/quarry-detection/config-prd.yaml b/examples/quarry-detection/config-prd.yaml index 02edd8b..eed3083 100644 --- a/examples/quarry-detection/config-prd.yaml +++ b/examples/quarry-detection/config-prd.yaml @@ -52,7 +52,7 @@ make_predictions.py: score_lower_threshold: 0.3 # 4-Filtering and merging prediction polygons to improve results -prediction_filter.py: +filter_prediction.py: year: 2020 input: ./output/output-prd/oth_predictions_at_0dot3_threshold.gpkg labels_shapefile: ./data/AOI/AOI_2020.shp diff --git a/examples/quarry-detection/prediction_filter.py b/examples/quarry-detection/filter_prediction.py similarity index 100% rename from examples/quarry-detection/prediction_filter.py rename to examples/quarry-detection/filter_prediction.py From e3e2f19cb1969f5eb44328bcbfb1af74fe20f1f8 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Thu, 31 Aug 2023 15:11:53 +0000 Subject: [PATCH 048/108] Update package version in requirements.in --- requirements.in | 5 +- requirements.txt | 358 ++++++++++++++++++----------------------------- 2 files changed, 136 insertions(+), 227 deletions(-) diff --git a/requirements.in b/requirements.in index bf6f9f5..4a1d3a9 100644 --- a/requirements.in +++ b/requirements.in @@ -5,13 +5,14 @@ GDAL==3.0.4 certifi>=2022.12.07 future>=0.18.3 -geopandas +geopandas==0.11.1 joblib loguru morecantile +numpy==1.23.3 oauthlib>=3.2.2 opencv-python -pillow==9.3.0 +pillow==9.5.0 plotly pyyaml rasterio diff --git a/requirements.txt b/requirements.txt index d681924..43a6e83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,51 +4,39 @@ # # pip-compile requirements.in # -absl-py==1.2.0 - # via - # stdl-object-detector - # tensorboard -affine==2.3.1 +absl-py==1.4.0 + # via tensorboard +affine==2.4.0 # via # rasterio - # stdl-object-detector # supermercado +annotated-types==0.5.0 + # via pydantic antlr4-python3-runtime==4.9.3 # via # hydra-core # omegaconf - # stdl-object-detector appdirs==1.4.4 - # via - # black - # stdl-object-detector -attrs==22.1.0 + # via black +attrs==23.1.0 # via # fiona # morecantile # rasterio - # stdl-object-detector black==21.4b2 + # via detectron2 +cachetools==5.3.1 + # via google-auth +certifi==2023.7.22 # via - # detectron2 - # stdl-object-detector -cachetools==5.2.0 - # via - # google-auth - # morecantile - # stdl-object-detector -certifi==2023.5.7 - # via + # -r requirements.in # fiona # pyproj # rasterio # requests - # stdl-object-detector -charset-normalizer==2.1.1 - # via - # requests - # stdl-object-detector -click==8.1.3 +charset-normalizer==3.2.0 + # via requests +click==8.1.7 # via # black # click-plugins @@ -56,125 +44,88 @@ click==8.1.3 # fiona # mercantile # rasterio - # stdl-object-detector # supermercado click-plugins==1.1.1 # via # fiona # rasterio - # stdl-object-detector # supermercado cligj==0.7.2 # via # fiona # rasterio - # stdl-object-detector # supermercado -cloudpickle==2.2.0 - # via - # detectron2 - # stdl-object-detector -contourpy==1.0.5 - # via - # matplotlib - # stdl-object-detector +cloudpickle==2.2.1 + # via detectron2 +contourpy==1.1.0 + # via matplotlib cycler==0.11.0 - # via - # matplotlib - # stdl-object-detector + # via matplotlib detectron2 @ https://dl.fbaipublicfiles.com/detectron2/wheels/cu113/torch1.10/detectron2-0.6%2Bcu113-cp38-cp38-linux_x86_64.whl - # via stdl-object-detector -fiona==1.8.21 - # via - # geopandas - # stdl-object-detector -fonttools==4.37.4 - # via - # matplotlib - # stdl-object-detector + # via -r requirements.in +fiona==1.9.4.post1 + # via geopandas +fonttools==4.42.1 + # via matplotlib future==0.18.3 # via + # -r requirements.in # detectron2 - # stdl-object-detector -fvcore==0.1.5.post20220512 - # via - # detectron2 - # stdl-object-detector +fvcore==0.1.5.post20221221 + # via detectron2 gdal==3.0.4 - # via stdl-object-detector + # via -r requirements.in geopandas==0.11.1 - # via stdl-object-detector -google-auth==2.12.0 + # via -r requirements.in +google-auth==2.22.0 # via # google-auth-oauthlib - # stdl-object-detector - # tensorboard -google-auth-oauthlib==0.4.6 - # via - # stdl-object-detector - # tensorboard -grpcio==1.49.1 - # via - # stdl-object-detector # tensorboard -hydra-core==1.2.0 - # via - # detectron2 - # stdl-object-detector +google-auth-oauthlib==1.0.0 + # via tensorboard +grpcio==1.57.0 + # via tensorboard +hydra-core==1.3.2 + # via detectron2 idna==3.4 + # via requests +importlib-metadata==6.8.0 # via - # requests - # stdl-object-detector -importlib-metadata==5.0.0 - # via + # fiona # markdown - # stdl-object-detector -importlib-resources==5.10.0 +importlib-resources==6.0.1 # via # hydra-core - # stdl-object-detector + # matplotlib iopath==0.1.9 # via # detectron2 # fvcore - # stdl-object-detector -joblib==1.2.0 - # via stdl-object-detector -kiwisolver==1.4.4 +joblib==1.3.2 # via - # matplotlib - # stdl-object-detector + # -r requirements.in + # scikit-learn +kiwisolver==1.4.5 + # via matplotlib loguru==0.7.0 - # via stdl-object-detector -markdown==3.4.1 - # via - # stdl-object-detector - # tensorboard -markupsafe==2.1.1 - # via - # stdl-object-detector - # werkzeug -matplotlib==3.6.1 + # via -r requirements.in +markdown==3.4.4 + # via tensorboard +markupsafe==2.1.3 + # via werkzeug +matplotlib==3.7.2 # via # detectron2 # pycocotools - # stdl-object-detector mercantile==1.2.1 - # via - # stdl-object-detector - # supermercado -morecantile==4.3.0 + # via supermercado +morecantile==5.0.0 # via -r requirements.in -munch==2.5.0 - # via - # fiona - # stdl-object-detector -mypy-extensions==0.4.3 - # via - # black - # stdl-object-detector +mypy-extensions==1.0.0 + # via black numpy==1.23.3 # via + # -r requirements.in # contourpy # fvcore # matplotlib @@ -183,215 +134,172 @@ numpy==1.23.3 # pycocotools # rasterio # rdp + # scikit-learn + # scipy # snuggs - # stdl-object-detector # supermercado # tensorboard # torchvision oauthlib==3.2.2 # via + # -r requirements.in # requests-oauthlib - # stdl-object-detector -omegaconf==2.2.3 +omegaconf==2.3.0 # via # detectron2 # hydra-core - # stdl-object-detector -opencv-python==4.6.0.66 - # via stdl-object-detector -packaging==21.3 +opencv-python==4.8.0.76 + # via -r requirements.in +packaging==23.1 # via # geopandas # hydra-core # matplotlib - # stdl-object-detector -pandas==1.5.0 - # via - # geopandas - # stdl-object-detector -pathspec==0.10.1 - # via - # black - # stdl-object-detector + # plotly +pandas==2.0.3 + # via geopandas +pathspec==0.11.2 + # via black pillow==9.5.0 # via + # -r requirements.in # detectron2 # fvcore # matplotlib - # stdl-object-detector # torchvision -plotly==5.10.0 - # via stdl-object-detector -portalocker==2.5.1 - # via - # iopath - # stdl-object-detector -protobuf==3.19.6 - # via - # stdl-object-detector - # tensorboard -pyasn1==0.4.8 +plotly==5.16.1 + # via -r requirements.in +portalocker==2.7.0 + # via iopath +protobuf==4.24.2 + # via tensorboard +pyasn1==0.5.0 # via # pyasn1-modules # rsa - # stdl-object-detector -pyasn1-modules==0.2.8 - # via - # google-auth - # stdl-object-detector -pycocotools==2.0.5 - # via - # detectron2 - # stdl-object-detector -pydantic==1.10.12 +pyasn1-modules==0.3.0 + # via google-auth +pycocotools==2.0.7 + # via detectron2 +pydantic==2.3.0 # via morecantile +pydantic-core==2.6.3 + # via pydantic pydot==1.4.2 - # via - # detectron2 - # stdl-object-detector + # via detectron2 pyparsing==3.0.9 # via # matplotlib - # packaging # pydot # snuggs - # stdl-object-detector -pyproj==3.4.0 +pyproj==3.5.0 # via # geopandas # morecantile - # stdl-object-detector python-dateutil==2.8.2 # via # matplotlib # pandas - # stdl-object-detector -pytz==2022.4 - # via - # pandas - # stdl-object-detector -pyyaml==6.0 +pytz==2023.3 + # via pandas +pyyaml==6.0.1 # via + # -r requirements.in # fvcore # omegaconf - # stdl-object-detector # yacs -rasterio==1.3.2 +rasterio==1.3.8 # via - # stdl-object-detector + # -r requirements.in # supermercado rdp==0.8 - # via stdl-object-detector -regex==2022.9.13 - # via - # black - # stdl-object-detector + # via -r requirements.in +regex==2023.8.8 + # via black requests==2.31.0 # via + # -r requirements.in # requests-oauthlib - # stdl-object-detector # tensorboard requests-oauthlib==1.3.1 - # via - # google-auth-oauthlib - # stdl-object-detector + # via google-auth-oauthlib rsa==4.9 - # via - # google-auth - # stdl-object-detector -rtree==1.0.0 - # via stdl-object-detector + # via google-auth +rtree==1.0.1 + # via -r requirements.in scikit-learn==0.24.2 # via -r requirements.in -shapely==1.8.4 - # via - # geopandas - # stdl-object-detector +scipy==1.10.1 + # via scikit-learn +shapely==1.8.5.post1 + # via geopandas six==1.16.0 # via # fiona # google-auth - # grpcio - # munch # python-dateutil - # stdl-object-detector snuggs==1.4.7 - # via - # rasterio - # stdl-object-detector -stdl-object-detector @ git+https://github.com/swiss-territorial-data-lab/object-detector.git@ac/add-setuptools - # via -r requirements.in + # via rasterio supermercado==0.2.0 - # via stdl-object-detector + # via -r requirements.in tabulate==0.9.0 # via # detectron2 # fvcore - # stdl-object-detector -tenacity==8.1.0 - # via - # plotly - # stdl-object-detector -tensorboard==2.10.1 - # via - # detectron2 - # stdl-object-detector -tensorboard-data-server==0.6.1 - # via - # stdl-object-detector - # tensorboard -tensorboard-plugin-wit==1.8.1 - # via - # stdl-object-detector - # tensorboard -termcolor==2.0.1 +tenacity==8.2.3 + # via plotly +tensorboard==2.14.0 + # via detectron2 +tensorboard-data-server==0.7.1 + # via tensorboard +termcolor==2.3.0 # via # detectron2 # fvcore - # stdl-object-detector +threadpoolctl==3.2.0 + # via scikit-learn toml==0.10.2 - # via - # black - # stdl-object-detector + # via black torch @ https://download.pytorch.org/whl/cu113/torch-1.10.2%2Bcu113-cp38-cp38-linux_x86_64.whl # via - # stdl-object-detector + # -r requirements.in # torchvision torchvision @ https://download.pytorch.org/whl/cu113/torchvision-0.11.3%2Bcu113-cp38-cp38-linux_x86_64.whl - # via stdl-object-detector -tqdm==4.64.1 + # via -r requirements.in +tqdm==4.66.1 # via + # -r requirements.in # detectron2 # fvcore # iopath - # stdl-object-detector -typing-extensions==4.4.0 +typing-extensions==4.7.1 # via + # annotated-types # pydantic - # stdl-object-detector + # pydantic-core # torch -urllib3==1.26.12 +tzdata==2023.3 + # via pandas +urllib3==1.26.16 # via + # google-auth # requests - # stdl-object-detector -werkzeug==2.3.4 +werkzeug==2.3.7 # via - # stdl-object-detector + # -r requirements.in # tensorboard -wheel==0.40.0 +wheel==0.41.2 # via - # stdl-object-detector + # -r requirements.in # tensorboard yacs==0.1.8 # via # detectron2 # fvcore - # stdl-object-detector -zipp==3.9.0 +zipp==3.16.2 # via # importlib-metadata # importlib-resources - # stdl-object-detector # The following packages are considered to be unsafe in a requirements file: # setuptools From 30c4ea7d812b5d75a85fa6affaf92c4dc3e4ca65 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Thu, 31 Aug 2023 15:43:34 +0000 Subject: [PATCH 049/108] Add README.md file --- examples/quarry-detection/README.md | 41 +++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 examples/quarry-detection/README.md diff --git a/examples/quarry-detection/README.md b/examples/quarry-detection/README.md new file mode 100644 index 0000000..144d2e5 --- /dev/null +++ b/examples/quarry-detection/README.md @@ -0,0 +1,41 @@ +# Example: detection of quarries + +A sample working setup is provided here, enabling the end-user to detect quarries and mineral extraction sites in Switzerland over several years.
+It consists of the following elements: + +- the read-to-use configuration files: + - `config_trne.yaml`, + - `config_prd.yaml`, + - `detectron2_config_dqry.yaml`, +- the input data in the `data` subfolder: + - quarries shapefile from the product [swissTLM3D](https://www.swisstopo.admin.ch/fr/geodata/landscape/tlm3d.html), revised and synchronised with the 2020 [SWISSIMAGE](https://www.swisstopo.admin.ch/fr/geodata/images/ortho/swissimage10.html) mosaic (**label**), + - the delimitation of the AOI to perform inference predictions (**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 - WGS 84 coordinate reference system. The raster must be first reprojected to EPSG:2056 - CH1903+ / LV95, named `switzerland_dem_EPSG2056.tif`and located in the **DEM** subfolder. +- a data preparation script (`prepare_data.py`) producing the files to be used as input to the `generate_tilesets.py`script. +- a results post-processing script (`filter_prediction.py`) filtering the predictions, produced from `make_prediction.py`script, to the final shapefile + +After creating and a new environment in python 3.8, the end-to-end workflow can be run by issuing the following list of commands, straight from this folder: + +```bash +$ sudo apt-get install -y python3-gdal gdal-bin libgdal-dev gcc g++ python3.8-dev +$ pip install -r ../../requirements.txt +$ python3 prepare_data.py config_trne.yaml +$ python3 ../../scripts/generate_tilesets.py config_trne.yaml +$ python3 ../../scripts/train_model.py config_trne.yaml +$ python3 ../../scripts/make_predictions.py config_trne.yaml +$ python3 ../../scripts/assess_predictions.py config_trne.yaml +$ python3 prepare_data.py config_prd.yaml +$ python3 ../../scripts/generate_tilesets.py config_prd.yaml +$ python3 ../../scripts/make_predictions.py config_prd.yaml +$ python3 filter_detection.py config_prd.yaml +``` + +We strongly encourage the end-user to review the provided `config_trne.yaml` and `config_prd.yaml` files as well as the various output files, a list of which is printed by each script before exiting. + +The model is trained on the 2020 [SWISSIMAGE](https://www.swisstopo.admin.ch/fr/geodata/images/ortho/swissimage10.html) mosaic. Inference can be performed on SWISSIMAGE mosaics of the product SWISSIMAGE time travel by changing the year in `config_prd.yaml`. It should be noted that the model has been trained on RGB color images and might not perform as well on Black and White images. + +For more information about this project, you can consult [the associated repository](https://github.com/swiss-territorial-data-lab/proj-dqry) (not public yet). + +## Disclaimer + +Depending on the end purpose, we strongly recommend users not to take for granted the detections obtained through this code. Indeed, results can exhibit false positives and false negatives, as is the case in all Machine Learning-based approaches. \ No newline at end of file From a0d1105fe21c7e0f994adc9cd6db211c9e6c174d Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Thu, 31 Aug 2023 15:44:24 +0000 Subject: [PATCH 050/108] Rename config files --- examples/quarry-detection/{config-prd.yaml => config_prd.yaml} | 0 examples/quarry-detection/{config-trne.yaml => config_trne.yaml} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename examples/quarry-detection/{config-prd.yaml => config_prd.yaml} (100%) rename examples/quarry-detection/{config-trne.yaml => config_trne.yaml} (100%) diff --git a/examples/quarry-detection/config-prd.yaml b/examples/quarry-detection/config_prd.yaml similarity index 100% rename from examples/quarry-detection/config-prd.yaml rename to examples/quarry-detection/config_prd.yaml diff --git a/examples/quarry-detection/config-trne.yaml b/examples/quarry-detection/config_trne.yaml similarity index 100% rename from examples/quarry-detection/config-trne.yaml rename to examples/quarry-detection/config_trne.yaml From 5f074bc7b0645c2ae457fd680a7ae787c8cd4c79 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Thu, 31 Aug 2023 16:03:14 +0000 Subject: [PATCH 051/108] Fix scikit-learn version --- requirements.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.in b/requirements.in index 4a1d3a9..b32d616 100644 --- a/requirements.in +++ b/requirements.in @@ -19,7 +19,7 @@ rasterio rdp requests>=2.31.0 rtree -scikit-learn +scikit-learn==0.24.2 supermercado tqdm # cf. https://pytorch.org/get-started/locally/ From e435fc556ee3208accdd4fcc5a0d92bb3bd1fbb1 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Fri, 8 Sep 2023 09:35:18 +0000 Subject: [PATCH 052/108] Remove logging.conf file (deprecated) --- examples/quarry-detection/logging.conf | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 examples/quarry-detection/logging.conf diff --git a/examples/quarry-detection/logging.conf b/examples/quarry-detection/logging.conf deleted file mode 100644 index 9e26be7..0000000 --- a/examples/quarry-detection/logging.conf +++ /dev/null @@ -1,23 +0,0 @@ -[loggers] -keys=root - -[handlers] -keys=consoleHandler - -[formatters] -keys=simpleFormatter - -[logger_root] -level=DEBUG -handlers=consoleHandler - - -[handler_consoleHandler] -class=StreamHandler -level=INFO -formatter=simpleFormatter -args=(sys.stdout,) - -[formatter_simpleFormatter] -format=%(asctime)s - %(name)s - %(levelname)s - %(message)s -datefmt= \ No newline at end of file From 47f1bfbb2c7d8c80c56ff2b04146ad81958187fc Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Fri, 8 Sep 2023 09:43:41 +0000 Subject: [PATCH 053/108] Generate tiles straight in EPSG:4326 --- examples/quarry-detection/prepare_data.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/examples/quarry-detection/prepare_data.py b/examples/quarry-detection/prepare_data.py index 7d1c09e..94c01de 100644 --- a/examples/quarry-detection/prepare_data.py +++ b/examples/quarry-detection/prepare_data.py @@ -79,21 +79,20 @@ # Iterate on geometric coordinates to defined tiles for a given label at a given zoom level # A gpd is created for each label and are then concatenate into a single gpd logger.info('- Compute tiles for each label(s) geometry') - tiles_3857_all = [] + tiles_4326_all = [] for row in range(len(boundary)): coords = (boundary.iloc[row,0],boundary.iloc[row,1],boundary.iloc[row,2],boundary.iloc[row,3]) - tiles_3857 = gpd.GeoDataFrame.from_features([tms.feature(x, projected=True) for x in tqdm(tms.tiles(*coords, zooms=[ZOOM_LEVEL]))]) - tiles_3857.set_crs(epsg=3857, inplace=True) - tiles_3857_all.append(tiles_3857) - tiles_3857_aoi = gpd.GeoDataFrame(pd.concat(tiles_3857_all, ignore_index=True) ) + tiles_4326 = gpd.GeoDataFrame.from_features([tms.feature(x, projected=False) for x in tqdm(tms.tiles(*coords, zooms=[ZOOM_LEVEL]))]) + tiles_4326.set_crs(epsg=4326, inplace=True) + tiles_4326_all.append(tiles_4326) + tiles_4326_aoi = gpd.GeoDataFrame(pd.concat(tiles_4326_all, ignore_index=True)) # Remove unrelevant tiles and reorganized the data set: logger.info('- Remove duplicated tiles and tiles that are not intersecting labels') - # - Keep only tiles that are intersecting the label - labels_3857 = labels_4326.to_crs(epsg=3857) - labels_3857.rename(columns={'FID': 'id_aoi'},inplace=True) - tiles_aoi = gpd.sjoin(tiles_3857_aoi, labels_3857, how='inner') + # - Keep only tiles that are intersecting labels + labels_4326.rename(columns={'FID': 'id_aoi'}, inplace=True) + tiles_aoi = gpd.sjoin(tiles_4326_aoi, labels_4326, how='inner') # - Remove duplicated tiles if nb_labels > 1: From b9eb3ad117e16ad523b70b26840f27a6bcb27864 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Fri, 8 Sep 2023 10:01:37 +0000 Subject: [PATCH 054/108] Improve code style for iteration over label boundaries. Avoid useless tile reprojection --- examples/quarry-detection/prepare_data.py | 32 +++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/examples/quarry-detection/prepare_data.py b/examples/quarry-detection/prepare_data.py index 94c01de..7c16940 100644 --- a/examples/quarry-detection/prepare_data.py +++ b/examples/quarry-detection/prepare_data.py @@ -74,14 +74,15 @@ # New gpd with only labels geometric info (minx, miny, maxx, maxy) logger.info('- Get geometric boundaries of the label(s)') - boundary = labels_4326.bounds + label_boundaries_df = labels_4326.bounds # Iterate on geometric coordinates to defined tiles for a given label at a given zoom level # A gpd is created for each label and are then concatenate into a single gpd logger.info('- Compute tiles for each label(s) geometry') tiles_4326_all = [] - for row in range(len(boundary)): - coords = (boundary.iloc[row,0],boundary.iloc[row,1],boundary.iloc[row,2],boundary.iloc[row,3]) + + for label_boundary in label_boundaries_df.itertuples(): + coords = (label_boundary.minx, label_boundary.miny, label_boundary.maxx, label_boundary.maxy) tiles_4326 = gpd.GeoDataFrame.from_features([tms.feature(x, projected=False) for x in tqdm(tms.tiles(*coords, zooms=[ZOOM_LEVEL]))]) tiles_4326.set_crs(epsg=4326, inplace=True) tiles_4326_all.append(tiles_4326) @@ -90,34 +91,33 @@ # Remove unrelevant tiles and reorganized the data set: logger.info('- Remove duplicated tiles and tiles that are not intersecting labels') - # - Keep only tiles that are intersecting labels + # - Keep only tiles that are actually intersecting labels labels_4326.rename(columns={'FID': 'id_aoi'}, inplace=True) - tiles_aoi = gpd.sjoin(tiles_4326_aoi, labels_4326, how='inner') + tiles_4326 = gpd.sjoin(tiles_4326_aoi, labels_4326, how='inner') # - Remove duplicated tiles if nb_labels > 1: - tiles_aoi.drop_duplicates('title', inplace=True) + tiles_4326.drop_duplicates('title', inplace=True) - # - Remove useless columns, reinitilize feature id and redifined it according to xyz format + # - Remove useless columns, reinitilize feature id and redifine it according to xyz format logger.info('- Format feature id and reorganise data set') - tiles_aoi.drop(tiles_aoi.columns.difference(['geometry','id','title']), axis=1, inplace=True) - tiles_aoi.reset_index(drop=True, inplace=True) + tiles_4326.drop(tiles_4326.columns.difference(['geometry','id','title']), axis=1, inplace=True) + tiles_4326.reset_index(drop=True, inplace=True) # Format the xyz parameters and filled in the attributes columns xyz = [] - for idx in tiles_aoi.index: - xyz.append([re.sub('\D','',coor) for coor in tiles_aoi.loc[idx,'title'].split(',')]) - tiles_aoi['id'] = [f'({x}, {y}, {z})' for x, y, z in xyz] - tiles_aoi = tiles_aoi[['geometry', 'title', 'id']] + for idx in tiles_4326.index: + xyz.append([re.sub('\D','',coor) for coor in tiles_4326.loc[idx,'title'].split(',')]) + tiles_4326['id'] = [f'({x}, {y}, {z})' for x, y, z in xyz] + tiles_4326 = tiles_4326[['geometry', 'title', 'id']] - nb_tiles = len(tiles_aoi) + nb_tiles = len(tiles_4326) logger.info('There was/were ' + str(nb_tiles) + ' tiles(s) created') # Convert datasets shapefiles into geojson format logger.info('Convert tiles shapefile into GeoJSON format (EPSG:4326)...') feature = 'tiles.geojson' feature_path = os.path.join(OUTPUT_DIR, feature) - tiles_4326 = tiles_aoi.to_crs(epsg=4326) tiles_4326.to_file(feature_path, driver='GeoJSON') written_files.append(feature_path) logger.info(f"...done. A file was written: {feature_path}") @@ -131,5 +131,5 @@ # Stop chronometer toc = time.time() logger.info(f"Nothing left to be done: exiting. Elapsed time: {(toc-tic):.2f} seconds") - + sys.stderr.flush() \ No newline at end of file From 940b534505b0027f9e004e6d91fec7aa57790f27 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Fri, 8 Sep 2023 10:03:26 +0000 Subject: [PATCH 055/108] Improve code readability --- examples/quarry-detection/prepare_data.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/quarry-detection/prepare_data.py b/examples/quarry-detection/prepare_data.py index 7c16940..d472a4b 100644 --- a/examples/quarry-detection/prepare_data.py +++ b/examples/quarry-detection/prepare_data.py @@ -61,11 +61,11 @@ nb_labels = len(labels) logger.info('There is/are ' + str(nb_labels) + ' polygon(s) in ' + LABELS_SHPFILE) - feature = 'labels.geojson' - feature_path = os.path.join(OUTPUT_DIR, feature) - labels_4326.to_file(feature_path, driver='GeoJSON') - written_files.append(feature_path) - logger.info(f"...done. A file was written: {feature_path}") + label_filename = 'labels.geojson' + label_filepath = os.path.join(OUTPUT_DIR, label_filename) + labels_4326.to_file(label_filepath, driver='GeoJSON') + written_files.append(label_filepath) + logger.info(f"...done. A file was written: {label_filepath}") logger.info('Creating tiles for the Area of Interest (AOI)...') @@ -104,7 +104,7 @@ tiles_4326.drop(tiles_4326.columns.difference(['geometry','id','title']), axis=1, inplace=True) tiles_4326.reset_index(drop=True, inplace=True) - # Format the xyz parameters and filled in the attributes columns + # Format the xyz parameters and fill in the attributes columns xyz = [] for idx in tiles_4326.index: xyz.append([re.sub('\D','',coor) for coor in tiles_4326.loc[idx,'title'].split(',')]) @@ -116,11 +116,11 @@ # Convert datasets shapefiles into geojson format logger.info('Convert tiles shapefile into GeoJSON format (EPSG:4326)...') - feature = 'tiles.geojson' - feature_path = os.path.join(OUTPUT_DIR, feature) - tiles_4326.to_file(feature_path, driver='GeoJSON') - written_files.append(feature_path) - logger.info(f"...done. A file was written: {feature_path}") + tile_filename = 'tiles.geojson' + tile_filepath = os.path.join(OUTPUT_DIR, tile_filename) + tiles_4326.to_file(tile_filepath, driver='GeoJSON') + written_files.append(tile_filepath) + logger.info(f"...done. A file was written: {tile_filepath}") print() logger.info("The following files were written. Let's check them out!") From 46b3e9a976c36b05e4a03eca4a186fbc278f1bb4 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Fri, 8 Sep 2023 10:06:26 +0000 Subject: [PATCH 056/108] Fix some comments and screen outputs --- examples/quarry-detection/prepare_data.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/quarry-detection/prepare_data.py b/examples/quarry-detection/prepare_data.py index d472a4b..eca0d16 100644 --- a/examples/quarry-detection/prepare_data.py +++ b/examples/quarry-detection/prepare_data.py @@ -99,8 +99,8 @@ if nb_labels > 1: tiles_4326.drop_duplicates('title', inplace=True) - # - Remove useless columns, reinitilize feature id and redifine it according to xyz format - logger.info('- Format feature id and reorganise data set') + # - Remove useless columns, reset feature id and redefine it according to xyz format + logger.info('- Format feature id and reorganise data set') tiles_4326.drop(tiles_4326.columns.difference(['geometry','id','title']), axis=1, inplace=True) tiles_4326.reset_index(drop=True, inplace=True) @@ -115,7 +115,7 @@ logger.info('There was/were ' + str(nb_tiles) + ' tiles(s) created') # Convert datasets shapefiles into geojson format - logger.info('Convert tiles shapefile into GeoJSON format (EPSG:4326)...') + logger.info('Export tiles to GeoJSON (EPSG:4326)...') tile_filename = 'tiles.geojson' tile_filepath = os.path.join(OUTPUT_DIR, tile_filename) tiles_4326.to_file(tile_filepath, driver='GeoJSON') From 84c3cbb12b7c06c7ca1764d64aa6dc1551cebdd8 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Fri, 8 Sep 2023 13:14:51 +0000 Subject: [PATCH 057/108] Improve code generating tile IDs --- examples/quarry-detection/prepare_data.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/examples/quarry-detection/prepare_data.py b/examples/quarry-detection/prepare_data.py index eca0d16..10df10f 100644 --- a/examples/quarry-detection/prepare_data.py +++ b/examples/quarry-detection/prepare_data.py @@ -24,6 +24,13 @@ logger.remove() logger.add(sys.stderr, format="{time:YYYY-MM-DD HH:mm:ss} - {level} - {message}", level="INFO") +def add_tile_id(row): + + re_search = re.search('(?P\(x=\d*, y=\d*, z=\d*\))', row.title) + row['id'] = re_search.group('xyz') + + return row + if __name__ == "__main__": @@ -83,7 +90,7 @@ for label_boundary in label_boundaries_df.itertuples(): coords = (label_boundary.minx, label_boundary.miny, label_boundary.maxx, label_boundary.maxy) - tiles_4326 = gpd.GeoDataFrame.from_features([tms.feature(x, projected=False) for x in tqdm(tms.tiles(*coords, zooms=[ZOOM_LEVEL]))]) + tiles_4326 = gpd.GeoDataFrame.from_features([tms.feature(x, projected=False) for x in tms.tiles(*coords, zooms=[ZOOM_LEVEL])]) tiles_4326.set_crs(epsg=4326, inplace=True) tiles_4326_all.append(tiles_4326) tiles_4326_aoi = gpd.GeoDataFrame(pd.concat(tiles_4326_all, ignore_index=True)) @@ -101,20 +108,16 @@ # - Remove useless columns, reset feature id and redefine it according to xyz format logger.info('- Format feature id and reorganise data set') - tiles_4326.drop(tiles_4326.columns.difference(['geometry','id','title']), axis=1, inplace=True) + tiles_4326 = tiles_4326[['geometry', 'title']].copy() tiles_4326.reset_index(drop=True, inplace=True) - # Format the xyz parameters and fill in the attributes columns - xyz = [] - for idx in tiles_4326.index: - xyz.append([re.sub('\D','',coor) for coor in tiles_4326.loc[idx,'title'].split(',')]) - tiles_4326['id'] = [f'({x}, {y}, {z})' for x, y, z in xyz] - tiles_4326 = tiles_4326[['geometry', 'title', 'id']] - + # Add the ID column + tiles_4326 = tiles_4326.apply(add_tile_id, axis=1) + nb_tiles = len(tiles_4326) logger.info('There was/were ' + str(nb_tiles) + ' tiles(s) created') - # Convert datasets shapefiles into geojson format + # Export tiles to GeoJSON logger.info('Export tiles to GeoJSON (EPSG:4326)...') tile_filename = 'tiles.geojson' tile_filepath = os.path.join(OUTPUT_DIR, tile_filename) From 094f2176144c57b1abf4b23a707f47e5779f44a9 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Fri, 8 Sep 2023 13:17:05 +0000 Subject: [PATCH 058/108] Remove unused import (tqdm) --- examples/quarry-detection/prepare_data.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/quarry-detection/prepare_data.py b/examples/quarry-detection/prepare_data.py index 10df10f..db377fb 100644 --- a/examples/quarry-detection/prepare_data.py +++ b/examples/quarry-detection/prepare_data.py @@ -10,7 +10,6 @@ import argparse import yaml import re -from tqdm import tqdm from loguru import logger import geopandas as gpd @@ -107,7 +106,7 @@ def add_tile_id(row): tiles_4326.drop_duplicates('title', inplace=True) # - Remove useless columns, reset feature id and redefine it according to xyz format - logger.info('- Format feature id and reorganise data set') + logger.info('- Add tile IDs and reorganise data set') tiles_4326 = tiles_4326[['geometry', 'title']].copy() tiles_4326.reset_index(drop=True, inplace=True) From b7925456c7ededfff93d2d1174116ea563d25c2d Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Fri, 8 Sep 2023 13:22:02 +0000 Subject: [PATCH 059/108] Fix tile ID format --- examples/quarry-detection/prepare_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/quarry-detection/prepare_data.py b/examples/quarry-detection/prepare_data.py index db377fb..2846511 100644 --- a/examples/quarry-detection/prepare_data.py +++ b/examples/quarry-detection/prepare_data.py @@ -25,8 +25,8 @@ def add_tile_id(row): - re_search = re.search('(?P\(x=\d*, y=\d*, z=\d*\))', row.title) - row['id'] = re_search.group('xyz') + re_search = re.search('(x=(?P\d*), y=(?P\d*), z=(?P\d*))', row.title) + row['id'] = f"({re_search.group('x')}, {re_search.group('y')}, {re_search.group('z')})" return row From 9a08ad32110c4a72e371d12f16e28f226050b913 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Fri, 8 Sep 2023 13:42:23 +0000 Subject: [PATCH 060/108] Comment/delete unused code --- scripts/train_model.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/scripts/train_model.py b/scripts/train_model.py index 9e2ac37..960d257 100644 --- a/scripts/train_model.py +++ b/scripts/train_model.py @@ -49,10 +49,11 @@ def main(cfg_file_path): else: MODEL_ZOO_CHECKPOINT_URL = None - if 'pth_file' in cfg['model_weights'].keys(): - MODEL_PTH_FILE = cfg['model_weights']['pth_file'] - else: - MODEL_PTH_FILE = None + # TODO: allow resuming from previous training + # if 'pth_file' in cfg['model_weights'].keys(): + # MODEL_PTH_FILE = cfg['model_weights']['pth_file'] + # else: + # MODEL_PTH_FILE = None if MODEL_ZOO_CHECKPOINT_URL == None: logger.critical("A model zoo checkpoint URL (\"model_zoo_checkpoint_url\") must be provided") @@ -72,9 +73,9 @@ def main(cfg_file_path): os.chdir(WORKING_DIR) # let's make the output directories in case they don't exist - for DIR in [SAMPLE_TAGGED_IMG_SUBDIR, LOG_SUBDIR]: - if not os.path.exists(DIR): - os.makedirs(DIR) + for dir in [SAMPLE_TAGGED_IMG_SUBDIR, LOG_SUBDIR]: + if not os.path.exists(dir): + os.makedirs(dir) @@ -88,10 +89,6 @@ def main(cfg_file_path): registered_datasets = ['trn_dataset', 'val_dataset', 'tst_dataset'] - - registered_datasets_prefixes = [x.split('_')[0] for x in registered_datasets] - - for dataset in registered_datasets: for d in DatasetCatalog.get(dataset)[0:min(len(DatasetCatalog.get(dataset)), 4)]: From 80c1333de881a9c45233127e4fcd5554a8ced18c Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Fri, 8 Sep 2023 15:47:53 +0000 Subject: [PATCH 061/108] Add Dockerfile and docker-compose.yml --- Dockerfile | 22 ++++++++++++++++++++++ docker-compose.yml | 15 +++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..28bc4b7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM nvidia/cuda:11.3.1-runtime-ubuntu20.04 + +RUN apt update &&\ + apt upgrade -y &&\ + apt install -y libgl1 &&\ + apt install -y libglib2.0-0 &&\ + apt install -y gdal-bin &&\ + apt install -y python3-pip + +WORKDIR /app + +ADD requirements.txt . +RUN pip install -r requirements.txt --no-cache-dir + +ADD helpers/*.py helpers/ +ADD scripts/*.py scripts/ + +ADD setup.py . +RUN pip install . + +ENTRYPOINT [""] +CMD ["stdl-objdet", "-h"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c97a576 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +version: "3" + +services: + stdl-objdet: + build: . + volumes: + - ./examples:/app/examples + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [gpu] + command: /bin/bash \ No newline at end of file From 6de3b86b5798a9428da00399291ceb4c58eaccbf Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Mon, 11 Sep 2023 12:01:15 +0200 Subject: [PATCH 062/108] Update README.md for quarry example Replace relative path to objedet scripts by the function stdl-objdet --- examples/quarry-detection/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/quarry-detection/README.md b/examples/quarry-detection/README.md index 144d2e5..1ce192f 100644 --- a/examples/quarry-detection/README.md +++ b/examples/quarry-detection/README.md @@ -20,13 +20,13 @@ After creating and a new environment in python 3.8, the end-to-end workflow can $ sudo apt-get install -y python3-gdal gdal-bin libgdal-dev gcc g++ python3.8-dev $ pip install -r ../../requirements.txt $ python3 prepare_data.py config_trne.yaml -$ python3 ../../scripts/generate_tilesets.py config_trne.yaml -$ python3 ../../scripts/train_model.py config_trne.yaml -$ python3 ../../scripts/make_predictions.py config_trne.yaml -$ python3 ../../scripts/assess_predictions.py config_trne.yaml +$ stdl-objdet generate_tilesets config_trne.yaml +$ stdl-objdet train_model config_trne.yaml +$ stdl-objdet make_predictions config_trne.yaml +$ stdl-objdet assess_predictions config_trne.yaml $ python3 prepare_data.py config_prd.yaml -$ python3 ../../scripts/generate_tilesets.py config_prd.yaml -$ python3 ../../scripts/make_predictions.py config_prd.yaml +$ stdl-objdet generate_tilesets config_prd.yaml +$ stdl-objdet make_predictions config_prd.yaml $ python3 filter_detection.py config_prd.yaml ``` From 0451395f8fb910a72297b4ff4dc31c9f0b6f8c3e Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Mon, 11 Sep 2023 12:10:00 +0000 Subject: [PATCH 063/108] Remove requirements info from README of the quarry example --- examples/quarry-detection/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/quarry-detection/README.md b/examples/quarry-detection/README.md index 1ce192f..ff55a9a 100644 --- a/examples/quarry-detection/README.md +++ b/examples/quarry-detection/README.md @@ -14,11 +14,9 @@ It consists of the following elements: - a data preparation script (`prepare_data.py`) producing the files to be used as input to the `generate_tilesets.py`script. - a results post-processing script (`filter_prediction.py`) filtering the predictions, produced from `make_prediction.py`script, to the final shapefile -After creating and a new environment in python 3.8, the end-to-end workflow can be run by issuing the following list of commands, straight from this folder: +In the provided Docker container, the end-to-end workflow can be run by issuing the following list of commands, straight from this folder: ```bash -$ sudo apt-get install -y python3-gdal gdal-bin libgdal-dev gcc g++ python3.8-dev -$ pip install -r ../../requirements.txt $ python3 prepare_data.py config_trne.yaml $ stdl-objdet generate_tilesets config_trne.yaml $ stdl-objdet train_model config_trne.yaml From de622da03b77e7e59019fff4db7bede6442ed159 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Tue, 12 Sep 2023 09:00:34 +0000 Subject: [PATCH 064/108] Rename tasks -> stages --- scripts/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/cli.py b/scripts/cli.py index e132406..5fa91da 100644 --- a/scripts/cli.py +++ b/scripts/cli.py @@ -12,7 +12,7 @@ def main(): global_parser = argparse.ArgumentParser(prog="stdl-objdet") subparsers = global_parser.add_subparsers( - title="tasks", help="the various tasks which can be performed by the STDL Object Detector" + title="stages", help="the various stages of the STDL Object Detector Framework" ) arg_template = { From e4821e743acf242938bdea667c4dd62bbba66218 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Tue, 12 Sep 2023 09:00:44 +0000 Subject: [PATCH 065/108] Update README.md --- README.md | 205 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 131 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index cb8412a..57ef739 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,109 @@ # Object Detector -This project provides a suite of Python scripts allowing the end-user to use Deep Learning to detect objects in georeferenced raster images. +This project provides a suite of Python scripts allowing the end-user to use Deep Learning to detect objects in geo-referenced raster images. -## Hardware requirements +### Table of contents -A CUDA-capable system is required. +- [Requirements](#requirements) + - [Hardware](#hardware) + - [Software](#software) +- [Installation](#installation) +- [How-to](#getting-started) +- [Examples](#examples) +- [License](#license) -## Software Requirements +## Requirements -* Python 3.8 +### Hardware -* Dependencies may be installed with either `pip` or `conda`, by making use of the provided `requirements.txt` file. The following method was tested successfully on a Linux system powered by CUDA 11.3: +A CUDA-enabled GPU is required. - ```bash - $ conda create -n -c conda-forge python=3.8 - $ conda activate - $ pip install -r requirements.txt - ``` +### Software -## Scripts +* CUDA driver. This code was developed and tested with CUDA 11.3 on Ubuntu 20.04. -Four scripts can be found in the `scripts` subfolder: +* Although we recommend the usage of Docker (see [here](#with-docker)), this code can also be run without Docker, provided that Python 3.8 is available. Python dependencies may be installed with either `pip` or `conda`, using the provided `requirements.txt` file. -1. `generate_tilesets.py` -2. `train_model.py` -3. `make_predictions.py` -4. `assess_predictions.py` +## Installation -which can be run one after the other following this very order, by issuing the following command from a terminal: +### Without Docker + +The object detector can be installed by issuing the following command: + +```bash +$ pip install . +``` + +In case of a successful installation, the command + +```bash +$ stdl-objdet -h +``` + +should display some basic usage information. + +### With Docker + +A Docker image can be built by issuing the following command: + +```bash +$ docker compose build +``` + +In case of a successful build, the command ```bash -$ python / -``` +$ docker compose run --rm stdl-objdet stdl-objdet -h +``` + +should display some basic usage information. Note that, for the code to properly run, + +1. the version of the CUDA driver installed on the host machine must match with the version used in the [Dockerfile](Dockerfile), namely version 11.3. We let end-user adapt the Dockerfile to her/his environment. +2. The NVIDIA Container Toolkit must be installed on the host machine (see [this guide](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)). + +## How-to + +### + +This project implements the workflow described [here](https://tech.stdl.ch/TASK-IDET/#workflow), which includes four stages: + +| Stage no. | Stage name | CLI command | Implementation | +| :-: | --- | --- | --- | +| 1 | Tileset generation | `generate_tilesets` | [here](scripts/generate_tilesets.py) | +| 2 | Model training | `train_model` | [here](scripts/train_model.py) | +| 3 | Prediction | `make_predictions` | [here](scripts/train_model.py) | +| 4 | Assessment | `assess_predictions` | [here](scripts/assess_predictions.py) | + +These stages/scripts can be run one after the other, by issuing the following command from a terminal: + +* w/o Docker: + + ```bash + $ stdl-objdet + ``` + +* w/ Docker: -Note concerning **inference-only scenarios**: the execution of the `train_model.py` script can be skipped in case the user wishes to only perform inference, using a model trained in advance. + ```bash + $ docker compose run --rm stdl-objdet stdl-objdet -The same configuration file can be used for all the scripts, as each script only reads the content related to a key named after itself - further details on the configuration file will be provided here-below. Before terminating, each script prints the list of output files: we strongly encourage the end-user to review those files, *e.g.* by loading them into [QGIS](https://qgis.org). + ``` -The following terminology will be used throughout the rest of this document: + Alternatively, + + ```bash + $ docker compose run --rm stdl-objdet + ``` + + then + + ``` + root@:/app# stdl-objdet + ``` + + For those who are less familiar with Docker, know that all output files created inside a container are not persistent, unless "volumes" or "bind mounts" are used (see [this](https://docs.docker.com/storage/)). + +The same configuration file can be used for all the commands, as each of them only reads the content related to a key named after its name. More detailed information about each stage and the related configuration is provided here-below. The following terminology is used: * **ground-truth data**: data to be used to train the Deep Learning-based predictive model; such data is expected to be 100% true @@ -45,23 +111,23 @@ The following terminology will be used throughout the rest of this document: * **other data**: data that is not ground-truth-grade -* **labels**: georeferenced polygons surrounding the objects targeted by a given analysis +* **labels**: geo-referenced polygons surrounding the objects targeted by a given analysis * **AoI**, abbreviation of "Area of Interest": geographical area over which the user intend to carry out the analysis. This area encompasses * regions for which ground-truth data is available, as well as * regions over which the user intends to detect potentially unknown objects -* **tiles**, or - more explicitly - "geographical map tiles": cf. [this link](https://wiki.openstreetmap.org/wiki/Tiles). More precisely, "Slippy Map Tiles" are used within this project, cf. [this link](https://developers.planet.com/tutorials/slippy-maps-101/). +* **tiles**, or - more explicitly - "geographical map tiles": see [this link](https://wiki.openstreetmap.org/wiki/Tiles). More precisely, "Slippy Map Tiles" are used within this project, see [this link](https://developers.planet.com/tutorials/slippy-maps-101/). -* **COCO data format**: cf. [this link](https://cocodataset.org/#format-data) +* **COCO data format**: see [this link](https://cocodataset.org/#format-data) * **trn**, **val**, **tst**, **oth**: abbreviations of "training", "validation", "test" and "other", respectively -### 1. `generate_tilesets.py` +### Stage 1: tileset generation -This script generates the various tilesets concerned by a given study. Each generated tileset is made up by: +This `generate_tilesets` command generates the various tilesets concerned by a given study. Each generated tileset is made up by: -* a collection of georeferenced raster images (in GeoTIFF format) +* a collection of geo-referenced raster images (in GeoTIFF format) * a JSON file compliant with the [COCO data format](https://cocodataset.org/#format-data) The following relations apply: @@ -82,13 +148,7 @@ In order to speed up some of the subsequent computations, each output image is a * bounding box; * spatial reference system. -The script can be run by issuing the following command from a terminal: - -```bash -$ python /generate_tilesets.py -``` - -Here's the excerpt of the configuration file relevant to this script, with values replaced by textual documentation: +Here's the excerpt of the configuration file relevant to this script, with values replaced by some documentation: ```yaml generate_tilesets.py: @@ -107,14 +167,14 @@ generate_tilesets.py: overwrite: n_jobs: COCO_metadata: - year: - version: - description: - contributor: - url: + year: + version: + description: + contributor: + url: license: - name: - url: + name: + url: category: name: supercategory: @@ -129,15 +189,12 @@ Note that: 2. the `id` field must not contain any duplicate value; 3. values of the `id` field must follow the following pattern: `(, , )`, e.g. `(135571, 92877, 18)`. -### 2. `train_model.py` - -This script allows one to train a predictive model based on a Convolutional Deep Neural Network, leveraging [FAIR's Detectron2](https://github.com/facebookresearch/detectron2). For further information, we refer the user to the [official documention](https://detectron2.readthedocs.io/en/latest/). +### Stage 2: model training -The script can be run by issuing the following command from a terminal: +> **Note** +This stage can be skipped if the user wishes to perform inference only, using a pre-trained model. -```bash -$ python /train_model.py -``` +The `train_model` command allows one to train a predictive model based on a Convolutional Deep Neural Network, leveraging [FAIR's Detectron2](https://github.com/facebookresearch/detectron2). For further information, we refer the user to the [official documention](https://detectron2.readthedocs.io/en/latest/). Here's the excerpt of the configuration file relevant to this script, with values replaced by textual documentation: @@ -155,21 +212,15 @@ train_model.py: model_zoo_checkpoint_url: ``` -Detectron2 configuration files are provided in the example folders mentioned here-below. We warn the end-user about the fact that, **for the time being, no hyperparameters tuning is automatically performed by this suite of scripts**. +Detectron2 configuration files are provided in the example folders mentioned here-below. We warn the end-user about the fact that, **for the time being, no hyperparameters tuning is automatically performed**. -### 3. `make_predictions.py` +### Stage 3: prediction -This script allows to use the predictive model trained at the previous step to make predictions over various input datasets: +The `make_predictions` command allows one to use the predictive model trained at the previous step to make predictions over various input datasets: * predictions over the `trn`, `val`, `tst` datasets can be used to assess the reliability of this approach on ground-truth data; * predictions over the `oth` dataset are, in principle, the main goal of this kind of analyses. -The script can be run by issuing the following command from a terminal: - -```bash -$ python /make_predictions.py -``` - Here's the excerpt of the configuration file relevant to this script, with values replaced by textual documentation: ```yaml @@ -185,13 +236,20 @@ make_predictions.py: detectron2_config_file: model_weights: pth_file: + image_metadata_json: + # the following section concerns the Ramer-Douglas-Peucker algorithm, + # which can be optionally applied to detection before they are exported + rdp_simplification: + enabled: + epsilon: + score_lower_threshold: ``` -### 4. `assess_predictions.py` +### Stage 4: assessment -This script allows one to assess the reliability of predictions made by the previous script, comparing predictions with ground-truth data. The assessment goes through the following steps: +The `assess_predictions` command allows one to assess the reliability of predictions, comparing predictions with ground-truth data. The assessment goes through the following steps: -1. Labels (GT + `oth`) geometries are clipped to the boundaries of the various AoI tiles, scaled by a factor 0.999 in order to prevent any "crosstalk" between neighbouring tiles. +1. Labels (GT + `oth`) geometries are clipped to the boundaries of the various AoI tiles, scaled by a factor 0.999 in order to prevent any "crosstalk" between neighboring tiles. 2. Vector features are extracted from Detectron2's predictions, which are originally in a raster format (`numpy` arrays, to be more precise). @@ -200,37 +258,36 @@ This script allows one to assess the reliability of predictions made by the prev * False Positives (FP), *i.e.* objects that are only found in the predictions dataset; * False Negatives (FN), *i.e.* objects that are only found in the labels dataset. -4. Finally, TPs, FPs and FNs are counted in order to compute the following metrics (cf. [this page](https://en.wikipedia.org/wiki/Precision_and_recall)) : +4. Finally, TPs, FPs and FNs are counted in order to compute the following metrics (see [this page](https://en.wikipedia.org/wiki/Precision_and_recall)) : * precision * recall * f1-score -The script can be run by issuing the following command from a terminal: - -```bash -$ python /assess_predictions.py -``` - -Here's the excerpt of the configuration file relevant to this script, with values replaced by textual documentation: +Here's the excerpt of the configuration file relevant to this command, with values replaced by textual documentation: ```yaml assess_predictions.py: - n_jobs: datasets: ground_truth_labels_geojson: other_labels_geojson: - image_metadata_json: + image_metadata_json: split_aoi_tiles_geojson: predictions: trn: val: tst: oth: - output_folder: + output_folder: ``` ## Examples -A few examples are provided within the folder `examples`. For further details, we refer the user to the various use-case specific readme files: +A few examples are provided within the `examples` folder. For further details, we refer the user to the various use-case specific readme files: * [Swimming Pool Detection over the Canton of Geneva](examples/swimming-pool-detection/GE/README.md) * [Swimming Pool Detection over the Canton of Neuchâtel](examples/swimming-pool-detection/NE/README.md) +* [Quarry Detection over the entire Switzerland](examples/quarry-detection/README.md) + + +## License + +The STDL Object Detector is released under the [MIT license](LICENSE.md). \ No newline at end of file From a297b980b4bc818ea1c1e365cb4feda3a106f648 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Tue, 12 Sep 2023 09:29:38 +0000 Subject: [PATCH 066/108] Install python-is-python3 --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 28bc4b7..8650a11 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,8 @@ RUN apt update &&\ apt install -y libgl1 &&\ apt install -y libglib2.0-0 &&\ apt install -y gdal-bin &&\ - apt install -y python3-pip + apt install -y python3-pip &&\ + apt install -y python-is-python3 WORKDIR /app From e3363254d6587e54c65823090ca2e03c673b81de Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Tue, 12 Sep 2023 09:35:55 +0000 Subject: [PATCH 067/108] Use nobody user inside the container --- Dockerfile | 2 ++ README.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 8650a11..3bffc15 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,5 +19,7 @@ ADD scripts/*.py scripts/ ADD setup.py . RUN pip install . +USER 65534:65534 + ENTRYPOINT [""] CMD ["stdl-objdet", "-h"] diff --git a/README.md b/README.md index 57ef739..ff6dfbf 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ These stages/scripts can be run one after the other, by issuing the following co then ``` - root@:/app# stdl-objdet + nobody@:/app# stdl-objdet ``` For those who are less familiar with Docker, know that all output files created inside a container are not persistent, unless "volumes" or "bind mounts" are used (see [this](https://docs.docker.com/storage/)). From 6be98362810935eb944ff8608310fc0b01f14376 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Tue, 12 Sep 2023 11:50:36 +0200 Subject: [PATCH 068/108] Add logger.success and done msg function --- .../quarry-detection/filter_prediction.py | 20 +++++++++---------- examples/quarry-detection/prepare_data.py | 17 ++++++++-------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/examples/quarry-detection/filter_prediction.py b/examples/quarry-detection/filter_prediction.py index 3c297e5..08ea07d 100644 --- a/examples/quarry-detection/filter_prediction.py +++ b/examples/quarry-detection/filter_prediction.py @@ -6,23 +6,21 @@ import os import sys -import inspect import time import argparse import yaml -from loguru import logger import geopandas as gpd import pandas as pd import rasterio from sklearn.cluster import KMeans - -# the following allows us to import modules from within this file's parent folder sys.path.insert(0, '.') +from helpers import misc +from helpers.constants import DONE_MSG -logger.remove() -logger.add(sys.stderr, format="{time:YYYY-MM-DD HH:mm:ss} - {level} - {message}", level="INFO") +from loguru import logger +logger = misc.format_logger(logger) if __name__ == "__main__": @@ -31,7 +29,7 @@ tic = time.time() logger.info('Starting...') - # argument parser + # Argument and parameter specification parser = argparse.ArgumentParser(description="The script filters the detection of potential Mineral Extraction Sites obtained with the object-detector scripts") parser.add_argument('config_file', type=str, help='input geojson path') args = parser.parse_args() @@ -103,9 +101,8 @@ # Clip prediction to AOI input = gpd.clip(input, aoi) - # Create empty data frame - geo_merge = gpd.GeoDataFrame() # Merge close labels using buffer and unions + geo_merge = gpd.GeoDataFrame() geo_merge = input.buffer(+DISTANCE, resolution = 2) geo_merge = geo_merge.geometry.unary_union geo_merge = gpd.GeoDataFrame(geometry=[geo_merge], crs = input.crs) @@ -128,12 +125,13 @@ intersection = gpd.sjoin(geo_tmp, input, how='inner') intersection['id'] = intersection.index score_final=intersection.groupby(['id']).mean(numeric_only=True) + # Formatting the final geo df data = {'id_feature': geo_merge.index,'score': score_final['score'] , 'area': geo_merge.area, 'centroid_x': geo_merge.centroid.x, 'centroid_y': geo_merge.centroid.y, 'geometry': geo_merge} geo_final = gpd.GeoDataFrame(data, crs=input.crs) logger.info(f"{len(geo_final)} predictions remaining") - # Format the ooutput name of the filtered prediction + # Formatting the output name of the filtered prediction feature = OUTPUT.replace('{score}', str(SCORE)).replace('0.', '0dot') \ .replace('{year}', str(int(YEAR)))\ .replace('{area}', str(int(AREA)))\ @@ -142,7 +140,7 @@ geo_final.to_file(feature, driver='GeoJSON') written_files.append(feature) - logger.info(f"...done. A file was written: {feature}") + logger.success(f"{DONE_MSG} A file was written: {feature}") logger.info("The following files were written. Let's check them out!") for written_file in written_files: diff --git a/examples/quarry-detection/prepare_data.py b/examples/quarry-detection/prepare_data.py index 2846511..ea4bbca 100644 --- a/examples/quarry-detection/prepare_data.py +++ b/examples/quarry-detection/prepare_data.py @@ -10,18 +10,18 @@ import argparse import yaml import re -from loguru import logger import geopandas as gpd import morecantile import pandas as pd - -# the following allows us to import modules from within this file's parent folder sys.path.insert(0, '.') +from helpers import misc +from helpers.constants import DONE_MSG + +from loguru import logger +logger = misc.format_logger(logger) -logger.remove() -logger.add(sys.stderr, format="{time:YYYY-MM-DD HH:mm:ss} - {level} - {message}", level="INFO") def add_tile_id(row): @@ -56,8 +56,9 @@ def add_tile_id(row): if not os.path.exists(OUTPUT_DIR): os.makedirs(OUTPUT_DIR) - # Prepare the tiles written_files = [] + + # Prepare the tiles ## Convert datasets shapefiles into geojson format logger.info('Convert labels shapefile into GeoJSON format (EPSG:4326)...') @@ -71,7 +72,7 @@ def add_tile_id(row): label_filepath = os.path.join(OUTPUT_DIR, label_filename) labels_4326.to_file(label_filepath, driver='GeoJSON') written_files.append(label_filepath) - logger.info(f"...done. A file was written: {label_filepath}") + logger.success(f"{DONE_MSG} A file was written: {label_filepath}") logger.info('Creating tiles for the Area of Interest (AOI)...') @@ -122,7 +123,7 @@ def add_tile_id(row): tile_filepath = os.path.join(OUTPUT_DIR, tile_filename) tiles_4326.to_file(tile_filepath, driver='GeoJSON') written_files.append(tile_filepath) - logger.info(f"...done. A file was written: {tile_filepath}") + logger.success(f"{DONE_MSG} A file was written: {tile_filepath}") print() logger.info("The following files were written. Let's check them out!") From 8580323462472fd148dc5ef6f031bbc3a82c016c Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Tue, 12 Sep 2023 11:52:01 +0200 Subject: [PATCH 069/108] Rename filter file name to filter_detection.py --- .../{filter_prediction.py => filter_detection.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/quarry-detection/{filter_prediction.py => filter_detection.py} (100%) diff --git a/examples/quarry-detection/filter_prediction.py b/examples/quarry-detection/filter_detection.py similarity index 100% rename from examples/quarry-detection/filter_prediction.py rename to examples/quarry-detection/filter_detection.py From e04959301fcd00b5ffe73279be0b0c9edc8b43de Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Tue, 12 Sep 2023 11:54:27 +0200 Subject: [PATCH 070/108] Replace - by _ in output folder names --- examples/quarry-detection/config_prd.yaml | 14 +++++------ examples/quarry-detection/config_trne.yaml | 28 +++++++++++----------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/examples/quarry-detection/config_prd.yaml b/examples/quarry-detection/config_prd.yaml index eed3083..c025f5c 100644 --- a/examples/quarry-detection/config_prd.yaml +++ b/examples/quarry-detection/config_prd.yaml @@ -7,18 +7,18 @@ prepare_data.py: srs: "EPSG:2056" # Projection of the input file datasets: labels_shapefile: ./data/AOI/AOI_2020.shp - output_folder: ./output/output-prd + output_folder: ./output/output_prd zoom_level: 16 # 2-Fetch of tiles (online server) and split into 3 datasets: train, test, validation generate_tilesets.py: debug_mode: False # sample of tiles datasets: - aoi_tiles_geojson: ./output/output-prd/tiles.geojson + aoi_tiles_geojson: ./output/output_prd/tiles.geojson orthophotos_web_service: type: XYZ # supported values: 1. MIL = Map Image Layer 2. WMS 3. XYZ url: https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swissimage-product/default/2020/3857/{z}/{x}/{y}.jpeg - output_folder: ./output/output-prd + output_folder: ./output/output_prd tile_size: 256 # per side, in pixels overwrite: True n_jobs: 10 @@ -37,7 +37,7 @@ generate_tilesets.py: # 3-Perform the object detection based on the optimized trained model make_predictions.py: - working_folder: ./output/output-prd + working_folder: ./output/output_prd log_subfolder: logs sample_tagged_img_subfolder: sample_tagged_images COCO_files: # relative paths, w/ respect to the working_folder @@ -45,7 +45,7 @@ make_predictions.py: detectron2_config_file: '../../detectron2_config_dqry.yaml' # path relative to the working_folder model_weights: pth_file: '../output-trne/logs/model_0002999.pth' # trained model minimizing the validation loss curve, monitor the training process via tensorboard (tensorboard --logdir ) - image_metadata_json: './output/output-prd/img_metadata.json' + image_metadata_json: './output/output_prd/img_metadata.json' rdp_simplification: # rdp = Ramer-Douglas-Peucker enabled: True epsilon: 2.0 # cf. https://rdp.readthedocs.io/en/latest/ @@ -54,11 +54,11 @@ make_predictions.py: # 4-Filtering and merging prediction polygons to improve results filter_prediction.py: year: 2020 - input: ./output/output-prd/oth_predictions_at_0dot3_threshold.gpkg + input: ./output/output_prd/oth_predictions_at_0dot3_threshold.gpkg labels_shapefile: ./data/AOI/AOI_2020.shp dem: ./data/DEM/switzerland_dem_EPSG2056.tif elevation: 1200.0 # m, altitude threshold score: 0.95 # prediction 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/output-prd/oth_prediction_at_0dot3_threshold_year-{year}_score-{score}_area-{area}_elevation-{elevation}_distance-{distance}.geojson \ No newline at end of file + output: ./output/output_prd/oth_prediction_at_0dot3_threshold_year-{year}_score-{score}_area-{area}_elevation-{elevation}_distance-{distance}.geojson \ No newline at end of file diff --git a/examples/quarry-detection/config_trne.yaml b/examples/quarry-detection/config_trne.yaml index e4bac49..5bf50fb 100644 --- a/examples/quarry-detection/config_trne.yaml +++ b/examples/quarry-detection/config_trne.yaml @@ -7,19 +7,19 @@ prepare_data.py: srs: "EPSG:2056" datasets: labels_shapefile: ./data/labels/tlm-hr-trn-topo.shp - output_folder: ./output/output-trne + output_folder: ./output/output_trne zoom_level: 16 # 2-Fetch of tiles (online server) and split into 3 datasets: train, test, validation generate_tilesets.py: debug_mode: False # sample of tiles datasets: - aoi_tiles_geojson: ./output/output-trne/tiles.geojson - ground_truth_labels_geojson: ./output/output-trne/labels.geojson + aoi_tiles_geojson: ./output/output_trne/tiles.geojson + ground_truth_labels_geojson: ./output/output_trne/labels.geojson orthophotos_web_service: type: XYZ # supported values: 1. MIL = Map Image Layer 2. WMS 3. XYZ url: https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swissimage-product/default/2020/3857/{z}/{x}/{y}.jpeg - output_folder: ./output/output-trne + output_folder: ./output/output_trne tile_size: 256 # per side, in pixels overwrite: False n_jobs: 10 @@ -38,7 +38,7 @@ generate_tilesets.py: # 3-Train the model with the detectron2 algorithm train_model.py: - working_folder: ./output/output-trne + working_folder: ./output/output_trne log_subfolder: logs sample_tagged_img_subfolder: sample_tagged_images COCO_files: # relative paths, w/ respect to the working_folder @@ -51,7 +51,7 @@ train_model.py: # 4-Perform the object detection based on the optimized trained model make_predictions.py: - working_folder: ./output/output-trne + working_folder: ./output/output_trne log_subfolder: logs sample_tagged_img_subfolder: sample_tagged_images COCO_files: # relative paths, w/ respect to the working_folder @@ -61,7 +61,7 @@ make_predictions.py: detectron2_config_file: '../../detectron2_config_dqry.yaml' # path relative to the working_folder model_weights: pth_file: './logs/model_0002999.pth' # trained model minimizing the validation loss curve, monitor the training process via tensorboard (tensorboard --logdir ) - image_metadata_json: './output/output-trne/img_metadata.json' + image_metadata_json: './output/output_trne/img_metadata.json' rdp_simplification: # rdp = Ramer-Douglas-Peucker enabled: true epsilon: 2.0 # cf. https://rdp.readthedocs.io/en/latest/ @@ -70,11 +70,11 @@ make_predictions.py: # 5-Evaluate the quality of the prediction for the different datasets with metrics calculation assess_predictions.py: datasets: - ground_truth_labels_geojson: ./output/output-trne/labels.geojson - image_metadata_json: ./output/output-trne/img_metadata.json - split_aoi_tiles_geojson: ./output/output-trne/split_aoi_tiles.geojson # aoi = Area of Interest + ground_truth_labels_geojson: ./output/output_trne/labels.geojson + image_metadata_json: ./output/output_trne/img_metadata.json + split_aoi_tiles_geojson: ./output/output_trne/split_aoi_tiles.geojson # aoi = Area of Interest predictions: - trn: ./output/output-trne/trn_predictions_at_0dot05_threshold.gpkg - val: ./output/output-trne/val_predictions_at_0dot05_threshold.gpkg - tst: ./output/output-trne/tst_predictions_at_0dot05_threshold.gpkg - output_folder: ./output/output-trne \ No newline at end of file + trn: ./output/output_trne/trn_predictions_at_0dot05_threshold.gpkg + val: ./output/output_trne/val_predictions_at_0dot05_threshold.gpkg + tst: ./output/output_trne/tst_predictions_at_0dot05_threshold.gpkg + output_folder: ./output/output_trne \ No newline at end of file From 2e649be3d501f8694cc24336d46b2533af1bcb93 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Tue, 12 Sep 2023 10:08:43 +0000 Subject: [PATCH 071/108] Update examples/swimming-pool-detection/GE/README.md --- examples/swimming-pool-detection/GE/README.md | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/examples/swimming-pool-detection/GE/README.md b/examples/swimming-pool-detection/GE/README.md index b05cae3..ad9c98b 100644 --- a/examples/swimming-pool-detection/GE/README.md +++ b/examples/swimming-pool-detection/GE/README.md @@ -1,25 +1,24 @@ - # Example: detecting swimming pools over the Canton of Geneva A sample working setup is here provided, allowing the end-user to detect swimming pools over the Canton of Geneva. It is made up by the following assets: -* ready-to-use configuration files, namely `config_GE.yaml` and `detectron2_config_GE.yaml`; -* supplementary data (`data/OK_z18_tile_IDs.csv`), *i.e.* a curated list of Slippy Map Tiles corresponding to zoom level 18, which seemed to include reliable "ground-truth data" when they were manually checked against [SITG's "Piscines" Open Dataset](https://ge.ch/sitg/fiche/1836), in Summer 2020. The thoughtful user should either review or regenerate this file in order to get better results. -* A data preparation script (`prepare_data.py`), producing files to be used as input to the `generate_training_sets.py` script. +* ready-to-use configuration files, namely `config_GE.yaml` and `detectron2_config_GE.yaml`. +* Supplementary data (`data/OK_z18_tile_IDs.csv`), *i.e.* a curated list of Slippy Map Tiles corresponding to zoom level 18, which seemed to include reliable "ground-truth data" when they were manually checked against the [SITG's "Piscines" Open Dataset](https://ge.ch/sitg/fiche/1836), in Summer 2020. The thoughtful user should either review or regenerate this file in order to get better results. +* A data preparation script (`prepare_data.py`), producing files to be used as input to the `generate_tilesets` stage. -The end-to-end workflow can be run by issuing the following list of commands, straight from this folder: +The workflow can be run end-to-end by issuing the following list of commands, from the root folder of this GitHub repo: -```bash -$ conda activate -$ python prepare_data.py config_GE.yaml -$ cd output_GE -$ cat parcels.geojson | supermercado burn 18 | mercantile shapes | fio collect > parcels_z18_tiles.geojson -$ cd - -$ python prepare_data.py config_GE.yaml -$ python ../../../scripts/generate_tilesets.py config_GE.yaml -$ python ../../../scripts/train_model.py config_GE.yaml -$ python ../../../scripts/make_predictions.py config_GE.yaml -$ python ../../../scripts/assess_predictions.py config_GE.yaml +``` +$ sudo chown -R 65534:65534 examples +$ docker compose run --rm stdl-objdet +nobody@:/app# cd examples/swimming-pool-detection/GE +nobody@:/app# python prepare_data.py config_GE.yaml +nobody@:/app# cd output_GE && cat parcels.geojson | supermercado burn 18 | mercantile shapes | fio collect > parcels_z18_tiles.geojson && cd - +nobody@:/app# python prepare_data.py config_GE.yaml +nobody@:/app# stdl-objdet generate_tilesets config_GE.yaml +nobody@:/app# stdl-objdet train_model config_GE.yaml +nobody@:/app# stdl-objdet make_predictions config_GE.yaml +nobody@:/app# stdl-objdet assess_predictions config_GE.yaml ``` We strongly encourage the end-user to review the provided `config_GE.yaml` file as well as the various output files, a list of which is printed by each script before exiting. From ee634d62d7110bcf60822078f011d3a5d63f65aa Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Tue, 12 Sep 2023 10:08:54 +0000 Subject: [PATCH 072/108] Update examples/swimming-pool-detection/NE/README.md --- examples/swimming-pool-detection/NE/README.md | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/examples/swimming-pool-detection/NE/README.md b/examples/swimming-pool-detection/NE/README.md index bcde861..532e203 100644 --- a/examples/swimming-pool-detection/NE/README.md +++ b/examples/swimming-pool-detection/NE/README.md @@ -1,29 +1,28 @@ - # Example: detecting swimming pools over the Canton of Neuchâtel A sample working setup is here provided, allowing the end-user to detect swimming pools over the Canton of Neuchâtel. It is made up by the following assets: -* ready-to-use configuration files, namely `config_NE.yaml` and `detectron2_config_NE.yaml`; -* supplementary data (`data/*`), *i.e.* +* ready-to-use configuration files, namely `config_NE.yaml` and `detectron2_config_NE.yaml`. +* Supplementary data (`data/*`), *i.e.* * geographical sectors covering ground-truth data; * other (non ground-truth) sectors; * ground-truth labels; * other labels. -* A data preparation script (`prepare_data.py`), producing files to be used as input to the `generate_training_sets.py` script. +* A data preparation script (`prepare_data.py`), producing files to be used as input to the `generate_tilesets` stage. -The end-to-end workflow can be run by issuing the following list of commands, straight from this folder: +The workflow can be run end-to-end by issuing the following list of commands, from the root folder of this GitHub repository: -```bash -$ conda activate -$ python prepare_data.py config_NE.yaml -$ cd output_NE -$ cat aoi.geojson | supermercado burn 18 | mercantile shapes | fio collect > aoi_z18_tiles.geojson -$ cd - -$ python prepare_data.py config_NE.yaml -$ python ../../../scripts/generate_tilesets.py config_NE.yaml -$ python ../../../scripts/train_model.py config_NE.yaml -$ python ../../../scripts/make_predictions.py config_NE.yaml -$ python ../../../scripts/assess_predictions.py config_NE.yaml +``` +$ sudo chown -R 65534:65534 examples +$ docker compose run --rm stdl-objdet +nobody@:/app# cd examples/swimming-pool-detection/NE +nobody@:/app# python prepare_data.py config_NE.yaml +nobody@:/app# cd output_NE && cat parcels.geojson | supermercado burn 18 | mercantile shapes | fio collect > parcels_z18_tiles.geojson && cd - +nobody@:/app# python prepare_data.py config_NE.yaml +nobody@:/app# stdl-objdet generate_tilesets config_NE.yaml +nobody@:/app# stdl-objdet train_model config_NE.yaml +nobody@:/app# stdl-objdet make_predictions config_NE.yaml +nobody@:/app# stdl-objdet assess_predictions config_NE.yaml ``` -We strongly encourage the end-user to review the provided `config_GE.yaml` file as well as the various output files, a list of which is printed by each script before exiting. +We strongly encourage the end-user to review the provided `config_NE.yaml` file as well as the various output files, a list of which is printed by each script before exiting. From 470a85934fb0de97e1e1a171b6e6b81889e2ce36 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Tue, 12 Sep 2023 10:09:19 +0000 Subject: [PATCH 073/108] Update examples/quarry-detection/README.md --- examples/quarry-detection/README.md | 53 +++++++++++++++-------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/examples/quarry-detection/README.md b/examples/quarry-detection/README.md index ff55a9a..aaf740f 100644 --- a/examples/quarry-detection/README.md +++ b/examples/quarry-detection/README.md @@ -3,36 +3,39 @@ A sample working setup is provided here, enabling the end-user to detect quarries and mineral extraction sites in Switzerland over several years.
It consists of the following elements: -- the read-to-use configuration files: - - `config_trne.yaml`, - - `config_prd.yaml`, - - `detectron2_config_dqry.yaml`, -- the input data in the `data` subfolder: - - quarries shapefile from the product [swissTLM3D](https://www.swisstopo.admin.ch/fr/geodata/landscape/tlm3d.html), revised and synchronised with the 2020 [SWISSIMAGE](https://www.swisstopo.admin.ch/fr/geodata/images/ortho/swissimage10.html) mosaic (**label**), - - the delimitation of the AOI to perform inference predictions (**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 - WGS 84 coordinate reference system. The raster must be first reprojected to EPSG:2056 - CH1903+ / LV95, named `switzerland_dem_EPSG2056.tif`and located in the **DEM** subfolder. -- a data preparation script (`prepare_data.py`) producing the files to be used as input to the `generate_tilesets.py`script. -- a results post-processing script (`filter_prediction.py`) filtering the predictions, produced from `make_prediction.py`script, to the final shapefile - -In the provided Docker container, the end-to-end workflow can be run by issuing the following list of commands, straight from this folder: - -```bash -$ python3 prepare_data.py config_trne.yaml -$ stdl-objdet generate_tilesets config_trne.yaml -$ stdl-objdet train_model config_trne.yaml -$ stdl-objdet make_predictions config_trne.yaml -$ stdl-objdet assess_predictions config_trne.yaml -$ python3 prepare_data.py config_prd.yaml -$ stdl-objdet generate_tilesets config_prd.yaml -$ stdl-objdet make_predictions config_prd.yaml -$ python3 filter_detection.py config_prd.yaml +- ready-to-use configuration files: + - `config_trne.yaml` + - `config_prd.yaml` + - `detectron2_config_dqry.yaml` +- input data in the `data` subfolder: + - quarry **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 first re-projected to [EPSG:2056](https://epsg.io/2056), renamed as `switzerland_dem_EPSG2056.tif` and located in the **DEM** subfolder +- 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_prediction.py`) which filters predictions [...] + +The workflow can be run end-to-end by issuing the following list of commands, from the root folder of this GitHub repository: + +``` +$ sudo chown -R 65534:65534 examples +$ docker compose run --rm stdl-objdet +nobody@:/app# cd examples/quarry-detection +nobody@:/app# python prepare_data.py config_trne.yaml +nobody@:/app# stdl-objdet generate_tilesets config_trne.yaml +nobody@:/app# stdl-objdet train_model config_trne.yaml +nobody@:/app# stdl-objdet make_predictions config_trne.yaml +nobody@:/app# stdl-objdet assess_predictions config_trne.yaml +nobody@:/app# python prepare_data.py config_prd.yaml +nobody@:/app# stdl-objdet generate_tilesets config_prd.yaml +nobody@:/app# stdl-objdet make_predictions config_prd.yaml +nobody@:/app# python filter_detection.py config_prd.yaml ``` We strongly encourage the end-user to review the provided `config_trne.yaml` and `config_prd.yaml` files as well as the various output files, a list of which is printed by each script before exiting. -The model is trained on the 2020 [SWISSIMAGE](https://www.swisstopo.admin.ch/fr/geodata/images/ortho/swissimage10.html) mosaic. Inference can be performed on SWISSIMAGE mosaics of the product SWISSIMAGE time travel by changing the year in `config_prd.yaml`. It should be noted that the model has been trained on RGB color images and might not perform as well on Black and White images. +The model is trained on the 2020 [SWISSIMAGE](https://www.swisstopo.admin.ch/fr/geodata/images/ortho/swissimage10.html) mosaic. Inference can be performed on SWISSIMAGE mosaics of the product SWISSIMAGE time travel by changing the year in `config_prd.yaml`. It should be noted that the model has been trained on RGB images and might not perform as well on B&W images. -For more information about this project, you can consult [the associated repository](https://github.com/swiss-territorial-data-lab/proj-dqry) (not public yet). +For more information about this project, see [this repository](https://github.com/swiss-territorial-data-lab/proj-dqry) (not public yet). ## Disclaimer From 9f16282903ef1b71e725e2630097304e5b21311b Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Tue, 12 Sep 2023 12:06:48 +0000 Subject: [PATCH 074/108] repo -> repository --- examples/swimming-pool-detection/GE/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/swimming-pool-detection/GE/README.md b/examples/swimming-pool-detection/GE/README.md index ad9c98b..d58f588 100644 --- a/examples/swimming-pool-detection/GE/README.md +++ b/examples/swimming-pool-detection/GE/README.md @@ -6,7 +6,7 @@ A sample working setup is here provided, allowing the end-user to detect swimmin * Supplementary data (`data/OK_z18_tile_IDs.csv`), *i.e.* a curated list of Slippy Map Tiles corresponding to zoom level 18, which seemed to include reliable "ground-truth data" when they were manually checked against the [SITG's "Piscines" Open Dataset](https://ge.ch/sitg/fiche/1836), in Summer 2020. The thoughtful user should either review or regenerate this file in order to get better results. * A data preparation script (`prepare_data.py`), producing files to be used as input to the `generate_tilesets` stage. -The workflow can be run end-to-end by issuing the following list of commands, from the root folder of this GitHub repo: +The workflow can be run end-to-end by issuing the following list of commands, from the root folder of this GitHub repository: ``` $ sudo chown -R 65534:65534 examples From 622c38e5e57a0acb063ee8c94bdab80e94ebca7e Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Tue, 12 Sep 2023 14:48:46 +0200 Subject: [PATCH 075/108] Correct filter name in config_prd --- examples/quarry-detection/config_prd.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/quarry-detection/config_prd.yaml b/examples/quarry-detection/config_prd.yaml index c025f5c..fea16d7 100644 --- a/examples/quarry-detection/config_prd.yaml +++ b/examples/quarry-detection/config_prd.yaml @@ -52,7 +52,7 @@ make_predictions.py: score_lower_threshold: 0.3 # 4-Filtering and merging prediction polygons to improve results -filter_prediction.py: +filter_detection.py: year: 2020 input: ./output/output_prd/oth_predictions_at_0dot3_threshold.gpkg labels_shapefile: ./data/AOI/AOI_2020.shp From 96eb8ba9bad5976f167e6814a42f5f7845456959 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Tue, 12 Sep 2023 15:13:26 +0200 Subject: [PATCH 076/108] Improve prompt comments --- examples/quarry-detection/filter_detection.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/quarry-detection/filter_detection.py b/examples/quarry-detection/filter_detection.py index 08ea07d..abadafb 100644 --- a/examples/quarry-detection/filter_detection.py +++ b/examples/quarry-detection/filter_detection.py @@ -59,7 +59,7 @@ input = gpd.read_file(INPUT) input = input.to_crs(2056) total = len(input) - logger.info(f"Total input = {total}") + logger.info(f"{total} input shapes") # Discard polygons detected above the threshold elevalation and 0 m r = rasterio.open(DEM) @@ -73,7 +73,7 @@ input = input[input.elev != 0] te = len(input) - logger.info(f"{str(total - te)} predictions removed by elevation threshold: {str(ELEVATION)} m") + logger.info(f"{total - te} detections were removed by elevation threshold: {ELEVATION} m") # Centroid of every prediction polygon centroids = gpd.GeoDataFrame() @@ -85,9 +85,9 @@ cluster = KMeans(n_clusters=k, algorithm='auto', random_state=1) model = cluster.fit(centroids) labels = model.predict(centroids) - logger.info(f"KMeans algorithm computed with k = {str(k)}") + logger.info(f"KMeans algorithm computed with k = {k}") - # Dissolve and Aggregate (keep the max value of aggregate attributes) + # Dissolve and aggregate (keep the max value of aggregate attributes) input['cluster'] = labels input = input.dissolve(by='cluster', aggfunc='max') @@ -96,26 +96,26 @@ # Filter dataframe by score value input = input[input['score'] > SCORE] sc = len(input) - logger.info(f"{str(total - sc)} predictions removed by score threshold: {str(SCORE)}") + logger.info(f"{total - sc} detections were removed by score threshold: {SCORE}") # Clip prediction to AOI input = gpd.clip(input, aoi) # Merge close labels using buffer and unions geo_merge = gpd.GeoDataFrame() - geo_merge = input.buffer(+DISTANCE, resolution = 2) + geo_merge = input.buffer(+DISTANCE, resolution=2) geo_merge = geo_merge.geometry.unary_union - geo_merge = gpd.GeoDataFrame(geometry=[geo_merge], crs = input.crs) + geo_merge = gpd.GeoDataFrame(geometry=[geo_merge], crs=input.crs) geo_merge = geo_merge.explode(index_parts=True).reset_index(drop=True) geo_merge = geo_merge.buffer(-DISTANCE, resolution=2) td = len(geo_merge) - logger.info(f"{str(sc - td)} difference to clustered predictions after union (distance {str(DISTANCE)})") + logger.info(f"{td} clustered detections remains after shape union (distance {DISTANCE})") # Discard polygons with area under the threshold geo_merge = geo_merge[geo_merge.area > AREA] ta = len(geo_merge) - logger.info(f"{str(td - ta)} difference to clustered predictions after union (distance {str(AREA)})") + logger.info(f"{td - ta} detections were removed to after union (distance {AREA})") # Preparation of a geo df data = {'id': geo_merge.index,'area': geo_merge.area, 'centroid_x': geo_merge.centroid.x, 'centroid_y': geo_merge.centroid.y, 'geometry': geo_merge} @@ -124,12 +124,12 @@ # Get the averaged prediction score of the merged polygons intersection = gpd.sjoin(geo_tmp, input, how='inner') intersection['id'] = intersection.index - score_final=intersection.groupby(['id']).mean(numeric_only=True) + score_final = intersection.groupby(['id']).mean(numeric_only=True) # Formatting the final geo df data = {'id_feature': geo_merge.index,'score': score_final['score'] , 'area': geo_merge.area, 'centroid_x': geo_merge.centroid.x, 'centroid_y': geo_merge.centroid.y, 'geometry': geo_merge} geo_final = gpd.GeoDataFrame(data, crs=input.crs) - logger.info(f"{len(geo_final)} predictions remaining") + logger.info(f"{len(geo_final)} detections remaining after filtering") # Formatting the output name of the filtered prediction feature = OUTPUT.replace('{score}', str(SCORE)).replace('0.', '0dot') \ From 0ca7c41718c80e3ea073135dd177e700635b7e90 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Tue, 12 Sep 2023 15:28:23 +0200 Subject: [PATCH 077/108] Update README.md Add precision on the filtering arguments --- examples/quarry-detection/README.md | 4 ++-- examples/quarry-detection/config_prd.yaml | 4 ++-- .../{filter_detection.py => filter_detections.py} | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) rename examples/quarry-detection/{filter_detection.py => filter_detections.py} (96%) diff --git a/examples/quarry-detection/README.md b/examples/quarry-detection/README.md index aaf740f..e9292da 100644 --- a/examples/quarry-detection/README.md +++ b/examples/quarry-detection/README.md @@ -12,7 +12,7 @@ It consists of the following elements: - 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 first re-projected to [EPSG:2056](https://epsg.io/2056), renamed as `switzerland_dem_EPSG2056.tif` and located in the **DEM** subfolder - 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_prediction.py`) which filters predictions [...] +- a post-processing script (`filter_detections.py`) which filters detections according to their confidence score, altitude and surface area. The script also identifies and merges groups of polygons and nearby polygons. The workflow can be run end-to-end by issuing the following list of commands, from the root folder of this GitHub repository: @@ -28,7 +28,7 @@ nobody@:/app# stdl-objdet assess_predictions config_trne.yaml nobody@:/app# python prepare_data.py config_prd.yaml nobody@:/app# stdl-objdet generate_tilesets config_prd.yaml nobody@:/app# stdl-objdet make_predictions config_prd.yaml -nobody@:/app# python filter_detection.py config_prd.yaml +nobody@:/app# python filter_detections.py config_prd.yaml ``` We strongly encourage the end-user to review the provided `config_trne.yaml` and `config_prd.yaml` files as well as the various output files, a list of which is printed by each script before exiting. diff --git a/examples/quarry-detection/config_prd.yaml b/examples/quarry-detection/config_prd.yaml index fea16d7..e472dfb 100644 --- a/examples/quarry-detection/config_prd.yaml +++ b/examples/quarry-detection/config_prd.yaml @@ -51,8 +51,8 @@ make_predictions.py: epsilon: 2.0 # cf. https://rdp.readthedocs.io/en/latest/ score_lower_threshold: 0.3 -# 4-Filtering and merging prediction polygons to improve results -filter_detection.py: +# 4-Filtering and merging detection polygons to improve results +filter_detections.py: year: 2020 input: ./output/output_prd/oth_predictions_at_0dot3_threshold.gpkg labels_shapefile: ./data/AOI/AOI_2020.shp diff --git a/examples/quarry-detection/filter_detection.py b/examples/quarry-detection/filter_detections.py similarity index 96% rename from examples/quarry-detection/filter_detection.py rename to examples/quarry-detection/filter_detections.py index abadafb..1a51294 100644 --- a/examples/quarry-detection/filter_detection.py +++ b/examples/quarry-detection/filter_detections.py @@ -75,7 +75,7 @@ te = len(input) logger.info(f"{total - te} detections were removed by elevation threshold: {ELEVATION} m") - # Centroid of every prediction polygon + # Centroid of every detection polygon centroids = gpd.GeoDataFrame() centroids.geometry = input.representative_point() @@ -98,7 +98,7 @@ sc = len(input) logger.info(f"{total - sc} detections were removed by score threshold: {SCORE}") - # Clip prediction to AOI + # Clip detection to AOI input = gpd.clip(input, aoi) # Merge close labels using buffer and unions @@ -121,7 +121,7 @@ data = {'id': geo_merge.index,'area': geo_merge.area, 'centroid_x': geo_merge.centroid.x, 'centroid_y': geo_merge.centroid.y, 'geometry': geo_merge} geo_tmp = gpd.GeoDataFrame(data, crs=input.crs) - # Get the averaged prediction score of the merged polygons + # Get the averaged detection score of the merged polygons intersection = gpd.sjoin(geo_tmp, input, how='inner') intersection['id'] = intersection.index score_final = intersection.groupby(['id']).mean(numeric_only=True) @@ -131,7 +131,7 @@ geo_final = gpd.GeoDataFrame(data, crs=input.crs) logger.info(f"{len(geo_final)} detections remaining after filtering") - # Formatting the output name of the filtered prediction + # Formatting the output name of the filtered detection feature = OUTPUT.replace('{score}', str(SCORE)).replace('0.', '0dot') \ .replace('{year}', str(int(YEAR)))\ .replace('{area}', str(int(AREA)))\ From 22f4cc050fcd5ea1cd6db8f6a0778d7662dfca35 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Tue, 12 Sep 2023 16:43:45 +0200 Subject: [PATCH 078/108] Add -it to docker compose command line in READMEs Enable interactive shell --- README.md | 4 ++-- examples/quarry-detection/README.md | 2 +- examples/swimming-pool-detection/GE/README.md | 2 +- examples/swimming-pool-detection/NE/README.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ff6dfbf..b168653 100644 --- a/README.md +++ b/README.md @@ -85,14 +85,14 @@ These stages/scripts can be run one after the other, by issuing the following co * w/ Docker: ```bash - $ docker compose run --rm stdl-objdet stdl-objdet + $ docker compose run --rm -it stdl-objdet stdl-objdet ``` Alternatively, ```bash - $ docker compose run --rm stdl-objdet + $ docker compose run --rm -it stdl-objdet ``` then diff --git a/examples/quarry-detection/README.md b/examples/quarry-detection/README.md index e9292da..1e2bc92 100644 --- a/examples/quarry-detection/README.md +++ b/examples/quarry-detection/README.md @@ -18,7 +18,7 @@ The workflow can be run end-to-end by issuing the following list of commands, fr ``` $ sudo chown -R 65534:65534 examples -$ docker compose run --rm stdl-objdet +$ docker compose run --rm -it stdl-objdet nobody@:/app# cd examples/quarry-detection nobody@:/app# python prepare_data.py config_trne.yaml nobody@:/app# stdl-objdet generate_tilesets config_trne.yaml diff --git a/examples/swimming-pool-detection/GE/README.md b/examples/swimming-pool-detection/GE/README.md index d58f588..25bb071 100644 --- a/examples/swimming-pool-detection/GE/README.md +++ b/examples/swimming-pool-detection/GE/README.md @@ -10,7 +10,7 @@ The workflow can be run end-to-end by issuing the following list of commands, fr ``` $ sudo chown -R 65534:65534 examples -$ docker compose run --rm stdl-objdet +$ docker compose run --rm -it stdl-objdet nobody@:/app# cd examples/swimming-pool-detection/GE nobody@:/app# python prepare_data.py config_GE.yaml nobody@:/app# cd output_GE && cat parcels.geojson | supermercado burn 18 | mercantile shapes | fio collect > parcels_z18_tiles.geojson && cd - diff --git a/examples/swimming-pool-detection/NE/README.md b/examples/swimming-pool-detection/NE/README.md index 532e203..c6b256e 100644 --- a/examples/swimming-pool-detection/NE/README.md +++ b/examples/swimming-pool-detection/NE/README.md @@ -14,7 +14,7 @@ The workflow can be run end-to-end by issuing the following list of commands, fr ``` $ sudo chown -R 65534:65534 examples -$ docker compose run --rm stdl-objdet +$ docker compose run --rm -it stdl-objdet nobody@:/app# cd examples/swimming-pool-detection/NE nobody@:/app# python prepare_data.py config_NE.yaml nobody@:/app# cd output_NE && cat parcels.geojson | supermercado burn 18 | mercantile shapes | fio collect > parcels_z18_tiles.geojson && cd - From db3b582d5423a62da5e419e79f686448dfbc7146 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Wed, 13 Sep 2023 17:46:08 +0200 Subject: [PATCH 079/108] Add bash script to dl and reproject swiss dem --- Dockerfile | 1 + examples/quarry-detection/README.md | 4 +++- examples/quarry-detection/get_dem.sh | 6 ++++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 examples/quarry-detection/get_dem.sh diff --git a/Dockerfile b/Dockerfile index 3bffc15..5f69ab7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ RUN apt update &&\ apt install -y libgl1 &&\ apt install -y libglib2.0-0 &&\ apt install -y gdal-bin &&\ + apt install -y wget &&\ apt install -y python3-pip &&\ apt install -y python-is-python3 diff --git a/examples/quarry-detection/README.md b/examples/quarry-detection/README.md index 1e2bc92..732a165 100644 --- a/examples/quarry-detection/README.md +++ b/examples/quarry-detection/README.md @@ -10,7 +10,7 @@ It consists of the following elements: - input data in the `data` subfolder: - quarry **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 first re-projected to [EPSG:2056](https://epsg.io/2056), renamed as `switzerland_dem_EPSG2056.tif` and located in the **DEM** subfolder + - 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`. - 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 surface area. The script also identifies and merges groups of polygons and nearby polygons. @@ -28,7 +28,9 @@ nobody@:/app# stdl-objdet assess_predictions config_trne.yaml nobody@:/app# python prepare_data.py config_prd.yaml nobody@:/app# stdl-objdet generate_tilesets config_prd.yaml nobody@:/app# stdl-objdet make_predictions config_prd.yaml +nobody@:/app# get_dem.sh nobody@:/app# python filter_detections.py config_prd.yaml +$ sudo chmod -R a+w examples ``` We strongly encourage the end-user to review the provided `config_trne.yaml` and `config_prd.yaml` files as well as the various output files, a list of which is printed by each script before exiting. diff --git a/examples/quarry-detection/get_dem.sh b/examples/quarry-detection/get_dem.sh new file mode 100644 index 0000000..da7563c --- /dev/null +++ b/examples/quarry-detection/get_dem.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +mkdir -p ./data/DEM/ +wget https://github.com/lukasmartinelli/swissdem/releases/download/v1.0/switzerland_dem.tif -O ./data/DEM/switzerland_dem.tif +gdalwarp -t_srs "EPSG:2056" ./data/DEM/switzerland_dem.tif ./data/DEM/switzerland_dem_EPSG2056.tif +rm ./data/DEM/switzerland_dem.tif \ No newline at end of file From 49a0c420829bbbbdc4ea47770ef5e4d688f80db5 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Wed, 13 Sep 2023 17:49:38 +0200 Subject: [PATCH 080/108] Fixed how-to TOC broken link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b168653..62b7b06 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This project provides a suite of Python scripts allowing the end-user to use Dee - [Hardware](#hardware) - [Software](#software) - [Installation](#installation) -- [How-to](#getting-started) +- [How-to](#how-to) - [Examples](#examples) - [License](#license) From 2c206c2dda72ecb645c95b1fa87b56aaed055469 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Wed, 13 Sep 2023 18:32:40 +0200 Subject: [PATCH 081/108] Add warning for pip install . --- README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 62b7b06..94b91ff 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ A CUDA-enabled GPU is required. * CUDA driver. This code was developed and tested with CUDA 11.3 on Ubuntu 20.04. -* Although we recommend the usage of Docker (see [here](#with-docker)), this code can also be run without Docker, provided that Python 3.8 is available. Python dependencies may be installed with either `pip` or `conda`, using the provided `requirements.txt` file. +* Although we recommend the usage of Docker (see [here](#with-docker)), this code can also be run without Docker, provided that Python 3.8 is available. Python dependencies may be installed with either `pip` or `conda`, using the provided `requirements.txt` file. We advise creating the virtual environment outside of this repository. ## Installation @@ -33,6 +33,11 @@ The object detector can be installed by issuing the following command: ```bash $ pip install . ``` +Note that the above command must be executed in the native repository configuration (no `output` folder containing data produced by the scripts) otherwise, install the dependencies in executable mode: + +```bash +$ pip install -e . +``` In case of a successful installation, the command @@ -56,7 +61,7 @@ In case of a successful build, the command $ docker compose run --rm stdl-objdet stdl-objdet -h ``` -should display some basic usage information. Note that, for the code to properly run, +should display some basic usage information. Note that, for the code to run properly, 1. the version of the CUDA driver installed on the host machine must match with the version used in the [Dockerfile](Dockerfile), namely version 11.3. We let end-user adapt the Dockerfile to her/his environment. 2. The NVIDIA Container Toolkit must be installed on the host machine (see [this guide](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)). @@ -154,7 +159,7 @@ Here's the excerpt of the configuration file relevant to this script, with value generate_tilesets.py: debug_mode: datasets: - aoi_tiles_geojson: + aoi_tiles_geojson: ground_truth_labels_geojson: other_labels_geojson: orthophotos_web_service: @@ -238,11 +243,11 @@ make_predictions.py: pth_file: image_metadata_json: # the following section concerns the Ramer-Douglas-Peucker algorithm, - # which can be optionally applied to detection before they are exported + # which can be optionally applied to prediction before they are exported rdp_simplification: enabled: epsilon: - score_lower_threshold: + score_lower_threshold: ``` ### Stage 4: assessment From 32674959fa32bae1e2cffd9bc25a21e95e7e9ea7 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Wed, 13 Sep 2023 18:36:14 +0200 Subject: [PATCH 082/108] Add command to get back write permission on te created files to swimming pool example --- examples/swimming-pool-detection/GE/README.md | 1 + examples/swimming-pool-detection/GE/prepare_data.py | 3 ++- examples/swimming-pool-detection/NE/README.md | 1 + examples/swimming-pool-detection/NE/prepare_data.py | 3 ++- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/swimming-pool-detection/GE/README.md b/examples/swimming-pool-detection/GE/README.md index 25bb071..7d1aa84 100644 --- a/examples/swimming-pool-detection/GE/README.md +++ b/examples/swimming-pool-detection/GE/README.md @@ -19,6 +19,7 @@ nobody@:/app# stdl-objdet generate_tilesets config_GE.yaml nobody@:/app# stdl-objdet train_model config_GE.yaml nobody@:/app# stdl-objdet make_predictions config_GE.yaml nobody@:/app# stdl-objdet assess_predictions config_GE.yaml +$ sudo chmod -R a+w examples ``` We strongly encourage the end-user to review the provided `config_GE.yaml` file as well as the various output files, a list of which is printed by each script before exiting. diff --git a/examples/swimming-pool-detection/GE/prepare_data.py b/examples/swimming-pool-detection/GE/prepare_data.py index 3120978..c917328 100644 --- a/examples/swimming-pool-detection/GE/prepare_data.py +++ b/examples/swimming-pool-detection/GE/prepare_data.py @@ -1,10 +1,11 @@ #!/bin/python # -*- coding: utf-8 -*- +import os +import sys import time import argparse import yaml -import os, sys import requests import geopandas as gpd import pandas as pd diff --git a/examples/swimming-pool-detection/NE/README.md b/examples/swimming-pool-detection/NE/README.md index c6b256e..d697463 100644 --- a/examples/swimming-pool-detection/NE/README.md +++ b/examples/swimming-pool-detection/NE/README.md @@ -23,6 +23,7 @@ nobody@:/app# stdl-objdet generate_tilesets config_NE.yaml nobody@:/app# stdl-objdet train_model config_NE.yaml nobody@:/app# stdl-objdet make_predictions config_NE.yaml nobody@:/app# stdl-objdet assess_predictions config_NE.yaml +$ sudo chmod -R a+w examples ``` We strongly encourage the end-user to review the provided `config_NE.yaml` file as well as the various output files, a list of which is printed by each script before exiting. diff --git a/examples/swimming-pool-detection/NE/prepare_data.py b/examples/swimming-pool-detection/NE/prepare_data.py index bd725ef..3848326 100644 --- a/examples/swimming-pool-detection/NE/prepare_data.py +++ b/examples/swimming-pool-detection/NE/prepare_data.py @@ -1,10 +1,11 @@ #!/bin/python # -*- coding: utf-8 -*- +import os +import sys import time import argparse import yaml -import os, sys import geopandas as gpd import pandas as pd From 12b228a088a8837e2257a41a4bfa03fbe289f108 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Wed, 13 Sep 2023 18:38:57 +0200 Subject: [PATCH 083/108] Correct import style PEP8 ref in scripts folder --- scripts/assess_predictions.py | 3 ++- scripts/generate_tilesets.py | 3 ++- scripts/make_predictions.py | 8 +++++--- scripts/train_model.py | 5 +++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/scripts/assess_predictions.py b/scripts/assess_predictions.py index 3df3e87..6f3fc15 100644 --- a/scripts/assess_predictions.py +++ b/scripts/assess_predictions.py @@ -1,10 +1,11 @@ #!/bin/python # -*- coding: utf-8 -*- +import os +import sys import time import argparse import yaml -import os, sys import json import geopandas as gpd import pandas as pd diff --git a/scripts/generate_tilesets.py b/scripts/generate_tilesets.py index cc62e9d..92700b3 100644 --- a/scripts/generate_tilesets.py +++ b/scripts/generate_tilesets.py @@ -4,10 +4,11 @@ import warnings warnings.simplefilter(action='ignore', category=FutureWarning) +import os +import sys import time import argparse import yaml -import os, sys import geopandas as gpd import pandas as pd import json diff --git a/scripts/make_predictions.py b/scripts/make_predictions.py index 91d80ac..90ecf51 100644 --- a/scripts/make_predictions.py +++ b/scripts/make_predictions.py @@ -4,11 +4,13 @@ import warnings warnings.simplefilter(action='ignore', category=UserWarning) -import os, sys +import os +import sys +import time import argparse -import json, yaml +import yaml +import json import cv2 -import time import geopandas as gpd from tqdm import tqdm diff --git a/scripts/train_model.py b/scripts/train_model.py index 960d257..d49ef9b 100644 --- a/scripts/train_model.py +++ b/scripts/train_model.py @@ -1,11 +1,12 @@ #!/usr/bin/env python # coding: utf-8 +import os +import sys +import time import argparse import yaml -import os, sys import cv2 -import time from detectron2.utils.logger import setup_logger setup_logger() From 7b6a370b0fee8cd776206e79b3271c0185731be8 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Wed, 13 Sep 2023 18:44:49 +0200 Subject: [PATCH 084/108] Change detections to predictions in quarry example to match swimming pool example --- examples/quarry-detection/README.md | 4 ++-- examples/quarry-detection/config_prd.yaml | 4 ++-- .../{filter_detections.py => filter_predictions.py} | 0 3 files changed, 4 insertions(+), 4 deletions(-) rename examples/quarry-detection/{filter_detections.py => filter_predictions.py} (100%) diff --git a/examples/quarry-detection/README.md b/examples/quarry-detection/README.md index 732a165..169c720 100644 --- a/examples/quarry-detection/README.md +++ b/examples/quarry-detection/README.md @@ -12,7 +12,7 @@ It consists of the following elements: - 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`. - 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 surface area. The script also identifies and merges groups of polygons and nearby polygons. +- a post-processing script (`filter_predictions.py`) which filters detections according to their confidence score, altitude and surface area. The script also identifies and merges groups of polygons and nearby polygons. The workflow can be run end-to-end by issuing the following list of commands, from the root folder of this GitHub repository: @@ -29,7 +29,7 @@ nobody@:/app# python prepare_data.py config_prd.yaml nobody@:/app# stdl-objdet generate_tilesets config_prd.yaml nobody@:/app# stdl-objdet make_predictions config_prd.yaml nobody@:/app# get_dem.sh -nobody@:/app# python filter_detections.py config_prd.yaml +nobody@:/app# python filter_predictions.py config_prd.yaml $ sudo chmod -R a+w examples ``` diff --git a/examples/quarry-detection/config_prd.yaml b/examples/quarry-detection/config_prd.yaml index e472dfb..2defd6a 100644 --- a/examples/quarry-detection/config_prd.yaml +++ b/examples/quarry-detection/config_prd.yaml @@ -52,7 +52,7 @@ make_predictions.py: score_lower_threshold: 0.3 # 4-Filtering and merging detection polygons to improve results -filter_detections.py: +filter_predictions.py: year: 2020 input: ./output/output_prd/oth_predictions_at_0dot3_threshold.gpkg labels_shapefile: ./data/AOI/AOI_2020.shp @@ -61,4 +61,4 @@ filter_detections.py: score: 0.95 # prediction 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/output_prd/oth_prediction_at_0dot3_threshold_year-{year}_score-{score}_area-{area}_elevation-{elevation}_distance-{distance}.geojson \ No newline at end of file + output: ./output/output_prd/oth_predictions_at_0dot3_threshold_year-{year}_score-{score}_area-{area}_elevation-{elevation}_distance-{distance}.geojson \ No newline at end of file diff --git a/examples/quarry-detection/filter_detections.py b/examples/quarry-detection/filter_predictions.py similarity index 100% rename from examples/quarry-detection/filter_detections.py rename to examples/quarry-detection/filter_predictions.py From e8e273d0ae0efe2c49c1e721ca7699545c6619be Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Wed, 13 Sep 2023 18:55:03 +0200 Subject: [PATCH 085/108] Add missing bash command in README.md command lines --- examples/quarry-detection/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/quarry-detection/README.md b/examples/quarry-detection/README.md index 169c720..edd05ca 100644 --- a/examples/quarry-detection/README.md +++ b/examples/quarry-detection/README.md @@ -28,7 +28,7 @@ nobody@:/app# stdl-objdet assess_predictions config_trne.yaml nobody@:/app# python prepare_data.py config_prd.yaml nobody@:/app# stdl-objdet generate_tilesets config_prd.yaml nobody@:/app# stdl-objdet make_predictions config_prd.yaml -nobody@:/app# get_dem.sh +nobody@:/app# bash get_dem.sh nobody@:/app# python filter_predictions.py config_prd.yaml $ sudo chmod -R a+w examples ``` From ed96a5cb1d8eba7f072f4f660110b6a516f3cf62 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Wed, 13 Sep 2023 19:08:05 +0200 Subject: [PATCH 086/108] Correct comma typo in the main README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 94b91ff..52839e9 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ The object detector can be installed by issuing the following command: ```bash $ pip install . ``` -Note that the above command must be executed in the native repository configuration (no `output` folder containing data produced by the scripts) otherwise, install the dependencies in executable mode: +Note that the above command must be executed in the native repository configuration (no `output` folder containing data produced by the scripts), otherwise install the dependencies in executable mode: ```bash $ pip install -e . From 12e33a1f24fb81d5ce749981ec66863fe5499bd5 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Thu, 14 Sep 2023 10:17:11 +0200 Subject: [PATCH 087/108] Correct AOI to AoI in quarry example --- examples/quarry-detection/config_prd.yaml | 6 +++--- examples/quarry-detection/config_trne.yaml | 2 +- .../data/{AOI/AOI_2020.cpg => AoI/AoI_2020.cpg} | 0 .../data/{AOI/AOI_2020.dbf => AoI/AoI_2020.dbf} | Bin .../data/{AOI/AOI_2020.prj => AoI/AoI_2020.prj} | 0 .../data/{AOI/AOI_2020.shp => AoI/AoI_2020.shp} | Bin .../data/{AOI/AOI_2020.shx => AoI/AoI_2020.shx} | Bin examples/quarry-detection/filter_predictions.py | 2 +- examples/quarry-detection/prepare_data.py | 2 +- 9 files changed, 6 insertions(+), 6 deletions(-) rename examples/quarry-detection/data/{AOI/AOI_2020.cpg => AoI/AoI_2020.cpg} (100%) rename examples/quarry-detection/data/{AOI/AOI_2020.dbf => AoI/AoI_2020.dbf} (100%) rename examples/quarry-detection/data/{AOI/AOI_2020.prj => AoI/AoI_2020.prj} (100%) rename examples/quarry-detection/data/{AOI/AOI_2020.shp => AoI/AoI_2020.shp} (100%) rename examples/quarry-detection/data/{AOI/AOI_2020.shx => AoI/AoI_2020.shx} (100%) diff --git a/examples/quarry-detection/config_prd.yaml b/examples/quarry-detection/config_prd.yaml index 2defd6a..2200d85 100644 --- a/examples/quarry-detection/config_prd.yaml +++ b/examples/quarry-detection/config_prd.yaml @@ -2,11 +2,11 @@ ####### Inference detection ####### # Automatic detection of Quarries and Mineral Extraction Sites (MES) in images -# 1-Produce tiles geometry according to the AOI extent and zoom level +# 1-Produce tiles geometry according to the AoI extent and zoom level prepare_data.py: srs: "EPSG:2056" # Projection of the input file datasets: - labels_shapefile: ./data/AOI/AOI_2020.shp + labels_shapefile: ./data/AoI/AoI_2020.shp output_folder: ./output/output_prd zoom_level: 16 @@ -55,7 +55,7 @@ make_predictions.py: filter_predictions.py: year: 2020 input: ./output/output_prd/oth_predictions_at_0dot3_threshold.gpkg - labels_shapefile: ./data/AOI/AOI_2020.shp + labels_shapefile: ./data/AoI/AoI_2020.shp dem: ./data/DEM/switzerland_dem_EPSG2056.tif elevation: 1200.0 # m, altitude threshold score: 0.95 # prediction score (from 0 to 1) provided by detectron2 diff --git a/examples/quarry-detection/config_trne.yaml b/examples/quarry-detection/config_trne.yaml index 5bf50fb..f84dd82 100644 --- a/examples/quarry-detection/config_trne.yaml +++ b/examples/quarry-detection/config_trne.yaml @@ -2,7 +2,7 @@ ####### Model training and evaluation ####### # Training of automatic detection of Quarries and Mineral Extraction Sites (MES) in images with a provided ground truth -# 1-Produce tiles geometry according to the AOI extent and zoom level +# 1-Produce tiles geometry according to the AoI extent and zoom level prepare_data.py: srs: "EPSG:2056" datasets: diff --git a/examples/quarry-detection/data/AOI/AOI_2020.cpg b/examples/quarry-detection/data/AoI/AoI_2020.cpg similarity index 100% rename from examples/quarry-detection/data/AOI/AOI_2020.cpg rename to examples/quarry-detection/data/AoI/AoI_2020.cpg diff --git a/examples/quarry-detection/data/AOI/AOI_2020.dbf b/examples/quarry-detection/data/AoI/AoI_2020.dbf similarity index 100% rename from examples/quarry-detection/data/AOI/AOI_2020.dbf rename to examples/quarry-detection/data/AoI/AoI_2020.dbf diff --git a/examples/quarry-detection/data/AOI/AOI_2020.prj b/examples/quarry-detection/data/AoI/AoI_2020.prj similarity index 100% rename from examples/quarry-detection/data/AOI/AOI_2020.prj rename to examples/quarry-detection/data/AoI/AoI_2020.prj diff --git a/examples/quarry-detection/data/AOI/AOI_2020.shp b/examples/quarry-detection/data/AoI/AoI_2020.shp similarity index 100% rename from examples/quarry-detection/data/AOI/AOI_2020.shp rename to examples/quarry-detection/data/AoI/AoI_2020.shp diff --git a/examples/quarry-detection/data/AOI/AOI_2020.shx b/examples/quarry-detection/data/AoI/AoI_2020.shx similarity index 100% rename from examples/quarry-detection/data/AOI/AOI_2020.shx rename to examples/quarry-detection/data/AoI/AoI_2020.shx diff --git a/examples/quarry-detection/filter_predictions.py b/examples/quarry-detection/filter_predictions.py index 1a51294..f6be5d3 100644 --- a/examples/quarry-detection/filter_predictions.py +++ b/examples/quarry-detection/filter_predictions.py @@ -98,7 +98,7 @@ sc = len(input) logger.info(f"{total - sc} detections were removed by score threshold: {SCORE}") - # Clip detection to AOI + # Clip detection to AoI input = gpd.clip(input, aoi) # Merge close labels using buffer and unions diff --git a/examples/quarry-detection/prepare_data.py b/examples/quarry-detection/prepare_data.py index ea4bbca..05a64a3 100644 --- a/examples/quarry-detection/prepare_data.py +++ b/examples/quarry-detection/prepare_data.py @@ -74,7 +74,7 @@ def add_tile_id(row): written_files.append(label_filepath) logger.success(f"{DONE_MSG} A file was written: {label_filepath}") - logger.info('Creating tiles for the Area of Interest (AOI)...') + logger.info('Creating tiles for the Area of Interest (AoI)...') # Grid definition tms = morecantile.tms.get("WebMercatorQuad") # epsg:3857 From 037ed5fa6fbcf0835383b52aa4a6fe23de8c8b56 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Thu, 14 Sep 2023 10:42:26 +0200 Subject: [PATCH 088/108] Correct AOI to AoI in swinning-pool examples --- examples/swimming-pool-detection/GE/prepare_data.py | 4 ++-- examples/swimming-pool-detection/NE/prepare_data.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/swimming-pool-detection/GE/prepare_data.py b/examples/swimming-pool-detection/GE/prepare_data.py index c917328..7962c08 100644 --- a/examples/swimming-pool-detection/GE/prepare_data.py +++ b/examples/swimming-pool-detection/GE/prepare_data.py @@ -64,9 +64,9 @@ logger.success(f"...done. {len(dataset_dict[dataset])} records were found.") - # ------ Computing the Area of Interest (AOI) = cadastral parcels - Léman lake + # ------ Computing the Area of Interest (AoI) = cadastral parcels - Léman lake - logger.info("Computing the Area of Interest (AOI)...") + logger.info("Computing the Area of Interest (AoI)...") # N.B.: # it's faster to first compute Slippy Map Tiles (cf. https://developers.planet.com/tutorials/slippy-maps-101/), diff --git a/examples/swimming-pool-detection/NE/prepare_data.py b/examples/swimming-pool-detection/NE/prepare_data.py index 3848326..9af9c71 100644 --- a/examples/swimming-pool-detection/NE/prepare_data.py +++ b/examples/swimming-pool-detection/NE/prepare_data.py @@ -60,7 +60,7 @@ logger.success(f"...done. {len(dataset_dict[dataset])} records were found.") - # ------ Computing the Area of Interest (AOI) + # ------ Computing the Area of Interest (AoI) aoi_gdf = pd.concat([ dataset_dict['ground_truth_sectors'], From 1bdf275c3e09093eb62178230901b46eda0dd715 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Thu, 14 Sep 2023 16:36:20 +0000 Subject: [PATCH 089/108] Set MPLCONFIGDIR env variable to a writable directory --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 5f69ab7..7a4a16d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,5 +22,7 @@ RUN pip install . USER 65534:65534 +ENV MPLCONFIGDIR /tmp + ENTRYPOINT [""] CMD ["stdl-objdet", "-h"] From dda49b1092886af07bb3c45d86ea89000d8f033f Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Thu, 14 Sep 2023 16:52:05 +0000 Subject: [PATCH 090/108] Improve README.md concerning Python venvs and editable installs --- README.md | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 52839e9..39a1d1a 100644 --- a/README.md +++ b/README.md @@ -22,21 +22,16 @@ A CUDA-enabled GPU is required. * CUDA driver. This code was developed and tested with CUDA 11.3 on Ubuntu 20.04. -* Although we recommend the usage of Docker (see [here](#with-docker)), this code can also be run without Docker, provided that Python 3.8 is available. Python dependencies may be installed with either `pip` or `conda`, using the provided `requirements.txt` file. We advise creating the virtual environment outside of this repository. +* Although we recommend the usage of Docker (see [here](#with-docker)), this code can also be run without Docker, provided that Python 3.8 is available. Python dependencies may be installed with either `pip` or `conda`, using the provided `requirements.txt` file. We advise using a [Python virtual environment](https://docs.python.org/3/library/venv.html). ## Installation ### Without Docker -The object detector can be installed by issuing the following command: +The object detector can be installed by issuing the following command (see [this page](https://setuptools.pypa.io/en/latest/userguide/development_mode.html) for more information on the "editable install"): ```bash -$ pip install . -``` -Note that the above command must be executed in the native repository configuration (no `output` folder containing data produced by the scripts), otherwise install the dependencies in executable mode: - -```bash -$ pip install -e . +$ pip install --editable . ``` In case of a successful installation, the command From ead4f4fa6bdf095123d9d2e3a33aced6c562b1e9 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Fri, 15 Sep 2023 08:20:06 +0000 Subject: [PATCH 091/108] Minor improvements --- examples/quarry-detection/README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/examples/quarry-detection/README.md b/examples/quarry-detection/README.md index edd05ca..97e01d7 100644 --- a/examples/quarry-detection/README.md +++ b/examples/quarry-detection/README.md @@ -4,15 +4,15 @@ A sample working setup is provided here, enabling the end-user to detect quarrie It consists of the following elements: - ready-to-use configuration files: - - `config_trne.yaml` - - `config_prd.yaml` - - `detectron2_config_dqry.yaml` -- input data in the `data` subfolder: - - quarry **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)** + - `config_trne.yaml`; + - `config_prd.yaml`; + - `detectron2_config_dqry.yaml`. +- Input data in the `data` subfolder: + - quarry **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`. -- 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_predictions.py`) which filters detections according to their confidence score, altitude and surface area. The script also identifies and merges groups of polygons and nearby polygons. +- 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_predictions.py`) which filters detections according to their confidence score, altitude and area. The script also identifies and merges groups of nearby polygons. The workflow can be run end-to-end by issuing the following list of commands, from the root folder of this GitHub repository: @@ -30,6 +30,7 @@ nobody@:/app# stdl-objdet generate_tilesets config_prd.yaml nobody@:/app# stdl-objdet make_predictions config_prd.yaml nobody@:/app# bash get_dem.sh nobody@:/app# python filter_predictions.py config_prd.yaml +nobody@:/app# exit $ sudo chmod -R a+w examples ``` From d4f5d332af116595926f3337f7987ecf40959629 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Fri, 15 Sep 2023 09:03:22 +0000 Subject: [PATCH 092/108] Imports on separate lines in order to comply with PEP 8 --- helpers/COCO.py | 3 ++- helpers/MIL.py | 3 ++- helpers/WMS.py | 3 ++- helpers/XYZ.py | 3 ++- helpers/detectron2.py | 2 +- helpers/misc.py | 3 ++- 6 files changed, 11 insertions(+), 6 deletions(-) diff --git a/helpers/COCO.py b/helpers/COCO.py index b7b65a3..948b853 100644 --- a/helpers/COCO.py +++ b/helpers/COCO.py @@ -1,7 +1,8 @@ #!/bin/python # -*- coding: utf-8 -*- -import os, sys +import os +import sys import json import numpy as np import logging diff --git a/helpers/MIL.py b/helpers/MIL.py index 3780223..66b0783 100644 --- a/helpers/MIL.py +++ b/helpers/MIL.py @@ -1,7 +1,8 @@ #!/bin/python # -*- coding: utf-8 -*- -import os, sys +import os +import sys import json import requests diff --git a/helpers/WMS.py b/helpers/WMS.py index a28f195..44b9baa 100644 --- a/helpers/WMS.py +++ b/helpers/WMS.py @@ -1,7 +1,8 @@ #!/bin/python # -*- coding: utf-8 -*- -import os, sys +import os +import sys import json import requests diff --git a/helpers/XYZ.py b/helpers/XYZ.py index 292d1a9..66d0c5e 100644 --- a/helpers/XYZ.py +++ b/helpers/XYZ.py @@ -1,7 +1,8 @@ #!/bin/python # -*- coding: utf-8 -*- -import os, sys +import os +import sys import json import requests diff --git a/helpers/detectron2.py b/helpers/detectron2.py index 0a654fd..d605bc9 100644 --- a/helpers/detectron2.py +++ b/helpers/detectron2.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # coding: utf-8 -import os, sys +import os import time import torch import numpy as np diff --git a/helpers/misc.py b/helpers/misc.py index 04274de..f0f4010 100644 --- a/helpers/misc.py +++ b/helpers/misc.py @@ -4,7 +4,8 @@ import warnings warnings.simplefilter(action='ignore', category=FutureWarning) -import os, sys +import os +import sys import geopandas as gpd from shapely.affinity import scale From 21e2dd3206a7c70125a5eca522c2dfe4c1ea02c3 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Fri, 15 Sep 2023 10:58:33 +0000 Subject: [PATCH 093/108] Fix path to file --- examples/quarry-detection/config_prd.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/quarry-detection/config_prd.yaml b/examples/quarry-detection/config_prd.yaml index 2200d85..802ce87 100644 --- a/examples/quarry-detection/config_prd.yaml +++ b/examples/quarry-detection/config_prd.yaml @@ -44,7 +44,7 @@ make_predictions.py: oth: COCO_oth.json detectron2_config_file: '../../detectron2_config_dqry.yaml' # path relative to the working_folder model_weights: - pth_file: '../output-trne/logs/model_0002999.pth' # trained model minimizing the validation loss curve, monitor the training process via tensorboard (tensorboard --logdir ) + pth_file: '../output_trne/logs/model_0002999.pth' # trained model minimizing the validation loss curve, monitor the training process via tensorboard (tensorboard --logdir ) image_metadata_json: './output/output_prd/img_metadata.json' rdp_simplification: # rdp = Ramer-Douglas-Peucker enabled: True From 85bf5e54077fdfff3f6eac28124283f9e0dd5b96 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Fri, 15 Sep 2023 14:24:01 +0200 Subject: [PATCH 094/108] Correct links + change predictions to detections --- README.md | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 39a1d1a..5c28293 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,13 @@ A CUDA-enabled GPU is required. * CUDA driver. This code was developed and tested with CUDA 11.3 on Ubuntu 20.04. -* Although we recommend the usage of Docker (see [here](#with-docker)), this code can also be run without Docker, provided that Python 3.8 is available. Python dependencies may be installed with either `pip` or `conda`, using the provided `requirements.txt` file. We advise using a [Python virtual environment](https://docs.python.org/3/library/venv.html). +* Although we recommend the usage of [Docker](https://www.docker.com/) (see [here](#with-docker)), this code can also be run without Docker, provided that Python 3.8 is available. Python dependencies may be installed with either `pip` or `conda`, using the provided `requirements.txt` file. We advise using a [Python virtual environment](https://docs.python.org/3/library/venv.html). ## Installation ### Without Docker -The object detector can be installed by issuing the following command (see [this page](https://setuptools.pypa.io/en/latest/userguide/development_mode.html) for more information on the "editable install"): +The object detector can be installed by issuing the following command (see [this page](https://setuptools.pypa.io/en/latest/userguide/development_mode.html) for more information on the "editable installs"): ```bash $ pip install --editable . @@ -71,8 +71,8 @@ This project implements the workflow described [here](https://tech.stdl.ch/TASK- | :-: | --- | --- | --- | | 1 | Tileset generation | `generate_tilesets` | [here](scripts/generate_tilesets.py) | | 2 | Model training | `train_model` | [here](scripts/train_model.py) | -| 3 | Prediction | `make_predictions` | [here](scripts/train_model.py) | -| 4 | Assessment | `assess_predictions` | [here](scripts/assess_predictions.py) | +| 3 | Prediction | `make_detections` | [here](scripts/make_detections.py) | +| 4 | Assessment | `assess_detections` | [here](scripts/assess_detections.py) | These stages/scripts can be run one after the other, by issuing the following command from a terminal: @@ -105,7 +105,7 @@ These stages/scripts can be run one after the other, by issuing the following co The same configuration file can be used for all the commands, as each of them only reads the content related to a key named after its name. More detailed information about each stage and the related configuration is provided here-below. The following terminology is used: -* **ground-truth data**: data to be used to train the Deep Learning-based predictive model; such data is expected to be 100% true +* **ground-truth data**: data to be used to train the Deep Learning-based detection model; such data is expected to be 100% true * **GT**: abbreviation of ground-truth @@ -194,7 +194,7 @@ Note that: > **Note** This stage can be skipped if the user wishes to perform inference only, using a pre-trained model. -The `train_model` command allows one to train a predictive model based on a Convolutional Deep Neural Network, leveraging [FAIR's Detectron2](https://github.com/facebookresearch/detectron2). For further information, we refer the user to the [official documention](https://detectron2.readthedocs.io/en/latest/). +The `train_model` command allows one to train a detection model based on a Convolutional Deep Neural Network, leveraging [FAIR's Detectron2](https://github.com/facebookresearch/detectron2). For further information, we refer the user to the [official documention](https://detectron2.readthedocs.io/en/latest/). Here's the excerpt of the configuration file relevant to this script, with values replaced by textual documentation: @@ -214,17 +214,17 @@ train_model.py: Detectron2 configuration files are provided in the example folders mentioned here-below. We warn the end-user about the fact that, **for the time being, no hyperparameters tuning is automatically performed**. -### Stage 3: prediction +### Stage 3: detection -The `make_predictions` command allows one to use the predictive model trained at the previous step to make predictions over various input datasets: +The `make_detections` command allows one to use the detection model trained at the previous step to make detections over various input datasets: -* predictions over the `trn`, `val`, `tst` datasets can be used to assess the reliability of this approach on ground-truth data; -* predictions over the `oth` dataset are, in principle, the main goal of this kind of analyses. +* detections over the `trn`, `val`, `tst` datasets can be used to assess the reliability of this approach on ground-truth data; +* detections over the `oth` dataset are, in principle, the main goal of this kind of analyses. Here's the excerpt of the configuration file relevant to this script, with values replaced by textual documentation: ```yaml -make_predictions.py: +make_detections.py: working_folder: log_subfolder: sample_tagged_img_subfolder: @@ -238,24 +238,24 @@ make_predictions.py: pth_file: image_metadata_json: # the following section concerns the Ramer-Douglas-Peucker algorithm, - # which can be optionally applied to prediction before they are exported + # which can be optionally applied to detection before they are exported rdp_simplification: enabled: epsilon: - score_lower_threshold: + score_lower_threshold: ``` ### Stage 4: assessment -The `assess_predictions` command allows one to assess the reliability of predictions, comparing predictions with ground-truth data. The assessment goes through the following steps: +The `assess_detections` command allows one to assess the reliability of detections, comparing detections with ground-truth data. The assessment goes through the following steps: 1. Labels (GT + `oth`) geometries are clipped to the boundaries of the various AoI tiles, scaled by a factor 0.999 in order to prevent any "crosstalk" between neighboring tiles. -2. Vector features are extracted from Detectron2's predictions, which are originally in a raster format (`numpy` arrays, to be more precise). +2. Vector features are extracted from Detectron2's detections, which are originally in a raster format (`numpy` arrays, to be more precise). -3. Spatial joins are computed between the vectorized predictions and the clipped labels, in order to identify - * True Positives (TP), *i.e.* objects that are found in both datasets, labels and predictions; - * False Positives (FP), *i.e.* objects that are only found in the predictions dataset; +3. Spatial joins are computed between the vectorized detections and the clipped labels, in order to identify + * True Positives (TP), *i.e.* objects that are found in both datasets, labels and detections; + * False Positives (FP), *i.e.* objects that are only found in the detections dataset; * False Negatives (FN), *i.e.* objects that are only found in the labels dataset. 4. Finally, TPs, FPs and FNs are counted in order to compute the following metrics (see [this page](https://en.wikipedia.org/wiki/Precision_and_recall)) : @@ -265,17 +265,17 @@ The `assess_predictions` command allows one to assess the reliability of predict Here's the excerpt of the configuration file relevant to this command, with values replaced by textual documentation: ```yaml -assess_predictions.py: +assess_detections.py: datasets: ground_truth_labels_geojson: other_labels_geojson: image_metadata_json: split_aoi_tiles_geojson: - predictions: - trn: - val: - tst: - oth: + detections: + trn: + val: + tst: + oth: output_folder: ``` From 8b99d25af059e16c46f2416dede61fe8fe58acdf Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Fri, 15 Sep 2023 14:26:12 +0200 Subject: [PATCH 095/108] Change predictions to detections in examples READMEs --- examples/quarry-detection/README.md | 10 +++++----- examples/swimming-pool-detection/GE/README.md | 4 ++-- examples/swimming-pool-detection/NE/README.md | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/quarry-detection/README.md b/examples/quarry-detection/README.md index 97e01d7..776e146 100644 --- a/examples/quarry-detection/README.md +++ b/examples/quarry-detection/README.md @@ -12,7 +12,7 @@ It consists of the following elements: - 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`. - 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_predictions.py`) which filters detections according to their confidence score, altitude and area. The script also identifies and merges groups of nearby polygons. +- 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. The workflow can be run end-to-end by issuing the following list of commands, from the root folder of this GitHub repository: @@ -23,13 +23,13 @@ nobody@:/app# cd examples/quarry-detection nobody@:/app# python prepare_data.py config_trne.yaml nobody@:/app# stdl-objdet generate_tilesets config_trne.yaml nobody@:/app# stdl-objdet train_model config_trne.yaml -nobody@:/app# stdl-objdet make_predictions config_trne.yaml -nobody@:/app# stdl-objdet assess_predictions config_trne.yaml +nobody@:/app# stdl-objdet make_detections config_trne.yaml +nobody@:/app# stdl-objdet assess_detections config_trne.yaml nobody@:/app# python prepare_data.py config_prd.yaml nobody@:/app# stdl-objdet generate_tilesets config_prd.yaml -nobody@:/app# stdl-objdet make_predictions config_prd.yaml +nobody@:/app# stdl-objdet make_detections config_prd.yaml nobody@:/app# bash get_dem.sh -nobody@:/app# python filter_predictions.py config_prd.yaml +nobody@:/app# python filter_detections.py config_prd.yaml nobody@:/app# exit $ sudo chmod -R a+w examples ``` diff --git a/examples/swimming-pool-detection/GE/README.md b/examples/swimming-pool-detection/GE/README.md index 7d1aa84..7c3e779 100644 --- a/examples/swimming-pool-detection/GE/README.md +++ b/examples/swimming-pool-detection/GE/README.md @@ -17,8 +17,8 @@ nobody@:/app# cd output_GE && cat parcels.geojson | supermercado burn 18 | m nobody@:/app# python prepare_data.py config_GE.yaml nobody@:/app# stdl-objdet generate_tilesets config_GE.yaml nobody@:/app# stdl-objdet train_model config_GE.yaml -nobody@:/app# stdl-objdet make_predictions config_GE.yaml -nobody@:/app# stdl-objdet assess_predictions config_GE.yaml +nobody@:/app# stdl-objdet make_detections config_GE.yaml +nobody@:/app# stdl-objdet assess_detections config_GE.yaml $ sudo chmod -R a+w examples ``` diff --git a/examples/swimming-pool-detection/NE/README.md b/examples/swimming-pool-detection/NE/README.md index d697463..6fc7ef8 100644 --- a/examples/swimming-pool-detection/NE/README.md +++ b/examples/swimming-pool-detection/NE/README.md @@ -21,8 +21,8 @@ nobody@:/app# cd output_NE && cat parcels.geojson | supermercado burn 18 | m nobody@:/app# python prepare_data.py config_NE.yaml nobody@:/app# stdl-objdet generate_tilesets config_NE.yaml nobody@:/app# stdl-objdet train_model config_NE.yaml -nobody@:/app# stdl-objdet make_predictions config_NE.yaml -nobody@:/app# stdl-objdet assess_predictions config_NE.yaml +nobody@:/app# stdl-objdet make_detections config_NE.yaml +nobody@:/app# stdl-objdet assess_detections config_NE.yaml $ sudo chmod -R a+w examples ``` From 627a5dae31cd3a9f9097ef9a5b4fb5d2072e5980 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Fri, 15 Sep 2023 12:28:33 +0000 Subject: [PATCH 096/108] Remove file --- .../quarry-detection/filter_prediction.py | 155 ------------------ 1 file changed, 155 deletions(-) delete mode 100644 examples/quarry-detection/filter_prediction.py diff --git a/examples/quarry-detection/filter_prediction.py b/examples/quarry-detection/filter_prediction.py deleted file mode 100644 index 3c297e5..0000000 --- a/examples/quarry-detection/filter_prediction.py +++ /dev/null @@ -1,155 +0,0 @@ -#!/bin/python -# -*- coding: utf-8 -*- - -# This source code is licensed under the license found in the -# LICENSE file in the root directory of this source tree. - -import os -import sys -import inspect -import time -import argparse -import yaml -from loguru import logger - -import geopandas as gpd -import pandas as pd -import rasterio -from sklearn.cluster import KMeans - - -# the following allows us to import modules from within this file's parent folder -sys.path.insert(0, '.') - -logger.remove() -logger.add(sys.stderr, format="{time:YYYY-MM-DD HH:mm:ss} - {level} - {message}", level="INFO") - - -if __name__ == "__main__": - - # Chronometer - tic = time.time() - logger.info('Starting...') - - # argument parser - parser = argparse.ArgumentParser(description="The script filters the detection of potential Mineral Extraction Sites obtained with the object-detector scripts") - parser.add_argument('config_file', type=str, help='input geojson path') - args = parser.parse_args() - - logger.info(f"Using {args.config_file} as config file.") - - with open(args.config_file) as fp: - cfg = yaml.load(fp, Loader=yaml.FullLoader)[os.path.basename(__file__)] - - # Load input parameters - YEAR = cfg['year'] - INPUT = cfg['input'] - LABELS_SHPFILE = cfg['labels_shapefile'] - DEM = cfg['dem'] - SCORE = cfg['score'] - AREA = cfg['area'] - ELEVATION = cfg['elevation'] - DISTANCE = cfg['distance'] - OUTPUT = cfg['output'] - - written_files = [] - - # Convert input detection to a geo dataframe - aoi = gpd.read_file(LABELS_SHPFILE) - aoi = aoi.to_crs(epsg=2056) - - input = gpd.read_file(INPUT) - input = input.to_crs(2056) - total = len(input) - logger.info(f"Total input = {total}") - - # Discard polygons detected above the threshold elevalation and 0 m - r = rasterio.open(DEM) - row, col = r.index(input.centroid.x, input.centroid.y) - values = r.read(1)[row, col] - input['elev'] = values - input = input[input.elev < ELEVATION] - row, col = r.index(input.centroid.x, input.centroid.y) - values = r.read(1)[row, col] - input['elev'] = values - - input = input[input.elev != 0] - te = len(input) - logger.info(f"{str(total - te)} predictions removed by elevation threshold: {str(ELEVATION)} m") - - # Centroid of every prediction polygon - centroids = gpd.GeoDataFrame() - centroids.geometry = input.representative_point() - - # KMeans Unsupervised Learning - centroids = pd.DataFrame({'x': centroids.geometry.x, 'y': centroids.geometry.y}) - k = int((len(input)/3) + 1) - cluster = KMeans(n_clusters=k, algorithm='auto', random_state=1) - model = cluster.fit(centroids) - labels = model.predict(centroids) - logger.info(f"KMeans algorithm computed with k = {str(k)}") - - # Dissolve and Aggregate (keep the max value of aggregate attributes) - input['cluster'] = labels - - input = input.dissolve(by='cluster', aggfunc='max') - total = len(input) - - # Filter dataframe by score value - input = input[input['score'] > SCORE] - sc = len(input) - logger.info(f"{str(total - sc)} predictions removed by score threshold: {str(SCORE)}") - - # Clip prediction to AOI - input = gpd.clip(input, aoi) - - # Create empty data frame - geo_merge = gpd.GeoDataFrame() - # Merge close labels using buffer and unions - geo_merge = input.buffer(+DISTANCE, resolution = 2) - geo_merge = geo_merge.geometry.unary_union - geo_merge = gpd.GeoDataFrame(geometry=[geo_merge], crs = input.crs) - geo_merge = geo_merge.explode(index_parts=True).reset_index(drop=True) - geo_merge = geo_merge.buffer(-DISTANCE, resolution=2) - - td = len(geo_merge) - logger.info(f"{str(sc - td)} difference to clustered predictions after union (distance {str(DISTANCE)})") - - # Discard polygons with area under the threshold - geo_merge = geo_merge[geo_merge.area > AREA] - ta = len(geo_merge) - logger.info(f"{str(td - ta)} difference to clustered predictions after union (distance {str(AREA)})") - - # Preparation of a geo df - data = {'id': geo_merge.index,'area': geo_merge.area, 'centroid_x': geo_merge.centroid.x, 'centroid_y': geo_merge.centroid.y, 'geometry': geo_merge} - geo_tmp = gpd.GeoDataFrame(data, crs=input.crs) - - # Get the averaged prediction score of the merged polygons - intersection = gpd.sjoin(geo_tmp, input, how='inner') - intersection['id'] = intersection.index - score_final=intersection.groupby(['id']).mean(numeric_only=True) - # Formatting the final geo df - data = {'id_feature': geo_merge.index,'score': score_final['score'] , 'area': geo_merge.area, 'centroid_x': geo_merge.centroid.x, 'centroid_y': geo_merge.centroid.y, 'geometry': geo_merge} - geo_final = gpd.GeoDataFrame(data, crs=input.crs) - logger.info(f"{len(geo_final)} predictions remaining") - - # Format the ooutput name of the filtered prediction - feature = OUTPUT.replace('{score}', str(SCORE)).replace('0.', '0dot') \ - .replace('{year}', str(int(YEAR)))\ - .replace('{area}', str(int(AREA)))\ - .replace('{elevation}', str(int(ELEVATION))) \ - .replace('{distance}', str(int(DISTANCE))) - geo_final.to_file(feature, driver='GeoJSON') - - written_files.append(feature) - logger.info(f"...done. A file was written: {feature}") - - logger.info("The following files were written. Let's check them out!") - for written_file in written_files: - logger.info(written_file) - - # Stop chronometer - toc = time.time() - logger.info(f"Nothing left to be done: exiting. Elapsed time: {(toc-tic):.2f} seconds") - - sys.stderr.flush() \ No newline at end of file From 1a6f19757b7513235d6b05ccbaa8eaf78933ce37 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Fri, 15 Sep 2023 14:28:37 +0200 Subject: [PATCH 097/108] Change predictions to detections in scripts folder --- scripts/{assess_predictions.py => assess_detections.py} | 0 scripts/{make_predictions.py => make_detections.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename scripts/{assess_predictions.py => assess_detections.py} (100%) rename scripts/{make_predictions.py => make_detections.py} (100%) diff --git a/scripts/assess_predictions.py b/scripts/assess_detections.py similarity index 100% rename from scripts/assess_predictions.py rename to scripts/assess_detections.py diff --git a/scripts/make_predictions.py b/scripts/make_detections.py similarity index 100% rename from scripts/make_predictions.py rename to scripts/make_detections.py From 635c6b7eb13001a5ad831d651fbe36ea7a32261e Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Fri, 15 Sep 2023 14:31:36 +0200 Subject: [PATCH 098/108] Delete duplicate filter predictions script --- .../quarry-detection/filter_prediction.py | 155 ------------------ 1 file changed, 155 deletions(-) delete mode 100644 examples/quarry-detection/filter_prediction.py diff --git a/examples/quarry-detection/filter_prediction.py b/examples/quarry-detection/filter_prediction.py deleted file mode 100644 index 3c297e5..0000000 --- a/examples/quarry-detection/filter_prediction.py +++ /dev/null @@ -1,155 +0,0 @@ -#!/bin/python -# -*- coding: utf-8 -*- - -# This source code is licensed under the license found in the -# LICENSE file in the root directory of this source tree. - -import os -import sys -import inspect -import time -import argparse -import yaml -from loguru import logger - -import geopandas as gpd -import pandas as pd -import rasterio -from sklearn.cluster import KMeans - - -# the following allows us to import modules from within this file's parent folder -sys.path.insert(0, '.') - -logger.remove() -logger.add(sys.stderr, format="{time:YYYY-MM-DD HH:mm:ss} - {level} - {message}", level="INFO") - - -if __name__ == "__main__": - - # Chronometer - tic = time.time() - logger.info('Starting...') - - # argument parser - parser = argparse.ArgumentParser(description="The script filters the detection of potential Mineral Extraction Sites obtained with the object-detector scripts") - parser.add_argument('config_file', type=str, help='input geojson path') - args = parser.parse_args() - - logger.info(f"Using {args.config_file} as config file.") - - with open(args.config_file) as fp: - cfg = yaml.load(fp, Loader=yaml.FullLoader)[os.path.basename(__file__)] - - # Load input parameters - YEAR = cfg['year'] - INPUT = cfg['input'] - LABELS_SHPFILE = cfg['labels_shapefile'] - DEM = cfg['dem'] - SCORE = cfg['score'] - AREA = cfg['area'] - ELEVATION = cfg['elevation'] - DISTANCE = cfg['distance'] - OUTPUT = cfg['output'] - - written_files = [] - - # Convert input detection to a geo dataframe - aoi = gpd.read_file(LABELS_SHPFILE) - aoi = aoi.to_crs(epsg=2056) - - input = gpd.read_file(INPUT) - input = input.to_crs(2056) - total = len(input) - logger.info(f"Total input = {total}") - - # Discard polygons detected above the threshold elevalation and 0 m - r = rasterio.open(DEM) - row, col = r.index(input.centroid.x, input.centroid.y) - values = r.read(1)[row, col] - input['elev'] = values - input = input[input.elev < ELEVATION] - row, col = r.index(input.centroid.x, input.centroid.y) - values = r.read(1)[row, col] - input['elev'] = values - - input = input[input.elev != 0] - te = len(input) - logger.info(f"{str(total - te)} predictions removed by elevation threshold: {str(ELEVATION)} m") - - # Centroid of every prediction polygon - centroids = gpd.GeoDataFrame() - centroids.geometry = input.representative_point() - - # KMeans Unsupervised Learning - centroids = pd.DataFrame({'x': centroids.geometry.x, 'y': centroids.geometry.y}) - k = int((len(input)/3) + 1) - cluster = KMeans(n_clusters=k, algorithm='auto', random_state=1) - model = cluster.fit(centroids) - labels = model.predict(centroids) - logger.info(f"KMeans algorithm computed with k = {str(k)}") - - # Dissolve and Aggregate (keep the max value of aggregate attributes) - input['cluster'] = labels - - input = input.dissolve(by='cluster', aggfunc='max') - total = len(input) - - # Filter dataframe by score value - input = input[input['score'] > SCORE] - sc = len(input) - logger.info(f"{str(total - sc)} predictions removed by score threshold: {str(SCORE)}") - - # Clip prediction to AOI - input = gpd.clip(input, aoi) - - # Create empty data frame - geo_merge = gpd.GeoDataFrame() - # Merge close labels using buffer and unions - geo_merge = input.buffer(+DISTANCE, resolution = 2) - geo_merge = geo_merge.geometry.unary_union - geo_merge = gpd.GeoDataFrame(geometry=[geo_merge], crs = input.crs) - geo_merge = geo_merge.explode(index_parts=True).reset_index(drop=True) - geo_merge = geo_merge.buffer(-DISTANCE, resolution=2) - - td = len(geo_merge) - logger.info(f"{str(sc - td)} difference to clustered predictions after union (distance {str(DISTANCE)})") - - # Discard polygons with area under the threshold - geo_merge = geo_merge[geo_merge.area > AREA] - ta = len(geo_merge) - logger.info(f"{str(td - ta)} difference to clustered predictions after union (distance {str(AREA)})") - - # Preparation of a geo df - data = {'id': geo_merge.index,'area': geo_merge.area, 'centroid_x': geo_merge.centroid.x, 'centroid_y': geo_merge.centroid.y, 'geometry': geo_merge} - geo_tmp = gpd.GeoDataFrame(data, crs=input.crs) - - # Get the averaged prediction score of the merged polygons - intersection = gpd.sjoin(geo_tmp, input, how='inner') - intersection['id'] = intersection.index - score_final=intersection.groupby(['id']).mean(numeric_only=True) - # Formatting the final geo df - data = {'id_feature': geo_merge.index,'score': score_final['score'] , 'area': geo_merge.area, 'centroid_x': geo_merge.centroid.x, 'centroid_y': geo_merge.centroid.y, 'geometry': geo_merge} - geo_final = gpd.GeoDataFrame(data, crs=input.crs) - logger.info(f"{len(geo_final)} predictions remaining") - - # Format the ooutput name of the filtered prediction - feature = OUTPUT.replace('{score}', str(SCORE)).replace('0.', '0dot') \ - .replace('{year}', str(int(YEAR)))\ - .replace('{area}', str(int(AREA)))\ - .replace('{elevation}', str(int(ELEVATION))) \ - .replace('{distance}', str(int(DISTANCE))) - geo_final.to_file(feature, driver='GeoJSON') - - written_files.append(feature) - logger.info(f"...done. A file was written: {feature}") - - logger.info("The following files were written. Let's check them out!") - for written_file in written_files: - logger.info(written_file) - - # Stop chronometer - toc = time.time() - logger.info(f"Nothing left to be done: exiting. Elapsed time: {(toc-tic):.2f} seconds") - - sys.stderr.flush() \ No newline at end of file From b0937997af48d1206d6175a5501072a207636ef0 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Fri, 15 Sep 2023 14:32:35 +0200 Subject: [PATCH 099/108] Change prediction to detection for filter script --- .../{filter_predictions.py => filter_detections.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/quarry-detection/{filter_predictions.py => filter_detections.py} (100%) diff --git a/examples/quarry-detection/filter_predictions.py b/examples/quarry-detection/filter_detections.py similarity index 100% rename from examples/quarry-detection/filter_predictions.py rename to examples/quarry-detection/filter_detections.py From 8c27f6c6fb6c279231c7a6b4fa3f5dec6ffa13a7 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Fri, 15 Sep 2023 14:35:22 +0200 Subject: [PATCH 100/108] Change predictions to detections in config files --- examples/quarry-detection/config_prd.yaml | 8 ++++---- examples/quarry-detection/config_trne.yaml | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/quarry-detection/config_prd.yaml b/examples/quarry-detection/config_prd.yaml index 802ce87..4f6a909 100644 --- a/examples/quarry-detection/config_prd.yaml +++ b/examples/quarry-detection/config_prd.yaml @@ -36,7 +36,7 @@ generate_tilesets.py: supercategory: "Land usage" # 3-Perform the object detection based on the optimized trained model -make_predictions.py: +make_detections.py: working_folder: ./output/output_prd log_subfolder: logs sample_tagged_img_subfolder: sample_tagged_images @@ -52,13 +52,13 @@ make_predictions.py: score_lower_threshold: 0.3 # 4-Filtering and merging detection polygons to improve results -filter_predictions.py: +filter_detections.py: year: 2020 - input: ./output/output_prd/oth_predictions_at_0dot3_threshold.gpkg + input: ./output/output_prd/oth_detections_at_0dot3_threshold.gpkg labels_shapefile: ./data/AoI/AoI_2020.shp dem: ./data/DEM/switzerland_dem_EPSG2056.tif elevation: 1200.0 # m, altitude threshold score: 0.95 # prediction 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/output_prd/oth_predictions_at_0dot3_threshold_year-{year}_score-{score}_area-{area}_elevation-{elevation}_distance-{distance}.geojson \ No newline at end of file + output: ./output/output_prd/oth_detections_at_0dot3_threshold_year-{year}_score-{score}_area-{area}_elevation-{elevation}_distance-{distance}.geojson \ No newline at end of file diff --git a/examples/quarry-detection/config_trne.yaml b/examples/quarry-detection/config_trne.yaml index f84dd82..a2862c0 100644 --- a/examples/quarry-detection/config_trne.yaml +++ b/examples/quarry-detection/config_trne.yaml @@ -50,7 +50,7 @@ train_model.py: model_zoo_checkpoint_url: "COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x.yaml" # 4-Perform the object detection based on the optimized trained model -make_predictions.py: +make_detections.py: working_folder: ./output/output_trne log_subfolder: logs sample_tagged_img_subfolder: sample_tagged_images @@ -68,13 +68,13 @@ make_predictions.py: score_lower_threshold: 0.05 # 5-Evaluate the quality of the prediction for the different datasets with metrics calculation -assess_predictions.py: +assess_detections.py: datasets: ground_truth_labels_geojson: ./output/output_trne/labels.geojson image_metadata_json: ./output/output_trne/img_metadata.json split_aoi_tiles_geojson: ./output/output_trne/split_aoi_tiles.geojson # aoi = Area of Interest - predictions: - trn: ./output/output_trne/trn_predictions_at_0dot05_threshold.gpkg - val: ./output/output_trne/val_predictions_at_0dot05_threshold.gpkg - tst: ./output/output_trne/tst_predictions_at_0dot05_threshold.gpkg + detections: + trn: ./output/output_trne/trn_detections_at_0dot05_threshold.gpkg + val: ./output/output_trne/val_detections_at_0dot05_threshold.gpkg + tst: ./output/output_trne/tst_detections_at_0dot05_threshold.gpkg output_folder: ./output/output_trne \ No newline at end of file From e00ca1195ebd445550db114b42f5072768cc56dc Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Fri, 15 Sep 2023 14:37:52 +0200 Subject: [PATCH 101/108] Add exit command to swimming pool READMEs --- examples/swimming-pool-detection/GE/README.md | 1 + examples/swimming-pool-detection/GE/config_GE.yaml | 14 +++++++------- examples/swimming-pool-detection/NE/README.md | 1 + examples/swimming-pool-detection/NE/config_NE.yaml | 14 +++++++------- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/examples/swimming-pool-detection/GE/README.md b/examples/swimming-pool-detection/GE/README.md index 7c3e779..38f71fb 100644 --- a/examples/swimming-pool-detection/GE/README.md +++ b/examples/swimming-pool-detection/GE/README.md @@ -19,6 +19,7 @@ nobody@:/app# stdl-objdet generate_tilesets config_GE.yaml nobody@:/app# stdl-objdet train_model config_GE.yaml nobody@:/app# stdl-objdet make_detections config_GE.yaml nobody@:/app# stdl-objdet assess_detections config_GE.yaml +nobody@:/app# exit $ sudo chmod -R a+w examples ``` diff --git a/examples/swimming-pool-detection/GE/config_GE.yaml b/examples/swimming-pool-detection/GE/config_GE.yaml index 69364f1..2c9dc30 100644 --- a/examples/swimming-pool-detection/GE/config_GE.yaml +++ b/examples/swimming-pool-detection/GE/config_GE.yaml @@ -45,7 +45,7 @@ train_model.py: model_weights: model_zoo_checkpoint_url: "COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x.yaml" -make_predictions.py: +make_detections.py: working_folder: output_GE log_subfolder: logs sample_tagged_img_subfolder: sample_prediction_images @@ -63,15 +63,15 @@ make_predictions.py: epsilon: 0.5 # cf. https://rdp.readthedocs.io/en/latest/ score_lower_threshold: 0.05 -assess_predictions.py: +assess_detections.py: datasets: ground_truth_labels_geojson: output_GE/ground_truth_labels.geojson other_labels_geojson: output_GE/other_labels.geojson image_metadata_json: output_GE/img_metadata.json split_aoi_tiles_geojson: output_GE/split_aoi_tiles.geojson # aoi = Area of Interest - predictions: - trn: output_GE/trn_predictions_at_0dot05_threshold.gpkg - val: output_GE/val_predictions_at_0dot05_threshold.gpkg - tst: output_GE/tst_predictions_at_0dot05_threshold.gpkg - oth: output_GE/oth_predictions_at_0dot05_threshold.gpkg + detections: + trn: output_GE/trn_detections_at_0dot05_threshold.gpkg + val: output_GE/val_detections_at_0dot05_threshold.gpkg + tst: output_GE/tst_detections_at_0dot05_threshold.gpkg + oth: output_GE/oth_detections_at_0dot05_threshold.gpkg output_folder: output_GE diff --git a/examples/swimming-pool-detection/NE/README.md b/examples/swimming-pool-detection/NE/README.md index 6fc7ef8..32972c6 100644 --- a/examples/swimming-pool-detection/NE/README.md +++ b/examples/swimming-pool-detection/NE/README.md @@ -23,6 +23,7 @@ nobody@:/app# stdl-objdet generate_tilesets config_NE.yaml nobody@:/app# stdl-objdet train_model config_NE.yaml nobody@:/app# stdl-objdet make_detections config_NE.yaml nobody@:/app# stdl-objdet assess_detections config_NE.yaml +nobody@:/app# exit $ sudo chmod -R a+w examples ``` diff --git a/examples/swimming-pool-detection/NE/config_NE.yaml b/examples/swimming-pool-detection/NE/config_NE.yaml index be56354..0c1428d 100644 --- a/examples/swimming-pool-detection/NE/config_NE.yaml +++ b/examples/swimming-pool-detection/NE/config_NE.yaml @@ -47,7 +47,7 @@ train_model.py: model_weights: model_zoo_checkpoint_url: "COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x.yaml" -make_predictions.py: +make_detections.py: working_folder: output_NE log_subfolder: logs sample_tagged_img_subfolder: sample_prediction_images @@ -65,15 +65,15 @@ make_predictions.py: epsilon: 0.5 # cf. https://rdp.readthedocs.io/en/latest/ score_lower_threshold: 0.05 -assess_predictions.py: +assess_detections.py: datasets: ground_truth_labels_geojson: output_NE/ground_truth_labels.geojson other_labels_geojson: output_NE/other_labels.geojson image_metadata_json: output_NE/img_metadata.json split_aoi_tiles_geojson: output_NE/split_aoi_tiles.geojson # aoi = Area of Interest - predictions: - trn: output_NE/trn_predictions_at_0dot05_threshold.gpkg - val: output_NE/val_predictions_at_0dot05_threshold.gpkg - tst: output_NE/tst_predictions_at_0dot05_threshold.gpkg - oth: output_NE/oth_predictions_at_0dot05_threshold.gpkg + detections: + trn: output_NE/trn_detections_at_0dot05_threshold.gpkg + val: output_NE/val_detections_at_0dot05_threshold.gpkg + tst: output_NE/tst_detections_at_0dot05_threshold.gpkg + oth: output_NE/oth_detections_at_0dot05_threshold.gpkg output_folder: output_NE \ No newline at end of file From b52cfd27bdda7041d9c5caee179c38f5181874f7 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Fri, 15 Sep 2023 14:40:57 +0200 Subject: [PATCH 102/108] Change predictions to detection in cli file --- scripts/cli.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/cli.py b/scripts/cli.py index 5fa91da..119ae25 100644 --- a/scripts/cli.py +++ b/scripts/cli.py @@ -3,8 +3,8 @@ import argparse from scripts.generate_tilesets import main as generate_tilesets from scripts.train_model import main as train_model -from scripts.make_predictions import main as make_predictions -from scripts.assess_predictions import main as assess_predictions +from scripts.make_detections import main as make_detections +from scripts.assess_detections import main as assess_detections def main(): @@ -31,13 +31,13 @@ def main(): add_parser.add_argument(**arg_template) add_parser.set_defaults(func=train_model) - add_parser = subparsers.add_parser("make_predictions", help="This script makes predictions, using a previously trained model.") + add_parser = subparsers.add_parser("make_detections", help="This script makes detections, using a previously trained model.") add_parser.add_argument(**arg_template) - add_parser.set_defaults(func=make_predictions) + add_parser.set_defaults(func=make_detections) - add_parser = subparsers.add_parser("assess_predictions", help="This script assesses the quality of predictions with respect to ground-truth/other labels.") + add_parser = subparsers.add_parser("assess_detections", help="This script assesses the quality of detections with respect to ground-truth/other labels.") add_parser.add_argument(**arg_template) - add_parser.set_defaults(func=assess_predictions) + add_parser.set_defaults(func=assess_detections) # https://stackoverflow.com/a/47440202 args = global_parser.parse_args(args=None if sys.argv[1:] else ['--help']) From 5a1774b6a2e6b9c5c1e5711681eeb84d52994ba9 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Fri, 15 Sep 2023 12:57:22 +0000 Subject: [PATCH 103/108] Rename predictions -> detections --- README.md | 40 +++++++++---------- examples/quarry-detection/README.md | 10 ++--- examples/quarry-detection/config_prd.yaml | 12 +++--- examples/quarry-detection/config_trne.yaml | 14 +++---- ...er_predictions.py => filter_detections.py} | 0 examples/swimming-pool-detection/GE/README.md | 4 +- .../swimming-pool-detection/GE/config_GE.yaml | 18 ++++----- examples/swimming-pool-detection/NE/README.md | 4 +- .../swimming-pool-detection/NE/config_NE.yaml | 16 ++++---- helpers/detectron2.py | 8 ++-- helpers/misc.py | 16 ++++---- ...ss_predictions.py => assess_detections.py} | 36 ++++++++--------- scripts/cli.py | 14 +++---- scripts/generate_tilesets.py | 2 +- ...make_predictions.py => make_detections.py} | 18 ++++----- scripts/train_model.py | 6 +-- 16 files changed, 109 insertions(+), 109 deletions(-) rename examples/quarry-detection/{filter_predictions.py => filter_detections.py} (100%) rename scripts/{assess_predictions.py => assess_detections.py} (88%) rename scripts/{make_predictions.py => make_detections.py} (89%) diff --git a/README.md b/README.md index 39a1d1a..d2c1b24 100644 --- a/README.md +++ b/README.md @@ -71,8 +71,8 @@ This project implements the workflow described [here](https://tech.stdl.ch/TASK- | :-: | --- | --- | --- | | 1 | Tileset generation | `generate_tilesets` | [here](scripts/generate_tilesets.py) | | 2 | Model training | `train_model` | [here](scripts/train_model.py) | -| 3 | Prediction | `make_predictions` | [here](scripts/train_model.py) | -| 4 | Assessment | `assess_predictions` | [here](scripts/assess_predictions.py) | +| 3 | Detection | `make_detections` | [here](scripts/train_model.py) | +| 4 | Assessment | `assess_detections` | [here](scripts/assess_detections.py) | These stages/scripts can be run one after the other, by issuing the following command from a terminal: @@ -214,17 +214,17 @@ train_model.py: Detectron2 configuration files are provided in the example folders mentioned here-below. We warn the end-user about the fact that, **for the time being, no hyperparameters tuning is automatically performed**. -### Stage 3: prediction +### Stage 3: detection -The `make_predictions` command allows one to use the predictive model trained at the previous step to make predictions over various input datasets: +The `make_detections` command allows one to use the object detection model trained at the previous step to make detections over various input datasets: -* predictions over the `trn`, `val`, `tst` datasets can be used to assess the reliability of this approach on ground-truth data; -* predictions over the `oth` dataset are, in principle, the main goal of this kind of analyses. +* detections over the `trn`, `val`, `tst` datasets can be used to assess the reliability of this approach on ground-truth data; +* detections over the `oth` dataset are, in principle, the main goal of this kind of analyses. Here's the excerpt of the configuration file relevant to this script, with values replaced by textual documentation: ```yaml -make_predictions.py: +make_detections.py: working_folder: log_subfolder: sample_tagged_img_subfolder: @@ -238,24 +238,24 @@ make_predictions.py: pth_file: image_metadata_json: # the following section concerns the Ramer-Douglas-Peucker algorithm, - # which can be optionally applied to prediction before they are exported + # which can be optionally applied to detections before they are exported rdp_simplification: enabled: epsilon: - score_lower_threshold: + score_lower_threshold: ``` ### Stage 4: assessment -The `assess_predictions` command allows one to assess the reliability of predictions, comparing predictions with ground-truth data. The assessment goes through the following steps: +The `assess_detections` command allows one to assess the reliability of detections, comparing detections with ground-truth data. The assessment goes through the following steps: 1. Labels (GT + `oth`) geometries are clipped to the boundaries of the various AoI tiles, scaled by a factor 0.999 in order to prevent any "crosstalk" between neighboring tiles. -2. Vector features are extracted from Detectron2's predictions, which are originally in a raster format (`numpy` arrays, to be more precise). +2. Vector features are extracted from Detectron2's detections, which are originally in a raster format (`numpy` arrays, to be more precise). -3. Spatial joins are computed between the vectorized predictions and the clipped labels, in order to identify - * True Positives (TP), *i.e.* objects that are found in both datasets, labels and predictions; - * False Positives (FP), *i.e.* objects that are only found in the predictions dataset; +3. Spatial joins are computed between the vectorized detections and the clipped labels, in order to identify + * True Positives (TP), *i.e.* objects that are found in both datasets, labels and detections; + * False Positives (FP), *i.e.* objects that are only found in the detections dataset; * False Negatives (FN), *i.e.* objects that are only found in the labels dataset. 4. Finally, TPs, FPs and FNs are counted in order to compute the following metrics (see [this page](https://en.wikipedia.org/wiki/Precision_and_recall)) : @@ -265,17 +265,17 @@ The `assess_predictions` command allows one to assess the reliability of predict Here's the excerpt of the configuration file relevant to this command, with values replaced by textual documentation: ```yaml -assess_predictions.py: +assess_detections.py: datasets: ground_truth_labels_geojson: other_labels_geojson: image_metadata_json: split_aoi_tiles_geojson: - predictions: - trn: - val: - tst: - oth: + detections: + trn: + val: + tst: + oth: output_folder: ``` diff --git a/examples/quarry-detection/README.md b/examples/quarry-detection/README.md index 97e01d7..776e146 100644 --- a/examples/quarry-detection/README.md +++ b/examples/quarry-detection/README.md @@ -12,7 +12,7 @@ It consists of the following elements: - 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`. - 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_predictions.py`) which filters detections according to their confidence score, altitude and area. The script also identifies and merges groups of nearby polygons. +- 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. The workflow can be run end-to-end by issuing the following list of commands, from the root folder of this GitHub repository: @@ -23,13 +23,13 @@ nobody@:/app# cd examples/quarry-detection nobody@:/app# python prepare_data.py config_trne.yaml nobody@:/app# stdl-objdet generate_tilesets config_trne.yaml nobody@:/app# stdl-objdet train_model config_trne.yaml -nobody@:/app# stdl-objdet make_predictions config_trne.yaml -nobody@:/app# stdl-objdet assess_predictions config_trne.yaml +nobody@:/app# stdl-objdet make_detections config_trne.yaml +nobody@:/app# stdl-objdet assess_detections config_trne.yaml nobody@:/app# python prepare_data.py config_prd.yaml nobody@:/app# stdl-objdet generate_tilesets config_prd.yaml -nobody@:/app# stdl-objdet make_predictions config_prd.yaml +nobody@:/app# stdl-objdet make_detections config_prd.yaml nobody@:/app# bash get_dem.sh -nobody@:/app# python filter_predictions.py config_prd.yaml +nobody@:/app# python filter_detections.py config_prd.yaml nobody@:/app# exit $ sudo chmod -R a+w examples ``` diff --git a/examples/quarry-detection/config_prd.yaml b/examples/quarry-detection/config_prd.yaml index 802ce87..7232561 100644 --- a/examples/quarry-detection/config_prd.yaml +++ b/examples/quarry-detection/config_prd.yaml @@ -20,7 +20,7 @@ generate_tilesets.py: url: https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swissimage-product/default/2020/3857/{z}/{x}/{y}.jpeg output_folder: ./output/output_prd tile_size: 256 # per side, in pixels - overwrite: True + overwrite: False n_jobs: 10 COCO_metadata: year: 2021 @@ -36,7 +36,7 @@ generate_tilesets.py: supercategory: "Land usage" # 3-Perform the object detection based on the optimized trained model -make_predictions.py: +make_detections.py: working_folder: ./output/output_prd log_subfolder: logs sample_tagged_img_subfolder: sample_tagged_images @@ -52,13 +52,13 @@ make_predictions.py: score_lower_threshold: 0.3 # 4-Filtering and merging detection polygons to improve results -filter_predictions.py: +filter_detections.py: year: 2020 - input: ./output/output_prd/oth_predictions_at_0dot3_threshold.gpkg + input: ./output/output_prd/oth_detections_at_0dot3_threshold.gpkg labels_shapefile: ./data/AoI/AoI_2020.shp dem: ./data/DEM/switzerland_dem_EPSG2056.tif elevation: 1200.0 # m, altitude threshold - score: 0.95 # prediction score (from 0 to 1) provided by detectron2 + 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/output_prd/oth_predictions_at_0dot3_threshold_year-{year}_score-{score}_area-{area}_elevation-{elevation}_distance-{distance}.geojson \ No newline at end of file + output: ./output/output_prd/oth_detections_at_0dot3_threshold_year-{year}_score-{score}_area-{area}_elevation-{elevation}_distance-{distance}.geojson \ No newline at end of file diff --git a/examples/quarry-detection/config_trne.yaml b/examples/quarry-detection/config_trne.yaml index f84dd82..f74356a 100644 --- a/examples/quarry-detection/config_trne.yaml +++ b/examples/quarry-detection/config_trne.yaml @@ -50,7 +50,7 @@ train_model.py: model_zoo_checkpoint_url: "COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x.yaml" # 4-Perform the object detection based on the optimized trained model -make_predictions.py: +make_detections.py: working_folder: ./output/output_trne log_subfolder: logs sample_tagged_img_subfolder: sample_tagged_images @@ -67,14 +67,14 @@ make_predictions.py: epsilon: 2.0 # cf. https://rdp.readthedocs.io/en/latest/ score_lower_threshold: 0.05 -# 5-Evaluate the quality of the prediction for the different datasets with metrics calculation -assess_predictions.py: +# 5-Evaluate the quality of the detections for the different datasets with metrics calculation +assess_detections.py: datasets: ground_truth_labels_geojson: ./output/output_trne/labels.geojson image_metadata_json: ./output/output_trne/img_metadata.json split_aoi_tiles_geojson: ./output/output_trne/split_aoi_tiles.geojson # aoi = Area of Interest - predictions: - trn: ./output/output_trne/trn_predictions_at_0dot05_threshold.gpkg - val: ./output/output_trne/val_predictions_at_0dot05_threshold.gpkg - tst: ./output/output_trne/tst_predictions_at_0dot05_threshold.gpkg + detections: + trn: ./output/output_trne/trn_detections_at_0dot05_threshold.gpkg + val: ./output/output_trne/val_detections_at_0dot05_threshold.gpkg + tst: ./output/output_trne/tst_detections_at_0dot05_threshold.gpkg output_folder: ./output/output_trne \ No newline at end of file diff --git a/examples/quarry-detection/filter_predictions.py b/examples/quarry-detection/filter_detections.py similarity index 100% rename from examples/quarry-detection/filter_predictions.py rename to examples/quarry-detection/filter_detections.py diff --git a/examples/swimming-pool-detection/GE/README.md b/examples/swimming-pool-detection/GE/README.md index 7d1aa84..7c3e779 100644 --- a/examples/swimming-pool-detection/GE/README.md +++ b/examples/swimming-pool-detection/GE/README.md @@ -17,8 +17,8 @@ nobody@:/app# cd output_GE && cat parcels.geojson | supermercado burn 18 | m nobody@:/app# python prepare_data.py config_GE.yaml nobody@:/app# stdl-objdet generate_tilesets config_GE.yaml nobody@:/app# stdl-objdet train_model config_GE.yaml -nobody@:/app# stdl-objdet make_predictions config_GE.yaml -nobody@:/app# stdl-objdet assess_predictions config_GE.yaml +nobody@:/app# stdl-objdet make_detections config_GE.yaml +nobody@:/app# stdl-objdet assess_detections config_GE.yaml $ sudo chmod -R a+w examples ``` diff --git a/examples/swimming-pool-detection/GE/config_GE.yaml b/examples/swimming-pool-detection/GE/config_GE.yaml index 69364f1..d92dd6f 100644 --- a/examples/swimming-pool-detection/GE/config_GE.yaml +++ b/examples/swimming-pool-detection/GE/config_GE.yaml @@ -19,7 +19,7 @@ generate_tilesets.py: output_folder: output_GE tile_size: 256 # per side, in pixels overwrite: False - n_jobs: 1 + n_jobs: 10 COCO_metadata: year: 2020 version: 1.0 @@ -45,10 +45,10 @@ train_model.py: model_weights: model_zoo_checkpoint_url: "COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x.yaml" -make_predictions.py: +make_detections.py: working_folder: output_GE log_subfolder: logs - sample_tagged_img_subfolder: sample_prediction_images + sample_tagged_img_subfolder: sample_detection_images COCO_files: # relative paths, w/ respect to the working_folder trn: COCO_trn.json val: COCO_val.json @@ -63,15 +63,15 @@ make_predictions.py: epsilon: 0.5 # cf. https://rdp.readthedocs.io/en/latest/ score_lower_threshold: 0.05 -assess_predictions.py: +assess_detections.py: datasets: ground_truth_labels_geojson: output_GE/ground_truth_labels.geojson other_labels_geojson: output_GE/other_labels.geojson image_metadata_json: output_GE/img_metadata.json split_aoi_tiles_geojson: output_GE/split_aoi_tiles.geojson # aoi = Area of Interest - predictions: - trn: output_GE/trn_predictions_at_0dot05_threshold.gpkg - val: output_GE/val_predictions_at_0dot05_threshold.gpkg - tst: output_GE/tst_predictions_at_0dot05_threshold.gpkg - oth: output_GE/oth_predictions_at_0dot05_threshold.gpkg + detections: + trn: output_GE/trn_detections_at_0dot05_threshold.gpkg + val: output_GE/val_detections_at_0dot05_threshold.gpkg + tst: output_GE/tst_detections_at_0dot05_threshold.gpkg + oth: output_GE/oth_detections_at_0dot05_threshold.gpkg output_folder: output_GE diff --git a/examples/swimming-pool-detection/NE/README.md b/examples/swimming-pool-detection/NE/README.md index d697463..6fc7ef8 100644 --- a/examples/swimming-pool-detection/NE/README.md +++ b/examples/swimming-pool-detection/NE/README.md @@ -21,8 +21,8 @@ nobody@:/app# cd output_NE && cat parcels.geojson | supermercado burn 18 | m nobody@:/app# python prepare_data.py config_NE.yaml nobody@:/app# stdl-objdet generate_tilesets config_NE.yaml nobody@:/app# stdl-objdet train_model config_NE.yaml -nobody@:/app# stdl-objdet make_predictions config_NE.yaml -nobody@:/app# stdl-objdet assess_predictions config_NE.yaml +nobody@:/app# stdl-objdet make_detections config_NE.yaml +nobody@:/app# stdl-objdet assess_detections config_NE.yaml $ sudo chmod -R a+w examples ``` diff --git a/examples/swimming-pool-detection/NE/config_NE.yaml b/examples/swimming-pool-detection/NE/config_NE.yaml index be56354..7b1c283 100644 --- a/examples/swimming-pool-detection/NE/config_NE.yaml +++ b/examples/swimming-pool-detection/NE/config_NE.yaml @@ -47,10 +47,10 @@ train_model.py: model_weights: model_zoo_checkpoint_url: "COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x.yaml" -make_predictions.py: +make_detections.py: working_folder: output_NE log_subfolder: logs - sample_tagged_img_subfolder: sample_prediction_images + sample_tagged_img_subfolder: sample_detection_images COCO_files: # relative paths, w/ respect to the working_folder trn: COCO_trn.json val: COCO_val.json @@ -65,15 +65,15 @@ make_predictions.py: epsilon: 0.5 # cf. https://rdp.readthedocs.io/en/latest/ score_lower_threshold: 0.05 -assess_predictions.py: +assess_detections.py: datasets: ground_truth_labels_geojson: output_NE/ground_truth_labels.geojson other_labels_geojson: output_NE/other_labels.geojson image_metadata_json: output_NE/img_metadata.json split_aoi_tiles_geojson: output_NE/split_aoi_tiles.geojson # aoi = Area of Interest - predictions: - trn: output_NE/trn_predictions_at_0dot05_threshold.gpkg - val: output_NE/val_predictions_at_0dot05_threshold.gpkg - tst: output_NE/tst_predictions_at_0dot05_threshold.gpkg - oth: output_NE/oth_predictions_at_0dot05_threshold.gpkg + detections: + trn: output_NE/trn_detections_at_0dot05_threshold.gpkg + val: output_NE/val_detections_at_0dot05_threshold.gpkg + tst: output_NE/tst_detections_at_0dot05_threshold.gpkg + oth: output_NE/oth_detections_at_0dot05_threshold.gpkg output_folder: output_NE \ No newline at end of file diff --git a/helpers/detectron2.py b/helpers/detectron2.py index d605bc9..42e76b0 100644 --- a/helpers/detectron2.py +++ b/helpers/detectron2.py @@ -126,9 +126,9 @@ def build_hooks(self): # HELPER FUNCTIONS -def _preprocess(preds): +def _preprocess(dets): - fields = preds['instances'].get_fields() + fields = dets['instances'].get_fields() out = {} @@ -148,11 +148,11 @@ def _preprocess(preds): return out -def detectron2preds_to_features(preds, crs, transform, rdp_enabled, rdp_eps): +def detectron2dets_to_features(dets, crs, transform, rdp_enabled, rdp_eps): feats = [] - tmp = _preprocess(preds) + tmp = _preprocess(dets) for idx in range(len(tmp['scores'])): diff --git a/helpers/misc.py b/helpers/misc.py index f0f4010..557fa4a 100644 --- a/helpers/misc.py +++ b/helpers/misc.py @@ -110,25 +110,25 @@ def get_metrics(tp_gdf, fp_gdf, fn_gdf): return precision, recall, f1 -def get_fractional_sets(preds_gdf, labels_gdf): +def get_fractional_sets(dets_gdf, labels_gdf): - _preds_gdf = preds_gdf.copy() + _dets_gdf = dets_gdf.copy() _labels_gdf = labels_gdf.copy() if len(_labels_gdf) == 0: - fp_gdf = _preds_gdf.copy() + fp_gdf = _dets_gdf.copy() tp_gdf = gpd.GeoDataFrame() fn_gdf = gpd.GeoDataFrame() return tp_gdf, fp_gdf, fn_gdf - assert(_preds_gdf.crs == _labels_gdf.crs), f"CRS Mismatch: predictions' CRS = {_preds_gdf.crs}, labels' CRS = {_labels_gdf.crs}" + assert(_dets_gdf.crs == _labels_gdf.crs), f"CRS Mismatch: detections' CRS = {_dets_gdf.crs}, labels' CRS = {_labels_gdf.crs}" - # we add a dummy column to the labels dataset, which should not exist in predictions too; - # this allows us to distinguish matching from non-matching predictions + # we add a dummy column to the labels dataset, which should not exist in detections too; + # this allows us to distinguish matching from non-matching detections _labels_gdf['dummy_id'] = _labels_gdf.index # TRUE POSITIVES - left_join = gpd.sjoin(_preds_gdf, _labels_gdf, how='left', predicate='intersects', lsuffix='left', rsuffix='right') + left_join = gpd.sjoin(_dets_gdf, _labels_gdf, how='left', predicate='intersects', lsuffix='left', rsuffix='right') tp_gdf = left_join[left_join.dummy_id.notnull()].copy() tp_gdf.drop_duplicates(subset=['dummy_id', 'tile_id'], inplace=True) @@ -140,7 +140,7 @@ def get_fractional_sets(preds_gdf, labels_gdf): fp_gdf.drop(columns=['dummy_id'], inplace=True) # FALSE NEGATIVES - right_join = gpd.sjoin(_preds_gdf, _labels_gdf, how='right', predicate='intersects', lsuffix='left', rsuffix='right') + right_join = gpd.sjoin(_dets_gdf, _labels_gdf, how='right', predicate='intersects', lsuffix='left', rsuffix='right') fn_gdf = right_join[right_join.score.isna()].copy() fn_gdf.drop_duplicates(subset=['dummy_id', 'tile_id'], inplace=True) diff --git a/scripts/assess_predictions.py b/scripts/assess_detections.py similarity index 88% rename from scripts/assess_predictions.py rename to scripts/assess_detections.py index 3c17628..1590f08 100644 --- a/scripts/assess_predictions.py +++ b/scripts/assess_detections.py @@ -40,7 +40,7 @@ def main(cfg_file_path): OUTPUT_DIR = cfg['output_folder'] IMG_METADATA_FILE = cfg['datasets']['image_metadata_json'] - PREDICTION_FILES = cfg['datasets']['predictions'] + DETECTION_FILES = cfg['datasets']['detections'] SPLIT_AOI_TILES_GEOJSON = cfg['datasets']['split_aoi_tiles_geojson'] if 'ground_truth_labels_geojson' in cfg['datasets'].keys(): @@ -114,21 +114,21 @@ def main(cfg_file_path): # let's extract filenames (w/o path) img_metadata_dict = {os.path.split(k)[-1]: v for (k, v) in tmp.items()} - # ------ Loading predictions + # ------ Loading detections - preds_gdf_dict = {} + dets_gdf_dict = {} - for dataset, preds_file in PREDICTION_FILES.items(): - preds_gdf_dict[dataset] = gpd.read_file(preds_file) + for dataset, dets_file in DETECTION_FILES.items(): + dets_gdf_dict[dataset] = gpd.read_file(dets_file) if len(labels_gdf)>0: - # ------ Comparing predictions with ground-truth data and computing metrics + # ------ Comparing detections with ground-truth data and computing metrics # init metrics = {} - for dataset in preds_gdf_dict.keys(): + for dataset in dets_gdf_dict.keys(): metrics[dataset] = [] metrics_df_dict = {} @@ -145,7 +145,7 @@ def main(cfg_file_path): inner_tqdm_log.set_description_str(f'Threshold = {threshold:.2f}') - tmp_gdf = preds_gdf_dict[dataset].copy() + tmp_gdf = dets_gdf_dict[dataset].copy() tmp_gdf.to_crs(epsg=clipped_labels_gdf.crs.to_epsg(), inplace=True) tmp_gdf = tmp_gdf[tmp_gdf.score >= threshold].copy() @@ -246,20 +246,20 @@ def main(cfg_file_path): written_files.append(file_to_write) - # ------ tagging predictions + # ------ tagging detections # we select the threshold which maximizes the f1-score on the val dataset selected_threshold = metrics_df_dict['val'].iloc[metrics_df_dict['val']['f1'].argmax()]['threshold'] - logger.info(f"Tagging predictions with threshold = {selected_threshold:.2f}, which maximizes the f1-score on the val dataset.") + logger.info(f"Tagging detections with threshold = {selected_threshold:.2f}, which maximizes the f1-score on the val dataset.") - tagged_preds_gdf_dict = {} + tagged_dets_gdf_dict = {} # TRUE/FALSE POSITIVES, FALSE NEGATIVES for dataset in metrics.keys(): - tmp_gdf = preds_gdf_dict[dataset].copy() + tmp_gdf = dets_gdf_dict[dataset].copy() tmp_gdf.to_crs(epsg=clipped_labels_gdf.crs.to_epsg(), inplace=True) tmp_gdf = tmp_gdf[tmp_gdf.score >= selected_threshold].copy() @@ -271,16 +271,16 @@ def main(cfg_file_path): fn_gdf['tag'] = 'FN' fn_gdf['dataset'] = dataset - tagged_preds_gdf_dict[dataset] = pd.concat([tp_gdf, fp_gdf, fn_gdf]) + tagged_dets_gdf_dict[dataset] = pd.concat([tp_gdf, fp_gdf, fn_gdf]) precision, recall, f1 = misc.get_metrics(tp_gdf, fp_gdf, fn_gdf) logger.info(f'Dataset = {dataset} => precision = {precision:.3f}, recall = {recall:.3f}, f1 = {f1:.3f}') - tagged_preds_gdf = pd.concat([ - tagged_preds_gdf_dict[x] for x in metrics.keys() + tagged_dets_gdf = pd.concat([ + tagged_dets_gdf_dict[x] for x in metrics.keys() ]) - file_to_write = os.path.join(OUTPUT_DIR, 'tagged_predictions.gpkg') - tagged_preds_gdf[['geometry', 'score', 'tag', 'dataset']].to_file(file_to_write, driver='GPKG', index=False) + file_to_write = os.path.join(OUTPUT_DIR, 'tagged_detections.gpkg') + tagged_dets_gdf[['geometry', 'score', 'tag', 'dataset']].to_file(file_to_write, driver='GPKG', index=False) written_files.append(file_to_write) # ------ wrap-up @@ -300,7 +300,7 @@ def main(cfg_file_path): if __name__ == "__main__": - parser = argparse.ArgumentParser(description="This script assesses the quality of predictions with respect to ground-truth/other labels.") + parser = argparse.ArgumentParser(description="This script assesses the quality of detections with respect to ground-truth/other labels.") parser.add_argument('config_file', type=str, help='a YAML config file') args = parser.parse_args() diff --git a/scripts/cli.py b/scripts/cli.py index 5fa91da..49fb63b 100644 --- a/scripts/cli.py +++ b/scripts/cli.py @@ -3,8 +3,8 @@ import argparse from scripts.generate_tilesets import main as generate_tilesets from scripts.train_model import main as train_model -from scripts.make_predictions import main as make_predictions -from scripts.assess_predictions import main as assess_predictions +from scripts.make_detections import main as make_detections +from scripts.assess_detections import main as assess_detections def main(): @@ -27,17 +27,17 @@ def main(): add_parser.add_argument(**arg_template) add_parser.set_defaults(func=generate_tilesets) - add_parser = subparsers.add_parser("train_model", help="This script trains a predictive model.") + add_parser = subparsers.add_parser("train_model", help="This script trains an object detection model.") add_parser.add_argument(**arg_template) add_parser.set_defaults(func=train_model) - add_parser = subparsers.add_parser("make_predictions", help="This script makes predictions, using a previously trained model.") + add_parser = subparsers.add_parser("make_detections", help="This script makes detections, using a previously trained model.") add_parser.add_argument(**arg_template) - add_parser.set_defaults(func=make_predictions) + add_parser.set_defaults(func=make_detections) - add_parser = subparsers.add_parser("assess_predictions", help="This script assesses the quality of predictions with respect to ground-truth/other labels.") + add_parser = subparsers.add_parser("assess_detections", help="This script assesses the quality of detections with respect to ground-truth/other labels.") add_parser.add_argument(**arg_template) - add_parser.set_defaults(func=assess_predictions) + add_parser.set_defaults(func=assess_detections) # https://stackoverflow.com/a/47440202 args = global_parser.parse_args(args=None if sys.argv[1:] else ['--help']) diff --git a/scripts/generate_tilesets.py b/scripts/generate_tilesets.py index 92700b3..f90eeb0 100644 --- a/scripts/generate_tilesets.py +++ b/scripts/generate_tilesets.py @@ -335,7 +335,7 @@ def main(cfg_file_path): sys.exit(1) - # ------ Collecting image metadata, to be used when assessing predictions + # ------ Collecting image metadata, to be used when assessing detections logger.info("Collecting image metadata...") diff --git a/scripts/make_predictions.py b/scripts/make_detections.py similarity index 89% rename from scripts/make_predictions.py rename to scripts/make_detections.py index 90ecf51..900c164 100644 --- a/scripts/make_predictions.py +++ b/scripts/make_detections.py @@ -31,7 +31,7 @@ parent_dir = current_dir[:current_dir.rfind(os.path.sep)] sys.path.insert(0, parent_dir) -from helpers.detectron2 import detectron2preds_to_features +from helpers.detectron2 import detectron2dets_to_features from helpers.misc import image_metadata_to_affine_transform, format_logger from helpers.constants import DONE_MSG @@ -105,15 +105,15 @@ def main(cfg_file_path): predictor = DefaultPredictor(cfg) - # ---- make predictions + # ---- make detections for dataset in COCO_FILES_DICT.keys(): all_feats = [] crs = None - logger.info(f"Making predictions over the entire {dataset} dataset...") + logger.info(f"Making detections over the entire {dataset} dataset...") - prediction_filename = f'{dataset}_predictions_at_{threshold_str}_threshold.gpkg' + detections_filename = f'{dataset}_detections_at_{threshold_str}_threshold.gpkg' for d in tqdm(DatasetCatalog.get(dataset)): @@ -136,21 +136,21 @@ def main(cfg_file_path): crs = _crs transform = image_metadata_to_affine_transform(im_md) - this_image_feats = detectron2preds_to_features(outputs, crs, transform, RDP_SIMPLIFICATION_ENABLED, RDP_SIMPLIFICATION_EPSILON) + this_image_feats = detectron2dets_to_features(outputs, crs, transform, RDP_SIMPLIFICATION_ENABLED, RDP_SIMPLIFICATION_EPSILON) all_feats += this_image_feats gdf = gpd.GeoDataFrame.from_features(all_feats) gdf['dataset'] = dataset gdf.crs = crs - gdf.to_file(prediction_filename, driver='GPKG', index=False) - written_files.append(os.path.join(WORKING_DIR, prediction_filename)) + gdf.to_file(detections_filename, driver='GPKG', index=False) + written_files.append(os.path.join(WORKING_DIR, detections_filename)) logger.success(DONE_MSG) logger.info("Let's tag some sample images...") for d in DatasetCatalog.get(dataset)[0:min(len(DatasetCatalog.get(dataset)), 10)]: - output_filename = f'{dataset}_pred_{d["file_name"].split("/")[-1]}' + output_filename = f'{dataset}_det_{d["file_name"].split("/")[-1]}' output_filename = output_filename.replace('tif', 'png') im = cv2.imread(d["file_name"]) outputs = predictor(im) @@ -182,7 +182,7 @@ def main(cfg_file_path): if __name__ == "__main__": - parser = argparse.ArgumentParser(description="This script makes predictions, using a previously trained model.") + parser = argparse.ArgumentParser(description="This script makes detections, using a previously trained model.") parser.add_argument('config_file', type=str, help='a YAML config file') args = parser.parse_args() diff --git a/scripts/train_model.py b/scripts/train_model.py index d49ef9b..179c41d 100644 --- a/scripts/train_model.py +++ b/scripts/train_model.py @@ -129,12 +129,12 @@ def main(cfg_file_path): #inference_on_dataset(trainer.model, val_loader, evaluator) cfg.MODEL.WEIGHTS = TRAINED_MODEL_PTH_FILE - logger.info("Make some sample predictions over the test dataset...") + logger.info("Make some sample detections over the test dataset...") predictor = DefaultPredictor(cfg) for d in DatasetCatalog.get("tst_dataset")[0:min(len(DatasetCatalog.get("tst_dataset")), 10)]: - output_filename = "pred_" + d["file_name"].split('/')[-1] + output_filename = "det_" + d["file_name"].split('/')[-1] output_filename = output_filename.replace('tif', 'png') im = cv2.imread(d["file_name"]) outputs = predictor(im) @@ -167,7 +167,7 @@ def main(cfg_file_path): if __name__ == "__main__": - parser = argparse.ArgumentParser(description="This script trains a predictive model.") + parser = argparse.ArgumentParser(description="This script trains an object detection model.") parser.add_argument('config_file', type=str, help='a YAML config file') args = parser.parse_args() From 97808698f087c60655ea737df23cf3510527b31e Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Fri, 15 Sep 2023 15:07:47 +0200 Subject: [PATCH 104/108] Change predictions to detections in functions --- helpers/misc.py | 6 +++--- scripts/assess_detections.py | 14 +++++++------- scripts/generate_tilesets.py | 2 +- scripts/make_detections.py | 10 +++++----- scripts/train_model.py | 4 ++-- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/helpers/misc.py b/helpers/misc.py index f0f4010..b73196c 100644 --- a/helpers/misc.py +++ b/helpers/misc.py @@ -121,10 +121,10 @@ def get_fractional_sets(preds_gdf, labels_gdf): fn_gdf = gpd.GeoDataFrame() return tp_gdf, fp_gdf, fn_gdf - assert(_preds_gdf.crs == _labels_gdf.crs), f"CRS Mismatch: predictions' CRS = {_preds_gdf.crs}, labels' CRS = {_labels_gdf.crs}" + assert(_preds_gdf.crs == _labels_gdf.crs), f"CRS Mismatch: detections' CRS = {_preds_gdf.crs}, labels' CRS = {_labels_gdf.crs}" - # we add a dummy column to the labels dataset, which should not exist in predictions too; - # this allows us to distinguish matching from non-matching predictions + # we add a dummy column to the labels dataset, which should not exist in detections too; + # this allows us to distinguish matching from non-matching detections _labels_gdf['dummy_id'] = _labels_gdf.index # TRUE POSITIVES diff --git a/scripts/assess_detections.py b/scripts/assess_detections.py index 3c17628..0bec33d 100644 --- a/scripts/assess_detections.py +++ b/scripts/assess_detections.py @@ -40,7 +40,7 @@ def main(cfg_file_path): OUTPUT_DIR = cfg['output_folder'] IMG_METADATA_FILE = cfg['datasets']['image_metadata_json'] - PREDICTION_FILES = cfg['datasets']['predictions'] + PREDICTION_FILES = cfg['datasets']['detections'] SPLIT_AOI_TILES_GEOJSON = cfg['datasets']['split_aoi_tiles_geojson'] if 'ground_truth_labels_geojson' in cfg['datasets'].keys(): @@ -114,7 +114,7 @@ def main(cfg_file_path): # let's extract filenames (w/o path) img_metadata_dict = {os.path.split(k)[-1]: v for (k, v) in tmp.items()} - # ------ Loading predictions + # ------ Loading detections preds_gdf_dict = {} @@ -124,7 +124,7 @@ def main(cfg_file_path): if len(labels_gdf)>0: - # ------ Comparing predictions with ground-truth data and computing metrics + # ------ Comparing detections with ground-truth data and computing metrics # init metrics = {} @@ -246,12 +246,12 @@ def main(cfg_file_path): written_files.append(file_to_write) - # ------ tagging predictions + # ------ tagging detections # we select the threshold which maximizes the f1-score on the val dataset selected_threshold = metrics_df_dict['val'].iloc[metrics_df_dict['val']['f1'].argmax()]['threshold'] - logger.info(f"Tagging predictions with threshold = {selected_threshold:.2f}, which maximizes the f1-score on the val dataset.") + logger.info(f"Tagging detections with threshold = {selected_threshold:.2f}, which maximizes the f1-score on the val dataset.") tagged_preds_gdf_dict = {} @@ -279,7 +279,7 @@ def main(cfg_file_path): tagged_preds_gdf_dict[x] for x in metrics.keys() ]) - file_to_write = os.path.join(OUTPUT_DIR, 'tagged_predictions.gpkg') + file_to_write = os.path.join(OUTPUT_DIR, 'tagged_detections.gpkg') tagged_preds_gdf[['geometry', 'score', 'tag', 'dataset']].to_file(file_to_write, driver='GPKG', index=False) written_files.append(file_to_write) @@ -300,7 +300,7 @@ def main(cfg_file_path): if __name__ == "__main__": - parser = argparse.ArgumentParser(description="This script assesses the quality of predictions with respect to ground-truth/other labels.") + parser = argparse.ArgumentParser(description="This script assesses the quality of detections with respect to ground-truth/other labels.") parser.add_argument('config_file', type=str, help='a YAML config file') args = parser.parse_args() diff --git a/scripts/generate_tilesets.py b/scripts/generate_tilesets.py index 92700b3..f90eeb0 100644 --- a/scripts/generate_tilesets.py +++ b/scripts/generate_tilesets.py @@ -335,7 +335,7 @@ def main(cfg_file_path): sys.exit(1) - # ------ Collecting image metadata, to be used when assessing predictions + # ------ Collecting image metadata, to be used when assessing detections logger.info("Collecting image metadata...") diff --git a/scripts/make_detections.py b/scripts/make_detections.py index 90ecf51..42b1ed6 100644 --- a/scripts/make_detections.py +++ b/scripts/make_detections.py @@ -105,15 +105,15 @@ def main(cfg_file_path): predictor = DefaultPredictor(cfg) - # ---- make predictions + # ---- make detections for dataset in COCO_FILES_DICT.keys(): all_feats = [] crs = None - logger.info(f"Making predictions over the entire {dataset} dataset...") + logger.info(f"Making detections over the entire {dataset} dataset...") - prediction_filename = f'{dataset}_predictions_at_{threshold_str}_threshold.gpkg' + prediction_filename = f'{dataset}_detections_at_{threshold_str}_threshold.gpkg' for d in tqdm(DatasetCatalog.get(dataset)): @@ -159,7 +159,7 @@ def main(cfg_file_path): scale=1.0, instance_mode=ColorMode.IMAGE_BW # remove the colors of unsegmented pixels ) - v = v.draw_instance_predictions(outputs["instances"].to("cpu")) + v = v.draw_instance_detections(outputs["instances"].to("cpu")) cv2.imwrite(os.path.join(SAMPLE_TAGGED_IMG_SUBDIR, output_filename), v.get_image()[:, :, ::-1]) written_files.append( os.path.join(WORKING_DIR, os.path.join(SAMPLE_TAGGED_IMG_SUBDIR, output_filename)) ) logger.success(DONE_MSG) @@ -182,7 +182,7 @@ def main(cfg_file_path): if __name__ == "__main__": - parser = argparse.ArgumentParser(description="This script makes predictions, using a previously trained model.") + parser = argparse.ArgumentParser(description="This script makes detections, using a previously trained model.") parser.add_argument('config_file', type=str, help='a YAML config file') args = parser.parse_args() diff --git a/scripts/train_model.py b/scripts/train_model.py index d49ef9b..9e252a0 100644 --- a/scripts/train_model.py +++ b/scripts/train_model.py @@ -129,7 +129,7 @@ def main(cfg_file_path): #inference_on_dataset(trainer.model, val_loader, evaluator) cfg.MODEL.WEIGHTS = TRAINED_MODEL_PTH_FILE - logger.info("Make some sample predictions over the test dataset...") + logger.info("Make some sample detections over the test dataset...") predictor = DefaultPredictor(cfg) @@ -143,7 +143,7 @@ def main(cfg_file_path): scale=1.0, instance_mode=ColorMode.IMAGE_BW # remove the colors of unsegmented pixels ) - v = v.draw_instance_predictions(outputs["instances"].to("cpu")) + v = v.draw_instance_detections(outputs["instances"].to("cpu")) cv2.imwrite(os.path.join(SAMPLE_TAGGED_IMG_SUBDIR, output_filename), v.get_image()[:, :, ::-1]) written_files.append( os.path.join(WORKING_DIR, os.path.join(SAMPLE_TAGGED_IMG_SUBDIR, output_filename)) ) From c2f88051e2abc14f89b475052fafbc6d79ef25d0 Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Fri, 15 Sep 2023 15:27:00 +0200 Subject: [PATCH 105/108] Correct function name in make_detections --- scripts/make_detections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/make_detections.py b/scripts/make_detections.py index 65dc83e..ae5780c 100644 --- a/scripts/make_detections.py +++ b/scripts/make_detections.py @@ -159,7 +159,7 @@ def main(cfg_file_path): scale=1.0, instance_mode=ColorMode.IMAGE_BW # remove the colors of unsegmented pixels ) - v = v.draw_instance_detections(outputs["instances"].to("cpu")) + v = v.draw_instance_predictions(outputs["instances"].to("cpu")) cv2.imwrite(os.path.join(SAMPLE_TAGGED_IMG_SUBDIR, output_filename), v.get_image()[:, :, ::-1]) written_files.append( os.path.join(WORKING_DIR, os.path.join(SAMPLE_TAGGED_IMG_SUBDIR, output_filename)) ) logger.success(DONE_MSG) From 76d37e32e30ac26b90e328e5236851f781dc006a Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Fri, 15 Sep 2023 15:43:32 +0200 Subject: [PATCH 106/108] Add link to swissimage time traval --- examples/quarry-detection/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/quarry-detection/README.md b/examples/quarry-detection/README.md index 776e146..b6704a9 100644 --- a/examples/quarry-detection/README.md +++ b/examples/quarry-detection/README.md @@ -36,7 +36,7 @@ $ sudo chmod -R a+w examples We strongly encourage the end-user to review the provided `config_trne.yaml` and `config_prd.yaml` files as well as the various output files, a list of which is printed by each script before exiting. -The model is trained on the 2020 [SWISSIMAGE](https://www.swisstopo.admin.ch/fr/geodata/images/ortho/swissimage10.html) mosaic. Inference can be performed on SWISSIMAGE mosaics of the product SWISSIMAGE time travel by changing the year in `config_prd.yaml`. It should be noted that the model has been trained on RGB images and might not perform as well on B&W images. +The model is trained on the 2020 [SWISSIMAGE](https://www.swisstopo.admin.ch/fr/geodata/images/ortho/swissimage10.html) mosaic. Inference can be performed on SWISSIMAGE mosaics of the product [SWISSIMAGE time travel](https://map.geo.admin.ch/?lang=en&topic=swisstopo&bgLayer=ch.swisstopo.pixelkarte-farbe&zoom=0&layers_timestamp=2004,2004,&layers=ch.swisstopo.swissimage-product,ch.swisstopo.swissimage-product.metadata,ch.swisstopo.images-swissimage-dop10.metadata&E=2594025.91&N=1221065.68&layers_opacity=1,0.7,1&time=2004&layers_visibility=true,true,false) by changing the year in `config_prd.yaml`. It should be noted that the model has been trained on RGB images and might not perform as well on B&W images. For more information about this project, see [this repository](https://github.com/swiss-territorial-data-lab/proj-dqry) (not public yet). From 85d7577aa45de91e19248c28e00bd1a32c51870b Mon Sep 17 00:00:00 2001 From: Clemence Herny Date: Fri, 15 Sep 2023 15:52:13 +0200 Subject: [PATCH 107/108] Correct error in function name of train model --- scripts/train_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/train_model.py b/scripts/train_model.py index a647aba..179c41d 100644 --- a/scripts/train_model.py +++ b/scripts/train_model.py @@ -143,7 +143,7 @@ def main(cfg_file_path): scale=1.0, instance_mode=ColorMode.IMAGE_BW # remove the colors of unsegmented pixels ) - v = v.draw_instance_detections(outputs["instances"].to("cpu")) + v = v.draw_instance_predictions(outputs["instances"].to("cpu")) cv2.imwrite(os.path.join(SAMPLE_TAGGED_IMG_SUBDIR, output_filename), v.get_image()[:, :, ::-1]) written_files.append( os.path.join(WORKING_DIR, os.path.join(SAMPLE_TAGGED_IMG_SUBDIR, output_filename)) ) From 7f6d1d74d12f434a3e6505c3ff6c25329bfdbde5 Mon Sep 17 00:00:00 2001 From: Alessandro Cerioni Date: Fri, 15 Sep 2023 16:13:31 +0200 Subject: [PATCH 108/108] Replace link to external info on Slippy Map Tiles with an active one --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 39a1d1a..63f5ba7 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ The same configuration file can be used for all the commands, as each of them on * regions for which ground-truth data is available, as well as * regions over which the user intends to detect potentially unknown objects -* **tiles**, or - more explicitly - "geographical map tiles": see [this link](https://wiki.openstreetmap.org/wiki/Tiles). More precisely, "Slippy Map Tiles" are used within this project, see [this link](https://developers.planet.com/tutorials/slippy-maps-101/). +* **tiles**, or - more explicitly - "geographical map tiles": see [this link](https://wiki.openstreetmap.org/wiki/Tiles). More precisely, "Slippy Map Tiles" are used within this project, see [this link](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames). * **COCO data format**: see [this link](https://cocodataset.org/#format-data) @@ -290,4 +290,4 @@ A few examples are provided within the `examples` folder. For further details, w ## License -The STDL Object Detector is released under the [MIT license](LICENSE.md). \ No newline at end of file +The STDL Object Detector is released under the [MIT license](LICENSE.md).