Repository: watson-developer-cloud/python-sdk

Issue:
Support Visual Recognition similarity search

Discussion:

Patch:
diff --git a/examples/visual_recognition_v3.py b/examples/visual_recognition_v3.py
index 4e9479f5..51d77a90 100644
--- a/examples/visual_recognition_v3.py
+++ b/examples/visual_recognition_v3.py
@@ -35,3 +35,7 @@ with open(join(dirname(__file__), '../resources/text.png'), 'rb') as image_file:
 
 with open(join(dirname(__file__), '../resources/face.jpg'), 'rb') as image_file:
     print(json.dumps(visual_recognition.detect_faces(images_file=image_file), indent=2))
+
+with open(join(dirname(__file__), '../resources/face.jpg'), 'rb') as image_file:
+    print(json.dumps(visual_recognition.find_similar(collection_id="YOUR_COLLECTION_ID", image_file=image_file), indent=2))
+
diff --git a/watson_developer_cloud/visual_recognition_v3.py b/watson_developer_cloud/visual_recognition_v3.py
index bd434afe..5d07fcc1 100644
--- a/watson_developer_cloud/visual_recognition_v3.py
+++ b/watson_developer_cloud/visual_recognition_v3.py
@@ -15,6 +15,7 @@
 The v3 Visual Recognition service
 (https://www.ibm.com/watson/developercloud/visual-recognition.html)
 """
+import json
 import mimetypes
 
 from .watson_developer_cloud_service import WatsonDeveloperCloudService
@@ -147,3 +148,159 @@ class VisualRecognitionV3(WatsonDeveloperCloudService):
 
         params = {'version': self.version}
         return self._image_call('/v3/recognize_text', images_file, images_url, params)
+
+    def create_collection(self, name):
+        """
+        Create a new collection of images to search. You can create a maximum of 5 collections.
+
+        :param name:   The name of the new collection. The name can be a maximum of 128 UTF8 characters, with no spaces.
+        :return:
+        """
+
+        return self.request(method='POST', url='/v3/collections',
+                            params={'version': self.version},
+                            files={'name': (None, name)},
+                            accept_json=True)
+
+    def get_collection(self, collection_id):
+        """
+        Retrieve collection details
+        :param collection_id: a valid collection id
+        :return:
+        """
+        return self.request(method='POST', url='/v3/collections/{0}'.format(collection_id),
+                            params={'version': self.version},
+                            accept_json=True)
+
+    def list_collections(self):
+        """
+        List all custom collections.
+        :return:
+        """
+        return self.request(method='GET', url='/v3/collections',
+                            params={'version': self.version},
+                            accept_json=True)
+
+    def delete_collection(self, collection_id):
+        """
+        Delete a user created collection
+        :param collection_id: a valid collection id
+        :return:
+        """
+        return self.request(method='DELETE', url='/v3/collections/{0}'.format(collection_id),
+                            params={'version': self.version},
+                            accept_json=True)
+
+    def add_image(self, collection_id, image_file, metadata=None):
+        """
+        Add an image to a collection
+        :param collection_id: a valid collection id
+        :param image_file: a file object of an image
+        :param metdata: metadata describing the image,  must be convertable to JSON
+        :return:
+        """
+        metadata = metadata or {}
+        filename = image_file.name
+        mime_type = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
+        return self.request(method='POST', url='/v3/collections/{0}/images'.format(collection_id),
+                            params={'version': self.version},
+                            files={'image_file': (filename, image_file, mime_type),
+                                   'metadata' : ('metadata.json', json.dumps(metadata), 'application/json')},
+                            accept_json=True)
+
+    def list_images(self, collection_id):
+        """
+        list images in a given collection
+        :param collection_id:  valid collection id
+        :return:
+        """
+        return self.request(method='GET', url='/v3/collections/{0}/images'.format(collection_id),
+                            params={'version': self.version},
+                            accept_json=True)
+
+    def get_image(self, collection_id, image_id):
+        """
+        Get an image from a collection
+        :param collection_id: valid collection id
+        :param image_id: valid image id
+        :return:
+        """
+        return self.request(method='GET', url='/v3/collections/{0}/images/{1}'.format(collection_id, image_id),
+                            params={'version': self.version},
+                            accept_json=True)
+
+    def delete_image(self, collection_id, image_id):
+        """
+        delete the specified image
+        :param collection_id: valid collection id
+        :param image_id: valid image id
+        :return:
+        """
+        return self.request(method='DELETE', url='/v3/collections/{0}/images/{1}'.format(collection_id, image_id),
+                            params={'version': self.version},
+                            accept_json=True)
+
+    def set_image_metadata(self, collection_id, image_id, metadata):
+        """
+        sets/overwrites the image metadata
+        :param collection_id: valid collection id
+        :param image_id: valid image id
+        :param metadata: key/value hash to set for the metadata
+        :return:
+        """
+        return self.request(method='PUT', url='/v3/collections/{0}/images/{1}/metadata'.format(collection_id, image_id),
+                            params={'version': self.version},
+                            files={'metadata': ('metadata.json', json.dumps(metadata), 'application/json')},
+                            accept_json=True)
+
+    def get_image_metadata(self, collection_id, image_id):
+        """
+        Return image metadata
+        :param collection_id: valid collection id
+        :param image_id: valid image id
+        :return:
+        """
+        return self.request(method='GET', url='/v3/collections/{0}/images/{1}/metadata'.format(collection_id, image_id),
+                            params={'version': self.version},
+                            accept_json=True)
+
+    def delete_image_metadata(self, collection_id, image_id):
+        """
+        Delete image metadata
+        :param collection_id: valid collection id
+        :param image_id: valid image id
+        :return:
+        """
+        return self.request(method='DELETE', url='/v3/collections/{0}/images/{1}/metadata'.format(collection_id, image_id),
+                            params={'version': self.version},
+                            accept_json=True)
+
+    def find_similar(self, collection_id, image_file, limit=10):
+        """
+        find similar images
+        :param collection_id: valid collection id
+        :param image_file: image file to use for searching
+        :param limit: number of returned results (default is 10)
+        :return:
+        {
+           "similar_images":[
+             {
+              "image_id":"dresses_1257263",
+              "created":"2016-09-04T21:49:16.908Z",
+              "metadata":{
+                "weight":10,
+                "cut":"a line",
+                "color":"red"
+               },
+               "score":"0.79"
+             }
+            ],
+           "image_file":"red_dress.jpg",
+           "images_processed": 1
+        }
+        """
+        mime_type = mimetypes.guess_type(image_file.name)[0] or 'application/octet-stream'
+        return self.request(method='POST', url='/v3/collections/{0}/find_similar'.format(collection_id),
+                            params={'version': self.version, 'limit': limit},
+                            files={'image_file': (image_file.name, image_file, mime_type)},
+                            accept_json=True)


Test patch:
diff --git a/test/test_integration_visual_recognition.py b/test/test_integration_visual_recognition.py
new file mode 100644
index 00000000..ec951126
--- /dev/null
+++ b/test/test_integration_visual_recognition.py
@@ -0,0 +1,55 @@
+import pytest
+import watson_developer_cloud
+import os
+from os.path import join, dirname
+from unittest import TestCase
+
+pytestmark = pytest.mark.skip('Run These Manually, they are destructive')
+
+class IntegrationTestVisualRecognitionV3(TestCase):
+
+    def setUp(self):
+        self.visual_recognition = watson_developer_cloud.VisualRecognitionV3('2016-05-20', api_key=os.environ.get(
+            'VISUAL_RECOGNITION_API_KEY'))
+        self.collections = self.visual_recognition.list_collections()
+
+        collection_json = self.visual_recognition.create_collection(name="test_integration_collection")
+        self.collection_id = collection_json['collection_id']
+
+    def tearDown(self):
+        results = self.visual_recognition.delete_collection(collection_id=self.collection_id)
+        assert not(results)
+
+    def test_list_collections(self):
+        results = self.visual_recognition.list_collections()
+        assert len(results['collections']) - len(self.collections['collections']) == 1
+
+    def test_add_image_check_metadata_delete_image(self):
+        with open(join(dirname(__file__), '../resources/face.jpg'), 'rb') as image_file:
+            self.visual_recognition.add_image(collection_id=self.collection_id, image_file=image_file, metadata={'name': 'face'})
+
+        images = self.visual_recognition.list_images(self.collection_id)
+        assert len(images['images']) == 1
+
+        image_id = images['images'][0]['image_id']
+        meta = self.visual_recognition.get_image_metadata(collection_id=self.collection_id, image_id=image_id)
+        assert not (meta) == False
+
+        assert meta['name'] == 'face'
+        assert 'neverland' not in meta
+
+        self.visual_recognition.set_image_metadata(collection_id=self.collection_id, image_id=image_id, metadata={'location': 'neverland'})
+        meta = self.visual_recognition.get_image_metadata(collection_id=self.collection_id, image_id=image_id)
+        assert not (meta) == False
+        assert 'name' not in meta
+        assert meta['location'] == 'neverland'
+
+        self.visual_recognition.delete_image(collection_id=self.collection_id, image_id=image_id)
+
+        images = self.visual_recognition.list_images(self.collection_id)
+        assert len(images['images']) == 0
+
+    def test_find_similar(self):
+        with open(join(dirname(__file__), '../resources/face.jpg'), 'rb') as image_file:
+            results = self.visual_recognition.find_similar(collection_id=self.collection_id, image_file=image_file)
+            assert results['images_processed'] == 1
\ No newline at end of file
diff --git a/test/test_visual_recognition_v3.py b/test/test_visual_recognition_v3.py
new file mode 100644
index 00000000..98b91f69
--- /dev/null
+++ b/test/test_visual_recognition_v3.py
@@ -0,0 +1,365 @@
+import responses
+import watson_developer_cloud
+import json
+import os
+
+from unittest import TestCase
+
+base_url = "https://gateway-a.watsonplatform.net/visual-recognition/api/"
+
+
+class TestVisualRecognitionV3(TestCase):
+    @responses.activate
+    def test_get_classifier(self):
+        vr_service = watson_developer_cloud.VisualRecognitionV3('2016-10-20', api_key='bogusapikey')
+
+        gc_url = "{0}{1}".format(base_url, 'v3/classifiers/bogusnumber')
+
+        responses.add(responses.GET,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+        vr_service.get_classifier(classifier_id='bogusnumber')
+
+        assert len(responses.calls) == 1
+
+    @responses.activate
+    def test_delete_classifier(self):
+        vr_service = watson_developer_cloud.VisualRecognitionV3('2016-10-20', api_key='bogusapikey')
+
+        gc_url = "{0}{1}".format(base_url, 'v3/classifiers/bogusnumber')
+
+        responses.add(responses.DELETE,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+        vr_service.delete_classifier(classifier_id='bogusnumber')
+
+        assert len(responses.calls) == 1
+
+    @responses.activate
+    def test_list_classifiers(self):
+        vr_service = watson_developer_cloud.VisualRecognitionV3('2016-10-20', api_key='bogusapikey')
+
+        gc_url = "{0}{1}".format(base_url, 'v3/classifiers')
+
+        responses.add(responses.GET,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+        vr_service.list_classifiers()
+
+        assert len(responses.calls) == 1
+
+    @responses.activate
+    def test_create_classifier(self):
+        vr_service = watson_developer_cloud.VisualRecognitionV3('2016-10-20', api_key='bogusapikey')
+
+        gc_url = "{0}{1}".format(base_url, 'v3/classifiers')
+
+        responses.add(responses.POST,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+
+        vr_service.create_classifier(name="bogus name")
+        assert len(responses.calls) == 1
+
+    @responses.activate
+    def test_update_classifier(self):
+        vr_service = watson_developer_cloud.VisualRecognitionV3('2016-10-20', api_key='bogusapikey')
+
+        gc_url = "{0}{1}".format(base_url, 'v3/classifiers/bogusid')
+
+        responses.add(responses.POST,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+
+        vr_service.update_classifier(classifier_id="bogusid", name="bogus name")
+        assert len(responses.calls) == 1
+
+    @responses.activate
+    def test_classify(self):
+        vr_service = watson_developer_cloud.VisualRecognitionV3('2016-10-20', api_key='bogusapikey')
+
+        gc_url = "{0}{1}".format(base_url, 'v3/classify')
+
+        responses.add(responses.GET,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+        responses.add(responses.POST,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+        thrown = False
+        try:
+            vr_service.classify()
+        except AssertionError as ae:
+            assert str(ae) == 'You must specify either a file or a url'
+            thrown = True
+        assert thrown
+
+        vr_service.classify(images_url="http://google.com")
+
+        vr_service.classify(images_url="http://google.com", classifier_ids=['one', 'two', 'three'])
+        vr_service.classify(images_url="http://google.com", owners=['one', 'two', 'three'])
+
+        with open(os.path.join(os.path.dirname(__file__), '../resources/test.jpg'), 'rb') as image_file:
+            vr_service.classify(images_file=image_file)
+        assert len(responses.calls) == 4
+
+    @responses.activate
+    def test_detect_faces(self):
+        vr_service = watson_developer_cloud.VisualRecognitionV3('2016-10-20', api_key='bogusapikey')
+
+        gc_url = "{0}{1}".format(base_url, 'v3/detect_faces')
+
+        responses.add(responses.GET,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+
+        responses.add(responses.POST,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+
+        thrown = False
+        try:
+            vr_service.detect_faces()
+        except AssertionError as ae:
+            assert str(ae) == 'You must specify either a file or a url'
+            thrown = True
+        assert thrown
+
+        vr_service.detect_faces(images_url="http://google.com")
+        with open(os.path.join(os.path.dirname(__file__), '../resources/test.jpg'), 'rb') as image_file:
+            vr_service.detect_faces(images_file=image_file)
+        assert len(responses.calls) == 2
+
+    @responses.activate
+    def test_recognize_text(self):
+        vr_service = watson_developer_cloud.VisualRecognitionV3('2016-10-20', api_key='bogusapikey')
+
+        gc_url = "{0}{1}".format(base_url, 'v3/recognize_text')
+
+        responses.add(responses.GET,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+        responses.add(responses.POST,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+
+        thrown = False
+        try:
+            vr_service.recognize_text()
+        except AssertionError as ae:
+            assert str(ae) == 'You must specify either a file or a url'
+            thrown = True
+        assert thrown
+
+        vr_service.recognize_text(images_url="http://google.com")
+
+        with open(os.path.join(os.path.dirname(__file__), '../resources/test.jpg'), 'rb') as image_file:
+            vr_service.recognize_text(images_file=image_file)
+
+        assert len(responses.calls) == 2
+
+    @responses.activate
+    def test_create_collection(self):
+
+        vr_service = watson_developer_cloud.VisualRecognitionV3('2016-10-20', api_key='bogusapikey')
+
+        gc_url = "{0}{1}".format(base_url, 'v3/collections')
+
+        responses.add(responses.POST,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+
+        vr_service.create_collection(name="bogus name")
+        assert len(responses.calls) == 1
+
+    @responses.activate
+    def test_get_collection(self):
+
+        vr_service = watson_developer_cloud.VisualRecognitionV3('2016-10-20', api_key='bogusapikey')
+
+        gc_url = "{0}{1}".format(base_url, 'v3/collections/bogusid')
+
+        responses.add(responses.POST,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+
+        vr_service.get_collection(collection_id="bogusid")
+        assert len(responses.calls) == 1
+
+    @responses.activate
+    def test_list_collections(self):
+        vr_service = watson_developer_cloud.VisualRecognitionV3('2016-10-20', api_key='bogusapikey')
+
+        gc_url = "{0}{1}".format(base_url, 'v3/collections')
+
+        responses.add(responses.GET,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+
+        vr_service.list_collections()
+        assert len(responses.calls) == 1
+
+    @responses.activate
+    def test_delete_collection(self):
+        vr_service = watson_developer_cloud.VisualRecognitionV3('2016-10-20', api_key='bogusapikey')
+
+        gc_url = "{0}{1}".format(base_url, 'v3/collections/bogusid')
+
+        responses.add(responses.DELETE,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+
+        vr_service.delete_collection(collection_id='bogusid')
+        assert len(responses.calls) == 1
+
+    @responses.activate
+    def test_add_image(self):
+        vr_service = watson_developer_cloud.VisualRecognitionV3('2016-10-20', api_key='bogusapikey')
+
+        gc_url = "{0}{1}".format(base_url, 'v3/collections/bogusid/images')
+
+        responses.add(responses.POST,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+
+        with open(os.path.join(os.path.dirname(__file__), '../resources/test.jpg'), 'rb') as image_file:
+            vr_service.add_image(collection_id='bogusid', image_file=image_file)
+        assert len(responses.calls) == 1
+
+    @responses.activate
+    def test_list_images(self):
+        vr_service = watson_developer_cloud.VisualRecognitionV3('2016-10-20', api_key='bogusapikey')
+
+        gc_url = "{0}{1}".format(base_url, 'v3/collections/bogusid/images')
+
+        responses.add(responses.GET,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+
+        vr_service.list_images(collection_id='bogusid')
+        assert len(responses.calls) == 1
+
+    @responses.activate
+    def test_get_image(self):
+        vr_service = watson_developer_cloud.VisualRecognitionV3('2016-10-20', api_key='bogusapikey')
+
+        gc_url = "{0}{1}".format(base_url, 'v3/collections/bogusid/images/imageid')
+
+        responses.add(responses.GET,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+
+        vr_service.get_image(collection_id='bogusid', image_id="imageid")
+        assert len(responses.calls) == 1
+
+    @responses.activate
+    def test_delete_image(self):
+        vr_service = watson_developer_cloud.VisualRecognitionV3('2016-10-20', api_key='bogusapikey')
+
+        gc_url = "{0}{1}".format(base_url, 'v3/collections/bogusid/images/imageid')
+
+        responses.add(responses.DELETE,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+
+        vr_service.delete_image(collection_id='bogusid', image_id="imageid")
+        assert len(responses.calls) == 1
+
+    @responses.activate
+    def test_set_image_metadata(self):
+        vr_service = watson_developer_cloud.VisualRecognitionV3('2016-10-20', api_key='bogusapikey')
+
+        gc_url = "{0}{1}".format(base_url, 'v3/collections/bogusid/images/imageid/metadata')
+
+        responses.add(responses.PUT,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+
+        vr_service.set_image_metadata(collection_id="bogusid", image_id="imageid", metadata={'one': 10})
+        assert len(responses.calls) == 1
+
+    @responses.activate
+    def test_get_image_metadata(self):
+        vr_service = watson_developer_cloud.VisualRecognitionV3('2016-10-20', api_key='bogusapikey')
+
+        gc_url = "{0}{1}".format(base_url, 'v3/collections/bogusid/images/imageid/metadata')
+
+        responses.add(responses.GET,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+
+        vr_service.get_image_metadata(collection_id="bogusid", image_id="imageid")
+        assert len(responses.calls) == 1
+
+    @responses.activate
+    def test_delete_image_metadata(self):
+        vr_service = watson_developer_cloud.VisualRecognitionV3('2016-10-20', api_key='bogusapikey')
+
+        gc_url = "{0}{1}".format(base_url, 'v3/collections/bogusid/images/imageid/metadata')
+
+        responses.add(responses.DELETE,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+
+        vr_service.delete_image_metadata(collection_id="bogusid", image_id="imageid")
+        assert len(responses.calls) == 1
+
+    @responses.activate
+    def test_find_similar(self):
+        vr_service = watson_developer_cloud.VisualRecognitionV3('2016-10-20', api_key='bogusapikey')
+
+        gc_url = "{0}{1}".format(base_url, 'v3/collections/bogusid/find_similar')
+
+        responses.add(responses.POST,
+                      gc_url,
+                      body=json.dumps({'response': 200}),
+                      status=200,
+                      content_type='application/json')
+
+        with open(os.path.join(os.path.dirname(__file__), '../resources/test.jpg'), 'rb') as image_file:
+            vr_service.find_similar(collection_id='bogusid', image_file=image_file)
+        assert len(responses.calls) == 1