inst/scripts/speed_comparison.R

# Speed Comparison: Color Walk vs Exact
# Tests the performance of different morphing modes

message("== Speed Comparison for lapr morphing modes ==\n")

suppressPackageStartupMessages({
  library(magick)
  library(png)
})

# Assume lapr functions are available
# If running standalone, you'll need to source utils_morph.R and have C++ compiled

# Create synthetic test images of different sizes
create_test_pair <- function(size) {
  H <- W <- size
  arrA <- array(0, c(H, W, 3))
  arrB <- array(0, c(H, W, 3))
  
  # Create varied color patterns
  for (y in 1:H) {
    for (x in 1:W) {
      # Image A: radial gradient with some structure
      r <- sqrt((x - H/2)^2 + (y - W/2)^2) / (H/2)
      arrA[y, x, 1] <- (1 - r) * 255
      arrA[y, x, 2] <- (x / W) * 255
      arrA[y, x, 3] <- (y / H) * 255
      
      # Image B: different pattern
      arrB[y, x, 1] <- (y / H) * 255
      arrB[y, x, 2] <- (1 - r) * 255
      arrB[y, x, 3] <- (x / W) * 255
    }
  }
  
  tmpA <- tempfile(fileext = ".png")
  tmpB <- tempfile(fileext = ".png")
  writePNG(arrA / 255, tmpA)
  writePNG(arrB / 255, tmpB)
  list(A = tmpA, B = tmpB)
}

# Test configurations
test_configs <- list(
  list(mode = "exact",      args = list(alpha = 1, beta = 0, patch_size = 1L), label = "exact_β=0"),
  list(mode = "exact",      args = list(alpha = 1, beta = 0.05, patch_size = 3L), label = "exact_patch3"),
  list(mode = "color_walk", args = list(quantize_bits = 6L), label = "color_walk_q6"),
  list(mode = "color_walk", args = list(quantize_bits = 5L), label = "color_walk_q5"),
  list(mode = "color_walk", args = list(quantize_bits = 7L), label = "color_walk_q7")
)

# Image sizes to test (keep under 100×100 for exact mode - limit is 10,000 pixels)
sizes <- c(16, 32, 64, 96)

# Results storage
results <- list()

cat("\n")
cat(sprintf("%-10s %-20s %12s %12s %15s\n", 
            "Size", "Mode", "Time (sec)", "Pixels", "μs/pixel"))
cat(strrep("-", 70), "\n")

for (size in sizes) {
  # Create test images
  pair <- create_test_pair(size)
  n_pixels <- size * size
  
  for (cfg in test_configs) {
    mode_label <- if (!is.null(cfg$label)) cfg$label else cfg$mode
    
    # Time the morphing operation
    timing <- system.time({
      tryCatch({
        do.call(lapr::pixel_morph, c(list(
          imgA = pair$A,
          imgB = pair$B,
          n_frames = 1L,  # Just test assignment computation
          fps = 1L,
          format = "gif",
          outfile = tempfile(fileext = ".gif"),
          show = FALSE,
          lap_method = "jv",
          maximize = FALSE,
          downscale_steps = 0L
        ), list(mode = cfg$mode), cfg$args))
      }, error = function(e) {
        message("Error for ", mode_label, " at size ", size, ": ", e$message)
      })
    })
    
    elapsed <- timing["elapsed"]
    us_per_pixel <- (elapsed * 1e6) / n_pixels
    
    cat(sprintf("%-10s %-20s %12.4f %12d %15.2f\n", 
                paste0(size, "×", size), 
                mode_label, 
                elapsed, 
                n_pixels,
                us_per_pixel))
    
    results[[length(results) + 1]] <- list(
      size = size,
      mode = cfg$mode,
      label = mode_label,
      time_sec = elapsed,
      n_pixels = n_pixels,
      us_per_pixel = us_per_pixel
    )
  }
  
  cat("\n")
}

# Summary statistics
cat("\n=== SUMMARY ===\n\n")

# Convert to data frame for easier analysis
df <- do.call(rbind, lapply(results, function(x) {
  data.frame(
    size = x$size,
    mode = x$mode,
    label = x$label,
    time_sec = x$time_sec,
    n_pixels = x$n_pixels,
    us_per_pixel = x$us_per_pixel,
    stringsAsFactors = FALSE
  )
}))

# Average time per mode across all sizes
cat("Average time per mode (across all sizes):\n")
for (mode_label in unique(df$label)) {
  subset_df <- df[df$label == mode_label, ]
  avg_time <- mean(subset_df$time_sec)
  avg_us <- mean(subset_df$us_per_pixel)
  cat(sprintf("  %-20s: %.4f sec (%.2f μs/pixel)\n", mode_label, avg_time, avg_us))
}

cat("\n")

# Scaling analysis (how does time grow with N?)
cat("Scaling behavior (time vs N):\n")
for (mode_label in unique(df$label)) {
  subset_df <- df[df$label == mode_label, ]
  if (nrow(subset_df) >= 2) {
    # Simple ratio: time at largest / time at smallest
    times <- subset_df$time_sec[order(subset_df$size)]
    sizes <- subset_df$size[order(subset_df$size)]
    if (times[1] > 0) {
      size_ratio <- sizes[length(sizes)] / sizes[1]
      time_ratio <- times[length(times)] / times[1]
      # Estimate exponent: time ~ N^k, so k = log(time_ratio) / log(size_ratio^2)
      k <- log(time_ratio) / log(size_ratio^2)
      cat(sprintf("  %-20s: ~O(N^%.2f)  [%d×%d is %.1fx slower than %d×%d]\n", 
                  mode_label, k,
                  sizes[length(sizes)], sizes[length(sizes)],
                  time_ratio,
                  sizes[1], sizes[1]))
    }
  }
}

cat("\n")
cat("Note: Exact modes should be ~O(N^2.5-3) due to LAP solving on N×N matrix\n")
cat("      Color walk should be ~O(K*M^2.5) where K=num_colors, M=avg_pixels_per_color\n")
cat("      With good quantization, K << N, making color_walk much faster for large N\n")

Try the couplr package in your browser

Any scripts or data that you put into this service are public.

couplr documentation built on Jan. 20, 2026, 5:07 p.m.