Skip to content
This repository has been archived by the owner on Nov 8, 2021. It is now read-only.

Commit

Permalink
Python SDK update for BUILD 2017 release. (#23)
Browse files Browse the repository at this point in the history
* requirements: add pillow, reorder alphabetically.

* Add start, top parameters for list persons.

* More robust response parser.

* Sample update:

- Support more face attributes.
- Fix image rotation bug.
- Minor enhancement and bug fix.

* Bump version to 1.3.0.

* Minor change in result shown.

* Update list person description.
  • Loading branch information
huxuan authored May 8, 2017
1 parent 6156abf commit 9f0a1dc
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 88 deletions.
17 changes: 12 additions & 5 deletions cognitive_face/person.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,20 +145,27 @@ def get_face(person_group_id, person_id, persisted_face_id):
return util.request('GET', url)


def lists(person_group_id):
"""List all persons in a person group, and retrieve person information
(including `person_id`, `name`, `user_data` and `persisited_face_ids` of
registered faces of the person).
def lists(person_group_id, start=None, top=None):
"""List `top` persons in a person group with `person_id` greater than
`start`, and retrieve person information (including `person_id`, `name`,
`user_data` and `persisited_face_ids` of registered faces of the person).
Args:
person_group_id: `person_group_id` of the target person group.
start: List persons from the least `person_id` greater than this.
top: The number of persons to list, rangeing in [1, 1000]. Default is
1000;
Returns:
An array of person information that belong to the person group.
"""
url = 'persongroups/{}/persons'.format(person_group_id)
params = {
'start': start,
'top': top,
}

return util.request('GET', url)
return util.request('GET', url, params=params)


def update(person_group_id, person_id, name=None, user_data=None):
Expand Down
10 changes: 7 additions & 3 deletions cognitive_face/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,13 @@ def request(method, url, data=None, json=None, headers=None, params=None):
result = None
# `person_group.train` return 202 status code for success.
if response.status_code not in (200, 202):
print('status_code: {}'.format(response.status_code))
print('response: {}'.format(response.text))
error_msg = response.json()['error']
try:
error_msg = response.json()['error']
except:
raise CognitiveFaceException(
response.status_code,
response.status_code,
response.text)
raise CognitiveFaceException(
response.status_code,
error_msg.get('code'),
Expand Down
5 changes: 3 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
requests
pep8
pillow
pyflakes
pylint
pylint
requests
91 changes: 66 additions & 25 deletions sample/model/face.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,90 @@
File: face.py
Description: Face model for Python SDK Sample.
"""

import wx

import util


class Rect(object):
"""Face Rectangle."""
def __init__(self, rect):
super(Rect, self).__init__()
self.set_rect(rect)

def set_rect(self, rect):
"""docstring for set_rect"""
self.left = int(rect['left'])
self.top = int(rect['top'])
self.width = int(rect['width'])
self.height = int(rect['height'])


class Attribute(object):
"""Attributes for face."""
def __init__(self, attr):
super(Attribute, self).__init__()
self.set_attr(attr)

def set_attr(self, attr):
"""Set the attribute value."""
self.gender = attr['gender']
self.age = int(attr['age'])
if not attr['hair']['hairColor']:
if attr['hair']['invisible']:
self.hair = 'Invisible'
else:
self.hair = 'Bald'
else:
self.hair = max(
attr['hair']['hairColor'],
key=lambda x: x['confidence']
)['color']
self.facial_hair = sum(attr['facialHair'].values()) > 0 and 'Yes' \
or 'No'
self.makeup = any(attr['makeup'].values())
self.emotion = util.key_with_max_value(attr['emotion'])
self.occlusion = any(attr['occlusion'].values())
self.exposure = attr['exposure']['exposureLevel']
self.head_pose = "Pitch: {}, Roll:{}, Yaw:{}".format(
attr['headPose']['pitch'],
attr['headPose']['roll'],
attr['headPose']['yaw']
)
if not attr['accessories']:
self.accessories = 'NoAccessories'
else:
self.accessories = ' '.join(
[str(x['type']) for x in attr['accessories']]
)


class Face(object):
"""Face Model for each face."""
def __init__(self, res, path, size=util.MAX_THUMBNAIL_SIZE):
super(Face, self).__init__()
self.path = path
self.bmp = wx.Bitmap(path)
img = util.rotate_image(path)
self.bmp = img.ConvertToBitmap()
self.name = None
if res.get('faceId'):
self.id = res['faceId']
if res.get('persistedFaceId'):
self.persisted_id = res['persistedFaceId']
if res.get('faceRectangle'):
rect = res['faceRectangle']
self.left = int(rect['left'])
self.top = int(rect['top'])
self.width = int(rect['width'])
self.height = int(rect['height'])
self.rect = Rect(res['faceRectangle'])
self.bmp = self.bmp.GetSubBitmap(wx.Rect(
self.left, self.top, self.width, self.height))
self.rect.left,
self.rect.top,
self.rect.width,
self.rect.height,
))
if res.get('faceAttributes'):
attr = res['faceAttributes']
self.age = int(attr['age'])
self.gender = attr['gender']
self.head_pose = "Pitch: {}, Roll:{}, Yaw:{}".format(
attr['headPose']['pitch'],
attr['headPose']['roll'],
attr['headPose']['yaw']
)
self.smile = float(attr['smile']) > 0 and 'Smile' or 'Not Smile'
self.facial_hair = sum(attr['facialHair'].values()) > 0 and 'Yes' \
or 'No'
self.glasses = attr['glasses']
self.emotion = max(
attr['emotion'],
key=lambda key: attr['emotion'][key]
)
self.bmp = util.scale_bitmap(self.bmp, size)
self.attr = Attribute(res['faceAttributes'])
self.bmp = util.scale_image(
self.bmp.ConvertToImage(),
size=size,
).ConvertToBitmap()

def set_name(self, name):
"""Set the name for the face."""
Expand Down
58 changes: 47 additions & 11 deletions sample/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
"""

from threading import Thread
import operator
import os.path

from PIL import Image
import wx

try:
Expand All @@ -25,14 +27,21 @@
MAX_THUMBNAIL_SIZE = 75
STYLE = wx.SIMPLE_BORDER
SUBSCRIPTION_KEY_FILENAME = 'Subscription.txt'
ORIENTATION_TAG = 274

LOG_FACE_LIST_REQUEST = (
'Request: Face List {} will be used for build person database. '
'Checking whether group exists.'
)
LOG_FACE_LIST_NOT_EXIST = 'Response: Face List {} does not exist before.'
LOG_FACE_LIST_EXIST = 'Response: Face List {} exists.'
LABEL_FACE = '{} years old, {}\n{}\n{}, {}\nFacial Hair: {}\nEmotion: {}\n'
LABEL_FACE = (
'{}, {} years old\n'
'Hair: {}, Facial Hair: {}\n'
'Makeup: {}, Emotion: {}\n'
'Occluded: {}, Exposure: {}\n'
'{}\n{}\n'
)


class SubscriptionKey(object):
Expand Down Expand Up @@ -69,9 +78,8 @@ def delete(cls):
CF.Key.set(cls.key)


def scale_bitmap(bitmap, size=MAX_IMAGE_SIZE):
"""Scale the image."""
img = bitmap.ConvertToImage()
def scale_image(img, size=MAX_IMAGE_SIZE):
"""Scale the wx.Image."""
width = img.GetWidth()
height = img.GetHeight()
if width > height:
Expand All @@ -81,7 +89,23 @@ def scale_bitmap(bitmap, size=MAX_IMAGE_SIZE):
new_height = size
new_width = size * width / height
img = img.Scale(new_width, new_height)
return wx.BitmapFromImage(img)
return img


def rotate_image(path):
"""Rotate the image from path and return wx.Image."""
img = Image.open(path)
try:
exif = img._getexif()
if exif[ORIENTATION_TAG] == 3:
img = img.rotate(180, expand=True)
elif exif[ORIENTATION_TAG] == 6:
img = img.rotate(270, expand=True)
elif exif[ORIENTATION_TAG] == 8:
img = img.rotate(90, expand=True)
except:
pass
return pil_image_to_wx_image(img)


def draw_bitmap_rectangle(bitmap, faces):
Expand All @@ -98,20 +122,32 @@ def draw_bitmap_rectangle(bitmap, faces):
wx.FONTWEIGHT_BOLD))
for face in faces:
dc.DrawRectangle(
face.left * bitmap.scale,
face.top * bitmap.scale,
face.width * bitmap.scale,
face.height * bitmap.scale,
face.rect.left * bitmap.scale,
face.rect.top * bitmap.scale,
face.rect.width * bitmap.scale,
face.rect.height * bitmap.scale,
)
if face.name:
text_width, text_height = dc.GetTextExtent(face.name)
dc.DrawText(face.name,
face.left * bitmap.scale,
face.top * bitmap.scale - text_height)
face.rect.left * bitmap.scale,
face.rect.top * bitmap.scale - text_height)
dc.SelectObject(wx.NullBitmap)
bitmap.bitmap.SetBitmap(bitmap.bmp)


def pil_image_to_wx_image(pil_image):
"""Convert from PIL image to wx image."""
wx_image = wx.EmptyImage(pil_image.width, pil_image.height)
wx_image.SetData(pil_image.convert("RGB").tobytes())
return wx_image


def key_with_max_value(item):
"""Get the key with maximum value in a dict."""
return max(item.iteritems(), key=operator.itemgetter(1))[0]


def async(func):
"""Async wrapper."""
def wrapper(*args, **kwargs):
Expand Down
50 changes: 20 additions & 30 deletions sample/view/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,42 +37,30 @@ def __init__(self, parent, bitmap=wx.NullBitmap, size=util.MAX_IMAGE_SIZE):

def set_path(self, path):
"""Set the image path."""
bitmap = wx.Bitmap(path)
self.bmp = util.scale_bitmap(bitmap, size=self.size)

width = bitmap.GetWidth()
new_width = self.bmp.GetWidth()

self.scale = 1.0 * new_width / width

self.bitmap.SetBitmap(self.bmp)
self.sizer.Layout()

def set_bmp(self, bitmap):
"""Set the image bitmap."""
self.bmp = util.scale_bitmap(bitmap, size=self.size)
width = bitmap.GetWidth()
new_width = self.bmp.GetWidth()

img = util.rotate_image(path)
width = img.GetWidth()
img = util.scale_image(img, size=self.size)
new_width = img.GetWidth()
self.scale = 1.0 * new_width / width

self.bmp = img.ConvertToBitmap()
self.bitmap.SetBitmap(self.bmp)
self.sizer.Layout()


class MyGridStaticBitmap(wx.Panel):
"""Base Grid StaticBitmap."""
def __init__(self, parent, rows=1, cols=0, vgap=0, hgap=0,
size=util.MAX_IMAGE_SIZE):
size=util.MAX_THUMBNAIL_SIZE):
super(MyGridStaticBitmap, self).__init__(parent)
self.sizer = wx.GridSizer(rows, cols, vgap, hgap)
self.SetSizer(self.sizer)
self.size = size

def set_paths(self, paths):
"""Set the paths for the images."""
self.sizer.Clear(True)
for path in paths:
bitmap = MyStaticBitmap(self, size=util.MAX_THUMBNAIL_SIZE)
bitmap = MyStaticBitmap(self, size=self.size)
bitmap.set_path(path)
self.sizer.Add(bitmap)
self.SetSizerAndFit(self.sizer)
Expand All @@ -82,8 +70,7 @@ def set_faces(self, faces):
"""Set the faces."""
self.sizer.Clear(True)
for face in faces:
bitmap = MyStaticBitmap(self, bitmap=face.bmp,
size=util.MAX_THUMBNAIL_SIZE)
bitmap = MyStaticBitmap(self, bitmap=face.bmp, size=self.size)
self.sizer.Add(bitmap)
self.SetSizerAndFit(self.sizer)
self.sizer.Layout()
Expand Down Expand Up @@ -231,7 +218,7 @@ def OnMeasureItem(self, index):
"""OnMeasureItem for Layout."""
face = self.faces[index]
bmp_height = face.bmp.GetHeight() + 4
label_height = self.GetTextExtent(face.glasses)[1] * 4 + 8
label_height = self.GetTextExtent(face.attr.gender)[1] * 6
return max(bmp_height, label_height)

def OnDrawItem(self, dc, rect, index):
Expand All @@ -243,13 +230,16 @@ def OnDrawItem(self, dc, rect, index):
textx = rect.x + 2 + face.bmp.GetWidth() + 2
label_rect = wx.Rect(textx, rect.y, rect.width - textx, rect.height)
label = util.LABEL_FACE.format(
face.age,
face.gender,
face.head_pose,
face.smile,
face.glasses,
face.facial_hair,
face.emotion
face.attr.gender,
face.attr.age,
face.attr.hair,
face.attr.facial_hair,
face.attr.makeup,
face.attr.emotion,
face.attr.occlusion,
face.attr.exposure,
face.attr.head_pose,
face.attr.accessories
)
dc.DrawLabel(label, label_rect, wx.ALIGN_LEFT | wx.ALIGN_TOP)

Expand Down
Loading

0 comments on commit 9f0a1dc

Please sign in to comment.