Thursday, January 3, 2013

Picture to LEGO!

If you have never been to Legoland you have missed a lot.
I was in Billund a couple of summers ago, on my way to a vacation in Copenhagen, and spent the night at the hotel inside Legoland Park.

Here is a picture of me beside Mona Lisa, Lego version, featured in the hotel hall.



And here (actually in a few paragraphs) is R code to translate a picture into a Lego painting.

A few notes before the actual code.

  1. I have heavily drawn from codes posted at is.R(), particularly from this post and this one.
  2. I limited the colors for the resulting Lego painting to the ones available on the Pick a Brick Lego Shop. I created one png file for each color: here is the zip containing them all (I could have specified RGB values directly in R.)
  3. The code only uses 1x2 and 1x1 plate pieces (or 2x2 and 1x2 if you want your Lego painting ticker.) That was to avoid over-complication. However I did not neglect the basics of Lego construction, so identical pieces are not on top of one another.
  4. It requires some time to run, so try it on small pictures first.
  5. It does not always produce satisfying results.
  6. Several improvements can be done on the code, but right now I'm happy with it. Should you make some changes, let everybody know.
# package loading
library(png)
library(ReadImages)
library(reshape)
 
# image to be rendered as a Lego painting
sourceImage <- "C:/yourFolder/monalisa.jpg"
 
# folder containing color files (provided in the zip above)
colFolder <- "C:/yourFolder/LegoColorFiles"
 
# files with the lego colors 
pngURLs <- list.files(colFolder, full.names=T)
pngCOLs <- list.files(colFolder, full.names=F)
 
# loading the color files into a list
pngList <- list()
for(ii in 1:length(pngURLs)){
  tempName <- paste("Pic", ii)
  tempPNG <- readPNG(pngURLs[ii]) 
  pngList[[tempName]] <- tempPNG 
}
 
# calculating the mean RGB values for each color file
# should actually be useless 
meanRGB <- t(sapply(pngList, function(ll){
  apply(ll[, , -4], 3, mean)[1:3]
}))
 
# reading image
readImage <- read.jpeg(sourceImage)
 
# processing image
longImage <- melt(readImage)
rgbImage <- reshape(longImage, timevar = "X3",
                    idvar = c("X1", "X2"), direction = "wide")
rgbImage[, 1:2] <- rgbImage[, 2:1]
rgbImage[,2] <- dim(readImage)[1] - rgbImage[,2] + 1
  #plot(rgbImage[, 1:2], col = rgb(rgbImage[, 3:5]), pch = 19, asp = 1)
 
# "Snap" colors to a smaller number of nearby colors
nNearbyCol <- 3
rgbImage[, c(3:5)] <- round(rgbImage[, c(3:5)] * nNearbyCol) / nNearbyCol
plot(rgbImage[, 1:2], col = rgb(rgbImage[, 3:5]), pch = 19, asp = 1)
 
 
# set image size 
pictureWd <- dim(readImage)[2]
pictureHt <- dim(readImage)[1]
canvasHt <- 600 # change this to improve LEGO resolution (it's the height of the Lego painting in millimeters)
canvasWd <- round(canvasHt / pictureHt * pictureWd)
 
# calculating the number of Lego pieces needed
piecesWd <- round(10 * canvasWd / 158)
piecesHt <- round(10 * canvasHt / 32)
 
 
# odd/even function
is.odd <- function(x) x %% 2 == 1
 
# empty plot
plot(c(0,piecesWd), c(0,piecesHt), type = "n", asp = 32/158)
 
# draw bricks and keep count of them
pieces <- data.frame(size=numeric(0), color=character(0))
for(ht in 1:piecesHt){
  cat(paste("doing line:", ht, "of", piecesHt, "\n"))
  flush.console()
  if(is.odd(ht)){
    for(wd in 1:piecesWd){
      #relative brick area
      brick <- c((wd-1)/piecesWd, (ht-1)/piecesHt, wd/piecesWd, ht/piecesHt)
      #area on the picture
      area <- ceiling(c(brick[1] * pictureWd, brick[2] * pictureHt, brick[3] * pictureWd, brick[4] * pictureHt))
      #picture portion
      picPort <- subset(rgbImage, X1 %in% ((area[1]:area[3])+1) & X2 %in% ((area[2]:area[4])+1))
      #portion color
      portCol <- c(mean(picPort$value.1),mean(picPort$value.2),mean(picPort$value.3))
      #closest Lego color
      nearestPic <- which.min(rowSums(sweep(meanRGB, 2, portCol)^2))
      #keep note of the piece
      if(length(nearestPic)) piece <- data.frame(size = 2, color = pngCOLs[nearestPic])
      pieces <- rbind(pieces, piece)
      #plot
      rect(wd-1, ht-1, wd, ht, col=rgb(meanRGB[nearestPic,1]
                          , meanRGB[nearestPic,2]
                          , meanRGB[nearestPic,3])
           , border = "grey70"
           )
     }
   } else {
     for(wd in seq(.5, piecesWd+1, 1)){
 
       #relative brick area
       brick <- c((wd-1)/piecesWd, (ht-1)/piecesHt, (wd+1)/piecesWd, (ht+1)/piecesHt)
       #area on the picture
       area <- ceiling(c(brick[1] * pictureWd, brick[2] * pictureHt, brick[3] * pictureWd, brick[4] * pictureHt))
       #picture portion
       picPort <- subset(rgbImage, X1 %in% ((area[1]:area[3])+1) & X2 %in% ((area[2]:area[4])+1))
       #portion color
       portCol <- c(mean(picPort$value.1),mean(picPort$value.2),mean(picPort$value.3))
       #closest Lego color
       nearestPic <- which.min(rowSums(sweep(meanRGB, 2, portCol)^2))
       #keep note of the piece
       piece <- data.frame(size = ifelse(wd %in% c(.5, piecesWd+.5), 1, 2), color = pngCOLs[nearestPic])
       pieces <- rbind(pieces, piece)
 
       #plot
       rect(max(wd-1,0), ht-1, min(wd,piecesWd), ht, col=rgb(meanRGB[nearestPic,1]
                                        , meanRGB[nearestPic,2]
                                        , meanRGB[nearestPic,3])
            , border = "grey70"
       )
 
     }
   }
}
 
# how many bricks of each size/color do you need?
table(pieces$color, pieces$size)
Created by Pretty R at inside-R.org

And now, let's test it with Mona Lisa herself!

Here is the original picture I used.



Here is the result for a 600mm height (click to enlarge):

Not the worst thing in the world, but I would have hoped for better. It requires over 5,000 pieces and, at the current price at  Pick a Brick, would cost over 350€.

Doubling the height (and thus the Lego resolution) produces a better result—but at a very high cost.
Again, click to enlarge.

Enjoy!

PS: Let me know if you come up with nice looking Lego paintings—especially if you actually build them!

No comments:

Post a Comment