Commit 25391245 authored by Sushil BHATTACHARJEE's avatar Sushil BHATTACHARJEE
Browse files

Merge branch 'cleanup' into 'master'

Code clean-up

See merge request !1
parents 208a18f3 dbdd3b4f
Pipeline #8443 passed with stages
in 11 minutes and 44 seconds
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
from .galbally_iqm_features import compute_quality_features from .galbally_iqm_features import compute_quality_features
from .msu_iqa_features import compute_msu_iqa_features from .msu_iqa_features import compute_msu_iqa_features
def get_config(): def get_config():
""" """
Returns a string containing the configuration information. Returns a string containing the configuration information.
...@@ -13,7 +14,5 @@ def get_config(): ...@@ -13,7 +14,5 @@ def get_config():
return bob.extension.get_config(__name__) return bob.extension.get_config(__name__)
# gets sphinx autodoc done right - don't remove it # gets sphinx autodoc done right - don't remove it
__all__ = [_ for _ in dir() if not _.startswith('_')] __all__ = [_ for _ in dir() if not _.startswith('_')]
...@@ -8,48 +8,51 @@ Created on 25 Sep 2015 ...@@ -8,48 +8,51 @@ Created on 25 Sep 2015
''' '''
#import re
#import os
import math
import numpy as np import numpy as np
import scipy as sp
import scipy.signal as ssg import scipy.signal as ssg
import scipy.ndimage.filters as snf import scipy.ndimage.filters as snf
import bob.ip.base import bob.ip.base
""" """
Main function to be called, to extract a set of image quality-features computed for the input image compute_quality_features is the main function to be called, to extract a set of
:param image: 2d numpy array. Should contain input image of size [M,N] (i.e. M rows x N cols). image quality-features computed for the input image
:return featSet: a tuple of float-scalars, each representing one image-quality measure. :param image: 2d numpy array. Should contain input image of size [M,N] (i.e. M
rows x N cols).
:return featSet: a tuple of float-scalars, each representing one image-quality
measure.
""" """
def compute_quality_features(image): def compute_quality_features(image):
"""Extract a set of image quality-features computed for the input image. """Extract a set of image quality-features computed for the input image.
Parameters: Parameters:
image (:py:class:`numpy.ndarray`): A ``uint8`` array with 2 or 3 dimensions, representing the input image of shape [M,N] (M rows x N cols). image (:py:class:`numpy.ndarray`): A ``uint8`` array with 2 or 3
If 2D, image should contain a gray-image of shape [M,N]. dimensions, representing the input image of shape [M,N] (M rows x N
If 3D, image should have a shape [3,M,N], and should contain an RGB-image. cols). If 2D, image should contain a gray-image of shape [M,N]. If 3D,
image should have a shape [3,M,N], and should contain an RGB-image.
Returns: Returns:
featSet (:py:class:`numpy.ndarray`): a 1D numpy array of 18 float32 scalars, each representing one image-quality measure. featSet (:py:class:`numpy.ndarray`): a 1D numpy array of 18 float32
This function returns a subset of the image-quality features (for face anti-spoofing) that have been scalars, each representing one image-quality measure. This function
described by Galbally et al. in their paper: returns a subset of the image-quality features (for face anti-spoofing)
"Image Quality Assessment for Fake Biometric Detection: Application to Iris, Fingerprint, and Face Recognition", that have been described by Galbally et al. in their paper:
IEEE Trans. on Image Processing Vol 23(2), 2014. "Image Quality Assessment for Fake Biometric Detection: Application to
Iris, Fingerprint, and Face Recognition", IEEE Trans. on Image
Processing Vol 23(2), 2014.
""" """
gray_image = None gray_image = None
#print("shape of input image:") #print("shape of input image:")
#print(image.shape) # print(image.shape)
if len(image.shape) == 3: if len(image.shape) == 3:
if(image.shape[0]==3): if(image.shape[0] == 3):
gray_image = matlab_rgb2gray(image) #compute gray-level image for input color-frame # compute gray-level image for input color-frame
gray_image = matlab_rgb2gray(image)
# print(gray_image.shape) # print(gray_image.shape)
else: else:
print('error. Wrong kind of input image') print('error. Wrong kind of input image')
...@@ -60,176 +63,229 @@ def compute_quality_features(image): ...@@ -60,176 +63,229 @@ def compute_quality_features(image):
else: else:
print('error -- wrong kind of input') print('error -- wrong kind of input')
if gray_image is not None: if gray_image is not None:
gwin = gauss_2d((3,3), 0.5) # set up the smoothing-filter gwin = gauss_2d((3, 3), 0.5) # set up the smoothing-filter
# print("computing degraded version of image") # print("computing degraded version of image")
smoothed = ssg.convolve2d(gray_image, gwin, boundary='symm', mode='same') smoothed = ssg.convolve2d(
gray_image, gwin, boundary='symm', mode='same')
""" """
Some of the image-quality measures computed here require a reference image. Some of the image-quality measures computed here require a reference
For these measures, we use the input-image itself as a reference-image, and we compute image. For these measures, we use the input-image itself as a
the quality-measure of a smoothed version of the input-image. The assumption in this reference-image, and we compute the quality-measure of a smoothed
approach is that smoothing degrades a spoof-image more than it does a genuine image version of the input-image. The assumption in this approach is that
(see Galbally's paper referenced above). smoothing degrades a spoof-image more than it does a genuine image (see
Galbally's paper referenced above).
""" """
# print("computing galbally quality features") # print("computing galbally quality features")
featSet = image_quality_measures(gray_image, smoothed) featSet = image_quality_measures(gray_image, smoothed)
return featSet return featSet
else: else:
return None return None
""" """
actually computes various measures of similarity between the two input images, but also returns some descriptors of the reference-image that are independent of any other image. actually computes various measures of similarity between the two input images,
Returns a tuple of 18 values, each of which is a float-scalar. but also returns some descriptors of the reference-image that are independent
The quality measures computed in this function correspond to the Image-quality features discussed in Galbally et al., 2014. of any other image. Returns a tuple of 18 values, each of which is a float-
scalar. The quality measures computed in this function correspond to the Image-
quality features discussed in Galbally et al., 2014.
""" """
def image_quality_measures(refImage, testImage): def image_quality_measures(refImage, testImage):
"""Compute image-quality measures for testImage and return a tuple of quality-measures. """Compute image-quality measures for testImage and return a tuple of
Some of the quality-measures require a reference-image, but others are 'no-reference' measures. quality-measures. Some of the quality-measures require a reference-
:input refImage: 2d numpy array. Should represent input 8-bit gray-level image of size [M,N]. image, but others are 'no-reference' measures.
:input testImage: 2d numpy array. Should represent input 8-bit gray-level image of size [M,N].. :input refImage: 2d numpy array. Should represent input 8-bit gray-level
:return a tuple of 18 values, each of which is a float-scalar. image of size [M,N].
The quality measures computed in this function correspond to the Image-quality features discussed in Galbally et al., 2014. :input testImage: 2d numpy array. Should represent input 8-bit gray-
level image of size [M,N]..
:return a tuple of 18 values, each of which is a float-scalar. The
quality measures computed in this function correspond to the Image-
quality features discussed in Galbally et al., 2014.
""" """
assert len(refImage.shape)==2, "refImage should be a 2D array" assert len(refImage.shape) == 2, "refImage should be a 2D array"
assert len(testImage.shape)==2, "testImage should be a 2D array" assert len(testImage.shape) == 2, "testImage should be a 2D array"
assert (refImage.shape[0] == testImage.shape[0]), "The two images should have the same width" assert (refImage.shape[0] == testImage.shape[0]
assert (refImage.shape[1] == testImage.shape[1]), "The two images should have the same height" ), "The two images should have the same width"
assert (refImage.shape[1] == testImage.shape[1]
diffImg = refImage.astype(np.float) - testImage.astype(np.float) ), "The two images should have the same height"
diffImg = refImage.astype(np.float) - testImage.astype(np.float)
diffSq = np.square(diffImg) diffSq = np.square(diffImg)
sumDiffSq = np.sum(diffSq) sumDiffSq = np.sum(diffSq)
absDiffImg = np.absolute(diffImg) absDiffImg = np.absolute(diffImg)
refSq = np.square(refImage.astype(np.float)) refSq = np.square(refImage.astype(np.float))
sumRefSq = np.sum(refSq) sumRefSq = np.sum(refSq)
numPx = refImage.shape[0]*refImage.shape[1] #number of pixels in each image # number of pixels in each image
maxPxVal = 255.0; numPx = refImage.shape[0] * refImage.shape[1]
maxPxVal = 255.0
#1 MSE
mse00 = float(sumDiffSq)/float(numPx) # 1 MSE
mse00 = float(sumDiffSq) / float(numPx)
#2 PSNR
# 2 PSNR
psnr01 = np.inf psnr01 = np.inf
if mse00>0: if mse00 > 0:
psnr01 = 10.0*np.log10(maxPxVal*maxPxVal/mse00) psnr01 = 10.0 * np.log10(maxPxVal * maxPxVal / mse00)
#3 AD: Average difference # 3 AD: Average difference
ad02 = float(np.sum(diffImg))/float(numPx) ad02 = float(np.sum(diffImg)) / float(numPx)
#4 SC: structural content # 4 SC: structural content
testSq = np.square(testImage.astype(np.float)) testSq = np.square(testImage.astype(np.float))
sumTestSq = np.sum(testSq) sumTestSq = np.sum(testSq)
sc03=np.inf sc03 = np.inf
if sumTestSq>0: if sumTestSq > 0:
sc03 = float(sumRefSq)/float(sumTestSq) sc03 = float(sumRefSq) / float(sumTestSq)
#5 NK: normalized cross-correlation # 5 NK: normalized cross-correlation
imgProd = refImage * testImage # element-wise product imgProd = refImage * testImage # element-wise product
nk04 = float(np.sum(imgProd))/float(sumRefSq) nk04 = float(np.sum(imgProd)) / float(sumRefSq)
#6 MD: Maximum difference # 6 MD: Maximum difference
md05 = float(np.amax(absDiffImg)) md05 = float(np.amax(absDiffImg))
#7 LMSE: Laplacian MSE # 7 LMSE: Laplacian MSE scipy implementation of laplacian is different from
#scipy implementation of laplacian is different from Matlab's version, especially at the image-borders # Matlab's version, especially at the image-borders To significant
# To significant differences between scipy...laplace and Matlab's del2() are: # differences between scipy...laplace and Matlab's del2() are:
# a. Matlab del2() divides the convolution result by 4, so the ratio (scipy.laplace() result)/(del2()-result) is 4 # a. Matlab del2() divides the convolution result by 4, so the ratio
# b. Matlab does a different kind of processing at the boundaries, so the results at the boundaries are different in the 2 calls. # (scipy.laplace() result)/(del2()-result) is 4
#In Galbally's Matlab code, there is a factor of 4, which I have dropped (no difference in result), # b. Matlab does a different kind of processing at the boundaries, so
#because this is implicit in scipy.ndimage.filters.laplace() # the results at the boundaries are different in the 2 calls.
op = snf.laplace(refImage, mode='reflect') #mode can be 'wrap', 'reflect', 'nearest', 'mirror', or ['constant' with a specified value] # In Galbally's Matlab code, there is a factor of 4, which I have dropped
# (no difference in result),
# because this is implicit in scipy.ndimage.filters.laplace()
# mode can be 'wrap', 'reflect', 'nearest', 'mirror', or ['constant' with
# a specified value]
op = snf.laplace(refImage, mode='reflect')
opSq = np.square(op) opSq = np.square(op)
sum_opSq = np.sum(opSq) sum_opSq = np.sum(opSq)
tmp1 = (op - (snf.laplace(testImage, mode='reflect'))) tmp1 = (op - (snf.laplace(testImage, mode='reflect')))
num_op = np.square(tmp1) num_op = np.square(tmp1)
lmse06 = float(np.sum(num_op))/float(sum_opSq) lmse06 = float(np.sum(num_op)) / float(sum_opSq)
#8 NAE: normalized abs. error # 8 NAE: normalized abs. error
sumRef = np.sum(np.absolute(refImage)) sumRef = np.sum(np.absolute(refImage))
nae07 = float(np.sum(absDiffImg))/float(sumRef) nae07 = float(np.sum(absDiffImg)) / float(sumRef)
#9 SNRv: SNR in db # 9 SNRv: SNR in db
snrv08 = 10.0*np.log10(float(sumRefSq)/float(sumDiffSq)) snrv08 = 10.0 * np.log10(float(sumRefSq) / float(sumDiffSq))
#10 RAMDv: R-averaged max diff (r=10) # 10 RAMDv: R-averaged max diff (r=10)
#implementation below is same as what Galbally does in Matlab # implementation below is same as what Galbally does in Matlab
r=10 r = 10
sorted = np.sort(diffImg.flatten())[::-1] #the [::-1] flips the sorted vector, so that it is in descending order # the [::-1] flips the sorted vector, so that it is in descending order
sorted = np.sort(diffImg.flatten())[::-1]
topsum = np.sum(sorted[0:r]) topsum = np.sum(sorted[0:r])
ramdv09 = np.sqrt(float(topsum)/float(r)) ramdv09 = np.sqrt(float(topsum) / float(r))
#11,12: MAS: Mean Angle Similarity, MAMS: Mean Angle-Magnitude Similarity # 11,12: MAS: Mean Angle Similarity, MAMS: Mean Angle-Magnitude Similarity
mas10, mams11 = angle_similarity(refImage, testImage, diffImg) mas10, mams11 = angle_similarity(refImage, testImage, diffImg)
fftRef = np.fft.fft2(refImage) fftRef = np.fft.fft2(refImage)
# fftTest = np.fft.fft2(testImage) # fftTest = np.fft.fft2(testImage)
#13, 14: SME: spectral magnitude error; SPE: spectral phase error # 13, 14: SME: spectral magnitude error; SPE: spectral phase error
sme12, spe13 = spectral_similarity(refImage, testImage) #spectralSimilarity(fftRef, fftTest, numPx) # spectralSimilarity(fftRef, fftTest, numPx)
sme12, spe13 = spectral_similarity(refImage, testImage)
#15 TED: Total edge difference
ted14 = edge_similarity(refImage, testImage) # 15 TED: Total edge difference
# ted14 = edge_similarity(refImage, testImage)
#16 TCD: Total corner difference
tcd15 = corner_similarity(refImage, testImage) # 16 TCD: Total corner difference
# tcd15 = corner_similarity(refImage, testImage)
#17, 18: GME: gradient-magnitude error; GPE: gradient phase error
# 17, 18: GME: gradient-magnitude error; GPE: gradient phase error
gme16, gpe17 = gradient_similarity(refImage, testImage) gme16, gpe17 = gradient_similarity(refImage, testImage)
#19 SSIM # 19 SSIM
ssim18, _ = ssim(refImage, testImage) ssim18, _ = ssim(refImage, testImage)
#20 VIF # 20 VIF
vif19 = vif(refImage, testImage) vif19 = vif(refImage, testImage)
#21,22,23,24,25: RRED, BIQI, JQI, NIQE: these parameters are not computed here. # 21,22,23,24,25: RRED, BIQI, JQI, NIQE: these parameters are not computed
# here.
#26 HLFI: high-low frequency index (implemented as done by Galbally in Matlab).
hlfi25=high_low_freq_index(fftRef, refImage.shape[1]) # 26 HLFI: high-low frequency index (implemented as done by Galbally in
# Matlab).
return np.asarray((mse00, psnr01, ad02, sc03, nk04, md05, lmse06, nae07, snrv08, ramdv09, mas10, mams11, sme12, gme16, gpe17, ssim18, vif19, hlfi25), dtype=np.float32) hlfi25 = high_low_freq_index(fftRef, refImage.shape[1])
return np.asarray(
(mse00,
psnr01,
ad02,
sc03,
nk04,
md05,
lmse06,
nae07,
snrv08,
ramdv09,
mas10,
mams11,
sme12,
gme16,
gpe17,
ssim18,
vif19,
hlfi25),
dtype=np.float32)
""" """
Matlab-like RGB to gray... Matlab-like RGB to gray...
""" """
def matlab_rgb2gray(rgbImage):
'''converts color rgbImage to gray to produce exactly the same result as Matlab would.
def matlab_rgb2gray(rgbImage):
'''converts color rgbImage to gray to produce exactly the same result as
Matlab would.
Inputs: Inputs:
rgbimage: numpy array of shape [3, height, width] rgbimage: numpy array of shape [3, height, width]
Return: Return:
numpy array of shape [height, width] containing a gray-image with floating-point pixel values, in the range[(16.0/255) .. (235.0/255)] numpy array of shape [height, width] containing a gray-image with floating-
point pixel values, in the range[(16.0/255) .. (235.0/255)]
''' '''
#g1 = 0.299*rgbFrame[0,:,:] + 0.587*rgbFrame[1,:,:] + 0.114*rgbFrame[2,:,:] #standard coeffs CCIR601 # g1 = 0.299*rgbFrame[0,:,:] + 0.587*rgbFrame[1,:,:] +
#this is how it's done in matlab... # 0.114*rgbFrame[2,:,:] #standard coeffs CCIR601
# this is how it's done in matlab...
rgbImage = rgbImage / 255.0 rgbImage = rgbImage / 255.0
C0 = 65.481/255.0 C0 = 65.481 / 255.0
C1 = 128.553/255.0 C1 = 128.553 / 255.0
C2 = 24.966/255.0 C2 = 24.966 / 255.0
scaleMin = 16.0/255.0 scaleMin = 16.0 / 255.0
#scaleMax = 235.0/255.0 # scaleMax = 235.0/255.0
gray = scaleMin + (C0*rgbImage[0,:,:] + C1*rgbImage[1,:,:] + C2*rgbImage[2,:,:]) gray = scaleMin + \
(C0 * rgbImage[0, :, :] + C1 * rgbImage[1, :, :] +
C2 * rgbImage[2, :, :])
return gray return gray
""" """
SSIM: Structural Similarity index between two gray-level images. The dynamic range is assumed to be 0..255. SSIM: Structural Similarity index between two gray-level images. The dynamic
Ref:Z. Wang, A.C. Bovik, H.R. Sheikh and E.P. Simoncelli: range is assumed to be 0..255.
Ref:Z. Wang, A.C. Bovik, H.R. Sheikh and E.P. Simoncelli:
"Image Quality Assessment: From error measurement to Structural Similarity" "Image Quality Assessment: From error measurement to Structural Similarity"
IEEE Trans. on Image Processing, 13(1), 01/2004 IEEE Trans. on Image Processing, 13(1), 01/2004
@param refImage: 2D numpy array (reference image) @param refImage: 2D numpy array (reference image)
@param testImage: 2D numpy array (test image) @param testImage: 2D numpy array (test image)
Both input images should have the same dimensions. This is assumed, and not verified in this function Both input images should have the same dimensions. This is assumed, and not
@return ssim: float-scalar. The mean structural similarity between the 2 input images. verified in this function
@return ssim_map: the SSIM index map of the test image (this map is smaller than the test image). @return ssim: float-scalar. The mean structural similarity between the 2
input images.
@return ssim_map: the SSIM index map of the test image (this map is smaller
than the test image).
""" """
def ssim(refImage, testImage): def ssim(refImage, testImage):
"""Compute and return SSIM between two images. """Compute and return SSIM between two images.
...@@ -241,565 +297,593 @@ def ssim(refImage, testImage): ...@@ -241,565 +297,593 @@ def ssim(refImage, testImage):
Returns: Returns:
Returns ssim and ssim_map Returns ssim and ssim_map
ssim: float-scalar. The mean structural similarity between the 2 input images. ssim: float-scalar. The mean structural similarity between the 2 input
ssim_map: the SSIM index map of the test image (this map is smaller than the test image). images.
ssim_map: the SSIM index map of the test image (this map is smaller than
the test image).
""" """
M=refImage.shape[0] M = refImage.shape[0]
N=refImage.shape[1] N = refImage.shape[1]
winSz=11 #window size for gaussian filter winSz = 11 # window size for gaussian filter
winSgm = 1.5 # sigma for gaussian filter winSgm = 1.5 # sigma for gaussian filter
#input image should be at least 11x11 in size. # input image should be at least 11x11 in size.
if(M<winSz) or (N<winSz): if(M < winSz) or (N < winSz):
ssim_index = -np.inf ssim_index = -np.inf
ssim_map = -np.inf ssim_map = -np.inf
return ssim_index, ssim_map return ssim_index, ssim_map
#construct the gaussian filter # construct the gaussian filter
gwin = gauss_2d((winSz, winSz), winSgm) gwin = gauss_2d((winSz, winSz), winSgm)
K1 = 0.01 # constants taken from the initial matlab implementation provided by Bovik's lab. # constants taken from the initial matlab implementation provided by
# Bovik's lab.
K1 = 0.01
K2 = 0.03 K2 = 0.03
L = 255 #dynamic range. L = 255 # dynamic range.
C1 = (K1*L)*(K1*L) C1 = (K1 * L) * (K1 * L)
C2 = (K2*L)*(K2*L) C2 = (K2 * L) * (K2 * L)
#refImage=refImage.astype(np.float) # refImage=refImage.astype(np.float)
#testImage=testImage.astype(np.float) # testImage=testImage.astype(np.float)
#ssg is scipy.signal # ssg is scipy.signal
mu1 = ssg.convolve2d(refImage, gwin, mode='valid') mu1 = ssg.convolve2d(refImage, gwin, mode='valid')
mu2 = ssg.convolve2d(testImage, gwin, mode='valid') mu2 = ssg.convolve2d(testImage, gwin, mode='valid')
mu1Sq = mu1*mu1 mu1Sq = mu1 * mu1
mu2Sq = mu2*mu2 mu2Sq = mu2 * mu2
mu1_mu2 = mu1*mu2 mu1_mu2 = mu1 * mu2
sigma1_sq = ssg.convolve2d((refImage*refImage), gwin, mode='valid') - mu1Sq sigma1_sq = ssg.convolve2d(
sigma2_sq = ssg.convolve2d((testImage*testImage), gwin, mode='valid') - mu1Sq (refImage * refImage),
sigma12 = ssg.convolve2d((refImage*testImage), gwin, mode='valid') - mu1_mu2 gwin,
mode='valid') - mu1Sq
assert (C1>0 and C2 > 0), "Conditions for computing ssim with this code are not met. Set the Ks and L to values > 0." sigma2_sq = ssg.convolve2d(
num1 = (2.0*mu1_mu2 + C1) *(2.0*sigma12 + C2) (testImage * testImage),
den1 = (mu1Sq + mu2Sq+C1)*(sigma1_sq + sigma2_sq +C2) gwin,
ssim_map = num1/den1 mode='valid') - mu1Sq
sigma12 = ssg.convolve2d(
(refImage * testImage),
gwin,
mode='valid') - mu1_mu2
assert (C1 > 0 and C2 > 0), "Conditions for computing ssim with this "
"code are not met. Set the Ks and L to values > 0."
num1 = (2.0 * mu1_mu2 + C1) * (2.0 * sigma12 + C2)
den1 = (mu1Sq + mu2Sq + C1) * (sigma1_sq + sigma2_sq + C2)
ssim_map = num1 / den1
ssim = np.average(ssim_map) ssim = np.average(ssim_map)
return ssim, ssim_map return ssim, ssim_map