From d3c82a5ac8f9e6c471ca2ad6e88a3f6308d35d44 Mon Sep 17 00:00:00 2001 From: Isaac Corley <22203655+isaaccorley@users.noreply.github.com> Date: Sun, 23 Apr 2023 17:37:29 -0500 Subject: [PATCH 01/17] Refactor train.py (#1237) * add pretrain.py tested with seco100k * refactor * add pretrain.py tested with seco100k * refactor * refactor to train.py and add simclr * revert simclr and pretrain.py changes * revert more simclr changesg * add refactor to configs and train.py * add hydra.utils.instantiate import * fix flake8 * update tests and add hydra-core to deps * fix byol tests * update exp name * format * remove evaluate.py * add hydra-core to min tests deps * update tests * add trainer to configs and use lightning.Trainer insteead of pl.Trainer * add eurosat100 test * update train.py * lightning.Trainer -> lightning.pytorch.Trainer * remove omegaconf * update hydra-core to 1.1.1 and fix mypy * add recursive hydra config test * update coment * update test config * fix tests * add omegaconf back in * remove hydra recursive test * update hydra-core to 2.3.0 for ci * Try older hydra * Older hydra requires old omegaconf * Try older hydra * Try newer hydra * Try older hydra * Try newer hydra * Finalize minimum dep versions --------- Co-authored-by: Adam J. Stewart --- .pre-commit-config.yaml | 2 +- conf/bigearthnet.yaml | 40 +-- conf/byol.yaml | 26 -- conf/chesapeake_cvpr.yaml | 61 ++--- conf/cowc_counting.yaml | 31 ++- conf/cyclone.yaml | 31 ++- conf/deepglobelandcover.yaml | 48 ++-- conf/defaults.yaml | 2 +- conf/etci2021.yaml | 40 +-- conf/eurosat.yaml | 36 ++- conf/gid15.yaml | 48 ++-- conf/inria.yaml | 46 ++-- conf/landcoverai.yaml | 42 +-- conf/naipchesapeake.yaml | 47 ++-- conf/nasa_marine_debris.yaml | 40 ++- conf/potsdam2d.yaml | 48 ++-- conf/resisc45.yaml | 36 +-- conf/seco_100k.yaml | 24 ++ conf/sen12ms.yaml | 43 +-- conf/so2sat.yaml | 38 +-- conf/spacenet1.yaml | 45 ++-- conf/ucmerced.yaml | 36 ++- conf/vaihingen2d.yaml | 48 ++-- environment.yml | 3 +- evaluate.py | 294 --------------------- requirements/min-reqs.old | 3 +- requirements/tests.txt | 1 + setup.cfg | 6 +- tests/conf/bigearthnet_all.yaml | 35 +-- tests/conf/bigearthnet_s1.yaml | 35 +-- tests/conf/bigearthnet_s2.yaml | 35 +-- tests/conf/chesapeake_cvpr_5.yaml | 55 ++-- tests/conf/chesapeake_cvpr_7.yaml | 55 ++-- tests/conf/chesapeake_cvpr_prior_byol.yaml | 50 ++-- tests/conf/cowc_counting.yaml | 29 +- tests/conf/cyclone.yaml | 29 +- tests/conf/deepglobelandcover.yaml | 41 +-- tests/conf/etci2021.yaml | 35 +-- tests/conf/eurosat.yaml | 31 +-- tests/conf/eurosat100.yaml | 16 ++ tests/conf/fire_risk.yaml | 31 +-- tests/conf/gid15.yaml | 43 +-- tests/conf/inria.yaml | 39 +-- tests/conf/l7irish.yaml | 43 +-- tests/conf/l8biome.yaml | 43 +-- tests/conf/landcoverai.yaml | 39 +-- tests/conf/loveda.yaml | 39 +-- tests/conf/naipchesapeake.yaml | 41 +-- tests/conf/nasa_marine_debris.yaml | 29 +- tests/conf/potsdam2d.yaml | 41 +-- tests/conf/resisc45.yaml | 31 +-- tests/conf/seco_byol_1.yaml | 27 +- tests/conf/seco_byol_2.yaml | 27 +- tests/conf/sen12ms_all.yaml | 35 +-- tests/conf/sen12ms_s1.yaml | 37 +-- tests/conf/sen12ms_s2_all.yaml | 35 +-- tests/conf/sen12ms_s2_reduced.yaml | 35 +-- tests/conf/skippd.yaml | 29 +- tests/conf/so2sat_all.yaml | 31 +-- tests/conf/so2sat_s1.yaml | 31 +-- tests/conf/so2sat_s2.yaml | 31 +-- tests/conf/spacenet1.yaml | 43 +-- tests/conf/ssl4eo_s12_byol_1.yaml | 27 +- tests/conf/ssl4eo_s12_byol_2.yaml | 27 +- tests/conf/sustainbench_crop_yield.yaml | 29 +- tests/conf/ucmerced.yaml | 31 +-- tests/conf/vaihingen2d.yaml | 41 +-- tests/trainers/test_byol.py | 39 +-- tests/trainers/test_classification.py | 67 ++--- tests/trainers/test_detection.py | 26 +- tests/trainers/test_regression.py | 36 +-- tests/trainers/test_segmentation.py | 77 ++---- train.py | 144 +++------- 73 files changed, 1292 insertions(+), 1643 deletions(-) delete mode 100644 conf/byol.yaml create mode 100644 conf/seco_100k.yaml delete mode 100755 evaluate.py create mode 100644 tests/conf/eurosat100.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c9bbd99ab4a..299079e1f00 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,5 +34,5 @@ repos: hooks: - id: mypy args: [--strict, --ignore-missing-imports, --show-error-codes] - additional_dependencies: [torch>=2, torchmetrics>=0.10, lightning>=1.8, pytest>=6, pyvista>=0.20, omegaconf>=2.1, kornia>=0.6, numpy>=1.22.0] + additional_dependencies: [torch>=2, torchmetrics>=0.10, lightning>=1.8, pytest>=6, pyvista>=0.20, omegaconf>=2.0.1, hydra-core>=1, kornia>=0.6, numpy>=1.22] exclude: (build|data|dist|logo|logs|output)/ diff --git a/conf/bigearthnet.yaml b/conf/bigearthnet.yaml index 131728d61d9..2c8a4da5218 100644 --- a/conf/bigearthnet.yaml +++ b/conf/bigearthnet.yaml @@ -1,22 +1,24 @@ +module: + _target_: torchgeo.trainers.MultiLabelClassificationTask + loss: "bce" + model: "resnet18" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + weights: null + in_channels: 14 + num_classes: 19 + +datamodule: + _target_: torchgeo.datamodules.BigEarthNetDataModule + root: "data/bigearthnet" + bands: "all" + num_classes: ${module.num_classes} + batch_size: 128 + num_workers: 4 + trainer: + _target_: lightning.pytorch.Trainer accelerator: gpu devices: 1 - min_epochs: 10 - max_epochs: 40 - benchmark: True -experiment: - task: "bigearthnet" - module: - loss: "bce" - model: "resnet18" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - weights: null - in_channels: 14 - num_classes: 19 - datamodule: - root: "data/bigearthnet" - bands: "all" - num_classes: ${experiment.module.num_classes} - batch_size: 128 - num_workers: 4 + min_epochs: 15 + max_epochs: 40 \ No newline at end of file diff --git a/conf/byol.yaml b/conf/byol.yaml deleted file mode 100644 index b1a7ac927a8..00000000000 --- a/conf/byol.yaml +++ /dev/null @@ -1,26 +0,0 @@ -trainer: - accelerator: gpu - devices: 1 - min_epochs: 20 - max_epochs: 100 - benchmark: True -experiment: - task: "ssl" - name: "test_byol" - module: - model: "byol" - backbone: "resnet18" - input_channels: 4 - weights: imagenet - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - datamodule: - root: "data/chesapeake/cvpr" - train_splits: - - "de-train" - val_splits: - - "de-val" - test_splits: - - "de-test" - batch_size: 64 - num_workers: 4 diff --git a/conf/chesapeake_cvpr.yaml b/conf/chesapeake_cvpr.yaml index bacf56358db..d30f555187e 100644 --- a/conf/chesapeake_cvpr.yaml +++ b/conf/chesapeake_cvpr.yaml @@ -1,33 +1,34 @@ +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + in_channels: 4 + num_classes: 7 + num_filters: 256 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.ChesapeakeCVPRDataModule + root: "data/chesapeake/cvpr" + train_splits: + - "de-train" + val_splits: + - "de-val" + test_splits: + - "de-test" + batch_size: 200 + patch_size: 256 + num_workers: 4 + class_set: ${module.num_classes} + use_prior_labels: False + trainer: + _target_: lightning.pytorch.Trainer accelerator: gpu devices: 1 - min_epochs: 20 - max_epochs: 100 - benchmark: True -experiment: - task: "chesapeake_cvpr" - name: "chesapeake_cvpr_example" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - in_channels: 4 - num_classes: 7 - num_filters: 256 - ignore_index: null - datamodule: - root: "data/chesapeake/cvpr" - train_splits: - - "de-train" - val_splits: - - "de-val" - test_splits: - - "de-test" - batch_size: 200 - patch_size: 256 - num_workers: 4 - class_set: ${experiment.module.num_classes} - use_prior_labels: False + min_epochs: 15 + max_epochs: 40 \ No newline at end of file diff --git a/conf/cowc_counting.yaml b/conf/cowc_counting.yaml index 334672d159a..787bb55b835 100644 --- a/conf/cowc_counting.yaml +++ b/conf/cowc_counting.yaml @@ -1,18 +1,21 @@ +module: + _target_: torchgeo.trainers.RegressionTask + model: resnet18 + weights: null + num_outputs: 1 + in_channels: 3 + learning_rate: 1e-3 + learning_rate_schedule_patience: 2 + +datamodule: + _target_: torchgeo.datamodules.COWCCountingDataModule + root: "data/cowc_counting" + batch_size: 64 + num_workers: 4 + trainer: + _target_: lightning.pytorch.Trainer accelerator: gpu devices: 1 min_epochs: 15 -experiment: - task: cowc_counting - name: cowc_counting_test - module: - model: resnet18 - weights: null - num_outputs: 1 - in_channels: 3 - learning_rate: 1e-3 - learning_rate_schedule_patience: 2 - datamodule: - root: "data/cowc_counting" - batch_size: 64 - num_workers: 4 + max_epochs: 40 \ No newline at end of file diff --git a/conf/cyclone.yaml b/conf/cyclone.yaml index 733a48885e7..f5200ae8d74 100644 --- a/conf/cyclone.yaml +++ b/conf/cyclone.yaml @@ -1,18 +1,21 @@ +module: + _target_: torchgeo.trainers.RegressionTask + model: "resnet18" + weights: null + num_outputs: 1 + in_channels: 3 + learning_rate: 1e-3 + learning_rate_schedule_patience: 2 + +datamodule: + _target_: torchgeo.datamodules.TropicalCycloneDataModule + root: "data/cyclone" + batch_size: 32 + num_workers: 4 + trainer: + _target_: lightning.pytorch.Trainer accelerator: gpu devices: 1 min_epochs: 15 -experiment: - task: "cyclone" - name: "cyclone_test" - module: - model: "resnet18" - weights: null - num_outputs: 1 - in_channels: 3 - learning_rate: 1e-3 - learning_rate_schedule_patience: 2 - datamodule: - root: "data/cyclone" - batch_size: 32 - num_workers: 4 + max_epochs: 40 \ No newline at end of file diff --git a/conf/deepglobelandcover.yaml b/conf/deepglobelandcover.yaml index 2e09eca0e4b..9c7da9adbb4 100644 --- a/conf/deepglobelandcover.yaml +++ b/conf/deepglobelandcover.yaml @@ -1,20 +1,28 @@ -experiment: - task: "deepglobelandcover" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - verbose: false - in_channels: 3 - num_classes: 7 - num_filters: 1 - ignore_index: null - datamodule: - root: "data/deepglobelandcover" - batch_size: 1 - patch_size: 64 - val_split_pct: 0.5 - num_workers: 0 +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + verbose: false + in_channels: 3 + num_classes: 7 + num_filters: 1 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.DeepGlobeLandCoverDataModule + root: "data/deepglobelandcover" + batch_size: 1 + patch_size: 64 + val_split_pct: 0.5 + num_workers: 0 + +trainer: + _target_: lightning.pytorch.Trainer + accelerator: gpu + devices: 1 + min_epochs: 15 + max_epochs: 40 \ No newline at end of file diff --git a/conf/defaults.yaml b/conf/defaults.yaml index 15d58be2656..adcdf816e43 100644 --- a/conf/defaults.yaml +++ b/conf/defaults.yaml @@ -5,4 +5,4 @@ program: # These are the arguments that define how the train.py script works output_dir: output data_dir: data log_dir: logs - overwrite: False + overwrite: False \ No newline at end of file diff --git a/conf/etci2021.yaml b/conf/etci2021.yaml index c6e02005d10..2550f7e234d 100644 --- a/conf/etci2021.yaml +++ b/conf/etci2021.yaml @@ -1,16 +1,24 @@ -experiment: - task: "etci2021" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: "imagenet" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - in_channels: 6 - num_classes: 2 - ignore_index: 0 - datamodule: - root: "data/etci2021" - batch_size: 32 - num_workers: 4 +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: "imagenet" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + in_channels: 6 + num_classes: 2 + ignore_index: 0 + +datamodule: + _target_: torchgeo.datamodules.ETCI2021DataModule + root: "data/etci2021" + batch_size: 32 + num_workers: 4 + +trainer: + _target_: lightning.pytorch.Trainer + accelerator: gpu + devices: 1 + min_epochs: 15 + max_epochs: 40 \ No newline at end of file diff --git a/conf/eurosat.yaml b/conf/eurosat.yaml index 89dddfd1941..6f744a04cda 100644 --- a/conf/eurosat.yaml +++ b/conf/eurosat.yaml @@ -1,14 +1,22 @@ -experiment: - task: "eurosat" - module: - loss: "ce" - model: "resnet18" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - weights: null - in_channels: 13 - num_classes: 10 - datamodule: - root: "data/eurosat" - batch_size: 128 - num_workers: 4 +module: + _target_: torchgeo.trainers.ClassificationTask + loss: "ce" + model: "resnet18" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + weights: null + in_channels: 13 + num_classes: 10 + +datamodule: + _target_: torchgeo.datamodules.EuroSATDataModule + root: "data/eurosat" + batch_size: 128 + num_workers: 4 + +trainer: + _target_: lightning.pytorch.Trainer + accelerator: gpu + devices: 1 + min_epochs: 15 + max_epochs: 40 \ No newline at end of file diff --git a/conf/gid15.yaml b/conf/gid15.yaml index 420c6b2f0e9..2f21fc94195 100644 --- a/conf/gid15.yaml +++ b/conf/gid15.yaml @@ -1,20 +1,28 @@ -experiment: - task: "gid15" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - verbose: false - in_channels: 3 - num_classes: 16 - num_filters: 1 - ignore_index: null - datamodule: - root: "data/gid15" - batch_size: 1 - patch_size: 64 - val_split_pct: 0.5 - num_workers: 0 +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + verbose: false + in_channels: 3 + num_classes: 16 + num_filters: 1 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.GID15DataModule + root: "data/gid15" + batch_size: 1 + patch_size: 64 + val_split_pct: 0.5 + num_workers: 0 + +trainer: + _target_: lightning.pytorch.Trainer + accelerator: gpu + devices: 1 + min_epochs: 15 + max_epochs: 40 \ No newline at end of file diff --git a/conf/inria.yaml b/conf/inria.yaml index a269f0bd5e9..e0f716e292b 100644 --- a/conf/inria.yaml +++ b/conf/inria.yaml @@ -1,29 +1,25 @@ -program: - overwrite: True +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: "imagenet" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + in_channels: 3 + num_classes: 2 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.InriaAerialImageLabelingDataModule + root: "data/inria" + batch_size: 1 + patch_size: 512 + num_workers: 32 trainer: + _target_: lightning.pytorch.Trainer accelerator: gpu devices: 1 - min_epochs: 5 - max_epochs: 100 - benchmark: True - log_every_n_steps: 2 - -experiment: - task: "inria" - name: "inria_test" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: "imagenet" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - in_channels: 3 - num_classes: 2 - ignore_index: null - datamodule: - root: "data/inria" - batch_size: 1 - patch_size: 512 - num_workers: 32 + min_epochs: 15 + max_epochs: 40 \ No newline at end of file diff --git a/conf/landcoverai.yaml b/conf/landcoverai.yaml index ef5261abdee..0136527a19a 100644 --- a/conf/landcoverai.yaml +++ b/conf/landcoverai.yaml @@ -1,23 +1,25 @@ +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: "imagenet" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + in_channels: 3 + num_classes: 5 + num_filters: 256 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.LandCoverAIDataModule + root: "data/landcoverai" + batch_size: 32 + num_workers: 4 + trainer: + _target_: lightning.pytorch.Trainer accelerator: gpu devices: 1 - min_epochs: 20 - max_epochs: 100 - benchmark: True -experiment: - task: "landcoverai" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: "imagenet" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - in_channels: 3 - num_classes: 5 - num_filters: 256 - ignore_index: null - datamodule: - root: "data/landcoverai" - batch_size: 32 - num_workers: 4 + min_epochs: 15 + max_epochs: 40 \ No newline at end of file diff --git a/conf/naipchesapeake.yaml b/conf/naipchesapeake.yaml index 709224eca9d..ede6db4e336 100644 --- a/conf/naipchesapeake.yaml +++ b/conf/naipchesapeake.yaml @@ -1,24 +1,27 @@ -program: - experiment_name: "naip_chesapeake_test" - overwrite: True +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "deeplabv3+" + backbone: "resnet34" + weights: "imagenet" + learning_rate: 1e-3 + learning_rate_schedule_patience: 2 + in_channels: 4 + num_classes: 14 + num_filters: 64 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.NAIPChesapeakeDataModule + naip_root: "data/naip" + chesapeake_root: "data/chesapeake/BAYWIDE" + batch_size: 32 + num_workers: 4 + patch_size: 32 + trainer: + _target_: lightning.pytorch.Trainer + accelerator: gpu + devices: 1 min_epochs: 15 -experiment: - task: "naipchesapeake" - module: - loss: "ce" - model: "deeplabv3+" - backbone: "resnet34" - weights: "imagenet" - learning_rate: 1e-3 - learning_rate_schedule_patience: 2 - in_channels: 4 - num_classes: 14 - num_filters: 64 - ignore_index: null - datamodule: - naip_root: "data/naip" - chesapeake_root: "data/chesapeake/BAYWIDE" - batch_size: 32 - num_workers: 4 - patch_size: 32 + max_epochs: 40 \ No newline at end of file diff --git a/conf/nasa_marine_debris.yaml b/conf/nasa_marine_debris.yaml index 4908400bec1..89164a63c74 100644 --- a/conf/nasa_marine_debris.yaml +++ b/conf/nasa_marine_debris.yaml @@ -1,26 +1,22 @@ -program: - overwrite: True +module: + _target_: torchgeo.trainers.ObjectDetectionTask + model: "faster-rcnn" + backbone: "resnet50" + num_classes: 2 + learning_rate: 1.2e-4 + learning_rate_schedule_patience: 6 + verbose: false + +datamodule: + _target_: torchgeo.datamodules.NASAMarineDebrisDataModule + root: "data/nasamr/nasa_marine_debris" + batch_size: 4 + num_workers: 6 + val_split_pct: 0.2 trainer: + _target_: lightning.pytorch.Trainer accelerator: gpu devices: 1 - min_epochs: 5 - max_epochs: 100 - auto_lr_find: False - benchmark: True - -experiment: - task: "nasa_marine_debris" - name: "nasa_marine_debris_test" - module: - model: "faster-rcnn" - backbone: "resnet50" - num_classes: 2 - learning_rate: 1.2e-4 - learning_rate_schedule_patience: 6 - verbose: false - datamodule: - root: "data/nasamr/nasa_marine_debris" - batch_size: 4 - num_workers: 6 - val_split_pct: 0.2 + min_epochs: 15 + max_epochs: 40 \ No newline at end of file diff --git a/conf/potsdam2d.yaml b/conf/potsdam2d.yaml index e1312fa57d4..076a1d75f72 100644 --- a/conf/potsdam2d.yaml +++ b/conf/potsdam2d.yaml @@ -1,20 +1,28 @@ -experiment: - task: "potsdam2d" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - verbose: false - in_channels: 4 - num_classes: 6 - num_filters: 1 - ignore_index: null - datamodule: - root: "data/potsdam" - batch_size: 1 - patch_size: 64 - val_split_pct: 0.5 - num_workers: 0 +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + verbose: false + in_channels: 4 + num_classes: 6 + num_filters: 1 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.Potsdam2DDataModule + root: "data/potsdam" + batch_size: 1 + patch_size: 64 + val_split_pct: 0.5 + num_workers: 0 + +trainer: + _target_: lightning.pytorch.Trainer + accelerator: gpu + devices: 1 + min_epochs: 15 + max_epochs: 40 \ No newline at end of file diff --git a/conf/resisc45.yaml b/conf/resisc45.yaml index ad57f856d5d..05978aa5e84 100644 --- a/conf/resisc45.yaml +++ b/conf/resisc45.yaml @@ -1,20 +1,22 @@ +module: + _target_: torchgeo.trainers.ClassificationTask + loss: "ce" + model: "resnet18" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + weights: null + in_channels: 3 + num_classes: 45 + +datamodule: + _target_: torchgeo.datamodules.RESISC45DataModule + root: "data/resisc45" + batch_size: 128 + num_workers: 4 + trainer: + _target_: lightning.pytorch.Trainer accelerator: gpu devices: 1 - min_epochs: 10 - max_epochs: 40 - benchmark: True -experiment: - task: "resisc45" - module: - loss: "ce" - model: "resnet18" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - weights: null - in_channels: 3 - num_classes: 45 - datamodule: - root: "data/resisc45" - batch_size: 128 - num_workers: 4 + min_epochs: 15 + max_epochs: 40 \ No newline at end of file diff --git a/conf/seco_100k.yaml b/conf/seco_100k.yaml new file mode 100644 index 00000000000..e9d83fa4e87 --- /dev/null +++ b/conf/seco_100k.yaml @@ -0,0 +1,24 @@ +module: + _target_: torchgeo.trainers.BYOLTask + in_channels: 12 + backbone: "resnet18" + weights: True + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + optimizer: "Adam" + +datamodule: + _target_: torchgeo.datamodules.SeasonalContrastS2DataModule + root: "data/seco" + version: "100k" + seasons: 2 + bands: ["B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B8A", "B9", "B11", "B12"] + batch_size: 64 + num_workers: 16 + +trainer: + _target_: lightning.pytorch.Trainer + accelerator: gpu + devices: 1 + min_epochs: 15 + max_epochs: 40 \ No newline at end of file diff --git a/conf/sen12ms.yaml b/conf/sen12ms.yaml index 3946774328a..553d5c996e8 100644 --- a/conf/sen12ms.yaml +++ b/conf/sen12ms.yaml @@ -1,22 +1,25 @@ -program: - experiment_name: sen12ms_test - overwrite: True +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 2 + in_channels: 15 + num_classes: 11 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.SEN12MSDataModule + root: "data/sen12ms" + band_set: "all" + batch_size: 32 + num_workers: 4 + trainer: + _target_: lightning.pytorch.Trainer + accelerator: gpu + devices: 1 min_epochs: 15 -experiment: - task: "sen12ms" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 2 - in_channels: 15 - num_classes: 11 - ignore_index: null - datamodule: - root: "data/sen12ms" - band_set: "all" - batch_size: 32 - num_workers: 4 + max_epochs: 40 \ No newline at end of file diff --git a/conf/so2sat.yaml b/conf/so2sat.yaml index f515622e0d8..b54025dfe51 100644 --- a/conf/so2sat.yaml +++ b/conf/so2sat.yaml @@ -1,21 +1,23 @@ +module: + _target_: torchgeo.trainers.ClassificationTask + loss: "ce" + model: "resnet18" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + weights: null + in_channels: 18 + num_classes: 17 + +datamodule: + _target_: torchgeo.datamodules.So2SatDataModule + root: "data/so2sat" + batch_size: 128 + num_workers: 4 + band_set: "all" + trainer: + _target_: lightning.pytorch.Trainer accelerator: gpu devices: 1 - min_epochs: 10 - max_epochs: 40 - benchmark: True -experiment: - task: "so2sat" - module: - loss: "ce" - model: "resnet18" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - weights: null - in_channels: 18 - num_classes: 17 - datamodule: - root: "data/so2sat" - batch_size: 128 - num_workers: 4 - band_set: "all" + min_epochs: 15 + max_epochs: 40 \ No newline at end of file diff --git a/conf/spacenet1.yaml b/conf/spacenet1.yaml index 5162c70c9d4..3bfd735680d 100644 --- a/conf/spacenet1.yaml +++ b/conf/spacenet1.yaml @@ -1,25 +1,24 @@ -program: - overwrite: False +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: "imagenet" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + in_channels: 3 + num_classes: 3 + ignore_index: 0 + +datamodule: + _target_: torchgeo.datamodules.SpaceNet1DataModule + root: "data/spacenet" + batch_size: 32 + num_workers: 4 + trainer: + _target_: lightning.pytorch.Trainer accelerator: gpu - devices: 3 - min_epochs: 50 - max_epochs: 200 - benchmark: True -experiment: - name: "spacenet-example" - task: "sen12ms" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: "imagenet" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - in_channels: 3 - num_classes: 3 - ignore_index: 0 - datamodule: - root: "data/spacenet" - batch_size: 32 - num_workers: 4 + devices: 1 + min_epochs: 15 + max_epochs: 40 \ No newline at end of file diff --git a/conf/ucmerced.yaml b/conf/ucmerced.yaml index 4ab6612d1ae..bae2aab676e 100644 --- a/conf/ucmerced.yaml +++ b/conf/ucmerced.yaml @@ -1,14 +1,22 @@ -experiment: - task: "ucmerced" - module: - loss: "ce" - model: "resnet18" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - in_channels: 3 - num_classes: 21 - datamodule: - root: "data/ucmerced" - batch_size: 128 - num_workers: 4 +module: + _target_: torchgeo.trainers.ClassificationTask + loss: "ce" + model: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + in_channels: 3 + num_classes: 21 + +datamodule: + _target_: torchgeo.datamodules.UCMercedDataModule + root: "data/ucmerced" + batch_size: 128 + num_workers: 4 + +trainer: + _target_: lightning.pytorch.Trainer + accelerator: gpu + devices: 1 + min_epochs: 15 + max_epochs: 40 \ No newline at end of file diff --git a/conf/vaihingen2d.yaml b/conf/vaihingen2d.yaml index c6fd448c6dd..db6248b052f 100644 --- a/conf/vaihingen2d.yaml +++ b/conf/vaihingen2d.yaml @@ -1,20 +1,28 @@ -experiment: - task: "vaihingen2d" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - verbose: false - in_channels: 3 - num_classes: 7 - num_filters: 1 - ignore_index: null - datamodule: - root: "data/vaihingen" - batch_size: 1 - patch_size: 64 - val_split_pct: 0.5 - num_workers: 0 +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + verbose: false + in_channels: 3 + num_classes: 7 + num_filters: 1 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.Vaihingen2DDataModule + root: "data/vaihingen" + batch_size: 1 + patch_size: 64 + val_split_pct: 0.5 + num_workers: 0 + +trainer: + _target_: lightning.pytorch.Trainer + accelerator: gpu + devices: 1 + min_epochs: 15 + max_epochs: 40 \ No newline at end of file diff --git a/environment.yml b/environment.yml index 1f289475f77..51ec988b274 100644 --- a/environment.yml +++ b/environment.yml @@ -21,6 +21,7 @@ dependencies: - pip: - black[jupyter]>=21.8 - flake8>=3.8 + - hydra-core>=1 - ipywidgets>=7 - isort[colors]>=5.8 - kornia>=0.6.5 @@ -29,7 +30,7 @@ dependencies: - mypy>=0.900 - nbmake>=1.3.3 - nbsphinx>=0.8.5 - - omegaconf>=2.1 + - omegaconf>=2.0.1 - opencv-python>=4.4.0.46 - pandas>=1.1.3 - pillow>=8 diff --git a/evaluate.py b/evaluate.py deleted file mode 100755 index beab7c25a8f..00000000000 --- a/evaluate.py +++ /dev/null @@ -1,294 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -"""torchgeo model evaluation script.""" - -import argparse -import csv -import os -from typing import Any, Union, cast - -import lightning.pytorch as pl -import torch -from torchmetrics import MetricCollection -from torchmetrics.classification import BinaryAccuracy, BinaryJaccardIndex - -from torchgeo.trainers import ( - ClassificationTask, - ObjectDetectionTask, - SemanticSegmentationTask, -) -from train import TASK_TO_MODULES_MAPPING - - -def set_up_parser() -> argparse.ArgumentParser: - """Set up the argument parser. - - Returns: - the argument parser - """ - parser = argparse.ArgumentParser( - description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter - ) - - parser.add_argument( - "--task", - choices=TASK_TO_MODULES_MAPPING.keys(), - type=str, - help="name of task to test", - ) - parser.add_argument( - "--input-checkpoint", - required=True, - help="path to the checkpoint file to test", - metavar="CKPT", - ) - parser.add_argument( - "--gpu", default=0, type=int, help="GPU ID to use", metavar="ID" - ) - parser.add_argument( - "--root", - required=True, - type=str, - help="root directory of the dataset for the accompanying task", - ) - parser.add_argument( - "-b", - "--batch-size", - default=2**4, - type=int, - help="number of samples in each mini-batch", - metavar="SIZE", - ) - parser.add_argument( - "-w", - "--num-workers", - default=6, - type=int, - help="number of workers for parallel data loading", - metavar="NUM", - ) - parser.add_argument( - "--seed", default=0, type=int, help="random seed for reproducibility" - ) - parser.add_argument( - "--output-fn", - required=True, - type=str, - help="path to the CSV file to write results", - metavar="FILE", - ) - parser.add_argument( - "-v", "--verbose", action="store_true", help="print results to stdout" - ) - - return parser - - -def run_eval_loop( - model: pl.LightningModule, - dataloader: Any, - device: torch.device, - metrics: MetricCollection, -) -> Any: - """Runs a standard test loop over a dataloader and records metrics. - - Args: - model: the model used for inference - dataloader: the dataloader to get samples from - device: the device to put data on - metrics: a torchmetrics compatible metric collection to score the output - from the model - - Returns: - the result of ``metrics.compute()`` - """ - for batch in dataloader: - x = batch["image"].to(device) - if "mask" in batch: - y = batch["mask"].to(device) - elif "label" in batch: - y = batch["label"].to(device) - elif "boxes" in batch: - y = [ - { - "boxes": batch["boxes"][i].to(device), - "labels": batch["labels"][i].to(device), - } - for i in range(len(batch["image"])) - ] - with torch.inference_mode(): - y_pred = model(x) - metrics(y_pred, y) - results = metrics.compute() - metrics.reset() - return results - - -def main(args: argparse.Namespace) -> None: - """High-level pipeline. - - Runs a model checkpoint on a test set and saves results to file. - - Args: - args: command-line arguments - """ - assert os.path.exists(args.input_checkpoint) - assert os.path.exists(args.root) - TASK = TASK_TO_MODULES_MAPPING[args.task][0] - DATAMODULE = TASK_TO_MODULES_MAPPING[args.task][1] - - # Loads the saved model from checkpoint based on the `args.task` name that was - # passed as input - model = TASK.load_from_checkpoint(args.input_checkpoint) - model.freeze() - model.eval() - - dm = DATAMODULE( - seed=args.seed, - root=args.root, - num_workers=args.num_workers, - batch_size=args.batch_size, - ) - dm.setup("validate") - - # Record model hyperparameters - hparams = cast(dict[str, Union[str, float]], model.hparams) - if issubclass(TASK, ClassificationTask): - val_row = { - "split": "val", - "model": hparams["model"], - "learning_rate": hparams["learning_rate"], - "weights": hparams["weights"], - "loss": hparams["loss"], - } - - test_row = { - "split": "test", - "model": hparams["model"], - "learning_rate": hparams["learning_rate"], - "weights": hparams["weights"], - "loss": hparams["loss"], - } - elif issubclass(TASK, SemanticSegmentationTask): - val_row = { - "split": "val", - "model": hparams["model"], - "backbone": hparams["backbone"], - "weights": hparams["weights"], - "learning_rate": hparams["learning_rate"], - "loss": hparams["loss"], - } - - test_row = { - "split": "test", - "model": hparams["model"], - "backbone": hparams["backbone"], - "weights": hparams["weights"], - "learning_rate": hparams["learning_rate"], - "loss": hparams["loss"], - } - elif issubclass(TASK, ObjectDetectionTask): - val_row = { - "split": "val", - "detection_model": hparams["detection_model"], - "backbone": hparams["backbone"], - "learning_rate": hparams["learning_rate"], - } - - test_row = { - "split": "test", - "detection_model": hparams["detection_model"], - "backbone": hparams["backbone"], - "learning_rate": hparams["learning_rate"], - } - else: - raise ValueError(f"{TASK} is not supported") - - # Compute metrics - device = torch.device("cuda:%d" % (args.gpu)) - model = model.to(device) - - if args.task == "etci2021": # Custom metric setup for testing ETCI2021 - metrics = MetricCollection([BinaryAccuracy(), BinaryJaccardIndex()]).to(device) - - val_results = run_eval_loop(model, dm.val_dataloader(), device, metrics) - test_results = run_eval_loop(model, dm.test_dataloader(), device, metrics) - - val_row.update( - { - "overall_accuracy": val_results["Accuracy"].item(), - "jaccard_index": val_results["JaccardIndex"][1].item(), - } - ) - test_row.update( - { - "overall_accuracy": test_results["Accuracy"].item(), - "jaccard_index": test_results["JaccardIndex"][1].item(), - } - ) - else: # Test with PyTorch Lightning as usual - model.val_metrics = cast(MetricCollection, model.val_metrics) - model.test_metrics = cast(MetricCollection, model.test_metrics) - - val_results = run_eval_loop( - model, dm.val_dataloader(), device, model.val_metrics - ) - test_results = run_eval_loop( - model, dm.test_dataloader(), device, model.test_metrics - ) - - # Save the results and model hyperparameters to a CSV file - if issubclass(TASK, ClassificationTask): - val_row.update( - { - "average_accuracy": val_results["val_AverageAccuracy"].item(), - "overall_accuracy": val_results["val_OverallAccuracy"].item(), - } - ) - test_row.update( - { - "average_accuracy": test_results["test_AverageAccuracy"].item(), - "overall_accuracy": test_results["test_OverallAccuracy"].item(), - } - ) - elif issubclass(TASK, SemanticSegmentationTask): - val_row.update( - { - "overall_accuracy": val_results["val_Accuracy"].item(), - "jaccard_index": val_results["val_JaccardIndex"].item(), - } - ) - test_row.update( - { - "overall_accuracy": test_results["test_Accuracy"].item(), - "jaccard_index": test_results["test_JaccardIndex"].item(), - } - ) - elif issubclass(TASK, ObjectDetectionTask): - val_row.update({"map": val_results["map"].item()}) - test_row.update({"map": test_results["map"].item()}) - - assert set(val_row.keys()) == set(test_row.keys()) - fieldnames = list(test_row.keys()) - - # Write to file - if not os.path.exists(args.output_fn): - with open(args.output_fn, "w") as f: - writer = csv.DictWriter(f, fieldnames=fieldnames) - writer.writeheader() - with open(args.output_fn, "a") as f: - writer = csv.DictWriter(f, fieldnames=fieldnames) - writer.writerow(val_row) - writer.writerow(test_row) - - -if __name__ == "__main__": - parser = set_up_parser() - args = parser.parse_args() - - pl.seed_everything(args.seed) - - main(args) diff --git a/requirements/min-reqs.old b/requirements/min-reqs.old index 8c8e9bd32c2..c484fb51efc 100644 --- a/requirements/min-reqs.old +++ b/requirements/min-reqs.old @@ -46,9 +46,10 @@ pydocstyle[toml]==6.1.0 pyupgrade==2.8.0 # tests +hydra-core==1.0.0 mypy==0.900 nbmake==1.3.3 -omegaconf==2.1.0 +omegaconf==2.0.1 pytest==6.1.2 pytest-cov==2.4.0 tensorboard==2.9.1 diff --git a/requirements/tests.txt b/requirements/tests.txt index 8330072dde0..284d6f858ab 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1,4 +1,5 @@ # tests +hydra-core==1.3.2 mypy==1.2.0 nbmake==1.4.1 omegaconf==2.3.0 diff --git a/setup.cfg b/setup.cfg index 98028441d90..d4dcefc9a39 100644 --- a/setup.cfg +++ b/setup.cfg @@ -117,12 +117,14 @@ style = # pyupgrade 2.8+ required for --py39-plus flag pyupgrade>=2.8,<4 tests = + # hydra-core 1+ required for omegaconf 2 support + hydra-core>=1 # mypy 0.900+ required for pyproject.toml support mypy>=0.900,<2 # nbmake 1.3.3+ required for variable mocking nbmake>=1.3.3,<2 - # omegaconf 2.1+ required for to_object method - omegaconf>=2.1,<3 + # omegaconf 2+ required by lightning, 2.0.1+ required by hydra-core + omegaconf>=2.0.1 # pytest 6.1.2+ required by nbmake pytest>=6.1.2,<8 # pytest-cov 2.4+ required for pytest --cov flags diff --git a/tests/conf/bigearthnet_all.yaml b/tests/conf/bigearthnet_all.yaml index e885c9db4c7..f034c155b9b 100644 --- a/tests/conf/bigearthnet_all.yaml +++ b/tests/conf/bigearthnet_all.yaml @@ -1,17 +1,18 @@ -experiment: - task: "bigearthnet" - module: - loss: "bce" - model: "resnet18" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - weights: null - in_channels: 14 - num_classes: 19 - datamodule: - root: "tests/data/bigearthnet" - bands: "all" - num_classes: ${experiment.module.num_classes} - download: true - batch_size: 1 - num_workers: 0 +module: + _target_: torchgeo.trainers.MultiLabelClassificationTask + loss: "bce" + model: "resnet18" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + weights: null + in_channels: 14 + num_classes: 19 + +datamodule: + _target_: torchgeo.datamodules.BigEarthNetDataModule + root: "tests/data/bigearthnet" + bands: "all" + num_classes: ${module.num_classes} + download: true + batch_size: 1 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/bigearthnet_s1.yaml b/tests/conf/bigearthnet_s1.yaml index 09b71cbd84c..fa49d81c775 100644 --- a/tests/conf/bigearthnet_s1.yaml +++ b/tests/conf/bigearthnet_s1.yaml @@ -1,17 +1,18 @@ -experiment: - task: "bigearthnet" - module: - loss: "bce" - model: "resnet18" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - weights: null - in_channels: 2 - num_classes: 19 - datamodule: - root: "tests/data/bigearthnet" - bands: "s1" - num_classes: ${experiment.module.num_classes} - download: true - batch_size: 1 - num_workers: 0 +module: + _target_: torchgeo.trainers.MultiLabelClassificationTask + loss: "bce" + model: "resnet18" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + weights: null + in_channels: 2 + num_classes: 19 + +datamodule: + _target_: torchgeo.datamodules.BigEarthNetDataModule + root: "tests/data/bigearthnet" + bands: "s1" + num_classes: ${module.num_classes} + download: true + batch_size: 1 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/bigearthnet_s2.yaml b/tests/conf/bigearthnet_s2.yaml index 487b1433810..3677de83c79 100644 --- a/tests/conf/bigearthnet_s2.yaml +++ b/tests/conf/bigearthnet_s2.yaml @@ -1,17 +1,18 @@ -experiment: - task: "bigearthnet" - module: - loss: "bce" - model: "resnet18" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - weights: null - in_channels: 12 - num_classes: 19 - datamodule: - root: "tests/data/bigearthnet" - bands: "s2" - num_classes: ${experiment.module.num_classes} - download: true - batch_size: 1 - num_workers: 0 +module: + _target_: torchgeo.trainers.MultiLabelClassificationTask + loss: "bce" + model: "resnet18" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + weights: null + in_channels: 12 + num_classes: 19 + +datamodule: + _target_: torchgeo.datamodules.BigEarthNetDataModule + root: "tests/data/bigearthnet" + bands: "s2" + num_classes: ${module.num_classes} + download: true + batch_size: 1 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/chesapeake_cvpr_5.yaml b/tests/conf/chesapeake_cvpr_5.yaml index 7ef269dd661..b4f345c3ab8 100644 --- a/tests/conf/chesapeake_cvpr_5.yaml +++ b/tests/conf/chesapeake_cvpr_5.yaml @@ -1,27 +1,28 @@ -experiment: - task: "chesapeake_cvpr" - module: - loss: "ce" - model: "unet" - backbone: "resnet50" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - in_channels: 4 - num_classes: 5 - num_filters: 1 - ignore_index: null - datamodule: - root: "tests/data/chesapeake/cvpr" - download: true - train_splits: - - "de-test" - val_splits: - - "de-test" - test_splits: - - "de-test" - batch_size: 2 - patch_size: 64 - num_workers: 0 - class_set: ${experiment.module.num_classes} - use_prior_labels: False +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet50" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + in_channels: 4 + num_classes: 5 + num_filters: 1 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.ChesapeakeCVPRDataModule + root: "tests/data/chesapeake/cvpr" + download: true + train_splits: + - "de-test" + val_splits: + - "de-test" + test_splits: + - "de-test" + batch_size: 2 + patch_size: 64 + num_workers: 0 + class_set: ${module.num_classes} + use_prior_labels: False \ No newline at end of file diff --git a/tests/conf/chesapeake_cvpr_7.yaml b/tests/conf/chesapeake_cvpr_7.yaml index 653f4934ca0..634440e680e 100644 --- a/tests/conf/chesapeake_cvpr_7.yaml +++ b/tests/conf/chesapeake_cvpr_7.yaml @@ -1,27 +1,28 @@ -experiment: - task: "chesapeake_cvpr" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - in_channels: 4 - num_classes: 7 - num_filters: 1 - ignore_index: null - weights: null - datamodule: - root: "tests/data/chesapeake/cvpr" - download: true - train_splits: - - "de-test" - val_splits: - - "de-test" - test_splits: - - "de-test" - batch_size: 2 - patch_size: 64 - num_workers: 0 - class_set: ${experiment.module.num_classes} - use_prior_labels: False +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + in_channels: 4 + num_classes: 7 + num_filters: 1 + ignore_index: null + weights: null + +datamodule: + _target_: torchgeo.datamodules.ChesapeakeCVPRDataModule + root: "tests/data/chesapeake/cvpr" + download: true + train_splits: + - "de-test" + val_splits: + - "de-test" + test_splits: + - "de-test" + batch_size: 2 + patch_size: 64 + num_workers: 0 + class_set: ${module.num_classes} + use_prior_labels: False \ No newline at end of file diff --git a/tests/conf/chesapeake_cvpr_prior_byol.yaml b/tests/conf/chesapeake_cvpr_prior_byol.yaml index 3e9713fbb59..6b6841d8f65 100644 --- a/tests/conf/chesapeake_cvpr_prior_byol.yaml +++ b/tests/conf/chesapeake_cvpr_prior_byol.yaml @@ -1,27 +1,23 @@ -experiment: - task: "chesapeake_cvpr" - module: - loss: "ce" - model: "unet" - backbone: "resnet50" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - in_channels: 4 - num_classes: 5 - num_filters: 1 - ignore_index: null - weights: null - datamodule: - root: "tests/data/chesapeake/cvpr" - download: true - train_splits: - - "de-test" - val_splits: - - "de-test" - test_splits: - - "de-test" - batch_size: 2 - patch_size: 64 - num_workers: 0 - class_set: ${experiment.module.num_classes} - use_prior_labels: True +module: + _target_: torchgeo.trainers.BYOLTask + in_channels: 4 + backbone: "resnet18" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + weights: null + +datamodule: + _target_: torchgeo.datamodules.ChesapeakeCVPRDataModule + root: "tests/data/chesapeake/cvpr" + download: true + train_splits: + - "de-test" + val_splits: + - "de-test" + test_splits: + - "de-test" + batch_size: 2 + patch_size: 64 + num_workers: 0 + class_set: 5 + use_prior_labels: True \ No newline at end of file diff --git a/tests/conf/cowc_counting.yaml b/tests/conf/cowc_counting.yaml index fc3218e8fef..76eb04763a6 100644 --- a/tests/conf/cowc_counting.yaml +++ b/tests/conf/cowc_counting.yaml @@ -1,14 +1,15 @@ -experiment: - task: cowc_counting - module: - model: resnet18 - weights: null - num_outputs: 1 - in_channels: 3 - learning_rate: 1e-3 - learning_rate_schedule_patience: 2 - datamodule: - root: "tests/data/cowc_counting" - download: true - batch_size: 1 - num_workers: 0 +module: + _target_: torchgeo.trainers.RegressionTask + model: resnet18 + weights: null + num_outputs: 1 + in_channels: 3 + learning_rate: 1e-3 + learning_rate_schedule_patience: 2 + +datamodule: + _target_: torchgeo.datamodules.COWCCountingDataModule + root: "tests/data/cowc_counting" + download: true + batch_size: 1 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/cyclone.yaml b/tests/conf/cyclone.yaml index b3323d28999..91a477a144d 100644 --- a/tests/conf/cyclone.yaml +++ b/tests/conf/cyclone.yaml @@ -1,14 +1,15 @@ -experiment: - task: "cyclone" - module: - model: "resnet18" - weights: null - num_outputs: 1 - in_channels: 3 - learning_rate: 1e-3 - learning_rate_schedule_patience: 2 - datamodule: - root: "tests/data/cyclone" - download: true - batch_size: 1 - num_workers: 0 +module: + _target_: torchgeo.trainers.RegressionTask + model: "resnet18" + weights: null + num_outputs: 1 + in_channels: 3 + learning_rate: 1e-3 + learning_rate_schedule_patience: 2 + +datamodule: + _target_: torchgeo.datamodules.TropicalCycloneDataModule + root: "tests/data/cyclone" + download: true + batch_size: 1 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/deepglobelandcover.yaml b/tests/conf/deepglobelandcover.yaml index e27fe1271c2..09b0f4d9414 100644 --- a/tests/conf/deepglobelandcover.yaml +++ b/tests/conf/deepglobelandcover.yaml @@ -1,20 +1,21 @@ -experiment: - task: "deepglobelandcover" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - verbose: false - in_channels: 3 - num_classes: 7 - num_filters: 1 - ignore_index: null - datamodule: - root: "tests/data/deepglobelandcover" - batch_size: 1 - patch_size: 2 - val_split_pct: 0.5 - num_workers: 0 +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + verbose: false + in_channels: 3 + num_classes: 7 + num_filters: 1 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.DeepGlobeLandCoverDataModule + root: "tests/data/deepglobelandcover" + batch_size: 1 + patch_size: 2 + val_split_pct: 0.5 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/etci2021.yaml b/tests/conf/etci2021.yaml index cbb766ea522..65c75374431 100644 --- a/tests/conf/etci2021.yaml +++ b/tests/conf/etci2021.yaml @@ -1,17 +1,18 @@ -experiment: - task: "etci2021" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - in_channels: 6 - num_classes: 2 - ignore_index: 0 - datamodule: - root: "tests/data/etci2021" - download: true - batch_size: 1 - num_workers: 0 +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + in_channels: 6 + num_classes: 2 + ignore_index: 0 + +datamodule: + _target_: torchgeo.datamodules.ETCI2021DataModule + root: "tests/data/etci2021" + download: true + batch_size: 1 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/eurosat.yaml b/tests/conf/eurosat.yaml index a4cbc9eb525..8e39dd50557 100644 --- a/tests/conf/eurosat.yaml +++ b/tests/conf/eurosat.yaml @@ -1,15 +1,16 @@ -experiment: - task: "eurosat" - module: - loss: "ce" - model: "resnet18" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - weights: null - in_channels: 13 - num_classes: 2 - datamodule: - root: "tests/data/eurosat" - download: true - batch_size: 1 - num_workers: 0 +module: + _target_: torchgeo.trainers.ClassificationTask + loss: "ce" + model: "resnet18" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + weights: null + in_channels: 13 + num_classes: 2 + +datamodule: + _target_: torchgeo.datamodules.EuroSATDataModule + root: "tests/data/eurosat" + download: true + batch_size: 1 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/eurosat100.yaml b/tests/conf/eurosat100.yaml new file mode 100644 index 00000000000..b1e5fe6438b --- /dev/null +++ b/tests/conf/eurosat100.yaml @@ -0,0 +1,16 @@ +module: + _target_: torchgeo.trainers.ClassificationTask + loss: "ce" + model: "resnet18" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + weights: null + in_channels: 13 + num_classes: 2 + +datamodule: + _target_: torchgeo.datamodules.EuroSAT100DataModule + root: "tests/data/eurosat" + download: true + batch_size: 1 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/fire_risk.yaml b/tests/conf/fire_risk.yaml index 4c13aeb05fd..0c86285235a 100644 --- a/tests/conf/fire_risk.yaml +++ b/tests/conf/fire_risk.yaml @@ -1,15 +1,16 @@ -experiment: - task: "fire_risk" - module: - loss: "ce" - model: "resnet18" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - weights: null - in_channels: 3 - num_classes: 5 - datamodule: - root: "tests/data/fire_risk" - download: false - batch_size: 1 - num_workers: 0 +module: + _target_: torchgeo.trainers.ClassificationTask + loss: "ce" + model: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + in_channels: 3 + num_classes: 5 + +datamodule: + _target_: torchgeo.datamodules.FireRiskDataModule + root: "tests/data/fire_risk" + download: false + batch_size: 2 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/gid15.yaml b/tests/conf/gid15.yaml index baaea0e1ba2..3af0a01f24e 100644 --- a/tests/conf/gid15.yaml +++ b/tests/conf/gid15.yaml @@ -1,21 +1,22 @@ -experiment: - task: "gid15" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - verbose: false - in_channels: 3 - num_classes: 16 - num_filters: 1 - ignore_index: null - datamodule: - root: "tests/data/gid15" - download: true - batch_size: 1 - patch_size: 2 - val_split_pct: 0.5 - num_workers: 0 +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + verbose: false + in_channels: 3 + num_classes: 16 + num_filters: 1 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.GID15DataModule + root: "tests/data/gid15" + download: true + batch_size: 1 + patch_size: 2 + val_split_pct: 0.5 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/inria.yaml b/tests/conf/inria.yaml index 995c073146b..04af3433f1e 100644 --- a/tests/conf/inria.yaml +++ b/tests/conf/inria.yaml @@ -1,19 +1,20 @@ -experiment: - task: "inria" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: "imagenet" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - in_channels: 3 - num_classes: 2 - ignore_index: null - datamodule: - root: "tests/data/inria" - batch_size: 1 - patch_size: 2 - num_workers: 0 - val_split_pct: 0.2 - test_split_pct: 0.2 +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: "imagenet" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + in_channels: 3 + num_classes: 2 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.InriaAerialImageLabelingDataModule + root: "tests/data/inria" + batch_size: 1 + patch_size: 2 + num_workers: 0 + val_split_pct: 0.2 + test_split_pct: 0.2 \ No newline at end of file diff --git a/tests/conf/l7irish.yaml b/tests/conf/l7irish.yaml index 1946e80ce2d..cb54362d964 100644 --- a/tests/conf/l7irish.yaml +++ b/tests/conf/l7irish.yaml @@ -1,21 +1,22 @@ -experiment: - task: "l7irish" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - verbose: false - in_channels: 9 - num_classes: 5 - num_filters: 1 - ignore_index: 0 - datamodule: - root: "tests/data/l7irish" - download: true - batch_size: 1 - patch_size: 32 - length: 5 - num_workers: 0 \ No newline at end of file +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + verbose: false + in_channels: 9 + num_classes: 5 + num_filters: 1 + ignore_index: 0 + +datamodule: + _target_: torchgeo.datamodules.L7IrishDataModule + root: "tests/data/l7irish" + download: true + batch_size: 1 + patch_size: 32 + length: 5 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/l8biome.yaml b/tests/conf/l8biome.yaml index b04211074b2..796266d2e24 100644 --- a/tests/conf/l8biome.yaml +++ b/tests/conf/l8biome.yaml @@ -1,21 +1,22 @@ -experiment: - task: "l8biome" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - verbose: false - in_channels: 11 - num_classes: 5 - num_filters: 1 - ignore_index: null - datamodule: - root: "tests/data/l8biome" - download: true - batch_size: 1 - patch_size: 32 - length: 5 - num_workers: 0 \ No newline at end of file +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + verbose: false + in_channels: 11 + num_classes: 5 + num_filters: 1 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.L8BiomeDataModule + root: "tests/data/l8biome" + download: true + batch_size: 1 + patch_size: 32 + length: 5 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/landcoverai.yaml b/tests/conf/landcoverai.yaml index 9bffc96b83d..20ec3653471 100644 --- a/tests/conf/landcoverai.yaml +++ b/tests/conf/landcoverai.yaml @@ -1,19 +1,20 @@ -experiment: - task: "landcoverai" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - verbose: false - in_channels: 3 - num_classes: 6 - num_filters: 1 - ignore_index: null - datamodule: - root: "tests/data/landcoverai" - download: true - batch_size: 1 - num_workers: 0 +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + verbose: false + in_channels: 3 + num_classes: 6 + num_filters: 1 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.LandCoverAIDataModule + root: "tests/data/landcoverai" + download: true + batch_size: 1 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/loveda.yaml b/tests/conf/loveda.yaml index df062a0e600..92f324cb018 100644 --- a/tests/conf/loveda.yaml +++ b/tests/conf/loveda.yaml @@ -1,19 +1,20 @@ -experiment: - task: "loveda" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - verbose: false - in_channels: 3 - num_classes: 8 - num_filters: 1 - ignore_index: null - datamodule: - root: "tests/data/loveda" - download: true - batch_size: 1 - num_workers: 0 +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + verbose: false + in_channels: 3 + num_classes: 8 + num_filters: 1 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.LoveDADataModule + root: "tests/data/loveda" + download: true + batch_size: 1 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/naipchesapeake.yaml b/tests/conf/naipchesapeake.yaml index 9cd0e2beb96..06aa921a6dd 100644 --- a/tests/conf/naipchesapeake.yaml +++ b/tests/conf/naipchesapeake.yaml @@ -1,20 +1,21 @@ -experiment: - task: "naipchesapeake" - module: - loss: "ce" - model: "deeplabv3+" - backbone: "resnet34" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 2 - in_channels: 4 - num_classes: 14 - num_filters: 1 - ignore_index: null - datamodule: - naip_root: "tests/data/naip" - chesapeake_root: "tests/data/chesapeake/BAYWIDE" - chesapeake_download: true - batch_size: 2 - num_workers: 0 - patch_size: 32 +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "deeplabv3+" + backbone: "resnet34" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 2 + in_channels: 4 + num_classes: 14 + num_filters: 1 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.NAIPChesapeakeDataModule + naip_root: "tests/data/naip" + chesapeake_root: "tests/data/chesapeake/BAYWIDE" + chesapeake_download: true + batch_size: 2 + num_workers: 0 + patch_size: 32 \ No newline at end of file diff --git a/tests/conf/nasa_marine_debris.yaml b/tests/conf/nasa_marine_debris.yaml index 5528b38c39c..01e6de32916 100644 --- a/tests/conf/nasa_marine_debris.yaml +++ b/tests/conf/nasa_marine_debris.yaml @@ -1,14 +1,15 @@ -experiment: - task: "nasa_marine_debris" - module: - model: "faster-rcnn" - backbone: "resnet18" - num_classes: 2 - learning_rate: 1.2e-4 - learning_rate_schedule_patience: 6 - verbose: false - datamodule: - root: "tests/data/nasa_marine_debris" - download: true - batch_size: 1 - num_workers: 0 +module: + _target_: torchgeo.trainers.ObjectDetectionTask + model: "faster-rcnn" + backbone: "resnet18" + num_classes: 2 + learning_rate: 1.2e-4 + learning_rate_schedule_patience: 6 + verbose: false + +datamodule: + _target_: torchgeo.datamodules.NASAMarineDebrisDataModule + root: "tests/data/nasa_marine_debris" + download: true + batch_size: 1 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/potsdam2d.yaml b/tests/conf/potsdam2d.yaml index 7492a8c0c86..9ac40d93681 100644 --- a/tests/conf/potsdam2d.yaml +++ b/tests/conf/potsdam2d.yaml @@ -1,20 +1,21 @@ -experiment: - task: "potsdam2d" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - verbose: false - in_channels: 4 - num_classes: 6 - num_filters: 1 - ignore_index: null - datamodule: - root: "tests/data/potsdam" - batch_size: 1 - patch_size: 2 - val_split_pct: 0.5 - num_workers: 0 +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + verbose: false + in_channels: 4 + num_classes: 6 + num_filters: 1 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.Potsdam2DDataModule + root: "tests/data/potsdam" + batch_size: 1 + patch_size: 2 + val_split_pct: 0.5 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/resisc45.yaml b/tests/conf/resisc45.yaml index fd354ad09f8..7dee7bc43fe 100644 --- a/tests/conf/resisc45.yaml +++ b/tests/conf/resisc45.yaml @@ -1,15 +1,16 @@ -experiment: - task: "resisc45" - module: - loss: "ce" - model: "resnet18" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - weights: null - in_channels: 3 - num_classes: 3 - datamodule: - root: "tests/data/resisc45" - download: true - batch_size: 1 - num_workers: 0 +module: + _target_: torchgeo.trainers.ClassificationTask + loss: "ce" + model: "resnet18" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + weights: null + in_channels: 3 + num_classes: 3 + +datamodule: + _target_: torchgeo.datamodules.RESISC45DataModule + root: "tests/data/resisc45" + download: true + batch_size: 1 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/seco_byol_1.yaml b/tests/conf/seco_byol_1.yaml index 50379b07f68..5f7e0b91b20 100644 --- a/tests/conf/seco_byol_1.yaml +++ b/tests/conf/seco_byol_1.yaml @@ -1,13 +1,14 @@ -experiment: - task: "seco" - module: - in_channels: 3 - backbone: "resnet18" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - weights: null - datamodule: - root: "tests/data/seco" - seasons: 1 - batch_size: 2 - num_workers: 0 +module: + _target_: torchgeo.trainers.BYOLTask + in_channels: 3 + backbone: "resnet18" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + weights: null + +datamodule: + _target_: torchgeo.datamodules.SeasonalContrastS2DataModule + root: "tests/data/seco" + seasons: 1 + batch_size: 2 + num_workers: 0 diff --git a/tests/conf/seco_byol_2.yaml b/tests/conf/seco_byol_2.yaml index e07354cb2a2..07ff81c0132 100644 --- a/tests/conf/seco_byol_2.yaml +++ b/tests/conf/seco_byol_2.yaml @@ -1,13 +1,14 @@ -experiment: - task: "seco" - module: - in_channels: 3 - backbone: "resnet18" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - weights: null - datamodule: - root: "tests/data/seco" - seasons: 2 - batch_size: 2 - num_workers: 0 +module: + _target_: torchgeo.trainers.BYOLTask + in_channels: 3 + backbone: "resnet18" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + weights: null + +datamodule: + _target_: torchgeo.datamodules.SeasonalContrastS2DataModule + root: "tests/data/seco" + seasons: 2 + batch_size: 2 + num_workers: 0 diff --git a/tests/conf/sen12ms_all.yaml b/tests/conf/sen12ms_all.yaml index e5676876550..0bdbc54ddff 100644 --- a/tests/conf/sen12ms_all.yaml +++ b/tests/conf/sen12ms_all.yaml @@ -1,17 +1,18 @@ -experiment: - task: "sen12ms" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 2 - in_channels: 15 - num_classes: 11 - ignore_index: null - datamodule: - root: "tests/data/sen12ms" - band_set: "all" - batch_size: 1 - num_workers: 0 +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 2 + in_channels: 15 + num_classes: 11 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.SEN12MSDataModule + root: "tests/data/sen12ms" + band_set: "all" + batch_size: 1 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/sen12ms_s1.yaml b/tests/conf/sen12ms_s1.yaml index 5289c3c8b63..8cf4435c624 100644 --- a/tests/conf/sen12ms_s1.yaml +++ b/tests/conf/sen12ms_s1.yaml @@ -1,18 +1,19 @@ -experiment: - task: "sen12ms" - module: - loss: "focal" - model: "fcn" - num_filters: 1 - backbone: "resnet18" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 2 - in_channels: 2 - num_classes: 11 - ignore_index: null - datamodule: - root: "tests/data/sen12ms" - band_set: "s1" - batch_size: 1 - num_workers: 0 +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "focal" + model: "fcn" + num_filters: 1 + backbone: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 2 + in_channels: 2 + num_classes: 11 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.SEN12MSDataModule + root: "tests/data/sen12ms" + band_set: "s1" + batch_size: 1 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/sen12ms_s2_all.yaml b/tests/conf/sen12ms_s2_all.yaml index f1499b523e3..a7712cf4a78 100644 --- a/tests/conf/sen12ms_s2_all.yaml +++ b/tests/conf/sen12ms_s2_all.yaml @@ -1,17 +1,18 @@ -experiment: - task: "sen12ms" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 2 - in_channels: 13 - num_classes: 11 - ignore_index: null - datamodule: - root: "tests/data/sen12ms" - band_set: "s2-all" - batch_size: 1 - num_workers: 0 +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 2 + in_channels: 13 + num_classes: 11 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.SEN12MSDataModule + root: "tests/data/sen12ms" + band_set: "s2-all" + batch_size: 1 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/sen12ms_s2_reduced.yaml b/tests/conf/sen12ms_s2_reduced.yaml index 72e85b56fc3..9493519da2d 100644 --- a/tests/conf/sen12ms_s2_reduced.yaml +++ b/tests/conf/sen12ms_s2_reduced.yaml @@ -1,17 +1,18 @@ -experiment: - task: "sen12ms" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 2 - in_channels: 6 - num_classes: 11 - ignore_index: null - datamodule: - root: "tests/data/sen12ms" - band_set: "s2-reduced" - batch_size: 1 - num_workers: 0 +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 2 + in_channels: 6 + num_classes: 11 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.SEN12MSDataModule + root: "tests/data/sen12ms" + band_set: "s2-reduced" + batch_size: 1 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/skippd.yaml b/tests/conf/skippd.yaml index 8f1c1cb655f..20ca10f24a6 100644 --- a/tests/conf/skippd.yaml +++ b/tests/conf/skippd.yaml @@ -1,14 +1,15 @@ -experiment: - task: "skippd" - module: - model: "resnet18" - weights: null - num_outputs: 1 - in_channels: 3 - learning_rate: 1e-3 - learning_rate_schedule_patience: 2 - datamodule: - root: "tests/data/skippd" - download: true - batch_size: 1 - num_workers: 0 \ No newline at end of file +module: + _target_: torchgeo.trainers.RegressionTask + model: "resnet18" + weights: null + num_outputs: 1 + in_channels: 3 + learning_rate: 1e-3 + learning_rate_schedule_patience: 2 + +datamodule: + _target_: torchgeo.datamodules.SKIPPDDataModule + root: "tests/data/skippd" + download: true + batch_size: 1 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/so2sat_all.yaml b/tests/conf/so2sat_all.yaml index a8d8c0bb8e3..1033918e0ff 100644 --- a/tests/conf/so2sat_all.yaml +++ b/tests/conf/so2sat_all.yaml @@ -1,15 +1,16 @@ -experiment: - task: "so2sat" - module: - loss: "ce" - model: "resnet18" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - weights: null - in_channels: 18 - num_classes: 17 - datamodule: - root: "tests/data/so2sat" - batch_size: 1 - num_workers: 0 - band_set: "all" +module: + _target_: torchgeo.trainers.ClassificationTask + loss: "ce" + model: "resnet18" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + weights: null + in_channels: 18 + num_classes: 17 + +datamodule: + _target_: torchgeo.datamodules.So2SatDataModule + root: "tests/data/so2sat" + batch_size: 1 + num_workers: 0 + band_set: "all" \ No newline at end of file diff --git a/tests/conf/so2sat_s1.yaml b/tests/conf/so2sat_s1.yaml index 8c87ff55a53..44a437d0ec5 100644 --- a/tests/conf/so2sat_s1.yaml +++ b/tests/conf/so2sat_s1.yaml @@ -1,15 +1,16 @@ -experiment: - task: "so2sat" - module: - loss: "focal" - model: "resnet18" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - weights: null - in_channels: 8 - num_classes: 17 - datamodule: - root: "tests/data/so2sat" - batch_size: 1 - num_workers: 0 - band_set: "s1" +module: + _target_: torchgeo.trainers.ClassificationTask + loss: "focal" + model: "resnet18" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + weights: null + in_channels: 8 + num_classes: 17 + +datamodule: + _target_: torchgeo.datamodules.So2SatDataModule + root: "tests/data/so2sat" + batch_size: 1 + num_workers: 0 + band_set: "s1" \ No newline at end of file diff --git a/tests/conf/so2sat_s2.yaml b/tests/conf/so2sat_s2.yaml index ab9c573a197..b7474bc7705 100644 --- a/tests/conf/so2sat_s2.yaml +++ b/tests/conf/so2sat_s2.yaml @@ -1,15 +1,16 @@ -experiment: - task: "so2sat" - module: - loss: "jaccard" - model: "resnet18" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - weights: null - in_channels: 10 - num_classes: 17 - datamodule: - root: "tests/data/so2sat" - batch_size: 1 - num_workers: 0 - band_set: "s2" +module: + _target_: torchgeo.trainers.ClassificationTask + loss: "jaccard" + model: "resnet18" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + weights: null + in_channels: 10 + num_classes: 17 + +datamodule: + _target_: torchgeo.datamodules.So2SatDataModule + root: "tests/data/so2sat" + batch_size: 1 + num_workers: 0 + band_set: "s2" \ No newline at end of file diff --git a/tests/conf/spacenet1.yaml b/tests/conf/spacenet1.yaml index 3f05a745573..e4feb50a37e 100644 --- a/tests/conf/spacenet1.yaml +++ b/tests/conf/spacenet1.yaml @@ -1,21 +1,22 @@ -experiment: - task: "spacenet1" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - verbose: false - in_channels: 3 - num_classes: 3 - num_filters: 1 - ignore_index: null - datamodule: - root: "tests/data/spacenet" - download: true - batch_size: 1 - num_workers: 0 - val_split_pct: 0.33 - test_split_pct: 0.33 +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + verbose: false + in_channels: 3 + num_classes: 3 + num_filters: 1 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.SpaceNet1DataModule + root: "tests/data/spacenet" + download: true + batch_size: 1 + num_workers: 0 + val_split_pct: 0.33 + test_split_pct: 0.33 \ No newline at end of file diff --git a/tests/conf/ssl4eo_s12_byol_1.yaml b/tests/conf/ssl4eo_s12_byol_1.yaml index f9b99601efc..0bc3267ecc0 100644 --- a/tests/conf/ssl4eo_s12_byol_1.yaml +++ b/tests/conf/ssl4eo_s12_byol_1.yaml @@ -1,13 +1,14 @@ -experiment: - task: "ssl4eo_s12" - module: - in_channels: 13 - backbone: "resnet18" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - weights: null - datamodule: - root: "tests/data/ssl4eo/s12" - seasons: 1 - batch_size: 2 - num_workers: 0 +module: + _target_: torchgeo.trainers.BYOLTask + in_channels: 13 + backbone: "resnet18" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + weights: null + +datamodule: + _target_: torchgeo.datamodules.SSL4EOS12DataModule + root: "tests/data/ssl4eo/s12" + seasons: 1 + batch_size: 2 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/ssl4eo_s12_byol_2.yaml b/tests/conf/ssl4eo_s12_byol_2.yaml index 7679454bf93..cced864fc6e 100644 --- a/tests/conf/ssl4eo_s12_byol_2.yaml +++ b/tests/conf/ssl4eo_s12_byol_2.yaml @@ -1,13 +1,14 @@ -experiment: - task: "ssl4eo_s12" - module: - in_channels: 13 - backbone: "resnet18" - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - weights: null - datamodule: - root: "tests/data/ssl4eo/s12" - seasons: 2 - batch_size: 2 - num_workers: 0 +module: + _target_: torchgeo.trainers.BYOLTask + in_channels: 13 + backbone: "resnet18" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + weights: null + +datamodule: + _target_: torchgeo.datamodules.SSL4EOS12DataModule + root: "tests/data/ssl4eo/s12" + seasons: 2 + batch_size: 2 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/sustainbench_crop_yield.yaml b/tests/conf/sustainbench_crop_yield.yaml index 60903ea7d4c..2f48a83d02a 100644 --- a/tests/conf/sustainbench_crop_yield.yaml +++ b/tests/conf/sustainbench_crop_yield.yaml @@ -1,14 +1,15 @@ -experiment: - task: "sustainbench_crop_yield" - module: - model: "resnet18" - weights: null - num_outputs: 1 - in_channels: 9 - learning_rate: 1e-3 - learning_rate_schedule_patience: 2 - datamodule: - root: "tests/data/sustainbench_crop_yield" - download: true - batch_size: 1 - num_workers: 0 +module: + _target_: torchgeo.trainers.RegressionTask + model: "resnet18" + weights: null + num_outputs: 1 + in_channels: 9 + learning_rate: 1e-3 + learning_rate_schedule_patience: 2 + +datamodule: + _target_: torchgeo.datamodules.SustainBenchCropYieldDataModule + root: "tests/data/sustainbench_crop_yield" + download: true + batch_size: 1 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/ucmerced.yaml b/tests/conf/ucmerced.yaml index 3c544564ae8..22f61ff7cd0 100644 --- a/tests/conf/ucmerced.yaml +++ b/tests/conf/ucmerced.yaml @@ -1,15 +1,16 @@ -experiment: - task: "ucmerced" - module: - loss: "ce" - model: "resnet18" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - in_channels: 3 - num_classes: 2 - datamodule: - root: "tests/data/ucmerced" - download: true - batch_size: 2 - num_workers: 0 +module: + _target_: torchgeo.trainers.ClassificationTask + loss: "ce" + model: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + in_channels: 3 + num_classes: 2 + +datamodule: + _target_: torchgeo.datamodules.UCMercedDataModule + root: "tests/data/ucmerced" + download: true + batch_size: 2 + num_workers: 0 \ No newline at end of file diff --git a/tests/conf/vaihingen2d.yaml b/tests/conf/vaihingen2d.yaml index 7f542f3310b..8bd3043a673 100644 --- a/tests/conf/vaihingen2d.yaml +++ b/tests/conf/vaihingen2d.yaml @@ -1,20 +1,21 @@ -experiment: - task: "vaihingen2d" - module: - loss: "ce" - model: "unet" - backbone: "resnet18" - weights: null - learning_rate: 1e-3 - learning_rate_schedule_patience: 6 - verbose: false - in_channels: 3 - num_classes: 7 - num_filters: 1 - ignore_index: null - datamodule: - root: "tests/data/vaihingen" - batch_size: 1 - patch_size: 2 - val_split_pct: 0.5 - num_workers: 0 +module: + _target_: torchgeo.trainers.SemanticSegmentationTask + loss: "ce" + model: "unet" + backbone: "resnet18" + weights: null + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + verbose: false + in_channels: 3 + num_classes: 7 + num_filters: 1 + ignore_index: null + +datamodule: + _target_: torchgeo.datamodules.Vaihingen2DDataModule + root: "tests/data/vaihingen" + batch_size: 1 + patch_size: 2 + val_split_pct: 0.5 + num_workers: 0 \ No newline at end of file diff --git a/tests/trainers/test_byol.py b/tests/trainers/test_byol.py index 67bfef7d5f1..4b0e9957c86 100644 --- a/tests/trainers/test_byol.py +++ b/tests/trainers/test_byol.py @@ -3,7 +3,7 @@ import os from pathlib import Path -from typing import Any, cast +from typing import Any import pytest import timm @@ -12,16 +12,12 @@ import torchvision from _pytest.fixtures import SubRequest from _pytest.monkeypatch import MonkeyPatch -from lightning.pytorch import LightningDataModule, Trainer +from hydra.utils import instantiate +from lightning.pytorch import Trainer from omegaconf import OmegaConf from torchvision.models import resnet18 from torchvision.models._api import WeightsEnum -from torchgeo.datamodules import ( - ChesapeakeCVPRDataModule, - SeasonalContrastS2DataModule, - SSL4EOS12DataModule, -) from torchgeo.datasets import SSL4EOS12, SeasonalContrastS2 from torchgeo.models import get_model_weights, list_models from torchgeo.trainers import BYOLTask @@ -54,25 +50,19 @@ def test_custom_augment_fn(self) -> None: class TestBYOLTask: @pytest.mark.parametrize( - "name,classname", + "name", [ - ("chesapeake_cvpr_prior_byol", ChesapeakeCVPRDataModule), - ("seco_byol_1", SeasonalContrastS2DataModule), - ("seco_byol_2", SeasonalContrastS2DataModule), - ("ssl4eo_s12_byol_1", SSL4EOS12DataModule), - ("ssl4eo_s12_byol_2", SSL4EOS12DataModule), + "chesapeake_cvpr_prior_byol", + "seco_byol_1", + "seco_byol_2", + "ssl4eo_s12_byol_1", + "ssl4eo_s12_byol_2", ], ) def test_trainer( - self, - monkeypatch: MonkeyPatch, - name: str, - classname: type[LightningDataModule], - fast_dev_run: bool, + self, monkeypatch: MonkeyPatch, name: str, fast_dev_run: bool ) -> None: conf = OmegaConf.load(os.path.join("tests", "conf", name + ".yaml")) - conf_dict = OmegaConf.to_object(conf.experiment) - conf_dict = cast(dict[str, dict[str, Any]], conf_dict) if name.startswith("seco"): monkeypatch.setattr(SeasonalContrastS2, "__len__", lambda self: 2) @@ -81,14 +71,11 @@ def test_trainer( monkeypatch.setattr(SSL4EOS12, "__len__", lambda self: 2) # Instantiate datamodule - datamodule_kwargs = conf_dict["datamodule"] - datamodule = classname(**datamodule_kwargs) + datamodule = instantiate(conf.datamodule) # Instantiate model - model_kwargs = conf_dict["module"] - model = BYOLTask(**model_kwargs) - - model.backbone = SegmentationTestModel(**model_kwargs) + model = instantiate(conf.module) + model.backbone = SegmentationTestModel(**conf.module) # Instantiate trainer trainer = Trainer( diff --git a/tests/trainers/test_classification.py b/tests/trainers/test_classification.py index 4f8bcbe6f90..4abdf95bb87 100644 --- a/tests/trainers/test_classification.py +++ b/tests/trainers/test_classification.py @@ -3,7 +3,7 @@ import os from pathlib import Path -from typing import Any, cast +from typing import Any import pytest import timm @@ -12,20 +12,16 @@ import torchvision from _pytest.fixtures import SubRequest from _pytest.monkeypatch import MonkeyPatch -from lightning.pytorch import LightningDataModule, Trainer +from hydra.utils import instantiate +from lightning.pytorch import Trainer from omegaconf import OmegaConf from torch.nn.modules import Module from torchvision.models._api import WeightsEnum from torchgeo.datamodules import ( BigEarthNetDataModule, - EuroSAT100DataModule, EuroSATDataModule, - FireRiskDataModule, MisconfigurationException, - RESISC45DataModule, - So2SatDataModule, - UCMercedDataModule, ) from torchgeo.datasets import BigEarthNet, EuroSAT from torchgeo.models import get_model_weights, list_models @@ -33,9 +29,7 @@ class ClassificationTestModel(Module): - def __init__( - self, in_chans: int = 3, num_classes: int = 1000, **kwargs: Any - ) -> None: + def __init__(self, in_chans: int = 3, num_classes: int = 10, **kwargs: Any) -> None: super().__init__() self.conv1 = nn.Conv2d(in_channels=in_chans, out_channels=1, kernel_size=1) self.pool = nn.AdaptiveAvgPool2d((1, 1)) @@ -74,40 +68,32 @@ def plot(*args: Any, **kwargs: Any) -> None: class TestClassificationTask: @pytest.mark.parametrize( - "name,classname", + "name", [ - ("eurosat", EuroSATDataModule), - ("eurosat", EuroSAT100DataModule), - ("fire_risk", FireRiskDataModule), - ("resisc45", RESISC45DataModule), - ("so2sat_all", So2SatDataModule), - ("so2sat_s1", So2SatDataModule), - ("so2sat_s2", So2SatDataModule), - ("ucmerced", UCMercedDataModule), + "eurosat", + "eurosat100", + "fire_risk", + "resisc45", + "so2sat_all", + "so2sat_s1", + "so2sat_s2", + "ucmerced", ], ) def test_trainer( - self, - monkeypatch: MonkeyPatch, - name: str, - classname: type[LightningDataModule], - fast_dev_run: bool, + self, monkeypatch: MonkeyPatch, name: str, fast_dev_run: bool ) -> None: if name.startswith("so2sat"): pytest.importorskip("h5py", minversion="2.6") conf = OmegaConf.load(os.path.join("tests", "conf", name + ".yaml")) - conf_dict = OmegaConf.to_object(conf.experiment) - conf_dict = cast(dict[str, dict[str, Any]], conf_dict) # Instantiate datamodule - datamodule_kwargs = conf_dict["datamodule"] - datamodule = classname(**datamodule_kwargs) + datamodule = instantiate(conf.datamodule) # Instantiate model monkeypatch.setattr(timm, "create_model", create_model) - model_kwargs = conf_dict["module"] - model = ClassificationTask(**model_kwargs) + model = instantiate(conf.module) # Instantiate trainer trainer = Trainer( @@ -239,32 +225,19 @@ def test_predict(self, model_kwargs: dict[Any, Any], fast_dev_run: bool) -> None class TestMultiLabelClassificationTask: @pytest.mark.parametrize( - "name,classname", - [ - ("bigearthnet_all", BigEarthNetDataModule), - ("bigearthnet_s1", BigEarthNetDataModule), - ("bigearthnet_s2", BigEarthNetDataModule), - ], + "name", ["bigearthnet_all", "bigearthnet_s1", "bigearthnet_s2"] ) def test_trainer( - self, - monkeypatch: MonkeyPatch, - name: str, - classname: type[LightningDataModule], - fast_dev_run: bool, + self, monkeypatch: MonkeyPatch, name: str, fast_dev_run: bool ) -> None: conf = OmegaConf.load(os.path.join("tests", "conf", name + ".yaml")) - conf_dict = OmegaConf.to_object(conf.experiment) - conf_dict = cast(dict[str, dict[str, Any]], conf_dict) # Instantiate datamodule - datamodule_kwargs = conf_dict["datamodule"] - datamodule = classname(**datamodule_kwargs) + datamodule = instantiate(conf.datamodule) # Instantiate model monkeypatch.setattr(timm, "create_model", create_model) - model_kwargs = conf_dict["module"] - model = MultiLabelClassificationTask(**model_kwargs) + model = instantiate(conf.module) # Instantiate trainer trainer = Trainer( diff --git a/tests/trainers/test_detection.py b/tests/trainers/test_detection.py index 48b8b0d4579..38419ea0c05 100644 --- a/tests/trainers/test_detection.py +++ b/tests/trainers/test_detection.py @@ -2,14 +2,15 @@ # Licensed under the MIT License. import os -from typing import Any, cast +from typing import Any import pytest import torch import torch.nn as nn import torchvision.models.detection from _pytest.monkeypatch import MonkeyPatch -from lightning.pytorch import LightningDataModule, Trainer +from hydra.utils import instantiate +from lightning.pytorch import Trainer from omegaconf import OmegaConf from torch.nn.modules import Module @@ -57,25 +58,15 @@ def plot(*args: Any, **kwargs: Any) -> None: class TestObjectDetectionTask: - @pytest.mark.parametrize( - "name,classname", [("nasa_marine_debris", NASAMarineDebrisDataModule)] - ) + @pytest.mark.parametrize("name", ["nasa_marine_debris"]) @pytest.mark.parametrize("model_name", ["faster-rcnn", "fcos", "retinanet"]) def test_trainer( - self, - monkeypatch: MonkeyPatch, - name: str, - classname: type[LightningDataModule], - model_name: str, - fast_dev_run: bool, + self, monkeypatch: MonkeyPatch, name: str, model_name: str, fast_dev_run: bool ) -> None: conf = OmegaConf.load(os.path.join("tests", "conf", f"{name}.yaml")) - conf_dict = OmegaConf.to_object(conf.experiment) - conf_dict = cast(dict[Any, dict[Any, Any]], conf_dict) # Instantiate datamodule - datamodule_kwargs = conf_dict["datamodule"] - datamodule = classname(**datamodule_kwargs) + datamodule = instantiate(conf.datamodule) # Instantiate model monkeypatch.setattr( @@ -87,9 +78,8 @@ def test_trainer( monkeypatch.setattr( torchvision.models.detection, "RetinaNet", ObjectDetectionTestModel ) - model_kwargs = conf_dict["module"] - model_kwargs["model"] = model_name - model = ObjectDetectionTask(**model_kwargs) + conf.module.model = model_name + model = instantiate(conf.module) # Instantiate trainer trainer = Trainer( diff --git a/tests/trainers/test_regression.py b/tests/trainers/test_regression.py index f4210a7cfa9..cabfe5a2cdc 100644 --- a/tests/trainers/test_regression.py +++ b/tests/trainers/test_regression.py @@ -3,7 +3,7 @@ import os from pathlib import Path -from typing import Any, cast +from typing import Any import pytest import timm @@ -11,17 +11,12 @@ import torchvision from _pytest.fixtures import SubRequest from _pytest.monkeypatch import MonkeyPatch -from lightning.pytorch import LightningDataModule, Trainer +from hydra.utils import instantiate +from lightning.pytorch import Trainer from omegaconf import OmegaConf from torchvision.models._api import WeightsEnum -from torchgeo.datamodules import ( - COWCCountingDataModule, - MisconfigurationException, - SKIPPDDataModule, - SustainBenchCropYieldDataModule, - TropicalCycloneDataModule, -) +from torchgeo.datamodules import MisconfigurationException, TropicalCycloneDataModule from torchgeo.datasets import TropicalCyclone from torchgeo.models import get_model_weights, list_models from torchgeo.trainers import RegressionTask @@ -50,32 +45,19 @@ def plot(*args: Any, **kwargs: Any) -> None: class TestRegressionTask: @pytest.mark.parametrize( - "name,classname", - [ - ("cowc_counting", COWCCountingDataModule), - ("cyclone", TropicalCycloneDataModule), - ("sustainbench_crop_yield", SustainBenchCropYieldDataModule), - ("skippd", SKIPPDDataModule), - ], + "name", ["cowc_counting", "cyclone", "sustainbench_crop_yield", "skippd"] ) - def test_trainer( - self, name: str, classname: type[LightningDataModule], fast_dev_run: bool - ) -> None: + def test_trainer(self, name: str, fast_dev_run: bool) -> None: conf = OmegaConf.load(os.path.join("tests", "conf", name + ".yaml")) - conf_dict = OmegaConf.to_object(conf.experiment) - conf_dict = cast(dict[str, dict[str, Any]], conf_dict) # Instantiate datamodule - datamodule_kwargs = conf_dict["datamodule"] - datamodule = classname(**datamodule_kwargs) + datamodule = instantiate(conf.datamodule) # Instantiate model - model_kwargs = conf_dict["module"] - model = RegressionTask(**model_kwargs) + model = instantiate(conf.module) model.model = RegressionTestModel( - in_chans=model_kwargs["in_channels"], - num_classes=model_kwargs["num_outputs"], + in_chans=conf.module.in_channels, num_classes=conf.module.num_outputs ) # Instantiate trainer diff --git a/tests/trainers/test_segmentation.py b/tests/trainers/test_segmentation.py index 80a3404e68a..eb34204b6e3 100644 --- a/tests/trainers/test_segmentation.py +++ b/tests/trainers/test_segmentation.py @@ -9,35 +9,18 @@ import torch import torch.nn as nn from _pytest.monkeypatch import MonkeyPatch -from lightning.pytorch import LightningDataModule, Trainer +from hydra.utils import instantiate +from lightning.pytorch import Trainer from omegaconf import OmegaConf from torch.nn.modules import Module -from torchgeo.datamodules import ( - ChesapeakeCVPRDataModule, - DeepGlobeLandCoverDataModule, - ETCI2021DataModule, - GID15DataModule, - InriaAerialImageLabelingDataModule, - L7IrishDataModule, - L8BiomeDataModule, - LandCoverAIDataModule, - LoveDADataModule, - MisconfigurationException, - NAIPChesapeakeDataModule, - Potsdam2DDataModule, - SEN12MSDataModule, - SpaceNet1DataModule, - Vaihingen2DDataModule, -) +from torchgeo.datamodules import MisconfigurationException, SEN12MSDataModule from torchgeo.datasets import LandCoverAI from torchgeo.trainers import SemanticSegmentationTask class SegmentationTestModel(Module): - def __init__( - self, in_channels: int = 3, classes: int = 1000, **kwargs: Any - ) -> None: + def __init__(self, in_channels: int = 3, classes: int = 3, **kwargs: Any) -> None: super().__init__() self.conv1 = nn.Conv2d( in_channels=in_channels, out_channels=classes, kernel_size=1, padding=0 @@ -57,34 +40,30 @@ def plot(*args: Any, **kwargs: Any) -> None: class TestSemanticSegmentationTask: @pytest.mark.parametrize( - "name,classname", + "name", [ - ("chesapeake_cvpr_5", ChesapeakeCVPRDataModule), - ("chesapeake_cvpr_7", ChesapeakeCVPRDataModule), - ("deepglobelandcover", DeepGlobeLandCoverDataModule), - ("etci2021", ETCI2021DataModule), - ("gid15", GID15DataModule), - ("inria", InriaAerialImageLabelingDataModule), - ("l7irish", L7IrishDataModule), - ("l8biome", L8BiomeDataModule), - ("landcoverai", LandCoverAIDataModule), - ("loveda", LoveDADataModule), - ("naipchesapeake", NAIPChesapeakeDataModule), - ("potsdam2d", Potsdam2DDataModule), - ("sen12ms_all", SEN12MSDataModule), - ("sen12ms_s1", SEN12MSDataModule), - ("sen12ms_s2_all", SEN12MSDataModule), - ("sen12ms_s2_reduced", SEN12MSDataModule), - ("spacenet1", SpaceNet1DataModule), - ("vaihingen2d", Vaihingen2DDataModule), + "chesapeake_cvpr_5", + "chesapeake_cvpr_7", + "deepglobelandcover", + "etci2021", + "gid15", + "inria", + "l7irish", + "l8biome", + "landcoverai", + "loveda", + "naipchesapeake", + "potsdam2d", + "sen12ms_all", + "sen12ms_s1", + "sen12ms_s2_all", + "sen12ms_s2_reduced", + "spacenet1", + "vaihingen2d", ], ) def test_trainer( - self, - monkeypatch: MonkeyPatch, - name: str, - classname: type[LightningDataModule], - fast_dev_run: bool, + self, monkeypatch: MonkeyPatch, name: str, fast_dev_run: bool ) -> None: if name == "naipchesapeake": pytest.importorskip("zipfile_deflate64") @@ -94,18 +73,14 @@ def test_trainer( monkeypatch.setattr(LandCoverAI, "sha256", sha256) conf = OmegaConf.load(os.path.join("tests", "conf", name + ".yaml")) - conf_dict = OmegaConf.to_object(conf.experiment) - conf_dict = cast(dict[Any, dict[Any, Any]], conf_dict) # Instantiate datamodule - datamodule_kwargs = conf_dict["datamodule"] - datamodule = classname(**datamodule_kwargs) + datamodule = instantiate(conf.datamodule) # Instantiate model monkeypatch.setattr(smp, "Unet", create_model) monkeypatch.setattr(smp, "DeepLabV3Plus", create_model) - model_kwargs = conf_dict["module"] - model = SemanticSegmentationTask(**model_kwargs) + model = instantiate(conf.module) # Instantiate trainer trainer = Trainer( diff --git a/train.py b/train.py index de2e6f85691..0722a3d7b96 100755 --- a/train.py +++ b/train.py @@ -6,70 +6,17 @@ """torchgeo model training script.""" import os -from typing import Any, cast +from typing import cast import lightning.pytorch as pl +from hydra.utils import instantiate from lightning.pytorch import LightningDataModule, LightningModule, Trainer from lightning.pytorch.callbacks import EarlyStopping, ModelCheckpoint from lightning.pytorch.loggers import CSVLogger, TensorBoardLogger from omegaconf import DictConfig, OmegaConf -from torchgeo.datamodules import ( - BigEarthNetDataModule, - ChesapeakeCVPRDataModule, - COWCCountingDataModule, - DeepGlobeLandCoverDataModule, - ETCI2021DataModule, - EuroSATDataModule, - GID15DataModule, - InriaAerialImageLabelingDataModule, - LandCoverAIDataModule, - LoveDADataModule, - NAIPChesapeakeDataModule, - NASAMarineDebrisDataModule, - Potsdam2DDataModule, - RESISC45DataModule, - SEN12MSDataModule, - So2SatDataModule, - SpaceNet1DataModule, - TropicalCycloneDataModule, - UCMercedDataModule, - Vaihingen2DDataModule, -) -from torchgeo.trainers import ( - BYOLTask, - ClassificationTask, - MultiLabelClassificationTask, - ObjectDetectionTask, - RegressionTask, - SemanticSegmentationTask, -) - -TASK_TO_MODULES_MAPPING: dict[ - str, tuple[type[LightningModule], type[LightningDataModule]] -] = { - "bigearthnet": (MultiLabelClassificationTask, BigEarthNetDataModule), - "byol": (BYOLTask, ChesapeakeCVPRDataModule), - "chesapeake_cvpr": (SemanticSegmentationTask, ChesapeakeCVPRDataModule), - "cowc_counting": (RegressionTask, COWCCountingDataModule), - "cyclone": (RegressionTask, TropicalCycloneDataModule), - "deepglobelandcover": (SemanticSegmentationTask, DeepGlobeLandCoverDataModule), - "eurosat": (ClassificationTask, EuroSATDataModule), - "etci2021": (SemanticSegmentationTask, ETCI2021DataModule), - "gid15": (SemanticSegmentationTask, GID15DataModule), - "inria": (SemanticSegmentationTask, InriaAerialImageLabelingDataModule), - "landcoverai": (SemanticSegmentationTask, LandCoverAIDataModule), - "loveda": (SemanticSegmentationTask, LoveDADataModule), - "naipchesapeake": (SemanticSegmentationTask, NAIPChesapeakeDataModule), - "nasa_marine_debris": (ObjectDetectionTask, NASAMarineDebrisDataModule), - "potsdam2d": (SemanticSegmentationTask, Potsdam2DDataModule), - "resisc45": (ClassificationTask, RESISC45DataModule), - "sen12ms": (SemanticSegmentationTask, SEN12MSDataModule), - "so2sat": (ClassificationTask, So2SatDataModule), - "spacenet1": (SemanticSegmentationTask, SpaceNet1DataModule), - "ucmerced": (ClassificationTask, UCMercedDataModule), - "vaihingen2d": (SemanticSegmentationTask, Vaihingen2DDataModule), -} +from torchgeo.datamodules import MisconfigurationException +from torchgeo.trainers import BYOLTask, ObjectDetectionTask def set_up_omegaconf() -> DictConfig: @@ -91,7 +38,6 @@ def set_up_omegaconf() -> DictConfig: Raises: FileNotFoundError: when ``config_file`` does not exist - ValueError: when ``task.name`` is not a valid task """ conf = OmegaConf.load("conf/defaults.yaml") command_line_conf = OmegaConf.from_cli() @@ -107,34 +53,15 @@ def set_up_omegaconf() -> DictConfig: conf = OmegaConf.merge( # Merge in any arguments passed via the command line conf, command_line_conf ) - - # These OmegaConf structured configs enforce a schema at runtime, see: - # https://omegaconf.readthedocs.io/en/2.0_branch/structured_config.html#merging-with-other-configs - task_name = conf.experiment.task - task_config_fn = os.path.join("conf", f"{task_name}.yaml") - if task_name == "test": - task_conf = OmegaConf.create() - elif os.path.exists(task_config_fn): - task_conf = cast(DictConfig, OmegaConf.load(task_config_fn)) - else: - raise ValueError( - f"experiment.task={task_name} is not recognized as a valid task" - ) - - conf = OmegaConf.merge(task_conf, conf) conf = cast(DictConfig, conf) # convince mypy that everything is alright - return conf def main(conf: DictConfig) -> None: """Main training loop.""" - ###################################### - # Setup output directory - ###################################### - - experiment_name = conf.experiment.name - task_name = conf.experiment.task + experiment_name = ( + f"{conf.datamodule._target_.lower()}_{conf.module._target_.lower()}" + ) if os.path.isfile(conf.program.output_dir): raise NotADirectoryError("`program.output_dir` must be a directory") os.makedirs(conf.program.output_dir, exist_ok=True) @@ -154,45 +81,30 @@ def main(conf: DictConfig) -> None: + "empty. We don't want to overwrite any existing results, exiting..." ) - with open(os.path.join(experiment_dir, "experiment_config.yaml"), "w") as f: + with open(os.path.join(experiment_dir, "config.yaml"), "w") as f: OmegaConf.save(config=conf, f=f) - ###################################### - # Choose task to run based on arguments or configuration - ###################################### - # Convert the DictConfig into a dictionary so that we can pass as kwargs. - task_args = cast(dict[str, Any], OmegaConf.to_object(conf.experiment.module)) - datamodule_args = cast( - dict[str, Any], OmegaConf.to_object(conf.experiment.datamodule) - ) + # Define module and datamodule + datamodule: LightningDataModule = instantiate(conf.datamodule) + task: LightningModule = instantiate(conf.module) - datamodule: LightningDataModule - task: LightningModule - if task_name in TASK_TO_MODULES_MAPPING: - task_class, datamodule_class = TASK_TO_MODULES_MAPPING[task_name] - task = task_class(**task_args) - datamodule = datamodule_class(**datamodule_args) - else: - raise ValueError( - f"experiment.task={task_name} is not recognized as a valid task" - ) - - ###################################### - # Setup trainer - ###################################### + # Define callbacks tb_logger = TensorBoardLogger(conf.program.log_dir, name=experiment_name) csv_logger = CSVLogger(conf.program.log_dir, name=experiment_name) if isinstance(task, ObjectDetectionTask): monitor_metric = "val_map" mode = "max" + elif isinstance(task, BYOLTask): + monitor_metric = "train_loss" + mode = "min" else: monitor_metric = "val_loss" mode = "min" checkpoint_callback = ModelCheckpoint( monitor=monitor_metric, - filename="checkpoint-epoch{epoch:02d}-val_loss{val_loss:.2f}", + filename=f"checkpoint-{{epoch:02d}}-{{{monitor_metric}:.2f}}", dirpath=experiment_dir, save_top_k=1, save_last=True, @@ -202,18 +114,22 @@ def main(conf: DictConfig) -> None: monitor=monitor_metric, min_delta=0.00, patience=18, mode=mode ) - trainer_args = cast(dict[str, Any], OmegaConf.to_object(conf.trainer)) - - trainer_args["callbacks"] = [checkpoint_callback, early_stopping_callback] - trainer_args["logger"] = [tb_logger, csv_logger] - trainer_args["default_root_dir"] = experiment_dir - trainer = Trainer(**trainer_args) + # Define trainer + trainer: Trainer = instantiate( + conf.trainer, + callbacks=[checkpoint_callback, early_stopping_callback], + logger=[tb_logger, csv_logger], + default_root_dir=experiment_dir, + ) - ###################################### - # Run experiment - ###################################### + # Train trainer.fit(model=task, datamodule=datamodule) - trainer.test(ckpt_path="best", datamodule=datamodule) + + # Test + try: + trainer.test(ckpt_path="best", datamodule=datamodule) + except MisconfigurationException: + pass if __name__ == "__main__": From e13de2684d03e2f2f9d0326c0d49a7a0e322d881 Mon Sep 17 00:00:00 2001 From: Caleb Robinson Date: Mon, 24 Apr 2023 09:17:15 -0700 Subject: [PATCH 02/17] Add dtype field to RasterDataset (#1149) * Adding dtype to RasterDataset * Removing explicit cast to float for images * Updating test case * Reverting * Compromising on a UserWarning * Update torchgeo/datasets/geo.py Co-authored-by: Adam J. Stewart * Adding test * Changing back * Set the docstring of dtype * Good grief * pydocstyle workaround * REquested changes --------- Co-authored-by: Adam J. Stewart --- torchgeo/datasets/geo.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/torchgeo/datasets/geo.py b/torchgeo/datasets/geo.py index 4cd54d5d9b3..9c440da896f 100644 --- a/torchgeo/datasets/geo.py +++ b/torchgeo/datasets/geo.py @@ -296,6 +296,20 @@ class RasterDataset(GeoDataset): #: Color map for the dataset, used for plotting cmap: dict[int, tuple[int, int, int, int]] = {} + @property + def dtype(self) -> torch.dtype: + """The dtype of the dataset (overrides the dtype of the data file via a cast). + + Returns: + the dtype of the dataset + + .. versionadded:: 5.0 + """ + if self.is_image: + return torch.float32 + else: + return torch.long + def __init__( self, root: str = "data", @@ -429,10 +443,12 @@ def __getitem__(self, query: BoundingBox) -> dict[str, Any]: data = self._merge_files(filepaths, query, self.band_indexes) sample = {"crs": self.crs, "bbox": query} + + data = data.to(self.dtype) if self.is_image: - sample["image"] = data.float() + sample["image"] = data else: - sample["mask"] = data.long() + sample["mask"] = data if self.transforms is not None: sample = self.transforms(sample) From 5b74c971cbaf13d2c5863572bb1c6d19cce5467a Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Mon, 24 Apr 2023 13:34:49 -0500 Subject: [PATCH 03/17] Update test minversions, pyvista min version (#1276) * Update test minversions * Test newer pyvista * Add headless server * try pyvista 0.30.0 * try pyvista 0.27.0 * try pyvista 0.28.0 * try pyvista 0.29.0 * try pyvista 0.28.1 * pyvista 0.29 confirmed as min version --- .github/workflows/tests.yaml | 2 ++ .pre-commit-config.yaml | 2 +- environment.yml | 2 +- requirements/min-reqs.old | 2 +- setup.cfg | 4 ++-- tests/datamodules/test_usavars.py | 2 +- tests/datasets/test_benin_cashews.py | 2 +- tests/datasets/test_cloud_cover.py | 2 +- tests/datasets/test_cv4a_kenya_crop_type.py | 2 +- tests/datasets/test_cyclone.py | 2 +- tests/datasets/test_eddmaps.py | 2 +- tests/datasets/test_gbif.py | 2 +- tests/datasets/test_idtrees.py | 4 ++-- tests/datasets/test_inaturalist.py | 2 +- tests/datasets/test_landcoverai.py | 4 ++-- tests/datasets/test_nasa_marine_debris.py | 4 ++-- tests/datasets/test_openbuildings.py | 2 +- tests/datasets/test_reforestree.py | 4 ++-- tests/datasets/test_resisc45.py | 2 +- tests/datasets/test_so2sat.py | 2 +- tests/datasets/test_spacenet.py | 2 +- tests/datasets/test_usavars.py | 2 +- tests/datasets/test_utils.py | 6 +++--- tests/datasets/test_vhr10.py | 2 +- tests/datasets/test_western_usa_live_fuel_moisture.py | 2 +- tests/datasets/test_zuericrop.py | 2 +- tests/trainers/test_classification.py | 2 +- 27 files changed, 35 insertions(+), 33 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 4a439a9d714..2874698656a 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -96,6 +96,8 @@ jobs: with: path: ${{ env.pythonLocation }} key: ${{ env.pythonLocation }}-${{ hashFiles('requirements/min-reqs.old') }}-${{ hashFiles('requirements/mins-cons.old') }} + - name: Setup headless display for pyvista + uses: pyvista/setup-headless-display-action@v2 - name: Install apt dependencies (Linux) run: | sudo apt-get update diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 299079e1f00..0387a44ca85 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,5 +34,5 @@ repos: hooks: - id: mypy args: [--strict, --ignore-missing-imports, --show-error-codes] - additional_dependencies: [torch>=2, torchmetrics>=0.10, lightning>=1.8, pytest>=6, pyvista>=0.20, omegaconf>=2.0.1, hydra-core>=1, kornia>=0.6, numpy>=1.22] + additional_dependencies: [torch>=2, torchmetrics>=0.10, lightning>=1.8, pytest>=6.1.2, pyvista>=0.29, omegaconf>=2.0.1, hydra-core>=1, kornia>=0.6.5, numpy>=1.22] exclude: (build|data|dist|logo|logs|output)/ diff --git a/environment.yml b/environment.yml index 51ec988b274..73317d5d103 100644 --- a/environment.yml +++ b/environment.yml @@ -13,7 +13,7 @@ dependencies: - pyproj>=3 - python>=3.9 - pytorch>=1.12 - - pyvista>=0.25.2 + - pyvista>=0.29 - rarfile>=4 - rasterio>=1.2 - shapely>=1.7.1 diff --git a/requirements/min-reqs.old b/requirements/min-reqs.old index c484fb51efc..c6330210e81 100644 --- a/requirements/min-reqs.old +++ b/requirements/min-reqs.old @@ -26,7 +26,7 @@ laspy==2.0.0 opencv-python==4.4.0.46 pandas==1.1.3 pycocotools==2.0.4 -pyvista==0.25.2 +pyvista==0.29.0 radiant-mlhub==0.3.0 rarfile==4.0 scikit-image==0.18.0 diff --git a/setup.cfg b/setup.cfg index d4dcefc9a39..45e9c8cae02 100644 --- a/setup.cfg +++ b/setup.cfg @@ -79,8 +79,8 @@ datasets = pandas>=1.1.3,<3 # pycocotools 2.0.4+ required to avoid use of deprecated setuptools fetch_build_eggs pycocotools>=2.0.4,<3 - # pyvista 0.25.2 required for wheels - pyvista>=0.25.2,<0.39 + # pyvista 0.29+ required for to avoid segfault during testing + pyvista>=0.29,<0.39 # radiant-mlhub 0.3+ required for newer tqdm support required by lightning radiant-mlhub>=0.3,<0.6 # rarfile 4+ required for wheels diff --git a/tests/datamodules/test_usavars.py b/tests/datamodules/test_usavars.py index 7f04644cc20..41b1939efe7 100644 --- a/tests/datamodules/test_usavars.py +++ b/tests/datamodules/test_usavars.py @@ -14,7 +14,7 @@ class TestUSAVarsDataModule: @pytest.fixture def datamodule(self, request: SubRequest) -> USAVarsDataModule: - pytest.importorskip("pandas", minversion="0.23.2") + pytest.importorskip("pandas", minversion="1.1.3") root = os.path.join("tests", "data", "usavars") batch_size = 1 num_workers = 0 diff --git a/tests/datasets/test_benin_cashews.py b/tests/datasets/test_benin_cashews.py index 6c42c59b534..2ae85f47e7b 100644 --- a/tests/datasets/test_benin_cashews.py +++ b/tests/datasets/test_benin_cashews.py @@ -32,7 +32,7 @@ class TestBeninSmallHolderCashews: def dataset( self, monkeypatch: MonkeyPatch, tmp_path: Path ) -> BeninSmallHolderCashews: - radiant_mlhub = pytest.importorskip("radiant_mlhub", minversion="0.2.1") + radiant_mlhub = pytest.importorskip("radiant_mlhub", minversion="0.3") monkeypatch.setattr(radiant_mlhub.Collection, "fetch", fetch) source_md5 = "255efff0f03bc6322470949a09bc76db" labels_md5 = "ed2195d93ca6822d48eb02bc3e81c127" diff --git a/tests/datasets/test_cloud_cover.py b/tests/datasets/test_cloud_cover.py index a389982c26f..c2b1242537f 100644 --- a/tests/datasets/test_cloud_cover.py +++ b/tests/datasets/test_cloud_cover.py @@ -31,7 +31,7 @@ def fetch(dataset_id: str, **kwargs: str) -> Collection: class TestCloudCoverDetection: @pytest.fixture def dataset(self, monkeypatch: MonkeyPatch, tmp_path: Path) -> CloudCoverDetection: - radiant_mlhub = pytest.importorskip("radiant_mlhub", minversion="0.2.1") + radiant_mlhub = pytest.importorskip("radiant_mlhub", minversion="0.3") monkeypatch.setattr(radiant_mlhub.Collection, "fetch", fetch) test_image_meta = { diff --git a/tests/datasets/test_cv4a_kenya_crop_type.py b/tests/datasets/test_cv4a_kenya_crop_type.py index 37578a13a1d..3271cfd818b 100644 --- a/tests/datasets/test_cv4a_kenya_crop_type.py +++ b/tests/datasets/test_cv4a_kenya_crop_type.py @@ -32,7 +32,7 @@ def fetch(dataset_id: str, **kwargs: str) -> Collection: class TestCV4AKenyaCropType: @pytest.fixture def dataset(self, monkeypatch: MonkeyPatch, tmp_path: Path) -> CV4AKenyaCropType: - radiant_mlhub = pytest.importorskip("radiant_mlhub", minversion="0.2.1") + radiant_mlhub = pytest.importorskip("radiant_mlhub", minversion="0.3") monkeypatch.setattr(radiant_mlhub.Collection, "fetch", fetch) source_md5 = "7f4dcb3f33743dddd73f453176308bfb" labels_md5 = "95fc59f1d94a85ec00931d4d1280bec9" diff --git a/tests/datasets/test_cyclone.py b/tests/datasets/test_cyclone.py index ee29d44ecc4..e573ce451fe 100644 --- a/tests/datasets/test_cyclone.py +++ b/tests/datasets/test_cyclone.py @@ -32,7 +32,7 @@ class TestTropicalCyclone: def dataset( self, monkeypatch: MonkeyPatch, tmp_path: Path, request: SubRequest ) -> TropicalCyclone: - radiant_mlhub = pytest.importorskip("radiant_mlhub", minversion="0.2.1") + radiant_mlhub = pytest.importorskip("radiant_mlhub", minversion="0.3") monkeypatch.setattr(radiant_mlhub.Collection, "fetch", fetch) md5s = { "train": { diff --git a/tests/datasets/test_eddmaps.py b/tests/datasets/test_eddmaps.py index e505bfc1fe0..d10ad8a298a 100644 --- a/tests/datasets/test_eddmaps.py +++ b/tests/datasets/test_eddmaps.py @@ -11,7 +11,7 @@ from torchgeo.datasets import BoundingBox, EDDMapS, IntersectionDataset, UnionDataset -pytest.importorskip("pandas", minversion="0.23.2") +pytest.importorskip("pandas", minversion="1.1.3") class TestEDDMapS: diff --git a/tests/datasets/test_gbif.py b/tests/datasets/test_gbif.py index 5f87a248f8f..df32844a8a5 100644 --- a/tests/datasets/test_gbif.py +++ b/tests/datasets/test_gbif.py @@ -11,7 +11,7 @@ from torchgeo.datasets import GBIF, BoundingBox, IntersectionDataset, UnionDataset -pytest.importorskip("pandas", minversion="0.23.2") +pytest.importorskip("pandas", minversion="1.1.3") class TestGBIF: diff --git a/tests/datasets/test_idtrees.py b/tests/datasets/test_idtrees.py index 7104d4abe72..5247157d63e 100644 --- a/tests/datasets/test_idtrees.py +++ b/tests/datasets/test_idtrees.py @@ -18,7 +18,7 @@ import torchgeo.datasets.utils from torchgeo.datasets import IDTReeS -pytest.importorskip("pandas", minversion="0.23.2") +pytest.importorskip("pandas", minversion="1.1.3") pytest.importorskip("laspy", minversion="2") @@ -140,7 +140,7 @@ def test_plot(self, dataset: IDTReeS) -> None: plt.close() def test_plot_las(self, dataset: IDTReeS) -> None: - pyvista = pytest.importorskip("pyvista", minversion="0.35.1") + pyvista = pytest.importorskip("pyvista", minversion="0.29") # Test point cloud without colors point_cloud = dataset.plot_las(index=0) diff --git a/tests/datasets/test_inaturalist.py b/tests/datasets/test_inaturalist.py index 623c64837bd..32891ec3279 100644 --- a/tests/datasets/test_inaturalist.py +++ b/tests/datasets/test_inaturalist.py @@ -16,7 +16,7 @@ UnionDataset, ) -pytest.importorskip("pandas", minversion="0.23.2") +pytest.importorskip("pandas", minversion="1.1.3") class TestINaturalist: diff --git a/tests/datasets/test_landcoverai.py b/tests/datasets/test_landcoverai.py index c4f11e05b61..a604533e3c9 100644 --- a/tests/datasets/test_landcoverai.py +++ b/tests/datasets/test_landcoverai.py @@ -75,7 +75,7 @@ class TestLandCoverAI: def dataset( self, monkeypatch: MonkeyPatch, tmp_path: Path, request: SubRequest ) -> LandCoverAI: - pytest.importorskip("cv2", minversion="3.4.2.17") + pytest.importorskip("cv2", minversion="4.4.0.46") monkeypatch.setattr(torchgeo.datasets.landcoverai, "download_url", download_url) md5 = "ff8998857cc8511f644d3f7d0f3688d0" monkeypatch.setattr(LandCoverAI, "md5", md5) @@ -106,7 +106,7 @@ def test_already_extracted(self, dataset: LandCoverAI) -> None: LandCoverAI(root=dataset.root, download=True) def test_already_downloaded(self, monkeypatch: MonkeyPatch, tmp_path: Path) -> None: - pytest.importorskip("cv2", minversion="3.4.2.17") + pytest.importorskip("cv2", minversion="4.4.0.46") sha256 = "ecec8e871faf1bbd8ca525ca95ddc1c1f5213f40afb94599884bd85f990ebd6b" monkeypatch.setattr(LandCoverAI, "sha256", sha256) url = os.path.join("tests", "data", "landcoverai", "landcover.ai.v1.zip") diff --git a/tests/datasets/test_nasa_marine_debris.py b/tests/datasets/test_nasa_marine_debris.py index 6887cc4173b..cf197d196db 100644 --- a/tests/datasets/test_nasa_marine_debris.py +++ b/tests/datasets/test_nasa_marine_debris.py @@ -41,7 +41,7 @@ def fetch_corrupted(collection_id: str, **kwargs: str) -> Collection_corrupted: class TestNASAMarineDebris: @pytest.fixture() def dataset(self, monkeypatch: MonkeyPatch, tmp_path: Path) -> NASAMarineDebris: - radiant_mlhub = pytest.importorskip("radiant_mlhub", minversion="0.2.1") + radiant_mlhub = pytest.importorskip("radiant_mlhub", minversion="0.3") monkeypatch.setattr(radiant_mlhub.Collection, "fetch", fetch) md5s = ["6f4f0d2313323950e45bf3fc0c09b5de", "540cf1cf4fd2c13b609d0355abe955d7"] monkeypatch.setattr(NASAMarineDebris, "md5s", md5s) @@ -85,7 +85,7 @@ def test_corrupted_new_download( self, tmp_path: Path, monkeypatch: MonkeyPatch ) -> None: with pytest.raises(RuntimeError, match="Dataset checksum mismatch."): - radiant_mlhub = pytest.importorskip("radiant_mlhub", minversion="0.2.1") + radiant_mlhub = pytest.importorskip("radiant_mlhub", minversion="0.3") monkeypatch.setattr(radiant_mlhub.Collection, "fetch", fetch_corrupted) NASAMarineDebris(root=str(tmp_path), download=True, checksum=True) diff --git a/tests/datasets/test_openbuildings.py b/tests/datasets/test_openbuildings.py index ba3dcb43321..04b87eced87 100644 --- a/tests/datasets/test_openbuildings.py +++ b/tests/datasets/test_openbuildings.py @@ -23,7 +23,7 @@ UnionDataset, ) -pd = pytest.importorskip("pandas", minversion="0.23.2") +pd = pytest.importorskip("pandas", minversion="1.1.3") class TestOpenBuildings: diff --git a/tests/datasets/test_reforestree.py b/tests/datasets/test_reforestree.py index a558393afa6..aa6bb3a02ac 100644 --- a/tests/datasets/test_reforestree.py +++ b/tests/datasets/test_reforestree.py @@ -24,7 +24,7 @@ def download_url(url: str, root: str, *args: str) -> None: class TestReforesTree: @pytest.fixture def dataset(self, monkeypatch: MonkeyPatch, tmp_path: Path) -> ReforesTree: - pytest.importorskip("pandas", minversion="0.23.2") + pytest.importorskip("pandas", minversion="1.1.3") monkeypatch.setattr(torchgeo.datasets.utils, "download_url", download_url) data_dir = os.path.join("tests", "data", "reforestree") @@ -79,7 +79,7 @@ def test_len(self, dataset: ReforesTree) -> None: assert len(dataset) == 2 def test_not_extracted(self, tmp_path: Path) -> None: - pytest.importorskip("pandas", minversion="0.23.2") + pytest.importorskip("pandas", minversion="1.1.3") url = os.path.join("tests", "data", "reforestree", "reforesTree.zip") shutil.copy(url, tmp_path) ReforesTree(root=str(tmp_path)) diff --git a/tests/datasets/test_resisc45.py b/tests/datasets/test_resisc45.py index 7a24af30631..fc4191512cc 100644 --- a/tests/datasets/test_resisc45.py +++ b/tests/datasets/test_resisc45.py @@ -25,7 +25,7 @@ class TestRESISC45: def dataset( self, monkeypatch: MonkeyPatch, tmp_path: Path, request: SubRequest ) -> RESISC45: - pytest.importorskip("rarfile", minversion="3") + pytest.importorskip("rarfile", minversion="4") monkeypatch.setattr(torchgeo.datasets.resisc45, "download_url", download_url) md5 = "5895dea3757ba88707d52f5521c444d3" diff --git a/tests/datasets/test_so2sat.py b/tests/datasets/test_so2sat.py index d8e3c9ca7bc..a112812abe5 100644 --- a/tests/datasets/test_so2sat.py +++ b/tests/datasets/test_so2sat.py @@ -15,7 +15,7 @@ from torchgeo.datasets import So2Sat -pytest.importorskip("h5py", minversion="2.6") +pytest.importorskip("h5py", minversion="3") class TestSo2Sat: diff --git a/tests/datasets/test_spacenet.py b/tests/datasets/test_spacenet.py index d409b6fe570..e3ef34b08f0 100644 --- a/tests/datasets/test_spacenet.py +++ b/tests/datasets/test_spacenet.py @@ -24,7 +24,7 @@ ) TEST_DATA_DIR = "tests/data/spacenet" -radiant_mlhub = pytest.importorskip("radiant_mlhub", minversion="0.2.1") +radiant_mlhub = pytest.importorskip("radiant_mlhub", minversion="0.3") class Collection: diff --git a/tests/datasets/test_usavars.py b/tests/datasets/test_usavars.py index 3fd4ce26b6d..8e437279afc 100644 --- a/tests/datasets/test_usavars.py +++ b/tests/datasets/test_usavars.py @@ -18,7 +18,7 @@ import torchgeo.datasets.utils from torchgeo.datasets import USAVars -pytest.importorskip("pandas", minversion="0.23.2") +pytest.importorskip("pandas", minversion="1.1.3") def download_url(url: str, root: str, *args: str, **kwargs: str) -> None: diff --git a/tests/datasets/test_utils.py b/tests/datasets/test_utils.py index 877caedf1fc..b2dd9cc5355 100644 --- a/tests/datasets/test_utils.py +++ b/tests/datasets/test_utils.py @@ -95,7 +95,7 @@ def test_mock_missing_module(mock_missing_module: None) -> None: ) def test_extract_archive(src: str, tmp_path: Path) -> None: if src.endswith(".rar"): - pytest.importorskip("rarfile", minversion="3") + pytest.importorskip("rarfile", minversion="4") if src.startswith("chesapeake"): pytest.importorskip("zipfile_deflate64") extract_archive(os.path.join("tests", "data", src), str(tmp_path)) @@ -134,7 +134,7 @@ def test_download_and_extract_archive(tmp_path: Path, monkeypatch: MonkeyPatch) def test_download_radiant_mlhub_dataset( tmp_path: Path, monkeypatch: MonkeyPatch ) -> None: - radiant_mlhub = pytest.importorskip("radiant_mlhub", minversion="0.2.1") + radiant_mlhub = pytest.importorskip("radiant_mlhub", minversion="0.3") monkeypatch.setattr(radiant_mlhub.Dataset, "fetch", fetch_dataset) download_radiant_mlhub_dataset("", str(tmp_path)) @@ -142,7 +142,7 @@ def test_download_radiant_mlhub_dataset( def test_download_radiant_mlhub_collection( tmp_path: Path, monkeypatch: MonkeyPatch ) -> None: - radiant_mlhub = pytest.importorskip("radiant_mlhub", minversion="0.2.1") + radiant_mlhub = pytest.importorskip("radiant_mlhub", minversion="0.3") monkeypatch.setattr(radiant_mlhub.Collection, "fetch", fetch_collection) download_radiant_mlhub_collection("", str(tmp_path)) diff --git a/tests/datasets/test_vhr10.py b/tests/datasets/test_vhr10.py index 6147c0bb9d5..46ff434adfc 100644 --- a/tests/datasets/test_vhr10.py +++ b/tests/datasets/test_vhr10.py @@ -30,7 +30,7 @@ class TestVHR10: def dataset( self, monkeypatch: MonkeyPatch, tmp_path: Path, request: SubRequest ) -> VHR10: - pytest.importorskip("rarfile", minversion="3") + pytest.importorskip("rarfile", minversion="4") monkeypatch.setattr(torchgeo.datasets.vhr10, "download_url", download_url) monkeypatch.setattr(torchgeo.datasets.utils, "download_url", download_url) url = os.path.join("tests", "data", "vhr10", "NWPU VHR-10 dataset.rar") diff --git a/tests/datasets/test_western_usa_live_fuel_moisture.py b/tests/datasets/test_western_usa_live_fuel_moisture.py index ad79ca7937c..c3246d2f0db 100644 --- a/tests/datasets/test_western_usa_live_fuel_moisture.py +++ b/tests/datasets/test_western_usa_live_fuel_moisture.py @@ -36,7 +36,7 @@ class TestWesternUSALiveFuelMoisture: def dataset( self, monkeypatch: MonkeyPatch, tmp_path: Path ) -> WesternUSALiveFuelMoisture: - radiant_mlhub = pytest.importorskip("radiant_mlhub", minversion="0.2.1") + radiant_mlhub = pytest.importorskip("radiant_mlhub", minversion="0.3") monkeypatch.setattr(radiant_mlhub.Collection, "fetch", fetch) md5 = "ecbc9269dd27c4efe7aa887960054351" monkeypatch.setattr(WesternUSALiveFuelMoisture, "md5", md5) diff --git a/tests/datasets/test_zuericrop.py b/tests/datasets/test_zuericrop.py index 70007b68b1f..49beeb3b957 100644 --- a/tests/datasets/test_zuericrop.py +++ b/tests/datasets/test_zuericrop.py @@ -16,7 +16,7 @@ import torchgeo.datasets.utils from torchgeo.datasets import ZueriCrop -pytest.importorskip("h5py", minversion="2.6") +pytest.importorskip("h5py", minversion="3") def download_url(url: str, root: str, *args: str, **kwargs: str) -> None: diff --git a/tests/trainers/test_classification.py b/tests/trainers/test_classification.py index 4abdf95bb87..d88923ce176 100644 --- a/tests/trainers/test_classification.py +++ b/tests/trainers/test_classification.py @@ -84,7 +84,7 @@ def test_trainer( self, monkeypatch: MonkeyPatch, name: str, fast_dev_run: bool ) -> None: if name.startswith("so2sat"): - pytest.importorskip("h5py", minversion="2.6") + pytest.importorskip("h5py", minversion="3") conf = OmegaConf.load(os.path.join("tests", "conf", name + ".yaml")) From f34525be715774e9da560e0627b2a2a950db3e05 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 16:20:39 -0500 Subject: [PATCH 04/17] Bump actions/setup-python from 4.5.0 to 4.6.0 (#1278) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4.5.0 to 4.6.0. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4.5.0...v4.6.0) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yaml | 6 +++--- .github/workflows/style.yaml | 10 +++++----- .github/workflows/tests.yaml | 6 +++--- .github/workflows/tutorials.yaml | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0729d65b940..564bfdf5b0e 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -14,7 +14,7 @@ jobs: - name: Clone repo uses: actions/checkout@v3.5.2 - name: Set up python - uses: actions/setup-python@v4.5.0 + uses: actions/setup-python@v4.6.0 with: python-version: '3.10' - name: Cache dependencies @@ -37,7 +37,7 @@ jobs: - name: Clone repo uses: actions/checkout@v3.5.2 - name: Set up python - uses: actions/setup-python@v4.5.0 + uses: actions/setup-python@v4.6.0 with: python-version: '3.10' - name: Cache dependencies @@ -60,7 +60,7 @@ jobs: - name: Clone repo uses: actions/checkout@v3.5.2 - name: Set up python - uses: actions/setup-python@v4.5.0 + uses: actions/setup-python@v4.6.0 with: python-version: '3.10' - name: Cache dependencies diff --git a/.github/workflows/style.yaml b/.github/workflows/style.yaml index da816748469..35a4b10ea82 100644 --- a/.github/workflows/style.yaml +++ b/.github/workflows/style.yaml @@ -16,7 +16,7 @@ jobs: - name: Clone repo uses: actions/checkout@v3.5.2 - name: Set up python - uses: actions/setup-python@v4.5.0 + uses: actions/setup-python@v4.6.0 with: python-version: '3.10' - name: Cache dependencies @@ -39,7 +39,7 @@ jobs: - name: Clone repo uses: actions/checkout@v3.5.2 - name: Set up python - uses: actions/setup-python@v4.5.0 + uses: actions/setup-python@v4.6.0 with: python-version: '3.10' - name: Cache dependencies @@ -62,7 +62,7 @@ jobs: - name: Clone repo uses: actions/checkout@v3.5.2 - name: Set up python - uses: actions/setup-python@v4.5.0 + uses: actions/setup-python@v4.6.0 with: python-version: '3.10' - name: Cache dependencies @@ -85,7 +85,7 @@ jobs: - name: Clone repo uses: actions/checkout@v3.5.2 - name: Set up python - uses: actions/setup-python@v4.5.0 + uses: actions/setup-python@v4.6.0 with: python-version: '3.10' - name: Cache dependencies @@ -108,7 +108,7 @@ jobs: - name: Clone repo uses: actions/checkout@v3.5.2 - name: Set up python - uses: actions/setup-python@v4.5.0 + uses: actions/setup-python@v4.6.0 with: python-version: '3.10' - name: Cache dependencies diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 2874698656a..d5a339bf42d 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -16,7 +16,7 @@ jobs: - name: Clone repo uses: actions/checkout@v3.5.2 - name: Set up python - uses: actions/setup-python@v4.5.0 + uses: actions/setup-python@v4.6.0 with: python-version: '3.10' - name: Cache dependencies @@ -45,7 +45,7 @@ jobs: - name: Clone repo uses: actions/checkout@v3.5.2 - name: Set up python - uses: actions/setup-python@v4.5.0 + uses: actions/setup-python@v4.6.0 with: python-version: ${{ matrix.python-version }} - name: Cache dependencies @@ -87,7 +87,7 @@ jobs: - name: Clone repo uses: actions/checkout@v3.5.2 - name: Set up python - uses: actions/setup-python@v4.5.0 + uses: actions/setup-python@v4.6.0 with: python-version: '3.9' - name: Cache dependencies diff --git a/.github/workflows/tutorials.yaml b/.github/workflows/tutorials.yaml index 384dfc0357a..e02cdc9452c 100644 --- a/.github/workflows/tutorials.yaml +++ b/.github/workflows/tutorials.yaml @@ -18,7 +18,7 @@ jobs: - name: Clone repo uses: actions/checkout@v3.5.2 - name: Set up python - uses: actions/setup-python@v4.5.0 + uses: actions/setup-python@v4.6.0 with: python-version: '3.10' - name: Cache dependencies From ffa38f96e6465bdebfddfd1a5094b4109494e0ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 21:22:38 +0000 Subject: [PATCH 05/17] Bump lightning from 2.0.1.post0 to 2.0.2 in /requirements (#1282) Bumps [lightning](https://github.com/Lightning-AI/lightning) from 2.0.1.post0 to 2.0.2. - [Release notes](https://github.com/Lightning-AI/lightning/releases) - [Commits](https://github.com/Lightning-AI/lightning/compare/2.0.1.post0...2.0.2) --- updated-dependencies: - dependency-name: lightning dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/required.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/required.txt b/requirements/required.txt index 41053c9f5fb..51d68f562fc 100644 --- a/requirements/required.txt +++ b/requirements/required.txt @@ -5,7 +5,7 @@ setuptools==67.7.0 einops==0.6.1 fiona==1.9.3 kornia==0.6.12 -lightning==2.0.1.post0 +lightning==2.0.2 matplotlib==3.7.1 numpy==1.24.2 pillow==9.5.0 From 4f8f4c162ce2320a93759ef621a4ff4a242d96e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 21:23:47 +0000 Subject: [PATCH 06/17] Bump numpy from 1.24.2 to 1.24.3 in /requirements (#1281) Bumps [numpy](https://github.com/numpy/numpy) from 1.24.2 to 1.24.3. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v1.24.2...v1.24.3) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/required.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/required.txt b/requirements/required.txt index 51d68f562fc..e6b558227a7 100644 --- a/requirements/required.txt +++ b/requirements/required.txt @@ -7,7 +7,7 @@ fiona==1.9.3 kornia==0.6.12 lightning==2.0.2 matplotlib==3.7.1 -numpy==1.24.2 +numpy==1.24.3 pillow==9.5.0 pyproj==3.5.0 rasterio==1.3.6 From 1623b1a96b555086511ded2905badf549ba6f58a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 21:27:16 +0000 Subject: [PATCH 07/17] Bump pandas from 2.0.0 to 2.0.1 in /requirements (#1280) Bumps [pandas](https://github.com/pandas-dev/pandas) from 2.0.0 to 2.0.1. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Commits](https://github.com/pandas-dev/pandas/compare/v2.0.0...v2.0.1) --- updated-dependencies: - dependency-name: pandas dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/datasets.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/datasets.txt b/requirements/datasets.txt index 5aabece7b96..8bac81abaf0 100644 --- a/requirements/datasets.txt +++ b/requirements/datasets.txt @@ -2,7 +2,7 @@ h5py==3.8.0 laspy==2.4.1 opencv-python==4.7.0.72 -pandas==2.0.0 +pandas==2.0.1 pycocotools==2.0.6 pyvista==0.38.5 radiant-mlhub==0.4.1 From 2247c76fda8e1aad8cf1719e28e314170a66e75c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 21:34:03 +0000 Subject: [PATCH 08/17] Bump codecov/codecov-action from 3.1.2 to 3.1.3 (#1279) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3.1.2 to 3.1.3. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v3.1.2...v3.1.3) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/tests.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index d5a339bf42d..0e41daa66fc 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -75,7 +75,7 @@ jobs: - name: Run pytest checks run: pytest --cov=torchgeo --cov-report=xml --durations=10 - name: Report coverage - uses: codecov/codecov-action@v3.1.2 + uses: codecov/codecov-action@v3.1.3 with: token: ${{ secrets.CODECOV_TOKEN }} minimum: @@ -110,7 +110,7 @@ jobs: - name: Run pytest checks run: pytest --cov=torchgeo --cov-report=xml --durations=10 - name: Report coverage - uses: codecov/codecov-action@v3.1.2 + uses: codecov/codecov-action@v3.1.3 with: token: ${{ secrets.CODECOV_TOKEN }} concurrency: From 4f714f7177318e71240ef50e30e3c1dfcefc061b Mon Sep 17 00:00:00 2001 From: Caleb Robinson Date: Mon, 24 Apr 2023 15:00:14 -0700 Subject: [PATCH 09/17] Remove scikit-learn (#1063) * Remove sklearn * New groupshufflesplit * added tests * Black and typing fix * Update torchgeo/datamodules/utils.py Co-authored-by: Adam J. Stewart * Requested changes * REquested changes * Change the tests to pass with round() * Typing * TIL Iterable is now in collections.abc * Adding type hints to returns * isort * Update torchgeo/datamodules/utils.py Co-authored-by: Adam J. Stewart * last * last for real * Comment out type hints * Just use lists * Link to docs --------- Co-authored-by: Adam J. Stewart --- docs/conf.py | 1 + environment.yml | 1 - requirements/min-reqs.old | 1 - requirements/required.txt | 1 - setup.cfg | 2 - tests/datamodules/test_utils.py | 35 ++++++++++++++- torchgeo/datamodules/cyclone.py | 8 ++-- torchgeo/datamodules/sen12ms.py | 8 ++-- torchgeo/datamodules/utils.py | 75 +++++++++++++++++++++++++++++++++ 9 files changed, 116 insertions(+), 16 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 6d409a1953f..9b2ec3ff7bd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -114,6 +114,7 @@ "rasterio": ("https://rasterio.readthedocs.io/en/stable/", None), "rtree": ("https://rtree.readthedocs.io/en/stable/", None), "segmentation_models_pytorch": ("https://smp.readthedocs.io/en/stable/", None), + "sklearn": ("https://scikit-learn.org/stable/", None), "timm": ("https://huggingface.co/docs/timm/main/en/", None), "torch": ("https://pytorch.org/docs/stable", None), "torchvision": ("https://pytorch.org/vision/stable", None), diff --git a/environment.yml b/environment.yml index 73317d5d103..625a33697c4 100644 --- a/environment.yml +++ b/environment.yml @@ -42,7 +42,6 @@ dependencies: - radiant-mlhub>=0.3 - rtree>=1 - scikit-image>=0.18 - - scikit-learn>=0.24 - scipy>=1.6.2 - segmentation-models-pytorch>=0.2 - setuptools>=42 diff --git a/requirements/min-reqs.old b/requirements/min-reqs.old index c6330210e81..38c842a9e9a 100644 --- a/requirements/min-reqs.old +++ b/requirements/min-reqs.old @@ -12,7 +12,6 @@ pillow==8.0.0 pyproj==3.0.0 rasterio==1.2.0 rtree==1.0.0 -scikit-learn==0.24 segmentation-models-pytorch==0.2.0 shapely==1.7.1 timm==0.4.12 diff --git a/requirements/required.txt b/requirements/required.txt index e6b558227a7..f42c80cf6a1 100644 --- a/requirements/required.txt +++ b/requirements/required.txt @@ -12,7 +12,6 @@ pillow==9.5.0 pyproj==3.5.0 rasterio==1.3.6 rtree==1.0.1 -scikit-learn==1.2.2 segmentation-models-pytorch==0.3.2 shapely==2.0.1 timm==0.6.12 diff --git a/setup.cfg b/setup.cfg index 45e9c8cae02..9fccee9854a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,8 +44,6 @@ install_requires = rasterio>=1.2,<2 # rtree 1+ required for len(index), index & index, index | index rtree>=1,<2 - # scikit-learn 0.24+ required for Python 3.9 wheels - scikit-learn>=0.24,<2 # segmentation-models-pytorch 0.2+ required for smp.losses module segmentation-models-pytorch>=0.2,<0.4 # shapely 1.7.1+ required for Python 3.9 wheels diff --git a/tests/datamodules/test_utils.py b/tests/datamodules/test_utils.py index a023cafe5e1..ea410fe4b1d 100644 --- a/tests/datamodules/test_utils.py +++ b/tests/datamodules/test_utils.py @@ -1,10 +1,14 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import re + +import numpy as np +import pytest import torch from torch.utils.data import TensorDataset -from torchgeo.datamodules.utils import dataset_split +from torchgeo.datamodules.utils import dataset_split, group_shuffle_split def test_dataset_split() -> None: @@ -23,3 +27,32 @@ def test_dataset_split() -> None: assert len(train_ds) == round(num_samples / 3) assert len(val_ds) == round(num_samples / 3) assert len(test_ds) == round(num_samples / 3) + + +def test_group_shuffle_split() -> None: + alphabet = np.array(list("abcdefghijklmnopqrstuvwxyz")) + groups = np.random.randint(0, 26, size=(1000)) + groups = alphabet[groups] + + with pytest.raises(ValueError, match="You must specify `train_size` *"): + group_shuffle_split(groups, train_size=None, test_size=None) + with pytest.raises(ValueError, match="`train_size` and `test_size` must sum to 1."): + group_shuffle_split(groups, train_size=0.2, test_size=1.0) + with pytest.raises( + ValueError, + match=re.escape("`train_size` and `test_size` must be in the range (0,1)."), + ): + group_shuffle_split(groups, train_size=-0.2, test_size=1.2) + with pytest.raises(ValueError, match="26 groups were found, however the current *"): + group_shuffle_split(groups, train_size=None, test_size=0.999) + + train_indices, test_indices = group_shuffle_split( + groups, train_size=None, test_size=0.2 + ) + assert len(set(train_indices) & set(test_indices)) == 0 + assert len(set(groups[train_indices])) == 21 + train_indices, test_indices = group_shuffle_split( + groups, train_size=0.8, test_size=None + ) + assert len(set(train_indices) & set(test_indices)) == 0 + assert len(set(groups[train_indices])) == 21 diff --git a/torchgeo/datamodules/cyclone.py b/torchgeo/datamodules/cyclone.py index b3c8d3121a9..34446280286 100644 --- a/torchgeo/datamodules/cyclone.py +++ b/torchgeo/datamodules/cyclone.py @@ -5,11 +5,11 @@ from typing import Any -from sklearn.model_selection import GroupShuffleSplit from torch.utils.data import Subset from ..datasets import TropicalCyclone from .geo import NonGeoDataModule +from .utils import group_shuffle_split class TropicalCycloneDataModule(NonGeoDataModule): @@ -50,10 +50,8 @@ def setup(self, stage: str) -> None: storm_id = item["href"].split("/")[0].split("_")[-2] storm_ids.append(storm_id) - train_indices, val_indices = next( - GroupShuffleSplit(test_size=0.2, n_splits=2, random_state=0).split( - storm_ids, groups=storm_ids - ) + train_indices, val_indices = group_shuffle_split( + storm_ids, test_size=0.2, random_state=0 ) self.train_dataset = Subset(self.dataset, train_indices) diff --git a/torchgeo/datamodules/sen12ms.py b/torchgeo/datamodules/sen12ms.py index e07b3d01e5a..a7c1710aaea 100644 --- a/torchgeo/datamodules/sen12ms.py +++ b/torchgeo/datamodules/sen12ms.py @@ -6,12 +6,12 @@ from typing import Any import torch -from sklearn.model_selection import GroupShuffleSplit from torch import Tensor from torch.utils.data import Subset from ..datasets import SEN12MS from .geo import NonGeoDataModule +from .utils import group_shuffle_split class SEN12MSDataModule(NonGeoDataModule): @@ -87,10 +87,8 @@ def setup(self, stage: str) -> None: scene_id = int(parts[3]) scenes.append(season_id + scene_id) - train_indices, val_indices = next( - GroupShuffleSplit(test_size=0.2, n_splits=2, random_state=0).split( - scenes, groups=scenes - ) + train_indices, val_indices = group_shuffle_split( + scenes, test_size=0.2, random_state=0 ) self.train_dataset = Subset(self.dataset, train_indices) diff --git a/torchgeo/datamodules/utils.py b/torchgeo/datamodules/utils.py index 832ab1d0318..d0bb6af9934 100644 --- a/torchgeo/datamodules/utils.py +++ b/torchgeo/datamodules/utils.py @@ -3,8 +3,11 @@ """Common datamodule utilities.""" +import math +from collections.abc import Iterable from typing import Any, Optional, Union +import numpy as np from torch import Generator from torch.utils.data import Subset, TensorDataset, random_split @@ -52,3 +55,75 @@ def dataset_split( [train_length, val_length, test_length], generator=Generator().manual_seed(0), ) + + +def group_shuffle_split( + groups: Iterable[Any], + train_size: Optional[float] = None, + test_size: Optional[float] = None, + random_state: Optional[int] = None, +) -> tuple[list[int], list[int]]: + """Method for performing a single group-wise shuffle split of data. + + Loosely based off of :class:`sklearn.model_selection.GroupShuffleSplit`. + + Args: + groups: a sequence of group values used to split. Should be in the same order as + the data you want to split. + train_size: the proportion of groups to include in the train split. If None, + then it is set to complement `test_size`. + test_size: the proportion of groups to include in the test split (rounded up). + If None, then it is set to complement `train_size`. + random_state: controls the random splits (passed a seed to a + numpy.random.Generator), set for reproducible splits. + + Returns: + train_indices, test_indices + + Raises: + ValueError if `train_size` and `test_size` do not sum to 1, aren't in the range + (0,1), or are both None. + ValueError if the number of training or testing groups turns out to be 0. + """ + if train_size is None and test_size is None: + raise ValueError("You must specify `train_size`, `test_size`, or both.") + if (train_size is not None and test_size is not None) and ( + not math.isclose(train_size + test_size, 1) + ): + raise ValueError("`train_size` and `test_size` must sum to 1.") + + if train_size is None and test_size is not None: + train_size = 1 - test_size + if test_size is None and train_size is not None: + test_size = 1 - train_size + + assert train_size is not None and test_size is not None + + if train_size <= 0 or train_size >= 1 or test_size <= 0 or test_size >= 1: + raise ValueError("`train_size` and `test_size` must be in the range (0,1).") + + group_vals = set(groups) + n_groups = len(group_vals) + n_test_groups = round(n_groups * test_size) + n_train_groups = n_groups - n_test_groups + + if n_train_groups == 0 or n_test_groups == 0: + raise ValueError( + f"{n_groups} groups were found, however the current settings of " + + "`train_size` and `test_size` result in 0 training or testing groups." + ) + + generator = np.random.default_rng(seed=random_state) + train_group_vals = set( + generator.choice(list(group_vals), size=n_train_groups, replace=False) + ) + + train_idxs = [] + test_idxs = [] + for i, group_val in enumerate(groups): + if group_val in train_group_vals: + train_idxs.append(i) + else: + test_idxs.append(i) + + return train_idxs, test_idxs From 76786277e54fb4342770834740d65d0258931579 Mon Sep 17 00:00:00 2001 From: Isaac Corley <22203655+isaaccorley@users.noreply.github.com> Date: Tue, 25 Apr 2023 09:05:10 -0500 Subject: [PATCH 10/17] Add PixelwiseRegressionTask (#1241) --- tests/conf/cowc_counting.yaml | 1 + tests/conf/cyclone.yaml | 1 + tests/conf/skippd.yaml | 1 + tests/conf/sustainbench_crop_yield.yaml | 1 + tests/trainers/test_regression.py | 106 +++++++++++++++++++++++- torchgeo/trainers/__init__.py | 3 +- torchgeo/trainers/regression.py | 105 +++++++++++++++++++---- 7 files changed, 200 insertions(+), 18 deletions(-) diff --git a/tests/conf/cowc_counting.yaml b/tests/conf/cowc_counting.yaml index 76eb04763a6..c5855bef5fb 100644 --- a/tests/conf/cowc_counting.yaml +++ b/tests/conf/cowc_counting.yaml @@ -6,6 +6,7 @@ module: in_channels: 3 learning_rate: 1e-3 learning_rate_schedule_patience: 2 + loss: "mse" datamodule: _target_: torchgeo.datamodules.COWCCountingDataModule diff --git a/tests/conf/cyclone.yaml b/tests/conf/cyclone.yaml index 91a477a144d..5b096dcfe7b 100644 --- a/tests/conf/cyclone.yaml +++ b/tests/conf/cyclone.yaml @@ -6,6 +6,7 @@ module: in_channels: 3 learning_rate: 1e-3 learning_rate_schedule_patience: 2 + loss: "mse" datamodule: _target_: torchgeo.datamodules.TropicalCycloneDataModule diff --git a/tests/conf/skippd.yaml b/tests/conf/skippd.yaml index 20ca10f24a6..6b1fdfdc22b 100644 --- a/tests/conf/skippd.yaml +++ b/tests/conf/skippd.yaml @@ -6,6 +6,7 @@ module: in_channels: 3 learning_rate: 1e-3 learning_rate_schedule_patience: 2 + loss: "mse" datamodule: _target_: torchgeo.datamodules.SKIPPDDataModule diff --git a/tests/conf/sustainbench_crop_yield.yaml b/tests/conf/sustainbench_crop_yield.yaml index 2f48a83d02a..09fbb37d05a 100644 --- a/tests/conf/sustainbench_crop_yield.yaml +++ b/tests/conf/sustainbench_crop_yield.yaml @@ -6,6 +6,7 @@ module: in_channels: 9 learning_rate: 1e-3 learning_rate_schedule_patience: 2 + loss: "mse" datamodule: _target_: torchgeo.datamodules.SustainBenchCropYieldDataModule diff --git a/tests/trainers/test_regression.py b/tests/trainers/test_regression.py index cabfe5a2cdc..3479ef385e6 100644 --- a/tests/trainers/test_regression.py +++ b/tests/trainers/test_regression.py @@ -3,27 +3,41 @@ import os from pathlib import Path -from typing import Any +from typing import Any, cast import pytest +import segmentation_models_pytorch as smp import timm import torch +import torch.nn as nn import torchvision from _pytest.fixtures import SubRequest from _pytest.monkeypatch import MonkeyPatch from hydra.utils import instantiate from lightning.pytorch import Trainer from omegaconf import OmegaConf +from torch.nn.modules import Module from torchvision.models._api import WeightsEnum from torchgeo.datamodules import MisconfigurationException, TropicalCycloneDataModule from torchgeo.datasets import TropicalCyclone from torchgeo.models import get_model_weights, list_models -from torchgeo.trainers import RegressionTask +from torchgeo.trainers import PixelwiseRegressionTask, RegressionTask from .test_classification import ClassificationTestModel +class PixelwiseRegressionTestModel(Module): + def __init__(self, in_channels: int = 3, classes: int = 1, **kwargs: Any) -> None: + super().__init__() + self.conv1 = nn.Conv2d( + in_channels=in_channels, out_channels=classes, kernel_size=1, padding=0 + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return cast(torch.Tensor, self.conv1(x)) + + class RegressionTestModel(ClassificationTestModel): def __init__(self, in_chans: int = 3, num_classes: int = 1, **kwargs: Any) -> None: super().__init__(in_chans=in_chans, num_classes=num_classes) @@ -43,6 +57,10 @@ def plot(*args: Any, **kwargs: Any) -> None: raise ValueError +def create_model(**kwargs: Any) -> Module: + return PixelwiseRegressionTestModel(**kwargs) + + class TestRegressionTask: @pytest.mark.parametrize( "name", ["cowc_counting", "cyclone", "sustainbench_crop_yield", "skippd"] @@ -85,6 +103,7 @@ def model_kwargs(self) -> dict[str, Any]: "weights": None, "num_outputs": 1, "in_channels": 3, + "loss": "mse", } @pytest.fixture( @@ -180,3 +199,86 @@ def test_predict(self, model_kwargs: dict[Any, Any], fast_dev_run: bool) -> None max_epochs=1, ) trainer.predict(model=model, datamodule=datamodule) + + def test_invalid_loss(self, model_kwargs: dict[str, Any]) -> None: + model_kwargs["loss"] = "invalid_loss" + match = "Loss type 'invalid_loss' is not valid." + with pytest.raises(ValueError, match=match): + RegressionTask(**model_kwargs) + + +class TestPixelwiseRegressionTask: + @pytest.mark.parametrize( + "name,batch_size,loss,model_type", + [ + ("inria", 1, "mse", "unet"), + ("inria", 2, "mae", "deeplabv3+"), + ("inria", 1, "mse", "fcn"), + ], + ) + def test_trainer( + self, + monkeypatch: MonkeyPatch, + name: str, + batch_size: int, + loss: str, + model_type: str, + fast_dev_run: bool, + model_kwargs: dict[str, Any], + ) -> None: + conf = OmegaConf.load(os.path.join("tests", "conf", name + ".yaml")) + + # Instantiate datamodule + conf.datamodule.batch_size = batch_size + datamodule = instantiate(conf.datamodule) + + # Instantiate model + monkeypatch.setattr(smp, "Unet", create_model) + monkeypatch.setattr(smp, "DeepLabV3Plus", create_model) + model_kwargs["model"] = model_type + model_kwargs["loss"] = loss + + if model_type == "fcn": + model_kwargs["num_filters"] = 2 + + model = PixelwiseRegressionTask(**model_kwargs) + model.model = PixelwiseRegressionTestModel( + in_channels=model_kwargs["in_channels"] + ) + + # Instantiate trainer + trainer = Trainer( + accelerator="cpu", + fast_dev_run=fast_dev_run, + log_every_n_steps=1, + max_epochs=1, + ) + + trainer.fit(model=model, datamodule=datamodule) + try: + trainer.test(model=model, datamodule=datamodule) + except MisconfigurationException: + pass + try: + trainer.predict(model=model, datamodule=datamodule) + except MisconfigurationException: + pass + + def test_invalid_model(self, model_kwargs: dict[str, Any]) -> None: + model_kwargs["model"] = "invalid_model" + match = "Model type 'invalid_model' is not valid." + with pytest.raises(ValueError, match=match): + PixelwiseRegressionTask(**model_kwargs) + + @pytest.fixture + def model_kwargs(self) -> dict[str, Any]: + return { + "model": "unet", + "backbone": "resnet18", + "weights": None, + "num_outputs": 1, + "in_channels": 3, + "loss": "mse", + "learning_rate": 1e-3, + "learning_rate_schedule_patience": 6, + } diff --git a/torchgeo/trainers/__init__.py b/torchgeo/trainers/__init__.py index 6240b53f681..e1db43fd3e5 100644 --- a/torchgeo/trainers/__init__.py +++ b/torchgeo/trainers/__init__.py @@ -6,7 +6,7 @@ from .byol import BYOLTask from .classification import ClassificationTask, MultiLabelClassificationTask from .detection import ObjectDetectionTask -from .regression import RegressionTask +from .regression import PixelwiseRegressionTask, RegressionTask from .segmentation import SemanticSegmentationTask __all__ = ( @@ -14,6 +14,7 @@ "ClassificationTask", "MultiLabelClassificationTask", "ObjectDetectionTask", + "PixelwiseRegressionTask", "RegressionTask", "SemanticSegmentationTask", ) diff --git a/torchgeo/trainers/regression.py b/torchgeo/trainers/regression.py index 45daf0788b7..c20b556e565 100644 --- a/torchgeo/trainers/regression.py +++ b/torchgeo/trainers/regression.py @@ -7,9 +7,10 @@ from typing import Any, cast import matplotlib.pyplot as plt +import segmentation_models_pytorch as smp import timm import torch -import torch.nn.functional as F +import torch.nn as nn from lightning.pytorch import LightningModule from torch import Tensor from torch.optim.lr_scheduler import ReduceLROnPlateau @@ -17,7 +18,7 @@ from torchvision.models._api import WeightsEnum from ..datasets import unbind_samples -from ..models import get_weight +from ..models import FCN, get_weight from . import utils @@ -35,8 +36,10 @@ class RegressionTask(LightningModule): # type: ignore[misc] print(timm.list_models()) """ - def config_task(self) -> None: - """Configures the task based on kwargs parameters.""" + target_key: str = "label" + + def config_model(self) -> None: + """Configures the model based on kwargs parameters.""" # Create model weights = self.hyperparams["weights"] self.model = timm.create_model( @@ -56,6 +59,21 @@ def config_task(self) -> None: state_dict = get_weight(weights).get_state_dict(progress=True) self.model = utils.load_state_dict(self.model, state_dict) + def config_task(self) -> None: + """Configures the task based on kwargs parameters.""" + self.config_model() + + self.loss: nn.Module + if self.hyperparams["loss"] == "mse": + self.loss = nn.MSELoss() + elif self.hyperparams["loss"] == "mae": + self.loss = nn.L1Loss() + else: + raise ValueError( + f"Loss type '{self.hyperparams['loss']}' is not valid. " + f"Currently, supports 'mse' or 'mae' loss." + ) + def __init__(self, **kwargs: Any) -> None: """Initialize a new LightningModule for training simple regression models. @@ -80,7 +98,11 @@ def __init__(self, **kwargs: Any) -> None: self.config_task() self.train_metrics = MetricCollection( - {"RMSE": MeanSquaredError(squared=False), "MAE": MeanAbsoluteError()}, + { + "RMSE": MeanSquaredError(squared=False), + "MSE": MeanSquaredError(squared=True), + "MAE": MeanAbsoluteError(), + }, prefix="train_", ) self.val_metrics = self.train_metrics.clone(prefix="val_") @@ -108,13 +130,15 @@ def training_step(self, *args: Any, **kwargs: Any) -> Tensor: """ batch = args[0] x = batch["image"] - y = batch["label"].view(-1, 1) + y = batch[self.target_key] y_hat = self(x) - loss = F.mse_loss(y_hat, y) + if y_hat.ndim != y.ndim: + y = y.unsqueeze(dim=1) + loss: Tensor = self.loss(y_hat, y.to(torch.float)) self.log("train_loss", loss) # logging to TensorBoard - self.train_metrics(y_hat, y) + self.train_metrics(y_hat, y.to(torch.float)) return loss @@ -133,12 +157,15 @@ def validation_step(self, *args: Any, **kwargs: Any) -> None: batch = args[0] batch_idx = args[1] x = batch["image"] - y = batch["label"].view(-1, 1) + y = batch[self.target_key] y_hat = self(x) - loss = F.mse_loss(y_hat, y) + if y_hat.ndim != y.ndim: + y = y.unsqueeze(dim=1) + + loss = self.loss(y_hat, y.to(torch.float)) self.log("val_loss", loss) - self.val_metrics(y_hat, y) + self.val_metrics(y_hat, y.to(torch.float)) if ( batch_idx < 10 @@ -149,8 +176,11 @@ def validation_step(self, *args: Any, **kwargs: Any) -> None: ): try: datamodule = self.trainer.datamodule + if self.target_key == "mask": + y = y.squeeze(dim=1) + y_hat = y_hat.squeeze(dim=1) batch["prediction"] = y_hat - for key in ["image", "label", "prediction"]: + for key in ["image", self.target_key, "prediction"]: batch[key] = batch[key].cpu() sample = unbind_samples(batch)[0] fig = datamodule.plot(sample) @@ -175,12 +205,15 @@ def test_step(self, *args: Any, **kwargs: Any) -> None: """ batch = args[0] x = batch["image"] - y = batch["label"].view(-1, 1) + y = batch[self.target_key] y_hat = self(x) - loss = F.mse_loss(y_hat, y) + if y_hat.ndim != y.ndim: + y = y.unsqueeze(dim=1) + + loss = self.loss(y_hat, y.to(torch.float)) self.log("test_loss", loss) - self.test_metrics(y_hat, y) + self.test_metrics(y_hat, y.to(torch.float)) def on_test_epoch_end(self) -> None: """Logs epoch level test metrics.""" @@ -219,3 +252,45 @@ def configure_optimizers(self) -> dict[str, Any]: "monitor": "val_loss", }, } + + +class PixelwiseRegressionTask(RegressionTask): + """LightningModule for pixelwise regression of images. + + Supports `Segmentation Models Pytorch + `_ + as an architecture choice in combination with any of these + `TIMM backbones `_. + + .. versionadded:: 0.5 + """ + + target_key: str = "mask" + + def config_model(self) -> None: + """Configures the model based on kwargs parameters.""" + if self.hyperparams["model"] == "unet": + self.model = smp.Unet( + encoder_name=self.hyperparams["backbone"], + encoder_weights=self.hyperparams["weights"], + in_channels=self.hyperparams["in_channels"], + classes=1, + ) + elif self.hyperparams["model"] == "deeplabv3+": + self.model = smp.DeepLabV3Plus( + encoder_name=self.hyperparams["backbone"], + encoder_weights=self.hyperparams["weights"], + in_channels=self.hyperparams["in_channels"], + classes=1, + ) + elif self.hyperparams["model"] == "fcn": + self.model = FCN( + in_channels=self.hyperparams["in_channels"], + classes=1, + num_filters=self.hyperparams["num_filters"], + ) + else: + raise ValueError( + f"Model type '{self.hyperparams['model']}' is not valid. " + f"Currently, only supports 'unet', 'deeplabv3+' and 'fcn'." + ) From 8a2e9b423383c23578efe1087a0906ec79c566ea Mon Sep 17 00:00:00 2001 From: nsutezo Date: Tue, 25 Apr 2023 09:43:01 -0700 Subject: [PATCH 11/17] added class_weights for cross entropy loss to segmentation.py (#1221) * added class_weights for cross entropy loss to segmentation.py * added class_weights for cross entropy loss to segmentation.py * added class_weights for cross entropy loss to segmentation.py and fixed formatting * added class_weights for cross entropy loss to segmentation.py, fixed formatting, added deleted loss * Made class_weights argument optional * fixed black formatting * included versionadded: 0.5 and parameter to docstring * added newline between sections, removed manual type checking, moved class_weights parameter after loss parameter * Deleted duplicated line --------- Co-authored-by: Caleb Robinson --- torchgeo/trainers/segmentation.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/torchgeo/trainers/segmentation.py b/torchgeo/trainers/segmentation.py index 759b4ffe41c..fed2c596cc9 100644 --- a/torchgeo/trainers/segmentation.py +++ b/torchgeo/trainers/segmentation.py @@ -59,7 +59,13 @@ def config_task(self) -> None: if self.hyperparams["loss"] == "ce": ignore_value = -1000 if self.ignore_index is None else self.ignore_index - self.loss = nn.CrossEntropyLoss(ignore_index=ignore_value) + + class_weights = ( + torch.FloatTensor(self.class_weights) if self.class_weights else None + ) + self.loss = nn.CrossEntropyLoss( + ignore_index=ignore_value, weight=class_weights + ) elif self.hyperparams["loss"] == "jaccard": self.loss = smp.losses.JaccardLoss( mode="multiclass", classes=self.hyperparams["num_classes"] @@ -86,6 +92,8 @@ def __init__(self, **kwargs: Any) -> None: num_classes: Number of semantic classes to predict loss: Name of the loss function, currently supports 'ce', 'jaccard' or 'focal' loss + class_weights: Optional rescaling weight given to each + class and used with 'ce' loss ignore_index: Optional integer class index to ignore in the loss and metrics learning_rate: Learning rate for optimizer learning_rate_schedule_patience: Patience for learning rate scheduler @@ -100,6 +108,9 @@ def __init__(self, **kwargs: Any) -> None: The *segmentation_model* parameter was renamed to *model*, *encoder_name* renamed to *backbone*, and *encoder_weights* to *weights*. + + .. versionadded: 0.5 + The *class_weights* parameter. """ super().__init__() @@ -115,6 +126,8 @@ def __init__(self, **kwargs: Any) -> None: UserWarning, ) self.ignore_index = kwargs["ignore_index"] + self.class_weights = kwargs.get("class_weights", None) + self.config_task() self.train_metrics = MetricCollection( From e2b5f36321c620d866081717d18dd342c41e5809 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Tue, 25 Apr 2023 12:21:52 -0500 Subject: [PATCH 12/17] Add EOL char before EOF char (#1286) --- SECURITY.md | 2 +- conf/bigearthnet.yaml | 2 +- conf/chesapeake_cvpr.yaml | 2 +- conf/cowc_counting.yaml | 2 +- conf/cyclone.yaml | 2 +- conf/deepglobelandcover.yaml | 2 +- conf/defaults.yaml | 2 +- conf/etci2021.yaml | 2 +- conf/eurosat.yaml | 2 +- conf/gid15.yaml | 2 +- conf/inria.yaml | 2 +- conf/landcoverai.yaml | 2 +- conf/naipchesapeake.yaml | 2 +- conf/nasa_marine_debris.yaml | 2 +- conf/potsdam2d.yaml | 2 +- conf/resisc45.yaml | 2 +- conf/seco_100k.yaml | 2 +- conf/sen12ms.yaml | 2 +- conf/so2sat.yaml | 2 +- conf/spacenet1.yaml | 2 +- conf/ucmerced.yaml | 2 +- conf/vaihingen2d.yaml | 2 +- tests/conf/bigearthnet_all.yaml | 2 +- tests/conf/bigearthnet_s1.yaml | 2 +- tests/conf/bigearthnet_s2.yaml | 2 +- tests/conf/chesapeake_cvpr_5.yaml | 2 +- tests/conf/chesapeake_cvpr_7.yaml | 2 +- tests/conf/chesapeake_cvpr_prior_byol.yaml | 2 +- tests/conf/cowc_counting.yaml | 2 +- tests/conf/cyclone.yaml | 2 +- tests/conf/deepglobelandcover.yaml | 2 +- tests/conf/etci2021.yaml | 2 +- tests/conf/eurosat.yaml | 2 +- tests/conf/eurosat100.yaml | 2 +- tests/conf/fire_risk.yaml | 2 +- tests/conf/gid15.yaml | 2 +- tests/conf/inria.yaml | 2 +- tests/conf/l7irish.yaml | 2 +- tests/conf/l8biome.yaml | 2 +- tests/conf/landcoverai.yaml | 2 +- tests/conf/loveda.yaml | 2 +- tests/conf/naipchesapeake.yaml | 2 +- tests/conf/nasa_marine_debris.yaml | 2 +- tests/conf/potsdam2d.yaml | 2 +- tests/conf/resisc45.yaml | 2 +- tests/conf/sen12ms_all.yaml | 2 +- tests/conf/sen12ms_s1.yaml | 2 +- tests/conf/sen12ms_s2_all.yaml | 2 +- tests/conf/sen12ms_s2_reduced.yaml | 2 +- tests/conf/skippd.yaml | 2 +- tests/conf/so2sat_all.yaml | 2 +- tests/conf/so2sat_s1.yaml | 2 +- tests/conf/so2sat_s2.yaml | 2 +- tests/conf/spacenet1.yaml | 2 +- tests/conf/ssl4eo_s12_byol_1.yaml | 2 +- tests/conf/ssl4eo_s12_byol_2.yaml | 2 +- tests/conf/sustainbench_crop_yield.yaml | 2 +- tests/conf/ucmerced.yaml | 2 +- tests/conf/vaihingen2d.yaml | 2 +- 59 files changed, 59 insertions(+), 59 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index f7b89984f0f..926b8ae4059 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -38,4 +38,4 @@ We prefer all communications to be in English. Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). - \ No newline at end of file + diff --git a/conf/bigearthnet.yaml b/conf/bigearthnet.yaml index 2c8a4da5218..3f159efa4b1 100644 --- a/conf/bigearthnet.yaml +++ b/conf/bigearthnet.yaml @@ -21,4 +21,4 @@ trainer: accelerator: gpu devices: 1 min_epochs: 15 - max_epochs: 40 \ No newline at end of file + max_epochs: 40 diff --git a/conf/chesapeake_cvpr.yaml b/conf/chesapeake_cvpr.yaml index d30f555187e..81af245a35a 100644 --- a/conf/chesapeake_cvpr.yaml +++ b/conf/chesapeake_cvpr.yaml @@ -31,4 +31,4 @@ trainer: accelerator: gpu devices: 1 min_epochs: 15 - max_epochs: 40 \ No newline at end of file + max_epochs: 40 diff --git a/conf/cowc_counting.yaml b/conf/cowc_counting.yaml index 787bb55b835..3b5d36779aa 100644 --- a/conf/cowc_counting.yaml +++ b/conf/cowc_counting.yaml @@ -18,4 +18,4 @@ trainer: accelerator: gpu devices: 1 min_epochs: 15 - max_epochs: 40 \ No newline at end of file + max_epochs: 40 diff --git a/conf/cyclone.yaml b/conf/cyclone.yaml index f5200ae8d74..2bb689ed4bf 100644 --- a/conf/cyclone.yaml +++ b/conf/cyclone.yaml @@ -18,4 +18,4 @@ trainer: accelerator: gpu devices: 1 min_epochs: 15 - max_epochs: 40 \ No newline at end of file + max_epochs: 40 diff --git a/conf/deepglobelandcover.yaml b/conf/deepglobelandcover.yaml index 9c7da9adbb4..0260e0ac0f3 100644 --- a/conf/deepglobelandcover.yaml +++ b/conf/deepglobelandcover.yaml @@ -25,4 +25,4 @@ trainer: accelerator: gpu devices: 1 min_epochs: 15 - max_epochs: 40 \ No newline at end of file + max_epochs: 40 diff --git a/conf/defaults.yaml b/conf/defaults.yaml index adcdf816e43..15d58be2656 100644 --- a/conf/defaults.yaml +++ b/conf/defaults.yaml @@ -5,4 +5,4 @@ program: # These are the arguments that define how the train.py script works output_dir: output data_dir: data log_dir: logs - overwrite: False \ No newline at end of file + overwrite: False diff --git a/conf/etci2021.yaml b/conf/etci2021.yaml index 2550f7e234d..5f06393cfd5 100644 --- a/conf/etci2021.yaml +++ b/conf/etci2021.yaml @@ -21,4 +21,4 @@ trainer: accelerator: gpu devices: 1 min_epochs: 15 - max_epochs: 40 \ No newline at end of file + max_epochs: 40 diff --git a/conf/eurosat.yaml b/conf/eurosat.yaml index 6f744a04cda..b90f7823e01 100644 --- a/conf/eurosat.yaml +++ b/conf/eurosat.yaml @@ -19,4 +19,4 @@ trainer: accelerator: gpu devices: 1 min_epochs: 15 - max_epochs: 40 \ No newline at end of file + max_epochs: 40 diff --git a/conf/gid15.yaml b/conf/gid15.yaml index 2f21fc94195..f46672da6ce 100644 --- a/conf/gid15.yaml +++ b/conf/gid15.yaml @@ -25,4 +25,4 @@ trainer: accelerator: gpu devices: 1 min_epochs: 15 - max_epochs: 40 \ No newline at end of file + max_epochs: 40 diff --git a/conf/inria.yaml b/conf/inria.yaml index e0f716e292b..32083a3c6b9 100644 --- a/conf/inria.yaml +++ b/conf/inria.yaml @@ -22,4 +22,4 @@ trainer: accelerator: gpu devices: 1 min_epochs: 15 - max_epochs: 40 \ No newline at end of file + max_epochs: 40 diff --git a/conf/landcoverai.yaml b/conf/landcoverai.yaml index 0136527a19a..c3742581972 100644 --- a/conf/landcoverai.yaml +++ b/conf/landcoverai.yaml @@ -22,4 +22,4 @@ trainer: accelerator: gpu devices: 1 min_epochs: 15 - max_epochs: 40 \ No newline at end of file + max_epochs: 40 diff --git a/conf/naipchesapeake.yaml b/conf/naipchesapeake.yaml index ede6db4e336..6b562778fe0 100644 --- a/conf/naipchesapeake.yaml +++ b/conf/naipchesapeake.yaml @@ -24,4 +24,4 @@ trainer: accelerator: gpu devices: 1 min_epochs: 15 - max_epochs: 40 \ No newline at end of file + max_epochs: 40 diff --git a/conf/nasa_marine_debris.yaml b/conf/nasa_marine_debris.yaml index 89164a63c74..d176e95c0e1 100644 --- a/conf/nasa_marine_debris.yaml +++ b/conf/nasa_marine_debris.yaml @@ -19,4 +19,4 @@ trainer: accelerator: gpu devices: 1 min_epochs: 15 - max_epochs: 40 \ No newline at end of file + max_epochs: 40 diff --git a/conf/potsdam2d.yaml b/conf/potsdam2d.yaml index 076a1d75f72..747e99c2047 100644 --- a/conf/potsdam2d.yaml +++ b/conf/potsdam2d.yaml @@ -25,4 +25,4 @@ trainer: accelerator: gpu devices: 1 min_epochs: 15 - max_epochs: 40 \ No newline at end of file + max_epochs: 40 diff --git a/conf/resisc45.yaml b/conf/resisc45.yaml index 05978aa5e84..fc22c9ca9e3 100644 --- a/conf/resisc45.yaml +++ b/conf/resisc45.yaml @@ -19,4 +19,4 @@ trainer: accelerator: gpu devices: 1 min_epochs: 15 - max_epochs: 40 \ No newline at end of file + max_epochs: 40 diff --git a/conf/seco_100k.yaml b/conf/seco_100k.yaml index e9d83fa4e87..41c6338bc02 100644 --- a/conf/seco_100k.yaml +++ b/conf/seco_100k.yaml @@ -21,4 +21,4 @@ trainer: accelerator: gpu devices: 1 min_epochs: 15 - max_epochs: 40 \ No newline at end of file + max_epochs: 40 diff --git a/conf/sen12ms.yaml b/conf/sen12ms.yaml index 553d5c996e8..f1b4643c426 100644 --- a/conf/sen12ms.yaml +++ b/conf/sen12ms.yaml @@ -22,4 +22,4 @@ trainer: accelerator: gpu devices: 1 min_epochs: 15 - max_epochs: 40 \ No newline at end of file + max_epochs: 40 diff --git a/conf/so2sat.yaml b/conf/so2sat.yaml index b54025dfe51..4a785a50e00 100644 --- a/conf/so2sat.yaml +++ b/conf/so2sat.yaml @@ -20,4 +20,4 @@ trainer: accelerator: gpu devices: 1 min_epochs: 15 - max_epochs: 40 \ No newline at end of file + max_epochs: 40 diff --git a/conf/spacenet1.yaml b/conf/spacenet1.yaml index 3bfd735680d..c7a236f4634 100644 --- a/conf/spacenet1.yaml +++ b/conf/spacenet1.yaml @@ -21,4 +21,4 @@ trainer: accelerator: gpu devices: 1 min_epochs: 15 - max_epochs: 40 \ No newline at end of file + max_epochs: 40 diff --git a/conf/ucmerced.yaml b/conf/ucmerced.yaml index bae2aab676e..95fbe6fb87c 100644 --- a/conf/ucmerced.yaml +++ b/conf/ucmerced.yaml @@ -19,4 +19,4 @@ trainer: accelerator: gpu devices: 1 min_epochs: 15 - max_epochs: 40 \ No newline at end of file + max_epochs: 40 diff --git a/conf/vaihingen2d.yaml b/conf/vaihingen2d.yaml index db6248b052f..4c5cf3b139a 100644 --- a/conf/vaihingen2d.yaml +++ b/conf/vaihingen2d.yaml @@ -25,4 +25,4 @@ trainer: accelerator: gpu devices: 1 min_epochs: 15 - max_epochs: 40 \ No newline at end of file + max_epochs: 40 diff --git a/tests/conf/bigearthnet_all.yaml b/tests/conf/bigearthnet_all.yaml index f034c155b9b..3babdc7fd8b 100644 --- a/tests/conf/bigearthnet_all.yaml +++ b/tests/conf/bigearthnet_all.yaml @@ -15,4 +15,4 @@ datamodule: num_classes: ${module.num_classes} download: true batch_size: 1 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/bigearthnet_s1.yaml b/tests/conf/bigearthnet_s1.yaml index fa49d81c775..8c07950cb5f 100644 --- a/tests/conf/bigearthnet_s1.yaml +++ b/tests/conf/bigearthnet_s1.yaml @@ -15,4 +15,4 @@ datamodule: num_classes: ${module.num_classes} download: true batch_size: 1 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/bigearthnet_s2.yaml b/tests/conf/bigearthnet_s2.yaml index 3677de83c79..9408e20b633 100644 --- a/tests/conf/bigearthnet_s2.yaml +++ b/tests/conf/bigearthnet_s2.yaml @@ -15,4 +15,4 @@ datamodule: num_classes: ${module.num_classes} download: true batch_size: 1 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/chesapeake_cvpr_5.yaml b/tests/conf/chesapeake_cvpr_5.yaml index b4f345c3ab8..a3f8e08b48d 100644 --- a/tests/conf/chesapeake_cvpr_5.yaml +++ b/tests/conf/chesapeake_cvpr_5.yaml @@ -25,4 +25,4 @@ datamodule: patch_size: 64 num_workers: 0 class_set: ${module.num_classes} - use_prior_labels: False \ No newline at end of file + use_prior_labels: False diff --git a/tests/conf/chesapeake_cvpr_7.yaml b/tests/conf/chesapeake_cvpr_7.yaml index 634440e680e..5b1f0669423 100644 --- a/tests/conf/chesapeake_cvpr_7.yaml +++ b/tests/conf/chesapeake_cvpr_7.yaml @@ -25,4 +25,4 @@ datamodule: patch_size: 64 num_workers: 0 class_set: ${module.num_classes} - use_prior_labels: False \ No newline at end of file + use_prior_labels: False diff --git a/tests/conf/chesapeake_cvpr_prior_byol.yaml b/tests/conf/chesapeake_cvpr_prior_byol.yaml index 6b6841d8f65..3ccf939feff 100644 --- a/tests/conf/chesapeake_cvpr_prior_byol.yaml +++ b/tests/conf/chesapeake_cvpr_prior_byol.yaml @@ -20,4 +20,4 @@ datamodule: patch_size: 64 num_workers: 0 class_set: 5 - use_prior_labels: True \ No newline at end of file + use_prior_labels: True diff --git a/tests/conf/cowc_counting.yaml b/tests/conf/cowc_counting.yaml index c5855bef5fb..f67b1b6a1be 100644 --- a/tests/conf/cowc_counting.yaml +++ b/tests/conf/cowc_counting.yaml @@ -13,4 +13,4 @@ datamodule: root: "tests/data/cowc_counting" download: true batch_size: 1 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/cyclone.yaml b/tests/conf/cyclone.yaml index 5b096dcfe7b..f7ecff850ba 100644 --- a/tests/conf/cyclone.yaml +++ b/tests/conf/cyclone.yaml @@ -13,4 +13,4 @@ datamodule: root: "tests/data/cyclone" download: true batch_size: 1 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/deepglobelandcover.yaml b/tests/conf/deepglobelandcover.yaml index 09b0f4d9414..392fe3ce7b7 100644 --- a/tests/conf/deepglobelandcover.yaml +++ b/tests/conf/deepglobelandcover.yaml @@ -18,4 +18,4 @@ datamodule: batch_size: 1 patch_size: 2 val_split_pct: 0.5 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/etci2021.yaml b/tests/conf/etci2021.yaml index 65c75374431..9af839e92e3 100644 --- a/tests/conf/etci2021.yaml +++ b/tests/conf/etci2021.yaml @@ -15,4 +15,4 @@ datamodule: root: "tests/data/etci2021" download: true batch_size: 1 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/eurosat.yaml b/tests/conf/eurosat.yaml index 8e39dd50557..7066f7f66ce 100644 --- a/tests/conf/eurosat.yaml +++ b/tests/conf/eurosat.yaml @@ -13,4 +13,4 @@ datamodule: root: "tests/data/eurosat" download: true batch_size: 1 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/eurosat100.yaml b/tests/conf/eurosat100.yaml index b1e5fe6438b..65e4be957f2 100644 --- a/tests/conf/eurosat100.yaml +++ b/tests/conf/eurosat100.yaml @@ -13,4 +13,4 @@ datamodule: root: "tests/data/eurosat" download: true batch_size: 1 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/fire_risk.yaml b/tests/conf/fire_risk.yaml index 0c86285235a..8971ee6839a 100644 --- a/tests/conf/fire_risk.yaml +++ b/tests/conf/fire_risk.yaml @@ -13,4 +13,4 @@ datamodule: root: "tests/data/fire_risk" download: false batch_size: 2 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/gid15.yaml b/tests/conf/gid15.yaml index 3af0a01f24e..c9af542d037 100644 --- a/tests/conf/gid15.yaml +++ b/tests/conf/gid15.yaml @@ -19,4 +19,4 @@ datamodule: batch_size: 1 patch_size: 2 val_split_pct: 0.5 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/inria.yaml b/tests/conf/inria.yaml index 04af3433f1e..c6296802f9e 100644 --- a/tests/conf/inria.yaml +++ b/tests/conf/inria.yaml @@ -17,4 +17,4 @@ datamodule: patch_size: 2 num_workers: 0 val_split_pct: 0.2 - test_split_pct: 0.2 \ No newline at end of file + test_split_pct: 0.2 diff --git a/tests/conf/l7irish.yaml b/tests/conf/l7irish.yaml index cb54362d964..d5147b0032d 100644 --- a/tests/conf/l7irish.yaml +++ b/tests/conf/l7irish.yaml @@ -19,4 +19,4 @@ datamodule: batch_size: 1 patch_size: 32 length: 5 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/l8biome.yaml b/tests/conf/l8biome.yaml index 796266d2e24..ae42f6efff3 100644 --- a/tests/conf/l8biome.yaml +++ b/tests/conf/l8biome.yaml @@ -19,4 +19,4 @@ datamodule: batch_size: 1 patch_size: 32 length: 5 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/landcoverai.yaml b/tests/conf/landcoverai.yaml index 20ec3653471..691d19bb9be 100644 --- a/tests/conf/landcoverai.yaml +++ b/tests/conf/landcoverai.yaml @@ -17,4 +17,4 @@ datamodule: root: "tests/data/landcoverai" download: true batch_size: 1 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/loveda.yaml b/tests/conf/loveda.yaml index 92f324cb018..7a558ea2207 100644 --- a/tests/conf/loveda.yaml +++ b/tests/conf/loveda.yaml @@ -17,4 +17,4 @@ datamodule: root: "tests/data/loveda" download: true batch_size: 1 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/naipchesapeake.yaml b/tests/conf/naipchesapeake.yaml index 06aa921a6dd..f9c0e4880fa 100644 --- a/tests/conf/naipchesapeake.yaml +++ b/tests/conf/naipchesapeake.yaml @@ -18,4 +18,4 @@ datamodule: chesapeake_download: true batch_size: 2 num_workers: 0 - patch_size: 32 \ No newline at end of file + patch_size: 32 diff --git a/tests/conf/nasa_marine_debris.yaml b/tests/conf/nasa_marine_debris.yaml index 01e6de32916..7103560c5f3 100644 --- a/tests/conf/nasa_marine_debris.yaml +++ b/tests/conf/nasa_marine_debris.yaml @@ -12,4 +12,4 @@ datamodule: root: "tests/data/nasa_marine_debris" download: true batch_size: 1 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/potsdam2d.yaml b/tests/conf/potsdam2d.yaml index 9ac40d93681..bd5f8f6c0ca 100644 --- a/tests/conf/potsdam2d.yaml +++ b/tests/conf/potsdam2d.yaml @@ -18,4 +18,4 @@ datamodule: batch_size: 1 patch_size: 2 val_split_pct: 0.5 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/resisc45.yaml b/tests/conf/resisc45.yaml index 7dee7bc43fe..f8d1729572e 100644 --- a/tests/conf/resisc45.yaml +++ b/tests/conf/resisc45.yaml @@ -13,4 +13,4 @@ datamodule: root: "tests/data/resisc45" download: true batch_size: 1 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/sen12ms_all.yaml b/tests/conf/sen12ms_all.yaml index 0bdbc54ddff..fe3d592a356 100644 --- a/tests/conf/sen12ms_all.yaml +++ b/tests/conf/sen12ms_all.yaml @@ -15,4 +15,4 @@ datamodule: root: "tests/data/sen12ms" band_set: "all" batch_size: 1 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/sen12ms_s1.yaml b/tests/conf/sen12ms_s1.yaml index 8cf4435c624..b0b9d553931 100644 --- a/tests/conf/sen12ms_s1.yaml +++ b/tests/conf/sen12ms_s1.yaml @@ -16,4 +16,4 @@ datamodule: root: "tests/data/sen12ms" band_set: "s1" batch_size: 1 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/sen12ms_s2_all.yaml b/tests/conf/sen12ms_s2_all.yaml index a7712cf4a78..e80b74896e0 100644 --- a/tests/conf/sen12ms_s2_all.yaml +++ b/tests/conf/sen12ms_s2_all.yaml @@ -15,4 +15,4 @@ datamodule: root: "tests/data/sen12ms" band_set: "s2-all" batch_size: 1 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/sen12ms_s2_reduced.yaml b/tests/conf/sen12ms_s2_reduced.yaml index 9493519da2d..15758690e03 100644 --- a/tests/conf/sen12ms_s2_reduced.yaml +++ b/tests/conf/sen12ms_s2_reduced.yaml @@ -15,4 +15,4 @@ datamodule: root: "tests/data/sen12ms" band_set: "s2-reduced" batch_size: 1 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/skippd.yaml b/tests/conf/skippd.yaml index 6b1fdfdc22b..14dd1bcaabe 100644 --- a/tests/conf/skippd.yaml +++ b/tests/conf/skippd.yaml @@ -13,4 +13,4 @@ datamodule: root: "tests/data/skippd" download: true batch_size: 1 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/so2sat_all.yaml b/tests/conf/so2sat_all.yaml index 1033918e0ff..e9b378bdbef 100644 --- a/tests/conf/so2sat_all.yaml +++ b/tests/conf/so2sat_all.yaml @@ -13,4 +13,4 @@ datamodule: root: "tests/data/so2sat" batch_size: 1 num_workers: 0 - band_set: "all" \ No newline at end of file + band_set: "all" diff --git a/tests/conf/so2sat_s1.yaml b/tests/conf/so2sat_s1.yaml index 44a437d0ec5..8c560ed86ec 100644 --- a/tests/conf/so2sat_s1.yaml +++ b/tests/conf/so2sat_s1.yaml @@ -13,4 +13,4 @@ datamodule: root: "tests/data/so2sat" batch_size: 1 num_workers: 0 - band_set: "s1" \ No newline at end of file + band_set: "s1" diff --git a/tests/conf/so2sat_s2.yaml b/tests/conf/so2sat_s2.yaml index b7474bc7705..d7ba063efac 100644 --- a/tests/conf/so2sat_s2.yaml +++ b/tests/conf/so2sat_s2.yaml @@ -13,4 +13,4 @@ datamodule: root: "tests/data/so2sat" batch_size: 1 num_workers: 0 - band_set: "s2" \ No newline at end of file + band_set: "s2" diff --git a/tests/conf/spacenet1.yaml b/tests/conf/spacenet1.yaml index e4feb50a37e..dc88c2504d1 100644 --- a/tests/conf/spacenet1.yaml +++ b/tests/conf/spacenet1.yaml @@ -19,4 +19,4 @@ datamodule: batch_size: 1 num_workers: 0 val_split_pct: 0.33 - test_split_pct: 0.33 \ No newline at end of file + test_split_pct: 0.33 diff --git a/tests/conf/ssl4eo_s12_byol_1.yaml b/tests/conf/ssl4eo_s12_byol_1.yaml index 0bc3267ecc0..0e1b825421a 100644 --- a/tests/conf/ssl4eo_s12_byol_1.yaml +++ b/tests/conf/ssl4eo_s12_byol_1.yaml @@ -11,4 +11,4 @@ datamodule: root: "tests/data/ssl4eo/s12" seasons: 1 batch_size: 2 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/ssl4eo_s12_byol_2.yaml b/tests/conf/ssl4eo_s12_byol_2.yaml index cced864fc6e..b784aab61aa 100644 --- a/tests/conf/ssl4eo_s12_byol_2.yaml +++ b/tests/conf/ssl4eo_s12_byol_2.yaml @@ -11,4 +11,4 @@ datamodule: root: "tests/data/ssl4eo/s12" seasons: 2 batch_size: 2 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/sustainbench_crop_yield.yaml b/tests/conf/sustainbench_crop_yield.yaml index 09fbb37d05a..9b092aab674 100644 --- a/tests/conf/sustainbench_crop_yield.yaml +++ b/tests/conf/sustainbench_crop_yield.yaml @@ -13,4 +13,4 @@ datamodule: root: "tests/data/sustainbench_crop_yield" download: true batch_size: 1 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/ucmerced.yaml b/tests/conf/ucmerced.yaml index 22f61ff7cd0..93e37db6059 100644 --- a/tests/conf/ucmerced.yaml +++ b/tests/conf/ucmerced.yaml @@ -13,4 +13,4 @@ datamodule: root: "tests/data/ucmerced" download: true batch_size: 2 - num_workers: 0 \ No newline at end of file + num_workers: 0 diff --git a/tests/conf/vaihingen2d.yaml b/tests/conf/vaihingen2d.yaml index 8bd3043a673..ebdc8613ad2 100644 --- a/tests/conf/vaihingen2d.yaml +++ b/tests/conf/vaihingen2d.yaml @@ -18,4 +18,4 @@ datamodule: batch_size: 1 patch_size: 2 val_split_pct: 0.5 - num_workers: 0 \ No newline at end of file + num_workers: 0 From a653f01c9d67b2227c9ead83478deccdc4e9fd8f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Apr 2023 21:15:29 +0000 Subject: [PATCH 13/17] Bump pyupgrade from 3.3.1 to 3.3.2 in /requirements (#1288) Bumps [pyupgrade](https://github.com/asottile/pyupgrade) from 3.3.1 to 3.3.2. - [Release notes](https://github.com/asottile/pyupgrade/releases) - [Commits](https://github.com/asottile/pyupgrade/compare/v3.3.1...v3.3.2) --- updated-dependencies: - dependency-name: pyupgrade dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/style.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/style.txt b/requirements/style.txt index 5ea62aa188a..2b7ff2f7284 100644 --- a/requirements/style.txt +++ b/requirements/style.txt @@ -3,4 +3,4 @@ black[jupyter]==23.3.0 flake8==6.0.0 isort[colors]==5.12.0 pydocstyle[toml]==6.3.0 -pyupgrade==3.3.1 +pyupgrade==3.3.2 From a0ae00cc9f5a38acf2feeb66b89f1b24c5766525 Mon Sep 17 00:00:00 2001 From: Caleb Robinson Date: Tue, 25 Apr 2023 15:03:29 -0700 Subject: [PATCH 14/17] Add multiple versions of the So2Sat dataset (#1283) * Add multiple versions of the So2Sat dataset * Update tests * Adding datamodule * Adding datamodule * black * Update tests * Updated min requirement for pytorch * Revert "Updated min requirement for pytorch" This reverts commit 9c0194ec527a94b47ba1b8789f1fd7750476c699. * Using old random_split * Shorten the mean and stdev lists * Add description of splits to docstring * Remove version parameter * Fix version * Requested updates * Update torchgeo/datasets/so2sat.py Co-authored-by: Adam J. Stewart * Update torchgeo/datasets/so2sat.py Co-authored-by: Adam J. Stewart * Update torchgeo/datasets/so2sat.py Co-authored-by: Adam J. Stewart * Update torchgeo/datasets/so2sat.py Co-authored-by: Adam J. Stewart * Requested changes * Playing with docs * Updating key in test config --------- Co-authored-by: Adam J. Stewart --- tests/conf/so2sat_all.yaml | 1 + tests/conf/so2sat_rgb.yaml | 18 ++ tests/conf/so2sat_s1.yaml | 1 + tests/data/so2sat/block/testing.h5 | Bin 0 -> 19448 bytes tests/data/so2sat/block/training.h5 | Bin 0 -> 19440 bytes tests/data/so2sat/culture_10/testing.h5 | Bin 0 -> 19448 bytes tests/data/so2sat/culture_10/training.h5 | Bin 0 -> 19440 bytes tests/data/so2sat/data.py | 20 +- tests/data/so2sat/random/testing.h5 | Bin 0 -> 19448 bytes tests/data/so2sat/random/training.h5 | Bin 0 -> 19440 bytes tests/data/so2sat/testing.h5 | Bin 75793 -> 19448 bytes tests/data/so2sat/training.h5 | Bin 75793 -> 19440 bytes tests/data/so2sat/validation.h5 | Bin 75793 -> 19432 bytes tests/datasets/test_so2sat.py | 16 +- tests/trainers/test_classification.py | 1 + torchgeo/datamodules/so2sat.py | 236 +++++++++++++++++------ torchgeo/datasets/so2sat.py | 94 +++++++-- 17 files changed, 295 insertions(+), 92 deletions(-) create mode 100644 tests/conf/so2sat_rgb.yaml create mode 100644 tests/data/so2sat/block/testing.h5 create mode 100644 tests/data/so2sat/block/training.h5 create mode 100644 tests/data/so2sat/culture_10/testing.h5 create mode 100644 tests/data/so2sat/culture_10/training.h5 create mode 100644 tests/data/so2sat/random/testing.h5 create mode 100644 tests/data/so2sat/random/training.h5 diff --git a/tests/conf/so2sat_all.yaml b/tests/conf/so2sat_all.yaml index e9b378bdbef..22919afe697 100644 --- a/tests/conf/so2sat_all.yaml +++ b/tests/conf/so2sat_all.yaml @@ -13,4 +13,5 @@ datamodule: root: "tests/data/so2sat" batch_size: 1 num_workers: 0 + version: "2" band_set: "all" diff --git a/tests/conf/so2sat_rgb.yaml b/tests/conf/so2sat_rgb.yaml new file mode 100644 index 00000000000..75f7490ce22 --- /dev/null +++ b/tests/conf/so2sat_rgb.yaml @@ -0,0 +1,18 @@ +module: + _target_: torchgeo.trainers.ClassificationTask + loss: "ce" + model: "resnet18" + learning_rate: 1e-3 + learning_rate_schedule_patience: 6 + weights: null + in_channels: 3 + num_classes: 17 + +datamodule: + _target_: torchgeo.datamodules.So2SatDataModule + root: "tests/data/so2sat" + batch_size: 1 + num_workers: 0 + version: "3_random" + band_set: "rgb" + val_split_pct: 0.5 diff --git a/tests/conf/so2sat_s1.yaml b/tests/conf/so2sat_s1.yaml index 8c560ed86ec..c81e79742b8 100644 --- a/tests/conf/so2sat_s1.yaml +++ b/tests/conf/so2sat_s1.yaml @@ -13,4 +13,5 @@ datamodule: root: "tests/data/so2sat" batch_size: 1 num_workers: 0 + version: "2" band_set: "s1" diff --git a/tests/data/so2sat/block/testing.h5 b/tests/data/so2sat/block/testing.h5 new file mode 100644 index 0000000000000000000000000000000000000000..9e5020028a014f391bfe6c516125947f7201503f GIT binary patch literal 19448 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 AAOHXW literal 0 HcmV?d00001 diff --git a/tests/data/so2sat/block/training.h5 b/tests/data/so2sat/block/training.h5 new file mode 100644 index 0000000000000000000000000000000000000000..f485280c8f3f59f4c33db094650aa81615fabcf9 GIT binary patch literal 19440 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 DMWvID literal 0 HcmV?d00001 diff --git a/tests/data/so2sat/culture_10/testing.h5 b/tests/data/so2sat/culture_10/testing.h5 new file mode 100644 index 0000000000000000000000000000000000000000..9e5020028a014f391bfe6c516125947f7201503f GIT binary patch literal 19448 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 AAOHXW literal 0 HcmV?d00001 diff --git a/tests/data/so2sat/culture_10/training.h5 b/tests/data/so2sat/culture_10/training.h5 new file mode 100644 index 0000000000000000000000000000000000000000..f485280c8f3f59f4c33db094650aa81615fabcf9 GIT binary patch literal 19440 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 DMWvID literal 0 HcmV?d00001 diff --git a/tests/data/so2sat/data.py b/tests/data/so2sat/data.py index 9a0ca7fe372..ef34aa68ddf 100755 --- a/tests/data/so2sat/data.py +++ b/tests/data/so2sat/data.py @@ -5,13 +5,14 @@ import hashlib import os +import shutil import h5py import numpy as np -SIZE = 64 # image width/height +SIZE = 32 # image width/height NUM_CLASSES = 17 -NUM_SAMPLES = 1 +NUM_SAMPLES = 2 np.random.seed(0) @@ -28,16 +29,21 @@ ] # Random images - sen1 = np.random.randint(256, size=(NUM_SAMPLES, SIZE, SIZE, 8), dtype=np.uint8) - sen2 = np.random.randint(256, size=(NUM_SAMPLES, SIZE, SIZE, 10), dtype=np.uint8) + sen1 = np.random.randint(2, size=(NUM_SAMPLES, SIZE, SIZE, 8), dtype=np.uint8) + sen2 = np.random.randint(2, size=(NUM_SAMPLES, SIZE, SIZE, 10), dtype=np.uint8) # Create datasets with h5py.File(filename, "w") as f: - f.create_dataset("label", data=label) - f.create_dataset("sen1", data=sen1) - f.create_dataset("sen2", data=sen2) + f.create_dataset("label", data=label, compression="gzip", compression_opts=9) + f.create_dataset("sen1", data=sen1, compression="gzip", compression_opts=9) + f.create_dataset("sen2", data=sen2, compression="gzip", compression_opts=9) # Compute checksums with open(filename, "rb") as f: md5 = hashlib.md5(f.read()).hexdigest() print(repr(split.replace("ing", "")) + ":", repr(md5) + ",") + +for version in ["random", "block", "culture_10"]: + os.makedirs(version, exist_ok=True) + shutil.copyfile("training.h5", os.path.join(version, "training.h5")) + shutil.copyfile("testing.h5", os.path.join(version, "testing.h5")) diff --git a/tests/data/so2sat/random/testing.h5 b/tests/data/so2sat/random/testing.h5 new file mode 100644 index 0000000000000000000000000000000000000000..9e5020028a014f391bfe6c516125947f7201503f GIT binary patch literal 19448 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 AAOHXW literal 0 HcmV?d00001 diff --git a/tests/data/so2sat/random/training.h5 b/tests/data/so2sat/random/training.h5 new file mode 100644 index 0000000000000000000000000000000000000000..f485280c8f3f59f4c33db094650aa81615fabcf9 GIT binary patch literal 19440 zcmeI41yEdFmdCqs2<{LxI5ZAHo8S(O2M7?faR|XB5C{<5A;I0a`(gI~2LV|rF_qh$YTs>_>dyAvowK_pz{bef%;s0a(aetX*7wVD-P-%}$$#7U zI|>kh^gH)%03d+z&uk#vZ^!-gICo?IbsPTIdB2Uky{dj^p!^w$2tfI>4F~_*{QyAq zX9f5>=vV!=9|$ltgW4E5nE_Dm#z(oklHlRs-4K8KZl}DR{x0*L&|Shm*r+Q*B!K|< zU#t6FsNnzh1owB73ZlOqcYj@@u>bA2|0}(>_q*Hg_x?V2Z|~31f40vGtiSB}>+^PZ z2>vn%)`Ag$5r7eZ5r7eZ5r7eZ5%`xS;5KJsC<>59Qf%w{wFf}}yxW{}BC5+g2fC#B zp3kO8FX7>nf;>yYUR*g)wW!j?xlQX6FYm1l>ksAEKab9Tc91xn)X>d z_`GpeK1!Z$|EkP(RPCINZ*U0==1U(;HV@sPUnGF$R$mB^MjVjIKUfzy+BY;$6xg*_ zw`|xS5pQkzc`C&2wD=WtetGS$Ip6bz9?j8zXoGB_p5I4cUg~JQOMWo6r!Yi>eN{kN z(8#PbrC^Fh5XF0%4WoojnT=N z>%t33QA;a@ma#lAZ^~m|wpGnZY*lKZ@z6}Hcz1Hh;7bQGz48+8z54bLurRl`h-@DUNzj-%Y@tBwXp8bcCK z7saDMf*@rcc66Qc$fD6E5s&x+-l@~fey4De(DEs+8VRSr0On&Z184G9{Lk1-_etak z1f7+=tje3`r^l)j1%wBSYT2lqIoE@nQTHWgyNcR(3!oMC-Hl82eoc8{$kQ`Fb$l1P zIUi#2XQCl0yx{*JN%0cd9$69(M+FPyho{SImgA6R-6RW0;eG#1Jl+MOM$P}=IRw$1 zFQ6vHkfG}}KLiS#B)88WAP41%mc}e#+s98Vq!t^bOzunxzd4g zoM*zNL187f4DU$*;*tsC2+;vCI&|Jj_}@79w0n*?!(87B6!gURcjU@Ui6}fnmsSwB zs^KO9xK}n?Rq$B_5NuYPM~j#WNnO5%($;4ms_xO60@uC{nWe5rW#;sB>o~P3I0iA<}##(w=Bi5Etl9NZcL$=(c;ae2IPOf7 zyiKiM@n+6uttDD&7*$i_!5)mt-~D&kj)I%j=-n}{%Y*TyF+!V-n)$(_aBL)Rh}>zO zkCL$94dy?l#mBJU{j}n%Fy;SbeL8ez`*G<2a&|)Z);y@Ui_9UFycl{lW=J#=EUi)` zeb)8n{dbK4mk4-#rSrl=kXURkklz80Sac1CbBnKc`Iw#xS4AMy`;olcVLx?k14=bz zpZn?PSf%AbM)`N30I)Ud#>)HfWP{@Mey1Zn@@g4UoeP6~r`sUhlh>KX$QNC)`e0K1 zm#+dCJsF=x2A*@UY*PonmGyv9byjRD(`yU9B2R!uH99AwuJ&t!aHoLh9o(O#P?q~x z=i4mNWR&uM`k&^XGN!mWgM;#N5A=fRDFQ`fd%EO^Sk%ehaW#4E*={@p|R&6~MzG zw%uubw+p>>-hOVE^?Ts}j$hTkzx>wG;QoV-7J4U+{&z=gzth$1@AB>z^L^!9p^&6ifugjam#`k~toV8r(U3lGifhoUxe%IN)oW5K+ zo;X|B2;Q%9XKxWgFh{v@j>sG|H(Kb;O;-=sIi_nKaeOtsb2(q_=(*<)AF_U&8*?(lZbUr|7GM@tkrO$jV=#>pDJ&%?Y zV9@7F3m+w)v7RD7i~Mc^Z7gsR77^z%jmhpEnvOU$ihtENd|YiVvHSMKUwuBQl1q*!5T!FlEh&CE0diNG#Yth+|)iQJG0ez~Y9JD)p5SL&VyZ z5abUxwby{+_uj)tCDiDN&MEwh%Dez z5Hs=e3-WizgxbhteUgO@EDZ%OlwLqYT-O>gUM|$Yg)6%UlEm`AMa>|fRYYS2B#TyY zW72cH*}<+EMxt1stk7;zP;vHq;Gt9c)}rHDdIIG_SL-c0kh<=5TaeX~W%QP)KYUmT z-&d^090?&=EmPZEST_nb3`Z7I`>JhfgDi^gK6Ebe#ev<6#<# zWLWNGJ1#z5WQa)UDsIJl;31*E=iIYAn`Bm2=Ke_#wpN7dvVKYC-Z;9eqAp^4EQ7*V z4tj0et_S%x?2suh?Us01@BDJ+QQTNJG*aDus8+#LP#>0OwcEP^cXI5B*WLP6h8n4H z-E`XZC?%O;UC}n6N8UiOWUZ#->{IgD-4OxkV4GVIk}H;|8*^Pm9(#lZt24Dw#Vp#h zKNNH)_=kuf$}jj{&0lU2bGVF=ZykY_2i44>9s=HuEu0?{zM zPT>=C|9JRO!&C8VWh@QTTxtQ0W&LN9tZoW#6-SGwa+X8bpJ$mJydrZ%DZ8nS_n6>@ zKc!T1J!_QkdLD~sS<5S;wqF)0Zaq=d!pK-?B<&~U&pbzwfzU`RG*&9{+DFF^8cCq! z9+y{Wd2$*qc_3B8LXCh<gaM+QpAlcn0XV=cQk?B9*BG{$1^{l~2&%Cj# zYEXXmBY!}3Gdr6NmQIFm0bwoQ0O*@hHfFI!cnGm}6)vhmaDX(IBWKQx9<@{D!rn>vwPJ1Uc|6_Ia9;9C*{~g6pbGxFME~Eq*EkG^#@%g@t&&^uvlkpAnn-7ZDA~$E%t%EoE zV~gomKkPqwb~=tPCbdr9=zBfA^c-?rsSaKA_PG85KD0a7A72;-Tc^yY(~Yd3`|fSc z?i{~L_ZdA~D;o(MGs&r5`Z8!=9{Ea$fi>)^`rNVqIH)a5^P{w)mIEyv2t`aLsXJyJ zdR^3EW9A;sdL;Qirs?_{Elpx>4m+o|cs^G$4r(3)yfJUdykwG)8jfg$oic3nNW8eN z>uoM^s3tZ_=?0am7598uv1YOz|3}cqjBFJ#|AP~A9!x)hncylZjV)5G*9)Z1(=vZR ziI&+&NxL<}i;J&5TR*Un$i%Uj27UvHzh6fj(nBH$Yrru=c@z7Q#_q65LMk}O+Z_c5 zgMT!r)C%Co6%_FDI~A9cGd;WRx}*LRV4L5dv>nX@F(4VQ@?|m&D5QGJQ-rLxG4MmSId44|~3s0VWD0!15mI*v$=tAzma#5B?eXc&8{Dp;;LTdpI zvW@SmU+%Gr0u=;k)+Tpsg#Mgv-H{iQ*;Hu**e(!B7+Z2IT#_WZ_~xnNz-2>-I*4CU z=I9GEvFC}6nKS}h{z#UY9OOU{tW=xQcp8bBeKHp)8VdgnOQyX*m#9K-X&uTKDsEDD zK&N3C+>pchn0PPSE`u!G*pMLGn9D!CAy+fD<2nKZj1|H1aj0-RiB=UQDzK%C>7$AP zrpQ1CUhb8JsU{?!LKRS1hSVFsM4zx?Q?E7vs_D1;DODn9&F4%?C7 zVHt=GY6eYYz8GeVN-{*w-Zy;PS)b@_RcN(~qnDgEw3_}=;c{*LD|nED{bkp>lP-cw zbE-DZTIR!JwdH}PE;N>lcuPXopxrOwA6`7c>lEe%{!~#bNOd0%3ZUE|k&giiOEA}C z1}e48oJZlQKE+v%f?!EuWAo)n6~#Zqj`LvbOSYARix#F6PmxPtjIWw>JE7kc%~i6; zr-}=Fo&hiZ)F#-SM>i6;&50ghO(%4&mJz+URDQ$Y`dpNY_1RNjRf#wvlJUe@(GYI6 zxc#U>@+TFMs?iQAN~q&dRz_coM19mP^bU>QdStQ~?s$p0qPLRzbl=~+G+nFps?px- zms{e6V%HN_V`+x0We`=TXNDGZw`~+j?}4+e>X>4nPQ{Xu^_P<*&Fyn8Inu=iGc3|{ z!@Az%vKa^mcCBNW-WJAR9`y87Bl;x9uD^;y zTgmdP$b;*M^{_7?a=Fx$aE-l`HbNpih>+f6sKhI^wGeecok4;-r4Q7CdtpHK@wi9N3A89MKqS!#|gOnRbY*{g#?g9fa`cR$X(}$jniK4#ftm%@xXFG~zPm-qG zSEP@}s@6=7!U?UzOdtu@SiU287q?Hb2Qd$Yem%u5pL>{qaT8Iu0y-%oo4V<@--u@l z%3bV3=30HOe-5>_rY~NXUEQ3uHa|JL(r-@pw)>%nVt=i73Hsi$xxaC?DX`eD^K-w> zTkEFz`lr@DQsu&8+8*n|7IsOc~t+@F6&Hm0~O;capj!ijd!c7maQRs!*jxK;IkGqh2=)D z@|5z@j}>KvB`^xcW_JV&@p$jkUbE-VC|lzkTz4#%@_TkdClO%tQV_9U&f^0_W^A1Q{_1`5oDD< z=S16-iHOqylu!{S$E`HI1Q8vrncRax0b!hPek5v-`LGFxqM)kU3&K#z>AqlA=_#6+ z6pSSxLVhz8r?Q2xG>f~M{|o`dDgmxnF&&Wgl|fYF;w0a0*rmYBC$2_k{@ewG{Cp}4%@e1-@6irpMG`mEj#vG(1Jvl$ z-YrY$?I_o6;jMO!U6O`eU=!FT=JAD7?ngi#yqLsc^9k9m*yFIOr63o?+Q*#sn9Di~ zFFw3P{gV0co4x5Hqn-y9B{3;I&Sf>8LQygx0|dxtdVS#+f|+U3?PG_!b@^hN%%k+)pPv`E{p)&ko=zCY!7p zJKRn+vIeBLHJu~UfYrk_AvQ~vu`>uYZkILFMU z%}n@J^NF*DL|<~T5=e*!rD`Ln_J?SO#h?e3x*prFUhqtpJIl5`Z0j-EdIX^oV6g5xQN>c)?@R_wYMbGnlte$ezgayvxA zmoYJtzKRSo?$*pqi`CZqr({~xT*lEV1|xapiqAFu)Qqa$lke|ZO*rVQzoRwAA3!gD zN7Y_ReT^(d@5ilwoQ=VeE5?CYNQRmjT8U52XEy`RLQGME%tTWLJq@3!&mBtJh&KzwuB(d^_MR8s00 zkWUo6Dz#;0cPZez`O$`yWj!gowb$`YK~Pw<7H=w>XstahkL^TiTB8%RD(-Dk@Pt}+ zz~@7YP8#Im&@d=JK6-Ug(hm_5L2af@^p5UsSW&AaBkW}KTvk$#0;$5DYLZD6 zvYyYBLR9p5ow_LvSGUDR+_sxt#vz44M~pq!EH7*JEL|3_OMPOaIY+)5a)TVrim&i* DMWvID literal 0 HcmV?d00001 diff --git a/tests/data/so2sat/testing.h5 b/tests/data/so2sat/testing.h5 index 464108e4b9d7000a25142e64bde91bbdde0c115b..9e5020028a014f391bfe6c516125947f7201503f 100644 GIT binary patch literal 19448 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 AAOHXW literal 75793 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 diff --git a/tests/data/so2sat/training.h5 b/tests/data/so2sat/training.h5 index b433518217f6e3674cafc8ba3d96cf02b5a9c0d2..f485280c8f3f59f4c33db094650aa81615fabcf9 100644 GIT binary patch literal 19440 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 DMWvID literal 75793 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 diff --git a/tests/data/so2sat/validation.h5 b/tests/data/so2sat/validation.h5 index a1ec7f5d1da91e6509a3502e7bca5b8503b7fb5d..40c1e9f1480750813033b1365734468cbbe44b56 100644 GIT binary patch literal 19432 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 literal 75793 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 Wvo%t1waZSOAt01%0uGy>;9m!PC1(%- diff --git a/tests/datasets/test_so2sat.py b/tests/datasets/test_so2sat.py index a112812abe5..df6c4c538c8 100644 --- a/tests/datasets/test_so2sat.py +++ b/tests/datasets/test_so2sat.py @@ -21,13 +21,15 @@ class TestSo2Sat: @pytest.fixture(params=["train", "validation", "test"]) def dataset(self, monkeypatch: MonkeyPatch, request: SubRequest) -> So2Sat: - md5s = { - "train": "82e0f2d51766b89cb905dbaf8275eb5b", - "validation": "bf292ae4737c1698b1a3c6f5e742e0e1", - "test": "9a3bbe181b038d4e51f122c4be3c569e", + md5s_by_version = { + "2": { + "train": "56e6fa0edb25b065124a3113372f76e5", + "validation": "940c95a737bd2fcdcc46c9a52b31424d", + "test": "e97a6746aadc731a1854097f32ab1755", + } } - monkeypatch.setattr(So2Sat, "md5s", md5s) + monkeypatch.setattr(So2Sat, "md5s_by_version", md5s_by_version) root = os.path.join("tests", "data", "so2sat") split = request.param transforms = nn.Identity() @@ -51,13 +53,13 @@ def test_getitem(self, dataset: So2Sat) -> None: assert isinstance(x["label"], torch.Tensor) def test_len(self, dataset: So2Sat) -> None: - assert len(dataset) == 1 + assert len(dataset) == 2 def test_out_of_bounds(self, dataset: So2Sat) -> None: # h5py at version 2.10.0 raises a ValueError instead of an IndexError so we # check for both here with pytest.raises((IndexError, ValueError)): - dataset[1] + dataset[2] def test_invalid_split(self) -> None: with pytest.raises(AssertionError): diff --git a/tests/trainers/test_classification.py b/tests/trainers/test_classification.py index d88923ce176..9060548aa3e 100644 --- a/tests/trainers/test_classification.py +++ b/tests/trainers/test_classification.py @@ -77,6 +77,7 @@ class TestClassificationTask: "so2sat_all", "so2sat_s1", "so2sat_s2", + "so2sat_rgb", "ucmerced", ], ) diff --git a/torchgeo/datamodules/so2sat.py b/torchgeo/datamodules/so2sat.py index e8e9c43f742..e7cc9be05a3 100644 --- a/torchgeo/datamodules/so2sat.py +++ b/torchgeo/datamodules/so2sat.py @@ -6,6 +6,8 @@ from typing import Any import torch +from torch import Generator, Tensor +from torch.utils.data import random_split from ..datasets import So2Sat from .geo import NonGeoDataModule @@ -14,60 +16,157 @@ class So2SatDataModule(NonGeoDataModule): """LightningDataModule implementation for the So2Sat dataset. - Uses the train/val/test splits from the dataset. + If using the version 2 dataset, we use the train/val/test splits from the dataset. + If using the version 3 datasets, we use a random 80/20 train/val split from the + "train" set and use the "test" set as the test set. """ - # TODO: calculate mean/std dev of s1 bands - mean = torch.tensor( - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.12375696117681859, - 0.1092774636368323, - 0.1010855203267882, - 0.1142398616114001, - 0.1592656692023089, - 0.18147236008771792, - 0.1745740312291377, - 0.19501607349635292, - 0.15428468872076637, - 0.10905050699570007, - ] - ) - std = torch.tensor( - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.03958795985905458, - 0.047778262752410296, - 0.06636616706371974, - 0.06358874912497474, - 0.07744387147984592, - 0.09101635085921553, - 0.09218466562387101, - 0.10164581233948201, - 0.09991773043519253, - 0.08780632509122865, - ] - ) + means_per_version: dict[str, Tensor] = { + "2": torch.tensor( + [ + -0.00003591224260, + -0.00000765856128, + 0.00005937385750, + 0.00002516623150, + 0.04420110660000, + 0.25761027100000, + 0.00075567433700, + 0.00135034668000, + 0.12375696117681, + 0.10927746363683, + 0.10108552032678, + 0.11423986161140, + 0.15926566920230, + 0.18147236008771, + 0.17457403122913, + 0.19501607349635, + 0.15428468872076, + 0.10905050699570, + ] + ), + "3_random": torch.tensor( + [ + -0.00005541164581, + -0.00001363245448, + 0.00004558943283, + 0.00002990907940, + 0.04451951629749, + 0.25862310103671, + 0.00032720731137, + 0.00123416595462, + 0.12428656593186, + 0.11001677362564, + 0.10230652367417, + 0.11532195526186, + 0.15989486018315, + 0.18204406482475, + 0.17513562590622, + 0.19565546643221, + 0.15648722649020, + 0.11122536338577, + ] + ), + "3_block": torch.tensor( + [ + -0.00004632368791, + 0.00001260869365, + 0.00005305557337, + 0.00003471369557, + 0.04449937686171, + 0.26046026815721, + 0.00087815394475, + 0.00086889627435, + 0.12381869777901, + 0.10944155483024, + 0.10176911573221, + 0.11465267892206, + 0.15870528223797, + 0.18053964470203, + 0.17366821871719, + 0.19390983961551, + 0.15536490486611, + 0.11057334452833, + ] + ), + } + means_per_version["3_culture_10"] = means_per_version["2"] + + stds_per_version: dict[str, Tensor] = { + "2": torch.tensor( + [ + 0.17555201, + 0.17556463, + 0.45998793, + 0.45598876, + 2.85599092, + 8.32480061, + 2.44987574, + 1.46473530, + 0.03958795, + 0.04777826, + 0.06636616, + 0.06358874, + 0.07744387, + 0.09101635, + 0.09218466, + 0.10164581, + 0.09991773, + 0.08780632, + ] + ), + "3_random": torch.tensor( + [ + 0.1756914, + 0.1761190, + 0.4600589, + 0.4563601, + 2.2492179, + 7.9056503, + 2.1917633, + 1.3148480, + 0.0392269, + 0.0470917, + 0.0653264, + 0.0624057, + 0.0758367, + 0.0891717, + 0.0905092, + 0.0996856, + 0.0990188, + 0.0873386, + ] + ), + "3_block": torch.tensor( + [ + 0.1751797, + 0.1754073, + 0.4610124, + 0.4572122, + 0.8294254, + 7.1771026, + 0.9642598, + 0.8770835, + 0.0388311, + 0.0464986, + 0.0643833, + 0.0616141, + 0.0753004, + 0.0886178, + 0.0899500, + 0.0991759, + 0.0983276, + 0.0865943, + ] + ), + } + stds_per_version["3_culture_10"] = stds_per_version["2"] def __init__( self, batch_size: int = 64, num_workers: int = 0, band_set: str = "all", + val_split_pct: float = 0.2, **kwargs: Any, ) -> None: """Initialize a new So2SatDataModule instance. @@ -75,18 +174,28 @@ def __init__( Args: batch_size: Size of each mini-batch. num_workers: Number of workers for parallel data loading. - band_set: One of 'all', 's1', or 's2'. + band_set: One of 'all', 's1', 's2', or 'rgb'. + val_split_pct: Percentage of training data to use for validation in with + the version 3 datasets. **kwargs: Additional keyword arguments passed to :class:`~torchgeo.datasets.So2Sat`. + + .. versionadded:: 0.5 + The *val_split_pct* parameter, and the 'rgb' argument to *band_set*. """ + version = kwargs.get("version", "2") kwargs["bands"] = So2Sat.BAND_SETS[band_set] + self.val_split_pct = val_split_pct if band_set == "s1": - self.mean = self.mean[:8] - self.std = self.std[:8] + self.mean = self.means_per_version[version][:8] + self.std = self.stds_per_version[version][:8] elif band_set == "s2": - self.mean = self.mean[8:] - self.std = self.std[8:] + self.mean = self.means_per_version[version][8:] + self.std = self.stds_per_version[version][8:] + elif band_set == "rgb": + self.mean = self.means_per_version[version][[10, 9, 8]] + self.std = self.stds_per_version[version][[10, 9, 8]] super().__init__(So2Sat, batch_size, num_workers, **kwargs) @@ -100,9 +209,22 @@ def setup(self, stage: str) -> None: Args: stage: Either 'fit', 'validate', 'test', or 'predict'. """ - if stage in ["fit"]: - self.train_dataset = So2Sat(split="train", **self.kwargs) - if stage in ["fit", "validate"]: - self.val_dataset = So2Sat(split="validation", **self.kwargs) - if stage in ["test"]: - self.test_dataset = So2Sat(split="test", **self.kwargs) + if self.kwargs.get("version", "2") == "2": + if stage in ["fit"]: + self.train_dataset = So2Sat(split="train", **self.kwargs) + if stage in ["fit", "validate"]: + self.val_dataset = So2Sat(split="validation", **self.kwargs) + if stage in ["test"]: + self.test_dataset = So2Sat(split="test", **self.kwargs) + else: + if stage in ["fit", "validate"]: + dataset = So2Sat(split="train", **self.kwargs) + val_length = round(len(dataset) * self.val_split_pct) + train_length = len(dataset) - val_length + self.train_dataset, self.val_dataset = random_split( + dataset, + [train_length, val_length], + generator=Generator().manual_seed(0), + ) + if stage in ["test"]: + self.test_dataset = So2Sat(split="test", **self.kwargs) diff --git a/torchgeo/datasets/so2sat.py b/torchgeo/datasets/so2sat.py index 0cd620a78a3..12a4a48dd6f 100644 --- a/torchgeo/datasets/so2sat.py +++ b/torchgeo/datasets/so2sat.py @@ -24,19 +24,25 @@ class So2Sat(NonGeoDataset): acquired by the Sentinel-1 and Sentinel-2 remote sensing satellites, and a corresponding local climate zones (LCZ) label. The dataset is distributed over 42 cities across different continents and cultural regions of the world, and comes - with a split into fully independent, non-overlapping training, validation, - and test sets. + with a variety of different splits. - This implementation focuses on the *2nd* version of the dataset as described in - the author's github repository https://github.com/zhu-xlab/So2Sat-LCZ42 and hosted - at https://mediatum.ub.tum.de/1483140. This version is identical to the first - version of the dataset but includes the test data. The splits are defined as - follows: + This implementation covers the *2nd* and *3rd* versions of the dataset as described + in the author's github repository: https://github.com/zhu-xlab/So2Sat-LCZ42. + + The different versions are as follows: + + Version 2: This version contains imagery from 52 cities and is split into train/val/test as follows: * Training: 42 cities around the world * Validation: western half of 10 other cities covering 10 cultural zones * Testing: eastern half of the 10 other cities + Version 3: A version of the dataset with 3 different train/test splits, as follows: + + * Random split: every city 80% training / 20% testing (randomly sampled) + * Block split: every city is split in a geospatial 80%/20%-manner + * Cultural 10: 10 cities from different cultural zones are held back for testing purposes + Dataset classes: 0. Compact high rise @@ -63,7 +69,8 @@ class So2Sat(NonGeoDataset): .. note:: - This dataset can be automatically downloaded using the following bash script: + The version 2 dataset can be automatically downloaded using the following bash + script: .. code-block:: bash @@ -74,18 +81,56 @@ class So2Sat(NonGeoDataset): or manually downloaded from https://dataserv.ub.tum.de/index.php/s/m1483140 This download will likely take several hours. - """ - filenames = { - "train": "training.h5", - "validation": "validation.h5", - "test": "testing.h5", + The version 3 datasets can be downloaded using the following bash script: + + .. code-block:: bash + + for version in random block culture_10 + do + for split in training testing + do + wget -P $version/ ftp://m1613658:m1613658@dataserv.ub.tum.de/$version/$split.h5 + done + done + + or manually downloaded from https://mediatum.ub.tum.de/1613658 + """ # noqa: E501 + + versions = ["2", "3_random", "3_block", "3_culture_10"] + filenames_by_version = { + "2": { + "train": "training.h5", + "validation": "validation.h5", + "test": "testing.h5", + }, + "3_random": {"train": "random/training.h5", "test": "random/testing.h5"}, + "3_block": {"train": "block/training.h5", "test": "block/testing.h5"}, + "3_culture_10": { + "train": "culture_10/training.h5", + "test": "culture_10/testing.h5", + }, } - md5s = { - "train": "702bc6a9368ebff4542d791e53469244", - "validation": "71cfa6795de3e22207229d06d6f8775d", - "test": "e81426102b488623a723beab52b31a8a", + md5s_by_version = { + "2": { + "train": "702bc6a9368ebff4542d791e53469244", + "validation": "71cfa6795de3e22207229d06d6f8775d", + "test": "e81426102b488623a723beab52b31a8a", + }, + "3_random": { + "train": "94e2e2e667b406c2adf61e113b42204e", + "test": "1e15c425585ce816342d1cd779d453d8", + }, + "3_block": { + "train": "a91d6150e8b059dac86105853f377a11", + "test": "6414af1ec33ace417e879f9c88066d47", + }, + "3_culture_10": { + "train": "702bc6a9368ebff4542d791e53469244", + "test": "58335ce34ca3a18424e19da84f2832fc", + }, } + classes = [ "Compact high rise", "Compact mid rise", @@ -136,11 +181,13 @@ class So2Sat(NonGeoDataset): "all": all_band_names, "s1": all_s1_band_names, "s2": all_s2_band_names, + "rgb": rgb_bands, } def __init__( self, root: str = "data", + version: str = "2", split: str = "train", bands: Sequence[str] = BAND_SETS["all"], transforms: Optional[Callable[[dict[str, Tensor]], dict[str, Tensor]]] = None, @@ -150,6 +197,7 @@ def __init__( Args: root: root directory where dataset can be found + version: one of "2", "3_random", "3_block", or "3_culture_10" split: one of "train", "validation", or "test" bands: a sequence of band names to use where the indices correspond to the array index of combined Sentinel 1 and Sentinel 2 @@ -163,6 +211,9 @@ def __init__( .. versionadded:: 0.3 The *bands* parameter. + + .. versionadded:: 0.5 + The *version* parameter. """ try: import h5py # noqa: F401 @@ -170,8 +221,8 @@ def __init__( raise ImportError( "h5py is not installed and is required to use this dataset" ) - - assert split in ["train", "validation", "test"] + assert version in self.versions + assert split in self.filenames_by_version[version] self._validate_bands(bands) self.s1_band_indices: "np.typing.NDArray[np.int_]" = np.array( @@ -197,11 +248,12 @@ def __init__( self.bands = bands self.root = root + self.version = version self.split = split self.transforms = transforms self.checksum = checksum - self.fn = os.path.join(self.root, self.filenames[split]) + self.fn = os.path.join(self.root, self.filenames_by_version[version][split]) if not self._check_integrity(): raise RuntimeError("Dataset not found or corrupted.") @@ -256,7 +308,7 @@ def _check_integrity(self) -> bool: Returns: True if dataset files are found and/or MD5s match, else False """ - md5 = self.md5s[self.split] + md5 = self.md5s_by_version[self.version][self.split] if not check_integrity(self.fn, md5 if self.checksum else None): return False return True From a02826781baccb0eb905f65720990bcaf5c52363 Mon Sep 17 00:00:00 2001 From: isaaccorley <22203655+isaaccorley@users.noreply.github.com> Date: Sat, 22 Apr 2023 18:51:26 +0000 Subject: [PATCH 15/17] update fair1m to work with latest dataset --- torchgeo/datasets/fair1m.py | 82 ++++++++++++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 10 deletions(-) diff --git a/torchgeo/datasets/fair1m.py b/torchgeo/datasets/fair1m.py index 33cc6d01683..6ebea6ed3ba 100644 --- a/torchgeo/datasets/fair1m.py +++ b/torchgeo/datasets/fair1m.py @@ -111,7 +111,7 @@ class FAIR1M(NonGeoDataset): If you use this dataset in your research, please cite the following paper: - * https://arxiv.org/abs/2103.05569 + * https://doi.org/10.1016/j.isprsjprs.2021.12.004 .. versionadded:: 0.2 """ @@ -156,15 +156,64 @@ class FAIR1M(NonGeoDataset): "Bridge": {"id": 36, "category": "Road"}, } + splits = { + "train": { + "filename_glob": os.path.join("train", "**", "images", "*.tif"), + "paths": [ + "train/part1/images.zip", + "train/part1/labelXml.zip", + "train/part2/images.zip", + "train/part2/labelXmls.zip", + ], + "urls": [ + "https://drive.google.com/file/d/1LWT_ybL-s88Lzg9A9wHpj0h2rJHrqrVf", + "https://drive.google.com/file/d/1CnOuS8oX6T9JMqQnfFsbmf7U38G6Vc8u", + "https://drive.google.com/file/d/1cx4MRfpmh68SnGAYetNlDy68w0NgKucJ", + "https://drive.google.com/file/d/1RFVjadTHA_bsB7BJwSZoQbiyM7KIDEUI", + ], + "md5s": [ + "a460fe6b1b5b276bf856ce9ac72d6568", + "80f833ff355f91445c92a0c0c1fa7414", + "ad237e61dba304fcef23cd14aa6c4280", + "5c5948e68cd0f991a0d73f10956a3b05", + ], + }, + "val": { + "filename_glob": os.path.join("validation", "images", "*.tif"), + "paths": ["validation/images.zip", "validation/labelXmls.zip"], + "urls": [ + "https://drive.google.com/file/d/1lSSHOD02B6_sUmr2b-R1iqhgWRQRw-S9", + "https://drive.google.com/file/d/1sTTna1C5n3Senpfo-73PdiNilnja1AV4", + ], + "md5s": [ + "dce782be65405aa381821b5f4d9eac94", + "700b516a21edc9eae66ca315b72a09a1", + ], + }, + "test": { + "filename_glob": os.path.join("test", "images", "*.tif"), + "paths": ["test/images0.zip", "test/images1.zip", "test/images2.zip"], + "urls": [ + "https://drive.google.com/file/d/1HtOOVfK9qetDBjE7MM0dK_u5u7n4gdw3", + "https://drive.google.com/file/d/1iXKCPmmJtRYcyuWCQC35bk97NmyAsasq", + "https://drive.google.com/file/d/1oUc25FVf8Zcp4pzJ31A1j1sOLNHu63P0", + ], + "md5s": [ + "fb8ccb274f3075d50ac9f7803fbafd3d", + "dc9bbbdee000e97f02276aa61b03e585", + "700b516a21edc9eae66ca315b72a09a1", + ], + }, + } image_root: str = "images" - labels_root: str = "labelXml" - filenames = ["images.zip", "labelXmls.zip"] - md5s = ["a460fe6b1b5b276bf856ce9ac72d6568", "80f833ff355f91445c92a0c0c1fa7414"] + label_root: str = "labelXml" def __init__( self, root: str = "data", + split: str = "train", transforms: Optional[Callable[[dict[str, Tensor]], dict[str, Tensor]]] = None, + download: bool = False, checksum: bool = False, ) -> None: """Initialize a new FAIR1M dataset instance. @@ -174,13 +223,21 @@ def __init__( transforms: a function/transform that takes input sample and its target as entry and returns a transformed version checksum: if True, check the MD5 of the downloaded files (may be slow) + + Raises: + AssertionError: if ``split`` argument is invalid + RuntimeError: if ``download=False`` and data is not found, or checksums + don't match """ + assert split in self.splits self.root = root + self.split = split self.transforms = transforms + self.download = download self.checksum = checksum self._verify() self.files = sorted( - glob.glob(os.path.join(self.root, self.labels_root, "*.xml")) + glob.glob(os.path.join(self.root, self.splits[split["filename_glob"]])) ) def __getitem__(self, index: int) -> dict[str, Tensor]: @@ -193,10 +250,16 @@ def __getitem__(self, index: int) -> dict[str, Tensor]: data and label at that index """ path = self.files[index] - parsed = parse_pascal_voc(path) - image = self._load_image(parsed["filename"]) - boxes, labels = self._load_target(parsed["points"], parsed["labels"]) - sample = {"image": image, "boxes": boxes, "label": labels} + + image = self.load_image(path) + sample = {"image": image} + + if self.split != "test": + label_path = path.replace(self.image_root, self.labels_root) + label_path = os.path.splitext(label_path)[0] + ".xml" + voc = parse_pascal_voc(label_path) + boxes, labels = self._load_target(voc["points"], voc["labels"]) + sample = {"image": image, "boxes": boxes, "label": labels} if self.transforms is not None: sample = self.transforms(sample) @@ -220,7 +283,6 @@ def _load_image(self, path: str) -> Tensor: Returns: the image """ - path = os.path.join(self.root, self.image_root, path) with Image.open(path) as img: array: "np.typing.NDArray[np.int_]" = np.array(img.convert("RGB")) tensor = torch.from_numpy(array) From 4b30f03c0033b6e0f5fd7703afb8f85f09fcea8c Mon Sep 17 00:00:00 2001 From: isaaccorley <22203655+isaaccorley@users.noreply.github.com> Date: Sun, 23 Apr 2023 02:44:14 +0000 Subject: [PATCH 16/17] add fair1m tests --- tests/data/fair1m/images.zip | Bin 1060 -> 0 bytes tests/data/fair1m/labelXmls.zip | Bin 1834 -> 0 bytes tests/data/fair1m/{ => test}/images/0.tif | Bin tests/data/fair1m/{ => test}/images/1.tif | Bin tests/data/fair1m/{ => test}/images/2.tif | Bin tests/data/fair1m/{ => test}/images/3.tif | Bin tests/data/fair1m/test/images0.zip | Bin 0 -> 470 bytes tests/data/fair1m/test/images1.zip | Bin 0 -> 246 bytes tests/data/fair1m/test/images2.zip | Bin 0 -> 246 bytes tests/data/fair1m/train/part1/images.zip | Bin 0 -> 1060 bytes tests/data/fair1m/train/part1/images/0.tif | Bin 0 -> 152 bytes tests/data/fair1m/train/part1/images/1.tif | Bin 0 -> 152 bytes tests/data/fair1m/train/part1/images/2.tif | Bin 0 -> 152 bytes tests/data/fair1m/train/part1/images/3.tif | Bin 0 -> 152 bytes tests/data/fair1m/train/part1/labelXml.zip | Bin 0 -> 1834 bytes .../fair1m/{ => train/part1}/labelXml/0.xml | 0 .../fair1m/{ => train/part1}/labelXml/1.xml | 0 .../fair1m/{ => train/part1}/labelXml/2.xml | 0 .../fair1m/{ => train/part1}/labelXml/3.xml | 0 tests/data/fair1m/train/part2/images.zip | Bin 0 -> 1060 bytes tests/data/fair1m/train/part2/images/0.tif | Bin 0 -> 152 bytes tests/data/fair1m/train/part2/images/1.tif | Bin 0 -> 152 bytes tests/data/fair1m/train/part2/images/2.tif | Bin 0 -> 152 bytes tests/data/fair1m/train/part2/images/3.tif | Bin 0 -> 152 bytes tests/data/fair1m/train/part2/labelXml/0.xml | 28 +++ tests/data/fair1m/train/part2/labelXml/1.xml | 28 +++ tests/data/fair1m/train/part2/labelXml/2.xml | 28 +++ tests/data/fair1m/train/part2/labelXml/3.xml | 28 +++ tests/data/fair1m/train/part2/labelXmls.zip | Bin 0 -> 1834 bytes tests/data/fair1m/validation/images.zip | Bin 0 -> 1060 bytes tests/data/fair1m/validation/images/0.tif | Bin 0 -> 152 bytes tests/data/fair1m/validation/images/1.tif | Bin 0 -> 152 bytes tests/data/fair1m/validation/images/2.tif | Bin 0 -> 152 bytes tests/data/fair1m/validation/images/3.tif | Bin 0 -> 152 bytes tests/data/fair1m/validation/labelXml/0.xml | 28 +++ tests/data/fair1m/validation/labelXml/1.xml | 28 +++ tests/data/fair1m/validation/labelXml/2.xml | 28 +++ tests/data/fair1m/validation/labelXml/3.xml | 28 +++ tests/data/fair1m/validation/labelXmls.zip | Bin 0 -> 1834 bytes tests/datamodules/test_fair1m.py | 21 +-- tests/datasets/test_fair1m.py | 104 ++++++++--- torchgeo/datamodules/fair1m.py | 48 +++-- torchgeo/datasets/fair1m.py | 167 +++++++++++------- 43 files changed, 451 insertions(+), 113 deletions(-) delete mode 100644 tests/data/fair1m/images.zip delete mode 100644 tests/data/fair1m/labelXmls.zip rename tests/data/fair1m/{ => test}/images/0.tif (100%) rename tests/data/fair1m/{ => test}/images/1.tif (100%) rename tests/data/fair1m/{ => test}/images/2.tif (100%) rename tests/data/fair1m/{ => test}/images/3.tif (100%) create mode 100644 tests/data/fair1m/test/images0.zip create mode 100644 tests/data/fair1m/test/images1.zip create mode 100644 tests/data/fair1m/test/images2.zip create mode 100644 tests/data/fair1m/train/part1/images.zip create mode 100644 tests/data/fair1m/train/part1/images/0.tif create mode 100644 tests/data/fair1m/train/part1/images/1.tif create mode 100644 tests/data/fair1m/train/part1/images/2.tif create mode 100644 tests/data/fair1m/train/part1/images/3.tif create mode 100644 tests/data/fair1m/train/part1/labelXml.zip rename tests/data/fair1m/{ => train/part1}/labelXml/0.xml (100%) rename tests/data/fair1m/{ => train/part1}/labelXml/1.xml (100%) rename tests/data/fair1m/{ => train/part1}/labelXml/2.xml (100%) rename tests/data/fair1m/{ => train/part1}/labelXml/3.xml (100%) create mode 100644 tests/data/fair1m/train/part2/images.zip create mode 100644 tests/data/fair1m/train/part2/images/0.tif create mode 100644 tests/data/fair1m/train/part2/images/1.tif create mode 100644 tests/data/fair1m/train/part2/images/2.tif create mode 100644 tests/data/fair1m/train/part2/images/3.tif create mode 100644 tests/data/fair1m/train/part2/labelXml/0.xml create mode 100644 tests/data/fair1m/train/part2/labelXml/1.xml create mode 100644 tests/data/fair1m/train/part2/labelXml/2.xml create mode 100644 tests/data/fair1m/train/part2/labelXml/3.xml create mode 100644 tests/data/fair1m/train/part2/labelXmls.zip create mode 100644 tests/data/fair1m/validation/images.zip create mode 100644 tests/data/fair1m/validation/images/0.tif create mode 100644 tests/data/fair1m/validation/images/1.tif create mode 100644 tests/data/fair1m/validation/images/2.tif create mode 100644 tests/data/fair1m/validation/images/3.tif create mode 100644 tests/data/fair1m/validation/labelXml/0.xml create mode 100644 tests/data/fair1m/validation/labelXml/1.xml create mode 100644 tests/data/fair1m/validation/labelXml/2.xml create mode 100644 tests/data/fair1m/validation/labelXml/3.xml create mode 100644 tests/data/fair1m/validation/labelXmls.zip diff --git a/tests/data/fair1m/images.zip b/tests/data/fair1m/images.zip deleted file mode 100644 index cf46b0d61463abd7a6d03cfed3e2a781848b9b95..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1060 zcmWIWW@h1H0D+dZoxxxRlwfC&VaUu)OiwM=4-MgDVCFcxDG`KAE4UdLSza(RFo1~w zpaCL4EgTF@Km)>V%zo?vhWC)hfh4Wy^=1}Srd-e*3tD4=kHoS{nbk%c*O3ey~o z*jX$N2uLz8AY9Xc+ck!8*DwHG0>U(R4JdGNgpD!WH8?_;`e9Ro+cgGo*StQnDG`Ke z7B-Aba?H4ro&+=z3NXBN1Tov$5 z0|S@{02(0z)WiWcqI8wTbuLB*hAbuq20mmX4D>2;bKvH50?k4*XX>Q=L5B@^jy?aP zSu}H@UqWKQ-NvcO4>|9+cpllgZKJG)jqSh5Ej+gGX1%<;e7?$QJ{kS=$ht+PORg@= z`c@jcD67;x%OYg*`}gx#Dc^4l@Tz{ue59P!!uFF zGWnLo_HM>?$D%KKbQZs$U7 zuLNmz!GE_Ewl8#8;i$AhWVuiKx~(afn$|}p&iVaZyKk7z2WFa7mSOi(*HiQClN@vMD7t#C>1oi(4WH+)qT zldMUYegBy_YKSK3FiWrkLo@|FL=8y@(W;q>;1FGCEv)~*%1f!>?VPs`4_|(quafej zFx-E>-Cvc+^ur&k0)P8&>FcqZ;u|`>`pTs(OMW|B{(Sdwsm&)@jHNv}dJF>49q&2{}&>T=ajof5lhkdZuZvTiBM=eYiL~)FfKQ`Ilsb*vja4 zjL-h`UE1A}Dx1o9NkaOauFa>3>sB=6&T;wiUT`NbtJ8O`#-$diQ6-ly%@C_Gm3n-~ zUR`lk=kyO;Q;$gb(3op?MjtgW!(ObgWB~?d5_(`75e`h>yu%6tt=G?K zDyf z;Irr{y;+{$me-WrywRbZhVy;)1RwSJ%yD9=F}ovM<|=M%TQz;bEBO^QoK@HIF1`GB^Ap47FLoQQ zmj7f%4a>7^fA{?dX8c(6urwwjEQ^{Acv`PN5s|#6%eLvIQ=yvp3t0}1fa4o&w`ES6 zx9=b0DW%IZE53gD;@O7;zAiY!@jv@ja2Oo1NcY zW>4@ulDCq5#cAHTrk2*LmH`KBgPTI$NqUo*WZ5DH7n&Ic9iyVJ`2> z9S@dIl6dydQt;xbs9hW;8R`?=(geb7ua|P>+`BdV4d<)Vp3A)T8TGr3=52RvIW^&i z(^c<)ulynVSZt-uUw`?MQ~TSsO8&;P<$st0ycwC~m~j=m63_xzfZ@L*h>27PvqB1C zw1N|29Ihf5VjM8ZGyHFKMm7#wAQNXMY9WMd=2W23a3?~GB;w3O&5OupJ_4pFxS7zL jNt~IeX%g8?Jr*Q0p(zxXnXGJ}fM*55`9R;5vVwR3;q#;( diff --git a/tests/data/fair1m/images/0.tif b/tests/data/fair1m/test/images/0.tif similarity index 100% rename from tests/data/fair1m/images/0.tif rename to tests/data/fair1m/test/images/0.tif diff --git a/tests/data/fair1m/images/1.tif b/tests/data/fair1m/test/images/1.tif similarity index 100% rename from tests/data/fair1m/images/1.tif rename to tests/data/fair1m/test/images/1.tif diff --git a/tests/data/fair1m/images/2.tif b/tests/data/fair1m/test/images/2.tif similarity index 100% rename from tests/data/fair1m/images/2.tif rename to tests/data/fair1m/test/images/2.tif diff --git a/tests/data/fair1m/images/3.tif b/tests/data/fair1m/test/images/3.tif similarity index 100% rename from tests/data/fair1m/images/3.tif rename to tests/data/fair1m/test/images/3.tif diff --git a/tests/data/fair1m/test/images0.zip b/tests/data/fair1m/test/images0.zip new file mode 100644 index 0000000000000000000000000000000000000000..19c4306bf8b96ca050f98a3011a1e056dd87699a GIT binary patch literal 470 zcmWIWW@Zs#U|`^2P-UMU7ItIyV-Fy21`zWw$S`E)CZ?wr>l^5mWTu6Na56BbHMyjK zaA^fM10xGi6$6;~{N+m0gM@^H2T2JDDTxUzn`Si35bQjA-+%#l-==4iyuVsSt~l7S(B7}prm&ozupa?H3wM*`$~ rFj&$EqEN$%6%tlx;S}IatciUH6QTaaWg;sZ$aW?mTmqzJ7(qM$+30v| literal 0 HcmV?d00001 diff --git a/tests/data/fair1m/test/images1.zip b/tests/data/fair1m/test/images1.zip new file mode 100644 index 0000000000000000000000000000000000000000..54b96fb0794abc7409af80026446c1c5b7522dac GIT binary patch literal 246 zcmWIWW@Zs#U|`^2P-UMU7ItIyV-Fy21`zWw$S`E)CZ?wr>l^8nWTu6Na56BbHMyjK zaA^fM10xGi6$6;~{N+m0gM@^H2T2JDDTxUzn`Si35bQjA-+%#l-==4iyuVsSt~l7S(>n~_P58JBw`K=y&bl131P b>N-}4>(E>l;LXYgQp*U0K|s0>#9;scL|Z(? literal 0 HcmV?d00001 diff --git a/tests/data/fair1m/test/images2.zip b/tests/data/fair1m/test/images2.zip new file mode 100644 index 0000000000000000000000000000000000000000..92063d5fbe2d772b2356fd398d8e8469648fb69f GIT binary patch literal 246 zcmWIWW@Zs#U|`^2P-UMU7ItIyV-Fy21`zWw$S`E)CZ?wr>l^EpWTu6Na56BbHMyjK zaA^fM10xGi6$6;~{N+m0gM@^H2T2JDDTxUzn`Si35bQjA-+%#l-==4iyuVsSt~l7S(>n~_P58JBw`K=y&bl131P b>N-}4>(E>l;LXYgQp*U0K|s0>#9;scMPEF} literal 0 HcmV?d00001 diff --git a/tests/data/fair1m/train/part1/images.zip b/tests/data/fair1m/train/part1/images.zip new file mode 100644 index 0000000000000000000000000000000000000000..d97d51cac57fcd154cc2c532504225d0a1b22743 GIT binary patch literal 1060 zcmWIWW@h1H0D-6LriFnSP=cL7h9NUIF+H_dKQx4sfq7rJO9}{=R&X;gvVatUi2$Gh zB0vrY14~1BSlEr(k3E3A89>Z~Fu+K!Br^?cN_)9X%0*05K7YBA^dKQ2;XzVDLP}x+ z%cdC(GXy)&9yxL50MC-!4KpN9uyr;YNKfGnQsxM~&wON2K;Z;ALzUtq3v=WYra2n1 zvsfGukYr#W$~6Xq$~DG=$~A`ca}6Vt95b$@Cjm@9ARxf-))7R*6Cx`lAz~y#kYTuz zA9w=*`REYdO@iA9w1v3 bDh|>khGdR7l+6Pamq9WI$OXCp1d#y%6VL+2 literal 0 HcmV?d00001 diff --git a/tests/data/fair1m/train/part1/images/1.tif b/tests/data/fair1m/train/part1/images/1.tif new file mode 100644 index 0000000000000000000000000000000000000000..8168bf8821b850151ba118253d63cc24f74a9902 GIT binary patch literal 152 zcmebD)MDUZU|`^4U|?inU<9(5fS3`9&BVyezzh^?17c<%8>9w=*`REYdO@iA9w1v3 bDh|>khGdR7l+6Pamq9WI$OXCp1d#y%6VL+2 literal 0 HcmV?d00001 diff --git a/tests/data/fair1m/train/part1/images/2.tif b/tests/data/fair1m/train/part1/images/2.tif new file mode 100644 index 0000000000000000000000000000000000000000..8168bf8821b850151ba118253d63cc24f74a9902 GIT binary patch literal 152 zcmebD)MDUZU|`^4U|?inU<9(5fS3`9&BVyezzh^?17c<%8>9w=*`REYdO@iA9w1v3 bDh|>khGdR7l+6Pamq9WI$OXCp1d#y%6VL+2 literal 0 HcmV?d00001 diff --git a/tests/data/fair1m/train/part1/images/3.tif b/tests/data/fair1m/train/part1/images/3.tif new file mode 100644 index 0000000000000000000000000000000000000000..8168bf8821b850151ba118253d63cc24f74a9902 GIT binary patch literal 152 zcmebD)MDUZU|`^4U|?inU<9(5fS3`9&BVyezzh^?17c<%8>9w=*`REYdO@iA9w1v3 bDh|>khGdR7l+6Pamq9WI$OXCp1d#y%6VL+2 literal 0 HcmV?d00001 diff --git a/tests/data/fair1m/train/part1/labelXml.zip b/tests/data/fair1m/train/part1/labelXml.zip new file mode 100644 index 0000000000000000000000000000000000000000..f8712235cbcb092ddf2a95834da72ec14234ba5e GIT binary patch literal 1834 zcmWIWW@h1H00EYU@-Q$1N^mmBFytgArRGHB=IDopa56Brm&>HA3U^59*0WC1Ay z69GUYM1UL)29Oa+I?NKRj0_AZObiTs$VM3IRpjQt&3Wd4Vb0V^`-2V}@Em*oMYC$A zqF+K{z}6aQhl{gAO`>I-e@RA&t&D!h`0P*LrQI#5vZ;)hB&6Tz+I*V0Zbd`x z9G4&O1$XkYI(_GATxyXTRdVUl46zzhsmFKh)fE>P_MPL>-R1doBI}Y+|3lx>{->$` zWXy@Pdvc?<>q2YEzTZM0cCETx8R4>~r&IUJ9ahu-4qsz0tS#JpPXE9)^@x-Yjk$Jb z^icz|bd|+*E?{70p$Db`34vKOb0Ih|8>c2elT$Rxw9Be$?{hJ^oeR0W5~S4y|J_#DzR+QXqtXVE51&I`+8Fv&V~zclr7c!qBPY!qN$+1^w&EvLG9!uZc}r| zv-SbC!X>$P)_k_!@KsSvvL<2n{b%B+A$peW@4o-Q#2SkpqQ-ICzv*PQQFP@uh+Ajaq`gT7P2V1 zYeZk2DAV`z$*=GIe~y%2@K_o1*yc#NWQ6HVvAw4js5frfsbnwGSCx?c#&BCv!b|m! z=CAIY$WHKN$##))6W|?rd zth1#$G)_{tx$kHuEn{PmYFIkmrCtK@GyTmFX$H5kKQ ztgvJOCd?%CU^F5k81oJ*2((^5r>V3)I9a{B#85Ga?}D|(w;S`i7?|!Xv#tHPQC5GE zSK`-aXOHtRANW}(d|TgS+7z3op@Gk$r}Sodep_Bsa`$$1-81PGOGDO`$vv9Qvqs9d zIWG0WM2E=x6W(*X%WhEpCJ}A1;mhnF`B`@ld_ORW<66TNe!YsxTxs36mh&H&)NwV4 zH{iN|&Ya$eOO9z5G*bWU%x;glEX%rKsxasG#S^9XFPyg5A;GY3rdV#W$8kpcDMv1R zf9T?(o&8el!U9>>teK87Ul(17zy7g+#jN+8@f*(f*%N%!=QGENrN-=zY?-ULv2E4# z1+U~+)Noc^%e(aQ-_1`9o4?p?xLW>`Il!BdNsbv;u`2;go**E=@YWGTBNf7|kU|)v z00tR{s|bb|2aGF*C5_I=#z6~Y;><)XiIB~l05lrzL}LX#;jGg;X{0nZAA^MSrAWd-p7#5Ino literal 0 HcmV?d00001 diff --git a/tests/data/fair1m/labelXml/0.xml b/tests/data/fair1m/train/part1/labelXml/0.xml similarity index 100% rename from tests/data/fair1m/labelXml/0.xml rename to tests/data/fair1m/train/part1/labelXml/0.xml diff --git a/tests/data/fair1m/labelXml/1.xml b/tests/data/fair1m/train/part1/labelXml/1.xml similarity index 100% rename from tests/data/fair1m/labelXml/1.xml rename to tests/data/fair1m/train/part1/labelXml/1.xml diff --git a/tests/data/fair1m/labelXml/2.xml b/tests/data/fair1m/train/part1/labelXml/2.xml similarity index 100% rename from tests/data/fair1m/labelXml/2.xml rename to tests/data/fair1m/train/part1/labelXml/2.xml diff --git a/tests/data/fair1m/labelXml/3.xml b/tests/data/fair1m/train/part1/labelXml/3.xml similarity index 100% rename from tests/data/fair1m/labelXml/3.xml rename to tests/data/fair1m/train/part1/labelXml/3.xml diff --git a/tests/data/fair1m/train/part2/images.zip b/tests/data/fair1m/train/part2/images.zip new file mode 100644 index 0000000000000000000000000000000000000000..14e338f5ba358f02fe918a1e4e450d371726b993 GIT binary patch literal 1060 zcmWIWW@h1H0D<@GriFnSP=cL7h9NUIF+H_dKQx4sf%#&%O9}{=R&X;gvVatUi2$Gh zB0vrY*nqGbvmbi^c{6~R2VsDbUP)#e+!X&t3{yUTxsvoCAtB*GQbIyXVgk#i84WW8 zJI@|DapnNelG_b4Bu}t)HXBG!;SEye2))mIWKlrj1UW;M;v)-l9w=*`REYdO@iA9w1v3 bDh|>khGdR7l+6Pamq9WI$OXCp1d#y%6VL+2 literal 0 HcmV?d00001 diff --git a/tests/data/fair1m/train/part2/images/1.tif b/tests/data/fair1m/train/part2/images/1.tif new file mode 100644 index 0000000000000000000000000000000000000000..8168bf8821b850151ba118253d63cc24f74a9902 GIT binary patch literal 152 zcmebD)MDUZU|`^4U|?inU<9(5fS3`9&BVyezzh^?17c<%8>9w=*`REYdO@iA9w1v3 bDh|>khGdR7l+6Pamq9WI$OXCp1d#y%6VL+2 literal 0 HcmV?d00001 diff --git a/tests/data/fair1m/train/part2/images/2.tif b/tests/data/fair1m/train/part2/images/2.tif new file mode 100644 index 0000000000000000000000000000000000000000..8168bf8821b850151ba118253d63cc24f74a9902 GIT binary patch literal 152 zcmebD)MDUZU|`^4U|?inU<9(5fS3`9&BVyezzh^?17c<%8>9w=*`REYdO@iA9w1v3 bDh|>khGdR7l+6Pamq9WI$OXCp1d#y%6VL+2 literal 0 HcmV?d00001 diff --git a/tests/data/fair1m/train/part2/images/3.tif b/tests/data/fair1m/train/part2/images/3.tif new file mode 100644 index 0000000000000000000000000000000000000000..8168bf8821b850151ba118253d63cc24f74a9902 GIT binary patch literal 152 zcmebD)MDUZU|`^4U|?inU<9(5fS3`9&BVyezzh^?17c<%8>9w=*`REYdO@iA9w1v3 bDh|>khGdR7l+6Pamq9WI$OXCp1d#y%6VL+2 literal 0 HcmV?d00001 diff --git a/tests/data/fair1m/train/part2/labelXml/0.xml b/tests/data/fair1m/train/part2/labelXml/0.xml new file mode 100644 index 00000000000..ae4e63412ca --- /dev/null +++ b/tests/data/fair1m/train/part2/labelXml/0.xml @@ -0,0 +1,28 @@ + + + + 0.tif + + + 2 + 2 + 3 + + + + pixel + rectangle + None + + Liquid Cargo Ship + + + 0.000000,0.000000 + 1.000000,0.000000 + 1.000000,1.000000 + 0.000000,1.000000 + 0.000000,0.000000 + + + + \ No newline at end of file diff --git a/tests/data/fair1m/train/part2/labelXml/1.xml b/tests/data/fair1m/train/part2/labelXml/1.xml new file mode 100644 index 00000000000..ba8c9d5635a --- /dev/null +++ b/tests/data/fair1m/train/part2/labelXml/1.xml @@ -0,0 +1,28 @@ + + + + 1.tif + + + 2 + 2 + 3 + + + + pixel + rectangle + None + + Cargo Truck + + + 0.000000,0.000000 + 1.000000,0.000000 + 1.000000,1.000000 + 0.000000,1.000000 + 0.000000,0.000000 + + + + \ No newline at end of file diff --git a/tests/data/fair1m/train/part2/labelXml/2.xml b/tests/data/fair1m/train/part2/labelXml/2.xml new file mode 100644 index 00000000000..1e8cc69d6a6 --- /dev/null +++ b/tests/data/fair1m/train/part2/labelXml/2.xml @@ -0,0 +1,28 @@ + + + + 2.tif + + + 2 + 2 + 3 + + + + pixel + rectangle + None + + Boeing737 + + + 0.000000,0.000000 + 1.000000,0.000000 + 1.000000,1.000000 + 0.000000,1.000000 + 0.000000,0.000000 + + + + \ No newline at end of file diff --git a/tests/data/fair1m/train/part2/labelXml/3.xml b/tests/data/fair1m/train/part2/labelXml/3.xml new file mode 100644 index 00000000000..4bfb99bec87 --- /dev/null +++ b/tests/data/fair1m/train/part2/labelXml/3.xml @@ -0,0 +1,28 @@ + + + + 3.tif + + + 2 + 2 + 3 + + + + pixel + rectangle + None + + A220 + + + 0.000000,0.000000 + 1.000000,0.000000 + 1.000000,1.000000 + 0.000000,1.000000 + 0.000000,0.000000 + + + + \ No newline at end of file diff --git a/tests/data/fair1m/train/part2/labelXmls.zip b/tests/data/fair1m/train/part2/labelXmls.zip new file mode 100644 index 0000000000000000000000000000000000000000..35d5f40034c66db7d9f8903bd535c0c6b9d2618f GIT binary patch literal 1834 zcmWIWW@h1H0D;fzriFnSP=b>|h9M_0DK#e|H%C7-gp+~!X1Gg=M1)I9X$3a}BMV3w zmp!saQYv^m=dHuTmmlY=q`W8$_n&X~S0yt2@W-ma-~LMGWs3kvp;>8cDJO;rZQfVkbb9Y^J(I`6%Dy_ zTz-otV=@u4}DAfpQiqk zF(=OM$&KEw3#}#lehYorwd!(Zgv*+qPTebaSWW*se2u-Zws7+~{R7w3BT_yz=GvXn zM-9xk)7K%%4*oy{+rywWBYE_%gf8> ztDNSO(NB-8TU5H_>cXsVrJ;+mO5L+8LMFd|KYx|-{l);V>W9on%2_RJZ;CZ*El}_D znltfwwZH8KQ$C4k%YrYHf9k*bb3(o$RdB6frbO+RG|{tsz8>~WrrH7m8!o_6wp8zn(p2+^rh@v?U+=^OwUd*$P0bz8 z+6UAMm*n1A^VxdCS4A<&nuOW+pNXS}=vlVE`~CwHYb<(*8WRpt-=by%p4RJ6L?o~2 zvTb_lRH!EYLY9Lg;P^({ZJCqi?fb`gO6l^!qAI_ioL8 z!};p8=Q3}7M*VK1dD~rEPEEMsbk#fHD}Ts77F%ia*I&Nm)c$s@lE3k6`5z|KU<`Y) z!jc7;Fq6=O(TIp(%sZ?g(0cuxrqcT0WcBV6L&YGz3)U9jZp`aqV7jx+w)W>nS^Y&` ziC>?cJfH8Y$oA zxYP?19U|{fc+c%FyFvAvM6|_*FSCE-XWc#U{lFxSYYkWU^(rQFrFGw0&VOK1$JHR- zfb0G_b9y5#Ii_9ENd2=jyFKQzEbE4;!kph1Pn6!jaN1sn1jD|WV!6p4#~JOX9J%oQ zp^J-l_Dih`3uIlhW;)7zU34M-`o{tmv)*^cZ#ds)Pw-Kn&m1R~8nZjHWv=4JwpG&? zypms0!&!AL@6yYEH$O3K{$jV`YWYv*0B=SnIc8kNt^}xL1p)zvw~inhsSsv`6v7w< zFvvJuMKHuTU|caQX>>+54q6}+XC`V%gly&npwVzALdzxM%tXzL$YwqQCM&p^(40w} jnWza9*-TAfWd%1AnoMz-$;t)_cvc{s5A;HY(UtJ*^fPdycs~ugD}8IuOu@KZi;^+hAE%FTuFM6kdW{oDIp;xF@a^%jD{J4 zooA1nICFq!$?b+2k|)?Yn+>F=@CGS!gx+U9vM8W%f}EjB@sWi&athNNjo4W%4hTpx zFc9S$gF)pQ<3Z&bL;AUfkx7mjSJIOJrXLUxV0h~YqTvaV6_OA!5+TSiT*(k-7z4wS zMv!Sx!=Q9w=*`REYdO@iA9w1v3 bDh|>khGdR7l+6Pamq9WI$OXCp1d#y%6VL+2 literal 0 HcmV?d00001 diff --git a/tests/data/fair1m/validation/images/1.tif b/tests/data/fair1m/validation/images/1.tif new file mode 100644 index 0000000000000000000000000000000000000000..8168bf8821b850151ba118253d63cc24f74a9902 GIT binary patch literal 152 zcmebD)MDUZU|`^4U|?inU<9(5fS3`9&BVyezzh^?17c<%8>9w=*`REYdO@iA9w1v3 bDh|>khGdR7l+6Pamq9WI$OXCp1d#y%6VL+2 literal 0 HcmV?d00001 diff --git a/tests/data/fair1m/validation/images/2.tif b/tests/data/fair1m/validation/images/2.tif new file mode 100644 index 0000000000000000000000000000000000000000..8168bf8821b850151ba118253d63cc24f74a9902 GIT binary patch literal 152 zcmebD)MDUZU|`^4U|?inU<9(5fS3`9&BVyezzh^?17c<%8>9w=*`REYdO@iA9w1v3 bDh|>khGdR7l+6Pamq9WI$OXCp1d#y%6VL+2 literal 0 HcmV?d00001 diff --git a/tests/data/fair1m/validation/images/3.tif b/tests/data/fair1m/validation/images/3.tif new file mode 100644 index 0000000000000000000000000000000000000000..8168bf8821b850151ba118253d63cc24f74a9902 GIT binary patch literal 152 zcmebD)MDUZU|`^4U|?inU<9(5fS3`9&BVyezzh^?17c<%8>9w=*`REYdO@iA9w1v3 bDh|>khGdR7l+6Pamq9WI$OXCp1d#y%6VL+2 literal 0 HcmV?d00001 diff --git a/tests/data/fair1m/validation/labelXml/0.xml b/tests/data/fair1m/validation/labelXml/0.xml new file mode 100644 index 00000000000..ae4e63412ca --- /dev/null +++ b/tests/data/fair1m/validation/labelXml/0.xml @@ -0,0 +1,28 @@ + + + + 0.tif + + + 2 + 2 + 3 + + + + pixel + rectangle + None + + Liquid Cargo Ship + + + 0.000000,0.000000 + 1.000000,0.000000 + 1.000000,1.000000 + 0.000000,1.000000 + 0.000000,0.000000 + + + + \ No newline at end of file diff --git a/tests/data/fair1m/validation/labelXml/1.xml b/tests/data/fair1m/validation/labelXml/1.xml new file mode 100644 index 00000000000..ba8c9d5635a --- /dev/null +++ b/tests/data/fair1m/validation/labelXml/1.xml @@ -0,0 +1,28 @@ + + + + 1.tif + + + 2 + 2 + 3 + + + + pixel + rectangle + None + + Cargo Truck + + + 0.000000,0.000000 + 1.000000,0.000000 + 1.000000,1.000000 + 0.000000,1.000000 + 0.000000,0.000000 + + + + \ No newline at end of file diff --git a/tests/data/fair1m/validation/labelXml/2.xml b/tests/data/fair1m/validation/labelXml/2.xml new file mode 100644 index 00000000000..1e8cc69d6a6 --- /dev/null +++ b/tests/data/fair1m/validation/labelXml/2.xml @@ -0,0 +1,28 @@ + + + + 2.tif + + + 2 + 2 + 3 + + + + pixel + rectangle + None + + Boeing737 + + + 0.000000,0.000000 + 1.000000,0.000000 + 1.000000,1.000000 + 0.000000,1.000000 + 0.000000,0.000000 + + + + \ No newline at end of file diff --git a/tests/data/fair1m/validation/labelXml/3.xml b/tests/data/fair1m/validation/labelXml/3.xml new file mode 100644 index 00000000000..4bfb99bec87 --- /dev/null +++ b/tests/data/fair1m/validation/labelXml/3.xml @@ -0,0 +1,28 @@ + + + + 3.tif + + + 2 + 2 + 3 + + + + pixel + rectangle + None + + A220 + + + 0.000000,0.000000 + 1.000000,0.000000 + 1.000000,1.000000 + 0.000000,1.000000 + 0.000000,0.000000 + + + + \ No newline at end of file diff --git a/tests/data/fair1m/validation/labelXmls.zip b/tests/data/fair1m/validation/labelXmls.zip new file mode 100644 index 0000000000000000000000000000000000000000..27815d1b1109e2991b84120d5f7adbde34a60747 GIT binary patch literal 1834 zcmWIWW@h1H00Hjx)55?ED8b1f!;q7hl$sNfo1-5Z!pXqQ8sU;60mP*h+zgB?AZ1`8 z0BD2=ki!8sB1wl?f|ZehA%%&7fe+aTL%oXJ9Jo3DjTq)kowPsbumR7p=U+6dW-9t6 zBnI4FXf3S&z{*Rh;O(5Z4i8^`oUfAdqA=WlzTIDy$n?V>s{()fZ|Uo?o8lWfz52?f zElYkoTmF3aajDHGS&p^6;&u)-;SI)e^j+HBk}8|Zcu7L~ovzKNiR)H0YkE3$uiRlZ{qOKK_QKl2&FAzFTvLxo`OuhacSavI zFiTfiT;~D?W)^y28jui}MKc$I1G8~z@5{7pv%ZyvF3Ku(&$0-a{Qmv?Rm%4p1H7sqG9M{twXnS@)~vNaz0+&X z#Ou}mwi`_OB%&<~zD)k9|LV^P`G!=%wSt)vwO`Ui&+_?t*fX70Dm`uMF#q1huX>4* z?vW0wxAHz0liRtF+bcm@UGU#+h3yL+RyZnc5LxcizHV#ErKa^!iF1BGS2>dL{DsQJ zMYB`nA|0Z>w%Vz^I+dQt9=oqMrQvM207uzUy)Q~r%_Eu$>PvsU6BE=kVHO#UyJIX5W7%jvAt8+5YbP4@|7F=pkxMI7EGmnhkhbuRjryyr#>x z>7`Sln)nM@4vv818*R5`PMWvxALA*d%QGv!e);0L$)@e{U#)NVGjud7&#Yc_=DSAp z)rm5FKcD>i-v8%F`2~-aF^_GIluJgK&J^2wYJqy=rkzUmB7Ic}*>4QD6(zh>|7iZ| z&WY>Yl6}Q#@oneabY3bLXG}HvcdyrZ-OE0v8^=0Z zsxzJ(6E`Um;Ltf{czIzi@5>zzmQRv+_RmuA;;E=z93~m+6W!7T!fmgYa^~E-HTwdm^>w}ZkyGslegZM63TYS4QuZw}{&NAEDpBrWM7kMRq zeRlRZ5A%Vab;7sxO{Pt;c^Vq{EP6_Bmgl$SH6?d%SJyq0Ua>S}U76gY**t5ce4FD^ zFHCfZyg%VRx4Y~H)o&8f78}0I{*j+`_rUi9lQ^z5T;bQNn9P;deQP=Yfk_=#gLnh3 z`{&H*jkx5Pc0nWc&(7@jn9H)P8>R|#eqTIMdjG;{dmR!C`(}#eCVL!bw4ZY1!uN+R zF51~IwJt1>bA#IX5`-G-~>KbZr(8JXmmaTU7~ppq2`1Q^~rf@q{dm=#h8V-&z3 z<8T$h5aWPx#jvE&8QC~!flQp4s3j4ynG=9U!<`5%mxwbHH7_EY`3RV-;ATQ|CUIt> hCQM{AHG!2C+)QXP#bqWd8z|sefp9+1ccrW#9stAIi&y{v literal 0 HcmV?d00001 diff --git a/tests/datamodules/test_fair1m.py b/tests/datamodules/test_fair1m.py index dd3f2bd13f2..1a577695d4e 100644 --- a/tests/datamodules/test_fair1m.py +++ b/tests/datamodules/test_fair1m.py @@ -7,7 +7,6 @@ import pytest from torchgeo.datamodules import FAIR1MDataModule -from torchgeo.datasets import unbind_samples class TestFAIR1MDataModule: @@ -16,13 +15,7 @@ def datamodule(self) -> FAIR1MDataModule: root = os.path.join("tests", "data", "fair1m") batch_size = 2 num_workers = 0 - dm = FAIR1MDataModule( - root=root, - batch_size=batch_size, - num_workers=num_workers, - val_split_pct=0.33, - test_split_pct=0.33, - ) + dm = FAIR1MDataModule(root=root, batch_size=batch_size, num_workers=num_workers) return dm def test_train_dataloader(self, datamodule: FAIR1MDataModule) -> None: @@ -33,13 +26,17 @@ def test_val_dataloader(self, datamodule: FAIR1MDataModule) -> None: datamodule.setup("validate") next(iter(datamodule.val_dataloader())) - def test_test_dataloader(self, datamodule: FAIR1MDataModule) -> None: - datamodule.setup("test") - next(iter(datamodule.test_dataloader())) + def test_predict_dataloader(self, datamodule: FAIR1MDataModule) -> None: + datamodule.setup("predict") + next(iter(datamodule.predict_dataloader())) def test_plot(self, datamodule: FAIR1MDataModule) -> None: datamodule.setup("validate") batch = next(iter(datamodule.val_dataloader())) - sample = unbind_samples(batch)[0] + sample = { + "image": batch["image"][0], + "boxes": batch["boxes"][0], + "label": batch["label"][0], + } datamodule.plot(sample) plt.close() diff --git a/tests/datasets/test_fair1m.py b/tests/datasets/test_fair1m.py index 73d91608c42..6886174d2ce 100644 --- a/tests/datasets/test_fair1m.py +++ b/tests/datasets/test_fair1m.py @@ -9,19 +9,65 @@ import pytest import torch import torch.nn as nn +from _pytest.fixtures import SubRequest from _pytest.monkeypatch import MonkeyPatch +import torchgeo.datasets.utils from torchgeo.datasets import FAIR1M +def download_url(url: str, root: str, *args: str, **kwargs: str) -> None: + shutil.copy(url, root) + + class TestFAIR1M: - @pytest.fixture - def dataset(self, monkeypatch: MonkeyPatch) -> FAIR1M: - md5s = ["f278aba757de9079225db42107e09e30", "ecef7bd264fcbc533bec5e9e1cacaff1"] + test_root = os.path.join("tests", "data", "fair1m") + + @pytest.fixture(params=["train", "val", "test"]) + def dataset( + self, monkeypatch: MonkeyPatch, tmp_path: Path, request: SubRequest + ) -> FAIR1M: + monkeypatch.setattr(torchgeo.datasets.fair1m, "download_url", download_url) + urls = { + "train": ( + os.path.join(self.test_root, "train", "part1", "images.zip"), + os.path.join(self.test_root, "train", "part1", "labelXml.zip"), + os.path.join(self.test_root, "train", "part2", "images.zip"), + os.path.join(self.test_root, "train", "part2", "labelXmls.zip"), + ), + "val": ( + os.path.join(self.test_root, "validation", "images.zip"), + os.path.join(self.test_root, "validation", "labelXmls.zip"), + ), + "test": ( + os.path.join(self.test_root, "test", "images0.zip"), + os.path.join(self.test_root, "test", "images1.zip"), + os.path.join(self.test_root, "test", "images2.zip"), + ), + } + md5s = { + "train": ( + "ffbe9329e51ae83161ce24b5b46dc934", + "2db6fbe64be6ebb0a03656da6c6effe7", + "401b0f1d75d9d23f2e088bfeaf274cfa", + "d62b18eae8c3201f6112c2e9db84d605", + ), + "val": ( + "83d2f06574fc7158ded0eb1fb256c8fe", + "316490b200503c54cf43835a341b6dbe", + ), + "test": ( + "3c02845752667b96a5749c90c7fdc994", + "9359107f1d0abac6a5b98725f4064bc0", + "d7bc2985c625ffd47d86cdabb2a9d2bc", + ), + } + monkeypatch.setattr(FAIR1M, "urls", urls) monkeypatch.setattr(FAIR1M, "md5s", md5s) - root = os.path.join("tests", "data", "fair1m") + root = str(tmp_path) + split = request.param transforms = nn.Identity() - return FAIR1M(root, transforms) + return FAIR1M(root, split, transforms, download=True, checksum=True) def test_getitem(self, dataset: FAIR1M) -> None: x = dataset[0] @@ -30,38 +76,46 @@ def test_getitem(self, dataset: FAIR1M) -> None: assert isinstance(x["boxes"], torch.Tensor) assert isinstance(x["label"], torch.Tensor) assert x["image"].shape[0] == 3 - assert x["boxes"].shape[-2:] == (5, 2) - assert x["label"].ndim == 1 + + if dataset.split != "test": + assert x["boxes"].shape[-2:] == (5, 2) + assert x["label"].ndim == 1 def test_len(self, dataset: FAIR1M) -> None: - assert len(dataset) == 4 + if dataset.split == "train": + assert len(dataset) == 8 + else: + assert len(dataset) == 4 def test_already_downloaded(self, dataset: FAIR1M, tmp_path: Path) -> None: shutil.rmtree(str(tmp_path)) - shutil.copytree(dataset.root, str(tmp_path)) - FAIR1M(root=str(tmp_path)) + shutil.copytree(self.test_root, str(tmp_path)) + FAIR1M(root=str(tmp_path), split=dataset.split) def test_already_downloaded_not_extracted( self, dataset: FAIR1M, tmp_path: Path ) -> None: - for filename in dataset.filenames: - filepath = os.path.join("tests", "data", "fair1m", filename) - shutil.copy(filepath, str(tmp_path)) - FAIR1M(root=str(tmp_path), checksum=True) - - def test_corrupted(self, tmp_path: Path) -> None: - filenames = ["images.zip", "labelXmls.zip"] - for filename in filenames: - with open(os.path.join(tmp_path, filename), "w") as f: + for path in dataset.paths[dataset.split]: + filepath = os.path.join(self.test_root, path) + output = os.path.join(str(tmp_path), os.path.dirname(filepath)) + os.makedirs(os.path.dirname(output)) + shutil.copy(filepath, output) + FAIR1M(root=str(tmp_path), split=dataset.split, checksum=True) + + def test_corrupted(self, tmp_path: Path, dataset: FAIR1M) -> None: + paths = dataset.paths[dataset.split] + for path in paths: + filepath = os.path.join(tmp_path, path) + os.makedirs(os.path.dirname(filepath), exist_ok=True) + with open(filepath, "w") as f: f.write("bad") with pytest.raises(RuntimeError, match="Dataset found, but corrupted."): - FAIR1M(root=str(tmp_path), checksum=True) + FAIR1M(root=str(tmp_path), split=dataset.split, checksum=True) - def test_not_downloaded(self, tmp_path: Path) -> None: - err = "Dataset not found in `root` directory, " - "specify a different `root` directory." - with pytest.raises(RuntimeError, match=err): - FAIR1M(str(tmp_path)) + def test_not_downloaded(self, tmp_path: Path, dataset: FAIR1M) -> None: + shutil.rmtree(str(tmp_path)) + with pytest.raises(RuntimeError, match="Dataset not found in"): + FAIR1M(root=str(tmp_path), split=dataset.split) def test_plot(self, dataset: FAIR1M) -> None: x = dataset[0].copy() diff --git a/torchgeo/datamodules/fair1m.py b/torchgeo/datamodules/fair1m.py index 24b6f46eae1..bec15f30b07 100644 --- a/torchgeo/datamodules/fair1m.py +++ b/torchgeo/datamodules/fair1m.py @@ -5,9 +5,31 @@ from typing import Any +import torch +from torch import Tensor + from ..datasets import FAIR1M from .geo import NonGeoDataModule -from .utils import dataset_split + + +def collate_fn(batch: list[dict[str, Tensor]]) -> dict[str, Any]: + """Custom object detection collate fn to handle variable boxes. + + Args: + batch: list of sample dicts return by dataset + + Returns: + batch dict output + """ + output: dict[str, Any] = {} + output["image"] = torch.stack([sample["image"] for sample in batch]) + + if "boxes" in batch[0]: + output["boxes"] = [sample["boxes"] for sample in batch] + if "label" in batch[0]: + output["label"] = [sample["label"] for sample in batch] + + return output class FAIR1MDataModule(NonGeoDataModule): @@ -17,27 +39,18 @@ class FAIR1MDataModule(NonGeoDataModule): """ def __init__( - self, - batch_size: int = 64, - num_workers: int = 0, - val_split_pct: float = 0.2, - test_split_pct: float = 0.2, - **kwargs: Any, + self, batch_size: int = 64, num_workers: int = 0, **kwargs: Any ) -> None: """Initialize a new FAIR1MDataModule instance. Args: batch_size: Size of each mini-batch. num_workers: Number of workers for parallel data loading. - val_split_pct: Percentage of the dataset to use as a validation set. - test_split_pct: Percentage of the dataset to use as a test set. **kwargs: Additional keyword arguments passed to :class:`~torchgeo.datasets.FAIR1M`. """ super().__init__(FAIR1M, batch_size, num_workers, **kwargs) - - self.val_split_pct = val_split_pct - self.test_split_pct = test_split_pct + self.collate_fn = collate_fn def setup(self, stage: str) -> None: """Set up datasets. @@ -45,7 +58,10 @@ def setup(self, stage: str) -> None: Args: stage: Either 'fit', 'validate', 'test', or 'predict'. """ - self.dataset = FAIR1M(**self.kwargs) - self.train_dataset, self.val_dataset, self.test_dataset = dataset_split( - self.dataset, val_pct=self.val_split_pct, test_pct=self.test_split_pct - ) + if stage in ["fit"]: + self.train_dataset = FAIR1M(split="train", **self.kwargs) + if stage in ["fit", "validate"]: + self.val_dataset = FAIR1M(split="val", **self.kwargs) + if stage in ["predict"]: + # Test set labels are not publicly available + self.predict_dataset = FAIR1M(split="test", **self.kwargs) diff --git a/torchgeo/datasets/fair1m.py b/torchgeo/datasets/fair1m.py index 6ebea6ed3ba..8f8cce84f5e 100644 --- a/torchgeo/datasets/fair1m.py +++ b/torchgeo/datasets/fair1m.py @@ -16,7 +16,7 @@ from torch import Tensor from .geo import NonGeoDataset -from .utils import check_integrity, extract_archive +from .utils import check_integrity, download_url, extract_archive def parse_pascal_voc(path: str) -> dict[str, Any]: @@ -156,54 +156,71 @@ class FAIR1M(NonGeoDataset): "Bridge": {"id": 36, "category": "Road"}, } - splits = { - "train": { - "filename_glob": os.path.join("train", "**", "images", "*.tif"), - "paths": [ - "train/part1/images.zip", - "train/part1/labelXml.zip", - "train/part2/images.zip", - "train/part2/labelXmls.zip", - ], - "urls": [ - "https://drive.google.com/file/d/1LWT_ybL-s88Lzg9A9wHpj0h2rJHrqrVf", - "https://drive.google.com/file/d/1CnOuS8oX6T9JMqQnfFsbmf7U38G6Vc8u", - "https://drive.google.com/file/d/1cx4MRfpmh68SnGAYetNlDy68w0NgKucJ", - "https://drive.google.com/file/d/1RFVjadTHA_bsB7BJwSZoQbiyM7KIDEUI", - ], - "md5s": [ - "a460fe6b1b5b276bf856ce9ac72d6568", - "80f833ff355f91445c92a0c0c1fa7414", - "ad237e61dba304fcef23cd14aa6c4280", - "5c5948e68cd0f991a0d73f10956a3b05", - ], - }, - "val": { - "filename_glob": os.path.join("validation", "images", "*.tif"), - "paths": ["validation/images.zip", "validation/labelXmls.zip"], - "urls": [ - "https://drive.google.com/file/d/1lSSHOD02B6_sUmr2b-R1iqhgWRQRw-S9", - "https://drive.google.com/file/d/1sTTna1C5n3Senpfo-73PdiNilnja1AV4", - ], - "md5s": [ - "dce782be65405aa381821b5f4d9eac94", - "700b516a21edc9eae66ca315b72a09a1", - ], - }, - "test": { - "filename_glob": os.path.join("test", "images", "*.tif"), - "paths": ["test/images0.zip", "test/images1.zip", "test/images2.zip"], - "urls": [ - "https://drive.google.com/file/d/1HtOOVfK9qetDBjE7MM0dK_u5u7n4gdw3", - "https://drive.google.com/file/d/1iXKCPmmJtRYcyuWCQC35bk97NmyAsasq", - "https://drive.google.com/file/d/1oUc25FVf8Zcp4pzJ31A1j1sOLNHu63P0", - ], - "md5s": [ - "fb8ccb274f3075d50ac9f7803fbafd3d", - "dc9bbbdee000e97f02276aa61b03e585", - "700b516a21edc9eae66ca315b72a09a1", - ], - }, + filename_glob = { + "train": os.path.join("train", "**", "images", "*.tif"), + "val": os.path.join("validation", "images", "*.tif"), + "test": os.path.join("test", "images", "*.tif"), + } + directories = { + "train": ( + os.path.join("train", "part1", "images"), + os.path.join("train", "part1", "labelXml"), + os.path.join("train", "part2", "images"), + os.path.join("train", "part2", "labelXml"), + ), + "val": ( + os.path.join("validation", "images"), + os.path.join("validation", "labelXml"), + ), + "test": (os.path.join("test", "images")), + } + paths = { + "train": ( + os.path.join("train", "part1", "images.zip"), + os.path.join("train", "part1", "labelXml.zip"), + os.path.join("train", "part2", "images.zip"), + os.path.join("train", "part2", "labelXmls.zip"), + ), + "val": ( + os.path.join("validation", "images.zip"), + os.path.join("validation", "labelXmls.zip"), + ), + "test": ( + os.path.join("test", "images0.zip"), + os.path.join("test", "images1.zip"), + os.path.join("test", "images2.zip"), + ), + } + urls = { + "train": ( + "https://drive.google.com/file/d/1LWT_ybL-s88Lzg9A9wHpj0h2rJHrqrVf", + "https://drive.google.com/file/d/1CnOuS8oX6T9JMqQnfFsbmf7U38G6Vc8u", + "https://drive.google.com/file/d/1cx4MRfpmh68SnGAYetNlDy68w0NgKucJ", + "https://drive.google.com/file/d/1RFVjadTHA_bsB7BJwSZoQbiyM7KIDEUI", + ), + "val": ( + "https://drive.google.com/file/d/1lSSHOD02B6_sUmr2b-R1iqhgWRQRw-S9", + "https://drive.google.com/file/d/1sTTna1C5n3Senpfo-73PdiNilnja1AV4", + ), + "test": ( + "https://drive.google.com/file/d/1HtOOVfK9qetDBjE7MM0dK_u5u7n4gdw3", + "https://drive.google.com/file/d/1iXKCPmmJtRYcyuWCQC35bk97NmyAsasq", + "https://drive.google.com/file/d/1oUc25FVf8Zcp4pzJ31A1j1sOLNHu63P0", + ), + } + md5s = { + "train": ( + "a460fe6b1b5b276bf856ce9ac72d6568", + "80f833ff355f91445c92a0c0c1fa7414", + "ad237e61dba304fcef23cd14aa6c4280", + "5c5948e68cd0f991a0d73f10956a3b05", + ), + "val": ("dce782be65405aa381821b5f4d9eac94", "700b516a21edc9eae66ca315b72a09a1"), + "test": ( + "fb8ccb274f3075d50ac9f7803fbafd3d", + "dc9bbbdee000e97f02276aa61b03e585", + "700b516a21edc9eae66ca315b72a09a1", + ), } image_root: str = "images" label_root: str = "labelXml" @@ -229,7 +246,7 @@ def __init__( RuntimeError: if ``download=False`` and data is not found, or checksums don't match """ - assert split in self.splits + assert split in self.directories self.root = root self.split = split self.transforms = transforms @@ -237,7 +254,7 @@ def __init__( self.checksum = checksum self._verify() self.files = sorted( - glob.glob(os.path.join(self.root, self.splits[split["filename_glob"]])) + glob.glob(os.path.join(self.root, self.filename_glob[split])) ) def __getitem__(self, index: int) -> dict[str, Tensor]: @@ -251,12 +268,12 @@ def __getitem__(self, index: int) -> dict[str, Tensor]: """ path = self.files[index] - image = self.load_image(path) + image = self._load_image(path) sample = {"image": image} if self.split != "test": - label_path = path.replace(self.image_root, self.labels_root) - label_path = os.path.splitext(label_path)[0] + ".xml" + label_path = path.replace(self.image_root, self.label_root) + label_path = label_path.replace(".tif", ".xml") voc = parse_pascal_voc(label_path) boxes, labels = self._load_target(voc["points"], voc["labels"]) sample = {"image": image, "boxes": boxes, "label": labels} @@ -313,17 +330,19 @@ def _verify(self) -> None: Raises: RuntimeError: if checksum fails or the dataset is not found """ - # Check if the files already exist + # Check if the directories already exist exists = [] - for directory in [self.image_root, self.labels_root]: + for directory in self.directories[self.split]: exists.append(os.path.exists(os.path.join(self.root, directory))) if all(exists): return # Check if .zip files already exists (if so extract) exists = [] - for filename, md5 in zip(self.filenames, self.md5s): - filepath = os.path.join(self.root, filename) + paths = self.paths[self.split] + md5s = self.md5s[self.split] + for path, md5 in zip(paths, md5s): + filepath = os.path.join(self.root, path) if os.path.isfile(filepath): if self.checksum and not check_integrity(filepath, md5): raise RuntimeError("Dataset found, but corrupted.") @@ -335,11 +354,39 @@ def _verify(self) -> None: if all(exists): return + if self.download: + self._download() + return + raise RuntimeError( - "Dataset not found in `root` directory, " - "specify a different `root` directory." + f"Dataset not found in `root={self.root}` and `download=False`, " + "either specify a different `root` directory or use `download=True` " + "to automatically download the dataset." ) + def _download(self) -> None: + """Download the dataset and extract it. + + Raises: + RuntimeError: if download doesn't work correctly or checksums don't match + """ + paths = self.paths[self.split] + urls = self.urls[self.split] + md5s = self.md5s[self.split] + for directory in self.directories[self.split]: + os.makedirs(os.path.join(self.root, directory), exist_ok=True) + + for path, url, md5 in zip(paths, urls, md5s): + filepath = os.path.join(self.root, path) + if not os.path.exists(filepath): + download_url( + url=url, + root=os.path.dirname(filepath), + filename=os.path.basename(filepath), + md5=md5 if self.checksum else None, + ) + extract_archive(filepath) + def plot( self, sample: dict[str, Tensor], From 2ef0cadd5c874f4502d0cf4fbcfffb37e91828e6 Mon Sep 17 00:00:00 2001 From: isaaccorley <22203655+isaaccorley@users.noreply.github.com> Date: Sun, 23 Apr 2023 03:19:09 +0000 Subject: [PATCH 17/17] fix tests --- tests/datasets/test_fair1m.py | 50 ++++++++++++++++++++--------------- torchgeo/datasets/fair1m.py | 14 +++++----- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/tests/datasets/test_fair1m.py b/tests/datasets/test_fair1m.py index 6886174d2ce..71e10d69567 100644 --- a/tests/datasets/test_fair1m.py +++ b/tests/datasets/test_fair1m.py @@ -16,8 +16,9 @@ from torchgeo.datasets import FAIR1M -def download_url(url: str, root: str, *args: str, **kwargs: str) -> None: - shutil.copy(url, root) +def download_url(url: str, root: str, filename: str, *args: str, **kwargs: str) -> None: + os.makedirs(root, exist_ok=True) + shutil.copy(url, os.path.join(root, filename)) class TestFAIR1M: @@ -73,11 +74,11 @@ def test_getitem(self, dataset: FAIR1M) -> None: x = dataset[0] assert isinstance(x, dict) assert isinstance(x["image"], torch.Tensor) - assert isinstance(x["boxes"], torch.Tensor) - assert isinstance(x["label"], torch.Tensor) assert x["image"].shape[0] == 3 if dataset.split != "test": + assert isinstance(x["boxes"], torch.Tensor) + assert isinstance(x["label"], torch.Tensor) assert x["boxes"].shape[-2:] == (5, 2) assert x["label"].ndim == 1 @@ -88,27 +89,32 @@ def test_len(self, dataset: FAIR1M) -> None: assert len(dataset) == 4 def test_already_downloaded(self, dataset: FAIR1M, tmp_path: Path) -> None: - shutil.rmtree(str(tmp_path)) - shutil.copytree(self.test_root, str(tmp_path)) - FAIR1M(root=str(tmp_path), split=dataset.split) + FAIR1M(root=str(tmp_path), split=dataset.split, download=True) def test_already_downloaded_not_extracted( self, dataset: FAIR1M, tmp_path: Path ) -> None: - for path in dataset.paths[dataset.split]: - filepath = os.path.join(self.test_root, path) - output = os.path.join(str(tmp_path), os.path.dirname(filepath)) - os.makedirs(os.path.dirname(output)) - shutil.copy(filepath, output) + shutil.rmtree(dataset.root) + for filepath, url in zip( + dataset.paths[dataset.split], dataset.urls[dataset.split] + ): + output = os.path.join(str(tmp_path), filepath) + os.makedirs(os.path.dirname(output), exist_ok=True) + download_url(url, root=os.path.dirname(output), filename=output) + FAIR1M(root=str(tmp_path), split=dataset.split, checksum=True) def test_corrupted(self, tmp_path: Path, dataset: FAIR1M) -> None: - paths = dataset.paths[dataset.split] - for path in paths: - filepath = os.path.join(tmp_path, path) - os.makedirs(os.path.dirname(filepath), exist_ok=True) - with open(filepath, "w") as f: - f.write("bad") + md5s = tuple(["randomhash"] * len(FAIR1M.md5s[dataset.split])) + FAIR1M.md5s[dataset.split] = md5s + shutil.rmtree(dataset.root) + for filepath, url in zip( + dataset.paths[dataset.split], dataset.urls[dataset.split] + ): + output = os.path.join(str(tmp_path), filepath) + os.makedirs(os.path.dirname(output), exist_ok=True) + download_url(url, root=os.path.dirname(output), filename=output) + with pytest.raises(RuntimeError, match="Dataset found, but corrupted."): FAIR1M(root=str(tmp_path), split=dataset.split, checksum=True) @@ -123,6 +129,8 @@ def test_plot(self, dataset: FAIR1M) -> None: plt.close() dataset.plot(x, show_titles=False) plt.close() - x["prediction_boxes"] = x["boxes"].clone() - dataset.plot(x) - plt.close() + + if dataset.split != "test": + x["prediction_boxes"] = x["boxes"].clone() + dataset.plot(x) + plt.close() diff --git a/torchgeo/datasets/fair1m.py b/torchgeo/datasets/fair1m.py index 8f8cce84f5e..dc2d4e99b48 100644 --- a/torchgeo/datasets/fair1m.py +++ b/torchgeo/datasets/fair1m.py @@ -415,12 +415,14 @@ def plot( axs[0].imshow(image) axs[0].axis("off") - polygons = [ - patches.Polygon(points, color="r", fill=False) - for points in sample["boxes"].numpy() - ] - for polygon in polygons: - axs[0].add_patch(polygon) + + if "boxes" in sample: + polygons = [ + patches.Polygon(points, color="r", fill=False) + for points in sample["boxes"].numpy() + ] + for polygon in polygons: + axs[0].add_patch(polygon) if show_titles: axs[0].set_title("Ground Truth")