Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Most efficient way to crop image to circle (in R)?

Tags:

r

crop

TL;DR: What is the most efficient way to crop a rectangular image to a circle?

Explanation/Background:

I'm working on some code in R that will display Spotify artist images as circles instead of the default rectanges/squares. I couldn't find any packages or commands that crop images in R, especially to a circle, so I wrote my own function, circ, which reads 3-Dimensional (or 4-Dimensional) RGB(A) arrays and crops them to a circle using the parametric equation of a circle to determine the x values for every unique y. Here's my psuedocode:

Given an RGB(A) array:
    Find the center of the image, radius = min(x coord, y coord)
    Pre-crop the image to a square of dimensions 2r x 2r
    For every unique y value:
        Determine the x coordinates on the circle
        Make pixels outside of the circle transparent
    Return the cropped image as an RGBA array

This function is a tremendous improvement over my previous one, which checked the position of every pixel to see if it was inside or outside of the circle, but I still feel like it could be sped up further.

Is there a way I could check maybe half of the y-values instead of all of them, and mirror across the circle? Is there an actual cropping function I could use instead? Any and all help is much appreciated!

Edited to add some copy-paste-run code (thanks @lukeA):

My original cropping method:

circ = function(a){
  # First part of the function finds the radius of the circle and crops the image accordingly
  xc = floor(dim(a[,,1])[2]/2)  # X coordinate of the center
  yc = floor(dim(a[,,1])[1]/2)  # Y coordinate of the center
  r = min(xc, yc) - 1  # Radius is the smaller of the two -1 to avoid reading nonexistent data
  ma = array(data = c(a[,,1][(yc-r):(yc+r),(xc-r):(xc+r)],  # Read in the cropped image
                      a[,,2][(yc-r):(yc+r),(xc-r):(xc+r)],  # Of dimensions 2r x 2r, centered
                      a[,,3][(yc-r):(yc+r),(xc-r):(xc+r)],  # Around (xc, yc)
                      rep(1,length(a[,,1][(yc-r):(yc+r),(xc-r):(xc+r)]))),  # Add fourth alpha layer
             dim = c(length((yc-r):(yc+r)),length((xc-r):(xc+r)),4))

  if(yc > xc) yc = xc else if(xc > yc) xc = yc  # Re-evaluate your center for the cropped image
  xmax = dim(ma[,,1])[2]; ymax = dim(ma[,,1])[1]  # Find maximum x and y values

  # Second part of the function traces circle by the parametric eqn. and makes outside pixels transparent
  for(y in 1:ymax){  # For every y in the cropped image
    theta = asin((y - yc) / r)  # y = yc + r * sin(theta) by parametric equation for a circle
    x = xc + r * cos(theta)  # Then we can find the exact x coordinate using the same formula
    x = which.min(abs(1:xmax - x))  # Find which x in array is closest to exact coordinate
    if(!x - xc == 0 && !xmax - x == 0){  # If you're not at the "corners" of the circle
      ma[,,4][y,c(1:(xmax-x), (x+1):xmax)] = 0  # Make pixels on either side of the circle trans.
    } else if(!xmax - x == 0) ma[,,4][y,] = 0  # This line makes tops/bottoms transparent
  }
  return(ma)
} 

library(jpeg)
a = readJPEG("http://1.bp.blogspot.com/-KYvXCEvK9T4/Uyv8xyDQnTI/AAAAAAAAHFY/swaAHLS-ql0/s1600/pink-smiley-face-balls-laughing-HD-image-for-faacebook-sharing.jpg")
par(bg = "grey"); plot(1:2, type="n")  # Color background to check transparency
rasterImage(circ(a),1,1,2,2)

Modified version (thanks @dww):

dwwcirc = function(a){
  # First part of the function finds the radius of the circle and crops the image accordingly
  xc = floor(dim(a[,,1])[2]/2)  # X coordinate of the center
  yc = floor(dim(a[,,1])[1]/2)  # Y coordinate of the center
  r = min(xc, yc) - 1  # Radius is the smaller of the two -1 to avoid reading nonexistent data
  ma = array(data = c(a[,,1][(yc-r):(yc+r),(xc-r):(xc+r)],  # Read in the cropped image
                      a[,,2][(yc-r):(yc+r),(xc-r):(xc+r)],  # Of dimensions 2r x 2r, centered
                      a[,,3][(yc-r):(yc+r),(xc-r):(xc+r)],  # Around (xc, yc)
                      rep(1,length(a[,,1][(yc-r):(yc+r),(xc-r):(xc+r)]))),  # Add fourth alpha layer
             dim = c(length((yc-r):(yc+r)),length((xc-r):(xc+r)),4))

  if(yc > xc) yc = xc else if(xc > yc) xc = yc  # Re-evaluate your center for the cropped image
  xmax = dim(ma[,,1])[2]; ymax = dim(ma[,,1])[1]  # Find maximum x and y values

  x = rep(1:xmax, ymax)  # Vector containing all x values
  y = rep(1:ymax, each=xmax)  # Value containing all y values
  r2 = r^2
  ma[,,4][which(( (x-xc)^2 + (y-yc)^2 ) > r2)] = 0
  return(ma)
} 

library(jpeg)
a = readJPEG("http://1.bp.blogspot.com/-KYvXCEvK9T4/Uyv8xyDQnTI/AAAAAAAAHFY/swaAHLS-ql0/s1600/pink-smiley-face-balls-laughing-HD-image-for-faacebook-sharing.jpg")
par(bg = "grey"); plot(1:2, type="n")  # Color background to check transparency
rasterImage(dwwcirc(a),1,1,2,2)

Version using magick and plotrix (thanks @lukeA and @hrbrmstr):

library(plotrix)
jpeg(tf <- tempfile(fileext = "jpeg"), 1000, 1000)
par(mar = rep(0,4), yaxs="i", xaxs = "i")
plot(0, type = "n", ylim = c(0, 1), xlim = c(0,1), axes=F, xlab=NA, ylab=NA)
draw.circle(.5,.5,.5,col="black")
dev.off()

library(magick)
img = image_read("http://1.bp.blogspot.com/-KYvXCEvK9T4/Uyv8xyDQnTI/AAAAAAAAHFY/swaAHLS-ql0/s1600/pink-smiley-face-balls-laughing-HD-image-for-faacebook-sharing.jpg")
mask = image_read(tf)
radius = min(c(image_info(img)$width, image_info(img)$height))
mask = image_scale(mask, as.character(radius))

par(bg = "grey"); plot(1:2, type="n")
rasterImage(as.raster(image_composite(image = mask, composite_image = img, operator = "plus")),1,1,2,2)
like image 667
Cass Avatar asked Oct 16 '16 04:10

Cass


1 Answers

I dunno about "efficiency", but I would not reinvent the wheel here. Like suggested in the comments by @hrbrmstr, you may wanna try magick, which gives you all the flexibility you might need:

png(tf <- tempfile(fileext = ".png"), 1000, 1000)
par(mar = rep(0,4), yaxs="i", xaxs="i")
plot(0, type = "n", ylim = c(0,1), xlim=c(0,1), axes=F, xlab=NA, ylab=NA)
plotrix::draw.circle(.5,0.5,.5, col="black")
dev.off()

library(magick)
fn <- "https://www.gravatar.com/avatar/f57aba01c52e5c67696817eb87df84f2?s=328&d=identicon&r=PG&f=1"
img <- image_read(fn)
mask <- image_read(tf)
mask <- image_scale(mask, as.character(image_info(img)$width))

Now

img

enter image description here

mask

enter image description here

image_composite(mask, img, "plus") 

enter image description here

image_composite(mask, img, "minus") 

enter image description here

Some other composite operators:

# https://www.imagemagick.org/Magick++/Enumerations.html#CompositeOperator
ops <- c("over", "in", "out", "atop", "xor", "plus", "minus", "add",  "difference", "multiply")
for (op in ops) {
  print(image_composite(img, mask, op))
  print(op)
  readline()
}
like image 187
lukeA Avatar answered Oct 04 '22 11:10

lukeA