Search code examples
rimage-processingcolorssavepng

Change image pixel colors in R and save image


I have hundreds of small (300x300 pixel) pure black and white PNG images. I want to convert them to different two colors in R and save them again as PNG. (Firstly I want to actually invert them: all black to white and all white to black - but later I will need other colors, e.g. black to red and white to green, etc.) This seems real simple, but whatever I try I run into problems.

For example, the simplest solution seems using as.raster in base R and the png package (at least for reading):

img = readPNG(newfile) # read black (background) and white (figure) image
img <- as.raster(img) 
img_white = img
img_white[img_white == "#000000"] <- 'red' # temporarily convert back to red as placeholder
img_white[img_white == "#FFFFFF"] <- '#000000' # convert white to black
img_white[img_white == "red"] <- '#FFFFFF' # convert originally black to white

(Here by the way I needed a placeholder because the goal color is the same as the other original - but that's beside the point.)

So this works nicely and I can plot it with plot(img_white), but incredibly I find no way of automatically saving the image as file. I tried e.g. writePNG, writeRaster, writeGDAL, but they all give various error messages due to wrong class or wrong format or similar. (I also tried various conversions without success.)

Among others I also tried the imager package, which nicely saves the image after manipulating it, but I cannot find the way to convert a single specified color in the entire image.

All in all, I'm open to any possible solutions as long as it gives a full working code. I don't much care whatever package I need to use, though if possible I'd prefer as simple code as possible and hence as few packages as possible.


SOLUTION:

Based on Allan Cameron's answer, I wrote this function:

change_cols = function(replace_black, replace_white, theimg) {
    r_b = col2rgb(replace_black) / 255
    r_w = col2rgb(replace_white) / 255
    theimg[theimg == 1] <- 2
    for (i in 1:3) {
        theimg[,,i][theimg[,,i] == 0] <- r_b[i]
    }
    for (i in 1:3) {
        theimg[,,i][theimg[,,i] == 2] <- r_w[i]
    }
    return(theimg)
}

Then it's as simple as:

img = readPNG(newfile)
newimg = change_cols("#FF0000", "#00FF00", img)
writePNG(newimg, "fileout.png")

(See also Allan Cameron's function which converts the raster object.)


Solution

  • You need to write the PNG as a numeric array, just as it was when you loaded it. Since you only have black and white images, it shouldn't be a problem to manually swap black and white (they have value black = 0, white = 1).

    You only need to convert it to a raster for plotting:

    library(png)
    newfile = "~/face.png"
    img = readPNG(newfile) # read black (background) and white (figure) image
    img_white = 1-img
    

    Now

    plot(raster::as.raster(img))
    

    enter image description here And

    plot(raster::as.raster(img_white))
    

    enter image description here

    Or if you want to invert a single channel (in this case red):

    img[,,1] <- 1 - img[,,1]
    plot(raster::as.raster(img))
    

    enter image description here


    EDIT

    After further comments from the OP, I thought it was reasonable to take this answer to its conclusion by writing a function that takes a raster object and saves it as a PNG file:

    save_raster_as_PNG <- function(raster_object, path) 
    {
      if(class(raster_object) != "raster") stop("This is not a raster object.")
      dims        <- dim(raster_object)
      red         <- as.numeric(paste0("0x", substr(raster_object, 2 , 3)))/255
      dim(red)    <- rev(dims)
      green       <- as.numeric(paste0("0x", substr(raster_object, 4 , 5)))/255
      dim(green)  <- rev(dims)
      blue        <- as.numeric(paste0("0x", substr(raster_object, 6 , 7)))/255
      dim(blue)   <- rev(dims)
      result      <- numeric(3 * dims[1] * dims[2])
      dim(result) <- c(dims, 3)
      result[,,1] <- t(red)
      result[,,2] <- t(blue)
      result[,,3] <- t(green)
    
      tryCatch(png::writePNG(result, path), error = function(e) stop(e))
      cat("Raster successfully saved to", path.expand(path))
    }
    
    img <- raster::as.raster(img)
    save_raster_as_PNG(img, "~/face3.png")
    # Raster successfully saved to C:/Users/AllanCameron/SO/R/face3.png