Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Binary file added .DS_Store
Binary file not shown.
Binary file added docs/.DS_Store
Binary file not shown.
5 changes: 5 additions & 0 deletions docs/api/datasets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ NLCD

.. autoclass:: NLCD

ODIAC CO2 Emissions
^^^^^^^^^^^^^^^^^^^

.. autoclass:: ODIAC

Open Buildings
^^^^^^^^^^^^^^

Expand Down
3 changes: 2 additions & 1 deletion docs/api/datasets/geo_datasets.csv
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ Dataset,Type,Source,License,Size (px),Resolution (m)
`NAIP`_,Imagery,Aerial,"public domain","6,100x7,600",0.3--2
`NCCM`_,Masks,Sentinel-2,"CC-BY-4.0",-,10
`NLCD`_,Masks,Landsat,"public domain",-,30
`ODIAC CO2 Emissions`_,Emissions,"ODIAC, NIES","CC BY 4.0","43200x21600","~1000"
`Open Buildings`_,Geometries,"Maxar, CNES/Airbus","CC-BY-4.0 OR ODbL-1.0",-,-
`PRISMA`_,Imagery,PRISMA,-,512x512,5--30
`Sentinel`_,Imagery,Sentinel,"CC-BY-SA-3.0-IGO","10,000x10,000",10
`South Africa Crop Type`_,"Imagery, Masks",Sentinel-2,"CC-BY-4.0","256x256",10
`South America Soybean`_,Masks,"Landsat, MODIS",-,-,30
`South America Soybean`_,Masks,"Landsat, MODIS",-,-,30
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
96 changes: 96 additions & 0 deletions tests/data/odiac/data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import os

Check failure on line 4 in tests/data/odiac/data.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (F401)

tests/data/odiac/data.py:4:8: F401 `os` imported but unused
import shutil
from pathlib import Path

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

# --- Configuration for Fake Data ---
# Use a small size for test files to keep them lightweight
SIZE = 32
# Define the ODIAC versions, years, and months to generate fake data for
# Keep this minimal to speed up test setup.
VERSIONS = [2023, 2022]
YEARS = {
2023: [2021, 2022], # Years available for version 2023
2022: [2020, 2021] # Years available for version 2022
}
MONTHS = [1, 7] # Generate only January and July for testing

# Metadata extracted from gdalinfo
CRS_ODIAC = CRS.from_epsg(4326)
# Origin = (-180.0, 90.0) Pixel Size = (0.008333..., -0.008333...)
TRANSFORM_ODIAC = Affine(0.008333333333333, 0.0, -180.0,
0.0, -0.008333333333333, 90.0)
DTYPE_ODIAC = rasterio.float32
# -----------------------------------

np.random.seed(0) # for reproducibility

def create_fake_odiac_tif(
filepath: Path,
dtype: np.dtype,
crs: CRS,
transform: Affine,
size: int
) -> None:
"""Creates a fake ODIAC GeoTIFF file."""
profile = {
'driver': 'GTiff',
'dtype': dtype,
'count': 1, # Single band for CO2 emission
'crs': crs,
'transform': transform,
'height': size,
'width': size,
'nodata': None # Or set if ODIAC uses a specific nodata value
}

# Generate random emission data (e.g., positive values)
# Adjust range if needed based on typical ODIAC values
emission_data = np.random.rand(size, size).astype(dtype) * 100

with rasterio.open(filepath, 'w', **profile) as dst:
dst.write(emission_data, 1)

if __name__ == '__main__':
# Get the directory of the script being run
script_dir = Path(__file__).parent

print(f"Generating fake ODIAC data in: {script_dir}")

# Remove old generated data directories first
for version in VERSIONS:
for year in YEARS.get(version, []):
year_dir = script_dir / str(year)
if year_dir.exists():
print(f"Removing old directory: {year_dir}")
shutil.rmtree(year_dir)

# Create fake data
for version in VERSIONS:
for year in YEARS.get(version, []):
year_dir = script_dir / str(year)
year_dir.mkdir(exist_ok=True)
print(f"Creating files for Version {version}, Year {year}...")

for month in MONTHS:
yymm_str = f"{str(year)[-2:]}{month:02d}"
filename = f"odiac{version}_1km_excl_intl_{yymm_str}.tif"
filepath = year_dir / filename

create_fake_odiac_tif(
filepath=filepath,
dtype=DTYPE_ODIAC,
crs=CRS_ODIAC,
transform=TRANSFORM_ODIAC, # Using real transform is good practice
size=SIZE
)
print(f" Created: {filepath.name}")

print("Fake data generation complete.")
151 changes: 151 additions & 0 deletions tests/datasets/test_odiac.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import os

Check failure on line 4 in tests/datasets/test_odiac.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (F401)

tests/datasets/test_odiac.py:4:8: F401 `os` imported but unused
import shutil

Check failure on line 5 in tests/datasets/test_odiac.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (F401)

tests/datasets/test_odiac.py:5:8: F401 `shutil` imported but unused
from pathlib import Path
import pytest
import torch
from torch.nn import Identity
from rasterio.crs import CRS
import matplotlib.pyplot as plt

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

Check failure on line 19 in tests/datasets/test_odiac.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (I001)

tests/datasets/test_odiac.py:4:1: I001 Import block is un-sorted or un-formatted

# Define root using Path for consistency
TEST_DATA_ROOT = Path("tests") / "data" / "odiac"

class TestODIAC:
@pytest.fixture
def dataset(self) -> ODIAC:
root = TEST_DATA_ROOT
transforms = Identity()
return ODIAC(
paths=str(root),
version=2023,
years=[2021, 2022],
months=[1, 7],
transforms=transforms,
)

def test_getitem(self, dataset: ODIAC) -> None:
"""Test __getitem__ returns the correct data structure."""
# Query using the dataset's overall bounds (simpler than specific item)
# Relying on fake data being small enough for this to work
query_box = dataset.bounds

# Ensure the query has non-zero spatial extent for RasterDataset logic
query_box = BoundingBox(
minx=query_box.minx,
maxx=max(query_box.maxx, query_box.minx + dataset.res[0]),
miny=query_box.miny,
maxy=max(query_box.maxy, query_box.miny + dataset.res[1]),
mint=query_box.mint,
maxt=query_box.maxt
)

x = dataset[query_box]
assert isinstance(x, dict)
assert 'image' in x
assert 'crs' in x
assert 'bounds' in x
assert isinstance(x['image'], torch.Tensor)
assert isinstance(x['crs'], CRS)
assert isinstance(x['bounds'], BoundingBox)
assert x['image'].dtype == torch.float32
# Check shape based on fake data size (32x32)
assert x['image'].shape == (1, 32, 32)

def test_len(self, dataset: ODIAC) -> None:
"""Test the __len__ method returns the correct number of files."""
assert len(dataset) == 4

def test_and(self, dataset: ODIAC) -> None:
"""Test the intersection operator."""
ds = dataset & dataset
assert isinstance(ds, IntersectionDataset)
assert len(ds) == len(dataset)

def test_or(self, dataset: ODIAC) -> None:
"""Test the union operator."""
ds = dataset | dataset
assert isinstance(ds, UnionDataset)
assert len(ds) == len(dataset) * 2

def test_plot(self, dataset: ODIAC) -> None:
"""Test the plot method."""
# Use overall bounds for plotting test
x = dataset[dataset.bounds].copy()
dataset.plot(x, suptitle='Test Plot')
plt.close()

x['prediction'] = x['image'].clone() * 0.5
dataset.plot(x, suptitle='Test Plot with Prediction')
plt.close()

def test_invalid_query(self, dataset: ODIAC) -> None:
"""Test querying outside the dataset bounds."""
query = BoundingBox(minx=200, maxx=201, miny=100, maxy=101, mint=0, maxt=1)
with pytest.raises(IndexError, match='query: .* not found in index'):
dataset[query]

def test_no_data(self, tmp_path: Path) -> None:
"""Test error when no data is found and download=False."""
with pytest.raises(DatasetNotFoundError, match='Dataset not found'):
ODIAC(tmp_path, download=False)

def test_invalid_version(self, tmp_path: Path) -> None:
"""Test error with invalid version."""
with pytest.raises(AssertionError, match='Invalid version'):
ODIAC(tmp_path, version=1999, download=False)

def test_invalid_year(self, tmp_path: Path) -> None:
"""Test error with invalid year for the specified version."""
with pytest.raises(AssertionError, match='Invalid year'):
ODIAC(tmp_path, version=2023, years=[2023], download=False)
with pytest.raises(AssertionError, match='Invalid year'):
ODIAC(tmp_path, version=2023, years=[1999], download=False)

def test_invalid_month(self, tmp_path: Path) -> None:
"""Test error with invalid month."""
with pytest.raises(AssertionError, match='Invalid month 0'):
ODIAC(tmp_path, months=[0], download=False)
with pytest.raises(AssertionError, match='Invalid month 13'):
ODIAC(tmp_path, months=[13], download=False)

def test_different_crs(self, dataset: ODIAC) -> None:
"""Test instantiating with a different CRS (attribute check only)."""
target_crs = CRS.from_epsg(3857)
ds_reprojected = ODIAC(
paths=str(TEST_DATA_ROOT), # Use string path
crs=target_crs,
version=2023,
years=[2021],
months=[1]
)
assert ds_reprojected.crs == target_crs
# Check that the internal _crs attribute was updated
assert ds_reprojected._crs == target_crs
# Check that the overall bounds changed, indicating index reprojection
assert dataset.bounds != ds_reprojected.bounds

def test_different_res(self, dataset: ODIAC) -> None:
"""Test instantiating with a different resolution (attribute check only)."""
target_res_val = dataset.res[0] * 2
target_res_tuple = (target_res_val, target_res_val)
ds_resampled = ODIAC(
paths=str(TEST_DATA_ROOT), # Use string path
res=target_res_val,
version=2023,
years=[2021],
months=[1]
)
assert ds_resampled.res == target_res_tuple
# Check that the internal _res attribute was updated
assert ds_resampled._res == target_res_tuple
2 changes: 2 additions & 0 deletions torchgeo/datasets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
from .nasa_marine_debris import NASAMarineDebris
from .nccm import NCCM
from .nlcd import NLCD
from .odiac import ODIAC
from .openbuildings import OpenBuildings
from .oscd import OSCD
from .pastis import PASTIS
Expand Down Expand Up @@ -197,6 +198,7 @@
'NAIP',
'NCCM',
'NLCD',
'ODIAC',
'OSCD',
'PASTIS',
'PRISMA',
Expand Down
Loading