├── 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 | [](https://travis-ci.org/scsouthw/project-oxford-python)
3 | [](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 | 
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 |
--------------------------------------------------------------------------------