Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vision: Add gRPC support for safe search. #2938

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()

This comment was marked as spam.

This comment was marked as spam.

>>> 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.

This comment was marked as spam.

#
# 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)