├── tests ├── __init__.py ├── test_remove.py ├── test_gallery.py ├── test_recognize.py └── test_enroll.py ├── .python-version ├── integration_tests ├── __init__.py └── test_enroll_integration.py ├── .gitignore ├── Makefile ├── .flake8 ├── requirements.txt ├── kairos_face ├── settings.py ├── entities.py ├── __init__.py ├── utils.py ├── exceptions.py ├── remove.py ├── detect.py ├── recognize.py ├── verify.py ├── enroll.py └── gallery.py ├── setup.py ├── LICENSE └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.4.4 2 | -------------------------------------------------------------------------------- /integration_tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | __pycache__ 3 | *.egg-info 4 | exps/* 5 | .DS_STORE 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | flake8: 2 | flake8 3 | 4 | test: flake8 5 | nosetests ./tests 6 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | max-complexity = 10 4 | exclude=kairos_face/__init__.py 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cookies==2.2.1 2 | requests==2.10.0 3 | requests-mock==1.0.0 4 | responses==0.5.1 5 | six==1.10.0 6 | nose==1.3.7 -------------------------------------------------------------------------------- /kairos_face/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | app_id = os.getenv('KAIROS_APP_ID') 4 | app_key = os.getenv('KAIROS_APP_KEY') 5 | base_url = 'https://api.kairos.com/' 6 | -------------------------------------------------------------------------------- /kairos_face/entities.py: -------------------------------------------------------------------------------- 1 | class KairosFaceGallery: 2 | def __init__(self, gallery_name, subject_ids): 3 | self.name = gallery_name 4 | self.subjects = subject_ids 5 | -------------------------------------------------------------------------------- /kairos_face/__init__.py: -------------------------------------------------------------------------------- 1 | from kairos_face import settings 2 | from kairos_face.exceptions import * 3 | from kairos_face.enroll import * 4 | from kairos_face.remove import * 5 | from kairos_face.recognize import * 6 | from kairos_face.gallery import * 7 | from kairos_face.detect import * 8 | from kairos_face.verify import * 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # from distutils.core import setup 2 | from setuptools import setup 3 | 4 | 5 | setup( 6 | name='kairos_face_recognition_lib', 7 | version='0.3.0', 8 | packages=['kairos_face'], 9 | url='https://github.com/ffmmjj/kairos-face-sdk-python', 10 | license='MIT', 11 | author='Felipe Martins', 12 | author_email='', 13 | description='Kairos Face Recognition API Python Client Library', 14 | install_requires=[ 15 | 'requests' 16 | ] 17 | ) 18 | -------------------------------------------------------------------------------- /kairos_face/utils.py: -------------------------------------------------------------------------------- 1 | from kairos_face import settings, exceptions 2 | 3 | 4 | def validate_file_and_url_presence(file, url): 5 | if not file and not url: 6 | raise ValueError('An image file or valid URL must be passed') 7 | if file and url: 8 | raise ValueError('Cannot receive both a file and URL as arguments') 9 | 10 | 11 | def validate_settings(): 12 | if settings.app_id is None: 13 | raise exceptions.SettingsNotPresentException('Kairos app_id was not set') 14 | if settings.app_key is None: 15 | raise exceptions.SettingsNotPresentException('Kairos app_key was not set') 16 | -------------------------------------------------------------------------------- /kairos_face/exceptions.py: -------------------------------------------------------------------------------- 1 | class SettingsNotPresentException(Exception): 2 | def __init__(self, msg): 3 | self.msg = msg 4 | 5 | def __repr__(self): 6 | return self.msg 7 | 8 | def __str__(self): 9 | return self.__repr__() 10 | 11 | 12 | class ServiceRequestError(Exception): 13 | def __init__(self, status_code, response_msg, payload): 14 | self.status_code = status_code 15 | self.response_msg = response_msg 16 | self.payload = payload 17 | 18 | def __repr__(self): 19 | return str(self.response_msg) 20 | 21 | def __str__(self): 22 | return self.__repr__() 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Felipe Martins 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 | -------------------------------------------------------------------------------- /kairos_face/remove.py: -------------------------------------------------------------------------------- 1 | from kairos_face import exceptions, validate_settings 2 | from kairos_face import settings 3 | import requests 4 | 5 | _remove_base_url = settings.base_url + 'gallery/remove_subject' 6 | 7 | 8 | def remove_face(subject_id, gallery_name): 9 | validate_settings() 10 | _validate_arguments_presence(gallery_name, subject_id) 11 | 12 | auth_headers = { 13 | 'app_id': settings.app_id, 14 | 'app_key': settings.app_key 15 | } 16 | 17 | payload = _build_payload(gallery_name, subject_id) 18 | 19 | response = requests.post(_remove_base_url, json=payload, headers=auth_headers) 20 | json_response = response.json() 21 | if response.status_code != 200 or 'Errors' in json_response: 22 | raise exceptions.ServiceRequestError(response.status_code, json_response, payload) 23 | 24 | return json_response 25 | 26 | 27 | def _validate_arguments_presence(gallery_name, subject_id): 28 | if not subject_id: 29 | raise ValueError('A subject ID must be passed') 30 | if not gallery_name: 31 | raise ValueError('A gallery name must be passed') 32 | 33 | 34 | def _build_payload(gallery_name, subject_id): 35 | return { 36 | 'gallery_name': gallery_name, 37 | 'subject_id': subject_id 38 | } 39 | -------------------------------------------------------------------------------- /integration_tests/test_enroll_integration.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | 3 | import kairos_face 4 | import os 5 | import unittest 6 | 7 | from kairos_face.exceptions import ServiceRequestError 8 | 9 | 10 | class EnrollIntegrationTest(unittest.TestCase): 11 | def setUp(self): 12 | # It was not possible to find a reliable, publicly available URL pointing to a face picture 13 | # with nice quality. 14 | # To avoid legal issues, you'l have to set up your own ;) 15 | self.face_example_url = os.environ.get('EXAMPLE_FACE_URL') 16 | 17 | def test_image_response_is_returned(self): 18 | try: 19 | response = kairos_face.enroll_face( 20 | 'integration-test-face', 21 | 'integration-test-gallery', 22 | url=self.face_example_url 23 | ) 24 | 25 | self.assertIsNotNone(response['face_id']) 26 | self.assertEqual('M', response['images'][0]['attributes']['gender']['type']) 27 | except ServiceRequestError: 28 | traceback.print_exc() 29 | self.fail('This should not be raising an exception...') 30 | finally: 31 | kairos_face.remove_face(subject_id='integration-test-face', 32 | gallery_name='integration-test-gallery') 33 | -------------------------------------------------------------------------------- /kairos_face/detect.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import base64 3 | from kairos_face import exceptions, validate_settings, validate_file_and_url_presence 4 | from kairos_face import settings 5 | 6 | _detect_base_url = settings.base_url + 'detect' 7 | 8 | 9 | def detect_face(url=None, file=None, additional_arguments={}): 10 | validate_settings() 11 | validate_file_and_url_presence(file, url) 12 | 13 | auth_headers = { 14 | 'app_id': settings.app_id, 15 | 'app_key': settings.app_key 16 | } 17 | payload = _build_payload(url, file, additional_arguments) 18 | 19 | response = requests.post(_detect_base_url, json=payload, headers=auth_headers) 20 | json_response = response.json() 21 | if response.status_code != 200 or 'Errors' in json_response: 22 | raise exceptions.ServiceRequestError(response.status_code, json_response, payload) 23 | 24 | return json_response 25 | 26 | 27 | def _build_payload(url, file, additional_arguments): 28 | if file is not None: 29 | image = _extract_base64_contents(file) 30 | else: 31 | image = url 32 | 33 | required_fields = { 34 | 'image': image 35 | } 36 | 37 | return dict(required_fields, **additional_arguments) 38 | 39 | 40 | def _extract_base64_contents(image_path): 41 | with open(image_path, 'rb') as fp: 42 | return base64.b64encode(fp.read()).decode('ascii') 43 | -------------------------------------------------------------------------------- /kairos_face/recognize.py: -------------------------------------------------------------------------------- 1 | from kairos_face import exceptions, validate_settings, validate_file_and_url_presence 2 | from kairos_face import settings 3 | import requests 4 | import base64 5 | 6 | _recognize_base_url = settings.base_url + 'recognize' 7 | 8 | 9 | def recognize_face(gallery_name, url=None, file=None, additional_arguments={}): 10 | validate_settings() 11 | validate_file_and_url_presence(file, url) 12 | 13 | auth_headers = { 14 | 'app_id': settings.app_id, 15 | 'app_key': settings.app_key 16 | } 17 | payload = _build_payload(gallery_name, url, file, additional_arguments) 18 | 19 | response = requests.post(_recognize_base_url, json=payload, headers=auth_headers) 20 | json_response = response.json() 21 | if response.status_code != 200 or 'Errors' in json_response: 22 | raise exceptions.ServiceRequestError(response.status_code, json_response, payload) 23 | 24 | return json_response 25 | 26 | 27 | def _build_payload(gallery_name, url, file, additional_arguments): 28 | if file is not None: 29 | image = _extract_base64_contents(file) 30 | else: 31 | image = url 32 | 33 | required_fields = { 34 | 'image': image, 35 | 'gallery_name': gallery_name 36 | } 37 | 38 | return dict(required_fields, **additional_arguments) 39 | 40 | 41 | def _extract_base64_contents(image_path): 42 | with open(image_path, 'rb') as fp: 43 | return base64.b64encode(fp.read()).decode('ascii') 44 | -------------------------------------------------------------------------------- /kairos_face/verify.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import requests 3 | from kairos_face import exceptions 4 | from kairos_face import settings 5 | from kairos_face.utils import validate_file_and_url_presence, validate_settings 6 | 7 | _verify_base_url = settings.base_url + 'verify' 8 | 9 | 10 | def verify_face(subject_id, gallery_name, url=None, file=None, additional_arguments={}): 11 | validate_settings() 12 | validate_file_and_url_presence(file, url) 13 | 14 | auth_headers = { 15 | 'app_id': settings.app_id, 16 | 'app_key': settings.app_key 17 | } 18 | 19 | payload = _build_payload(subject_id, gallery_name, url, file, additional_arguments) 20 | 21 | response = requests.post(_verify_base_url, json=payload, headers=auth_headers) 22 | json_response = response.json() 23 | if response.status_code != 200 or 'Errors' in json_response: 24 | raise exceptions.ServiceRequestError(response.status_code, json_response, payload) 25 | 26 | return json_response 27 | 28 | 29 | def _build_payload(subject_id, gallery_name, url, file, additional_arguments): 30 | if file is not None: 31 | image = _extract_base64_contents(file) 32 | else: 33 | image = url 34 | required_fields = {'image': image, 'subject_id': subject_id, 35 | 'gallery_name': gallery_name} 36 | 37 | return dict(required_fields, **additional_arguments) 38 | 39 | 40 | def _extract_base64_contents(file): 41 | with open(file, 'rb') as fp: 42 | image = base64.b64encode(fp.read()).decode('ascii') 43 | return image 44 | -------------------------------------------------------------------------------- /kairos_face/enroll.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | import requests 4 | 5 | from kairos_face import exceptions 6 | from kairos_face import settings 7 | from kairos_face.utils import validate_file_and_url_presence, validate_settings 8 | 9 | _enroll_base_url = settings.base_url + 'enroll' 10 | 11 | 12 | def enroll_face(subject_id, gallery_name, 13 | url=None, file=None, base64_image_contents=None, 14 | multiple_faces=False, additional_arguments={}): 15 | validate_settings() 16 | validate_file_and_url_presence(file, url) 17 | 18 | auth_headers = { 19 | 'app_id': settings.app_id, 20 | 'app_key': settings.app_key 21 | } 22 | 23 | payload = _build_payload(subject_id, gallery_name, url, file, 24 | base64_image_contents, multiple_faces, additional_arguments) 25 | 26 | response = requests.post(_enroll_base_url, json=payload, headers=auth_headers) 27 | json_response = response.json() 28 | if response.status_code != 200 or 'Errors' in json_response: 29 | raise exceptions.ServiceRequestError(response.status_code, json_response, payload) 30 | 31 | return json_response 32 | 33 | 34 | def _build_payload(subject_id, gallery_name, url, file, imgframe, multiple_faces, additional_arguments): 35 | if imgframe is not None: 36 | image = imgframe 37 | elif file is not None: 38 | image = _extract_base64_contents(file) 39 | else: 40 | image = url 41 | required_fields = {'image': image, 'subject_id': subject_id, 42 | 'gallery_name': gallery_name, 'multiple_faces': multiple_faces} 43 | 44 | return dict(required_fields, **additional_arguments) 45 | 46 | 47 | def _extract_base64_contents(file): 48 | with open(file, 'rb') as fp: 49 | image = base64.b64encode(fp.read()).decode('ascii') 50 | return image 51 | -------------------------------------------------------------------------------- /tests/test_remove.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import responses 3 | 4 | import kairos_face 5 | 6 | 7 | class KairosApiRemoveFaceTest(unittest.TestCase): 8 | def setUp(self): 9 | kairos_face.settings.app_id = 'app_id' 10 | kairos_face.settings.app_key = 'app_key' 11 | 12 | def test_throws_exception_when_app_id_is_not_set(self): 13 | kairos_face.settings.app_id = None 14 | 15 | with self.assertRaises(kairos_face.SettingsNotPresentException): 16 | kairos_face.remove_face(subject_id='sub_id', gallery_name='gallery') 17 | 18 | def test_throws_exception_when_app_key_is_not_set(self): 19 | kairos_face.settings.app_key = None 20 | 21 | with self.assertRaises(kairos_face.SettingsNotPresentException): 22 | kairos_face.remove_face(subject_id='sub_id', gallery_name='gallery') 23 | 24 | def test_throws_exception_when_subject_id_is_empty_string(self): 25 | with self.assertRaises(ValueError): 26 | kairos_face.remove_face(subject_id='', gallery_name='gallery') 27 | 28 | def test_throws_exception_when_gallery_name_is_empty_string(self): 29 | with self.assertRaises(ValueError): 30 | kairos_face.remove_face(subject_id='sub_id', gallery_name='') 31 | 32 | @responses.activate 33 | def test_remove_face_that_does_not_exist_raises_exception(self): 34 | response_body = '{"Errors":[{"ErrCode":5003,"Message":"subject id was not found"}]}' 35 | responses.add(responses.POST, 'https://api.kairos.com/gallery/remove_subject', 36 | status=200, 37 | body=response_body) 38 | 39 | with self.assertRaises(kairos_face.ServiceRequestError): 40 | kairos_face.remove_face(gallery_name='gallery_name', subject_id='non_existing_face') 41 | 42 | @responses.activate 43 | def test_remove_existing_face_returns_success_payload(self): 44 | response_body = """ 45 | { 46 | "status": "Complete", 47 | "message": "subject id integration-test-face2 has been successfully removed" 48 | } 49 | """ 50 | responses.add(responses.POST, 'https://api.kairos.com/gallery/remove_subject', 51 | status=200, 52 | body=response_body) 53 | 54 | response = kairos_face.remove_face(gallery_name='gallery_name', subject_id='existing_face') 55 | 56 | self.assertEqual('Complete', response['status']) 57 | self.assertTrue('message' in response) 58 | -------------------------------------------------------------------------------- /kairos_face/gallery.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from kairos_face import exceptions, validate_settings 4 | from kairos_face import settings 5 | from kairos_face.entities import KairosFaceGallery 6 | 7 | _gallery_base_url = settings.base_url + 'gallery/view' 8 | _galleries_list_url = settings.base_url + 'gallery/list_all' 9 | _gallery_remove_url = settings.base_url + 'gallery/remove' 10 | 11 | 12 | def get_gallery(gallery_name): 13 | validate_settings() 14 | _validate_gallery_name(gallery_name) 15 | 16 | auth_headers = { 17 | 'app_id': settings.app_id, 18 | 'app_key': settings.app_key 19 | } 20 | 21 | payload = {'gallery_name': gallery_name} 22 | response = requests.post(_gallery_base_url, json=payload, headers=auth_headers) 23 | json_response = response.json() 24 | if response.status_code != 200 or 'Errors' in json_response: 25 | raise exceptions.ServiceRequestError(response.status_code, json_response, payload) 26 | 27 | return response.json() 28 | 29 | 30 | def get_galleries_names_list(): 31 | validate_settings() 32 | 33 | auth_headers = { 34 | 'app_id': settings.app_id, 35 | 'app_key': settings.app_key 36 | } 37 | 38 | response = requests.post(_galleries_list_url, headers=auth_headers) 39 | json_response = response.json() 40 | if response.status_code != 200 or 'Errors' in json_response: 41 | raise exceptions.ServiceRequestError(response.status_code, json_response, None) 42 | 43 | return json_response 44 | 45 | 46 | def remove_gallery(gallery_name): 47 | validate_settings() 48 | _validate_gallery_name(gallery_name) 49 | 50 | auth_headers = { 51 | 'app_id': settings.app_id, 52 | 'app_key': settings.app_key 53 | } 54 | 55 | payload = {'gallery_name': gallery_name} 56 | 57 | response = requests.post(_gallery_remove_url, json=payload, headers=auth_headers) 58 | json_response = response.json() 59 | if response.status_code != 200 or 'Errors' in json_response: 60 | raise exceptions.ServiceRequestError(response.status_code, json_response, payload) 61 | 62 | return json_response 63 | 64 | 65 | def get_galleries_names_object(): 66 | validate_settings() 67 | 68 | auth_headers = { 69 | 'app_id': settings.app_id, 70 | 'app_key': settings.app_key 71 | } 72 | 73 | response = requests.post(_galleries_list_url, headers=auth_headers) 74 | json_response = response.json() 75 | if response.status_code != 200 or 'Errors' in json_response: 76 | raise exceptions.ServiceRequestError(response.status_code, json_response, None) 77 | 78 | return json_response['gallery_ids'] 79 | 80 | 81 | def get_gallery_object(gallery_name): 82 | validate_settings() 83 | _validate_gallery_name(gallery_name) 84 | 85 | auth_headers = { 86 | 'app_id': settings.app_id, 87 | 'app_key': settings.app_key 88 | } 89 | 90 | payload = {'gallery_name': gallery_name} 91 | response = requests.post(_gallery_base_url, json=payload, headers=auth_headers) 92 | json_response = response.json() 93 | if response.status_code != 200 or 'Errors' in json_response: 94 | raise exceptions.ServiceRequestError(response.status_code, json_response, payload) 95 | 96 | return KairosFaceGallery(gallery_name, json_response['subject_ids']) 97 | 98 | 99 | def _validate_gallery_name(gallery_name): 100 | if not gallery_name: 101 | raise ValueError('gallery_name cannot be empty') 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kairos-face-sdk-python 2 | Kairos Face Recognition API Python Client Library 3 | 4 | ## Installation 5 | `pip install .` inside the project root directory 6 | 7 | ## Usage 8 | ### Setting up the API keys 9 | The library exposes a *settings* object where the API keys can be set. It should be remarked that the API keys **must be set before** any of the library's functions is used: 10 | 11 | ```python 12 | import kairos_face 13 | 14 | kairos_face.settings.app_id = 15 | kairos_face.settings.app_key = 16 | ``` 17 | 18 | Your API keys can be found in your Kairo's admin dashboard. 19 | 20 | ## Enrolling new faces 21 | A face can be enrolled by passing an image URL or file: 22 | 23 | ```python 24 | import kairos_face 25 | 26 | # Enrolling from a URL 27 | kairos_face.enroll_face(url='http://some.server/some-image.jpg', subject_id='subject1', gallery_name='a-gallery') 28 | 29 | # Enrolling from a file 30 | kairos_face.enroll_face(file=image_file, subject_id='subject1', gallery_name='a-gallery') 31 | ``` 32 | 33 | ## Detect a face 34 | The API can detect a face from a image passed as an URL or a file. 35 | 36 | ```python 37 | import kairos_face 38 | 39 | # Detect from an URL 40 | recognized_faces = kairos_face.detect_face(url='http://some.server/some-image.jpg', gallery_name='a-gallery') 41 | 42 | # Detect from a file 43 | recognized_faces = kairos_face.detect_face(file=local_image_file, gallery_name='a-gallery') 44 | ``` 45 | 46 | ## Recognizing a face 47 | The API can identify a face in an existing gallery from a image passed as an URL or a file. 48 | 49 | ```python 50 | import kairos_face 51 | 52 | # Recognizing from an URL 53 | recognized_faces = kairos_face.recognize_face(url='http://some.server/some-image.jpg', gallery_name='a-gallery') 54 | 55 | # Recognizing from a file 56 | recognized_faces = kairos_face.recognize_face(file=local_image_file, gallery_name='a-gallery') 57 | ``` 58 | ## Verify a face 59 | The API can verify that a face belongs to a specific person in an existing gallery from a image passed as an URL or a file. 60 | 61 | ```python 62 | import kairos_face 63 | 64 | # Verify from an URL 65 | recognized_faces = kairos_face.verify_face(url='http://some.server/some-image.jpg', gallery_name='a-gallery') 66 | 67 | # Verify from a file 68 | recognized_faces = kairos_face.verify_face(file=local_image_file, gallery_name='a-gallery') 69 | ``` 70 | 71 | ## Galleries 72 | Face subjects are grouped in galleries. 73 | 74 | #### Get Galleries 75 | List all galleries that have been created. 76 | 77 | ```python 78 | import kairos_face 79 | 80 | galleries_list = kairos_face.get_galleries_names_list() 81 | ``` 82 | 83 | #### Get Gallery 84 | Get a list of subjects in a specific gallery. 85 | 86 | ```python 87 | import kairos_face 88 | 89 | gallery_subjects = kairos_face.get_gallery('a-gallery') 90 | ``` 91 | 92 | #### Remove Gallery 93 | Remove a gallery and all its subjects. 94 | 95 | ```python 96 | import kairos_face 97 | 98 | remove_gallery = kairos_face.remove_gallery('a-gallery') 99 | ``` 100 | 101 | There are special methods which combine to render each gallery, followed by a list of subjects enrolled in that gallery: 102 | 103 | ```python 104 | import kairos_face 105 | 106 | galleries_object = kairos_face.get_galleries_names_object() 107 | 108 | for gallery_name in galleries_object: 109 | gallery = kairos_face.get_gallery_object(gallery_name) 110 | print('Gallery name: {}'.format(gallery.name)) 111 | print('Gallery subjects: {}'.format(gallery.subjects)) 112 | ``` 113 | 114 | ## Removing an enrolled face 115 | Previously enrolled faces can be removed from a gallery: 116 | 117 | ```python 118 | import kairos_face 119 | 120 | kairos_face.remove_face(subject_id='subject1', gallery_name='a-gallery') 121 | ``` 122 | 123 | -------------------------------------------------------------------------------- /tests/test_gallery.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | import responses 4 | from unittest import mock 5 | 6 | import kairos_face 7 | 8 | 9 | class KairosApiGalleryTest(unittest.TestCase): 10 | def setUp(self): 11 | kairos_face.settings.app_id = 'app_id' 12 | kairos_face.settings.app_key = 'app_key' 13 | 14 | def test_throws_exception_when_app_id_is_not_set(self): 15 | kairos_face.settings.app_id = None 16 | 17 | with self.assertRaises(kairos_face.SettingsNotPresentException): 18 | kairos_face.get_gallery(gallery_name='gallery') 19 | 20 | def test_throws_exception_when_app_key_is_not_set(self): 21 | kairos_face.settings.app_key = None 22 | 23 | with self.assertRaises(kairos_face.SettingsNotPresentException): 24 | kairos_face.get_gallery(gallery_name='gallery') 25 | 26 | @mock.patch('kairos_face.requests.post') 27 | def test_passes_app_id_and_key_in_post_header(self, post_mock): 28 | post_mock.return_value.status_code = 200 29 | 30 | kairos_face.get_gallery(gallery_name='gallery') 31 | 32 | _, kwargs = post_mock.call_args 33 | expected_headers = { 34 | 'app_id': 'app_id', 35 | 'app_key': 'app_key' 36 | } 37 | self.assertTrue('headers' in kwargs) 38 | self.assertEqual(expected_headers, kwargs['headers']) 39 | 40 | @mock.patch('kairos_face.requests.post') 41 | def test_payload_with_gallery_name_is_passed_in_request(self, post_mock): 42 | post_mock.return_value.status_code = 200 43 | 44 | kairos_face.get_gallery(gallery_name='gallery') 45 | 46 | _, kwargs = post_mock.call_args 47 | expected_payload = { 48 | 'gallery_name': 'gallery' 49 | } 50 | self.assertTrue('json' in kwargs) 51 | self.assertEqual(expected_payload, kwargs['json']) 52 | 53 | @responses.activate 54 | def test_getting_non_existing_gallery_raises_an_exception(self): 55 | response_body = {'Errors': [{'ErrCode': 5004, 'Message': 'gallery name not found'}]} 56 | responses.add(responses.POST, 'https://api.kairos.com/gallery/view', 57 | status=200, 58 | body=json.dumps(response_body)) 59 | 60 | with self.assertRaises(kairos_face.ServiceRequestError): 61 | kairos_face.get_gallery('non-existing-gallery') 62 | 63 | @responses.activate 64 | def test_returned_gallery_has_face_subjects_list(self): 65 | response_body = { 66 | 'time': 0.00991, 67 | 'status': 'Complete', 68 | 'subject_ids': [ 69 | 'subject1', 70 | 'subject2', 71 | 'subject3' 72 | ] 73 | } 74 | responses.add(responses.POST, 'https://api.kairos.com/gallery/view', 75 | status=200, 76 | body=json.dumps(response_body)) 77 | 78 | actual_response = kairos_face.get_gallery('a-gallery') 79 | 80 | self.assertEqual('Complete', actual_response['status']) 81 | self.assertEqual(3, len(actual_response['subject_ids'])) 82 | self.assertTrue('subject1' in actual_response['subject_ids']) 83 | self.assertTrue('subject2' in actual_response['subject_ids']) 84 | self.assertTrue('subject3' in actual_response['subject_ids']) 85 | 86 | 87 | class KairosApiGetGalleriesListTest(unittest.TestCase): 88 | def setUp(self): 89 | kairos_face.settings.app_id = 'app_id' 90 | kairos_face.settings.app_key = 'app_key' 91 | 92 | def test_throws_exception_when_app_id_is_not_set(self): 93 | kairos_face.settings.app_id = None 94 | 95 | with self.assertRaises(kairos_face.SettingsNotPresentException): 96 | kairos_face.get_galleries_names_list() 97 | 98 | def test_throws_exception_when_app_key_is_not_set(self): 99 | kairos_face.settings.app_key = None 100 | 101 | with self.assertRaises(kairos_face.SettingsNotPresentException): 102 | kairos_face.get_galleries_names_list() 103 | 104 | @responses.activate 105 | def test_returns_empty_list_when_no_galleries_are_present(self): 106 | response_body = { 107 | 'time': 0.00991, 108 | 'status': 'Complete', 109 | 'gallery_ids': [] 110 | } 111 | responses.add(responses.POST, 'https://api.kairos.com/gallery/list_all', 112 | status=200, 113 | body=json.dumps(response_body)) 114 | 115 | actual_response = kairos_face.get_galleries_names_list() 116 | 117 | self.assertEqual(0, len(actual_response['gallery_ids'])) 118 | 119 | @responses.activate 120 | def test_returns_available_galleries_names(self): 121 | response_body = { 122 | 'time': 0.00991, 123 | 'status': 'Complete', 124 | 'gallery_ids': [ 125 | 'gallery1', 126 | 'gallery2' 127 | ] 128 | } 129 | responses.add(responses.POST, 'https://api.kairos.com/gallery/list_all', 130 | status=200, 131 | body=json.dumps(response_body)) 132 | 133 | actual_response = kairos_face.get_galleries_names_list() 134 | 135 | self.assertEquals('Complete', actual_response['status']) 136 | self.assertEqual(2, len(actual_response['gallery_ids'])) 137 | self.assertTrue('gallery1' in actual_response['gallery_ids']) 138 | self.assertTrue('gallery2' in actual_response['gallery_ids']) 139 | -------------------------------------------------------------------------------- /tests/test_recognize.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | from io import BufferedReader 4 | from unittest import mock 5 | 6 | import responses 7 | 8 | import kairos_face 9 | 10 | 11 | class KairosApiRecognizeFaceTest(unittest.TestCase): 12 | def setUp(self): 13 | kairos_face.settings.app_id = 'app_id' 14 | kairos_face.settings.app_key = 'app_key' 15 | 16 | def test_throws_exception_when_app_id_is_not_set(self): 17 | kairos_face.settings.app_id = None 18 | 19 | with self.assertRaises(kairos_face.SettingsNotPresentException): 20 | kairos_face.recognize_face('gallery', url='an_image_url.jpg') 21 | 22 | def test_throws_exception_when_app_key_is_not_set(self): 23 | kairos_face.settings.app_key = None 24 | 25 | with self.assertRaises(kairos_face.SettingsNotPresentException): 26 | kairos_face.recognize_face('gallery', url='an_image_url.jpg') 27 | 28 | def test_throws_exception_when_url_is_empty_string(self): 29 | with self.assertRaises(ValueError): 30 | kairos_face.recognize_face('gallery', url='') 31 | 32 | def test_throws_exception_when_file_is_empty_string(self): 33 | with self.assertRaises(ValueError): 34 | kairos_face.recognize_face('gallery', file='') 35 | 36 | def test_throws_exception_when_both_file_and_url_are_passed(self): 37 | with self.assertRaises(ValueError): 38 | kairos_face.recognize_face('gallery', url='an_image_url.jpg', file='/path/tp/image.jpg') 39 | 40 | @mock.patch('kairos_face.requests.post') 41 | def test_passes_required_arguments_in_payload_as_json_when_image_is_file(self, post_mock): 42 | post_mock.return_value.status_code = 200 43 | 44 | with mock.patch('builtins.open', mock.mock_open(read_data=str.encode('test'))): 45 | with open('/a/image/file.jpg', 'rb') as image_file: 46 | image_file.__class__ = BufferedReader 47 | kairos_face.recognize_face('gallery', file=image_file) 48 | 49 | _, kwargs = post_mock.call_args 50 | expected_payload = { 51 | 'image': 'dGVzdA==', 52 | 'gallery_name': 'gallery' 53 | } 54 | self.assertTrue('json' in kwargs) 55 | self.assertEqual(expected_payload, kwargs['json']) 56 | 57 | @mock.patch('kairos_face.requests.post') 58 | def test_passes_additional_arguments_in_payload(self, post_mock): 59 | post_mock.return_value.status_code = 200 60 | additional_arguments = { 61 | 'max_num_results': '5', 62 | 'selector': 'EYES' 63 | } 64 | 65 | kairos_face.recognize_face('gallery', url='an_image_url.jpg', 66 | additional_arguments=additional_arguments) 67 | 68 | _, kwargs = post_mock.call_args 69 | passed_payload = kwargs['json'] 70 | self.assertTrue('max_num_results' in passed_payload) 71 | self.assertEqual('5', passed_payload['max_num_results']) 72 | self.assertTrue('selector' in passed_payload) 73 | self.assertEqual('EYES', passed_payload['selector']) 74 | 75 | @responses.activate 76 | def test_returns_matching_images(self): 77 | response_body = { 78 | 'images': [ 79 | { 80 | 'time': 2.86091, 81 | 'transaction': 82 | { 83 | 'status': 'Complete', 84 | 'subject': 'test2', 85 | 'confidence': '0.802138030529022', 86 | 'gallery_name': 'gallerytest1', 87 | }, 88 | 'candidates': [ 89 | { 90 | 'test2': '0.802138030529022', 91 | 'enrollment_timestamp': '1416850761' 92 | }, 93 | { 94 | 'elizabeth': '0.602138030529022', 95 | 'enrollment_timestamp': '1417207485' 96 | } 97 | ] 98 | } 99 | ] 100 | } 101 | responses.add(responses.POST, 'https://api.kairos.com/recognize', 102 | status=200, 103 | body=json.dumps(response_body)) 104 | 105 | face_candidates_subjects = kairos_face.recognize_face('gallery_name', url='an_image_url.jpg') 106 | 107 | self.assertEqual(2, len(face_candidates_subjects['images'][0]['candidates'])) 108 | 109 | image_response = face_candidates_subjects['images'][0] 110 | self.assertEquals('Complete', image_response['transaction']['status']) 111 | 112 | candidates = image_response['candidates'] 113 | self.assertEquals(2, len(candidates)) 114 | self.assertIn('test2', candidates[0]) 115 | self.assertIn('0.802138030529022', candidates[0].values()) 116 | self.assertIn('elizabeth', candidates[1]) 117 | self.assertIn('0.602138030529022', candidates[1].values()) 118 | 119 | @responses.activate 120 | def test_returns_transaction_failure_when_face_is_not_recognized(self): 121 | response_body = { 122 | 'images': [{ 123 | 'time': 6.43752, 124 | 'transaction': { 125 | 'status': 'failure', 126 | 'message': 'No match found', 127 | 'gallery_name': 'gallery_name' 128 | }, 129 | }] 130 | } 131 | responses.add(responses.POST, 'https://api.kairos.com/recognize', 132 | status=200, 133 | body=json.dumps(response_body)) 134 | 135 | face_candidates_subjects = kairos_face.recognize_face('gallery_name', url='an_image_url.jpg') 136 | 137 | self.assertEquals(1, len(face_candidates_subjects['images'])) 138 | 139 | image_response = face_candidates_subjects['images'][0] 140 | self.assertEquals('failure', image_response['transaction']['status']) 141 | self.assertEquals('No match found', image_response['transaction']['message']) 142 | -------------------------------------------------------------------------------- /tests/test_enroll.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | from unittest import mock 4 | import responses 5 | 6 | import kairos_face 7 | 8 | 9 | class KairosApiEnrollFacesTest(unittest.TestCase): 10 | def setUp(self): 11 | kairos_face.settings.app_id = 'app_id' 12 | kairos_face.settings.app_key = 'app_key' 13 | 14 | def test_throws_exception_when_app_id_is_not_set(self): 15 | kairos_face.settings.app_id = None 16 | 17 | with self.assertRaises(kairos_face.SettingsNotPresentException): 18 | kairos_face.enroll_face('sub_id', 'gallery', url='a_image_path.jpg') 19 | 20 | def test_throws_exception_when_app_key_is_not_set(self): 21 | kairos_face.settings.app_key = None 22 | 23 | with self.assertRaises(kairos_face.SettingsNotPresentException): 24 | kairos_face.enroll_face('sub_id', 'gallery', url='a_image_path.jpg') 25 | 26 | def test_throws_exception_when_url_is_empty_string(self): 27 | with self.assertRaises(ValueError): 28 | kairos_face.enroll_face('subject_id', 'gallery', url='') 29 | 30 | def test_throws_exception_when_file_is_empty_string(self): 31 | with self.assertRaises(ValueError): 32 | kairos_face.enroll_face('subject_id', 'gallery', file='') 33 | 34 | def test_throws_exception_when_both_file_and_url_are_passed(self): 35 | with self.assertRaises(ValueError): 36 | kairos_face.enroll_face('subject_id', 'gallery', 37 | url='an_image_url.jpg', file='/path/tp/image.jpg') 38 | 39 | def test_throws_exception_when_both_file_and_imgframe_are_passed(self): 40 | with self.assertRaises(ValueError): 41 | kairos_face.enroll_face('subject_id', 'gallery', 42 | url='an_image_url.jpg', base64_image_contents='aBase64EncodedImageContents') 43 | 44 | @mock.patch('kairos_face.requests.post') 45 | def test_passes_api_url_in_post_request(self, post_mock): 46 | post_mock.return_value.status_code = 200 47 | 48 | kairos_face.enroll_face('sub_id', 'gallery', url='image') 49 | 50 | args, _ = post_mock.call_args 51 | self.assertEqual(1, len(args), 'No positional arguments were passed to post request') 52 | self.assertEqual('https://api.kairos.com/enroll', args[0]) 53 | 54 | @mock.patch('kairos_face.requests.post') 55 | def test_passes_app_id_and_key_in_post_header(self, post_mock): 56 | post_mock.return_value.status_code = 200 57 | 58 | kairos_face.enroll_face('sub_id', 'gallery', url='a_image_url.jpg') 59 | 60 | _, kwargs = post_mock.call_args 61 | expected_headers = { 62 | 'app_id': 'app_id', 63 | 'app_key': 'app_key' 64 | } 65 | self.assertTrue('headers' in kwargs) 66 | self.assertEqual(expected_headers, kwargs['headers']) 67 | 68 | @mock.patch('kairos_face.requests.post') 69 | def test_passes_required_arguments_in_payload_as_json_when_image_is_url(self, post_mock): 70 | post_mock.return_value.status_code = 200 71 | 72 | kairos_face.enroll_face('sub_id', 'gallery', url='a_image_url.jpg') 73 | 74 | _, kwargs = post_mock.call_args 75 | expected_payload = { 76 | 'image': 'a_image_url.jpg', 77 | 'subject_id': 'sub_id', 78 | 'gallery_name': 'gallery', 79 | 'multiple_faces': False 80 | } 81 | self.assertTrue('json' in kwargs) 82 | self.assertEqual(expected_payload, kwargs['json']) 83 | 84 | @mock.patch('kairos_face.requests.post') 85 | def test_passes_required_arguments_in_payload_as_json_when_image_is_file(self, post_mock): 86 | post_mock.return_value.status_code = 200 87 | 88 | m = mock.mock_open(read_data=str.encode('test')) 89 | with mock.patch('builtins.open', m, create=True): 90 | kairos_face.enroll_face('sub_id', 'gallery', file='/a/image/file.jpg') 91 | 92 | _, kwargs = post_mock.call_args 93 | expected_payload = { 94 | 'image': 'dGVzdA==', 95 | 'subject_id': 'sub_id', 96 | 'gallery_name': 'gallery', 97 | 'multiple_faces': False 98 | } 99 | self.assertTrue('json' in kwargs) 100 | self.assertEqual(expected_payload, kwargs['json']) 101 | 102 | @mock.patch('kairos_face.requests.post') 103 | def test_passes_multiple_faces_argument_in_payload_when_flag_is_set(self, post_mock): 104 | post_mock.return_value.status_code = 200 105 | 106 | m = mock.mock_open(read_data=str.encode('test')) 107 | with mock.patch('builtins.open', m, create=True): 108 | kairos_face.enroll_face('sub_id', 'gallery', file='/a/image/file.jpg', multiple_faces=True) 109 | 110 | _, kwargs = post_mock.call_args 111 | expected_payload = { 112 | 'image': 'dGVzdA==', 113 | 'subject_id': 'sub_id', 114 | 'gallery_name': 'gallery', 115 | 'multiple_faces': True 116 | } 117 | self.assertTrue('json' in kwargs) 118 | self.assertEqual(expected_payload, kwargs['json']) 119 | 120 | @mock.patch('kairos_face.requests.post') 121 | def test_passes_additional_arguments_in_payload(self, post_mock): 122 | post_mock.return_value.status_code = 200 123 | additional_arguments = { 124 | 'selector': 'SETPOSE', 125 | 'symmetricFill': True 126 | } 127 | 128 | kairos_face.enroll_face('sub_id', 'gallery', 129 | url='a_image_url.jpg', additional_arguments=additional_arguments) 130 | 131 | _, kwargs = post_mock.call_args 132 | passed_payload = kwargs['json'] 133 | self.assertTrue('selector' in passed_payload) 134 | self.assertEqual('SETPOSE', passed_payload['selector']) 135 | self.assertTrue('symmetricFill' in passed_payload) 136 | self.assertEqual(True, passed_payload['symmetricFill']) 137 | 138 | @responses.activate 139 | def test_raises_exception_when_http_status_response_is_error(self): 140 | response_body = '{"error_message": "something something dark side..."}' 141 | responses.add(responses.POST, 'https://api.kairos.com/enroll', status=400, 142 | body=response_body) 143 | 144 | with self.assertRaises(kairos_face.ServiceRequestError): 145 | kairos_face.enroll_face('sub_id', 'gallery', url='a_image_url.jpg') 146 | 147 | @responses.activate 148 | def test_raises_exception_when_http_response_body_contains_error_field(self): 149 | response_body = '{"Errors": "something something dark side..."}' 150 | responses.add(responses.POST, 'https://api.kairos.com/enroll', status=200, 151 | body=response_body) 152 | 153 | with self.assertRaises(kairos_face.ServiceRequestError): 154 | kairos_face.enroll_face('sub_id', 'gallery', url='a_image_url.jpg') 155 | 156 | @responses.activate 157 | def test_returns_face_id_from_kairos_response(self): 158 | response_dict = { 159 | 'images': [{ 160 | 'transaction': {'face_id': 'new_face_id'}, 161 | 'attributes': {} 162 | }] 163 | } 164 | responses.add(responses.POST, 'https://api.kairos.com/enroll', status=200, 165 | body=(json.dumps(response_dict))) 166 | 167 | actual_response = kairos_face.enroll_face('sub_id', 'gallery', url='a_image_url.jpg') 168 | 169 | self.assertEqual('new_face_id', actual_response['images'][0]['transaction']['face_id']) 170 | 171 | @responses.activate 172 | def test_returns_image_attributes_from_kairos_response(self): 173 | response_dict = { 174 | 'images': [{ 175 | 'transaction': {'face_id': 'new_face_id'}, 176 | 'attributes': { 177 | 'gender': {'type': 'F', 'confidence': '80%'} 178 | } 179 | }] 180 | } 181 | responses.add(responses.POST, 'https://api.kairos.com/enroll', status=200, 182 | body=json.dumps(response_dict)) 183 | 184 | actual_response = kairos_face.enroll_face('sub_id', 'gallery', url='a_image_url.jpg') 185 | 186 | expected_attributes = { 187 | 'gender': { 188 | 'type': 'F', 189 | 'confidence': '80%' 190 | } 191 | } 192 | self.assertEqual(expected_attributes, actual_response['images'][0]['attributes']) 193 | --------------------------------------------------------------------------------