diff --git a/docs/vision-usage.rst b/docs/vision-usage.rst index d7a6605dbf62..7e04e9bc8338 100644 --- a/docs/vision-usage.rst +++ b/docs/vision-usage.rst @@ -271,13 +271,13 @@ image and determine the dominant colors in the image. >>> client = vision.Client() >>> with open('./image.jpg', 'rb') as image_file: ... image = client.image(content=image_file.read()) - >>> results = image.detect_properties() - >>> colors = results[0].colors + >>> properties = image.detect_properties() + >>> colors = properties.colors >>> first_color = colors[0] >>> first_color.red - 244 + 244.0 >>> first_color.blue - 134 + 134.0 >>> first_color.score 0.65519291 >>> first_color.pixel_fraction diff --git a/system_tests/vision.py b/system_tests/vision.py index 3d0c029370f5..6f6230bfac8e 100644 --- a/system_tests/vision.py +++ b/system_tests/vision.py @@ -474,13 +474,13 @@ def tearDown(self): value.delete() def _assert_color(self, color): - self.assertIsInstance(color.red, int) - self.assertIsInstance(color.green, int) - self.assertIsInstance(color.blue, int) + self.assertIsInstance(color.red, float) + self.assertIsInstance(color.green, float) + self.assertIsInstance(color.blue, float) + self.assertIsInstance(color.alpha, float) self.assertNotEqual(color.red, 0.0) self.assertNotEqual(color.green, 0.0) self.assertNotEqual(color.blue, 0.0) - self.assertIsInstance(color.alpha, float) def _assert_properties(self, image_property): from google.cloud.vision.color import ImagePropertiesAnnotation @@ -493,19 +493,13 @@ def _assert_properties(self, image_property): self.assertNotEqual(color_info.score, 0.0) def test_detect_properties_content(self): - self._pb_not_implemented_skip( - 'gRPC not implemented for image properties detection.') client = Config.CLIENT with open(FACE_FILE, 'rb') as image_file: image = client.image(content=image_file.read()) properties = image.detect_properties() - self.assertEqual(len(properties), 1) - image_property = properties[0] - self._assert_properties(image_property) + self._assert_properties(properties) def test_detect_properties_gcs(self): - self._pb_not_implemented_skip( - 'gRPC not implemented for image properties detection.') client = Config.CLIENT bucket_name = Config.TEST_BUCKET.name blob_name = 'faces.jpg' @@ -518,16 +512,10 @@ def test_detect_properties_gcs(self): image = client.image(source_uri=source_uri) properties = image.detect_properties() - self.assertEqual(len(properties), 1) - image_property = properties[0] - self._assert_properties(image_property) + self._assert_properties(properties) def test_detect_properties_filename(self): - self._pb_not_implemented_skip( - 'gRPC not implemented for image properties detection.') client = Config.CLIENT image = client.image(filename=FACE_FILE) properties = image.detect_properties() - self.assertEqual(len(properties), 1) - image_property = properties[0] - self._assert_properties(image_property) + self._assert_properties(properties) diff --git a/vision/google/cloud/vision/annotations.py b/vision/google/cloud/vision/annotations.py index bb26d413751a..e85d1a0c3549 100644 --- a/vision/google/cloud/vision/annotations.py +++ b/vision/google/cloud/vision/annotations.py @@ -14,6 +14,7 @@ """Annotations management for Vision API responses.""" +import six from google.cloud.vision.color import ImagePropertiesAnnotation from google.cloud.vision.entity import EntityAnnotation @@ -86,11 +87,11 @@ def from_api_repr(cls, response): :rtype: :class:`~google.cloud.vision.annotations.Annotations` :returns: An instance of ``Annotations`` with detection types loaded. """ - annotations = {} - for feature_type, annotation in response.items(): - curr_feature = annotations.setdefault(_KEY_MAP[feature_type], []) - curr_feature.extend( - _entity_from_response_type(feature_type, annotation)) + annotations = { + _KEY_MAP[feature_type]: _entity_from_response_type( + feature_type, annotation) + for feature_type, annotation in six.iteritems(response) + } return cls(**annotations) @classmethod @@ -123,12 +124,14 @@ def _process_image_annotations(image): 'labels': _make_entity_from_pb(image.label_annotations), 'landmarks': _make_entity_from_pb(image.landmark_annotations), 'logos': _make_entity_from_pb(image.logo_annotations), + 'properties': _make_image_properties_from_pb( + image.image_properties_annotation), 'texts': _make_entity_from_pb(image.text_annotations), } def _make_entity_from_pb(annotations): - """Create an entity from a gRPC response. + """Create an entity from a protobuf response. :type annotations: :class:`~google.cloud.grpc.vision.v1.image_annotator_pb2.EntityAnnotation` @@ -141,7 +144,7 @@ def _make_entity_from_pb(annotations): def _make_faces_from_pb(faces): - """Create face objects from a gRPC response. + """Create face objects from a protobuf response. :type faces: :class:`~google.cloud.grpc.vision.v1.image_annotator_pb2.FaceAnnotation` @@ -153,6 +156,20 @@ def _make_faces_from_pb(faces): return [Face.from_pb(face) for face in faces] +def _make_image_properties_from_pb(image_properties): + """Create ``ImageProperties`` object from a protobuf response. + + :type image_properties: :class:`~google.cloud.grpc.vision.v1.\ + image_annotator_pb2.ImagePropertiesAnnotation` + :param image_properties: Protobuf instance of + ``ImagePropertiesAnnotation``. + + :rtype: list or ``None`` + :returns: List of ``ImageProperties`` or ``None``. + """ + return ImagePropertiesAnnotation.from_pb(image_properties) + + def _entity_from_response_type(feature_type, results): """Convert a JSON result to an entity type based on the feature. @@ -168,8 +185,7 @@ def _entity_from_response_type(feature_type, results): detected_objects.extend( Face.from_api_repr(face) for face in results) elif feature_type == _IMAGE_PROPERTIES_ANNOTATION: - detected_objects.append( - ImagePropertiesAnnotation.from_api_repr(results)) + return ImagePropertiesAnnotation.from_api_repr(results) elif feature_type == _SAFE_SEARCH_ANNOTATION: detected_objects.append(SafeSearchAnnotation.from_api_repr(results)) else: diff --git a/vision/google/cloud/vision/color.py b/vision/google/cloud/vision/color.py index 7c6aa4a74e4e..634842567525 100644 --- a/vision/google/cloud/vision/color.py +++ b/vision/google/cloud/vision/color.py @@ -26,20 +26,37 @@ def __init__(self, colors): self._colors = colors @classmethod - def from_api_repr(cls, response): + def from_api_repr(cls, image_properties): """Factory: construct ``ImagePropertiesAnnotation`` from a response. - :type response: dict - :param response: Dictionary response from Vision API with image - properties data. + :type image_properties: dict + :param image_properties: Dictionary response from Vision API with image + properties data. + + :rtype: list of + :class:`~google.cloud.vision.color.ImagePropertiesAnnotation`. + :returns: List of ``ImagePropertiesAnnotation``. + """ + colors = image_properties.get('dominantColors', {}).get('colors', ()) + return cls([ColorInformation.from_api_repr(color) + for color in colors]) + + @classmethod + def from_pb(cls, image_properties): + """Factory: construct ``ImagePropertiesAnnotation`` from a response. + + :type image_properties: :class:`~google.cloud.grpc.vision.v1.\ + image_annotator_pb2.ImageProperties` + :param image_properties: Protobuf response from Vision API with image + properties data. - :rtype: :class:`~google.cloud.vision.color.ImagePropertiesAnnotation`. - :returns: Populated instance of ``ImagePropertiesAnnotation``. + :rtype: list of + :class:`~google.cloud.vision.color.ImagePropertiesAnnotation` + :returns: List of ``ImagePropertiesAnnotation``. """ - raw_colors = response.get('dominantColors', {}).get('colors', ()) - colors = [ColorInformation.from_api_repr(color) - for color in raw_colors] - return cls(colors) + colors = getattr(image_properties.dominant_colors, 'colors', ()) + if len(colors) > 0: + return cls([ColorInformation.from_pb(color) for color in colors]) @property def colors(self): @@ -54,17 +71,17 @@ def colors(self): class Color(object): """Representation of RGBA color information. - :type red: int + :type red: float :param red: The amount of red in the color as a value in the interval - [0, 255]. + [0.0, 255.0]. - :type green: int + :type green: float :param green: The amount of green in the color as a value in the interval - [0, 255]. + [0.0, 255.0]. - :type blue: int + :type blue: float :param blue: The amount of blue in the color as a value in the interval - [0, 255]. + [0.0, 255.0]. :type alpha: float :param alpha: The fraction of this color that should be applied to the @@ -86,13 +103,25 @@ def from_api_repr(cls, response): :rtype: :class:`~google.cloud.vision.color.Color` :returns: Instance of :class:`~google.cloud.vision.color.Color`. """ - red = response.get('red', 0) - green = response.get('green', 0) - blue = response.get('blue', 0) + red = float(response.get('red', 0.0)) + green = float(response.get('green', 0.0)) + blue = float(response.get('blue', 0.0)) alpha = response.get('alpha', 0.0) return cls(red, green, blue, alpha) + @classmethod + def from_pb(cls, color): + """Factory: construct a ``Color`` from a protobuf response. + + :type color: :module: `google.type.color_pb2` + :param color: ``Color`` from API Response. + + :rtype: :class:`~google.cloud.vision.color.Color` + :returns: Instance of :class:`~google.cloud.vision.color.Color`. + """ + return cls(color.red, color.green, color.blue, color.alpha.value) + @property def red(self): """Red component of the color. @@ -149,19 +178,34 @@ def __init__(self, color, score, pixel_fraction): self._pixel_fraction = pixel_fraction @classmethod - def from_api_repr(cls, response): - """Factory: construct ``ColorInformation`` for a color found. + def from_api_repr(cls, color_information): + """Factory: construct ``ColorInformation`` for a color. - :type response: dict - :param response: Color data with extra meta information. + :type color_information: dict + :param color_information: Color data with extra meta information. :rtype: :class:`~google.cloud.vision.color.ColorInformation` :returns: Instance of ``ColorInformation``. """ - color = Color.from_api_repr(response.get('color')) - score = response.get('score') - pixel_fraction = response.get('pixelFraction') + color = Color.from_api_repr(color_information.get('color', {})) + score = color_information.get('score') + pixel_fraction = color_information.get('pixelFraction') + return cls(color, score, pixel_fraction) + @classmethod + def from_pb(cls, color_information): + """Factory: construct ``ColorInformation`` for a color. + + :type color_information: :class:`~google.cloud.grpc.vision.v1.\ + image_annotator_pb2.ColorInfo` + :param color_information: Color data with extra meta information. + + :rtype: :class:`~google.cloud.vision.color.ColorInformation` + :returns: Instance of ``ColorInformation``. + """ + color = Color.from_pb(color_information.color) + score = color_information.score + pixel_fraction = color_information.pixel_fraction return cls(color, score, pixel_fraction) @property diff --git a/vision/unit_tests/test_annotations.py b/vision/unit_tests/test_annotations.py index 4ea57988174f..d98ecb376196 100644 --- a/vision/unit_tests/test_annotations.py +++ b/vision/unit_tests/test_annotations.py @@ -77,7 +77,7 @@ def test_from_pb(self): self.assertEqual(annotations.landmarks, []) self.assertEqual(annotations.texts, []) self.assertEqual(annotations.safe_searches, ()) - self.assertEqual(annotations.properties, ()) + self.assertIsNone(annotations.properties) class Test__make_entity_from_pb(unittest.TestCase): @@ -122,6 +122,37 @@ def test_it(self): self.assertIsInstance(faces[0], Face) +class Test__make_image_properties_from_pb(unittest.TestCase): + def _call_fut(self, annotations): + from google.cloud.vision.annotations import ( + _make_image_properties_from_pb) + return _make_image_properties_from_pb(annotations) + + def test_it(self): + from google.cloud.grpc.vision.v1 import image_annotator_pb2 + from google.protobuf.wrappers_pb2 import FloatValue + from google.type.color_pb2 import Color + + alpha = FloatValue(value=1.0) + color_pb = Color(red=1.0, green=2.0, blue=3.0, alpha=alpha) + color_info_pb = image_annotator_pb2.ColorInfo(color=color_pb, + score=1.0, + pixel_fraction=1.0) + dominant_colors = image_annotator_pb2.DominantColorsAnnotation( + colors=[color_info_pb]) + + image_properties_pb = image_annotator_pb2.ImageProperties( + dominant_colors=dominant_colors) + + image_properties = self._call_fut(image_properties_pb) + self.assertEqual(image_properties.colors[0].pixel_fraction, 1.0) + self.assertEqual(image_properties.colors[0].score, 1.0) + self.assertEqual(image_properties.colors[0].color.red, 1.0) + self.assertEqual(image_properties.colors[0].color.green, 2.0) + self.assertEqual(image_properties.colors[0].color.blue, 3.0) + self.assertEqual(image_properties.colors[0].color.alpha, 1.0) + + class Test__process_image_annotations(unittest.TestCase): def _call_fut(self, image): from google.cloud.vision.annotations import _process_image_annotations diff --git a/vision/unit_tests/test_client.py b/vision/unit_tests/test_client.py index 6191d19571a0..0941390a0f61 100644 --- a/vision/unit_tests/test_client.py +++ b/vision/unit_tests/test_client.py @@ -455,7 +455,7 @@ def test_image_properties_detection_from_source(self): client._connection = _Connection(RETURNED) image = client.image(source_uri=IMAGE_SOURCE) - image_properties = image.detect_properties()[0] + image_properties = image.detect_properties() self.assertIsInstance(image_properties, ImagePropertiesAnnotation) image_request = client._connection._requested[0]['data']['requests'][0] self.assertEqual(IMAGE_SOURCE, diff --git a/vision/unit_tests/test_color.py b/vision/unit_tests/test_color.py index eec7ceefb778..9a9055dac7d2 100644 --- a/vision/unit_tests/test_color.py +++ b/vision/unit_tests/test_color.py @@ -31,11 +31,52 @@ def test_rgb_color_data(self): color_class = self._get_target_class() colors = color_class.from_api_repr(colors) - self.assertEqual(colors.red, 255) - self.assertEqual(colors.green, 255) - self.assertEqual(colors.blue, 255) + self.assertIsInstance(colors.red, float) + self.assertIsInstance(colors.green, float) + self.assertIsInstance(colors.blue, float) + self.assertIsInstance(colors.alpha, float) + self.assertEqual(colors.red, 255.0) + self.assertEqual(colors.green, 255.0) + self.assertEqual(colors.blue, 255.0) self.assertEqual(colors.alpha, 0.5) + def test_empty_pb_rgb_color_data(self): + from google.type.color_pb2 import Color + + color_pb = Color() + color_class = self._get_target_class() + color = color_class.from_pb(color_pb) + self.assertEqual(color.red, 0.0) + self.assertEqual(color.green, 0.0) + self.assertEqual(color.blue, 0.0) + self.assertEqual(color.alpha, 0.0) + + def test_pb_rgb_color_data(self): + from google.protobuf.wrappers_pb2 import FloatValue + from google.type.color_pb2 import Color + + alpha = FloatValue(value=1.0) + color_pb = Color(red=1.0, green=2.0, blue=3.0, alpha=alpha) + color_class = self._get_target_class() + color = color_class.from_pb(color_pb) + self.assertEqual(color.red, 1.0) + self.assertEqual(color.green, 2.0) + self.assertEqual(color.blue, 3.0) + self.assertEqual(color.alpha, 1.0) + + def test_pb_rgb_color_no_alpha_data(self): + from google.protobuf.wrappers_pb2 import FloatValue + from google.type.color_pb2 import Color + + alpha = FloatValue() + color_pb = Color(red=1.0, green=2.0, blue=3.0, alpha=alpha) + color_class = self._get_target_class() + color = color_class.from_pb(color_pb) + self.assertEqual(color.red, 1.0) + self.assertEqual(color.green, 2.0) + self.assertEqual(color.blue, 3.0) + self.assertEqual(color.alpha, 0.0) + def test_missing_rgb_values(self): colors = {} color_class = self._get_target_class() @@ -45,3 +86,45 @@ def test_missing_rgb_values(self): self.assertEqual(colors.green, 0) self.assertEqual(colors.blue, 0) self.assertEqual(colors.alpha, 0.0) + + +class TestImagePropertiesAnnotation(unittest.TestCase): + @staticmethod + def _get_target_class(): + from google.cloud.vision.color import ImagePropertiesAnnotation + return ImagePropertiesAnnotation + + def test_image_properties_annotation_from_pb(self): + from google.cloud.grpc.vision.v1 import image_annotator_pb2 + from google.protobuf.wrappers_pb2 import FloatValue + from google.type.color_pb2 import Color + + alpha = FloatValue(value=1.0) + color_pb = Color(red=1.0, green=2.0, blue=3.0, alpha=alpha) + color_info_pb = image_annotator_pb2.ColorInfo(color=color_pb, + score=1.0, + pixel_fraction=1.0) + dominant_colors = image_annotator_pb2.DominantColorsAnnotation( + colors=[color_info_pb]) + + image_properties_pb = image_annotator_pb2.ImageProperties( + dominant_colors=dominant_colors) + + color_info = self._get_target_class() + image_properties = color_info.from_pb(image_properties_pb) + + self.assertEqual(image_properties.colors[0].pixel_fraction, 1.0) + self.assertEqual(image_properties.colors[0].score, 1.0) + self.assertEqual(image_properties.colors[0].color.red, 1.0) + self.assertEqual(image_properties.colors[0].color.green, 2.0) + self.assertEqual(image_properties.colors[0].color.blue, 3.0) + self.assertEqual(image_properties.colors[0].color.alpha, 1.0) + + def test_empty_image_properties_annotation_from_pb(self): + from google.cloud.grpc.vision.v1 import image_annotator_pb2 + + image_properties_pb = image_annotator_pb2.ImageProperties() + + color_info = self._get_target_class() + image_properties = color_info.from_pb(image_properties_pb) + self.assertIsNone(image_properties)