Skip to content

Commit f0ebc7a

Browse files
authored
Merge pull request #13 from up42/metrics_review
Metrics review
2 parents 051e127 + a782584 commit f0ebc7a

File tree

4 files changed

+81
-56
lines changed

4 files changed

+81
-56
lines changed

README.md

+8-10
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,14 @@
22

33
Implementation of eight evaluation metrics to access the similarity between two images. The eight metrics are as follows:
44

5-
<i><a href="https://en.wikipedia.org/wiki/Root-mean-square_deviation">Root mean square error (RMSE)</a></i>,
6-
<i><a href="https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio">Peak signal-to-noise ratio (PSNR)</a></i>,
7-
<i><a href="https://en.wikipedia.org/wiki/Structural_similarity">Structural Similarity Index (SSIM)</a></i>,
8-
<i><a href="https://www.tandfonline.com/doi/full/10.1080/22797254.2019.1628617">Information theoretic-based Statistic Similarity Measure (ISSM)</a></i>,
9-
<i><a href="https://www4.comp.polyu.edu.hk/~cslzhang/IQA/TIP_IQA_FSIM.pdf">Feature-based similarity index (FSIM)</a></i>,
10-
<i><a href="https://www.sciencedirect.com/science/article/abs/pii/S0924271618302636">Signal to reconstruction error ratio (SRE)</a></i>,
11-
<i><a href="https://ntrs.nasa.gov/citations/19940012238">Spectral angle mapper (SAM)</a></i>, and
12-
<i><a href="https://www.researchgate.net/publication/3342733_A_Universal_Image_Quality_Index">Universal image quality index (UIQ)</a></i>
13-
14-
5+
* <i><a href="https://en.wikipedia.org/wiki/Root-mean-square_deviation">Root mean square error (RMSE)</a></i>,
6+
* <i><a href="https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio">Peak signal-to-noise ratio (PSNR)</a></i>,
7+
* <i><a href="https://en.wikipedia.org/wiki/Structural_similarity">Structural Similarity Index (SSIM)</a></i>,
8+
* <i><a href="https://www4.comp.polyu.edu.hk/~cslzhang/IQA/TIP_IQA_FSIM.pdf">Feature-based similarity index (FSIM)</a></i>,
9+
* <i><a href="https://www.tandfonline.com/doi/full/10.1080/22797254.2019.1628617">Information theoretic-based Statistic Similarity Measure (ISSM)</a></i>,
10+
* <i><a href="https://www.sciencedirect.com/science/article/abs/pii/S0924271618302636">Signal to reconstruction error ratio (SRE)</a></i>,
11+
* <i><a href="https://ntrs.nasa.gov/citations/19940012238">Spectral angle mapper (SAM)</a></i>, and
12+
* <i><a href="https://ece.uwaterloo.ca/~z70wang/publications/quality_2c.pdf">Universal image quality index (UIQ)</a></i>
1513

1614
## Instructions
1715

image_similarity_measures/evaluate.py

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ def evaluation(org_img_path, pred_img_path, mode, metric, write_to_file):
7878
metric_dict[metric] = {f"{metric.upper()}": out_value}
7979
write_final_dict(metric, metric_dict)
8080

81+
8182
def main():
8283
parser = argparse.ArgumentParser(description="Evaluates an Image Super Resolution Model")
8384
parser.add_argument("--org_img_path", type=str, help="Path to original input image")

image_similarity_measures/quality_metrics.py

+71-45
Original file line numberDiff line numberDiff line change
@@ -17,37 +17,40 @@ def _assert_image_shapes_equal(org_img: np.ndarray, pred_img: np.ndarray, metric
1717
assert org_img.shape == pred_img.shape, msg
1818

1919

20-
def rmse(org_img: np.ndarray, pred_img: np.ndarray, data_range=4096):
20+
def rmse(org_img: np.ndarray, pred_img: np.ndarray, max_p=4095) -> float:
2121
"""
2222
Root Mean Squared Error
23-
"""
2423
24+
Calculated individually for all bands, then averaged
25+
"""
2526
_assert_image_shapes_equal(org_img, pred_img, "RMSE")
26-
rmse_final = []
27+
28+
rmse_bands = []
2729
for i in range(org_img.shape[2]):
28-
m = np.mean(np.square((org_img[:, :, i] - pred_img[:, :, i]) / data_range))
30+
m = np.mean(np.square((org_img[:, :, i] - pred_img[:, :, i]) / max_p))
2931
s = np.sqrt(m)
30-
rmse_final.append(s)
31-
return np.mean(rmse_final)
32+
rmse_bands.append(s)
3233

34+
return np.mean(rmse_bands)
3335

34-
def psnr(org_img: np.ndarray, pred_img: np.ndarray, data_range=4096):
36+
37+
def psnr(org_img: np.ndarray, pred_img: np.ndarray, max_p=4095) -> float:
3538
"""
36-
Peek Signal to Noise Ratio, a measure similar to mean squared error.
39+
Peek Signal to Noise Ratio, implemented as mean squared error converted to dB.
3740
3841
It can be calculated as
3942
PSNR = 20 * log10(MAXp) - 10 * log10(MSE)
4043
41-
When using 12-bit imagery MaxP is 4096, for 8-bit imagery 256
44+
When using 12-bit imagery MaxP is 4095, for 8-bit imagery 255. For floating point imagery using values between
45+
0 and 1 (e.g. unscaled reflectance) the first logarithmic term can be dropped as it becomes 0
4246
"""
4347
_assert_image_shapes_equal(org_img, pred_img, "PSNR")
4448

45-
r = []
49+
mse_bands = []
4650
for i in range(org_img.shape[2]):
47-
val = 20 * np.log10(data_range) - 10. * np.log10(np.mean(np.square(org_img[:, :, i] - pred_img[:, :, i])))
48-
r.append(val)
51+
mse_bands.append(np.mean(np.square(org_img[:, :, i] - pred_img[:, :, i])))
4952

50-
return np.mean(r)
53+
return 20 * np.log10(max_p) - 10. * np.log10(np.mean(mse_bands))
5154

5255

5356
def _similarity_measure(x, y, constant):
@@ -70,7 +73,7 @@ def _gradient_magnitude(img: np.ndarray, img_depth):
7073
return np.sqrt(scharrx ** 2 + scharry ** 2)
7174

7275

73-
def fsim(org_img: np.ndarray, pred_img: np.ndarray):
76+
def fsim(org_img: np.ndarray, pred_img: np.ndarray, T1=0.85, T2=160) -> float:
7477
"""
7578
Feature-based similarity index, based on phase congruency (PC) and image gradient magnitude (GM)
7679
@@ -80,11 +83,22 @@ def fsim(org_img: np.ndarray, pred_img: np.ndarray):
8083
8184
There are also alternatives to implement GM, the FSIM authors suggest to use the Scharr
8285
operation which is implemented in OpenCV.
86+
87+
Note that FSIM is defined in the original papers for grayscale as well as for RGB images. Our use cases
88+
are mostly multi-band images e.g. RGB + NIR. To accommodate for this fact, we compute FSIM for each individual
89+
band and then take the average.
90+
91+
Note also that T1 and T2 are constants depending on the dynamic range of PC/GM values. In theory this parameters
92+
would benefit from fine-tuning based on the used data, we use the values found in the original paper as defaults.
93+
94+
Args:
95+
org_img -- numpy array containing the original image
96+
pred_img -- predicted image
97+
T1 -- constant based on the dynamic range of PC values
98+
T2 -- constant based on the dynamic range of GM values
8399
"""
84100
_assert_image_shapes_equal(org_img, pred_img, "FSIM")
85101

86-
T1 = 0.85 # a constant based on the dynamic range PC
87-
T2 = 160 # a constant based on the dynamic range GM
88102
alpha = beta = 1 # parameters used to adjust the relative importance of PC and GM features
89103
fsim_list = []
90104
for i in range(org_img.shape[2]):
@@ -145,9 +159,12 @@ def _edge_c(x, y):
145159
return numerator / denominator
146160

147161

148-
def issm(org_img: np.ndarray, pred_img: np.ndarray):
162+
def issm(org_img: np.ndarray, pred_img: np.ndarray) -> float:
149163
"""
150164
Information theoretic-based Statistic Similarity Measure
165+
166+
Note that the term e which is added to both the numerator as well as the denominator is not properly
167+
introduced in the paper. We assume the authers refer to the Euler number.
151168
"""
152169
_assert_image_shapes_equal(org_img, pred_img, "ISSM")
153170

@@ -167,13 +184,13 @@ def issm(org_img: np.ndarray, pred_img: np.ndarray):
167184
return np.nan_to_num(numerator / denominator)
168185

169186

170-
def ssim(org_img: np.ndarray, pred_img: np.ndarray, data_range=4096):
187+
def ssim(org_img: np.ndarray, pred_img: np.ndarray, max_p=4095) -> float:
171188
"""
172189
Structural SIMularity index
173190
"""
174191
_assert_image_shapes_equal(org_img, pred_img, "SSIM")
175192

176-
return structural_similarity(org_img, pred_img, data_range=data_range, multichannel=True)
193+
return structural_similarity(org_img, pred_img, data_range=max_p, multichannel=True)
177194

178195

179196
def sliding_window(image, stepSize, windowSize):
@@ -184,49 +201,58 @@ def sliding_window(image, stepSize, windowSize):
184201
yield (x, y, image[y:y + windowSize[1], x:x + windowSize[0]])
185202

186203

187-
def uiq(org_img: np.ndarray, pred_img: np.ndarray):
204+
def uiq(org_img: np.ndarray, pred_img: np.ndarray, step_size=1, window_size=8):
188205
"""
189206
Universal Image Quality index
190207
"""
191208
# TODO: Apply optimization, right now it is very slow
192209
_assert_image_shapes_equal(org_img, pred_img, "UIQ")
193210
q_all = []
194-
for (x, y, window_org), (x, y, window_pred) in zip(sliding_window(org_img, stepSize=1, windowSize=(8, 8)),
195-
sliding_window(pred_img, stepSize=1, windowSize=(8, 8))):
211+
for (x, y, window_org), (x, y, window_pred) in zip(sliding_window(org_img, stepSize=step_size,
212+
windowSize=(window_size, window_size)),
213+
sliding_window(pred_img, stepSize=step_size,
214+
windowSize=(window_size, window_size))):
196215
# if the window does not meet our desired window size, ignore it
197216
if window_org.shape[0] != 8 or window_org.shape[1] != 8:
198217
continue
199-
org_img_mean = np.mean(org_img)
200-
pred_img_mean = np.mean(pred_img)
201-
org_img_variance = np.var(org_img)
202-
pred_img_variance = np.var(pred_img)
203-
org_pred_img_variance = np.mean((window_org - org_img_mean) * (window_pred - pred_img_mean))
204218

205-
numerator = 4 * org_pred_img_variance * org_img_mean * pred_img_mean
206-
denominator = (org_img_variance + pred_img_variance) * (org_img_mean**2 + pred_img_mean**2)
219+
for i in range(org_img.shape[2]):
220+
org_band = window_org[:, :, i]
221+
pred_band = window_pred[:, :, i]
222+
org_band_mean = np.mean(org_band)
223+
pred_band_mean = np.mean(pred_band)
224+
org_band_variance = np.var(org_band)
225+
pred_band_variance = np.var(pred_band)
226+
org_pred_band_variance = np.mean((org_band - org_band_mean) * (pred_band - pred_band_mean))
207227

208-
if denominator != 0.0:
209-
q = numerator / denominator
210-
q_all.append(q)
228+
numerator = 4 * org_pred_band_variance * org_band_mean * pred_band_mean
229+
denominator = (org_band_variance + pred_band_variance) * (org_band_mean**2 + pred_band_mean**2)
230+
231+
if denominator != 0.0:
232+
q = numerator / denominator
233+
q_all.append(q)
211234

212235
return np.mean(q_all)
213236

214237

215-
def sam(org_img: np.ndarray, pred_img: np.ndarray):
238+
def sam(org_img: np.ndarray, pred_img: np.ndarray, convert_to_degree=True):
216239
"""
217-
calculates spectral angle mapper
240+
Spectral Angle Mapper which defines the spectral similarity between two spectra
218241
"""
242+
219243
_assert_image_shapes_equal(org_img, pred_img, "SAM")
220-
org_img = org_img.reshape((org_img.shape[0] * org_img.shape[1], org_img.shape[2]))
221-
pred_img = pred_img.reshape((pred_img.shape[0] * pred_img.shape[1], pred_img.shape[2]))
222244

223-
N = org_img.shape[1]
224-
sam_angles = np.zeros(N)
225-
for i in range(org_img.shape[1]):
226-
val = np.clip(np.dot(org_img[:, i], pred_img[:, i]) / (np.linalg.norm(org_img[:, i]) * np.linalg.norm(pred_img[:, i])), -1, 1)
227-
sam_angles[i] = np.arccos(val)
245+
# Spectral angles are first computed for each pair of pixels
246+
numerator = np.sum(np.multiply(pred_img, org_img), axis=2)
247+
denominator = np.linalg.norm(org_img, axis=2) * np.linalg.norm(pred_img, axis=2)
248+
val = np.clip(numerator / denominator, -1, 1)
249+
sam_angles = np.arccos(val)
250+
if convert_to_degree:
251+
sam_angles = sam_angles * 180.0 / np.pi
228252

229-
return np.mean(sam_angles * 180.0 / np.pi)
253+
# The original paper states that SAM values are expressed as radians, while e.g. Lanares
254+
# et al. (2018) use degrees. We therefore made this configurable, with degree the default
255+
return np.mean(sam_angles)
230256

231257

232258
def sre(org_img: np.ndarray, pred_img: np.ndarray):
@@ -238,8 +264,8 @@ def sre(org_img: np.ndarray, pred_img: np.ndarray):
238264
sre_final = []
239265
for i in range(org_img.shape[2]):
240266
numerator = np.square(np.mean(org_img[:, :, i]))
241-
denominator = ((np.linalg.norm(org_img[:, :, i] - pred_img[:, :, i]))) /\
267+
denominator = (np.linalg.norm(org_img[:, :, i] - pred_img[:, :, i])) /\
242268
(org_img.shape[0] * org_img.shape[1])
243-
sre_final.append(10 * np.log10(numerator/denominator))
269+
sre_final.append(numerator/denominator)
244270

245-
return np.mean(sre_final)
271+
return 10 * np.log10(np.mean(sre_final))

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setuptools.setup(
77
name="image-similarity-measures",
8-
version="0.2.2",
8+
version="0.3.0",
99
author="UP42",
1010
author_email="[email protected]",
1111
description="Evaluation metrics to assess the similarity between two images.",

0 commit comments

Comments
 (0)