Land use Classification#

holoviewsbokehdaskxarraykerastensorflow
Published: November 30, 2018 · Modified: December 6, 2024


Satellite images often need to be classified (assigned to a fixed set of types) or to be used for detection of various features of interest. Here we will look at the classification case, using labelled satellite images from various categories from the UCMerced LandUse dataset. scikit-learn is useful for general numeric data types, but it doesn’t have significant support for working with images. Luckily, there are various deep-learning and convolutional-network libraries that do support images well, including Keras (backed by TensorFlow) as we will use here.

import intake
import numpy as np
import holoviews as hv
import pandas as pd
import random

np.random.seed(101)

from holoviews import opts
hv.extension('bokeh')

Get the classes and files#

All of the labeled image classification data is in a public bucket on s3 with a corresponding intake catalog. This catalog provides several ways to access the data. You can access one image by landuse and id, access all the images for a given landuse, or access all the images.

cat = intake.open_catalog('./catalog.yml')
list(cat)
['UCMerced_LandUse_all',
 'UCMerced_LandUse_by_landuse',
 'UCMerced_LandUse_by_image']

The first time you run the cell below it will download all the images which takes about 3 minutes on my machine. After that, the images are cached and it’ll take about 100 ms.

%%time
da = cat.UCMerced_LandUse_all().to_dask()
da
CPU times: user 16 s, sys: 2.75 s, total: 18.8 s
Wall time: 1min 35s
<xarray.DataArray 'imread-d08352d6a5af7c4f4d7c401871bbdc62' (id: 100,
                                                             landuse: 21,
                                                             y: 256, x: 256,
                                                             channel: 3)>
dask.array<transpose, shape=(100, 21, 256, 256, 3), dtype=uint8, chunksize=(1, 21, 256, 256, 3), chunktype=numpy.ndarray>
Coordinates:
  * id       (id) int64 0 1 2 3 4 5 6 7 8 9 10 ... 90 91 92 93 94 95 96 97 98 99
  * landuse  (landuse) object 'agricultural' 'airplane' ... 'tenniscourt'
  * y        (y) int64 0 1 2 3 4 5 6 7 8 ... 247 248 249 250 251 252 253 254 255
  * x        (x) int64 0 1 2 3 4 5 6 7 8 ... 247 248 249 250 251 252 253 254 255
  * channel  (channel) int64 0 1 2

We can see what’s going on more easily if we convert the data to a dataset with each data variable representing a different landuse.

ds = da.to_dataset(dim='landuse')
ds
<xarray.Dataset>
Dimensions:            (id: 100, y: 256, x: 256, channel: 3)
Coordinates:
  * id                 (id) int64 0 1 2 3 4 5 6 7 8 ... 92 93 94 95 96 97 98 99
  * y                  (y) int64 0 1 2 3 4 5 6 7 ... 249 250 251 252 253 254 255
  * x                  (x) int64 0 1 2 3 4 5 6 7 ... 249 250 251 252 253 254 255
  * channel            (channel) int64 0 1 2
Data variables: (12/21)
    agricultural       (id, y, x, channel) uint8 dask.array<chunksize=(1, 256, 256, 3), meta=np.ndarray>
    airplane           (id, y, x, channel) uint8 dask.array<chunksize=(1, 256, 256, 3), meta=np.ndarray>
    baseballdiamond    (id, y, x, channel) uint8 dask.array<chunksize=(1, 256, 256, 3), meta=np.ndarray>
    beach              (id, y, x, channel) uint8 dask.array<chunksize=(1, 256, 256, 3), meta=np.ndarray>
    buildings          (id, y, x, channel) uint8 dask.array<chunksize=(1, 256, 256, 3), meta=np.ndarray>
    chaparral          (id, y, x, channel) uint8 dask.array<chunksize=(1, 256, 256, 3), meta=np.ndarray>
    ...                 ...
    parkinglot         (id, y, x, channel) uint8 dask.array<chunksize=(1, 256, 256, 3), meta=np.ndarray>
    river              (id, y, x, channel) uint8 dask.array<chunksize=(1, 256, 256, 3), meta=np.ndarray>
    runway             (id, y, x, channel) uint8 dask.array<chunksize=(1, 256, 256, 3), meta=np.ndarray>
    sparseresidential  (id, y, x, channel) uint8 dask.array<chunksize=(1, 256, 256, 3), meta=np.ndarray>
    storagetanks       (id, y, x, channel) uint8 dask.array<chunksize=(1, 256, 256, 3), meta=np.ndarray>
    tenniscourt        (id, y, x, channel) uint8 dask.array<chunksize=(1, 256, 256, 3), meta=np.ndarray>

Split files into train and test sets#

In order to accurately test the performance of the classifier we are building we will split the data into training and test sets with an 80/20 split. We randomly sample the images in each category and assign them either to the training or test set:

train_set = np.random.choice(ds.id, int(0.8 * len(ds.id)), False)
test_set = np.setdiff1d(ds.id, train_set)

Define function to sample from train or test set#

landuses = da.landuse.data
landuse_list = list(landuses)

Next we define a function that randomly samples an image, either from the training or test set and ant

def get_sample(landuse=None, set='training'):
    landuse = landuse or np.random.choice(landuses)
    i = random.choice(train_set if set == 'training' else train_set)
    return ds[landuse].sel(id=i)

def plot(data):
    options = opts.RGB(xaxis=None, yaxis=None)
    title = '{}: {}'.format(data.name, data.id.item())
    plot = hv.RGB(data.data.compute())
    return plot.options(options).relabel(title)

We can inspect the data on one of these samples to see that the data is loaded as an xarray.DataArray.

data = get_sample()
data
<xarray.DataArray 'sparseresidential' (y: 256, x: 256, channel: 3)>
dask.array<getitem, shape=(256, 256, 3), dtype=uint8, chunksize=(256, 256, 3), chunktype=numpy.ndarray>
Coordinates:
    id       int64 59
  * y        (y) int64 0 1 2 3 4 5 6 7 8 ... 247 248 249 250 251 252 253 254 255
  * x        (x) int64 0 1 2 3 4 5 6 7 8 ... 247 248 249 250 251 252 253 254 255
  * channel  (channel) int64 0 1 2

We can plot this array as a holoviews RGB image so we can visualize it:

plot(data)
hv.Layout(list(map(plot, map(get_sample, np.random.choice(landuses, 4))))).cols(2)