diff --git a/dev/.buildinfo b/dev/.buildinfo index 8b52596..1fe2aa1 100644 --- a/dev/.buildinfo +++ b/dev/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: b61d3afd14ca59986b902bdc5a436473 +config: 9cfc092811c16851bd44535e82908ad7 tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/dev/_images/composites_2_1.png b/dev/_images/composites_2_1.png new file mode 100644 index 0000000..7302a07 Binary files /dev/null and b/dev/_images/composites_2_1.png differ diff --git a/dev/_images/composites_3_0.png b/dev/_images/composites_3_0.png index 08598f3..6654ba6 100644 Binary files a/dev/_images/composites_3_0.png and b/dev/_images/composites_3_0.png differ diff --git a/dev/_images/composites_4_0.png b/dev/_images/composites_4_0.png index 39ba43d..020117b 100644 Binary files a/dev/_images/composites_4_0.png and b/dev/_images/composites_4_0.png differ diff --git a/dev/_images/composites_5_0.png b/dev/_images/composites_5_0.png new file mode 100644 index 0000000..78d671d Binary files /dev/null and b/dev/_images/composites_5_0.png differ diff --git a/dev/_images/composites_6_0.png b/dev/_images/composites_6_0.png new file mode 100644 index 0000000..8e19771 Binary files /dev/null and b/dev/_images/composites_6_0.png differ diff --git a/dev/_images/equalize-histogram_2_0.png b/dev/_images/equalize-histogram_2_0.png new file mode 100644 index 0000000..ec85b86 Binary files /dev/null and b/dev/_images/equalize-histogram_2_0.png differ diff --git a/dev/_images/equalize-histogram_3_0.png b/dev/_images/equalize-histogram_3_0.png new file mode 100644 index 0000000..ca239ef Binary files /dev/null and b/dev/_images/equalize-histogram_3_0.png differ diff --git a/dev/_images/equalize-histogram_4_0.png b/dev/_images/equalize-histogram_4_0.png new file mode 100644 index 0000000..cd15fce Binary files /dev/null and b/dev/_images/equalize-histogram_4_0.png differ diff --git a/dev/_images/index_0_0.png b/dev/_images/index_0_0.png new file mode 100644 index 0000000..b526b61 Binary files /dev/null and b/dev/_images/index_0_0.png differ diff --git a/dev/_images/index_0_1.png b/dev/_images/index_0_1.png deleted file mode 100644 index 9111396..0000000 Binary files a/dev/_images/index_0_1.png and /dev/null differ diff --git a/dev/_images/indices_10_0.png b/dev/_images/indices_10_0.png new file mode 100644 index 0000000..4932d87 Binary files /dev/null and b/dev/_images/indices_10_0.png differ diff --git a/dev/_images/indices_14_0.png b/dev/_images/indices_14_0.png new file mode 100644 index 0000000..50e7426 Binary files /dev/null and b/dev/_images/indices_14_0.png differ diff --git a/dev/_images/indices_1_1.png b/dev/_images/indices_1_1.png new file mode 100644 index 0000000..5a1dac7 Binary files /dev/null and b/dev/_images/indices_1_1.png differ diff --git a/dev/_images/indices_2_0.png b/dev/_images/indices_2_0.png deleted file mode 100644 index 7109e4f..0000000 Binary files a/dev/_images/indices_2_0.png and /dev/null differ diff --git a/dev/_images/indices_4_0.png b/dev/_images/indices_4_0.png deleted file mode 100644 index f6f8695..0000000 Binary files a/dev/_images/indices_4_0.png and /dev/null differ diff --git a/dev/_images/indices_5_0.png b/dev/_images/indices_5_0.png new file mode 100644 index 0000000..a17adea Binary files /dev/null and b/dev/_images/indices_5_0.png differ diff --git a/dev/_images/indices_7_0.png b/dev/_images/indices_7_0.png new file mode 100644 index 0000000..90ebc53 Binary files /dev/null and b/dev/_images/indices_7_0.png differ diff --git a/dev/_images/indices_8_0.png b/dev/_images/indices_8_0.png new file mode 100644 index 0000000..29398dc Binary files /dev/null and b/dev/_images/indices_8_0.png differ diff --git a/dev/_images/missing-values_1_1.png b/dev/_images/missing-values_1_1.png new file mode 100644 index 0000000..bbcb482 Binary files /dev/null and b/dev/_images/missing-values_1_1.png differ diff --git a/dev/_images/missing-values_2_0.png b/dev/_images/missing-values_2_0.png new file mode 100644 index 0000000..2180aff Binary files /dev/null and b/dev/_images/missing-values_2_0.png differ diff --git a/dev/_images/overview_4_0.png b/dev/_images/overview_4_0.png index 89317bd..e7e1fdc 100644 Binary files a/dev/_images/overview_4_0.png and b/dev/_images/overview_4_0.png differ diff --git a/dev/_images/pansharpen_3_0.png b/dev/_images/pansharpen_3_0.png index f05be3d..3941065 100644 Binary files a/dev/_images/pansharpen_3_0.png and b/dev/_images/pansharpen_3_0.png differ diff --git a/dev/_images/pansharpen_5_0.png b/dev/_images/pansharpen_5_0.png index 2ca8716..3b1307c 100644 Binary files a/dev/_images/pansharpen_5_0.png and b/dev/_images/pansharpen_5_0.png differ diff --git a/dev/_images/plot-overlay_1_0.png b/dev/_images/plot-overlay_1_0.png new file mode 100644 index 0000000..46c4fd3 Binary files /dev/null and b/dev/_images/plot-overlay_1_0.png differ diff --git a/dev/_images/plot-overlay_2_0.png b/dev/_images/plot-overlay_2_0.png new file mode 100644 index 0000000..8c39d69 Binary files /dev/null and b/dev/_images/plot-overlay_2_0.png differ diff --git a/dev/_images/plot-overlay_3_0.png b/dev/_images/plot-overlay_3_0.png new file mode 100644 index 0000000..2170004 Binary files /dev/null and b/dev/_images/plot-overlay_3_0.png differ diff --git a/dev/_modules/index.html b/dev/_modules/index.html index 61d0d70..b39e857 100644 --- a/dev/_modules/index.html +++ b/dev/_modules/index.html @@ -7,7 +7,7 @@ - Overview: module code | xlandsat v0.0.post36 + Overview: module code | xlandsat v0.0.post67+gd60f8e0 @@ -81,9 +81,14 @@
diff --git a/dev/_modules/xlandsat/_composite.html b/dev/_modules/xlandsat/_composite.html index a1e1997..139195e 100644 --- a/dev/_modules/xlandsat/_composite.html +++ b/dev/_modules/xlandsat/_composite.html @@ -7,7 +7,7 @@ - xlandsat._composite | xlandsat v0.0.post36 + xlandsat._composite | xlandsat v0.0.post67+gd60f8e0 @@ -81,9 +81,14 @@
@@ -114,14 +119,19 @@

xlandsat v0.

@@ -171,6 +196,16 @@

xlandsat v0. xlandsat.pansharpen +
  • + + xlandsat.equalize_histogram + +
  • +
  • + + xlandsat.interpolate_missing + +
  • xlandsat.datasets.fetch_brumadinho_after @@ -191,11 +226,26 @@

    xlandsat v0. xlandsat.datasets.fetch_liverpool_panchromatic

  • +
  • + + xlandsat.datasets.fetch_manaus + +
  • +
  • + + xlandsat.datasets.fetch_momotombo + +
  • +
  • + + xlandsat.datasets.fetch_roraima + +
  • - Citing + Citing xlandsat
  • @@ -221,10 +271,20 @@

    xlandsat v0.

    - Links + Community

  • - Citing + Citing xlandsat
  • @@ -221,10 +271,20 @@

    xlandsat v0.

    - Links + Community

  • - Citing + Citing xlandsat
  • @@ -221,10 +271,20 @@

    xlandsat v0.

    - Links + Community

  • - Citing + Citing xlandsat
  • @@ -221,10 +271,20 @@

    xlandsat v0.

    - Links + Community

  • @@ -573,7 +734,7 @@

    Source code for xlandsat.datasets

             
         
    diff --git a/dev/_sources/api/generated/xlandsat.datasets.fetch_manaus.rst.txt b/dev/_sources/api/generated/xlandsat.datasets.fetch_manaus.rst.txt new file mode 100644 index 0000000..9032988 --- /dev/null +++ b/dev/_sources/api/generated/xlandsat.datasets.fetch_manaus.rst.txt @@ -0,0 +1,10 @@ +xlandsat.datasets.fetch\_manaus +=============================== + +.. currentmodule:: xlandsat.datasets + +.. autofunction:: fetch_manaus + +.. raw:: html + +
    diff --git a/dev/_sources/api/generated/xlandsat.datasets.fetch_momotombo.rst.txt b/dev/_sources/api/generated/xlandsat.datasets.fetch_momotombo.rst.txt new file mode 100644 index 0000000..9965b21 --- /dev/null +++ b/dev/_sources/api/generated/xlandsat.datasets.fetch_momotombo.rst.txt @@ -0,0 +1,10 @@ +xlandsat.datasets.fetch\_momotombo +================================== + +.. currentmodule:: xlandsat.datasets + +.. autofunction:: fetch_momotombo + +.. raw:: html + +
    diff --git a/dev/_sources/api/generated/xlandsat.datasets.fetch_roraima.rst.txt b/dev/_sources/api/generated/xlandsat.datasets.fetch_roraima.rst.txt new file mode 100644 index 0000000..404832f --- /dev/null +++ b/dev/_sources/api/generated/xlandsat.datasets.fetch_roraima.rst.txt @@ -0,0 +1,10 @@ +xlandsat.datasets.fetch\_roraima +================================ + +.. currentmodule:: xlandsat.datasets + +.. autofunction:: fetch_roraima + +.. raw:: html + +
    diff --git a/dev/_sources/api/generated/xlandsat.equalize_histogram.rst.txt b/dev/_sources/api/generated/xlandsat.equalize_histogram.rst.txt new file mode 100644 index 0000000..5e55682 --- /dev/null +++ b/dev/_sources/api/generated/xlandsat.equalize_histogram.rst.txt @@ -0,0 +1,10 @@ +xlandsat.equalize\_histogram +============================ + +.. currentmodule:: xlandsat + +.. autofunction:: equalize_histogram + +.. raw:: html + +
    diff --git a/dev/_sources/api/generated/xlandsat.interpolate_missing.rst.txt b/dev/_sources/api/generated/xlandsat.interpolate_missing.rst.txt new file mode 100644 index 0000000..03475e1 --- /dev/null +++ b/dev/_sources/api/generated/xlandsat.interpolate_missing.rst.txt @@ -0,0 +1,10 @@ +xlandsat.interpolate\_missing +============================= + +.. currentmodule:: xlandsat + +.. autofunction:: interpolate_missing + +.. raw:: html + +
    diff --git a/dev/_sources/api/index.rst.txt b/dev/_sources/api/index.rst.txt index 59a1537..c89f923 100644 --- a/dev/_sources/api/index.rst.txt +++ b/dev/_sources/api/index.rst.txt @@ -25,6 +25,8 @@ Processing composite pansharpen + equalize_histogram + interpolate_missing Sample datasets --------------- @@ -40,3 +42,6 @@ Sample datasets datasets.fetch_brumadinho_before datasets.fetch_liverpool datasets.fetch_liverpool_panchromatic + datasets.fetch_manaus + datasets.fetch_momotombo + datasets.fetch_roraima diff --git a/dev/_sources/citing.rst.txt b/dev/_sources/citing.rst.txt index ed35ff6..4cb3e9e 100644 --- a/dev/_sources/citing.rst.txt +++ b/dev/_sources/citing.rst.txt @@ -1,7 +1,7 @@ .. _citing: -Citing -====== +Citing xlandsat +=============== This is research software **made by scientists**. Citations help us justify the effort that goes into building and maintaining this project. diff --git a/dev/_sources/composites.rst.txt b/dev/_sources/composites.rst.txt index 44ad4db..3ea0779 100644 --- a/dev/_sources/composites.rst.txt +++ b/dev/_sources/composites.rst.txt @@ -1,48 +1,49 @@ .. _composites: -Composites -========== +Making composites +================= -Plotting individual bands is good but we usually want to make some composite -images to visualize information from multiple bands at once. -For that, we have to create **composites**. -We provide the :func:`xlandsat.composite` function to make this process easier. +Plotting individual bands is nice but we usually want to make some composite +images, both RGB and false-color, to visualize information from multiple bands +at once. +We provide the :func:`xlandsat.composite` function to make this process easier +and produce composites that are compatible with both :mod:`xarray` and +:mod:`matplotlib`. -As an example, let's load two example scenes from the -`Brumadinho tailings dam disaster `__: +As an example, let's load our example scene from Manaus, Brazil, which is where +the Solimões (brown water) and Negro (black water) rivers merge to form the +Amazon river: .. jupyter-execute:: import xlandsat as xls import matplotlib.pyplot as plt - path_before = xls.datasets.fetch_brumadinho_before() - path_after = xls.datasets.fetch_brumadinho_after() - before = xls.load_scene(path_before) - after = xls.load_scene(path_after) + path = xls.datasets.fetch_manaus() + scene = xls.load_scene(path) + scene -Creating composites -------------------- +RGB composites +-------------- -Let's make both RGB (true color) and CIR (color infrared) composites for both -of our scenes: +Let's make an RGB (true color) composite since that is the most fundamental +type and it allows us to get a good handle on what we're seeing in the scene. +The RGB composite is also the default made by :func:`xlandsat.composite` if the +bands aren't specified. .. jupyter-execute:: - # Make the composite and add it as a variable to the scene - before = before.assign(rgb=xls.composite(before, rescale_to=[0.03, 0.2])) - cir_bands = ("nir", "red", "green") - before = before.assign( - cir=xls.composite(before, bands=cir_bands, rescale_to=[0, 0.4]), - ) - before + rgb = xls.composite(scene) + rgb -The composites have a similar layout as the bands but with an extra -``"channel"`` dimension corresponding to red, green, blue, and -alpha/transparency. The values are scaled to the [0, 255] range and the -composite is an array of unsigned 8-bit integers. +The composite is also an :class:`xarray.DataArray` and is similar to the bands. +It has the same easting and northing dimensions but also an extra ``"channel"`` +dimension, which corresponds to red, green, blue, and alpha/transparency. This +extra dimension is what combines the three bands into a single color image. The +values are scaled to the [0, 255] range and the composite is an array of +unsigned 8-bit integers. .. admonition:: Transparency :class: note @@ -53,55 +54,127 @@ composite is an array of unsigned 8-bit integers. blue). -Now do the same for the after scene: +Plotting composites +------------------- -.. jupyter-execute:: +Composites can be plotted using the :meth:`xarray.DataArray.plot.imshow` method: - after = after.assign(rgb=xls.composite(after, rescale_to=[0.03, 0.2])) - after = after.assign( - cir=xls.composite(after, bands=cir_bands, rescale_to=[0, 0.4]), - ) - after +.. jupyter-execute:: + rgb.plot.imshow() -Plotting composites -------------------- -Composites can be plotted using :meth:`xarray.DataArray.plot.imshow` (using -:meth:`~xarray.DataArray.plot` won't work and will display histograms instead). -Let's make the before and after figures again for each of the composites we -generated. +With no arguments, xarray takes care of creating the new figure and adding a +lot of the different plot elements, like axis labels and units. +If we want more control over the plot, we must create a matplotlib figure and +axes separately and tell :meth:`~xarray.DataArray.plot.imshow` to plot on those +instead: .. jupyter-execute:: - fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 12)) + fig, ax = plt.subplots(1, 1, figsize=(10, 6)) - # Plot the composites - before.rgb.plot.imshow(ax=ax1) - after.rgb.plot.imshow(ax=ax2) + rgb.plot.imshow(ax=ax) # The "long_name" of the composite is the band combination - ax1.set_title(f"Before: {before.rgb.attrs['long_name']}") - ax2.set_title(f"After: {after.rgb.attrs['long_name']}") + ax.set_title(rgb.attrs["long_name"].title()) + + # Make sure pixels are square when plotting to avoid distortions + ax.set_aspect("equal") + + plt.show() + +Well, this looks bad because some very bright pixels in the city are making the +majority of the other pixels have only a small share of the full range of +available values. This can be mitigated by rescaling the intensity of the image +to a smaller range of reflectance values. + +.. note:: + + Using :meth:`xarray.DataArray.plot` instead of + :meth:`xarray.DataArray.plot.imshow` won't work and will display histograms + instead. + + +Rescaling intensity (AKA contrast stretching) +--------------------------------------------- - ax1.set_aspect("equal") - ax2.set_aspect("equal") +We rescale the intensities of a composite to a given reflectance range by +setting the ``rescale_to`` parameter when creating a composite. It takes a list +of the min and max reflectance values allowed. For this image, we can arrive at +the following values by trial and error until it looks nice: +.. jupyter-execute:: + + rgb = xls.composite(scene, rescale_to=[0.01, 0.2]) + + # Pretty much the same plotting code + fig, ax = plt.subplots(1, 1, figsize=(10, 6)) + rgb.plot.imshow(ax=ax) + ax.set_title(f"Rescaled {rgb.attrs['long_name'].title()}") + ax.set_aspect("equal") plt.show() -And now the CIR composites: +Notice that we can more clearly see the different colors of the forest and the +rivers. +However, it can still be a bit hard to distinguish between some of the water +bodies and the forest in the right side of the scene. +Other band combinations can generate composites that better highlight these +features. + +.. note:: + + The rescaling has to be done when creating the composite so that we can use + min/max values in reflectance units. After a composite is created, the + original range of values is lost and we'd have to specify the min/max + between 0 and 255 instead. + + +Color infrared composites +------------------------- + +Another common type of composite is the color infrared (CIR) composites, +which uses the NIR, red, and green bands as the red, green, blue channels. +The added information of the NIR band helps highlight vegetation, which can +help us distinguish between the water and forest on the right. +Let's make one by specifying +this band combination to :func:`xlandsat.composite` to see what happens: .. jupyter-execute:: - fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 12)) + cir = xls.composite( + scene, rescale_to=[0, 0.4], bands=("nir", "red", "green"), + ) - before.cir.plot.imshow(ax=ax1) - after.cir.plot.imshow(ax=ax2) + fig, ax = plt.subplots(1, 1, figsize=(10, 6)) + cir.plot.imshow(ax=ax) + ax.set_title(cir.attrs["long_name"].title()) + ax.set_aspect("equal") + plt.show() + +In this composite, the contrast between the forest and water bodies on the +right are much clearer! + + +Other composites +---------------- - ax1.set_title(f"Before: {before.cir.attrs['long_name']}") - ax2.set_title(f"After: {after.cir.attrs['long_name']}") +You can make pretty much any composite you'd like by passing the correct band +combination to :func:`xlandsat.composite`. +For example, let's make one with NIR as red, SWIR1 as green, and red as blue: - ax1.set_aspect("equal") - ax2.set_aspect("equal") +.. jupyter-execute:: + + custom = xls.composite( + scene, rescale_to=[0, 0.4], bands=("nir", "swir1", "red"), + ) + fig, ax = plt.subplots(1, 1, figsize=(10, 6)) + custom.plot.imshow(ax=ax) + ax.set_title(custom.attrs["long_name"].title()) + ax.set_aspect("equal") plt.show() + +This particular composite is great at distinguishing between built structures +in the city and along the canals (light green), the water ways (dark blue and +purple), and the forest (orange). diff --git a/dev/_sources/equalize-histogram.rst.txt b/dev/_sources/equalize-histogram.rst.txt new file mode 100644 index 0000000..63e82fb --- /dev/null +++ b/dev/_sources/equalize-histogram.rst.txt @@ -0,0 +1,90 @@ +.. _equalize-histogram: + +Histogram equalization +====================== + +Scenes with very dark or very bright spots (like clouds) can be difficult to +visualize without some sort of contrast enhancement when generating composites. +The simplest enhancement is to stretch the contrast linearly, but doing so +erases information in the very dark/light regions and won't always work. An +alternative is to use histogram equalization, which is implemented in +:func:`xlandsat.equalize_histogram`. + +Let's use our sample scene of October 2015 around `Mount Roraima +`__ to demonstrate how it's done. +The *tepui*, as it's called, is famous for it's near constant cloud coverage an +will make a good target for this example. + +First, we'll import the required packages and load the sample scene: + +.. jupyter-execute:: + + import xlandsat as xls + import matplotlib.pyplot as plt + import xarray as xr + import numpy as np + + path = xls.datasets.fetch_roraima() + scene = xls.load_scene(path) + +Histogram equalization doesn't work well with missing data, which this dataset +has, and so we need to first fill in the missing values through interpolation +with :func:`xlandsat.interpolate_missing`: + +.. jupyter-execute:: + + scene = xls.interpolate_missing(scene) + +Once that's done, we can make an RGB composite with no enhancements as a basis +for comparison: + +.. jupyter-execute:: + + rgb = xls.composite(scene) + + fig, ax = plt.subplots(1, 1, figsize=(10, 6)) + rgb.plot.imshow(ax=ax) + ax.set_aspect("equal") + plt.show() + +Notice how the clouds dominate the intensity range, making it difficult to make +out features of the *tepui* and the surrounding forest. + +Now we can do our best to stretch the contrast so that we can see more detail +in the cloud-free regions: + +.. jupyter-execute:: + + rgb_strech = xls.composite(scene, rescale_to=(0, 0.28)) + + fig, ax = plt.subplots(1, 1, figsize=(10, 6)) + rgb_strech.plot.imshow(ax=ax) + ax.set_aspect("equal") + plt.show() + +But, as we mentioned earlier, this means we don't get to see details of the +clouds anymore. For a more pleasing image, we can use the adaptive histogram +equalization in :func:`xlandsat.equalize_histogram`. + +.. tip:: + + It can be helpful to do a bit of contrast stretching first, but to a lesser + degree than we did previously. + +.. jupyter-execute:: + + rgb = xls.composite(scene, rescale_to=(0, 0.6)) + rgb_eq = xls.equalize_histogram(rgb, clip_limit=0.02, kernel_size=300) + + fig, ax = plt.subplots(1, 1, figsize=(10, 6)) + rgb_eq.plot.imshow(ax=ax) + ax.set_aspect("equal") + plt.show() + +Now that's a much better visualization, we can see details in the clouds, +mountains, and forests! + +.. note:: + + Notice that :func:`xlandsat.equalize_histogram` must be **given a + composite** instead of the scene. diff --git a/dev/_sources/index.rst.txt b/dev/_sources/index.rst.txt index 1f11cc6..d34e36a 100644 --- a/dev/_sources/index.rst.txt +++ b/dev/_sources/index.rst.txt @@ -14,37 +14,40 @@ .. div:: sd-fs-3 - Load Landsat remote sensing images into xarray + Analyze Landsat remote sensing images using xarray -**xlandsat** is Python library for -loading Landsat scenes downloaded from -`USGS EarthExplorer `__ into -:class:`xarray.Dataset` containers. +**xlandsat** is Python library for loading and analyzing Landsat scenes +downloaded from `USGS EarthExplorer `__ with +the power of :mod:`xarray`. We take care of reading the metadata from the ``*_MTL.txt`` files provided by -EarthExplorer and organizing the bands into a single data structure for easier -manipulation, processing, and visualization. +EarthExplorer and organizing the bands into a single :class:`xarray.Dataset` +data structure for easier manipulation, processing, and visualization. Here's a quick example: .. jupyter-execute:: import xlandsat as xls + import matplotlib.pyplot as plt - # Download a cropped Landsat 8 scene from the Brumadinho dam disaster - # (Brazil). The function downloads it and returns the path to the .tar file - # containing the scene. - path = xls.datasets.fetch_brumadinho_after() + # Download a sample Landsat 9 scene in EarthExplorer format + path_to_scene_file = xls.datasets.fetch_manaus() - # Load the scene directly from the archive (no need to unpack it) - scene = xls.load_scene(path) + # Load the data from the file into an xarray.Dataset + scene = xls.load_scene(path_to_scene_file) - # Make an RGB composite and stretch the contrast - rgb = xls.composite(scene, rescale_to=[0.03, 0.2]) + # Make an RGB composite as an xarray.DataArray + rgb = xls.composite(scene, rescale_to=[0.02, 0.2]) - # Plot the composite + # Plot the composite using xarray's plotting machinery rgb.plot.imshow() + # Annotate the plot with the rich metadata xlandsat adds to the scene + plt.title(f"{rgb.attrs['title']}\n{rgb.attrs['long_name']}") + plt.axis("scaled") + plt.show() + ---- @@ -74,7 +77,7 @@ Here's a quick example: Open an Issue on GitHub. - .. button-link:: https://github.com/compgeolab/xlandsat + .. button-link:: https://github.com/compgeolab/xlandsat/issues :click-parent: :color: primary :outline: @@ -112,8 +115,8 @@ Here's a quick example: .. note:: - Only **Landsat 8 and 9 Collection 2 Level 2 data products** are supported at - the moment. + Only **Landsat 8 and 9 Level 1 & 2 data products** have been tested at the + moment. .. admonition:: xlandsat is ready for use but still changing :class: important @@ -127,13 +130,15 @@ Here's a quick example: time to bring new ideas on how we can improve the project. Submit `issues on GitHub `__. -.. admonition:: Looking for large-scale cloud-based processing? +.. admonition:: Looking for large-scale processing or other satellites? :class: seealso - Our goal is not to provide a solution for large-scale data processing. The - target is smaller scale analysis done on individual computers (which is - probably the main way EarthExplorer is used). For cloud-based data - processing, see the `Pangeo Project `__. + Our goal is **not** to provide a solution for large-scale data processing. + Our target is smaller scale analysis done on individual computers (which is + probably the main way EarthExplorer is used). + + * For cloud-based data processing, see the `Pangeo Project `__. + * For other satellites and more powerful features, use `Satpy `__. .. toctree:: @@ -149,9 +154,13 @@ Here's a quick example: :hidden: :caption: User Guide + io.rst composites.rst indices.rst pansharpen.rst + missing-values.rst + equalize-histogram.rst + plot-overlay.rst .. toctree:: :maxdepth: 2 @@ -168,7 +177,9 @@ Here's a quick example: .. toctree:: :maxdepth: 2 :hidden: - :caption: Links + :caption: Community + How to contribute + Code of Conduct Source code on GitHub Computer-Oriented Geoscience Lab diff --git a/dev/_sources/indices.rst.txt b/dev/_sources/indices.rst.txt index 91f0334..22af927 100644 --- a/dev/_sources/indices.rst.txt +++ b/dev/_sources/indices.rst.txt @@ -1,83 +1,142 @@ .. _indices: -Indices -------- +Working with indices +-------------------- Indices calculated from multispectral satellite imagery are powerful ways to quantitatively analyze these data. They take advantage of different spectral properties of materials to differentiate between them. Many of these indices can be calculated with simple arithmetic operations. -So now that our data are in :class:`xarray.Dataset`'s it's fairly easy to +So now that our data are in :class:`xarray.Dataset`'s, it's fairly easy to calculate them. +As an example, we'll use two example scenes from before and after the +`Brumadinho tailings dam disaster `__ +to try to image and quantify the total area flooded by the damn collapse. -NDVI ----- +.. admonition:: Trigger warning + :class: warning + + This tutorial uses data from the tragic + `Brumadinho tailings dam disaster `__, + in which over 250 people lost their lives. We use this dataset to + illustrate the usefulness of remote sensing data for monitoring such + disasters but we want to acknowledge its tragic human consequences. + **Some readers may find this topic disturbing and may not wish to read + futher.** -As an example, let's load two example scenes from the -`Brumadinho tailings dam disaster `__: +First, we must import the required packages, download our two sample scenes, +and load them with :func:`xlandsat.load_scene`: .. jupyter-execute:: import xlandsat as xls import matplotlib.pyplot as plt + path_before = xls.datasets.fetch_brumadinho_before() path_after = xls.datasets.fetch_brumadinho_after() before = xls.load_scene(path_before) after = xls.load_scene(path_after) + after + +Let's make RGB composites to get a sense of what these +two scenes contain: + +.. jupyter-execute:: + + rgb_before = xls.composite(before, rescale_to=(0.03, 0.2)) + rgb_after = xls.composite(after, rescale_to=(0.03, 0.2)) + + fig, axes = plt.subplots(2, 1, figsize=(10, 12), layout="tight") + for ax, rgb in zip(axes, [rgb_before, rgb_after]): + rgb.plot.imshow(ax=ax) + ax.set_title(rgb.attrs["title"]) + ax.set_aspect("equal") + plt.show() +The dam is located at around 592000 east and -2225000 north. The after scene +clearly shows all of the red mud that flooded the region to the southwest of +the dam. Notice also the red tinge of the Paraopeba River in the after image +as it was contaminated by the mud flow. + +.. tip:: + + See :ref:`composites` for more information on what we did above. + +NDVI +---- We can calculate the `NDVI `__ for these scenes to see if we can isolate the effect of the flood following the -dam collapse: +dam collapse. +NDVI highlights vegetation, which we assume will have decreased in the after +scene due to the flood. +NDVI is defined as: +.. math:: -.. jupyter-execute:: + NDVI = \dfrac{NIR - Red}{NIR + Red} - before = before.assign( - ndvi=(before.nir - before.red) / (before.nir + before.red), - ) - after = after.assign( - ndvi=(after.nir - after.red) / (after.nir + after.red), - ) +which we can calculate with xarray as: - # Set some metadata for xarray to find - before.ndvi.attrs["long_name"] = "normalized difference vegetation index" - before.ndvi.attrs["units"] = "dimensionless" - after.ndvi.attrs["long_name"] = "normalized difference vegetation index" - after.ndvi.attrs["units"] = "dimensionless" +.. jupyter-execute:: - after + ndvi_before = (before.nir - before.red) / (before.nir + before.red) + ndvi_before -And now we can make pseudo-color plots of the NDVI: +Now we can do the same for the after scene: .. jupyter-execute:: - fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 12)) + ndvi_after = (after.nir - after.red) / (after.nir + after.red) + ndvi_after - # Limit the scale to [-1, +1] so the plots are easier to compare - before.ndvi.plot(ax=ax1, vmin=-1, vmax=1, cmap="RdBu_r") - after.ndvi.plot(ax=ax2, vmin=-1, vmax=1, cmap="RdBu_r") +And add some metadata for xarray to find when making plots: - ax1.set_title(f"Before: {before.attrs['title']}") - ax2.set_title(f"After: {after.attrs['title']}") +.. jupyter-execute:: + + for ndvi in [ndvi_before, ndvi_after]: + ndvi.attrs["long_name"] = "normalized difference vegetation index" + ndvi.attrs["units"] = "dimensionless" + ndvi_before.attrs["title"] = "NDVI before" + ndvi_after.attrs["title"] = "NDVI after" - ax1.set_aspect("equal") - ax2.set_aspect("equal") +Now we can make pseudo-color plots of the NDVI from before and after the +disaster: +.. jupyter-execute:: + + fig, axes = plt.subplots(2, 1, figsize=(10, 12), layout="tight") + for ax, ndvi in zip(axes, [ndvi_before, ndvi_after]): + # Limit the scale to [-1, +1] so the plots are easier to compare + ndvi.plot(ax=ax, vmin=-1, vmax=1, cmap="RdBu_r") + ax.set_title(ndvi.attrs["title"]) + ax.set_aspect("equal") plt.show() -Finally, we can calculate the change in NDVI from one scene to the other by -taking the difference: + +Tracking differences +-------------------- + +An advantage of having our data in :class:`xarray.DataArray` format is that we +can perform **coordinate-aware** calculations. This means that taking the +difference between our two arrays will take into account the coordinates of +each pixel and only perform the operation where the coordinates align. + +We can calculate the change in NDVI from one scene to the other by taking the +difference: .. jupyter-execute:: - ndvi_change = before.ndvi - after.ndvi + ndvi_change = ndvi_before - ndvi_after + + # Add som metadata for xarray ndvi_change.name = "ndvi_change" - ndvi_change.attrs["long_name"] = ( + ndvi_change.attrs["long_name"] = "NDVI change" + ndvi_change.attrs["title"] = ( f"NDVI change between {before.attrs['date_acquired']} and " f"{after.attrs['date_acquired']}" ) @@ -92,7 +151,7 @@ taking the difference: this case, there was an East-West shift between scenes but our calculations take that into account. -Now lets plot it: +Now lets plot the difference: .. jupyter-execute:: @@ -100,8 +159,151 @@ Now lets plot it: fig, ax = plt.subplots(1, 1, figsize=(10, 6)) ndvi_change.plot(ax=ax, vmin=-1, vmax=1, cmap="PuOr") ax.set_aspect("equal") + ax.set_title(ndvi_change.attrs["title"]) plt.show() There's some noise in the cloudy areas of both scenes in the northeast but otherwise this plots highlights the area affected by flooding from the dam -collapse in bright red at the center. +collapse in purple at the center. + + +Estimating area +--------------- + +One things we can do with indices and their differences in time is calculated +**area estimates**. If we know that the region of interest has index values +within a given value range, the area can be calculated by counting the number +of pixels within that range (a pixel in Landsat 8/9 scenes is 30 x 30 = 900 m²). + +First, let's slice our NDVI difference to just the flooded area to avoid the +effect of the clouds in North. We'll use the :meth:`xarray.DataArray.sel` +method to slice using the UTM coordinates of the scene: + +.. jupyter-execute:: + + flood = ndvi_change.sel( + easting=slice(587000, 594000), + northing=slice(-2230000, -2225000), + ) + + fig, ax = plt.subplots(1, 1, figsize=(10, 6)) + flood.plot(ax=ax, vmin=-1, vmax=1, cmap="PuOr") + ax.set_aspect("equal") + plt.show() + +Now we can create a mask of the flood area by selecting pixels that have a high +NDVI difference. Using a ``>`` comparison (or any other logical operator in +Python), we can create a boolean (``True`` or ``False``) +:class:`xarray.DataArray` as our mask: + +.. jupyter-execute:: + + # Threshold value determined by trial-and-error + flood_mask = flood > 0.3 + + # Add some metadata for xarray + flood_mask.attrs["long_name"] = "flood mask" + + flood_mask + +Plotting boolean arrays will use 1 to represent ``True`` and 0 to represent +``False``: + +.. jupyter-execute:: + + fig, ax = plt.subplots(1, 1, figsize=(10, 6)) + flood_mask.plot(ax=ax, cmap="gray") + ax.set_aspect("equal") + ax.set_title("Flood mask") + plt.show() + +.. seealso:: + + Notice that our mask isn't perfect. There are little bloobs classified as + flood pixels that are clearly outside the flood region. For more + sophisticated analysis, see the image segmentation methods in + `scikit-image `__. + +Counting the number of ``True`` values is as easy as adding all of the boolean +values (remember that ``True`` corresponds to 1 and ``False`` to 0), which +we'll do with :meth:`xarray.DataArray.sum`: + +.. jupyter-execute:: + + flood_pixels = flood_mask.sum().values + print(flood_pixels) + +.. note:: + + We use ``.values`` above because :meth:`~xarray.DataArray.sum` returns an + :class:`xarray.DataArray` with a single value instead of the actual number. + This is usually not a problem but it looks ugly when printed, so we grab + the number with ``.values``. + +Finally, the flood area is the number of pixels multiplied by the area of each +pixel (30 x 30 m²): + +.. jupyter-execute:: + + flood_area = flood_pixels * 30**2 + + print(f"Flooded area is approximately {flood_area:.0f} m²") + +Values in m² are difficult to imagine so a good way to communicate these +numbers is to put them into real-life context. In this case, we can use the +`football pitches `__ as a unit +that many people will understand: + +.. jupyter-execute:: + + flood_area_pitches = flood_area / 7140 + + print(f"Flooded area is approximately {flood_area_pitches:.0f} football pitches") + +.. warning:: + + **This is a very rough estimate!** The final value will vary greatly if you + change the threshold used to generate the mask (try it yourself). + For a more thorough analysis of the disaster using remote-sensing data, see + `Silva Rotta et al. (2020) `__. + + +Other indices +------------- + +Calculating other indices will follow a very similar strategy to NDVI since +most of them only involve arithmetic operations on different bands. +As an example, let's calculate and plot the +`Modified Soil Adjusted Vegetation Index (MSAVI) `__ +for our two scenes: + +.. jupyter-execute:: + + import numpy as np + + # This time, use a loop and put them in a list to avoid repeated code + msavi_collection = [] + for scene in [before, after]: + msavi = ( + ( + 2 * scene.nir + 1 - np.sqrt( + (2 * scene.nir + 1) * 2 - 8 * (scene.nir - scene.red) + ) + ) / 2 + ) + msavi.name = "msavi" + msavi.attrs["long_name"] = "modified soil adjusted vegetation index" + msavi.attrs["units"] = "dimensionless" + msavi.attrs["title"] = scene.attrs["title"] + msavi_collection.append(msavi) + + # Plotting is mostly the same + fig, axes = plt.subplots(2, 1, figsize=(10, 12), layout="tight") + for ax, msavi in zip(axes, msavi_collection): + msavi.plot(ax=ax, vmin=-0.5, vmax=0.5, cmap="RdBu_r") + ax.set_title(msavi.attrs["title"]) + ax.set_aspect("equal") + plt.show() + +**With this same logic, you could calculate NBR and dNBR, other variants of +NDVI, NDSI, etc.** diff --git a/dev/_sources/install.rst.txt b/dev/_sources/install.rst.txt index e7ab468..8c964e5 100644 --- a/dev/_sources/install.rst.txt +++ b/dev/_sources/install.rst.txt @@ -52,6 +52,7 @@ These required dependencies should be installed automatically when you install xlandsat with ``pip`` or ``conda``: * `numpy `__ +* `scipy `__ * `xarray `__ * `scikit-image `__ * `pooch `__ diff --git a/dev/_sources/io.rst.txt b/dev/_sources/io.rst.txt new file mode 100644 index 0000000..78a45e5 --- /dev/null +++ b/dev/_sources/io.rst.txt @@ -0,0 +1,264 @@ +.. _io: + +Loading and cropping scenes +=========================== + +One of the main features of xlandsat is being able to read scenes downloaded +from `USGS EarthExplorer `__ along with all +of the associated metadata. +EarthExplorer allows you to download scenes in two main formats: + +1. As a single ``.tar`` file containing ``.TIF`` files for the bands and a file + ending in ``MTL.txt`` with the metadata. +2. As individual ``.TIF`` and metadata files. + +We support reading from both formats so you don't have really have to do much +after downloading the scene. + +In this tutorial, we'll be using some of our sample datasets instead of actual +full scenes from EarthExplorer. This is mostly so we don't have to use the +large (~1Gb) files, which can take a bit of time to download and load. The +scenes we use have been cropped to make them smaller. But everything we do here +is exactly the same when you use it on full scenes from EarthExplorer. + +.. admonition:: Downloading scenes from EarthExplorer + :class: tip + + New to EarthExplorer? Watch this tutorial on how to use the service and + download scenes that you can use with xlandsat: + https://www.youtube.com/watch?v=Wn_G4fvitV8 + +As always, we'll start by importing xlandsat and other libraries we'll use: + +.. jupyter-execute:: + + import xlandsat as xls + import matplotlib.pyplot as plt + import pathlib + +.. note:: + + All of this works for Collection 2 Level 2 and Level 1 scenes. + +Load a scene from a ``.tar`` archive +------------------------------------ + +If you downloaded a full scene from EarthExplorer in a ``.tar`` archive, +xlandsat can load the data from the archive directly. You don't have to unpack +it yourself and xlandsat reads everything in it by default. **This is usually +the easiest way to work with these data** but the downside is that the archive +can be quite large, particularly if you don't need all of the different bands +(see below for an alternative). + +To simulate this use case, let's download the ``.tar`` archive for one of our +sample scenes using :func:`xlandsat.datasets.fetch_liverpool`: + +.. jupyter-execute:: + + path_to_archive = xls.datasets.fetch_liverpool() + print(path_to_archive) + +This will download the ``.tar`` archive to your computer and return the path +to the file. + +.. note:: + + Our sample data come in ``.tar.gz`` archives, which have been compressed + (hence the ``.gz`` to save space and bandwidth. But all our functions work + the same with ``.tar`` or ``.tar.gz`` archives. + +To load a scene directly from the archive, use :func:`xlandsat.load_scene` with +the path to the archive file, which can be a string or a :class:`pathlib.Path`: + +.. jupyter-execute:: + + scene = xls.load_scene(path_to_archive) + scene + +The ``scene`` contains all of the bands available in the archive and has +metadata populated from the ``MTL.txt`` file. Notice also that the values for +each band have been converted to **surface reflectance** and **surface +temperature** automatically. + +Load a scene from a folder +-------------------------- + +If you don't need all of the bands, you can save space by downloading only the +``.TIF`` files that you need from EarthExplorer. Once you do that, place the +``.TIF`` files and the associated ``MTL.txt`` file (**don't forget it**) in +the same folder. It's important that a **folder can only contain a single +scene**, so if you're working with multiple scenes you'll have to place them in +different folders. + +Let's simulate this use case by telling +:func:`~xlandsat.datasets.fetch_liverpool` to unpack the archive and give us +the path to the folder instead of the archive: + +.. jupyter-execute:: + + path_to_folder = xls.datasets.fetch_liverpool(untar=True) + print(path_to_folder) + +Notice that there is now a ``.untar`` at the end of the name, indicating that +this is the folder where the archive has been unpacked. +We can use the :mod:`pathlib` module from the Python standard library to list +all of the files that are in this folder: + +.. jupyter-execute:: + + path_to_folder = pathlib.Path(path_to_folder) + print(f"This is indeed a folder: {path_to_folder.is_dir()}") + print("Folder contents:") + for file in path_to_folder.iterdir(): + print(f" {file.name}") + +As you can see, the band ``.TIF`` files are there as well as the ``MTL.txt`` +file. Now that we have the path to a folder that has these files, we can pass +it to :func:`xlandsat.load_scene` and it will do its job: + +.. jupyter-execute:: + + scene = xls.load_scene(path_to_folder) + scene + + +Notice that this is the same result we had before. + +.. hint:: + + Only the ``.TIF`` files present will be loaded by + :func:`xlandsat.load_scene`. So you don't need some of them, don't include + them in the folder. + + +The scene, bands, and metadata +------------------------------ + +The ``scene`` itself is a :class:`xarray.Dataset` that contains: + +1. ``easting`` and ``northing`` dimensions which are the UTM coordinates of + the pixels (in meters). +2. Several data variables that each represent a band. The bands are referenced + by name, not by number. Each band is a :class:`xarray.DataArray` that has + the same dimensions as the scene. +3. Missing values in the scene (either from the padding or out-of-bounds + pixels) are represented by :class:`numpy.nan`. +4. Metadata for the scene, each dimension, and each band. + +Placing a :class:`xarray.Dataset` or :class:`xarray.DataArray` at the end of a +Jupyter notebook cell will display a nice preview of the contents: + +.. jupyter-execute:: + + scene + +In the preview above, click on the icons to the right to access the metadata +for each dimension and band and a preview of their data values. The metadata +for the scene itself can be accessed by clicking in "Attributes". Go ahead and +explore what's available! + +The metadata is available programmatically through the ``attrs`` attribute of +the scene. It behaves like a dictionary: + +.. jupyter-execute:: + + print(scene.attrs["landsat_product_id"]) + print(scene.attrs["date_acquired"]) + +The metadata for the bands and the dimensions can be accessed the same way: + +.. jupyter-execute:: + + print(scene.blue.attrs["filename"]) + print(scene.easting.attrs["long_name"]) + +Selecting which bands to load +----------------------------- + +If you have more bands downloaded than you actually want to use, then we can +save time and memory by only loading the desired bands. +For example, if our only goal is to make an RGB composite, then we only really +need the red, green, and blue bands. +Instead of having to edit the ``.tar`` archive or move files out of our data +folder, we can tell :func:`xlandsat.load_scene` which bands we want by passing +it a list of band names like so: + +.. jupyter-execute:: + + scene = xls.load_scene(path_to_archive, bands=["red", "green", "blue"]) + scene + +This works the same if reading from an archive or from a folder that contains +more band files than we want: + +.. jupyter-execute:: + + scene = xls.load_scene(path_to_folder, bands=["red", "green", "blue"]) + scene + + +Loading only a segment of the scene +----------------------------------- + +Since Landsat scenes are large, it's not uncommon to need only a smaller +section of a scene. Limiting the spatial extent loaded can also help reduce the +memory requirement, particularly when loading a time series of scenes. +We could crop an existing scene after loading by using :meth:`xarray.Dataset.sel` +with the UTM bounding box of the desired region: + +.. jupyter-execute:: + + scene = xls.load_scene(path_to_archive) + cropped = scene.sel( + easting=slice(4.88e5, 4.90e5), + northing=slice(5.925e6, 5.927e6), + ) + cropped + +But this will still load the full scene, which **takes up time and memory**. A +**better way** to do this is to crop the scene directly when loading it: + +.. jupyter-execute:: + + cropped = xls.load_scene( + path_to_archive, + region=(4.88e5, 4.90e5, 5.925e6, 5.927e6), + ) + cropped + +Notice that in both examples we were able to use the natural UTM coordinates +of the scene instead of pixel numbers. This is particularly important when +cropping scenes with the same WRS path/row at different times, since their +boundaries won't coincide exactly and cropping by pixels would result in +misaligned images. + +Load the panchromatic band +-------------------------- + +The panchromatic band from Level 1 scenes will be ignored by +:func:`xlandsat.load_scene` if it's present in an archive or folder. +This is because of it's higher spatial resolution, which means that it can't +share dimension coordinates with the other bands. For this reason, we have +the separate function :func:`xarray.load_panchromatic` for loading it. +Just like with regular scenes, we can provide either a ``.tar`` archive or a +folder that contains the band and the ``MTL.txt`` file: + +.. jupyter-execute:: + + path_to_pan = xls.datasets.fetch_liverpool_panchromatic() + pan = xls.load_panchromatic(path_to_pan) + pan + +And we can also crop the panchromatic band upon loading to the same extent as +our regular scene: + +.. jupyter-execute:: + + cropped_pan = xls.load_panchromatic( + path_to_pan, + region=(4.88e5, 4.90e5, 5.925e6, 5.927e6), + ) + cropped_pan + +This is particularly useful for :ref:`pansharpening ` to make +higher resolution RGB composites. diff --git a/dev/_sources/missing-values.rst.txt b/dev/_sources/missing-values.rst.txt new file mode 100644 index 0000000..b9df8d3 --- /dev/null +++ b/dev/_sources/missing-values.rst.txt @@ -0,0 +1,69 @@ +.. _missing-values: + +Filling missing values +====================== + +Landsat Level 2 data can sometimes contain missing values, particularly around +bright clouds with dark shadows. These pixels will have a value of +``numpy.nan`` and can cause problems in other calculations. To fill them, we +can use the values of neighboring pixels to interpolate the missing values with +:func:`xlandsat.interpolate_missing`. + +Let's use our sample scene of the December 2015 eruption of `Momotombo volcano +`__, Nicaragua, to demonstrate how +it's done. + +First, we'll import the required packages and load the sample scene: + +.. jupyter-execute:: + + import xlandsat as xls + import matplotlib.pyplot as plt + import xarray as xr + import numpy as np + + path = xls.datasets.fetch_momotombo() + scene = xls.load_scene(path) + +Now we can plot an RGB composite to show some of the missing values. In order +to highlight them, we'll color the background of the plot in magenta so that +the missing values don't simply show up as white: + +.. jupyter-execute:: + + # Make the composite and add it to the scene + rgb = xls.composite(scene, rescale_to=(0.04, 0.17)) + + fig, ax = plt.subplots(1, 1, figsize=(10, 6)) + + rgb.plot.imshow(ax=ax) + + ax.set_facecolor("magenta") + ax.set_aspect("equal") + + plt.show() + +We can fill these values with reasonable estimates using interpolation: + +.. jupyter-execute:: + + scene_filled = xls.interpolate_missing(scene) + + rgb_filled = xls.composite(scene_filled, rescale_to=(0.04, 0.17)) + + fig, ax = plt.subplots(1, 1, figsize=(10, 6)) + + rgb_filled.plot.imshow(ax=ax) + + ax.set_facecolor("magenta") + ax.set_aspect("equal") + + plt.show() + +The interpolated scene no longer contains the magenta patches! + +.. warning:: + + This method works well when the missing data are only a few pixels or small + patches. Large portions of the image missing cannot be filled in accurately + by interpolation. diff --git a/dev/_sources/overview.rst.txt b/dev/_sources/overview.rst.txt index e26f600..a670c13 100644 --- a/dev/_sources/overview.rst.txt +++ b/dev/_sources/overview.rst.txt @@ -3,6 +3,39 @@ Overview ======== +Why use xlandsat? +----------------- + +One of the main features of xlandsat is the ability to easily read scenes +downloaded from `USGS EarthExplorer `__ into +:class:`xarray.Dataset` along with all of the available metadata, which is very +useful for processing and plotting multidimensional array data. +We offer a simple and easy-to-use tool for smaller scale analysis and +visualization, which we can do thanks to the power of xarray. + +When reading the data from the TIF files files using other tools like +`rioxarray `__, some of +the rich metadata can be missing since it's not always present in the TIF files +themselves. +Things like conversion factors, units, data provenance, WRS path/row numbers, +etc. +We take care of fetching that information from the ``*_MTL.txt`` files provided +by EarthExplorer so that xarray can use it, for example when annotating plots. + +On top of that, xlandsat also offers tools for +:ref:`generating composites `, +:ref:`pansharpening `, +:ref:`histogram equalization `, +and more! + +.. seealso:: + + More powerful (and more complicated) tools exist if your use case is beyond + what we can handle. + For cloud-based data processing, see the `Pangeo Project `__. + For other satellites and more powerful features, use `Satpy `__. + + The library ----------- @@ -19,66 +52,71 @@ single import: Download a sample scene ----------------------- -As an example, lets download a tar archive of a Landsat 8 scene of the -`Brumadinho tailings dam disaster `__ -that happened in 2019 in Brazil. -The archive is available on figshare at -https://doi.org/10.6084/m9.figshare.21665630 and includes scenes from before -and after the disaster as both the full scene and a cropped version. +The :mod:`xlandsat.datasets` module includes functions for downloading some +sample scenes that we can use. These are cropped to smaller regions of interest +in order to save download time. But everything we do here with these sample +scenes is exactly the same as you would do with a full scene from +EarthExplorer. -We'll use the functions in :mod:`xlandsat.datasets` to download the scenes from -before and after the disaster to our computer. To save space and bandwidth, -these are cropped versions of the full Landsat scenes. +As an example, lets download a ``.tar`` archive of a Landsat 9 scene of the +city of Manaus, in the Brazilian Amazon: .. jupyter-execute:: - path_before = xls.datasets.fetch_brumadinho_before() - path_after = xls.datasets.fetch_brumadinho_after() - print(path_before) - print(path_after) + path_to_archive = xls.datasets.fetch_manaus() + print(path_to_archive) +The :func:`~xlandsat.datasets.fetch_manaus` function downloads the data file +and returns the **path** to the archive on your machine as an :class:`str`. +The rest of this tutorial can be executed with your own data by changing the +``path_to_archive`` to point to your data file instead. .. tip:: + The path can also point to a folder with the ``.TIF`` and the ``*_MTL.txt`` + file instead of a ``.tar`` archive. + +.. note:: + Running the code above will only download the data once. We use `Pooch `__ to handle the downloads and it's smart - enough to check if the file already exists on your computer. See - :func:`pooch.retrieve` for more information. + enough to check if the file already exists on your computer. .. seealso:: If you want to use the full scenes instead of the cropped version, use :func:`pooch.retrieve` to fetch them from the figshare archive - https://doi.org/10.6084/m9.figshare.21665630. + https://doi.org/10.6084/m9.figshare.24167235.v1. -Load the scenes ---------------- +Load the scene +-------------- -Now that we have paths to the tar archives of the scenes, we can use +Now that we have the path to the tar archive of the scene, we can use :func:`xlandsat.load_scene` to read the bands and metadata directly from the -archives (without unpacking): +archive: .. jupyter-execute:: - before = xls.load_scene(path_before) - before + scene = xls.load_scene(path_to_archive) + scene -And the after scene: -.. jupyter-execute:: +.. tip:: - after = xls.load_scene(path_after) - after + Placing the ``scene`` variable at the end of a code cell in a Jupyter + notebook will display a nice preview of the data. This is very useful for + looking up metadata and seeing which bands were loaded. -.. admonition:: Did you notice? - :class: note +The scene is an :class:`xarray.Dataset`. It contains general metadata for the +scene and all of the bands available in the archive as +:class:`xarray.DataArray`. +The bands each have their own set of metadata as well and can be accessed by +name: + +.. jupyter-execute:: - If you look carefully at the coordinates for each scene, you may notice - that they don't exactly coincide in area. That's OK since :mod:`xarray` - knows how to take the pixel coordinates into account when doing - mathematical operations like calculating indices and differences between - scenes. + scene.nir Plot some reflectance bands @@ -87,25 +125,26 @@ Plot some reflectance bands Now we can use the :meth:`xarray.DataArray.plot` method to make plots of individual bands with :mod:`matplotlib`. A bonus is that :mod:`xarray` uses the metadata that :func:`xlandsat.load_scene` inserts into the scene to -automatically add labels and annotations to the plot. +automatically add labels and annotations to the plot: .. jupyter-execute:: import matplotlib.pyplot as plt - fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 12)) + band_names = list(scene.data_vars.keys()) - # Make the pseudocolor plots of the near infrared band - before.nir.plot(ax=ax1) - after.nir.plot(ax=ax2) + fig, axes = plt.subplots( + len(band_names), 1, figsize=(8, 16), layout="compressed", + ) # Set the title using metadata from each scene - ax1.set_title(f"Before: {before.attrs['title']}") - ax2.set_title(f"After: {after.attrs['title']}") + fig.suptitle(scene.attrs["title"]) - # Set the aspect to equal so that pixels are squares, not rectangles - ax1.set_aspect("equal") - ax2.set_aspect("equal") + for band, ax in zip(band_names, axes.ravel()): + # Make a pseudocolor plot of the band + scene[band].plot(ax=ax) + # Set the aspect to equal so that pixels are squares, not rectangles + ax.set_aspect("equal") plt.show() @@ -113,13 +152,13 @@ automatically add labels and annotations to the plot. What now? --------- -Learn more about what you can do with xlandsat and xarray: +Checkout some of the other things that you can do with xlandsat: +* :ref:`io` * :ref:`composites` * :ref:`indices` -* :ref:`pansharpen` -By getting the data into an :class:`xarray.Dataset`, xlandsat opens the door -for a huge range of operations. You now have access to everything that -:mod:`xarray` can do: interpolation, reduction, slicing, grouping, saving to -cloud-optimized formats, and much more. So go off and do something cool! +Plus, by getting the data into an :class:`xarray.Dataset`, xlandsat opens the +door for a huge range of operations. You now have access to everything that +:mod:`xarray` can do: reduction, slicing, grouping, saving to cloud-optimized +formats, and much more. So go off and do something cool! diff --git a/dev/_sources/plot-overlay.rst.txt b/dev/_sources/plot-overlay.rst.txt new file mode 100644 index 0000000..a88d2d6 --- /dev/null +++ b/dev/_sources/plot-overlay.rst.txt @@ -0,0 +1,100 @@ +.. _plot-overlay: + +Plotting bands overlaid on composites +===================================== + +Sometimes, we may want to overlay a part of a band (for example, the thermal +band) on top of a composite. The overlay band can be partially transparent, +cropped to a range of values, or both. +As an example, let's make a plot of the December 2015 eruption of `Momotombo +volcano `__, Nicaragua. +We'll overlay the thermal band (only pixels above 320 K) on top of an RGB +composite to show the ongoing lava flow. + +First, we'll import the required packages and load the sample scene: + +.. jupyter-execute:: + + import xlandsat as xls + import matplotlib.pyplot as plt + import xarray as xr + import numpy as np + + path = xls.datasets.fetch_momotombo() + scene = xls.load_scene(path) + # Fill the missing values due to the volcanic clouds to make it look nicer + scene = xls.interpolate_missing(scene) + scene + +Now we can plot an RGB composite and thermal band separately to see that they +have to show: + +.. jupyter-execute:: + + # Make the composite + rgb = xls.composite(scene, rescale_to=(0, 0.6)) + + # Histogram equalization for a better looking image + rgb = xls.equalize_histogram(rgb, clip_limit=0.02, kernel_size=200) + + # Plot the RGB and thermal separately + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 12)) + + rgb.plot.imshow(ax=ax1) + scene.thermal.plot.imshow(ax=ax2, cmap="magma") + + ax1.set_aspect("equal") + ax2.set_aspect("equal") + + plt.show() + +Notice that the lava flow is clearly visible as temperatures above 320 K in the +thermal band but it's difficult to see where the volcano and other landmarks +are. Looking at the RGB composite, we can't really make out the lava flow but +we have a clear picture of where the volcano is and where the old lava flows +are. A way to show the thermal data with the geographic context of the RGB is +to overlay the two in a single plot. + +To do so, we'll first create a version of the thermal band that has all pixels +with temperature below 320 K set to NaN (not-a-number). This is used to +indicate to matplotlib that a pixel should be transparent. An easy way to do +this is with the :func:`xarray.where` function: + +.. jupyter-execute:: + + # If the condition is true, use the thermal values. If it's false, use nan + lava = xr.where(scene.thermal >= 320, scene.thermal, np.nan, keep_attrs=True) + + fig, ax = plt.subplots(1, 1, figsize=(10, 6)) + + lava.plot.imshow(ax=ax, cmap="magma") + + ax.set_aspect("equal") + plt.show() + + +.. note:: + + We used the ``keep_attrs=True`` parameter to tell xarray that it should + keep the metadata from the original band in the lava-only version. This + will preserve the information on units, procedence, etc. But be careful + with this since it can lead to metadata being propagated when it's no + longer valid. + +Now that we have an :class:`xarray.DataArray` with the lava flow only, we can +plot that on top of the RGB composite and add a bit of transparency using the +``alpha`` parameter of ``imshow``. + +.. jupyter-execute:: + + fig, ax = plt.subplots(1, 1, figsize=(10, 6)) + + # RGB goes first so it's at the bottom + rgb.plot.imshow(ax=ax) + lava.plot.imshow(ax=ax, cmap="magma", alpha=0.6) + + ax.set_aspect("equal") + plt.show() + +With the plot above, all of the information we have available about the lava +flow is displayed in a nice format. diff --git a/dev/_static/copybutton.js b/dev/_static/copybutton.js index 02c5c82..2ea7ff3 100644 --- a/dev/_static/copybutton.js +++ b/dev/_static/copybutton.js @@ -20,7 +20,7 @@ const messages = { }, 'fr' : { 'copy': 'Copier', - 'copy_to_clipboard': 'Copié dans le presse-papier', + 'copy_to_clipboard': 'Copier dans le presse-papier', 'copy_success': 'Copié !', 'copy_failure': 'Échec de la copie', }, @@ -224,7 +224,7 @@ var copyTargetText = (trigger) => { var target = document.querySelector(trigger.attributes['data-clipboard-target'].value); // get filtered text - let exclude = '.linenos, .gp'; + let exclude = '.linenos'; let text = filterText(target, exclude); return formatCopyText(text, '', false, true, true, true, '', '') diff --git a/dev/_static/favicon.png b/dev/_static/favicon.png index 3b9494d..08e297f 100644 Binary files a/dev/_static/favicon.png and b/dev/_static/favicon.png differ diff --git a/dev/_static/logo.png b/dev/_static/logo.png new file mode 100644 index 0000000..e1bce55 Binary files /dev/null and b/dev/_static/logo.png differ diff --git a/dev/_static/logo.svg b/dev/_static/logo.svg new file mode 100644 index 0000000..5049062 --- /dev/null +++ b/dev/_static/logo.svg @@ -0,0 +1,286 @@ + + + + diff --git a/dev/_static/readme-example.jpg b/dev/_static/readme-example.jpg index fcd52f5..9adf9b8 100644 Binary files a/dev/_static/readme-example.jpg and b/dev/_static/readme-example.jpg differ diff --git a/dev/api/generated/xlandsat.composite.html b/dev/api/generated/xlandsat.composite.html index aee0159..b828f87 100644 --- a/dev/api/generated/xlandsat.composite.html +++ b/dev/api/generated/xlandsat.composite.html @@ -8,7 +8,7 @@ - xlandsat.composite | xlandsat v0.0.post36 + xlandsat.composite | xlandsat v0.0.post67+gd60f8e0 @@ -84,9 +84,14 @@
    @@ -117,14 +122,19 @@

    xlandsat v0.

    @@ -174,6 +199,16 @@

    xlandsat v0. xlandsat.pansharpen +
  • + + xlandsat.equalize_histogram + +
  • +
  • + + xlandsat.interpolate_missing + +
  • xlandsat.datasets.fetch_brumadinho_after @@ -194,11 +229,26 @@

    xlandsat v0. xlandsat.datasets.fetch_liverpool_panchromatic

  • +
  • + + xlandsat.datasets.fetch_manaus + +
  • +
  • + + xlandsat.datasets.fetch_momotombo + +
  • +
  • + + xlandsat.datasets.fetch_roraima + +
  • - Citing + Citing xlandsat
  • @@ -224,10 +274,20 @@

    xlandsat v0.

    - Links + Community

  • diff --git a/dev/api/generated/xlandsat.datasets.fetch_brumadinho_after.html b/dev/api/generated/xlandsat.datasets.fetch_brumadinho_after.html index f6ea7d0..1ecb240 100644 --- a/dev/api/generated/xlandsat.datasets.fetch_brumadinho_after.html +++ b/dev/api/generated/xlandsat.datasets.fetch_brumadinho_after.html @@ -8,7 +8,7 @@ - xlandsat.datasets.fetch_brumadinho_after | xlandsat v0.0.post36 + xlandsat.datasets.fetch_brumadinho_after | xlandsat v0.0.post67+gd60f8e0 @@ -50,7 +50,7 @@ - + @@ -84,9 +84,14 @@ diff --git a/dev/api/generated/xlandsat.datasets.fetch_brumadinho_before.html b/dev/api/generated/xlandsat.datasets.fetch_brumadinho_before.html index 911de1c..3eb01a7 100644 --- a/dev/api/generated/xlandsat.datasets.fetch_brumadinho_before.html +++ b/dev/api/generated/xlandsat.datasets.fetch_brumadinho_before.html @@ -8,7 +8,7 @@ - xlandsat.datasets.fetch_brumadinho_before | xlandsat v0.0.post36 + xlandsat.datasets.fetch_brumadinho_before | xlandsat v0.0.post67+gd60f8e0 @@ -84,9 +84,14 @@ diff --git a/dev/api/generated/xlandsat.datasets.fetch_liverpool.html b/dev/api/generated/xlandsat.datasets.fetch_liverpool.html index 5a4aff2..5f9cf65 100644 --- a/dev/api/generated/xlandsat.datasets.fetch_liverpool.html +++ b/dev/api/generated/xlandsat.datasets.fetch_liverpool.html @@ -8,7 +8,7 @@ - xlandsat.datasets.fetch_liverpool | xlandsat v0.0.post36 + xlandsat.datasets.fetch_liverpool | xlandsat v0.0.post67+gd60f8e0 @@ -84,9 +84,14 @@ diff --git a/dev/api/generated/xlandsat.datasets.fetch_liverpool_panchromatic.html b/dev/api/generated/xlandsat.datasets.fetch_liverpool_panchromatic.html index 84bf3ab..ab6741c 100644 --- a/dev/api/generated/xlandsat.datasets.fetch_liverpool_panchromatic.html +++ b/dev/api/generated/xlandsat.datasets.fetch_liverpool_panchromatic.html @@ -8,7 +8,7 @@ - xlandsat.datasets.fetch_liverpool_panchromatic | xlandsat v0.0.post36 + xlandsat.datasets.fetch_liverpool_panchromatic | xlandsat v0.0.post67+gd60f8e0 @@ -49,7 +49,7 @@ - + @@ -84,9 +84,14 @@ - +

    next

    -

    Citing

    +

    xlandsat.datasets.fetch_manaus

    @@ -491,7 +551,7 @@

    xlandsat.datasets.fetch_liverpool_panchromatic

    © Copyright 2023, The xlandsat developers.
    - Last updated on Feb 08, 2023.
    + Last updated on Sep 28, 2023.

    diff --git a/dev/api/generated/xlandsat.datasets.fetch_manaus.html b/dev/api/generated/xlandsat.datasets.fetch_manaus.html new file mode 100644 index 0000000..4cc474c --- /dev/null +++ b/dev/api/generated/xlandsat.datasets.fetch_manaus.html @@ -0,0 +1,572 @@ + + + + + + + + + + + xlandsat.datasets.fetch_manaus | xlandsat v0.0.post67+gd60f8e0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + +
    +
    + + + + + + + + + + +
    + +
    + +
    + + + + +
    +
    + + + + +
    +
    + + + + + + +
    +
    + + +
    +
    +
    +
    +
    + +
    +

    xlandsat.datasets.fetch_manaus

    + +
    +
    + +
    +
    +
    +
    + +
    + +
    +

    xlandsat.datasets.fetch_manaus#

    +
    +
    +xlandsat.datasets.fetch_manaus(untar=False)[source]#
    +

    Download a sample scene from Manaus, Brazil

    +

    Manaus is located in the Brazilian Amazon. The scene shows a part of the +city and the meeting of the Solimões and Negro rivers to form the Amazon +river.

    +

    This is a cropped version of a Landsat 9 scene from 2023/07/23, during +the annual Amazon river floods.

    +

    The scene was downloaded from USGS Earth Explorer. Original data are in the public +domain and are redistributed here in accordance with the Landsat Data +Distribution Policy.

    +

    Source: https://doi.org/10.6084/m9.figshare.24167235.v1 +(CC0)

    +
    +
    Parameters
    +

    untar (bool) – If True, unpack the tar archive after downloading and return a path to +the folder containing the unpacked files instead. Default is False.

    +
    +
    Returns
    +

    path (str) – The path to the downloaded .tar file that contains the scene.

    +
    +
    +
    + +
    + + +
    + +
    + +
    +
    + + +
    + + +
    +
    + + + + + + + \ No newline at end of file diff --git a/dev/api/generated/xlandsat.datasets.fetch_momotombo.html b/dev/api/generated/xlandsat.datasets.fetch_momotombo.html new file mode 100644 index 0000000..9d30500 --- /dev/null +++ b/dev/api/generated/xlandsat.datasets.fetch_momotombo.html @@ -0,0 +1,569 @@ + + + + + + + + + + + xlandsat.datasets.fetch_momotombo | xlandsat v0.0.post67+gd60f8e0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + +
    +
    + + + + + + + + + + +
    + +
    + +
    + + + + +
    +
    + + + + +
    +
    + + + + + + +
    +
    + + +
    +
    +
    +
    +
    + +
    +

    xlandsat.datasets.fetch_momotombo

    + +
    +
    + +
    +
    +
    +
    + +
    + +
    +

    xlandsat.datasets.fetch_momotombo#

    +
    +
    +xlandsat.datasets.fetch_momotombo(untar=False)[source]#
    +

    Download a sample scene from the December 2015 Momotombo volcano eruption

    +

    This is a cropped version of a Landsat 8 scene from 2015/12/05. It was +taken during the December 2015 eruption of Momotombo volcano, Nicaragua.

    +

    The scene was downloaded from USGS Earth Explorer. Original data are in the public +domain and are redistributed here in accordance with the Landsat Data +Distribution Policy.

    +

    Source: https://doi.org/10.6084/m9.figshare.21931089.v3 +(CC0)

    +
    +
    Parameters
    +

    untar (bool) – If True, unpack the tar archive after downloading and return a path to +the folder containing the unpacked files instead. Default is False.

    +
    +
    Returns
    +

    path (str) – The path to the downloaded .tar file that contains the scene.

    +
    +
    +
    + +
    + + +
    + +
    + +
    +
    + + +
    + + +
    +
    + + + + + + + \ No newline at end of file diff --git a/dev/api/generated/xlandsat.datasets.fetch_roraima.html b/dev/api/generated/xlandsat.datasets.fetch_roraima.html new file mode 100644 index 0000000..e05c39f --- /dev/null +++ b/dev/api/generated/xlandsat.datasets.fetch_roraima.html @@ -0,0 +1,571 @@ + + + + + + + + + + + xlandsat.datasets.fetch_roraima | xlandsat v0.0.post67+gd60f8e0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + +
    +
    + + + + + + + + + + +
    + +
    + +
    + + + + +
    +
    + + + + +
    +
    + + + + + + +
    +
    + + +
    +
    +
    +
    +
    + +
    +

    xlandsat.datasets.fetch_roraima

    + +
    +
    + +
    +
    +
    +
    + +
    + +
    +

    xlandsat.datasets.fetch_roraima#

    +
    +
    +xlandsat.datasets.fetch_roraima(untar=False)[source]#
    +

    Download a sample scene from Mount Roraima surrounded by clouds

    +

    Roraima is a tepui located in the junction of Brazil, Guyana, and +Venezuela. It’s famous for the near-constant cloud coverage.

    +

    This is a cropped version of a Landsat 8 scene from 2015/10/04, which is +one of the rare relatively cloud-free scenes available.

    +

    The scene was downloaded from USGS Earth Explorer. Original data are in the public +domain and are redistributed here in accordance with the Landsat Data +Distribution Policy.

    +

    Source: https://doi.org/10.6084/m9.figshare.24143622.v1 +(CC0)

    +
    +
    Parameters
    +

    untar (bool) – If True, unpack the tar archive after downloading and return a path to +the folder containing the unpacked files instead. Default is False.

    +
    +
    Returns
    +

    path (str) – The path to the downloaded .tar file that contains the scene.

    +
    +
    +
    + +
    + + +
    + +
    + +
    +
    + + +
    + + +
    +
    + + + + + + + \ No newline at end of file diff --git a/dev/api/generated/xlandsat.equalize_histogram.html b/dev/api/generated/xlandsat.equalize_histogram.html new file mode 100644 index 0000000..2c1297f --- /dev/null +++ b/dev/api/generated/xlandsat.equalize_histogram.html @@ -0,0 +1,586 @@ + + + + + + + + + + + xlandsat.equalize_histogram | xlandsat v0.0.post67+gd60f8e0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + +
    +
    + + + + + + + + + + +
    + +
    + +
    + + + + +
    +
    + + + + +
    +
    + + + + + + +
    +
    + + +
    +
    +
    +
    +
    + +
    +

    xlandsat.equalize_histogram

    + +
    +
    + +
    +
    +
    +
    + +
    + +
    +

    xlandsat.equalize_histogram#

    +
    +
    +xlandsat.equalize_histogram(composite, kernel_size=None, clip_limit=0.01)[source]#
    +

    Adaptive histogram equalization for a composite

    +

    Use this function to enhance the contrast of a composite when there are a +few very dark or very light patches that dominate the color range. Use this +instead of rescaling intensity (contrast stretching) to try to preserve +some detail in the light/dark patches.

    +

    If the composite has an alpha channel (transparency), it will be copied to +the output intact.

    +
    +

    Warning

    +

    Results can be very bad if there are missing values (NaNs) in the +composite. Use xlandsat.interpolate_missing on the scene first +(before creating the composite) if that is the case.

    +
    +
    +
    Parameters
    +
    +
    +
    Returns
    +

    equalized_composite (xarray.DataArray) – The composite after equalization, scaled back to unsigned 8-bit integer +range.

    +
    +
    +

    Notes

    +

    This function first converts the composite from the RGB color space to the +HSV color space. Then, it +applies skimage.exposure.equalize_adapthist to the values +(intensity) channel. Finally, the composite is converted back into the RGB +color space.

    +
    + +
    + + +
    + +
    + +
    +
    + + +
    + + +
    +
    + + + + + + + \ No newline at end of file diff --git a/dev/api/generated/xlandsat.interpolate_missing.html b/dev/api/generated/xlandsat.interpolate_missing.html new file mode 100644 index 0000000..103c0ba --- /dev/null +++ b/dev/api/generated/xlandsat.interpolate_missing.html @@ -0,0 +1,572 @@ + + + + + + + + + + + xlandsat.interpolate_missing | xlandsat v0.0.post67+gd60f8e0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + +
    +
    + + + + + + + + + + +
    + +
    + +
    + + + + +
    +
    + + + + +
    +
    + + + + + + +
    +
    + + +
    +
    +
    +
    +
    + +
    +

    xlandsat.interpolate_missing

    + +
    +
    + +
    +
    +
    +
    + +
    + +
    +

    xlandsat.interpolate_missing#

    +
    +
    +xlandsat.interpolate_missing(scene, pixel_radius=20)[source]#
    +

    Fill missing values (NaNs) in a scene by cubic interpolation

    +

    Each missing value is filled by interpolating the pixels within a +neighboring region (controlled by pixel_radius) using a piecewise cubic +2D interpolator. Interpolation is done for each band in a scene separately.

    +

    Note that this is mostly good if there are a few missing values, not large +regions of the scene.

    +
    +
    Parameters
    +
      +
    • scene (xarray.Dataset) – A Landsat scene, as read with xlandsat.load_scene.

    • +
    • pixel_radius (int) – Number of pixels to the east, west, south, and north of a missing value +that will be used for interpolation. Smaller values make for faster +interpolation but may lead to bad results if many missing values are +grouped together.

    • +
    +
    +
    Returns
    +

    filled_scene (xarray.Dataset) – The scene with missing values filled in.

    +
    +
    +
    + +
    + + +
    + +
    + +
    +
    + + +
    + + +
    +
    + + + + + + + \ No newline at end of file diff --git a/dev/api/generated/xlandsat.load_panchromatic.html b/dev/api/generated/xlandsat.load_panchromatic.html index de5c9a7..1ff85d1 100644 --- a/dev/api/generated/xlandsat.load_panchromatic.html +++ b/dev/api/generated/xlandsat.load_panchromatic.html @@ -8,7 +8,7 @@ - xlandsat.load_panchromatic | xlandsat v0.0.post36 + xlandsat.load_panchromatic | xlandsat v0.0.post67+gd60f8e0 @@ -84,9 +84,14 @@
    - +

    next

    -

    xlandsat.datasets.fetch_brumadinho_after

    +

    xlandsat.equalize_histogram

    @@ -495,7 +555,7 @@

    xlandsat.pansharpen

    © Copyright 2023, The xlandsat developers.
    - Last updated on Feb 08, 2023.
    + Last updated on Sep 28, 2023.

    diff --git a/dev/api/generated/xlandsat.save_scene.html b/dev/api/generated/xlandsat.save_scene.html index 478a24a..76c8c4e 100644 --- a/dev/api/generated/xlandsat.save_scene.html +++ b/dev/api/generated/xlandsat.save_scene.html @@ -8,7 +8,7 @@ - xlandsat.save_scene | xlandsat v0.0.post36 + xlandsat.save_scene | xlandsat v0.0.post67+gd60f8e0 @@ -84,9 +84,14 @@
    @@ -117,14 +122,19 @@

    xlandsat v0.

    @@ -174,6 +199,16 @@

    xlandsat v0. xlandsat.pansharpen +
  • + + xlandsat.equalize_histogram + +
  • +
  • + + xlandsat.interpolate_missing + +
  • xlandsat.datasets.fetch_brumadinho_after @@ -194,11 +229,26 @@

    xlandsat v0. xlandsat.datasets.fetch_liverpool_panchromatic

  • +
  • + + xlandsat.datasets.fetch_manaus + +
  • +
  • + + xlandsat.datasets.fetch_momotombo + +
  • +
  • + + xlandsat.datasets.fetch_roraima + +
  • - Citing + Citing xlandsat
  • @@ -224,10 +274,20 @@

    xlandsat v0.

    - Links + Community

  • @@ -467,7 +527,7 @@

    xlandsat.save_scenepathlib.Path) – The desired path of the output tar archive. The file extension can be .tar (uncompressed) or .tar.gz, .tar.xz, or .tar.bz2 to make a compressed archive.

    -
  • scene (xarray.Dataset) – The scene including UTM easting and northing as dimensional +

  • scene (xarray.Dataset) – The scene including UTM easting and northing as dimensional coordinates, bands as 2D arrays of the given type as variables, and metadata read from the MTL file and other CF compliant fields in the attrs attribute.

  • @@ -508,7 +568,7 @@

    xlandsat.save_scene

    © Copyright 2023, The xlandsat developers.
    - Last updated on Feb 08, 2023.
    + Last updated on Sep 28, 2023.

    diff --git a/dev/api/index.html b/dev/api/index.html index 2f9d763..fecc5ba 100644 --- a/dev/api/index.html +++ b/dev/api/index.html @@ -8,7 +8,7 @@ - List of functions and classes (API) | xlandsat v0.0.post36 + List of functions and classes (API) | xlandsat v0.0.post67+gd60f8e0 @@ -50,7 +50,7 @@ - + @@ -84,9 +84,14 @@
    @@ -117,14 +122,19 @@

    xlandsat v0.

    @@ -174,6 +199,16 @@

    xlandsat v0. xlandsat.pansharpen +
  • + + xlandsat.equalize_histogram + +
  • +
  • + + xlandsat.interpolate_missing + +
  • xlandsat.datasets.fetch_brumadinho_after @@ -194,11 +229,26 @@

    xlandsat v0. xlandsat.datasets.fetch_liverpool_panchromatic

  • +
  • + + xlandsat.datasets.fetch_manaus + +
  • +
  • + + xlandsat.datasets.fetch_momotombo + +
  • +
  • + + xlandsat.datasets.fetch_roraima + +
  • - Citing + Citing xlandsat
  • @@ -224,10 +274,20 @@

    xlandsat v0.

    - Links + Community

  • diff --git a/dev/changes.html b/dev/changes.html index f7da80b..d411b65 100644 --- a/dev/changes.html +++ b/dev/changes.html @@ -8,7 +8,7 @@ - Changelog | xlandsat v0.0.post36 + Changelog | xlandsat v0.0.post67+gd60f8e0 @@ -84,9 +84,14 @@ diff --git a/dev/citing.html b/dev/citing.html index 93d9628..f64397c 100644 --- a/dev/citing.html +++ b/dev/citing.html @@ -8,7 +8,7 @@ - Citing | xlandsat v0.0.post36 + Citing xlandsat | xlandsat v0.0.post67+gd60f8e0 @@ -50,7 +50,7 @@ - + @@ -84,9 +84,14 @@
    @@ -117,14 +122,19 @@

    xlandsat v0.

    @@ -174,6 +199,16 @@

    xlandsat v0. xlandsat.pansharpen +
  • + + xlandsat.equalize_histogram + +
  • +
  • + + xlandsat.interpolate_missing + +
  • xlandsat.datasets.fetch_brumadinho_after @@ -194,11 +229,26 @@

    xlandsat v0. xlandsat.datasets.fetch_liverpool_panchromatic

  • +
  • + + xlandsat.datasets.fetch_manaus + +
  • +
  • + + xlandsat.datasets.fetch_momotombo + +
  • +
  • + + xlandsat.datasets.fetch_roraima + +
  • - Citing + Citing xlandsat
  • @@ -224,10 +274,20 @@

    xlandsat v0.

    - Links + Community

  • - Citing + Citing xlandsat
  • @@ -224,10 +274,20 @@

    xlandsat v0.

    - Links + Community

  • diff --git a/dev/composites.html b/dev/composites.html index 43f9056..654eeac 100644 --- a/dev/composites.html +++ b/dev/composites.html @@ -8,7 +8,7 @@ - Composites | xlandsat v0.0.post36 + Making composites | xlandsat v0.0.post67+gd60f8e0 @@ -49,8 +49,8 @@ - - + + @@ -84,9 +84,14 @@
    @@ -117,14 +122,19 @@

    xlandsat v0.

    @@ -174,6 +199,16 @@

    xlandsat v0. xlandsat.pansharpen +
  • + + xlandsat.equalize_histogram + +
  • +
  • + + xlandsat.interpolate_missing + +
  • xlandsat.datasets.fetch_brumadinho_after @@ -194,11 +229,26 @@

    xlandsat v0. xlandsat.datasets.fetch_liverpool_panchromatic

  • +
  • + + xlandsat.datasets.fetch_manaus + +
  • +
  • + + xlandsat.datasets.fetch_momotombo + +
  • +
  • + + xlandsat.datasets.fetch_roraima + +
  • - Citing + Citing xlandsat
  • @@ -224,10 +274,20 @@

    xlandsat v0.

    - Links + Community

  • - Citing + Citing xlandsat
  • @@ -221,10 +271,20 @@

    xlandsat v0.

    - Links + Community

    +

    E

    + + +
    +

    F

    - + +
    + +

    I

    + +
    @@ -487,7 +571,7 @@

    X

    © Copyright 2023, The xlandsat developers.
    - Last updated on Feb 08, 2023.
    + Last updated on Sep 28, 2023.

  • diff --git a/dev/index.html b/dev/index.html index 2c4c8be..a8786b8 100644 --- a/dev/index.html +++ b/dev/index.html @@ -8,7 +8,7 @@ - xlandsat v0.0.post36 + xlandsat v0.0.post67+gd60f8e0 @@ -83,9 +83,14 @@ -

    xlandsat is Python library for -loading Landsat scenes downloaded from -USGS EarthExplorer into -xarray.Dataset containers. +

    xlandsat is Python library for loading and analyzing Landsat scenes +downloaded from USGS EarthExplorer with +the power of xarray. We take care of reading the metadata from the *_MTL.txt files provided by -EarthExplorer and organizing the bands into a single data structure for easier -manipulation, processing, and visualization.

    +EarthExplorer and organizing the bands into a single xarray.Dataset +data structure for easier manipulation, processing, and visualization.

    Here’s a quick example:

    -
    import xlandsat as xls
    +
    import xlandsat as xls
    +import matplotlib.pyplot as plt
     
    -# Download a cropped Landsat 8 scene from the Brumadinho dam disaster
    -# (Brazil). The function downloads it and returns the path to the .tar file
    -# containing the scene.
    -path = xls.datasets.fetch_brumadinho_after()
    +# Download a sample Landsat 9 scene in EarthExplorer format
    +path_to_scene_file = xls.datasets.fetch_manaus()
     
    -# Load the scene directly from the archive (no need to unpack it)
    -scene = xls.load_scene(path)
    +# Load the data from the file into an xarray.Dataset
    +scene = xls.load_scene(path_to_scene_file)
     
    -# Make an RGB composite and stretch the contrast
    -rgb = xls.composite(scene, rescale_to=[0.03, 0.2])
    +# Make an RGB composite as an xarray.DataArray
    +rgb = xls.composite(scene, rescale_to=[0.02, 0.2])
     
    -# Plot the composite
    +# Plot the composite using xarray's plotting machinery
     rgb.plot.imshow()
    +
    +# Annotate the plot with the rich metadata xlandsat adds to the scene
    +plt.title(f"{rgb.attrs['title']}\n{rgb.attrs['long_name']}")
    +plt.axis("scaled")
    +plt.show()
     
    -
    <matplotlib.image.AxesImage at 0x7fb2e641e7d0>
    -
    -
    -_images/index_0_1.png +_images/index_0_0.png

    @@ -523,7 +583,7 @@

    xlandsat

    Need help?

    Open an Issue on GitHub.

    -

    Join the conversation

    +

    Join the conversation

    @@ -543,7 +603,7 @@

    xlandsat

    Using for research?

    Citations help support our work!

    -

    Citing

    +

    Citing xlandsat

    @@ -552,8 +612,8 @@

    xlandsat


    Note

    -

    Only Landsat 8 and 9 Collection 2 Level 2 data products are supported at -the moment.

    +

    Only Landsat 8 and 9 Level 1 & 2 data products have been tested at the +moment.

    xlandsat is ready for use but still changing

    @@ -566,11 +626,14 @@

    xlandsat

    -

    Looking for large-scale cloud-based processing?

    -

    Our goal is not to provide a solution for large-scale data processing. The -target is smaller scale analysis done on individual computers (which is -probably the main way EarthExplorer is used). For cloud-based data -processing, see the Pangeo Project.

    +

    Looking for large-scale processing or other satellites?

    +

    Our goal is not to provide a solution for large-scale data processing. +Our target is smaller scale analysis done on individual computers (which is +probably the main way EarthExplorer is used).

    +
      +
    • For cloud-based data processing, see the Pangeo Project.

    • +
    • For other satellites and more powerful features, use Satpy.

    • +
    @@ -604,7 +667,7 @@

    xlandsat

    © Copyright 2023, The xlandsat developers.
    - Last updated on Feb 08, 2023.
    + Last updated on Sep 28, 2023.

    diff --git a/dev/indices.html b/dev/indices.html index 1c7b021..67f7147 100644 --- a/dev/indices.html +++ b/dev/indices.html @@ -8,7 +8,7 @@ - Indices | xlandsat v0.0.post36 + Working with indices | xlandsat v0.0.post67+gd60f8e0 @@ -46,11 +46,12 @@ + - + @@ -84,9 +85,14 @@
    @@ -117,14 +123,19 @@

    xlandsat v0.

    @@ -174,6 +200,16 @@

    xlandsat v0. xlandsat.pansharpen +
  • + + xlandsat.equalize_histogram + +
  • +
  • + + xlandsat.interpolate_missing + +
  • xlandsat.datasets.fetch_brumadinho_after @@ -194,11 +230,26 @@

    xlandsat v0. xlandsat.datasets.fetch_liverpool_panchromatic

  • +
  • + + xlandsat.datasets.fetch_manaus + +
  • +
  • + + xlandsat.datasets.fetch_momotombo + +
  • +
  • + + xlandsat.datasets.fetch_roraima + +
  • - Citing + Citing xlandsat
  • @@ -224,10 +275,20 @@

    xlandsat v0.

    - Links + Community

  • -

    Now lets plot it:

    +

    Now we can do the same for the after scene:

    -
    fig, ax = plt.subplots(1, 1, figsize=(10, 6))
    -ndvi_change.plot(ax=ax, vmin=-1, vmax=1, cmap="PuOr")
    -ax.set_aspect("equal")
    -plt.show()
    +
    ndvi_after = (after.nir - after.red) / (after.nir + after.red)
    +ndvi_after
     
    -_images/indices_4_0.png -
    -
    -

    There’s some noise in the cloudy areas of both scenes in the northeast but -otherwise this plots highlights the area affected by flooding from the dam -collapse in bright red at the center.

    +
    + + + + + + + + + + + + + + +
    <xarray.DataArray (northing: 300, easting: 400)>
    +array([[0.5073, 0.4705, 0.4907, ..., 0.515 , 0.4946, 0.4565],
    +       [0.4277, 0.478 , 0.4817, ..., 0.4949, 0.5166, 0.5474],
    +       [0.37  , 0.4304, 0.441 , ..., 0.522 , 0.5786, 0.6226],
    +       ...,
    +       [0.57  , 0.594 , 0.595 , ..., 0.328 , 0.462 , 0.54  ],
    +       [0.5845, 0.5796, 0.583 , ..., 0.4995, 0.566 , 0.572 ],
    +       [0.61  , 0.5664, 0.5938, ..., 0.609 , 0.5903, 0.605 ]],
    +      dtype=float16)
    +Coordinates:
    +  * easting   (easting) float64 5.844e+05 5.844e+05 ... 5.963e+05 5.964e+05
    +  * northing  (northing) float64 -2.232e+06 -2.232e+06 ... -2.223e+06 -2.223e+06
    +
    +

    And add some metadata for xarray to find when making plots:

    +
    +
    +
    for ndvi in [ndvi_before, ndvi_after]:
    +    ndvi.attrs["long_name"] = "normalized difference vegetation index"
    +    ndvi.attrs["units"] = "dimensionless"
    +ndvi_before.attrs["title"] = "NDVI before"
    +ndvi_after.attrs["title"] = "NDVI after"
    +
    +
    +
    +
    +
    +
    +

    Now we can make pseudo-color plots of the NDVI from before and after the +disaster:

    +
    +
    +
    fig, axes = plt.subplots(2, 1, figsize=(10, 12), layout="tight")
    +for ax, ndvi in zip(axes, [ndvi_before, ndvi_after]):
    +    # Limit the scale to [-1, +1] so the plots are easier to compare
    +    ndvi.plot(ax=ax, vmin=-1, vmax=1, cmap="RdBu_r")
    +    ax.set_title(ndvi.attrs["title"])
    +    ax.set_aspect("equal")
    +plt.show()
    +
    +
    +
    +
    +_images/indices_5_0.png +
    +
    + +
    +

    Tracking differences#

    +

    An advantage of having our data in xarray.DataArray format is that we +can perform coordinate-aware calculations. This means that taking the +difference between our two arrays will take into account the coordinates of +each pixel and only perform the operation where the coordinates align.

    +

    We can calculate the change in NDVI from one scene to the other by taking the +difference:

    +
    +
    +
    ndvi_change = ndvi_before - ndvi_after
    +
    +# Add som metadata for xarray
    +ndvi_change.name = "ndvi_change"
    +ndvi_change.attrs["long_name"] = "NDVI change"
    +ndvi_change.attrs["title"] = (
    +    f"NDVI change between {before.attrs['date_acquired']} and "
    +    f"{after.attrs['date_acquired']}"
    +)
    +ndvi_change
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + +
    <xarray.DataArray 'ndvi_change' (northing: 300, easting: 370)>
    +array([[ 0.05908 ,  0.06323 ,  0.0542  , ...,  0.004395, -0.009766,
    +         0.07324 ],
    +       [ 0.0498  ,  0.07764 ,  0.07495 , ...,  0.0957  ,  0.012695,
    +         0.003906],
    +       [-0.010254,  0.11743 ,  0.0747  , ...,  0.03125 ,  0.01807 ,
    +         0.04004 ],
    +       ...,
    +       [-0.000977,  0.01123 ,  0.000977, ...,  0.00928 ,  0.01367 ,
    +         0.00708 ],
    +       [ 0.00879 ,  0.02344 ,  0.01318 , ...,  0.006836,  0.00586 ,
    +         0.001221],
    +       [-0.01221 ,  0.02637 ,  0.006836, ...,  0.01343 ,  0.01221 ,
    +         0.0105  ]], dtype=float16)
    +Coordinates:
    +  * easting   (easting) float64 5.844e+05 5.844e+05 ... 5.954e+05 5.955e+05
    +  * northing  (northing) float64 -2.232e+06 -2.232e+06 ... -2.223e+06 -2.223e+06
    +Attributes:
    +    long_name:  NDVI change
    +    title:      NDVI change between 2019-01-14 and 2019-01-30
    +
    +
    +

    Did you notice?

    +

    The keen-eyed among you may have noticed that the number of points along +the "easting" dimension has decreased. This is because xarray +only makes the calculations for pixels where the two scenes coincide. In +this case, there was an East-West shift between scenes but our calculations +take that into account.

    +
    +

    Now lets plot the difference:

    +
    +
    +
    fig, ax = plt.subplots(1, 1, figsize=(10, 6))
    +ndvi_change.plot(ax=ax, vmin=-1, vmax=1, cmap="PuOr")
    +ax.set_aspect("equal")
    +ax.set_title(ndvi_change.attrs["title"])
    +plt.show()
    +
    +
    +
    +
    +_images/indices_7_0.png +
    +
    +

    There’s some noise in the cloudy areas of both scenes in the northeast but +otherwise this plots highlights the area affected by flooding from the dam +collapse in purple at the center.

    +
    +
    +

    Estimating area#

    +

    One things we can do with indices and their differences in time is calculated +area estimates. If we know that the region of interest has index values +within a given value range, the area can be calculated by counting the number +of pixels within that range (a pixel in Landsat 8/9 scenes is 30 x 30 = 900 m²).

    +

    First, let’s slice our NDVI difference to just the flooded area to avoid the +effect of the clouds in North. We’ll use the xarray.DataArray.sel +method to slice using the UTM coordinates of the scene:

    +
    +
    +
    flood = ndvi_change.sel(
    +    easting=slice(587000, 594000),
    +    northing=slice(-2230000, -2225000),
    +)
    +
    +fig, ax = plt.subplots(1, 1, figsize=(10, 6))
    +flood.plot(ax=ax, vmin=-1, vmax=1, cmap="PuOr")
    +ax.set_aspect("equal")
    +plt.show()
    +
    +
    +
    +
    +_images/indices_8_0.png +
    +
    +

    Now we can create a mask of the flood area by selecting pixels that have a high +NDVI difference. Using a > comparison (or any other logical operator in +Python), we can create a boolean (True or False) +xarray.DataArray as our mask:

    +
    +
    +
    # Threshold value determined by trial-and-error
    +flood_mask = flood > 0.3
    +
    +# Add some metadata for xarray
    +flood_mask.attrs["long_name"] = "flood mask"
    +
    +flood_mask
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + +
    <xarray.DataArray 'ndvi_change' (northing: 167, easting: 234)>
    +array([[False, False, False, ..., False, False, False],
    +       [False, False, False, ..., False, False, False],
    +       [False, False, False, ..., False, False, False],
    +       ...,
    +       [False, False, False, ..., False, False, False],
    +       [False, False, False, ..., False, False, False],
    +       [False, False, False, ..., False, False, False]])
    +Coordinates:
    +  * easting   (easting) float64 5.87e+05 5.87e+05 ... 5.94e+05 5.94e+05
    +  * northing  (northing) float64 -2.23e+06 -2.23e+06 ... -2.225e+06 -2.225e+06
    +Attributes:
    +    long_name:  flood mask
    +
    +

    Plotting boolean arrays will use 1 to represent True and 0 to represent +False:

    +
    +
    +
    fig, ax = plt.subplots(1, 1, figsize=(10, 6))
    +flood_mask.plot(ax=ax, cmap="gray")
    +ax.set_aspect("equal")
    +ax.set_title("Flood mask")
    +plt.show()
    +
    +
    +
    +
    +_images/indices_10_0.png +
    +
    +
    +

    See also

    +

    Notice that our mask isn’t perfect. There are little bloobs classified as +flood pixels that are clearly outside the flood region. For more +sophisticated analysis, see the image segmentation methods in +scikit-image.

    +
    +

    Counting the number of True values is as easy as adding all of the boolean +values (remember that True corresponds to 1 and False to 0), which +we’ll do with xarray.DataArray.sum:

    +
    +
    +
    flood_pixels = flood_mask.sum().values
    +print(flood_pixels)
    +
    +
    +
    +
    +
    2095
    +
    +
    +
    +
    +
    +

    Note

    +

    We use .values above because sum returns an +xarray.DataArray with a single value instead of the actual number. +This is usually not a problem but it looks ugly when printed, so we grab +the number with .values.

    +
    +

    Finally, the flood area is the number of pixels multiplied by the area of each +pixel (30 x 30 m²):

    +
    +
    +
    flood_area = flood_pixels * 30**2
    +
    +print(f"Flooded area is approximately {flood_area:.0f} m²")
    +
    +
    +
    +
    +
    Flooded area is approximately 1885500 m²
    +
    +
    +
    +
    +

    Values in m² are difficult to imagine so a good way to communicate these +numbers is to put them into real-life context. In this case, we can use the +football pitches as a unit +that many people will understand:

    +
    +
    +
    flood_area_pitches = flood_area / 7140
    +
    +print(f"Flooded area is approximately {flood_area_pitches:.0f} football pitches")
    +
    +
    +
    +
    +
    Flooded area is approximately 264 football pitches
    +
    +
    +
    +
    +
    +

    Warning

    +

    This is a very rough estimate! The final value will vary greatly if you +change the threshold used to generate the mask (try it yourself). +For a more thorough analysis of the disaster using remote-sensing data, see +Silva Rotta et al. (2020).

    +
    +
    +
    +

    Other indices#

    +

    Calculating other indices will follow a very similar strategy to NDVI since +most of them only involve arithmetic operations on different bands. +As an example, let’s calculate and plot the +Modified Soil Adjusted Vegetation Index (MSAVI) +for our two scenes:

    +
    +
    +
    import numpy as np
    +
    +# This time, use a loop and put them in a list to avoid repeated code
    +msavi_collection = []
    +for scene in [before, after]:
    +    msavi = (
    +        (
    +            2 * scene.nir + 1 - np.sqrt(
    +                (2 * scene.nir + 1) * 2 - 8 * (scene.nir - scene.red)
    +            )
    +        ) / 2
    +    )
    +    msavi.name = "msavi"
    +    msavi.attrs["long_name"] = "modified soil adjusted vegetation index"
    +    msavi.attrs["units"] = "dimensionless"
    +    msavi.attrs["title"] = scene.attrs["title"]
    +    msavi_collection.append(msavi)
    +
    +# Plotting is mostly the same
    +fig, axes = plt.subplots(2, 1, figsize=(10, 12), layout="tight")
    +for ax, msavi in zip(axes, msavi_collection):
    +    msavi.plot(ax=ax, vmin=-0.5, vmax=0.5, cmap="RdBu_r")
    +    ax.set_title(msavi.attrs["title"])
    +    ax.set_aspect("equal")
    +plt.show()
    +
    +
    +
    +
    +_images/indices_14_0.png +
    +
    +

    With this same logic, you could calculate NBR and dNBR, other variants of +NDVI, NDSI, etc.

    @@ -1823,7 +3353,7 @@

    NDVI#<

    previous

    -

    Composites

    +

    Making composites

    @@ -1841,7 +3371,7 @@

    NDVI#<

    © Copyright 2023, The xlandsat developers.
    - Last updated on Feb 08, 2023.
    + Last updated on Sep 28, 2023.

    diff --git a/dev/install.html b/dev/install.html index 6779862..14ddd83 100644 --- a/dev/install.html +++ b/dev/install.html @@ -8,7 +8,7 @@ - Installing | xlandsat v0.0.post36 + Installing | xlandsat v0.0.post67+gd60f8e0 @@ -49,7 +49,7 @@ - + @@ -84,9 +84,14 @@
    - -