Nothing
#' Apply time shifts to JPEG image metadata
#'
#' Change the values of digital timestamps in image metadata using ExifTool. If
#' date/time of images were set incorrectly, they can be corrected easily in
#' batch mode for further analyses. Please, always make a backup of your data
#' before using this function to avoid data loss or damage. This is because
#' ExifTool will make a copy of your images and applies the time shifts to the
#' copies. The file extension of the original images (.JPG) will be renamed to
#' ".JPG_original".
#'
#'
#' \code{timeShiftTable} is a data frame with columns for station ID, camera ID
#' (optional), time shift value and direction of time shift (for an example see
#' \code{\link{timeShiftTable}}). Images in \code{inDir} must be sorted into
#' station directories. If \code{hasCameraFolders = TRUE}, the function expects
#' camera subdirectories in the station directories and will only apply time
#' shifts to the camera subdirectories specified by \code{CameraCol} in
#' \code{timeShiftTable}. If \code{hasCameraFolders = FALSE}, shifts will be
#' applied to the whole station directory (including potential subdirectories).
#'
#' The values of \code{timeShiftColumn} must adhere to the following pattern:
#' "YYYY:mm:dd HH:MM:SS" ("year:month:day hour:minute:second"). Examples:
#' "1:0:0 0:0:0" is a shift of exactly 1 year and "0:0:0 12:10:01" 12 hours and
#' 10 minutes and 1 second. Note that stating "00" may cause problems, so use
#' "0" instead if an entry is zero.
#'
#' \code{timeShiftSignColumn} signifies the direction of the time shift. "+"
#' moves image dates into the future (i.e. the image date lagged behind the
#' actual date) and "-" moves image dates back (if the image dates were ahead
#' of actual time).
#'
#' ExifTool stores the original images as \code{.JPG_original} files in the
#' original file location. By setting \code{undo = TRUE}, any JPG files in the
#' directories specified by \code{timeShiftTable} will be deleted and the
#' original JPEGs will be restored from the JPG_original files. Please make a
#' backup before using \code{undo}.
#'
#' Years can have 365 or 366 days, and months 28 to 31 days. Here is how the
#' function handles these (from the exiftool help page): "The ability to shift
#' dates by Y years, M months, etc, conflicts with the design goal of
#' maintaining a constant shift for all time values when applying a batch
#' shift. This is because shifting by 1 month can be equivalent to anything
#' from 28 to 31 days, and 1 year can be 365 or 366 days, depending on the
#' starting date. The inconsistency is handled by shifting the first tag found
#' with the actual specified shift, then calculating the equivalent time
#' difference in seconds for this shift and applying this difference to
#' subsequent tags in a batch conversion."
#'
#' \code{ignoreMinorErrors} is useful if image timestamps are not updated
#' correctly (entries in column "n_images" of the output are "... files weren't
#' updated due to errors"). This can be caused by bad MakerNotes and so far was
#' only observed in Panthera V4 and V6 cameras. In that case, set
#' \code{ignoreMinorErrors} to \code{TRUE}. This will add the "-m" option to
#' the Exiftool call, thereby ignoring minor errors and warnings and applying
#' the time shift nevertheless.
#'
#' @param inDir character. Name of directory containing station directories
#' with images
#' @param hasCameraFolders logical. Do the station directories in \code{inDir}
#' have camera subdirectories (e.g. "inDir/StationA/Camera1")?
#' @param timeShiftTable data.frame containing information about
#' station-/camera-specific time shifts.
#' @param stationCol character. name of the column specifying Station ID in
#' \code{timeShiftTable}
#' @param cameraCol character. name of the column specifying Camera ID in
#' \code{timeShiftTable} (optional)
#' @param timeShiftColumn character. The name of the column containing time
#' shift values in \code{timeShiftTable}
#' @param timeShiftSignColumn character. The name of the column with the
#' direction of time shifts in \code{timeShiftTable}. Can only be "-" or "+".
#' @param undo logical. Undo changes and restore the original images? Please be
#' careful, this deletes any edited images if \code{TRUE}
#' @param ignoreMinorErrors logical. Ignore minor errors that would cause the
#' function to fail (set TRUE for images with bad MakerNotes, observed in
#' Panthera V4 cameras)
#'
#' @return A \code{data.frame} containing the information about the processed
#' directories and the number of images.
#'
#' @author Juergen Niedballa
#'
#' @references \url{https://exiftool.org/#shift}
#'
#' @examples
#'
#'
#' \dontrun{
#'
#' # copy sample images to temporary directory (so we don't mess around in the package directory)
#' wd_images_ID <- system.file("pictures/sample_images_species_dir", package = "camtrapR")
#' file.copy(from = wd_images_ID, to = tempdir(), recursive = TRUE)
#' wd_images_ID_copy <- file.path(tempdir(), "sample_images_species_dir")
#'
#' data(timeShiftTable)
#'
#'
#' timeshift_run <- timeShiftImages(inDir = wd_images_ID_copy,
#' timeShiftTable = timeShiftTable,
#' stationCol = "Station",
#' hasCameraFolders = FALSE,
#' timeShiftColumn = "timeshift",
#' timeShiftSignColumn = "sign",
#' undo = FALSE
#' )
#'
#'
#' timeshift_undo <- timeShiftImages(inDir = wd_images_ID_copy,
#' timeShiftTable = timeShiftTable,
#' stationCol = "Station",
#' hasCameraFolders = FALSE,
#' timeShiftColumn = "timeshift",
#' timeShiftSignColumn = "sign",
#' undo = TRUE
#' )
#' }
#'
#' @export timeShiftImages
#'
timeShiftImages <- function(inDir,
hasCameraFolders,
timeShiftTable,
stationCol,
cameraCol,
timeShiftColumn,
timeShiftSignColumn,
undo = FALSE,
ignoreMinorErrors = FALSE
)
{
if(Sys.which("exiftool") == "") stop("cannot find ExifTool")
timeShiftTable <- dataFrameTibbleCheck(df = timeShiftTable)
# convert all columns to character
for(i in 1:ncol(timeShiftTable)){
timeShiftTable[,i] <- as.character(timeShiftTable[,i])
}
rm(i)
# check column names
checkForSpacesInColumnNames(stationCol = stationCol, timeShiftColumn = timeShiftColumn, timeShiftSignColumn = timeShiftSignColumn)
if(!stationCol %in% colnames(timeShiftTable)) stop(paste('stationCol = "', stationCol, '" is not a column name in timeShiftTable', sep = ''), call. = FALSE)
if(!timeShiftColumn %in% colnames(timeShiftTable)) stop(paste('timeShiftColumn = "', timeShiftColumn, '" is not a column name in timeShiftTable', sep = ''), call. = FALSE)
if(!timeShiftSignColumn %in% colnames(timeShiftTable)) stop(paste('timeShiftSignColumn = "', timeShiftSignColumn, '" is not a column name in timeShiftTable', sep = ''), call. = FALSE)
if(isTRUE(hasCameraFolders)){
stopifnot(cameraCol %in% colnames(timeShiftTable))
} else {
if(any(duplicated(timeShiftTable[,stationCol]))) stop("There are duplicates in stationCol. Check argument hasCameraFolders")
}
stopifnot(is.logical(hasCameraFolders))
for(xy in 1:nrow(timeShiftTable)){
if(length(unlist(strsplit(timeShiftTable[xy,timeShiftColumn], split = " "))) != 2) stop(paste("there is more than 1 space in your timeShiftColumn string. Only 1 space is allowed (", timeShiftTable[xy,stationCol], ")"))
if(nchar(timeShiftTable[xy,timeShiftColumn]) - nchar(gsub(":","",timeShiftTable[xy,timeShiftColumn])) != 4) stop("there should be 4 colons in timeShiftColumn (", timeShiftTable[xy,stationCol], ")")
if(timeShiftTable[xy,timeShiftSignColumn] %in% c("+", "-") == FALSE) stop(paste0('timeShiftSignColumn can only be "+" or "-". Found value "', timeShiftTable[xy,timeShiftSignColumn], '" at station ',
timeShiftTable[xy,stationCol]))
if(length(unlist(lapply(strsplit(timeShiftTable[xy,timeShiftColumn], split = " "), FUN = strsplit, split = ":"))) != 6) stop("there must be six numbers in timeShiftColumn (",
timeShiftTable[xy,stationCol], ")")
}
if(!dir.exists(inDir)) stop("Could not find inDir:\n", inDir, call. = FALSE)
if(isTRUE(hasCameraFolders)){
shift.dirs <- file.path(inDir, timeShiftTable[,stationCol], timeShiftTable[,cameraCol])
} else {
shift.dirs <- file.path(inDir, timeShiftTable[,stationCol])
}
if(any(file.exists(shift.dirs) == FALSE)){
stop(paste("Station directory does not exist:\n",
paste(shift.dirs[file.exists(shift.dirs) == FALSE], collapse = "\n"), sep = ""), call. = FALSE)
}
if(isTRUE(undo)){
results_undo <- data.frame(directory = rep(NA, times = nrow(timeShiftTable)),
n_images_undo = rep(NA, times = nrow(timeShiftTable)))
jpg2undo <- list()
jpg2keep <- list()
jpg2keep_newname <- list()
remove.tmp <- rename.tmp <- list()
for(i in 1:length(shift.dirs)){
jpg2undo[[i]] <- list.files(shift.dirs[i], pattern = ".jpg$|.JPG$", recursive = TRUE, full.names = TRUE)
jpg2keep[[i]] <- list.files(shift.dirs[i], pattern = ".jpg_original$|.JPG_original$", recursive = TRUE, full.names = TRUE)
jpg2keep_newname[[i]] <- gsub(pattern = "_original$", replacement = "", x = jpg2keep[[i]])
if(length(jpg2keep[[i]]) == 0) stop(paste("found no .JPG_original files in", shift.dirs[i], "check argument 'undo'"))
if(length(jpg2undo[[i]]) != length(jpg2keep[[i]])) stop(paste("number of jpgs in", shift.dirs[i], "is not equal to the number of JPG_original files" ))
results_undo$directory[i] <- shift.dirs[i]
results_undo$n_images_undo[i] <- length(jpg2undo[[i]])
}
remove.tmp <- lapply(jpg2undo, FUN = file.remove)
for(xyz in 1:length(jpg2keep)){
rename.tmp[[xyz]] <- file.rename(from = jpg2keep[[xyz]], to = jpg2keep_newname[[xyz]])
}
return(results_undo)
} else {
results.tmp <- list()
for(i in 1:nrow(timeShiftTable)){
message(shift.dirs[i])
command.tmp2b <- paste0('exiftool -r ', ifelse(ignoreMinorErrors, "-m ", ""), '"-DateTimeOriginal', timeShiftTable[i,timeShiftSignColumn], '=',
timeShiftTable[i,timeShiftColumn], '" "', shift.dirs[i], '"')
results.tmp[[i]] <- system(command.tmp2b, intern=TRUE)
rm(command.tmp2b)
}
results.tmp2 <- lapply(results.tmp, FUN = function(x){x[c(length(x) - 1, length(x))]})
output <- data.frame(shift.dirs, matrix(unlist(results.tmp2),ncol = 2, byrow = 2))
colnames(output) <- c("directory", "n_directories", "n_images")
if(any(grepl("files weren't updated due to errors", output$n_images))){
warning("There were problems changing the time stamps in:\n",
paste(output$directory[grepl("files weren't updated due to errors", output$n_images)], collapse = "\n"),
"\n\nTry setting ignoreMinorErrors = TRUE", call. = FALSE)
}
return(output)
}
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.