Skip to content

Commit

Permalink
Vision 1.1 API Client (#3069)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukesneeringer authored Feb 25, 2017
1 parent 74b8622 commit 83a0b98
Show file tree
Hide file tree
Showing 18 changed files with 1,463 additions and 30 deletions.
45 changes: 31 additions & 14 deletions packages/google-cloud-vision/google/cloud/vision/_gax.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def __init__(self, client=None):
credentials=client._credentials, lib_name='gccl',
lib_version=__version__)

def annotate(self, images):
def annotate(self, images=None, requests_pb=None):
"""Annotate images through GAX.
:type images: list
Expand All @@ -42,18 +42,28 @@ def annotate(self, images):
:class:`~google.cloud.vision.feature.Feature`.
e.g. [(image, [feature_one, feature_two]),]
:type requests_pb: list
:param requests_pb: List of :class:`google.cloud.proto.vision.v1.\
image_annotator_pb2.AnnotateImageRequest`
:rtype: list
:returns: List of
:class:`~google.cloud.vision.annotations.Annotations`.
"""
requests = []
for image, features in images:
gapic_features = [_to_gapic_feature(feature)
for feature in features]
gapic_image = _to_gapic_image(image)
request = image_annotator_pb2.AnnotateImageRequest(
image=gapic_image, features=gapic_features)
requests.append(request)
if any([images, requests_pb]) is False:
return []

if requests_pb is None:
requests = []
for image, features in images:
gapic_features = [_to_gapic_feature(feature)
for feature in features]
gapic_image = _to_gapic_image(image)
request = image_annotator_pb2.AnnotateImageRequest(
image=gapic_image, features=gapic_features)
requests.append(request)
else:
requests = requests_pb

annotator_client = self._annotator_client
responses = annotator_client.batch_annotate_images(requests).responses
Expand Down Expand Up @@ -89,9 +99,16 @@ def _to_gapic_image(image):
if image.content is not None:
return image_annotator_pb2.Image(content=image.content)
if image.source is not None:
return image_annotator_pb2.Image(
source=image_annotator_pb2.ImageSource(
gcs_image_uri=image.source
),
)
if image.source.startswith('gs://'):
return image_annotator_pb2.Image(
source=image_annotator_pb2.ImageSource(
gcs_image_uri=image.source
),
)
elif image.source.startswith(('http://', 'https://')):
return image_annotator_pb2.Image(
source=image_annotator_pb2.ImageSource(
image_uri=image.source
),
)
raise ValueError('No image content or source found.')
25 changes: 22 additions & 3 deletions packages/google-cloud-vision/google/cloud/vision/_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@

"""HTTP Client for interacting with the Google Cloud Vision API."""

import json

from google.cloud import _http

from google.cloud.vision import __version__
from google.cloud.vision.annotations import Annotations
from google.cloud.vision.feature import Feature

from google.protobuf import json_format


_CLIENT_INFO = _http.CLIENT_INFO_TEMPLATE.format(__version__)

Expand Down Expand Up @@ -57,19 +60,35 @@ def __init__(self, client):
self._client = client
self._connection = Connection(client)

def annotate(self, images):
def annotate(self, images=None, requests_pb=None):
"""Annotate an image to discover it's attributes.
:type images: list of :class:`~google.cloud.vision.image.Image`
:param images: A list of ``Image``.
:rtype: list
:returns: List of :class:`~googe.cloud.vision.annotations.Annotations`.
:type requests_pb: list
:param requests_pb: List of :class:`google.cloud.proto.vision.v1.\
image_annotator_b2.AnnotateImageRequest`.
:rtype: list
:returns: List of :class:`~googe.cloud.vision.annotations.Annotations`.
"""
if any([images, requests_pb]) is False:
return []

requests = []
for image, features in images:
requests.append(_make_request(image, features))
if requests_pb is None:
for image, features in images:
requests.append(_make_request(image, features))
else:
requests = [json.loads(json_format.MessageToJson(request))
for request in requests_pb]

data = {'requests': requests}

api_response = self._connection.api_request(
method='POST', path='/images:annotate', data=data)
responses = api_response.get('responses')
Expand Down
82 changes: 79 additions & 3 deletions packages/google-cloud-vision/google/cloud/vision/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,55 @@
# See the License for the specific language governing permissions and
# limitations under the License.

# pylint: disable=too-many-arguments
"""Annotations management for Vision API responses."""

import six

from google.cloud.vision.color import ImagePropertiesAnnotation
from google.cloud.vision.crop_hint import CropHint
from google.cloud.vision.entity import EntityAnnotation
from google.cloud.vision.face import Face
from google.cloud.vision.safe_search import SafeSearchAnnotation
from google.cloud.vision.text import TextAnnotation
from google.cloud.vision.web import WebDetection


_CROP_HINTS_ANNOTATION = 'cropHintsAnnotation'
_FACE_ANNOTATIONS = 'faceAnnotations'
_FULL_TEXT_ANNOTATION = 'fullTextAnnotation'
_IMAGE_PROPERTIES_ANNOTATION = 'imagePropertiesAnnotation'
_SAFE_SEARCH_ANNOTATION = 'safeSearchAnnotation'
_WEB_ANNOTATION = 'webDetection'

_KEY_MAP = {
_CROP_HINTS_ANNOTATION: 'crop_hints',
_FACE_ANNOTATIONS: 'faces',
_FULL_TEXT_ANNOTATION: 'full_texts',
_IMAGE_PROPERTIES_ANNOTATION: 'properties',
'labelAnnotations': 'labels',
'landmarkAnnotations': 'landmarks',
'logoAnnotations': 'logos',
_SAFE_SEARCH_ANNOTATION: 'safe_searches',
'textAnnotations': 'texts'
'textAnnotations': 'texts',
_WEB_ANNOTATION: 'web',
}


class Annotations(object):
"""Helper class to bundle annotation responses.
:type crop_hints: list
:param crop_hints: List of
:class:`~google.cloud.vision.crop_hint.CropHintsAnnotation`.
:type faces: list
:param faces: List of :class:`~google.cloud.vision.face.Face`.
:type full_texts: list
:param full_texts: List of
:class:`~google.cloud.vision.text.TextAnnotation`.
:type properties: list
:param properties:
List of :class:`~google.cloud.vision.color.ImagePropertiesAnnotation`.
Expand All @@ -66,16 +84,23 @@ class Annotations(object):
:type texts: list
:param texts: List of
:class:`~google.cloud.vision.entity.EntityAnnotation`.
:type web: list
:param web: List of :class:`~google.cloud.vision.web.WebDetection`.
"""
def __init__(self, faces=(), properties=(), labels=(), landmarks=(),
logos=(), safe_searches=(), texts=()):
def __init__(self, crop_hints=(), faces=(), full_texts=(), properties=(),
labels=(), landmarks=(), logos=(), safe_searches=(),
texts=(), web=()):
self.crop_hints = crop_hints
self.faces = faces
self.full_texts = full_texts
self.properties = properties
self.labels = labels
self.landmarks = landmarks
self.logos = logos
self.safe_searches = safe_searches
self.texts = texts
self.web = web

@classmethod
def from_api_repr(cls, response):
Expand Down Expand Up @@ -121,7 +146,9 @@ def _process_image_annotations(image):
:returns: Dictionary populated with entities from response.
"""
return {
'crop_hints': _make_crop_hints_from_pb(image.crop_hints_annotation),
'faces': _make_faces_from_pb(image.face_annotations),
'full_texts': _make_full_text_from_pb(image.full_text_annotation),
'labels': _make_entity_from_pb(image.label_annotations),
'landmarks': _make_entity_from_pb(image.landmark_annotations),
'logos': _make_entity_from_pb(image.logo_annotations),
Expand All @@ -130,9 +157,24 @@ def _process_image_annotations(image):
'safe_searches': _make_safe_search_from_pb(
image.safe_search_annotation),
'texts': _make_entity_from_pb(image.text_annotations),
'web': _make_web_detection_from_pb(image.web_detection)
}


def _make_crop_hints_from_pb(crop_hints):
"""Create list of ``CropHint`` objects from a protobuf response.
:type crop_hints: list
:param crop_hints: List of
:class:`google.cloud.grpc.vision.v1.\
image_annotator_pb2.CropHintsAnnotation`
:rtype: list
:returns: List of ``CropHint`` objects.
"""
return [CropHint.from_pb(hint) for hint in crop_hints.crop_hints]


def _make_entity_from_pb(annotations):
"""Create an entity from a protobuf response.
Expand All @@ -159,6 +201,19 @@ def _make_faces_from_pb(faces):
return [Face.from_pb(face) for face in faces]


def _make_full_text_from_pb(full_text):
"""Create text annotation object from protobuf response.
:type full_text: :class:`~google.cloud.proto.vision.v1.\
text_annotation_pb2.TextAnnotation`
:param full_text: Protobuf instance of ``TextAnnotation``.
:rtype: :class:`~google.cloud.vision.text.TextAnnotation`
:returns: Instance of ``TextAnnotation``.
"""
return TextAnnotation.from_pb(full_text)


def _make_image_properties_from_pb(image_properties):
"""Create ``ImageProperties`` object from a protobuf response.
Expand Down Expand Up @@ -186,6 +241,19 @@ def _make_safe_search_from_pb(safe_search):
return SafeSearchAnnotation.from_pb(safe_search)


def _make_web_detection_from_pb(annotation):
"""Create ``WebDetection`` object from a protobuf response.
:type annotation: :class:`~google.cloud.proto.vision.v1.web_detection_pb2\
.WebDetection`
:param annotation: Protobuf instance of ``WebDetection``.
:rtype: :class: `~google.cloud.vision.web.WebDetection`
:returns: Instance of ``WebDetection``.
"""
return WebDetection.from_pb(annotation)


def _entity_from_response_type(feature_type, results):
"""Convert a JSON result to an entity type based on the feature.
Expand All @@ -207,6 +275,14 @@ def _entity_from_response_type(feature_type, results):
return ImagePropertiesAnnotation.from_api_repr(results)
elif feature_type == _SAFE_SEARCH_ANNOTATION:
return SafeSearchAnnotation.from_api_repr(results)
elif feature_type == _WEB_ANNOTATION:
return WebDetection.from_api_repr(results)
elif feature_type == _CROP_HINTS_ANNOTATION:
crop_hints = results.get('cropHints', [])
detected_objects.extend(
CropHint.from_api_repr(result) for result in crop_hints)
elif feature_type == _FULL_TEXT_ANNOTATION:
return TextAnnotation.from_api_repr(results)
else:
for result in results:
detected_objects.append(EntityAnnotation.from_api_repr(result))
Expand Down
2 changes: 1 addition & 1 deletion packages/google-cloud-vision/google/cloud/vision/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def image(self, content=None, filename=None, source_uri=None):
:param filename: Filename to image.
:type source_uri: str
:param source_uri: Google Cloud Storage URI of image.
:param source_uri: URL or Google Cloud Storage URI of image.
:rtype: :class:`~google.cloud.vision.image.Image`
:returns: Image instance with the current client attached.
Expand Down
92 changes: 92 additions & 0 deletions packages/google-cloud-vision/google/cloud/vision/crop_hint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# 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.
# 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.

"""Representation of Vision API's crop hints."""

from google.cloud.vision.geometry import Bounds


class CropHint(object):
"""Representation of a crop hint returned from the Vision API.
:type bounds: dict
:param bounds: Dictionary of boundary information of detected entity.
:type confidence: float
:param confidence: Confidence of this being a salient region.
:type importance_fraction: float
:param importance_fraction: Fraction of importance of this region.
"""
def __init__(self, bounds, confidence, importance_fraction):
self._bounds = bounds
self._confidence = confidence
self._importance_fraction = importance_fraction

@classmethod
def from_api_repr(cls, response):
"""Factory: construct ``CropHint`` from Vision API response.
:type response: dict
:param response: Dictionary response from Vision API with entity data.
:rtype: :class:`~google.cloud.vision.crop_hint.CropHint`
:returns: Instance of ``CropHint``.
"""
bounds = Bounds.from_api_repr(response.get('boundingPoly'))
confidence = response.get('confidence', 0.0)
importance_fraction = response.get('importanceFraction', 0.0)
return cls(bounds, confidence, importance_fraction)

@classmethod
def from_pb(cls, response):
"""Factory: construct ``CropHint`` from Vision gRPC response.
:type response: :class:`google.cloud.proto.vision.v1.\
image_annotator_pb2.CropHint`
:param response: gRPC response from Vision API with entity data.
:rtype: :class:`~google.cloud.vision.crop_hint.CropHint`
:returns: Instance of ``CropHint``.
"""
bounds = Bounds.from_pb(response.bounding_poly)
return cls(bounds, response.confidence, response.importance_fraction)

@property
def bounds(self):
"""Bounding polygon of crop hints.
:rtype: :class:`~google.cloud.vision.geometry.Bounds`
:returns: Instance of ``Bounds`` with populated vertices.
"""
return self._bounds

@property
def confidence(self):
"""Confidence of this being a salient region. Range [0, 1].
:rtype: float
:returns: float between 0 and 1, inclusive.
"""
return self._confidence

@property
def importance_fraction(self):
"""Fraction of importance of this salient region with respect to the
original image.
:rtype: float
:returns: float
"""
return self._importance_fraction
Loading

0 comments on commit 83a0b98

Please sign in to comment.