Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add script to evaluate face recognition by LFW #72

Merged
merged 7 commits into from
Jul 29, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions models/face_recognition_sface/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ Note:
- [face_recognition_sface_2021sep.onnx](./face_recognition_sface_2021sep.onnx) is converted from the model from https://github.com/zhongyy/SFace thanks to [Chengrui Wang](https://github.com/crywang).
- Support 5-landmark warpping for now (2021sep)

Results of accuracy evaluation with [tools/eval](../../tools/eval).

| Models | Accuracy |
|-------------|----------|
| SFace | 0.8205 |
| SFace quant | 0.8150 |

\*: 'quant' stands for 'quantized'.


## Demo

***NOTE***: This demo uses [../face_detection_yunet](../face_detection_yunet) as face detector, which supports 5-landmark detection for now (2021sep).
Expand All @@ -17,6 +27,7 @@ Run the following command to try the demo:
python demo.py --input1 /path/to/image1 --input2 /path/to/image2
```


## License

All files in this directory are licensed under [Apache 2.0 License](./LICENSE).
Expand Down
27 changes: 27 additions & 0 deletions tools/eval/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Make sure you have the following packages installed:

```shell
pip install tqdm
pip install scikit-learn
```

Generally speaking, evaluation can be done with the following command:
Expand All @@ -14,6 +15,7 @@ python eval.py -m model_name -d dataset_name -dr dataset_root_dir

Supported datasets:
- [ImageNet](./datasets/imagenet.py)
- [LFW](#lfw)

## ImageNet

Expand Down Expand Up @@ -53,3 +55,28 @@ Run evaluation with the following command:
python eval.py -m mobilenet -d imagenet -dr /path/to/imagenet
```

## LFW
The script is modified based on [evaluation of InsightFace](https://github.com/deepinsight/insightface/blob/f92bf1e48470fdd567e003f196f8ff70461f7a20/src/eval/lfw.py).

### Prepare data

Please visit http://vis-www.cs.umass.edu/lfw to download the LFW [all images](http://vis-www.cs.umass.edu/lfw/lfw.tgz)(needs to be decompressed) and [pairs.txt](http://vis-www.cs.umass.edu/lfw/pairs.txt)(needs to be placed in the `view2` folder). Organize files as follow:

```shell
$ tree -L 2 /path/to/lfw
.
├── lfw
│   ├── Aaron_Eckhart
│   ├── ...
│   └── Zydrunas_Ilgauskas
└── view2
   └── pairs.txt
```

### Evaluation

Run evaluation with the following command:

```shell
python eval.py -m sface -d lfw -dr /path/to/lfw
```
2 changes: 2 additions & 0 deletions tools/eval/datasets/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .imagenet import ImageNet
from .lfw import LFW

class Registery:
def __init__(self, name):
Expand All @@ -13,3 +14,4 @@ def register(self, item):

DATASETS = Registery("Datasets")
DATASETS.register(ImageNet)
DATASETS.register(LFW)
240 changes: 240 additions & 0 deletions tools/eval/datasets/lfw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import os
import numpy as np
from scipy import misc
from sklearn.model_selection import KFold
from scipy import interpolate
import sklearn
from sklearn.decomposition import PCA

import cv2 as cv
from tqdm import tqdm


def calculate_roc(thresholds,
embeddings1,
embeddings2,
actual_issame,
nrof_folds=10,
pca=0):
assert (embeddings1.shape[0] == embeddings2.shape[0])
assert (embeddings1.shape[1] == embeddings2.shape[1])
nrof_pairs = min(len(actual_issame), embeddings1.shape[0])
nrof_thresholds = len(thresholds)
k_fold = KFold(n_splits=nrof_folds, shuffle=False)

tprs = np.zeros((nrof_folds, nrof_thresholds))
fprs = np.zeros((nrof_folds, nrof_thresholds))
accuracy = np.zeros((nrof_folds))
indices = np.arange(nrof_pairs)
# print('pca', pca)

if pca == 0:
diff = np.subtract(embeddings1, embeddings2)
dist = np.sum(np.square(diff), 1)

for fold_idx, (train_set, test_set) in enumerate(k_fold.split(indices)):
# print('train_set', train_set)
# print('test_set', test_set)
if pca > 0:
print('doing pca on', fold_idx)
embed1_train = embeddings1[train_set]
embed2_train = embeddings2[train_set]
_embed_train = np.concatenate((embed1_train, embed2_train), axis=0)
# print(_embed_train.shape)
pca_model = PCA(n_components=pca)
pca_model.fit(_embed_train)
embed1 = pca_model.transform(embeddings1)
embed2 = pca_model.transform(embeddings2)
embed1 = sklearn.preprocessing.normalize(embed1)
embed2 = sklearn.preprocessing.normalize(embed2)
# print(embed1.shape, embed2.shape)
diff = np.subtract(embed1, embed2)
dist = np.sum(np.square(diff), 1)

# Find the best threshold for the fold
acc_train = np.zeros((nrof_thresholds))
for threshold_idx, threshold in enumerate(thresholds):
_, _, acc_train[threshold_idx] = calculate_accuracy(
threshold, dist[train_set], actual_issame[train_set])
best_threshold_index = np.argmax(acc_train)
for threshold_idx, threshold in enumerate(thresholds):
tprs[fold_idx,
threshold_idx], fprs[fold_idx,
threshold_idx], _ = calculate_accuracy(
threshold, dist[test_set],
actual_issame[test_set])
_, _, accuracy[fold_idx] = calculate_accuracy(
thresholds[best_threshold_index], dist[test_set],
actual_issame[test_set])

tpr = np.mean(tprs, 0)
fpr = np.mean(fprs, 0)
return tpr, fpr, accuracy


def calculate_accuracy(threshold, dist, actual_issame):
predict_issame = np.less(dist, threshold)
tp = np.sum(np.logical_and(predict_issame, actual_issame))
fp = np.sum(np.logical_and(predict_issame, np.logical_not(actual_issame)))
tn = np.sum(
np.logical_and(np.logical_not(predict_issame),
np.logical_not(actual_issame)))
fn = np.sum(np.logical_and(np.logical_not(predict_issame), actual_issame))

tpr = 0 if (tp + fn == 0) else float(tp) / float(tp + fn)
fpr = 0 if (fp + tn == 0) else float(fp) / float(fp + tn)
acc = float(tp + tn) / dist.size
return tpr, fpr, acc


def calculate_val(thresholds,
embeddings1,
embeddings2,
actual_issame,
far_target,
nrof_folds=10):
assert (embeddings1.shape[0] == embeddings2.shape[0])
assert (embeddings1.shape[1] == embeddings2.shape[1])
nrof_pairs = min(len(actual_issame), embeddings1.shape[0])
nrof_thresholds = len(thresholds)
k_fold = KFold(n_splits=nrof_folds, shuffle=False)

val = np.zeros(nrof_folds)
far = np.zeros(nrof_folds)

diff = np.subtract(embeddings1, embeddings2)
dist = np.sum(np.square(diff), 1)
indices = np.arange(nrof_pairs)

for fold_idx, (train_set, test_set) in enumerate(k_fold.split(indices)):

# Find the threshold that gives FAR = far_target
far_train = np.zeros(nrof_thresholds)
for threshold_idx, threshold in enumerate(thresholds):
_, far_train[threshold_idx] = calculate_val_far(
threshold, dist[train_set], actual_issame[train_set])
if np.max(far_train) >= far_target:
f = interpolate.interp1d(far_train, thresholds, kind='slinear')
threshold = f(far_target)
else:
threshold = 0.0

val[fold_idx], far[fold_idx] = calculate_val_far(
threshold, dist[test_set], actual_issame[test_set])

val_mean = np.mean(val)
far_mean = np.mean(far)
val_std = np.std(val)
return val_mean, val_std, far_mean


def calculate_val_far(threshold, dist, actual_issame):
predict_issame = np.less(dist, threshold)
true_accept = np.sum(np.logical_and(predict_issame, actual_issame))
false_accept = np.sum(
np.logical_and(predict_issame, np.logical_not(actual_issame)))
n_same = np.sum(actual_issame)
n_diff = np.sum(np.logical_not(actual_issame))
val = float(true_accept) / float(n_same)
far = float(false_accept) / float(n_diff)
return val, far


def evaluate(embeddings, actual_issame, nrof_folds=10, pca=0):
# Calculate evaluation metrics
thresholds = np.arange(0, 4, 0.01)
embeddings1 = embeddings[0::2]
embeddings2 = embeddings[1::2]
tpr, fpr, accuracy = calculate_roc(thresholds,
embeddings1,
embeddings2,
np.asarray(actual_issame),
nrof_folds=nrof_folds,
pca=pca)
thresholds = np.arange(0, 4, 0.001)
val, val_std, far = calculate_val(thresholds,
embeddings1,
embeddings2,
np.asarray(actual_issame),
1e-3,
nrof_folds=nrof_folds)
return tpr, fpr, accuracy, val, val_std, far


class LFW:
def __init__(self, root, target_size=250):
self.LFW_IMAGE_SIZE = 250

self.lfw_root = root
self.target_size = target_size

self.lfw_pairs_path = os.path.join(self.lfw_root, 'view2/pairs.txt')
self.image_path_pattern = os.path.join(self.lfw_root, 'lfw', '{person_name}', '{image_name}')

self.lfw_image_paths, self.id_list = self.load_pairs()

@property
def name(self):
return 'LFW'

def __len__(self):
return len(self.lfw_image_paths)

@property
def ids(self):
return self.id_list

def load_pairs(self):
image_paths = []
id_list = []
with open(self.lfw_pairs_path, 'r') as f:
for line in f.readlines()[1:]:
line = line.strip().split()
if len(line) == 3:
person_name = line[0]
image1_name = '{}_{:04d}.jpg'.format(person_name, int(line[1]))
image2_name = '{}_{:04d}.jpg'.format(person_name, int(line[2]))
image_paths += [
self.image_path_pattern.format(person_name=person_name, image_name=image1_name),
self.image_path_pattern.format(person_name=person_name, image_name=image2_name)
]
id_list.append(True)
elif len(line) == 4:
person1_name = line[0]
image1_name = '{}_{:04d}.jpg'.format(person1_name, int(line[1]))
person2_name = line[2]
image2_name = '{}_{:04d}.jpg'.format(person2_name, int(line[3]))
image_paths += [
self.image_path_pattern.format(person_name=person1_name, image_name=image1_name),
self.image_path_pattern.format(person_name=person2_name, image_name=image2_name)
]
id_list.append(False)
return image_paths, id_list

def __getitem__(self, key):
img = cv.imread(self.lfw_image_paths[key])
if self.target_size != self.LFW_IMAGE_SIZE:
img = cv.resize(img, (self.target_size, self.target_size))
return img

def eval(self, model):
ids = self.ids
embeddings = np.zeros(shape=(len(self), 128))
for idx, img in tqdm(enumerate(self), desc="Evaluating {} with {} val set".format(model.name, self.name)):
embedding = model.infer(img)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The image should be aligned before being fed into the model. This can be the problem of low accuracy.

To get aligned image, you need to get the bounding box of the face using YuNet. For simplicity, you can start with treating the bounding box of highest score as the only face in the image. So I suggest you:

  1. Validate whether alignment using YuNet improves the accuracy.
  2. If it does, store those boxes in a constant in this file (or a separate .npy file) and load it when requires to save time.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! After using YuNet to get bbox, the accuracy is 97.92%. this is still not 99.6% as described in the documentation. I will try to solve this problem.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference can be YuNet failing to detect a face in some images. You can have a check on the images with low score faces or no face.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may not be the problem. Each image is detected with a face and the selected bbox scores are all above 0.8.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sometimes it may fail even with score 0.8. Please, take a look at those faces with score lower than 0.9.

embeddings[idx] = embedding

embeddings = sklearn.preprocessing.normalize(embeddings)
self.tpr, self.fpr, self.acc, self.val, self.std, self.far = evaluate(embeddings,
ids,
nrof_folds=10)
self.acc, self.std = np.mean(self.acc), np.std(self.acc)

def print_result(self):
print("==================== Results ====================")
print("Average Accuracy: {:.4f}".format(self.acc))
print("=================================================")
12 changes: 12 additions & 0 deletions tools/eval/eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,25 @@
topic="image_classification",
modelPath=os.path.join(root_dir, "models/image_classification_ppresnet/image_classification_ppresnet50_2022jan-act_int8-wt_int8-quantized.onnx"),
topK=5),
sface=dict(
name="SFace",
topic="face_recognition",
modelPath=os.path.join(root_dir, "models/face_recognition_sface/face_recognition_sface_2021dec.onnx")),
sface_q=dict(
name="SFace",
topic="face_recognition",
modelPath=os.path.join(root_dir, "models/face_recognition_sface/face_recognition_sface_2021dec-act_int8-wt_int8-quantized.onnx")),
)

datasets = dict(
imagenet=dict(
name="ImageNet",
topic="image_classification",
size=224),
lfw=dict(
name="LFW",
topic="face_recognition",
target_size=112),
)

def main(args):
Expand Down