From 6baa00d5b361957a33a9d07fcc423e68279cd256 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Sat, 30 Nov 2024 13:16:51 +0100 Subject: [PATCH] HySpecNet-11k: add new dataset (#2410) * HySpecNet-11k: add new dataset * Add docs for data module * Shorten filename to appease Windows git * Add more tests * Even shorter * Fix mypy * Clarify benchmark task --- docs/api/datamodules.rst | 5 + docs/api/datasets.rst | 5 + docs/api/datasets/non_geo_datasets.csv | 1 + tests/conf/hyspecnet_byol.yaml | 11 + tests/conf/hyspecnet_moco.yaml | 11 + tests/conf/hyspecnet_simclr.yaml | 11 + tests/data/hyspecnet/data.py | 61 +++++ tests/data/hyspecnet/hyspecnet-11k-01.tar.gz | Bin 0 -> 26832 bytes .../hyspecnet/hyspecnet-11k-splits.tar.gz | Bin 0 -> 413 bytes ...38Z-Y01460273_X05670694-SPECTRAL_IMAGE.TIF | Bin 0 -> 502385 bytes ...38Z-Y01460273_X06950822-SPECTRAL_IMAGE.TIF | Bin 0 -> 502385 bytes .../hyspecnet-11k/splits/easy/test.csv | 2 + .../hyspecnet-11k/splits/easy/train.csv | 2 + .../hyspecnet-11k/splits/easy/val.csv | 2 + tests/datasets/test_hyspecnet.py | 58 +++++ tests/trainers/test_byol.py | 1 + tests/trainers/test_moco.py | 1 + tests/trainers/test_simclr.py | 1 + torchgeo/datamodules/__init__.py | 2 + torchgeo/datamodules/hyspecnet.py | 35 +++ torchgeo/datasets/__init__.py | 2 + torchgeo/datasets/hyspecnet.py | 229 ++++++++++++++++++ 22 files changed, 440 insertions(+) create mode 100644 tests/conf/hyspecnet_byol.yaml create mode 100644 tests/conf/hyspecnet_moco.yaml create mode 100644 tests/conf/hyspecnet_simclr.yaml create mode 100755 tests/data/hyspecnet/data.py create mode 100644 tests/data/hyspecnet/hyspecnet-11k-01.tar.gz create mode 100644 tests/data/hyspecnet/hyspecnet-11k-splits.tar.gz create mode 100644 tests/data/hyspecnet/hyspecnet-11k/patches/ENMAP01_20221103T162438Z/ENMAP01_20221103T162438Z-Y01460273_X05670694/ENMAP01_20221103T162438Z-Y01460273_X05670694-SPECTRAL_IMAGE.TIF create mode 100644 tests/data/hyspecnet/hyspecnet-11k/patches/ENMAP01_20221103T162438Z/ENMAP01_20221103T162438Z-Y01460273_X06950822/ENMAP01_20221103T162438Z-Y01460273_X06950822-SPECTRAL_IMAGE.TIF create mode 100644 tests/data/hyspecnet/hyspecnet-11k/splits/easy/test.csv create mode 100644 tests/data/hyspecnet/hyspecnet-11k/splits/easy/train.csv create mode 100644 tests/data/hyspecnet/hyspecnet-11k/splits/easy/val.csv create mode 100644 tests/datasets/test_hyspecnet.py create mode 100644 torchgeo/datamodules/hyspecnet.py create mode 100644 torchgeo/datasets/hyspecnet.py diff --git a/docs/api/datamodules.rst b/docs/api/datamodules.rst index abb0c6eaefa..cab21f872d4 100644 --- a/docs/api/datamodules.rst +++ b/docs/api/datamodules.rst @@ -124,6 +124,11 @@ GID-15 .. autoclass:: GID15DataModule +HySpecNet-11k +^^^^^^^^^^^^^ + +.. autoclass:: HySpecNet11kDataModule + Inria Aerial Image Labeling ^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/api/datasets.rst b/docs/api/datasets.rst index 96ca225344a..99cc4d75427 100644 --- a/docs/api/datasets.rst +++ b/docs/api/datasets.rst @@ -312,6 +312,11 @@ GID-15 .. autoclass:: GID15 +HySpecNet-11k +^^^^^^^^^^^^^ + +.. autoclass:: HySpecNet11k + IDTReeS ^^^^^^^ diff --git a/docs/api/datasets/non_geo_datasets.csv b/docs/api/datasets/non_geo_datasets.csv index f000bc1d8da..2f9b64227fa 100644 --- a/docs/api/datasets/non_geo_datasets.csv +++ b/docs/api/datasets/non_geo_datasets.csv @@ -21,6 +21,7 @@ Dataset,Task,Source,License,# Samples,# Classes,Size (px),Resolution (m),Bands `Forest Damage`_,OD,Drone imagery,"CDLA-Permissive-1.0","1,543",4,"1,500x1,500",,RGB `GeoNRW`_,S,Aerial,"CC-BY-4.0","7,783",11,"1,000x1,000",1,"RGB, DEM" `GID-15`_,S,Gaofen-2,-,150,15,"6,800x7,200",3,RGB +`HySpecNet-11k`_,-,EnMAP,CC0-1.0,11k,-,128,30,HSI `IDTReeS`_,"OD,C",Aerial,"CC-BY-4.0",591,33,200x200,0.1--1,RGB `Inria Aerial Image Labeling`_,S,Aerial,-,360,2,"5,000x5,000",0.3,RGB `LandCover.ai`_,S,Aerial,"CC-BY-NC-SA-4.0","10,674",5,512x512,0.25--0.5,RGB diff --git a/tests/conf/hyspecnet_byol.yaml b/tests/conf/hyspecnet_byol.yaml new file mode 100644 index 00000000000..5c0fa31d609 --- /dev/null +++ b/tests/conf/hyspecnet_byol.yaml @@ -0,0 +1,11 @@ +model: + class_path: BYOLTask + init_args: + model: 'resnet18' + in_channels: 202 +data: + class_path: HySpecNet11kDataModule + init_args: + batch_size: 2 + dict_kwargs: + root: 'tests/data/hyspecnet' diff --git a/tests/conf/hyspecnet_moco.yaml b/tests/conf/hyspecnet_moco.yaml new file mode 100644 index 00000000000..732b83912c1 --- /dev/null +++ b/tests/conf/hyspecnet_moco.yaml @@ -0,0 +1,11 @@ +model: + class_path: MoCoTask + init_args: + model: 'resnet18' + in_channels: 202 +data: + class_path: HySpecNet11kDataModule + init_args: + batch_size: 2 + dict_kwargs: + root: 'tests/data/hyspecnet' diff --git a/tests/conf/hyspecnet_simclr.yaml b/tests/conf/hyspecnet_simclr.yaml new file mode 100644 index 00000000000..d16e8209326 --- /dev/null +++ b/tests/conf/hyspecnet_simclr.yaml @@ -0,0 +1,11 @@ +model: + class_path: SimCLRTask + init_args: + model: 'resnet18' + in_channels: 202 +data: + class_path: HySpecNet11kDataModule + init_args: + batch_size: 2 + dict_kwargs: + root: 'tests/data/hyspecnet' diff --git a/tests/data/hyspecnet/data.py b/tests/data/hyspecnet/data.py new file mode 100755 index 00000000000..3b4b701106e --- /dev/null +++ b/tests/data/hyspecnet/data.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import shutil + +import numpy as np +import rasterio +from rasterio import Affine +from rasterio.crs import CRS + +SIZE = 32 +DTYPE = 'int16' + +np.random.seed(0) + +# Tile name purposefully shortened to avoid Windows git filename length limit. +tiles = ['ENMAP01_20221103T162438Z'] +patches = ['Y01460273_X05670694', 'Y01460273_X06950822'] + +profile = { + 'driver': 'GTiff', + 'dtype': DTYPE, + 'nodata': -32768.0, + 'width': SIZE, + 'height': SIZE, + 'count': 224, + 'crs': CRS.from_epsg(32618), + 'transform': Affine(30.0, 0.0, 691845.0, 0.0, -30.0, 4561935.0), + 'blockysize': 3, + 'tiled': False, + 'compress': 'deflate', + 'interleave': 'band', +} + +root = 'hyspecnet-11k' +path = os.path.join(root, 'splits', 'easy') +os.makedirs(path, exist_ok=True) +for tile in tiles: + for patch in patches: + # Split CSV + path = os.path.join(tile, f'{tile}-{patch}', f'{tile}-{patch}-DATA.npy') + for split in ['train', 'val', 'test']: + with open(os.path.join(root, 'splits', 'easy', f'{split}.csv'), 'a+') as f: + f.write(f'{path}\n') + + # Spectral image + path = os.path.join(root, 'patches', path) + os.makedirs(os.path.dirname(path), exist_ok=True) + path = path.replace('DATA.npy', 'SPECTRAL_IMAGE.TIF') + Z = np.random.randint( + np.iinfo(DTYPE).min, np.iinfo(DTYPE).max, size=(SIZE, SIZE), dtype=DTYPE + ) + with rasterio.open(path, 'w', **profile) as src: + for i in range(1, profile['count'] + 1): + src.write(Z, i) + +shutil.make_archive(f'{root}-01', 'gztar', '.', os.path.join(root, 'patches')) +shutil.make_archive(f'{root}-splits', 'gztar', '.', os.path.join(root, 'splits')) diff --git a/tests/data/hyspecnet/hyspecnet-11k-01.tar.gz b/tests/data/hyspecnet/hyspecnet-11k-01.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..b5a5ec766a5863566ad74f592d4f73f8d94d4d71 GIT binary patch literal 26832 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 NC-sK@I+Q1W=)aXv#GC*C literal 0 HcmV?d00001 diff --git a/tests/data/hyspecnet/hyspecnet-11k-splits.tar.gz b/tests/data/hyspecnet/hyspecnet-11k-splits.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..152f71c040f5bc153706d0df4d91928dd6a9b36b GIT binary patch literal 413 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 K!lnN|F8v=x8$k#F literal 0 HcmV?d00001 diff --git a/tests/data/hyspecnet/hyspecnet-11k/patches/ENMAP01_20221103T162438Z/ENMAP01_20221103T162438Z-Y01460273_X06950822/ENMAP01_20221103T162438Z-Y01460273_X06950822-SPECTRAL_IMAGE.TIF b/tests/data/hyspecnet/hyspecnet-11k/patches/ENMAP01_20221103T162438Z/ENMAP01_20221103T162438Z-Y01460273_X06950822/ENMAP01_20221103T162438Z-Y01460273_X06950822-SPECTRAL_IMAGE.TIF new file mode 100644 index 0000000000000000000000000000000000000000..5142ff4fbcf2cc4b0cc2017f416cca5a1a184291 GIT binary patch literal 502385 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 HySpecNet11k: + monkeypatch.setattr(HySpecNet11k, 'url', root + os.sep) + monkeypatch.setattr(HySpecNet11k, 'md5s', md5s) + transforms = nn.Identity() + return HySpecNet11k(root, transforms=transforms) + + def test_getitem(self, dataset: HySpecNet11k) -> None: + x = dataset[0] + assert isinstance(x, dict) + assert isinstance(x['image'], Tensor) + + def test_len(self, dataset: HySpecNet11k) -> None: + assert len(dataset) == 2 + + def test_download(self, dataset: HySpecNet11k, tmp_path: Path) -> None: + HySpecNet11k(tmp_path, download=True) + + def test_extract(self, dataset: HySpecNet11k, tmp_path: Path) -> None: + for file in glob.iglob(os.path.join(root, '*.tar.gz')): + shutil.copy(file, tmp_path) + HySpecNet11k(tmp_path) + + def test_not_downloaded(self, tmp_path: Path) -> None: + with pytest.raises(DatasetNotFoundError, match='Dataset not found'): + HySpecNet11k(tmp_path) + + def test_plot(self, dataset: HySpecNet11k) -> None: + x = dataset[0] + dataset.plot(x, suptitle='Test') + plt.close() + + def test_plot_rgb(self, dataset: HySpecNet11k) -> None: + dataset = HySpecNet11k(root=dataset.root, bands=(1, 2, 3)) + match = 'Dataset does not contain some of the RGB bands' + with pytest.raises(RGBBandsMissingError, match=match): + dataset.plot(dataset[0]) diff --git a/tests/trainers/test_byol.py b/tests/trainers/test_byol.py index b0c13b6075b..808bf937220 100644 --- a/tests/trainers/test_byol.py +++ b/tests/trainers/test_byol.py @@ -41,6 +41,7 @@ class TestBYOLTask: 'name', [ 'chesapeake_cvpr_prior_byol', + 'hyspecnet_byol', 'seco_byol_1', 'seco_byol_2', 'ssl4eo_l_byol_1', diff --git a/tests/trainers/test_moco.py b/tests/trainers/test_moco.py index 32c002dc573..002944b929e 100644 --- a/tests/trainers/test_moco.py +++ b/tests/trainers/test_moco.py @@ -29,6 +29,7 @@ class TestMoCoTask: 'name', [ 'chesapeake_cvpr_prior_moco', + 'hyspecnet_moco', 'seco_moco_1', 'seco_moco_2', 'ssl4eo_l_moco_1', diff --git a/tests/trainers/test_simclr.py b/tests/trainers/test_simclr.py index 7e1292ab7c0..3924b6e3785 100644 --- a/tests/trainers/test_simclr.py +++ b/tests/trainers/test_simclr.py @@ -29,6 +29,7 @@ class TestSimCLRTask: 'name', [ 'chesapeake_cvpr_prior_simclr', + 'hyspecnet_simclr', 'seco_simclr_1', 'seco_simclr_2', 'ssl4eo_l_simclr_1', diff --git a/torchgeo/datamodules/__init__.py b/torchgeo/datamodules/__init__.py index dc9513a6524..8f1cee47720 100644 --- a/torchgeo/datamodules/__init__.py +++ b/torchgeo/datamodules/__init__.py @@ -21,6 +21,7 @@ from .geo import BaseDataModule, GeoDataModule, NonGeoDataModule from .geonrw import GeoNRWDataModule from .gid15 import GID15DataModule +from .hyspecnet import HySpecNet11kDataModule from .inria import InriaAerialImageLabelingDataModule from .iobench import IOBenchDataModule from .l7irish import L7IrishDataModule @@ -75,6 +76,7 @@ 'GID15DataModule', 'GeoDataModule', 'GeoNRWDataModule', + 'HySpecNet11kDataModule', 'IOBenchDataModule', 'InriaAerialImageLabelingDataModule', 'L7IrishDataModule', diff --git a/torchgeo/datamodules/hyspecnet.py b/torchgeo/datamodules/hyspecnet.py new file mode 100644 index 00000000000..3e508ef11a7 --- /dev/null +++ b/torchgeo/datamodules/hyspecnet.py @@ -0,0 +1,35 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +"""HySpecNet datamodule.""" + +from typing import Any + +import torch + +from ..datasets import HySpecNet11k +from .geo import NonGeoDataModule + + +class HySpecNet11kDataModule(NonGeoDataModule): + """LightningDataModule implementation for the HySpecNet11k dataset. + + .. versionadded:: 0.7 + """ + + # https://git.tu-berlin.de/rsim/hyspecnet-tools/-/blob/main/tif_to_npy.ipynb + mean = torch.tensor(0) + std = torch.tensor(10000) + + def __init__( + self, batch_size: int = 64, num_workers: int = 0, **kwargs: Any + ) -> None: + """Initialize a new HySpecNet11kDataModule instance. + + Args: + batch_size: Size of each mini-batch. + num_workers: Number of workers for parallel data loading. + **kwargs: Additional keyword arguments passed to + :class:`~torchgeo.datasets.HySpecNet11k`. + """ + super().__init__(HySpecNet11k, batch_size, num_workers, **kwargs) diff --git a/torchgeo/datasets/__init__.py b/torchgeo/datasets/__init__.py index 3016c3af7e2..d77e13475a0 100644 --- a/torchgeo/datasets/__init__.py +++ b/torchgeo/datasets/__init__.py @@ -61,6 +61,7 @@ from .geonrw import GeoNRW from .gid15 import GID15 from .globbiomass import GlobBiomass +from .hyspecnet import HySpecNet11k from .idtrees import IDTReeS from .inaturalist import INaturalist from .inria import InriaAerialImageLabeling @@ -214,6 +215,7 @@ 'GeoDataset', 'GeoNRW', 'GlobBiomass', + 'HySpecNet11k', 'IDTReeS', 'INaturalist', 'IOBench', diff --git a/torchgeo/datasets/hyspecnet.py b/torchgeo/datasets/hyspecnet.py new file mode 100644 index 00000000000..412ea504b24 --- /dev/null +++ b/torchgeo/datasets/hyspecnet.py @@ -0,0 +1,229 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +"""HySpecNet dataset.""" + +import os +from collections.abc import Callable, Sequence +from typing import ClassVar + +import rasterio as rio +import torch +from einops import rearrange +from matplotlib import pyplot as plt +from matplotlib.figure import Figure +from torch import Tensor + +from .errors import DatasetNotFoundError, RGBBandsMissingError +from .geo import NonGeoDataset +from .utils import Path, download_url, extract_archive, percentile_normalization + +# https://git.tu-berlin.de/rsim/hyspecnet-tools/-/blob/main/tif_to_npy.ipynb +invalid_channels = [ + 126, + 127, + 128, + 129, + 130, + 131, + 132, + 133, + 134, + 135, + 136, + 137, + 138, + 139, + 140, + 160, + 161, + 162, + 163, + 164, + 165, + 166, +] +valid_channels_ids = [c + 1 for c in range(224) if c not in invalid_channels] + + +class HySpecNet11k(NonGeoDataset): + """HySpecNet-11k dataset. + + `HySpecNet-11k `__ is a large-scale + benchmark dataset for hyperspectral image compression and self-supervised learning. + It is made up of 11,483 nonoverlapping image patches acquired by the + `EnMAP satellite `_. Each patch is a portion of 128 x 128 + pixels with 224 spectral bands and with a ground sample distance of 30 m. + + To construct HySpecNet-11k, a total of 250 EnMAP tiles acquired during the routine + operation phase between 2 November 2022 and 9 November 2022 were considered. The + considered tiles are associated with less than 10% cloud and snow cover. The tiles + were radiometrically, geometrically and atmospherically corrected (L2A water & land + product). Then, the tiles were divided into nonoverlapping image patches. The + cropped patches at the borders of the tiles were eliminated. As a result, more than + 45 patches per tile are obtained, resulting in 11,483 patches for the full dataset. + + We provide predefined splits obtained by randomly dividing HySpecNet into: + + #. a training set that includes 70% of the patches, + #. a validation set that includes 20% of the patches, and + #. a test set that includes 10% of the patches. + + Depending on the way that we used for splitting the dataset, we define two + different splits: + + #. an easy split, where patches from the same tile can be present in different sets + (patchwise splitting); and + #. a hard split, where all patches from one tile belong to the same set + (tilewise splitting). + + If you use this dataset in your research, please cite the following paper: + + * https://arxiv.org/abs/2306.00385 + + .. versionadded:: 0.7 + """ + + url = 'https://hf.co/datasets/torchgeo/hyspecnet/resolve/13e110422a6925cbac0f11edff610219b9399227/' + md5s: ClassVar[dict[str, str]] = { + 'hyspecnet-11k-01.tar.gz': '974aae9197006727b42ec81796049efe', + 'hyspecnet-11k-02.tar.gz': 'f80574485f835b8a263b6c64076c0c62', + 'hyspecnet-11k-03.tar.gz': '6bc1de573f97fa4a75b79719b9270cb3', + 'hyspecnet-11k-04.tar.gz': '2463dc10653cb8be10d44951307c5e7d', + 'hyspecnet-11k-05.tar.gz': '16c1bd9e684673e741c0849bd015c988', + 'hyspecnet-11k-06.tar.gz': '8eef16b67d71af6eb4bc836d294fe3c4', + 'hyspecnet-11k-07.tar.gz': 'f61f0e7d6b05c861e69026b09130a5d6', + 'hyspecnet-11k-08.tar.gz': '19d390bc9e61b85e7d765f3077984976', + 'hyspecnet-11k-09.tar.gz': '197ff47befe5b9de88be5e1321c5ce5d', + 'hyspecnet-11k-10.tar.gz': '9e674cca126a9d139d6584be148d4bac', + 'hyspecnet-11k-splits.tar.gz': '94fad9e3c979c612c29a045406247d6c', + } + + all_bands = valid_channels_ids + rgb_bands = (43, 28, 10) + + def __init__( + self, + root: Path = 'data', + split: str = 'train', + strategy: str = 'easy', + bands: Sequence[int] = all_bands, + transforms: Callable[[dict[str, Tensor]], dict[str, Tensor]] | None = None, + download: bool = False, + checksum: bool = False, + ) -> None: + """Initialize a new HySpecNet11k instance. + + Args: + root: Root directory where dataset can be found. + split: One of 'train', 'val', or 'test'. + strategy: Either 'easy' for patchwise splitting or 'hard' for tilewise + splitting. + bands: Bands to return. + transforms: A function/transform that takes input sample and its target as + entry and returns a transformed version. + download: If True, download dataset and store it in the root directory. + checksum: If True, check the MD5 of the downloaded files (may be slow). + + Raises: + DatasetNotFoundError: If dataset is not found and *download* is False. + """ + self.root = root + self.split = split + self.strategy = strategy + self.bands = bands + self.transforms = transforms + self.download = download + self.checksum = checksum + + self._verify() + + path = os.path.join(root, 'hyspecnet-11k', 'splits', strategy, f'{split}.csv') + with open(path) as f: + self.files = f.read().strip().split('\n') + + def __len__(self) -> int: + """Return the number of data points in the dataset. + + Returns: + Length of the dataset. + """ + return len(self.files) + + def __getitem__(self, index: int) -> dict[str, Tensor]: + """Return an index within the dataset. + + Args: + index: Index to return. + + Returns: + Data and label at that index. + """ + file = self.files[index].replace('DATA.npy', 'SPECTRAL_IMAGE.TIF') + with rio.open(os.path.join(self.root, 'hyspecnet-11k', 'patches', file)) as src: + sample = {'image': torch.tensor(src.read(self.bands).astype('float32'))} + + if self.transforms is not None: + sample = self.transforms(sample) + + return sample + + def _verify(self) -> None: + """Verify the integrity of the dataset.""" + # Check if the extracted files already exist + exists = [] + for directory in ['patches', 'splits']: + path = os.path.join(self.root, 'hyspecnet-11k', directory) + exists.append(os.path.isdir(path)) + + if all(exists): + return + + for file, md5 in self.md5s.items(): + # Check if the file has already been downloaded + path = os.path.join(self.root, file) + if os.path.isfile(path): + extract_archive(path) + continue + + # Check if the user requested to download the dataset + if self.download: + url = self.url + file + download_url(url, self.root, md5=md5 if self.checksum else None) + extract_archive(path) + continue + + raise DatasetNotFoundError(self) + + def plot(self, sample: dict[str, Tensor], suptitle: str | None = None) -> Figure: + """Plot a sample from the dataset. + + Args: + sample: A sample returned by :meth:`__getitem__`. + suptitle: optional string to use as a suptitle + + Returns: + A matplotlib Figure with the rendered sample. + + Raises: + RGBBandsMissingError: If *bands* does not include all RGB bands. + """ + rgb_indices = [] + for band in self.rgb_bands: + if band in self.bands: + rgb_indices.append(self.bands.index(band)) + else: + raise RGBBandsMissingError() + + image = sample['image'][rgb_indices].cpu().numpy() + image = rearrange(image, 'c h w -> h w c') + image = percentile_normalization(image) + + fig, ax = plt.subplots() + ax.imshow(image) + ax.axis('off') + + if suptitle: + fig.suptitle(suptitle) + + return fig