Fisheye effect with R

Posted on April 29, 2023 by Stéphane Laurent
Tags: R

The fisheye function below distorts a bitmap image with a fisheye effect or an anti-fisheye effect (rho > 0.5 or rho < 0.5).

fisheye_xy <- function(x, y, rho, stick) {
  p <- c(x, y)
  if(rho == 0.5) {
    return(p)
  }
  m <- c(0.5, 0.5)
  d <- p - m
  r <- sqrt(c(crossprod(d)))
  dnormalized <- d / r
  mnorm <- sqrt(c(crossprod(m)))
  power <- pi / mnorm * (rho - 0.5)
  if(power > 0) {
    bind <- if(stick == "corners") mnorm else m[2L]
    uv <- m + dnormalized * tan(r*power) * bind / tan(bind*power)
  } else {
    bind <- m[2L]
    uv <- m + dnormalized * atan(-10*r*power) * bind / atan(-10*bind*power)
  }
  uv
}

library(imager)
library(cooltools)
#' @importFrom imager load.image add.color squeeze R G B
#' @importFrom cooltools approxfun2
#' @importFrom grDevices col2rgb rgb
#' @param bitmapFile path to a bitmap file (jpg, png, ...)
#' @param rho amount of effect; no effect if 0.5, fisheye if >0.5, 
#'   antifisheye if <0.5
#' @param stick where to stick the image when rho>0.5, to the 
#'   corners or to the borders; if you stick to the corners, a 
#'   part of the image is lost
#' @param bkg background color; it appears only if rho>0.5 and 
#'   stick="borders" 
fisheye <- function(bitmapFile, rho, stick = "corners", bkg = "black") {
  stopifnot(rho > 0, rho < 1)
  stick <- match.arg(stick, c("borders", "corners"))
  # load the image
  img <- load.image(bitmapFile)
  dims <- dim(img)
  nx <- dims[1L]
  ny <- dims[2L]
  nchannels <- dims[4L]
  # if the image is gray, add colors
  if(nchannels == 1L) {
    img <- add.color(img)
  } else if(nchannels != 3L) {
    stop("Cannot process this image.")
  }
  # fisheye matrix
  PSI <- matrix(NA_complex_, nrow = nx, ncol = ny)
  for(i in 1L:nx) {
    x <- (i-1L) / (nx-1L)
    for(j in 1L:ny) {
      y <- (j-1L) / (ny-1L)
      uv <- fisheye_xy(x, y, rho, stick)
      PSI[i, j] <- complex(real = uv[1L], imaginary = uv[2L])
    }
  }
  # take the r, g, b channels
  r <- squeeze(R(img))
  g <- squeeze(G(img))
  b <- squeeze(B(img))
  # interpolation
  x_ <- seq(0, 1, length.out = nx)
  y_ <- seq(0, 1, length.out = ny)
  f_r <- approxfun2(x_, y_, r)
  f_g <- approxfun2(x_, y_, g)
  f_b <- approxfun2(x_, y_, b)
  M_r <- f_r(Re(PSI), Im(PSI))
  M_g <- f_g(Re(PSI), Im(PSI))
  M_b <- f_b(Re(PSI), Im(PSI))
  # set outside color
  RGB <- col2rgb(bkg)[, 1L] / 255
  M_r[is.na(M_r)] <- RGB[1L]
  M_g[is.na(M_g)] <- RGB[2L]
  M_b[is.na(M_b)] <- RGB[3L]
  # convert to hex codes
  rstr <- rgb(M_r, M_g, M_b)
  dim(rstr) <- c(nx, ny)
  # rotate
  t(rstr)
}

Let’s take for example this picture of Dilbert, named dilbert512x512.png:

It has a transparent background. We firstly transform this background it to a gray color (#aaaaaa) with the help of ImageMagick. The command to do that is:

convert in.png -background '#aaaaaa' -alpha remove -alpha off out.png

(magick convert if you use Windows).

We can run this command from R:

dilbert_transparent <- "dilbert512x512.png"
dilbert_gray        <- "dilbert_gray.png"
gray_color <- "#aaaaaa"
cmd <- sprintf(
  "convert %s -background '%s' -alpha remove -alpha off %s", 
  dilbert_transparent, gray_color, dilbert_gray
)
system(cmd)

Here is dilbert_gray.png:

Now let’s perform a fisheye distortion of this image with rho=0.95:

img <- fisheye(dilbert_gray, rho = 0.95, stick = "borders", bkg = gray_color)
# plot
opar <- par(mar = c(0, 0, 0, 0))
plot(c(-100, 100), c(-100, 100), type = "n", asp = 1, 
     xlab = NA, ylab = NA, axes = FALSE, xaxs = "i", yaxs = "i")
rasterImage(img, -100, -100, 100, 100)
par(opar)

The anti-fisheye effect is obtained by setting rho<0.5. We will do it, with something more: we will get a transparent background at the end.

To do so, first transform the transparent background to a color, for example green (#00ff00):

dilbert_transparent  <- "dilbert512x512.png"
dilbert_green        <- "dilbert_green.png"
green_color <- "#00ff00"
cmd <- sprintf(
  "convert %s -background '%s' -alpha remove -alpha off %s", 
  dilbert_transparent, green_color, dilbert_green
)
system(cmd)

Here is dilbert_green.png:

Now perform the anti-fisheye effect, and use the same green color as background:

img <- fisheye(dilbert_green, rho = 0.45, bkg = green_color)
# save image
png("dilbert_antifisheye_green.png", width = 512, height = 512)
opar <- par(mar = c(0, 0, 0, 0))
plot(c(-100, 100), c(-100, 100), type = "n", asp = 1, 
     xlab = NA, ylab = NA, axes = FALSE, xaxs = "i", yaxs = "i")
rasterImage(img, -100, -100, 100, 100)
par(opar)
dev.off()

Here is dilbert_antifisheye_green.png:

Finally, using ImageMagick, transform the green color to transparent:

cmd <- sprintf(
  "convert -fuzz 30% -transparent '%s' %s %s",
  green_color, "dilbert_antifisheye_green.png", "dilbert_antifisheye.png"
)
system(cmd)