Skip to content

Commit 794620b

Browse files
committed
Merge master
2 parents 8ae9b82 + d5c736d commit 794620b

File tree

20 files changed

+183
-214
lines changed

20 files changed

+183
-214
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,11 @@ train_model.py:
232232
tst: <the COCO JSON file related to the test dataset>
233233
detectron2_config_file: <the detectron2 configuration file (relative path w/ respect to the working_folder>
234234
model_weights:
235-
model_zoo_checkpoint_url: <e.g. "COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x.yaml">
235+
pth_file: <path to the model if training is resumed. Defaults to "<log directory>/model_final.pth">
236+
model_zoo_checkpoint_url: <zoo model to start training from, e.g. "COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x.yaml">
237+
init_model_weights: <True or False; if True, the model weights will be initialized to 0 (optional, defaults to False)>
238+
resume_training: <True or False; if True, the training is resumed from the final weights saved in the log folder. Defaults to False>
239+
data_augmentation: <True or False; if True, apply random adjustment of brightness, contrast, saturation, lightning, and size, plus flip the image horizontally. Defaults to False>
236240
```
237241

238242
Detectron2's 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**.
@@ -323,7 +327,7 @@ A few examples are provided within the `examples` folder. For further details, w
323327
* [Segmentation of Border Points based on Analog Cadastral Plans](examples/borderpoints/README.md): multi-class instance segmentation with images from another folder based on a custom grid,
324328
* [Evolution of Mineral Extraction Sites over the Entire Switzerland](examples/mineral-extrac-sites-detection/README.md): object monitoring with images from an XYZ service,
325329
* [Swimming Pool Detection over the Canton of Geneva](examples/swimming-pool-detection/GE/README.md): instance segmentation with images from a MIL service,
326-
* [Swimming Pool Detection over the Canton of Neuchâtel](examples/swimming-pool-detection/NE/README.md): instance segmentation with images from a WMS service.service,
330+
* [Swimming Pool Detection over the Canton of Neuchâtel](examples/swimming-pool-detection/NE/README.md): instance segmentation with images from a WMS service.
327331

328332
It is brought to the reader attention that the examples are provided with a debug parameter that can be set to `True` for quick tests.
329333

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Example: segmentation of the border points based on the analog cadastral plans
1+
# Example: segmentation of anthropic soils on historic images
22

33
A working setup is provided here to test the multi-class classification and the use of empty tiles, as well as false positive ones.
44
It consists of the following elements:
@@ -7,10 +7,10 @@ It consists of the following elements:
77
* input data
88
* scripts for data preparation and the first step of post-processing
99

10-
The full project is available is its [own repository](https://github.com/swiss-territorial-data-lab/proj-borderpoints).
10+
The full project is available is its [own repository](https://github.com/swiss-territorial-data-lab/proj-sda).
1111

1212

13-
The **installation** can be carried out by following the instructions in the main readme file. When using Docker, the container must be launched from the repository root folder before running the workflow:
13+
The **installation** can be carried out by following the instructions [here](../../README.md). When using Docker, the container must be launched from this repository root folder before running the workflow:
1414

1515
```bash
1616
$ sudo chown -R 65534:65534 examples
@@ -28,39 +28,53 @@ $ sudo chmod -R a+w examples
2828

2929
## Data
3030

31+
"[SWISSIMAGE Journey through time](https://www.swisstopo.admin.ch/en/timetravel-aerial-images)" is an annual dataset of aerial images of Switzerland from 1946 to today. These images are downloaded from the geo.admin.ch server using the XYZ connector.
32+
3133
The following datasets are available for this example in the `data` folder:
3234

33-
* images: SWISSIMAGE Journey is an annual dataset of aerial images of Switzerland from 1946 to today. The images are downloaded from the geo.admin.ch server using XYZ connector.
34-
* empty tiles: tiles without any object of interest added to the training dataset to provide more contextual tiles.
35-
* FP labels: objects frequently present among false positives, used to include the corresponding tiles in the training.
36-
* ground truth: labels vectorised by the domain experts.
37-
Disclaimer: the ground truth dataset is unofficial and has been produced specifically for the purposes of the project.
35+
* Empty tiles: tiles without any object of interest can be added to the training dataset to provide more context.
36+
* False positive (FP) labels: objects frequently present among false positives, used to include the corresponding tiles in the training.
37+
* Ground truth: labels vectorised by domain experts.
38+
39+
> [!CAUTION]
40+
> The ground truth dataset is unofficial and has been produced specifically for the purposes of the project.
41+
3842

3943
## Workflow
4044

4145
The workflow can be executed by running the following list of actions and commands.
4246

4347
Prepare the data:
44-
```
48+
49+
```bash
4550
$ python prepare_data.py config_trne.yaml
4651
$ stdl-objdet generate_tilesets config_trne.yaml
4752
```
4853

4954
Train the model:
50-
```
55+
56+
```bash
5157
$ stdl-objdet train_model config_trne.yaml
5258
$ tensorboard --logdir output/trne/logs
5359
```
5460

55-
Open the following link with a web browser: `http://localhost:6006` and identify the iteration minimising the validation loss and select the model accordingly (`model_*.pth`) in `config_trne`. The path to the trained model is `output/trne/logs/model_<number of iterations>.pth`, currently `model_0002499.pth` is used. <br>
61+
Open another shell and launch TensorBoard:
5662

57-
Perform and assess detections:
63+
```bash
64+
$ docker compose run --rm -p 6006:6006 stdl-objdet tensorboard --logdir /app/examples/anthropogenic-activities/output/trne/logs --bind_all
5865
```
66+
67+
Open the following link with a web browser: `http://localhost:6006` and identify the iteration minimising the validation loss and select the model accordingly (`model_*.pth`) in `config_trne`. The path to the trained model is `output/trne/logs/model_<number of iterations>.pth`, currently `model_0002499.pth` is used.
68+
69+
Perform and assess detections:
70+
71+
```bash
5972
$ stdl-objdet make_detections config_trne.yaml
6073
$ stdl-objdet assess_detections config_trne.yaml
6174
```
6275

6376
The detections obtained by tiles can be merged when adjacent:
64-
```
77+
78+
```bash
6579
$ python merge_detections.py config_trne.yaml
6680
```

examples/anthropogenic-activities/config_trne.yaml

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ prepare_data.py:
55
fp_shapefile: data/FP_labels.gpkg # FP labels
66
# empty_tiles_aoi: data/AoI/<AOI_SHPFILE> # AOI in which additional empty tiles can be selected. Only one 'empty_tiles' option can be selected
77
# empty_tiles_year: 2023 # If "empty_tiles_aoi" selected then provide a year. Choice: (1) numeric (i.e. 2020), (2) [year1, year2] (random selection of a year within a given year range)
8-
empty_tiles_shp: data/20240726_EPT.gpkg # Provided shapefile of selected empty tiles. Only one 'empty_tiles' option can be selected
8+
empty_tiles_shp: data/20250924_emtpy_tiles.gpkg # Provided shapefile of selected empty tiles. Only one 'empty_tiles' option can be selected
99
category_field: Classe
1010
output_folder: output/trne/
1111
zoom_level: 16
@@ -16,11 +16,12 @@ generate_tilesets.py:
1616
enable: False # sample of tiles
1717
nb_tiles_max: 2000
1818
working_directory: .
19+
output_folder: output/trne/
1920
datasets:
2021
aoi_tiles: output/trne/tiles.geojson
2122
ground_truth_labels: output/trne/labels.geojson
22-
fp_labels: # Uncomment if FP shapefile exists in prepare_data.py
23-
fp_shp: output/trne/FP.geojson
23+
add_fp_labels: # Uncomment if FP shapefile exists in prepare_data.py
24+
fp_labels: output/trne/FP.geojson
2425
frac_trn: 0.7 # fraction of fp tiles to add to the trn dataset, then the remaining tiles will be split in 2 and added to tst and val datasets
2526
image_source:
2627
type: XYZ # supported values: 1. MIL = Map Image Layer 2. WMS 3. XYZ 4. FOLDER
@@ -30,7 +31,6 @@ generate_tilesets.py:
3031
tiles_frac: 0.5 # fraction (relative to the number of tiles intersecting labels) of empty tiles to add
3132
frac_trn: 0.7 # fraction of empty tiles to add to the trn dataset, then the remaining tiles will be split in 2 and added to tst and val datasets
3233
keep_oth_tiles: False # keep tiles in oth dataset not intersecting oth labels
33-
output_folder: output/trne/
3434
tile_size: 256 # per side, in pixels
3535
overwrite: True
3636
n_jobs: 10
@@ -94,21 +94,10 @@ assess_detections.py:
9494
metrics_method: micro-average # 1: macro-average ; 2: macro-weighted-average ; 3: micro-average
9595
# confidence_threshold: 0.05
9696

97-
# Plots (optional)
98-
result_analysis.py:
99-
working_directory: output/trne
100-
output_directory: plots
101-
detections: tagged_detections.gpkg
102-
min_year: 1950
103-
max_year: 2023
104-
class_dict: {'Activité non agricole': 'Non-agricultural activity', # Provide a customed legend
105-
'Mouvement de terrain': 'Land movement'}
106-
10797
# Merge detections across tiles
10898
merge_detections.py:
10999
working_directory: output/trne
110100
output_dir: post_processed
111-
labels: labels.geojson
112101
detections:
113102
trn: trn_detections_at_0dot05_threshold.gpkg
114103
val: val_detections_at_0dot05_threshold.gpkg

examples/anthropogenic-activities/detectron2_config.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,6 @@ MODEL:
262262
NAME: SemSegFPNHead
263263
NORM: GN
264264
NUM_CLASSES: 54
265-
WEIGHTS: https://dl.fbaipublicfiles.com/detectron2/COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x/137260431/model_final_a54504.pkl
266265
OUTPUT_DIR: logs
267266
SEED: 42
268267
SOLVER:

examples/anthropogenic-activities/merge_detections.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
# Load input parameters
3636
WORKING_DIR = cfg['working_directory']
3737
OUTPUT_DIR = cfg['output_dir']
38-
LABELS = cfg['labels'] if 'labels' in cfg.keys() else None
3938
DETECTION_FILES = cfg['detections']
4039

4140
DISTANCE = cfg['distance']
@@ -100,8 +99,8 @@
10099
detections_tiles_join_gdf = gpd.sjoin(tiles_gdf, detections_buffer_gdf, how='left', predicate='contains')
101100
remove_det_list = detections_tiles_join_gdf.det_id.unique().tolist()
102101

103-
detections_overlap_tiles_gdf = detections_by_year_gdf[~detections_by_year_gdf.det_id.isin(remove_det_list)].drop_duplicates(subset=['det_id'], ignore_index=True)
104102
detections_within_tiles_gdf = detections_by_year_gdf[detections_by_year_gdf.det_id.isin(remove_det_list)].drop_duplicates(subset=['det_id'], ignore_index=True)
103+
detections_overlap_tiles_gdf = detections_by_year_gdf[~detections_by_year_gdf.det_id.isin(remove_det_list)].drop_duplicates(subset=['det_id'], ignore_index=True)
105104

106105
# Merge polygons within the thd distance
107106
detections_overlap_tiles_gdf.loc[:, 'geometry'] = detections_overlap_tiles_gdf.buffer(DISTANCE, resolution=2)

examples/anthropogenic-activities/prepare_data.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161

6262
written_files = []
6363

64-
gt_labels_4326_gdf = ffe.preapre_labels(SHPFILE, CATEGORY, supercategory=SUPERCATEGORY)
64+
gt_labels_4326_gdf = ffe.prepare_labels(SHPFILE, CATEGORY, supercategory=SUPERCATEGORY)
6565

6666
label_filepath = os.path.join(OUTPUT_DIR, 'labels.geojson')
6767
gt_labels_4326_gdf.to_file(label_filepath, driver='GeoJSON')

examples/borderpoints/config/detectron2_config.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,6 @@ MODEL:
258258
LOSS_WEIGHT: 1.0
259259
NAME: SemSegFPNHead
260260
NORM: GN
261-
NUM_CLASSES: 54
262-
WEIGHTS: https://dl.fbaipublicfiles.com/detectron2/COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x/137260431/model_final_a54504.pkl
263261
OUTPUT_DIR: logs
264262
SEED: 42
265263
SOLVER:

examples/mineral-extract-sites-detection/detectron2_config.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,6 @@ MODEL:
261261
LOSS_WEIGHT: 1.0
262262
NAME: SemSegFPNHead
263263
NORM: GN
264-
NUM_CLASSES: 54
265-
WEIGHTS: https://dl.fbaipublicfiles.com/detectron2/COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x/137260431/model_final_a54504.pkl
266264
OUTPUT_DIR: logs
267265
SEED: 42
268266
SOLVER:

examples/mineral-extract-sites-detection/prepare_data.py

Lines changed: 3 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
from shapely.geometry import Polygon
1616

1717
sys.path.insert(0, '../..')
18-
import helpers.misc as misc
18+
from helpers.functions_for_examples import format_all_tiles, prepare_labels
19+
from helpers.misc import format_logger
1920
from helpers.constants import DONE_MSG
2021

2122
from loguru import logger
@@ -176,70 +177,7 @@ def prepare_labels(shpfile, written_files, prefix=''):
176177

177178
written_files = []
178179

179-
# Prepare the tiles
180-
181-
## Convert datasets shapefiles into geojson format
182-
logger.info('Convert the label shapefiles into GeoJSON format (EPSG:4326)...')
183-
labels_4326_gdf, written_files = prepare_labels(SHPFILE, written_files)
184-
gt_labels_4326_gdf = labels_4326_gdf[['geometry', 'CATEGORY', 'SUPERCATEGORY']].copy()
185-
186-
# Add FP labels if it exists
187-
if FP_SHPFILE:
188-
logger.info('Convert the FP label shapefiles into GeoJSON format (EPSG:4326)...')
189-
fp_labels_4326_gdf, written_files = prepare_labels(FP_SHPFILE, written_files, prefix='FP_')
190-
labels_4326_gdf = pd.concat([labels_4326_gdf, fp_labels_4326_gdf], ignore_index=True)
191-
192-
# Tiling of the AoI
193-
logger.info("- Get the label boundaries")
194-
boundaries_df = labels_4326_gdf.bounds
195-
logger.info("- Tiling of the AoI")
196-
tiles_4326_aoi_gdf = aoi_tiling(boundaries_df)
197-
tiles_4326_labels_gdf = gpd.sjoin(tiles_4326_aoi_gdf, labels_4326_gdf, how='inner', predicate='intersects')
198-
199-
# Tiling of the AoI from which empty tiles will be selected
200-
if EPT_SHPFILE:
201-
EPT_aoi_gdf = gpd.read_file(EPT_SHPFILE)
202-
EPT_aoi_4326_gdf = EPT_aoi_gdf.to_crs(epsg=4326)
203-
assert_year(labels_4326_gdf, EPT_aoi_4326_gdf, 'empty_tiles', EPT_YEAR)
204-
205-
if EPT_TYPE == 'aoi':
206-
logger.info("- Get AoI boundaries")
207-
EPT_aoi_boundaries_df = EPT_aoi_4326_gdf.bounds
208-
209-
# Get tile coordinates and shapes
210-
logger.info("- Tiling of the empty tiles AoI")
211-
empty_tiles_4326_all_gdf = aoi_tiling(EPT_aoi_boundaries_df)
212-
# Delete tiles outside of the AoI limits
213-
empty_tiles_4326_aoi_gdf = gpd.sjoin(empty_tiles_4326_all_gdf, EPT_aoi_4326_gdf, how='inner', lsuffix='ept_tiles', rsuffix='ept_aoi')
214-
# Attribute a year to empty tiles if necessary
215-
if 'year' in labels_4326_gdf.keys():
216-
if isinstance(EPT_YEAR, int):
217-
empty_tiles_4326_aoi_gdf['year'] = int(EPT_YEAR)
218-
else:
219-
empty_tiles_4326_aoi_gdf['year'] = np.random.randint(low=EPT_YEAR[0], high=EPT_YEAR[1], size=(len(empty_tiles_4326_aoi_gdf)))
220-
elif EPT_TYPE == 'shp':
221-
if EPT_YEAR:
222-
logger.warning("A shapefile of selected empty tiles are provided. The year set for the empty tiles in the configuration file will be ignored")
223-
EPT_YEAR = None
224-
empty_tiles_4326_aoi_gdf = EPT_aoi_4326_gdf.copy()
225-
226-
# Get all the tiles in one gdf
227-
logger.info("- Concatenate label tiles and empty AoI tiles")
228-
tiles_4326_all_gdf = pd.concat([tiles_4326_labels_gdf, empty_tiles_4326_aoi_gdf])
229-
else:
230-
tiles_4326_all_gdf = tiles_4326_labels_gdf.copy()
231-
232-
# - Remove useless columns, reset feature id and redefine it according to xyz format
233-
logger.info('- Add tile IDs and reorganise the data set')
234-
tiles_4326_all_gdf = tiles_4326_all_gdf[['geometry', 'title', 'year'] if 'year' in tiles_4326_all_gdf.keys() else ['geometry', 'title']].copy()
235-
tiles_4326_all_gdf.reset_index(drop=True, inplace=True)
236-
tiles_4326_all_gdf = tiles_4326_all_gdf.apply(add_tile_id, axis=1)
237-
238-
# - Remove duplicated tiles
239-
tiles_4326_all_gdf.drop_duplicates(['id'], inplace=True)
240-
241-
nb_tiles = len(tiles_4326_all_gdf)
242-
logger.info(f"There were {nb_tiles} tiles created")
180+
gt_labels_4326_gdf = prepare_labels(SHPFILE, CATEGORY, supercategory=SUPERCATEGORY)
243181

244182
# Get the number of tiles intersecting labels
245183
tiles_4326_gt_gdf = gpd.sjoin(tiles_4326_all_gdf, gt_labels_4326_gdf, how='inner', predicate='intersects')

0 commit comments

Comments
 (0)