From eb15a09f1f6d108342a59663919afab941710b69 Mon Sep 17 00:00:00 2001 From: joizhang Date: Thu, 18 Nov 2021 10:36:30 +0800 Subject: [PATCH 1/6] feat: face recognition pytorch version. --- README.md | 8 ++++---- app/__init__.py | 2 +- app/recognition/{face_net.py => facenet_tf.py} | 0 app/recognition/facenet_torch.py | 0 tests/test_env.py | 8 ++++++-- tests/{test_face_net.py => test_facenet_tf.py} | 2 +- 6 files changed, 12 insertions(+), 8 deletions(-) rename app/recognition/{face_net.py => facenet_tf.py} (100%) create mode 100644 app/recognition/facenet_torch.py rename tests/{test_face_net.py => test_facenet_tf.py} (96%) diff --git a/README.md b/README.md index bb9cd06..2edbf8d 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Face recognition server end #### 参与贡献 -1. Fork 本仓库 -2. 添加修改代码 -3. 提交代码 -4. 新建 Pull Request \ No newline at end of file +1. Fork 本仓库 +2. 添加修改代码 +3. 提交代码 +4. 新建 Pull Request diff --git a/app/__init__.py b/app/__init__.py index fd327b9..902d4b5 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -8,7 +8,7 @@ from flask_moment import Moment from flask_sqlalchemy import SQLAlchemy from config import app_config -from app.recognition.face_net import FaceNet +from app.recognition.facenet_tf import FaceNet db = SQLAlchemy() moment = Moment() diff --git a/app/recognition/face_net.py b/app/recognition/facenet_tf.py similarity index 100% rename from app/recognition/face_net.py rename to app/recognition/facenet_tf.py diff --git a/app/recognition/facenet_torch.py b/app/recognition/facenet_torch.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_env.py b/tests/test_env.py index 0fe967a..4e27364 100644 --- a/tests/test_env.py +++ b/tests/test_env.py @@ -1,14 +1,18 @@ import unittest import tensorflow.compat.v1 as tf +import torch -class TFEnvTestCase(unittest.TestCase): +class EnvTestCase(unittest.TestCase): - def test_env(self): + def test_tensorflow_env(self): gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction=0.5) with tf.Session(config=tf.ConfigProto(gpu_options=gpu_options)) as sess: a = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[2, 3], name='a') b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[3, 2], name='b') c = tf.matmul(a, b) self.assertEqual([[22., 28.], [49., 64.]], sess.run(c).tolist()) + + def test_pytorch_env(self): + self.assertTrue(torch.cuda.is_available()) diff --git a/tests/test_face_net.py b/tests/test_facenet_tf.py similarity index 96% rename from tests/test_face_net.py rename to tests/test_facenet_tf.py index f847a46..a4f282e 100644 --- a/tests/test_face_net.py +++ b/tests/test_facenet_tf.py @@ -4,7 +4,7 @@ from pathlib import Path import numpy as np import tensorflow.compat.v1 as tf -from app.recognition.face_net import FaceNet +from app.recognition.facenet_tf import FaceNet from app.utils import file_utils FILE_PREFIX = Path(Path(__file__).resolve().drive + '/face-recognition-data') -- Gitee From 0f7c5e0f73e0d7b40859ab14505440c4ced6609d Mon Sep 17 00:00:00 2001 From: joizhang Date: Thu, 18 Nov 2021 15:05:34 +0800 Subject: [PATCH 2/6] fix: tensorflow unresolved warning. --- app/recognition/facenet_tf.py | 3 ++- tests/test_facenet_tf.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/recognition/facenet_tf.py b/app/recognition/facenet_tf.py index ef4dc35..25efa06 100644 --- a/app/recognition/facenet_tf.py +++ b/app/recognition/facenet_tf.py @@ -6,6 +6,7 @@ import logging import numpy as np import tensorflow.compat.v1 as tf +from tensorflow.compat.v1 import gfile LOG = logging.getLogger(__name__) @@ -55,7 +56,7 @@ class FaceNet: def create_inference(self, model_path): # await asyncio.sleep(1) - with tf.gfile.FastGFile(model_path, "rb") as f: + with gfile.FastGFile(model_path, "rb") as f: graph_def = tf.GraphDef() graph_def.ParseFromString(f.read()) tf.import_graph_def(graph_def) diff --git a/tests/test_facenet_tf.py b/tests/test_facenet_tf.py index a4f282e..f997aac 100644 --- a/tests/test_facenet_tf.py +++ b/tests/test_facenet_tf.py @@ -3,6 +3,7 @@ from pathlib import Path import numpy as np import tensorflow.compat.v1 as tf +from tensorflow.compat.v1 import gfile from app.recognition.facenet_tf import FaceNet from app.utils import file_utils @@ -16,7 +17,7 @@ class FaceNetTestCase(unittest.TestCase): def test_model_node(self): model_path = str(MODEL_PATH_PREFIX / '20180402-114759' / '20180402-114759.pb') - with tf.gfile.FastGFile(model_path, "rb") as f: + with gfile.FastGFile(model_path, "rb") as f: graph_def = tf.GraphDef() graph_def.ParseFromString(f.read()) tf.import_graph_def(graph_def) -- Gitee From 2e9419a4c30655d2155b8be2d4979929f7874eae Mon Sep 17 00:00:00 2001 From: joizhang Date: Thu, 18 Nov 2021 21:05:10 +0800 Subject: [PATCH 3/6] feat: Add pytorch facenet. --- app/__init__.py | 8 ++- app/api/__init__.py | 1 + app/api/face.py | 39 +++++++++++++ app/api/users.py | 32 +--------- app/recognition/face_recognition.py | 11 ++++ app/recognition/{facenet_tf.py => facenet.py} | 57 +++++++++++++----- app/recognition/facenet_torch.py | 0 app/utils/file_utils.py | 58 +++++++++++-------- config.py | 1 + tests/{test_facenet_tf.py => test_facenet.py} | 15 ++++- 10 files changed, 150 insertions(+), 72 deletions(-) create mode 100644 app/api/face.py create mode 100644 app/recognition/face_recognition.py rename app/recognition/{facenet_tf.py => facenet.py} (61%) delete mode 100644 app/recognition/facenet_torch.py rename tests/{test_facenet_tf.py => test_facenet.py} (75%) diff --git a/app/__init__.py b/app/__init__.py index 902d4b5..fe665ac 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -8,11 +8,12 @@ from flask_moment import Moment from flask_sqlalchemy import SQLAlchemy from config import app_config -from app.recognition.facenet_tf import FaceNet +from app.recognition.facenet import FaceNetTF, FaceNetTorch db = SQLAlchemy() moment = Moment() -face_net = FaceNet() +face_net_tf = FaceNetTF() +face_net_torch = FaceNetTorch() def create_app(config): @@ -29,7 +30,8 @@ def create_app(config): db.init_app(app) moment.init_app(app) if not app.testing: - face_net.init_app(app) + face_net_tf.init_app(app) + face_net_torch.init_app(app) from .errors import bp as errors_bp app.register_blueprint(errors_bp) diff --git a/app/api/__init__.py b/app/api/__init__.py index 90286e6..8466333 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -4,3 +4,4 @@ bp = Blueprint('api', __name__) from . import users from . import foo +from . import face diff --git a/app/api/face.py b/app/api/face.py new file mode 100644 index 0000000..74e860f --- /dev/null +++ b/app/api/face.py @@ -0,0 +1,39 @@ +import logging +import pickle + +import numpy as np +from flask import jsonify, request +from werkzeug.exceptions import BadRequest + +from app import face_net_tf +from app.model.models import User, Photo +from app.recognition import compare +from app.utils import file_utils +from . import bp + +LOG = logging.getLogger(__name__) + + +@bp.route('/face/recognition', methods=['POST']) +def face_recognition(): + data = request.json or {} + if 'photo' not in data: + raise BadRequest('Must include photo.') + photo = data['photo'] + input_images = [file_utils.resize_blob_to_160x160(photo)] + embedding = face_net_tf.get_embeddings(input_images) + photos = Photo.query.all() + x, y = [], [] + for p in photos: + x.append(pickle.loads(p.embedding)) + y.append(p.user_id) + x = np.array(x) + y = np.array(y) + LOG.info([embedding.shape, x.shape, y.shape]) + target = compare.face_net_compare(embedding, x, y) + if target == 0: + user = User() + user.from_dict({'id': 0, 'name': '无法识别', 'cell_phone_number': ''}) + else: + user = User.query.filter_by(id=int(target)).first() + return jsonify(success=True, data=user.to_dict(), status_code=200) diff --git a/app/api/users.py b/app/api/users.py index e7f367b..bc1756c 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -1,15 +1,12 @@ import logging -import pickle -import numpy as np from flask import jsonify, request from werkzeug.exceptions import BadRequest -from app import db, face_net +from app import db, face_net_tf from app.model.models import User, Photo from app.utils import file_utils, crypto_utils from . import bp -from app.recognition import compare LOG = logging.getLogger(__name__) @@ -40,35 +37,10 @@ def add_user_face(): photo_count = Photo.query.filter_by(id=user.id).count() for i in range(len(photos)): input_images = [file_utils.resize_blob_to_160x160(photos[i])] - embedding = face_net.get_embeddings(input_images)[0].dumps() + embedding = face_net_tf.get_embeddings(input_images)[0].dumps() photo_name = crypto_utils.encrypt(user.name + '-' + user.cell_phone_number + '-' + str(i + 1 + photo_count)) file_utils.write_webcam_blob(photos[i], photo_name) p = Photo(name=photo_name, embedding=embedding, user_id=user.id) db.session.add(p) db.session.commit() return jsonify(success=True, data=None, status_code=200) - - -@bp.route('/user/recognition', methods=['POST']) -def face_recognition(): - data = request.json or {} - if 'photo' not in data: - raise BadRequest('Must include photo.') - photo = data['photo'] - input_images = [file_utils.resize_blob_to_160x160(photo)] - embedding = face_net.get_embeddings(input_images) - photos = Photo.query.all() - x, y = [], [] - for p in photos: - x.append(pickle.loads(p.embedding)) - y.append(p.user_id) - x = np.array(x) - y = np.array(y) - LOG.info([embedding.shape, x.shape, y.shape]) - target = compare.face_net_compare(embedding, x, y) - if target == 0: - user = User() - user.from_dict({'id': 0, 'name': '无法识别', 'cell_phone_number': ''}) - else: - user = User.query.filter_by(id=int(target)).first() - return jsonify(success=True, data=user.to_dict(), status_code=200) diff --git a/app/recognition/face_recognition.py b/app/recognition/face_recognition.py new file mode 100644 index 0000000..1c5d46f --- /dev/null +++ b/app/recognition/face_recognition.py @@ -0,0 +1,11 @@ +class FaceRecognition: + __shared_state = {} + + def __init__(self): + self.__dict__ = self.__shared_state + + def init_app(self, app): + raise NotImplementedError + + def get_embeddings(self, input_images): + raise NotImplementedError diff --git a/app/recognition/facenet_tf.py b/app/recognition/facenet.py similarity index 61% rename from app/recognition/facenet_tf.py rename to app/recognition/facenet.py index 25efa06..4d7699e 100644 --- a/app/recognition/facenet_tf.py +++ b/app/recognition/facenet.py @@ -4,13 +4,19 @@ from __future__ import print_function import logging +import numpy import numpy as np import tensorflow.compat.v1 as tf +import torch.cuda +from facenet_pytorch import InceptionResnetV1 from tensorflow.compat.v1 import gfile +from torchvision.transforms import transforms + +from .face_recognition import FaceRecognition LOG = logging.getLogger(__name__) -CONFIG_OPTIONS = ['FACE_NET_MODEL_PATH'] +CONFIG_OPTIONS = ['FACE_NET_MODEL_PATH', 'FACE_NET_TORCH_HOME'] DEFAULT_OPTIONS = dict(model_path='') @@ -30,23 +36,17 @@ def get_app_dict(app_instance): } -class FaceNet: - __shared_state = {} - - def __init__(self, app=None, phase_train_placeholder=False, images_placeholder=None, embeddings=None, sess=None): - self.__dict__ = self.__shared_state - self.state = 'Init' - self.phase_train_placeholder = phase_train_placeholder - self.images_placeholder = images_placeholder - self.embeddings = embeddings - self.sess = sess +class FaceNetTF(FaceRecognition): + def __init__(self, app=None): + super().__init__() + self.phase_train_placeholder = False + self.images_placeholder = None + self.embeddings = None + self.sess = None if app is not None: self.init_app(app) - def __str__(self): - return self.state - def init_app(self, app): if 'FACE_NET_MODEL_PATH' not in app.config: LOG.error('Please config model path') @@ -78,3 +78,32 @@ class FaceNet: for embedding in embeddings: str_embeddings.append(str(embedding, encoding='utf8')) return str_embeddings + + +class FaceNetTorch(FaceRecognition): + + def __init__(self, app=None): + super().__init__() + self.model = None + self.transform = transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) + ]) + if app is not None: + self.init_app(app) + + def init_app(self, app): + self.create_model() + + def create_model(self): + # set TORCH_HOME in your os environment + self.model = InceptionResnetV1(pretrained='vggface2') + assert torch.cuda.is_available() + self.model.cuda() + self.model.eval() + + def get_embeddings(self, input_images: numpy.ndarray): + x = self.transform(input_images) + x = x.unsqueeze(0).cuda() + embeddings = self.model(x) + return embeddings diff --git a/app/recognition/facenet_torch.py b/app/recognition/facenet_torch.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/utils/file_utils.py b/app/utils/file_utils.py index 7edb689..6813968 100644 --- a/app/utils/file_utils.py +++ b/app/utils/file_utils.py @@ -2,7 +2,7 @@ import base64 import re from pathlib import Path -import cv2 +import cv2.cv2 as cv2 import numpy as np FILE_PREFIX = Path(Path(__file__).resolve().drive + '/face-recognition-data') @@ -22,12 +22,12 @@ def write_webcam_blob(blob: str, file_name: str) -> bool: data = result.groupdict().get("data") else: raise Exception("Do not parse!") - img = base64.urlsafe_b64decode(data) + image = base64.urlsafe_b64decode(data) if not FILE_PREFIX.exists(): FILE_PREFIX.mkdir(parents=True) write_file_path = "{}.{}".format(str(FILE_PREFIX / file_name), ext) with open(write_file_path, 'wb') as f: - f.write(img) + f.write(image) return True @@ -41,33 +41,45 @@ def image_to_base64(image_path) -> str: return image_base64_format -def image_decode(base64_data: str): - img = base64.b64decode(base64_data.split(',')[1]) - np_data = np.fromstring(img, np.uint8) +def decode_image(base64_data: str): + image = str(base64.b64decode(base64_data.split(',')[1])) + np_data = np.fromstring(image, np.uint8) return cv2.imdecode(np_data, cv2.COLOR_BGR2RGB) -def _resize_to_160x160(img): - height, width = img.shape[:2] - res = img - if height < FACE_NET_INPUT_SIZE or width < FACE_NET_INPUT_SIZE: - diff = FACE_NET_INPUT_SIZE - min(height, width) - res = cv2.resize(img, (width + diff, height + diff), interpolation=cv2.INTER_AREA) - elif height == width: - res = cv2.resize(img, (FACE_NET_INPUT_SIZE, FACE_NET_INPUT_SIZE), interpolation=cv2.INTER_AREA) - height, width = res.shape[:2] +def _isotropically_resize_image(image, size, interpolation_down=cv2.INTER_AREA, interpolation_up=cv2.INTER_CUBIC): + h, w = image.shape[:2] + if max(w, h) == size: + return image + if w > h: + scale = size / w + h = h * scale + w = size + else: + scale = size / h + w = w * scale + h = size + interpolation = interpolation_up if scale > 1 else interpolation_down + resized = cv2.resize(image, (int(w), int(h)), interpolation=interpolation) + return resized + + +def _resize_to_160x160(image: np.ndarray) -> np.ndarray: + image = _isotropically_resize_image(image, FACE_NET_INPUT_SIZE) + height, width = image.shape[:2] x_center, y_center = height // 2, width // 2 x1, y1, x2, y2 = x_center - 80, y_center - 80, x_center + 80, y_center + 80 assert x2 - x1 == 160 and y2 - y1 == 160 - res = res[x1:x2, y1:y2] - return res + cropped = image[x1:x2, y1:y2] + return cropped -def resize_blob_to_160x160(blob: str): - img = image_decode(blob) - return _resize_to_160x160(img) +def resize_blob_to_160x160(blob: str) -> np.ndarray: + image = decode_image(blob) + return _resize_to_160x160(image) -def resize_image_to_160x160(image: str): - img = cv2.imread(image) - return _resize_to_160x160(img) +def resize_image_to_160x160(image_path: str) -> np.ndarray: + image = cv2.imread(image_path, cv2.IMREAD_COLOR) + image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + return _resize_to_160x160(image) diff --git a/config.py b/config.py index 30c76bc..6fc7e5e 100644 --- a/config.py +++ b/config.py @@ -15,6 +15,7 @@ class Config: SQLALCHEMY_ECHO = (os.getenv("SQLALCHEMY_ECHO") == 'true') SQLALCHEMY_TRACK_MODIFICATIONS = (os.getenv("SQLALCHEMY_TRACK_MODIFICATIONS") == 'true') FACE_NET_MODEL_PATH = os.getenv('FACE_NET_MODEL_PATH') + FACE_NET_TORCH_HOME = os.getenv('TORCH_HOME') @staticmethod def init_app(app): diff --git a/tests/test_facenet_tf.py b/tests/test_facenet.py similarity index 75% rename from tests/test_facenet_tf.py rename to tests/test_facenet.py index f997aac..c33efae 100644 --- a/tests/test_facenet_tf.py +++ b/tests/test_facenet.py @@ -5,7 +5,7 @@ import numpy as np import tensorflow.compat.v1 as tf from tensorflow.compat.v1 import gfile -from app.recognition.facenet_tf import FaceNet +from app.recognition.facenet import FaceNetTF, FaceNetTorch from app.utils import file_utils FILE_PREFIX = Path(Path(__file__).resolve().drive + '/face-recognition-data') @@ -36,8 +36,19 @@ class FaceNetTestCase(unittest.TestCase): image = str(FILE_PREFIX / 'test1-15922865715-1.jpg') res = file_utils.resize_image_to_160x160(image) model_path = str(MODEL_PATH_PREFIX / '20170512-110547' / '20170512-110547.pb') - face_net = FaceNet() + face_net = FaceNetTF() face_net.create_inference(model_path) embedding = face_net.get_embeddings(np.array([res]))[0] self.assertEqual(128, len(embedding)) print(embedding.dumps()) + + +class FaceNetTorchTestCase(unittest.TestCase): + + def test_get_embedding(self): + image_path = str(FILE_PREFIX / 'Aaron_Eckhart_0001.jpg') + image = file_utils.resize_image_to_160x160(image_path) + face_net = FaceNetTorch() + face_net.create_model() + embedding = face_net.get_embeddings(image) + self.assertEqual(512, len(embedding[0])) -- Gitee From cbf02cfbb23eaf5ef7ce1df7e39b99b073570871 Mon Sep 17 00:00:00 2001 From: joizhang Date: Fri, 19 Nov 2021 10:24:15 +0800 Subject: [PATCH 4/6] feat: Add date of creation. --- app/api/users.py | 16 ++++++++++++++-- app/model/models.py | 4 +++- sql/scheme.sql | 39 ++++----------------------------------- 3 files changed, 21 insertions(+), 38 deletions(-) diff --git a/app/api/users.py b/app/api/users.py index bc1756c..d80687d 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -19,8 +19,8 @@ def get_users(): return jsonify(success=True, data=data, status_code=200) -@bp.route('/user/face', methods=['POST']) -def add_user_face(): +@bp.route('/user', methods=['POST']) +def add_user(): data = request.json or {} if 'name' not in data or 'cell_phone_number' not in data or 'photos' not in data: raise BadRequest('Must include name, cell_phone_number and photos fields.') @@ -44,3 +44,15 @@ def add_user_face(): db.session.add(p) db.session.commit() return jsonify(success=True, data=None, status_code=200) + + +@bp.route('/user', methods=['DELETE']) +def delete_user(): + user_id = request.args.get('id', default=0, type=int) + if user_id == 0: + raise BadRequest('Invalid Request!') + u = User() + u.id = user_id + db.session.delete(u) + db.session.commit() + return jsonify(success=True, data=None, status_code=200) diff --git a/app/model/models.py b/app/model/models.py index a33e891..2c85a6a 100644 --- a/app/model/models.py +++ b/app/model/models.py @@ -1,6 +1,7 @@ from flask import url_for from app import db +from datetime import datetime class PaginatedAPIMixin(object): @@ -32,6 +33,7 @@ class User(PaginatedAPIMixin, db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(32), nullable=False) cell_phone_number = db.Column(db.String(32), nullable=False) + create_date = db.Columb(db.DateTime, default=datetime.utcnow) photos = db.relationship('Photo', backref='user', lazy=True) def __repr__(self): @@ -54,9 +56,9 @@ class User(PaginatedAPIMixin, db.Model): class Photo(db.Model): __tablename__ = 't_photo' id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, nullable=False) name = db.Column(db.String(512), nullable=False) embedding = db.Column(db.BLOB(), nullable=False) - user_id = db.Column(db.Integer, db.ForeignKey('t_user.id'), nullable=False) def __repr__(self): return ''.format(self.photo_path) diff --git a/sql/scheme.sql b/sql/scheme.sql index 2cdaea6..46d1f72 100644 --- a/sql/scheme.sql +++ b/sql/scheme.sql @@ -1,56 +1,25 @@ -- 用户表 +drop table if exists t_user; create table t_user ( id int not null auto_increment, name varchar(32) not null, cell_phone_number varchar(32) not null, + create_date datetime not null, primary key (`id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4; --- 签到表 t_sign_in -# create table t_sign_in -# ( -# id int not null primary key auto_increment, -# name int not null, -# start_time datetime not null, -# end_time datetime not null -# ); - --- 用户照片表 -# drop table if exists t_user_photo; -# create table t_user_photo -# ( -# user_id int not null, -# photo_id int not null, -# constraint userid2 foreign key (user_id) references t_user (id), -# constraint photoid2 foreign key (photo_id) references t_photo (id) -# ); - - -- 照片表 drop table if exists t_photo; create table t_photo ( id int not null auto_increment, + user_id int not null, name varchar(512) not null, embedding blob not null, - user_id int not null, - primary key (`id`), - constraint userid foreign key (user_id) references t_user (id) + primary key (`id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4; - --- 用户签到表 t_user_sign -# create table t_user_sign -# ( -# user_id int not null, -# sign_id int not null, -# constraint userid3 foreign key (user_id) references t_user (id), -# constraint signid3 foreign key (sign_id) references t_sign_in (id) -# ); - - - -- Gitee From da634ca6113dffc7a02f90d9813d4e59154da073 Mon Sep 17 00:00:00 2001 From: joizhang Date: Fri, 19 Nov 2021 10:55:04 +0800 Subject: [PATCH 5/6] feat: Apply facenet pytorch. --- app/__init__.py | 4 ++-- app/api/users.py | 14 +++++++------- app/model/models.py | 2 +- app/recognition/face_recognition.py | 5 ++++- app/recognition/facenet.py | 21 ++++++++------------- tests/test_facenet.py | 18 +++++++++--------- 6 files changed, 31 insertions(+), 33 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index fe665ac..6be8d26 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -12,7 +12,7 @@ from app.recognition.facenet import FaceNetTF, FaceNetTorch db = SQLAlchemy() moment = Moment() -face_net_tf = FaceNetTF() +# face_net_tf = FaceNetTF() face_net_torch = FaceNetTorch() @@ -30,7 +30,7 @@ def create_app(config): db.init_app(app) moment.init_app(app) if not app.testing: - face_net_tf.init_app(app) + # face_net_tf.init_app(app) face_net_torch.init_app(app) from .errors import bp as errors_bp diff --git a/app/api/users.py b/app/api/users.py index d80687d..1721818 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -3,7 +3,7 @@ import logging from flask import jsonify, request from werkzeug.exceptions import BadRequest -from app import db, face_net_tf +from app import db, face_net_torch from app.model.models import User, Photo from app.utils import file_utils, crypto_utils from . import bp @@ -30,17 +30,17 @@ def add_user(): u = User() u.from_dict(data) db.session.add(u) - db.session.commit() user = u # Photo photos = data['photos'] photo_count = Photo.query.filter_by(id=user.id).count() for i in range(len(photos)): - input_images = [file_utils.resize_blob_to_160x160(photos[i])] - embedding = face_net_tf.get_embeddings(input_images)[0].dumps() - photo_name = crypto_utils.encrypt(user.name + '-' + user.cell_phone_number + '-' + str(i + 1 + photo_count)) + input_image = file_utils.resize_blob_to_160x160(photos[i]) + embedding = face_net_torch.get_embeddings(input_image) + photo_name = f'{user.name}-{user.cell_phone_number}-{str(i + 1 + photo_count)}' + photo_name = crypto_utils.encrypt(photo_name) file_utils.write_webcam_blob(photos[i], photo_name) - p = Photo(name=photo_name, embedding=embedding, user_id=user.id) + p = Photo(name=photo_name, embedding=embedding.dumps(), user_id=user.id) db.session.add(p) db.session.commit() return jsonify(success=True, data=None, status_code=200) @@ -50,7 +50,7 @@ def add_user(): def delete_user(): user_id = request.args.get('id', default=0, type=int) if user_id == 0: - raise BadRequest('Invalid Request!') + raise BadRequest('Illegal Request!') u = User() u.id = user_id db.session.delete(u) diff --git a/app/model/models.py b/app/model/models.py index 2c85a6a..67cc7dc 100644 --- a/app/model/models.py +++ b/app/model/models.py @@ -33,7 +33,7 @@ class User(PaginatedAPIMixin, db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(32), nullable=False) cell_phone_number = db.Column(db.String(32), nullable=False) - create_date = db.Columb(db.DateTime, default=datetime.utcnow) + create_date = db.Column(db.DateTime, default=datetime.utcnow) photos = db.relationship('Photo', backref='user', lazy=True) def __repr__(self): diff --git a/app/recognition/face_recognition.py b/app/recognition/face_recognition.py index 1c5d46f..3d804b4 100644 --- a/app/recognition/face_recognition.py +++ b/app/recognition/face_recognition.py @@ -1,3 +1,6 @@ +import numpy + + class FaceRecognition: __shared_state = {} @@ -7,5 +10,5 @@ class FaceRecognition: def init_app(self, app): raise NotImplementedError - def get_embeddings(self, input_images): + def get_embeddings(self, input_image) -> numpy.ndarray: raise NotImplementedError diff --git a/app/recognition/facenet.py b/app/recognition/facenet.py index 4d7699e..44dc824 100644 --- a/app/recognition/facenet.py +++ b/app/recognition/facenet.py @@ -67,17 +67,11 @@ class FaceNetTF(FaceRecognition): self.sess = tf.Session() self.sess.run(tf.global_variables_initializer()) - def get_embeddings(self, input_images): - feed_dict = {self.images_placeholder: np.array(input_images), self.phase_train_placeholder: False} + def get_embeddings(self, input_image: np.ndarray): + x = np.expand_dims(input_image, axis=0) + feed_dict = {self.images_placeholder: x, self.phase_train_placeholder: False} embeddings = self.sess.run(self.embeddings, feed_dict=feed_dict) - return embeddings - - def get_str_embeddings(self, input_images): - embeddings = self.get_embeddings(input_images) - str_embeddings = [] - for embedding in embeddings: - str_embeddings.append(str(embedding, encoding='utf8')) - return str_embeddings + return embeddings[0] class FaceNetTorch(FaceRecognition): @@ -102,8 +96,9 @@ class FaceNetTorch(FaceRecognition): self.model.cuda() self.model.eval() - def get_embeddings(self, input_images: numpy.ndarray): - x = self.transform(input_images) + def get_embeddings(self, input_image: numpy.ndarray): + x = self.transform(input_image) x = x.unsqueeze(0).cuda() embeddings = self.model(x) - return embeddings + embeddings = embeddings.detach().cpu().numpy() + return embeddings[0] diff --git a/tests/test_facenet.py b/tests/test_facenet.py index c33efae..040a10b 100644 --- a/tests/test_facenet.py +++ b/tests/test_facenet.py @@ -15,8 +15,13 @@ MODEL_PATH_PREFIX = Path(__file__).parents[1] class FaceNetTestCase(unittest.TestCase): + def test_model_exists(self): + model_path = MODEL_PATH_PREFIX / '20170512-110547' / '20170512-110547.pb' + print(model_path) + self.assertTrue(model_path.exists()) + def test_model_node(self): - model_path = str(MODEL_PATH_PREFIX / '20180402-114759' / '20180402-114759.pb') + model_path = str(MODEL_PATH_PREFIX / '20170512-110547' / '20170512-110547.pb') with gfile.FastGFile(model_path, "rb") as f: graph_def = tf.GraphDef() graph_def.ParseFromString(f.read()) @@ -27,18 +32,13 @@ class FaceNetTestCase(unittest.TestCase): for operation in operations: print(operation.name) - def test_model_exists(self): - model_path = MODEL_PATH_PREFIX / '20180402-114759' / '20180402-114759.pb' - print(model_path) - self.assertTrue(model_path.exists()) - def test_get_embeddings(self): - image = str(FILE_PREFIX / 'test1-15922865715-1.jpg') - res = file_utils.resize_image_to_160x160(image) + image_path = str(FILE_PREFIX / 'Aaron_Eckhart_0001.jpg') + image = file_utils.resize_image_to_160x160(image_path) model_path = str(MODEL_PATH_PREFIX / '20170512-110547' / '20170512-110547.pb') face_net = FaceNetTF() face_net.create_inference(model_path) - embedding = face_net.get_embeddings(np.array([res]))[0] + embedding = face_net.get_embeddings(np.array([image]))[0] self.assertEqual(128, len(embedding)) print(embedding.dumps()) -- Gitee From 97f284b17191bb1c19da17e19554f6581ca971f1 Mon Sep 17 00:00:00 2001 From: joizhang Date: Fri, 19 Nov 2021 15:22:55 +0800 Subject: [PATCH 6/6] feat: Delete user. --- app/api/face.py | 6 ++--- app/api/users.py | 19 +++++++++------ app/model/models.py | 1 - app/utils/file_utils.py | 51 +++++++++++++++++++++++++--------------- tests/test_file_utils.py | 10 ++++---- 5 files changed, 52 insertions(+), 35 deletions(-) diff --git a/app/api/face.py b/app/api/face.py index 74e860f..f17a572 100644 --- a/app/api/face.py +++ b/app/api/face.py @@ -5,7 +5,7 @@ import numpy as np from flask import jsonify, request from werkzeug.exceptions import BadRequest -from app import face_net_tf +from app import face_net_torch from app.model.models import User, Photo from app.recognition import compare from app.utils import file_utils @@ -20,8 +20,8 @@ def face_recognition(): if 'photo' not in data: raise BadRequest('Must include photo.') photo = data['photo'] - input_images = [file_utils.resize_blob_to_160x160(photo)] - embedding = face_net_tf.get_embeddings(input_images) + input_image = file_utils.resize_blob_to_160x160(photo) + embedding = face_net_torch.get_embeddings(input_image) photos = Photo.query.all() x, y = [], [] for p in photos: diff --git a/app/api/users.py b/app/api/users.py index 1721818..c107ddd 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -30,16 +30,17 @@ def add_user(): u = User() u.from_dict(data) db.session.add(u) - user = u + user = User.query.filter_by(name=data['name'], cell_phone_number=data['cell_phone_number']).first() # Photo photos = data['photos'] photo_count = Photo.query.filter_by(id=user.id).count() for i in range(len(photos)): input_image = file_utils.resize_blob_to_160x160(photos[i]) embedding = face_net_torch.get_embeddings(input_image) - photo_name = f'{user.name}-{user.cell_phone_number}-{str(i + 1 + photo_count)}' - photo_name = crypto_utils.encrypt(photo_name) - file_utils.write_webcam_blob(photos[i], photo_name) + data, ext = file_utils.split_data_ext(photos[i]) + photo_name = '{}-{}-{}'.format(user.name, user.cell_phone_number, str(i + 1 + photo_count)) + photo_name = '{}.{}'.format(crypto_utils.encrypt(photo_name), ext) + file_utils.write_photo(data, photo_name) p = Photo(name=photo_name, embedding=embedding.dumps(), user_id=user.id) db.session.add(p) db.session.commit() @@ -51,8 +52,12 @@ def delete_user(): user_id = request.args.get('id', default=0, type=int) if user_id == 0: raise BadRequest('Illegal Request!') - u = User() - u.id = user_id - db.session.delete(u) + photos = Photo.query.filter_by(user_id=user_id).all() + if photos is not None: + for photo in photos: + file_utils.remove_photo(photo.name) + db.session.delete(photo) + user = User.query.filter_by(id=user_id).first() + db.session.delete(user) db.session.commit() return jsonify(success=True, data=None, status_code=200) diff --git a/app/model/models.py b/app/model/models.py index 67cc7dc..e324546 100644 --- a/app/model/models.py +++ b/app/model/models.py @@ -34,7 +34,6 @@ class User(PaginatedAPIMixin, db.Model): name = db.Column(db.String(32), nullable=False) cell_phone_number = db.Column(db.String(32), nullable=False) create_date = db.Column(db.DateTime, default=datetime.utcnow) - photos = db.relationship('Photo', backref='user', lazy=True) def __repr__(self): return ''.format(self.name) diff --git a/app/utils/file_utils.py b/app/utils/file_utils.py index 6813968..c3ba57a 100644 --- a/app/utils/file_utils.py +++ b/app/utils/file_utils.py @@ -1,4 +1,5 @@ import base64 +import os.path import re from pathlib import Path @@ -10,27 +11,38 @@ FILE_PREFIX = Path(Path(__file__).resolve().drive + '/face-recognition-data') FACE_NET_INPUT_SIZE = 160 -def write_webcam_blob(blob: str, file_name: str) -> bool: - """Store file to disk, the name is "name-cell_phone_number-index.webp" - :param blob: webp data - :param file_name: username - :return: - """ +def split_data_ext(blob): result = re.search("data:image/(?P.*?);base64,(?P.*)", blob, re.DOTALL) if result: ext = result.groupdict().get("ext") data = result.groupdict().get("data") else: - raise Exception("Do not parse!") + raise ValueError("Do not parse!") + return data, ext + + +def write_photo(data: str, file_name: str) -> bool: + """Store file to disk, the name is "name-cell phone number-index.webp" + :param data: webp data + :param file_name: username + :return: + """ image = base64.urlsafe_b64decode(data) if not FILE_PREFIX.exists(): FILE_PREFIX.mkdir(parents=True) - write_file_path = "{}.{}".format(str(FILE_PREFIX / file_name), ext) - with open(write_file_path, 'wb') as f: + file_path = FILE_PREFIX / file_name + with open(file_path, 'wb') as f: f.write(image) return True +def remove_photo(file_name: str) -> bool: + file_path = FILE_PREFIX / file_name + if file_path.exists(): + os.remove(file_path) + return True + + def image_to_base64(image_path) -> str: """Transfer imagee to base64 string""" ext = image_path.split('.')[-1] @@ -42,23 +54,24 @@ def image_to_base64(image_path) -> str: def decode_image(base64_data: str): - image = str(base64.b64decode(base64_data.split(',')[1])) - np_data = np.fromstring(image, np.uint8) - return cv2.imdecode(np_data, cv2.COLOR_BGR2RGB) + image = base64.b64decode(base64_data.split(',')[1]) + image = np.frombuffer(image, np.uint8) + return cv2.imdecode(image, cv2.IMREAD_COLOR) def _isotropically_resize_image(image, size, interpolation_down=cv2.INTER_AREA, interpolation_up=cv2.INTER_CUBIC): + """Ensure min side >= size""" h, w = image.shape[:2] - if max(w, h) == size: + if min(w, h) == size: return image - if w > h: - scale = size / w - h = h * scale - w = size - else: + if h < w: scale = size / h w = w * scale h = size + else: + scale = size / w + h = h * scale + w = size interpolation = interpolation_up if scale > 1 else interpolation_down resized = cv2.resize(image, (int(w), int(h)), interpolation=interpolation) return resized @@ -82,4 +95,4 @@ def resize_blob_to_160x160(blob: str) -> np.ndarray: def resize_image_to_160x160(image_path: str) -> np.ndarray: image = cv2.imread(image_path, cv2.IMREAD_COLOR) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) - return _resize_to_160x160(image) + return _resize_to_160x160(image) \ No newline at end of file diff --git a/tests/test_file_utils.py b/tests/test_file_utils.py index d42fda8..4463854 100644 --- a/tests/test_file_utils.py +++ b/tests/test_file_utils.py @@ -1,7 +1,7 @@ import unittest from pathlib import Path -import cv2 +from cv2 import cv2 from app.utils import file_utils @@ -21,7 +21,7 @@ class FileUtilsTestCase(unittest.TestCase): blob = """ data:image/jpg;base64,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 """ - self.assertTrue(file_utils.write_webcam_blob(blob, 'test1-15922865715-1')) + self.assertTrue(file_utils.write_photo(blob, 'test1-15922865715-1')) def test_resize_image_to_160x160(self): image = str(FILE_PREFIX / 'test1-15922865715-1.jpg') @@ -35,6 +35,6 @@ class FileUtilsTestCase(unittest.TestCase): blob = """ data:image/jpg;base64,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 """ - res = file_utils.resize_blob_to_160x160(blob) - cv2.imwrite(str(FILE_PREFIX / 'test1-15922865715-2.jpg'), res) - self.assertEqual((160, 160), res.shape[:2]) + image = file_utils.resize_blob_to_160x160(blob) + cv2.imwrite(str(FILE_PREFIX / 'test1-15922865715-2.jpg'), image) + self.assertEqual((160, 160), image.shape[:2]) -- Gitee