4.1.2 Nettoyage des cartes brutes

Les series temporelles des cartes forêt/non-forêt brutes sont lissées et filtrées pixel par pixel pour les raisons suivantes:

  • Remplissage des données manquantes (nuages, ombres, L7 SLC-off),
  • Lissage des bruits (pixels qui changent entre forêt et non-forêt plusieurs fois)
  • Filtrer la régénération temporaire par les fourrés, observée sur les jachères.
  1. La première étappe, est la remplissage des données manquantes: les données manquantes entre deux observations de forêt deviennent forêt, celles entre deux observations de non-forêt deviennent non-forêt.
  1. Ensuite, une fenêtre coulissante de taille 5 est appliquée, c’est-à-dire qu’une observation est attribuée à la classe qui est observé le plus fréquemment dans la fenêtre qui inclu les deux observations précédentes et les deux suivantes. Pour l’évaluation de la deuxième et de l’avant-dernière observation, une fenêtre coulissante de taille 3 est appliquée. La première et la dernière observation ne sont pas ajustées, il faut les ignorer dans la suite. Les observations manquantes sont remplis dans la mesure possible. Toute la procédure est répétée jusqu’à ce qu’il n’y a plus de changements.
  1. Les années manquantes sont ajoutées pour créer une série annuelle. Les observations manquantes sont remplacées par la classe de l’observation précédente. Lorsque les observations initiales sont manquantes, elles sont remplacées par la classe de l’observation suivante. Dans les situations de régénération (forêt suit non-forêt), les 9 premières années sont marquées comme “reboisement potentiel”. Si la forêt disparaît à nouveau après moins de 10 ans, les observations sont remplacées par la classe non-forêt. Si la régénération reste, elle est considérée comme un reboisement après 10 ans.

Après la nettoyage des séries temporelles pixel par pixel, une nettoyage spatiale est réalisé pour éliminer les surfaces forestières < 0,5 hectares. Pour cela, une carte est créée de tous les pixels qui étaient une fois observées comme forêt sur toute la série des cartes. Sur cette carte, toutes les surfaces forestières ayant moins de 6 pixels liés (soit moins de 0,54 hectares) sont éliminées. Ce masque forestier est ensuite appliqué à toutes les cartes forêt/non-forêt de la série. Cela signifie que seuls les pixels forestier qui ont fait partie d’une surface forestière ≥ 0,54 hectares dans la série des cartes sont retenus comme forêt.

Example

La figure suivante montre les cartes forêt/non-forêt après la nettoyage temporelle et spatiale pour une région au sud de Kpalimé (voir série des cartes brutes pour comparaison).

Script R: 03_NRF-MRV/01_MCF/_src/02_clean-fc-maps.R

###############################################################################
# 02_clean-fc-maps.R: nettoyage des cartes brutes du couvert forestier
# -----------------------------------------------------------------------------
# Bern University of Applied Sciences
# Oliver Gardi, <oliver.gardi@bfh.ch>
# 13 Mai 2020


# Définitions des variables ===================================================

COV.FC         <- 30

RAW.DIR <- DIR.MRV.MCF.RAW
CLN.DIR <- DIR.MRV.MCF.CLN

# Définitions des fonctions ===================================================

# Nettoyage temporel d'une série d'images -------------------------------------
#
# @param path  Chemin WRS
#
# @return       -- 
#

clean.temporal <- function(path) {

  # Préparation des images --------------------------------
  
  # Charger les cartes brutes
  maps <- stack(dir(paste0(RAW.DIR, "/FC", COV.FC, "/", path), 
                    pattern=".*[[:digit:]]{4}\\_F.*\\.tif$", full.names=TRUE))
  map.names <- sub("r$", "", names(maps))
  # Noms de cartes à utiliser dans les colonnes de la matrice
  map.cols  <- sub(paste0(path, "\\_"), "X", sub("\\_[[:alnum:]]+$", "M", map.names))
  # Names à utiliser pour les années sans carte
  no.map.cols <- paste0("X", YEARS.ALL[!YEARS.ALL %in% gsub("[[:alpha:]]", "", map.cols)], "_")
  # Joindre et ordonner les noms de colonnes
  col.order <- c(map.cols, no.map.cols)[order(c(map.cols, no.map.cols))]
  # Convertir les cartes en matrice (une carte par colonne, prend du temps)
  maps.values <- values(maps)                           
  colnames(maps.values) <- map.cols
  
  # Nettoyage parallèle des trajectoires des pixels -------
  
  # Définir des sous-ensembles des pixels (lignes) pour le traitement parallèle
  nsubsets <- CORES               
  subsets  <- c(0, floor((1:nsubsets)*(nrow(maps.values)/nsubsets)))    
  # Traitement en parallèle
  registerDoParallel(CORES)
  maps.values.clean <- foreach(i=1:nsubsets, .combine=rbind) %dopar% {
    
    # 0. obtenir un sous-ensemble à partir de maps.values
    val <- maps.values[(subsets[i]+1):subsets[i+1], ]     
    # mettre tout en NA qui n'est pas de la forêt ou non-forêt
    val[!(is.na(val) | val %in% c(FOREST,NONFOR))] <- NA    
    
    # 1. Supprimer les NA isolées ----
    str   <- apply(val, 1, paste, collapse="")  # convertir en chaînes de caractères
    str.c <- gsub("NA", "9", str)               # remplacer NA avec 9
    # Nettoyer jusqu'à la convergence
    while(!identical(str, str.c)) {             
      str <- str.c
      # Mettre NA dans la classe correspondante (forêt ou non-forêt)
      str.c <- gsub(paste0("^(.*", NONFOR, ")9(9*", NONFOR, ".*)$"), paste0("\\1", NONFOR, "\\2"), str.c) 
      str.c <- gsub(paste0("^(.*", FOREST, ")9(9*", FOREST, ".*)$"), paste0("\\1", FOREST, "\\2"), str.c)                 
    }                                                       
    # Reconvertir en matrice numérique
    val <- matrix(as.numeric(unlist(strsplit(str.c, ""))), ncol=ncol(val), byrow=TRUE)   
    val[val==9] <- NA                           # remplacer les 9 avec NA
    colnames(val) <- map.cols
    
    # 2. nettoyer les trajectoires avec fenêtre coulissante ----
    val.c   <- val
    val.o <- val[] <- 0
    iter    <- 0   
    # Nettoyer jusqu'à la convergence
    while(!identical(val, val.c) & !identical(val.o, val.c)) {   
      iter <- iter+1
      message("    -Clean modal: iteration ", iter, " ... ", appendLF = FALSE)
      val.o <- val  # valeurs actuelles -> anciennes valeurs
      val <- val.c  # valeurs nettoyées -> valuers actuelles
      # 3ème - 3ème l'année dernière : fenêtre modale taille 5 
      for(l in 3:(ncol(val)-2)) {                             
        val.c[,l] <- apply(val[,(l-2):(l+2)], 1, modal, na.rm=TRUE, ties='NA')
      }
      # 2e et 2e l'année dernière : fenêtre modale taille 3 
      for(l in c(2, ncol(val)-1)) {                           
        val.c[,l] <- apply(val.c[,(l-1):(l+1)], 1, modal, na.rm=TRUE, ties='NA')
      }
      message("done")
    }
    val <- val.c    # valeurs nettoyées -> valuers actuelles
    
    # 3. nettoyage final regexp ----
    # ajouter des années sans observation (cartes)
    val   <- cbind(val, matrix(nrow=nrow(val),                            
                               ncol=length(no.map.cols), dimnames=list(NULL, no.map.cols)))
    # ordonner correctement 
    val   <- val[, col.order]                                        
    str   <- apply(val, 1, paste, collapse="")           # convertir en chaînes de caractères
    str.c <- gsub("NA", "9", str)                        # remplacer NA avec 9
    # Nettoyer jusqu'à la convergence
    while(!identical(str, str.c)) {                     
      str <- str.c
      # 3.1 remplacer NA par la classe précédente
      str.c <- gsub(paste0(NONFOR, "9"), paste0(NONFOR, NONFOR), str.c)                   
      str.c <- gsub(paste0(FOREST, "9"), paste0(FOREST, FOREST), str.c)                
    }
    str <- ""
    # Nettoyer jusqu'à la convergence
    while(!identical(str, str.c)) {                         
      str <- str.c
      # 3.2 remplacer NA par la classe suivante
      str.c <- gsub(paste0("9", NONFOR), paste0(NONFOR, NONFOR), str.c)                   
      str.c <- gsub(paste0("9", FOREST), paste0(FOREST, FOREST), str.c)                
    }
    str <- ""
    # Nettoyer jusqu'à la convergence
    while(!identical(str, str.c)) {
      str <- str.c
      # 3.3 suppression des 1 isolés jusqu'à une longueur de 10 (régénération qui est à nouveau perdue)
      str.c <-gsub(paste0("^(.*", NONFOR, ")", FOREST, "(", FOREST, "{0,9}", NONFOR, ".*)$"), 
                   paste0("\\1", NONFOR, "\\2"), str.c) 
    }
    str <- ""
    # Nettoyer jusqu'à la convergence
    while(!identical(str, str.c)) {
      str <- str.c
      # 3.4 considérer la régénération seulement comme une forêt à partir de 10 ans 
      # avant qu'elle ne soit considérée comme une régénération potentielle PREGEN
      str.c <-gsub(paste0("^(.*",NONFOREST,PREGEN, "{0,8})", FOREST, "(.*)$"), 
                   paste0("\\1", PREGEN, "\\2"), str.c)
    }
    str <- ""
    # Reconvertir en matrice numérique
    val <- matrix(as.numeric(unlist(strsplit(str.c, ""))), ncol=ncol(val), byrow=TRUE) 
    val[val==9] <- NA                           # remplacer les 9 avec NA
    colnames(val) <- col.order
    val[,grepl("M$", colnames(val))]            # et extraire les données pour les années des cartes
  }
  
  # Sauvegarder le résultat -------------------------------
  values(maps) <- maps.values.clean
  # supprimer la première et la dernière carte "non nettoyée"
  writeRaster(dropLayer(maps, c(1,nlayers(maps))), 
              filename=paste0(CLN.DIR, "/FC", COV.FC, "/", path, "/", map.names[2:(nlayers(maps)-1)], "c.tif"), 
              bylayer=TRUE, format="GTiff", datatype="INT2U", overwrite=TRUE)
  
  # écrire des trajectoires dans un fichier texte
  maps.strings <- apply(maps.values.clean, 1, paste, collapse="")
  sink(paste0(CLN.DIR, "/FC", COV.FC, "/", path, "/Trajectories.txt"))
  print(table(maps.strings))
  sink()

}


# Nettoyage spatiale d'une série d'images -------------------------------------
#
# @description Supprimer les pixels de forêt/déforestion/régénération qui n'ont 
#              JAMAIS fait partie de "forêt > 0,5 ha" depuis 2003
#
# @param maps           Série des cartes
# @param exclude        Cartes à ignorer
# @param size           Nombre minimal de pixels connectés
# @param connectedness  Nombre de pixels qui comptent comme connectés
#
# @return       -- 
#

# 
clean.spatial.forest <- function(maps, exclude = NULL, size=6, connectedness=8){
  
  tmp1 <- tempfile(pattern = "", fileext = ".tif")
  tmp2 <- tempfile(pattern = "", fileext = ".tif")
  
  fcc.map <- apply(maps[[-exclude]][], 1, paste, collapse="")
  onceforest.map <- raster(maps)
  onceforest.map[] <- NA 
  
  # créer une carte "forêt une fois"
  onceforest.map[grepl(paste0("^", NONFOR, "*$"),    fcc.map)] <- NONFOR
  onceforest.map[grepl(paste0("^.*", FOREST, ".*$"), fcc.map)] <- FOREST
  onceforest.map[grepl(paste0("^.*", PREGEN, ".*$"), fcc.map)] <- FOREST
  writeRaster(onceforest.map, tmp1)
  
  # supprimer les parcelles de forêt isolées < xy ha
  system(paste0("gdal_sieve.py -st ", size, " -", connectedness, " -nomask ", tmp1, " ", tmp2))
  onceforest.map.clean <- raster(tmp2)
  onceforest.map.clean[onceforest.map.clean == -2147483648] <- NA
                                                        
  # masquer les cartes avec cette carte forestière nettoyée
  maps.clean <- mask(maps, onceforest.map.clean, maskvalue=NONFOR, updatevalue=NONFOR)
  writeRaster(maps.clean, filename=paste0(CLN.DIR, "/FC", COV.FC, "/TGO/", names(maps.clean), "f.tif"), 
              bylayer=TRUE, format="GTiff", datatype="INT2U", overwrite=TRUE)
  
}


# COMMENCER LE TRAITEMENT #####################################################

# changer l'étendue de p192_2019 à l'étendue des autres images p192 -----------
# TODO : à faire déjà dans 01_SSTS/01_data/_src/prep-Landsat.R
extend(brick(paste0(RAW.DIR, "/FC", COV.FC, "/p192/p192_2019_F", COV.FC, "r.tif")), raster(paste0(RAW.DIR, "/FC", COV.FC, "/p192/p192_2018_F", COV.FC, "r.tif")), 
       filename=paste0(RAW.DIR, "/FC", COV.FC, "/p192/p192_2019_F", COV.FC, "rt.tif"), format="GTiff", datatype="INT2U")
file.rename(paste0(RAW.DIR, "/FC", COV.FC, "/p192/p192_2019_F", COV.FC, "rt.tif"), paste0(RAW.DIR, "/FC", COV.FC, "/p192/p192_2019_F", COV.FC, "r.tif"))

# Nettoyage temporel des chemins ----------------------------------------------
for(path in c("p192", "p193", "p194")) {
  clean.temporal(path)
} 

# Fusionner les cartes p192, p193 et p194 pour les dates conjointes -----------
for(year in YEARS.JNT) {
  merge(mask(crop(brick(paste0(CLN.DIR, "/FC", COV.FC, "/p193/p193_", year, "_F", COV.FC, "c.tif")), TGO), TGO),
        mask(crop(brick(paste0(CLN.DIR, "/FC", COV.FC, "/p192/p192_", year, "_F", COV.FC, "c.tif")), TGO), TGO),
        mask(crop(brick(paste0(CLN.DIR, "/FC", COV.FC, "/p194/p194_", year, "_F", COV.FC, "c.tif")), TGO), TGO),
        filename=paste0(CLN.DIR, "/FC", COV.FC, "/TGO/TGO_", year, "_F", COV.FC, "c.tif"), overwrite=TRUE)
}

# Spatial cleaning of results (only from 2003 onwards) -----------------------------------
# exclure la première couche (1987) pour la création du masque forêt/non-forêt  
clean.spatial.forest(maps = stack(dir(paste0(CLN.DIR, "/FC", COV.FC, "/TGO"), pattern = "c\\.tif", full.names = TRUE)),
                     exclude = c(1), size = 6, connectedness = 8)