Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Airphen: add new dataset #1803

Merged
merged 3 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/api/datasets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ Aboveground Woody Biomass

.. autoclass:: AbovegroundLiveWoodyBiomassDensity

Airphen
^^^^^^^

.. autoclass:: Airphen

Aster Global DEM
^^^^^^^^^^^^^^^^

Expand Down
1 change: 1 addition & 0 deletions docs/api/geo_datasets.csv
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
Dataset,Type,Source,License,Size (px),Resolution (m)
`Aboveground Woody Biomass`_,Masks,"Landsat, LiDAR","CC-BY-4.0","40,000x40,000",30
`Airphen`_,Imagery,Airphen,-,"1,280x960",0.047--0.09
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

License is up to the user who publishes their imagery.

Images are usually stitched together to create larger mosaics.

Resolution depends on customizable focal length and drone height, above values are for an altitude of 100 m.

`Aster Global DEM`_,Masks,Aster,"public domain","3,601x3,601",30
`Canadian Building Footprints`_,Geometries,Bing Imagery,"ODbL-1.0",-,-
`Chesapeake Land Cover`_,"Imagery, Masks",NAIP,"CC-BY-4.0",-,1
Expand Down
39 changes: 39 additions & 0 deletions tests/data/airphen/data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env python3

# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import numpy as np
import rasterio
from rasterio import Affine
from rasterio.crs import CRS

SIZE = 36

np.random.seed(0)


profile = {
"driver": "GTiff",
"dtype": "uint16",
"width": SIZE,
"height": SIZE,
"count": 6,
"crs": CRS.from_epsg(4326),
"transform": Affine(
4.497249999999613e-07,
0.0,
12.567765446921205,
0.0,
-4.4972499999996745e-07,
47.42974580435403,
),
}

Z = np.random.randint(
np.iinfo(profile["dtype"]).max, size=(SIZE, SIZE), dtype=profile["dtype"]
)

with rasterio.open("zoneA_B_R_NIR.tif", "w", **profile) as src:
for i in range(profile["count"]):
src.write(Z, i + 1)
Binary file added tests/data/airphen/zoneA_B_R_NIR.tif
Binary file not shown.
71 changes: 71 additions & 0 deletions tests/datasets/test_airphen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import os
from pathlib import Path

import matplotlib.pyplot as plt
import pytest
import torch
import torch.nn as nn
from rasterio.crs import CRS

from torchgeo.datasets import (
Airphen,
BoundingBox,
DatasetNotFoundError,
IntersectionDataset,
RGBBandsMissingError,
UnionDataset,
)


class TestAirphen:
@pytest.fixture
def dataset(self) -> Airphen:
paths = os.path.join("tests", "data", "airphen")
bands = ["B1", "B3", "B4"]
transforms = nn.Identity()
return Airphen(paths, bands=bands, transforms=transforms)

def test_len(self, dataset: Airphen) -> None:
assert len(dataset) == 1

def test_getitem(self, dataset: Airphen) -> None:
x = dataset[dataset.bounds]
assert isinstance(x, dict)
assert isinstance(x["crs"], CRS)
assert isinstance(x["image"], torch.Tensor)

def test_and(self, dataset: Airphen) -> None:
ds = dataset & dataset
assert isinstance(ds, IntersectionDataset)

def test_or(self, dataset: Airphen) -> None:
ds = dataset | dataset
assert isinstance(ds, UnionDataset)

def test_plot(self, dataset: Airphen) -> None:
x = dataset[dataset.bounds]
dataset.plot(x, suptitle="Test")
plt.close()

def test_no_data(self, tmp_path: Path) -> None:
with pytest.raises(DatasetNotFoundError, match="Dataset not found"):
Airphen(str(tmp_path))

def test_invalid_query(self, dataset: Airphen) -> None:
query = BoundingBox(0, 0, 0, 0, 0, 0)
with pytest.raises(
IndexError, match="query: .* not found in index with bounds:"
):
dataset[query]

def test_plot_wrong_bands(self, dataset: Airphen) -> None:
bands = ("B1", "B2", "B3")
ds = Airphen(dataset.paths, bands=bands)
x = dataset[dataset.bounds]
with pytest.raises(
RGBBandsMissingError, match="Dataset does not contain some of the RGB bands"
):
ds.plot(x)
2 changes: 2 additions & 0 deletions torchgeo/datasets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from .advance import ADVANCE
from .agb_live_woody_density import AbovegroundLiveWoodyBiomassDensity
from .airphen import Airphen
from .astergdem import AsterGDEM
from .benin_cashews import BeninSmallHolderCashews
from .bigearthnet import BigEarthNet
Expand Down Expand Up @@ -135,6 +136,7 @@
__all__ = (
# GeoDataset
"AbovegroundLiveWoodyBiomassDensity",
"Airphen",
"AsterGDEM",
"CanadianBuildingFootprints",
"CDL",
Expand Down
88 changes: 88 additions & 0 deletions torchgeo/datasets/airphen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

"""Airphen dataset."""

from typing import Any, Optional

import matplotlib.pyplot as plt
from matplotlib.figure import Figure

from .geo import RasterDataset
from .utils import RGBBandsMissingError, percentile_normalization


class Airphen(RasterDataset):
"""Airphen dataset.

`Airphen <https://ideol.sakura.ne.jp/img/20170123_HiphenAirphenKeyfeatures.pdf>`__
is a multispectral scientific camera developed by agronomists and photonics
engineers at `Hiphen <https://www.hiphen-plant.com/>`_ to match plant measurements
needs and constraints. Its high flexibility, ease of use and radiometric quality
adamjstewart marked this conversation as resolved.
Show resolved Hide resolved
give you a wide range of opportunities to develop your customized applications.

Main characteristics:

* 6 Synchronized global shutter sensors
* Sensor resolution 1280 x 960 pixels
* Data format (.tiff, 12 bit)
* SD card storage
* Metadata information: Exif and XMP
* Internal or external GPS
* Synchronization with different sensors (TIR, RGB, others)

Key features:

* Customized sensing configurations
* Easy integration
* Full control and configuration
* Powerful analysis with a specific Agisoft module

adamjstewart marked this conversation as resolved.
Show resolved Hide resolved
.. versionadded:: 0.6
"""

# Each camera measures a custom set of spectral bands chosen at purchase time.
# Hiphen offers 8 bands to choose from, sorted from short to long wavelength.
all_bands = ["B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8"]
rgb_bands = ["B4", "B3", "B1"]
Comment on lines +40 to +43
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is weird. Basically anyone who uses this dataset should really subclass it to specify which bands their data includes, such as:

class MyAirphen(Airphen):
    all_bands = ["B1", "B3", "B4"]


def plot(
self,
sample: dict[str, Any],
show_titles: bool = True,
suptitle: Optional[str] = None,
) -> Figure:
"""Plot a sample from the dataset.

Args:
sample: a sample returned by :meth:`RasterDataset.__getitem__`
show_titles: flag indicating whether to show titles above each panel
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].permute(1, 2, 0).float()
image = percentile_normalization(image, axis=(0, 1))

fig, ax = plt.subplots(1, 1, figsize=(4, 4))
ax.imshow(image)
ax.axis("off")

if show_titles:
ax.set_title("Image")

if suptitle is not None:
plt.suptitle(suptitle)

return fig