2 Copyright (c) 2019 Intel Corporation
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
8 http://www.apache.org/licenses/LICENSE-2.0
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
21 from ..representation import (
24 FacialLandmarksAnnotation,
25 FacialLandmarksPrediction,
26 SuperResolutionAnnotation,
27 SuperResolutionPrediction,
32 from .metric import PerImageEvaluationMetric, BaseMetricConfig
33 from ..config import BaseField, NumberField, BoolField, ConfigError, StringField
34 from ..utils import string_to_tuple, finalize_metric_result
37 class BaseIntervalRegressionMetricConfig(BaseMetricConfig):
38 intervals = BaseField(optional=True)
39 start = NumberField(optional=True)
40 end = NumberField(optional=True)
41 step = NumberField(optional=True)
42 ignore_values_not_in_interval = BoolField(optional=True)
45 class BaseRegressionMetric(PerImageEvaluationMetric):
46 annotation_types = (RegressionAnnotation, )
47 prediction_types = (RegressionPrediction, )
49 def __init__(self, value_differ, *args, **kwargs):
50 super().__init__(*args, **kwargs)
51 self.value_differ = value_differ
54 self.meta.update({'names': ['mean', 'std'], 'scale': 1, 'postfix': ' ', 'calculate_mean': False})
57 def update(self, annotation, prediction):
58 self.magnitude.append(self.value_differ(annotation.value, prediction.value))
60 def evaluate(self, annotations, predictions):
61 return np.mean(self.magnitude), np.std(self.magnitude)
64 class BaseRegressionOnIntervals(PerImageEvaluationMetric):
65 annotation_types = (RegressionAnnotation, )
66 prediction_types = (RegressionPrediction, )
67 _config_validator_type = BaseIntervalRegressionMetricConfig
69 def __init__(self, value_differ, *args, **kwargs):
70 super().__init__(*args, **kwargs)
71 self.value_differ = value_differ
74 self.meta.update({'scale': 1, 'postfix': ' ', 'calculate_mean': False})
75 self.ignore_out_of_range = self.config.get('ignore_values_not_in_interval', True)
77 self.intervals = self.config.get('intervals')
78 if not self.intervals:
79 stop = self.config.get('end')
81 raise ConfigError('intervals or start-step-end of interval should be specified for metric')
83 start = self.config.get('start', 0.0)
84 step = self.config.get('step', 1.0)
85 self.intervals = np.arange(start, stop + step, step)
87 if not isinstance(self.intervals, (list, np.ndarray)):
88 self.intervals = string_to_tuple(self.intervals)
90 self.intervals = np.unique(self.intervals)
91 self.magnitude = [[] for _ in range(len(self.intervals) + 1)]
93 self.meta['names'] = ([])
94 if not self.ignore_out_of_range:
95 self.meta['names'] = (['mean: < ' + str(self.intervals[0]), 'std: < ' + str(self.intervals[0])])
97 for index in range(len(self.intervals) - 1):
98 self.meta['names'].append('mean: <= ' + str(self.intervals[index]) + ' < ' + str(self.intervals[index + 1]))
99 self.meta['names'].append('std: <= ' + str(self.intervals[index]) + ' < ' + str(self.intervals[index + 1]))
101 if not self.ignore_out_of_range:
102 self.meta['names'].append('mean: > ' + str(self.intervals[-1]))
103 self.meta['names'].append('std: > ' + str(self.intervals[-1]))
105 def update(self, annotation, prediction):
106 index = find_interval(annotation.value, self.intervals)
107 self.magnitude[index].append(self.value_differ(annotation.value, prediction.value))
109 def evaluate(self, annotations, predictions):
110 if self.ignore_out_of_range:
111 self.magnitude = self.magnitude[1:-1]
113 result = [[np.mean(values), np.std(values)] if values else [np.nan, np.nan] for values in self.magnitude]
114 result, self.meta['names'] = finalize_metric_result(np.reshape(result, -1), self.meta['names'])
117 warnings.warn("No values in given interval")
123 class MeanAbsoluteError(BaseRegressionMetric):
126 def __init__(self, *args, **kwargs):
127 super().__init__(mae_differ, *args, **kwargs)
130 class MeanSquaredError(BaseRegressionMetric):
133 def __init__(self, *args, **kwargs):
134 super().__init__(mse_differ, *args, **kwargs)
137 class RootMeanSquaredError(BaseRegressionMetric):
138 __provider__ = 'rmse'
140 def __init__(self, *args, **kwargs):
141 super().__init__(mse_differ, *args, **kwargs)
143 def evaluate(self, annotations, predictions):
144 return np.sqrt(np.mean(self.magnitude)), np.sqrt(np.std(self.magnitude))
147 class MeanAbsoluteErrorOnInterval(BaseRegressionOnIntervals):
148 __provider__ = 'mae_on_interval'
150 def __init__(self, *args, **kwargs):
151 super().__init__(mae_differ, *args, **kwargs)
154 class MeanSquaredErrorOnInterval(BaseRegressionOnIntervals):
155 __provider__ = 'mse_on_interval'
157 def __init__(self, *args, **kwargs):
158 super().__init__(mse_differ, *args, **kwargs)
161 class RootMeanSquaredErrorOnInterval(BaseRegressionOnIntervals):
162 __provider__ = 'rmse_on_interval'
164 def __init__(self, *args, **kwargs):
165 super().__init__(mse_differ, *args, **kwargs)
167 def evaluate(self, annotations, predictions):
168 if self.ignore_out_of_range:
169 self.magnitude = self.magnitude[1:-1]
172 for values in self.magnitude:
173 error = [np.sqrt(np.mean(values)), np.sqrt(np.std(values))] if values else [np.nan, np.nan]
176 result, self.meta['names'] = finalize_metric_result(np.reshape(result, -1), self.meta['names'])
179 warnings.warn("No values in given interval")
185 class FacialLandmarksPerPointNormedError(PerImageEvaluationMetric):
186 __provider__ = 'per_point_normed_error'
188 annotation_types = (FacialLandmarksAnnotation, )
189 prediction_types = (FacialLandmarksPrediction, )
192 self.meta.update({'scale': 1, 'postfix': ' ', 'calculate_mean': True, 'data_format': '{:.4f}'})
195 def update(self, annotation, prediction):
196 result = point_regression_differ(
197 annotation.x_values, annotation.y_values, prediction.x_values, prediction.y_values
199 result /= np.maximum(annotation.interocular_distance, np.finfo(np.float64).eps)
200 self.magnitude.append(result)
202 def evaluate(self, annotations, predictions):
203 num_points = np.shape(self.magnitude)[1]
204 point_result_name_pattern = 'point_{}_normed_error'
205 self.meta['names'] = [point_result_name_pattern.format(point_id) for point_id in range(num_points)]
206 per_point_rmse = np.mean(self.magnitude, axis=1)
207 per_point_rmse, self.meta['names'] = finalize_metric_result(per_point_rmse, self.meta['names'])
209 return per_point_rmse
212 class NormedErrorMetricConfig(BaseMetricConfig):
213 calculate_std = BoolField(optional=True)
214 percentile = NumberField(optional=True, floats=False, min_value=0, max_value=100)
217 class FacialLandmarksNormedError(PerImageEvaluationMetric):
218 __provider__ = 'normed_error'
220 annotation_types = (FacialLandmarksAnnotation, )
221 prediction_types = (FacialLandmarksPrediction, )
222 _config_validator_type = NormedErrorMetricConfig
225 self.calculate_std = self.config.get('calculate_std', False)
226 self.percentile = self.config.get('percentile')
230 'calculate_mean': not self.calculate_std or not self.percentile,
231 'data_format': '{:.4f}',
236 def update(self, annotation, prediction):
237 per_point_result = point_regression_differ(
238 annotation.x_values, annotation.y_values, prediction.x_values, prediction.y_values
240 avg_result = np.sum(per_point_result) / len(per_point_result)
241 avg_result /= np.maximum(annotation.interocular_distance, np.finfo(np.float64).eps)
242 self.magnitude.append(avg_result)
244 def evaluate(self, annotations, predictions):
245 result = [np.mean(self.magnitude)]
247 if self.calculate_std:
248 result.append(np.std(self.magnitude))
249 self.meta['names'].append('std')
252 sorted_magnitude = np.sort(self.magnitude)
253 index = len(self.magnitude) / 100 * self.percentile
254 result.append(sorted_magnitude[int(index)])
255 self.meta['names'].append('{}th percentile'.format(self.percentile))
260 def calculate_distance(x_coords, y_coords, selected_points):
261 first_point = [x_coords[selected_points[0]], y_coords[selected_points[0]]]
262 second_point = [x_coords[selected_points[1]], y_coords[selected_points[1]]]
263 return np.linalg.norm(np.subtract(first_point, second_point))
266 def mae_differ(annotation_val, prediction_val):
267 return np.abs(annotation_val - prediction_val)
270 def mse_differ(annotation_val, prediction_val):
271 return (annotation_val - prediction_val)**2
274 def find_interval(value, intervals):
275 for index, point in enumerate(intervals):
279 return len(intervals)
282 def point_regression_differ(annotation_val_x, annotation_val_y, prediction_val_x, prediction_val_y):
283 loss = np.subtract(list(zip(annotation_val_x, annotation_val_y)), list(zip(prediction_val_x, prediction_val_y)))
284 return np.linalg.norm(loss, 2, axis=1)
287 class PeakSignalToNoiseRatio(BaseRegressionMetric):
288 __provider__ = 'psnr'
290 annotation_types = (SuperResolutionAnnotation, )
291 prediction_types = (SuperResolutionPrediction, )
293 def __init__(self, *args, **kwargs):
294 super().__init__(self._psnr_differ, *args, **kwargs)
296 def validate_config(self):
297 class _PSNRConfig(BaseMetricConfig):
298 scale_border = NumberField(optional=True, min_value=0)
299 color_order = StringField(optional=True, choices=['BGR', 'RGB'])
301 config_validator = _PSNRConfig('psnr', on_extra_argument=_PSNRConfig.ERROR_ON_EXTRA_ARGUMENT)
302 config_validator.validate(self.config)
306 self.scale_border = self.config.get('scale_border', 4)
307 color_order = self.config.get('color_order', 'RGB')
312 self.meta['postfix'] = 'Db'
313 self.channel_order = channel_order[color_order]
315 def _psnr_differ(self, annotation_image, prediction_image):
316 prediction = np.asarray(prediction_image).astype(np.float)
317 ground_truth = np.asarray(annotation_image).astype(np.float)
319 height, width = prediction.shape[:2]
320 prediction = prediction[
321 self.scale_border:height - self.scale_border,
322 self.scale_border:width - self.scale_border
324 ground_truth = ground_truth[
325 self.scale_border:height - self.scale_border,
326 self.scale_border:width - self.scale_border
328 image_difference = (prediction - ground_truth) / 255. # rgb color space
330 r_channel_diff = image_difference[:, :, self.channel_order[0]]
331 g_channel_diff = image_difference[:, :, self.channel_order[1]]
332 b_channel_diff = image_difference[:, :, self.channel_order[2]]
334 channels_diff = (r_channel_diff * 65.738 + g_channel_diff * 129.057 + b_channel_diff * 25.064) / 256
336 mse = np.mean(channels_diff ** 2)
340 return -10 * math.log10(mse)
343 def angle_differ(gt_gaze_vector, predicted_gaze_vector):
345 gt_gaze_vector.dot(predicted_gaze_vector) / np.linalg.norm(gt_gaze_vector)
346 / np.linalg.norm(predicted_gaze_vector)
350 class AngleError(BaseRegressionMetric):
351 __provider__ = 'angle_error'
353 annotation_types = (GazeVectorAnnotation, )
354 prediction_types = (GazeVectorPrediction, )
356 def __init__(self, *args, **kwargs):
357 super().__init__(angle_differ, *args, **kwargs)