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.
- I have heavily drawn from codes posted at is.R(), particularly from this post and this one.
- 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.)
- 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.
- It requires some time to run, so try it on small pictures first.
- It does not always produce satisfying results.
- 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)
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