This is a primer for manipulating a Canada-wide land cover raster of the Northwest Territories. This raster is not as detailed as the EOSD, but it provides an opportuntity to practice manipulating rasters. In particular, we DEFINE and COLLAPSE categorical layers.
The land cover raster we are using is from the North American Land Cover Management System (NALCMS), a joint project between Canadian, US, and Mexican organizations. The project is a multi-scale land cover monitoring framework which can be applied across North America. This raster is of a small section of the Northwest Territories, Canada, on the western edge of Great Slave Lake.
Load the raster with the raster
command:
require(raster)
landcover <- raster("_data/LandCover/LandCovCanada_c2010_NALCMSlegend_HRL_clip.tif")
landcover
## class : RasterLayer
## dimensions : 10290, 10687, 109969230 (nrow, ncol, ncell)
## resolution : 30, 30 (x, y)
## extent : -1358210, -1037600, 7962570, 8271270 (xmin, xmax, ymin, ymax)
## crs : +proj=lcc +lat_1=49 +lat_2=77 +lat_0=0 +lon_0=-95 +x_0=0 +y_0=0 +ellps=GRS80 +units=m +no_defs
## source : C:/Users/Elie/Box Sync/caribou/NWT/rsf/primers/_data/LandCover/LandCovCanada_c2010_NALCMSlegend_HRL_clip.tif
## names : LandCovCanada_c2010_NALCMSlegend_HRL_clip
## values : 1, 18 (min, max)
## attributes :
## ID OBJECTID Value Count
## from: 0 1 1 24924814
## to : 11 12 18 4645750
It plots nicely:
plot(landcover)
But there are no classifications: the raster has an attribute table, but the names of the land cover types are not specified. We need to match up the “Values” to their respective land cover types.
levels(landcover)
## [[1]]
## ID OBJECTID Value Count
## 1 0 1 1 24924814
## 2 1 2 2 1973279
## 3 2 3 5 1192339
## 4 3 4 6 2274680
## 5 4 5 8 4593568
## 6 5 6 10 55546
## 7 6 7 11 74603
## 8 7 8 12 692538
## 9 8 9 14 4990755
## 10 9 10 16 7184
## 11 10 11 17 31523
## 12 11 12 18 4645750
These same land cover categories are used throughout the NALCMS dataset, so we can pull the legend from their website: https://landcover.usgs.gov/nalcms.php Unfortunately the legend is in .jpg
format, but it’s easy enough to copy into an excel file, which we did manually.
Value | Description | R | G | B |
---|---|---|---|---|
1 | Temperate or sub-polar needleleaf forest | 0 | 61 | 1 |
2 | Sub-polar taiga needleleaf forest | 147 | 156 | 113 |
3 | Tropical or sub-tropical broadleaf evergreen forest | 1 | 99 | 0 |
4 | Tropical or sub-tropical broadleaf deciduous forest | 29 | 170 | 4 |
5 | Temperate or sub-polar broadleaf deciduous forest | 19 | 140 | 61 |
6 | Mixed Forest | 91 | 117 | 43 |
7 | Tropical or sub-tropical Shrubland | 178 | 158 | 43 |
8 | Temperate or sub-polar Shrubland | 177 | 137 | 50 |
9 | Tropical or sub-tropical Grassland | 233 | 219 | 94 |
10 | Temperate or sub-polar Grassland | 225 | 206 | 137 |
11 | Sub-polar or polar shrubland-lichen-moss | 156 | 117 | 84 |
12 | Sub-polar or polar grassland-lichen-moss | 186 | 211 | 143 |
13 | Sub-polar or polar barren-lichen-moss | 63 | 137 | 112 |
14 | Wetland | 107 | 163 | 136 |
15 | Cropland | 230 | 173 | 102 |
16 | Barren land | 169 | 170 | 174 |
17 | Urban | 219 | 33 | 38 |
18 | Water | 76 | 112 | 164 |
19 | Snow and Ice | 255 | 248 | 255 |
Here is a useful website that can be used to find the specific RGB, HEX or CYMK code of any color in an image. I used it to pull out the RGB code for all the colors in the legend .jpg
file: https://www.ginifab.com/feeds/pms/pms_color_in_image.php
In order to attach the legend to our raster, we need to merge the levels of the landcover
raster with the legend
dataframe. Both files have a column named “Value” which we will base the merge on:
levels(landcover)[[1]] <- merge(levels(landcover)[[1]], legend, by = "Value")[,c(2,1,3:5)]
Now we add the colors. The correct colortable is already present in the landcover
raster. Here we take the colors in the colortable and add them as a new column in the raster attribute table.(See examples from primer 1)
Note: the raster’s colortable is offset by 1 in relation to our imported legend (the color of the first landcover type is actually the second entry in the list). To keep everything lined up correctly we use
Value + 1
instead of just the value.
levels(landcover)[[1]]$Colors <- landcover@legend@colortable[levels(landcover)[[1]]$Value + 1]
And there you have it:
plot(landcover)
legend("topright", fill = levels(landcover)[[1]]$Color, legend = levels(landcover)[[1]]$Description, ncol = 1, bg = "white", cex = 0.6)
We have a lot of different land cover types here. To simplify things we can combine similar types into one large land cover category. For example, let’s pool both Temperate or sub-polar needleleaf forest and Sub-polar taiga needleleaf forest (values 17 and 18) into a single class called “1: Needleleaf Forest”.
Steps are simple:
landcover.pooled <- landcover
values(landcover.pooled)[values(landcover.pooled) == 2] <- 1
mylevels <- levels(landcover.pooled)[[1]]
mylevels$Description <- as.character(mylevels$Description)
mylevels[mylevels$Value == 1,]$Description <- "Needleleaf Forest"
mylevels <- subset(mylevels, Value != 2)
levels(landcover.pooled)[[1]] <- mylevels
To make this more general, e.g. to pool values from a vector sourcevalues
to a new value targetvalue
and new description newdescription
, we write a function:
poolValues <- function(raster, sourcevalues, targetvalue, newdescription){
raster.pooled <- raster
values(raster.pooled)[values(raster.pooled) %in% sourcevalues] <- targetvalue
mylevels <- levels(raster.pooled)[[1]]
mylevels$Description = as.character(mylevels$Description)
mylevels[mylevels$Value == targetvalue,]$Description <- newdescription
mylevels <- subset(mylevels, !(Value %in% sourcevalues) | Value == targetvalue )
levels(raster.pooled)[[1]] <- suppressWarnings(mylevels)
return(raster.pooled)
}
Note that this function works on any raster, as long as there is a Description
column in the levels.
Let’s test this by pooling all of the Forests (classes 1, 2, 5 and 6) into a single monolithic FOREST class:
landcover.forestpooled <- poolValues(landcover, c(1,2,5,6), 1, "Forest")
plot(landcover.forestpooled)
legend("topright", fill = levels(landcover.forestpooled)[[1]]$Color,
legend = levels(landcover.forestpooled)[[1]]$Description,
ncol = 1, bg = "white", cex = 0.6)
A lot of the forest nuance is now gone! For caribou - probably too much.
ID | Value | OBJECTID | Count | Description | Colors | |
---|---|---|---|---|---|---|
1 | 0 | 1 | 1 | 24924814 | Forest | #003D00 |
5 | 4 | 8 | 5 | 4593568 | Temperate or sub-polar Shrubland | #B38A33 |
6 | 5 | 10 | 6 | 55546 | Temperate or sub-polar Grassland | #E1CF8A |
7 | 6 | 11 | 7 | 74603 | Sub-polar or polar shrubland-lichen-moss | #9C7554 |
8 | 7 | 12 | 8 | 692538 | Sub-polar or polar grassland-lichen-moss | #BAD48F |
9 | 8 | 14 | 9 | 4990755 | Wetland | #6BA38A |
10 | 9 | 16 | 10 | 7184 | Barren land | #A8ABAE |
11 | 10 | 17 | 11 | 31523 | Urban | #DC2126 |
12 | 11 | 18 | 12 | 4645750 | Water | #4C70A3 |
Note: Although there is a column in the dataframe named “Count” this doesn’t actually COUNT anything! Now that multiple rows have been pooled together the count listed in the dataframe isn’t necesarily correct.If you want to know the count (the number of pixels in each land cover category) that would have to be done with the
table
function (which - for a large raster - can be pretty slow). Below, the count is correct:
levels(landcover.forestpooled)[[1]]$Count <- table(getValues(landcover.forestpooled))
ID | Value | OBJECTID | Count | Description | Colors | |
---|---|---|---|---|---|---|
1 | 0 | 1 | 1 | 30365112 | Forest | #003D00 |
5 | 4 | 8 | 5 | 4593568 | Temperate or sub-polar Shrubland | #B38A33 |
6 | 5 | 10 | 6 | 55546 | Temperate or sub-polar Grassland | #E1CF8A |
7 | 6 | 11 | 7 | 74603 | Sub-polar or polar shrubland-lichen-moss | #9C7554 |
8 | 7 | 12 | 8 | 692538 | Sub-polar or polar grassland-lichen-moss | #BAD48F |
9 | 8 | 14 | 9 | 4990755 | Wetland | #6BA38A |
10 | 9 | 16 | 10 | 7184 | Barren land | #A8ABAE |
11 | 10 | 17 | 11 | 31523 | Urban | #DC2126 |
12 | 11 | 18 | 12 | 4645750 | Water | #4C70A3 |
Click below to show complete code from this document.
knitr::opts_chunk$set(echo = TRUE, cache=TRUE, message = FALSE, warning = FALSE)
require(raster)
landcover <- raster("_data/LandCover/LandCovCanada_c2010_NALCMSlegend_HRL_clip.tif")
landcover
plot(landcover)
levels(landcover)
require(knitr)
legend <- read.csv("_data/LandCover/NALCMS_Legend.csv")
names(legend)[1] <- "Value"
kable(legend)
levels(landcover)[[1]] <- merge(levels(landcover)[[1]], legend, by = "Value")[,c(2,1,3:5)]
levels(landcover)[[1]]$Colors <- landcover@legend@colortable[levels(landcover)[[1]]$Value + 1]
plot(landcover)
legend("topright", fill = levels(landcover)[[1]]$Color, legend = levels(landcover)[[1]]$Description, ncol = 1, bg = "white", cex = 0.6)
landcover.pooled <- landcover
values(landcover.pooled)[values(landcover.pooled) == 2] <- 1
mylevels <- levels(landcover.pooled)[[1]]
mylevels$Description <- as.character(mylevels$Description)
mylevels[mylevels$Value == 1,]$Description <- "Needleleaf Forest"
mylevels <- subset(mylevels, Value != 2)
levels(landcover.pooled)[[1]] <- mylevels
poolValues <- function(raster, sourcevalues, targetvalue, newdescription){
raster.pooled <- raster
values(raster.pooled)[values(raster.pooled) %in% sourcevalues] <- targetvalue
mylevels <- levels(raster.pooled)[[1]]
mylevels$Description = as.character(mylevels$Description)
mylevels[mylevels$Value == targetvalue,]$Description <- newdescription
mylevels <- subset(mylevels, !(Value %in% sourcevalues) | Value == targetvalue )
levels(raster.pooled)[[1]] <- suppressWarnings(mylevels)
return(raster.pooled)
}
landcover.forestpooled <- poolValues(landcover, c(1,2,5,6), 1, "Forest")
plot(landcover.forestpooled)
legend("topright", fill = levels(landcover.forestpooled)[[1]]$Color,
legend = levels(landcover.forestpooled)[[1]]$Description,
ncol = 1, bg = "white", cex = 0.6)
require(knitr); require(magrittr)
levels(landcover.forestpooled)[[1]] %>% kable
levels(landcover.forestpooled)[[1]]$Count <- table(getValues(landcover.forestpooled))
levels(landcover.forestpooled)[[1]] %>% kable