├── .gitignore
├── .gitlab-ci.yml
├── LICENSE
├── README.md
├── configs
└── dakar-example
│ └── config_pleiades_and_spot_k7_eval_pleiades_dakar.json
├── figs
├── hiector-logo.png
├── hiector.png
├── showcase-pleiades-001.png
├── showcase-pleiades-002.png
├── showcase-pleiades-003.png
├── showcase-pleiades-004.png
├── showcase-pleiades-005.png
├── showcase-sentinel-2-001.png
├── showcase-sentinel-2-002.png
├── showcase-sentinel-2-003.png
├── showcase-sentinel-2-004.png
├── showcase-sentinel-2-005.png
├── showcase-spot-001.png
├── showcase-spot-002.png
├── showcase-spot-003.png
├── showcase-spot-004.png
└── showcase-spot-005.png
├── hiector
├── __init__.py
├── ssrdd
│ ├── LICENSE
│ ├── README.md
│ ├── __init__.py
│ ├── config.py
│ ├── data
│ │ ├── __init__.py
│ │ ├── aug
│ │ │ ├── __init__.py
│ │ │ ├── compose.py
│ │ │ ├── func.py
│ │ │ └── ops
│ │ │ │ ├── __init__.py
│ │ │ │ ├── ops_det.py
│ │ │ │ └── ops_img.py
│ │ └── dataset
│ │ │ ├── __init__.py
│ │ │ └── dataset.py
│ ├── model
│ │ ├── __init__.py
│ │ ├── backbone
│ │ │ ├── __init__.py
│ │ │ ├── darknet.py
│ │ │ └── resnet
│ │ │ │ ├── __init__.py
│ │ │ │ ├── resnet.py
│ │ │ │ └── splat.py
│ │ └── rdd
│ │ │ ├── __init__.py
│ │ │ ├── rdd.py
│ │ │ └── utils
│ │ │ ├── __init__.py
│ │ │ ├── detect.py
│ │ │ ├── loss.py
│ │ │ ├── modules.py
│ │ │ └── priorbox.py
│ ├── utils
│ │ ├── __init__.py
│ │ ├── adjust_lr.py
│ │ ├── box
│ │ │ ├── __init__.py
│ │ │ ├── bbox.py
│ │ │ ├── bbox_np.py
│ │ │ ├── ext
│ │ │ │ ├── __init__.py
│ │ │ │ ├── rbbox_overlap_cpu
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── rbbox_overlap.h
│ │ │ │ │ ├── rbbox_overlap.pyx
│ │ │ │ │ └── setup.py
│ │ │ │ └── rbbox_overlap_gpu
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── rbbox_overlap.cpp
│ │ │ │ │ ├── rbbox_overlap.hpp
│ │ │ │ │ ├── rbbox_overlap.pyx
│ │ │ │ │ ├── rbbox_overlap_kernel.cu
│ │ │ │ │ └── setup.py
│ │ │ ├── metric.py
│ │ │ ├── rbbox.py
│ │ │ └── rbbox_np.py
│ │ ├── init.py
│ │ ├── misc.py
│ │ └── parallel
│ │ │ ├── __init__.py
│ │ │ ├── data_parallel.py
│ │ │ └── sync_batchnorm
│ │ │ ├── __init__.py
│ │ │ ├── batchnorm.py
│ │ │ ├── comm.py
│ │ │ └── replicate.py
│ └── xtorch
│ │ ├── README.md
│ │ ├── __init__.py
│ │ └── xnn
│ │ ├── __init__.py
│ │ ├── containers.py
│ │ └── layers.py
├── tasks
│ ├── __init__.py
│ ├── cropping.py
│ ├── execute.py
│ ├── preprocessing.py
│ ├── reference.py
│ ├── satellite_data.py
│ └── training_data.py
└── utils
│ ├── __init__.py
│ ├── aws_utils.py
│ ├── download.py
│ ├── geometry.py
│ ├── grid.py
│ ├── metrics.py
│ ├── multiprocess.py
│ ├── preprocessing.py
│ ├── training_data.py
│ └── vector.py
├── infrastructure
├── Dockerfile
└── cluster.yaml
├── pyproject.toml
├── requirements-dev.txt
├── requirements.txt
├── scripts
├── compute_ap.py
├── compute_normalization_stats.py
├── prepare_spot_areas.py
├── prepare_training_data.py
├── ssrdd
│ ├── dota_to_gpkg.py
│ ├── execute.py
│ └── raypredict.py
└── training_data_selection.py
├── setup.py
└── tests
├── _data
├── ssrdd
│ └── test_nms
│ │ └── input
│ │ ├── merged-bboxes.gpkg
│ │ ├── test-nms-bboxes.npy
│ │ ├── test-nms-labels.npy
│ │ └── test-nms-scores.npy
├── tasks
│ └── test_reference
│ │ └── input
│ │ ├── test-eop-prepared
│ │ ├── bbox.geojson
│ │ └── vector_timeless
│ │ │ └── BUILDINGS_MRR.gpkg
│ │ └── test-eop
│ │ ├── bbox.geojson
│ │ └── vector_timeless
│ │ └── BUILDINGS.gpkg
└── utils
│ ├── test_cropping
│ └── input
│ │ └── eop-preprocessed
│ │ ├── bbox.geojson
│ │ ├── data
│ │ └── bands.npy
│ │ ├── mask
│ │ └── dataMask.npy
│ │ ├── mask_timeless
│ │ └── BUILDINGS.npy
│ │ ├── timestamp.json
│ │ └── vector
│ │ └── SNOW_MASK.gpkg
│ └── test_metrics
│ └── input
│ ├── Task1_building.txt
│ ├── eop_data.csv
│ ├── gdf-gt.gpkg
│ └── gdf-pr.gpkg
├── conftest.py
├── ssrdd
└── test_nms.py
├── tasks
└── test_reference.py
└── utils
├── test_cropping.py
├── test_geometry.py
└── test_metrics.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # pycache
2 | __pycache__
3 |
4 | # wandb
5 | wandb
6 |
7 | # notebooks
8 | *.ipynb_checkpoints
9 |
10 | # pycharm
11 | .idea/
12 |
13 | # eggs
14 | *.egg-info/
15 |
16 | # C extensions
17 | *.so
18 | *.cpp
19 |
20 | # build
21 | build/
22 |
23 | # dist
24 | dist/
25 |
26 | # pytest cache
27 | .pytest_cache/
28 |
29 | # MacOS cache
30 | .DS_store
31 |
32 | # .vscode
33 | .vscode/
34 |
35 | # test output
36 | tests/_data/ssrdd/test_nms/output/*
37 |
38 | Pipfile*
39 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | image: alpine:3.14
2 |
3 | stages:
4 | - build
5 | - release
6 |
7 | build docker:
8 | image: tmaier/docker-compose:latest
9 | stage: build
10 | when: manual
11 | variables:
12 | IMAGE_TAG: qp3:${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA}
13 | script:
14 | - IMAGE_SAFE_TAG=$(echo $IMAGE_TAG | sed s#/#-#g)
15 | - pip install awscli
16 | - aws configure set aws_access_key_id $AMI_AWS_ACCESS_KEY
17 | - aws configure set aws_secret_access_key $AMI_AWS_SECRET_KEY
18 | - aws configure set region eu-central-1
19 | - aws ecr get-login-password | docker login --username AWS --password-stdin ${AMI_OWNER}.dkr.ecr.eu-central-1.amazonaws.com
20 | - cd ./infrastructure
21 | - docker build --no-cache -f=Dockerfile --tag=$IMAGE_SAFE_TAG
22 | --build-arg S3_AWS_ACCESS_KEY=${S3_AWS_ACCESS_KEY}
23 | --build-arg S3_AWS_SECRET_KEY=${S3_AWS_SECRET_KEY}
24 | --build-arg SH_INSTANCE_ID=${SH_INSTANCE_ID}
25 | --build-arg SH_CLIENT_ID=${SH_CLIENT_ID}
26 | --build-arg SH_CLIENT_SECRET=${SH_CLIENT_SECRET}
27 | --build-arg SENTINELHUB_BRANCH=${SENTINELHUB_BRANCH}
28 | --build-arg EOLEARN_BRANCH=${EOLEARN_BRANCH}
29 | --build-arg LCMS_BRANCH=${LCMS_BRANCH}
30 | --build-arg HIECTOR_BRANCH=${CI_COMMIT_REF_NAME}
31 | .
32 | - docker tag ${IMAGE_SAFE_TAG} ${AMI_OWNER}.dkr.ecr.eu-central-1.amazonaws.com/${IMAGE_SAFE_TAG}
33 | - docker push ${AMI_OWNER}.dkr.ecr.eu-central-1.amazonaws.com/${IMAGE_SAFE_TAG}
34 | - echo ${IMAGE_SAFE_TAG}
35 |
36 |
37 | release to github:
38 | stage: release
39 | when: always
40 | only:
41 | refs:
42 | - tags
43 | before_script:
44 | - apk add --no-cache git openssh-client
45 | script:
46 | - mkdir -m 700 ~/.ssh
47 | - echo "${GITHUB_SSH_KEY}" | tr -d '\r' > ~/.ssh/id_rsa
48 | - chmod 600 ~/.ssh/id_rsa
49 | - eval "$(ssh-agent -s)"
50 | - ssh-add ~/.ssh/id_rsa
51 | - ssh-keyscan -t rsa github.com > ~/.ssh/known_hosts
52 | - git config user.email "eoresearch@sinergise.com"
53 | - git config user.name "Gitlab CI"
54 | - git remote rm github || true
55 | - git remote add github git@github.com:sentinel-hub/hiector.git
56 | - git branch -D github-upload || true
57 | - git checkout -b github-upload
58 | - git fetch origin release-changes
59 | - git merge origin/release-changes
60 | - git fetch github main
61 | - git reset --soft github/main
62 | - git commit -m "version $CI_COMMIT_TAG"
63 | - git push github github-upload:main
64 | - git push github HEAD:refs/tags/$CI_COMMIT_TAG
65 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Sinergise ltd.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/configs/dakar-example/config_pleiades_and_spot_k7_eval_pleiades_dakar.json:
--------------------------------------------------------------------------------
1 | {
2 | "prepare_eopatch": {
3 | "data_dir": "",
4 | "tmp_dir": "",
5 | "out_dir": "",
6 | "grid_file": "",
7 | "logs_dir": "",
8 | "aws_profile": "",
9 | "bands_feature": "bands",
10 | "bands": [0, 1, 2, 3],
11 | "data_mask_feature": "mask",
12 | "no_data_value": 0,
13 | "reference_feature": "",
14 | "valid_reference_mask_feature": "",
15 | "cropped_grid_feature": "PATCHLETS",
16 | "bbox_type": "obb",
17 | "resolution": 0.5,
18 | "scale_sizes": [128, 256, 512],
19 | "valid_thr": 0.6,
20 | "overlap": 0.25,
21 | "workers": 8,
22 | "use_ray": false
23 | },
24 | "select-data": {
25 | "data_dir": "",
26 | "input_dataframe_filename": "",
27 | "output_dataframe_filename": "",
28 | "aws_profile": "",
29 | "query": "(N_BBOXES > 0) & (VALID_DATA_RATIO>0.6)",
30 | "frac": 1,
31 | "exlude_eops": null,
32 | "fraction_train": 0.8,
33 | "fraction_test": 0.1,
34 | "fraction_val": 0.1,
35 | "scale_sizes": [128, 256, 512],
36 | "seed": 42
37 | },
38 | "compute_norm_stats": {
39 | "aws_profile": "",
40 | "bucket_name": "",
41 | "samples_file": "",
42 | "data_dir": "",
43 | "scales": [256],
44 | "query": "(N_BBOXES>30) & (VALID_DATA_RATIO == 1)",
45 | "fraction": 1,
46 | "modality": "pleiades",
47 | "output_file": ""
48 | },
49 | "execute": {
50 | "datasources": {
51 | "train": [
52 | {
53 | "modality": "pleiades",
54 | "data_dir": "",
55 | "metadata_filename": "",
56 | "normalization": {
57 | "filename": "",
58 | "modality": "pleiades"
59 | },
60 | "query_train": "(SUBSET == 'train')",
61 | "query_cval": "(SUBSET == 'val')"
62 | },
63 | {
64 | "modality": "spot",
65 | "data_dir": "",
66 | "metadata_filename": "",
67 | "normalization": {
68 | "filename": "",
69 | "modality": "spot"
70 | },
71 | "query_train": "(SUBSET == 'train')",
72 | "query_cval": "(SUBSET == 'val')"
73 | }],
74 | "evaluate": {
75 | "modality": "pleiades",
76 | "data_dir": "",
77 | "eopatches_dir": "",
78 | "metadata_filename": "",
79 | "normalization": {
80 | "filename": "",
81 | "modality": "pleiades"
82 | },
83 | "query_test": null,
84 | "resolution": 0.5
85 | }
86 | },
87 | "gridding_config":
88 | {
89 | "bands_feature": "bands",
90 | "bands": [0, 1, 2, 3],
91 | "take_closest_time_frame": null,
92 | "data_mask_feature": "mask",
93 | "cloud_mask_feature": null,
94 | "no_data_value": 0,
95 | "reference_feature": "",
96 | "cropped_grid_feature": "PATCHLETS",
97 | "bbox_type": "obb",
98 | "resolution": 0.5,
99 | "scale_sizes": [128, 256, 512],
100 | "valid_thr": 0.6,
101 | "overlap": 0.25
102 | },
103 | "num_workers": 4,
104 | "grid_file": "",
105 | "model_dir": "",
106 | "aws_model_dir": "",
107 | "aws_dota_dir": "",
108 | "aws_gpkg_dir": "",
109 | "s3_bucket_name": "",
110 | "s3_profile_name": "",
111 | "image_size": 256,
112 | "batch_size": 8,
113 | "class_names": ["building"],
114 | "lr": 1e-3,
115 | "max_step": 40000,
116 | "save_interval": 250,
117 | "backbone": "resnet.resnet34",
118 | "prior_box": {
119 | "strides": [2, 4, 8, 16],
120 | "sizes": [7],
121 | "aspects": [1, 2, 4, 8, 16],
122 | "scales": [1, 1.2599, 1.5874, 2]
123 | },
124 | "head_stride_1": 1,
125 | "head_stride_2": 2,
126 | "conf_thresh": 0.01,
127 | "conf_thresh_2": 0.1,
128 | "nms_thresh": 0.2,
129 | "extra": 0
130 | },
131 | "compute-ap": {
132 | "s3_bucket_name": "",
133 | "s3_profile_name": "",
134 | "resolution": 0.5,
135 | "predictions_filename": "",
136 | "reference_filename": "",
137 | "ml_aois_filename": "",
138 | "eopatch_names": null,
139 | "eopatches_dir": "",
140 | "iou_thr": 0.2,
141 | "proba_thr": 0.2,
142 | "max_workers": 4,
143 | "aps_filename": ""
144 | }
145 | }
--------------------------------------------------------------------------------
/figs/hiector-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/figs/hiector-logo.png
--------------------------------------------------------------------------------
/figs/hiector.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/figs/hiector.png
--------------------------------------------------------------------------------
/figs/showcase-pleiades-001.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/figs/showcase-pleiades-001.png
--------------------------------------------------------------------------------
/figs/showcase-pleiades-002.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/figs/showcase-pleiades-002.png
--------------------------------------------------------------------------------
/figs/showcase-pleiades-003.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/figs/showcase-pleiades-003.png
--------------------------------------------------------------------------------
/figs/showcase-pleiades-004.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/figs/showcase-pleiades-004.png
--------------------------------------------------------------------------------
/figs/showcase-pleiades-005.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/figs/showcase-pleiades-005.png
--------------------------------------------------------------------------------
/figs/showcase-sentinel-2-001.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/figs/showcase-sentinel-2-001.png
--------------------------------------------------------------------------------
/figs/showcase-sentinel-2-002.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/figs/showcase-sentinel-2-002.png
--------------------------------------------------------------------------------
/figs/showcase-sentinel-2-003.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/figs/showcase-sentinel-2-003.png
--------------------------------------------------------------------------------
/figs/showcase-sentinel-2-004.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/figs/showcase-sentinel-2-004.png
--------------------------------------------------------------------------------
/figs/showcase-sentinel-2-005.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/figs/showcase-sentinel-2-005.png
--------------------------------------------------------------------------------
/figs/showcase-spot-001.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/figs/showcase-spot-001.png
--------------------------------------------------------------------------------
/figs/showcase-spot-002.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/figs/showcase-spot-002.png
--------------------------------------------------------------------------------
/figs/showcase-spot-003.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/figs/showcase-spot-003.png
--------------------------------------------------------------------------------
/figs/showcase-spot-004.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/figs/showcase-spot-004.png
--------------------------------------------------------------------------------
/figs/showcase-spot-005.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/figs/showcase-spot-005.png
--------------------------------------------------------------------------------
/hiector/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | The main package init
3 | """
4 |
5 | __version__ = "0.2.0"
6 |
--------------------------------------------------------------------------------
/hiector/ssrdd/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Capino512
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/hiector/ssrdd/README.md:
--------------------------------------------------------------------------------
1 | # Single-Stage Rotation-Decoupled Detector for Oriented Object
2 |
3 | This code is taken and adapted from [this GitHub repository](https://github.com/Capino512/pytorch-rotation-decoupled-detector), which implements the algorithm reported in the paper **Single-Stage Rotation-Decoupled Detector for Oriented Object**. [[Paper]](https://www.mdpi.com/2072-4292/12/19/3262/htm) [[PDF]](https://www.mdpi.com/2072-4292/12/19/3262/pdf)
4 |
5 | ## Compile
6 |
7 | ```bash
8 | # 'rbbox_batched_nms' will be used as post-processing in the interface stage
9 | # use gpu, for Linux only
10 | cd $PATH_ROOT/utils/box/ext/rbbox_overlap_gpu
11 | python setup.py build_ext --inplace
12 |
13 | # alternative, use cpu, for Windows and Linux
14 | cd $PATH_ROOT/utils/box/ext/rbbox_overlap_cpu
15 | python setup.py build_ext --inplace
16 | ```
17 |
18 | ## Citation
19 |
20 | ```
21 | @article{rdd,
22 | title={Single-Stage Rotation-Decoupled Detector for Oriented Object},
23 | author={Zhong, Bo and Ao, Kai},
24 | journal={Remote Sensing},
25 | year={2020}
26 | }
27 | ```
28 |
29 | ## TODO
30 |
31 | things to change/parameterize:
32 |
33 | * [ ] have a single script for training/evaluation, e.g. `execute.py` where a flag is specified for either training/testing
34 | * [ ] have a .json config file with model parameters
35 | * [ ] extract the `stride` parameter of the backbone architecture (i.e. head), to allow to change this as hyper-param (used in SPOT/S2)
36 | * [ ] test/remove/replace the nms functionality provided in `utils/box/ext`
37 | * [ ] test/remove parallel implementation of training
--------------------------------------------------------------------------------
/hiector/ssrdd/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/hiector/ssrdd/__init__.py
--------------------------------------------------------------------------------
/hiector/ssrdd/config.py:
--------------------------------------------------------------------------------
1 | """
2 | Module with configuration parameters
3 | """
4 |
5 | DIR_WEIGHT = "/home/ubuntu/pre-trained-weights"
6 |
--------------------------------------------------------------------------------
/hiector/ssrdd/data/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/hiector/ssrdd/data/__init__.py
--------------------------------------------------------------------------------
/hiector/ssrdd/data/aug/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/hiector/ssrdd/data/aug/__init__.py
--------------------------------------------------------------------------------
/hiector/ssrdd/data/aug/compose.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 |
4 | class Compose:
5 | def __init__(self, ops):
6 | self.ops = ops
7 |
8 | def __call__(self, *args):
9 | for op in self.ops:
10 | args = op(*args)
11 | return args
12 |
13 |
14 | class RandomSelect:
15 | def __init__(self, ops):
16 | self.ops = ops
17 |
18 | def __call__(self, *args):
19 | op = np.random.choice(self.ops)
20 | return op(*args)
21 |
--------------------------------------------------------------------------------
/hiector/ssrdd/data/aug/func.py:
--------------------------------------------------------------------------------
1 | import cv2 as cv
2 | import numpy as np
3 |
4 | from hiector.ssrdd.utils.misc import containerize
5 |
6 | __all__ = ["hflip", "vflip", "rgb2gray", "resize", "rotate90", "pad"]
7 |
8 |
9 | INTER_MODE = {"NEAREST": cv.INTER_NEAREST, "BILINEAR": cv.INTER_LINEAR, "BICUBIC": cv.INTER_CUBIC}
10 |
11 |
12 | def hflip(img):
13 | return np.ascontiguousarray(np.fliplr(img))
14 |
15 |
16 | def vflip(img):
17 | return np.ascontiguousarray(np.flipud(img))
18 |
19 |
20 | def rgb2gray(img):
21 | return cv.cvtColor(cv.cvtColor(img, cv.COLOR_RGB2GRAY), cv.COLOR_GRAY2RGB)
22 |
23 |
24 | def resize(img, size, interpolate="BILINEAR"):
25 | w, h = containerize(size, 2)
26 | ih, iw = img.shape[:2]
27 | if ih != h or iw != w:
28 | img = cv.resize(img, (w, h), interpolation=INTER_MODE[interpolate])
29 | return img
30 |
31 |
32 | def rotate90(img, k): # CLOCKWISE k=0, 1, 2, 3
33 | if k % 4 != 0:
34 | img = np.ascontiguousarray(np.rot90(img, -k))
35 | return img
36 |
37 |
38 | def pad(img, padding, mode="constant", **kwargs):
39 | if isinstance(padding, int):
40 | padding = [[padding, padding], [padding, padding]]
41 | else:
42 | padding = [containerize(p, 2) for p in padding]
43 | if img.ndim == 3 and len(padding) == 2:
44 | padding.append([0, 0])
45 | return np.pad(img, padding, mode, **kwargs)
46 |
--------------------------------------------------------------------------------
/hiector/ssrdd/data/aug/ops/__init__.py:
--------------------------------------------------------------------------------
1 | from .ops_det import *
2 | from .ops_img import *
3 |
--------------------------------------------------------------------------------
/hiector/ssrdd/data/aug/ops/ops_img.py:
--------------------------------------------------------------------------------
1 | import cv2 as cv
2 | import numpy as np
3 |
4 | from ..compose import Compose
5 | from ..func import *
6 |
7 | __all__ = [
8 | "ToFloat",
9 | "Normalize",
10 | "ConvertColor",
11 | "RandomGray",
12 | "RandomBrightness",
13 | "RandomContrast",
14 | "RandomLightingNoise",
15 | "RandomHue",
16 | "RandomSaturation",
17 | "PhotometricDistort",
18 | ]
19 |
20 |
21 | class ToFloat:
22 | def __call__(self, img, anno=None):
23 | img = img.astype(np.float32)
24 | return img, anno
25 |
26 |
27 | class Normalize:
28 | def __init__(self, mean, std):
29 | self.mean = mean
30 | self.std = std
31 |
32 | def __call__(self, img, anno=None):
33 | img = (img - self.mean) / self.std
34 | return img, anno
35 |
36 |
37 | class ConvertColor:
38 | def __init__(self, current="RGB", transform="HSV"):
39 | self.transform = transform
40 | self.current = current
41 |
42 | def __call__(self, img, anno=None):
43 | if self.current == "RGB" and self.transform == "HSV":
44 | img = cv.cvtColor(img, cv.COLOR_RGB2HSV)
45 | elif self.current == "HSV" and self.transform == "RGB":
46 | img = cv.cvtColor(img, cv.COLOR_HSV2RGB)
47 | else:
48 | raise NotImplementedError
49 | return img, anno
50 |
51 |
52 | class RandomGray: # RGB
53 | def __call__(self, img, anno=None):
54 | if np.random.randint(2):
55 | img = rgb2gray(img)
56 | return img, anno
57 |
58 |
59 | class RandomBrightness: # RGB
60 | def __init__(self, delta=32):
61 | assert 0 <= delta <= 255
62 | self.delta = delta
63 |
64 | def __call__(self, img, anno=None):
65 | if np.random.randint(2):
66 | delta = np.random.uniform(-self.delta, self.delta)
67 | img = np.clip(img + delta, 0, 255)
68 | return img, anno
69 |
70 |
71 | class RandomContrast: # RGB
72 | def __init__(self, lower=0.5, upper=1.5):
73 | assert 0 < lower < upper
74 | self.lower = lower
75 | self.upper = upper
76 |
77 | def __call__(self, img, anno=None):
78 | if np.random.randint(2):
79 | alpha = np.random.uniform(self.lower, self.upper)
80 | img = np.clip(alpha * img, 0, 255)
81 | return img, anno
82 |
83 |
84 | class RandomLightingNoise: # RGB
85 | def __call__(self, img, anno=None):
86 | if np.random.randint(2):
87 | indexes = [0, 1, 2]
88 | np.random.shuffle(indexes)
89 | img = img[..., indexes]
90 | return img, anno
91 |
92 |
93 | class RandomHue: # HSV
94 | def __init__(self, delta=18.0):
95 | assert 0 <= delta <= 360
96 | self.delta = delta
97 |
98 | def __call__(self, img, anno=None):
99 | if np.random.randint(2):
100 | delta = np.random.uniform(-self.delta, self.delta)
101 | img[:, :, 0] = (img[:, :, 0] + delta) % 360
102 | return img, anno
103 |
104 |
105 | class RandomSaturation: # HSV
106 | def __init__(self, lower=0.5, upper=1.5):
107 | assert 0 < lower < upper
108 | self.lower = lower
109 | self.upper = upper
110 |
111 | def __call__(self, img, anno=None):
112 | if np.random.randint(2):
113 | alpha = np.random.uniform(self.lower, self.upper)
114 | img[:, :, 1] = np.clip(alpha * img[:, :, 1], 0, 1)
115 | return img, anno
116 |
117 |
118 | class PhotometricDistort:
119 | def __init__(self, prob_light_noise=0.2, prob_gray=0.2):
120 | self.prob_light_noise = prob_light_noise
121 | self.prob_gray = prob_gray
122 | self.pd = [
123 | RandomContrast(),
124 | ConvertColor(current="RGB", transform="HSV"),
125 | RandomSaturation(),
126 | RandomHue(),
127 | ConvertColor(current="HSV", transform="RGB"),
128 | RandomContrast(),
129 | ]
130 | self.rand_brightness = RandomBrightness()
131 | self.rand_light_noise = RandomLightingNoise()
132 | self.rand_gray = RandomGray()
133 |
134 | def __call__(self, img, anno=None):
135 | img, anno = self.rand_brightness(img, anno)
136 | distort = Compose(self.pd[:-1] if np.random.randint(2) else self.pd[1:])
137 | img, anno = distort(img, anno)
138 | if np.random.randint(2):
139 | if np.random.rand() < self.prob_light_noise:
140 | img, anno = self.rand_light_noise(img, anno)
141 | else:
142 | if np.random.rand() < self.prob_gray:
143 | img, anno = self.rand_gray(img, anno)
144 | return img, anno
145 |
--------------------------------------------------------------------------------
/hiector/ssrdd/data/dataset/__init__.py:
--------------------------------------------------------------------------------
1 | from .dataset import ObjectDetectionDataset
2 |
--------------------------------------------------------------------------------
/hiector/ssrdd/model/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/hiector/ssrdd/model/__init__.py
--------------------------------------------------------------------------------
/hiector/ssrdd/model/backbone/__init__.py:
--------------------------------------------------------------------------------
1 | from .darknet import darknet21, darknet53
2 | from .resnet import *
3 |
--------------------------------------------------------------------------------
/hiector/ssrdd/model/backbone/darknet.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import torch
4 | from torch import nn
5 |
6 | from hiector.ssrdd.config import DIR_WEIGHT
7 | from hiector.ssrdd.utils.init import weight_init
8 | from hiector.ssrdd.xtorch import xnn
9 |
10 | # all pre-trained on image-net
11 |
12 | weights = {
13 | # from YOLO-v3
14 | "darknet21": os.path.join(DIR_WEIGHT, "darknet", "darknet21.pth"),
15 | "darknet53": os.path.join(DIR_WEIGHT, "darknet", "darknet53.pth"),
16 | }
17 |
18 |
19 | def CBR(plane, kernel_size, stride=1, padding=0):
20 | return nn.Sequential(
21 | xnn.Conv2d(plane, kernel_size, stride, padding, bias=False), xnn.BatchNorm2d(), nn.ReLU(inplace=True)
22 | )
23 |
24 |
25 | class BasicBlock(xnn.Module):
26 | def __init__(self, plane):
27 | super(BasicBlock, self).__init__()
28 | self.body = nn.Sequential(
29 | CBR(plane // 2, kernel_size=1, stride=1, padding=0), CBR(plane, kernel_size=3, stride=1, padding=1)
30 | )
31 |
32 | def forward(self, x):
33 | return x + self.body(x)
34 |
35 |
36 | class Backbone(xnn.Module):
37 | def __init__(self, layers, name=None, fetch_feature=False):
38 | super(Backbone, self).__init__()
39 | self.name = name
40 | self.fetch_feature = fetch_feature
41 | self.head = CBR(32, kernel_size=3, stride=1, padding=1)
42 | self.layers = nn.ModuleList([self._make_layer(64 * 2**i, blocks) for i, blocks in enumerate(layers)])
43 |
44 | @staticmethod
45 | def _make_layer(plane, blocks):
46 | layers = [CBR(plane, kernel_size=3, stride=2, padding=1)]
47 | for i in range(0, blocks):
48 | layers.append(BasicBlock(plane))
49 | return nn.Sequential(*layers)
50 |
51 | def init(self):
52 | if self.name in weights:
53 | print("load pre-training weights for", self.name)
54 | weight = torch.load(weights[self.name])
55 | ret = self.load_state_dict(weight, strict=False)
56 | print(ret)
57 | else:
58 | self.apply(weight_init["normal"])
59 |
60 | def forward(self, x):
61 | feature = self.head(x)
62 | features = []
63 | for layer in self.layers:
64 | feature = layer(feature)
65 | if self.fetch_feature:
66 | features.append(feature)
67 | return features if self.fetch_feature else feature
68 |
69 |
70 | def darknet21(fetch_feature=False):
71 | return Backbone([1, 1, 2, 2, 1], "darknet21", fetch_feature)
72 |
73 |
74 | def darknet53(fetch_feature=False):
75 | return Backbone([1, 2, 8, 8, 4], "darknet53", fetch_feature)
76 |
--------------------------------------------------------------------------------
/hiector/ssrdd/model/backbone/resnet/__init__.py:
--------------------------------------------------------------------------------
1 | from .resnet import (
2 | resnest50,
3 | resnest101,
4 | resnest200,
5 | resnest269,
6 | resnet18,
7 | resnet18_d,
8 | resnet34,
9 | resnet34_d,
10 | resnet50,
11 | resnet50_d,
12 | resnet101,
13 | resnet101_d,
14 | resnet152,
15 | resnet152_d,
16 | resnext50_32x4d,
17 | resnext101_32x8d,
18 | )
19 |
--------------------------------------------------------------------------------
/hiector/ssrdd/model/backbone/resnet/splat.py:
--------------------------------------------------------------------------------
1 | import torch
2 | from torch import nn
3 | from torch.nn import functional as F
4 |
5 | from hiector.ssrdd.xtorch import xnn
6 |
7 |
8 | class SplAtConv2d(xnn.Module):
9 | def __init__(
10 | self,
11 | out_channels,
12 | kernel_size,
13 | stride=1,
14 | padding=0,
15 | dilation=1,
16 | groups=1,
17 | bias=True,
18 | padding_mode="zeros",
19 | radix=2,
20 | reduction_factor=4,
21 | ):
22 | super(SplAtConv2d, self).__init__()
23 | inter_channels = max(out_channels * radix // reduction_factor, 32)
24 | self.radix = radix
25 | self.conv = xnn.Conv2d(
26 | out_channels * radix, kernel_size, stride, padding, dilation, groups * radix, bias, padding_mode
27 | )
28 | self.bn0 = xnn.BatchNorm2d()
29 | self.relu = nn.ReLU(inplace=True)
30 | self.fc1 = xnn.Conv2d(inter_channels, 1, groups=groups)
31 | self.bn1 = xnn.BatchNorm2d()
32 | self.fc2 = xnn.Conv2d(out_channels * radix, 1, groups=groups)
33 | self.rsoftmax = rSoftMax(radix, groups)
34 |
35 | def forward(self, x):
36 | x = self.conv(x)
37 | x = self.bn0(x)
38 | x = self.relu(x)
39 | split = torch.chunk(x, self.radix, 1)
40 | gap = sum(split)
41 | gap = F.adaptive_avg_pool2d(gap, (1, 1))
42 | gap = self.fc1(gap)
43 | gap = self.bn1(gap)
44 | gap = self.relu(gap)
45 | atten = self.fc2(gap)
46 | atten = self.rsoftmax(atten)
47 | atten = torch.chunk(atten, self.radix, 1)
48 | out = sum([att * split for (att, split) in zip(atten, split)])
49 | return out
50 |
51 |
52 | class rSoftMax(xnn.Module):
53 | def __init__(self, radix, cardinality):
54 | super().__init__()
55 | self.radix = radix
56 | self.cardinality = cardinality
57 |
58 | def forward(self, x):
59 | shape = x.shape
60 | if self.radix > 1:
61 | x = x.view(x.size(0), self.cardinality, self.radix, -1).transpose(1, 2)
62 | x = F.softmax(x, dim=1)
63 | x = x.reshape(shape)
64 | else:
65 | x = torch.sigmoid(x)
66 | return x
67 |
--------------------------------------------------------------------------------
/hiector/ssrdd/model/rdd/__init__.py:
--------------------------------------------------------------------------------
1 | from .rdd import RDD
2 |
--------------------------------------------------------------------------------
/hiector/ssrdd/model/rdd/rdd.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # File : rdd.py
3 | # Author : Kai Ao
4 | # Email : capino627@163.com
5 | # Date : 2020/12/12 10:58
6 | #
7 | # This file is part of Rotation-Decoupled Detector.
8 | # https://github.com/Capino512/pytorch-rotation-decoupled-detector
9 | # Distributed under MIT License.
10 |
11 | import torch
12 | from torch import nn
13 |
14 | from hiector.ssrdd.utils.init import weight_init
15 | from hiector.ssrdd.xtorch import xnn
16 |
17 | from .utils.detect import detect
18 | from .utils.loss import calc_loss
19 | from .utils.modules import DetPredict, FeaturePyramidNet
20 | from .utils.priorbox import LFUPriorBox
21 |
22 |
23 | class RDD(xnn.Module):
24 | def __init__(self, backbone, cfg):
25 | super(RDD, self).__init__()
26 |
27 | cfg.setdefault("iou_thresh", [0.4, 0.5])
28 | cfg.setdefault("variance", [0.1, 0.2, 0.1])
29 | cfg.setdefault("balance", 0.5)
30 |
31 | cfg.setdefault("conf_thresh", 0.01)
32 | cfg.setdefault("conf_thresh_2", 0.4)
33 | cfg.setdefault("nms_thresh", 0.5)
34 | cfg.setdefault("top_n", None)
35 |
36 | cfg.setdefault("extra", 0)
37 | cfg.setdefault("fpn_plane", 256)
38 | cfg.setdefault("extra_plane", 512)
39 |
40 | self.backbone = backbone
41 | self.prior_box = LFUPriorBox(cfg["prior_box"])
42 | self.num_levels = self.prior_box.num_levels
43 | self.num_classes = cfg["num_classes"]
44 | self.iou_thresh = cfg["iou_thresh"]
45 | self.variance = cfg["variance"]
46 | self.balance = cfg["balance"]
47 |
48 | self.conf_thresh = cfg["conf_thresh"]
49 | self.conf_thresh_2 = cfg["conf_thresh_2"]
50 | self.nms_thresh = cfg["nms_thresh"]
51 | self.top_n = cfg["top_n"]
52 |
53 | self.extra = cfg["extra"]
54 | self.fpn_plane = cfg["fpn_plane"]
55 | self.extra_plane = cfg["extra_plane"]
56 |
57 | self.fpn = FeaturePyramidNet(self.num_levels, self.fpn_plane)
58 | self.predict = DetPredict(self.num_levels, self.fpn_plane, self.prior_box.num_prior_boxes, self.num_classes, 5)
59 |
60 | if self.extra > 0:
61 | self.extra_layers = nn.ModuleList()
62 | for i in range(self.extra):
63 | self.extra_layers.append(
64 | nn.Sequential(
65 | xnn.Conv2d(self.extra_plane, 3, 2, 1, bias=False), xnn.BatchNorm2d(), nn.ReLU(inplace=True)
66 | )
67 | )
68 |
69 | def init(self):
70 | self.apply(weight_init["normal"])
71 | self.backbone.init()
72 |
73 | def restore(self, path):
74 | weight = torch.load(path) if torch.cuda.is_available() else torch.load(path, map_location=torch.device("cpu"))
75 | self.load_state_dict(weight, strict=True)
76 |
77 | def forward(self, images, targets=None):
78 | features = list(self.backbone(images))
79 | features = features[-(self.num_levels - self.extra) :]
80 | if self.extra > 0:
81 | for layer in self.extra_layers:
82 | features.append(layer(features[-1]))
83 | features = self.fpn(features)
84 |
85 | pred_cls, pred_loc = self.predict(features)
86 | anchors = self.prior_box.get_anchors(images.shape[2:]).to(images)
87 | if self.training:
88 | if targets is not None:
89 | return calc_loss(pred_cls, pred_loc, targets, anchors, self.iou_thresh, self.variance, self.balance)
90 | else:
91 | pred_cls, pred_loc = pred_cls.detach(), pred_loc.detach()
92 | top_n = (images.size(2) // 32) * (images.size(3) // 32) if self.top_n is None else self.top_n
93 | return detect(
94 | pred_cls, pred_loc, anchors, self.variance, self.conf_thresh, self.nms_thresh, top_n, self.conf_thresh_2
95 | )
96 |
--------------------------------------------------------------------------------
/hiector/ssrdd/model/rdd/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/hiector/ssrdd/model/rdd/utils/__init__.py
--------------------------------------------------------------------------------
/hiector/ssrdd/model/rdd/utils/detect.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # File : detect.py
3 | # Author : Kai Ao
4 | # Email : capino627@163.com
5 | # Date : 2020/12/12 10:58
6 | #
7 | # This file is part of Rotation-Decoupled Detector.
8 | # https://github.com/Capino512/pytorch-rotation-decoupled-detector
9 | # Distributed under MIT License.
10 |
11 | from collections import Counter
12 |
13 | import torch
14 |
15 | from hiector.ssrdd.utils.box.bbox import decode
16 | from hiector.ssrdd.utils.box.rbbox import rbbox_batched_nms as nms
17 |
18 |
19 | def detect(pred_cls, pred_loc, anchors, variance, conf_thresh, nms_thresh, top_n=None, conf_thresh_2=0.4):
20 | scores = torch.sigmoid(pred_cls)
21 | bboxes = decode(pred_loc, anchors[None], variance)
22 | indexes_img, indexes_anchor, indexes_cls = torch.where(scores > conf_thresh)
23 |
24 | bboxes = bboxes[indexes_img, indexes_anchor]
25 | scores = scores[indexes_img, indexes_anchor, indexes_cls]
26 | labels = indexes_cls
27 |
28 | start = 0
29 | dets = [None] * pred_cls.size(0)
30 | for image_id, n in sorted(Counter(indexes_img.tolist()).items()):
31 | bboxes_ = bboxes[start : start + n]
32 | scores_ = scores[start : start + n]
33 | labels_ = labels[start : start + n]
34 | keeps = nms(bboxes_, scores_, labels_, nms_thresh)
35 | mask = scores_[keeps] > conf_thresh_2
36 | dets[image_id] = [
37 | bboxes_[keeps[mask.cpu().numpy()]],
38 | scores_[keeps[mask.cpu().numpy()]],
39 | labels_[keeps[mask.cpu().numpy()]],
40 | ]
41 | start += n
42 |
43 | return dets
44 |
--------------------------------------------------------------------------------
/hiector/ssrdd/model/rdd/utils/loss.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # File : loss.py
3 | # Author : Kai Ao
4 | # Email : capino627@163.com
5 | # Date : 2020/12/12 10:59
6 | #
7 | # This file is part of Rotation-Decoupled Detector.
8 | # https://github.com/Capino512/pytorch-rotation-decoupled-detector
9 | # Distributed under MIT License.
10 |
11 | from collections import OrderedDict
12 |
13 | import torch
14 | from torch import nn
15 | from torch.nn.functional import one_hot
16 |
17 | from hiector.ssrdd.utils.box.bbox import bbox_iou, bbox_switch, encode
18 |
19 |
20 | def match(bboxes, anchors, iou_thresh, batch=16):
21 | # Reduce GPU memory usage
22 | ious = torch.cat([bbox_iou(bboxes[i : i + batch], anchors) for i in range(0, bboxes.size(0), batch)])
23 | max_ious, bbox_indexes = torch.max(ious, dim=0)
24 | mask_neg = max_ious < iou_thresh[0]
25 | mask_pos = max_ious > iou_thresh[1]
26 | return mask_pos, mask_neg, bbox_indexes
27 |
28 |
29 | def calc_loss_v1(pred_cls, pred_loc, targets, anchors, iou_thresh, variance, balance):
30 | device = pred_cls.device
31 | num_classes = pred_cls.size(-1)
32 | weight_pos, weight_neg = 2 * balance, 2 * (1 - balance)
33 | anchors_xyxy = bbox_switch(anchors, "xywh", "xyxy")
34 |
35 | criterion_cls = nn.BCEWithLogitsLoss(reduction="none")
36 | criterion_loc = nn.SmoothL1Loss(reduction="sum")
37 | loss_cls, loss_loc = torch.zeros([2], dtype=torch.float, device=device, requires_grad=True)
38 | num_pos = 0
39 | for i, target in enumerate(targets):
40 | if target:
41 | bboxes = target["bboxes"].to(device)
42 | labels = target["labels"].to(device)
43 | bboxes_xyxy = bbox_switch(bboxes[:, :4], "xywh", "xyxy")
44 | mask_pos, mask_neg, bbox_indexes = match(bboxes_xyxy, anchors_xyxy, iou_thresh)
45 |
46 | labels = labels[bbox_indexes]
47 | indexes_pos = bbox_indexes[mask_pos]
48 | bboxes_matched = bboxes[indexes_pos]
49 | anchors_matched = anchors[mask_pos]
50 | bboxes_pred = pred_loc[i][mask_pos]
51 | gt_bboxes, det_bboxes = encode(bboxes_matched, bboxes_pred, anchors_matched, variance)
52 |
53 | labels = one_hot(labels, num_classes=num_classes).float()
54 | labels[mask_neg] = 0
55 | loss_cls_ = criterion_cls(pred_cls[i], labels)
56 | loss_cls = loss_cls + loss_cls_[mask_pos].sum() * weight_pos + loss_cls_[mask_neg].sum() * weight_neg
57 | loss_loc = loss_loc + criterion_loc(gt_bboxes, det_bboxes)
58 | num_pos += mask_pos.sum().item()
59 | else:
60 | loss_cls = loss_cls + criterion_cls(pred_cls[i], torch.zeros_like(pred_cls[i])).sum()
61 | num_pos = max(num_pos, 1)
62 | return OrderedDict([("loss_cls", loss_cls / num_pos), ("loss_loc", loss_loc / num_pos)])
63 |
64 |
65 | def calc_loss_v2(pred_cls, pred_loc, targets, anchors, iou_thresh, variance, balance):
66 | # Calculate the loss centrally, has only a small acceleration effect
67 | device = pred_cls.device
68 | num_classes = pred_cls.size(-1)
69 | weight_pos, weight_neg = 2 * balance, 2 * (1 - balance)
70 | criterion_cls = nn.BCEWithLogitsLoss(reduction="none")
71 | criterion_loc = nn.SmoothL1Loss(reduction="sum")
72 |
73 | num_bboxes = [target["bboxes"].size(0) if target else 0 for target in targets]
74 | bboxes = [target["bboxes"] for target in targets if target]
75 | labels = [target["labels"] for target in targets if target]
76 | if len(bboxes) > 0:
77 | bboxes = torch.cat(bboxes).to(device)
78 | labels = torch.cat(labels).to(device)
79 | else:
80 | loss_cls = criterion_cls(pred_cls, torch.zeros_like(pred_cls)).sum()
81 | return OrderedDict([("loss_cls", loss_cls), ("loss_loc", torch.tensor(0.0, requires_grad=True))])
82 |
83 | # Reduce GPU memory usage
84 | batch = 16
85 | iou = torch.cat([bbox_iou(bboxes[i : i + batch, :4], anchors, "xywh") for i in range(0, bboxes.size(0), batch)])
86 | start = 0
87 | max_iou_merged, bbox_indexes_merged = [], []
88 | for i, num in enumerate(num_bboxes):
89 | if num == 0:
90 | max_iou = torch.zeros_like(pred_cls[i, :, 0])
91 | bbox_indexes = torch.zeros_like(pred_cls[i, :, 0], dtype=torch.long)
92 | else:
93 | max_iou, bbox_indexes = torch.max(iou[start : start + num], dim=0) # a
94 | max_iou_merged.append(max_iou)
95 | bbox_indexes_merged.append(bbox_indexes + start)
96 | start += num
97 | max_iou_merged = torch.stack(max_iou_merged)
98 | bbox_indexes_merged = torch.stack(bbox_indexes_merged)
99 | masks_pos = max_iou_merged > iou_thresh[1]
100 | masks_neg = max_iou_merged < iou_thresh[0]
101 | labels_matched = labels[bbox_indexes_merged]
102 | labels_matched = one_hot(labels_matched, num_classes=num_classes)
103 | labels_matched[masks_neg] = 0
104 | bboxes_matched = bboxes[bbox_indexes_merged[masks_pos]]
105 | anchors_matched = anchors[None].repeat(len(targets), 1, 1)[masks_pos]
106 | loss_cls = criterion_cls(pred_cls, labels_matched.float())
107 | loss_cls = loss_cls[masks_pos].sum() * weight_pos + loss_cls[masks_neg].sum() * weight_neg
108 | gt_bboxes, det_bboxes = encode(bboxes_matched, pred_loc[masks_pos], anchors_matched, variance)
109 | loss_loc = criterion_loc(det_bboxes, gt_bboxes)
110 | num_pos = max(masks_pos.sum().item(), 1)
111 | return OrderedDict([("loss_cls", loss_cls / num_pos), ("loss_loc", loss_loc / num_pos)])
112 |
113 |
114 | calc_loss = calc_loss_v1
115 |
--------------------------------------------------------------------------------
/hiector/ssrdd/model/rdd/utils/modules.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # File : modules.py
3 | # Author : Kai Ao
4 | # Email : capino627@163.com
5 | # Date : 2020/12/12 11:03
6 | #
7 | # This file is part of Rotation-Decoupled Detector.
8 | # https://github.com/Capino512/pytorch-rotation-decoupled-detector
9 | # Distributed under MIT License.
10 |
11 | import torch
12 | from torch import nn
13 |
14 | from hiector.ssrdd.xtorch import xnn
15 |
16 |
17 | class FeaturePyramidNet(xnn.Module):
18 | def __init__(self, depth, plane):
19 | super(FeaturePyramidNet, self).__init__()
20 | self.link = nn.ModuleList()
21 | self.fuse = nn.ModuleList()
22 | for i in range(depth):
23 | self.link.append(nn.Sequential(xnn.Conv2d(plane, 1, 1, 0, bias=False), xnn.BatchNorm2d()))
24 | if i != depth:
25 | self.fuse.append(
26 | nn.Sequential(nn.ReLU(inplace=True), xnn.Conv2d(plane, 3, 1, 1, bias=False), xnn.BatchNorm2d())
27 | )
28 |
29 | def forward(self, features):
30 | features = [self.link[i](feature) for i, feature in enumerate(features)]
31 | for i in range(len(features))[::-1]:
32 | if i != len(features) - 1:
33 | features[i] = self.fuse[i](features[i] + nn.Upsample(scale_factor=2)(features[i + 1]))
34 | features = [nn.ReLU(inplace=True)(feature) for feature in features]
35 | return features
36 |
37 |
38 | class PredictHead(xnn.Module):
39 | def __init__(self, plane, num_anchors, num_classes):
40 | super(PredictHead, self).__init__()
41 | self.num_classes = num_classes
42 | self.body = nn.Sequential(
43 | xnn.Conv2d(plane, 3, 1, 1, bias=False),
44 | xnn.BatchNorm2d(),
45 | nn.ReLU(inplace=True),
46 | xnn.Conv2d(num_anchors * num_classes, 3, 1, 1),
47 | )
48 |
49 | def forward(self, x):
50 | x = self.body(x)
51 | return x.permute(0, 2, 3, 1).reshape(x.size(0), -1, self.num_classes)
52 |
53 |
54 | class DetPredict(xnn.Module):
55 | def __init__(self, depth, plane, num_anchors, num_classes, num_loc_params):
56 | super(DetPredict, self).__init__()
57 | self.heads_cls = nn.ModuleList()
58 | self.heads_loc = nn.ModuleList()
59 | for i in range(depth):
60 | self.heads_cls.append(PredictHead(plane, num_anchors[i], num_classes))
61 | self.heads_loc.append(PredictHead(plane, num_anchors[i], num_loc_params))
62 |
63 | def forward(self, features):
64 | predict_cls, predict_loc = [], []
65 | for i, feature in enumerate(features):
66 | predict_cls.append(self.heads_cls[i](feature))
67 | predict_loc.append(self.heads_loc[i](feature))
68 | predict_cls = torch.cat(predict_cls, dim=1)
69 | predict_loc = torch.cat(predict_loc, dim=1)
70 | return predict_cls, predict_loc
71 |
--------------------------------------------------------------------------------
/hiector/ssrdd/model/rdd/utils/priorbox.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # File : priorbox.py
3 | # Author : Kai Ao
4 | # Email : capino627@163.com
5 | # Date : 2020/12/12 11:03
6 | #
7 | # This file is part of Rotation-Decoupled Detector.
8 | # https://github.com/Capino512/pytorch-rotation-decoupled-detector
9 | # Distributed under MIT License.
10 |
11 | from collections import OrderedDict
12 |
13 | import torch
14 |
15 | from hiector.ssrdd.utils.misc import LFUCache
16 |
17 |
18 | class PriorBox:
19 | def __init__(self, cfg):
20 | self.cfg = cfg
21 | self.prior_boxes = OrderedDict()
22 |
23 | for stride, size, aspects, scales in zip(cfg["strides"], cfg["sizes"], cfg["aspects"], cfg["scales"]):
24 | self.prior_boxes[stride] = self._get_prior_box(stride, size, aspects, scales, cfg.get("old_version", False))
25 |
26 | @staticmethod
27 | def _get_prior_box(stride, size, aspects, scales, old_version=False):
28 | boxes = []
29 | if old_version:
30 | # To be compatible with previous weights
31 | pair = [[aspect, scale] for scale in scales for aspect in aspects]
32 | else:
33 | pair = [[aspect, scale] for aspect in aspects for scale in scales]
34 | for aspect, scale in pair:
35 | length = stride * size * scale
36 | if aspect == 1:
37 | boxes.append([length, length])
38 | else:
39 | boxes.append([length * aspect**0.5, length / aspect**0.5])
40 | boxes.append([length / aspect**0.5, length * aspect**0.5])
41 | return boxes
42 |
43 | @staticmethod
44 | def _get_anchors(img_size, prior_boxes):
45 | h, w = img_size
46 | anchors = []
47 | for stride, prior_box in prior_boxes:
48 | assert w % stride == 0 and h % stride == 0
49 | fmw, fmh = w // stride, h // stride
50 | prior_box = torch.tensor(prior_box, dtype=torch.float)
51 | offset_y, offset_x = torch.meshgrid([torch.arange(fmh), torch.arange(fmw)])
52 | offset_x = offset_x.to(prior_box) + 0.5
53 | offset_y = offset_y.to(prior_box) + 0.5
54 | offset = torch.stack([offset_x, offset_y], dim=-1) * stride
55 | offset = offset[:, :, None, :].repeat(1, 1, prior_box.size(0), 1)
56 | prior_box = prior_box[None, None, :, :].repeat(fmh, fmw, 1, 1)
57 | anchors.append(torch.cat([offset, prior_box], dim=-1).reshape(-1, 4))
58 | anchors = torch.cat(anchors)
59 | return anchors
60 |
61 | def get_anchors(self, img_size):
62 | return self._get_anchors(img_size, self.prior_boxes.items())
63 |
64 |
65 | class LFUPriorBox:
66 | def __init__(self, prior_box_cfg, capacity=3):
67 | self.prior_box = PriorBox(prior_box_cfg)
68 | self.num_levels = len(self.prior_box.prior_boxes)
69 | self.num_prior_boxes = [len(prior_boxes) for prior_boxes in self.prior_box.prior_boxes.values()]
70 | self.lfu_cache = LFUCache(capacity)
71 |
72 | def get_anchors(self, img_size):
73 | name = "anchors-%d-%d" % tuple(img_size)
74 | anchors = self.lfu_cache.get(name, None)
75 | if anchors is None:
76 | anchors = self.prior_box.get_anchors(img_size)
77 | self.lfu_cache.put(name, anchors)
78 | return anchors
79 |
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/hiector/ssrdd/utils/__init__.py
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/adjust_lr.py:
--------------------------------------------------------------------------------
1 | def adjust_lr_multi_step(optimizer, step, cfg, warm_up=None):
2 | for param_group in optimizer.param_groups:
3 | if warm_up is not None and step <= warm_up[0]:
4 | param_group["lr"] = warm_up[1] + step / warm_up[0] * (warm_up[2] - warm_up[1])
5 | else:
6 | for s, lr in cfg:
7 | if s is None or step <= s:
8 | param_group["lr"] = lr
9 | break
10 |
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/box/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/hiector/ssrdd/utils/box/__init__.py
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/box/bbox.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # File : bbox.py
3 | # Author : Kai Ao
4 | # Email : capino627@163.com
5 | # Date : 2020/12/12 11:08
6 | #
7 | # This file is part of Rotation-Decoupled Detector.
8 | # https://github.com/Capino512/pytorch-rotation-decoupled-detector
9 | # Distributed under MIT License.
10 |
11 | import torch
12 | from torchvision.ops.boxes import batched_nms, box_iou, nms
13 |
14 |
15 | def bbox_switch(bbox, in_type, out_type): # 'xyxy', 'xywh'
16 | if in_type == "xyxy" and out_type == "xywh":
17 | bbox = torch.cat([(bbox[..., 0:2] + bbox[..., 2:4]) / 2, bbox[..., 2:4] - bbox[..., 0:2]], dim=-1)
18 | elif in_type == "xywh" and out_type == "xyxy":
19 | bbox = torch.cat([bbox[..., 0:2] - bbox[..., 2:4] / 2, bbox[..., 0:2] + bbox[..., 2:4] / 2], dim=-1)
20 | return bbox
21 |
22 |
23 | def bbox_iou(bbox1, bbox2, bbox_type="xyxy"): # nx4, mx4 -> nxm
24 | bbox1 = bbox_switch(bbox1, bbox_type, "xyxy")
25 | bbox2 = bbox_switch(bbox2, bbox_type, "xyxy")
26 | return box_iou(bbox1, bbox2)
27 |
28 |
29 | def bbox_nms(bboxes, scores, iou_thresh):
30 | return nms(bboxes, scores, iou_thresh)
31 |
32 |
33 | def bbox_batched_nms(bboxes, scores, labels, iou_thresh):
34 | return batched_nms(bboxes, scores, labels, iou_thresh)
35 |
36 |
37 | def encode(gt_bbox, det_bbox, anchor, variance):
38 | xy = (gt_bbox[..., 0:2] - anchor[..., 0:2]) / anchor[..., 2:4] / variance[0]
39 | wh = torch.log(gt_bbox[..., 2:4] / anchor[..., 2:4]) / variance[1]
40 | a = gt_bbox[..., [4]] / 45 / variance[2]
41 | gt_bbox = torch.cat([xy, wh, a], dim=-1)
42 | det_bbox = torch.cat([det_bbox[..., :4], torch.tanh(det_bbox[..., [4]]) / variance[2]], dim=-1)
43 | return gt_bbox, det_bbox
44 |
45 |
46 | def decode(det_bbox, anchor, variance):
47 | xy = det_bbox[..., 0:2] * variance[0] * anchor[..., 2:4] + anchor[..., 0:2]
48 | wh = torch.exp(det_bbox[..., 2:4] * variance[1]) * anchor[..., 2:4]
49 | a = torch.tanh(det_bbox[..., [4]]) * 45
50 | return torch.cat([xy, wh, a], dim=-1)
51 |
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/box/bbox_np.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # File : bbox_np.py
3 | # Author : Kai Ao
4 | # Email : capino627@163.com
5 | # Date : 2020/12/12 11:08
6 | #
7 | # This file is part of Rotation-Decoupled Detector.
8 | # https://github.com/Capino512/pytorch-rotation-decoupled-detector
9 | # Distributed under MIT License.
10 |
11 | import numpy as np
12 |
13 |
14 | def bbox_switch(bbox, in_type, out_type): # 'xyxy', 'xywh'
15 | if in_type == "xyxy" and out_type == "xywh":
16 | bbox = np.concatenate([(bbox[..., 0:2] + bbox[..., 2:4]) / 2, bbox[..., 2:4] - bbox[..., 0:2]], axis=-1)
17 | elif in_type == "xywh" and out_type == "xyxy":
18 | bbox = np.concatenate([bbox[..., 0:2] - bbox[..., 2:4] / 2, bbox[..., 0:2] + bbox[..., 2:4] / 2], axis=-1)
19 | return bbox
20 |
21 |
22 | def xywha2xy4(xywha): # a represents the angle(degree), clockwise, a=0 along the X axis
23 | x, y, w, h, a = xywha
24 | corner = np.array([[-w / 2, -h / 2], [w / 2, -h / 2], [w / 2, h / 2], [-w / 2, h / 2]])
25 | a = np.deg2rad(a)
26 | transform = np.array([[np.cos(a), -np.sin(a)], [np.sin(a), np.cos(a)]])
27 | return transform.dot(corner.T).T + [x, y]
28 |
29 |
30 | def xy42xywha(xy4, flag=0): # bbox(4x2) represents a rectangle
31 | # flag=0, 0 <= a < 180
32 | # flag=1, 0 <= a < 180, w >= h
33 | # flag=2, -45 <= a < 45
34 | x, y = np.mean(xy4, axis=0)
35 | diff01 = xy4[0] - xy4[1]
36 | diff03 = xy4[0] - xy4[3]
37 | w = np.sqrt(np.square(diff01).sum())
38 | h = np.sqrt(np.square(diff03).sum())
39 | if w >= h:
40 | a = np.rad2deg(np.arctan2(diff01[1], diff01[0]))
41 | else:
42 | a = np.rad2deg(np.arctan2(diff03[1], diff03[0])) + 90
43 | if flag > 0:
44 | if w < h:
45 | w, h = h, w
46 | a += 90
47 | a = (a % 180 + 180) % 180
48 | if flag > 1:
49 | if 45 <= a < 135:
50 | w, h = h, w
51 | a -= 90
52 | elif a >= 135:
53 | a -= 180
54 | return np.stack([x, y, w, h, a])
55 |
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/box/ext/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/hiector/ssrdd/utils/box/ext/__init__.py
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/box/ext/rbbox_overlap_cpu/__init__.py:
--------------------------------------------------------------------------------
1 | try:
2 | from .rbbox_overlap import rbbox_iou, rbbox_iou_1x1, rbbox_iou_nxn, rbbox_nms
3 | except ImportError:
4 | # A hack to make rbbox_overlap work,
5 | # see https://git.sinergise.com/clients/esa/query-planet-ccn3/-/issues/13#note_220460
6 | import os
7 | import sys
8 |
9 | sys.path.insert(1, os.path.dirname(__file__))
10 | from rbbox_overlap_cpu.rbbox_overlap import rbbox_iou, rbbox_iou_1x1, rbbox_iou_nxn, rbbox_nms
11 |
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/box/ext/rbbox_overlap_cpu/rbbox_overlap.h:
--------------------------------------------------------------------------------
1 |
2 |
3 | #include
4 |
5 |
6 | # define PI 3.14159265358979323846
7 |
8 |
9 | double trangle_area(double * a, double * b, double * c) {
10 | return ((a[0] - c[0]) * (b[1] - c[1]) - (a[1] - c[1]) * (b[0] - c[0]))/2.0;
11 | }
12 |
13 |
14 | double area(double * int_pts, int num_of_inter) {
15 |
16 | double area = 0;
17 | for(int i = 0;i < num_of_inter - 2;i++) {
18 | area += fabs(trangle_area(int_pts, int_pts + 2 * i + 2, int_pts + 2 * i + 4));
19 | }
20 | return area;
21 | }
22 |
23 |
24 | void reorder_pts(double * int_pts, int num_of_inter) {
25 |
26 | if(num_of_inter > 0) {
27 |
28 | double center[2];
29 |
30 | center[0] = 0.0;
31 | center[1] = 0.0;
32 |
33 | for(int i = 0;i < num_of_inter;i++) {
34 | center[0] += int_pts[2 * i];
35 | center[1] += int_pts[2 * i + 1];
36 | }
37 | center[0] /= num_of_inter;
38 | center[1] /= num_of_inter;
39 |
40 | double vs[16];
41 | double v[2];
42 | double d;
43 | for(int i = 0;i < num_of_inter;i++) {
44 | v[0] = int_pts[2 * i]-center[0];
45 | v[1] = int_pts[2 * i + 1]-center[1];
46 | d = sqrt(v[0] * v[0] + v[1] * v[1]);
47 | v[0] = v[0] / d;
48 | v[1] = v[1] / d;
49 | if(v[1] < 0) {
50 | v[0]= - 2 - v[0];
51 | }
52 | vs[i] = v[0];
53 | }
54 |
55 | double temp,tx,ty;
56 | int j;
57 | for(int i=1;ivs[i]){
59 | temp = vs[i];
60 | tx = int_pts[2*i];
61 | ty = int_pts[2*i+1];
62 | j=i;
63 | while(j>0&&vs[j-1]>temp){
64 | vs[j] = vs[j-1];
65 | int_pts[j*2] = int_pts[j*2-2];
66 | int_pts[j*2+1] = int_pts[j*2-1];
67 | j--;
68 | }
69 | vs[j] = temp;
70 | int_pts[j*2] = tx;
71 | int_pts[j*2+1] = ty;
72 | }
73 | }
74 | }
75 | }
76 |
77 | bool inter2line(double * pts1, double *pts2, int i, int j, double * temp_pts) {
78 |
79 | double a[2];
80 | double b[2];
81 | double c[2];
82 | double d[2];
83 |
84 | double area_abc, area_abd, area_cda, area_cdb;
85 |
86 | a[0] = pts1[2 * i];
87 | a[1] = pts1[2 * i + 1];
88 |
89 | b[0] = pts1[2 * ((i + 1) % 4)];
90 | b[1] = pts1[2 * ((i + 1) % 4) + 1];
91 |
92 | c[0] = pts2[2 * j];
93 | c[1] = pts2[2 * j + 1];
94 |
95 | d[0] = pts2[2 * ((j + 1) % 4)];
96 | d[1] = pts2[2 * ((j + 1) % 4) + 1];
97 |
98 | area_abc = trangle_area(a, b, c);
99 | area_abd = trangle_area(a, b, d);
100 |
101 | if(area_abc * area_abd >= 0) {
102 | return false;
103 | }
104 |
105 | area_cda = trangle_area(c, d, a);
106 | area_cdb = area_cda + area_abc - area_abd;
107 |
108 | if (area_cda * area_cdb >= 0) {
109 | return false;
110 | }
111 | double t = area_cda / (area_abd - area_abc);
112 |
113 | double dx = t * (b[0] - a[0]);
114 | double dy = t * (b[1] - a[1]);
115 | temp_pts[0] = a[0] + dx;
116 | temp_pts[1] = a[1] + dy;
117 |
118 | return true;
119 | }
120 |
121 | bool in_rect(double pt_x, double pt_y, double * pts) {
122 |
123 | double ab[2];
124 | double ad[2];
125 | double ap[2];
126 |
127 | double abab;
128 | double abap;
129 | double adad;
130 | double adap;
131 |
132 | ab[0] = pts[2] - pts[0];
133 | ab[1] = pts[3] - pts[1];
134 |
135 | ad[0] = pts[6] - pts[0];
136 | ad[1] = pts[7] - pts[1];
137 |
138 | ap[0] = pt_x - pts[0];
139 | ap[1] = pt_y - pts[1];
140 |
141 | abab = ab[0] * ab[0] + ab[1] * ab[1];
142 | abap = ab[0] * ap[0] + ab[1] * ap[1];
143 | adad = ad[0] * ad[0] + ad[1] * ad[1];
144 | adap = ad[0] * ap[0] + ad[1] * ap[1];
145 |
146 | return abab >= abap && abap >= 0 && adad >= adap && adap >= 0;
147 | }
148 |
149 | int inter_pts(double * pts1, double * pts2, double * int_pts) {
150 |
151 | int num_of_inter = 0;
152 |
153 | for(int i = 0;i < 4;i++) {
154 | if(in_rect(pts1[2 * i], pts1[2 * i + 1], pts2)) {
155 | int_pts[num_of_inter * 2] = pts1[2 * i];
156 | int_pts[num_of_inter * 2 + 1] = pts1[2 * i + 1];
157 | num_of_inter++;
158 | }
159 | if(in_rect(pts2[2 * i], pts2[2 * i + 1], pts1)) {
160 | int_pts[num_of_inter * 2] = pts2[2 * i];
161 | int_pts[num_of_inter * 2 + 1] = pts2[2 * i + 1];
162 | num_of_inter++;
163 | }
164 | }
165 |
166 | double temp_pts[2];
167 |
168 | for(int i = 0;i < 4;i++) {
169 | for(int j = 0;j < 4;j++) {
170 | bool has_pts = inter2line(pts1, pts2, i, j, temp_pts);
171 | if(has_pts) {
172 | int_pts[num_of_inter * 2] = temp_pts[0];
173 | int_pts[num_of_inter * 2 + 1] = temp_pts[1];
174 | num_of_inter++;
175 | }
176 | }
177 | }
178 |
179 |
180 | return num_of_inter;
181 | }
182 |
183 |
184 | void convert_region(double * pts , double * region) {
185 |
186 | double angle = region[4];
187 | double a_cos = cos(angle/180.0*PI);
188 | double a_sin = sin(angle/180.0*PI);
189 |
190 | double ctr_x = region[0];
191 | double ctr_y = region[1];
192 |
193 | double w = region[2];
194 | double h = region[3];
195 |
196 | double pts_x[4];
197 | double pts_y[4];
198 |
199 | pts_x[0] = - w / 2;
200 | pts_x[1] = w / 2;
201 | pts_x[2] = w / 2;
202 | pts_x[3] = - w / 2;
203 |
204 | pts_y[0] = - h / 2;
205 | pts_y[1] = - h / 2;
206 | pts_y[2] = h / 2;
207 | pts_y[3] = h / 2;
208 |
209 | for(int i = 0;i < 4;i++) {
210 | pts[7 - 2 * i - 1] = a_cos * pts_x[i] - a_sin * pts_y[i] + ctr_x;
211 | pts[7 - 2 * i] = a_sin * pts_x[i] + a_cos * pts_y[i] + ctr_y;
212 |
213 | }
214 |
215 | }
216 |
217 |
218 | double inter(double * region1, double * region2) {
219 |
220 | double pts1[8];
221 | double pts2[8];
222 | double int_pts[16];
223 | int num_of_inter;
224 |
225 | convert_region(pts1, region1);
226 | convert_region(pts2, region2);
227 |
228 | num_of_inter = inter_pts(pts1, pts2, int_pts);
229 |
230 | reorder_pts(int_pts, num_of_inter);
231 |
232 | return area(int_pts, num_of_inter);
233 |
234 |
235 | }
236 |
237 | double RotateIoU(double * region1, double * region2) {
238 |
239 | double area1 = region1[2] * region1[3];
240 | double area2 = region2[2] * region2[3];
241 | double area_inter = inter(region1, region2);
242 |
243 | return area_inter / (area1 + area2 - area_inter);
244 |
245 | }
246 |
247 |
248 | void RotateIoU_1x1(double * region1, double * region2, int n, double * ret){
249 | for ( int i = 0; i < n; i++ ){
250 | ret[i] = RotateIoU(region1 + i * 5, region2 + i * 5);
251 | }
252 | }
253 |
254 |
255 | void RotateIoU_nxn(double * region1, double * region2, int n1, int n2, double * ret){
256 | for ( int i = 0; i < n1; i++ ){
257 | for ( int j = 0; j < n2; j++ ){
258 | ret[i * n2 + j] = RotateIoU(region1 + i * 5, region2 + j * 5);
259 | }
260 | }
261 | }
262 |
263 | void RotateNMS(double * bboxes, int n, double thresh, int * keeps){
264 | int i, flag;
265 | n--;
266 | while(n > 0){
267 | flag = 0;
268 | for ( i = 0; i < n; i++ ){
269 | if (keeps[i]){
270 | if (RotateIoU(bboxes + n * 5, bboxes + i * 5) > thresh){
271 | keeps[i] = 0;
272 | }
273 | else{
274 | flag = i;
275 | }
276 | }
277 | }
278 | n = flag;
279 | }
280 | }
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/box/ext/rbbox_overlap_cpu/rbbox_overlap.pyx:
--------------------------------------------------------------------------------
1 |
2 |
3 | # distutils: language = c++
4 |
5 |
6 | import numpy as np
7 |
8 | cimport numpy as np
9 |
10 | assert sizeof(int) == sizeof(np.int32_t)
11 |
12 |
13 | cdef extern from 'rbbox_overlap.h':
14 | cdef float RotateIoU(np.float64_t * region1, np.float64_t * region2)
15 | cdef void RotateIoU_1x1(np.float64_t * region1, np.float64_t * region2, int n, np.float64_t * ret)
16 | cdef void RotateIoU_nxn(np.float64_t * region1, np.float64_t * region2, int n1, int n2, np.float64_t * ret)
17 | cdef void RotateNMS(np.float64_t * bboxes, int n, float thresh, np.int32_t * keeps)
18 |
19 |
20 | def rbbox_iou(np.ndarray[np.float64_t, ndim=1] a, np.ndarray[np.float64_t, ndim=1] b):
21 | return RotateIoU(&a[0], &b[0])
22 |
23 |
24 | def rbbox_iou_1x1(np.ndarray[np.float64_t, ndim=2] a, np.ndarray[np.float64_t, ndim=2] b):
25 | cdef int n1 = a.shape[0]
26 | cdef int n2 = b.shape[0]
27 | assert n1 == n2
28 | cdef np.ndarray[np.float64_t, ndim=1] ret = np.zeros([n1], dtype=np.float64)
29 | RotateIoU_1x1(&a[0, 0], &b[0, 0], n1, &ret[0])
30 | return ret
31 |
32 |
33 | def rbbox_iou_nxn(np.ndarray[np.float64_t, ndim=2] a, np.ndarray[np.float64_t, ndim=2] b):
34 | cdef int n1 = a.shape[0]
35 | cdef int n2 = b.shape[0]
36 | cdef np.ndarray[np.float64_t, ndim=2] ret = np.zeros([n1, n2], dtype=np.float64)
37 | RotateIoU_nxn(&a[0, 0], &b[0, 0], n1, n2, &ret[0, 0])
38 | return ret
39 |
40 |
41 | def rbbox_nms(np.ndarray[np.float64_t, ndim=2] boxes, np.ndarray[np.float64_t, ndim=1] scores, float thresh):
42 | cdef int n = boxes.shape[0]
43 | cdef np.ndarray[np.int32_t, ndim=1] keeps = np.ones([n], dtype=np.int32)
44 | cdef np.ndarray[np.int32_t, ndim=1] indexes = np.argsort(scores).astype(np.int32)
45 | boxes = boxes[indexes]
46 | RotateNMS(&boxes[0, 0], n, thresh, &keeps[0])
47 | keeps = indexes[keeps.astype(np.bool)]
48 | if len(keeps) > 1:
49 | keeps = np.ascontiguousarray(keeps[::-1])
50 | return keeps
51 |
52 |
53 | # python setup.py build_ext --inplace
54 |
55 | # iou.cpp(2961): error C2664: 'void RotateNMS(float *,int,float,int *)': cannot convert argument 4 from '__pyx_t_5numpy_int32_t *' to 'int *'
56 | #
57 | # go to line(2961) in the generated file in iou.cpp
58 | # Modify corresponding __pyx_t_5numpy_int32_t to int
59 |
60 |
61 |
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/box/ext/rbbox_overlap_cpu/setup.py:
--------------------------------------------------------------------------------
1 | from distutils.core import setup
2 |
3 | import numpy as np
4 | from Cython.Build import cythonize
5 |
6 | try:
7 | numpy_include = np.get_include()
8 | except AttributeError:
9 | numpy_include = np.get_numpy_include()
10 |
11 |
12 | setup(
13 | ext_modules=cythonize("rbbox_overlap.pyx"),
14 | include_dirs=[numpy_include],
15 | )
16 |
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/box/ext/rbbox_overlap_gpu/__init__.py:
--------------------------------------------------------------------------------
1 | try:
2 | from .rbbox_overlap import rbbox_overlaps as rbbox_iou
3 | from .rbbox_overlap import rotate_gpu_nms as rbbox_nms
4 | except ImportError:
5 | import os
6 | import sys
7 |
8 | sys.path.insert(1, os.path.dirname(__file__))
9 |
10 | from rbbox_overlap_gpu.rbbox_overlap import rbbox_overlaps as rbbox_iou
11 | from rbbox_overlap_gpu.rbbox_overlap import rotate_gpu_nms as rbbox_nms
12 |
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/box/ext/rbbox_overlap_gpu/rbbox_overlap.hpp:
--------------------------------------------------------------------------------
1 |
2 |
3 | void _overlaps(float* overlaps,const float* boxes,const float* query_boxes, int n, int k, int device_id);
4 |
5 |
6 | void _rotate_nms(int* keep_out, int* num_out, const float* boxes_host, int boxes_num,
7 | int boxes_dim, float nms_overlap_thresh, int device_id);
8 |
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/box/ext/rbbox_overlap_gpu/rbbox_overlap.pyx:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | cimport numpy as np
3 |
4 | assert sizeof(int) == sizeof(np.int32_t)
5 |
6 | cdef extern from "rbbox_overlap.hpp":
7 | void _rotate_nms(np.int32_t*, int*, np.float32_t*, int, int, float, int)
8 | void _overlaps(np.float32_t*, np.float32_t*, np.float32_t*, int, int, int)
9 |
10 |
11 | def rbbox_overlaps (np.ndarray[np.float32_t, ndim=2] boxes, np.ndarray[np.float32_t, ndim=2] query_boxes, np.int32_t device_id=0):
12 | # boxes: [x, y, w, h, theta]
13 | cdef int N = boxes.shape[0]
14 | cdef int K = query_boxes.shape[0]
15 | cdef np.ndarray[np.float32_t, ndim=2] overlaps = np.zeros((N, K), dtype = np.float32)
16 | _overlaps(&overlaps[0, 0], &boxes[0, 0], &query_boxes[0, 0], N, K, device_id)
17 | return overlaps
18 |
19 |
20 | def rotate_gpu_nms(np.ndarray[np.float32_t, ndim=2] dets, np.float_t thresh, np.int32_t device_id=0):
21 | cdef int boxes_num = dets.shape[0]
22 | cdef int boxes_dim = dets.shape[1]
23 | cdef int num_out
24 | cdef np.ndarray[np.int32_t, ndim=1] \
25 | keep = np.zeros(boxes_num, dtype=np.int32)
26 | cdef np.ndarray[np.float32_t, ndim=1] \
27 | scores = dets[:, 5]
28 | cdef np.ndarray[np.int_t, ndim=1] \
29 | order = scores.argsort()[::-1]
30 | cdef np.ndarray[np.float32_t, ndim=2] \
31 | sorted_dets = dets[order, :]
32 | thresh = thresh
33 | _rotate_nms(&keep[0], &num_out, &sorted_dets[0, 0], boxes_num, boxes_dim, thresh, device_id)
34 | keep = keep[:num_out]
35 | return order[keep]
36 |
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/box/ext/rbbox_overlap_gpu/setup.py:
--------------------------------------------------------------------------------
1 | # --------------------------------------------------------
2 | # Fast R-CNN
3 | # Copyright (c) 2015 Microsoft
4 | # Licensed under The MIT License [see LICENSE for details]
5 | # Written by Ross Girshick
6 | # --------------------------------------------------------
7 |
8 | import os
9 | from distutils.extension import Extension
10 | from os.path import join as pjoin
11 |
12 | import numpy as np
13 | from Cython.Distutils import build_ext
14 | from setuptools import setup
15 |
16 |
17 | def find_in_path(name, path):
18 | """Find a file in a search path"""
19 | # Adapted fom
20 | # http://code.activestate.com/recipes/52224-find-a-file-given-a-search-path/
21 | for dir in path.split(os.pathsep):
22 | binpath = pjoin(dir, name)
23 | if os.path.exists(binpath):
24 | return os.path.abspath(binpath)
25 | return None
26 |
27 |
28 | def hacky_hack(cuda_version: str) -> str:
29 | """This utility function replaces the cuda version from .1 to .0 in order to allow compilation of the code
30 |
31 | Args:
32 | cuda_version ([str]): string specifying cuda version
33 |
34 | Returns:
35 | str: modified cuda version
36 | """
37 | return cuda_version.replace(".1", ".0") if ".1" in cuda_version else cuda_version
38 |
39 |
40 | def locate_cuda():
41 | """Locate the CUDA environment on the system
42 |
43 | Returns a dict with keys 'home', 'nvcc', 'include', and 'lib64'
44 | and values giving the absolute path to each directory.
45 |
46 | Starts by looking for the CUDAHOME env variable. If not found, everything
47 | is based on finding 'nvcc' in the PATH.
48 | """
49 |
50 | # first check if the CUDAHOME env variable is in use
51 | if "CUDAHOME" in os.environ:
52 | home = os.path.realpath(os.environ["CUDAHOME"])
53 | home = hacky_hack(home)
54 | nvcc = pjoin(home, "bin", "nvcc")
55 | else:
56 | # otherwise, search the PATH for NVCC
57 | default_path = pjoin(os.sep, "usr", "local", "cuda", "bin")
58 | nvcc = os.path.realpath(find_in_path("nvcc", os.environ["PATH"] + os.pathsep + default_path))
59 | if nvcc is None:
60 | raise EnvironmentError(
61 | "The nvcc binary could not be located in your $PATH. Either add it to your path, or set $CUDAHOME"
62 | )
63 | nvcc = hacky_hack(nvcc)
64 | home = os.path.dirname(os.path.dirname(nvcc))
65 |
66 | cudaconfig = {"home": home, "nvcc": nvcc, "include": pjoin(home, "include"), "lib64": pjoin(home, "lib64")}
67 | for k, v in cudaconfig.items():
68 | if not os.path.exists(v):
69 | raise EnvironmentError("The CUDA %s path could not be located in %s" % (k, v))
70 |
71 | return cudaconfig
72 |
73 |
74 | CUDA = locate_cuda()
75 |
76 |
77 | # Obtain the numpy include directory. This logic works across numpy versions.
78 | try:
79 | numpy_include = np.get_include()
80 | except AttributeError:
81 | numpy_include = np.get_numpy_include()
82 |
83 |
84 | def customize_compiler_for_nvcc(self):
85 | """inject deep into distutils to customize how the dispatch
86 | to gcc/nvcc works.
87 |
88 | If you subclass UnixCCompiler, it's not trivial to get your subclass
89 | injected in, and still have the right customizations (i.e.
90 | distutils.sysconfig.customize_compiler) run on it. So instead of going
91 | the OO route, I have this. Note, it's kindof like a wierd functional
92 | subclassing going on."""
93 |
94 | # tell the compiler it can processes .cu
95 | self.src_extensions.append(".cu")
96 |
97 | # save references to the default compiler_so and _comple methods
98 | default_compiler_so = self.compiler_so
99 | super = self._compile
100 |
101 | # now redefine the _compile method. This gets executed for each
102 | # object but distutils doesn't have the ability to change compilers
103 | # based on source extension: we add it.
104 | def _compile(obj, src, ext, cc_args, extra_postargs, pp_opts):
105 | if os.path.splitext(src)[1] == ".cu":
106 | # use the cuda for .cu files
107 | self.set_executable("compiler_so", CUDA["nvcc"])
108 | # use only a subset of the extra_postargs, which are 1-1 translated
109 | # from the extra_compile_args in the Extension class
110 | postargs = extra_postargs["nvcc"]
111 | else:
112 | postargs = extra_postargs["gcc"]
113 |
114 | super(obj, src, ext, cc_args, postargs, pp_opts)
115 | # reset the default compiler_so, which we might have changed for cuda
116 | self.compiler_so = default_compiler_so
117 |
118 | # inject our redefined _compile method into the class
119 | self._compile = _compile
120 |
121 |
122 | # run the customize_compiler
123 | class custom_build_ext(build_ext):
124 | def build_extensions(self):
125 | customize_compiler_for_nvcc(self.compiler)
126 | build_ext.build_extensions(self)
127 |
128 |
129 | ext_modules = [
130 | Extension(
131 | "rbbox_overlap",
132 | ["rbbox_overlap_kernel.cu", "rbbox_overlap.pyx"],
133 | library_dirs=[CUDA["lib64"]],
134 | libraries=["cudart"],
135 | language="c++",
136 | runtime_library_dirs=[CUDA["lib64"]],
137 | # this syntax is specific to this build system
138 | # we're only going to use certain compiler args with nvcc and not with
139 | # gcc the implementation of this trick is in customize_compiler() below
140 | extra_compile_args={
141 | "gcc": ["-Wno-unused-function"],
142 | "nvcc": ["-arch=sm_35", "--ptxas-options=-v", "-c", "--compiler-options", "'-fPIC'"],
143 | },
144 | include_dirs=[numpy_include, CUDA["include"]],
145 | ),
146 | ]
147 |
148 | setup(
149 | name="fast_rcnn",
150 | ext_modules=ext_modules,
151 | # inject our custom trigger
152 | cmdclass={"build_ext": custom_build_ext},
153 | )
154 |
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/box/metric.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # File : metric.py
3 | # Author : Kai Ao
4 | # Email : capino627@163.com
5 | # Date : 2020/12/12 11:08
6 | #
7 | # This file is part of Rotation-Decoupled Detector.
8 | # https://github.com/Capino512/pytorch-rotation-decoupled-detector
9 | # Distributed under MIT License.
10 |
11 | from collections import Counter, defaultdict
12 |
13 | import numpy as np
14 |
15 | from .rbbox_np import rbbox_iou
16 |
17 |
18 | def get_ap(recall, precision):
19 | recall = [0] + list(recall) + [1]
20 | precision = [0] + list(precision) + [0]
21 | for i in range(len(precision) - 1, 0, -1):
22 | precision[i - 1] = max(precision[i - 1], precision[i])
23 | ap = sum((recall[i] - recall[i - 1]) * precision[i] for i in range(1, len(recall)) if recall[i] != recall[i - 1])
24 | return ap * 100
25 |
26 |
27 | def get_ap_07(recall, precision):
28 | ap = 0.0
29 | for t in np.linspace(0, 1, 11, endpoint=True):
30 | mask = recall >= t
31 | if np.any(mask):
32 | ap += np.max(precision[mask]) / 11
33 | return ap * 100
34 |
35 |
36 | def get_det_aps(detect, target, num_classes, iou_thresh=0.5, use_07_metric=False):
37 | # [[index, bbox, score, label], ...]
38 | aps = []
39 | for c in range(num_classes):
40 | target_c = list(filter(lambda x: x[3] == c, target))
41 | detect_c = filter(lambda x: x[3] == c, detect)
42 | detect_c = sorted(detect_c, key=lambda x: x[2], reverse=True)
43 | tp = np.zeros(len(detect_c))
44 | fp = np.zeros(len(detect_c))
45 | target_count = Counter([x[0] for x in target_c])
46 | target_count = {index: np.zeros(count) for index, count in target_count.items()}
47 | target_lut = defaultdict(list)
48 | for index, bbox, conf, label in target_c:
49 | target_lut[index].append(bbox)
50 | detect_lut = defaultdict(list)
51 | for index, bbox, conf, label in detect_c:
52 | detect_lut[index].append(bbox)
53 | iou_lut = dict()
54 | for index, bboxes in detect_lut.items():
55 | if index in target_lut:
56 | iou_lut[index] = rbbox_iou(np.stack(bboxes), np.stack(target_lut[index]))
57 | counter = defaultdict(int)
58 | for i, (index, bbox, conf, label) in enumerate(detect_c):
59 | count = counter[index]
60 | counter[index] += 1
61 | iou_max = -np.inf
62 | hit_j = 0
63 | if index in iou_lut:
64 | for j, iou in enumerate(iou_lut[index][count]):
65 | if iou > iou_max:
66 | iou_max = iou
67 | hit_j = j
68 | if iou_max > iou_thresh and target_count[index][hit_j] == 0:
69 | tp[i] = 1
70 | target_count[index][hit_j] = 1
71 | else:
72 | fp[i] = 1
73 | tp_sum = np.cumsum(tp)
74 | fp_sum = np.cumsum(fp)
75 | npos = len(target_c)
76 | recall = tp_sum / npos
77 | precision = tp_sum / (tp_sum + fp_sum)
78 | aps.append((get_ap_07 if use_07_metric else get_ap)(recall, precision))
79 | return aps
80 |
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/box/rbbox.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | try:
4 | from .ext.rbbox_overlap_gpu import rbbox_iou as rbbox_iou_gpu
5 | from .ext.rbbox_overlap_gpu import rbbox_nms as rbbox_nms_gpu
6 |
7 | def rbbox_iou(boxes1, boxes2, device=None): # [x, y, w, h, a]
8 | if device is None:
9 | device = 0 if boxes1.device.type == "cpu" else boxes1.device.index
10 | boxes1 = boxes1.reshape([-1, 5]).detach().cpu().numpy().astype(np.float32)
11 | boxes2 = boxes2.reshape([-1, 5]).detach().cpu().numpy().astype(np.float32)
12 | ious = rbbox_iou_gpu(boxes1, boxes2, device)
13 | return ious
14 |
15 | def rbbox_nms(boxes, scores, iou_thresh=0.5, device=None):
16 | if device is None:
17 | device = 0 if boxes.device.type == "cpu" else boxes.device.index
18 | boxes = boxes.reshape([-1, 5]).detach().cpu().numpy().astype(np.float32)
19 | scores = scores.reshape([-1, 1]).detach().cpu().numpy().astype(np.float32)
20 | boxes = np.c_[boxes, scores]
21 | keeps = rbbox_nms_gpu(boxes, iou_thresh, device)
22 | return keeps
23 |
24 | except ModuleNotFoundError as e:
25 |
26 | from .ext.rbbox_overlap_cpu import rbbox_iou_nxn as rbbox_iou_cpu
27 | from .ext.rbbox_overlap_cpu import rbbox_nms as rbbox_nms_cpu
28 |
29 | def rbbox_iou(boxes1, boxes2):
30 | boxes1 = boxes1.reshape([-1, 5]).detach().cpu().numpy().astype(np.float64)
31 | boxes2 = boxes2.reshape([-1, 5]).detach().cpu().numpy().astype(np.float64)
32 | ious = rbbox_iou_cpu(boxes1, boxes2)
33 | return ious
34 |
35 | def rbbox_nms(boxes, scores, iou_thresh=0.5):
36 | boxes = boxes.reshape([-1, 5]).detach().cpu().numpy().astype(np.float64)
37 | scores = scores.reshape([-1]).detach().cpu().numpy().astype(np.float64)
38 | keeps = rbbox_nms_cpu(boxes, scores, iou_thresh)
39 | return keeps
40 |
41 |
42 | def rbbox_batched_nms(boxes, scores, labels, iou_thresh=0.5):
43 | if len(boxes) == 0:
44 | return np.empty([0], dtype=np.int)
45 | max_coordinate = boxes[:, 0:2].max() + boxes[:, 2:4].max()
46 | labels = labels.to(boxes)
47 | offsets = labels * (max_coordinate + 1)
48 | boxes = boxes.clone()
49 | boxes[:, :2] += offsets[:, None]
50 | return rbbox_nms(boxes, scores, iou_thresh)
51 |
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/box/rbbox_np.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | try:
4 | from .ext.rbbox_overlap_gpu import rbbox_iou as rbbox_iou_gpu
5 | from .ext.rbbox_overlap_gpu import rbbox_nms as rbbox_nms_gpu
6 |
7 | def rbbox_iou(boxes1, boxes2, device=0): # [x, y, w, h, a]
8 | boxes1 = boxes1.reshape([-1, 5]).astype(np.float32)
9 | boxes2 = boxes2.reshape([-1, 5]).astype(np.float32)
10 | ious = rbbox_iou_gpu(boxes1, boxes2, device)
11 | return ious
12 |
13 | def rbbox_nms(boxes, scores, iou_thresh=0.5, device=0):
14 | boxes = boxes.reshape([-1, 5]).astype(np.float32)
15 | scores = scores.reshape([-1, 1]).astype(np.float32)
16 | boxes = np.c_[boxes, scores]
17 | keeps = rbbox_nms_gpu(boxes, iou_thresh, device)
18 | return keeps
19 |
20 | except ModuleNotFoundError as e:
21 |
22 | from .ext.rbbox_overlap_cpu import rbbox_iou_nxn as rbbox_iou_cpu
23 | from .ext.rbbox_overlap_cpu import rbbox_nms as rbbox_nms_cpu
24 |
25 | def rbbox_iou(boxes1, boxes2):
26 | boxes1 = boxes1.reshape([-1, 5]).astype(np.float64)
27 | boxes2 = boxes2.reshape([-1, 5]).astype(np.float64)
28 | ious = rbbox_iou_cpu(boxes1, boxes2)
29 | return ious
30 |
31 | def rbbox_nms(boxes, scores, iou_thresh=0.5):
32 | boxes = boxes.reshape([-1, 5]).astype(np.float64)
33 | scores = scores.reshape([-1]).astype(np.float64)
34 | keeps = rbbox_nms_cpu(boxes, scores, iou_thresh)
35 | return keeps
36 |
37 |
38 | def rbbox_batched_nms(boxes, scores, labels, iou_thresh=0.5):
39 | if len(boxes) == 0:
40 | return np.empty([0], dtype=np.int)
41 | max_coordinate = boxes[:, 0:2].max() + boxes[:, 2:4].max()
42 | offsets = labels * (max_coordinate + 1)
43 | boxes = boxes.copy()
44 | boxes[:, :2] += offsets[:, None]
45 | return rbbox_nms(boxes, scores, iou_thresh)
46 |
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/init.py:
--------------------------------------------------------------------------------
1 | from torch import nn
2 |
3 |
4 | def weight_init_normal(m):
5 | if isinstance(m, (nn.Linear, nn.Conv2d, nn.ConvTranspose2d)):
6 | nn.init.normal_(m.weight, 0, 0.02)
7 | elif isinstance(m, nn.BatchNorm2d):
8 | nn.init.normal_(m.weight, 1, 0.02)
9 | nn.init.constant_(m.bias, 0)
10 |
11 |
12 | def weight_init_uniform(m):
13 | if isinstance(m, (nn.Linear, nn.Conv2d, nn.ConvTranspose2d)):
14 | nn.init.uniform_(m.weight, 0, 0.02)
15 | elif isinstance(m, nn.BatchNorm2d):
16 | nn.init.constant_(m.weight, 1)
17 | nn.init.constant_(m.bias, 0)
18 |
19 |
20 | def weight_init_kaiming_normal(m):
21 | if isinstance(m, (nn.Linear, nn.Conv2d, nn.ConvTranspose2d)):
22 | nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu")
23 | elif isinstance(m, nn.BatchNorm2d):
24 | nn.init.constant_(m.weight, 1)
25 | nn.init.constant_(m.bias, 0)
26 |
27 |
28 | def weight_init_kaiming_uniform(m):
29 | if isinstance(m, (nn.Linear, nn.Conv2d, nn.ConvTranspose2d)):
30 | nn.init.kaiming_uniform_(m.weight, mode="fan_out", nonlinearity="relu")
31 | elif isinstance(m, nn.BatchNorm2d):
32 | nn.init.constant_(m.weight, 1)
33 | nn.init.constant_(m.bias, 0)
34 |
35 |
36 | def weight_init_xavier_normal(m):
37 | if isinstance(m, (nn.Linear, nn.Conv2d, nn.ConvTranspose2d)):
38 | nn.init.xavier_normal_(m.weight)
39 | elif isinstance(m, nn.BatchNorm2d):
40 | nn.init.constant_(m.weight, 1)
41 | nn.init.constant_(m.bias, 0)
42 |
43 |
44 | def weight_init_xavier_uniform(m):
45 | if isinstance(m, (nn.Linear, nn.Conv2d, nn.ConvTranspose2d)):
46 | nn.init.xavier_uniform_(m.weight)
47 | elif isinstance(m, nn.BatchNorm2d):
48 | nn.init.constant_(m.weight, 1)
49 | nn.init.constant_(m.bias, 0)
50 |
51 |
52 | weight_init = {
53 | "normal": weight_init_normal,
54 | "uniform": weight_init_uniform,
55 | "kaiming_normal": weight_init_kaiming_normal,
56 | "kaiming_uniform": weight_init_kaiming_uniform,
57 | "xavier_normal": weight_init_xavier_normal,
58 | "xavier_uniform": weight_init_xavier_uniform,
59 | }
60 |
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/misc.py:
--------------------------------------------------------------------------------
1 | import os
2 | from collections import OrderedDict, defaultdict
3 |
4 |
5 | def containerize(x, n=1):
6 | return x if isinstance(x, (list, tuple)) else [x] * n
7 |
8 |
9 | def convert_path(path):
10 | return path.replace(r"\/".replace(os.sep, ""), os.sep)
11 |
12 |
13 | class Node:
14 | __slots__ = "key", "val", "cnt"
15 |
16 | def __init__(self, key, val, cnt=0):
17 | self.key, self.val, self.cnt = key, val, cnt
18 |
19 |
20 | class LFUCache:
21 | def __init__(self, capacity):
22 | self.capacity = capacity
23 | self.cache = {} # type {key: node}
24 | self.cnt2node = defaultdict(OrderedDict)
25 | self.mincnt = 0
26 |
27 | def get(self, key, default=None):
28 | if key not in self.cache:
29 | return default
30 |
31 | node = self.cache[key]
32 | del self.cnt2node[node.cnt][key]
33 |
34 | if not self.cnt2node[node.cnt]:
35 | del self.cnt2node[node.cnt]
36 |
37 | node.cnt += 1
38 | self.cnt2node[node.cnt][key] = node
39 |
40 | if not self.cnt2node[self.mincnt]:
41 | self.mincnt += 1
42 | return node.val
43 |
44 | def put(self, key, value):
45 | if key in self.cache:
46 | self.cache[key].val = value
47 | self.get(key)
48 | return
49 | if len(self.cache) >= self.capacity:
50 | pop_key, _pop_node = self.cnt2node[self.mincnt].popitem(last=False)
51 | del self.cache[pop_key]
52 |
53 | self.cache[key] = self.cnt2node[1][key] = Node(key, value, 1)
54 | self.mincnt = 1
55 |
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/parallel/__init__.py:
--------------------------------------------------------------------------------
1 | from .data_parallel import CustomDetDataParallel
2 | from .sync_batchnorm import convert_model
3 |
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/parallel/data_parallel.py:
--------------------------------------------------------------------------------
1 | import torch
2 | from torch import nn
3 |
4 |
5 | class CustomDetDataParallel(nn.DataParallel):
6 | """
7 | force splitting data to all gpus instead of sending all data to cuda:0 and then moving around.
8 | """
9 |
10 | def __init__(self, module, device_ids):
11 | super().__init__(module, device_ids)
12 |
13 | def scatter(self, inputs, kwargs, device_ids):
14 | # More like scatter and data prep at the same time. The point is we prep the data in such a way
15 | # that no scatter is necessary, and there's no need to shuffle stuff around different GPUs.
16 | data_splits = []
17 | for i, device in enumerate(device_ids):
18 | data_split = []
19 | for data in inputs:
20 | data = data[i :: len(device_ids)]
21 | if isinstance(data, torch.Tensor):
22 | data = data.to(f"cuda:{device}", non_blocking=True)
23 | data_split.append(data)
24 | data_splits.append(data_split)
25 | return data_splits, [kwargs] * len(device_ids)
26 |
27 | def gather(self, outputs, output_device):
28 | if self.training:
29 | # (
30 | # {}, {}, ...
31 | # )
32 | outputs = super().gather(outputs, output_device)
33 | for key, val in list(outputs.items()):
34 | outputs[key] = val.mean()
35 | else:
36 | # (
37 | # [[], [], ...], [[], [], ...]
38 | # )
39 | outputs = sum(map(list, zip(*outputs)), [])
40 | return outputs
41 |
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/parallel/sync_batchnorm/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # File : __init__.py
3 | # Author : Jiayuan Mao
4 | # Email : maojiayuan@gmail.com
5 | # Date : 27/01/2018
6 | #
7 | # This file is part of Synchronized-BatchNorm-PyTorch.
8 | # https://github.com/vacancy/Synchronized-BatchNorm-PyTorch
9 | # Distributed under MIT License.
10 |
11 | from .batchnorm import (
12 | SynchronizedBatchNorm1d,
13 | SynchronizedBatchNorm2d,
14 | SynchronizedBatchNorm3d,
15 | convert_model,
16 | patch_sync_batchnorm,
17 | )
18 | from .replicate import DataParallelWithCallback, patch_replication_callback
19 |
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/parallel/sync_batchnorm/comm.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # File : comm.py
3 | # Author : Jiayuan Mao
4 | # Email : maojiayuan@gmail.com
5 | # Date : 27/01/2018
6 | #
7 | # This file is part of Synchronized-BatchNorm-PyTorch.
8 | # https://github.com/vacancy/Synchronized-BatchNorm-PyTorch
9 | # Distributed under MIT License.
10 |
11 | import collections
12 | import queue
13 | import threading
14 |
15 | __all__ = ["FutureResult", "SlavePipe", "SyncMaster"]
16 |
17 |
18 | class FutureResult(object):
19 | """A thread-safe future implementation. Used only as one-to-one pipe."""
20 |
21 | def __init__(self):
22 | self._result = None
23 | self._lock = threading.Lock()
24 | self._cond = threading.Condition(self._lock)
25 |
26 | def put(self, result):
27 | with self._lock:
28 | assert self._result is None, "Previous result has't been fetched."
29 | self._result = result
30 | self._cond.notify()
31 |
32 | def get(self):
33 | with self._lock:
34 | if self._result is None:
35 | self._cond.wait()
36 |
37 | res = self._result
38 | self._result = None
39 | return res
40 |
41 |
42 | _MasterRegistry = collections.namedtuple("MasterRegistry", ["result"])
43 | _SlavePipeBase = collections.namedtuple("_SlavePipeBase", ["identifier", "queue", "result"])
44 |
45 |
46 | class SlavePipe(_SlavePipeBase):
47 | """Pipe for master-slave communication."""
48 |
49 | def run_slave(self, msg):
50 | self.queue.put((self.identifier, msg))
51 | ret = self.result.get()
52 | self.queue.put(True)
53 | return ret
54 |
55 |
56 | class SyncMaster(object):
57 | """An abstract `SyncMaster` object.
58 |
59 | - During the replication, as the data parallel will trigger an callback of each module, all slave devices should
60 | call `register(id)` and obtain an `SlavePipe` to communicate with the master.
61 | - During the forward pass, master device invokes `run_master`, all messages from slave devices will be collected,
62 | and passed to a registered callback.
63 | - After receiving the messages, the master device should gather the information and determine to message passed
64 | back to each slave devices.
65 | """
66 |
67 | def __init__(self, master_callback):
68 | """
69 |
70 | Args:
71 | master_callback: a callback to be invoked after having collected messages from slave devices.
72 | """
73 | self._master_callback = master_callback
74 | self._queue = queue.Queue()
75 | self._registry = collections.OrderedDict()
76 | self._activated = False
77 |
78 | def __getstate__(self):
79 | return {"master_callback": self._master_callback}
80 |
81 | def __setstate__(self, state):
82 | self.__init__(state["master_callback"])
83 |
84 | def register_slave(self, identifier):
85 | """
86 | Register an slave device.
87 |
88 | Args:
89 | identifier: an identifier, usually is the device id.
90 |
91 | Returns: a `SlavePipe` object which can be used to communicate with the master device.
92 |
93 | """
94 | if self._activated:
95 | assert self._queue.empty(), "Queue is not clean before next initialization."
96 | self._activated = False
97 | self._registry.clear()
98 | future = FutureResult()
99 | self._registry[identifier] = _MasterRegistry(future)
100 | return SlavePipe(identifier, self._queue, future)
101 |
102 | def run_master(self, master_msg):
103 | """
104 | Main entry for the master device in each forward pass.
105 | The messages were first collected from each devices (including the master device), and then
106 | an callback will be invoked to compute the message to be sent back to each devices
107 | (including the master device).
108 |
109 | Args:
110 | master_msg: the message that the master want to send to itself. This will be placed as the first
111 | message when calling `master_callback`. For detailed usage, see `_SynchronizedBatchNorm` for an example.
112 |
113 | Returns: the message to be sent back to the master device.
114 |
115 | """
116 | self._activated = True
117 |
118 | intermediates = [(0, master_msg)]
119 | for i in range(self.nr_slaves):
120 | intermediates.append(self._queue.get())
121 |
122 | results = self._master_callback(intermediates)
123 | assert results[0][0] == 0, "The first result should belongs to the master."
124 |
125 | for i, res in results:
126 | if i == 0:
127 | continue
128 | self._registry[i].result.put(res)
129 |
130 | for i in range(self.nr_slaves):
131 | assert self._queue.get() is True
132 |
133 | return results[0][1]
134 |
135 | @property
136 | def nr_slaves(self):
137 | return len(self._registry)
138 |
--------------------------------------------------------------------------------
/hiector/ssrdd/utils/parallel/sync_batchnorm/replicate.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # File : replicate.py
3 | # Author : Jiayuan Mao
4 | # Email : maojiayuan@gmail.com
5 | # Date : 27/01/2018
6 | #
7 | # This file is part of Synchronized-BatchNorm-PyTorch.
8 | # https://github.com/vacancy/Synchronized-BatchNorm-PyTorch
9 | # Distributed under MIT License.
10 |
11 | import functools
12 |
13 | from torch.nn.parallel.data_parallel import DataParallel
14 |
15 | __all__ = ["CallbackContext", "execute_replication_callbacks", "DataParallelWithCallback", "patch_replication_callback"]
16 |
17 |
18 | class CallbackContext(object):
19 | pass
20 |
21 |
22 | def execute_replication_callbacks(modules):
23 | """
24 | Execute an replication callback `__data_parallel_replicate__` on each module created by original replication.
25 |
26 | The callback will be invoked with arguments `__data_parallel_replicate__(ctx, copy_id)`
27 |
28 | Note that, as all modules are isomorphism, we assign each sub-module with a context
29 | (shared among multiple copies of this module on different devices).
30 | Through this context, different copies can share some information.
31 |
32 | We guarantee that the callback on the master copy (the first copy) will be called ahead of calling the callback
33 | of any slave copies.
34 | """
35 | master_copy = modules[0]
36 | nr_modules = len(list(master_copy.modules()))
37 | ctxs = [CallbackContext() for _ in range(nr_modules)]
38 |
39 | for i, module in enumerate(modules):
40 | for j, m in enumerate(module.modules()):
41 | if hasattr(m, "__data_parallel_replicate__"):
42 | m.__data_parallel_replicate__(ctxs[j], i)
43 |
44 |
45 | class DataParallelWithCallback(DataParallel):
46 | """
47 | Data Parallel with a replication callback.
48 |
49 | An replication callback `__data_parallel_replicate__` of each module will be invoked after being created by
50 | original `replicate` function.
51 | The callback will be invoked with arguments `__data_parallel_replicate__(ctx, copy_id)`
52 |
53 | Examples:
54 | > sync_bn = SynchronizedBatchNorm1d(10, eps=1e-5, affine=False)
55 | > sync_bn = DataParallelWithCallback(sync_bn, device_ids=[0, 1])
56 | # sync_bn.__data_parallel_replicate__ will be invoked.
57 | """
58 |
59 | def replicate(self, module, device_ids):
60 | modules = super(DataParallelWithCallback, self).replicate(module, device_ids)
61 | execute_replication_callbacks(modules)
62 | return modules
63 |
64 |
65 | def patch_replication_callback(data_parallel):
66 | """
67 | Monkey-patch an existing `DataParallel` object. Add the replication callback.
68 | Useful when you have customized `DataParallel` implementation.
69 |
70 | Examples:
71 | > sync_bn = SynchronizedBatchNorm1d(10, eps=1e-5, affine=False)
72 | > sync_bn = DataParallel(sync_bn, device_ids=[0, 1])
73 | > patch_replication_callback(sync_bn)
74 | # this is equivalent to
75 | > sync_bn = SynchronizedBatchNorm1d(10, eps=1e-5, affine=False)
76 | > sync_bn = DataParallelWithCallback(sync_bn, device_ids=[0, 1])
77 | """
78 |
79 | assert isinstance(data_parallel, DataParallel)
80 |
81 | old_replicate = data_parallel.replicate
82 |
83 | @functools.wraps(old_replicate)
84 | def new_replicate(module, device_ids):
85 | modules = old_replicate(module, device_ids)
86 | execute_replication_callbacks(modules)
87 | return modules
88 |
89 | data_parallel.replicate = new_replicate
90 |
--------------------------------------------------------------------------------
/hiector/ssrdd/xtorch/README.md:
--------------------------------------------------------------------------------
1 | # xtorch
2 |
3 | This is a simple encapsulation of pytorch, so that in_featuers/in_channels can be implicitly determined when the model is defined, rather than explicitly specified.
4 |
5 | A simple example is as follows:
6 |
7 | ```python
8 | import torch
9 | from torch import nn
10 | from xtorch import xnn
11 |
12 | model = xnn.Sequential(xnn.Linear(16), nn.ReLU(), xnn.Linear(2))
13 | # <===> nn.Sequential(nn.Linear(8, 16), nn.ReLU(), xnn.Linear(16, 2))
14 |
15 | model.build_pipe(shape=[2, 8])
16 | # alternative
17 | # model.build(torch.randn(2, 8))
18 |
19 | x = torch.randn(32, 8)
20 | y = model(x)
21 | ```
22 |
--------------------------------------------------------------------------------
/hiector/ssrdd/xtorch/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/hiector/ssrdd/xtorch/__init__.py
--------------------------------------------------------------------------------
/hiector/ssrdd/xtorch/xnn/__init__.py:
--------------------------------------------------------------------------------
1 | from .containers import *
2 | from .layers import *
3 |
--------------------------------------------------------------------------------
/hiector/ssrdd/xtorch/xnn/containers.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # File : containers.py
3 | # Author : Kai Ao
4 | # Email : capino627@163.com
5 | # Date : 2020/12/12 12:07
6 | #
7 | # This file is part of Rotation-Decoupled Detector.
8 | # https://github.com/Capino512/pytorch-rotation-decoupled-detector
9 | # Distributed under MIT License.
10 |
11 | import torch
12 | from torch import nn
13 |
14 | __all__ = ["Module", "ModuleAtom", "ModulePipe", "Sequential"]
15 |
16 |
17 | class Module(nn.Module):
18 | def __init__(self):
19 | super(Module, self).__init__()
20 |
21 | def forward(self, *args, **kwargs):
22 | raise NotImplementedError
23 |
24 | def __call__(self, *args, **kwargs):
25 | return self.forward(*args, **kwargs)
26 |
27 | def build_pipe(self, shape):
28 | return self(torch.randn(shape))
29 |
30 | build = __call__
31 |
32 |
33 | class ModuleAtom(Module):
34 | def __init__(self, *args, **kwargs):
35 | super(ModuleAtom, self).__init__()
36 | self.args = args
37 | self.kwargs = kwargs
38 | self.module = None
39 |
40 | def _init_module(self, *args, **kwargs):
41 | raise NotImplementedError
42 |
43 | def forward(self, *args, **kwargs):
44 | if self.module is None:
45 | self._init_module(*args, **kwargs)
46 | return self.module(*args, **kwargs)
47 |
48 |
49 | class ModulePipe(Module):
50 | def __init__(self):
51 | super(ModulePipe, self).__init__()
52 |
53 | def forward(self, x):
54 | for module in self._modules.values():
55 | x = module(x)
56 | return x
57 |
58 |
59 | class Sequential(nn.Sequential, Module):
60 | def __init__(self, *args):
61 | super(Sequential, self).__init__(*args)
62 |
--------------------------------------------------------------------------------
/hiector/ssrdd/xtorch/xnn/layers.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # File : layers.py
3 | # Author : Kai Ao
4 | # Email : capino627@163.com
5 | # Date : 2020/12/12 12:07
6 | #
7 | # This file is part of Rotation-Decoupled Detector.
8 | # https://github.com/Capino512/pytorch-rotation-decoupled-detector
9 | # Distributed under MIT License.
10 |
11 | from torch import nn
12 |
13 | from .containers import ModuleAtom
14 |
15 | __all__ = [
16 | "Linear",
17 | "Conv1d",
18 | "Conv2d",
19 | "Conv3d",
20 | "ConvTranspose1d",
21 | "ConvTranspose2d",
22 | "ConvTranspose3d",
23 | "BatchNorm1d",
24 | "BatchNorm2d",
25 | "BatchNorm3d",
26 | "GroupNorm",
27 | "InstanceNorm1d",
28 | "InstanceNorm2d",
29 | "InstanceNorm3d",
30 | "LayerNorm",
31 | ]
32 |
33 |
34 | class Linear(ModuleAtom):
35 | def __init__(self, out_features, bias=True):
36 | super(Linear, self).__init__(out_features, bias=bias)
37 |
38 | def _init_module(self, x):
39 | if self.args[0] is None:
40 | self.args = (x.shape[1], *self.args[1:])
41 | self.args = (x.shape[1], *self.args)
42 | self.module = nn.Linear(*self.args, **self.kwargs)
43 |
44 |
45 | class ConvNd(ModuleAtom):
46 | def __init__(
47 | self, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode="zeros"
48 | ):
49 | super(ConvNd, self).__init__(
50 | out_channels,
51 | kernel_size,
52 | stride=stride,
53 | padding=padding,
54 | dilation=dilation,
55 | groups=groups,
56 | bias=bias,
57 | padding_mode=padding_mode,
58 | )
59 |
60 | def _init_params(self, x):
61 | if self.kwargs["groups"] < 0:
62 | assert x.shape[1] % self.kwargs["groups"] == 0
63 | self.kwargs["groups"] = x.shape[1] // -self.kwargs["groups"]
64 | if self.args[0] is None:
65 | self.args = (x.shape[1], *self.args[1:])
66 | self.args = (x.shape[1], *self.args)
67 |
68 |
69 | class Conv1d(ConvNd):
70 | def _init_module(self, x):
71 | self._init_params(x)
72 | self.module = nn.Conv1d(*self.args, **self.kwargs)
73 |
74 |
75 | class Conv2d(ConvNd):
76 | def _init_module(self, x):
77 | self._init_params(x)
78 | self.module = nn.Conv2d(*self.args, **self.kwargs)
79 |
80 |
81 | class Conv3d(ConvNd):
82 | def _init_module(self, x):
83 | self._init_params(x)
84 | self.module = nn.Conv3d(*self.args, **self.kwargs)
85 |
86 |
87 | class ConvTransposeNd(ModuleAtom):
88 | def __init__(
89 | self,
90 | out_channels,
91 | kernel_size,
92 | stride=1,
93 | padding=0,
94 | output_padding=0,
95 | dilation=1,
96 | groups=1,
97 | bias=True,
98 | padding_mode="zeros",
99 | ):
100 | super(ConvTransposeNd, self).__init__(
101 | out_channels,
102 | kernel_size,
103 | stride=stride,
104 | padding=padding,
105 | output_padding=output_padding,
106 | dilation=dilation,
107 | groups=groups,
108 | bias=bias,
109 | padding_mode=padding_mode,
110 | )
111 |
112 | def _init_params(self, x):
113 | if self.kwargs["groups"] < 0:
114 | assert x.shape[1] % self.kwargs["groups"] == 0
115 | self.kwargs["groups"] = x.shape[1] // -self.kwargs["groups"]
116 | if self.args[0] is None:
117 | self.args = (x.shape[1], *self.args[1:])
118 | self.args = (x.shape[1], *self.args)
119 |
120 |
121 | class ConvTranspose1d(ConvTransposeNd):
122 | def _init_module(self, x):
123 | self._init_params(x)
124 | self.module = nn.ConvTranspose1d(*self.args, **self.kwargs)
125 |
126 |
127 | class ConvTranspose2d(ConvTransposeNd):
128 | def _init_module(self, x):
129 | self._init_params(x)
130 | self.module = nn.ConvTranspose2d(*self.args, **self.kwargs)
131 |
132 |
133 | class ConvTranspose3d(ConvTransposeNd):
134 | def _init_module(self, x):
135 | self._init_params(x)
136 | self.module = nn.ConvTranspose3d(*self.args, **self.kwargs)
137 |
138 |
139 | class BatchNormNd(ModuleAtom):
140 | def __init__(self, eps=1e-5, momentum=0.1, affine=True, track_running_stats=True):
141 | super(BatchNormNd, self).__init__(
142 | eps=eps, momentum=momentum, affine=affine, track_running_stats=track_running_stats
143 | )
144 |
145 |
146 | class BatchNorm1d(BatchNormNd):
147 | def _init_module(self, x):
148 | self.args = (x.shape[1], *self.args)
149 | self.module = nn.BatchNorm1d(*self.args, **self.kwargs)
150 |
151 |
152 | class BatchNorm2d(BatchNormNd):
153 | def _init_module(self, x):
154 | self.args = (x.shape[1], *self.args)
155 | self.module = nn.BatchNorm2d(*self.args, **self.kwargs)
156 |
157 |
158 | class BatchNorm3d(BatchNormNd):
159 | def _init_module(self, x):
160 | self.args = (x.shape[1], *self.args)
161 | self.module = nn.BatchNorm3d(*self.args, **self.kwargs)
162 |
163 |
164 | class GroupNorm(ModuleAtom):
165 | def __init__(self, num_groups, eps=1e-5, affine=True):
166 | super(GroupNorm, self).__init__(num_groups, eps=eps, affine=affine)
167 |
168 | def _init_module(self, x):
169 | num_groups = self.args[0]
170 | if num_groups < 0:
171 | assert x.shape[1] % num_groups == 0
172 | num_groups = x.shape[1] // -num_groups
173 | self.args = (num_groups, x.shape[1])
174 | self.module = nn.GroupNorm(*self.args, **self.kwargs)
175 |
176 |
177 | class InstanceNormNd(ModuleAtom):
178 | def __init__(self, eps=1e-5, momentum=0.1, affine=False, track_running_stats=False):
179 | super(InstanceNormNd, self).__init__(
180 | eps=eps, momentum=momentum, affine=affine, track_running_stats=track_running_stats
181 | )
182 |
183 |
184 | class InstanceNorm1d(InstanceNormNd):
185 | def _init_module(self, x):
186 | self.args = (x.shape[1], *self.args)
187 | self.module = nn.InstanceNorm1d(*self.args, **self.kwargs)
188 |
189 |
190 | class InstanceNorm2d(InstanceNormNd):
191 | def _init_module(self, x):
192 | self.args = (x.shape[1], *self.args)
193 | self.module = nn.InstanceNorm2d(*self.args, **self.kwargs)
194 |
195 |
196 | class InstanceNorm3d(InstanceNormNd):
197 | def _init_module(self, x):
198 | self.args = (x.shape[1], *self.args)
199 | self.module = nn.InstanceNorm3d(*self.args, **self.kwargs)
200 |
201 |
202 | class LayerNorm(ModuleAtom):
203 | def __init__(self, num_last_dimensions, *args, **kwargs):
204 | super(LayerNorm, self).__init__(num_last_dimensions, *args, **kwargs)
205 |
206 | def _init_module(self, x):
207 | self.args = (x.shape[-self.args[0] :],)
208 | self.module = nn.LayerNorm(*self.args, **self.kwargs)
209 |
--------------------------------------------------------------------------------
/hiector/tasks/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | A subfolder containing implementations of EOTasks
3 | """
4 |
--------------------------------------------------------------------------------
/hiector/tasks/cropping.py:
--------------------------------------------------------------------------------
1 | """
2 | Utilities for cropping data
3 | """
4 | import functools
5 | from collections import defaultdict
6 |
7 | import geopandas as gpd
8 | import numpy as np
9 | import shapely.ops
10 | from shapely.affinity import translate
11 | from shapely.wkt import loads as load_wkt
12 |
13 | from eolearn.core import EOTask
14 | from sentinelhub import parse_time, pixel_to_utm
15 |
16 | from ..utils.preprocessing import round_point_coords
17 |
18 |
19 | class CroppingTask(EOTask):
20 | def __init__(
21 | self,
22 | raster_feature,
23 | data_mask_feature,
24 | vector_feature,
25 | grid_feature,
26 | intersection_feature,
27 | data_stack_feature,
28 | no_data_value: float,
29 | size: int,
30 | overlap: float,
31 | resolution: float,
32 | cloud_mask_feature=None,
33 | valid_reference_mask_feature=None,
34 | valid_threshold: float = 0.0,
35 | iou_threshold: float = 0.5,
36 | take_closest_time_frame=None,
37 | ):
38 | """
39 | :param raster_feature: A data feature to crop
40 | :param data_mask_feature: A mask feature showing areas with no valid data
41 | :param vector_feature: A vector feature to crop
42 | :param grid_feature: A vector feature where cropped grid is saved at.
43 | :param intersection_feature: A vector feature where a spatial join between grid and reference polygons is
44 | stored.
45 | :param data_stack_feature: A data feature where output stack of data is stored.
46 | :param no_data_value: Value that will be set to all pixels that are masked.
47 | :param size: A size of of images to crop out of input image.
48 | :param overlap: Overlap between sub-images extracted.
49 | :param resolution: Resolution on which task is running.
50 | :param cloud_mask_feature: An input cloud mask feature. If not provided it will be ignored.
51 | :param valid_reference_mask_feature: An input mask feature defining where reference labels are available. If
52 | not provided it will be ignored.
53 | :param valid_threshold: Discard sub-images that have a fraction of valid data lower than threshold.
54 | :param iou_threshold: A minimal percentage of area of a building reference polygon that has to intersect
55 | with a grid polygon so that it will still be used for training for that grid polygon.
56 | :param take_closest_time_frame: Take a time frame that is closest to the given timestamp
57 | """
58 | self.raster_feature = raster_feature
59 | self.data_mask_feature = data_mask_feature
60 | self.cloud_mask_feature = cloud_mask_feature
61 | self.valid_reference_mask_feature = valid_reference_mask_feature
62 | self.vector_feature = vector_feature
63 | self.grid_feature = grid_feature
64 | self.intersection_feature = intersection_feature
65 | self.data_stack_feature = data_stack_feature
66 | self.no_data_value = no_data_value
67 | self.size = size
68 | self.overlap = overlap
69 | self.resolution = resolution
70 | self.valid_threshold = valid_threshold
71 | self.iou_threshold = iou_threshold
72 | self.take_closest_time_frame = (
73 | parse_time(take_closest_time_frame, force_datetime=True, ignoretz=True) if take_closest_time_frame else None
74 | )
75 |
76 | def _choose_time_index(self, timestamps):
77 | """Chooses an index of a timestamp that is closest to specified timestamp"""
78 | if not self.take_closest_time_frame:
79 | return 0
80 |
81 | if not timestamps:
82 | raise ValueError(
83 | "EOPatch has no timestamps, hence we cannot choose a timestamp closest to "
84 | f"{self.take_closest_time_frame}"
85 | )
86 |
87 | timestamps = [timestamp.replace(tzinfo=None) for timestamp in timestamps]
88 | time_differences = [abs((timestamp - self.take_closest_time_frame).total_seconds()) for timestamp in timestamps]
89 | return np.argmin(time_differences)
90 |
91 | def _apply_no_data_value(self, data, data_mask, cloud_mask, valid_reference_mask):
92 | """Sets no_data_value to all invalid data pixels"""
93 | if cloud_mask is not None:
94 | data_mask = data_mask & ~cloud_mask
95 |
96 | if valid_reference_mask is not None:
97 | data_mask = data_mask & valid_reference_mask
98 |
99 | data_mask = data_mask.squeeze(axis=-1)
100 | data[~data_mask.astype(bool), :] = self.no_data_value
101 | return data
102 |
103 | def _crop_data(self, data, data_mask, cloud_mask, valid_reference_mask):
104 | height, width, bands = data.shape
105 | stride = int(self.size * (1 - self.overlap))
106 |
107 | cropped_data = []
108 | stats = defaultdict(list)
109 | for x in range(0, width, stride):
110 | for y in range(0, height, stride):
111 | x2, y2 = min(x + self.size, width), min(y + self.size, height)
112 | x1, y1 = max(0, x2 - self.size), max(0, y2 - self.size)
113 |
114 | if x1 == x2 or y1 == y2:
115 | continue
116 |
117 | data_slice = data[y1:y2, x1:x2, ...]
118 | data_mask_slice = data_mask[y1:y2, x1:x2, ...]
119 | valid_mask_slice = data_mask_slice
120 | if cloud_mask is not None:
121 | cloud_mask_slice = cloud_mask[y1:y2, x1:x2, ...]
122 | valid_mask_slice = valid_mask_slice & ~cloud_mask_slice
123 |
124 | if valid_reference_mask is not None:
125 | valid_reference_mask_slice = valid_reference_mask[y1:y2, x1:x2, ...]
126 | valid_mask_slice = valid_mask_slice & valid_reference_mask_slice
127 |
128 | valid_fraction = np.mean(valid_mask_slice)
129 | if valid_fraction < self.valid_threshold:
130 | continue
131 |
132 | cropped_data.append(data_slice)
133 |
134 | stats["IS_DATA_RATIO"].append(np.mean(data_mask_slice))
135 | stats["VALID_DATA_RATIO"].append(np.mean(valid_mask_slice))
136 | if cloud_mask is not None:
137 | stats["CLOUD_COVERAGE"].append(1 - np.mean(cloud_mask_slice))
138 | if valid_reference_mask is not None:
139 | stats["HAS_REF_RATIO"].append(np.mean(valid_reference_mask_slice))
140 |
141 | polygon = shapely.geometry.box(x1, y1, x2, y2)
142 | stats["pixel_geometry"].append(polygon)
143 |
144 | cropped_data = (
145 | np.stack(cropped_data, axis=0)
146 | if cropped_data
147 | else np.zeros((0, self.size, self.size, bands), dtype=data.dtype)
148 | )
149 | return cropped_data, stats
150 |
151 | def _threshold_by_iou(self, row):
152 | """Intersects grid bbox with reference bbox and thresholds by intersection over union"""
153 |
154 | if isinstance(row.pixel_bbox, str):
155 | pixel_bbox = load_wkt(row.pixel_bbox)
156 | else:
157 | pixel_bbox = row.pixel_bbox
158 |
159 | pixel_geometry = row.pixel_geometry
160 | if pixel_bbox.area == 0:
161 | return False
162 | intersection_geo = pixel_geometry.intersection(pixel_bbox)
163 | iou = intersection_geo.area / pixel_bbox.area
164 | return iou > self.iou_threshold
165 |
166 | def execute(self, eopatch, *, eopatch_name):
167 | time_index = self._choose_time_index(eopatch.timestamp)
168 | data = eopatch[self.raster_feature][time_index]
169 | data_mask = eopatch[self.data_mask_feature][time_index]
170 | cloud_mask = eopatch[self.cloud_mask_feature][time_index].astype(bool) if self.cloud_mask_feature else None
171 | reference_mask = (
172 | eopatch[self.valid_reference_mask_feature].astype(bool) if self.valid_reference_mask_feature else None
173 | )
174 |
175 | reference_gdf = (
176 | eopatch[self.vector_feature]
177 | if self.vector_feature in eopatch
178 | else gpd.GeoDataFrame(geometry=[], crs=eopatch.bbox.crs.pyproj_crs())
179 | )
180 |
181 | data = self._apply_no_data_value(data, data_mask, cloud_mask, reference_mask)
182 |
183 | transform = eopatch.bbox.get_transform_vector(self.resolution, self.resolution)
184 |
185 | def pixel_to_utm_transformer(column, row):
186 | return pixel_to_utm(row, column, transform=transform)
187 |
188 | cropped_data, stats = self._crop_data(data, data_mask, cloud_mask, reference_mask)
189 | utm_polygons = [shapely.ops.transform(pixel_to_utm_transformer, polygon) for polygon in stats["pixel_geometry"]]
190 |
191 | crop_grid_gdf = gpd.GeoDataFrame(
192 | stats, geometry=utm_polygons, crs=eopatch.bbox.crs.pyproj_crs()
193 | ).drop_duplicates(subset="geometry", keep="first")
194 | # We drop the duplicate geometries, since the _crop_data function can return duplicates due to the clipping.
195 |
196 | cropped_data = cropped_data[crop_grid_gdf.index.values, ...]
197 | assert len(cropped_data) == len(
198 | crop_grid_gdf
199 | ), "Number of sampled images doesn't match number of bounding boxes"
200 |
201 | crop_grid_gdf["EOPATCH_NAME"] = eopatch_name
202 | crop_grid_gdf["NAME"] = crop_grid_gdf.pixel_geometry.apply(
203 | lambda geo: f"{eopatch_name}-{int(geo.bounds[0])}-{int(geo.bounds[1])}-{self.size}"
204 | )
205 | crop_grid_gdf["XB"] = crop_grid_gdf.pixel_geometry.apply(lambda geo: int(geo.bounds[0]))
206 | crop_grid_gdf["YB"] = crop_grid_gdf.pixel_geometry.apply(lambda geo: int(geo.bounds[1]))
207 | if reference_gdf.crs != crop_grid_gdf.crs:
208 | reference_gdf.to_crs(crop_grid_gdf.crs, inplace=True)
209 | joined_gdf = gpd.sjoin(crop_grid_gdf, reference_gdf)
210 | is_large_enough_iou = joined_gdf.apply(self._threshold_by_iou, axis=1)
211 | joined_gdf = joined_gdf[is_large_enough_iou]
212 |
213 | if not joined_gdf.empty:
214 | joined_gdf["pixel_bbox"] = joined_gdf[["pixel_bbox", "XB", "YB"]].apply(
215 | lambda row: translate(load_wkt(row.pixel_bbox), xoff=-row.XB, yoff=-row.YB), axis=1
216 | )
217 | # TODO: harmonize this with rounding in TransformToPixelsCoordTask
218 | rounder = functools.partial(round_point_coords, decimals=1)
219 | joined_gdf["pixel_bbox"] = joined_gdf["pixel_bbox"].apply(
220 | lambda bbox_polygon: shapely.ops.transform(rounder, bbox_polygon).wkt
221 | )
222 |
223 | counts = {name: count for name, count in joined_gdf.groupby("NAME").size().iteritems()}
224 |
225 | crop_grid_gdf["N_BBOXES"] = crop_grid_gdf.NAME.apply(lambda name: counts.get(name, 0))
226 | crop_grid_gdf["pixel_geometry"] = crop_grid_gdf.pixel_geometry.apply(lambda x: x.wkt)
227 |
228 | eopatch[self.data_stack_feature] = cropped_data
229 | eopatch[self.grid_feature] = crop_grid_gdf
230 | eopatch[self.intersection_feature] = joined_gdf
231 | return eopatch
232 |
--------------------------------------------------------------------------------
/hiector/tasks/preprocessing.py:
--------------------------------------------------------------------------------
1 | import functools
2 | from typing import Optional
3 |
4 | import shapely.ops
5 | from shapely.wkt import loads as loads_wkt
6 | import geopandas as gpd
7 |
8 | from eolearn.core import EOTask, MapFeatureTask
9 | from sentinelhub import utm_to_pixel
10 |
11 | from ..utils.preprocessing import calculate_bbox_ratio, calculate_hbb_and_obb, round_point_coords
12 |
13 |
14 | class ReprojectReferenceTask(EOTask):
15 | def __init__(self, reference_feature):
16 | self.reference_feature = reference_feature
17 |
18 | def execute(self, eopatch):
19 | new_ref = gpd.GeoDataFrame(data=[], geometry=[], crs=eopatch.bbox.crs.pyproj_crs())
20 | if self.reference_feature in eopatch:
21 | ref = eopatch[self.reference_feature]
22 | target_crs = eopatch.bbox.crs.pyproj_crs()
23 | new_ref = ref.to_crs(target_crs)
24 | eopatch[self.reference_feature] = new_ref
25 | return eopatch
26 |
27 |
28 | class DropDuplicatePolygonsTask(MapFeatureTask):
29 | """Creates bounding boxes around each polygon"""
30 |
31 | def map_method(self, dataframe, *, column):
32 | return dataframe.drop_duplicates(subset=column)
33 |
34 |
35 | class RemoveInvalidGeometryTask(MapFeatureTask):
36 | """There are some LineString geometries, we remove them here"""
37 |
38 | def map_method(self, dataframe):
39 | dataframe["geometry"] = dataframe["geometry"].buffer(0)
40 | return dataframe[dataframe.is_valid]
41 |
42 |
43 | class CreatePolygonBBoxesTask(MapFeatureTask):
44 | """Creates bounding boxes around each polygon"""
45 |
46 | def map_method(self, dataframe):
47 | return calculate_hbb_and_obb(dataframe)
48 |
49 |
50 | class FilterEmptyGeometriesTask(MapFeatureTask):
51 | """Filters geometries with 0 area"""
52 |
53 | def map_method(self, dataframe, *, column):
54 | return dataframe[~dataframe[column].is_empty]
55 |
56 |
57 | class TransformToPixelsCoordTask(EOTask):
58 | """Transform bounding boxes into pixel coordinates"""
59 |
60 | def __init__(
61 | self,
62 | input_feature,
63 | output_feature,
64 | bbox_column: str,
65 | resolution: float,
66 | round_decimals: Optional[int] = None,
67 | ):
68 | self.input_feature = input_feature
69 | self.output_feature = output_feature
70 | self.bbox_column = bbox_column
71 | self.resolution = resolution
72 | self.round_decimals = round_decimals
73 |
74 | def execute(self, eopatch):
75 | transform = eopatch.bbox.get_transform_vector(self.resolution, self.resolution)
76 | dataframe = eopatch[self.input_feature]
77 |
78 | def utm_to_pixel_transformer(east, north):
79 | row, column = utm_to_pixel(east, north, transform=transform, truncate=False)
80 | return column, row
81 |
82 | bboxes = dataframe[self.bbox_column]
83 |
84 | new_bbox_column_name = "pixel_bbox"
85 | dataframe[new_bbox_column_name] = bboxes.apply(
86 | lambda bbox_polygon: shapely.ops.transform(utm_to_pixel_transformer, loads_wkt(bbox_polygon)).wkt
87 | )
88 |
89 | if self.round_decimals is not None:
90 | rounder = functools.partial(round_point_coords, decimals=self.round_decimals)
91 | dataframe[new_bbox_column_name] = dataframe[new_bbox_column_name].apply(
92 | lambda bbox_polygon: shapely.ops.transform(rounder, loads_wkt(bbox_polygon)).wkt
93 | )
94 |
95 | eopatch[self.output_feature] = dataframe
96 | return eopatch
97 |
98 |
99 | class CalculateBBoxRatioTask(MapFeatureTask):
100 | """For each bounding box it calculates ratios between its larger and smaller sides"""
101 |
102 | def map_method(self, dataframe, bbox_column):
103 | bboxes = dataframe[bbox_column]
104 |
105 | ratio_column_name = f"{bbox_column}_ratio"
106 | dataframe[ratio_column_name] = bboxes.apply(calculate_bbox_ratio)
107 |
108 | return dataframe
109 |
--------------------------------------------------------------------------------
/hiector/tasks/reference.py:
--------------------------------------------------------------------------------
1 | import math
2 | from typing import Callable
3 |
4 | import geopandas as gpd
5 | import pandas as pd
6 | import shapely
7 | from shapely.geometry import Polygon
8 |
9 | from eolearn.core import EOPatch, EOTask, FeatureType, MapFeatureTask
10 | from eolearn.io import VectorImportTask
11 |
12 | from ..utils.geometry import merge_bboxes
13 |
14 |
15 | class PrepareMRRTask(EOTask):
16 | """This task will prepare the geopandas dataframe with minimum rotated bounding boxes from
17 | reference geometries (buildings).
18 | """
19 |
20 | @staticmethod
21 | def _calculate_mrr(g: shapely.geometry.Polygon, minsize: float) -> shapely.geometry.Polygon:
22 | # Calculate minimal rotated rectangle for a geometry. If the MRR is below a certain size
23 | # it is scaled to minsize.
24 | _mrr = g.minimum_rotated_rectangle
25 | if _mrr.area < minsize:
26 | ratio = math.sqrt(minsize / _mrr.area)
27 | _mrr = shapely.affinity.scale(_mrr, xfact=ratio, yfact=ratio, origin="centroid")
28 | return _mrr
29 |
30 | def _prepare_df(
31 | self, df: gpd.GeoDataFrame, erode_buffer: float = 2, minsize: float = 3 * 4 * 100
32 | ) -> gpd.GeoDataFrame:
33 | _df = df.copy()
34 | _df["dissolve_col"] = 1
35 | # Building geometries are buffered and then merged/dissolved so that blocks of buildings that are inseparable
36 | # by S-2 are joined into one geometry / MRR
37 | _df.geometry = _df.buffer(erode_buffer)
38 | _df = _df.dissolve(by="dissolve_col").explode(ignore_index=True)
39 | # The geometries are unbuffered to reflect the original size
40 | _df.geometry = _df.buffer(-1 * erode_buffer)
41 | # Remove empty geometries
42 | _df = _df[~_df.geometry.is_empty]
43 | # minimal rotated rectangle is calculated for each of these joined geometries
44 | _df = _df.geometry.apply(lambda g: self._calculate_mrr(g, minsize))
45 | return _df.reset_index(drop=True)
46 |
47 | def __init__(
48 | self,
49 | input_feature: FeatureType,
50 | output_feature: FeatureType,
51 | closing_buffer: int = 2,
52 | minsize: int = 3 * 4 * 100,
53 | ):
54 | """
55 | Args:
56 | input_feature (FeatureType): input vector feature (with reference geometries)
57 | output_feature (FeatureType): output vector feature (with minimum rotated rectangle geometries)
58 | closing_buffer (float): buffer used to join "touching" reference geometries
59 | minsize (float): minimum area size for the minimum rotated rectangle geometries
60 | """
61 | self.input_feature = self.parse_feature(
62 | input_feature, allowed_feature_types=[FeatureType.VECTOR, FeatureType.VECTOR_TIMELESS]
63 | )
64 |
65 | self.output_feature = self.parse_feature(
66 | output_feature, allowed_feature_types=[FeatureType.VECTOR, FeatureType.VECTOR_TIMELESS]
67 | )
68 |
69 | self.closing_buffer = closing_buffer
70 | self.minsize = minsize
71 |
72 | def execute(self, eopatch: EOPatch):
73 | gdf = gpd.GeoDataFrame({"geometry": [], "area": None, "merging_idx": None}, crs=None)
74 | if not eopatch[self.input_feature].empty:
75 | gdf = self._prepare_df(eopatch[self.input_feature], self.closing_buffer, self.minsize)
76 | eopatch[self.output_feature] = gdf
77 | return eopatch
78 |
79 |
80 | class MergeBBoxesTask(EOTask):
81 | """This task will merge MRRs based on sorting column and "IoU"."""
82 |
83 | def __init__(
84 | self,
85 | input_feature: FeatureType,
86 | output_feature: FeatureType,
87 | iou_method: Callable[[Polygon, gpd.GeoSeries], pd.Series],
88 | iou_thr: float = 0.4,
89 | sorting_col: str = "area",
90 | ):
91 | """
92 | Args:
93 | input_feature (FeatureType): input vector feature (with prepared MRRs)
94 | output_feature (FeatureType): output vector feature (with remaining/merged MRR geometries)
95 | iou_method (Callable): method used to calculate "IoU"
96 | iou_thr (float): iou threshold (everything above iou_thr is deemed to be the same MRR and hence merged)
97 | sorting_col (str): column used to define "best" candidate for MRRs that need to be merged
98 | """
99 | self.input_feature = self.parse_feature(
100 | input_feature, allowed_feature_types=[FeatureType.VECTOR, FeatureType.VECTOR_TIMELESS]
101 | )
102 |
103 | self.output_feature = self.parse_feature(
104 | output_feature, allowed_feature_types=[FeatureType.VECTOR, FeatureType.VECTOR_TIMELESS]
105 | )
106 |
107 | self.iou_method = iou_method
108 | self.iou_thr = iou_thr
109 | self.sorting_col = sorting_col
110 |
111 | def execute(self, eopatch: EOPatch):
112 | bboxes = merge_bboxes(
113 | eopatch[self.input_feature], iou_method=self.iou_method, iou_thr=self.iou_thr, sorting_col=self.sorting_col
114 | )
115 |
116 | eopatch[self.output_feature] = bboxes
117 | return eopatch
118 |
119 |
120 | class QPVectorImportTask(VectorImportTask):
121 | """Very similar to VectorImportTask, but allows for final transformation of data (e.g. to eopatch bbox crs)"""
122 |
123 | def __init__(self, feature, path, reproject=True, clip=False, config=None, **kwargs):
124 | super().__init__(feature=feature, path=path, reproject=reproject, clip=clip, config=config, **kwargs)
125 |
126 | def execute(self, eopatch=None, *, bbox=None, to_crs=None):
127 | """
128 | Args:
129 | eopatch (EOPatch): input EOPatch to execute task on (new EOPatch will be created else)
130 | bbox (BBox): A bounding box for which to load data. By default, if none is provided, it will take a
131 | bounding box of given EOPatch. If given EOPatch is not provided it will load the entire dataset.
132 | to_crs (pyproj.crs): crs to which feature should be reprojected
133 | Returns:
134 | EOPatch
135 | """
136 | eopatch = eopatch or EOPatch()
137 | bbox = bbox or eopatch.bbox
138 |
139 | data = self._load_vector_data(bbox)
140 |
141 | if to_crs:
142 | data = data.to_crs(to_crs)
143 |
144 | eopatch[self.feature] = data
145 |
146 | return eopatch
147 |
148 |
149 | class FilterReferenceBuildingsTask(MapFeatureTask):
150 | """Takes only building polygons that are ok to use"""
151 |
152 | def map_method(self, feature):
153 | if not feature.empty:
154 | feature = feature[feature["is confirmed"]]
155 | feature = feature[feature["is building"]]
156 | return feature
157 |
158 |
159 | class CalculateAreaProperty(MapFeatureTask):
160 | """Task to calculate area (column)"""
161 |
162 | def map_method(self, feature):
163 | if not feature.empty:
164 | feature["area"] = feature.area
165 | return feature
166 |
--------------------------------------------------------------------------------
/hiector/tasks/satellite_data.py:
--------------------------------------------------------------------------------
1 | from eolearn.core import FeatureType
2 | from eolearn.io import SentinelHubInputTask
3 | from sentinelhub import DataCollection, SHConfig
4 |
5 |
6 | def s2_data_input_task(
7 | maxcc: float = 0.2, mosaicking_order: str = "leastCC", config: SHConfig = None
8 | ) -> SentinelHubInputTask:
9 | """Construct a SentinelHubInput task to download single mosaicked image from time interval
10 |
11 | Args:
12 | maxcc (float): max cloud coverage to be used
13 | mosaicking_order (str): mosaicking order (e.g. mostRecent)
14 | config (SHConfig): SHConfig object
15 |
16 | Returns:
17 | SentinelHubInputTask: eo-learn task to download (all bands + dataMask + CLM) Sentinel-2 data
18 | """
19 | return SentinelHubInputTask(
20 | bands_feature=(FeatureType.DATA, "BANDS"),
21 | bands=["B01", "B02", "B03", "B04", "B05", "B06", "B07", "B08", "B8A", "B09", "B10", "B11", "B12"],
22 | data_collection=DataCollection.SENTINEL2_L1C,
23 | single_scene=True,
24 | resolution=10,
25 | additional_data=[(FeatureType.MASK, "dataMask"), (FeatureType.MASK, "CLM")],
26 | maxcc=maxcc,
27 | config=config,
28 | mosaicking_order=mosaicking_order,
29 | )
30 |
--------------------------------------------------------------------------------
/hiector/tasks/training_data.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import json
3 |
4 | import numpy as np
5 | from shapely.wkt import loads
6 |
7 | from eolearn.core import EOTask, get_filesystem
8 | from sentinelhub import SHConfig
9 |
10 |
11 | class ExportRasterDataTask(EOTask):
12 | def __init__(self, raster_feature, grid_feature, path, config=None):
13 | self.raster_feature = raster_feature
14 | self.grid_feature = grid_feature
15 | self.path = path
16 | self.config = config or SHConfig()
17 |
18 | def execute(self, eopatch):
19 | data = eopatch[self.raster_feature]
20 | grid = eopatch[self.grid_feature]
21 |
22 | filesystem = get_filesystem(self.path, config=self.config)
23 | for sample, name in zip(data, grid.NAME):
24 | with filesystem.openbin(f"{name}.npy", "w") as file_handle:
25 | np.save(file_handle, sample)
26 |
27 | return eopatch
28 |
29 |
30 | class ExportGeometriesAndLabelsTask(EOTask):
31 | def __init__(self, reference_feature, path, config=None):
32 | self.reference_feature = reference_feature
33 | self.path = path
34 | self.config = config or SHConfig()
35 |
36 | @staticmethod
37 | def _export_to_json(tile_df, filesystem):
38 | payload = [
39 | {
40 | "label": "building",
41 | "geometry": list(loads(polygon).exterior.coords)[:-1]
42 | if isinstance(polygon, str)
43 | else list(polygon.exterior.coords)[:-1],
44 | }
45 | for polygon in tile_df.pixel_bbox.values
46 | ]
47 |
48 | filename = f"{tile_df.NAME.values[0]}.json"
49 | with filesystem.open(filename, "w") as file_handle:
50 | json.dump(payload, file_handle, indent=2)
51 |
52 | def execute(self, eopatch):
53 | reference_data = eopatch[self.reference_feature]
54 | reference_data = reference_data[["pixel_bbox", "NAME"]]
55 |
56 | filesystem = get_filesystem(self.path, config=self.config)
57 | export_function = functools.partial(self._export_to_json, filesystem=filesystem)
58 | reference_data.groupby("NAME").apply(export_function)
59 |
60 | return eopatch
61 |
--------------------------------------------------------------------------------
/hiector/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/hiector/utils/__init__.py
--------------------------------------------------------------------------------
/hiector/utils/aws_utils.py:
--------------------------------------------------------------------------------
1 | import fs
2 | import fs.copy
3 | from fs.osfs import OSFS
4 | from fs.tempfs import TempFS
5 | from fs_s3fs import S3FS
6 |
7 | from eolearn.core.utils.fs import get_aws_credentials, get_base_filesystem_and_path
8 | from sentinelhub import SHConfig
9 |
10 |
11 | def get_filesystem(bucket_name: str, profile_name: str) -> S3FS:
12 | """
13 | Method to create a S3FS filesystem
14 |
15 | :param bucket_name: Name of the S3 bucket
16 | :param profile_name: profile name that will be used to create session and retrieve credentials from
17 | :return: a S3FS filesystem
18 | """
19 | config = get_aws_credentials(aws_profile=profile_name)
20 |
21 | return S3FS(
22 | bucket_name=bucket_name,
23 | aws_access_key_id=config.aws_access_key_id,
24 | aws_secret_access_key=config.aws_secret_access_key,
25 | )
26 |
27 |
28 | class LocalFile:
29 | """An abstraction for working with a local version of a file.
30 |
31 | If file's original location is remote (e.g. on S3) then this will ensure working with a local copy. If file's
32 | original location is already on local filesystem then it will work with that, unless `always_copy=True` is used.
33 |
34 | Note: this class was copied from lcms.utils.fs
35 | """
36 |
37 | def __init__(self, path, mode="r", filesystem=None, config=None, always_copy=False, **temp_fs_kwargs):
38 | """
39 | :param path: Either a full path to a remote file or a path to remote file which is relative to given filesystem
40 | object.
41 | :type path: str
42 | :param mode: One of the option `r', 'w', and 'rw`, which specify if a file should be read or written to remote.
43 | The default is 'r'.
44 | :type mode: str
45 | :param filesystem: A filesystem of the remote. If not given, it will be determined from the path.
46 | :type: fs.FS
47 | :param config: A config object with which AWS credentials could be used to initialize a remote filesystem object
48 | :type config: SHConfig
49 | :param always_copy: If True it will always make a local copy to a temporary folder, even if a file is already
50 | in the local filesystem.
51 | :type always_copy: bool
52 | :param temp_fs_kwargs: Parameters that will be propagated to fs.tempfs.TempFS
53 | """
54 | if filesystem is None:
55 | filesystem, path = get_base_filesystem_and_path(path, config=config)
56 | self._remote_path = path
57 | self._remote_filesystem = filesystem
58 |
59 | if not (mode and isinstance(mode, str) and set(mode) <= {"r", "w"}):
60 | raise ValueError(f"Parameter mode should be one of the strings 'r', 'w' or 'rw' but {mode} found")
61 | self._mode = mode
62 |
63 | if isinstance(self._remote_filesystem, OSFS) and not always_copy:
64 | self._filesystem = self._remote_filesystem
65 | self._local_path = self._remote_path
66 | else:
67 | self._filesystem = TempFS(**temp_fs_kwargs)
68 | self._local_path = fs.path.basename(self._remote_path)
69 |
70 | self._absolute_local_path = self._filesystem.getsyspath(self._local_path)
71 |
72 | if "r" in self._mode:
73 | self._copy_to_local()
74 | elif "w" in self._mode:
75 | remote_dirs = fs.path.dirname(self._remote_path)
76 | self._remote_filesystem.makedirs(remote_dirs, recreate=True)
77 |
78 | @property
79 | def path(self):
80 | """Provides an absolute path to the copy in the local filesystem"""
81 | return self._absolute_local_path
82 |
83 | def __enter__(self):
84 | """This allows the class to be used as a context manager"""
85 | return self
86 |
87 | def __exit__(self, *_, **__):
88 | """This allows the class to be used as a context manager. In case an error is raised this will by default
89 | still delete the object from local folder. In case you don't want that, initialize `LocalFile` with
90 | `auto_clean=False`.
91 | """
92 | self.close()
93 |
94 | def close(self):
95 | """Close the local copy"""
96 | if "w" in self._mode:
97 | self._copy_to_remote()
98 |
99 | if self._filesystem is not self._remote_filesystem:
100 | self._filesystem.close()
101 |
102 | def _copy_to_local(self):
103 | """Copy from remote to local location"""
104 | if self._filesystem is not self._remote_filesystem:
105 | fs.copy.copy_file(self._remote_filesystem, self._remote_path, self._filesystem, self._local_path)
106 |
107 | def _copy_to_remote(self):
108 | """Copy from local to remote location"""
109 | if self._filesystem is not self._remote_filesystem:
110 | fs.copy.copy_file(self._filesystem, self._local_path, self._remote_filesystem, self._remote_path)
111 |
--------------------------------------------------------------------------------
/hiector/utils/download.py:
--------------------------------------------------------------------------------
1 | from eolearn.core import EOWorkflow, FeatureType, OverwritePermission, SaveTask, linearly_connect_tasks
2 | from eolearn.geometry import VectorToRasterTask
3 | from eolearn.io import SentinelHubEvalscriptTask, VectorImportTask
4 | from sentinelhub import DataCollection, SHConfig
5 |
6 |
7 | def get_download_workflow(
8 | collection: DataCollection,
9 | resolution: float,
10 | evalscript: str,
11 | sh_config: SHConfig,
12 | ref_path: str,
13 | ml_aois_path: str,
14 | save_path: str,
15 | mosaicking_order: str = "mostRecent",
16 | ref_feature_name: str = "reference_v0_0_1",
17 | ml_aois_feature_name: str = "ml_aois",
18 | has_ref_feature_name: str = "has_ref",
19 | raster_value: int = 1,
20 | ) -> EOWorkflow:
21 | """Construct a workflow for downloading data from SH, adding vectors and saving.
22 |
23 | Args:
24 | collection (DataCollection): SH Collection from which data will be downloaded.
25 | resolution (float): The resolution at which the data will be downloaded.
26 | evalscript (str): Evalscript used for download.
27 | sh_config (SHConfig): SHconfig object.
28 | ref_path (str): Location of the DataFrame with reference vector data.
29 | ml_aois_path (str): Location of the DataFrame containing ML AOI areas.
30 | save_path (str): Where the resulting EOPatch will be saved.
31 | mosaicking_order (str): Type of maosaicking applied by the SH service. Default to "mostRecent".
32 | ref_feature_name (str): Name of feature holding the vector reference buildings. Default to "reference_v_0_0_1".
33 | ml_aois_feature_name (str): Name of feature holding the vector ML AOIs. Default to "ml_aois".
34 | has_ref_feature_name (str): Name of feature holding the reference mask. Default to "has_ref".
35 | raster_value (int): Value used to rasterize the vector timeless reference feature. Default to 1.
36 |
37 | Returns:
38 | LinearWorkflow: eo-learn LinearWorkflow for downloading and processing data.
39 | """
40 | input_task = SentinelHubEvalscriptTask(
41 | features=[(FeatureType.DATA, "bands"), (FeatureType.MASK, "mask")],
42 | evalscript=evalscript,
43 | data_collection=collection,
44 | resolution=resolution,
45 | config=sh_config,
46 | mosaicking_order=mosaicking_order,
47 | max_threads=3,
48 | )
49 | add_ref = VectorImportTask(
50 | feature=(FeatureType.VECTOR_TIMELESS, ref_feature_name), path=ref_path, config=sh_config, reproject=True
51 | )
52 | add_ml_aois = VectorImportTask(
53 | feature=(FeatureType.VECTOR_TIMELESS, ml_aois_feature_name), path=ml_aois_path, config=sh_config, reproject=True
54 | )
55 | rasterise = VectorToRasterTask(
56 | vector_input=(FeatureType.VECTOR_TIMELESS, ml_aois_feature_name),
57 | raster_feature=(FeatureType.MASK_TIMELESS, has_ref_feature_name),
58 | values=raster_value,
59 | raster_resolution=resolution,
60 | )
61 | save = SaveTask(path=save_path, overwrite_permission=OverwritePermission.OVERWRITE_PATCH, config=sh_config)
62 | return EOWorkflow(workflow_nodes=linearly_connect_tasks(input_task, add_ref, add_ml_aois, rasterise, save))
63 |
--------------------------------------------------------------------------------
/hiector/utils/geometry.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | import geopandas as gpd
4 | import pandas as pd
5 | from shapely.geometry import Polygon
6 |
7 |
8 | def merge_bboxes(
9 | df: gpd.GeoDataFrame,
10 | iou_method: Callable[[Polygon, gpd.GeoSeries], pd.Series],
11 | iou_thr: float = 0.4,
12 | sorting_col: str = "area",
13 | ) -> gpd.GeoDataFrame:
14 | """Merges bounding boxes in DF based on some threshold over score calculated with iou_method.
15 |
16 | Args:
17 | df (gpd.GeoDataFrame): DataFrame with oriented bouding boxes.
18 | iou_method (Callable[[Polygon, gpd.GeoSeries], pd.Series]): IOU-type function to calculate intersection between
19 | bounding boxes.
20 | iou_thr (float, optional): Threshold to determine which bounding boxes to discard. Defaults to .4.
21 | sorting_col (str, optional): Property over which to sort (prioritize) bounding boxes. Defaults to 'area'.
22 |
23 | Returns:
24 | gpd.GeoDataFrame: Filtered dataset of bounding boxes.
25 | """
26 | assert sorting_col in df.columns, f"Sorting column: {sorting_col} is not present in the dataframe."
27 |
28 | # This is needed because joining on dataframes with CRS is significantly (15x) slower than if dataframe doesn't
29 | # have CRS, since we do a join for each bounding boxes and geopandas asserts that CRSes match during each join.
30 | crs = df.crs
31 | df.crs = None
32 | df["merging_idx"] = df.index.values
33 |
34 | remaining = df.sort_values(by=sorting_col, ascending=False)
35 |
36 | # It's cheaper to do spatial index in the beginning over the whole dataframe and consequently check a few more
37 | # candidates than it is to build a new index for each new iteration view.
38 | spatial_idx = df.sindex
39 | final_idxs = []
40 |
41 | while len(remaining):
42 | first = remaining.iloc[0]
43 | candidates = df.iloc[spatial_idx.query(first.geometry)]
44 |
45 | ious = iou_method(first.geometry, candidates.geometry)
46 | toremove = candidates[ious >= iou_thr].merging_idx
47 |
48 | remaining = remaining[~remaining.merging_idx.isin(toremove)]
49 | final_idxs.append(first.merging_idx)
50 |
51 | df.crs = crs
52 | return df[df.merging_idx.isin(final_idxs)]
53 |
54 |
55 | def close_holes(poly: Polygon, thr: float) -> Polygon:
56 | """Close polygon holes that are lower than the threshold.
57 |
58 | Args:
59 | poly (Polygon): Input shapely polygon.
60 | thr (float): Hole area threshold.
61 |
62 | Returns:
63 | Polygon: Shapely polygon with holes below the threshold sized removed.
64 | """
65 | if poly.interiors:
66 | return Polygon(
67 | shell=list(poly.exterior.coords), holes=[hole for hole in poly.interiors if hole.envelope.area > thr]
68 | )
69 | return poly
70 |
71 |
72 | def merge_predictions(predictions: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
73 | """Dissolve overlapping predictions and explode into connected components.
74 |
75 | Args:
76 | predictions (gpd.GeoDataFrame): Dataframe with building predictions.
77 | Returns:
78 | gpd.GeoDataFrame: GeoDataFrame with only Geometry column where each row represents one spot area.
79 | """
80 | predictions["dissolve_col"] = 1
81 | predictions_exploded = predictions.dissolve(by="dissolve_col").explode()
82 | return predictions_exploded[["geometry"]]
83 |
--------------------------------------------------------------------------------
/hiector/utils/grid.py:
--------------------------------------------------------------------------------
1 | from functools import partial
2 | from typing import Optional, Tuple, Union, Callable
3 |
4 | import geopandas as gpd
5 | import pandas as pd
6 | import pyproj
7 | from shapely.geometry import MultiPolygon, Polygon
8 | from shapely.ops import transform
9 |
10 | from eolearn.core import (
11 | EOPatch,
12 | EOWorkflow,
13 | ExtractBandsTask,
14 | FeatureType,
15 | LoadTask,
16 | OverwritePermission,
17 | RemoveFeatureTask,
18 | SaveTask,
19 | get_filesystem,
20 | linearly_connect_tasks,
21 | )
22 | from eolearn.core.utils.fs import join_path
23 | from sentinelhub import CRS, BBox
24 | from sentinelhub.areas import UtmZoneSplitter, BBoxSplitter
25 |
26 | from .geometry import merge_bboxes
27 | from .multiprocess import multiprocess
28 | from ..tasks.cropping import CroppingTask
29 | from ..tasks.preprocessing import (
30 | CreatePolygonBBoxesTask,
31 | DropDuplicatePolygonsTask,
32 | FilterEmptyGeometriesTask,
33 | RemoveInvalidGeometryTask,
34 | ReprojectReferenceTask,
35 | TransformToPixelsCoordTask,
36 | )
37 | from ..tasks.training_data import ExportGeometriesAndLabelsTask, ExportRasterDataTask
38 |
39 | Poly = Union[Polygon, MultiPolygon]
40 |
41 |
42 | def split_zone(geometry: Poly, geometry_crs: CRS, grid_size: int, buffer: float = 0.0) -> gpd.GeoDataFrame:
43 | """Split zone into a UTM aligned grid.
44 |
45 | Args:
46 | geometry (Poly): Area to be split.
47 | geometry_crs (CRS): CRS of the input area.
48 | grid_size (int): Size of the grid.
49 |
50 | Returns:
51 | [gpd.geoDataFrame]: GeoDataFrame where each row represents a grid cell. Doesn't have CRS defined.
52 | """
53 | splitter = UtmZoneSplitter([geometry], geometry_crs, grid_size)
54 | bboxes = gpd.GeoDataFrame(
55 | [{"geometry": bbox.geometry, "bbox_crs": str(bbox.crs)} for bbox in splitter.get_bbox_list(buffer=buffer)]
56 | )
57 | return bboxes
58 |
59 |
60 | def bboxes_to_wgs84(df: gpd.GeoDataFrame, crs_column: str = "bbox_crs") -> gpd.GeoDataFrame:
61 | """Converts bboxes returned from grid splitter to wGS84 and concatenates into single dataframe
62 |
63 | Args:
64 | df (gpd.GeoDataFrame): Input dataframe. Requires column with CRS info.
65 | crs_column (str): column from which CRS of each geometry (row) is retrieved
66 |
67 | Returns:
68 | gpd.GeoDataFrame: Returns dataframe with bboxes from all UTMs in WGS84.
69 | """
70 |
71 | dfs = [
72 | df[df[crs_column] == crs].copy().set_crs(str(crs), inplace=True).to_crs("epsg:4326")
73 | for crs in df[crs_column].unique()
74 | ]
75 | return pd.concat(dfs)
76 |
77 |
78 | def reproject_poly(poly: Poly, in_crs: str, out_crs: str) -> Poly:
79 | """Transforms polygon geometry from input crs to output crs.
80 |
81 | Args:
82 | poly (Poly): Polygon that will be transformed.
83 | in_crs (str): CRS of the polygon before transformation.
84 | out_crs (str): CRS of the polygon after transformation.
85 | Returns:
86 | Poly: Original polygon transformed into a new CRS.
87 | """
88 | in_crs = pyproj.CRS(in_crs)
89 | out_crs = pyproj.CRS(out_crs)
90 |
91 | project = pyproj.Transformer.from_crs(in_crs, out_crs, always_xy=True).transform
92 | return transform(project, poly)
93 |
94 |
95 | def construct_eopatch_name(geometry: Poly, out_crs: Optional[str] = None) -> str:
96 | """Constructs eopatch name from coordinates of the geometry.
97 |
98 | Args:
99 | geometry (Poly): Bounding box geometry (in WGS84)
100 | out_crs (str): If specified, the coordinates will be taken from geometry transformed to this crs.
101 | Returns:
102 | str: Name of the eopatch constructed from geometry coordinates.
103 | """
104 | if out_crs is not None:
105 | geometry = reproject_poly(geometry, "EPSG:4326", out_crs=out_crs)
106 | return f"eopatch-{int(geometry.bounds[0]):d}-{int(geometry.bounds[1]):d}"
107 |
108 |
109 | def get_extent(eopatch: EOPatch) -> Tuple[float, float, float, float]:
110 | """Calculate the extent (bounds) of the patch.
111 |
112 | Args:
113 | eopatch: EOPatch for which the extent is calculated.
114 | Returns:
115 | Tuple[float, float, float, float]: The list of EOPatch bounds (min_x, max_x, min_y, max_y)
116 | """
117 | return eopatch.bbox.min_x, eopatch.bbox.max_x, eopatch.bbox.min_y, eopatch.bbox.max_y
118 |
119 |
120 | def get_features(config):
121 | return {
122 | "bands": (FeatureType.DATA, config["bands_feature"]),
123 | "data_mask": (FeatureType.MASK, config["data_mask_feature"]),
124 | "reference": (FeatureType.VECTOR_TIMELESS, config["reference_feature"]),
125 | "cloud_mask": (FeatureType.MASK, config["cloud_mask_feature"]) if config.get("cloud_mask_feature") else None,
126 | "valid_reference_mask": (FeatureType.MASK_TIMELESS, config["valid_reference_mask_feature"])
127 | if config.get("valid_reference_mask_feature")
128 | else None,
129 | "grid": [
130 | (FeatureType.VECTOR_TIMELESS, f"{config['cropped_grid_feature']}_{size}") for size in config["scale_sizes"]
131 | ],
132 | "intersection": [(FeatureType.VECTOR_TIMELESS, f"BBOXES_IN_GRID_{size}") for size in config["scale_sizes"]],
133 | "data_stack": [(FeatureType.META_INFO, f"DATA_STACK_{size}") for size in config["scale_sizes"]]
134 | # Because the shape of arrays will be (n, h, w, b) and n is not the number of timestamps. These features
135 | # don't have to be saved, hence we can just put them under META_INFO
136 | }
137 |
138 |
139 | def preprocess_workflow(config):
140 |
141 | features = get_features(config)
142 |
143 | reproject = ReprojectReferenceTask(reference_feature=features["reference"])
144 | filter_bands = ExtractBandsTask(features["bands"], features["bands"], bands=config["bands"])
145 | drop_duplicates = DropDuplicatePolygonsTask(features["reference"], features["reference"], column="geometry")
146 | create_bboxes = CreatePolygonBBoxesTask(features["reference"], features["reference"])
147 | filter_empty_geometries = FilterEmptyGeometriesTask(
148 | features["reference"], features["reference"], column=config["bbox_type"]
149 | )
150 | remove_invalid = RemoveInvalidGeometryTask(features["reference"], features["reference"])
151 | transform_coordinates = TransformToPixelsCoordTask(
152 | features["reference"],
153 | features["reference"],
154 | bbox_column=config["bbox_type"],
155 | resolution=config["resolution"],
156 | round_decimals=1,
157 | )
158 | cropping_tasks = []
159 | for size, grid_feature, intersection_feature, data_stack_feature in zip(
160 | config["scale_sizes"], features["grid"], features["intersection"], features["data_stack"]
161 | ):
162 | cropping = CroppingTask(
163 | raster_feature=features["bands"],
164 | data_mask_feature=features["data_mask"],
165 | cloud_mask_feature=features["cloud_mask"],
166 | valid_reference_mask_feature=features["valid_reference_mask"],
167 | take_closest_time_frame=config.get("take_closest_time_frame"),
168 | no_data_value=config["no_data_value"],
169 | vector_feature=features["reference"],
170 | grid_feature=grid_feature,
171 | intersection_feature=intersection_feature,
172 | data_stack_feature=data_stack_feature,
173 | size=size,
174 | overlap=config["overlap"],
175 | resolution=config["resolution"],
176 | valid_threshold=config["valid_thr"],
177 | )
178 | cropping_tasks.append(cropping)
179 | remove_bands_feature = RemoveFeatureTask([features["bands"], features["data_mask"]])
180 |
181 | workflow_nodes = linearly_connect_tasks(
182 | reproject,
183 | filter_bands,
184 | drop_duplicates,
185 | remove_invalid,
186 | create_bboxes,
187 | filter_empty_geometries,
188 | transform_coordinates,
189 | *cropping_tasks,
190 | remove_bands_feature,
191 | )
192 |
193 | return EOWorkflow(workflow_nodes=workflow_nodes)
194 |
195 |
196 | def training_data_workflow(config, sh_config=None, filesystem=None):
197 | """Creates an EOWorkflow for creating training data"""
198 | features = get_features(config)
199 | features_to_load = [
200 | features["bands"],
201 | features["reference"],
202 | features["data_mask"],
203 | FeatureType.BBOX,
204 | FeatureType.TIMESTAMP,
205 | ]
206 | if features["cloud_mask"]:
207 | features_to_load.append(features["cloud_mask"])
208 | if features["valid_reference_mask"]:
209 | features_to_load.append(features["valid_reference_mask"])
210 | load = LoadTask(path=config["data_dir"], features=features_to_load, config=sh_config, filesystem=filesystem)
211 | save_vector = SaveTask(
212 | path=config["tmp_dir"],
213 | features=[features["reference"], *features["grid"]],
214 | overwrite_permission=OverwritePermission.OVERWRITE_FEATURES,
215 | config=sh_config,
216 | )
217 |
218 | preprocess_tasks = [node.task for node in preprocess_workflow(config).get_nodes()]
219 |
220 | output_filesystem = get_filesystem(config["out_dir"], config=sh_config)
221 | for folder in ["images", "labels"]:
222 | output_filesystem.makedirs(folder, recreate=True)
223 |
224 | export_tasks = []
225 | for grid_feature, intersection_feature, data_stack_feature in zip(
226 | features["grid"], features["intersection"], features["data_stack"]
227 | ):
228 | export_data = ExportRasterDataTask(
229 | raster_feature=data_stack_feature,
230 | grid_feature=grid_feature,
231 | path=join_path(config["out_dir"], "images"),
232 | config=sh_config,
233 | )
234 | export_labels = ExportGeometriesAndLabelsTask(
235 | reference_feature=intersection_feature, path=join_path(config["out_dir"], "labels"), config=sh_config
236 | )
237 | export_tasks.extend([export_data, export_labels])
238 |
239 | workflow_nodes = linearly_connect_tasks(
240 | load,
241 | *preprocess_tasks,
242 | save_vector,
243 | *export_tasks,
244 | )
245 |
246 | return EOWorkflow(workflow_nodes=workflow_nodes)
247 |
248 |
249 | def merge_multiprocess(
250 | gdf: gpd.GeoDataFrame,
251 | iou_method: Callable[[Polygon, gpd.GeoSeries], pd.Series],
252 | split_size: Tuple[int, int] = (100, 100),
253 | iou_thr: float = 0.4,
254 | sorting_col: str = "area",
255 | max_workers: int = 5,
256 | ) -> gpd.GeoDataFrame:
257 |
258 | spatial_idx = gdf.sindex
259 | bounds = BBox(spatial_idx.bounds, crs=CRS(gdf.crs.to_epsg()))
260 |
261 | splitter = BBoxSplitter([bounds.geometry], bounds.crs, split_size)
262 |
263 | split_bboxes = []
264 | done_ilocs = set()
265 | for bb in splitter.bbox_list:
266 | ilocs = set(spatial_idx.query(bb.geometry))
267 | a = gdf.iloc[list(ilocs.difference(done_ilocs))].copy()
268 | done_ilocs.update(ilocs)
269 | if not a.empty:
270 | split_bboxes.append(a.reset_index(drop=True))
271 | del gdf
272 |
273 | merger = partial(merge_bboxes, iou_method=iou_method, iou_thr=iou_thr, sorting_col=sorting_col)
274 |
275 | results = multiprocess(merger, split_bboxes, total=len(split_bboxes), max_workers=max_workers)
276 |
277 | return pd.concat(results)
278 |
--------------------------------------------------------------------------------
/hiector/utils/multiprocess.py:
--------------------------------------------------------------------------------
1 | from concurrent.futures import ProcessPoolExecutor
2 | from typing import Any, Callable, Iterable, List, Optional
3 |
4 | from tqdm.auto import tqdm
5 |
6 |
7 | def multiprocess(
8 | process_fun: Callable, arguments: Iterable[Any], total: Optional[int] = None, max_workers: int = 4
9 | ) -> List[Any]:
10 | """
11 | Executes multiprocessing with tqdm.
12 | Parameters
13 | ----------
14 | process_fun: A function that processes a single item.
15 | arguments: Arguments with which te function is called.
16 | total: Number of iterations to run (for cases of iterators)
17 | max_workers: Max workers for the process pool executor.
18 |
19 | Returns A list of results.
20 | -------
21 |
22 |
23 | """
24 | with ProcessPoolExecutor(max_workers=max_workers) as executor:
25 | results = list(tqdm(executor.map(process_fun, arguments), total=total))
26 | return results
27 |
--------------------------------------------------------------------------------
/hiector/utils/preprocessing.py:
--------------------------------------------------------------------------------
1 | """
2 | Module with utilities for preprocessing data that will be used for training
3 | """
4 | from typing import Tuple
5 |
6 | from geopandas import GeoDataFrame
7 | from shapely.geometry import LineString, Polygon
8 |
9 |
10 | def calculate_hbb_and_obb(dataframe: GeoDataFrame, as_wkt: bool = True) -> GeoDataFrame:
11 | """Calculates horizontal and oriented bounding boxes for geometries in dataframe.
12 |
13 | :param dataframe: A dataframe with geometries
14 | :param as_wkt: If True geometries will be added as WKT strings, otherwise they will be added as shapely objects.
15 | :return: A dataframe with two added columns, 'hbb' and 'obb' for horizontal and oriented bounding boxes.
16 | """
17 | dataframe["hbb"] = dataframe.geometry.envelope
18 | dataframe["obb"] = dataframe.geometry.apply(lambda x: x.minimum_rotated_rectangle)
19 |
20 | if as_wkt:
21 | dataframe["hbb"] = dataframe["hbb"].apply(lambda x: x.wkt)
22 | dataframe["obb"] = dataframe["obb"].apply(lambda x: x.wkt)
23 |
24 | return dataframe
25 |
26 |
27 | def round_point_coords(x: float, y: float, decimals: int) -> Tuple[float, float]:
28 | """Rounds coordinates of a point"""
29 | return round(x, decimals), round(y, decimals)
30 |
31 |
32 | def calculate_bbox_ratio(bbox_polygon: Polygon) -> float:
33 | """Calculate a ratio between larger and smaller sides of a bounding box polygon"""
34 | size1 = LineString(list(bbox_polygon.exterior.coords)[:2]).length
35 | size2 = LineString(list(bbox_polygon.exterior.coords)[1:3]).length
36 |
37 | small_size = min(size1, size2)
38 | large_size = max(size1, size2)
39 |
40 | area = bbox_polygon.area
41 | relative_error = abs(small_size * large_size - area) / area
42 | if relative_error > 1e-6:
43 | raise ValueError(f"It seems that this polygon is not a bounding box: {bbox_polygon.wkt}")
44 |
45 | ratio = large_size / small_size
46 | return ratio
47 |
--------------------------------------------------------------------------------
/hiector/utils/training_data.py:
--------------------------------------------------------------------------------
1 | """
2 | Utilities for working with training data
3 | """
4 | from typing import List, Optional
5 |
6 | import geopandas as gpd
7 | import numpy as np
8 |
9 |
10 | def filter_dataframe(
11 | gdf: gpd.GeoDataFrame,
12 | query: Optional[str] = None,
13 | frac: float = 1.0,
14 | exclude_eops: Optional[List[str]] = None,
15 | seed: int = 42,
16 | ):
17 | if query is not None:
18 | gdf = gdf.query(query)
19 | if exclude_eops is not None:
20 | gdf = gdf[~gdf.EOPATCH_NAME.isin(exclude_eops)]
21 | return gdf.sample(frac=frac, random_state=seed)
22 |
23 |
24 | def train_test_val_split(
25 | gdf: gpd.GeoDataFrame, fraction_train: float = 0.6, fraction_test: float = 0.2, fraction_val: float = 0.2
26 | ):
27 | assert (fraction_train + fraction_val + fraction_test) == 1, "Fractions of train, test, val must sum up to 1."
28 |
29 | eopatches = gdf.EOPATCH_NAME.unique()
30 | n_eops = len(eopatches)
31 | n_train, n_val = int(n_eops * fraction_train), int(n_eops * fraction_val)
32 | n_test = n_eops - n_train - n_val
33 |
34 | train_eops = np.random.choice(eopatches, n_train, replace=False)
35 | test_eops = np.random.choice(list(set(eopatches) - set(train_eops)), n_test, replace=False)
36 | val_eops = np.random.choice(list(set(eopatches) - set(train_eops) - set(test_eops)), n_val, replace=False)
37 |
38 | assert len(train_eops) + len(test_eops) + len(val_eops) == n_eops
39 | assert set(train_eops).union(set(test_eops)).union(set(val_eops)) == set(eopatches)
40 |
41 | def split(x, train_set, test_set, val_set):
42 | if x in train_set:
43 | return "train"
44 | if x in test_set:
45 | return "test"
46 | if x in val_set:
47 | return "val"
48 | raise ValueError(f"Could not find a subset for eopatch: {x}")
49 |
50 | gdf["SUBSET"] = gdf.EOPATCH_NAME.apply(lambda x: split(x, train_eops, test_eops, val_eops))
51 | return gdf
52 |
--------------------------------------------------------------------------------
/hiector/utils/vector.py:
--------------------------------------------------------------------------------
1 | """
2 | Vector utilities
3 | """
4 | import os
5 | from typing import List, Optional
6 |
7 | import fiona
8 | from shapely.wkt import loads
9 |
10 |
11 | def export_geopackage(
12 | eopatch,
13 | geopackage_path,
14 | feature,
15 | geometry_column: str = "geometry",
16 | columns: Optional[List[str]] = None,
17 | ):
18 | """A utility function for exporting
19 |
20 | :param eopatch: EOPatch to save
21 | :param geopackage_path: Output path where Geopackage will be written.
22 | :param feature: A vector feature from EOPatches that will be exported to Geopackage
23 | :param geometry_column: Name of a column that will be taken as a geometry column.
24 | :param columns: Columns from dataframe that will be written into Geopackage besides geometry column. By default
25 | all columns will be taken.
26 |
27 | Note: in the future it could be implemented as an eo-learn task, the main problem is that writing has to be
28 | consecutive.
29 | """
30 | existing_layers = fiona.listlayers(geopackage_path) if os.path.exists(geopackage_path) else []
31 |
32 | gdf = eopatch[feature]
33 | layer_name = f"{feature[1]}_{gdf.crs.to_epsg()}"
34 | mode = "a" if layer_name in existing_layers else "w"
35 |
36 | if not len(gdf.index):
37 | return
38 |
39 | # Only one geometry column can be saved to a Geopackage
40 | if isinstance(gdf[geometry_column].iloc[0], str):
41 | gdf[geometry_column] = gdf[geometry_column].apply(loads)
42 |
43 | gdf = gdf.set_geometry(geometry_column)
44 | if columns is not None:
45 | gdf = gdf.filter(columns + [geometry_column], axis=1)
46 |
47 | gdf.to_file(geopackage_path, mode=mode, layer=layer_name, driver="GPKG", encoding="utf-8")
48 |
--------------------------------------------------------------------------------
/infrastructure/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM rayproject/ray:latest-cpu
2 |
3 | LABEL maintainer="Sinergise EO research team "
4 | ARG S3_AWS_ACCESS_KEY
5 | ARG S3_AWS_SECRET_KEY
6 | ARG SH_INSTANCE_ID
7 | ARG SH_CLIENT_ID
8 | ARG SH_CLIENT_SECRET
9 | ARG SENTINELHUB_BRANCH
10 | ARG EOLEARN_BRANCH
11 | ARG LCMS_BRANCH
12 | ARG HIECTOR_BRANCH
13 |
14 | RUN sudo apt-get update && sudo apt-get install -y software-properties-common
15 | RUN sudo add-apt-repository ppa:ubuntugis/ubuntugis-unstable -y
16 | RUN sudo apt-get update && sudo apt-get install -y \
17 | libspatialindex-dev gcc libgeos-c1v5 libgeos-dev curl vim \
18 | s3fs cmake graphviz proj-bin libproj-dev gdal-bin libgdal-dev build-essential python3-opencv \
19 | && sudo apt-get clean && sudo apt-get autoremove -y && sudo rm -rf /var/lib/apt/lists/*
20 |
21 | RUN pip install pip --upgrade --no-cache-dir
22 | RUN pip install awscli wandb boto3 ipdb --upgrade --no-cache-dir
23 | RUN pip install rasterio fiona pyproj rtree --no-cache-dir
24 | RUN pip install gdal==$(gdalinfo --version | awk -F "," '{print $1}' | awk '{print $2}') --no-cache-dir
25 |
26 | RUN conda clean --all --force-pkgs-dirs -y
27 |
28 | RUN mkdir packages
29 | WORKDIR /home/ray/packages
30 |
31 | RUN git clone --depth 1 -b ${SENTINELHUB_BRANCH} https://github.com/sentinel-hub/sentinelhub-py.git
32 | RUN pip install -e ./sentinelhub-py --no-cache-dir
33 |
34 | RUN git clone --depth 1 -b ${EOLEARN_BRANCH} https://github.com/sentinel-hub/eo-learn.git
35 | RUN pip install \
36 | -e ./eo-learn/core \
37 | -e ./eo-learn/coregistration \
38 | -e ./eo-learn/features \
39 | -e ./eo-learn/geometry \
40 | -e ./eo-learn/io \
41 | -e ./eo-learn/mask \
42 | -e ./eo-learn/ml_tools \
43 | -e ./eo-learn/visualization --no-cache-dir
44 |
45 | RUN pip install ray[default]
46 |
47 | RUN git clone --depth 1 -b ${HIECTOR_BRANCH} https://github.com/sentinel-hub/hiector.git
48 | RUN pip install -e ./hiector --no-cache-dir
49 |
50 | WORKDIR /home/ray/packages/query-planet-ccn3/hiector/ssrdd/utils/box/ext/rbbox_overlap_cpu
51 | RUN python setup.py build_ext --inplace
52 |
53 | WORKDIR /home/ray/
54 | RUN sentinelhub.config --sh_client_id ${SH_CLIENT_ID} --sh_client_secret ${SH_CLIENT_SECRET} --instance_id ${SH_INSTANCE_ID}
55 | RUN aws --profile hiector configure set aws_access_key_id ${S3_AWS_ACCESS_KEY}
56 | RUN aws --profile hiector configure set aws_secret_access_key ${S3_AWS_SECRET_KEY}
57 | RUN aws --profile hiector configure set region eu-central-1
58 |
59 | RUN mkdir data
60 | RUN cat .aws/credentials | grep -m 2 access | awk '{print $3}' | xargs | sed 's/ /:/g' > ~/.passwd-s3fs
61 | RUN chmod 600 ~/.passwd-s3fs
62 |
--------------------------------------------------------------------------------
/infrastructure/cluster.yaml:
--------------------------------------------------------------------------------
1 | # A configuration of ray cluster for Query Planet CCN3 project
2 | # For info about parameters check https://docs.ray.io/en/latest/cluster/config.html#full-configuration
3 |
4 | cluster_name: hiector-cluster
5 |
6 | max_workers: 20 # Max number of worker instances
7 | upscaling_speed: 1.0
8 | idle_timeout_minutes: 5
9 |
10 | docker:
11 | image: ".dkr.ecr.eu-central-1.amazonaws.com/" # Edit this!
12 | container_name: "hiector_container"
13 | pull_before_run: True
14 | run_options:
15 | - --privileged # Because of s3fs-fuse
16 |
17 | provider:
18 | type: aws
19 | region: eu-central-1
20 | availability_zone: eu-central-1a,eu-central-1b,eu-central-1c
21 | cache_stopped_nodes: False # Change for terminating instances
22 |
23 | auth:
24 | ssh_user: ubuntu
25 |
26 | available_node_types:
27 | ray.head:
28 | min_workers: 0
29 | max_workers: 0
30 | node_config:
31 | InstanceType: m5.2xlarge
32 | ImageId: ami- # Edit this!
33 | BlockDeviceMappings:
34 | - DeviceName: /dev/sda1
35 | Ebs:
36 | VolumeSize: 20
37 | resources: {"CPU": 1}
38 | ray.worker:
39 | min_workers: 0
40 | max_workers: 20 # Max number of workers of this type
41 | node_config:
42 | InstanceType: m5.xlarge
43 | ImageId: ami- # Edit this!
44 | InstanceMarketOptions:
45 | MarketType: spot
46 | BlockDeviceMappings:
47 | - DeviceName: /dev/sda1
48 | Ebs:
49 | VolumeSize: 20
50 | # resources: {"CPU": 1}
51 |
52 | head_node_type: ray.head
53 |
54 | file_mounts: {}
55 | cluster_synced_files: []
56 | file_mounts_sync_continuously: False
57 | rsync_exclude:
58 | - "**/.git"
59 | - "**/.git/**"
60 | rsync_filter:
61 | - ".gitignore"
62 |
63 | initialization_commands:
64 | - aws ecr get-login-password | docker login --username AWS --password-stdin .dkr.ecr.eu-central-1.amazonaws.com
65 |
66 | setup_commands:
67 | - s3fs ~/data -o umask=0000 | true
68 |
69 | head_setup_commands:
70 | - pip install jupyter
71 |
72 | worker_setup_commands: []
73 |
74 | head_start_ray_commands:
75 | - ray stop
76 | - ulimit -n 65536; ray start --head --port=6379 --object-manager-port=8076 --autoscaling-config=~/ray_bootstrap_config.yaml
77 |
78 | worker_start_ray_commands:
79 | - ray stop
80 | - ulimit -n 65536; ray start --address=$RAY_HEAD_IP:6379 --object-manager-port=8076
81 |
82 | head_node: {}
83 | worker_nodes: {}
84 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.black]
2 | line-length = 120
3 | preview = true
4 |
5 | [tool.isort]
6 | profile = "black"
7 | known_first_party = ["sentinelhub", "eolearn"]
8 | known_absolute = "hiector"
9 | sections = ["FUTURE","STDLIB","THIRDPARTY","FIRSTPARTY","ABSOLUTE","LOCALFOLDER"]
10 | line_length = 120
11 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | pytest>=4.0.0
2 | pytest-cov
3 | codecov
4 | pylint
5 | black
6 | isort
7 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | tqdm
2 | numpy~=1.20.3
3 | pillow~=8.3.1
4 | cython~=0.29.22
5 | opencv-python~=4.5.3.56
6 | matplotlib~=3.2.2
7 | shapely~=1.8.1
8 | geopandas~=0.10.2
9 | pandas~=1.4.1
10 | pyproj~=3.3.0
11 | setuptools~=47.3.1
12 | boto3~=1.14.7
13 | torch>=1.7.1
14 | torchvision>=0.8.2
15 | tensorboard>=2.2
16 | wandb>=0.12.0
17 | sentinelhub~=3.4.4
18 | eo-learn-core~=1.0.0
19 | eo-learn-io~=1.0.0
20 | eo-learn-geometry~=1.0.0
21 | eo-learn-visualization~=1.0.0
22 |
--------------------------------------------------------------------------------
/scripts/compute_ap.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import json
3 | import logging
4 | import sys
5 | from itertools import product
6 |
7 | import fiona
8 | import geopandas as gpd
9 | import numpy as np
10 | import pandas as pd
11 | from tqdm.auto import tqdm as tqdm
12 |
13 | from sentinelhub import CRS
14 |
15 | from hiector.utils.aws_utils import LocalFile, get_filesystem
16 | from hiector.utils.metrics import get_ap, get_bbox
17 | from hiector.utils.multiprocess import multiprocess
18 |
19 | stdout_handler = logging.StreamHandler(sys.stdout)
20 | handlers = [stdout_handler]
21 | logging.basicConfig(
22 | level=logging.INFO, format="[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s", handlers=handlers
23 | )
24 | LOGGER = logging.getLogger(__name__)
25 |
26 |
27 | parser = argparse.ArgumentParser(description="Compute mAP on predicted OBBs.\n")
28 |
29 | parser.add_argument("--config", type=str, help="Path to config file with execution parameters", required=True)
30 |
31 | args = parser.parse_args()
32 |
33 |
34 | RANGE = np.arange(0.0, 0.7, 0.05)
35 | REF_CRS = "epsg:4326"
36 |
37 |
38 | def get_ap_unpack(args):
39 | return get_ap(*args)
40 |
41 |
42 | def compute_ap(config):
43 |
44 | filesystem = get_filesystem(bucket_name=config["s3_bucket_name"], profile_name=config["s3_profile_name"])
45 |
46 | LOGGER.info(f"Read predictions file: {config['predictions_filename']}")
47 | predictions = []
48 | with LocalFile(config["predictions_filename"], mode="r", filesystem=filesystem) as f:
49 | crss = fiona.listlayers(f.path)
50 | LOGGER.info(f"Reading and concatenating predictions: {crss}")
51 | for crs in crss:
52 | predictions.append(gpd.read_file(f.path, layer=crs).to_crs(crs=CRS(REF_CRS).pyproj_crs()))
53 |
54 | predictions = gpd.GeoDataFrame(pd.concat(predictions, axis=0, ignore_index=True))
55 | predictions.set_crs(crs=CRS(REF_CRS).pyproj_crs(), inplace=True)
56 |
57 | eopatches = predictions.eopatch.unique()
58 | to_keep = config["eopatch_names"]
59 | if to_keep is not None:
60 | eopatches = [eopatch for eopatch in eopatches if eopatch in to_keep]
61 |
62 | predictions = predictions[predictions["eopatch"].isin(eopatches)]
63 |
64 | LOGGER.info(f"Retrieving bounding box information")
65 | eop_data = {
66 | eopatch: get_bbox(eopatch, config["eopatches_dir"], filesystem, config["resolution"])
67 | for eopatch in tqdm(eopatches)
68 | }
69 |
70 | LOGGER.info(f"Read reference file: {config['reference_filename']}")
71 | with filesystem.openbin(config["reference_filename"], "rb") as handle_file:
72 | gt = gpd.read_file(handle_file)
73 |
74 | gt = gt[gt.is_valid]
75 | gt = gt[~gt.is_empty]
76 | gt.geometry = gt.geometry.buffer(0)
77 | gt.to_crs(CRS(REF_CRS).pyproj_crs(), inplace=True)
78 | assert gt.crs == predictions.crs, "CRSs don't match"
79 |
80 | if config["ml_aois_filename"] is not None:
81 | LOGGER.info(f"Read ML AOIs file: {config['ml_aois_filename']}")
82 | with filesystem.openbin(config["ml_aois_filename"], "rb") as handle_file:
83 | ml_aois = gpd.read_file(handle_file)
84 | ml_aois.to_crs(CRS(REF_CRS).pyproj_crs(), inplace=True)
85 | predictions = gpd.sjoin(predictions, ml_aois)
86 | gt = gpd.sjoin(gt, ml_aois)
87 |
88 | eopatches_gdf = gpd.GeoDataFrame(data={"eopatch": eopatches}, geometry=None, crs=CRS(REF_CRS).pyproj_crs())
89 | eopatches_gdf["geometry"] = eopatches_gdf.eopatch.apply(
90 | lambda e: eop_data[e]["bbox"].transform_bounds(CRS(REF_CRS)).geometry
91 | )
92 |
93 | gt = gt[gt.intersects(eopatches_gdf.geometry.unary_union)].copy()
94 | gt_obb = gt.copy()
95 | gt_obb.geometry = gt.geometry.apply(lambda g: g.minimum_rotated_rectangle)
96 |
97 | LOGGER.info(f"Computing APs for different IoUs and probas..")
98 | iou_thr, proba_thr = config["iou_thr"], config["proba_thr"]
99 | thresholds = list(product(RANGE, RANGE))
100 | if iou_thr is not None and proba_thr is not None:
101 | thresholds = [(proba_thr, iou_thr)]
102 |
103 | arguments = [(gt_obb, predictions, thrs[0], thrs[1]) for thrs in thresholds]
104 | results = multiprocess(get_ap_unpack, arguments, len(arguments), max_workers=config["max_workers"])
105 | aps = [dict(AP=result[0], PROBA=thrs[0], IOU=thrs[1]) for result, thrs in zip(results, thresholds)]
106 |
107 | LOGGER.info(f"Saving AP dataframe to: {config['aps_filename']}")
108 | with filesystem.open(config["aps_filename"], "w") as handle_file:
109 | pd.DataFrame(aps).to_csv(handle_file, index=False)
110 |
111 |
112 | if __name__ == "__main__":
113 | # read config parameter file
114 | LOGGER.info(f"Reading configuration from {args.config}")
115 | with open(args.config, "r") as jfile:
116 | cfg_dict = json.load(jfile)
117 |
118 | cfg = cfg_dict["compute-ap"]
119 |
120 | compute_ap(cfg)
121 |
--------------------------------------------------------------------------------
/scripts/compute_normalization_stats.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import json
3 | import logging
4 | import os
5 | import sys
6 | from functools import partial
7 |
8 | import fiona
9 | import geopandas as gpd
10 | import numpy as np
11 | import pandas as pd
12 | from tqdm.auto import tqdm
13 |
14 | from hiector.utils.aws_utils import LocalFile, get_filesystem
15 |
16 | stdout_handler = logging.StreamHandler(sys.stdout)
17 | handlers = [stdout_handler]
18 | logging.basicConfig(
19 | level=logging.INFO, format="[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s", handlers=handlers
20 | )
21 | LOGGER = logging.getLogger(__name__)
22 | parser = argparse.ArgumentParser(description="Compute normalization factors")
23 | parser.add_argument("--config", type=str, help="Path to config file with execution parameters", required=True)
24 | args = parser.parse_args()
25 |
26 | statistic_mapping = {
27 | "mean": np.mean,
28 | "median": np.median,
29 | "min": np.min,
30 | "max": np.max,
31 | "perc1": partial(np.percentile, q=1),
32 | "perc5": partial(np.percentile, q=5),
33 | "perc95": partial(np.percentile, q=95),
34 | "perc99": partial(np.percentile, q=99),
35 | }
36 |
37 |
38 | def compute_norm_stats(config):
39 | filesystem = get_filesystem(config["bucket_name"], config["aws_profile"])
40 | LOGGER.info("Opening the file descriptor the the samples file.")
41 | with LocalFile(config["samples_file"], mode="r", filesystem=filesystem) as f:
42 | layers = fiona.listlayers(f.path)
43 | layers_to_read = [x for x in layers if int(x.split("_")[1]) in config["scales"]]
44 | LOGGER.info(f"Reading and concatenating layers: {layers_to_read}")
45 | gdf = pd.concat([gpd.read_file(f.path, layer=layer) for layer in layers_to_read])
46 |
47 | if "query" in config:
48 | gdf = gdf.query(config["query"])
49 |
50 | gdf = gdf.sample(frac=config["fraction"], replace=False)
51 | sampled = []
52 | LOGGER.info("Sampling images...")
53 | for image_name in tqdm(gdf.NAME.values):
54 | imgpath = os.path.join(config["data_dir"], "images", f"{image_name}.npy")
55 | imgs = np.load(filesystem.openbin(imgpath, "rb"))
56 | sampled.append(imgs[np.newaxis, ...])
57 | sampled = np.concatenate(sampled)
58 | rows = []
59 | LOGGER.info("Calculating statistics...")
60 | for (
61 | statistic_name,
62 | statistic_f,
63 | ) in statistic_mapping.items():
64 | rows.append(
65 | {
66 | "modality": config["modality"],
67 | "statistic": statistic_name,
68 | "B": statistic_f(sampled[..., 0]),
69 | "G": statistic_f(sampled[..., 1]),
70 | "R": statistic_f(sampled[..., 2]),
71 | "N": statistic_f(sampled[..., 3]),
72 | }
73 | )
74 | rows = pd.DataFrame(rows)
75 | output_file = config["output_file"]
76 | with filesystem.open(output_file, "w") as f:
77 | LOGGER.info(f"Saving to file {output_file}..")
78 | rows.to_csv(f)
79 |
80 |
81 | if __name__ == "__main__":
82 | LOGGER.info(f"Reading configuration from {args.config}")
83 | with open(args.config, "r") as jfile:
84 | cfg_dict = json.load(jfile)
85 | cfg = cfg_dict["compute_norm_stats"]
86 | compute_norm_stats(cfg)
87 |
--------------------------------------------------------------------------------
/scripts/prepare_spot_areas.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import logging
3 | import sys
4 | from typing import Optional
5 |
6 | import geopandas as gpd
7 |
8 | from hiector.utils.geometry import close_holes, merge_predictions
9 |
10 | stdout_handler = logging.StreamHandler(sys.stdout)
11 | handlers = [stdout_handler]
12 | logging.basicConfig(
13 | level=logging.INFO, format="[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s", handlers=handlers
14 | )
15 | LOGGER = logging.getLogger(__name__)
16 |
17 | parser = argparse.ArgumentParser(description="Convert DOTA-style detections to geo-spatial dataframe.")
18 |
19 | parser.add_argument("--predictions_file", type=str, help="Path to vector predictions file. ", required=True)
20 | parser.add_argument(
21 | "--pseudoproba_thr",
22 | type=float,
23 | help="The pseudoprobability threshold. Predictions with pseudoprobability below this value will be discarded.",
24 | required=True,
25 | )
26 | parser.add_argument(
27 | "--interior_thr",
28 | type=float,
29 | help="The threshold on the size of the interior hole. Holes with are below this size will be closed.",
30 | required=True,
31 | )
32 | parser.add_argument(
33 | "--simplify_tolerance", type=float, help="The tolerance for the simplify operation.", required=False
34 | )
35 | parser.add_argument("--outfile", type=str, help="The path to the output GeoPackage file.", required=True)
36 | args = parser.parse_args()
37 |
38 |
39 | def main(
40 | predictions_file: str,
41 | pseudoproba_thr: float,
42 | interior_thr: float,
43 | outfile: str,
44 | simplify_tolerance: Optional[float],
45 | ):
46 | predictions = gpd.read_file(predictions_file)
47 | predictions = predictions[predictions.pseudo_probability > pseudoproba_thr]
48 | predictions_merged = merge_predictions(predictions)
49 | predictions_merged.geometry = predictions_merged.geometry.apply(lambda x: close_holes(x, interior_thr))
50 |
51 | if simplify_tolerance:
52 | predictions_merged.geometry = predictions_merged.geometry.simplify(tolerance=simplify_tolerance)
53 |
54 | predictions_merged.to_file(outfile, driver="GPKG")
55 |
56 |
57 | if __name__ == "__main__":
58 | main(**vars(args))
59 |
--------------------------------------------------------------------------------
/scripts/prepare_training_data.py:
--------------------------------------------------------------------------------
1 | """
2 | Prepare training data by processing EOPatches
3 | """
4 | import argparse
5 | import json
6 | import logging
7 | import sys
8 |
9 | import fs
10 | import geopandas as gpd
11 | import ray
12 | from tqdm.auto import tqdm
13 |
14 | from eolearn.core import EOExecutor, EOPatch, FeatureType, LoadTask, SaveTask, get_filesystem
15 | from eolearn.core.extra.ray import RayExecutor
16 | from eolearn.core.utils.fs import get_aws_credentials, join_path
17 | from sentinelhub import SHConfig
18 |
19 | from hiector.tasks.cropping import CroppingTask
20 | from hiector.utils.aws_utils import LocalFile
21 | from hiector.utils.grid import training_data_workflow
22 | from hiector.utils.vector import export_geopackage
23 |
24 | stdout_handler = logging.StreamHandler(sys.stdout)
25 | handlers = [stdout_handler]
26 | logging.basicConfig(
27 | level=logging.INFO, format="[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s", handlers=handlers
28 | )
29 | LOGGER = logging.getLogger(__name__)
30 |
31 | parser = argparse.ArgumentParser(description="Process EOPatches and prepare data for training/testing.\n")
32 | parser.add_argument("--config", type=str, help="Path to config file with execution parameters", required=True)
33 | args = parser.parse_args()
34 |
35 |
36 | def get_execution_arguments(workflow, eopatch_names):
37 | """Prepares execution parameters for an EOWorkflow"""
38 | exec_args = []
39 | nodes = workflow.get_nodes()
40 | for eopatch_name in eopatch_names:
41 | single_exec_dict = {}
42 | for node in nodes:
43 | if isinstance(node.task, (SaveTask, LoadTask)):
44 | single_exec_dict[node] = dict(eopatch_folder=eopatch_name)
45 | if isinstance(node.task, CroppingTask):
46 | single_exec_dict[node] = dict(eopatch_name=eopatch_name)
47 | exec_args.append(single_exec_dict)
48 |
49 | return exec_args
50 |
51 |
52 | def run_execution(workflow, exec_args, eopatch_names, config):
53 |
54 | """Runs EOWorkflow execution"""
55 | if config["use_ray"]:
56 | executor_cls = RayExecutor
57 | run_args = dict()
58 | else:
59 | executor_cls = EOExecutor
60 | run_args = dict(workers=config["workers"])
61 | executor = executor_cls(
62 | workflow,
63 | exec_args,
64 | save_logs=False, # TODO: logs are also being sent to stout
65 | logs_folder=config["logs_dir"],
66 | execution_names=eopatch_names,
67 | )
68 | executor.run(**run_args)
69 | executor.make_report()
70 |
71 | successful = executor.get_successful_executions()
72 | failed = executor.get_failed_executions()
73 | LOGGER.info(
74 | "EOExecution finished with %d / %d success rate",
75 | len(successful),
76 | len(successful) + len(failed),
77 | )
78 | return successful, failed
79 |
80 |
81 | def export_grids(config, eopatch_names, sh_config):
82 | """Exports Geopackages with grids of EOPatches and grids of training patchlets"""
83 | filename_ref = f"buildings-{config['bbox_type']}.gpkg"
84 | filename_grid = "-".join(
85 | map(str, ["grid", config["bbox_type"], *config["scale_sizes"], config["overlap"], config["valid_thr"]])
86 | )
87 | ref_geopackage_path = join_path(config["out_dir"], filename_ref)
88 | grid_geopackage_path = join_path(config["out_dir"], f"{filename_grid}.gpkg")
89 |
90 | input_filesystem = get_filesystem(config["tmp_dir"], config=sh_config)
91 |
92 | grid_features = [
93 | (FeatureType.VECTOR_TIMELESS, f"{config['cropped_grid_feature']}_{size}") for size in config["scale_sizes"]
94 | ]
95 | reference_feature = (FeatureType.VECTOR_TIMELESS, config["reference_feature"])
96 | features = grid_features + [reference_feature]
97 |
98 | columns = ["NAME", "EOPATCH_NAME", "N_BBOXES", "IS_DATA_RATIO", "VALID_DATA_RATIO"]
99 | if config.get("cloud_mask_feature"):
100 | columns.append("CLOUD_COVERAGE")
101 | if config.get("valid_reference_mask_feature"):
102 | columns.append("HAS_REF_RATIO")
103 | with LocalFile(ref_geopackage_path, mode="w", config=sh_config) as ref_file, LocalFile(
104 | grid_geopackage_path, mode="w", config=sh_config
105 | ) as grid_file:
106 | for eopatch_name in tqdm(eopatch_names, desc=f"Creating {ref_geopackage_path}, {grid_geopackage_path}"):
107 | eopatch = EOPatch.load(eopatch_name, filesystem=input_filesystem, features=features)
108 | export_geopackage(
109 | eopatch=eopatch,
110 | geopackage_path=ref_file.path,
111 | feature=reference_feature,
112 | geometry_column=config["bbox_type"],
113 | columns=["area"],
114 | )
115 | for grid_feature in grid_features:
116 | export_geopackage(
117 | eopatch=eopatch, geopackage_path=grid_file.path, feature=grid_feature, columns=columns
118 | )
119 |
120 |
121 | def main():
122 | LOGGER.info(f"Reading configuration from {args.config}")
123 | with open(args.config, "r") as jfile:
124 | full_config = json.load(jfile)
125 |
126 | config = full_config["prepare_eopatch"]
127 |
128 | if config["use_ray"]:
129 | ray.init(address="auto")
130 |
131 | sh_config = SHConfig()
132 | if config["aws_profile"]:
133 | sh_config = get_aws_credentials(aws_profile=config["aws_profile"], config=sh_config)
134 |
135 | workflow = training_data_workflow(config, sh_config)
136 |
137 | dirname, basename = fs.path.dirname(config["grid_file"]), fs.path.basename(config["grid_file"])
138 | filesystem = get_filesystem(dirname, config=sh_config)
139 |
140 | with LocalFile(basename, mode="r", filesystem=filesystem) as gridfile:
141 | eopatch_names = list(gpd.read_file(gridfile.path).eopatch.values)
142 | exec_args = get_execution_arguments(workflow, eopatch_names)
143 |
144 | finished, failed = run_execution(workflow, exec_args, eopatch_names, config)
145 | if failed:
146 | LOGGER.info("Some executions failed. The produced Geopackages might not have all EOPatches!")
147 | eopatch_names = [eopatch_names[index] for index in finished]
148 |
149 | export_grids(config, eopatch_names, sh_config)
150 |
151 | # Clean up data in temp dir
152 | LOGGER.info(f"Cleaning up temporary directory")
153 | tmp_filesystem = get_filesystem(config["tmp_dir"], config=sh_config)
154 | tmp_filesystem.removetree("/")
155 |
156 |
157 | if __name__ == "__main__":
158 | main()
159 |
--------------------------------------------------------------------------------
/scripts/ssrdd/dota_to_gpkg.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import json
3 | import os
4 | from collections import defaultdict
5 | from functools import partial
6 |
7 | import fs as fsys
8 | import geopandas as gpd
9 | import pandas as pd
10 |
11 | from hiector.utils.aws_utils import LocalFile, get_filesystem
12 | from hiector.utils.grid import merge_multiprocess
13 | from hiector.utils.metrics import iou_single, convert_dota
14 | from hiector.utils.multiprocess import multiprocess
15 |
16 | parser = argparse.ArgumentParser(description="Converts predictions from DOTA into a combined GPKG.")
17 |
18 | parser.add_argument("--config", type=str, help="Path to config file with execution parameters", required=True)
19 | args = parser.parse_args()
20 |
21 |
22 | def save_dota_to_gpkg(
23 | eop_dota: str, dota_dir: str, eops_dir: str, aws_profile: str, aws_bucket: str, resolution: int, out_folder: str
24 | ) -> None:
25 | filesystem = get_filesystem(aws_bucket, aws_profile)
26 | try:
27 | df = convert_dota(dota_dir, eops_dir, eop_dota, filesystem, resolution)
28 | except fsys.errors.ResourceNotFound as e:
29 | print(f"Could not open DOTA file: {eop_dota}. Skipping.")
30 | return
31 | except AttributeError as e:
32 | print(f"Could not open EOPatch for: {eop_dota}. Skipping.")
33 | return
34 | eopname = eop_dota.split("_")[2][:-4]
35 | for utm in df:
36 | filepath = os.path.join(out_folder, f"{eopname}_{utm}.gpkg")
37 | with LocalFile(filepath, mode="w", filesystem=filesystem) as outf:
38 | df[utm].to_file(outf.path, driver="GPKG")
39 |
40 |
41 | def load_gpkg(gpkg):
42 | basename = os.path.splitext(os.path.basename(gpkg))[0]
43 | _, epsg = basename.split("_")
44 | try:
45 | with LocalFile(gpkg, filesystem=fs) as f:
46 | file = gpd.read_file(f.path)
47 | fs.remove(gpkg)
48 | return file, epsg
49 | except Exception as e:
50 | print(f"Something went wrong for {gpkg} with error {e}")
51 | return None, None
52 |
53 |
54 | if __name__ == "__main__":
55 | # read config parameter file
56 | with open(args.config, "r") as jfile:
57 | cfg_dict = json.load(jfile)
58 |
59 | cfg = cfg_dict["execute"]
60 | fs = get_filesystem(cfg["s3_bucket_name"], cfg["s3_profile_name"])
61 | modality = cfg["datasources"]["evaluate"]["modality"]
62 | resolution = cfg["datasources"]["evaluate"]["resolution"]
63 | eopatches_dir = cfg["datasources"]["evaluate"]["eopatches_dir"]
64 | data_dir = cfg["datasources"]["evaluate"]["data_dir"]
65 | evaluate_gdf = cfg["grid_file"]
66 | dota_dir = cfg["aws_dota_dir"]
67 | gpkg_dir = cfg["aws_gpkg_dir"]
68 | num_workers = cfg["num_workers"]
69 | with LocalFile(evaluate_gdf, filesystem=fs) as f:
70 | gdf = gpd.read_file(f.path)
71 | dota_eopatches = gdf.eopatch.unique()
72 | dota_filenames = [f"Task1_building_{x}.txt" for x in dota_eopatches]
73 | gpkg_filenames = [
74 | f"{eop}_{crs.lower()}.gpkg" for eop, crs in gdf[["eopatch", "bbox_crs"]].drop_duplicates().values
75 | ]
76 |
77 | save_gpkg_func = partial(
78 | save_dota_to_gpkg,
79 | dota_dir=dota_dir,
80 | eops_dir=eopatches_dir,
81 | aws_profile=cfg["s3_profile_name"],
82 | aws_bucket=cfg["s3_bucket_name"],
83 | resolution=resolution,
84 | out_folder=gpkg_dir,
85 | )
86 |
87 | _ = multiprocess(save_gpkg_func, dota_filenames, max_workers=num_workers)
88 |
89 | predictions_per_utm = defaultdict(list)
90 |
91 | gpkgs = [os.path.join(gpkg_dir, gpkg) for gpkg in gpkg_filenames]
92 | results = multiprocess(load_gpkg, gpkgs, max_workers=num_workers)
93 | for gpkg, epsg in results:
94 | if gpkg is not None:
95 | predictions_per_utm[epsg].append(gpkg)
96 |
97 | with LocalFile(os.path.join(gpkg_dir, f"predictions_merged_{modality}.gpkg"), mode="w", filesystem=fs) as f:
98 | for utm in predictions_per_utm:
99 | utm_predictions = pd.concat(predictions_per_utm[utm])
100 | merged = merge_multiprocess(
101 | utm_predictions, iou_single, sorting_col="pseudo_probability", max_workers=num_workers
102 | )
103 | merged.to_file(f.path, layer=utm, driver="GPKG")
104 |
--------------------------------------------------------------------------------
/scripts/ssrdd/raypredict.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import json
3 | import os
4 |
5 | import geopandas as gpd
6 | import ray
7 |
8 | from eolearn.core import EOExecutor, EONode, EOWorkflow
9 | from eolearn.core.extra.ray import RayExecutor
10 |
11 | from hiector.tasks.execute import PredictEOPatch
12 | from hiector.utils.aws_utils import LocalFile, get_filesystem
13 |
14 | parser = argparse.ArgumentParser(description="Execute evaluation using the SSRDD model and Ray")
15 |
16 | parser.add_argument("--config", type=str, help="Path to config file with execution parameters", required=True)
17 | parser.add_argument("--on-the-fly", help="Should the grid be calculated on the fly.", action="store_true")
18 | args = parser.parse_args()
19 |
20 | if __name__ == "__main__":
21 | ray.init(address="auto")
22 | # read config parameter file
23 | with open(args.config, "r") as jfile:
24 | cfg_dict = json.load(jfile)
25 |
26 | cfg = cfg_dict["execute"]
27 |
28 | cfg["checkpoint"] = None
29 | cfg["old_version"] = False
30 | fs = get_filesystem(cfg["s3_bucket_name"], cfg["s3_profile_name"])
31 |
32 | if args.on_the_fly:
33 | df_filename = cfg["grid_file"]
34 | eopatch_col = "eopatch"
35 | else:
36 | data_dir = cfg["datasources"]["evaluate"]["data_dir"]
37 | metadata_filename = cfg["datasources"]["evaluate"]["metadata_filename"]
38 | df_filename = os.path.join(data_dir, metadata_filename)
39 | eopatch_col = "EOPATCH_NAME"
40 |
41 | with LocalFile(df_filename, filesystem=fs) as file:
42 | df = gpd.read_file(file.path)
43 |
44 | predict_eop_task = PredictEOPatch(cfg, args.on_the_fly)
45 | predict_eop_node = EONode(task=predict_eop_task)
46 | workflow = EOWorkflow(workflow_nodes=[predict_eop_node])
47 | exec_args = [{predict_eop_node: dict(eopatch_name=eopname)} for eopname in df[eopatch_col].unique()]
48 |
49 | executor = RayExecutor(workflow, exec_args)
50 | executor.run()
51 | executor.make_report()
52 |
--------------------------------------------------------------------------------
/scripts/training_data_selection.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import json
3 | import logging
4 | import sys
5 |
6 | import fiona
7 | import geopandas as gpd
8 | import numpy as np
9 | import pandas as pd
10 | import torch
11 |
12 | from eolearn.core.utils.fs import get_aws_credentials, join_path
13 | from sentinelhub import SHConfig
14 |
15 | from hiector.utils.aws_utils import LocalFile
16 | from hiector.utils.training_data import filter_dataframe, train_test_val_split
17 |
18 | stdout_handler = logging.StreamHandler(sys.stdout)
19 | handlers = [stdout_handler]
20 | logging.basicConfig(
21 | level=logging.INFO, format="[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s", handlers=handlers
22 | )
23 | LOGGER = logging.getLogger(__name__)
24 |
25 |
26 | parser = argparse.ArgumentParser(description="Execute training and evaluation using the SSRDD model.\n")
27 |
28 | parser.add_argument("--config", type=str, help="Path to config file with execution parameters", required=True)
29 |
30 | args = parser.parse_args()
31 |
32 |
33 | def prepare_data(config):
34 | data_dir = config["data_dir"]
35 | sh_config = SHConfig()
36 |
37 | if config["aws_profile"]:
38 | sh_config = get_aws_credentials(aws_profile=config["aws_profile"], config=sh_config)
39 |
40 | input_gpkg_path = join_path(data_dir, config["input_dataframe_filename"])
41 | dfs = []
42 | with LocalFile(input_gpkg_path, mode="r", config=sh_config) as local_file:
43 | for layername in fiona.listlayers(local_file.path):
44 | scale = int(layername.split("_")[1])
45 | # Assumes layers to be named as PATCHLETS__
46 | if scale in config["scale_sizes"]:
47 | LOGGER.info(f"Reading layer: {layername}")
48 | df = gpd.read_file(local_file.path, layer=layername)
49 | df["CRS"] = str(df.crs)
50 | df["LAYER_NAME"] = layername
51 | df["SCALE"] = scale
52 | # Convert to WGS84, because we want stuff from different CRSes to be stored together
53 | df = df.to_crs("epsg:4326")
54 | dfs.append(df)
55 |
56 | LOGGER.info("Concatenating layers together.")
57 | dataframe = pd.concat(dfs)
58 | LOGGER.info("Filtering dataframe.")
59 | filtered_df = filter_dataframe(
60 | dataframe,
61 | query=config.get("query"),
62 | frac=config.get("frac"),
63 | exclude_eops=config.get("exclude_eops"),
64 | seed=config.get("seed"),
65 | )
66 |
67 | LOGGER.info("Performing train/test/val/split.")
68 | split_df = train_test_val_split(
69 | filtered_df,
70 | fraction_train=config["fraction_train"],
71 | fraction_test=config["fraction_test"],
72 | fraction_val=config["fraction_val"],
73 | )
74 | output_gpkg_path = join_path(data_dir, config["output_dataframe_filename"])
75 |
76 | LOGGER.info(f"Saving prepared dataframe to: {output_gpkg_path}")
77 | with LocalFile(output_gpkg_path, mode="w", config=sh_config) as local_file:
78 | split_df.to_file(local_file.path, driver="GPKG")
79 |
80 |
81 | if __name__ == "__main__":
82 | # read config parameter file
83 | LOGGER.info(f"Reading configuration from {args.config}")
84 | with open(args.config, "r") as jfile:
85 | cfg_dict = json.load(jfile)
86 |
87 | cfg = cfg_dict["select-data"]
88 |
89 | torch.manual_seed(0)
90 | np.random.seed(cfg["seed"])
91 | prepare_data(cfg)
92 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | from setuptools import setup, find_packages
3 |
4 |
5 | def parse_requirements(file):
6 | with open(os.path.join(os.path.dirname(__file__), file)) as req_file:
7 | return [line.strip() for line in req_file if "/" not in line]
8 |
9 |
10 | def get_version():
11 | for line in open(os.path.join(os.path.dirname(__file__), "hiector", "__init__.py")):
12 | if line.find("__version__") >= 0:
13 | version = line.split("=")[1].strip()
14 | version = version.strip('"').strip("'")
15 | return version
16 |
17 |
18 | setup(
19 | name="hiector",
20 | python_requires=">=3.7",
21 | version=get_version(),
22 | description="HIErarchical deteCTOR, a package for hierarchical building detection",
23 | url="https://github.com/sentinel-hub/hiector",
24 | author="Sinergise EO research team",
25 | author_email="eoresearch@sinergise.com",
26 | license="MIT",
27 | packages=find_packages(),
28 | install_requires=parse_requirements("requirements.txt"),
29 | extras_require={"DEV": parse_requirements("requirements-dev.txt")},
30 | zip_safe=False,
31 | keywords="building, object detection, hierarchical",
32 | classifiers=[
33 | "Development Status :: 3 - Alpha",
34 | "Intended Audience :: Developers",
35 | "License :: OSI Approved :: MIT License",
36 | "Programming Language :: Python :: 3",
37 | "Programming Language :: Python",
38 | "Programming Language :: Python :: 3",
39 | "Programming Language :: Python :: 3.7",
40 | "Programming Language :: Python :: 3.8",
41 | "Programming Language :: Python :: 3.9",
42 | "Topic :: Software Development :: Build Tools",
43 | ],
44 | )
45 |
--------------------------------------------------------------------------------
/tests/_data/ssrdd/test_nms/input/merged-bboxes.gpkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/tests/_data/ssrdd/test_nms/input/merged-bboxes.gpkg
--------------------------------------------------------------------------------
/tests/_data/ssrdd/test_nms/input/test-nms-bboxes.npy:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/tests/_data/ssrdd/test_nms/input/test-nms-bboxes.npy
--------------------------------------------------------------------------------
/tests/_data/ssrdd/test_nms/input/test-nms-labels.npy:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/tests/_data/ssrdd/test_nms/input/test-nms-labels.npy
--------------------------------------------------------------------------------
/tests/_data/ssrdd/test_nms/input/test-nms-scores.npy:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/tests/_data/ssrdd/test_nms/input/test-nms-scores.npy
--------------------------------------------------------------------------------
/tests/_data/tasks/test_reference/input/test-eop-prepared/bbox.geojson:
--------------------------------------------------------------------------------
1 | {
2 | "crs": {
3 | "type": "name",
4 | "properties": {
5 | "name": "urn:ogc:def:crs:EPSG::32638"
6 | }
7 | },
8 | "type": "Polygon",
9 | "coordinates": [
10 | [
11 | [
12 | 833507.6264808136,
13 | 4490462.988387336
14 | ],
15 | [
16 | 833507.6264808136,
17 | 4490514.873089392
18 | ],
19 | [
20 | 833581.232227395,
21 | 4490514.873089392
22 | ],
23 | [
24 | 833581.232227395,
25 | 4490462.988387336
26 | ],
27 | [
28 | 833507.6264808136,
29 | 4490462.988387336
30 | ]
31 | ]
32 | ]
33 | }
--------------------------------------------------------------------------------
/tests/_data/tasks/test_reference/input/test-eop-prepared/vector_timeless/BUILDINGS_MRR.gpkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/tests/_data/tasks/test_reference/input/test-eop-prepared/vector_timeless/BUILDINGS_MRR.gpkg
--------------------------------------------------------------------------------
/tests/_data/tasks/test_reference/input/test-eop/bbox.geojson:
--------------------------------------------------------------------------------
1 | {
2 | "crs": {
3 | "type": "name",
4 | "properties": {
5 | "name": "urn:ogc:def:crs:EPSG::32639"
6 | }
7 | },
8 | "type": "Polygon",
9 | "coordinates": [
10 | [
11 | [
12 | 369879.99999999977,
13 | 4469880.0
14 | ],
15 | [
16 | 369879.99999999977,
17 | 4480120.0
18 | ],
19 | [
20 | 380120.0000000001,
21 | 4480120.0
22 | ],
23 | [
24 | 380120.0000000001,
25 | 4469880.0
26 | ],
27 | [
28 | 369879.99999999977,
29 | 4469880.0
30 | ]
31 | ]
32 | ]
33 | }
--------------------------------------------------------------------------------
/tests/_data/tasks/test_reference/input/test-eop/vector_timeless/BUILDINGS.gpkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/tests/_data/tasks/test_reference/input/test-eop/vector_timeless/BUILDINGS.gpkg
--------------------------------------------------------------------------------
/tests/_data/utils/test_cropping/input/eop-preprocessed/bbox.geojson:
--------------------------------------------------------------------------------
1 | {
2 | "crs": {
3 | "type": "name",
4 | "properties": {
5 | "name": "urn:ogc:def:crs:EPSG::32638"
6 | }
7 | },
8 | "type": "Polygon",
9 | "coordinates": [
10 | [
11 | [
12 | 509166.0,
13 | 4369902.0
14 | ],
15 | [
16 | 509166.0,
17 | 4370706.0
18 | ],
19 | [
20 | 509970.0,
21 | 4370706.0
22 | ],
23 | [
24 | 509970.0,
25 | 4369902.0
26 | ],
27 | [
28 | 509166.0,
29 | 4369902.0
30 | ]
31 | ]
32 | ]
33 | }
--------------------------------------------------------------------------------
/tests/_data/utils/test_cropping/input/eop-preprocessed/data/bands.npy:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/tests/_data/utils/test_cropping/input/eop-preprocessed/data/bands.npy
--------------------------------------------------------------------------------
/tests/_data/utils/test_cropping/input/eop-preprocessed/mask/dataMask.npy:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/tests/_data/utils/test_cropping/input/eop-preprocessed/mask/dataMask.npy
--------------------------------------------------------------------------------
/tests/_data/utils/test_cropping/input/eop-preprocessed/mask_timeless/BUILDINGS.npy:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/tests/_data/utils/test_cropping/input/eop-preprocessed/mask_timeless/BUILDINGS.npy
--------------------------------------------------------------------------------
/tests/_data/utils/test_cropping/input/eop-preprocessed/timestamp.json:
--------------------------------------------------------------------------------
1 | [
2 | "2020-06-01T07:28:46",
3 | "2020-06-13T07:36:20",
4 | "2020-07-26T07:07:40",
5 | "2020-07-30T07:24:10"
6 | ]
--------------------------------------------------------------------------------
/tests/_data/utils/test_cropping/input/eop-preprocessed/vector/SNOW_MASK.gpkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/tests/_data/utils/test_cropping/input/eop-preprocessed/vector/SNOW_MASK.gpkg
--------------------------------------------------------------------------------
/tests/_data/utils/test_metrics/input/eop_data.csv:
--------------------------------------------------------------------------------
1 | eopatch-id-0988-col-184-row-182,eopatch-id-0198-col-6-row-42
2 | "396270.0,4479726.0,397074.0,4480530.0","614382.0,4501998.0,615186.0,4502802.0"
3 | 32639,32638
4 | "(396270.0, 0.5, 0, 4480530.0, 0, -0.5)","(614382.0, 0.5, 0, 4502802.0, 0, -0.5)"
5 |
--------------------------------------------------------------------------------
/tests/_data/utils/test_metrics/input/gdf-gt.gpkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/tests/_data/utils/test_metrics/input/gdf-gt.gpkg
--------------------------------------------------------------------------------
/tests/_data/utils/test_metrics/input/gdf-pr.gpkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sentinel-hub/hiector/95102c1fcfa63d127a389262e9d569e3aa3495cc/tests/_data/utils/test_metrics/input/gdf-pr.gpkg
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 | import shutil
4 | import subprocess
5 | import time
6 |
7 | import _pytest
8 | import pytest
9 |
10 | ROOT = os.path.dirname(os.path.realpath(__file__))
11 | DATA_ROOT = os.path.join(ROOT, "_data")
12 |
13 |
14 | def module_relpath(request: _pytest.fixtures.FixtureRequest, folder_name: str = ""):
15 | """Constructs path for the test execution from the test file's name, which it gets from
16 | pytest.FixtureRequest (https://docs.pytest.org/en/latest/reference.html#request).
17 | """
18 | test_name = request.fspath.basename.split(".")[0]
19 | relpath = os.path.relpath(str(request.fspath.dirname), ROOT)
20 | return os.path.join(relpath, test_name, folder_name)
21 |
22 |
23 | def function_relpath(request: _pytest.fixtures.FixtureRequest, folder_name: str = ""):
24 | """Constructs path for the test execution from the test file's name and function, which it gets from
25 | pytest.FixtureRequest (https://docs.pytest.org/en/latest/reference.html#request).
26 | """
27 | mod_path = module_relpath(request, folder_name=folder_name)
28 | return os.path.join(mod_path, request.function.__name__)
29 |
30 |
31 | # --------------------------------- PATH FIXTURES ---------------------------------
32 |
33 |
34 | @pytest.fixture(scope="module")
35 | def input_folder(request: _pytest.fixtures.FixtureRequest):
36 | """Returns the input folder path `test/_data/test_name/input`."""
37 | return os.path.join(DATA_ROOT, module_relpath(request, folder_name="input"))
38 |
39 |
40 | @pytest.fixture(scope="function")
41 | def output_folder(request: _pytest.fixtures.FixtureRequest):
42 | """Creates the output folder path `test/_data/test_name/output`.
43 |
44 | It also cleans the output folder before the test runs.
45 | """
46 |
47 | out_path = os.path.join(DATA_ROOT, function_relpath(request, folder_name="output"))
48 | if os.path.exists(out_path):
49 | shutil.rmtree(out_path)
50 |
51 | os.makedirs(out_path)
52 |
53 | yield out_path
54 |
--------------------------------------------------------------------------------
/tests/ssrdd/test_nms.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import geopandas as gpd
4 | import numpy as np
5 | import pandas as pd
6 | import pytest
7 | from shapely.geometry import Polygon
8 |
9 | from hiector.ssrdd.utils.box.bbox_np import xywha2xy4
10 | from hiector.ssrdd.utils.box.rbbox_np import rbbox_batched_nms
11 |
12 |
13 | def to_polygon(bb):
14 | bbox = xywha2xy4(bb).ravel()
15 | return Polygon(list(zip(bbox[::2], bbox[1::2])))
16 |
17 |
18 | @pytest.fixture(name="npys_in", scope="module")
19 | def get_input_np_files(input_folder):
20 | """A pytest fixture to retrieve the input numpy arrays"""
21 | return (
22 | np.load(os.path.join(input_folder, "test-nms-bboxes.npy")),
23 | np.load(os.path.join(input_folder, "test-nms-scores.npy")),
24 | np.load(os.path.join(input_folder, "test-nms-labels.npy")),
25 | )
26 |
27 |
28 | @pytest.fixture(name="gdf_out", scope="module")
29 | def get_gdf_out(input_folder):
30 | """A pytest fixture to retrieve the output GeoDataFrame"""
31 | return gpd.read_file(os.path.join(input_folder, "merged-bboxes.gpkg"))
32 |
33 |
34 | def test_nms(npys_in, gdf_out, output_folder):
35 | bboxes, scores, labels = npys_in
36 |
37 | geo_df = gpd.GeoDataFrame(data={"labels": labels, "scores": scores}, geometry=[to_polygon(b) for b in bboxes])
38 |
39 | nms = rbbox_batched_nms(bboxes, scores, labels, iou_thresh=0.5)
40 | gdf_nms = geo_df.iloc[nms].copy()
41 | gdf_nms = gdf_nms.sort_values("scores", ascending=False)
42 |
43 | gdf_nms.to_file(os.path.join(output_folder, "nms-output.gpkg"), driver="GPKG")
44 |
45 | pd.testing.assert_frame_equal(
46 | gdf_nms.reset_index(drop=True),
47 | gdf_out[["labels", "scores", "geometry"]],
48 | check_dtype=False,
49 | check_index_type=False,
50 | )
51 |
--------------------------------------------------------------------------------
/tests/tasks/test_reference.py:
--------------------------------------------------------------------------------
1 | import os
2 | from copy import deepcopy
3 |
4 | import numpy as np
5 | import pytest
6 |
7 | from eolearn.core import EOPatch, FeatureType
8 |
9 | from hiector.tasks.reference import MergeBBoxesTask, PrepareMRRTask
10 | from hiector.utils.metrics import intersection_over_smallest
11 |
12 |
13 | @pytest.fixture(name="test_eop", scope="module")
14 | def test_eop(input_folder):
15 | eop = EOPatch.load(os.path.join(input_folder, "test-eop"))
16 | return eop
17 |
18 |
19 | @pytest.fixture(name="test_eop_prepared", scope="module")
20 | def test_eop_prepared(input_folder):
21 | eop = EOPatch.load(os.path.join(input_folder, "test-eop-prepared"))
22 | return eop
23 |
24 |
25 | def test_prepare_mmr(test_eop):
26 |
27 | task = PrepareMRRTask(
28 | input_feature=(FeatureType.VECTOR_TIMELESS, "BUILDINGS"),
29 | output_feature=(FeatureType.VECTOR_TIMELESS, "BUILDINGS"),
30 | )
31 | prepared = task.execute(deepcopy(test_eop))
32 |
33 | vec_in = test_eop.vector_timeless["BUILDINGS"]
34 | vec_out = prepared.vector_timeless["BUILDINGS"]
35 |
36 | assert len(vec_in) >= len(vec_out)
37 | for _id, geom in vec_in[["_ID_", "geometry"]].values:
38 | assert np.array([x.buffer(1e-1).contains(geom) for x in vec_out.geometry.values], dtype=bool).sum() > 0, _id
39 |
40 |
41 | def test_merge_bbox(test_eop_prepared):
42 | merge_task = MergeBBoxesTask(
43 | input_feature=(FeatureType.VECTOR_TIMELESS, "BUILDINGS_MRR"),
44 | output_feature=(FeatureType.VECTOR_TIMELESS, "BUILDINGS_MRR"),
45 | iou_method=intersection_over_smallest,
46 | iou_thr=0.4,
47 | sorting_col="area",
48 | )
49 |
50 | eop_merged = merge_task.execute(deepcopy(test_eop_prepared))
51 |
52 | vec_in = test_eop_prepared.vector_timeless["BUILDINGS_MRR"]
53 | vec_out = eop_merged.vector_timeless["BUILDINGS_MRR"]
54 |
55 | assert set(vec_out._ID_.values) == {7417907, 8063303, 8375354, 8376000}
56 |
--------------------------------------------------------------------------------
/tests/utils/test_cropping.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pytest
4 |
5 | from eolearn.core import EOPatch, FeatureType
6 |
7 | from hiector.tasks.cropping import CroppingTask
8 |
9 |
10 | @pytest.fixture(name="test_eop", scope="module")
11 | def eop(input_folder):
12 | """A pytest fixture to retrieve GeoDataFrame for ground truth"""
13 | return EOPatch.load(os.path.join(input_folder, "eop-preprocessed"))
14 |
15 |
16 | def test_cropping_task(test_eop):
17 | crop = CroppingTask(
18 | raster_feature=(FeatureType.DATA, "bands"),
19 | data_mask_feature=(FeatureType.MASK, "dataMask"),
20 | cloud_mask_feature=None,
21 | valid_reference_mask_feature=None,
22 | no_data_value=0,
23 | vector_feature=(FeatureType.VECTOR_TIMELESS, "BUILDINGS"),
24 | grid_feature=(FeatureType.VECTOR_TIMELESS, "PATCHLETS_64"),
25 | intersection_feature=(FeatureType.VECTOR_TIMELESS, "BBOXES_IN_GRID_64"),
26 | data_stack_feature=(FeatureType.META_INFO, "DATA_STACK_64"),
27 | size=64,
28 | overlap=0.25,
29 | resolution=1.5,
30 | valid_threshold=0.6,
31 | )
32 | cropping_eop = crop.execute(test_eop, eopatch_name="preprocessed-eop")
33 | assert "BBOXES_IN_GRID_64" in cropping_eop.vector_timeless
34 | assert "PATCHLETS_64" in cropping_eop.vector_timeless
35 | assert "DATA_STACK_64" in cropping_eop.meta_info
36 | assert cropping_eop.meta_info["DATA_STACK_64"].shape == (121, 64, 64, 4)
37 | assert len(cropping_eop.vector_timeless["PATCHLETS_64"]) == 121
38 |
--------------------------------------------------------------------------------
/tests/utils/test_geometry.py:
--------------------------------------------------------------------------------
1 | import geopandas as gpd
2 | import pytest
3 | from shapely.geometry import Polygon, box
4 |
5 | from hiector.utils.geometry import merge_bboxes
6 | from hiector.utils.metrics import intersection_over_smallest, iou_single
7 |
8 |
9 | @pytest.fixture(name="basic_bboxes_example", scope="module")
10 | def basic_bboxes_example():
11 | """A pytest fixture to retrieve GeoDataFrame for ground truth"""
12 | polys = [box(0, 0, 100, 100), box(0, 0, 50, 50), box(75, 75, 110, 110), box(5, 5, 95, 95), box(50, 102, 70, 105)]
13 | example_basic = gpd.GeoDataFrame(geometry=polys, crs="epsg:32639")
14 | example_basic["area"] = example_basic.area
15 | return example_basic
16 |
17 |
18 | def test_merge_bboxes(basic_bboxes_example):
19 | merged_iou_01 = merge_bboxes(basic_bboxes_example, iou_method=intersection_over_smallest, iou_thr=0.1)
20 |
21 | assert len(merged_iou_01) == 2
22 | assert merged_iou_01.iloc[0].geometry == basic_bboxes_example.iloc[0].geometry
23 | assert merged_iou_01.iloc[1].geometry == basic_bboxes_example.iloc[4].geometry
24 |
25 | merged_iou_09 = merge_bboxes(basic_bboxes_example, iou_method=intersection_over_smallest, iou_thr=0.9)
26 |
27 | assert len(merged_iou_09) == 3
28 | assert merged_iou_09.iloc[0].geometry == basic_bboxes_example.iloc[0].geometry
29 | assert merged_iou_09.iloc[1].geometry == basic_bboxes_example.iloc[2].geometry
30 | assert merged_iou_09.iloc[2].geometry == basic_bboxes_example.iloc[4].geometry
31 |
--------------------------------------------------------------------------------
/tests/utils/test_metrics.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import geopandas as gpd
4 | import pandas as pd
5 | import pytest
6 |
7 | from sentinelhub import CRS, BBox
8 |
9 | from hiector.utils.metrics import dota_results_to_gdf, get_ap, get_error_matrix, iou
10 |
11 |
12 | @pytest.fixture(name="gdf_gt", scope="module")
13 | def gdf_gt(input_folder):
14 | """A pytest fixture to retrieve GeoDataFrame for ground truth"""
15 | return gpd.read_file(os.path.join(input_folder, "gdf-gt.gpkg"))
16 |
17 |
18 | @pytest.fixture(name="gdf_pr", scope="module")
19 | def gdf_pr(input_folder):
20 | """A pytest fixture to retrieve GeoDataFrame for ground truth"""
21 | return gpd.read_file(os.path.join(input_folder, "gdf-pr.gpkg"))
22 |
23 |
24 | @pytest.fixture(name="eop_data", scope="module")
25 | def eop_data(input_folder):
26 | """A pytest fixture to retrieve dictionary with EOPatch BBox information"""
27 | df = pd.read_csv(os.path.join(input_folder, "eop_data.csv"))
28 | eop_data = {}
29 | for item in df.iteritems():
30 | eop_data[item[0]] = {
31 | "bbox": BBox([float(coord) for coord in item[1].values[0].split(",")], crs=CRS(int(item[1].values[1]))),
32 | "transform": tuple(float(cc) for cc in item[1].values[2].split("(")[-1].split(")")[0].split(",")),
33 | }
34 | return eop_data
35 |
36 |
37 | def test_iou(gdf_gt, gdf_pr):
38 |
39 | iou_ = iou(gdf_gt, gdf_pr)
40 |
41 | assert len(iou_) == 35
42 | assert len(iou_[iou_.iou == 1]) == 1
43 | assert len(iou_[iou_.iou == 0]) == 7
44 | assert len(iou_[iou_.iou >= 0.5]) == 4
45 |
46 | iou_dupl = iou(gdf_gt, pd.concat([gdf_pr, gdf_pr[:1]]), drop_duplicates=False)
47 | iou_nodupl = iou(gdf_gt, pd.concat([gdf_pr, gdf_pr[:1]]), drop_duplicates=True)
48 |
49 | assert len(iou_dupl) == 36
50 | assert len(iou_nodupl) == 35
51 |
52 |
53 | def test_error_matrix(gdf_gt, gdf_pr):
54 |
55 | for proba in [0.0, 0.5]:
56 | errors = get_error_matrix(gdf_gt, gdf_pr, proba_thr=proba, iou_thr=0.5)
57 |
58 | assert all([errors[df] is None for df in ["TP_df", "FP_df", "FN_df"]])
59 | assert errors["TP"] == 4
60 | assert errors["FN"] == 96
61 | assert errors["FP"] + errors["TP"] == len(gdf_pr[gdf_pr.pseudo_probability >= proba])
62 |
63 | errors = get_error_matrix(gdf_gt, gdf_pr, proba_thr=0.0, iou_thr=0.0, return_dfs=True)
64 | assert errors["TP"] == 35
65 | assert all([isinstance(errors[df], gpd.GeoDataFrame) for df in ["TP_df", "FP_df", "FN_df"]])
66 |
67 | # check geometries in TP and FP match the predicted ones
68 | pd.testing.assert_frame_equal(errors["TP_df"][["geometry"]][1:2], gdf_pr[["geometry"]][1:2])
69 | pd.testing.assert_frame_equal(errors["FP_df"][["geometry"]][:1], gdf_pr[["geometry"]][5:6])
70 |
71 |
72 | def test_ap(gdf_gt, gdf_pr):
73 |
74 | proba = 0.0
75 | ap, mrec, mprec = get_ap(gdf_gt, gdf_pr, proba_thr=proba, iou_thr=0.0)
76 |
77 | assert ap == 0.3108179542562844
78 | assert (len(mrec) == len(mprec)) and (len(mrec) == len(gdf_pr[gdf_pr.pseudo_probability >= proba]) + 2)
79 |
80 |
81 | def test_dota_results_to_gdf(input_folder, eop_data):
82 | detections_file = os.path.join(input_folder, "Task1_building.txt")
83 | results = dota_results_to_gdf(detections_file, eop_data, underscore=False)
84 |
85 | assert set(results.keys()) == {"epsg:32638", "epsg:32639"}, "Wrong key dictionaries generated"
86 | for crs in ["32638", "32639"]:
87 | assert results[f"epsg:{crs}"].crs == CRS(crs).pyproj_crs(), "Incorrect CRS"
88 |
--------------------------------------------------------------------------------