Skip to content

Vision: Add gRPC support for image properties. #2937

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

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
8 changes: 4 additions & 4 deletions docs/vision-usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 7 additions & 19 deletions system_tests/vision.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'
Expand All @@ -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)
34 changes: 25 additions & 9 deletions vision/google/cloud/vision/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`
Expand All @@ -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`
Expand All @@ -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.

Expand All @@ -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:
Expand Down
96 changes: 70 additions & 26 deletions vision/google/cloud/vision/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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')

This comment was marked as spam.

This comment was marked as spam.

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
Expand Down
33 changes: 32 additions & 1 deletion vision/unit_tests/test_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion vision/unit_tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading