21  Image Quality Control // Contrôle de la qualité de l’image

Author

Natalie Williams

Published

October 14, 2025

21.1 Overview // Aperçu

This notebook provides a workflow for performing standardized quality control on image files. Images are complex digital objects composed of technical properties and embedded metadata (EXIF/IPTC).

//

Ce cahier de travail présente une procédure permettant d’effectuer un contrôle qualité standardisé sur des fichiers image. Les images sont des objets numériques complexes composés de propriétés techniques et de métadonnées intégrées (EXIF/IPTC).

NoteCuration Goal // Objectif de la curation

Perform standardized quality control and metadata harvesting. Our objective is to validate technical properties (dimensions, colorspace), calculate fixity checksums, and extract deep archival metadata to ensure long-term accessibility and provenance.

//

Effectuer un contrôle qualité standardisé et la collecte de métadonnées. Notre objectif est de valider les propriétés techniques (dimensions, espace colorimétrique), de calculer les sommes de contrôle de l’intégrité et d’extraire des métadonnées d’archivage approfondies afin de garantir l’accessibilité à long terme et la traçabilité des documents.

WarningIdentifying Risks // Identification des risques

Loss of embedded metadata during format conversion, “bit-rot” corruption, and accidental privacy leaks through GPS coordinates (EXIF) are the primary risks to the integrity and ethical sharing of digital image collections.

//

La perte de métadonnées intégrées lors de la conversion de format, la détérioration due à la « dégradation des bits » et les fuites accidentelles de données personnelles via les coordonnées GPS (EXIF) constituent les principaux risques pour l’intégrité et le partage éthique des collections d’images numériques.

This notebook integrates three specialized R packages:

  • digest: Calculating MD5 checksums for fixity and duplicate detection (Eddelbuettel 2024).
  • magick: Validating file headers and technical properties (Ooms 2025).
  • exiftoolr: Harvesting deep metadata (Camera, GPS, Software) (O’Brien 2025).

//

Ce cahier intègre trois paquets R spécialisés :

  • digest : calcul des sommes de contrôle MD5 pour la vérification de l’intégrité et la détection des doublons (Eddelbuettel 2024).
  • magick : validation des en-têtes de fichiers et des propriétés techniques (Ooms 2025).
  • exiftoolr : Extraction de métadonnées approfondies (appareil photo, GPS, logiciel) (O’Brien 2025).

21.2 Setup // Configuration

Before running this notebook, ensure the required R packages and the ExifTool command-line software are installed.

//

Avant d’exécuter ce notebook, assurez-vous que les packages R requis et le logiciel en ligne de commande ExifTool sont installés.

21.2.1 R Packages // Packages R

The following R packages are required. If you don’t have these packages, uncomment this code and run it once in your R console:

//

Les packages R suivants sont requis. Si vous ne disposez pas de ces packages, décommentez ce code et exécutez-le une fois dans votre console R :

Code
# install.packages(c("tidyverse", "digest", "magick", "exiftoolr", "rstudioapi"))

21.2.2 ExifTool software // Logiciel ExifTool

The exiftoolr package is a wrapper around a command-line tool called ExifTool. You must install this tool for the detailed metadata extraction to work. Run the following command once in your console to install the software.

//

Le package exiftoolr est une interface pour un outil en ligne de commande appelé ExifTool. Vous devez installer cet outil pour que l’extraction détaillée des métadonnées fonctionne. Exécutez la commande suivante une seule fois dans votre console pour installer le logiciel.

Code
# exiftoolr::install_exiftool()

21.3 Load libraries // Charger les bibliothèques

Code
library(tidyverse)
library(digest)
library(magick)
library(exiftoolr)
library(rstudioapi)

21.4 Select a target directory // Sélectionnez un répertoire de destination

This block allows for interactive selection of the image directory. If running in a non-interactive environment, it defaults to the path defined in the YAML header.

//

Ce bloc permet de sélectionner de manière interactive le répertoire contenant les images. En cas d’exécution dans un environnement non interactif, le chemin par défaut est celui défini dans l’en-tête YAML.

Code
# 1. Try to select interactively if in RStudio
if (interactive() && .Platform$OS.type == "windows") { 
  # .Platform check helps avoid errors in some non-interactive rendering contexts
  selected_dir <- rstudioapi::selectDirectory(caption = "Select Image Directory")
} else {
  selected_dir <- NULL
}

# 2. Logic to determine final directory (Interactive vs Parameter)
if (!is.null(selected_dir)) {
  target_dir <- selected_dir
} else {
  target_dir <- params$target_dir
}

print(paste("Analyzing directory:", target_dir))
[1] "Analyzing directory: data/Inspect_Images/"

21.4.1 Find Image Files // Rechercher des fichiers image

Here, we locate any image files and calculate an MD5 Checksum for each. This alphanumeric string acts as a unique fingerprint. If a single bit of the file changes in the future (due to disk failure or accidental modification) (Rosenthal 2010), this checksum will change. We will be using the checksums to identify duplicate images.

//

Ici, nous localisons tous les fichiers image et calculons une somme de contrôle MD5 pour chacun d’entre eux. Cette chaîne alphanumérique fait office d’empreinte digitale unique. Si un seul bit du fichier venait à changer à l’avenir (à la suite d’une panne de disque ou d’une modification accidentelle) (Rosenthal 2010), cette somme de contrôle changerait. Nous utiliserons ces sommes de contrôle pour identifier les images en double.

Code
# Find all image files recursively using Regex for extensions
image_files <- list.files(
  path = target_dir,
  pattern = "\\.(jpg|jpeg|png|tiff|tif)$",
  recursive = TRUE,
  full.names = TRUE,
  ignore.case = TRUE
)

print(paste("Found", length(image_files), "potential image files. Calculating checksums..."))
[1] "Found 4 potential image files. Calculating checksums..."
Code
# Inventory and Checksum Calculation
file_inventory <- tibble(file_path = image_files) %>%
  mutate(
    filename = basename(file_path),
    # Calculate MD5 hash for fixity
    md5_checksum = map_chr(file_path, digest::digest, file = TRUE, algo = "md5")
  )

head(file_inventory)
# A tibble: 4 × 3
  file_path                                                filename md5_checksum
  <chr>                                                    <chr>    <chr>       
1 data/Inspect_Images//CHIRPS_precipitation_1981-01-01.tif CHIRPS_… aea0ea1f3e3…
2 data/Inspect_Images//CHIRPS_precipitation_1981-01-03.tif CHIRPS_… b35611fe6fc…
3 data/Inspect_Images//CHIRTS_maximum_temperature_1983-01… CHIRTS_… 87496450b7b…
4 data/Inspect_Images//CHIRTS_maximum_temperature_1983-01… CHIRTS_… 206884ca8ae…

21.5 Technical Validation using magick // Validation technique à l’aide de magick

The magick package allow us to confirm these files are readable and extract basic technical properties. This step loops through each file, reads its information, and collects it into a single table.

//

Le package magick nous permet de vérifier que ces fichiers sont lisibles et d’en extraire les propriétés techniques de base. Cette étape parcourt chaque fichier, en lit les informations et les rassemble dans un seul tableau.

Code
magick_results <- purrr::map_dfr(image_files, function(fp) {
  tryCatch({
    img <- image_read(fp)
    info <- image_info(img)
    
    # Return a clean row of data
    tibble(
      file_path = fp,
      format_magick = info$format,
      width = info$width,
      height = info$height,
      colorspace = info$colorspace,
      filesize_mb = round(info$filesize / 1024^2, 2),
      valid_image = TRUE
    )
  }, error = function(e) {
    # Log corrupt files
    tibble(
      file_path = fp,
      valid_image = FALSE,
      error_msg = e$message
    )
  })
})

print("Technical validation complete.")
[1] "Technical validation complete."

21.6 Detailed EXIF Metadata with exiftoolr // Métadonnées EXIF détaillées avec exiftoolr

This step uses exiftoolr to extract all available embedded metadata from the image files. This can include hundreds of fields detailing everything from the camera settings and lens information to GPS coordinates and software versions. Please note that the output can have many columns.

//

Cette étape utilise exiftoolr pour extraire toutes les métadonnées intégrées disponibles des fichiers image. Cela peut inclure des centaines de champs détaillant tout, des réglages de l’appareil photo et des informations sur l’objectif aux coordonnées GPS et aux versions logicielles. Veuillez noter que le résultat peut comporter de nombreuses colonnes.

Code
# Run ExifTool on all files at once
exif_results <- tryCatch({
  exif_read(image_files) %>%
    mutate(SourceFile = image_files) # Ensure joining key exists
}, error = function(e) {
  message("ExifTool warning: ", e$message)
  return(data.frame(SourceFile = image_files))
})

# Select high-value columns for the report (customizable)
common_cols <- intersect(names(exif_results), c("SourceFile", "Make", "Model", "Software", "DateTimeOriginal", "GPSLatitude", "GPSLongitude", "Megapixels"))

if(length(common_cols) > 0) {
  exif_subset <- exif_results %>% select(all_of(common_cols))
} else {
  exif_subset <- exif_results
}

21.7 Compile Flags // Indicateurs de compilation

We now merge the three data streams (Inventory, Magick, Exif) to perform automated quality control. We apply logic to “flag” files that deviate from the norm or pose risks.

The Flagging Logic:

  • Duplicate_File: Files sharing the exact same MD5 checksum (Redundant storage).

  • Privacy_Risk: Files containing GPSLatitude data (requires review).

  • Dimension_Outlier: Images that do not match the Mode (most common) width/height of the dataset.

  • Corrupt: Files that magick failed to read.

//

Nous fusionnons désormais les trois flux de données (Inventory, Magick, Exif) afin d’effectuer un contrôle qualité automatisé. Nous appliquons une logique pour « marquer » les fichiers qui s’écartent de la norme ou présentent des risques.

La logique de marquage :

  • Duplicate_File : Fichiers partageant exactement la même somme de contrôle MD5 (stockage redondant).

  • Privacy_Risk : Fichiers contenant des données de latitude GPS (nécessite une vérification).

  • Dimension_Outlier : Images dont la largeur/hauteur ne correspond pas à la valeur modale (la plus courante) de l’ensemble de données.

  • Corrupt : Fichiers que Magick n’a pas pu lire.

Code
# 1. Merge all data sources
full_report <- file_inventory %>%
  left_join(magick_results, by = "file_path") %>%
  left_join(exif_subset, by = c("file_path" = "SourceFile"))

# 2. Defensive Step: Ensure critical columns exist
# If no files have GPS, these columns won't exist. We create them as NA to prevent errors.
cols_to_ensure <- c("GPSLatitude", "width", "height")

for (col in cols_to_ensure) {
  if (!col %in% names(full_report)) {
    full_report[[col]] <- NA
  }
}

# 3. Calculate the "Mode" (most common) dimensions for outlier detection
get_mode <- function(v) {
  # Remove NAs before calculating mode to avoid errors
  v <- na.omit(v)
  if (length(v) == 0) return(NA)
  uniqv <- unique(v)
  uniqv[which.max(tabulate(match(v, uniqv)))]
}

mode_width <- get_mode(full_report$width)
mode_height <- get_mode(full_report$height)

# 4. Apply Curation Flags
curated_data <- full_report %>%
  group_by(md5_checksum) %>%
  mutate(is_duplicate = n() > 1) %>%
  ungroup() %>%
  mutate(
    flag_duplicate = ifelse(is_duplicate, "DUPLICATE", NA),
    # Check if valid_image exists; if not, assume FALSE (safety check)
    flag_corrupt = ifelse(exists("valid_image") & !valid_image, "CORRUPT", NA),
    # Safe check for GPS: if it was missing, we made it NA above, so this just returns NA
    flag_privacy_gps = ifelse(!is.na(GPSLatitude), "HAS_GPS_DATA", NA),
    # Safe check for outliers: handles cases where width/height might be NA
    flag_outlier = ifelse(
      !is.na(width) & !is.na(height) & (width != mode_width | height != mode_height), 
      "DIMENSION_OUTLIER", 
      NA
    )
  ) %>%
  # Combine flags into a single readable column
  unite("curation_flags", starts_with("flag_"), sep = "; ", na.rm = TRUE, remove = FALSE)

# Preview flagged issues
print("--- Flagged Issues for Review ---")
[1] "--- Flagged Issues for Review ---"
Code
curated_data %>% 
  filter(curated_data$curation_flags != "") %>%
  select(filename, curation_flags) %>%
  head()
# A tibble: 0 × 2
# ℹ 2 variables: filename <chr>, curation_flags <chr>
Code
output_dir <- "Results/Inspect_Images"
dir.create(output_dir, recursive = TRUE, showWarnings = FALSE)

timestamp <- format(Sys.Date(), "%Y%m%d")
output_file <- paste0(output_dir, "/Curation_Report_Images_", timestamp, ".csv")

write.csv(curated_data, output_file, row.names = FALSE)

print(paste("Curation report saved to:", output_file))
[1] "Curation report saved to: Results/Inspect_Images/Curation_Report_Images_20260703.csv"

21.8 Curation Insights // Conseils pour la curation

Use the generated CSV to perform these checks:

  • Privacy Risks (GPS): Filter the CSV for flag_privacy_gps == “HAS_GPS_DATA”. These images contain embedded location coordinates (Latitude/Longitude). If the dataset involves human subjects or protected species, these coordinates must be scrubbed using ExifTool (-gps:all=) before publication to prevent “mosaic effect” re-identification. Discuss with the researcher about reducing the precision of the coordinates or removing them altogether.

  • Format Obsolescence: Check the format_magick column. While .png and .tiff are standard, proprietary raw formats (e.g., .CR2, .NEF, .ARW) are less stable for long-term preservation. Discuss with the researcher about transforming these files to a standard lossless format such as TIFF or PNG.

  • Digital Fixity (Duplicates): Filter for flag_duplicate == “DUPLICATE”. These files share the exact same MD5 hash, meaning they are bit-for-bit identical, even if they have different filenames.

//

Utilisez le fichier CSV généré pour effectuer les vérifications suivantes :

  • Risques liés à la confidentialité (GPS) : Filtrez le fichier CSV en recherchant flag_privacy_gps == « HAS_GPS_DATA ». Ces images contiennent des coordonnées de localisation intégrées (latitude/longitude). Si l’ensemble de données concerne des sujets humains ou des espèces protégées, ces coordonnées doivent être effacées à l’aide d’ExifTool (-gps:all=) avant publication afin d’éviter toute réidentification par « effet de mosaïque ». Discutez avec le chercheur de la possibilité de réduire la précision des coordonnées ou de les supprimer complètement.

  • Obsolescence du format : Vérifiez la colonne format_magick. Alors que les formats .png et .tiff sont standard, les formats bruts propriétaires (par exemple, .CR2, .NEF, .ARW) sont moins stables pour la conservation à long terme. Discutez avec le chercheur de la possibilité de convertir ces fichiers vers un format standard tel que TIFF ou PNG.

  • Fermeté numérique (doublons) : Filtrez sur flag_duplicate == « DUPLICATE ». Ces fichiers partagent exactement le même hachage MD5, ce qui signifie qu’ils sont identiques bit à bit, même s’ils ont des noms de fichiers différents.

21.9 Additional Tools & Resources // Outils et ressources supplémentaires

While R is excellent for batch processing and statistical summaries, visual and command-line tools are often required for deep analysis or manual correction.

  • ExifTool (Command Line): The industry standard for reading, writing, and editing metadata. In addition to the functionality from the R wrapper, the direct command line also allows you to write data (e.g., scrubbing GPS tags) (see https://https://exiftool.org/).

  • JHOVE (JSTOR/Harvard Object Validation Environment): A widely used digital preservation tool. It performs stricter format validation than magick. It can verify if a file claims to be a TIFF but violates the specific version 6.0 specification.

  • Tesseract OCR: Is an optical character recognition engine. It is essential for “Content Analysis.” It can scan thousands of images to detect text, helping curators flag files that contain sensitive documents (PII) which might have been mixed into a photo dataset.

  • ImageMagick (GUI/CLI): It is a tool useful for batch file format transformations, such as proprietary image formats to TIFF.

//

Si R est idéal pour le traitement par lots et les résumés statistiques, des outils visuels et en ligne de commande sont souvent nécessaires pour une analyse approfondie ou une correction manuelle.

  • ExifTool (ligne de commande) : La norme industrielle pour la lecture, l’écriture et la modification des métadonnées. Outre les fonctionnalités offertes par le wrapper R, la ligne de commande directe vous permet également d’écrire des données (par exemple, effacer les balises GPS) (voir https://https://exiftool.org/).

  • JHOVE (JSTOR/Harvard Object Validation Environment) : Un outil de préservation numérique largement utilisé. Il effectue une validation de format plus stricte que magick. Il peut vérifier si un fichier prétend être un TIFF mais enfreint la spécification de la version 6.0.

  • Tesseract OCR : Moteur de reconnaissance optique de caractères. Indispensable pour l’« analyse de contenu ». Il peut analyser des milliers d’images pour détecter du texte, aidant ainsi les conservateurs à signaler les fichiers contenant des documents sensibles (informations personnelles identifiables) qui auraient pu se retrouver dans un ensemble de données photographiques.

  • ImageMagick (GUI/CLI) : C’est un outil utile pour les transformations de format de fichiers par lots, par exemple pour convertir des formats d’image propriétaires en TIFF.

21.10 Using the Non-Interactive R Script // Utilisation du script R non interactif

For users who want to run this analysis on a server, in a batch job, or from the command line, here is a pure R script that performs the same process.

Prerequisites: * R with tidyverse, magick, and exiftoolr installed. * ExifTool command-line software must be installed on the system.

Download the R Script: Inspect_Images_Script.R

//

Pour les utilisateurs qui souhaitent exécuter cette analyse sur un serveur, dans le cadre d’un traitement par lots ou depuis la ligne de commande, voici un script R pur qui effectue le même processus.

Prérequis : * R avec les paquets tidyverse, magick et exiftoolr installés. * Le logiciel en ligne de commande ExifTool doit être installé sur le système.

Télécharger le script R : Inspect_Images_Script.R

21.10.1 Example HPC Submission Script // Exemple de script de soumission CHP

#!/bin/bash
#SBATCH --job-name=img_curation     
#SBATCH --nodes=1                   
#SBATCH --ntasks=1                  
#SBATCH --cpus-per-task=4           
#SBATCH --mem=16G                  
#SBATCH --time=01:00:00             
#SBATCH --output=logs/img_qc_%j.log 
#SBATCH --error=logs/img_qc_%j.err  

# 1. Load Modules
# Adjust these based on your specific cluster's environment
module load R/4.2.0           
module load exiftool          

# 2. Define Variables
# Point this to the folder containing your images
TARGET_DIR="/scratch/user/project_data/images_raw"

# 3. Run the R Script
# We pass the target directory as the first argument ($1)
echo "Starting Image Curation Pipeline on $TARGET_DIR"

Rscript Inspect_Images_Script.R "$TARGET_DIR"

echo "Job finished."

21.11 References