Skip to content

Commit

Permalink
Implement face mask
Browse files Browse the repository at this point in the history
  • Loading branch information
Prodesire committed Jan 31, 2020
1 parent f3f8111 commit 623d055
Show file tree
Hide file tree
Showing 12 changed files with 263 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,5 @@ dmypy.json

# Pyre type checker
.pyre/

.idea/
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
include requirements.txt README.md LICENSE
include face_mask/images/*.png
43 changes: 43 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Env
export PYTHONDONTWRITEBYTECODE=1

# Func
.PHONY: docs

help:
@echo "\033[32minit\033[0m"
@echo " Init environment for face-mask."
@echo "\033[32mclean\033[0m"
@echo " Remove python and build artifacts."
@echo "\033[32mclean-pyc\033[0m"
@echo " Remove python artifacts."
@echo "\033[32mclean-build\033[0m"
@echo " Remove build artifacts."
@echo "\033[32mpublish\033[0m"
@echo " Publish to pypi."

init:
pip install -r requirements.txt

clean: clean-pyc clean-build clean-test

clean-pyc:
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
find . -name '*.log' -exec rm -f {} +
find . -name '*~' -exec rm -f {} +
find . -name '__pycache__' -exec rm -rf {} +

clean-build:
rm -rf build dist *.egg-info .eggs

clean-test:
find . -name '.pytest_cache' -exec rm -rf {} +
find . -name '.log' -exec rm -rf {} +

publish: clean
python setup.py bdist_wheel
python setup.py sdist
twine check dist/*
twine upload dist/*
make clean
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,37 @@
# face-mask
Wear a face mask in the given picture.

## Install
Install by pip:
```bash
pip install face-mask
```

Or install locally (`cd` project root directory first):
```bash
python3 setup.py install
```

## Usage
Specify face picture path and then auto save to face with mask picture path
(whose name with "-with-mask" suffix).
```bash
face-mask /path/to/face/picture
```

If you want see the new picture with default viewer, please specify `--show` option.
```bash
face-mask /path/to/face/picture --show
```

## Effect
### One person wears a mask
![](images/face-mask-single.jpg)

### Many persons wear masks
![](images/face-mask-multi.jpg)

### Comic persons wear masks
Attention: Face recognition for comic face is not accurate enough.

![](images/face-mask-comic.jpg)
Empty file added face_mask/__init__.py
Empty file.
139 changes: 139 additions & 0 deletions face_mask/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import os
import sys
import argparse
import numpy as np
from PIL import Image, ImageFile

__version__ = '0.1.0'


IMAGE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'images')
DEFAULT_IMAGE_PATH = os.path.join(IMAGE_DIR, 'default-mask.png')


def cli():
parser = argparse.ArgumentParser(description='Wear a face mask in the given picture.')
parser.add_argument('pic_path', help='Picture path.')
parser.add_argument('--show', action='store_true', help='Whether show picture with mask or not.')
args = parser.parse_args()

pic_path = args.pic_path
if not os.path.exists(args.pic_path):
print(f'Picture {pic_path} not exists.')
sys.exit(1)

FaceMasker(pic_path, args.show).mask()


class FaceMasker:
KEY_FACIAL_FEATURES = ('nose_bridge', 'chin')

def __init__(self, face_path, show=False):
self.face_path = face_path
self.show = show
self._face_img: ImageFile = None
self._mask_img: ImageFile = None

def mask(self):
import face_recognition

face_image_np = face_recognition.load_image_file(self.face_path)
face_landmarks = face_recognition.face_landmarks(face_image_np)
self._face_img = Image.fromarray(face_image_np)
self._mask_img = Image.open(DEFAULT_IMAGE_PATH)

found_face = False
for face_landmark in face_landmarks:
# check whether facial features meet requirement
skip = False
for facial_feature in self.KEY_FACIAL_FEATURES:
if facial_feature not in face_landmark:
skip = True
break
if skip:
continue

# mask face
found_face = True
self._mask_face(face_landmark)

if found_face:
if self.show:
self._face_img.show()

# save
self._save()
else:
print('Found no face.')

def _mask_face(self, face_landmark: dict):
nose_bridge = face_landmark['nose_bridge']
nose_point = nose_bridge[len(nose_bridge) * 1 // 4]
nose_v = np.array(nose_point)

chin = face_landmark['chin']
chin_len = len(chin)
chin_bottom_point = chin[chin_len // 2]
chin_bottom_v = np.array(chin_bottom_point)
chin_left_point = chin[chin_len // 8]
chin_right_point = chin[chin_len * 7 // 8]

# split mask and resize
width = self._mask_img.width
height = self._mask_img.height
width_ratio = 1.2
new_height = int(np.linalg.norm(nose_v - chin_bottom_v))

# left
mask_left_img = self._mask_img.crop((0, 0, width // 2, height))
mask_left_width = self.get_distance_from_point_to_line(chin_left_point, nose_point, chin_bottom_point)
mask_left_width = int(mask_left_width * width_ratio)
mask_left_img = mask_left_img.resize((mask_left_width, new_height))

# right
mask_right_img = self._mask_img.crop((width // 2, 0, width, height))
mask_right_width = self.get_distance_from_point_to_line(chin_right_point, nose_point, chin_bottom_point)
mask_right_width = int(mask_right_width * width_ratio)
mask_right_img = mask_right_img.resize((mask_right_width, new_height))

# merge mask
size = (mask_left_img.width + mask_right_img.width, new_height)
mask_img = Image.new('RGBA', size)
mask_img.paste(mask_left_img, (0, 0), mask_left_img)
mask_img.paste(mask_right_img, (mask_left_img.width, 0), mask_right_img)

# rotate mask
angle = np.arctan2(chin_bottom_point[1] - nose_point[1], chin_bottom_point[0] - nose_point[0])
rotated_mask_img = mask_img.rotate(angle, expand=True)

# calculate mask location
center_x = (nose_point[0] + chin_bottom_point[0]) // 2
center_y = (nose_point[1] + chin_bottom_point[1]) // 2

offset = mask_img.width // 2 - mask_left_img.width
radian = angle * np.pi / 180
box_x = center_x + int(offset * np.cos(radian)) - rotated_mask_img.width // 2
box_y = center_y + int(offset * np.sin(radian)) - rotated_mask_img.height // 2

# add mask
self._face_img.paste(mask_img, (box_x, box_y), mask_img)

def _save(self):
path_splits = os.path.splitext(self.face_path)
new_face_path = path_splits[0] + '-with-mask' + path_splits[1]
self._face_img.save(new_face_path)
print(f'Save to {new_face_path}')

@staticmethod
def get_distance_from_point_to_line(point, line_point1, line_point2):
distance = np.abs((line_point2[1] - line_point1[1]) * point[0] +
(line_point1[0] - line_point2[0]) * point[1] +
(line_point2[0] - line_point1[0]) * line_point1[1] +
(line_point1[1] - line_point2[1]) * line_point1[0]) / \
np.sqrt((line_point2[1] - line_point1[1]) * (line_point2[1] - line_point1[1]) +
(line_point1[0] - line_point2[0]) * (line_point1[0] - line_point2[0]))
return int(distance)


if __name__ == '__main__':
cli()
Binary file added face_mask/images/default-mask.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/face-mask-comic.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/face-mask-multi.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/face-mask-single.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
face_recognition==1.2.3
Pillow==7.0.0
numpy==1.18.1
39 changes: 39 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from face_mask.__main__ import __version__
from setuptools import setup, find_packages


install_requirements = []
for line in open('requirements.txt'):
requirement = line.strip()
if requirement:
install_requirements.append(requirement)


setup(
name="face-mask",
version=__version__,
description="Useful data structures, utils for Python.",
long_description=open('README.md').read(),
author="Prodesire",
author_email='[email protected]',
license='MIT License',
url="https://github.com/Prodesire/face-mask",
install_requires=install_requirements,
packages=find_packages(),
classifiers=[
'Operating System :: OS Independent',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python :: Implementation',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Topic :: Software Development :: Libraries'
],
entry_points={
'console_scripts': ['face-mask=face_mask.__main__:cli'],
},
include_package_data=True,
)

0 comments on commit 623d055

Please sign in to comment.