├── tests ├── __init__.py ├── images │ ├── face1.jpg │ ├── face2.jpg │ ├── vision.jpg │ └── face-group.jpg └── projectoxford_tests │ ├── __init__.py │ ├── TestClient.py │ ├── TestEmotion.py │ ├── TestPerson.py │ ├── TestPersonGroup.py │ ├── TestVision.py │ └── TestFace.py ├── requirements.txt ├── projectoxford ├── __init__.py ├── Emotion.py ├── Client.py ├── Base.py ├── Vision.py ├── Face.py ├── PersonGroup.py └── Person.py ├── MANIFEST.in ├── .travis.yml ├── setup.cfg ├── project-oxford-python.sln ├── .gitignore ├── LICENSE ├── DESCRIPTION.rst ├── setup.py ├── project-oxford-python.pyproj └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.6.0 2 | -------------------------------------------------------------------------------- /projectoxford/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['Base', 'Client', 'Face', 'Person', 'PersonGroup', 'Vision', 'Emotion'] 2 | -------------------------------------------------------------------------------- /tests/images/face1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/southwood/project-oxford-python/HEAD/tests/images/face1.jpg -------------------------------------------------------------------------------- /tests/images/face2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/southwood/project-oxford-python/HEAD/tests/images/face2.jpg -------------------------------------------------------------------------------- /tests/images/vision.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/southwood/project-oxford-python/HEAD/tests/images/vision.jpg -------------------------------------------------------------------------------- /tests/images/face-group.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/southwood/project-oxford-python/HEAD/tests/images/face-group.jpg -------------------------------------------------------------------------------- /tests/projectoxford_tests/__init__.py: -------------------------------------------------------------------------------- 1 | from . import TestClient 2 | from . import TestFace 3 | from . import TestPersonGroup 4 | from . import TestVision 5 | from . import TestEmotion 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include DESCRIPTION.rst 2 | include README.md 3 | include LICENSE 4 | include project-oxford-python.pyproj 5 | include project-oxford-python.sln 6 | # Include the test suite 7 | recursive-include tests * 8 | global-exclude *.pyc -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | install: 6 | - pip install -r requirements.txt 7 | - pip install flake8 8 | before_script: 9 | - flake8 --ignore="E501,E225" projectoxford 10 | script: 11 | - python setup.py sdist 12 | - python setup.py test -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says that the code is written to work on both Python 2 and Python 3 | # 3. If at all possible, it is good practice to do this. If you cannot, you 4 | # will need to generate wheels for each Python version that you support. 5 | universal=1 6 | 7 | [metadata] 8 | description-file = README.md -------------------------------------------------------------------------------- /project-oxford-python.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.23107.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "project-oxford-python", "project-oxford-python.pyproj", "{2B581BB6-D72C-4BD4-818A-F75D7CCAD8F3}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {2B581BB6-D72C-4BD4-818A-F75D7CCAD8F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {2B581BB6-D72C-4BD4-818A-F75D7CCAD8F3}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | EndGlobal 21 | -------------------------------------------------------------------------------- /tests/projectoxford_tests/TestClient.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import sys, os, os.path 4 | 5 | __file__ == '__file__' 6 | 7 | rootDirectory = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..') 8 | if rootDirectory not in sys.path: 9 | sys.path.append(rootDirectory) 10 | 11 | from projectoxford.Client import Client 12 | 13 | class TestClient(unittest.TestCase): 14 | """Tests the project oxford API client""" 15 | 16 | def test_constructor_throws_with_no_instrumentation_key(self): 17 | self.assertRaises(Exception, Client, None) 18 | 19 | def test_constructor_sets_instrumentation_key(self): 20 | face = Client.face('key') 21 | vision = Client.vision('key') 22 | emotion = Client.emotion('key') 23 | self.assertIsNotNone(face) 24 | self.assertIsNotNone(vision) 25 | self.assertIsNotNone(emotion) 26 | 27 | def test_face_return_throws_for_bad_request(self): 28 | client = Client.face('key') 29 | self.assertRaises(Exception, client.detect, {'url': 'http://bing.com'}); 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # User-specific files 60 | *.suo 61 | *.user 62 | *.sln.docstates 63 | .idea/ 64 | doc/_build/ 65 | *.mdf 66 | *.ldf 67 | thumbnail* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Scott Southwood 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /projectoxford/Emotion.py: -------------------------------------------------------------------------------- 1 | from .Base import Base 2 | 3 | _emotionRecognizeUrl = 'https://api.projectoxford.ai/emotion/v1.0/recognize' 4 | 5 | 6 | class Emotion(Base): 7 | """Client for using the Project Oxford Emotion APIs""" 8 | 9 | def __init__(self, key): 10 | """Initializes a new instance of the class. 11 | Args: 12 | key (str). the API key to use for this client. 13 | """ 14 | Base.__init__(self, key) 15 | 16 | def recognize(self, options): 17 | """Recognizes the emotions expressed by one or more people in an image, 18 | as well as returns a bounding box for the face. The emotions detected are happiness, 19 | sadness, surprise, anger, fear, contempt, and disgust or neutral. 20 | 21 | Note: exactly one of url, path, or stream must be provided in the options object 22 | 23 | Args: 24 | options (Object). The Options object describing features to extract 25 | options.url (string). The Url to image to be thumbnailed 26 | options.path (string). The Path to image to be thumbnailed 27 | options.stream (stream). The stream of the image to be used 28 | 29 | Returns: 30 | object. The resulting image binary stream 31 | """ 32 | params = { 33 | 'faceRectangles': options['faceRectangles'] if 'faceRectangles' in options else '' 34 | } 35 | 36 | return Base._postWithOptions(self, _emotionRecognizeUrl, options, params) 37 | -------------------------------------------------------------------------------- /projectoxford/Client.py: -------------------------------------------------------------------------------- 1 | from .Face import Face 2 | from .Vision import Vision 3 | from .Emotion import Emotion 4 | 5 | 6 | class Client(object): 7 | """Client for using project oxford APIs""" 8 | 9 | @staticmethod 10 | def face(key): 11 | """The face API interface. 12 | Returns: 13 | :class:`face`. the face API instance. 14 | Args: 15 | key (str). the API key to use for this client. 16 | """ 17 | 18 | if key and isinstance(key, str): 19 | return Face(key) 20 | else: 21 | raise Exception('Key is required but a string was not provided') 22 | 23 | @staticmethod 24 | def vision(key): 25 | """The vision API interface. 26 | Returns: 27 | :class:`vision`. the vision API instance. 28 | Args: 29 | key (str). the API key to use for this client. 30 | """ 31 | if key and isinstance(key, str): 32 | return Vision(key) 33 | else: 34 | raise Exception('Key is required but a string was not provided') 35 | 36 | @staticmethod 37 | def emotion(key): 38 | """The emotion API interface. 39 | Returns: 40 | :class:`emotion`. the emotion API instance. 41 | Args: 42 | key (str). the API key to use for this client. 43 | """ 44 | if key and isinstance(key, str): 45 | return Emotion(key) 46 | else: 47 | raise Exception('Key is required but a string was not provided') 48 | -------------------------------------------------------------------------------- /DESCRIPTION.rst: -------------------------------------------------------------------------------- 1 | Project Oxford for Python 2 | ========================= 3 | 4 | This package contains a set of intelligent APIs understanding images: It can detect and analyze people's faces, their age, gender, and similarity. It can identify people based on a set of images. It can understand what is displayed in a picture and crop it according to where the important features are. It can tell you whether an image contains adult content, what the main colors are, and which of your images belong in a group. If your image features text, it will tell you the language and return the text as a string. It's basically magic. For more details on the Project Oxford API, please visit projectoxford.ai. 5 | 6 | This python module implements all APIs available in the Face and Vision APIs of Project Oxford. 7 | 8 | .. image:: https://i.imgur.com/Zrsnhd3.jpg 9 | 10 | Installation 11 | ------------ 12 | 13 | To install the latest release you can use `pip `_. 14 | 15 | :: 16 | 17 | $ pip install projectoxford 18 | 19 | Usage 20 | ----- 21 | 22 | **Note**: before you can send data to you will need an API key. There is are separate API keys for face and vision. 23 | 24 | **Initialize a client** 25 | 26 | .. code:: python 27 | 28 | from projectoxford import Client 29 | client = Client('') 30 | 31 | **Face detection** 32 | 33 | .. code:: python 34 | 35 | result = client.face.detect({'url': 'https://upload.wikimedia.org/wikipedia/commons/1/19/Bill_Gates_June_2015.jpg'}) 36 | print result['faceId'] 37 | print result['attributes']['age'] 38 | 39 | 40 | License 41 | ------- 42 | Licensed as MIT - please see LICENSE.txt for details. 43 | -------------------------------------------------------------------------------- /tests/projectoxford_tests/TestEmotion.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import inspect 3 | import os 4 | import sys 5 | import unittest 6 | import uuid 7 | 8 | rootDirectory = os.path.dirname(os.path.realpath('__file__')) 9 | if rootDirectory not in sys.path: 10 | sys.path.append(os.path.join(rootDirectory, '..')) 11 | 12 | from test import test_support 13 | from projectoxford.Client import Client 14 | from projectoxford.Emotion import Emotion 15 | 16 | 17 | class TestEmotion(unittest.TestCase): 18 | '''Tests the project oxford Emotion API self.client''' 19 | 20 | @classmethod 21 | def setUpClass(cls): 22 | # set up self.client for tests 23 | cls.client = Client.emotion(os.environ['OXFORD_EMOTION_API_KEY']) 24 | 25 | cls.localFilePrefix = os.path.join(rootDirectory, 'tests', 'images') 26 | 27 | # set common recognize options 28 | cls.recognizeOptions = { 29 | 'faceRectangles': '' 30 | } 31 | 32 | # 33 | # test the recognize API 34 | # 35 | def _verifyRecognize(self, recognizeResult): 36 | for emotionResult in recognizeResult: 37 | self.assertIsInstance(emotionResult['faceRectangle'], object, 'face rectangle is returned') 38 | scores = emotionResult['scores'] 39 | self.assertIsInstance(scores, object, 'scores are returned') 40 | 41 | def test_emotion_recognize_url(self): 42 | options = copy.copy(self.recognizeOptions) 43 | options['url'] = 'https://upload.wikimedia.org/wikipedia/commons/1/19/Bill_Gates_June_2015.jpg' 44 | recognizeResult = self.client.recognize(options) 45 | self._verifyRecognize(recognizeResult) 46 | 47 | def test_emotion_recognize_file(self): 48 | options = copy.copy(self.recognizeOptions) 49 | options['path'] = os.path.join(self.localFilePrefix, 'face1.jpg') 50 | recognizeResult = self.client.recognize(options) 51 | self._verifyRecognize(recognizeResult) 52 | 53 | def test_emotion_recognize_stream(self): 54 | options = copy.copy(self.recognizeOptions) 55 | with open(os.path.join(self.localFilePrefix, 'face1.jpg'), 'rb') as file: 56 | options['stream'] = file.read() 57 | recognizeResult = self.client.recognize(options) 58 | self._verifyRecognize(recognizeResult) 59 | 60 | def test_emotion_recognize_throws_invalid_options(self): 61 | self.assertRaises(Exception, self.client.recognize, {}) 62 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages # Always prefer setuptools over distutils 2 | from codecs import open # To use a consistent encoding 3 | from os import path 4 | 5 | here = path.abspath(path.dirname(__file__)) 6 | 7 | # Get the long description from the relevant file 8 | with open(path.join(here, 'DESCRIPTION.rst'), encoding='utf-8') as f: 9 | long_description = f.read() 10 | 11 | setup( 12 | name='projectoxford', 13 | 14 | # Versions should comply with PEP440. For a discussion on single-sourcing 15 | # the version across setup.py and the project code, see 16 | # http://packaging.python.org/en/latest/tutorial.html#version 17 | version='0.2.0', 18 | 19 | description='This project extends the Project Oxford API surface to support Python.', 20 | long_description=long_description, 21 | 22 | # The project's main homepage. 23 | url='https://github.com/scsouthw/project-oxford-python', 24 | download_url='https://github.com/scsouthw/project-oxford-python', 25 | 26 | # Author details 27 | author='Microsoft', 28 | author_email='scsouthw@microsoft.com', 29 | 30 | # Choose your license 31 | license='MIT', 32 | 33 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 34 | classifiers=[ 35 | # How mature is this project? Common values are 36 | # 3 - Alpha 37 | # 4 - Beta 38 | # 5 - Production/Stable 39 | 'Development Status :: 3 - Alpha', 40 | 41 | # Indicate who your project is intended for 42 | 'Intended Audience :: Developers', 43 | 'Topic :: Software Development :: Libraries :: Python Modules', 44 | 45 | # operating systems 46 | 'Operating System :: OS Independent', 47 | 48 | # Pick your license as you wish (should match "license" above) 49 | 'License :: OSI Approved :: MIT License', 50 | 51 | # Specify the Python versions you support here. In particular, ensure 52 | # that you indicate whether you support Python 2, Python 3 or both. 53 | 'Programming Language :: Python :: 2.7', 54 | 'Programming Language :: Python :: 3.4', 55 | ], 56 | 57 | # What does your project relate to? 58 | keywords='computer vision face detection linguistics language project oxford', 59 | 60 | # You can just specify the packages manually here if your project is 61 | # simple. Or you can use find_packages(). 62 | packages=find_packages(exclude=['contrib', 'docs', 'tests*']), 63 | 64 | test_suite='tests.projectoxford_tests' 65 | ) 66 | -------------------------------------------------------------------------------- /project-oxford-python.pyproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | 2.0 6 | {2b581bb6-d72c-4bd4-818a-f75d7ccad8f3} 7 | 8 | setup.py 9 | 10 | . 11 | . 12 | {888888a0-9f3d-457c-b088-3a5042f75d52} 13 | Standard Python launcher 14 | 15 | 16 | False 17 | test 18 | False 19 | 20 | 21 | 22 | 23 | 10.0 24 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.targets 25 | 26 | 27 | 28 | Code 29 | 30 | 31 | 32 | 33 | 34 | 35 | Code 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /tests/projectoxford_tests/TestPerson.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import unittest 4 | import uuid 5 | 6 | rootDirectory = os.path.dirname(os.path.realpath('__file__')) 7 | if rootDirectory not in sys.path: 8 | sys.path.append(os.path.join(rootDirectory, '..')) 9 | 10 | from projectoxford.Client import Client 11 | 12 | class TestPerson(unittest.TestCase): 13 | '''Tests the project oxford API client''' 14 | 15 | @classmethod 16 | def setUpClass(cls): 17 | # set up client for tests 18 | cls.client = Client.face(os.environ['OXFORD_FACE_API_KEY']) 19 | 20 | # detect two faces 21 | cls.knownFaceIds = []; 22 | localFilePrefix = os.path.join(rootDirectory, 'tests', 'images') 23 | face1 = cls.client.detect({'path': os.path.join(localFilePrefix, 'face1.jpg')}) 24 | face2 = cls.client.detect({'path': os.path.join(localFilePrefix, 'face2.jpg')}) 25 | cls.knownFaceIds.append(face1[0]['faceId']) 26 | cls.knownFaceIds.append(face2[0]['faceId']) 27 | 28 | # create a person group 29 | cls.personGroupId = str(uuid.uuid4()) 30 | cls.client.personGroup.create(cls.personGroupId, 'test-person-group') 31 | 32 | @classmethod 33 | def tearDownClass(cls): 34 | cls.client.personGroup.delete(cls.personGroupId) 35 | 36 | def test_person_create_update_get_delete(self): 37 | # create 38 | result = self.client.person.create(self.personGroupId, self.knownFaceIds, 'billg', 'test-person') 39 | personId = result['personId'] 40 | self.assertIsInstance(personId, object, 'person id was returned') 41 | 42 | # update 43 | result = self.client.person.update(self.personGroupId, personId, self.knownFaceIds, 'bill gates', 'test-person') 44 | self.assertIsNone(result, 'person was updated') 45 | 46 | # get 47 | result = self.client.person.get(self.personGroupId, personId) 48 | personIdVerify = result['personId'] 49 | self.assertEqual(personId, personIdVerify, 'person id was verified') 50 | 51 | # delete 52 | self.client.person.delete(self.personGroupId, personId) 53 | self.assertTrue(True, 'person was deleted') 54 | 55 | def test_person_face_add_update_delete(self): 56 | # create 57 | result = self.client.person.create(self.personGroupId, [self.knownFaceIds[0]], 'billg', 'test-person') 58 | personId = result['personId'] 59 | self.assertIsInstance(personId, object, 'create succeeded') 60 | 61 | # add a new face ID 62 | self.client.person.addFace(self.personGroupId, personId, self.knownFaceIds[1]) 63 | self.assertTrue(True, 'add succeeded') 64 | 65 | # delete the original face ID 66 | self.client.person.deleteFace(self.personGroupId, personId, self.knownFaceIds[0]) 67 | self.assertTrue(True, 'delete succeeded') 68 | 69 | # verify expected face ID 70 | self.assertIsNone(self.client.person.getFace(self.personGroupId, personId, self.knownFaceIds[0])) 71 | face = self.client.person.getFace(self.personGroupId, personId, self.knownFaceIds[1]) 72 | self.assertEqual(face['faceId'], self.knownFaceIds[1]) 73 | 74 | # clean up 75 | self.client.person.delete(self.personGroupId, personId) 76 | 77 | def test_person_list(self): 78 | # create some people 79 | self.client.person.create(self.personGroupId, [self.knownFaceIds[0]], 'billg1', 'test-person') 80 | self.client.person.create(self.personGroupId, [self.knownFaceIds[1]], 'billg2', 'test-person') 81 | 82 | # list them 83 | listResult = self.client.person.list(self.personGroupId) 84 | self.assertEqual(len(listResult), 2) 85 | 86 | # remove them 87 | for person in listResult: 88 | self.client.person.delete(self.personGroupId, person['personId']) 89 | 90 | def test_person_create_or_update(self): 91 | self.client.person.createOrUpdate(self.personGroupId, [self.knownFaceIds[0]], 'billg1', 'test-person') 92 | result = self.client.person.createOrUpdate(self.personGroupId, [self.knownFaceIds[1]], 'billg1', 'test-person-updated') 93 | result = self.client.person.get(self.personGroupId, result['personId']) 94 | 95 | self.assertEqual(result['userData'], 'test-person-updated') 96 | 97 | self.client.person.delete(self.personGroupId, result['personId']) -------------------------------------------------------------------------------- /tests/projectoxford_tests/TestPersonGroup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import unittest 4 | import uuid 5 | 6 | rootDirectory = os.path.dirname(os.path.realpath('__file__')) 7 | if rootDirectory not in sys.path: 8 | sys.path.append(os.path.join(rootDirectory, '..')) 9 | 10 | from projectoxford.Client import Client 11 | 12 | class TestPersonGroup(unittest.TestCase): 13 | '''Tests the project oxford API client''' 14 | 15 | @classmethod 16 | def setUpClass(cls): 17 | # set up self.client for tests 18 | cls.client = Client.face(os.environ['OXFORD_FACE_API_KEY']) 19 | 20 | # detect two faces 21 | cls.knownFaceIds = []; 22 | cls.localFilePrefix = os.path.join(rootDirectory, 'tests', 'images') 23 | face1 = cls.client.detect({'path': os.path.join(cls.localFilePrefix, 'face1.jpg')}) 24 | face2 = cls.client.detect({'path': os.path.join(cls.localFilePrefix, 'face2.jpg')}) 25 | cls.knownFaceIds.append(face1[0]['faceId']) 26 | cls.knownFaceIds.append(face2[0]['faceId']) 27 | 28 | def test_person_group_create_delete(self): 29 | personGroupId = str(uuid.uuid4()) 30 | result = self.client.personGroup.create(personGroupId, 'python-test-group', 'test-data') 31 | self.assertIsNone(result, "empty response expected") 32 | self.client.personGroup.delete(personGroupId) 33 | 34 | def test_person_group_list(self): 35 | personGroupId = str(uuid.uuid4()) 36 | self.client.personGroup.create(personGroupId, 'python-test-group', 'test-data') 37 | result = self.client.personGroup.list() 38 | match = next((x for x in result if x['personGroupId'] == personGroupId), None) 39 | 40 | self.assertEqual(match['personGroupId'], personGroupId) 41 | self.assertEqual(match['name'], 'python-test-group') 42 | self.assertEqual(match['userData'], 'test-data') 43 | self.client.personGroup.delete(personGroupId) 44 | 45 | def test_person_group_get(self): 46 | personGroupId = str(uuid.uuid4()) 47 | self.client.personGroup.create(personGroupId, 'python-test-group', 'test-data') 48 | result = self.client.personGroup.get(personGroupId) 49 | self.assertEqual(result['personGroupId'], personGroupId) 50 | self.assertEqual(result['name'], 'python-test-group') 51 | self.assertEqual(result['userData'], 'test-data') 52 | self.client.personGroup.delete(personGroupId) 53 | 54 | def test_person_group_update(self): 55 | personGroupId = str(uuid.uuid4()) 56 | self.client.personGroup.create(personGroupId, 'python-test-group', 'test-data') 57 | result = self.client.personGroup.update(personGroupId, 'python-test-group2', 'test-data2') 58 | self.assertIsNone(result, "empty response expected") 59 | self.client.personGroup.delete(personGroupId) 60 | 61 | def test_person_group_training(self): 62 | personGroupId = str(uuid.uuid4()) 63 | self.client.personGroup.create(personGroupId, 'python-test-group', 'test-data') 64 | result = self.client.personGroup.trainingStart(personGroupId) 65 | self.assertEqual(result['status'], 'running') 66 | 67 | countDown = 10 68 | while countDown > 0 and result['status'] == 'running': 69 | result = self.client.personGroup.trainingStatus(personGroupId) 70 | countdown = countDown - 1 71 | 72 | self.assertNotEqual(result['status'], 'running') 73 | self.client.personGroup.delete(personGroupId) 74 | 75 | def test_person_group_create_or_update(self): 76 | personGroupId = str(uuid.uuid4()) 77 | self.client.personGroup.createOrUpdate(personGroupId, 'name1') 78 | result = self.client.personGroup.createOrUpdate(personGroupId, 'name2', 'user-data') 79 | result = self.client.personGroup.get(personGroupId) 80 | 81 | self.assertEqual(result['name'], 'name2') 82 | self.assertEqual(result['userData'], 'user-data') 83 | 84 | self.client.personGroup.delete(personGroupId) 85 | 86 | def test_person_group_train_and_poll(self): 87 | personGroupId = str(uuid.uuid4()) 88 | self.client.personGroup.create(personGroupId, 'python-test-group', 'test-data') 89 | result = self.client.personGroup.trainAndPollForCompletion(personGroupId) 90 | self.assertNotEqual(result['status'], 'running') 91 | self.client.personGroup.delete(personGroupId) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Project Oxford for Python 2 | [![Build Status](https://travis-ci.org/southwood/project-oxford-python.svg?branch=master)](https://travis-ci.org/scsouthw/project-oxford-python) 3 | [![PyPI version](https://badge.fury.io/py/projectoxford.svg)](http://badge.fury.io/py/projectoxford) 4 | 5 | This package contains a set of intelligent APIs understanding images: It can detect and analyze people's faces, their age, gender, and similarity. It can identify people based on a set of images. It can understand what is displayed in a picture and crop it according to where the important features are. It can tell you whether an image contains adult content, what the main colors are, and which of your images belong in a group. If your image features text, it will tell you the language and return the text as a string. It's basically magic. For more details on the Project Oxford API, please visit [projectoxford.ai](projectoxford.ai/demo/face#detection). 6 | 7 | This python module implements all APIs available in the Face and Vision APIs of Project Oxford. 8 | 9 | ![](https://i.imgur.com/Zrsnhd3.jpg) 10 | 11 | ## Installation ## 12 | 13 | To install the latest release you can use [pip](http://www.pip-installer.org/). 14 | 15 | ``` 16 | $ pip install projectoxford 17 | ``` 18 | 19 | ## Usage ## 20 | 21 | >**Note**: before you can send data to you will need an API key. There are separate API keys for face and vision. 22 | 23 | **Initialize a client** 24 | ```python 25 | from projectoxford import Client 26 | client = Client('') 27 | ``` 28 | 29 | **Face detection** 30 | ```python 31 | result = client.face.detect({'url': 'https://upload.wikimedia.org/wikipedia/commons/1/19/Bill_Gates_June_2015.jpg'}) 32 | print(result['faceId']) 33 | print(result['attributes']['age']) 34 | ``` 35 | 36 | **Face identification** 37 | ```python 38 | personGroup = 'example-person-group' 39 | bill = 'https://upload.wikimedia.org/wikipedia/commons/1/19/Bill_Gates_June_2015.jpg' 40 | billAndMelinda = 'https://upload.wikimedia.org/wikipedia/commons/2/28/Bill_og_Melinda_Gates_2009-06-03_%28bilde_01%29.JPG' 41 | 42 | # get a face ID and create and train a person group with a person 43 | faceId = client.face.detect({'url': bill})[0]['faceId'] 44 | client.face.personGroup.createOrUpdate(personGroup, 'my person group') 45 | client.face.person.createOrUpdate(personGroup, [faceId], 'bill gates') 46 | client.face.personGroup.trainAndPollForCompletion(personGroup) 47 | 48 | # detect faces in a second photo 49 | detectResults = client.face.detect({'url': billAndMelinda}) 50 | faceIds = [] 51 | for result in detectResults: 52 | faceIds.append(result['faceId']) 53 | 54 | # identify any known faces from the second photo 55 | identifyResults = client.face.identify(personGroup, faceIds) 56 | for result in identifyResults: 57 | for candidate in result['candidates']: 58 | confidence = candidate['confidence'] 59 | personData = client.face.person.get(personGroup, candidate['personId']) 60 | name = personData['name'] 61 | print('identified {0} with {1}% confidence'.format(name, str(float(confidence) * 100))) 62 | 63 | # remove the example person group from your subscription 64 | client.face.personGroup.delete(personGroup) 65 | ``` 66 | 67 | ## Contributing 68 | **Development environment** 69 | 70 | * Install [python](https://www.python.org/downloads/), [pip](http://pip.readthedocs.org/en/stable/installing/) 71 | * Optionally Install [Visual Studio](https://www.visualstudio.com/en-us/visual-studio-homepage-vs.aspx), [python tools for VS](https://www.visualstudio.com/en-us/features/python-vs.aspx) 72 | 73 | * Get a [Project Oxford API key](https://www.projectoxford.ai/) 74 | 75 | * Install dev dependencies 76 | 77 | ``` 78 | pip install -r requirements.txt 79 | ``` 80 | * Set environment variable API key 81 | 82 | ``` 83 | set OXFORD_FACE_API_KEY= 84 | set OXFORD_VISION_API_KEY= 85 | ``` 86 | * Run tests 87 | 88 | ``` 89 | python setup.py test 90 | ``` 91 | * Publishing 92 | - update version number in setup.py 93 | ``` 94 | git tag -m "update tag version" 95 | git push --tags origin master 96 | python setup.py register -r pypi # first time only 97 | python setup.py sdist upload 98 | ``` 99 | 100 | ## License 101 | Licensed as MIT - please see LICENSE for details. 102 | -------------------------------------------------------------------------------- /projectoxford/Base.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | 4 | retryCount = 5 5 | 6 | 7 | class Base(object): 8 | """The base class for oxford API clients""" 9 | 10 | def __init__(self, key): 11 | """Initializes a new instance of the class. 12 | Args: 13 | key (str). the API key to use for this client. 14 | """ 15 | if key and isinstance(key, str): 16 | self.key = key 17 | else: 18 | raise Exception('Key is required but a string was not provided') 19 | 20 | def _invoke(self, method, url, json=None, data=None, headers={}, params={}, retries=0): 21 | """Attempt to invoke the a call to oxford. If the call is trottled, retry. 22 | Args: 23 | :param method: method for the new :class:`Request` object. 24 | :param url: URL for the new :class:`Request` object. 25 | :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. 26 | :param json: (optional) json data to send in the body of the :class:`Request`. 27 | :param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`. 28 | :param params: (optional) Dictionary or bytes to be sent in the query string for the :class:`Request`. 29 | :param retries: The number of times this call has been retried. 30 | """ 31 | 32 | response = requests.request(method, url, json=json, data=data, headers=headers, params=params) 33 | 34 | if response.status_code == 429: # throttling response code 35 | if retries <= retryCount: 36 | delay = int(response.headers['retry-after']) 37 | print('The projectoxford API was throttled. Retrying after {0} seconds'.format(str(delay))) 38 | time.sleep(delay) 39 | return self._invoke(method, url, json=json, data=data, headers=headers, params=params, retries=retries + 1) 40 | else: 41 | raise Exception('retry count ({0}) exceeded: {1}'.format(str(retryCount), response.text)) 42 | elif response.status_code == 200 or response.status_code == 201: 43 | result = response # return the raw response if an unexpected content type is returned 44 | if 'content-length' in response.headers and int(response.headers['content-length']) == 0: 45 | result = None 46 | elif 'content-type' in response.headers and isinstance(response.headers['content-type'], str): 47 | if 'application/json' in response.headers['content-type'].lower(): 48 | result = response.json() if response.content else None 49 | elif 'image' in response.headers['content-type'].lower(): 50 | result = response.content 51 | 52 | return result 53 | elif response.status_code == 404: 54 | return None 55 | else: 56 | raise Exception('status {0}: {1}'.format(str(response.status_code), response.text)) 57 | 58 | def _postWithOptions(self, url, options, params={}): 59 | """Common options handler for vision / face detection 60 | 61 | Args: 62 | url (string). The url to invoke in the Oxford API 63 | options (Object). The Options dictionary describing features to extract 64 | options.url (string). The Url to image to be analyzed 65 | options.path (string). The Path to image to be analyzed 66 | options.stream (string). The image stream to be analyzed 67 | params (Object). The url parameters dictionary 68 | 69 | Returns: 70 | object. The resulting JSON 71 | """ 72 | 73 | # The types of data that can be passed to the API 74 | json = None 75 | data = None 76 | 77 | # common header 78 | headers = {'Ocp-Apim-Subscription-Key': self.key} 79 | 80 | # detect faces in a URL 81 | if 'url' in options and options['url'] != '': 82 | headers['Content-Type'] = 'application/json' 83 | json={'url': options['url']} 84 | 85 | # detect faces from a local file 86 | elif 'path' in options and options['path'] != '': 87 | headers['Content-Type'] = 'application/octet-stream' 88 | data_file = open(options['path'], 'rb') 89 | data = data_file.read() 90 | data_file.close() 91 | 92 | # detect faces in an octect stream 93 | elif 'stream' in options: 94 | headers['Content-Type'] = 'application/octet-stream' 95 | data = options['stream'] 96 | 97 | # fail if the options didn't specify an image source 98 | if not json and not data: 99 | raise Exception('Data must be supplied as either JSON or a Binary image data.') 100 | 101 | return self._invoke('post', url, json=json, data=data, headers=headers, params=params) 102 | -------------------------------------------------------------------------------- /tests/projectoxford_tests/TestVision.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import os 3 | import sys 4 | import unittest 5 | 6 | rootDirectory = os.path.dirname(os.path.realpath('__file__')) 7 | if rootDirectory not in sys.path: 8 | sys.path.append(os.path.join(rootDirectory, '..')) 9 | 10 | from projectoxford.Client import Client 11 | 12 | class TestFace(unittest.TestCase): 13 | '''Tests the project oxford face API self.client''' 14 | 15 | @classmethod 16 | def setUpClass(cls): 17 | # set up self.client for tests 18 | cls.client = Client.vision(os.environ['OXFORD_VISION_API_KEY']) 19 | cls.localFilePrefix = os.path.join(rootDirectory, 'tests', 'images') 20 | cls.analyzeOptions = { 21 | 'ImageType': True, 22 | 'Color': True, 23 | 'Faces': True, 24 | 'Adult': True, 25 | 'Categories': True 26 | } 27 | 28 | cls.thumbnailOptions = { 29 | 'width': 100, 30 | 'height': 100, 31 | 'smartCropping': True 32 | } 33 | 34 | cls.ocrOptions = { 35 | 'language': 'en', 36 | 'detectOrientation': True 37 | } 38 | 39 | # 40 | # test the analyze API 41 | # 42 | def _verify_analyze_result(self, result): 43 | self.assertIsNotNone(result['imageType']) 44 | self.assertIsNotNone(result['color']) 45 | self.assertIsNotNone(result['faces']) 46 | self.assertIsNotNone(result['adult']) 47 | self.assertIsNotNone(result['categories']) 48 | 49 | def test_vision_analyze_file(self): 50 | options = copy.copy(self.analyzeOptions) 51 | options['path'] = os.path.join(self.localFilePrefix, 'vision.jpg') 52 | result = self.client.analyze(options) 53 | self._verify_analyze_result(result) 54 | 55 | def test_vision_analyze_url(self): 56 | options = copy.copy(self.analyzeOptions) 57 | options['url'] = 'https://upload.wikimedia.org/wikipedia/commons/1/19/Bill_Gates_June_2015.jpg' 58 | result = self.client.analyze(options) 59 | self._verify_analyze_result(result) 60 | 61 | def test_vision_analyze_stream(self): 62 | options = copy.copy(self.analyzeOptions) 63 | with open(os.path.join(self.localFilePrefix, 'face1.jpg'), 'rb') as file: 64 | options['stream'] = file.read() 65 | result = self.client.analyze(options) 66 | 67 | self._verify_analyze_result(result) 68 | 69 | # 70 | # test the thumbnail API 71 | # 72 | def _verify_thumbnail_result(self, result, fileName): 73 | outputPath = os.path.join(self.localFilePrefix, fileName) 74 | with open(outputPath, 'wb+') as file: file.write(result) 75 | self.assertTrue(True, 'file write succeeded for: {0}'.format(fileName)) 76 | 77 | def test_vision_thumbnail_file(self): 78 | options = copy.copy(self.thumbnailOptions) 79 | options['path'] = os.path.join(self.localFilePrefix, 'vision.jpg') 80 | result = self.client.thumbnail(options) 81 | self._verify_thumbnail_result(result, 'thumbnail_from_file.jpg') 82 | 83 | #def test_vision_thumbnail_url(self): 84 | # options = copy.copy(self.thumbnailOptions) 85 | # options['url'] = 'https://upload.wikimedia.org/wikipedia/commons/1/19/Bill_Gates_June_2015.jpg' 86 | # result = self.client.thumbnail(options) 87 | # self._verify_thumbnail_result(result, 'thumbnail_from_url.jpg') 88 | 89 | #def test_vision_thumbnail_stream(self): 90 | # options = copy.copy(self.thumbnailOptions) 91 | # with open(os.path.join(self.localFilePrefix, 'face1.jpg'), 'rb') as file: 92 | # options['stream'] = file.read() 93 | # result = self.client.thumbnail(options) 94 | # self._verify_thumbnail_result(result, 'thumbnail_from_stream.jpg') 95 | 96 | # 97 | # test the OCR API 98 | # 99 | def _verify_ocr_result(self, result): 100 | self.assertIsNotNone(result['language']) 101 | self.assertIsNotNone(result['orientation']) 102 | 103 | def test_vision_ocr_file(self): 104 | options = copy.copy(self.ocrOptions) 105 | options['path'] = os.path.join(self.localFilePrefix, 'vision.jpg') 106 | result = self.client.ocr(options) 107 | self._verify_ocr_result(result) 108 | 109 | def test_vision_ocr_url(self): 110 | options = copy.copy(self.ocrOptions) 111 | options['url'] = 'https://upload.wikimedia.org/wikipedia/commons/1/19/Bill_Gates_June_2015.jpg' 112 | result = self.client.ocr(options) 113 | self._verify_ocr_result(result) 114 | 115 | def test_vision_ocr_stream(self): 116 | options = copy.copy(self.ocrOptions) 117 | with open(os.path.join(self.localFilePrefix, 'face1.jpg'), 'rb') as file: 118 | options['stream'] = file.read() 119 | result = self.client.ocr(options) 120 | 121 | self._verify_ocr_result(result) -------------------------------------------------------------------------------- /projectoxford/Vision.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from .Base import Base 4 | 5 | _analyzeUrl = 'https://api.projectoxford.ai/vision/v1/analyses' 6 | _thumbnailUrl = 'https://api.projectoxford.ai/vision/v1/thumbnails' 7 | _ocrUrl = 'https://api.projectoxford.ai/vision/v1/ocr' 8 | 9 | 10 | class Vision(Base): 11 | """Client for using the Project Oxford face APIs""" 12 | 13 | def __init__(self, key): 14 | """Initializes a new instance of the class. 15 | Args: 16 | key (str). the API key to use for this client. 17 | """ 18 | Base.__init__(self, key) 19 | 20 | def analyze(self, options): 21 | """This operation does a deep analysis on the given image and then extracts a 22 | set of rich visual features based on the image content. 23 | 24 | Note: exactly one of url, path, or stream must be provided in the options object 25 | 26 | Args: 27 | options (Object). The Options object describing features to extract 28 | options.url (string). The Url to image to be analyzed 29 | options.path (string). The Path to image to be analyzed 30 | options.stream (stream). The stream of the image to be used 31 | options.ImageType (boolean). The Detects if image is clipart or a line drawing. 32 | options.Color (boolean). The Determines the accent color, dominant color, if image is black&white. 33 | options.Faces (boolean). The Detects if faces are present. If present, generate coordinates, gender and age. 34 | options.Adult (boolean). The Detects if image is pornographic in nature (nudity or sex act). Sexually suggestive content is also detected. 35 | options.Categories (boolean). The Image categorization; taxonomy defined in documentation. 36 | 37 | Returns: 38 | object. The resulting JSON 39 | """ 40 | flags = [] 41 | for option in options: 42 | match = re.match(r'(ImageType)|(Color)|(Faces)|(Adult)|(Categories)', option) 43 | if match and options[option]: 44 | flags.append(option) 45 | 46 | params = {'visualFeatures': ','.join(flags)} if flags else {} 47 | return Base._postWithOptions(self, _analyzeUrl, options, params) 48 | 49 | def thumbnail(self, options): 50 | """Generate a thumbnail image to the user-specified width and height. By default, the 51 | service analyzes the image, identifies the region of interest (ROI), and generates 52 | smart crop coordinates based on the ROI. Smart cropping is designed to help when you 53 | specify an aspect ratio that differs from the input image. 54 | 55 | Note: exactly one of url, path, or stream must be provided in the options object 56 | 57 | Args: 58 | options (Object). The Options object describing features to extract 59 | options.url (string). The Url to image to be thumbnailed 60 | options.path (string). The Path to image to be thumbnailed 61 | options.stream (stream). The stream of the image to be used 62 | options.width (number). The Width of the thumb in pixels 63 | options.height (number). The Height of the thumb in pixels 64 | options.smartCropping (boolean). The Should SmartCropping be enabled? 65 | 66 | Returns: 67 | object. The resulting image binary stream 68 | """ 69 | params = { 70 | 'width': options['width'] if 'width' in options else 50, 71 | 'height': options['height'] if 'height' in options else 50, 72 | 'smartCropping': options['smartCropping'] if 'smartCropping' in options else False 73 | } 74 | 75 | return Base._postWithOptions(self, _thumbnailUrl, options, params) 76 | 77 | def ocr(self, options): 78 | """Optical Character Recognition (OCR) detects text in an image and extracts the recognized 79 | characters into a machine-usable character stream. 80 | 81 | Note: exactly one of url, path, or stream must be provided in the options object 82 | 83 | Args: 84 | options (Object). The Options object describing features to extract 85 | options.url (string). The Url to image to be thumbnailed 86 | options.path (string). The Path to image to be thumbnailed 87 | options.stream (stream). The stream of the image to be used 88 | options.language (string). The BCP-47 language code of the text to be detected in the image. Default value is "unk", then the service will auto detect the language of the text in the image. 89 | options.detectOrientation (string). The Detect orientation of text in the image 90 | 91 | Returns: 92 | object. The resulting JSON 93 | """ 94 | params = { 95 | 'language': options['language'] if 'language' in options else 'unk', 96 | 'detectOrientation': options['detectOrientation'] if 'detectOrientation' in options else True 97 | } 98 | 99 | return Base._postWithOptions(self, _ocrUrl, options, params) 100 | -------------------------------------------------------------------------------- /tests/projectoxford_tests/TestFace.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import os 3 | import sys 4 | import unittest 5 | 6 | rootDirectory = os.path.dirname(os.path.realpath('__file__')) 7 | if rootDirectory not in sys.path: 8 | sys.path.append(os.path.join(rootDirectory, '..')) 9 | 10 | from projectoxford.Client import Client 11 | 12 | class TestFace(unittest.TestCase): 13 | '''Tests the project oxford face API self.client''' 14 | 15 | @classmethod 16 | def setUpClass(cls): 17 | # set up self.client for tests 18 | cls.client = Client.face(os.environ['OXFORD_FACE_API_KEY']) 19 | 20 | # detect two faces 21 | cls.knownFaceIds = []; 22 | cls.localFilePrefix = os.path.join(rootDirectory, 'tests', 'images') 23 | face1 = cls.client.detect({'path': os.path.join(cls.localFilePrefix, 'face1.jpg')}) 24 | face2 = cls.client.detect({'path': os.path.join(cls.localFilePrefix, 'face2.jpg')}) 25 | cls.knownFaceIds.append(face1[0]['faceId']) 26 | cls.knownFaceIds.append(face2[0]['faceId']) 27 | 28 | # set common detect options 29 | cls.detectOptions = { 30 | 'analyzesFaceLandmarks': True, 31 | 'analyzesAge': True, 32 | 'analyzesGender': True, 33 | 'analyzesHeadPose': True 34 | } 35 | 36 | # 37 | # test the detect API 38 | # 39 | def _verifyDetect(self, detectResult): 40 | faceIdResult = detectResult[0] 41 | 42 | self.assertIsInstance(faceIdResult['faceId'], object, 'face ID is returned') 43 | self.assertIsInstance(faceIdResult['faceRectangle'], object, 'faceRectangle is returned') 44 | self.assertIsInstance(faceIdResult['faceLandmarks'], object, 'faceLandmarks are returned') 45 | 46 | attributes = faceIdResult['attributes'] 47 | self.assertIsInstance(attributes, object, 'attributes are returned') 48 | self.assertIsInstance(attributes['gender'], object, 'gender is returned') 49 | self.assertIsInstance(attributes['age'], int, 'age is returned') 50 | 51 | def test_face_detect_url(self): 52 | options = copy.copy(self.detectOptions) 53 | options['url'] = 'https://upload.wikimedia.org/wikipedia/commons/1/19/Bill_Gates_June_2015.jpg' 54 | detectResult = self.client.detect(options) 55 | self._verifyDetect(detectResult) 56 | 57 | def test_face_detect_file(self): 58 | options = copy.copy(self.detectOptions) 59 | options['path'] = os.path.join(self.localFilePrefix, 'face1.jpg') 60 | detectResult = self.client.detect(options) 61 | self._verifyDetect(detectResult) 62 | 63 | def test_face_detect_stream(self): 64 | options = copy.copy(self.detectOptions) 65 | with open(os.path.join(self.localFilePrefix, 'face1.jpg'), 'rb') as file: 66 | options['stream'] = file.read() 67 | detectResult = self.client.detect(options) 68 | self._verifyDetect(detectResult) 69 | 70 | def test_face_detect_throws_invalid_options(self): 71 | self.assertRaises(Exception, self.client.detect, {}) 72 | 73 | # 74 | # test the similar API 75 | # 76 | def test_face_similar(self): 77 | similarResult = self.client.similar(self.knownFaceIds[0], [self.knownFaceIds[1]]) 78 | self.assertIsInstance(similarResult, list, 'similar result is returned') 79 | self.assertEqual(self.knownFaceIds[1], similarResult[0]['faceId'], 'expected similar face is returned') 80 | 81 | # 82 | # test the grouping API 83 | # 84 | def test_face_grouping(self): 85 | faces = self.client.detect({'path': os.path.join(self.localFilePrefix, 'face-group.jpg')}) 86 | 87 | 88 | faceIds = [] 89 | for face in faces: 90 | faceIds.append(face['faceId']) 91 | 92 | groupingResult = self.client.grouping(faceIds) 93 | self.assertIsInstance(groupingResult, object, 'grouping result is returned') 94 | self.assertIsInstance(groupingResult['groups'], list, 'groups list is returned') 95 | self.assertIsInstance(groupingResult['messyGroup'], list, 'messygroup list is returned') 96 | 97 | # 98 | # test the verify API 99 | # 100 | def test_face_verify(self): 101 | verifyResult = self.client.verify(self.knownFaceIds[0], self.knownFaceIds[1]) 102 | self.assertIsInstance(verifyResult, object, 'grouping result is returned') 103 | self.assertEqual(verifyResult['isIdentical'], True, 'verify succeeded') 104 | self.assertGreaterEqual(verifyResult['confidence'], 0.5, 'confidence is returned') 105 | 106 | # 107 | # test the identify API 108 | # 109 | def test_face_identify(self): 110 | self.client.personGroup.createOrUpdate('testgroup', 'name') 111 | faceId = self.client.detect({'path': os.path.join(self.localFilePrefix, 'face1.jpg')})[0]['faceId'] 112 | personId = self.client.person.createOrUpdate('testgroup', [faceId], 'billG')['personId'] 113 | self.client.personGroup.trainAndPollForCompletion('testgroup') 114 | faceId2 = self.client.detect({'path': os.path.join(self.localFilePrefix, 'face2.jpg')})[0]['faceId'] 115 | identifyResult = self.client.identify('testgroup', [faceId2]) 116 | 117 | self.assertIsInstance(identifyResult, object, 'identify result is returned') 118 | self.assertEqual(identifyResult[0]['candidates'][0]['personId'], personId) 119 | self.client.personGroup.delete('testgroup') -------------------------------------------------------------------------------- /projectoxford/Face.py: -------------------------------------------------------------------------------- 1 | from .Base import Base 2 | from .Person import Person 3 | from .PersonGroup import PersonGroup 4 | 5 | _detectUrl = 'https://api.projectoxford.ai/face/v0/detections' 6 | _similarUrl = 'https://api.projectoxford.ai/face/v0/findsimilars' 7 | _groupingUrl = 'https://api.projectoxford.ai/face/v0/groupings' 8 | _identifyUrl = 'https://api.projectoxford.ai/face/v0/identifications' 9 | _verifyUrl = 'https://api.projectoxford.ai/face/v0/verifications' 10 | 11 | 12 | class Face(Base): 13 | """Client for using the Project Oxford face APIs""" 14 | 15 | def __init__(self, key): 16 | """Initializes a new instance of the class. 17 | Args: 18 | key (str). the API key to use for this client. 19 | """ 20 | Base.__init__(self, key) 21 | self.person = Person(self.key) 22 | self.personGroup = PersonGroup(self.key) 23 | 24 | def detect(self, options): 25 | """Detects human faces in an image and returns face locations, face landmarks, and 26 | optional attributes including head-pose, gender, and age. Detection is an essential 27 | API that provides faceId to other APIs like Identification, Verification, 28 | and Find Similar. 29 | 30 | Note: exactly one of url, path, or stream must be provided in the options object 31 | 32 | Args: 33 | options (object). The Options object 34 | options.url (str). The URL to image to be used 35 | options.path (str). The Path to image to be used 36 | options.stream (stream). The stream of the image to be used 37 | options.analyzesFaceLandmarks (boolean). The Analyze face landmarks? 38 | options.analyzesAge (boolean). The Analyze age? 39 | options.analyzesGender (boolean). The Analyze gender? 40 | options.analyzesHeadPose (boolean). The Analyze headpose? 41 | 42 | Returns: 43 | object. The resulting JSON 44 | """ 45 | 46 | # build params query string 47 | params = { 48 | 'analyzesFaceLandmarks': 'true' if 'analyzesFaceLandmarks' in options else 'false', 49 | 'analyzesAge': 'true' if 'analyzesAge' in options else 'false', 50 | 'analyzesGender': 'true' if 'analyzesGender' in options else 'false', 51 | 'analyzesHeadPose': 'true' if 'analyzesHeadPose' in options else 'false' 52 | } 53 | 54 | return Base._postWithOptions(self, _detectUrl, options, params) 55 | 56 | def similar(self, sourceFace, candidateFaces): 57 | """Detect similar faces using faceIds (as returned from the detect API) 58 | 59 | Args: 60 | sourceFace (str). The source face 61 | candidateFaces (str[]). The source face 62 | 63 | Returns: 64 | object. The resulting JSON 65 | """ 66 | 67 | body = { 68 | 'faceId': sourceFace, 69 | 'faceIds': candidateFaces 70 | } 71 | 72 | return self._invoke('post', _similarUrl, json=body, headers={'Ocp-Apim-Subscription-Key': self.key}) 73 | 74 | def grouping(self, faceIds): 75 | """Divides candidate faces into groups based on face similarity using faceIds. 76 | The output is one or more disjointed face groups and a MessyGroup. 77 | A face group contains the faces that have similar looking, often of the same person. 78 | There will be one or more face groups ranked by group size, i.e. number of face. 79 | Faces belonging to the same person might be split into several groups in the result. 80 | The MessyGroup is a special face group that each face is not similar to any other 81 | faces in original candidate faces. The messyGroup will not appear in the result if 82 | all faces found their similar counterparts. The candidate face list has a 83 | limit of 100 faces. 84 | 85 | Args: 86 | faceIds (str[]). Array of faceIds to use 87 | 88 | Returns: 89 | object. The resulting JSON 90 | """ 91 | 92 | body = {'faceIds': faceIds} 93 | 94 | return self._invoke('post', _groupingUrl, json=body, headers={'Ocp-Apim-Subscription-Key': self.key}) 95 | 96 | def identify(self, personGroupId, faces, maxNumOfCandidatesReturned=1): 97 | """Identifies persons from a person group by one or more input faces. 98 | To recognize which person a face belongs to, Face Identification needs a person group 99 | that contains number of persons. Each person contains one or more faces. After a person 100 | group prepared, it should be trained to make it ready for identification. Then the 101 | identification API compares the input face to those persons' faces in person group and 102 | returns the best-matched candidate persons, ranked by confidence. 103 | 104 | Args: 105 | faces (str[]). Array of faceIds to use 106 | personGroupId (str). The person group ID to use 107 | maxNumOfCandidatesReturned (str). Optional maximum number of candidates to return 108 | 109 | Returns: 110 | object. The resulting JSON 111 | """ 112 | 113 | body = { 114 | 'faceIds': faces, 115 | 'personGroupId': personGroupId, 116 | 'maxNumOfCandidatesReturned': maxNumOfCandidatesReturned 117 | } 118 | 119 | return self._invoke('post', _identifyUrl, json=body, headers={'Ocp-Apim-Subscription-Key': self.key}) 120 | 121 | def verify(self, faceId1, faceId2): 122 | """Analyzes two faces and determine whether they are from the same person. 123 | Verification works well for frontal and near-frontal faces. 124 | For the scenarios that are sensitive to accuracy please use with own judgment. 125 | 126 | Args: 127 | faceId1 (str). The first face to compare 128 | faceId2 (str). The second face to compare 129 | 130 | Returns: 131 | object. The resulting JSON 132 | """ 133 | 134 | body = { 135 | 'faceId1': faceId1, 136 | 'faceId2': faceId2 137 | } 138 | 139 | return self._invoke('post', _verifyUrl, json=body, headers={'Ocp-Apim-Subscription-Key': self.key}) 140 | -------------------------------------------------------------------------------- /projectoxford/PersonGroup.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from .Base import Base 4 | 5 | _personGroupUrl = 'https://api.projectoxford.ai/face/v0/persongroups' 6 | 7 | 8 | class PersonGroup(Base): 9 | """Client for using the Project Oxford person group APIs""" 10 | 11 | def __init__(self, key): 12 | """Initializes a new instance of the class. 13 | Args: 14 | key (str). the API key to use for this client. 15 | """ 16 | Base.__init__(self, key) 17 | 18 | def create(self, personGroupId, name, userData=None): 19 | """Creates a new person group with a user-specified ID. 20 | A person group is one of the most important parameters for the Identification API. 21 | The Identification searches person faces in a specified person group. 22 | 23 | Args: 24 | personGroupId (str). Numbers, en-us letters in lower case, '-', '_'. Max length: 64 25 | name (str). Person group display name. The maximum length is 128. 26 | userData (str). Optional user-provided data attached to the group. The size limit is 16KB. 27 | 28 | Returns: 29 | object. The resulting JSON 30 | """ 31 | 32 | body = { 33 | 'name': name, 34 | 'userData': userData 35 | } 36 | 37 | return self._invoke('put', 38 | _personGroupUrl + '/' + personGroupId, 39 | json=body, 40 | headers={'Ocp-Apim-Subscription-Key': self.key}) 41 | 42 | def delete(self, personGroupId): 43 | """Deletes an existing person group. 44 | 45 | Args: 46 | personGroupId (str). Name of person group to delete 47 | 48 | Returns: 49 | object. The resulting JSON 50 | """ 51 | 52 | return self._invoke('delete', 53 | _personGroupUrl + '/' + personGroupId, 54 | headers={'Ocp-Apim-Subscription-Key': self.key}) 55 | 56 | def get(self, personGroupId): 57 | """Gets an existing person group. 58 | 59 | Args: 60 | personGroupId (str). Name of person group to get 61 | 62 | Returns: 63 | object. The resulting JSON 64 | """ 65 | 66 | return self._invoke('get', 67 | _personGroupUrl + '/' + personGroupId, 68 | headers={'Ocp-Apim-Subscription-Key': self.key}) 69 | 70 | def trainingStatus(self, personGroupId): 71 | """Retrieves the training status of a person group. Training is triggered by the Train PersonGroup API. 72 | The training will process for a while on the server side. This API can query whether the training 73 | is completed or ongoing. 74 | 75 | Args: 76 | personGroupId (str). Name of person group under training 77 | 78 | Returns: 79 | object. The resulting JSON 80 | """ 81 | 82 | return self._invoke('get', 83 | _personGroupUrl + '/' + personGroupId + '/training', 84 | headers={'Ocp-Apim-Subscription-Key': self.key}) 85 | 86 | def trainingStart(self, personGroupId): 87 | """Starts a person group training. 88 | Training is a necessary preparation process of a person group before identification. 89 | Each person group needs to be trained in order to call Identification. The training 90 | will process for a while on the server side even after this API has responded. 91 | 92 | Args: 93 | personGroupId (str). Name of person group to train 94 | 95 | Returns: 96 | object. The resulting JSON 97 | """ 98 | 99 | return self._invoke('post', 100 | _personGroupUrl + '/' + personGroupId + '/training', 101 | headers={'Ocp-Apim-Subscription-Key': self.key}) 102 | 103 | def update(self, personGroupId, name, userData=None): 104 | """Updates a person group with a user-specified ID. 105 | A person group is one of the most important parameters for the Identification API. 106 | The Identification searches person faces in a specified person group. 107 | 108 | Args: 109 | personGroupId (str). Numbers, en-us letters in lower case, '-', '_'. Max length: 64 110 | name (str). Person group display name. The maximum length is 128. 111 | userData (str). User-provided data attached to the group. The size limit is 16KB. 112 | 113 | Returns: 114 | object. The resulting JSON 115 | """ 116 | 117 | body = { 118 | 'name': name, 119 | 'userData': userData 120 | } 121 | 122 | return self._invoke('patch', 123 | _personGroupUrl + '/' + personGroupId, 124 | json=body, 125 | headers={'Ocp-Apim-Subscription-Key': self.key}) 126 | 127 | def createOrUpdate(self, personGroupId, name, userData=None): 128 | """Creates or updates a person group with a user-specified ID. 129 | A person group is one of the most important parameters for the Identification API. 130 | The Identification searches person faces in a specified person group. 131 | 132 | Args: 133 | personGroupId (str). Numbers, en-us letters in lower case, '-', '_'. Max length: 64 134 | name (str). Person group display name. The maximum length is 128. 135 | userData (str). User-provided data attached to the group. The size limit is 16KB. 136 | 137 | Returns: 138 | object. The resulting JSON 139 | """ 140 | if self.get(personGroupId) is None: 141 | return self.create(personGroupId, name, userData) 142 | else: 143 | return self.update(personGroupId, name, userData) 144 | 145 | def trainAndPollForCompletion(self, personGroupId, timeoutSeconds=30): 146 | """Starts a person group training and polls until the status is not 'running' 147 | 148 | Args: 149 | personGroupId (str). Name of person group to train 150 | 151 | Returns: 152 | object. The resulting JSON 153 | """ 154 | timeout = 0 155 | status = self.trainingStart(personGroupId) 156 | while status['status'] == 'running': 157 | time.sleep(1) 158 | status = self.trainingStatus(personGroupId) 159 | timeout += 1 160 | 161 | if timeout >= timeoutSeconds: 162 | raise Exception('training timed out after {0} seconds, last known status: {1}'.format(timeoutSeconds, status)) 163 | 164 | return status 165 | 166 | def list(self): 167 | """Lists all person groups in the current subscription. 168 | 169 | Returns: 170 | object. The resulting JSON 171 | """ 172 | return self._invoke('get', _personGroupUrl, headers={'Ocp-Apim-Subscription-Key': self.key}) 173 | -------------------------------------------------------------------------------- /projectoxford/Person.py: -------------------------------------------------------------------------------- 1 | from .Base import Base 2 | 3 | _personUrl = 'https://api.projectoxford.ai/face/v0/persongroups' 4 | 5 | 6 | class Person(Base): 7 | """Client for using the Project Oxford person APIs""" 8 | 9 | def __init__(self, key): 10 | """Initializes a new instance of the class. 11 | Args: 12 | key (str). the API key to use for this client. 13 | """ 14 | Base.__init__(self, key) 15 | 16 | def addFace(self, personGroupId, personId, faceId, userData=None): 17 | """Adds a face to a person for identification. The maximum face count for each person is 32. 18 | The face ID must be added to a person before its expiration. Typically a face ID expires 19 | 24 hours after detection. 20 | 21 | Args: 22 | personGroupId (str). The target person's person group. 23 | personId (str). The target person that the face is added to. 24 | faceId (str). The ID of the face to be added. The maximum face amount for each person is 32. 25 | userData (str). Optional. Attach user data to person's face. The maximum length is 1024. 26 | 27 | Returns: 28 | object. The resulting JSON 29 | """ 30 | 31 | body = {} if userData is None else {'userData': userData} 32 | uri = _personUrl + '/' + personGroupId + '/persons/' + personId + '/faces/' + faceId 33 | return self._invoke('put', uri, json=body, headers={'Ocp-Apim-Subscription-Key': self.key}) 34 | 35 | def deleteFace(self, personGroupId, personId, faceId): 36 | """Deletes a face from a person. 37 | 38 | Args: 39 | personGroupId (str). The target person's person group. 40 | personId (str). The target person that the face is removed from. 41 | faceId (str). The ID of the face to be deleted. 42 | 43 | Returns: 44 | object. The resulting JSON 45 | """ 46 | 47 | uri = _personUrl + '/' + personGroupId + '/persons/' + personId + '/faces/' + faceId 48 | return self._invoke('delete', uri, headers={'Ocp-Apim-Subscription-Key': self.key}) 49 | 50 | def updateFace(self, personGroupId, personId, faceId, userData=None): 51 | """Updates a face for a person. 52 | 53 | Args: 54 | personGroupId (str). The target person's person group. 55 | personId (str). The target person that the face is updated on. 56 | faceId (str). The ID of the face to be updated. 57 | userData (str). Optional. Attach user data to person's face. The maximum length is 1024. 58 | 59 | Returns: 60 | object. The resulting JSON 61 | """ 62 | 63 | body = {} if userData is None else {'userData': userData} 64 | uri = _personUrl + '/' + personGroupId + '/persons/' + personId + '/faces/' + faceId 65 | return self._invoke('patch', uri, json=body, headers={'Ocp-Apim-Subscription-Key': self.key}) 66 | 67 | def getFace(self, personGroupId, personId, faceId): 68 | """Get a face for a person. 69 | 70 | Args: 71 | personGroupId (str). The target person's person group. 72 | personId (str). The target person that the face is to get from. 73 | faceId (str). The ID of the face to get. 74 | 75 | Returns: 76 | object. The resulting JSON 77 | """ 78 | 79 | uri = _personUrl + '/' + personGroupId + '/persons/' + personId + '/faces/' + faceId 80 | return self._invoke('get', uri, headers={'Ocp-Apim-Subscription-Key': self.key}) 81 | 82 | def create(self, personGroupId, faceIds, name, userData=None): 83 | """Creates a new person in a specified person group for identification. 84 | The number of persons has a subscription limit. Free subscription amount is 1000 persons. 85 | The maximum face count for each person is 32. 86 | 87 | Args: 88 | personGroupId (str). The target person's person group. 89 | faceIds ([str]). Array of face id's for the target person 90 | name (str). Target person's display name. The maximum length is 128. 91 | userData (str). Optional fields for user-provided data attached to a person. Size limit is 16KB. 92 | 93 | Returns: 94 | object. The resulting JSON 95 | """ 96 | 97 | body = { 98 | 'faceIds': faceIds, 99 | 'name': name 100 | } 101 | 102 | if userData is not None: 103 | body['userData'] = userData 104 | 105 | uri = _personUrl + '/' + personGroupId + '/persons' 106 | return self._invoke('post', uri, json=body, headers={'Ocp-Apim-Subscription-Key': self.key}) 107 | 108 | def delete(self, personGroupId, personId): 109 | """Deletes an existing person from a person group. 110 | 111 | Args: 112 | personGroupId (str). The target person's person group. 113 | personId (str). The target person to delete. 114 | 115 | Returns: 116 | object. The resulting JSON 117 | """ 118 | 119 | uri = _personUrl + '/' + personGroupId + '/persons/' + personId 120 | return self._invoke('delete', uri, headers={'Ocp-Apim-Subscription-Key': self.key}) 121 | 122 | def get(self, personGroupId, personId): 123 | """Gets an existing person from a person group. 124 | 125 | Args: 126 | personGroupId (str). The target person's person group. 127 | personId (str). The target person to get. 128 | 129 | Returns: 130 | object. The resulting JSON 131 | """ 132 | 133 | uri = _personUrl + '/' + personGroupId + '/persons/' + personId 134 | return self._invoke('get', uri, headers={'Ocp-Apim-Subscription-Key': self.key}) 135 | 136 | def update(self, personGroupId, personId, faceIds, name, userData=None): 137 | """Updates a person's information. 138 | 139 | Args: 140 | personGroupId (str). The target person's person group. 141 | personId (str). The target persons Id. 142 | faceIds ([str]). Array of face id's for the target person. 143 | name (str). Target person's display name. The maximum length is 128. 144 | userData (str). Optional fields for user-provided data attached to a person. Size limit is 16KB. 145 | 146 | Returns: 147 | object. The resulting JSON 148 | """ 149 | 150 | body = { 151 | 'faceIds': faceIds, 152 | 'name': name, 153 | 'userData': userData 154 | } 155 | 156 | uri = _personUrl + '/' + personGroupId + '/persons/' + personId 157 | return self._invoke('patch', uri, json=body, headers={'Ocp-Apim-Subscription-Key': self.key}) 158 | 159 | def createOrUpdate(self, personGroupId, faceIds, name, userData=None): 160 | """Creates or updates a person's information. 161 | 162 | Args: 163 | personGroupId (str). The target person's person group. 164 | faceIds ([str]). Array of face id's for the target person. 165 | name (str). Target person's display name. The maximum length is 128. 166 | userData (str). Optional fields for user-provided data attached to a person. Size limit is 16KB. 167 | 168 | Returns: 169 | object. The resulting JSON 170 | """ 171 | persons = self.list(personGroupId) 172 | for person in persons: 173 | if person['name'] == name: 174 | self.update(personGroupId, person['personId'], faceIds, name, userData) 175 | return person 176 | 177 | return self.create(personGroupId, faceIds, name, userData) 178 | 179 | def list(self, personGroupId): 180 | """Lists all persons in a person group, with the person information. 181 | 182 | Args: 183 | personGroupId (str). The target person's person group. 184 | 185 | Returns: 186 | object. The resulting JSON 187 | """ 188 | 189 | uri = _personUrl + '/' + personGroupId + '/persons' 190 | return self._invoke('get', uri, headers={'Ocp-Apim-Subscription-Key': self.key}) 191 | --------------------------------------------------------------------------------