Skip to content

Commit

Permalink
Merge pull request #2938 from daspecster/vision-add-safe-search-from-pb
Browse files Browse the repository at this point in the history
Vision: Add gRPC support for safe search.
  • Loading branch information
daspecster committed Jan 23, 2017
2 parents 4223781 + 1086bc4 commit 7d8860f
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 54 deletions.
2 changes: 1 addition & 1 deletion docs/vision-safe-search.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Vision Safe Search
Safe Search Annotation
~~~~~~~~~~~~~~~~~~~~~~

.. automodule:: google.cloud.vision.safe
.. automodule:: google.cloud.vision.safe_search
:members:
:undoc-members:
:show-inheritance:
3 changes: 1 addition & 2 deletions docs/vision-usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,7 @@ categorize the entire contents of the image under four categories.
>>> client = vision.Client()
>>> with open('./image.jpg', 'rb') as image_file:
... image = client.image(content=image_file.read())
>>> safe_search_results = image.detect_safe_search()
>>> safe_search = safe_search_results[0]
>>> safe_search = image.detect_safe_search()
>>> safe_search.adult
<Likelihood.VERY_UNLIKELY: 'VERY_UNLIKELY'>
>>> safe_search.spoof
Expand Down
20 changes: 4 additions & 16 deletions system_tests/vision.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ def tearDown(self):
value.delete()

def _assert_safe_search(self, safe_search):
from google.cloud.vision.safe import SafeSearchAnnotation
from google.cloud.vision.safe_search import SafeSearchAnnotation

self.assertIsInstance(safe_search, SafeSearchAnnotation)
self._assert_likelihood(safe_search.adult)
Expand All @@ -364,19 +364,13 @@ def _assert_safe_search(self, safe_search):
self._assert_likelihood(safe_search.violence)

def test_detect_safe_search_content(self):
self._pb_not_implemented_skip(
'gRPC not implemented for safe search detection.')
client = Config.CLIENT
with open(FACE_FILE, 'rb') as image_file:
image = client.image(content=image_file.read())
safe_searches = image.detect_safe_search()
self.assertEqual(len(safe_searches), 1)
safe_search = safe_searches[0]
safe_search = image.detect_safe_search()
self._assert_safe_search(safe_search)

def test_detect_safe_search_gcs(self):
self._pb_not_implemented_skip(
'gRPC not implemented for safe search detection.')
bucket_name = Config.TEST_BUCKET.name
blob_name = 'faces.jpg'
blob = Config.TEST_BUCKET.blob(blob_name)
Expand All @@ -388,19 +382,13 @@ def test_detect_safe_search_gcs(self):

client = Config.CLIENT
image = client.image(source_uri=source_uri)
safe_searches = image.detect_safe_search()
self.assertEqual(len(safe_searches), 1)
safe_search = safe_searches[0]
safe_search = image.detect_safe_search()
self._assert_safe_search(safe_search)

def test_detect_safe_search_filename(self):
self._pb_not_implemented_skip(
'gRPC not implemented for safe search detection.')
client = Config.CLIENT
image = client.image(filename=FACE_FILE)
safe_searches = image.detect_safe_search()
self.assertEqual(len(safe_searches), 1)
safe_search = safe_searches[0]
safe_search = image.detect_safe_search()
self._assert_safe_search(safe_search)


Expand Down
30 changes: 24 additions & 6 deletions vision/google/cloud/vision/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from google.cloud.vision.color import ImagePropertiesAnnotation
from google.cloud.vision.entity import EntityAnnotation
from google.cloud.vision.face import Face
from google.cloud.vision.safe import SafeSearchAnnotation
from google.cloud.vision.safe_search import SafeSearchAnnotation


_FACE_ANNOTATIONS = 'faceAnnotations'
Expand Down Expand Up @@ -61,7 +61,7 @@ class Annotations(object):
:type safe_searches: list
:param safe_searches:
List of :class:`~google.cloud.vision.safe.SafeSearchAnnotation`
List of :class:`~google.cloud.vision.safe_search.SafeSearchAnnotation`
:type texts: list
:param texts: List of
Expand Down Expand Up @@ -126,6 +126,8 @@ def _process_image_annotations(image):
'logos': _make_entity_from_pb(image.logo_annotations),
'properties': _make_image_properties_from_pb(
image.image_properties_annotation),
'safe_searches': _make_safe_search_from_pb(
image.safe_search_annotation),
'texts': _make_entity_from_pb(image.text_annotations),
}

Expand Down Expand Up @@ -170,15 +172,31 @@ def _make_image_properties_from_pb(image_properties):
return ImagePropertiesAnnotation.from_pb(image_properties)


def _make_safe_search_from_pb(safe_search):
"""Create ``SafeSearchAnnotation`` object from a protobuf response.
:type safe_search: :class:`~google.cloud.grpc.vision.v1.\
image_annotator_pb2.SafeSearchAnnotation`
:param safe_search: Protobuf instance of ``SafeSearchAnnotation``.
:rtype: :class: `~google.cloud.vision.safe_search.SafeSearchAnnotation`
:returns: Instance of ``SafeSearchAnnotation``.
"""
return SafeSearchAnnotation.from_pb(safe_search)


def _entity_from_response_type(feature_type, results):
"""Convert a JSON result to an entity type based on the feature.
:rtype: list
:returns: List containing any of
:class:`~google.cloud.vision.color.ImagePropertiesAnnotation`,
:class:`~google.cloud.vision.entity.EntityAnnotation`,
:class:`~google.cloud.vision.face.Face`,
:class:`~google.cloud.vision.safe.SafeSearchAnnotation`.
:class:`~google.cloud.vision.face.Face`
or one of
:class:`~google.cloud.vision.safe_search.SafeSearchAnnotation`,
:class:`~google.cloud.vision.color.ImagePropertiesAnnotation`.
"""
detected_objects = []
if feature_type == _FACE_ANNOTATIONS:
Expand All @@ -187,7 +205,7 @@ def _entity_from_response_type(feature_type, results):
elif feature_type == _IMAGE_PROPERTIES_ANNOTATION:
return ImagePropertiesAnnotation.from_api_repr(results)
elif feature_type == _SAFE_SEARCH_ANNOTATION:
detected_objects.append(SafeSearchAnnotation.from_api_repr(results))
return SafeSearchAnnotation.from_api_repr(results)
else:
for result in results:
detected_objects.append(EntityAnnotation.from_api_repr(result))
Expand Down
16 changes: 1 addition & 15 deletions vision/google/cloud/vision/face.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,12 @@

from enum import Enum

from google.cloud.grpc.vision.v1 import image_annotator_pb2

from google.cloud.vision.geometry import BoundsBase
from google.cloud.vision.likelihood import _get_pb_likelihood
from google.cloud.vision.likelihood import Likelihood
from google.cloud.vision.geometry import Position


def _get_pb_likelihood(likelihood):
"""Convert protobuf Likelihood integer value to Likelihood instance.
:type likelihood: int
:param likelihood: Protobuf integer representing ``Likelihood``.
:rtype: :class:`~google.cloud.vision.likelihood.Likelihood`
:returns: Instance of ``Likelihood`` converted from protobuf value.
"""
likelihood_pb = image_annotator_pb2.Likelihood.Name(likelihood)
return Likelihood[likelihood_pb]


class Angles(object):
"""Angles representing the positions of a face."""
def __init__(self, roll, pan, tilt):
Expand Down
15 changes: 15 additions & 0 deletions vision/google/cloud/vision/likelihood.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,21 @@

from enum import Enum

from google.cloud.grpc.vision.v1 import image_annotator_pb2


def _get_pb_likelihood(likelihood):
"""Convert protobuf Likelihood integer value to Likelihood enum.
:type likelihood: int
:param likelihood: Protobuf integer representing ``Likelihood``.
:rtype: :class:`~google.cloud.vision.likelihood.Likelihood`
:returns: Enum ``Likelihood`` converted from protobuf value.
"""
likelihood_pb = image_annotator_pb2.Likelihood.Name(likelihood)
return Likelihood[likelihood_pb]


class Likelihood(Enum):
"""A representation of likelihood to give stable results across upgrades.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2016 Google Inc.
# Copyright 2017 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -14,7 +14,7 @@

"""Safe search class for information returned from annotating an image."""


from google.cloud.vision.likelihood import _get_pb_likelihood
from google.cloud.vision.likelihood import Likelihood


Expand Down Expand Up @@ -51,17 +51,32 @@ def from_api_repr(cls, response):
:param response: Dictionary response from Vision API with safe search
data.
:rtype: :class:`~google.cloud.vision.safe.SafeSearchAnnotation`
:rtype: :class:`~google.cloud.vision.safe_search.SafeSearchAnnotation`
:returns: Instance of ``SafeSearchAnnotation``.
"""
adult_likelihood = getattr(Likelihood, response['adult'])
spoof_likelihood = getattr(Likelihood, response['spoof'])
medical_likelihood = getattr(Likelihood, response['medical'])
violence_likelihood = getattr(Likelihood, response['violence'])
adult_likelihood = Likelihood[response['adult']]
spoof_likelihood = Likelihood[response['spoof']]
medical_likelihood = Likelihood[response['medical']]
violence_likelihood = Likelihood[response['violence']]

return cls(adult_likelihood, spoof_likelihood, medical_likelihood,
violence_likelihood)

@classmethod
def from_pb(cls, image):
"""Factory: construct SafeSearchAnnotation from Vision API response.
:type image: :class:`~google.cloud.grpc.vision.v1.image_annotator_pb2.\
SafeSearchAnnotation`
:param image: Protobuf response from Vision API with safe search data.
:rtype: :class:`~google.cloud.vision.safe_search.SafeSearchAnnotation`
:returns: Instance of ``SafeSearchAnnotation``.
"""
values = [image.adult, image.spoof, image.medical, image.violence]
classifications = map(_get_pb_likelihood, values)
return cls(*classifications)

@property
def adult(self):
"""Represents the adult contents likelihood for the image.
Expand Down
11 changes: 10 additions & 1 deletion vision/unit_tests/test_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ def test_ctor(self):
self.assertEqual(annotations.texts, [True])

def test_from_pb(self):
from google.cloud.vision.likelihood import Likelihood
from google.cloud.vision.safe_search import SafeSearchAnnotation
from google.cloud.grpc.vision.v1 import image_annotator_pb2

image_response = image_annotator_pb2.AnnotateImageResponse()
Expand All @@ -76,9 +78,16 @@ def test_from_pb(self):
self.assertEqual(annotations.faces, [])
self.assertEqual(annotations.landmarks, [])
self.assertEqual(annotations.texts, [])
self.assertEqual(annotations.safe_searches, ())
self.assertIsNone(annotations.properties)

self.assertIsInstance(annotations.safe_searches, SafeSearchAnnotation)
safe_search = annotations.safe_searches
unknown = Likelihood.UNKNOWN
self.assertIs(safe_search.adult, unknown)
self.assertIs(safe_search.spoof, unknown)
self.assertIs(safe_search.medical, unknown)
self.assertIs(safe_search.violence, unknown)


class Test__make_entity_from_pb(unittest.TestCase):
def _call_fut(self, annotations):
Expand Down
13 changes: 7 additions & 6 deletions vision/unit_tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ def test_text_detection_from_source(self):

def test_safe_search_detection_from_source(self):
from google.cloud.vision.likelihood import Likelihood
from google.cloud.vision.safe import SafeSearchAnnotation
from google.cloud.vision.safe_search import SafeSearchAnnotation
from unit_tests._fixtures import SAFE_SEARCH_DETECTION_RESPONSE

RETURNED = SAFE_SEARCH_DETECTION_RESPONSE
Expand All @@ -420,15 +420,16 @@ def test_safe_search_detection_from_source(self):
client._connection = _Connection(RETURNED)

image = client.image(source_uri=IMAGE_SOURCE)
safe_search = image.detect_safe_search()[0]
safe_search = image.detect_safe_search()
self.assertIsInstance(safe_search, SafeSearchAnnotation)
image_request = client._connection._requested[0]['data']['requests'][0]
self.assertEqual(IMAGE_SOURCE,
image_request['image']['source']['gcs_image_uri'])
self.assertEqual(safe_search.adult, Likelihood.VERY_UNLIKELY)
self.assertEqual(safe_search.spoof, Likelihood.UNLIKELY)
self.assertEqual(safe_search.medical, Likelihood.POSSIBLE)
self.assertEqual(safe_search.violence, Likelihood.VERY_UNLIKELY)

self.assertIs(safe_search.adult, Likelihood.VERY_UNLIKELY)
self.assertIs(safe_search.spoof, Likelihood.UNLIKELY)
self.assertIs(safe_search.medical, Likelihood.POSSIBLE)
self.assertIs(safe_search.violence, Likelihood.VERY_UNLIKELY)

def test_safe_search_no_results(self):
RETURNED = {
Expand Down
70 changes: 70 additions & 0 deletions vision/unit_tests/test_safe_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Copyright 2016 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import unittest


class TestSafeSearchAnnotation(unittest.TestCase):
@staticmethod
def _get_target_class():
from google.cloud.vision.safe_search import SafeSearchAnnotation
return SafeSearchAnnotation

def test_safe_search_annotation(self):
from google.cloud.vision.likelihood import Likelihood
from unit_tests._fixtures import SAFE_SEARCH_DETECTION_RESPONSE

response = SAFE_SEARCH_DETECTION_RESPONSE['responses'][0]
safe_search_response = response['safeSearchAnnotation']

safe_search = self._get_target_class().from_api_repr(
safe_search_response)

self.assertIs(safe_search.adult, Likelihood.VERY_UNLIKELY)
self.assertIs(safe_search.spoof, Likelihood.UNLIKELY)
self.assertIs(safe_search.medical, Likelihood.POSSIBLE)
self.assertIs(safe_search.violence, Likelihood.VERY_UNLIKELY)

def test_pb_safe_search_annotation(self):
from google.cloud.vision.likelihood import Likelihood
from google.cloud.grpc.vision.v1.image_annotator_pb2 import (
Likelihood as LikelihoodPB)
from google.cloud.grpc.vision.v1 import image_annotator_pb2

possible = LikelihoodPB.Value('POSSIBLE')
possible_name = Likelihood.POSSIBLE
safe_search_annotation = image_annotator_pb2.SafeSearchAnnotation(
adult=possible, spoof=possible, medical=possible, violence=possible
)

safe_search = self._get_target_class().from_pb(safe_search_annotation)

self.assertIs(safe_search.adult, possible_name)
self.assertIs(safe_search.spoof, possible_name)
self.assertIs(safe_search.medical, possible_name)
self.assertIs(safe_search.violence, possible_name)

def test_empty_pb_safe_search_annotation(self):
from google.cloud.vision.likelihood import Likelihood
from google.cloud.grpc.vision.v1 import image_annotator_pb2

unknown = Likelihood.UNKNOWN
safe_search_annotation = image_annotator_pb2.SafeSearchAnnotation()

safe_search = self._get_target_class().from_pb(safe_search_annotation)

self.assertIs(safe_search.adult, unknown)
self.assertIs(safe_search.spoof, unknown)
self.assertIs(safe_search.medical, unknown)
self.assertIs(safe_search.violence, unknown)

0 comments on commit 7d8860f

Please sign in to comment.