From 44a2db3bea0ec256c7194663b50ad918766b1dbc Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Sun, 8 Dec 2024 02:27:01 +0100 Subject: [PATCH 01/35] Add Introduction to PyTorch tutorial (#2440) --- docs/index.rst | 1 + docs/tutorials/pytorch.ipynb | 507 +++++++++++++++++++++++++++++++++++ 2 files changed, 508 insertions(+) create mode 100644 docs/tutorials/pytorch.ipynb diff --git a/docs/index.rst b/docs/index.rst index ced959493a8..1a8f0058e6b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -29,6 +29,7 @@ torchgeo :caption: Tutorials tutorials/getting_started + tutorials/pytorch tutorials/custom_raster_dataset tutorials/transforms tutorials/indices diff --git a/docs/tutorials/pytorch.ipynb b/docs/tutorials/pytorch.ipynb new file mode 100644 index 00000000000..db7bcdb4c88 --- /dev/null +++ b/docs/tutorials/pytorch.ipynb @@ -0,0 +1,507 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "45973fd5-6259-4e03-9501-02ee96f3f870", + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright (c) Microsoft Corporation. All rights reserved.\n", + "# Licensed under the MIT License." + ] + }, + { + "cell_type": "markdown", + "id": "9478ed9a", + "metadata": { + "id": "NdrXRgjU7Zih" + }, + "source": [ + "# Introduction to PyTorch\n", + "\n", + "_Written by: Adam J. Stewart_\n", + "\n", + "In this tutorial, we introduce the basics of deep learning with PyTorch. Understanding deep learning terminology and the training and evaluation pipeline in PyTorch is essential to using TorchGeo." + ] + }, + { + "cell_type": "markdown", + "id": "34f10e9f", + "metadata": { + "id": "lCqHTGRYBZcz" + }, + "source": [ + "## Setup\n", + "\n", + "First, we install TorchGeo and all of its dependencies, including PyTorch." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "019092f0", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install torchgeo" + ] + }, + { + "cell_type": "markdown", + "id": "4db9f791", + "metadata": { + "id": "dV0NLHfGBMWl" + }, + "source": [ + "## Imports\n", + "\n", + "Next, we import PyTorch, TorchGeo, and any other libraries we need. We also manually set the random seed to ensure the reproducibility of our experiments." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d92b0f1", + "metadata": { + "id": "entire-albania" + }, + "outputs": [], + "source": [ + "import os\n", + "import tempfile\n", + "\n", + "import kornia.augmentation as K\n", + "import torch\n", + "from torch import nn, optim\n", + "from torch.utils.data import DataLoader\n", + "\n", + "from torchgeo.datasets import EuroSAT100\n", + "from torchgeo.models import ResNet18_Weights, resnet18\n", + "\n", + "torch.manual_seed(0)" + ] + }, + { + "cell_type": "markdown", + "id": "9d13c2db-e5d4-4d83-846b-a2c32774bb44", + "metadata": {}, + "source": [ + "## Definitions\n", + "\n", + "If this is your first introduction to deep learning (DL), a natural question might be \"what _is_ deep learning?\". You may also be curious how it relates to other similar buzz words, including artificial intelligence (AI) and machine learning (ML). We can define these terms as follows:\n", + "\n", + "* AI: when machines exhibit human intelligence\n", + "* ML: when machines learn from example\n", + "* DL: when machines learn using neural networks\n", + "\n", + "In this definition, DL is a subset of ML, and ML is a subset of AI. Some common examples of models and applications of these include:\n", + "\n", + "* AI: Minimax, A*, Deep Blue, video game AI\n", + "* ML: OLS, SVM, $k$-means, spam filtering\n", + "* DL: MLP, CNN, ChatGPT, self-driving cars\n", + "\n", + "In this tutorial, we will specifically focus on deep learning, but many of the same concepts are shared with machine learning." + ] + }, + { + "cell_type": "markdown", + "id": "7f26e4b8", + "metadata": { + "id": "5rLknZxrBEMz" + }, + "source": [ + "## Datasets\n", + "\n", + "In order to learn by example, we first need examples. In machine learning, we construct datasets of the form:\n", + "\n", + "$$\\mathcal{D} = \\left\\{\\left(x^{(i)}, y^{(i)}\\right)\\right\\}_{i=1}^N$$\n", + "\n", + "Written in English, dataset $\\mathcal{D}$ is composed of $N$ pairs of inputs $x$ and expected outputs $y$. $x$ and $y$ can be tabular data, images, text, or any other object that can be represented mathematically.\n", + "\n", + "![EuroSAT](https://github.com/phelber/EuroSAT/blob/master/eurosat-overview.png?raw=true)\n", + "\n", + "In this tutorial (and many later tutorials), we will use EuroSAT100, a toy dataset composed of 100 images from the [EuroSAT](https://github.com/phelber/EuroSAT) dataset. EuroSAT is a popular image classification dataset with multispectral images from the Sentinel-2 satellites. Each image is classified into one of ten categories or \"classes\":\n", + "\n", + "0. Annual Crop\n", + "1. Forest\n", + "2. Herbaceous Vegetation\n", + "3. Highway\n", + "4. Industrial Buildings\n", + "5. Pasture\n", + "6. Permanent Crop\n", + "7. Residential Buildings\n", + "8. River\n", + "9. Sea & Lake\n", + "\n", + "We can load this dataset and visualize the RGB bands of some example $(x, y)$ pairs like so:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa0c5a0c-ac4c-44c5-9fb7-fe4be07a0f01", + "metadata": {}, + "outputs": [], + "source": [ + "root = os.path.join(tempfile.gettempdir(), 'eurosat100')\n", + "dataset = EuroSAT100(root, download=True)\n", + "\n", + "for i in torch.randint(len(dataset), (10,)):\n", + " sample = dataset[i]\n", + " dataset.plot(sample)" + ] + }, + { + "cell_type": "markdown", + "id": "f89e20ae-d3b6-4f05-a83f-f7034dd9862f", + "metadata": {}, + "source": [ + "In machine learning, we not only want to train a model, but also evaluate its performance on unseen data. Oftentimes, our dataset is split into three separate subsets:\n", + "\n", + "* train: for training the model *parameters*\n", + "* val: for validating the model *hyperparameters*\n", + "* test: for testing the model *performance*\n", + "\n", + "Parameters are the actual model weights, while hyperparameters are things like model width or learning rate that are chosen by the user. We can initialize datasets for all three splits like so:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4785cddb-9821-4a2a-aa86-c08ffb6f2ebc", + "metadata": {}, + "outputs": [], + "source": [ + "train_dataset = EuroSAT100(root, split='train')\n", + "val_dataset = EuroSAT100(root, split='val')\n", + "test_dataset = EuroSAT100(root, split='test')" + ] + }, + { + "cell_type": "markdown", + "id": "3e92d5be-8400-4c8a-83b0-314a672f22d1", + "metadata": {}, + "source": [ + "## Data Loaders\n", + "\n", + "While our dataset objects know how to load a single $(x, y)$ pair, machine learning often operates on what are called *mini-batches* of data. We can pass our above datasets to a PyTorch DataLoader object to construct these mini-batches:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8909c035-cbe9-49b6-8380-360914093f9a", + "metadata": {}, + "outputs": [], + "source": [ + "batch_size = 10\n", + "\n", + "train_dataloader = DataLoader(train_dataset, batch_size, shuffle=True)\n", + "val_dataloader = DataLoader(val_dataset, batch_size, shuffle=False)\n", + "test_dataloader = DataLoader(test_dataset, batch_size, shuffle=False)" + ] + }, + { + "cell_type": "markdown", + "id": "e7162d06-8814-4680-8192-aff279e70049", + "metadata": {}, + "source": [ + "## Transforms\n", + "\n", + "There are two categories of transforms a user may want to apply to their data:\n", + "\n", + "* Preprocessing: required to make data \"ML-ready\"\n", + "* Data augmentation: designed to artificially inflate the size of the dataset\n", + "\n", + "Preprocessing transforms such as normalization and one-hot encodings are applied to both training and evaluation data. Data augmentation transforms such as random flip and rotation are typically only performed during training. Below, we initialize transforms for both using the [Kornia](https://kornia.readthedocs.io/en/latest/augmentation.html) library." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "efc0152c-e7e4-4f06-9418-9d2c5dd803c3", + "metadata": {}, + "outputs": [], + "source": [ + "preprocess = K.Normalize(0, 10000)\n", + "augment = K.ImageSequential(K.RandomHorizontalFlip(), K.RandomVerticalFlip())" + ] + }, + { + "cell_type": "markdown", + "id": "e80cda68-19ea-4cd5-a3bc-cf8fcf8147af", + "metadata": {}, + "source": [ + "## Model\n", + "\n", + "Our goal is to learn some function $f$ that can map between input $x$ and expected output $y$. Mathematically, this can be expressed as:\n", + "\n", + "$$x \\overset{f}{\\mapsto} y, \\quad y = f(x)$$\n", + "\n", + "Since our $x$ in this case is an image, we choose to use ResNet-18, a popular *convolutional neural network* (CNN). We also initialize our model with weights that have been pre-trained on Sentinel-2 imagery so we don't have to start from scratch. This process is known as *transfer learning*." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c7cda7a8-2cd6-46a0-a1c2-bafc751a23f2", + "metadata": {}, + "outputs": [], + "source": [ + "model = resnet18(ResNet18_Weights.SENTINEL2_ALL_MOCO)" + ] + }, + { + "cell_type": "markdown", + "id": "a12b9e9b-26cc-43f4-a517-31b805862df5", + "metadata": {}, + "source": [ + "## Loss Function\n", + "\n", + "If $y$ is our expected output (also called \"ground truth\") and $\\hat{y}$ is our predicted output, our goal is to minimize the difference between $y$ and $\\hat{y}$. This difference is referred to as *error* or *loss*, and the loss function tells us how big of a mistake we made. For regression tasks, a simple mean squared error is sufficient:\n", + "\n", + "$$\\mathcal{L}(y, \\hat{y}) = \\left(y - \\hat{y}\\right)^2$$\n", + "\n", + "For classification tasks, such as EuroSAT, we instead use a negative log-likelihood:\n", + "\n", + "$$\\mathcal{L}_c(y, \\hat{y}) = - \\sum_{c=1}^C \\mathbb{1}_{y=\\hat{y}}\\log{p_c}$$\n", + "\n", + "where $\\mathbb{1}$ is the indicator function and $p_c$ is the probability with which the model predicts class $c$. By normalizing this over the log probability of all classes, we get the cross-entropy loss." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a0a699e-9bb3-4a06-91d2-401dd048ba66", + "metadata": {}, + "outputs": [], + "source": [ + "loss_fn = nn.CrossEntropyLoss()" + ] + }, + { + "cell_type": "markdown", + "id": "7743edf6-5fec-494d-8842-6cf8b45a2289", + "metadata": {}, + "source": [ + "## Optimizer\n", + "\n", + "In order to minimize our loss, we compute the gradient of the loss function with respect to model parameters $\\theta$. We then take a small step $\\alpha$ (also called the *learning rate*) in the direction of the negative gradient to update our model parameters in a process called *backpropagation*:\n", + "\n", + "$$\\theta \\leftarrow \\theta - \\alpha \\nabla_\\theta \\mathcal{L}(y, \\hat{y})$$\n", + "\n", + "When done one image or one mini-batch at a time, this is known as *stochastic gradient descent* (SGD)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15b0c17b-db53-41b2-96aa-3b732684b4cd", + "metadata": {}, + "outputs": [], + "source": [ + "optimizer = optim.SGD(model.parameters(), lr=1e-2)" + ] + }, + { + "cell_type": "markdown", + "id": "7efbe79d-a9a0-4a23-b2f3-21b4ea0af7bd", + "metadata": {}, + "source": [ + "## Device\n", + "\n", + "If you peak into the internals of deep learning models, you'll notice that most of it is actually linear algebra. This linear algebra is extremely easy to parallelize, and therefore can run very quickly on a GPU. We now transfer our model and all data to the GPU (if one is available):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a006a71f-0802-49b3-bd45-ddd524ae36a4", + "metadata": {}, + "outputs": [], + "source": [ + "device = 'cuda' if torch.cuda.is_available() else 'cpu'\n", + "model = model.to(device)" + ] + }, + { + "cell_type": "markdown", + "id": "7af95903-79c9-4a61-a7a4-2d41c884fba0", + "metadata": {}, + "source": [ + "## Training\n", + "\n", + "We finally have all the basic components we need to train our ResNet-18 model on the EuroSAT100 dataset. During training, we set the model to train mode, then iterate over all mini-batches in the dataset. During the forward pass, we ask the model $f$ to predict $\\hat{y}$ given $x$. We then calculate the loss accrued by these predictions. During the backward pass, we backpropagate our gradients to update all model weights." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d235772d-475e-42e7-bc7c-f50729ee0e22", + "metadata": {}, + "outputs": [], + "source": [ + "def train(dataloader):\n", + " model.train()\n", + " total_loss = 0\n", + " for batch in dataloader:\n", + " x = batch['image'].to(device)\n", + " y = batch['label'].to(device)\n", + "\n", + " # Forward pass\n", + " y_hat = model(x)\n", + " loss = loss_fn(y_hat, y)\n", + " total_loss += loss.item()\n", + "\n", + " # Backward pass\n", + " loss.backward()\n", + " optimizer.step()\n", + " optimizer.zero_grad()\n", + "\n", + " print(f'Loss: {total_loss:.2f}')" + ] + }, + { + "cell_type": "markdown", + "id": "1fd82312-cd17-4886-bcb6-8e42633e5009", + "metadata": {}, + "source": [ + "## Evaluation\n", + "\n", + "Once the model is trained, we need to evaluate its performance on unseen data. To do this, we set the model to evaluation mode, then iterate over all mini-batches in the dataset. Note that we also disable the computation of gradients, since we do not need to backpropagate them. Finally, we compute the number of correctly classified images." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3bddce3b-ed2f-4a5c-b3c6-2f1a3a51c2d9", + "metadata": {}, + "outputs": [], + "source": [ + "def evaluate(dataloader):\n", + " model.eval()\n", + " correct = 0\n", + " with torch.no_grad():\n", + " for batch in dataloader:\n", + " x = batch['image'].to(device)\n", + " y = batch['label'].to(device)\n", + "\n", + " # Forward pass\n", + " y_hat = model(x)\n", + " correct += (y_hat.argmax(1) == y).type(torch.float).sum().item()\n", + "\n", + " correct /= len(dataloader.dataset)\n", + " print(f'Accuracy: {correct:.0%}')" + ] + }, + { + "cell_type": "markdown", + "id": "f62a54e5-897d-476c-8d84-381993dbabbd", + "metadata": {}, + "source": [ + "## Putting It All Together\n", + "\n", + "In machine learning, we typically iterate over our datasets multiple times. Each full pass through the dataset is called an *epoch*. The following hyperparameter controls the number of epoch for which we train our model, and can be modified to train the model for longer:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eb5dc7e8-6cb3-4457-83ad-7fa5aef8ea0c", + "metadata": { + "nbmake": { + "mock": { + "epochs": 1 + } + } + }, + "outputs": [], + "source": [ + "epochs = 100" + ] + }, + { + "cell_type": "markdown", + "id": "f53526e6-54a3-43f7-a377-dca298730387", + "metadata": {}, + "source": [ + "During each epoch, we train the model on our training dataset, then evaluate its performance on the validation dataset. The goal is for training loss to decrease and validation accuracy to increase, although you should expect noise in the training process. Generally, you want to train the model until the validation accuracy starts to plateau or even decrease." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97601568-ba75-443d-81cf-494956b2924c", + "metadata": {}, + "outputs": [], + "source": [ + "for epoch in range(epochs):\n", + " print(f'Epoch: {epoch}')\n", + " train(train_dataloader)\n", + " evaluate(val_dataloader)" + ] + }, + { + "cell_type": "markdown", + "id": "e130fc89-0823-4814-85f8-d4416d6df395", + "metadata": {}, + "source": [ + "Finally, we evaluate our performance on the test dataset. Note that we are only training our model on a toy dataset consisting of 100 images. If we instead trained on the full dataset (replace `EuroSAT100` with `EuroSAT` in the above code), we would likely get much higher performance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7cd0bd25-e19a-4b26-94a1-fe9a544e8afd", + "metadata": {}, + "outputs": [], + "source": [ + "evaluate(test_dataloader)" + ] + }, + { + "cell_type": "markdown", + "id": "a3acc64e-8dc0-46b4-a677-ecb9723d4f56", + "metadata": {}, + "source": [ + "## Additional Reading\n", + "\n", + "If you are new to machine learning and overwhelmed by all of the above terminology, or would like to gain a better understanding of some of the math that goes into machine learning, I would highly recommend a formal machine learning or deep learning course. The following official PyTorch tutorials are also worth exploring:\n", + "\n", + "* [PyTorch: Learn the Basics](https://pytorch.org/tutorials/beginner/basics/intro.html)\n", + "* [Deep Learning with PyTorch: A 60 Minute Blitz](https://pytorch.org/tutorials/beginner/deep_learning_60min_blitz.html)\n", + "* [Transfer Learning for Computer Vision](https://pytorch.org/tutorials/beginner/transfer_learning_tutorial.html)" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "getting_started.ipynb", + "provenance": [] + }, + "execution": { + "timeout": 1200 + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From ac0034abb2bd106678e7dd2bdc6d2c1915393223 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Sun, 8 Dec 2024 03:34:53 +0100 Subject: [PATCH 02/35] Add Introduction to Geospatial Data tutorial (#2446) * Add Introduction to Geospatial Data tutorial * Fix several formatting issues * Try some hacks --- docs/index.rst | 1 + docs/tutorials/geospatial.ipynb | 355 ++++++++++++++++++++++++++++++++ 2 files changed, 356 insertions(+) create mode 100644 docs/tutorials/geospatial.ipynb diff --git a/docs/index.rst b/docs/index.rst index 1a8f0058e6b..e91379d6f3d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,6 +30,7 @@ torchgeo tutorials/getting_started tutorials/pytorch + tutorials/geospatial tutorials/custom_raster_dataset tutorials/transforms tutorials/indices diff --git a/docs/tutorials/geospatial.ipynb b/docs/tutorials/geospatial.ipynb new file mode 100644 index 00000000000..844ce5b8747 --- /dev/null +++ b/docs/tutorials/geospatial.ipynb @@ -0,0 +1,355 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "45973fd5-6259-4e03-9501-02ee96f3f870", + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright (c) Microsoft Corporation. All rights reserved.\n", + "# Licensed under the MIT License." + ] + }, + { + "cell_type": "markdown", + "id": "9478ed9a", + "metadata": { + "id": "NdrXRgjU7Zih" + }, + "source": [ + "# Introduction to Geospatial Data\n", + "\n", + "_Written by: Adam J. Stewart_\n", + "\n", + "In this tutorial, we introduce the challenges of working with geospatial data, especially remote sensing imagery. This is not meant to discourage practicioners, but to elucidate why existing computer vision domain libraries like torchvision are insufficient for working with multispectral satellite imagery." + ] + }, + { + "cell_type": "markdown", + "id": "4cc902a5-0a06-4b02-af47-31b124da8193", + "metadata": {}, + "source": [ + "## Common Modalities\n", + "\n", + "Geospatial data come in a wide variety of common modalities. Below, we dive into each modality and discuss what makes it unique." + ] + }, + { + "cell_type": "markdown", + "id": "7d02bf4d-e979-4d41-bf70-e1b5a73bac2f", + "metadata": {}, + "source": [ + "### Tabular data\n", + "\n", + "Many geospatial datasets, especially those collected by in-situ sensors, are distributed in tabular format. For example, imagine weather or air quality stations that distribute example data like:\n", + "\n", + "| Latitude | Longitude | Temperature | Pressure | PM$_{2.5}$ | O$_3$ | CO |\n", + "| -------: | --------: | ----------: | -------: | ---------: | ----: | -----: |\n", + "| 40.7128 | 74.0060 | 1 | 1025 | 20.0 | 4 | 473.9 |\n", + "| 37.7749 | 122.4194 | 11 | 1021 | 21.4 | 6 | 1259.5 |\n", + "| ... | ... | ... | ... | ... | ... | ... |\n", + "| 41.8781 | 87.6298 | -1 | 1024 | 14.5 | 30 | - |\n", + "| 25.7617 | 80.1918 | 17 | 1026 | 5.0 | - | - |\n", + "\n", + "This kind of data is relatively easy to load and integrate into a machine learning pipeline. The following models work well for tabular data:\n", + "\n", + "* Multi-Layer Perceptrons (MLPs): for unstructured data\n", + "* Recurrent Neural Networks (RNNs): for time-series data\n", + "* Graph Neural Networks (GNNs): for ungridded geospatial data\n", + "\n", + "Note that it is not uncommon for there to be missing values (as is the case for air pollutants in some cities) due to missing or faulty sensors. Data imputation may be required to fill in these missing values. Also make sure all values are converted to a common set of units." + ] + }, + { + "cell_type": "markdown", + "id": "b0076503-57d4-4803-b7ba-dc6b96dd5cf8", + "metadata": {}, + "source": [ + "### Multispectral\n", + "\n", + "Although traditional computer vision datasets are typically restricted to red-green-blue (RGB) images, remote sensing satellites typically capture 3–15 different spectral bands with wavelengths far outside of the visible spectrum. Mathematically speaking, each image will be formatted as:\n", + "\n", + "$$ x \\in \\mathbb{R}^{C \\times H \\times W},$$\n", + "\n", + "where:\n", + "\n", + "* $C$ is the number of spectral bands (color channels),\n", + "* $H$ is the height of each image (in pixels), and\n", + "* $W$ is the width of each image (in pixels).\n", + "\n", + "Below, we see a false-color composite created using spectral channels outside of the visible spectrum (such as near-infrared):\n", + "\n", + "
\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "03de4b08-3941-4b76-9eb1-11a57a7c9684", + "metadata": {}, + "source": [ + "### Hyperspectral\n", + "\n", + "While multispectral images are often limited to 3–15 disjoint spectral bands, hyperspectral sensors capture hundreds of spectral bands to approximate the continuous color spectrum. These images often present a particular challenge to convolutional neural networks (CNNs) due to the sheer data volume, and require either small image patches (decreased $H$ and $W$) or dimensionality reduction (decreased $C$) in order to avoid out-of-memory errors on the GPU.\n", + "\n", + "Below, we see a hyperspectral data cube, with each color channel visualized along the $z$-axis:\n", + "\n", + "
\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "20c83407-0496-45af-b0e0-2c615b0d9a03", + "metadata": {}, + "source": [ + "### Radar\n", + "\n", + "Passive sensors (ones that do not emit light) are limited by daylight hours and cloud-free conditions. Active sensors such as radar emit polarized microwave pulses and measure the time it takes for the signal to reflect or scatter off of objects. This allows radar satellites to operate at night and in adverse weather conditions. The images captured by these sensors are stored as complex numbers, with a real (amplitude) and imaginary (phase) component, making it difficult to integrate them into machine learning pipelines.\n", + "\n", + "Radar is commonly used in meteorology (Doppler radar) and geophysics (ground penetrating radar). By attaching a radar antenna to a moving satellite, a larger effective aperature is created, increasing the spatial resolution of the captured image. This technique is known as synthetic aperature radar (SAR), and has many common applications in geodesy, flood mapping, and glaciology. Finally, by comparing the phases of multiple SAR snapshots of a single location at different times, we can analyze minute changes in surface elevation, in a technique known as Interferometric Synthetic Aperature Radar (InSAR). Below, we see an interferogram of earthquake deformation:\n", + "\n", + "
\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "53b27a38-64cc-43e7-91da-1244fb9dd416", + "metadata": {}, + "source": [ + "### Lidar\n", + "\n", + "Similar to radar, lidar is another active remote sensing method that replaces microwave pulses with lasers. By measuring the time it takes light to reflect off of an object and return to the sensor, we can generate a 3D point cloud mapping object structures. Mathematically, our dataset would then become:\n", + "\n", + "$$\\mathcal{D} = \\left\\{\\left(x^{(i)}, y^{(i)}, z^{(i)}\\right)\\right\\}_{i=1}^N$$\n", + "\n", + "This technology is frequently used in several different application domains:\n", + "\n", + "* Meteorology: clouds, aerosols\n", + "* Geodesy: surveying, archaeology\n", + "* Forestry: tree height, biomass density\n", + "\n", + "Below, we see a 3D point cloud captured for a city:\n", + "\n", + "
\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "da27ea73-2e3d-43d8-ba0b-cdac92ab2f81", + "metadata": {}, + "source": [ + "## Resolution\n", + "\n", + "Remote sensing data comes in a number of spatial, temporal, and spectral resolutions.\n", + "\n", + "
\n", + "Warning: In computer vision, resolution usually refers to the dimensions of an image (in pixels). In remote sensing, resolution instead refers to the dimensions of each pixel (in meters). Throughout this tutorial, we will use the latter definition unless otherwise specified.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "bcd51946-62eb-44db-bf3b-bd2df5308ab6", + "metadata": {}, + "source": [ + "### Spatial resolution\n", + "\n", + "Choosing the right data for your application is often controlled by the resolution of the imagery. Spatial resolution, also called ground sample distance (GSD), is the size of each pixel as measured on the Earth's surface. While the exact definitions change as satellites become better, approximate ranges of resolution include:\n", + "\n", + "| Category | Resolution | Examples |\n", + "| -------: | ---------: | :------: |\n", + "| Low resolution | > 30 m | MODIS (250 m–1 km), GOES-16 (500 m–2 km) |\n", + "| Medium resolution | 5–30 m | Sentinel-2 (10–60 m), Landsat-9 (15–100 m) |\n", + "| High resolution | 1–5 m | Planet Dove (3–5 m), RapidEye (5 m) |\n", + "| Very high resolution | < 1 m | Maxar WorldView-3 (0.3 m), QuickBird (0.6 m) |\n", + "\n", + "It is not uncommon for a single sensor to capture high resolution panchromatic bands, medium resolution visible bands, and low resolution thermal bands. It is also possible for pixels to be non-square, as is the case for OCO-2. All bands must be resampled to the same resolution for use in machine learning pipelines." + ] + }, + { + "cell_type": "markdown", + "id": "bce349d9-8b5e-48e4-800a-c2fbb1c343cb", + "metadata": {}, + "source": [ + "### Temporal resolution\n", + "\n", + "For time-series applications, it is also important to think about the repeat period of the satellite you want to use. Depending on the orbit of the satellite, imagery can be anywhere from biweekly (for polar, sun-synchronous orbits) to continuous (for geostationary orbits). The latter is common for global Earth observation missions, while the latter is common for weather and communications satellites. Below, we see an illustration of a geostationary orbit:\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "Due to partial overlap in orbit paths and intermittent cloud cover, satellite image time series (SITS) are often of irregular length and irregular spacing. This can be especially challenging for naïve time-series models to handle." + ] + }, + { + "cell_type": "markdown", + "id": "6c278653-d14b-44c8-971b-9851b7515b0f", + "metadata": {}, + "source": [ + "### Spectral resolution\n", + "\n", + "It is also important to consider the spectral resolution of a sensor, including both the number of spectral bands and the bandwidth that is captured. Different downstream applications require different spectral bands, and there is often a tradeoff between additional spectral bands and higher spatial resolution. The following figure compares the wavelengths captured by sensors onboard different satellites:\n", + "\n", + "
\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "c79a050a-5389-4fb4-8501-3f1be067a166", + "metadata": {}, + "source": [ + "## Preprocessing\n", + "\n", + "Geospatial data also has unique preprocessing requirements that necessitate experience working with a variety of tools like GDAL, the geospatial data abstraction library. GDAL support ~160 raster drivers and ~80 vector drivers, allowing users to reproject, resample, and rasterize data from a variety of specialty file formats." + ] + }, + { + "cell_type": "markdown", + "id": "5cadbfeb-40da-455f-8b9a-bb5a983aaa8b", + "metadata": {}, + "source": [ + "### Reprojection\n", + "\n", + "The Earth is three dimensional, but images are two dimensional. This requires a *projection* to map the 3D surface onto a 2D image, and a *coordinate reference system* (CRS) to map each point back to a specific latitude/longitude. Below, we see examples of a few common projections:\n", + "\n", + "
\n", + "
\n", + " \n", + "
Mercator
\n", + "
\n", + "
\n", + "\n", + "
\n", + "
\n", + " \n", + "
Albers Equal Area
\n", + "
\n", + "
\n", + "\n", + "
\n", + "
\n", + " \n", + "
Interrupted Goode Homolosine
\n", + "
\n", + "
\n", + "\n", + "There are literally thousands of different projections out there, and every dataset (or even different images within a single dataset) can have different projections. Even if you correctly georeference images during indexing, if you forget to project them to a common CRS, you can end up with rotated images with nodata values around them, and the images will not be pixel-aligned.\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "We can use a command like:\n", + "\n", + "```\n", + "$ gdalwarp -s_srs EPSG:5070 -t_srs EPSG:4326 src.tif dst.tif\n", + "```\n", + "\n", + "to reproject a file from one CRS to another." + ] + }, + { + "cell_type": "markdown", + "id": "b0362b1e-1c47-4884-a414-699db82acb6e", + "metadata": {}, + "source": [ + "### Resampling\n", + "\n", + "As previously mentioned, each dataset may have its own unique spatial resolution, and even separate bands (channels) in a single image may have different resolutions. All data (including input images and target masks for semantic segmentation) must be resampled to the same resolution. This can be done using GDAL like so:\n", + "\n", + "```\n", + "$ gdalwarp -tr 30 30 src.tif dst.tif\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "ddbd742a-11b4-4fb9-a27a-58f5f14e2982", + "metadata": {}, + "source": [ + "Just because two files have the same resolution does not mean that they have *target-aligned pixels* (TAP). Our goal is that every input pixel is perfectly aligned with every expected output pixel, but differences in geolocation can result in masks that are offset by half a pixel from the input image. We can ensure TAP by adding the `-tap` flag:\n", + "\n", + "```\n", + "$ gdalwarp -tr 30 30 -tap src.tif dst.tif\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "c0c499d9-e266-4619-863f-e416cc823c58", + "metadata": {}, + "source": [ + "### Rasterization\n", + "\n", + "Not all geospatial data is raster data. Many files come in vector format, including points, lines, and polygons.\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "Of course, semantic segmentation requires these polygon masks to be converted to raster masks. This process is called rasterization, and can be performed like so:\n", + "\n", + "```\n", + "$ gdal_rasterize -tr 30 30 -a BUILDING_HEIGHT -l buildings buildings.shp buildings.tif\n", + "```\n", + "\n", + "Above, we set the resolution to 30 m/pixel and use the `BUILDING_HEIGHT` attribute of the `buildings` layer as the burn-in value.\n" + ] + }, + { + "cell_type": "markdown", + "id": "a3acc64e-8dc0-46b4-a677-ecb9723d4f56", + "metadata": {}, + "source": [ + "## Additional Reading\n", + "\n", + "Luckily, TorchGeo can handle most preprocessing for us. If you would like to learn more about working with geospatial data, including how to manually do the above tasks, the following additional reading may be useful:\n", + "\n", + "* [GDAL documentation](https://gdal.org/en/stable/index.html)\n", + "* [rasterio documentation](https://rasterio.readthedocs.io/en/stable/index.html)\n", + "* [Guide to GeoTIFF compression and optimization with GDAL](https://kokoalberti.com/articles/geotiff-compression-optimization-guide/)\n", + "* [A survival guide to Landsat preprocessing](https://doi.org/10.1002/ecy.1730)" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "getting_started.ipynb", + "provenance": [] + }, + "execution": { + "timeout": 1200 + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 53254d2958790757ae5cfd9956324f3a868ae168 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Sun, 8 Dec 2024 04:48:14 +0100 Subject: [PATCH 03/35] Docs: update alternatives (#2437) * Docs: update alternatives * markdown -> markup * DeepForest: +1 weight --- docs/user/alternatives.rst | 4 ++-- docs/user/metrics/downloads.csv | 18 +++++++++--------- docs/user/metrics/features.csv | 8 ++++---- docs/user/metrics/github.csv | 18 +++++++++--------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/docs/user/alternatives.rst b/docs/user/alternatives.rst index a5636ef5b5d..dd241f55f1c 100644 --- a/docs/user/alternatives.rst +++ b/docs/user/alternatives.rst @@ -13,7 +13,7 @@ When deciding which library is most useful to you, it is worth considering the f Software is a living, breathing organism and is constantly undergoing change. If any of the above information is incorrect or out of date, or if you want to add a new project to this list, please open a PR! - *Last updated: 28 August 2024* + *Last updated: 30 November 2024* Features -------- @@ -75,7 +75,7 @@ These are metrics that can be scraped from GitHub. **Commits**: The number of commits on the main development branch. This is another metric for how active development has been. However, this can vary a lot depending on whether PRs are merged with or without squashing first. -**Core SLOCs**: The number of source lines of code in the core library, excluding empty lines and comments. This tells you how large the library is, and how long it would take someone to write something like it themselves. We use `scc `_ to compute SLOCs and exclude markdown languages from the count. +**Core SLOCs**: The number of source lines of code in the core library, excluding empty lines and comments. This tells you how large the library is, and how long it would take someone to write something like it themselves. We use `scc `_ to compute SLOCs and exclude markup languages from the count. **Test SLOCs**: The number of source lines of code in the testing suite, excluding empty lines and comments. This tells you how well tested the project is. A good goal to strive for is a similar amount of code for testing as there is in the core library itself. diff --git a/docs/user/metrics/downloads.csv b/docs/user/metrics/downloads.csv index 65e9a7eac3c..c7049b95d89 100644 --- a/docs/user/metrics/downloads.csv +++ b/docs/user/metrics/downloads.csv @@ -1,10 +1,10 @@ Library,PyPI/CRAN Last Week,PyPI/CRAN Last Month,PyPI/CRAN All Time,Conda All Time,Total All Time -`TorchGeo`_,"1,828","9,789","255,293","21,108","276,401" -`eo-learn`_,319,"1,560","141,983","36,205","178,188" -`Raster Vision`_,138,"652","61,938","3,254","65,192" -`PaddleRS`_,10,36,"1,642",0,"1,642" -`segment-geospatial`_,"1,553","7,363","117,664","18,147","135,811" -`DeepForest`_,564,"3,652","761,520","62,869","824,389" -`SITS`_,304,648,"12,767","78,976","91,743" -`TerraTorch`_,259,988,"2,378",0,"2,378" -`scikit-eo`_,162,621,"12,048",0,"12,048" \ No newline at end of file +`TorchGeo`_,"8,435","30,948","311,897","25,174","337,071" +`eo-learn`_,309,"2,370","156,309","40,325","196,634" +`Raster Vision`_,"9,198","31,588","115,670","3,968","119,638" +`PaddleRS`_,16,53,"2,029",0,"2,029" +`segment-geospatial`_,"1,956","10,689","157,443","26,576","184,019" +`DeepForest`_,767,"13,925","827,339","71,367","898,706" +`TerraTorch`_,318,"1,322","7,037",0,"7,037" +`SITS`_,120,539,"14,618","78,976","91,743" +`scikit-eo`_,115,717,"14,700",0,"14,700" diff --git a/docs/user/metrics/features.csv b/docs/user/metrics/features.csv index e782921faa8..67e26cd710e 100644 --- a/docs/user/metrics/features.csv +++ b/docs/user/metrics/features.csv @@ -1,10 +1,10 @@ Library,ML Backend,I/O Backend,Spatial Backend,Transform Backend,Datasets,Weights,CLI,Reprojection,STAC,Time-Series -`TorchGeo`_,PyTorch,"GDAL, h5py, laspy, OpenCV, pandas, pillow, scipy",R-tree,Kornia,82,68,✅,✅,❌,🚧 +`TorchGeo`_,PyTorch,"GDAL, h5py, laspy, OpenCV, pandas, pillow, scipy",R-tree,Kornia,92,69,✅,✅,❌,🚧 `eo-learn`_,scikit-learn,"GDAL, OpenCV, pandas",geopandas,numpy,0,0,❌,✅,❌,🚧 `Raster Vision`_,"PyTorch, TensorFlow*","GDAL, OpenCV, pandas, pillow, scipy, xarray",STAC,Albumentations,0,6,✅,✅,✅,✅ `PaddleRS`_,PaddlePaddle,"GDAL, OpenCV",shapely,numpy,7,14,🚧,✅,❌,🚧 `segment-geospatial`_,PyTorch,"GDAL, OpenCV, pandas",geopandas,numpy,0,0,❌,✅,❌,❌ -`DeepForest`_,PyTorch,"GDAL, OpenCV, pandas, pillow, scipy",R-tree,Albumentations,0,3,❌,❌,❌,❌ +`DeepForest`_,PyTorch,"GDAL, OpenCV, pandas, pillow, scipy",R-tree,Albumentations,0,4,❌,❌,❌,❌ +`TerraTorch`_,PyTorch,"GDAL, h5py, pandas, xarray",R-tree,Albumentations,22,1,✅,✅,❌,🚧 `SITS`_,R Torch,GDAL,-,tidyverse,22,0,❌,✅,✅,✅ -`TerraTorch`_,PyTorch,"GDAL, h5py, pandas, xarray",R-tree,Albumentations,16,1,✅,✅,❌,🚧 -`scikit-eo`_,"scikit-learn, TensorFlow","pandas, scipy, numpy, rasterio","geopandas",numpy,0,0,❌,❌,❌,🚧 \ No newline at end of file +`scikit-eo`_,"scikit-learn, TensorFlow","pandas, scipy, numpy, rasterio","geopandas",numpy,0,0,❌,❌,❌,🚧 diff --git a/docs/user/metrics/github.csv b/docs/user/metrics/github.csv index bac263c4d86..96c678e6c3e 100644 --- a/docs/user/metrics/github.csv +++ b/docs/user/metrics/github.csv @@ -1,10 +1,10 @@ Library,Contributors,Forks,Watchers,Stars,Issues,PRs,Releases,Commits,Core SLOCs,Test SLOCs,Test Coverage,License -`TorchGeo`_,72,308,44,"2,409",419,"1,714",11,"2,074","30,761","16,058",100%,MIT -`eo-learn`_,40,300,46,"1,108",159,638,44,"2,470","8,207","5,932",92%,MIT -`Raster Vision`_,32,381,71,"2,046",697,"1,382",22,"3,614","22,779","9,429",90%,Apache-2.0 -`PaddleRS`_,23,89,13,374,91,116,3,644,"21,859","3,384",48%,Apache-2.0 -`segment-geospatial`_,17,281,55,"2,834",129,104,27,186,"5,598",92,22%,MIT -`DeepForest`_,17,172,17,474,413,301,44,864,"3,357","1,794",86%,MIT -`SITS`_,14,76,28,451,622,583,44,"6,244","24,284","8,697",94%,GPL-2.0 -`TerraTorch`_,9,10,9,121,46,92,2,243,"10,101",583,44%,Apache-2.0 -`scikit-eo`_,6,17,8,132,20,11,15,496,"1,636","94",37%,Apache-2.0 \ No newline at end of file +`TorchGeo`_,76,352,51,"2,790",446,"1,860",13,"2,193","29,305","17,294",100%,MIT +`eo-learn`_,40,299,46,"1,131",160,640,45,"2,472","7,497","5,872",92%,MIT +`Raster Vision`_,32,388,71,"2,090",701,"1,430",23,"3,614","21,734","8,792",90%,Apache-2.0 +`PaddleRS`_,23,91,13,400,93,116,3,644,"20,679","3,239",48%,Apache-2.0 +`segment-geospatial`_,20,316,61,"3,078",150,136,38,229,"6,845",92,22%,MIT +`DeepForest`_,17,176,17,524,439,351,47,938,"3,320","1,886",86%,MIT +`TerraTorch`_,16,24,12,171,78,185,8,606,"14,933","2,077",44%,Apache-2.0 +`SITS`_,14,78,28,483,654,590,44,"6,244","24,284","8,697",94%,GPL-2.0 +`scikit-eo`_,7,20,9,192,24,13,17,510,"1,617","170",37%,Apache-2.0 From 6109fea550d732adb0d93865c77de2bc91bccc69 Mon Sep 17 00:00:00 2001 From: Heng Fang Date: Sun, 8 Dec 2024 14:15:53 +0100 Subject: [PATCH 04/35] Update tutorial for transforms and pretrained weights (#2454) * update tutorial for transforms * update tutorial for pretrained_weights * fix tutorial of transforms * solve Adam's reviews * Change to original author * Change to original author, add more readings * Code formatting --------- Co-authored-by: Adam J. Stewart --- docs/tutorials/pretrained_weights.ipynb | 32 +++++++++++----- docs/tutorials/transforms.ipynb | 51 ++++++++++++++++++++----- 2 files changed, 65 insertions(+), 18 deletions(-) diff --git a/docs/tutorials/pretrained_weights.ipynb b/docs/tutorials/pretrained_weights.ipynb index 0c2c4a0fc48..3a7023f0ee9 100644 --- a/docs/tutorials/pretrained_weights.ipynb +++ b/docs/tutorials/pretrained_weights.ipynb @@ -1,14 +1,15 @@ { "cells": [ { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": { "id": "p63J-QmUrMN-" }, + "outputs": [], "source": [ - "Copyright (c) Microsoft Corporation. All rights reserved.\n", - "\n", - "Licensed under the MIT License." + "# Copyright (c) Microsoft Corporation. All rights reserved.\n", + "# Licensed under the MIT License." ] }, { @@ -19,6 +20,8 @@ "source": [ "# Pretrained Weights\n", "\n", + "_Written by: Nils Lehmann_\n", + "\n", "In this tutorial, we demonstrate some available pretrained weights in TorchGeo. The implementation follows torchvisions' recently introduced [Multi-Weight API](https://pytorch.org/blog/introducing-torchvision-new-multi-weight-support-api/). We will use the [EuroSAT](https://torchgeo.readthedocs.io/en/stable/api/datasets.html#eurosat) dataset throughout this tutorial. Specifically, a subset containing only 100 images.\n", "\n", "It's recommended to run this notebook on Google Colab if you don't have your own GPU. Click the \"Open in Colab\" button above to get started." @@ -147,9 +150,11 @@ "source": [ "## Weights\n", "\n", - "Available pretrained weights are listed on the model documentation [page](https://torchgeo.readthedocs.io/en/stable/api/models.html). While some weights only accept RGB channel input, some weights have been pretrained on Sentinel 2 imagery with 13 input channels and can hence prove useful for transfer learning tasks involving Sentinel 2 data.\n", + "Pretrained weights for `torchgeo.models` are available and sorted by satellite or sensor type: sensor-agnostic, Landsat, NAIP, Sentinel-1, and Sentinel-2. Refer to the [model documentation](https://torchgeo.readthedocs.io/en/stable/api/models.html#pretrained-weights) for a complete list of weights. Choose from the provided pre-trained weights based on your specific use case.\n", + "\n", + "While some weights only accept RGB channel input, some weights have been pretrained on Sentinel-2 imagery with 13 input channels and can hence prove useful for transfer learning tasks involving Sentinel-2 data.\n", "\n", - "To access these weights you can do the following:" + "To use these weights, you can load them as follows:" ] }, { @@ -169,7 +174,16 @@ "id": "EIpnXuXgrMOM" }, "source": [ - "This set of weights is a torchvision `WeightEnum` and holds information such as the download url link or additional meta data. TorchGeo takes care of the downloading and initialization of models with a desired set of weights. Given that EuroSAT is a classification dataset, we can use a `ClassificationTask` object that holds the model and optimizer object as well as the training logic." + "This set of weights is a torchvision `WeightEnum` and holds information such as the download url link or additional meta data. TorchGeo takes care of the downloading and initialization of models with a desired set of weights. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`torchgeo.trainers` provides specialized task classes that simplify training workflows for common geospatial tasks. Depending on your objective, you can select the appropriate trainer class, such as `ClassificationTask` for classification, `SemanticSegmentationTask` for semantic segmentation, or other task-specific trainers. Check the [trainers documentation](https://torchgeo.readthedocs.io/en/stable/api/trainers.html) for more information.\n", + "\n", + "Given that EuroSAT is a classification dataset, we can use a `ClassificationTask` object that holds the model and optimizer as well as the training logic." ] }, { @@ -495,7 +509,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.13.0" }, "vscode": { "interpreter": { @@ -504,5 +518,5 @@ } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 4 } diff --git a/docs/tutorials/transforms.ipynb b/docs/tutorials/transforms.ipynb index 38a9b825bbf..42a16e26e24 100644 --- a/docs/tutorials/transforms.ipynb +++ b/docs/tutorials/transforms.ipynb @@ -1,14 +1,15 @@ { "cells": [ { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": { "id": "DYndcZst_kdr" }, + "outputs": [], "source": [ - "Copyright (c) Microsoft Corporation. All rights reserved.\n", - "\n", - "Licensed under the MIT License." + "# Copyright (c) Microsoft Corporation. All rights reserved.\n", + "# Licensed under the MIT License." ] }, { @@ -20,6 +21,13 @@ "# Transforms" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "_Written by: Isaac A. Corley_" + ] + }, { "cell_type": "markdown", "metadata": { @@ -408,7 +416,7 @@ "id": "p28C8cTGE3dP" }, "source": [ - "Transforms are able to operate across batches of samples and singular samples. This allows them to be used inside the dataset itself or externally, chained together with other transform operations using `nn.Sequential`. " + "`torchgeo.transforms` work seamlessly with both singular samples and batches of data. They can be applied within datasets or externally and combined with other transforms using `nn.Sequential`. Built for multispectral imagery, they are fully compatible with `torchvision.transforms` and `kornia.augmentation`." ] }, { @@ -429,13 +437,24 @@ "print(x.dtype, x.min(), x.max())" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Appending Indices\n", + "\n", + "`torchgeo.transforms` support appending indices to a specified channel dimension.\n", + "\n", + "For detailed usage of all available transforms, refer to the [transforms documentation](https://torchgeo.readthedocs.io/en/stable/api/transforms.html)." + ] + }, { "cell_type": "markdown", "metadata": { "id": "KRjb-u0EEmDf" }, "source": [ - "Indices can also be computed on batches of images and appended as an additional band to the specified channel dimension. Notice how the number of channels increases from 13 -> 14." + "The following example shows how indices can be computed on batches of images and appended as an additional band to the specified channel dimension. Notice how the number of channels increases from 13 -> 14." ] }, { @@ -500,7 +519,9 @@ "id": "w4ZbjxPyHoiB" }, "source": [ - "It's even possible to chain indices along with augmentations from Kornia for a single callable during training." + "It's even possible to chain indices along with augmentations from Kornia for a single callable during training.\n", + "\n", + "When using Kornia with a dictionary input, you must explicitly set `data_keys=None` during the creation of the augmentation pipeline." ] }, { @@ -689,6 +710,18 @@ "print(f\"Class Label: {dataset.classes[sample['label']]}\")\n", "image.resize((256, 256), resample=Image.BILINEAR)" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Additional Reading\n", + "\n", + "To learn more about preprocessing and data augmentation transforms, the following external resources may be helpful:\n", + "\n", + "* [Kornia augmentations](https://kornia.readthedocs.io/en/latest/augmentation.html)\n", + "* [torchvision transforms](https://pytorch.org/vision/main/transforms.html)" + ] } ], "metadata": { @@ -717,9 +750,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.13.0" } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 4 } From 04483d717d81299c23873f858d0ca1f7140ab3ed Mon Sep 17 00:00:00 2001 From: Nils Lehmann <35272119+nilsleh@users.noreply.github.com> Date: Sun, 8 Dec 2024 15:12:15 +0100 Subject: [PATCH 05/35] Add contribution tutorial for Non-Geo Datasets (#2451) * add non-geodataset tutorial * add to index * Convert markdown code to code blocks --------- Co-authored-by: Adam J. Stewart --- docs/index.rst | 1 + .../contribute_non_geo_dataset.ipynb | 485 ++++++++++++++++++ 2 files changed, 486 insertions(+) create mode 100644 docs/tutorials/contribute_non_geo_dataset.ipynb diff --git a/docs/index.rst b/docs/index.rst index e91379d6f3d..b9693ec905d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -31,6 +31,7 @@ torchgeo tutorials/getting_started tutorials/pytorch tutorials/geospatial + tutorials/contribute_non_geo_dataset tutorials/custom_raster_dataset tutorials/transforms tutorials/indices diff --git a/docs/tutorials/contribute_non_geo_dataset.ipynb b/docs/tutorials/contribute_non_geo_dataset.ipynb new file mode 100644 index 00000000000..655deb1c8d7 --- /dev/null +++ b/docs/tutorials/contribute_non_geo_dataset.ipynb @@ -0,0 +1,485 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright (c) Microsoft Corporation. All rights reserved.\n", + "# Licensed under the MIT License." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Contribute a New Non-Geospatial Dataset\n", + "\n", + "_Written by: Nils Lehmann_\n", + "\n", + "Open-source datasets have significantly accelerated machine learning research. Geospatial machine learning datasets can be particularly complex to work with compared to more standard RGB-based vision datasets. To spare the community from having to repeatly implement data loading logic over and over, TorchGeo provides dozens of built-in datasets such that they can be downloaded and ready for use in a PyTorch framework with a single line of code. This tutorial will show how you can add a new non-geospatial dataset to this growing collection. \n", + "\n", + "As a reminder, TorchGeo differentiates between two types of datasets: geospatial and non-geospatial datasets. Non-geospatial datasets are integer indexed, like the datasets one might be familar with from torchvision, while geospatial datasets are indexed via spatiotemporal bounding boxes. Non-geospatial datasets can still return geospatial and other metadata and should be specific to the remote sensing domain. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First, we install TorchGeo and its dependencies." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%pip install torchgeo" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Where to start\n", + "\n", + "There are many types of remote sensing datasets. [Satellite-Image-Deep-Learning](https://github.com/satellite-image-deep-learning/datasets) maintains a list of many of these datasets, as well as links to other similar curated lists.\n", + "\n", + "Two aspects that will make it a lot easier to add the dataset are whether or not the dataset can be easily downloaded and whether or the dataset comes with a Github repository and publication that outlines how the authors intend the dataset to be used. These are not necessariy criteria, and sometimes it might be even more worthwhile to add a dataset without an existing code base, precisely because the marginal contribution to the community might be greater since a use of the dataset does not necessitate writing the loading implementation from scratch." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adding the dataset" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once you have identified a dataset that you would like to add to TorchGeo, you could identify in what application category it might roughly fall in. For example, a segmentation dataset based on a collection of *.png* files, versus a classification dataset based on pre-defined image chips in *.tif* files. In the later case, if you find that the dataset contains *.tif* files that have very large pixel sizes, such that loading a single file might be costly, consider adding the dataset as a geospatial dataset for easier indexing. Once, you have identified the \"task\" such as segmentation vs classification and the dataset format, see whether a dataset of the same or similar category exists in TorchGeo already. All datasets inherit from a `NonGeoDataset` or `GeoDataset` base class that provides an outline for the implementation logic as well as additional utility functions that should be reused. This reduces code duplication and makes it easier to unit test datasets.\n", + "\n", + "Adding a dataset to TorchGeo consists of roughly four steps:\n", + "\n", + "1. a `dataset_name.py` file itself that implements the logic of the dataset\n", + "2. a `data.py` file that creates dummy data in the same structure and format as the original dataset for unit tests\n", + "3. a `test_dataset_name.py` file that implements unit tests for the dataset\n", + "4. an entry to the documentation page files: `non_geo_datasets.csv` and `datasets.rst`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The `dataset_name.py` file" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This file implements the logic to load a sample from the dataset as well as downloading the dataset automatically if possible. The new dataset inherits from a base class and the documentation string (docstring) of the class should contain:\n", + "\n", + "* a short summary of the dataset\n", + "* outline the features, such as the task the dataset is designed to solve\n", + "* outline the format the dataset comes in, e.g., file types, pixel dimensions, etc.\n", + "* a proper reference to the dataset such as a link to the paper so users can adequately cite the dataset when using it\n", + "* if required, a note about additional dependencies that are not part of TorchGeo's required dependencies\n", + "\n", + "The dataset implementation itself should contain:\n", + "\n", + "* a method to create an index structure the dataset can iterate over to load samples. This index structure also defines the length (`__len__`) of the dataset, i.e. how many individual samples can be loaded from the dataset\n", + "* a `__getitem__` method that takes an integer index argument, loads a sample of the dataset, and returns its components in a dictionary\n", + "* a `_verify` method that checks whether the dataset can be found on the filesystem, has already been downloaded and only needs to be extracted, or downloads and extracts the dataset from the web\n", + "* a `plot` method that can visually display a single sample of the dataset\n", + "\n", + "The code below attempts to roughly outline the parts required for a new `NonGeoDataset`. Specifics are of course very dependent on the type of dataset you want to add, but this template and other existing datasets should give you a decent starting point." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from collections.abc import Callable\n", + "\n", + "from matplotlib.pyplot import Figure\n", + "from torch import Tensor\n", + "\n", + "from torchgeo.datasets import NonGeoDataset\n", + "from torchgeo.datasets.utils import Path\n", + "\n", + "\n", + "class MyNewDataset(NonGeoDataset):\n", + " \"\"\"MyNewDataset.\n", + "\n", + " Short summary of the dataset and link to its homepage.\n", + "\n", + " Dataset features:\n", + "\n", + " * number of classes\n", + " * sensors\n", + " * area covered\n", + " * etc.\n", + "\n", + " Dataset format:\n", + "\n", + " * what file format and shape the input data comes in\n", + " * what file format and shape the target data comes in\n", + " * possible metadata files\n", + "\n", + " If you use this dataset in your research, please cite the following paper:\n", + "\n", + " * URL of publication or citation information\n", + "\n", + " .. versionadded: next TorchGeo minor release version, e.g., 1.0\n", + " \"\"\"\n", + "\n", + " # In this part of the code you can define class attributes such as a list of\n", + " # class names, color maps, url and checksums for data download, and other\n", + " # attributes that one might require repeatedly in the subsequent class methods.\n", + "\n", + " def __init__(\n", + " self,\n", + " root: Path = 'data',\n", + " split: str = 'train',\n", + " transforms: Callable[[dict[str, Tensor]], dict[str, Tensor]] | None = None,\n", + " download: bool = False,\n", + " ) -> None:\n", + " \"\"\"Initialize the dataset.\n", + "\n", + " The init parameters can include additional arguments, such as an option to\n", + " select specific image bands, data modalities, or other arguments that give\n", + " greater control over data loading. They should all have reasonable defaults.\n", + "\n", + " Args:\n", + " root: root directory where dataset can be found\n", + " split: one of \"train\", \"val\", or \"test\"\n", + " transforms: a function/transform that takes input sample and its target as\n", + " entry and returns a transformed version\n", + " download: if True, download dataset and store it in the root directory\n", + " \"\"\"\n", + "\n", + " def __len__(self) -> int:\n", + " \"\"\"The length of the dataset.\n", + "\n", + " This is the total number of samples per epoch, and is used to define the\n", + " maximum allow index that can be passed to `__getitem__`.\n", + " \"\"\"\n", + "\n", + " def __getitem__(self, index: int) -> dict[str, Tensor]:\n", + " \"\"\"A single sample from the dataset.\n", + "\n", + " Load a single input image and target label or mask, and return it in a\n", + " dictionary.\n", + " \"\"\"\n", + "\n", + " def plot(self) -> Figure:\n", + " \"\"\"Plot a sample of the dataset for visualization purposes.\n", + "\n", + " This might involve selecting the RGB bands, using a colormap to display a mask,\n", + " adding a legend with class labels, etc.\n", + " \"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The `data.py` file" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `data.py` file is placed under `tests/data/dataset_name/` directory and creates a smaller dummy dataset that replicates the features and formats of the actual full datasets for unit tests. This is needed to keep the tests fast (we don't have time or storage space to download the real dataset) and to comply with the dataset license. \n", + "\n", + "The script should:\n", + "\n", + "* replicate the directory structure and file names\n", + "* replicate the file format, data type, and range of values\n", + "* use the same compression scheme to simulate downloading the dataset\n", + "\n", + "This is usually highly dependent on the dataset format and structure the new dataset comes in. You should always look for a similar dataset first and use that as a reference. However, below is an outline of the usual building blocks of a `data.py` script, for example an image segmentation dataset with 10 classes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import shutil\n", + "\n", + "import numpy as np\n", + "from PIL import Image\n", + "\n", + "# Define the root directory and subdirectories\n", + "root_dir = 'my_new_dataset'\n", + "sub_dirs = ['sub_dir_1', 'sub_dir_2', 'sub_dir_3']\n", + "splits = ['train', 'val', 'test']\n", + "\n", + "image_file_names = ['sample_1.png', 'sample_2.png', 'sample_3.png']\n", + "\n", + "IMG_SIZE = 32\n", + "\n", + "\n", + "# Function to create dummy input images\n", + "def create_input_image(path: str, shape: tuple[int], pixel_values: list[int]) -> None:\n", + " data = np.random.choice(pixel_values, size=shape, replace=True).astype(np.uint8)\n", + " img = Image.fromarray(data)\n", + " img.save(path)\n", + "\n", + "\n", + "# Function to create dummy targets\n", + "def create_target_images(split: str, filename: str) -> None:\n", + " target_pixel_values = range(10)\n", + " path = os.path.join(root_dir, 'target', split, filename)\n", + " create_input_image(path, (IMG_SIZE, IMG_SIZE), target_pixel_values)\n", + "\n", + "\n", + "# Create a new clean version when re-running the script\n", + "if os.path.exists(root_dir):\n", + " shutil.rmtree(root_dir)\n", + "\n", + "# Create the directory structure\n", + "for sub_dir in sub_dirs:\n", + " for split in splits:\n", + " os.makedirs(os.path.join(root_dir, sub_dir, split), exist_ok=True)\n", + "\n", + "# Create dummy data for all splits and filenames\n", + "for split in splits:\n", + " for filename in image_file_names:\n", + " create_input_image(split, filename)\n", + " create_target_images(split, filename.replace('_', '_target_'))\n", + "\n", + "# Zip directory\n", + "shutil.make_archive(root_dir, 'zip', '.', root_dir)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The `test_dataset_name.py` file" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `test_dataset_name.py` file is placed under the `tests/datasets/` directory. This file implements the unit tests for the dataset, such that every line of code in `dataset_name.py` is tested. The logic of the individual test cases will likely be very similar to existing test files so you can look at those to to see how you can test the individual parts of the dataset logic." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import shutil\n", + "from pathlib import Path\n", + "\n", + "import pytest\n", + "import torch\n", + "import torch.nn as nn\n", + "from _pytest.fixtures import SubRequest\n", + "from matplotlib import pyplot as plt\n", + "from pytest import MonkeyPatch\n", + "\n", + "from torchgeo.datasets import DatasetNotFoundError\n", + "\n", + "\n", + "def download_url(url: str, root: str | Path, *args: str, **kwargs: str) -> None:\n", + " shutil.copy(url, root)\n", + "\n", + "\n", + "class TestMyNewDataset:\n", + " # pytest fixtures can be used to define variables to test different argument\n", + " # configurations to test, for example the different splits of the dataset\n", + " # or subselection of modalities/bands\n", + " @pytest.fixture(params=['train', 'val', 'test'])\n", + " def dataset(\n", + " self, monkeypatch: MonkeyPatch, tmp_path: Path, request: SubRequest\n", + " ) -> MyNewDataset:\n", + " # monkeypatch can overwrite the class attributes defined above the __init__\n", + " # method and use the specific unit tests settings to mock behavior\n", + "\n", + " split: str = request.param\n", + " transforms = nn.Identity()\n", + " return MyNewDataset(tmp_path, split=split, transforms=transforms, download=True)\n", + "\n", + " def test_getitem(self, dataset: MyNewDataset) -> None:\n", + " # Retrieve a sample and check some of the desired properties\n", + " x = dataset[0]\n", + " assert isinstance(x, dict)\n", + " assert isinstance(x['image'], torch.Tensor)\n", + " assert isinstance(x['label'], torch.Tensor)\n", + "\n", + " # For all additional class arguments, check behavior for invalid parameters\n", + " def test_invalid_split(self) -> None:\n", + " with pytest.raises(AssertionError):\n", + " MyNewDataset(foo='bar')\n", + "\n", + " # Test the length of the dataset, this should coincide with the dummy data\n", + " def test_len(self, dataset: MyNewDataset) -> None:\n", + " assert len(dataset) == 2\n", + "\n", + " # Test the logic when the dataset is already downloaded\n", + " def test_already_downloaded(self, dataset: MyNewDataset, tmp_path: Path) -> None:\n", + " MyNewDataset(root=tmp_path, download=True)\n", + "\n", + " # Test the logic when the dataset is already downloaded but not extracted\n", + " def test_already_downloaded_not_extracted(\n", + " self, dataset: MyNewDataset, tmp_path: Path\n", + " ) -> None:\n", + " shutil.rmtree(dataset.root)\n", + " download_url(dataset.url, root=tmp_path)\n", + " MyNewDataset(root=tmp_path, download=False)\n", + "\n", + " # Test the logic when the dataset is not downloaded\n", + " def test_not_downloaded(self, tmp_path: Path) -> None:\n", + " with pytest.raises(DatasetNotFoundError, match='Dataset not found'):\n", + " MyNewDataset(tmp_path)\n", + "\n", + " # Test the plotting method through something like the following\n", + " def test_plot(self, dataset: MyNewDataset) -> None:\n", + " x = dataset[0].copy()\n", + " x['prediction'] = x['label'].clone()\n", + " dataset.plot(x, suptitle='Test')\n", + " plt.close()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Documentation Entries" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The entry point for new and experienced users of domain libraries is often the dedicated documentation page that accompanies a Github repository. TorchGeo uses the popular [Sphinx](https://www.sphinx-doc.org/en/master/) framework to build its documentation. To display the documentation strings you have written in `dataset_name.py` on the actual documentation page, you need to create an entry in `docs/api/datasets.rst` in alphabetical order:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```rst\n", + "Dataset Name\n", + "^^^^^^^^^^^^\n", + "\n", + ".. autoclass:: MyNewDataset\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Additionally, add a row in the `non_geo_datasets.csv` file under `docs/api/datasets` to include the dataset in the overview table." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Linters\n", + "\n", + "See the [linter docs](https://torchgeo.readthedocs.io/en/stable/user/contributing.html#linters) for an overview of linters that TorchGeo employs and how to apply them during commits." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test Coverage" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "TorchGeo maintains a test coverage of 100%. This means, that every line of code written within the torchgeo directory is being run by some unit test. The [testing docs](https://torchgeo.readthedocs.io/en/stable/user/contributing.html#tests) provide instructions on how you can test the coverage locally for the `dataset_new.py` file that you are adding." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Final Checklist" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This final checklist might provide a useful overview of the individual parts discussed in this tutorial. You definitely do not need to check all boxes, before submitting a PR. If you have any questions feel free to ask on Slack or open a PR already such that maintainers or other community members can answer specific questions or give pointers. If you want to run your PR as a work of progress, such that the CI tests are run against your code while you work on ticking more boxes you can also convert the PR to a draft on the right side menu." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- Dataset implementation in `dataset_name.py`\n", + " - Class docstring containing:\n", + " - Summary intro\n", + " - Dataset features\n", + " - Dataset format\n", + " - Link to publication\n", + " - `versionadded` tag\n", + " - if applicable a note on additional dependencies\n", + " - all class methods have docstrings\n", + " - all class methods have argument and return type hints, mypy (the tool that checks type hints) can be confusing at the beginning so don't hesitate to ask for help\n", + " - if dataset is on GitHub or Huggingface, url link should contain the commit hash\n", + " - checksum added\n", + " - plot method that can display a single sample from the dataset (you can add the resulting figure in your PR description)\n", + " - add the dataset to `torchgeo/datastes/__init__.py`\n", + " - Add the copyright at the top of the file\n", + "- Dummy data script `data.py`\n", + " - replicate directory structure\n", + " - replicate naming of directory and files\n", + " - for image based datasets, use a small size, like 32x32\n", + "- Unit tests `test_dataset_name.py`\n", + " - 100% test coverage \n", + "- Documentation with `non_geo_datasets.csv` and `datasets.rst`\n", + " - entry in `datasets.rst`\n", + " - entry in `non_geo_datasets.csv`\n", + " - documentation displays properly, this can be checked locally or via the GitHub CI tests under `docs/readthedocs.org:torchgeo`" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} From be763320b7fa1a51eb07f20b65ac6e807ea9dfa0 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Sun, 8 Dec 2024 17:05:53 +0100 Subject: [PATCH 06/35] Add Introduction to TorchGeo tutorial (#2457) * Add Introduction to TorchGeo tutorial * ruff * Add to docs index * Add toy dataset --- docs/index.rst | 1 + docs/tutorials/torchgeo.ipynb | 445 ++++++++++++++++++++++++++++++++++ 2 files changed, 446 insertions(+) create mode 100644 docs/tutorials/torchgeo.ipynb diff --git a/docs/index.rst b/docs/index.rst index b9693ec905d..1a9f2f2bc22 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -31,6 +31,7 @@ torchgeo tutorials/getting_started tutorials/pytorch tutorials/geospatial + tutorials/torchgeo tutorials/contribute_non_geo_dataset tutorials/custom_raster_dataset tutorials/transforms diff --git a/docs/tutorials/torchgeo.ipynb b/docs/tutorials/torchgeo.ipynb new file mode 100644 index 00000000000..cc6e17c6170 --- /dev/null +++ b/docs/tutorials/torchgeo.ipynb @@ -0,0 +1,445 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "45973fd5-6259-4e03-9501-02ee96f3f870", + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright (c) Microsoft Corporation. All rights reserved.\n", + "# Licensed under the MIT License." + ] + }, + { + "cell_type": "markdown", + "id": "9478ed9a", + "metadata": { + "id": "NdrXRgjU7Zih" + }, + "source": [ + "# Introduction to TorchGeo\n", + "\n", + "_Written by: Adam J. Stewart_\n", + "\n", + "Now that we've seen the basics of PyTorch and the challenges of working with geospatial data, let's see how TorchGeo addresses these challenges." + ] + }, + { + "cell_type": "markdown", + "id": "34f10e9f", + "metadata": { + "id": "lCqHTGRYBZcz" + }, + "source": [ + "## Setup\n", + "\n", + "First, we install TorchGeo and all of its dependencies." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "019092f0", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install torchgeo" + ] + }, + { + "cell_type": "markdown", + "id": "4db9f791", + "metadata": { + "id": "dV0NLHfGBMWl" + }, + "source": [ + "## Imports\n", + "\n", + "Next, we import TorchGeo and any other libraries we need." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d92b0f1", + "metadata": { + "id": "entire-albania" + }, + "outputs": [], + "source": [ + "import os\n", + "import tempfile\n", + "from datetime import datetime\n", + "\n", + "from matplotlib import pyplot as plt\n", + "from torch.utils.data import DataLoader\n", + "\n", + "from torchgeo.datasets import CDL, BoundingBox, Landsat7, Landsat8, stack_samples\n", + "from torchgeo.datasets.utils import download_and_extract_archive\n", + "from torchgeo.samplers import GridGeoSampler, RandomGeoSampler" + ] + }, + { + "cell_type": "markdown", + "id": "b813beba-62ad-430c-96e5-1d81bef1e244", + "metadata": {}, + "source": [ + "## Motivation\n", + "\n", + "Let's start with a common task in geospatial machine learning to motivate us: land cover mapping. Imagine you have a collection of imagery and a land cover layer or *mask* you would like to learn to predict. In machine learning, this pixelwise classification process is referred to as *semantic segmentation*.\n", + "\n", + "More concretely, imagine you would like to combine a set of Landsat 7 and 8 scenes with the Cropland Data Layer (CDL). This presents a number of challenges for a typical machine learning pipeline:\n", + "\n", + "* We may have hundreds of partially overlapping Landsat images that need to be mosaiced together\n", + "* We have a single CDL mask covering the entire continental US\n", + "* Neither the Landsat input or CDL output will have the same geospatial bounds\n", + "* Landsat is multispectral, and may have a different resolution for each spectral band\n", + "* Landsat 7 and 8 have a different number of spectral bands\n", + "* Landsat and CDL may have a differerent CRS\n", + "* Every single Landsat file may be in a different CRS (e.g., multiple UTM zones)\n", + "* We may have multiple years of input and output data, and need to ensure matching time spans\n", + "\n", + "We can't have a dataset of length 1, and it isn't obvious what to do when the number, bounds, and size of input images differ from the output masks. Furthermore, each image is far too large to pass to a neural network. \n", + "\n", + "Traditionally, people either performed classification on a single pixel at a time or curated their own benchmark dataset. This works fine for training, but isn't really useful for inference. What we would really like to be able to do is sample small pixel-aligned pairs of input images and output masks from the region of overlap between both datasets. This exact situation is illustrated in the following figure:\n", + "\n", + "![Landsat CDL intersection](https://github.com/microsoft/torchgeo/blob/main/images/geodataset.png?raw=true)\n", + "\n", + "Now, let's see what features TorchGeo has to support this kind of use case." + ] + }, + { + "cell_type": "markdown", + "id": "41119706-0722-4fd0-85a7-787bb12bbab8", + "metadata": {}, + "source": [ + "## Datasets\n", + "\n", + "Geospatial data comes in a wide variety of formats. TorchGeo has two separate classes of datasets to deal with this dataset diversity:\n", + "\n", + "* `NonGeoDataset`: for curated benchmark datasets, where geospatial metadata is either missing or unnecessary\n", + "* `GeoDataset`: for uncurated raster and vector data layers, where geospatial metadata is critical for merging datasets\n", + "\n", + "We have already seen the former in the Introduction to PyTorch tutorial, as `EuroSAT100` is a subclass of `NonGeoDataset`. In this tutorial, we will focus on the latter and its advantages for working with uncurated data." + ] + }, + { + "cell_type": "markdown", + "id": "914b39c6-3373-4ae8-b9ea-d377e73e9fbe", + "metadata": {}, + "source": [ + "### Landsat\n", + "\n", + "First, let's start with our Landsat imagery. We will download a couple of Landsat 7 and 8 scenes, then pass them to builtin TorchGeo datasets for each." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48d2a61d-16bb-4809-9da0-3bd369bff070", + "metadata": {}, + "outputs": [], + "source": [ + "landsat_root = os.path.join(tempfile.gettempdir(), 'landsat')\n", + "\n", + "url = 'https://hf.co/datasets/torchgeo/tutorials/resolve/ff30b729e3cbf906148d69a4441cc68023898924/'\n", + "landsat7_url = url + 'LE07_L2SP_022032_20230725_20230820_02_T1.tar.gz'\n", + "landsat8_url = url + 'LC08_L2SP_023032_20230831_20230911_02_T1.tar.gz'\n", + "\n", + "download_and_extract_archive(landsat7_url, landsat_root)\n", + "download_and_extract_archive(landsat8_url, landsat_root)\n", + "\n", + "landsat7_bands = ['SR_B1', 'SR_B2', 'SR_B3', 'SR_B4', 'SR_B5', 'SR_B7']\n", + "landsat8_bands = ['SR_B2', 'SR_B3', 'SR_B4', 'SR_B5', 'SR_B6', 'SR_B7']\n", + "\n", + "landsat7 = Landsat7(paths=landsat_root, bands=landsat7_bands)\n", + "landsat8 = Landsat8(paths=landsat_root, bands=landsat8_bands)\n", + "\n", + "print(landsat7)\n", + "print(landsat8)\n", + "\n", + "print(landsat7.crs)\n", + "print(landsat8.crs)" + ] + }, + { + "cell_type": "markdown", + "id": "ce12838a-1010-46cb-bcca-6379f9e327ac", + "metadata": {}, + "source": [ + "The following details are worth noting:\n", + "\n", + "* We ignore the \"coastal blue\" band of Landsat 8 because it does not exist in Landsat 7\n", + "* Even though all files are stored in the same directory, the datasets know which files to include\n", + "* `paths` can be a directory to recursively search, a list of local files, or even a list of remote cloud assets" + ] + }, + { + "cell_type": "markdown", + "id": "a51c5df2-5543-41ae-a9cf-254e29b6bdfd", + "metadata": {}, + "source": [ + "### CDL\n", + "\n", + "Next, let's do the same for the CDL dataset. We are using a smaller cropped version of this dataset to make the download faster." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "909d233b-b212-48f1-b910-3065f8fcf083", + "metadata": {}, + "outputs": [], + "source": [ + "cdl_root = os.path.join(tempfile.gettempdir(), 'cdl')\n", + "\n", + "cdl_url = url + '2023_30m_cdls.zip'\n", + "\n", + "download_and_extract_archive(cdl_url, cdl_root)\n", + "\n", + "cdl = CDL(paths=cdl_root)\n", + "\n", + "print(cdl)\n", + "print(cdl.crs)" + ] + }, + { + "cell_type": "markdown", + "id": "571a6512-494f-401a-bf2f-599f28b2fad5", + "metadata": {}, + "source": [ + "Again, the following details are worth noting:\n", + "\n", + "* We could actually ask the `CDL` dataset to download our data for us by adding `download=True`\n", + "* All datasets have different spatial extents\n", + "* All datasets have different CRSs" + ] + }, + { + "cell_type": "markdown", + "id": "4a15b938-3277-46bc-86e4-a5d7f57e838a", + "metadata": {}, + "source": [ + "### Composing datasets\n", + "\n", + "We would like to be able to intelligently combine all three datasets in order to train a land cover mapping model. This requires us to create a virtual mosaic of all Landsat scenes, regardless of overlap. This can be done by taking the *union* of both datasets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4b5adace-d7c9-4c27-9e53-ae532b081046", + "metadata": {}, + "outputs": [], + "source": [ + "landsat = landsat7 | landsat8\n", + "print(landsat)\n", + "print(landsat.crs)" + ] + }, + { + "cell_type": "markdown", + "id": "ddac6f18-36de-4241-a150-0ee50d0f40dd", + "metadata": {}, + "source": [ + "Similarly, we only want to sample from locations with both input imagery and output masks, not locations with only one or the other. We can achieve this by taking the *intersection* of both datasets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6dd9f067-0e00-47ac-8bc1-6e7cd9e41e4d", + "metadata": {}, + "outputs": [], + "source": [ + "dataset = landsat & cdl\n", + "print(dataset)\n", + "print(dataset.crs)" + ] + }, + { + "cell_type": "markdown", + "id": "48d2afbe-aab8-415e-a0df-fdb0d5209a49", + "metadata": {}, + "source": [ + "Note that all datasets now have the same CRS. When you run this code, you should notice it happen very quickly. TorchGeo hasn't actually created a mosaic yet or reprojected anything, it will do this on the fly for us." + ] + }, + { + "cell_type": "markdown", + "id": "4df7ee26-2c11-4e70-b113-e633fbbc2cd9", + "metadata": {}, + "source": [ + "### Spatiotemporal indexing\n", + "\n", + "How did we do this? TorchGeo uses a data structure called an *R-tree* to store the spatiotemporal bounding box of every file in the dataset. \n", + "\n", + "![R-tree](https://raw.githubusercontent.com/davidmoten/davidmoten.github.io/master/resources/rtree-3d/plot2.png)\n", + "\n", + "TorchGeo extracts the spatial bounding box from the metadata of each file, and the timestamp from the filename. This geospatial and geotemporal metadata allows us to efficiently compute the intersection or union of two datasets. It also lets us quickly retrieve an image and corresponding mask for a particular location in space and time." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3992c571-0a6f-4d28-a2dc-e5915c00901e", + "metadata": {}, + "outputs": [], + "source": [ + "size = 256\n", + "\n", + "xmin = 925000\n", + "xmax = xmin + size * 30\n", + "ymin = 4470000\n", + "ymax = ymin + size * 30\n", + "tmin = datetime(2023, 1, 1).timestamp()\n", + "tmax = datetime(2023, 12, 31).timestamp()\n", + "\n", + "bbox = BoundingBox(xmin, xmax, ymin, ymax, tmin, tmax)\n", + "sample = dataset[bbox]\n", + "\n", + "landsat8.plot(sample)\n", + "cdl.plot(sample)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "bc591543-6d74-47b3-8c24-feada66d0a38", + "metadata": {}, + "source": [ + "TorchGeo uses *windowed-reading* to only read the blocks of memory needed to load a small patch from a large raster tile. It also automatically reprojects all data to the same CRS and resolution (from the first dataset). This can be controlled by explicitly passing `crs` or `res` to the dataset." + ] + }, + { + "cell_type": "markdown", + "id": "e2e4221e-dfb7-4966-96a6-e52400ae266c", + "metadata": {}, + "source": [ + "## Samplers\n", + "\n", + "The above `BoundingBox` makes it easy to index into complex datasets consisting of hundreds of files. However, it is a bit cumbersome to manually construct these queries every time, especially if we want thousands or even millions of bounding boxes. Luckily, TorchGeo provides a `GeoSampler` class to construct these for us." + ] + }, + { + "cell_type": "markdown", + "id": "47a7423d-32a9-40ae-be62-d54805835b19", + "metadata": {}, + "source": [ + "### Random sampling\n", + "\n", + "Usually, at training time, we want the largest possible dataset we can muster. For curated benchmark datasets like `EuroSAT100`, we achieved this by applying data augmentation to artificially inflate the size and diversity of our dataset. For `GeoDataset` objects, we can achieve this using random sampling. It doesn't matter if two or more of our images have partial overlap, as long as they bring unique pixels that help our model learn. \n", + "\n", + "TorchGeo provides a `RandomGeoSampler` to achieve this. We just tell the sampler how large we want each image patch to be (in pixel coordinates or CRS units) and, optionally, the number of image patches per epoch." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36a60164-aa88-4773-a38f-d40960f4bfb2", + "metadata": {}, + "outputs": [], + "source": [ + "train_sampler = RandomGeoSampler(dataset, size=size, length=1000)\n", + "next(iter(train_sampler))" + ] + }, + { + "cell_type": "markdown", + "id": "b6d35b26-edae-46dc-b232-878421faa84d", + "metadata": {}, + "source": [ + "### Gridded sampling\n", + "\n", + "At evaluation time, this actually becomes a problem. We want to make sure we aren't making multiple predictions for the same location. We also want to make sure we don't miss any locations. To achieve this, TorchGeo also provides a `GridGeoSampler`. We can tell the sampler the size of each image patch and the stride of our sliding window." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33340c1a-756f-4ffe-ae3d-c2307fc98d07", + "metadata": {}, + "outputs": [], + "source": [ + "test_sampler = GridGeoSampler(dataset, size=size, stride=size)\n", + "next(iter(test_sampler))" + ] + }, + { + "cell_type": "markdown", + "id": "b9806919-6520-4da6-9eb3-3e1e6a10498e", + "metadata": {}, + "source": [ + "## Data Loaders\n", + "\n", + "All of these abstractions (`GeoDataset` and `GeoSampler`) are fully compatible with all of the rest of PyTorch. We can simply pass them to a data loader like below. Note that we also need the `stack_samples` collation function to convert a list of samples to a mini-batch." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fd44d29d-b7c0-4617-bb94-d41a14e8f54a", + "metadata": {}, + "outputs": [], + "source": [ + "train_dataloader = DataLoader(\n", + " dataset, batch_size=128, sampler=train_sampler, collate_fn=stack_samples\n", + ")\n", + "test_dataloader = DataLoader(\n", + " dataset, batch_size=128, sampler=test_sampler, collate_fn=stack_samples\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "e46e8453-df25-4265-a85b-75dce7dea047", + "metadata": {}, + "source": [ + "Now that we have working data loaders, we can copy-n-paste our training code from the Introduction to PyTorch tutorial. We only need to change our model to one designed for semantic segmentation, such as a U-Net. Every other line of code would be identical to how you would do this in your normal PyTorch workflow." + ] + }, + { + "cell_type": "markdown", + "id": "a3acc64e-8dc0-46b4-a677-ecb9723d4f56", + "metadata": {}, + "source": [ + "## Additional Reading\n", + "\n", + "TorchGeo has plenty of other tutorials and documentation. If you would like to get more insight into the design of TorchGeo, the following external resources are also helpful:\n", + "\n", + "* [TorchGeo: Deep Learning With Geospatial Data](https://arxiv.org/abs/2111.08872)\n", + "* [Geospatial deep learning with TorchGeo](https://pytorch.org/blog/geospatial-deep-learning-with-torchgeo/)" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "getting_started.ipynb", + "provenance": [] + }, + "execution": { + "timeout": 1200 + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 8393e86b86b6d0d8efaa2ad493597a597ac77f38 Mon Sep 17 00:00:00 2001 From: Nils Lehmann <35272119+nilsleh@users.noreply.github.com> Date: Sun, 8 Dec 2024 17:06:12 +0100 Subject: [PATCH 07/35] Add Contribution Guide for Datamodule (#2452) * datamodule contrib * typo * datamodule commit * Add author --------- Co-authored-by: Adam J. Stewart --- docs/index.rst | 1 + docs/tutorials/contribute_datamodule.ipynb | 240 +++++++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 docs/tutorials/contribute_datamodule.ipynb diff --git a/docs/index.rst b/docs/index.rst index 1a9f2f2bc22..b763481e7ea 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -34,6 +34,7 @@ torchgeo tutorials/torchgeo tutorials/contribute_non_geo_dataset tutorials/custom_raster_dataset + tutorials/contribute_datamodule tutorials/transforms tutorials/indices tutorials/trainers diff --git a/docs/tutorials/contribute_datamodule.ipynb b/docs/tutorials/contribute_datamodule.ipynb new file mode 100644 index 00000000000..1bbb728e0e3 --- /dev/null +++ b/docs/tutorials/contribute_datamodule.ipynb @@ -0,0 +1,240 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Contribute a New DataModule" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "_Written by: Nils Lehmann_\n", + "\n", + "TorchGeo provides Lightning `DataModules` and trainers to faciliate easy and scalabel model training based on simple configuration files. Essentially, a `DataModule` implements the logic for splitting a dataset into train, validation and test splits for reproducability, wrapping them in PyTorch `DataLoaders` and apply augmentations to batches of data. This tutorial will outline a guide to adding a new datamodule to TorchGeo. It is often easy to do so alongside a new dataset and will make the dataset directly useable for a Lightning training and evaluation pipeline" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adding the datamodule" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Adding a datamodule to TorchGeo consists of roughly four parts:\n", + "\n", + "1. a `dataset_name.py` file under `torchgeo/datamodules` that implements the split logic and defines augmentation\n", + "2. a `dataset_name.yaml` file under `tests/configs` that defines arguments to directly test the datamodule with the appropriate task\n", + "3. add the above yaml file to the list of files to be tested in the corresponding `test_{task}.py` file under `tests/trainers`\n", + "4. an entry to the documentation page file `datamodules.rst` under `docs/api/`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The datamodule `dataset_name.py` file" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The vast majority of new DataModules can inherit from one of the base classes that take care of the majority of the work. The goal of the dataset specific DataModule is to specify how the dataset should be split into train/val/test and any augmentations that should be applied to batches of data.\n", + "\n", + "\n", + "```python\n", + "\n", + "\"\"\"NewDatasetDataModule datamodule.\"\"\"\n", + "\n", + "import os\n", + "from typing import Any\n", + "\n", + "import kornia.augmentation as K\n", + "import torch\n", + "from torch.utils.data import Subset\n", + "\n", + "from .geo import NonGeoDataModule\n", + "from .utils import group_shuffle_split\n", + "\n", + "\n", + "# We follow the convention of appending the dataset_name with \"DataModule\"\n", + "class NewDatasetDataModule(NonGeoDataModule):\n", + " \"\"\"LightningDataModule implementation for the NewDataset dataset.\n", + "\n", + " Make a comment here about how the dataset is split into train/val/test.\n", + "\n", + " You can also add any other comments or references that are helpful to \n", + " understand implementation decisions\n", + "\n", + " .. versionadded:: for example 0.7\n", + " \"\"\"\n", + " # you can define channelwise normalization statistics that will be applied\n", + " # to data batches, which is usually crucial for training stability and decent performance\n", + " mean = torch.Tensor([0.5, 0.4, 0.3])\n", + " std = torch.Tensor([1.5, 1.4, 1.3])\n", + "\n", + " def __init__(\n", + " self, batch_size: int = 64, num_workers: int = 0, size: int = 256, **kwargs: Any\n", + " ) -> None:\n", + " \"\"\"Initialize a new NewDatasetModule instance.\n", + "\n", + " Args:\n", + " batch_size: Size of each mini-batch.\n", + " num_workers: Number of workers for parallel data loading.\n", + " size: resize images of input size 1000x1000 to size x size\n", + " **kwargs: Additional keyword arguments passed to\n", + " :class:`~torchgeo.datasets.NewDataset`.\n", + " \"\"\"\n", + " # in the init method of the base class the dataset will be instantiated with **kwargs\n", + " super().__init__(NewDatasetName, batch_size, num_workers, **kwargs)\n", + "\n", + " # you can specify a series of Kornia augmentations that will be\n", + " # applied to a batch of training data in `on_after_batch_transfer` in the NonGeoDataModule base class\n", + " self.train_aug = K.AugmentationSequential(\n", + " K.Resize(size),\n", + " K.Normalize(self.mean, self.std),\n", + " K.RandomHorizontalFlip(p=0.5),\n", + " K.RandomVerticalFlip(p=0.5),\n", + " data_keys=None,\n", + " keepdim=True,\n", + " )\n", + "\n", + " # you can also define specific augmentations for other experiment phases, if not specified\n", + " # self.aug Augmentations will be applied\n", + " self.aug = K.AugmentationSequential(\n", + " K.Normalize(self.mean, self.std),\n", + " K.Resize(size), data_keys=None, keepdim=True\n", + " )\n", + "\n", + " self.size = size\n", + "\n", + " # setup defines how the dataset should be split\n", + " # this could either be predefined from the dataset authors or\n", + " # done in a prescribed way if some or no splits are specified\n", + " def setup(self, stage: str) -> None:\n", + " \"\"\"Set up datasets.\n", + "\n", + " Args:\n", + " stage: Either 'fit', 'validate', 'test', or 'predict'.\n", + " \"\"\"\n", + " if stage in ['fit', 'validate']:\n", + " dataset = NewDatasetName(split='train', **self.kwargs)\n", + " # perhaps the dataset contains some geographical metadata based on which you would create reproducible random\n", + " # splits\n", + " grouping_paths = [os.path.dirname(path) for path in dataset.file_list]\n", + " train_indices, val_indices = group_shuffle_split(\n", + " grouping_paths, test_size=0.2, random_state=0\n", + " )\n", + " self.train_dataset = Subset(dataset, train_indices)\n", + " self.val_dataset = Subset(dataset, val_indices)\n", + " if stage in ['test']:\n", + " self.test_dataset = NewDatasetName(split='test', **self.kwargs)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Linters\n", + "\n", + "See the [linter docs](https://torchgeo.readthedocs.io/en/stable/user/contributing.html#linters) for an overview of linters that TorchGeo employs and how to apply them during commits for example. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Unit tests" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "TorchGeo maintains a test coverage of 100%. This means, that every line of code written within the torchgeo directory is being called by some unit test. For new datasets, we commonly write a separate test file, however, for datamodules we would like to test them directly with one of the task trainers. To do this, you simply need to define a `config.yaml` file and add it to the list of files to be tested by a task. For example, if you added a new datamodule for image segmentation you would write a config file that should look something like this:\n", + "\n", + "```yaml\n", + "model:\n", + " class_path: SemanticSegmentationTask\n", + " init_args:\n", + " loss: 'ce'\n", + " model: 'unet'\n", + " backbone: 'resnet18'\n", + " in_channels: 3 # number of input channels for the dataset\n", + " num_classes: 7 # number of segmentation models\n", + " num_filters: 1 # a smaller model version for faster unit tests\n", + " ignore_index: null # one can ignore certain classes during the loss computation\n", + "data:\n", + " class_path: NewDatasetNameDataModule # arguments to the DataModule above you wrote\n", + " init_args:\n", + " batch_size: 1 # \n", + " dict_kwargs:\n", + " root: 'tests/data/deepglobelandcover' # necessary arguments for the underlying dataset class that the datamodule builds on\n", + "```\n", + "\n", + "The yaml file should \"simulate\" how you would use this datamodule for an actual experiment. Add this file with `dataset_name.yaml` to the `tests/conf` directory." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Final Checklist" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This final checklist might provide a useful overview of the individual parts discussed in this tutorial. You definitely do not need to check all boxes, before submitting a PR. If you have any questions feel free to ask in the Slack channel or open a PR already such that maintainers or other community members can answer specific questions or give pointers. If you want to run your PR as a work of progress, such that the CI tests are run against your code while you work on ticking more boxes you can also convert the PR to a draft on the right side." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- The datamodule implementation\n", + " - define training/val/test split\n", + " - if there are dataset specific augmentations, implement and reference them\n", + " - add microsoft copyright notice to top of the file\n", + "- The config test file\n", + " - select the appropriate task, if the dataset supports multiple ones, you can create one for each task\n", + " - correct arguments such as the number of targets (classes)\n", + " - add the config file to the list of files to be tested in the corresponding `test_{task}.py` file under `tests/trainers`\n", + "- Unit Tests\n", + " - 100% test coverage\n", + "- Documentation\n", + " - an entry to the documentation page file `datamodules.rst` under `docs/api/`" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} From e743e6d6862fe11a1d18ad639cc3b09483d25ec8 Mon Sep 17 00:00:00 2001 From: Mauricio Cordeiro Date: Sun, 8 Dec 2024 13:11:35 -0300 Subject: [PATCH 08/35] Added EarthSurfaceWater tutorial (#960) * Added EarthSurfaceWater tutorial * Updated earh_surface_water tutorial * Resolved CI failure on earth_surface_water tutorial * Resolved CI notebook failure on earth_surface_water tutorial * Increased nbmake timeout * Limit the time to run in CPUs * Solved some requests * Solved most PR requests * ruff * Install torchgeo, clear output --------- Co-authored-by: Adam J. Stewart --- docs/index.rst | 1 + docs/tutorials/earth_surface_water.ipynb | 878 +++++++++++++++++++++++ 2 files changed, 879 insertions(+) create mode 100644 docs/tutorials/earth_surface_water.ipynb diff --git a/docs/index.rst b/docs/index.rst index b763481e7ea..561a496d67a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -39,6 +39,7 @@ torchgeo tutorials/indices tutorials/trainers tutorials/pretrained_weights + tutorials/earth_surface_water .. toctree:: :maxdepth: 1 diff --git a/docs/tutorials/earth_surface_water.ipynb b/docs/tutorials/earth_surface_water.ipynb new file mode 100644 index 00000000000..e44d1e385f7 --- /dev/null +++ b/docs/tutorials/earth_surface_water.ipynb @@ -0,0 +1,878 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "MAR190Aszv8r" + }, + "source": [ + "# Earth Water Surface\n", + "\n", + "_Written by: Mauricio Cordeiro_" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0lZeGJNTz1y5" + }, + "source": [ + "## Introduction" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VXG-oXGUz39X" + }, + "source": [ + "The objective of this tutorial is to go through the Earth Water Surface dataset and cover the following topics:
\n", + "\n", + "* Creating RasterDatasets, DataLoaders and Samplers for images and masks;\n", + "* Intersection Dataset;\n", + "* Normalizing the data;\n", + "* Creating spectral indices;\n", + "* Creating the segmentation model (DeepLabV3);\n", + "* Loss function and metrics; and\n", + "* Training loop.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JTt_Ysyl4El5" + }, + "source": [ + "## Environment" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-U9kWwoL4GqT" + }, + "source": [ + "For the environment, we will install the torchgeo and scikit-learn packages." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "A14-syGAFahE", + "outputId": "d5b0ac1d-5cc2-4532-b2e6-7134638e9389" + }, + "outputs": [], + "source": [ + "%pip install torchgeo scikit-learn" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Imports" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import tempfile\n", + "from collections.abc import Callable, Iterable\n", + "from pathlib import Path\n", + "\n", + "import kornia.augmentation as K\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import rasterio as rio\n", + "import torch\n", + "from sklearn.metrics import jaccard_score\n", + "from torch.utils.data import DataLoader\n", + "\n", + "from torchgeo.datasets import RasterDataset, stack_samples, unbind_samples, utils\n", + "from torchgeo.samplers import RandomGeoSampler, Units\n", + "from torchgeo.transforms import indices" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "73l1wc-fFWuU" + }, + "source": [ + "## Dataset" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yY7mCdoq00Yo" + }, + "source": [ + "The dataset we will use is the Earth Surface Water dataset [1] (licensed under Creative Commons Attribution 4.0 International Public License), which has patches from different parts of the world (Figure below) and its corresponding water masks. The dataset uses optical imagery from Sentinel-2 satellite with 10m of spatial resolution.\n", + "\n", + "![Image1](https://raw.githubusercontent.com/xinluo2018/WatNet/main/figures/dataset.png)\n", + "\n", + "[1] Xin Luo. (2021). Earth Surface Water Dataset [Data set]. Zenodo. https://doi.org/10.5281/zenodo.5205674\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Download and extract dataset to a temp folder\n", + "tmp_path = Path(tempfile.gettempdir()) / 'surface_water/'\n", + "utils.download_and_extract_archive(\n", + " 'https://hf.co/datasets/cordmaur/earth_surface_water/resolve/main/earth_surface_water.zip',\n", + " tmp_path,\n", + ")\n", + "\n", + "# Set the root to the extracted folder\n", + "root = tmp_path / 'dset-s2'" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "abnT63f1GOh8" + }, + "source": [ + "## Creating the Datasets" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7ke5sQ1_4nEq" + }, + "source": [ + "Now that we have the original dataset already uncompressed in Colab’s environment, we can prepare it to be loaded into a neural network. For that, we will create an instance of the RasterDataset class, provided by TorchGeo, and point to the specific directory, using the following commands. The `scale` function will apply the `1e-4` scale necessary to get the Sentinel-2 values in reflectance. Once the datasets are created, we can combine images with masks (labels) using the `&` operator." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "iXiPVrjXGSes" + }, + "outputs": [], + "source": [ + "def scale(item: dict):\n", + " item['image'] = item['image'] / 10000\n", + " return item" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yWApUYVYZ1Lr" + }, + "outputs": [], + "source": [ + "train_imgs = RasterDataset(\n", + " paths=(root / 'tra_scene').as_posix(), crs='epsg:3395', res=10, transforms=scale\n", + ")\n", + "train_msks = RasterDataset(\n", + " paths=(root / 'tra_truth').as_posix(), crs='epsg:3395', res=10\n", + ")\n", + "\n", + "valid_imgs = RasterDataset(\n", + " paths=(root / 'val_scene').as_posix(), crs='epsg:3395', res=10, transforms=scale\n", + ")\n", + "valid_msks = RasterDataset(\n", + " paths=(root / 'val_truth').as_posix(), crs='epsg:3395', res=10\n", + ")\n", + "\n", + "# IMPORTANT\n", + "train_msks.is_image = False\n", + "valid_msks.is_image = False\n", + "\n", + "train_dset = train_imgs & train_msks\n", + "valid_dset = valid_imgs & valid_msks\n", + "\n", + "# Create the samplers\n", + "\n", + "train_sampler = RandomGeoSampler(train_imgs, size=512, length=130, units=Units.PIXELS)\n", + "valid_sampler = RandomGeoSampler(valid_imgs, size=512, length=64, units=Units.PIXELS)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MaTZ03eJ5FGa" + }, + "source": [ + "Note that we are specifying the CRS (Coordinate Reference System) to EPSG:3395. TorchGeo requires that all the images are loaded in the same CRS. However, the patches in the dataset are in different UTM projections and the default behavior of TorchGeo is to use the first CRS found as its default. In this case, we have to inform a CRS that is able to cope with these different regions around the globe. To minimize the deformations due to the huge differences in latitude (I can create a history specific for this purpose) within the patches, I have selected World Mercator as the main CRS for the project. Figure 3 shows the world projected in World Mercator CRS.\n", + "\n", + "\n", + "![Image2](https://miro.medium.com/max/4800/1*sUdRKEfIAbm79jpB3bCShQ.webp)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TqNOU7WaOJ2t" + }, + "source": [ + "### Understanding the sampler" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8RFrF3bTOSJn" + }, + "source": [ + "To create training patches that can be fed into a neural network from our dataset, we need to select samples of fixed sizes. TorchGeo has many samplers, but here we will use the `RandomGeoSampler` class. Basically, the sampler selects random bounding boxes of fixed size that belongs to the original image. Then, these bounding boxes are used in the `RasterDataset` to query the portion of the image we want. Here is an exmple using the previously created samplers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "r8VGVIWNPI_W", + "outputId": "5b779cd3-25bc-4ec4-e29d-99391e906a4d" + }, + "outputs": [], + "source": [ + "bbox = next(iter(train_sampler))\n", + "bbox" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "KFCssvlqPI87", + "outputId": "4bb06a55-375e-4c14-d790-2429d4f14d0a" + }, + "outputs": [], + "source": [ + "sample = train_dset[bbox]\n", + "sample.keys()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "G-gF-8k1PI6N", + "outputId": "60002cb9-8d8c-4e1e-ba18-51edf4191ea3" + }, + "outputs": [], + "source": [ + "sample['image'].shape, sample['mask'].shape" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dGiqU2TcPcTq" + }, + "source": [ + "Notice we have now patches of same size (..., 512 x 512)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TBNfy4X5Pn-G" + }, + "source": [ + "## Creating Dataloaders" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a97dvCYVP5D5" + }, + "source": [ + "Creating a `DataLoader` in TorchGeo is very straightforward, just like it is with Pytorch (we are actually using the same class). Note below that we are also using the same samplers already defined. Additionally we inform the dataset that the dataloader will use to pull data from, the batch_size (number of samples in each batch) and a collate function that specifies how to “concatenate” the multiple samples into one single batch.\n", + "\n", + "Finally, we can iterate through the dataloader to grab batches from it. To test it, we will get the first batch." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "IhWa0SYfav2V", + "outputId": "6e80207e-30cf-46b8-d507-e15c0619aa9d" + }, + "outputs": [], + "source": [ + "# Adjust the batch size according to your GPU memory\n", + "train_dataloader = DataLoader(\n", + " train_dset, sampler=train_sampler, batch_size=4, collate_fn=stack_samples\n", + ")\n", + "valid_dataloader = DataLoader(\n", + " valid_dset, sampler=valid_sampler, batch_size=4, collate_fn=stack_samples\n", + ")\n", + "\n", + "train_batch = next(iter(train_dataloader))\n", + "valid_batch = next(iter(valid_dataloader))\n", + "train_batch.keys(), valid_batch.keys()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "o7VRAzpkQIvr" + }, + "source": [ + "## Batch Visualization" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "78IKqlWUQMTM" + }, + "source": [ + "Now that we can draw batches from our datasets, let’s create a function to display the batches.\n", + "\n", + "The function `plot_batch` will will check automatically the number of items in the batch and if there are masks associated to arrange the output grid accordingly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zynH3etxQQmG" + }, + "outputs": [], + "source": [ + "def plot_imgs(\n", + " images: Iterable, axs: Iterable, chnls: list[int] = [2, 1, 0], bright: float = 3.0\n", + "):\n", + " for img, ax in zip(images, axs):\n", + " arr = torch.clamp(bright * img, min=0, max=1).numpy()\n", + " rgb = arr.transpose(1, 2, 0)[:, :, chnls]\n", + " ax.imshow(rgb)\n", + " ax.axis('off')\n", + "\n", + "\n", + "def plot_msks(masks: Iterable, axs: Iterable):\n", + " for mask, ax in zip(masks, axs):\n", + " ax.imshow(mask.squeeze().numpy(), cmap='Blues')\n", + " ax.axis('off')\n", + "\n", + "\n", + "def plot_batch(\n", + " batch: dict,\n", + " bright: float = 3.0,\n", + " cols: int = 4,\n", + " width: int = 5,\n", + " chnls: list[int] = [2, 1, 0],\n", + "):\n", + " # Get the samples and the number of items in the batch\n", + " samples = unbind_samples(batch.copy())\n", + "\n", + " # if batch contains images and masks, the number of images will be doubled\n", + " n = 2 * len(samples) if ('image' in batch) and ('mask' in batch) else len(samples)\n", + "\n", + " # calculate the number of rows in the grid\n", + " rows = n // cols + (1 if n % cols != 0 else 0)\n", + "\n", + " # create a grid\n", + " _, axs = plt.subplots(rows, cols, figsize=(cols * width, rows * width))\n", + "\n", + " if ('image' in batch) and ('mask' in batch):\n", + " # plot the images on the even axis\n", + " plot_imgs(\n", + " images=map(lambda x: x['image'], samples),\n", + " axs=axs.reshape(-1)[::2],\n", + " chnls=chnls,\n", + " bright=bright,\n", + " )\n", + "\n", + " # plot the masks on the odd axis\n", + " plot_msks(masks=map(lambda x: x['mask'], samples), axs=axs.reshape(-1)[1::2])\n", + "\n", + " else:\n", + " if 'image' in batch:\n", + " plot_imgs(\n", + " images=map(lambda x: x['image'], samples),\n", + " axs=axs.reshape(-1),\n", + " chnls=chnls,\n", + " bright=bright,\n", + " )\n", + "\n", + " elif 'mask' in batch:\n", + " plot_msks(masks=map(lambda x: x['mask'], samples), axs=axs.reshape(-1))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 672 + }, + "id": "0S4DZa8aQd8Z", + "outputId": "755d6f7a-d4fd-4cab-ec1d-c293255a7e8c" + }, + "outputs": [], + "source": [ + "plot_batch(train_batch)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SkGQoaWlQVFC" + }, + "source": [ + "## Data Standardization and Spectral Indices" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mvce_wKMQ3pT" + }, + "source": [ + "Normally, machine learning methods (deep learning included) benefit from feature scaling. That means standard deviation around 1 and zero mean, by applying the following formula:
\n", + "$X'=\\frac{X-Mean}{\\text{Standard deviation}}$\n", + "\n", + "To do that, we need to first find the mean and standard deviation for each one of the 6s channels in the dataset.\n", + "\n", + "Let’s define a function calculate these statistics and write its results in the variables mean and std. We will use our previously installed rasterio package to open the images and perform a simple average over the statistics for each batch/channel. For the standard deviation, this method is an approximation. For a more precise calculation, please refer to: http://notmatthancock.github.io/2017/03/23/simple-batch-stat-updates.htm." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "IZ5yXZPzNIdh" + }, + "outputs": [], + "source": [ + "def calc_statistics(dset: RasterDataset):\n", + " \"\"\"\n", + " Calculate the statistics (mean and std) for the entire dataset\n", + " Warning: This is an approximation. The correct value should take into account the\n", + " mean for the whole dataset for computing individual stds.\n", + " For correctness I suggest checking: http://notmatthancock.github.io/2017/03/23/simple-batch-stat-updates.html\n", + " \"\"\"\n", + "\n", + " # To avoid loading the entire dataset in memory, we will loop through each img\n", + " # The filenames will be retrieved from the dataset's rtree index\n", + " files = [\n", + " item.object for item in dset.index.intersection(dset.index.bounds, objects=True)\n", + " ]\n", + "\n", + " # Reseting statistics\n", + " accum_mean = 0\n", + " accum_std = 0\n", + "\n", + " for file in files:\n", + " img = rio.open(file).read() / 10000 # type: ignore\n", + " accum_mean += img.reshape((img.shape[0], -1)).mean(axis=1)\n", + " accum_std += img.reshape((img.shape[0], -1)).std(axis=1)\n", + "\n", + " # at the end, we shall have 2 vectors with lenght n=chnls\n", + " # we will average them considering the number of images\n", + " return accum_mean / len(files), accum_std / len(files)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4VtubMAPxXYq" + }, + "outputs": [], + "source": [ + "# Calculate the statistics (Mean and std) for the dataset\n", + "mean, std = calc_statistics(train_imgs)\n", + "\n", + "# Please, note that we will create spectral indices using the raw (non-normalized) data. Then, when normalizing, the sensors will have more channels (the indices) that should not be normalized.\n", + "# To solve this, we will add the indices to the 0's to the mean vector and 1's to the std vectors\n", + "mean = np.concat([mean, [0, 0, 0]])\n", + "std = np.concat([std, [1, 1, 1]])\n", + "\n", + "norm = K.Normalize(mean=mean, std=std)\n", + "\n", + "tfms = torch.nn.Sequential(\n", + " indices.AppendNDWI(index_green=1, index_nir=3),\n", + " indices.AppendNDWI(index_green=1, index_nir=5),\n", + " indices.AppendNDVI(index_nir=3, index_red=2),\n", + " norm,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "hVsDAOaVRhiN", + "outputId": "b128640d-b3bc-4cf6-82e1-187b5df2a164" + }, + "outputs": [], + "source": [ + "transformed_img = tfms(train_batch['image'])\n", + "print(transformed_img.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HA-jTEKeRuA4" + }, + "source": [ + "Note that our transformed batch has now 9 channels, instead of 6." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hWfUOS1RR14g" + }, + "source": [ + "> Important: the normalize method we created will apply the normalization just to the original bands and it will ignore the previously appended indices. That’s important to avoid errors due to distinct shapes between the batch and the mean and std vectors." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JfNmPcwWSNFv" + }, + "source": [ + "## Segmentation Model" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bMmXdpZWSSNR" + }, + "source": [ + "For the semantic segmentation model, we are going to use a predefined architecture that is available in Pytorch. Looking at list (https://pytorch.org/vision/stable/models.html#semantic-segmentation) it is possible to note 3 models available for semantic segmentation, but one (LRASPP) is intended for mobile applications. In our tutorial, we will use the DeepLabV3 model.\n", + "\n", + "Here, we will create a DeepLabV3 model for 2 classes. In this case, I will skip the pretrained weights, as the weights represent another domain (not water segmentation from multispectral imagery)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "OzhOtV3IyubJ", + "outputId": "b7e57773-1f4c-4140-9039-b0e84e59aa58" + }, + "outputs": [], + "source": [ + "from torchvision.models.segmentation import deeplabv3_resnet50\n", + "\n", + "model = deeplabv3_resnet50(weights=None, num_classes=2)\n", + "model" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AvsK_LXuSmnL" + }, + "source": [ + "The first thing we have to pay attention in the model architecture is the number of channels expected in the first convolution (Conv2d), that is defined as 3. That’s because the model is prepared to work with RGB images. After the first convolution, the 3 channels will produce 64 channels in lower resolution, and so on. As we have now 9 channels, we will change this first processing layer to adapt correctly to our model. We can do this by replacing the first convolutional layer for a new one, by following the commands. Finally, we check a mock batch can pass through the model and provide the output with 2 channels (water / no_water) as desired." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "xYPdGBPOSfHJ", + "outputId": "7e9267b7-a4e7-428d-a0cd-9292dd2b1923" + }, + "outputs": [], + "source": [ + "backbone = model.get_submodule('backbone')\n", + "\n", + "conv = torch.nn.modules.conv.Conv2d(\n", + " in_channels=9,\n", + " out_channels=64,\n", + " kernel_size=(7, 7),\n", + " stride=(2, 2),\n", + " padding=(3, 3),\n", + " bias=False,\n", + ")\n", + "backbone.register_module('conv1', conv)\n", + "\n", + "pred = model(torch.randn(3, 9, 512, 512))\n", + "pred['out'].shape" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PvvGWtMrTdCE" + }, + "source": [ + "## Training Loop" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YuUPhlzxTftk" + }, + "source": [ + "The training function should receive the number of epochs, the model, the dataloaders, the loss function (to be optimized) the accuracy function (to assess the results), the optimizer (that will adjust the parameters of the model in the correct direction) and the transformations to be applied to each batch." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Check if GPU is available\n", + "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n", + "device" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sFlsT8glSq3x" + }, + "outputs": [], + "source": [ + "def train_loop(\n", + " epochs: int,\n", + " train_dl: DataLoader,\n", + " val_dl: DataLoader | None,\n", + " model: torch.nn.Module,\n", + " loss_fn: Callable,\n", + " optimizer: torch.optim.Optimizer,\n", + " acc_fns: list | None = None,\n", + " batch_tfms: Callable | None = None,\n", + "):\n", + " # size = len(dataloader.dataset)\n", + " cuda_model = model.to(device)\n", + "\n", + " for epoch in range(epochs):\n", + " accum_loss = 0\n", + " for batch in train_dl:\n", + " if batch_tfms is not None:\n", + " X = batch_tfms(batch['image']).to(device)\n", + " else:\n", + " X = batch['image'].to(device)\n", + "\n", + " y = batch['mask'].type(torch.long).to(device)\n", + " pred = cuda_model(X)['out']\n", + " loss = loss_fn(pred, y)\n", + "\n", + " # BackProp\n", + " optimizer.zero_grad()\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " # update the accum loss\n", + " accum_loss += float(loss) / len(train_dl)\n", + "\n", + " # Testing against the validation dataset\n", + " if acc_fns is not None and val_dl is not None:\n", + " # reset the accuracies metrics\n", + " acc = [0.0] * len(acc_fns)\n", + "\n", + " with torch.no_grad():\n", + " for batch in val_dl:\n", + " if batch_tfms is not None:\n", + " X = batch_tfms(batch['image']).to(device)\n", + " else:\n", + " X = batch['image'].type(torch.float32).to(device)\n", + "\n", + " y = batch['mask'].type(torch.long).to(device)\n", + "\n", + " pred = cuda_model(X)['out']\n", + "\n", + " for i, acc_fn in enumerate(acc_fns):\n", + " acc[i] = float(acc[i] + acc_fn(pred, y) / len(val_dl))\n", + "\n", + " # at the end of the epoch, print the errors, etc.\n", + " print(\n", + " f'Epoch {epoch}: Train Loss={accum_loss:.5f} - Accs={[round(a, 3) for a in acc]}'\n", + " )\n", + " else:\n", + " print(f'Epoch {epoch}: Train Loss={accum_loss:.5f}')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kUIPlgndUB_9" + }, + "source": [ + "## Loss and Accuracy Functions" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Xs5LOs8LUS7j" + }, + "source": [ + "For the loss function, normally the Cross Entropy Loss should work, but it requires the mask to have shape (N, d1, d2). In this case, we will need to squeeze our second dimension manually." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "O8K3e5NwTrYg" + }, + "outputs": [], + "source": [ + "def oa(pred, y):\n", + " flat_y = y.squeeze()\n", + " flat_pred = pred.argmax(dim=1)\n", + " acc = torch.count_nonzero(flat_y == flat_pred) / torch.numel(flat_y)\n", + " return acc\n", + "\n", + "\n", + "def iou(pred, y):\n", + " flat_y = y.cpu().numpy().squeeze()\n", + " flat_pred = pred.argmax(dim=1).detach().cpu().numpy()\n", + " return jaccard_score(flat_y.reshape(-1), flat_pred.reshape(-1), zero_division=1.0)\n", + "\n", + "\n", + "def loss(p, t):\n", + " return torch.nn.functional.cross_entropy(p, t.squeeze())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UW6YfmnyUa1A" + }, + "source": [ + "## Training" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xajn1unSUh3h" + }, + "source": [ + "> To train the model it is important to have CUDA GPUs available. In Colab, it can be done by changing the runtime type and re-running the notebook. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "moU9ol78UUqP", + "outputId": "a1b5af92-b7fe-43c0-c4cd-f92906a46fb3" + }, + "outputs": [], + "source": [ + "# adjust number of epochs depending on the device\n", + "if torch.cuda.is_available():\n", + " num_epochs = 2\n", + "else:\n", + " # if GPU is not available, just make 1 pass and limit the size of the datasets\n", + " num_epochs = 1\n", + "\n", + " # by limiting the length of the sampler we limit the iterations in each epoch\n", + " train_dataloader.sampler.length = 8\n", + " valid_dataloader.sampler.length = 8\n", + "\n", + "# train the model\n", + "optimizer = torch.optim.Adam(model.parameters(), lr=0.0001, weight_decay=0.01)\n", + "train_loop(\n", + " num_epochs,\n", + " train_dataloader,\n", + " valid_dataloader,\n", + " model,\n", + " loss,\n", + " optimizer,\n", + " acc_fns=[oa, iou],\n", + " batch_tfms=tfms,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Additional Reading" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This tutorial is also available as a 3 parts Medium story:
https://medium.com/towards-data-science/artificial-intelligence-for-geospatial-analysis-with-pytorchs-torchgeo-part-1-52d17e409f09" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "authorship_tag": "ABX9TyPk0gtwHzQoTqfC6uudCTRe", + "include_colab_link": true, + "provenance": [], + "toc_visible": true + }, + "gpuClass": "standard", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} From ac0723e32e882cff536ab4db0e9a23b6b6e29bac Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Sun, 8 Dec 2024 17:45:22 +0100 Subject: [PATCH 09/35] Docs: reorganize tutorial hierarchy (#2439) * Docs: redesign tutorial organization * Typo fixes * Try .ipynb links * Add input range * ruff * more fix * Fix filename --- docs/index.rst | 26 +- docs/tutorials/basic_usage.rst | 18 + docs/tutorials/case_studies.rst | 14 + .../contribute_non_geo_dataset.ipynb | 6 +- docs/tutorials/customization.rst | 20 ++ docs/tutorials/getting_started.ipynb | 324 ------------------ docs/tutorials/getting_started.rst | 18 + docs/user/contributing.rst | 2 + 8 files changed, 86 insertions(+), 342 deletions(-) create mode 100644 docs/tutorials/basic_usage.rst create mode 100644 docs/tutorials/case_studies.rst create mode 100644 docs/tutorials/customization.rst delete mode 100644 docs/tutorials/getting_started.ipynb create mode 100644 docs/tutorials/getting_started.rst diff --git a/docs/index.rst b/docs/index.rst index 561a496d67a..02ba03a8cc5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,6 +12,15 @@ torchgeo user/glossary user/alternatives +.. toctree:: + :maxdepth: 2 + :caption: Tutorials + + tutorials/getting_started + tutorials/basic_usage + tutorials/case_studies + tutorials/customization + .. toctree:: :maxdepth: 2 :caption: Package Reference @@ -24,23 +33,6 @@ torchgeo api/trainers api/transforms -.. toctree:: - :maxdepth: 1 - :caption: Tutorials - - tutorials/getting_started - tutorials/pytorch - tutorials/geospatial - tutorials/torchgeo - tutorials/contribute_non_geo_dataset - tutorials/custom_raster_dataset - tutorials/contribute_datamodule - tutorials/transforms - tutorials/indices - tutorials/trainers - tutorials/pretrained_weights - tutorials/earth_surface_water - .. toctree:: :maxdepth: 1 :caption: PyTorch Libraries diff --git a/docs/tutorials/basic_usage.rst b/docs/tutorials/basic_usage.rst new file mode 100644 index 00000000000..10fcb97b42d --- /dev/null +++ b/docs/tutorials/basic_usage.rst @@ -0,0 +1,18 @@ +Basic Usage +=========== + +The following tutorials introduce the basic concepts and components of TorchGeo: + +* `Transforms `_: Preprocessing and data augmentation transforms for geospatial data +* `Indices `_: Spectral indices +* `Pretrained Weights `_: Models and pretrained weights +* `Lightning Trainers `_: PyTorch Lightning data modules and trainers + +.. toctree:: + :hidden: + :maxdepth: 1 + + transforms + indices + pretrained_weights + trainers diff --git a/docs/tutorials/case_studies.rst b/docs/tutorials/case_studies.rst new file mode 100644 index 00000000000..aee09a94265 --- /dev/null +++ b/docs/tutorials/case_studies.rst @@ -0,0 +1,14 @@ +Case Studies +============ + +The following case studies present end-to-end workflows for common use cases of geospatial machine learning: + +* `Earth Surface Water `_: A workflow for mapping surface water, including lakes and rivers + +Do you have a use case that is missing from this list? Please open a pull request to add tutorials for your own use cases. + +.. toctree:: + :hidden: + :maxdepth: 1 + + earth_surface_water diff --git a/docs/tutorials/contribute_non_geo_dataset.ipynb b/docs/tutorials/contribute_non_geo_dataset.ipynb index 655deb1c8d7..4a702222daf 100644 --- a/docs/tutorials/contribute_non_geo_dataset.ipynb +++ b/docs/tutorials/contribute_non_geo_dataset.ipynb @@ -259,7 +259,11 @@ "# Create dummy data for all splits and filenames\n", "for split in splits:\n", " for filename in image_file_names:\n", - " create_input_image(split, filename)\n", + " create_input_image(\n", + " os.path.join(root_dir, 'image', split, filename),\n", + " (IMG_SIZE, IMG_SIZE),\n", + " range(2**16),\n", + " )\n", " create_target_images(split, filename.replace('_', '_target_'))\n", "\n", "# Zip directory\n", diff --git a/docs/tutorials/customization.rst b/docs/tutorials/customization.rst new file mode 100644 index 00000000000..07b0870607b --- /dev/null +++ b/docs/tutorials/customization.rst @@ -0,0 +1,20 @@ +Customization +============= + +Is TorchGeo missing a dataset or model you need? Would you like to modify the default augmentations for a data module or extend a builtin trainer? + +The following tutorials will teach you how to customize TorchGeo to meet your needs: + +* `Custom Non-Geospatial Datasets `_: How to create and contribute a new NonGeoDataset +* `Custom Raster Datasets `_: How to create a new RasterDataset +* `Custom Data Module `_: How to create and contribute a new DataModule + +TorchGeo is a community-driven open source library. If there is a feature missing that you would like to add, please open a pull request to add it. See the ref:`contributing` guidelines to get started. + +.. toctree:: + :hidden: + :maxdepth: 1 + + contribute_non_geo_dataset + custom_raster_dataset + contribute_datamodule diff --git a/docs/tutorials/getting_started.ipynb b/docs/tutorials/getting_started.ipynb deleted file mode 100644 index 9bdb0eed11c..00000000000 --- a/docs/tutorials/getting_started.ipynb +++ /dev/null @@ -1,324 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "35303546", - "metadata": {}, - "source": [ - "Copyright (c) Microsoft Corporation. All rights reserved.\n", - "\n", - "Licensed under the MIT License." - ] - }, - { - "cell_type": "markdown", - "id": "9478ed9a", - "metadata": { - "id": "NdrXRgjU7Zih" - }, - "source": [ - "# Getting Started\n", - "\n", - "In this tutorial, we demonstrate some of the basic features of TorchGeo and show how easy it is to use if you're already familiar with other PyTorch domain libraries like torchvision.\n", - "\n", - "It's recommended to run this notebook on Google Colab if you don't have your own GPU. Click the \"Open in Colab\" button above to get started." - ] - }, - { - "cell_type": "markdown", - "id": "34f10e9f", - "metadata": { - "id": "lCqHTGRYBZcz" - }, - "source": [ - "## Setup\n", - "\n", - "First, we install TorchGeo." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "019092f0", - "metadata": {}, - "outputs": [], - "source": [ - "%pip install torchgeo planetary_computer" - ] - }, - { - "cell_type": "markdown", - "id": "4db9f791", - "metadata": { - "id": "dV0NLHfGBMWl" - }, - "source": [ - "## Imports\n", - "\n", - "Next, we import TorchGeo and any other libraries we need." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3d92b0f1", - "metadata": { - "id": "entire-albania" - }, - "outputs": [], - "source": [ - "import os\n", - "import tempfile\n", - "\n", - "import planetary_computer\n", - "from torch.utils.data import DataLoader\n", - "\n", - "from torchgeo.datasets import NAIP, ChesapeakeDE, stack_samples\n", - "from torchgeo.datasets.utils import download_url\n", - "from torchgeo.samplers import RandomGeoSampler" - ] - }, - { - "cell_type": "markdown", - "id": "7f26e4b8", - "metadata": { - "id": "5rLknZxrBEMz" - }, - "source": [ - "## Datasets\n", - "\n", - "For this tutorial, we'll be using imagery from the [National Agriculture Imagery Program (NAIP)](https://catalog.data.gov/dataset/national-agriculture-imagery-program-naip) and labels from the [Chesapeake Bay High-Resolution Land Cover Project](https://www.chesapeakeconservancy.org/conservation-innovation-center/high-resolution-data/land-cover-data-project/). First, we manually download a few NAIP tiles and create a PyTorch Dataset." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4a39af46", - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 232, - "referenced_widgets": [ - "d00a2177bf4b4b8191bfc8796f0e749f", - "17d6b81aec50455989276b595457cc7f", - "06ccd130058b432dbfa025c102eaeb27", - "6bc5b9872b574cb5aa6ebd1d44e7a71f", - "f7746f028f874a85b6101185fc9a8efc", - "f7ef78d6f87a4a2685788e395525fa7c", - "5b2450e316e64b4ba432c78b63275124", - "d3bbd6112f144c77bc68e5f2a7a355ff", - "a0300b1252cd4da5a798b55c15f8f5fd", - "793c2851b6464b398f7b4d2f2f509722", - "8dd61c8479d74c95a55de147e04446b3", - "b57d15e6c32b4fff8994ae67320972f6", - "9a34f8907a264232adf6b0d0543461dd", - "e680eda3c84c440083e2959f04431bea", - "a073e33fd9ae4125822fc17971233770", - "87faaa32454a42939d3bd405e726228c", - "b3d4c9c99bec4e69a199e45920d52ce4", - "a215f3310ea543d1a8991f57ec824872", - "569f60397fd6440d825e8afb83b4e1ae", - "b7f604d2ba4e4328a451725973fa755f", - "737fa148dfae49a18cc0eabbe05f2d0f", - "0b6613adbcc74165a9d9f74988af366e", - "b25f274c737d4212b3ffeedb2372ba22", - "ef0fc75ff5044171be942a6b3ba0c2da", - "612d84013a6e4890a48eb229f6431233", - "9a689285370646ab800155432ea042a5", - "014ed48a23234e8b81dd7ac4dbf95817", - "93c536a27b024728a00486b1f68b4dde", - "8a8538a91a74439b81e3f7c6516763e3", - "caf540562b484594bab8d6210dd7c2c1", - "99cd2e65fb104380953745f2e0a93fac", - "c5b818707bb64c5a865236a46399cea2", - "54f5db9555c44efa9370cbb7ab58e142", - "1d83b20dbb9c4c6a9d5c100fe4770ba4", - "c51b2400ca9442a9a9e0712d5778cd9a", - "bd2e44a8eb1a4c19a32da5a1edd647d1", - "0f9feea4b8344a7f8054c9417150825e", - "31acb7a1ca8940078e1aacd72e547f47", - "0d0ca8d64d3e4c2f88d87342808dd677", - "54402c5f8df34b83b95c94104b26e2c6", - "910b98584fa74bb5ad308fe770f5b40e", - "b2dce834ee044d69858389178b493a2b", - "237f2e31bcfe476baafae8d922877e07", - "43ac7d95481b4ea3866feef6ace2f043" - ] - }, - "id": "e3138ac3", - "outputId": "11589c46-eee6-455d-839b-390f2934d834" - }, - "outputs": [], - "source": [ - "naip_root = os.path.join(tempfile.gettempdir(), 'naip')\n", - "naip_url = (\n", - " 'https://naipeuwest.blob.core.windows.net/naip/v002/de/2018/de_060cm_2018/38075/'\n", - ")\n", - "tiles = [\n", - " 'm_3807511_ne_18_060_20181104.tif',\n", - " 'm_3807511_se_18_060_20181104.tif',\n", - " 'm_3807512_nw_18_060_20180815.tif',\n", - " 'm_3807512_sw_18_060_20180815.tif',\n", - "]\n", - "for tile in tiles:\n", - " url = planetary_computer.sign(naip_url + tile)\n", - " download_url(url, naip_root, filename=tile)\n", - "\n", - "naip = NAIP(naip_root)" - ] - }, - { - "cell_type": "markdown", - "id": "e25bad40", - "metadata": { - "id": "HQVji2B22Qfu" - }, - "source": [ - "Next, we tell TorchGeo to automatically download the corresponding Chesapeake labels." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "689bb2b0", - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "2Ah34KAw2biY", - "outputId": "03b7bdf0-78c1-4a13-ac56-59de740d7f59" - }, - "outputs": [], - "source": [ - "chesapeake_root = os.path.join(tempfile.gettempdir(), 'chesapeake')\n", - "os.makedirs(chesapeake_root, exist_ok=True)\n", - "chesapeake = ChesapeakeDE(chesapeake_root, crs=naip.crs, res=naip.res, download=True)" - ] - }, - { - "cell_type": "markdown", - "id": "56f2d78b", - "metadata": { - "id": "OWUhlfpD22IX" - }, - "source": [ - "Finally, we create an IntersectionDataset so that we can automatically sample from both GeoDatasets simultaneously." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "daefbc4d", - "metadata": { - "id": "WXxy8F8l2-aC" - }, - "outputs": [], - "source": [ - "dataset = naip & chesapeake" - ] - }, - { - "cell_type": "markdown", - "id": "ded44652", - "metadata": { - "id": "yF_R54Yf3EUd" - }, - "source": [ - "## Sampler\n", - "\n", - "Unlike typical PyTorch Datasets, TorchGeo GeoDatasets are indexed using lat/long/time bounding boxes. This requires us to use a custom GeoSampler instead of the default sampler/batch_sampler that comes with PyTorch." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b8a0d99c", - "metadata": { - "id": "RLczuU293itT" - }, - "outputs": [], - "source": [ - "sampler = RandomGeoSampler(dataset, size=1000, length=10)" - ] - }, - { - "cell_type": "markdown", - "id": "5b8c1c52", - "metadata": { - "id": "OWa-mmYd8S6K" - }, - "source": [ - "## DataLoader\n", - "\n", - "Now that we have a Dataset and Sampler, we can combine these into a single DataLoader." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "96faa142", - "metadata": { - "id": "jfx-9ZmU8ZTc" - }, - "outputs": [], - "source": [ - "dataloader = DataLoader(dataset, sampler=sampler, collate_fn=stack_samples)" - ] - }, - { - "cell_type": "markdown", - "id": "64ae63f7", - "metadata": { - "id": "HZIfqqW58oZe" - }, - "source": [ - "## Training\n", - "\n", - "Other than that, the rest of the training pipeline is the same as it is for torchvision." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8a2b44f8", - "metadata": { - "id": "7sGmNvBy8uIg" - }, - "outputs": [], - "source": [ - "for sample in dataloader:\n", - " image = sample['image']\n", - " target = sample['mask']" - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [], - "name": "getting_started.ipynb", - "provenance": [] - }, - "execution": { - "timeout": 1200 - }, - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.8" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/tutorials/getting_started.rst b/docs/tutorials/getting_started.rst new file mode 100644 index 00000000000..8049a95a20f --- /dev/null +++ b/docs/tutorials/getting_started.rst @@ -0,0 +1,18 @@ +Getting Started +=============== + +New to deep learning or remote sensing? First time using PyTorch or TorchGeo? You've come to the right place. + +The following tutorials will teach you enough to get started: + +* `Introduction to PyTorch `_: A brief overview of deep learning with PyTorch +* `Introduction to Geospatial Data `_: A brief overview of the challenges of working with geospatial data +* `Introduction to TorchGeo `_: A brief overview of the design of TorchGeo + +.. toctree:: + :hidden: + :maxdepth: 1 + + pytorch + geospatial + torchgeo diff --git a/docs/user/contributing.rst b/docs/user/contributing.rst index ae3cdd41045..033fcecdea2 100644 --- a/docs/user/contributing.rst +++ b/docs/user/contributing.rst @@ -1,3 +1,5 @@ +.. _contributing: + Contributing ============ From 50a5b265f9712883fb8d804001f81a2173ef2eb1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 00:34:22 +0100 Subject: [PATCH 10/35] Bump pypa/gh-action-pypi-publish from 1.12.2 to 1.12.3 (#2464) Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.12.2 to 1.12.3. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.12.2...v1.12.3) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish 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/deploy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 73e3bfbf269..40878faf794 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -42,4 +42,4 @@ jobs: name: pypi-dist path: dist/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.12.2 + uses: pypa/gh-action-pypi-publish@v1.12.3 From 5ceccea59ef24918d2cdb12d68c5fef77135733e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 00:34:44 +0100 Subject: [PATCH 11/35] Bump codecov/codecov-action from 5.0.7 to 5.1.1 (#2463) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.0.7 to 5.1.1. - [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/v5.0.7...v5.1.1) --- updated-dependencies: - dependency-name: codecov/codecov-action 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/tests.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 05ac0754060..8cc435460b1 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -48,7 +48,7 @@ jobs: pytest --cov --cov-report=xml python3 -m torchgeo --help - name: Report coverage - uses: codecov/codecov-action@v5.0.7 + uses: codecov/codecov-action@v5.1.1 with: token: ${{ secrets.CODECOV_TOKEN }} minimum: @@ -82,7 +82,7 @@ jobs: pytest --cov --cov-report=xml python3 -m torchgeo --help - name: Report coverage - uses: codecov/codecov-action@v5.0.7 + uses: codecov/codecov-action@v5.1.1 with: token: ${{ secrets.CODECOV_TOKEN }} datasets: @@ -114,7 +114,7 @@ jobs: pytest --cov --cov-report=xml python3 -m torchgeo --help - name: Report coverage - uses: codecov/codecov-action@v5.0.7 + uses: codecov/codecov-action@v5.1.1 with: token: ${{ secrets.CODECOV_TOKEN }} concurrency: From a05554888750ca7cc94176f0918ee8d3c8876c52 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 00:35:09 +0100 Subject: [PATCH 12/35] Bump actions/cache from 4.1.2 to 4.2.0 (#2462) Bumps [actions/cache](https://github.com/actions/cache) from 4.1.2 to 4.2.0. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v4.1.2...v4.2.0) --- updated-dependencies: - dependency-name: actions/cache 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 | 4 ++-- .github/workflows/style.yaml | 4 ++-- .github/workflows/tests.yaml | 6 +++--- .github/workflows/tutorials.yaml | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 8b82486f327..301ce26efe8 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -22,7 +22,7 @@ jobs: with: python-version: '3.12' - name: Cache dependencies - uses: actions/cache@v4.1.2 + uses: actions/cache@v4.2.0 id: cache with: path: ${{ env.pythonLocation }} @@ -51,7 +51,7 @@ jobs: with: python-version: '3.12' - name: Cache dependencies - uses: actions/cache@v4.1.2 + uses: actions/cache@v4.2.0 id: cache with: path: ${{ env.pythonLocation }} diff --git a/.github/workflows/style.yaml b/.github/workflows/style.yaml index b0a63f54478..73b26cac561 100644 --- a/.github/workflows/style.yaml +++ b/.github/workflows/style.yaml @@ -24,7 +24,7 @@ jobs: with: python-version: '3.12' - name: Cache dependencies - uses: actions/cache@v4.1.2 + uses: actions/cache@v4.2.0 id: cache with: path: ${{ env.pythonLocation }} @@ -50,7 +50,7 @@ jobs: with: python-version: '3.12' - name: Cache dependencies - uses: actions/cache@v4.1.2 + uses: actions/cache@v4.2.0 id: cache with: path: ${{ env.pythonLocation }} diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 8cc435460b1..9d1242697c1 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -28,7 +28,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Cache dependencies - uses: actions/cache@v4.1.2 + uses: actions/cache@v4.2.0 id: cache with: path: ${{ env.pythonLocation }} @@ -63,7 +63,7 @@ jobs: with: python-version: '3.10' - name: Cache dependencies - uses: actions/cache@v4.1.2 + uses: actions/cache@v4.2.0 id: cache with: path: ${{ env.pythonLocation }} @@ -97,7 +97,7 @@ jobs: with: python-version: '3.12' - name: Cache dependencies - uses: actions/cache@v4.1.2 + uses: actions/cache@v4.2.0 id: cache with: path: ${{ env.pythonLocation }} diff --git a/.github/workflows/tutorials.yaml b/.github/workflows/tutorials.yaml index 1183b40fe24..de0da385b08 100644 --- a/.github/workflows/tutorials.yaml +++ b/.github/workflows/tutorials.yaml @@ -26,7 +26,7 @@ jobs: with: python-version: '3.12' - name: Cache dependencies - uses: actions/cache@v4.1.2 + uses: actions/cache@v4.2.0 id: cache with: path: ${{ env.pythonLocation }} From f80f62a718024a6a44c2efdbc7276eda0b525b43 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 00:35:36 +0100 Subject: [PATCH 13/35] Bump prettier from 3.4.1 to 3.4.2 (#2461) Bumps [prettier](https://github.com/prettier/prettier) from 3.4.1 to 3.4.2. - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/3.4.1...3.4.2) --- updated-dependencies: - dependency-name: prettier 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> --- package-lock.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 07cbc636bd9..b57536fde23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,9 @@ } }, "node_modules/prettier": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.1.tgz", - "integrity": "sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==", - "license": "MIT", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "bin": { "prettier": "bin/prettier.cjs" }, From ff3d087f76cd3df2ee01c3b7999f82459785a0a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 04:36:24 +0000 Subject: [PATCH 14/35] Bump numpy from 2.1.3 to 2.2.0 in /requirements (#2465) * Bump numpy from 2.1.3 to 2.2.0 in /requirements Bumps [numpy](https://github.com/numpy/numpy) from 2.1.3 to 2.2.0. - [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/v2.1.3...v2.2.0) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Ensure that array dtype is static * Workaround for typing bug --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Adam J. Stewart --- requirements/required.txt | 2 +- torchgeo/datasets/chesapeake.py | 6 ++---- torchgeo/datasets/landcoverai.py | 3 +-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/requirements/required.txt b/requirements/required.txt index 288fc286918..9e9074a826a 100644 --- a/requirements/required.txt +++ b/requirements/required.txt @@ -8,7 +8,7 @@ kornia==0.7.4 lightly==1.5.15 lightning[pytorch-extra]==2.4.0 matplotlib==3.9.3 -numpy==2.1.3 +numpy==2.2.0 pandas==2.2.3 pillow==11.0.0 pyproj==3.7.0 diff --git a/torchgeo/datasets/chesapeake.py b/torchgeo/datasets/chesapeake.py index 8843fd9ad86..459d096043c 100644 --- a/torchgeo/datasets/chesapeake.py +++ b/torchgeo/datasets/chesapeake.py @@ -476,13 +476,11 @@ def __init__( lc_colors = np.zeros((max(self.lc_cmap.keys()) + 1, 4)) lc_colors[list(self.lc_cmap.keys())] = list(self.lc_cmap.values()) - lc_colors = lc_colors[:, :3] / 255 - self._lc_cmap = ListedColormap(lc_colors) + self._lc_cmap = ListedColormap(lc_colors[:, :3] / 255) nlcd_colors = np.zeros((max(NLCD.cmap.keys()) + 1, 4)) nlcd_colors[list(NLCD.cmap.keys())] = list(NLCD.cmap.values()) - nlcd_colors = nlcd_colors[:, :3] / 255 - self._nlcd_cmap = ListedColormap(nlcd_colors) + self._nlcd_cmap = ListedColormap(nlcd_colors[:, :3] / 255) # Add all tiles into the index in epsg:3857 based on the included geojson mint: float = 0 diff --git a/torchgeo/datasets/landcoverai.py b/torchgeo/datasets/landcoverai.py index e0ff04755bf..d2c0acf88e7 100644 --- a/torchgeo/datasets/landcoverai.py +++ b/torchgeo/datasets/landcoverai.py @@ -95,8 +95,7 @@ def __init__( lc_colors = np.zeros((max(self.cmap.keys()) + 1, 4)) lc_colors[list(self.cmap.keys())] = list(self.cmap.values()) - lc_colors = lc_colors[:, :3] / 255 - self._lc_cmap = ListedColormap(lc_colors) + self._lc_cmap = ListedColormap(lc_colors[:, :3] / 255) self._verify() From 7e63038a56376198d83980fa154a36a8e96dd6a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:34:59 +0100 Subject: [PATCH 15/35] Bump ruff from 0.8.2 to 0.8.3 in /requirements (#2468) --- requirements/style.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/style.txt b/requirements/style.txt index ca9398e5e49..bf142f51306 100644 --- a/requirements/style.txt +++ b/requirements/style.txt @@ -1,3 +1,3 @@ # style mypy==1.13.0 -ruff==0.8.2 +ruff==0.8.3 From 9ed8a603f94fdbc78fd906eccc60c2cf2b6a1c30 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Mon, 16 Dec 2024 15:54:27 +0100 Subject: [PATCH 16/35] Tutorials: fix dataset contribution tutorial (#2469) --- docs/tutorials/contribute_non_geo_dataset.ipynb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/contribute_non_geo_dataset.ipynb b/docs/tutorials/contribute_non_geo_dataset.ipynb index 4a702222daf..2ea23365a41 100644 --- a/docs/tutorials/contribute_non_geo_dataset.ipynb +++ b/docs/tutorials/contribute_non_geo_dataset.ipynb @@ -219,13 +219,15 @@ "source": [ "import os\n", "import shutil\n", + "import tempfile\n", "\n", "import numpy as np\n", "from PIL import Image\n", "\n", "# Define the root directory and subdirectories\n", - "root_dir = 'my_new_dataset'\n", - "sub_dirs = ['sub_dir_1', 'sub_dir_2', 'sub_dir_3']\n", + "# Normally this would be the current directory (tests/data/my_new_dataset)\n", + "root_dir = os.path.join(tempfile.gettempdir(), 'my_new_dataset')\n", + "sub_dirs = ['image', 'target']\n", "splits = ['train', 'val', 'test']\n", "\n", "image_file_names = ['sample_1.png', 'sample_2.png', 'sample_3.png']\n", From a2acc0de70aa34e9e32a211513151b098add46e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 23:37:21 +0100 Subject: [PATCH 17/35] Bump matplotlib from 3.9.3 to 3.10.0 in /requirements (#2471) Bumps [matplotlib](https://github.com/matplotlib/matplotlib) from 3.9.3 to 3.10.0. - [Release notes](https://github.com/matplotlib/matplotlib/releases) - [Commits](https://github.com/matplotlib/matplotlib/compare/v3.9.3...v3.10.0) --- updated-dependencies: - dependency-name: matplotlib 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> --- requirements/required.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/required.txt b/requirements/required.txt index 9e9074a826a..c5b651ad3ba 100644 --- a/requirements/required.txt +++ b/requirements/required.txt @@ -7,7 +7,7 @@ fiona==1.10.1 kornia==0.7.4 lightly==1.5.15 lightning[pytorch-extra]==2.4.0 -matplotlib==3.9.3 +matplotlib==3.10.0 numpy==2.2.0 pandas==2.2.3 pillow==11.0.0 From 60ba6cb209dc4f042886ffba81d85164b7863702 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 23:37:41 +0100 Subject: [PATCH 18/35] Bump scikit-image from 0.24.0 to 0.25.0 in /requirements (#2472) Bumps [scikit-image](https://github.com/scikit-image/scikit-image) from 0.24.0 to 0.25.0. - [Release notes](https://github.com/scikit-image/scikit-image/releases) - [Changelog](https://github.com/scikit-image/scikit-image/blob/main/RELEASE.txt) - [Commits](https://github.com/scikit-image/scikit-image/compare/v0.24.0...v0.25.0) --- updated-dependencies: - dependency-name: scikit-image 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> --- requirements/datasets.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/datasets.txt b/requirements/datasets.txt index 336ab01fcf6..11a82e9276c 100644 --- a/requirements/datasets.txt +++ b/requirements/datasets.txt @@ -5,5 +5,5 @@ opencv-python==4.10.0.84 pandas[parquet]==2.2.3 pycocotools==2.0.8 pyvista==0.44.2 -scikit-image==0.24.0 +scikit-image==0.25.0 scipy==1.14.1 From 58dc3217f4fcb63240105cf02ebcfc23ee88325d Mon Sep 17 00:00:00 2001 From: lhackel-tub <123642395+lhackel-tub@users.noreply.github.com> Date: Thu, 19 Dec 2024 19:26:36 +0100 Subject: [PATCH 19/35] Fixed the bandorder of EuroSAT dataset, datamodule and within the tutorial to align with [the issue raised in the EuroSAT git repository](https://github.com/phelber/EuroSAT/issues/7) (#2480) --- docs/tutorials/transforms.ipynb | 2 +- torchgeo/datamodules/eurosat.py | 40 ++++++++++++++++----------------- torchgeo/datasets/eurosat.py | 2 +- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/tutorials/transforms.ipynb b/docs/tutorials/transforms.ipynb index 42a16e26e24..689b2eebd33 100644 --- a/docs/tutorials/transforms.ipynb +++ b/docs/tutorials/transforms.ipynb @@ -216,11 +216,11 @@ " 'B06': 'Vegetation Red Edge 2',\n", " 'B07': 'Vegetation Red Edge 3',\n", " 'B08': 'NIR 1',\n", - " 'B8A': 'NIR 2',\n", " 'B09': 'Water Vapour',\n", " 'B10': 'SWIR 1',\n", " 'B11': 'SWIR 2',\n", " 'B12': 'SWIR 3',\n", + " 'B8A': 'NIR 2',\n", "}" ] }, diff --git a/torchgeo/datamodules/eurosat.py b/torchgeo/datamodules/eurosat.py index 23e09610968..6b8b9115d1c 100644 --- a/torchgeo/datamodules/eurosat.py +++ b/torchgeo/datamodules/eurosat.py @@ -19,11 +19,11 @@ 'B06': 2130.3491, 'B07': 2524.0549, 'B08': 2454.1938, - 'B8A': 785.4963, - 'B09': 12.4639, - 'B10': 1969.9224, - 'B11': 1206.2421, - 'B12': 2779.4104, + 'B09': 785.4963, + 'B10': 12.4639, + 'B11': 1969.9224, + 'B12': 1206.2421, + 'B8A': 2779.4104, } SPATIAL_STD = { @@ -35,11 +35,11 @@ 'B06': 806.8271, 'B07': 1022.6378, 'B08': 1065.4312, - 'B8A': 410.5831, - 'B09': 4.8878, - 'B10': 958.4751, - 'B11': 740.6196, - 'B12': 1157.2896, + 'B09': 410.5831, + 'B10': 4.8878, + 'B11': 958.4751, + 'B12': 740.6196, + 'B8A': 1157.2896, } MEAN = { @@ -51,11 +51,11 @@ 'B06': 1999.79090914, 'B07': 2369.22292565, 'B08': 2296.82608323, - 'B8A': 732.08340178, - 'B09': 12.11327804, - 'B10': 1819.01027855, - 'B11': 1118.92391149, - 'B12': 2594.14080798, + 'B09': 732.08340178, + 'B10': 12.11327804, + 'B11': 1819.01027855, + 'B12': 1118.92391149, + 'B8A': 2594.14080798, } STD = { @@ -67,11 +67,11 @@ 'B06': 861.18399006, 'B07': 1086.63139075, 'B08': 1117.98170791, - 'B8A': 404.91978886, - 'B09': 4.77584468, - 'B10': 1002.58768311, - 'B11': 761.30323499, - 'B12': 1231.58581042, + 'B09': 404.91978886, + 'B10': 4.77584468, + 'B11': 1002.58768311, + 'B12': 761.30323499, + 'B8A': 1231.58581042, } diff --git a/torchgeo/datasets/eurosat.py b/torchgeo/datasets/eurosat.py index 45c0d708f92..5caef0ee4ee 100644 --- a/torchgeo/datasets/eurosat.py +++ b/torchgeo/datasets/eurosat.py @@ -84,11 +84,11 @@ class EuroSAT(NonGeoClassificationDataset): 'B06', 'B07', 'B08', - 'B8A', 'B09', 'B10', 'B11', 'B12', + 'B8A', ) rgb_bands = ('B04', 'B03', 'B02') From 686767db09d5836f6812280d39021cc7a86e3673 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Thu, 19 Dec 2024 19:40:19 +0100 Subject: [PATCH 20/35] Add consistent author and copyright info to all tutorials (#2478) --- docs/tutorials/basic_usage.rst | 2 +- docs/tutorials/contribute_datamodule.ipynb | 10 ++++++++++ docs/tutorials/custom_raster_dataset.ipynb | 15 +++++++++------ docs/tutorials/earth_surface_water.ipynb | 10 ++++++++++ docs/tutorials/indices.ipynb | 17 ++++++++++------- docs/tutorials/trainers.ipynb | 15 +++++++++------ 6 files changed, 49 insertions(+), 20 deletions(-) diff --git a/docs/tutorials/basic_usage.rst b/docs/tutorials/basic_usage.rst index 10fcb97b42d..57848bede19 100644 --- a/docs/tutorials/basic_usage.rst +++ b/docs/tutorials/basic_usage.rst @@ -4,7 +4,7 @@ Basic Usage The following tutorials introduce the basic concepts and components of TorchGeo: * `Transforms `_: Preprocessing and data augmentation transforms for geospatial data -* `Indices `_: Spectral indices +* `Spectral Indices `_: Visualizing and appending spectral indices * `Pretrained Weights `_: Models and pretrained weights * `Lightning Trainers `_: PyTorch Lightning data modules and trainers diff --git a/docs/tutorials/contribute_datamodule.ipynb b/docs/tutorials/contribute_datamodule.ipynb index 1bbb728e0e3..3182213c510 100644 --- a/docs/tutorials/contribute_datamodule.ipynb +++ b/docs/tutorials/contribute_datamodule.ipynb @@ -1,5 +1,15 @@ { "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright (c) Microsoft Corporation. All rights reserved.\n", + "# Licensed under the MIT License." + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/docs/tutorials/custom_raster_dataset.ipynb b/docs/tutorials/custom_raster_dataset.ipynb index 0e2785bb7ff..f171c31363b 100644 --- a/docs/tutorials/custom_raster_dataset.ipynb +++ b/docs/tutorials/custom_raster_dataset.ipynb @@ -1,14 +1,15 @@ { "cells": [ { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": { "id": "iiqWbXISOEAQ" }, + "outputs": [], "source": [ - "Copyright (c) Microsoft Corporation. All rights reserved.\n", - "\n", - "Licensed under the MIT License." + "# Copyright (c) Microsoft Corporation. All rights reserved.\n", + "# Licensed under the MIT License." ] }, { @@ -19,6 +20,8 @@ "source": [ "# Custom Raster Datasets\n", "\n", + "_Written by: Ritwik Gupta_\n", + "\n", "In this tutorial, we'll describe how to write a custom dataset in TorchGeo. There are many types of datasets that you may encounter, from image data, to segmentation masks, to point labels. We'll focus on the most common type of dataset: a raster file containing an image or mask. Let's get started!" ] }, @@ -562,9 +565,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.13.0" } }, "nbformat": 4, - "nbformat_minor": 1 + "nbformat_minor": 4 } diff --git a/docs/tutorials/earth_surface_water.ipynb b/docs/tutorials/earth_surface_water.ipynb index e44d1e385f7..f65aedbf726 100644 --- a/docs/tutorials/earth_surface_water.ipynb +++ b/docs/tutorials/earth_surface_water.ipynb @@ -1,5 +1,15 @@ { "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright (c) Microsoft Corporation. All rights reserved.\n", + "# Licensed under the MIT License." + ] + }, { "cell_type": "markdown", "metadata": { diff --git a/docs/tutorials/indices.ipynb b/docs/tutorials/indices.ipynb index 30576609ac8..5695cd51b0d 100644 --- a/docs/tutorials/indices.ipynb +++ b/docs/tutorials/indices.ipynb @@ -1,14 +1,15 @@ { "cells": [ { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": { "id": "DYndcZst_kdr" }, + "outputs": [], "source": [ - "Copyright (c) Microsoft Corporation. All rights reserved.\n", - "\n", - "Licensed under the MIT License." + "# Copyright (c) Microsoft Corporation. All rights reserved.\n", + "# Licensed under the MIT License." ] }, { @@ -17,7 +18,9 @@ "id": "ZKIkyiLScf9P" }, "source": [ - "# Indices" + "# Spectral Indices\n", + "\n", + "_Written by: Isaac A. Corley_" ] }, { @@ -374,9 +377,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.13.0" } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 4 } diff --git a/docs/tutorials/trainers.ipynb b/docs/tutorials/trainers.ipynb index 5de3937a026..0394f3faebc 100644 --- a/docs/tutorials/trainers.ipynb +++ b/docs/tutorials/trainers.ipynb @@ -1,15 +1,16 @@ { "cells": [ { - "cell_type": "markdown", - "id": "b13c2251", + "cell_type": "code", + "execution_count": null, + "id": "16421d50-8d7a-4972-b06f-160fd890cc86", "metadata": { "id": "b13c2251" }, + "outputs": [], "source": [ - "Copyright (c) Microsoft Corporation. All rights reserved.\n", - "\n", - "Licensed under the MIT License." + "# Copyright (c) Microsoft Corporation. All rights reserved.\n", + "# Licensed under the MIT License." ] }, { @@ -21,6 +22,8 @@ "source": [ "# Lightning Trainers\n", "\n", + "_Written by: Caleb Robinson_\n", + "\n", "In this tutorial, we demonstrate TorchGeo trainers to train and test a model. We will use the [EuroSAT](https://torchgeo.readthedocs.io/en/stable/api/datasets.html#eurosat) dataset throughout this tutorial. Specifically, a subset containing only 100 images. We will train models to predict land cover classes.\n", "\n", "It's recommended to run this notebook on Google Colab if you don't have your own GPU. Click the \"Open in Colab\" button above to get started." @@ -328,7 +331,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.13.0" } }, "nbformat": 4, From f87d686fe1e53f4793ed6fdfeaac3cf03af39c2e Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Thu, 19 Dec 2024 19:58:01 +0100 Subject: [PATCH 21/35] Docs: remove mathcal (#2467) --- docs/tutorials/geospatial.ipynb | 2 +- docs/tutorials/pytorch.ipynb | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/tutorials/geospatial.ipynb b/docs/tutorials/geospatial.ipynb index 844ce5b8747..880ad1ab998 100644 --- a/docs/tutorials/geospatial.ipynb +++ b/docs/tutorials/geospatial.ipynb @@ -126,7 +126,7 @@ "\n", "Similar to radar, lidar is another active remote sensing method that replaces microwave pulses with lasers. By measuring the time it takes light to reflect off of an object and return to the sensor, we can generate a 3D point cloud mapping object structures. Mathematically, our dataset would then become:\n", "\n", - "$$\\mathcal{D} = \\left\\{\\left(x^{(i)}, y^{(i)}, z^{(i)}\\right)\\right\\}_{i=1}^N$$\n", + "$$D = \\left\\{\\left(x^{(i)}, y^{(i)}, z^{(i)}\\right)\\right\\}_{i=1}^N$$\n", "\n", "This technology is frequently used in several different application domains:\n", "\n", diff --git a/docs/tutorials/pytorch.ipynb b/docs/tutorials/pytorch.ipynb index db7bcdb4c88..b66d3cc95b8 100644 --- a/docs/tutorials/pytorch.ipynb +++ b/docs/tutorials/pytorch.ipynb @@ -115,9 +115,9 @@ "\n", "In order to learn by example, we first need examples. In machine learning, we construct datasets of the form:\n", "\n", - "$$\\mathcal{D} = \\left\\{\\left(x^{(i)}, y^{(i)}\\right)\\right\\}_{i=1}^N$$\n", + "$$D = \\left\\{\\left(x^{(i)}, y^{(i)}\\right)\\right\\}_{i=1}^N$$\n", "\n", - "Written in English, dataset $\\mathcal{D}$ is composed of $N$ pairs of inputs $x$ and expected outputs $y$. $x$ and $y$ can be tabular data, images, text, or any other object that can be represented mathematically.\n", + "Written in English, dataset $D$ is composed of $N$ pairs of inputs $x$ and expected outputs $y$. $x$ and $y$ can be tabular data, images, text, or any other object that can be represented mathematically.\n", "\n", "![EuroSAT](https://github.com/phelber/EuroSAT/blob/master/eurosat-overview.png?raw=true)\n", "\n", @@ -261,11 +261,11 @@ "\n", "If $y$ is our expected output (also called \"ground truth\") and $\\hat{y}$ is our predicted output, our goal is to minimize the difference between $y$ and $\\hat{y}$. This difference is referred to as *error* or *loss*, and the loss function tells us how big of a mistake we made. For regression tasks, a simple mean squared error is sufficient:\n", "\n", - "$$\\mathcal{L}(y, \\hat{y}) = \\left(y - \\hat{y}\\right)^2$$\n", + "$$L(y, \\hat{y}) = \\left(y - \\hat{y}\\right)^2$$\n", "\n", "For classification tasks, such as EuroSAT, we instead use a negative log-likelihood:\n", "\n", - "$$\\mathcal{L}_c(y, \\hat{y}) = - \\sum_{c=1}^C \\mathbb{1}_{y=\\hat{y}}\\log{p_c}$$\n", + "$$L_c(y, \\hat{y}) = - \\sum_{c=1}^C \\mathbb{1}_{y=\\hat{y}}\\log{p_c}$$\n", "\n", "where $\\mathbb{1}$ is the indicator function and $p_c$ is the probability with which the model predicts class $c$. By normalizing this over the log probability of all classes, we get the cross-entropy loss." ] @@ -289,7 +289,7 @@ "\n", "In order to minimize our loss, we compute the gradient of the loss function with respect to model parameters $\\theta$. We then take a small step $\\alpha$ (also called the *learning rate*) in the direction of the negative gradient to update our model parameters in a process called *backpropagation*:\n", "\n", - "$$\\theta \\leftarrow \\theta - \\alpha \\nabla_\\theta \\mathcal{L}(y, \\hat{y})$$\n", + "$$\\theta \\leftarrow \\theta - \\alpha \\nabla_\\theta L(y, \\hat{y})$$\n", "\n", "When done one image or one mini-batch at a time, this is known as *stochastic gradient descent* (SGD)." ] From 7bcb6d4ad1d5567ef6f3a8c160fb4ae66d9cc629 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Thu, 19 Dec 2024 22:37:19 +0100 Subject: [PATCH 22/35] Add TorchGeo CLI tutorial (#2479) * Add TorchGeo CLI tutorial * Pass checkpoint path --- docs/tutorials/basic_usage.rst | 2 + docs/tutorials/cli.ipynb | 292 +++++++++++++++++++++++++++++++++ 2 files changed, 294 insertions(+) create mode 100644 docs/tutorials/cli.ipynb diff --git a/docs/tutorials/basic_usage.rst b/docs/tutorials/basic_usage.rst index 57848bede19..51eb77a85ca 100644 --- a/docs/tutorials/basic_usage.rst +++ b/docs/tutorials/basic_usage.rst @@ -7,6 +7,7 @@ The following tutorials introduce the basic concepts and components of TorchGeo: * `Spectral Indices `_: Visualizing and appending spectral indices * `Pretrained Weights `_: Models and pretrained weights * `Lightning Trainers `_: PyTorch Lightning data modules and trainers +* `Command-Line Interface `_: TorchGeo's command-line interface .. toctree:: :hidden: @@ -16,3 +17,4 @@ The following tutorials introduce the basic concepts and components of TorchGeo: indices pretrained_weights trainers + cli diff --git a/docs/tutorials/cli.ipynb b/docs/tutorials/cli.ipynb new file mode 100644 index 00000000000..1424b13291a --- /dev/null +++ b/docs/tutorials/cli.ipynb @@ -0,0 +1,292 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "16421d50-8d7a-4972-b06f-160fd890cc86", + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright (c) Microsoft Corporation. All rights reserved.\n", + "# Licensed under the MIT License." + ] + }, + { + "cell_type": "markdown", + "id": "e563313d", + "metadata": {}, + "source": [ + "# Command-Line Interface\n", + "\n", + "_Written by: Adam J. Stewart_\n", + "\n", + "TorchGeo provides a command-line interface based on [LightningCLI](https://lightning.ai/docs/pytorch/stable/api/lightning.pytorch.cli.LightningCLI.html) that allows users to combine our data modules and trainers from the comfort of the command line. This no-code solution can be attractive for both beginners and experts, as it offers flexibility and reproducibility. In this tutorial, we demonstrate some of the features of this interface." + ] + }, + { + "cell_type": "markdown", + "id": "8c1f4156", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First, we install TorchGeo. In addition to the Python library, this also installs a `torchgeo` executable." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3f0d31a8", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install torchgeo" + ] + }, + { + "cell_type": "markdown", + "id": "7801ab8b-0ee3-40ac-88c2-4bdc29bb4e1b", + "metadata": {}, + "source": [ + "## Subcommands\n", + "\n", + "The `torchgeo` command has a number of *subcommands* that can be run. The `--help` flag can be used to list them." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6ccac4e-7f20-4aa8-b851-27234ffd259f", + "metadata": {}, + "outputs": [], + "source": [ + "!torchgeo --help" + ] + }, + { + "cell_type": "markdown", + "id": "19ee017d-0d8f-41c6-8e7c-68495c7e62b6", + "metadata": {}, + "source": [ + "## Trainer\n", + "\n", + "Below, we run `--help` on the `fit` subcommand to see what options are available to us. `fit` is used to train and validate a model, and we can customize many aspects of the training process." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "afe1dc9d-4cee-43b0-ae30-200c64d3401a", + "metadata": {}, + "outputs": [], + "source": [ + "!torchgeo fit --help" + ] + }, + { + "cell_type": "markdown", + "id": "b437860c-b406-4150-b30b-8aa895eebfcd", + "metadata": {}, + "source": [ + "## Model\n", + "\n", + "We must first select an `nn.Module` model architecture to train and a `lightning.pytorch.LightningModule` trainer to train it. We will experiment with the `ClassificationTask` trainer and see what options we can customize. Any of TorchGeo's builtin trainers, or trainers written by the user, can be used in this way." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7cd9bbd0-17c9-4e87-b10d-ea846c39bc24", + "metadata": {}, + "outputs": [], + "source": [ + "!torchgeo fit --model.help ClassificationTask" + ] + }, + { + "cell_type": "markdown", + "id": "3daacd8d-64f4-4357-bdf3-759295a14224", + "metadata": {}, + "source": [ + "## Data\n", + "\n", + "We must also select a `Dataset` we would like to train on and a `lightning.pytorch.LightningDataModule` we can use to access the train/val/test split and any augmentations to apply to the data. Similarly, we use the `--help` flag to see what options are available for the `EuroSAT100` dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "136eb59f-6662-44af-82e9-c55bdb3f17ac", + "metadata": {}, + "outputs": [], + "source": [ + "!torchgeo fit --data.help EuroSAT100DataModule" + ] + }, + { + "cell_type": "markdown", + "id": "8039cb67-ee18-4b41-8bf5-0e939493f5bb", + "metadata": {}, + "source": [ + "## Config\n", + "\n", + "Now that we have seen all important configuration options, we can put them together in a YAML file. LightingCLI supports YAML, JSON, and command-line configuration. While we will write this file using Python in this tutorial, normally this file would be written in your favorite text editor." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e25c8efb-ed8c-4795-862c-bfb84cc84e1f", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import tempfile\n", + "\n", + "root = os.path.join(tempfile.gettempdir(), 'eurosat100')\n", + "config = f\"\"\"\n", + "trainer:\n", + " max_epochs: 1\n", + " default_root_dir: '{root}'\n", + "model:\n", + " class_path: ClassificationTask\n", + " init_args:\n", + " model: 'resnet18'\n", + " in_channels: 13\n", + " num_classes: 10\n", + "data:\n", + " class_path: EuroSAT100DataModule\n", + " init_args:\n", + " batch_size: 8\n", + " dict_kwargs:\n", + " root: '{root}'\n", + " download: true\n", + "\"\"\"\n", + "os.makedirs(root, exist_ok=True)\n", + "with open(os.path.join(root, 'config.yaml'), 'w') as f:\n", + " f.write(config)" + ] + }, + { + "cell_type": "markdown", + "id": "a661b8d7-2dc9-4a30-8842-bd52d130e080", + "metadata": {}, + "source": [ + "This YAML file has three sections:\n", + "\n", + "* trainer: Arguments to pass to the [Trainer](https://lightning.ai/docs/pytorch/stable/common/trainer.html)\n", + "* model: Arguments to pass to the task\n", + "* data: Arguments to pass to the data module\n", + "\n", + "The `class_path` gives the class to instantiate, `init_args` lists standard arguments, and `dict_kwargs` lists keyword arguments." + ] + }, + { + "cell_type": "markdown", + "id": "e132f933-4edf-42bb-b585-e0d8ceb65eab", + "metadata": {}, + "source": [ + "## Training\n", + "\n", + "We can now train our model like so." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f84b0739-c9e7-4057-8864-98ab69a11f64", + "metadata": {}, + "outputs": [], + "source": [ + "!torchgeo fit --config {root}/config.yaml" + ] + }, + { + "cell_type": "markdown", + "id": "cb1557f1-6cc0-46da-909c-836911acb248", + "metadata": {}, + "source": [ + "## Validation\n", + "\n", + "Now that we have a trained model, we can evaluate performance on the validation set. Note that we need to explicitly pass in the location of the checkpoint from the previous run." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b9cbb4f4-1879-4ae7-bae4-2c24d49a4a61", + "metadata": {}, + "outputs": [], + "source": [ + "import glob\n", + "\n", + "checkpoint = glob.glob(\n", + " os.path.join(root, 'lightning_logs', 'version_0', 'checkpoints', '*.ckpt')\n", + ")[0]\n", + "\n", + "!torchgeo validate --config {root}/config.yaml --ckpt_path {checkpoint}" + ] + }, + { + "cell_type": "markdown", + "id": "ba816fc3-5cac-4cbc-a6ef-effc6c9faa61", + "metadata": {}, + "source": [ + "## Testing\n", + "\n", + "After finishing our hyperparameter tuning, we can calculate and report the final test performance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f1faa997-9f81-4847-94fc-5a8bb7687369", + "metadata": {}, + "outputs": [], + "source": [ + "!torchgeo test --config {root}/config.yaml --ckpt_path {checkpoint}" + ] + }, + { + "cell_type": "markdown", + "id": "f5383d30-8f76-44a2-8366-e6fcbd1e6042", + "metadata": {}, + "source": [ + "## Additional Reading\n", + "\n", + "Lightning CLI has many more features that are worth learning. You can learn more by reading the following set of tutorials:\n", + "\n", + "* [Configure hyperparameters from the CLI](https://lightning.ai/docs/pytorch/stable/cli/lightning_cli.html)" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "provenance": [] + }, + "execution": { + "timeout": 1200 + }, + "gpuClass": "standard", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 0e7897419d2b9491d5454215091f8c11e232e0ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 22:19:26 +0000 Subject: [PATCH 23/35] Bump ruff from 0.8.3 to 0.8.4 in /requirements (#2481) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.8.3 to 0.8.4. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.8.3...0.8.4) --- updated-dependencies: - dependency-name: ruff 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 bf142f51306..622e35ab81c 100644 --- a/requirements/style.txt +++ b/requirements/style.txt @@ -1,3 +1,3 @@ # style mypy==1.13.0 -ruff==0.8.3 +ruff==0.8.4 From f789460533d25b02e5d10063539c38b97846d3f5 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Fri, 20 Dec 2024 00:09:01 +0100 Subject: [PATCH 24/35] README: correct syntax highlighting for console code (#2482) --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c27ffa9c426..b5fd835430b 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,8 @@ Testing: The recommended way to install TorchGeo is with [pip](https://pip.pypa.io/): -```console -$ pip install torchgeo +```sh +pip install torchgeo ``` For [conda](https://docs.conda.io/) and [spack](https://spack.io/) installation instructions, see the [documentation](https://torchgeo.readthedocs.io/en/stable/user/installation.html). @@ -192,7 +192,7 @@ trainer.fit(model=task, datamodule=datamodule) TorchGeo also supports command-line interface training using [LightningCLI](https://lightning.ai/docs/pytorch/stable/cli/lightning_cli.html). It can be invoked in two ways: -```console +```sh # If torchgeo has been installed torchgeo # If torchgeo has been installed, or if it has been cloned to the current directory @@ -201,7 +201,7 @@ python3 -m torchgeo It supports command-line configuration or YAML/JSON config files. Valid options can be found from the help messages: -```console +```sh # See valid stages torchgeo --help # See valid trainer options @@ -233,7 +233,7 @@ data: we can see the script in action: -```console +```sh # Train and validate a model torchgeo fit --config config.yaml # Validate-only From 4717f2d3b38aae004cfd68b9dc24eeced6d2123a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 22:07:56 +0100 Subject: [PATCH 25/35] Bump mypy from 1.13.0 to 1.14.0 in /requirements (#2483) --- requirements/style.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/style.txt b/requirements/style.txt index 622e35ab81c..06151a9f86b 100644 --- a/requirements/style.txt +++ b/requirements/style.txt @@ -1,3 +1,3 @@ # style -mypy==1.13.0 +mypy==1.14.0 ruff==0.8.4 From 97123f6f9577391fe7a3c28bb50b39af9ed27aa8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 22:27:44 +0100 Subject: [PATCH 26/35] Bump numpy from 2.2.0 to 2.2.1 in /requirements (#2488) Bumps [numpy](https://github.com/numpy/numpy) from 2.2.0 to 2.2.1. - [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/v2.2.0...v2.2.1) --- 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 c5b651ad3ba..d40386f4246 100644 --- a/requirements/required.txt +++ b/requirements/required.txt @@ -8,7 +8,7 @@ kornia==0.7.4 lightly==1.5.15 lightning[pytorch-extra]==2.4.0 matplotlib==3.10.0 -numpy==2.2.0 +numpy==2.2.1 pandas==2.2.3 pillow==11.0.0 pyproj==3.7.0 From beaa68451d63048158210f1104d2768b81ee143c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 22:28:08 +0100 Subject: [PATCH 27/35] Bump nbmake from 1.5.4 to 1.5.5 in /requirements (#2487) Bumps [nbmake](https://github.com/treebeardtech/nbmake) from 1.5.4 to 1.5.5. - [Release notes](https://github.com/treebeardtech/nbmake/releases) - [Commits](https://github.com/treebeardtech/nbmake/compare/v1.5.4...v1.5.5) --- updated-dependencies: - dependency-name: nbmake 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/tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/tests.txt b/requirements/tests.txt index 8a4d222b6b3..f79028e4de2 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1,4 +1,4 @@ # tests -nbmake==1.5.4 +nbmake==1.5.5 pytest==8.3.4 pytest-cov==6.0.0 From 0ef9a4b4a933a7b4f87318352bad1e9ef2e2c665 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 22:28:32 +0100 Subject: [PATCH 28/35] Bump actions/upload-artifact from 4.4.3 to 4.5.0 (#2486) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.4.3 to 4.5.0. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4.4.3...v4.5.0) --- updated-dependencies: - dependency-name: actions/upload-artifact 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/deploy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 40878faf794..3af8fd64935 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -21,7 +21,7 @@ jobs: - name: Build project run: python3 -m build - name: Upload artifacts - uses: actions/upload-artifact@v4.4.3 + uses: actions/upload-artifact@v4.5.0 with: name: pypi-dist path: dist/ From 6e6644e94e3de2d54e84ac11c3929159b1c73252 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 22:28:51 +0100 Subject: [PATCH 29/35] Bump codecov/codecov-action from 5.1.1 to 5.1.2 (#2485) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.1.1 to 5.1.2. - [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/v5.1.1...v5.1.2) --- 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 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 9d1242697c1..a44a6cf7669 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -48,7 +48,7 @@ jobs: pytest --cov --cov-report=xml python3 -m torchgeo --help - name: Report coverage - uses: codecov/codecov-action@v5.1.1 + uses: codecov/codecov-action@v5.1.2 with: token: ${{ secrets.CODECOV_TOKEN }} minimum: @@ -82,7 +82,7 @@ jobs: pytest --cov --cov-report=xml python3 -m torchgeo --help - name: Report coverage - uses: codecov/codecov-action@v5.1.1 + uses: codecov/codecov-action@v5.1.2 with: token: ${{ secrets.CODECOV_TOKEN }} datasets: @@ -114,7 +114,7 @@ jobs: pytest --cov --cov-report=xml python3 -m torchgeo --help - name: Report coverage - uses: codecov/codecov-action@v5.1.1 + uses: codecov/codecov-action@v5.1.2 with: token: ${{ secrets.CODECOV_TOKEN }} concurrency: From 04cc115d387fd4a353c7b4a282013ee11912cb2d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Dec 2024 19:54:31 +0000 Subject: [PATCH 30/35] Bump lightning[pytorch-extra] from 2.4.0 to 2.5.0.post0 in /requirements (#2489) * Bump lightning[pytorch-extra] from 2.4.0 to 2.5.0.post0 in /requirements Bumps [lightning[pytorch-extra]](https://github.com/Lightning-AI/lightning) from 2.4.0 to 2.5.0.post0. - [Release notes](https://github.com/Lightning-AI/lightning/releases) - [Commits](https://github.com/Lightning-AI/lightning/compare/2.4.0...2.5.0.post0) --- updated-dependencies: - dependency-name: lightning[pytorch-extra] dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Small type changes * Ignore undocumented type --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Adam J. Stewart --- docs/conf.py | 2 ++ pyproject.toml | 2 +- requirements/required.txt | 2 +- torchgeo/trainers/base.py | 2 +- torchgeo/trainers/iobench.py | 2 +- torchgeo/trainers/moco.py | 2 +- torchgeo/trainers/simclr.py | 2 +- 7 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 4078970c2e4..df1de398185 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -59,6 +59,8 @@ ('py:class', 'fiona.model.Feature'), ('py:class', 'kornia.augmentation._2d.intensity.base.IntensityAugmentationBase2D'), ('py:class', 'kornia.augmentation.base._AugmentationBase'), + ('py:class', 'lightning.pytorch.utilities.types.LRSchedulerConfig'), + ('py:class', 'lightning.pytorch.utilities.types.OptimizerConfig'), ('py:class', 'lightning.pytorch.utilities.types.OptimizerLRSchedulerConfig'), ('py:class', 'segmentation_models_pytorch.base.model.SegmentationModel'), ('py:class', 'timm.models.resnet.ResNet'), diff --git a/pyproject.toml b/pyproject.toml index a37a7bd759e..a4e125f5a31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ dependencies = [ # lightning 2+ required for LightningCLI args + sys.argv support # lightning 2.3 contains known bugs related to YAML parsing # https://github.com/Lightning-AI/pytorch-lightning/issues/19977 - "lightning[pytorch-extra]>=2,!=2.3.*", + "lightning[pytorch-extra]>=2,!=2.3.*,!=2.5.0", # matplotlib 3.5+ required for Python 3.10 wheels "matplotlib>=3.5", # numpy 1.21.2+ required by Python 3.10 wheels diff --git a/requirements/required.txt b/requirements/required.txt index d40386f4246..815960b0661 100644 --- a/requirements/required.txt +++ b/requirements/required.txt @@ -6,7 +6,7 @@ einops==0.8.0 fiona==1.10.1 kornia==0.7.4 lightly==1.5.15 -lightning[pytorch-extra]==2.4.0 +lightning[pytorch-extra]==2.5.0.post0 matplotlib==3.10.0 numpy==2.2.1 pandas==2.2.3 diff --git a/torchgeo/trainers/base.py b/torchgeo/trainers/base.py index 35666e98e4c..02628ba08cb 100644 --- a/torchgeo/trainers/base.py +++ b/torchgeo/trainers/base.py @@ -55,7 +55,7 @@ def configure_metrics(self) -> None: def configure_optimizers( self, - ) -> 'lightning.pytorch.utilities.types.OptimizerLRSchedulerConfig': + ) -> 'lightning.pytorch.utilities.types.OptimizerLRScheduler': """Initialize the optimizer and learning rate scheduler. Returns: diff --git a/torchgeo/trainers/iobench.py b/torchgeo/trainers/iobench.py index c8826a1dce5..c7be263dac9 100644 --- a/torchgeo/trainers/iobench.py +++ b/torchgeo/trainers/iobench.py @@ -24,7 +24,7 @@ def configure_models(self) -> None: def configure_optimizers( self, - ) -> 'lightning.pytorch.utilities.types.OptimizerLRSchedulerConfig': + ) -> 'lightning.pytorch.utilities.types.OptimizerLRScheduler': """Initialize the optimizer. Returns: diff --git a/torchgeo/trainers/moco.py b/torchgeo/trainers/moco.py index b079543adba..ce35855c12f 100644 --- a/torchgeo/trainers/moco.py +++ b/torchgeo/trainers/moco.py @@ -293,7 +293,7 @@ def configure_losses(self) -> None: def configure_optimizers( self, - ) -> 'lightning.pytorch.utilities.types.OptimizerLRSchedulerConfig': + ) -> 'lightning.pytorch.utilities.types.OptimizerLRScheduler': """Initialize the optimizer and learning rate scheduler. Returns: diff --git a/torchgeo/trainers/simclr.py b/torchgeo/trainers/simclr.py index 1cb05315f60..a0625f26ebb 100644 --- a/torchgeo/trainers/simclr.py +++ b/torchgeo/trainers/simclr.py @@ -286,7 +286,7 @@ def predict_step(self, batch: Any, batch_idx: int, dataloader_idx: int = 0) -> N def configure_optimizers( self, - ) -> 'lightning.pytorch.utilities.types.OptimizerLRSchedulerConfig': + ) -> 'lightning.pytorch.utilities.types.OptimizerLRScheduler': """Initialize the optimizer and learning rate scheduler. .. versionchanged:: 0.6 From f47924fca0a3773648f925b35839cda370998571 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Dec 2024 20:28:40 +0000 Subject: [PATCH 31/35] Bump nbsphinx from 0.9.5 to 0.9.6 in /requirements (#2490) Bumps [nbsphinx](https://github.com/spatialaudio/nbsphinx) from 0.9.5 to 0.9.6. - [Release notes](https://github.com/spatialaudio/nbsphinx/releases) - [Changelog](https://github.com/spatialaudio/nbsphinx/blob/master/NEWS.rst) - [Commits](https://github.com/spatialaudio/nbsphinx/compare/0.9.5...0.9.6) --- updated-dependencies: - dependency-name: nbsphinx 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/docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/docs.txt b/requirements/docs.txt index 74fa5f6207c..2903d4b4c97 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -1,4 +1,4 @@ # docs ipywidgets==8.1.5 -nbsphinx==0.9.5 +nbsphinx==0.9.6 sphinx==5.3.0 From 801e94746f9fe9d1b84b4220c3eb2fc8248a59a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Dec 2024 00:49:58 +0100 Subject: [PATCH 32/35] Bump torchmetrics from 1.6.0 to 1.6.1 in /requirements (#2491) --- requirements/required.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/required.txt b/requirements/required.txt index 815960b0661..055d27670f6 100644 --- a/requirements/required.txt +++ b/requirements/required.txt @@ -18,5 +18,5 @@ segmentation-models-pytorch==0.3.4 shapely==2.0.6 timm==0.9.7 torch==2.5.1 -torchmetrics==1.6.0 +torchmetrics==1.6.1 torchvision==0.20.1 From 3285d54f5f15c2cb53e7ca5b61b46175ce6b4b29 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Dec 2024 21:34:30 +0100 Subject: [PATCH 33/35] Bump mypy from 1.14.0 to 1.14.1 in /requirements (#2493) Bumps [mypy](https://github.com/python/mypy) from 1.14.0 to 1.14.1. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.14.0...v1.14.1) --- updated-dependencies: - dependency-name: mypy 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 06151a9f86b..dc9b06b247a 100644 --- a/requirements/style.txt +++ b/requirements/style.txt @@ -1,3 +1,3 @@ # style -mypy==1.14.0 +mypy==1.14.1 ruff==0.8.4 From 7ae2db91988fe147448d2824f5c2bb38f2119dd4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jan 2025 22:22:27 +0100 Subject: [PATCH 34/35] Bump ruff from 0.8.4 to 0.8.5 in /requirements (#2495) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.8.4 to 0.8.5. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.8.4...0.8.5) --- updated-dependencies: - dependency-name: ruff 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 dc9b06b247a..cb4ef80189d 100644 --- a/requirements/style.txt +++ b/requirements/style.txt @@ -1,3 +1,3 @@ # style mypy==1.14.1 -ruff==0.8.4 +ruff==0.8.5 From 5d94e185ef316f5f2200721a03afbd9321f27584 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jan 2025 22:22:51 +0100 Subject: [PATCH 35/35] Bump pillow from 11.0.0 to 11.1.0 in /requirements (#2494) Bumps [pillow](https://github.com/python-pillow/Pillow) from 11.0.0 to 11.1.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/11.0.0...11.1.0) --- updated-dependencies: - dependency-name: pillow 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> --- requirements/required.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/required.txt b/requirements/required.txt index 055d27670f6..bbe4e855a74 100644 --- a/requirements/required.txt +++ b/requirements/required.txt @@ -10,7 +10,7 @@ lightning[pytorch-extra]==2.5.0.post0 matplotlib==3.10.0 numpy==2.2.1 pandas==2.2.3 -pillow==11.0.0 +pillow==11.1.0 pyproj==3.7.0 rasterio==1.4.3 rtree==1.3.0