├── .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 | --------------------------------------------------------------------------------