knitr::opts_chunk$set(echo = TRUE) library(patchwork) library(eyesim) library(dplyr) library(ggplot2) library(ggridges)
MultiMatch (Dewhurst et al. 2012) is a comprehensive method for analyzing and comparing eye movement scanpaths. It evaluates scanpath similarity across five dimensions: "vector", "direction", "length", "position", and "duration". By breaking down eye movements into these distinct components, MultiMatch provides a detailed and nuanced comparison of visual behavior. This method is particularly useful for studies in psychology, cognitive science, and human-computer interaction where understanding visual attention and patterns is crucial. In our implementation, we add another metric based on the earth mover's distance (EMD; 'position_emd') between the positions of the fixations. This should capture similarities in the distributions of the fixations that are not order-specific.
Let's simulate a simple eye-movement pattern and compare them with one another.
# Simulate fixation data simulate_fixations_linear <- function(n) { out <- data.frame( fixation = 1:n, x = cumsum(rnorm(n, mean = 50, sd = 10)), y = cumsum(rnorm(n, mean = 50, sd = 10)), duration = rpois(n, lambda = 200) ) fixation_group(out$x, out$y, out$duration, out$fixation) } fixations1 <- simulate_fixations_linear(10) fixations2 <- simulate_fixations_linear(12) p1 <- plot(fixations1) p2 <- plot(fixations2) p1 + p2
Now, we will compare the two fixation groups using the multimatch
function.
sp1 <- scanpath(fixations1) sp2 <- scanpath(fixations2) eyesim:::multi_match(sp1, sp2, c(500,500))
All of the metrics give pretty high values, consistent with the known similarity of the scanpaths.
# Simulate fixation data for each scenario simulate_fixations_zigzag <- function() { out <- data.frame( fixation = 1:10, x = cumsum(c(50, 50, 50, 50, 50, 50, 50, 50, 50, 50)), y = cumsum(c(50, -50, 50, -50, 50, -50, 50, -50, 50, -50)), duration = rpois(10, lambda = 200) ) fixation_group(out$x, out$y, out$duration, out$fixation) } fixations1_linear <- simulate_fixations_linear(10) fixations2_zigzag <- simulate_fixations_zigzag() sp1 <- scanpath(fixations1_linear) sp2 <- scanpath(fixations2_zigzag) eyesim:::multi_match(sp1, sp2, c(500,500))
We will simulate 8000 pairs of randomly generated scanpaths and show the distribution of the similarity scores for each metric.
fgs <- replicate(8000, { fixations1 <- fixation_group(runif(10)*500, runif(10)*500, round(runif(10)*10)+1, 1:10) fixations2 <- fixation_group(runif(10)*500, runif(10)*500, round(runif(10)*10)+1, 1:10) list(fg1=fixations1, fg2=fixations2) #sp1 <- scanpath(fixations1) #sp2 <- scanpath(fixations2) #eyesim:::multi_match(sp1, sp2, c(500,500)) }, simplify=FALSE) ssdf <- lapply(fgs, function(x) eyesim:::multi_match(scanpath(x$fg1), scanpath(x$fg2), c(500,500))) %>% bind_rows() # Create ridgeline plots for each MultiMatch metric ridgeline_vector <- ggplot(ssdf, aes(x = mm_vector, y = factor(1, labels = "Vector Similarity"))) + geom_density_ridges(scale = 3, rel_min_height = 0.01, bandwidth = 0.05, fill = 'blue', color = 'black', alpha = 0.7) + labs(x = 'Similarity', y = '') + theme_ridges() + theme(axis.title.y = element_blank()) ridgeline_direction <- ggplot(ssdf, aes(x = mm_direction, y = factor(1, labels = "Direction Similarity"))) + geom_density_ridges(scale = 3, rel_min_height = 0.01, bandwidth = 0.05, fill = 'green', color = 'black', alpha = 0.7) + labs(x = 'Similarity', y = '') + theme_ridges() + theme(axis.title.y = element_blank()) ridgeline_length <- ggplot(ssdf, aes(x = mm_length, y = factor(1, labels = "Length Similarity"))) + geom_density_ridges(scale = 3, rel_min_height = 0.01, bandwidth = 0.05, fill = 'red', color = 'black', alpha = 0.7) + labs(x = 'Similarity', y = '') + theme_ridges() + theme(axis.title.y = element_blank()) ridgeline_position <- ggplot(ssdf, aes(x = mm_position, y = factor(1, labels = "Position Similarity"))) + geom_density_ridges(scale = 3, rel_min_height = 0.01, bandwidth = 0.05, fill = 'purple', color = 'black', alpha = 0.7) + labs(x = 'Similarity', y = '') + theme_ridges() + theme(axis.title.y = element_blank()) ridgeline_duration <- ggplot(ssdf, aes(x = mm_duration, y = factor(1, labels = "Duration Similarity"))) + geom_density_ridges(scale = 3, rel_min_height = 0.01, bandwidth = 0.05, fill = 'orange', color = 'black', alpha = 0.7) + labs(x = 'Similarity', y = '') + theme_ridges() + theme(axis.title.y = element_blank()) ridgeline_emd <- ggplot(ssdf, aes(x = mm_position_emd, y = factor(1, labels = "EMD Similarity"))) + geom_density_ridges(scale = 3, rel_min_height = 0.01, bandwidth = 0.05, fill = 'cyan', color = 'black', alpha = 0.7) + labs(x = 'Similarity', y = '') + theme_ridges() + theme(axis.title.y = element_blank()) # Combine the ridgeline plots using patchwork combined_plot <- ridgeline_vector / ridgeline_direction / ridgeline_length / ridgeline_position / ridgeline_duration / ridgeline_emd + plot_layout(ncol = 1) # Display the combined plot print(combined_plot)
Let's plot the fixations for the extreme values of the multimatch metrics. By examining the highest and lowest pairs we can gain insight into what each metric is capturing. Note that the size of points captures duration of each fixation.
metrics <- c("vector", "direction", "length", "position", "duration", "position_emd") extremes <- lapply(metrics, function(metric) { m <- paste0("mm_", metric) list( high = which.max(ssdf[[m]]), low = which.min(ssdf[[m]]) ) }) names(extremes) <- metrics # Function to create a plot for a fixation group with a title and annotation plot_fixation_group <- function(fg, title, metric_value) { plot(fg) + ggtitle(title) + annotate("text", x = Inf, y = -Inf, label = paste("score:", round(metric_value, 2)), hjust = 1.1, vjust = -1.1, size = 2, color = "blue") } # Create plots for each metric plots <- lapply(names(extremes), function(metric) { high_index <- extremes[[metric]]$high low_index <- extremes[[metric]]$low metric_high_value <- ssdf[high_index, paste0("mm_", metric)] metric_low_value <- ssdf[low_index, paste0("mm_", metric)] high_fg1 <- fgs[[high_index]]$fg1 high_fg2 <- fgs[[high_index]]$fg2 low_fg1 <- fgs[[low_index]]$fg1 low_fg2 <- fgs[[low_index]]$fg2 high_plot1 <- plot_fixation_group(high_fg1, paste(metric, "High(1)"), metric_high_value) high_plot2 <- plot_fixation_group(high_fg2, paste(metric, "High(2)"), metric_high_value) low_plot1 <- plot_fixation_group(low_fg1, paste(metric, "Low(1)"), metric_low_value) low_plot2 <- plot_fixation_group(low_fg2, paste(metric, "Low(2)"), metric_low_value) list(high_plot = high_plot1 + high_plot2, low_plot = low_plot1 + low_plot2) }) # Set the size of each plot plot_height <- 12 # Adjust height as needed plot_width <- 12 # Adjust width as needed # Combine and arrange plots using patchwork high_plots <- wrap_plots( plots[[1]]$high_plot, plots[[2]]$high_plot, plots[[3]]$high_plot, plots[[4]]$high_plot, plots[[5]]$high_plot, plots[[6]]$high_plot, nrow = 6 ) + plot_layout(guides = 'collect', heights = rep(plot_height, 6)) + ggtitle("High MultiMatch Scores") low_plots <- wrap_plots( plots[[1]]$low_plot, plots[[2]]$low_plot, plots[[3]]$low_plot, plots[[4]]$low_plot, plots[[5]]$low_plot, plots[[6]]$low_plot, nrow = 6 ) + plot_layout(guides = 'collect', heights = rep(plot_height, 6)) + ggtitle("Low MultiMatch Scores") # Save the plots to file #ggsave("high_plots.png", plot = high_plots, height = plot_height * 5, width = plot_width, units = "in") #ggsave("low_plots.png", plot = low_plots, height = plot_height * 5, width = plot_width, units = "in")
These are the highest pairs of scanpaths for each metric.
high_plots
And the lowest.
low_plots
Let's generate a random scanpath and apply some transformations to it to see how the multimatch metrics change.
fg <- fixation_group(runif(10)*500, runif(10)*500, round(runif(10)*10)+1, 1:10) plot(fg) + plot(fg) eyesim:::multi_match(scanpath(fg), scanpath(fg), c(500,500))
Now, we will scale the second scanpath by .5 and recompute the multimatch metrics. You will notice that the "direction" metric remains 1, while position (especially) decreases dramatically. This is because the absolute positions of the fixations are not preserved under scaling.
fg2 <- fg fg2$y <- fg2$y * .5 fg2$x <- fg2$x * .5 p1 <- plot(fg) + ylim(0,500) p2 <- plot(fg2) + ylim(0,500) p1+p2 eyesim:::multi_match(scanpath(fg), scanpath(fg2), c(500,500))
Next we will preserve the positions of the fixations but scramble their order. This will have a large impact on the "direction" metric, but not on the other metrics. Note also the earth mover's distance should remain high.
fg2 <- fg ord <- sample(1:nrow(fg2)) fg2$x <- fg2$x[ord] fg2$y <- fg2$y[ord] fg2$duration <- fg2$duration[ord] p1 <- plot(fg) + ylim(0,500) p2 <- plot(fg2) + ylim(0,500) p1+p2 eyesim:::multi_match(scanpath(fg), scanpath(fg2), c(500,500))
Dewhurst, R., Nyström, M., Jarodzka, H., Foulsham, T., Johansson, R., & Holmqvist, K. (2012). It depends on how you look at it: Scanpath comparison in multiple dimensions with MultiMatch, a vector-based approach. Behavior Research Methods, 44, 1079-1100.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.