Repository: googleapis/google-cloud-python

Issue:
Image.detect_faces raises KeyError when the image doesn't contain any faces


Expected behavior: An empty list is returned from detect_faces.

Ubuntu 16.04
Python 3.5.2
google-cloud version: 0.21.0

## Code example
~~~python
import google.cloud.vision.client
import base64

test_image_base64 = '''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'''

test_image_bytes = base64.b64decode(test_image_base64)

client = google.cloud.vision.Client()

image = client.image(
    content=test_image_bytes,
)

faces = image.detect_faces()
print(faces)

~~~


## Stack trace
Traceback (most recent call last):
  File "test.py", line 42, in <module>
    faces = image.detect_faces()
  File "/home/tal/virtualenv/lib/python3.5/site-packages/google/cloud/vision/image.py", line 138, in detect_faces
    return self._detect_annotation(features)
  File "/home/tal/virtualenv/lib/python3.5/site-packages/google/cloud/vision/image.py", line 125, in _detect_annotation
    _entity_from_response_type(feature.feature_type, results))
  File "/home/tal/virtualenv/lib/python3.5/site-packages/google/cloud/vision/image.py", line 226, in _entity_from_response_type
    Face.from_api_repr(face) for face in results[feature_key])
KeyError: 'faceAnnotations'



Discussion:

Resolving patch:
diff --git a/vision/google/cloud/vision/image.py b/vision/google/cloud/vision/image.py
--- a/vision/google/cloud/vision/image.py
+++ b/vision/google/cloud/vision/image.py
@@ -217,20 +217,22 @@ def detect_text(self, limit=10):
 
 def _entity_from_response_type(feature_type, results):
     """Convert a JSON result to an entity type based on the feature."""
-
-    detected_objects = []
     feature_key = _REVERSE_TYPES[feature_type]
+    annotations = results.get(feature_key, ())
+    if not annotations:
+        return []
 
+    detected_objects = []
     if feature_type == _FACE_DETECTION:
         detected_objects.extend(
-            Face.from_api_repr(face) for face in results[feature_key])
+            Face.from_api_repr(face) for face in annotations)
     elif feature_type == _IMAGE_PROPERTIES:
         detected_objects.append(
-            ImagePropertiesAnnotation.from_api_repr(results[feature_key]))
+            ImagePropertiesAnnotation.from_api_repr(annotations))
     elif feature_type == _SAFE_SEARCH_DETECTION:
-        result = results[feature_key]
-        detected_objects.append(SafeSearchAnnotation.from_api_repr(result))
+        detected_objects.append(
+            SafeSearchAnnotation.from_api_repr(annotations))
     else:
-        for result in results[feature_key]:
+        for result in annotations:
             detected_objects.append(EntityAnnotation.from_api_repr(result))
     return detected_objects


Test patch:
diff --git a/vision/unit_tests/test_client.py b/vision/unit_tests/test_client.py
--- a/vision/unit_tests/test_client.py
+++ b/vision/unit_tests/test_client.py
@@ -116,6 +116,24 @@ def test_face_detection_from_content(self):
                          image_request['image']['content'])
         self.assertEqual(5, image_request['features'][0]['maxResults'])
 
+    def test_face_detection_from_content_no_results(self):
+        RETURNED = {
+            'responses': [{}]
+        }
+        credentials = _Credentials()
+        client = self._make_one(project=PROJECT, credentials=credentials)
+        client._connection = _Connection(RETURNED)
+
+        image = client.image(content=IMAGE_CONTENT)
+        faces = image.detect_faces(limit=5)
+        self.assertEqual(faces, [])
+        self.assertEqual(len(faces), 0)
+        image_request = client._connection._requested[0]['data']['requests'][0]
+
+        self.assertEqual(B64_IMAGE_CONTENT,
+                         image_request['image']['content'])
+        self.assertEqual(5, image_request['features'][0]['maxResults'])
+
     def test_label_detection_from_source(self):
         from google.cloud.vision.entity import EntityAnnotation
         from unit_tests._fixtures import (
@@ -138,6 +156,19 @@ def test_label_detection_from_source(self):
         self.assertEqual('/m/0k4j', labels[0].mid)
         self.assertEqual('/m/07yv9', labels[1].mid)
 
+    def test_label_detection_no_results(self):
+        RETURNED = {
+            'responses': [{}]
+        }
+        credentials = _Credentials()
+        client = self._make_one(project=PROJECT, credentials=credentials)
+        client._connection = _Connection(RETURNED)
+
+        image = client.image(content=IMAGE_CONTENT)
+        labels = image.detect_labels()
+        self.assertEqual(labels, [])
+        self.assertEqual(len(labels), 0)
+
     def test_landmark_detection_from_source(self):
         from google.cloud.vision.entity import EntityAnnotation
         from unit_tests._fixtures import (
@@ -178,6 +209,19 @@ def test_landmark_detection_from_content(self):
                          image_request['image']['content'])
         self.assertEqual(5, image_request['features'][0]['maxResults'])
 
+    def test_landmark_detection_no_results(self):
+        RETURNED = {
+            'responses': [{}]
+        }
+        credentials = _Credentials()
+        client = self._make_one(project=PROJECT, credentials=credentials)
+        client._connection = _Connection(RETURNED)
+
+        image = client.image(content=IMAGE_CONTENT)
+        landmarks = image.detect_landmarks()
+        self.assertEqual(landmarks, [])
+        self.assertEqual(len(landmarks), 0)
+
     def test_logo_detection_from_source(self):
         from google.cloud.vision.entity import EntityAnnotation
         from unit_tests._fixtures import LOGO_DETECTION_RESPONSE
@@ -254,6 +298,19 @@ def test_safe_search_detection_from_source(self):
         self.assertEqual('POSSIBLE', safe_search.medical)
         self.assertEqual('VERY_UNLIKELY', safe_search.violence)
 
+    def test_safe_search_no_results(self):
+        RETURNED = {
+            'responses': [{}]
+        }
+        credentials = _Credentials()
+        client = self._make_one(project=PROJECT, credentials=credentials)
+        client._connection = _Connection(RETURNED)
+
+        image = client.image(content=IMAGE_CONTENT)
+        safe_search = image.detect_safe_search()
+        self.assertEqual(safe_search, [])
+        self.assertEqual(len(safe_search), 0)
+
     def test_image_properties_detection_from_source(self):
         from google.cloud.vision.color import ImagePropertiesAnnotation
         from unit_tests._fixtures import IMAGE_PROPERTIES_RESPONSE
@@ -277,6 +334,19 @@ def test_image_properties_detection_from_source(self):
         self.assertEqual(65, image_properties.colors[0].color.blue)
         self.assertEqual(0.0, image_properties.colors[0].color.alpha)
 
+    def test_image_properties_no_results(self):
+        RETURNED = {
+            'responses': [{}]
+        }
+        credentials = _Credentials()
+        client = self._make_one(project=PROJECT, credentials=credentials)
+        client._connection = _Connection(RETURNED)
+
+        image = client.image(content=IMAGE_CONTENT)
+        image_properties = image.detect_properties()
+        self.assertEqual(image_properties, [])
+        self.assertEqual(len(image_properties), 0)
+
 
 class TestVisionRequest(unittest.TestCase):
     @staticmethod