From 60c22fcbd0f8bfe50103ba77d6f947034a3cc1bb Mon Sep 17 00:00:00 2001 From: Thomas Schultz Date: Tue, 17 Jan 2017 15:28:06 -0500 Subject: [PATCH] Add GAPIC support for safe search. --- docs/vision-safe-search.rst | 2 +- docs/vision-usage.rst | 3 +- system_tests/vision.py | 20 ++---- vision/google/cloud/vision/annotations.py | 30 ++++++-- vision/google/cloud/vision/face.py | 16 +---- vision/google/cloud/vision/likelihood.py | 15 ++++ .../cloud/vision/{safe.py => safe_search.py} | 29 ++++++-- vision/unit_tests/test_annotations.py | 11 ++- vision/unit_tests/test_client.py | 13 ++-- vision/unit_tests/test_safe_search.py | 70 +++++++++++++++++++ 10 files changed, 155 insertions(+), 54 deletions(-) rename vision/google/cloud/vision/{safe.py => safe_search.py} (76%) create mode 100644 vision/unit_tests/test_safe_search.py diff --git a/docs/vision-safe-search.rst b/docs/vision-safe-search.rst index f86f675c21ba..8f84bc5a9d19 100644 --- a/docs/vision-safe-search.rst +++ b/docs/vision-safe-search.rst @@ -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: diff --git a/docs/vision-usage.rst b/docs/vision-usage.rst index 7e04e9bc8338..c1ca134b8f22 100644 --- a/docs/vision-usage.rst +++ b/docs/vision-usage.rst @@ -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 >>> safe_search.spoof diff --git a/system_tests/vision.py b/system_tests/vision.py index 6f6230bfac8e..43bf28875921 100644 --- a/system_tests/vision.py +++ b/system_tests/vision.py @@ -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) @@ -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) @@ -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) diff --git a/vision/google/cloud/vision/annotations.py b/vision/google/cloud/vision/annotations.py index e85d1a0c3549..a19527a96c11 100644 --- a/vision/google/cloud/vision/annotations.py +++ b/vision/google/cloud/vision/annotations.py @@ -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' @@ -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 @@ -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), } @@ -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: @@ -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)) diff --git a/vision/google/cloud/vision/face.py b/vision/google/cloud/vision/face.py index 0809ba16b7dd..354ff08d59ce 100644 --- a/vision/google/cloud/vision/face.py +++ b/vision/google/cloud/vision/face.py @@ -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): diff --git a/vision/google/cloud/vision/likelihood.py b/vision/google/cloud/vision/likelihood.py index 93c9ddacd81d..fd249e41dff1 100644 --- a/vision/google/cloud/vision/likelihood.py +++ b/vision/google/cloud/vision/likelihood.py @@ -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. diff --git a/vision/google/cloud/vision/safe.py b/vision/google/cloud/vision/safe_search.py similarity index 76% rename from vision/google/cloud/vision/safe.py rename to vision/google/cloud/vision/safe_search.py index 9483da6d1aa1..9b531837db8c 100644 --- a/vision/google/cloud/vision/safe.py +++ b/vision/google/cloud/vision/safe_search.py @@ -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. @@ -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 @@ -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. diff --git a/vision/unit_tests/test_annotations.py b/vision/unit_tests/test_annotations.py index d98ecb376196..68a270e3122d 100644 --- a/vision/unit_tests/test_annotations.py +++ b/vision/unit_tests/test_annotations.py @@ -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() @@ -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): diff --git a/vision/unit_tests/test_client.py b/vision/unit_tests/test_client.py index 0941390a0f61..f3f972c1f6cc 100644 --- a/vision/unit_tests/test_client.py +++ b/vision/unit_tests/test_client.py @@ -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 @@ -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 = { diff --git a/vision/unit_tests/test_safe_search.py b/vision/unit_tests/test_safe_search.py new file mode 100644 index 000000000000..5bc06ac47c52 --- /dev/null +++ b/vision/unit_tests/test_safe_search.py @@ -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)