Skip to content

Commit

Permalink
feat: use OpenCV instead of Microsoft's API
Browse files Browse the repository at this point in the history
  • Loading branch information
Ovyerus committed May 21, 2019
1 parent af8d3da commit e86f999
Show file tree
Hide file tree
Showing 10 changed files with 45,652 additions and 108 deletions.
13 changes: 13 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# editorconfig.org
root = true

[*]
indent_size = 4
indent_style = space
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
37 changes: 13 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# deeppyer
![banner image](./banner.jpg)

deeppyer is an image deepfryer written in Python using [Pillow](https://python-pillow.org/
) and using the [Microsoft Facial Recognition API](https://azure.microsoft.com/services/cognitive-services/face/).
deeppyer is an image deepfryer written in Python using [Pillow](https://python-pillow.org/)
and [OpenCV](https://pypi.org/project/opencv-python/).

NOTE: This *requires* at least Python v3.6 in order to run.

Expand All @@ -13,23 +13,23 @@ You can either use deeppyer as a module, or straight from the command line.
```
$ python deeppyer.py -h
usage: deeppyer.py [-h] [-v] [-t TOKEN] [-o OUTPUT] FILE
usage: deeppyer.py [-h] [-v] [-o OUTPUT] [-f] FILE
Deepfry an image, optionally adding lens flares for eyes.
Deepfry an image.
positional arguments:
FILE File to deepfry.
optional arguments:
-h, --help show this help message and exit
-v, --version Display program version.
-t TOKEN, --token TOKEN
Token to use for facial recognition API.
-o OUTPUT, --output OUTPUT
Filename to output to.
-f, --flares Try and detected faces for adding lens flares.
```

When a token is supplied, the script will automatically try to add lens flares for the eyes, otherwise it won't.
By default, flares will try to be added to the image, unless you're using the CLI script,
in which case it is off by default.

### Program usage
```py
Expand All @@ -38,22 +38,21 @@ import deeppyer, asyncio

async def main():
img = Image.open('./foo.jpg')
img = await deeppyer.deepfry(img, token='optional token')
img = await deeppyer.deepfry(img)
img.save('./bar.jpg')

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
```

## API Documentation
#### `async deeppyer.deepfry(img: PIL.Image, *, token: str=None, url_base: str='westcentralus', session: aiohttp.ClientSession=None)`
#### `async deepfry(img: Image, type=DeepfryTypes.RED, *, flares: bool = True)`
Deepfry a given image.

**Arguments**
- *img* (PIL.Image) - Image to apply the deepfry effect on.
- *[token]* (str) - Token to use for the facial recognition API. Defining this will add lens flares to the eyes of a face in the image.
- *[url_base]* (str='westcentralus') - URL base to use for the facial recognition API. Can either be `westus`, `eastus2`, `westcentralus`, `westeurope` or `southeastasia`.
- *[session]* (aiohttp.ClientSession) - Optional session to use when making the request to the API. May make it a tad faster if you already have a created session, and allows you to give it your own options.
- *[type]* (DeepfryTypes) - Colours to apply on the image.
- *[flares] (bool) - Whether or not to try and detect faces for applying lens flares.

Returns:
`PIL.Image` - Deepfried image.
Expand All @@ -62,17 +61,7 @@ Returns:
¯\\\_(ツ)_/¯ Why not

## Contributing
If you wish to contribute something to this, go ahead! Just please try to keep your code similar-ish to mine, and make sure that it works with the tests.
If you wish to contribute something to this, go ahead! Just please make sure to format it with flake8 + isort, and that the test(s) pass fine.

## Testing
Create a file in [tests](./tests) called `token.json` with the following format:
```json
{
"token": "",
"url_base": ""
}
```
`token` is your token for the facial recognition API.
`url_base` is optional, and is for if your token is from a different region.

After that, simply run `test.py` and make sure that all the images output as you want.
Simply run `tests/test.py` and make sure that all the images output properly.
137 changes: 64 additions & 73 deletions deeppyer.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
from PIL import Image, ImageOps, ImageEnhance
from io import BytesIO
import argparse
import asyncio
from collections import namedtuple
from enum import Enum
import aiohttp, asyncio, math, argparse
import math

from PIL import Image, ImageOps, ImageEnhance
import cv2
import numpy


# TODO: instead, take in tuples of colours
class DeepfryTypes(Enum):
"""
Enum for the various possible effects added to the image.
Expand All @@ -18,67 +25,57 @@ class Colours:
WHITE = (255,) * 3


# TODO: Replace face recognition API with something like OpenCV.
face_cascade = cv2.CascadeClassifier('./face_cascade.xml')
eye_cascade = cv2.CascadeClassifier('./eye_cascade.xml')
flare_img = Image.open('./flare.png')

async def deepfry(img: Image, *, token: str=None, url_base: str='westcentralus', session: aiohttp.ClientSession=None, type=DeepfryTypes.RED) -> Image:
"""
Deepfry an image.
img: PIL.Image - Image to deepfry.
[token]: str - Token to use for Microsoft facial recognition API. If this is not supplied, lens flares will not be added.
[url_base]: str = 'westcentralus' - API base to use. Only needed if your key's region is not `westcentralus`.
[session]: aiohttp.ClientSession - Optional session to use with API requests. If provided, may provide a bit more speed.
Returns: PIL.Image - Deepfried image.
FlarePosition = namedtuple('FlarePosition', ['x', 'y', 'size'])


async def deepfry(img: Image, type=DeepfryTypes.RED, *, flares: bool = True) -> Image:
"""
img = img.copy().convert('RGB')
Deepfry a given image.
:param img: Image to manipulate.
:param type: Colours to apply on the image.
:param flares: Whether or not to try and detect faces for applying lens flares.
:type img: PIL.Image
:type type: DeepfryTypes
:type flares: bool
:returns: Deepfried image.
:rtype: PIL.Image
"""
if type not in DeepfryTypes:
raise ValueError(f'Unknown deepfry type "{type}", expected a value from deeppyer.DeepfryTypes')

if token:
req_url = f'https://{url_base}.api.cognitive.microsoft.com/face/v1.0/detect?returnFaceId=false&returnFaceLandmarks=true' # WHY THE FUCK IS THIS SO LONG
headers = {
'Content-Type': 'application/octet-stream',
'Ocp-Apim-Subscription-Key': token,
'User-Agent': 'DeepPyer/1.0'
}
b = BytesIO()

img.save(b, 'jpeg')
b.seek(0)

if session:
async with session.post(req_url, headers=headers, data=b.read()) as r:
face_data = await r.json()
else:
async with aiohttp.ClientSession() as s, s.post(req_url, headers=headers, data=b.read()) as r:
face_data = await r.json()

if 'error' in face_data:
err = face_data['error']
code = err.get('code', err.get('statusCode'))
msg = err['message']

raise Exception(f'Error with Microsoft Face Recognition API\n{code}: {msg}')

if face_data:
landmarks = face_data[0]['faceLandmarks']

# Get size and positions of eyes, and generate sizes for the flares
eye_left_width = math.ceil(landmarks['eyeLeftInner']['x'] - landmarks['eyeLeftOuter']['x'])
eye_left_height = math.ceil(landmarks['eyeLeftBottom']['y'] - landmarks['eyeLeftTop']['y'])
eye_left_corner = (landmarks['eyeLeftOuter']['x'], landmarks['eyeLeftTop']['y'])
flare_left_size = eye_left_height if eye_left_height > eye_left_width else eye_left_width
flare_left_size *= 4
eye_left_corner = tuple(math.floor(x - flare_left_size / 2.5 + 5) for x in eye_left_corner)

eye_right_width = math.ceil(landmarks['eyeRightOuter']['x'] - landmarks['eyeRightInner']['x'])
eye_right_height = math.ceil(landmarks['eyeRightBottom']['y'] - landmarks['eyeRightTop']['y'])
eye_right_corner = (landmarks['eyeRightInner']['x'], landmarks['eyeRightTop']['y'])
flare_right_size = eye_right_height if eye_right_height > eye_right_width else eye_right_width
flare_right_size *= 4
eye_right_corner = tuple(math.floor(x - flare_right_size / 2.5 + 5) for x in eye_right_corner)
img = img.copy().convert('RGB')
flare_positions = []

if flares:
opencv_img = cv2.cvtColor(numpy.array(img), cv2.COLOR_RGB2GRAY)

faces = face_cascade.detectMultiScale(
opencv_img,
scaleFactor=1.3,
minNeighbors=5,
minSize=(30, 30),
flags=cv2.CASCADE_SCALE_IMAGE
)

for (x, y, w, h) in faces:
face_roi = opencv_img[y:y+h, x:x+w] # Get region of interest (detected face)

eyes = eye_cascade.detectMultiScale(face_roi)

for (ex, ey, ew, eh) in eyes:
eye_corner = (ex + ew / 2, ey + eh / 2)
flare_size = eh if eh > ew else ew
flare_size *= 4
corners = [math.floor(x) for x in eye_corner]
eye_corner = FlarePosition(*corners, flare_size)

flare_positions.append(eye_corner)

# Crush image to hell and back
img = img.convert('RGB')
Expand All @@ -103,32 +100,26 @@ async def deepfry(img: Image, *, token: str=None, url_base: str='westcentralus',
img = Image.blend(img, r, 0.75)
img = ImageEnhance.Sharpness(img).enhance(100.0)

if token and face_data:
# Copy and resize flares
flare = Image.open('./flare.png')
flare_left = flare.copy().resize((flare_left_size,) * 2, resample=Image.BILINEAR)
flare_right = flare.copy().resize((flare_right_size,) * 2, resample=Image.BILINEAR)

del flare

img.paste(flare_left, eye_left_corner, flare_left)
img.paste(flare_right, eye_right_corner, flare_right)
# Apply flares on any detected eyes
for flare in flare_positions:
flare_transformed = flare_img.copy().resize((flare.size,) * 2, resample=Image.BILINEAR)
img.paste(flare_transformed, (flare.x, flare.y), flare_transformed)

return img

if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Deepfry an image, optionally adding lens flares for eyes.')
parser = argparse.ArgumentParser(description='Deepfry an image.')
parser.add_argument('-v', '--version', action='version', version='%(prog)s 1.0', help='Display program version.')
parser.add_argument('-t', '--token', help='Token to use for facial recognition API.')
parser.add_argument('-o', '--output', help='Filename to output to.')
parser.add_argument('-f', '--flares', help='Try and detected faces for adding lens flares.', action='store_true',
default=False)
parser.add_argument('file', metavar='FILE', help='File to deepfry.')
args = parser.parse_args()

token = args.token
img = Image.open(args.file)
out = args.output or './deepfried.jpg'

loop = asyncio.get_event_loop()
img = loop.run_until_complete(deepfry(img, token=token))
img = loop.run_until_complete(deepfry(img, flares=args.flares))

img.save(out, 'jpeg')
img.save(out, 'jpeg')
2 changes: 0 additions & 2 deletions dependencies.txt

This file was deleted.

Loading

0 comments on commit e86f999

Please sign in to comment.