@@ -17,37 +17,40 @@ def _assert_image_shapes_equal(org_img: np.ndarray, pred_img: np.ndarray, metric
17
17
assert org_img .shape == pred_img .shape , msg
18
18
19
19
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 :
21
21
"""
22
22
Root Mean Squared Error
23
- """
24
23
24
+ Calculated individually for all bands, then averaged
25
+ """
25
26
_assert_image_shapes_equal (org_img , pred_img , "RMSE" )
26
- rmse_final = []
27
+
28
+ rmse_bands = []
27
29
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 ))
29
31
s = np .sqrt (m )
30
- rmse_final .append (s )
31
- return np .mean (rmse_final )
32
+ rmse_bands .append (s )
32
33
34
+ return np .mean (rmse_bands )
33
35
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 :
35
38
"""
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 .
37
40
38
41
It can be calculated as
39
42
PSNR = 20 * log10(MAXp) - 10 * log10(MSE)
40
43
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
42
46
"""
43
47
_assert_image_shapes_equal (org_img , pred_img , "PSNR" )
44
48
45
- r = []
49
+ mse_bands = []
46
50
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 ])))
49
52
50
- return np .mean (r )
53
+ return 20 * np .log10 ( max_p ) - 10. * np . log10 ( np . mean (mse_bands ) )
51
54
52
55
53
56
def _similarity_measure (x , y , constant ):
@@ -70,7 +73,7 @@ def _gradient_magnitude(img: np.ndarray, img_depth):
70
73
return np .sqrt (scharrx ** 2 + scharry ** 2 )
71
74
72
75
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 :
74
77
"""
75
78
Feature-based similarity index, based on phase congruency (PC) and image gradient magnitude (GM)
76
79
@@ -80,11 +83,22 @@ def fsim(org_img: np.ndarray, pred_img: np.ndarray):
80
83
81
84
There are also alternatives to implement GM, the FSIM authors suggest to use the Scharr
82
85
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
83
99
"""
84
100
_assert_image_shapes_equal (org_img , pred_img , "FSIM" )
85
101
86
- T1 = 0.85 # a constant based on the dynamic range PC
87
- T2 = 160 # a constant based on the dynamic range GM
88
102
alpha = beta = 1 # parameters used to adjust the relative importance of PC and GM features
89
103
fsim_list = []
90
104
for i in range (org_img .shape [2 ]):
@@ -145,9 +159,12 @@ def _edge_c(x, y):
145
159
return numerator / denominator
146
160
147
161
148
- def issm (org_img : np .ndarray , pred_img : np .ndarray ):
162
+ def issm (org_img : np .ndarray , pred_img : np .ndarray ) -> float :
149
163
"""
150
164
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.
151
168
"""
152
169
_assert_image_shapes_equal (org_img , pred_img , "ISSM" )
153
170
@@ -167,13 +184,13 @@ def issm(org_img: np.ndarray, pred_img: np.ndarray):
167
184
return np .nan_to_num (numerator / denominator )
168
185
169
186
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 :
171
188
"""
172
189
Structural SIMularity index
173
190
"""
174
191
_assert_image_shapes_equal (org_img , pred_img , "SSIM" )
175
192
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 )
177
194
178
195
179
196
def sliding_window (image , stepSize , windowSize ):
@@ -184,49 +201,58 @@ def sliding_window(image, stepSize, windowSize):
184
201
yield (x , y , image [y :y + windowSize [1 ], x :x + windowSize [0 ]])
185
202
186
203
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 ):
188
205
"""
189
206
Universal Image Quality index
190
207
"""
191
208
# TODO: Apply optimization, right now it is very slow
192
209
_assert_image_shapes_equal (org_img , pred_img , "UIQ" )
193
210
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 ))):
196
215
# if the window does not meet our desired window size, ignore it
197
216
if window_org .shape [0 ] != 8 or window_org .shape [1 ] != 8 :
198
217
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 ))
204
218
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 ))
207
227
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 )
211
234
212
235
return np .mean (q_all )
213
236
214
237
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 ):
216
239
"""
217
- calculates spectral angle mapper
240
+ Spectral Angle Mapper which defines the spectral similarity between two spectra
218
241
"""
242
+
219
243
_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 ]))
222
244
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
228
252
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 )
230
256
231
257
232
258
def sre (org_img : np .ndarray , pred_img : np .ndarray ):
@@ -238,8 +264,8 @@ def sre(org_img: np.ndarray, pred_img: np.ndarray):
238
264
sre_final = []
239
265
for i in range (org_img .shape [2 ]):
240
266
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 ])) / \
242
268
(org_img .shape [0 ] * org_img .shape [1 ])
243
- sre_final .append (10 * np . log10 ( numerator / denominator ) )
269
+ sre_final .append (numerator / denominator )
244
270
245
- return np .mean (sre_final )
271
+ return 10 * np .log10 ( np . mean (sre_final ) )
0 commit comments