diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..09bf216 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "facenet"] + path = facenet + url = git@github.com:arsfutura/facenet-pytorch.git diff --git a/README.md b/README.md new file mode 100644 index 0000000..ae813e6 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +### `face_recognition.py` +``` +usage: Script for recognising faces on picture. Output of this script is json with list of people on picture and base64 encoded picture which has bounding boxes of people. + [-h] (--image-path IMAGE_PATH | --image-bs64 IMAGE_BS64) + --classifier-path CLASSIFIER_PATH + +optional arguments: + -h, --help show this help message and exit + --image-path IMAGE_PATH + Path to image file. + --image-bs64 IMAGE_BS64 + Base64 representation of image. + --classifier-path CLASSIFIER_PATH + Path to serialized classifier. +``` + +# + +### `real_time_face_detection.py` +``` +usage: Script for real-time face recognition. [-h] + [--classifier-path CLASSIFIER_PATH] + +optional arguments: + -h, --help show this help message and exit + --classifier-path CLASSIFIER_PATH + Path to serialized classifier. +``` \ No newline at end of file diff --git a/arsfutura-face-similarity/__init__.py b/arsfutura-face-similarity/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/arsfutura-face-similarity/constants.py b/arsfutura-face-similarity/constants.py deleted file mode 100644 index f1e2290..0000000 --- a/arsfutura-face-similarity/constants.py +++ /dev/null @@ -1,2 +0,0 @@ -DATA_PATH = 'data/features/' -MODEL_PATH = 'models/model.pkl' diff --git a/arsfutura-face-similarity/dataset.py b/arsfutura-face-similarity/dataset.py deleted file mode 100644 index a7e938a..0000000 --- a/arsfutura-face-similarity/dataset.py +++ /dev/null @@ -1,15 +0,0 @@ -import pandas as pd -from constants import DATA_PATH - -EMBEDDINGS_PATH = DATA_PATH + 'reps.csv' -LABELS_PATH = DATA_PATH + 'labels.csv' - - -def load_data(): - embeddings = pd.read_csv(EMBEDDINGS_PATH, header=None).values - labels = map(lambda label: label.split('/')[-2].upper(), pd.read_csv(LABELS_PATH, header=None).values[:, 1].tolist()) - return embeddings, labels - - -if __name__ == '__main__': - print load_data() diff --git a/arsfutura-face-similarity/predict.py b/arsfutura-face-similarity/predict.py deleted file mode 100644 index c3e3299..0000000 --- a/arsfutura-face-similarity/predict.py +++ /dev/null @@ -1,45 +0,0 @@ -import openface -import cv2 -import pickle -import numpy as np -from collections import namedtuple -from constants import MODEL_PATH - -Face = namedtuple('Face', 'bb identity probability') - -align = openface.AlignDlib('models/shape_predictor_68_face_landmarks.dat') -net = openface.TorchNeuralNet('models/nn4.small2.v1.t7', imgDim=96, cuda=False) -le, model = pickle.load(open(MODEL_PATH, 'rb')) - - -def faces_embeddings_and_bbs(img): - rgb_img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - bb = align.getAllFaceBoundingBoxes(rgb_img) - - aligned_faces = [] - for box in bb: - aligned_faces.append( - align.align( - 96, - rgb_img, - box, - landmarkIndices=openface.AlignDlib.OUTER_EYES_AND_NOSE)) - - embeddings = np.array([net.forward(alignedFace) for alignedFace in aligned_faces]) - - return embeddings, bb - - -def predict(img): - embeddings, bb = faces_embeddings_and_bbs(img) - if embeddings.size == 0 or not bb: - return None - probs = model.predict_proba(embeddings) - predicted = probs.argmax(axis=1) - identities = le.inverse_transform(predicted) - probs = probs[np.arange(len(predicted)), predicted] - return [Face(box, identity, prob * 100) for identity, box, prob in zip(identities, bb, probs)] - - -if __name__ == '__main__': - predict('') diff --git a/arsfutura-face-similarity/real_time_face_detection.py b/arsfutura-face-similarity/real_time_face_detection.py deleted file mode 100644 index a489113..0000000 --- a/arsfutura-face-similarity/real_time_face_detection.py +++ /dev/null @@ -1,33 +0,0 @@ -from __future__ import print_function, division - -import cv2 -from predict import predict - - -def main(): - cap = cv2.VideoCapture(0) - - while True: - # Capture frame-by-frame - ret, frame = cap.read() - frame = cv2.flip(frame, 1) - - faces = predict(frame) - if faces is not None: - for face in faces: - cv2.rectangle(frame, (face.bb.left(), face.bb.top()), (face.bb.right(), face.bb.bottom()), (0, 255, 0), 2) - cv2.putText(frame, "%s %.2f%%" % (face.identity, face.probability), - (face.bb.left(), face.bb.bottom() + 20), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (255, 255, 255), 1) - - # Display the resulting frame - cv2.imshow('frame', frame) - if cv2.waitKey(1) & 0xFF == ord('q'): - break - - # When everything done, release the captureq - cap.release() - cv2.destroyAllWindows() - - -if __name__ == '__main__': - main() diff --git a/arsfutura-face-similarity/util/__init__.py b/arsfutura-face-similarity/util/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/arsfutura-face-similarity/util/align-dlib.py b/arsfutura-face-similarity/util/align-dlib.py deleted file mode 100755 index ff6b55a..0000000 --- a/arsfutura-face-similarity/util/align-dlib.py +++ /dev/null @@ -1,175 +0,0 @@ -#!/usr/bin/env python2 -# -# Copyright 2015-2016 Carnegie Mellon University -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import argparse -import cv2 -import numpy as np -import os -import random -import shutil - -import openface -import openface.helper -from openface.data import iterImgs - -fileDir = os.path.dirname(os.path.realpath(__file__)) -modelDir = os.path.join(fileDir, '..', 'models') -dlibModelDir = modelDir -openfaceModelDir = modelDir - - -def write(vals, fName): - if os.path.isfile(fName): - print("{} exists. Backing up.".format(fName)) - os.rename(fName, "{}.bak".format(fName)) - with open(fName, 'w') as f: - for p in vals: - f.write(",".join(str(x) for x in p)) - f.write("\n") - - -def computeMeanMain(args): - align = openface.AlignDlib(args.dlibFacePredictor) - - imgs = list(iterImgs(args.inputDir)) - if args.numImages > 0: - imgs = random.sample(imgs, args.numImages) - - facePoints = [] - for img in imgs: - rgb = img.getRGB() - bb = align.getLargestFaceBoundingBox(rgb) - alignedPoints = align.align(rgb, bb) - if alignedPoints: - facePoints.append(alignedPoints) - - facePointsNp = np.array(facePoints) - mean = np.mean(facePointsNp, axis=0) - std = np.std(facePointsNp, axis=0) - - write(mean, "{}/mean.csv".format(args.modelDir)) - write(std, "{}/std.csv".format(args.modelDir)) - - # Only import in this mode. - import matplotlib as mpl - mpl.use('Agg') - import matplotlib.pyplot as plt - - fig, ax = plt.subplots() - ax.scatter(mean[:, 0], -mean[:, 1], color='k') - ax.axis('equal') - for i, p in enumerate(mean): - ax.annotate(str(i), (p[0] + 0.005, -p[1] + 0.005), fontsize=8) - plt.savefig("{}/mean.png".format(args.modelDir)) - - -def alignMain(args): - openface.helper.mkdirP(args.outputDir) - - imgs = list(iterImgs(args.inputDir)) - - # Shuffle so multiple versions can be run at once. - random.shuffle(imgs) - - landmarkMap = { - 'outerEyesAndNose': openface.AlignDlib.OUTER_EYES_AND_NOSE, - 'innerEyesAndBottomLip': openface.AlignDlib.INNER_EYES_AND_BOTTOM_LIP - } - if args.landmarks not in landmarkMap: - raise Exception("Landmarks unrecognized: {}".format(args.landmarks)) - - landmarkIndices = landmarkMap[args.landmarks] - - align = openface.AlignDlib(args.dlibFacePredictor) - - nFallbacks = 0 - for imgObject in imgs: - print("=== {} ===".format(imgObject.path)) - outDir = os.path.join(args.outputDir, imgObject.cls) - openface.helper.mkdirP(outDir) - outputPrefix = os.path.join(outDir, imgObject.name) - imgName = outputPrefix + ".png" - - if os.path.isfile(imgName): - if args.verbose: - print(" + Already found, skipping.") - else: - rgb = imgObject.getRGB() - if rgb is None: - if args.verbose: - print(" + Unable to load.") - outRgb = None - else: - outRgb = align.align(args.size, rgb, - landmarkIndices=landmarkIndices, - skipMulti=args.skipMulti) - if outRgb is None and args.verbose: - print(" + Unable to align.") - - if args.fallbackLfw and outRgb is None: - nFallbacks += 1 - deepFunneled = "{}/{}.jpg".format(os.path.join(args.fallbackLfw, - imgObject.cls), - imgObject.name) - shutil.copy(deepFunneled, "{}/{}.jpg".format(os.path.join(args.outputDir, - imgObject.cls), - imgObject.name)) - - if outRgb is not None: - if args.verbose: - print(" + Writing aligned file to disk.") - outBgr = cv2.cvtColor(outRgb, cv2.COLOR_RGB2BGR) - cv2.imwrite(imgName, outBgr) - - if args.fallbackLfw: - print('nFallbacks:', nFallbacks) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - - parser.add_argument('inputDir', type=str, help="Input image directory.") - parser.add_argument('--dlibFacePredictor', type=str, help="Path to dlib's face predictor.", - default=os.path.join(dlibModelDir, "shape_predictor_68_face_landmarks.dat")) - - subparsers = parser.add_subparsers(dest='mode', help="Mode") - computeMeanParser = subparsers.add_parser( - 'computeMean', help='Compute the image mean of a directory of images.') - computeMeanParser.add_argument('--numImages', type=int, help="The number of images. '0' for all images.", - default=0) # <= 0 ===> all imgs - alignmentParser = subparsers.add_parser( - 'align', help='Align a directory of images.') - alignmentParser.add_argument('landmarks', type=str, - choices=['outerEyesAndNose', - 'innerEyesAndBottomLip', - 'eyes_1'], - help='The landmarks to align to.') - alignmentParser.add_argument( - 'outputDir', type=str, help="Output directory of aligned images.") - alignmentParser.add_argument('--size', type=int, help="Default image size.", - default=96) - alignmentParser.add_argument('--fallbackLfw', type=str, - help="If alignment doesn't work, fallback to copying the deep funneled version from this directory..") - alignmentParser.add_argument( - '--skipMulti', action='store_true', help="Skip images with more than one face.") - alignmentParser.add_argument('--verbose', action='store_true') - - args = parser.parse_args() - - if args.mode == 'computeMean': - computeMeanMain(args) - else: - alignMain(args) diff --git a/arsfutura_face_recognition/__init__.py b/arsfutura_face_recognition/__init__.py new file mode 100644 index 0000000..af0ebc9 --- /dev/null +++ b/arsfutura_face_recognition/__init__.py @@ -0,0 +1 @@ +from .face_recogniser import face_recogniser_factory diff --git a/arsfutura_face_recognition/aligner/__init__.py b/arsfutura_face_recognition/aligner/__init__.py new file mode 100644 index 0000000..50ce507 --- /dev/null +++ b/arsfutura_face_recognition/aligner/__init__.py @@ -0,0 +1,10 @@ +from abc import ABC, abstractmethod + + +class Aligner(ABC): + @abstractmethod + def align(self, img): + pass + + def __call__(self, *args, **kwargs): + return self.align(*args, **kwargs) diff --git a/arsfutura_face_recognition/aligner/factory.py b/arsfutura_face_recognition/aligner/factory.py new file mode 100644 index 0000000..5bea955 --- /dev/null +++ b/arsfutura_face_recognition/aligner/factory.py @@ -0,0 +1,5 @@ +from .mtcnn import MTCNNAligner + + +def aligner_factory(args): + return MTCNNAligner() diff --git a/arsfutura_face_recognition/aligner/mtcnn.py b/arsfutura_face_recognition/aligner/mtcnn.py new file mode 100644 index 0000000..bf5fbc9 --- /dev/null +++ b/arsfutura_face_recognition/aligner/mtcnn.py @@ -0,0 +1,10 @@ +from facenet import MTCNN +from . import Aligner + + +class MTCNNAligner(Aligner): + def __init__(self): + self.mtcnn = MTCNN(keep_all=True) + + def align(self, img): + return self.mtcnn(img) diff --git a/arsfutura_face_recognition/classifier/__init__.py b/arsfutura_face_recognition/classifier/__init__.py new file mode 100644 index 0000000..5132c22 --- /dev/null +++ b/arsfutura_face_recognition/classifier/__init__.py @@ -0,0 +1,10 @@ +from abc import ABC, abstractmethod + + +class FaceClassifier(ABC): + @abstractmethod + def predict(self, face_embedding): + pass + + def __call__(self, *args, **kwargs): + return self.predict(*args, **kwargs) diff --git a/arsfutura_face_recognition/classifier/classifier.py b/arsfutura_face_recognition/classifier/classifier.py new file mode 100644 index 0000000..7f4be29 --- /dev/null +++ b/arsfutura_face_recognition/classifier/classifier.py @@ -0,0 +1,10 @@ +from . import FaceClassifier +import pickle + + +class FaceClassifierImpl(FaceClassifier): + def __init__(self, model_path): + self.le, self.model = pickle.load(open(model_path, 'rb')) + + def predict(self, face_embedding): + return self.le.inverse_transform(self.model.predict(face_embedding)) diff --git a/arsfutura_face_recognition/classifier/factory.py b/arsfutura_face_recognition/classifier/factory.py new file mode 100644 index 0000000..1e275c2 --- /dev/null +++ b/arsfutura_face_recognition/classifier/factory.py @@ -0,0 +1,5 @@ +from .classifier import FaceClassifierImpl + + +def classifier_factory(args): + return FaceClassifierImpl(args.classifier_path) diff --git a/arsfutura-face-similarity/collect_face_images.py b/arsfutura_face_recognition/collect_face_images.py similarity index 93% rename from arsfutura-face-similarity/collect_face_images.py rename to arsfutura_face_recognition/collect_face_images.py index c12edfb..8615d1e 100644 --- a/arsfutura-face-similarity/collect_face_images.py +++ b/arsfutura_face_recognition/collect_face_images.py @@ -36,4 +36,4 @@ def main(directory, name, test): try: main(directory, args.person, args.test) except KeyboardInterrupt: - print "Photo session done for {} :)".format(args.person) + print("Photo session done for {} :)".format(args.person)) diff --git a/arsfutura_face_recognition/dataset.py b/arsfutura_face_recognition/dataset.py new file mode 100644 index 0000000..1381b18 --- /dev/null +++ b/arsfutura_face_recognition/dataset.py @@ -0,0 +1,9 @@ +import numpy as np + + +def load_data(): + return np.loadtxt('../embeddings.txt'), np.loadtxt('../labels.txt', dtype=np.str) + + +if __name__ == '__main__': + print(load_data()) diff --git a/arsfutura_face_recognition/face_recogniser.py b/arsfutura_face_recognition/face_recogniser.py new file mode 100644 index 0000000..21a340b --- /dev/null +++ b/arsfutura_face_recognition/face_recogniser.py @@ -0,0 +1,59 @@ +import cv2 +from PIL import Image +from .aligner.factory import aligner_factory +from .facenet.factory import facenet_factory +from .classifier.factory import classifier_factory +from collections import namedtuple + +Face = namedtuple('Face', 'bb identity probability') + + +class BoundingBox: + def __init__(self, left, top, right, bottom): + self._left = left + self._top = top + self._right = right + self._bottom = bottom + + def left(self): + return self._left + + def top(self): + return self._top + + def right(self): + return self._right + + def bottom(self): + return self._bottom + + +def face_recogniser_factory(args): + return FaceRecogniser( + aligner=aligner_factory(args), + facenet=facenet_factory(args), + classifier=classifier_factory(args) + ) + + +class FaceRecogniser: + def __init__(self, aligner, facenet, classifier): + self.aligner = aligner + self.facenet = facenet + self.classifier = classifier + + def recognise_faces(self, img): + img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) + faces_imgs, bbs = self.aligner(img_pil) + if faces_imgs is None: + # if no face is detected + return None + + embeddings = self.facenet(faces_imgs).detach().numpy() + people = self.classifier(embeddings) + + return [Face(BoundingBox(left=bb[0], top=bb[1], right=bb[2], bottom=bb[3]), person, 100) + for bb, person in zip(bbs, people)] + + def __call__(self, *args, **kwargs): + return self.recognise_faces(*args, **kwargs) diff --git a/arsfutura_face_recognition/facenet/__init__.py b/arsfutura_face_recognition/facenet/__init__.py new file mode 100644 index 0000000..c6ec59c --- /dev/null +++ b/arsfutura_face_recognition/facenet/__init__.py @@ -0,0 +1,10 @@ +from abc import ABC, abstractmethod + + +class FaceNet(ABC): + @abstractmethod + def forward(self, img): + pass + + def __call__(self, *args, **kwargs): + return self.forward(*args, **kwargs) diff --git a/arsfutura_face_recognition/facenet/facenet.py b/arsfutura_face_recognition/facenet/facenet.py new file mode 100644 index 0000000..0408656 --- /dev/null +++ b/arsfutura_face_recognition/facenet/facenet.py @@ -0,0 +1,10 @@ +from . import FaceNet +from facenet import InceptionResnetV1 + + +class FaceNetImpl(FaceNet): + def __init__(self): + self.facenet = InceptionResnetV1(pretrained='vggface2').eval() + + def forward(self, img): + return self.facenet(img) diff --git a/arsfutura_face_recognition/facenet/factory.py b/arsfutura_face_recognition/facenet/factory.py new file mode 100644 index 0000000..ab98308 --- /dev/null +++ b/arsfutura_face_recognition/facenet/factory.py @@ -0,0 +1,5 @@ +from .facenet import FaceNetImpl + + +def facenet_factory(args): + return FaceNetImpl() diff --git a/arsfutura-face-similarity/train.py b/arsfutura_face_recognition/train.py similarity index 83% rename from arsfutura-face-similarity/train.py rename to arsfutura_face_recognition/train.py index 1b133c3..330f39c 100644 --- a/arsfutura-face-similarity/train.py +++ b/arsfutura_face_recognition/train.py @@ -5,7 +5,6 @@ from sklearn.preprocessing import LabelEncoder from sklearn.metrics import classification_report from dataset import load_data -from constants import MODEL_PATH from sklearn.ensemble import RandomForestClassifier CLASSIFIERS = { @@ -33,9 +32,9 @@ def main(): clf = GridSearchCV(classifier[0], classifier[1], cv=5, verbose=1) clf.fit(X, le.transform(y)) - print clf.best_params_ - print classification_report(le.transform(y), clf.predict(X), target_names=le.classes_) - pickle.dump((le, clf), open(MODEL_PATH, 'wb')) + print(clf.best_params_) + print(classification_report(le.transform(y), clf.predict(X), target_names=le.classes_)) + pickle.dump((le, clf), open('../models/model.pkl', 'wb')) if __name__ == '__main__': diff --git a/arsfutura-face-similarity/visualize_data.py b/arsfutura_face_recognition/visualize_data.py similarity index 68% rename from arsfutura-face-similarity/visualize_data.py rename to arsfutura_face_recognition/visualize_data.py index 9eb463c..531f50c 100644 --- a/arsfutura-face-similarity/visualize_data.py +++ b/arsfutura_face_recognition/visualize_data.py @@ -1,5 +1,5 @@ from collections import OrderedDict - +import random import numpy as np import matplotlib.pyplot as plt from matplotlib.pyplot import cm @@ -18,20 +18,21 @@ def main(): - X, y = load_data() + X, labels = load_data() cls = METHODS['tsne'] method = cls(n_components=2) transformed = method.fit_transform(X) - y = np.array(y) + y = set(labels) + labels = np.array(labels) plt.figure(figsize=(10, 12)) colors = cm.rainbow(np.linspace(0, 1, len(y))) for label, color in zip(y, colors): - points = transformed[y == label, :] - plt.scatter(points[:, 0], points[:, 1], c=color, label=label, s=200, alpha=0.5) - plt.annotate(label, (points[0, 0], points[0, 1]), fontsize=15) + points = transformed[labels == label, :] + plt.scatter(points[:, 0], points[:, 1], c=[color], label=label, s=200, alpha=0.5) + for p1, p2 in random.sample(list(zip(points[:, 0], points[:, 1])), k=min(1, len(points))): + plt.annotate(label, (p1, p2), fontsize=15) - #plt.grid() handles, labels = plt.gca().get_legend_handles_labels() by_label = OrderedDict(zip(labels, handles)) plt.legend(by_label.values(), by_label.keys()) diff --git a/bin/align-mtcnn.py b/bin/align-mtcnn.py new file mode 100755 index 0000000..ee494a5 --- /dev/null +++ b/bin/align-mtcnn.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python + +import os +import argparse +from torchvision import datasets, transforms +from facenet_pytorch.models.mtcnn import MTCNN +from PIL import Image + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument('--input-folder', required=True, + help='Root folder where input images are. This folder contains sub-folders for each class.') + parser.add_argument('--output-folder', required=True, help='Output folder where aligned images will be saved.') + return parser.parse_args() + + +def create_dirs(root_dir, classes): + if not os.path.isdir(root_dir): + os.mkdir(root_dir) + for clazz in classes: + path = root_dir + os.path.sep + clazz + if not os.path.isdir(path): + os.mkdir(path) + + +def main(): + args = parse_args() + trans = transforms.Compose([ + transforms.Resize(1024) + ]) + + images = datasets.ImageFolder(root=args.input_folder) + images.idx_to_class = {v: k for k, v in images.class_to_idx.items()} + create_dirs(args.output_folder, images.classes) + + mtcnn = MTCNN() + + for idx, (path, y) in enumerate(images.imgs): + print("Aligning {} {}/{} ".format(path, idx + 1, len(images)), end='') + aligned_path = args.output_folder + os.path.sep + images.idx_to_class[y] + os.path.sep + os.path.basename(path) + if not os.path.exists(aligned_path): + img = mtcnn(img=trans(Image.open(path).convert('RGB')), save_path=aligned_path) + print("No face found" if img is None else '') + else: + print('Already aligned') + + +if __name__ == '__main__': + main() diff --git a/bin/batch-represent/batch-represent.lua b/bin/batch-represent/batch-represent.lua deleted file mode 100644 index 1203aca..0000000 --- a/bin/batch-represent/batch-represent.lua +++ /dev/null @@ -1,77 +0,0 @@ -local ffi = require 'ffi' - -local batchNumber, nImgs = 0 - -torch.setdefaulttensortype('torch.FloatTensor') - -function batchRepresent() - local loadSize = {3, opt.imgDim, opt.imgDim} - print(opt.data) - local cacheFile = paths.concat(opt.data, 'cache.t7') - print('cache lotation: ', cacheFile) - local dumpLoader - if paths.filep(cacheFile) then - print('Loading metadata from cache.') - print('If your dataset has changed, delete the cache file.') - dumpLoader = torch.load(cacheFile) - else - print('Creating metadata for cache.') - dumpLoader = dataLoader{ - paths = {opt.data}, - loadSize = loadSize, - sampleSize = loadSize, - split = 0, - verbose = true - } - torch.save(cacheFile, dumpLoader) - end - collectgarbage() - nImgs = dumpLoader:sizeTest() - print('nImgs: ', nImgs) - assert(nImgs > 0, "Failed to get nImgs") - - batchNumber = 0 - - for i=1,math.ceil(nImgs/opt.batchSize) do - local indexStart = (i-1) * opt.batchSize + 1 - local indexEnd = math.min(nImgs, indexStart + opt.batchSize - 1) - local batchSz = indexEnd-indexStart+1 - local inputs, labels = dumpLoader:get(indexStart, indexEnd) - local paths = {} - for j=indexStart,indexEnd do - table.insert(paths, - ffi.string(dumpLoader.imagePath[dumpLoader.testIndices[j]]:data())) - end - repBatch(paths, inputs, labels, batchSz) - if i % 5 == 0 then - collectgarbage() - end - end - - if opt.cuda then - cutorch.synchronize() - end -end - -function repBatch(paths, inputs, labels, batchSz) - batchNumber = batchNumber + batchSz - - if opt.cuda then - inputs = inputs:cuda() - end - local embeddings = model:forward(inputs):float() - if opt.cuda then - cutorch.synchronize() - end - - if batchSz == 1 then - embeddings = embeddings:reshape(1, embeddings:size(1)) - end - - for i=1,batchSz do - labelsCSV:write({labels[i], paths[i]}) - repsCSV:write(embeddings[i]:totable()) - end - - print(('Represent: %d/%d'):format(batchNumber, nImgs)) -end diff --git a/bin/batch-represent/dataset.lua b/bin/batch-represent/dataset.lua deleted file mode 100644 index 7d9cedc..0000000 --- a/bin/batch-represent/dataset.lua +++ /dev/null @@ -1,410 +0,0 @@ --- Source: https://github.com/soumith/imagenet-multiGPU.torch/blob/master/dataset.lua - -require 'torch' -torch.setdefaulttensortype('torch.FloatTensor') -local ffi = require 'ffi' -local dir = require 'pl.dir' -local argcheck = require 'argcheck' -require 'sys' -require 'xlua' -require 'image' - -local dataset = torch.class('dataLoader') - -local initcheck = argcheck{ - pack=true, - help=[[ - A dataset class for images in a flat folder structure (folder-name is class-name). - Optimized for extremely large datasets (upwards of 14 million images). - Tested only on Linux (as it uses command-line linux utilities to scale up) -]], - {check=function(paths) - local out = true; - for _,v in ipairs(paths) do - if type(v) ~= 'string' then - print('paths can only be of string input'); - out = false - end - end - return out - end, - name="paths", - type="table", - help="Multiple paths of directories with images"}, - - {name="sampleSize", - type="table", - help="a consistent sample size to resize the images"}, - - {name="split", - type="number", - help="Percentage of split to go to Training" - }, - - {name="samplingMode", - type="string", - help="Sampling mode: random | balanced ", - default = "balanced"}, - - {name="verbose", - type="boolean", - help="Verbose mode during initialization", - default = false}, - - {name="loadSize", - type="table", - help="a size to load the images to, initially", - opt = true}, - - {name="forceClasses", - type="table", - help="If you want this loader to map certain classes to certain indices, " - .. "pass a classes table that has {classname : classindex} pairs." - .. " For example: {3 : 'dog', 5 : 'cat'}" - .. "This function is very useful when you want two loaders to have the same " - .. "class indices (trainLoader/testLoader for example)", - opt = true}, - - {name="sampleHookTrain", - type="function", - help="applied to sample during training(ex: for lighting jitter). " - .. "It takes the image path as input", - opt = true}, - - {name="sampleHookTest", - type="function", - help="applied to sample during testing", - opt = true}, -} - -function dataset:__init(...) - - -- argcheck - local args = initcheck(...) - print(args) - for k,v in pairs(args) do self[k] = v end - - if not self.loadSize then self.loadSize = self.sampleSize; end - - if not self.sampleHookTrain then self.sampleHookTrain = self.defaultSampleHook end - if not self.sampleHookTest then self.sampleHookTest = self.defaultSampleHook end - - -- find class names - self.classes = {} - local classPaths = {} - if self.forceClasses then - for k,v in pairs(self.forceClasses) do - self.classes[k] = v - classPaths[k] = {} - end - end - local function tableFind(t, o) for k,v in pairs(t) do if v == o then return k end end end - -- loop over each paths folder, get list of unique class names, - -- also store the directory paths per class - -- for each class, - for _,path in ipairs(self.paths) do - local dirs = dir.getdirectories(path); - for _,dirpath in ipairs(dirs) do - local class = paths.basename(dirpath) - local idx = tableFind(self.classes, class) - if not idx then - table.insert(self.classes, class) - idx = #self.classes - classPaths[idx] = {} - end - if not tableFind(classPaths[idx], dirpath) then - table.insert(classPaths[idx], dirpath); - end - end - end - - self.classIndices = {} - for k,v in ipairs(self.classes) do - self.classIndices[v] = k - end - - -- define command-line tools, try your best to maintain OSX compatibility - local wc = 'wc' - local cut = 'cut' - local find = 'find' - if jit.os == 'OSX' then - wc = 'gwc' - cut = 'gcut' - find = 'gfind' - end - ---------------------------------------------------------------------- - -- Options for the GNU find command - local extensionList = {'jpg', 'png','JPG','PNG','JPEG', 'ppm', 'PPM', 'bmp', 'BMP'} - local findOptions = ' -iname "*.' .. extensionList[1] .. '"' - for i=2,#extensionList do - findOptions = findOptions .. ' -o -iname "*.' .. extensionList[i] .. '"' - end - - -- find the image path names - self.imagePath = torch.CharTensor() -- path to each image in dataset - self.imageClass = torch.LongTensor() -- class index of each image (class index in self.classes) - self.classList = {} -- index of imageList to each image of a particular class - self.classListSample = self.classList -- the main list used when sampling data - - print('running "find" on each class directory, and concatenate all' - .. ' those filenames into a single file containing all image paths for a given class') - -- so, generates one file per class - local classFindFiles = {} - for i=1,#self.classes do - classFindFiles[i] = os.tmpname() - end - local combinedFindList = os.tmpname(); - - local tmpfile = os.tmpname() - local tmphandle = assert(io.open(tmpfile, 'w')) - -- iterate over classes - for i, _ in ipairs(self.classes) do - -- iterate over classPaths - for _,path in ipairs(classPaths[i]) do - local command = find .. ' "' .. path .. '" ' .. findOptions - .. ' >>"' .. classFindFiles[i] .. '" \n' - tmphandle:write(command) - end - end - io.close(tmphandle) - os.execute('bash ' .. tmpfile) - os.execute('rm -f ' .. tmpfile) - - print('now combine all the files to a single large file') - tmpfile = os.tmpname() - tmphandle = assert(io.open(tmpfile, 'w')) - -- concat all finds to a single large file in the order of self.classes - for i=1,#self.classes do - local command = 'cat "' .. classFindFiles[i] .. '" >>' .. combinedFindList .. ' \n' - tmphandle:write(command) - end - io.close(tmphandle) - os.execute('bash ' .. tmpfile) - os.execute('rm -f ' .. tmpfile) - - --========================================================================== - print('load the large concatenated list of sample paths to self.imagePath') - local maxPathLength = tonumber(sys.fexecute(wc .. " -L '" - .. combinedFindList .. "' |" - .. cut .. " -f1 -d' '")) + 1 - local length = tonumber(sys.fexecute(wc .. " -l '" - .. combinedFindList .. "' |" - .. cut .. " -f1 -d' '")) - assert(length > 0, "Could not find any image file in the given input paths") - assert(maxPathLength > 0, "paths of files are length 0?") - self.imagePath:resize(length, maxPathLength):fill(0) - local s_data = self.imagePath:data() - local count = 0 - for line in io.lines(combinedFindList) do - ffi.copy(s_data, line) - s_data = s_data + maxPathLength - if self.verbose and count % 10000 == 0 then - xlua.progress(count, length) - end; - count = count + 1 - end - - self.numSamples = self.imagePath:size(1) - if self.verbose then print(self.numSamples .. ' samples found.') end - --========================================================================== - print('Updating classList and imageClass appropriately') - self.imageClass:resize(self.numSamples) - local runningIndex = 0 - for i=1,#self.classes do - if self.verbose then xlua.progress(i, #(self.classes)) end - local clsLength = tonumber(sys.fexecute(wc .. " -l '" - .. classFindFiles[i] .. "' |" - .. cut .. " -f1 -d' '")) - if clsLength == 0 then - error('Class has zero samples: ' .. self.classes[i]) - else - -- self.classList[i] = torch.linspace(runningIndex + 1, runningIndex + clsLength, clsLength):long() - self.classList[i] = torch.range(runningIndex + 1, runningIndex + clsLength):long() - self.imageClass[{{runningIndex + 1, runningIndex + clsLength}}]:fill(i) - end - runningIndex = runningIndex + clsLength - end - - --========================================================================== - -- clean up temporary files - print('Cleaning up temporary files') - local tmpfilelistall = '' - for i=1,#(classFindFiles) do - tmpfilelistall = tmpfilelistall .. ' "' .. classFindFiles[i] .. '"' - if i % 1000 == 0 then - os.execute('rm -f ' .. tmpfilelistall) - tmpfilelistall = '' - end - end - os.execute('rm -f ' .. tmpfilelistall) - os.execute('rm -f "' .. combinedFindList .. '"') - --========================================================================== - - if self.split == 100 then - self.testIndicesSize = 0 - else - print('Splitting training and test sets to a ratio of ' - .. self.split .. '/' .. (100-self.split)) - self.classListTrain = {} - self.classListTest = {} - self.classListSample = self.classListTrain - local totalTestSamples = 0 - -- split the classList into classListTrain and classListTest - for i=1,#self.classes do - local list = self.classList[i] - count = self.classList[i]:size(1) - local splitidx = math.floor((count * self.split / 100) + 0.5) -- +round - local perm = torch.randperm(count) - self.classListTrain[i] = torch.LongTensor(splitidx) - for j=1,splitidx do - self.classListTrain[i][j] = list[perm[j]] - end - if splitidx == count then -- all samples were allocated to train set - self.classListTest[i] = torch.LongTensor() - else - self.classListTest[i] = torch.LongTensor(count-splitidx) - totalTestSamples = totalTestSamples + self.classListTest[i]:size(1) - local idx = 1 - for j=splitidx+1,count do - self.classListTest[i][idx] = list[perm[j]] - idx = idx + 1 - end - end - end - -- Now combine classListTest into a single tensor - self.testIndices = torch.LongTensor(totalTestSamples) - self.testIndicesSize = totalTestSamples - local tdata = self.testIndices:data() - local tidx = 0 - for i=1,#self.classes do - local list = self.classListTest[i] - if list:dim() ~= 0 then - local ldata = list:data() - for j=0,list:size(1)-1 do - tdata[tidx] = ldata[j] - tidx = tidx + 1 - end - end - end - end -end - --- size(), size(class) -function dataset:size(class, list) - list = list or self.classList - if not class then - return self.numSamples - elseif type(class) == 'string' then - return list[self.classIndices[class]]:size(1) - elseif type(class) == 'number' then - return list[class]:size(1) - end -end - --- size(), size(class) -function dataset:sizeTrain(class) - if self.split == 0 then - return 0; - end - if class then - return self:size(class, self.classListTrain) - else - return self.numSamples - self.testIndicesSize - end -end - --- size(), size(class) -function dataset:sizeTest(class) - if self.split == 100 then - return 0 - end - if class then - return self:size(class, self.classListTest) - else - return self.testIndicesSize - end -end - --- by default, just load the image and return it -function dataset:defaultSampleHook(imgpath) - local out = image.load(imgpath, 3, 'float') - out = image.scale(out, self.sampleSize[3], self.sampleSize[2]) - return out -end - --- getByClass -function dataset:getByClass(class) - local index = math.ceil(torch.uniform() * self.classListSample[class]:nElement()) - local imgpath = ffi.string(torch.data(self.imagePath[self.classListSample[class][index]])) - return self:sampleHookTrain(imgpath) -end - --- converts a table of samples (and corresponding labels) to a clean tensor -local function tableToOutput(self, dataTable, scalarTable) - local data, scalarLabels, labels - local quantity = #scalarTable - local samplesPerDraw - if dataTable[1]:dim() == 3 then samplesPerDraw = 1 - else samplesPerDraw = dataTable[1]:size(1) end - data = torch.Tensor(quantity * samplesPerDraw, - self.sampleSize[1], self.sampleSize[2], self.sampleSize[3]) - scalarLabels = torch.LongTensor(quantity * samplesPerDraw) - labels = torch.LongTensor(quantity * samplesPerDraw, #(self.classes)):fill(-1) - for i=1,#dataTable do - local idx = (i-1)*samplesPerDraw - data[{{idx+1,idx+samplesPerDraw}}]:copy(dataTable[i]) - scalarLabels[{{idx+1,idx+samplesPerDraw}}]:fill(scalarTable[i]) - labels[{{idx+1,idx+samplesPerDraw},{scalarTable[i]}}]:fill(1) - end - return data, scalarLabels, labels -end - -function dataset:get(i1, i2) - local indices, quantity - if type(i1) == 'number' then - if type(i2) == 'number' then -- range of indices - indices = torch.range(i1, i2); - quantity = i2 - i1 + 1; - else -- single index - indices = {i1}; quantity = 1 - end - elseif type(i1) == 'table' then - indices = i1; quantity = #i1; -- table - elseif (type(i1) == 'userdata' and i1:nDimension() == 1) then - indices = i1; quantity = (#i1)[1]; -- tensor - else - error('Unsupported input types: ' .. type(i1) .. ' ' .. type(i2)) - end - assert(quantity > 0) - -- now that indices has been initialized, get the samples - local dataTable = {} - local scalarTable = {} - for i=1,quantity do - -- load the sample - local idx = self.testIndices[indices[i]] - local imgpath = ffi.string(torch.data(self.imagePath[idx])) - local out = self:sampleHookTest(imgpath) - table.insert(dataTable, out) - table.insert(scalarTable, self.imageClass[idx]) - end - local data, scalarLabels, labels = tableToOutput(self, dataTable, scalarTable) - return data, scalarLabels, labels -end - -function dataset:test(quantity) - if self.split == 100 then - error('No test mode when you are not splitting the data') - end - local i = 1 - local n = self.testIndicesSize - local qty = quantity or 1 - return function () - if i+qty-1 <= n then - local data, scalarLabelss, labels = self:get(i, i+qty-1) - i = i + qty - return data, scalarLabelss, labels - end - end -end - -return dataset diff --git a/bin/batch-represent/main.lua b/bin/batch-represent/main.lua deleted file mode 100755 index a2fdb13..0000000 --- a/bin/batch-represent/main.lua +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env th - -require 'torch' -require 'optim' - -require 'paths' - -require 'xlua' -require 'csvigo' - -require 'nn' -require 'dpnn' - -local opts = paths.dofile('opts.lua') - -opt = opts.parse(arg) -print(opt) - -torch.setdefaulttensortype('torch.FloatTensor') - -if opt.cuda then - require 'cutorch' - require 'cunn' - cutorch.setDevice(opt.device) -end - -opt.manualSeed = 2 -torch.manualSeed(opt.manualSeed) - -paths.dofile('dataset.lua') -paths.dofile('batch-represent.lua') - -model = torch.load(opt.model) -model:evaluate() -if opt.cuda then - model:cuda() -end - -repsCSV = csvigo.File(paths.concat(opt.outDir, "reps.csv"), 'w') -labelsCSV = csvigo.File(paths.concat(opt.outDir, "labels.csv"), 'w') - -batchRepresent() - -repsCSV:close() -labelsCSV:close() diff --git a/bin/batch-represent/opts.lua b/bin/batch-represent/opts.lua deleted file mode 100644 index bda7c2b..0000000 --- a/bin/batch-represent/opts.lua +++ /dev/null @@ -1,37 +0,0 @@ -local M = { } - --- http://stackoverflow.com/questions/6380820/get-containing-path-of-lua-file -function script_path() - local str = debug.getinfo(2, "S").source:sub(2) - return str:match("(.*/)") -end - -function M.parse(arg) - local cmd = torch.CmdLine() - cmd:text() - cmd:text('OpenFace') - cmd:text() - cmd:text('Options:') - - ------------ General options -------------------- - cmd:option('-outDir', './reps/', 'Subdirectory to output the representations') - cmd:option('-data', - paths.concat(script_path(), '..', 'data', 'lfw', 'dlib-affine-sz:96'), - 'Home of dataset') - cmd:option('-model', - paths.concat(script_path(), '..', '..', 'models', 'nn4.small2.v1.t7'), - 'Path to model to use.') - cmd:option('-imgDim', 96, 'Image dimension. nn1=224, nn4=96') - cmd:option('-batchSize', 50, 'mini-batch size') - cmd:option('-cuda', false, 'Use cuda') - cmd:option('-device', 1, 'Cuda device to use') - cmd:option('-cache', false, 'Cache loaded data.') - cmd:text() - - local opt = cmd:parse(arg or {}) - os.execute('mkdir -p ' .. opt.outDir) - - return opt -end - -return M diff --git a/bin/exif_orientation_normalize.py b/bin/exif_orientation_normalize.py new file mode 100755 index 0000000..67d0b9a --- /dev/null +++ b/bin/exif_orientation_normalize.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +import argparse +from torchvision.datasets import ImageFolder +from PIL import Image + +# EXIF orientation info http://sylvana.net/jpegcrop/exif_orientation.html + +exif_orientation_tag = 0x0112 +exif_transpose_sequences = [ # Val 0th row 0th col + [], # 0 (reserved) + [], # 1 top left + [Image.FLIP_LEFT_RIGHT], # 2 top right + [Image.ROTATE_180], # 3 bottom right + [Image.FLIP_TOP_BOTTOM], # 4 bottom left + [Image.FLIP_LEFT_RIGHT, Image.ROTATE_90], # 5 left top + [Image.ROTATE_270], # 6 right top + [Image.FLIP_TOP_BOTTOM, Image.ROTATE_90], # 7 right bottom + [Image.ROTATE_90], # 8 left bottom +] + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Script for normalizing images orientation based on exif orientation tag. " + "This script will search for exif orientation tag in image, if it exists script will change image " + "orientation to 1 (top, left side). These changes will be saved to image and exif info will be " + "erased from image. If image doesn't have exif info, this script will leave it unchanged") + parser.add_argument('--images-path', required=True, help='Path to folder with images.') + return parser.parse_args() + + +def main(): + args = parse_args() + images = ImageFolder(args.images_path) + + for img_path, y in images.imgs: + img = Image.open(img_path).convert('RGB') + if 'parsed_exif' in img.info: + orientation = img.info['parsed_exif'][exif_orientation_tag] + transposes = exif_transpose_sequences[orientation] + for trans in transposes: + img = img.transpose(trans) + print('Processing {} with orientation {}'.format(img_path, orientation)) + img.save(img_path) + + +if __name__ == '__main__': + main() diff --git a/bin/generate_embeddings.py b/bin/generate_embeddings.py new file mode 100644 index 0000000..85e125f --- /dev/null +++ b/bin/generate_embeddings.py @@ -0,0 +1,43 @@ +import argparse +import os +import numpy as np +import torch +from torchvision import datasets, transforms +from torch.utils.data import DataLoader +from facenet_pytorch.models.inception_resnet_v1 import InceptionResnetV1 + + +BATCH_SIZE = 32 + + +def parse_args(): + parser = argparse.ArgumentParser( + "Script for generating face embeddings. Output of this script is 'embeddings.txt' which contains embeddings " + "for all input images and 'labels.txt' which contains label for every embedding.") + parser.add_argument('--input-folder', required=True, + help='Root folder where *aligned* images are. This folder contains sub-folders for each class.') + parser.add_argument('--output-folder', required=True, + help='Output folder where image embeddings and labels will be saved.') + return parser.parse_args() + + +def main(): + torch.set_grad_enabled(False) + args = parse_args() + + aligned_images = datasets.ImageFolder(args.input_folder, transform=transforms.ToTensor()) + aligned_images.idx_to_class = {v: k for k, v in aligned_images.class_to_idx.items()} + data_loader = DataLoader(aligned_images, batch_size=BATCH_SIZE) + facenet = InceptionResnetV1(pretrained='vggface2').eval() + + embeddings = None + labels = [] + for x, y in data_loader: + embeddings = facenet(x) if embeddings is None else torch.cat([embeddings, facenet(x)], dim=0) + labels += map(lambda idx: aligned_images.idx_to_class[idx], y.detach().numpy().tolist()) + np.savetxt(args.output_folder + os.path.sep + 'embeddings.txt', embeddings.detach().numpy()) + np.savetxt(args.output_folder + os.path.sep + 'labels.txt', np.array(labels, dtype=np.str).reshape(-1, 1), fmt="%s") + + +if __name__ == '__main__': + main() diff --git a/bin/generate_embeddings.sh b/bin/generate_embeddings.sh deleted file mode 100755 index e373ebf..0000000 --- a/bin/generate_embeddings.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -rm -rf data/aligned/* -rm -rf data/features/* - -../util/align-dlib.py ../data/images align outerEyesAndNose ../data/aligned --size 96 -./batch-represent/main.lua -outDir ../data/features -data ../data/aligned diff --git a/face_recognition.py b/face_recognition.py new file mode 100755 index 0000000..222a9f9 --- /dev/null +++ b/face_recognition.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python + +from arsfutura_face_recognition import face_recogniser_factory +import argparse +import base64 +import json +import cv2 +import numpy as np + + +def parse_args(): + parser = argparse.ArgumentParser( + 'Script for recognising faces on picture. Output of this script is json with list of people on picture and ' + 'base64 encoded picture which has bounding boxes of people.') + image_group = parser.add_mutually_exclusive_group(required=True) + image_group.add_argument('--image-path', help='Path to image file.') + image_group.add_argument('--image-bs64', help='Base64 representation of image.') + parser.add_argument('--classifier-path', required=True, help='Path to serialized classifier.') + return parser.parse_args() + + +def base64_to_img(bs64_img): + decoded = base64.b64decode(bs64_img) + return cv2.imdecode(np.frombuffer(decoded, dtype=np.uint8), flags=cv2.IMREAD_COLOR) + + +def img_to_base64(img): + ret, buff = cv2.imencode('.png', img) + return base64.b64encode(buff) + + +def load_image(args): + if args.image_path: + return cv2.imread(args.image_path) + if args.image_bs64: + return base64_to_img(args.image_bs64) + + +def draw_bb_on_img(faces, img): + for face in faces: + cv2.rectangle(img, (int(face.bb.left()), int(face.bb.top())), (int(face.bb.right()), int(face.bb.bottom())), + (0, 255, 0), 2) + cv2.putText(img, "%s %.2f%%" % (face.identity, face.probability), + (int(face.bb.left()), int(face.bb.bottom()) + 20), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (255, 255, 255), 1) + + +def main(): + args = parse_args() + img = load_image(args) + faces = face_recogniser_factory(args)(img) + draw_bb_on_img(faces, img) + print(json.dumps( + { + 'people': list(map(lambda f: f.identity, faces)), + 'img': str(img_to_base64(img), encoding='ascii') + } + )) + + +if __name__ == '__main__': + main() diff --git a/facenet b/facenet new file mode 160000 index 0000000..44c54d8 --- /dev/null +++ b/facenet @@ -0,0 +1 @@ +Subproject commit 44c54d827523e63babc04284dd8efc3d8bde94d6 diff --git a/generate_embeddings.sh b/generate_embeddings.sh new file mode 100755 index 0000000..1e7fee6 --- /dev/null +++ b/generate_embeddings.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +python -m bin.exif_orientation_normalize --images-path $1 +echo -e "\n====================================" +echo "Exif orientation normalization done." +echo "====================================" + +python -m bin.align-mtcnn --input-folder $1 --output-folder $2 +echo -e "\n====================================" +echo "Cropping faces (aligning) done." +echo "====================================" + +python -m bin.generate_embeddings --input-folder $2 --output-folder $3 +echo -e "\n====================================" +echo "Generating face embeddings done." +echo "====================================" \ No newline at end of file diff --git a/models/model.pkl b/models/model.pkl index cd38270..ae5328e 100644 Binary files a/models/model.pkl and b/models/model.pkl differ diff --git a/models/nn4.small2.v1.t7 b/models/nn4.small2.v1.t7 deleted file mode 100644 index 8a0d019..0000000 Binary files a/models/nn4.small2.v1.t7 and /dev/null differ diff --git a/models/shape_predictor_68_face_landmarks.dat.REMOVED.git-id b/models/shape_predictor_68_face_landmarks.dat.REMOVED.git-id deleted file mode 100644 index 09b4303..0000000 --- a/models/shape_predictor_68_face_landmarks.dat.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -e0ec20d69ac9195f056214f071e12afcfe714199 \ No newline at end of file diff --git a/real_time_face_detection.py b/real_time_face_detection.py new file mode 100755 index 0000000..007dcaf --- /dev/null +++ b/real_time_face_detection.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python + +import cv2 +import argparse +from arsfutura_face_recognition import face_recogniser_factory + + +def parse_args(): + parser = argparse.ArgumentParser('Script for real-time face recognition.') + parser.add_argument('--classifier-path', default='models/model.pkl', help='Path to serialized classifier.') + return parser.parse_args() + + +def main(): + args = parse_args() + cap = cv2.VideoCapture(0) + face_recogniser = face_recogniser_factory(args) + + while True: + # Capture frame-by-frame + ret, frame = cap.read() + frame = cv2.flip(frame, 1) + + faces = face_recogniser(frame) + if faces is not None: + for face in faces: + cv2.rectangle(frame, (int(face.bb.left()), int(face.bb.top())), + (int(face.bb.right()), int(face.bb.bottom())), + (0, 255, 0), 2) + cv2.putText(frame, "%s %.2f%%" % (face.identity, face.probability), + (int(face.bb.left()), int(face.bb.bottom()) + 20), cv2.FONT_HERSHEY_TRIPLEX, 0.5, + (255, 255, 255), 1) + + # Display the resulting frame + cv2.imshow('frame', frame) + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + # When everything done, release the captureq + cap.release() + cv2.destroyAllWindows() + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..68a31fd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +requests==2.19.1 +Pillow==6.0.0 +matplotlib==3.1.0 +numpy==1.16.4 +pandas==0.24.2 +seaborn==0.9.0 +skimage==0.0 +scikit_learn==0.21.2 +tensorflow==1.14.0 +torch==1.1.0 +torchvision==0.3.0