Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add function to plot composites with ipyleaflet #98

Merged
merged 2 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions doc/_static/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ div.jupyter_container .cell_output {
.jupyter_container div.code_cell pre {
padding: 10px;
}

/* Disable borders in darkmode since it messes up the ipyleaflet plots */
html[data-theme="dark"] .bd-content img:not(.only-dark):not(.dark-light) {
background: none;
}
1 change: 1 addition & 0 deletions doc/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Visualization
composite
equalize_histogram
adjust_l1_colors
plot_composite_leaflet

Indices
-------
Expand Down
12 changes: 10 additions & 2 deletions doc/composites.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,16 @@ instead:

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
It's also possible to add a composite to an interactive `ipyleaflet
<https://ipyleaflet.readthedocs.io/en/latest/>`__ map using
:func:`xlandsat.plot_composite_leaflet`:

.. jupyter-execute::

xls.plot_composite_leaflet(rgb)

This composite 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.

Expand Down
1 change: 1 addition & 0 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"pooch": ("https://www.fatiando.org/pooch/latest/", None),
"matplotlib": ("https://matplotlib.org/stable/", None),
"scipy": ("https://docs.scipy.org/doc/scipy/", None),
"ipyleaflet": ("https://ipyleaflet.readthedocs.io/en/latest/", None),
}

# Autosummary pages will be generated by sphinx-autogen instead of sphinx-build
Expand Down
16 changes: 6 additions & 10 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,20 @@ Here's a quick example:
.. jupyter-execute::

import xlandsat as xls
import matplotlib.pyplot as plt

# Download a sample Landsat 9 scene in EarthExplorer format
path_to_scene_file = xls.datasets.fetch_manaus()

# Load the data from the file into an xarray.Dataset
scene = xls.load_scene(path_to_scene_file)
# Display the scene and included metadata
scene

.. jupyter-execute::

# Make an RGB composite as an xarray.DataArray
rgb = xls.composite(scene, rescale_to=[0.02, 0.2])

# 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()
# Plot the composite on an interactive Leaflet map
xls.plot_composite_leaflet(rgb, height="400px")


----
Expand Down
1 change: 0 additions & 1 deletion env/requirements-docs.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@ sphinx-book-theme==1.1.*
sphinx-copybutton==0.5.*
sphinx-design==0.5.*
jupyter-sphinx==0.5.*
matplotlib
ipykernel
4 changes: 3 additions & 1 deletion environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ dependencies:
- xarray
- scikit-image
- pooch
- pyproj
- ipyleaflet
- matplotlib
# Build
- build
- twine
Expand All @@ -25,7 +28,6 @@ dependencies:
- sphinx-copybutton==0.5.*
- sphinx-design==0.5.*
- jupyter-sphinx==0.5.*
- matplotlib
- ipykernel
# Style
- black
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ dependencies = [
"xarray>=2022.6.0",
"scikit-image>=0.20",
"pooch>=1.3.0",
"pyproj>=3.3.0",
"ipyleaflet>=0.18",
"matplotlib>=3.5",
]

[project.urls]
Expand Down
1 change: 1 addition & 0 deletions xlandsat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
from ._indices import nbr, ndvi
from ._interpolation import interpolate_missing
from ._io import load_panchromatic, load_scene, save_scene
from ._leaflet import plot_composite_leaflet
from ._pansharpen import pansharpen
from ._version import __version__
116 changes: 116 additions & 0 deletions xlandsat/_leaflet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Copyright (c) 2022 The xlandsat developers.
# Distributed under the terms of the MIT License.
# SPDX-License-Identifier: MIT
"""
Functions for plotting data with ipyleaflet
"""
import base64
import io

import ipyleaflet

# Dependecy of ipyleaflet
import ipywidgets
import matplotlib.pyplot as plt
import pyproj


def _scene_boundaries_geodetic(scene):
"""
Determine the boundaries of the scene in geodetic longitude and latitude

Uses pyproj to project the UTM coordinates of the scene and get the
geographic bounding box.

Returns
-------
[w, e, s, n] : list of floats
The west, east, south, north boundaries of the scene in degrees.
"""
projection = pyproj.Proj(

Check warning on line 30 in xlandsat/_leaflet.py

View check run for this annotation

Codecov / codecov/patch

xlandsat/_leaflet.py#L30

Added line #L30 was not covered by tests
proj="utm", zone=scene.attrs["utm_zone"], ellps=scene.attrs["ellipsoid"]
)
west, south = projection(

Check warning on line 33 in xlandsat/_leaflet.py

View check run for this annotation

Codecov / codecov/patch

xlandsat/_leaflet.py#L33

Added line #L33 was not covered by tests
scene.easting.min().values, scene.northing.min().values, inverse=True
)
east, north = projection(

Check warning on line 36 in xlandsat/_leaflet.py

View check run for this annotation

Codecov / codecov/patch

xlandsat/_leaflet.py#L36

Added line #L36 was not covered by tests
scene.easting.max().values, scene.northing.max().values, inverse=True
)
return (west, east, south, north)

Check warning on line 39 in xlandsat/_leaflet.py

View check run for this annotation

Codecov / codecov/patch

xlandsat/_leaflet.py#L39

Added line #L39 was not covered by tests


def plot_composite_leaflet(composite, dpi=70, leaflet_map=None, height="600px"):
"""
Display a composite as an image overlay in an interactive HTML map

Adds the composite to a Leaflet.js map, which can be displayed in a Jupyter
notebook or HTML page. By default, adds a control widget for the opacity of
the image overlay.

Parameters
----------
composite : :class:`xarray.DataArray`
A composite, as generated by :func:`xlandsat.composite`.
dpi : int
The dots-per-inch resolution of the image.
leaflet_map : :class:`ipyleaflet.Map`
A Leaflet map instance to which the image overlay will be added. If
None (default), a new map will be created. Pass an existing map to add
the overlay to it.
height : str
The height of the map which is embedded in the HTML. Should contain the
proper CSS units (px, em, rem, etc).

Returns
-------
leaflet_map : :class:`ipyleaflet.Map`
The map with the image overlay and opacity controls added to it.
"""
west, east, south, north = _scene_boundaries_geodetic(composite)
center = (0.5 * (north + south), 0.5 * (east + west))
bounds = ((south, west), (north, east))

Check warning on line 71 in xlandsat/_leaflet.py

View check run for this annotation

Codecov / codecov/patch

xlandsat/_leaflet.py#L69-L71

Added lines #L69 - L71 were not covered by tests
# Create a plot of the composite with no decoration
fig, ax = plt.subplots(1, 1, layout="constrained")
composite.plot.imshow(ax=ax, add_labels=False)
ax.axis("off")
ax.set_aspect("equal")

Check warning on line 76 in xlandsat/_leaflet.py

View check run for this annotation

Codecov / codecov/patch

xlandsat/_leaflet.py#L73-L76

Added lines #L73 - L76 were not covered by tests
# Save the PNG to an in-memory buffer
png = io.BytesIO()
fig.savefig(png, bbox_inches="tight", dpi=dpi, pad_inches=0, transparent=True)
plt.close(fig)

Check warning on line 80 in xlandsat/_leaflet.py

View check run for this annotation

Codecov / codecov/patch

xlandsat/_leaflet.py#L78-L80

Added lines #L78 - L80 were not covered by tests
# Create the image overlay with the figure as base64 encoded png
image_overlay = ipyleaflet.ImageOverlay(

Check warning on line 82 in xlandsat/_leaflet.py

View check run for this annotation

Codecov / codecov/patch

xlandsat/_leaflet.py#L82

Added line #L82 was not covered by tests
url=f"data:image/png;base64,{base64.b64encode(png.getvalue()).decode()}",
bounds=bounds,
)
image_overlay.name = (

Check warning on line 86 in xlandsat/_leaflet.py

View check run for this annotation

Codecov / codecov/patch

xlandsat/_leaflet.py#L86

Added line #L86 was not covered by tests
f"{composite.attrs['long_name'].title()} | {composite.attrs['title']}"
)
# Create a map if one wasn't given
if leaflet_map is None:
leaflet_map = ipyleaflet.Map(

Check warning on line 91 in xlandsat/_leaflet.py

View check run for this annotation

Codecov / codecov/patch

xlandsat/_leaflet.py#L91

Added line #L91 was not covered by tests
center=center,
scroll_wheel_zoom=True,
layout={"height": height},
)
leaflet_map.add(ipyleaflet.ScaleControl(position="bottomleft"))
leaflet_map.add(ipyleaflet.LayersControl(position="bottomright"))
leaflet_map.add(ipyleaflet.FullScreenControl())
leaflet_map.fit_bounds(bounds)

Check warning on line 99 in xlandsat/_leaflet.py

View check run for this annotation

Codecov / codecov/patch

xlandsat/_leaflet.py#L96-L99

Added lines #L96 - L99 were not covered by tests
# Add a widget to control the opacity of the image
opacity_slider = ipywidgets.FloatSlider(

Check warning on line 101 in xlandsat/_leaflet.py

View check run for this annotation

Codecov / codecov/patch

xlandsat/_leaflet.py#L101

Added line #L101 was not covered by tests
description="Opacity:",
min=0,
max=1,
step=0.1,
value=1,
readout_format=".1f",
style={"description_width": "initial"},
layout={"margin": "0 0 0 0.5rem"},
)
ipywidgets.jslink((opacity_slider, "value"), (image_overlay, "opacity"))
leaflet_map.add(

Check warning on line 112 in xlandsat/_leaflet.py

View check run for this annotation

Codecov / codecov/patch

xlandsat/_leaflet.py#L111-L112

Added lines #L111 - L112 were not covered by tests
ipyleaflet.WidgetControl(widget=opacity_slider, position="topright")
)
leaflet_map.add(image_overlay)
return leaflet_map

Check warning on line 116 in xlandsat/_leaflet.py

View check run for this annotation

Codecov / codecov/patch

xlandsat/_leaflet.py#L115-L116

Added lines #L115 - L116 were not covered by tests
Loading