diff --git a/README.md b/README.md index bb9cd06c2d6877f142b00dffb9cf305979c2fe8f..2edbf8dc6fec5a207dc64301ffbc9d01a3525b8d 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 fd327b9259cd937406a0abfb4d0fc9975e463406..6be8d26135107230d6cda7935ee58c5aedad0156 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.face_net 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 90286e68ecd65e6d8b7c52676da97acc797b3401..8466333f112fe7e2d53b089a4efd4026ab177a94 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 0000000000000000000000000000000000000000..f17a5726212a9bda8ffbef969dc62c759670a936 --- /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_torch +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_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: + 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 e7f367be84faf1f2ec4656871f2f8639b476141e..c107ddd4d22073607a59167e7aaa3739f1672641 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_torch 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__) @@ -22,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.') @@ -33,42 +30,34 @@ def add_user_face(): u = User() u.from_dict(data) db.session.add(u) - db.session.commit() - 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_images = [file_utils.resize_blob_to_160x160(photos[i])] - embedding = face_net.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) + input_image = file_utils.resize_blob_to_160x160(photos[i]) + embedding = face_net_torch.get_embeddings(input_image) + 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() 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) +@bp.route('/user', methods=['DELETE']) +def delete_user(): + user_id = request.args.get('id', default=0, type=int) + if user_id == 0: + raise BadRequest('Illegal Request!') + 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 a33e891288f4ce54004e3ea02a18031aaf40dacd..e3245464fadc80a0b273019a07a50cb232ef2aaf 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,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) - photos = db.relationship('Photo', backref='user', lazy=True) + create_date = db.Column(db.DateTime, default=datetime.utcnow) def __repr__(self): return ''.format(self.name) @@ -54,9 +55,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/app/recognition/face_recognition.py b/app/recognition/face_recognition.py new file mode 100644 index 0000000000000000000000000000000000000000..3d804b441a9dba746de85c7f4f4e0641a6955fec --- /dev/null +++ b/app/recognition/face_recognition.py @@ -0,0 +1,14 @@ +import numpy + + +class FaceRecognition: + __shared_state = {} + + def __init__(self): + self.__dict__ = self.__shared_state + + def init_app(self, app): + raise NotImplementedError + + def get_embeddings(self, input_image) -> numpy.ndarray: + raise NotImplementedError diff --git a/app/recognition/face_net.py b/app/recognition/facenet.py similarity index 48% rename from app/recognition/face_net.py rename to app/recognition/facenet.py index ef4dc35962e4ef3fffd538541b73d4956a4da5dc..44dc82463019369360abbe719f5b71dc699bc20e 100644 --- a/app/recognition/face_net.py +++ b/app/recognition/facenet.py @@ -4,12 +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='') @@ -29,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') @@ -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) @@ -66,14 +67,38 @@ class FaceNet: 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): + + 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_image: numpy.ndarray): + x = self.transform(input_image) + x = x.unsqueeze(0).cuda() + embeddings = self.model(x) + embeddings = embeddings.detach().cpu().numpy() + return embeddings[0] diff --git a/app/utils/file_utils.py b/app/utils/file_utils.py index 7edb689fd650f1df0aae215a91cf5a02556783d5..c3ba57af322839a073fc54faf478839dc2335a01 100644 --- a/app/utils/file_utils.py +++ b/app/utils/file_utils.py @@ -1,8 +1,9 @@ import base64 +import os.path 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') @@ -10,24 +11,35 @@ 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!") - img = base64.urlsafe_b64decode(data) + 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: - f.write(img) + 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 @@ -41,33 +53,46 @@ 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) - return cv2.imdecode(np_data, cv2.COLOR_BGR2RGB) +def decode_image(base64_data: str): + 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 min(w, h) == size: + return image + 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 -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 _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) \ No newline at end of file diff --git a/config.py b/config.py index 30c76bc07e9ae5ed409ea65ad8b8cdc8a5b936fa..6fc7e5e333d86d3e91edd466e2ade1bb78cf45fb 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/sql/scheme.sql b/sql/scheme.sql index 2cdaea63ab04de650fa02df46ffb841cff9104af..46d1f72d74cf2e730e0a027ca9105ad1462a9c76 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) -# ); - - - diff --git a/tests/test_env.py b/tests/test_env.py index 0fe967ae899e384fb6ecff977cfa4cc14299276b..4e273649600f3b8cdf344d5a8ffe07454b3177d5 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.py similarity index 52% rename from tests/test_face_net.py rename to tests/test_facenet.py index f847a46e1f0568c248220998d053aaae16795654..040a10bad609a5491bfd2cab9374e6548d7ec198 100644 --- a/tests/test_face_net.py +++ b/tests/test_facenet.py @@ -3,8 +3,9 @@ from pathlib import Path import numpy as np import tensorflow.compat.v1 as tf +from tensorflow.compat.v1 import gfile -from app.recognition.face_net 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') @@ -14,9 +15,14 @@ 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') - with tf.gfile.FastGFile(model_path, "rb") as f: + 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()) tf.import_graph_def(graph_def) @@ -26,17 +32,23 @@ 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 = FaceNet() + 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()) + + +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])) diff --git a/tests/test_file_utils.py b/tests/test_file_utils.py index d42fda8b3a9a62a344aeae786defa01f13563f85..4463854c0761f6107d3f0673ef6db539165499b8 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])