To test the image analysis capabilities of the ‘imager’ R package, I decided to download an image of a corneal disc and experiment with it.
# download and load appropriate packages
list.of.packages <- c("dplyr", "ggplot2", "imager")
new.packages <- list.of.packages[!(list.of.packages %in% installed.packages()[,"Package"])]
if(length(new.packages)) install.packages(new.packages)
# Loading relevant packages
lapply(list.of.packages, library, character.only = TRUE)# Thesholding (normal)
par(mfrow = c(3,1))
threshold(im,"80%") %>% plot
threshold(im,"85%") %>% plot
threshold(im,"90%") %>% plot# Thesholding (inverted)
threshold(im_neg,"20%") %>% plot
threshold(im_neg,"15%") %>% plot
threshold(im_neg,"10%") %>% plot# Removing any luminance trends with R's built-in lm function so that a fixed threshold works better.
# The first step is to convert the image data to a data.frame, then fit a model:
df <- as.data.frame(im)
m <- lm(value ~ x + y,data=df) #linear trend
summary(m)##
## Call:
## lm(formula = value ~ x + y, data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.15632 -0.07907 -0.03028 0.04578 0.86355
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 1.585e-01 4.764e-04 332.73 <2e-16 ***
## x -6.827e-06 5.350e-07 -12.76 <2e-16 ***
## y -3.845e-04 1.679e-06 -229.03 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.1181 on 431841 degrees of freedom
## Multiple R-squared: 0.1086, Adjusted R-squared: 0.1086
## F-statistic: 2.631e+04 on 2 and 431841 DF, p-value: < 2.2e-16
# Morphological operations (based on a threshold of 85%)
im.t <- threshold(im.f,"85%")
plot(im.t)
# Finding average pixel intensity of pixels above threshold
# Convert threshold to pixset
px <- as.pixset(1-im.t)
layout(t(1:1))# taking the pixel set from a threshold image and converting it into a data frame to select pixels from the original picture.
px_to_image <- as.cimg(px)
plot(px_to_image)## x y value
## 1 1 1 1
## 2 2 1 1
## 3 3 1 1
## 4 4 1 1
## 5 5 1 1
## 6 6 1 1
##
## 0 1
## 66311 365533
# If the pixset is 1, then assign the value of the original image to zero, otherwise, leave the value alone
values_filtered <- NA
for(i in 1:width(df)){
if(px.image_to_df[i,3] == 1){
values_filtered[i] = 0
}else{
values_filtered[i] = df[i,3]
}
}
# creating a dataframe with only the values above threshold
df_filtered <- mutate(df, values_filtered) %>% select(-value) %>% rename(value = values_filtered)
head(df_filtered)## x y value
## 1 1 1 0
## 2 2 1 0
## 3 3 1 0
## 4 4 1 0
## 5 5 1 0
## 6 6 1 0
im_filtered <- as.cimg(df_filtered)
# plots demontrating that the below threshold pixels were removed
par(mfrow = c(2,1))
plot(im)
plot(im_filtered)# Average pixel intensity for pixels above threshold
average_above_threshold <- sum(df_filtered$value)/sum(ifelse(df_filtered$value!=0,1,0))
average_above_threshold ## [1] 0.339059
# Getting rid of noise using Gaussian filtering
im_edge <- grayscale(im) %>% isoblur(2) #2 pix. blur
# Computing the image gradient:
gr <- imgradient(im_edge,"xy")
plot(gr,layout="row")#Going along the (normalised) gradient
#Xc(im) is an image containing the x coordinates of the image
nX <- Xc(im) + gr$x/mag
nY <- Yc(im) + gr$y/mag
#nX and nY are not integer values, so we can't use them directly as indices.
#We can use interpolation, though:
val.fwd <- interp(mag,data.frame(x=as.vector(nX),y=as.vector(nY)))
# We can naturally also go backwards along the gradient:
nX <- Xc(im) - gr$x/mag
nY <- Yc(im) - gr$y/mag
val.bwd <- interp(mag,data.frame(x=as.vector(nX),y=as.vector(nY)))
# Given these two sets of values, non-maxima suppression comes down to
# killing all values that aren’t larger than their two neighbours along the flow:
throw <- (mag < val.bwd) | (mag < val.fwd)
mag[throw] <- 0
plot(mag)# Hysteresis
# The ultimate goal is to classify each pixel as edge/non-edge,
# which means picking a threshold. Instead of picking a single threshold,
# Canny suggested picking a double threshold t1,t2, with t1<t, where points with
# magnitudes above t2 are called “strong edges” points with magnitudes
# above t1 are called “weak edges”
# strong threshold
t2 <- quantile(mag,.96)
# weak threshold
t1 <- quantile(mag,.90)
layout(t(1:2))
strong <- mag>t2
plot(strong,main="Initial set of strong edges")
weak <- mag %inr% c(t1,t2)
plot(weak,main="Initial set of weak edges")layout(t(1:1))
# Hysteresis rescues such weak edges progressively. The usual implementation is iterative.
overlap <- grow(strong,3) & weak
strong.new <- strong | overlap
plot(strong.new,main="New set of strong edges")