Converting image array to RGB to HSL/HSV and back? - r

I read in colored jpg images using readJPEG() from the jpeg package. Now I have my images as three-dimensional arrays (width, height, channels) in R.
I want to convert these image arrays into the HSL or HSV color space, mutate the images and save them as JPGs in the RGB format again. However, as the images are quite large (5000 x 8000), it would be too time consuming to loop through every single cell. I found the package OpenImageRto convert the image to the HSV color space quickly, however, I am confused by large negative values in the "saturation" channel. Also, the package contains no functions to convert the image back.
Is there any package to perform fast conversions from RGB to HSL or HSV (and back)? Or is there any other way to perform the converison quickly?
These are my current attempts for converting into one direction, element-wise:
# load packages
library(jpeg)
library(plotwidgets)
# load image
img <- readJPEG(img_path)
img <- img * 255
# new empty image
img_new <- array(NA, dim = dim(img))
# this takes way too long
for (img_row in 1:dim(img)[1]) {
for (img_col in 1:dim(img)[2]) {
img_new[img_row,img_col,] <- round(rgb2hsl(as.matrix(img[img_row,img_col,])))
}
}
# this takes also way too long
for (img_row in 1:dim(img)[1]) {
img_new[img_row,,] <- t(round(rgb2hsl(t(matrix(img[img_row,,], ncol = 3)))))
}
# this takes also ages
rgb_hsl_fun <- function(x) {
as.numeric(rgb2hsl(matrix(x)))
}
img_hsl <- apply(X = img, MARGIN = c(1,2), FUN = rgb_hsl_fun)

The whole thing is quite simple to do.
Use the colorspace library for this.
Here is my original img.jpg file.
Here is the code.
library(jpeg)
library(colorspace)
#Reading a jpg file
img = readJPEG("img.jpg") * 255
#Row-by-row conversion
for(i in 1:dim(img)[1]){
#Convert to HSV format
hsv = RGB(img[i,,1], img[i,,2], img[i,,3]) |> as("HSV")
#Mutation of H, S, V components
attributes(hsv)$coords[,"H"] = attributes(hsv)$coords[,"H"]/2
attributes(hsv)$coords[,"S"] = attributes(hsv)$coords[,"S"]*.998
attributes(hsv)$coords[,"V"] = attributes(hsv)$coords[,"V"]-1
#Convert to RGB format and save to the current line.
rgb = as(hsv, "RGB")
img[i,,1] = attributes(rgb)$coords[,"R"]
img[i,,2] = attributes(rgb)$coords[,"G"]
img[i,,3] = attributes(rgb)$coords[,"B"]
}
#Save to JPG file
writeJPEG(img / 255, "img_hsv.jpg")
Just note that to get to the individual H, S, V (or R, G, B) components you have to use the coords attribute.
As you can see, my mutation of the components H, S, V was as follows:
H = H / 2
S = S * 0.998
V = V-1
After this mutation, the original file looks like this.
However, if you prefer to carry out the mutation on the HLS palette, it is possible.
#Reading a jpg file
img = readJPEG("img.jpg") * 255
#Row-by-row conversion
for(i in 1:dim(img)[1]){
#Convert to HLS format
hls = RGB(img[i,,1], img[i,,2], img[i,,3]) |> as("HLS")
#Mutation of H, S, V components
attributes(hls)$coords[,"H"] = attributes(hls)$coords[,"H"]/2
attributes(hls)$coords[,"L"] = attributes(hls)$coords[,"L"]/2
attributes(hls)$coords[,"S"] = attributes(hls)$coords[,"S"]/2
#Convert to RGB format and save to the current line.
rgb = as(hls, "RGB")
img[i,,1] = attributes(rgb)$coords[,"R"]
img[i,,2] = attributes(rgb)$coords[,"G"]
img[i,,3] = attributes(rgb)$coords[,"B"]
}
#Save to JPG file
writeJPEG(img / 255, "img_hls.jpg")
Here is the image with H/2, L/2 and S/2 conversion.
Hope this is what you were looking for.

It would be wise to open an issue to the Github repository (in case that there is a quick fix to the error case for the HSV transformation). For the record I'm the author and maintainer of the OpenImageR package.
I took a look once again to the code of the RGB_to_HSV function and as I mention at the top of the function of the Rcpp code the implementation is based on the paper
Analytical Study of Colour Spaces for Plant Pixel Detection, Pankaj Kumar and Stanley J. Miklavcic, 2018, Journal of Imaging (page 3 of 12) or section 2.1.3,
The negative values of the saturation channel were highly probable related to a mistake of the following line,
S(i) = 1.0 - (3.0 * s_val) * (R(i) + G(i) + B(i));
which actually (based on the paper) should have been:
S(i) = 1.0 - (3.0 * s_val) / (R(i) + G(i) + B(i));
(division rather than multiplication of the last term)
I uploaded the updated version to Github and you can install it using
remotes::install_github('mlampros/OpenImageR')
and please report back if it works so that I can upload the new version to CRAN.
The package does not include a transformation from HSV to RGB (from what I understand you want to modify the pixel values and then convert to RGB).

Related

R SLIC image segmentation for 3D image (package OpenImageR)

I have an 3D image (for example a MRI). I want to use the SLIC algorithm for segmentation.
Package OpenImageR provides the function superpixels. In the description (see https://search.r-project.org/CRAN/refmans/OpenImageR/html/superpixels.html) is written, one can give a 3D image as input.
But even for the simple example provided at the link above, it will not work...
library(OpenImageR)
#-------------------
# 3-dimensional data
#-------------------
path = system.file("tmp_images", "slic_im.png", package = "OpenImageR")
im = readImage(path)
dim(im)
# [1] 360 360 4
res = superpixels(input_image = im, method = "slic", superpixel = 200,
compactness = 20, return_slic_data = TRUE)
# Error: In interface_superpixels(input_image, method, superpixel, compactness, :
# The input data has more than 3 dimensions. The dimensions were reduced from 4 to 3!
I can not imagine an array with dim = c(x, y, 3) to store any 3-dimensional image...
My questions:
Most important: IS there a package providing to use SLIC at "real" 3-dimensional images?
For my curiosity: Why does the mentioned function restricts 3D images to dim = c(x, y, 3)?

r how to set x and y ranges in an animation video generated with saveVideo

I make mpeg videos using the code below. r.mean is a spatRaster (the format for rasters in the terra package)
library(terra)
animation::saveVideo(animate(r.mean, n=1, pause = 0, sub = title_animate, col = rev(colorList), videoName_out))
I have included a screenshot of one frame of the animation. There is lots of white space above and below the actual image. I'd like to set the y range from -60 to 90. I've seen one example of use of saveVideo that looks like this with par parameters included in {}
saveVideo({
par(xxx, yy, zzz)},
some other stuff)
But I haven't had any luck making that work.
Here is a minimal, self-contained, reproducible example.
Example data
library(terra)
r <- rast(nlyrs=3)
values(r) <- rep(1:3, ncell(r))
You can use the ani.height and ani.width arguments.
animation::saveGIF(animate(r, n=1), ani.height=200, ani.width=300)

How to compute the volume of a single voxel of nifti medical image?

I have loaded a nifti image file using nibabel tool and I have played with some properties.
But I don’t have idea how to compute the volume (in mm³) of a single voxel.
Here's the answer using NiBabel, as OP asked:
import nibabel as nib
nii = nib.load('t1.nii.gz')
sx, sy, sz = nii.header.get_zooms()
volume = sx * sy * sz
I am not a NiBabel expert, but I can instead recommend the SimpleITK package for Python. I often use it for reading NifTi image files. It has a method GetSpacing() which returns the pixel spacing in mm.
import SimpleITK as sitk
# read image
im = sitk.ReadImage("/path/to/input/image.nii")
# get voxel spacing (for 3-D image)
spacing = im.GetSpacing()
spacing_x = spacing[0]
spacing_y = spacing[1]
spacing_z = spacing[2]
# determine volume of a single voxel
voxel_volume = spacing_x * spacing_y * spacing_z

How can I get the same piece (duplicate code) of an image from many different photos every time?

From 5000 photos of license plates I want to determine which duplicate code these license plates have.
Here are 2 examples of a duplicate code on a license plate.
In the first example the duplicate code is 2 and in the second example the duplicate code is 1.
With the package Magick and Tesseract, see code below, I was able to retrieve the piece of the photo from the first example where the duplicate code is and to read the duplicate code. Only in the second example and other photos is the photo different.
So I am looking for something that can recognize where the duplicate code is and that will read the duplicate code. Note: The duplicate code is always above the 1st indent mark.
Does someone have an idea how to read the duplicate code automatically from 5000 different photos?
library(magick)
library(tesseract)
#Load foto:
foto <- image_read("C:/Users/camie/OneDrive/Documenten/kenteken3.jpg")
#Get piece of photo where duplicate code is retrieved:
foto2 <- image_crop(foto,"10X24-620-170")
#read duplicate code:
cat(ocr(foto3))
Here is an approach based on the package EBImage. ImageMagik is great for image manipulation but I think EBImage may provide more quantitative tools that are useful here. As for all image processing, the quality of input image matters a great deal. The approach suggested here would likely benefit from noise and artifact removal, scaling and possibly cropping.
Also, some licenses seem to have additional symbols in the position of interest that are not numbers. Clearly more pre-processing and filtering are needed for such cases.
Sample image
# Starting from EBImage
if (!require(EBImage)) {
source("http://bioconductor.org/biocLite.R")
biocLite("EBImage")
library(EBImage)
}
# Test images
# setwd(<image directory>)
f1 <- "license1.jpg"
f2 <- "license2.jpg"
# Read image and convert to normalized greyscale
img0 <- readImage(f1)
img <- channel(img0, "grey")
img <- normalize(img)
# plot(img) # insert plot or display commands as desired
# Rudimentary image process for ~300 pixel wide JPEG
xmf <- medianFilter(img, 1)
xgb <- gblur(xmf, 1)
xth <- xgb < otsu(xgb) # Otsu's algorithm to determine best threshold
xto <- opening(xth, makeBrush(3, shape = "diamond"))
A binary (thresholded) image has been produced and cleaned up to identify objects as shown here.
# Create object mask with unique integer for each object
xm <- bwlabel(xto)
# plot(colorLabels(xm)) # optional code to visualize the objects
In addition to the rudimentary image process, some "object processing" can be applied as shown here. Objects along the edge are not going to be of interest so they are removed. Similarly, artifacts that give rise to horizontal (wide) streaks can be removed as well.
# Drop objects touching the edge
nx <- dim(xm)[1]
ny <- dim(xm)[2]
sel <- unique(c(xm[1,], xm[nx,], xm[,1], xm[,ny]))
sel <- sel[sel != 0]
xm <- rmObjects(xm, sel, reenumerate = TRUE)
# Drop exceptionally wide objects (33% of image width)
major <- computeFeatures.moment(xm)[,"m.majoraxis"]
sel <- which(major > nx/3)
xm <- rmObjects(xm, sel, reenumerate = TRUE)
The following logic identifies the center of mass for each object with the computeFeatures.moment function of EBImage. It seems that the main symbols will be along a horizontal line while the candidate object will be above that line (lower y-value in EBImage Image object). An alternative approach would be to find objects stacked on one another, i.e., objects with similar x-values.
For the examples I explored, one standard deviation away from the median y-value for the center of mass appears to be sufficient to identify candidate object. This is used to determine the limits shown below. Of course, this logic should be adjusted as dictated by the actual data.
# Determine center of mass for remaining objects
M <- computeFeatures.moment(xm)
x <- M[,1]
y <- M[,2]
# Show suggested limit on image (y coordinates are inverted)
plot(img)
limit <- median(y) - sd(y)
abline(h = limit, col = "red")
# Show centers of mass on original image
ok <- y < limit
points(x[!ok], y[!ok], pch = 16, col = "blue")
points(x[ok], y[ok], pch = 16, col = "red")
The image shows the segmented objects after having discarded objects along the edge. Red shows the candidate, blue shows the non-candidates.
Because some licenses have two symbols above the dash, the following code selects the leftmost of possible candidates, expands the object mask and returns a rectangular crop of the image that can be passed to ocr().
# Accept leftmost (first) of candidate objects
left <- min(x[which(ok)])
sel <- which(x == left)
# Enlarge object mask and extract the candidate image
xm <- dilate(xm, makeBrush(7, "disc"))
ix <- range(apply(xm, 2, function(v) which(v == sel)))
iy <- range(apply(xm, 1, function(v) which(v == sel)))
xx <- ix[1]:ix[2]
yy <- iy[1]:iy[2]
# "Return" selected portion of image
ans <- img[xx, yy] # this is what can be passed to tesseract
plot(ans, interpolate = FALSE)
Here is the unscaled and extracted candidate image from example 1:
Another sample image
The same code applied to this example gives the following:
With a few more checks for errors and for illogical conditions, the code could be assembled into single function and applied to the list of 5000 files! But of course that assumes they are properly formatted, etc. etc.
What with the existance of multiple layouts for Dutch license plates, I'm not sure if you just can hardcode a method to extract a duplication value. Also you don't mention if every image you have always has the same quality and/or orientation/scale/skew/etc.
You could in theory apply a Convolutional Neural Network that categorizes license plates in a several categories. (0 for n/a, 1 for 1, 2 for 2, etc.) However I am not familiar with related packages in R, so I won't be able to point you to some.

Converting raster (tiff) image to a pixel image in R - problems when converting spatial polygon into owin object class

I am not an R expert, but i use it for all kinds of image processing. Now I am trying to apply Gaussian blur smoothing (spatstat package) on my satellite S-2 image. Original type of my image is Raster (Raster layer) tiff, actually a subtract image from two Sentinel-2 bands (green and blue). To apply blur on this kind of image I have to first convert it to a pixel image. I've tried doing this following few other questions (like this one Converting a raster object to an im object in R), but i did not succed. I have tried few possibilities, like converting raster image into matrix and than to pixel image, but this does not work, because the image is than too large (although I use small, croped area of the whole Sentinel-2 image).
So, my function in brief looks something like that:
blue <- raster("S2A_OPER_MSI_T33TWH_B02.tif")
green <- raster("S2A_OPER_MSI_T33TWH_B03.tif")
subt <- function(r1, r2) {
return(r2-r1)
}
out_sub1 <- (blue, green, fun = subt)
I tried to apply blur directly on a Raster image, but i soon realized its not working on raster data:
gauss_sub1 <- blur(out_sub1, sigma = 5)
#Error: is.im(x) is not TRUE
So, I try to convert my image into a pixel one
out_sub11 <- as.im(X = "out_sub1")
Error in as.im.function(X, W, ..., dimyx = dimyx, na.replace = na.replace): A window W is required
Therefore I try to define a window following my raster extent
e <- out_sub1#extent
sp_w <- as(e, "SpatialPolygons")
W <- as(sp_w, "owin")
Error in as(SP.win, "owin") : no method or default for coercing “SpatialPolygons” to “owin”
Can anyone tell me what am I doing wrong or how can I convert spatial polygon into owin object class, so I can further process blur command?
And can please someone explain me what difference there is between raster image and a pixel image in R?
You can apply a filter using raster library:
library(raster)
r <- blue - green
# 3 by 3 mean filter
r_mf <- focal(r, w=matrix(1/9,nrow=3,ncol=3))
# gaussian filter
gf <- focalWeight(r, 2, "Gauss")
r_gf <- focal(r, w=gf)

Resources