Skip to content

Commit 81cb07c

Browse files
committed
Add Vision Logo detection.
1 parent 2bb5cd9 commit 81cb07c

File tree

9 files changed

+225
-23
lines changed

9 files changed

+225
-23
lines changed

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@
150150
vision-usage
151151
vision-client
152152
vision-image
153+
vision-entity
153154
vision-feature
154155
vision-face
155156

docs/vision-entity.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Vision Entity
2+
=============
3+
4+
Entity
5+
~~~~~~
6+
7+
.. automodule:: gcloud.vision.entity
8+
:members:
9+
:undoc-members:
10+
:show-inheritance:

gcloud/vision/_fixtures.py

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,56 @@
1-
LABEL_DETECTION_RESPONSE = {
1+
LOGO_DETECTION_RESPONSE = {
22
"responses": [
33
{
4-
"labelAnnotations": [
4+
"logoAnnotations": [
55
{
6-
"mid": "/m/0k4j",
7-
"description": "automobile",
8-
"score": 0.9776855
6+
"mid": "/m/05b5c",
7+
"description": "Brand1",
8+
"score": 0.63192177,
9+
"boundingPoly": {
10+
"vertices": [
11+
{
12+
"x": 78,
13+
"y": 162
14+
},
15+
{
16+
"x": 282,
17+
"y": 162
18+
},
19+
{
20+
"x": 282,
21+
"y": 211
22+
},
23+
{
24+
"x": 78,
25+
"y": 211
26+
}
27+
]
28+
}
929
},
1030
{
11-
"mid": "/m/07yv9",
12-
"description": "vehicle",
13-
"score": 0.947987
14-
},
15-
{
16-
"mid": "/m/07r04",
17-
"description": "truck",
18-
"score": 0.88429511
31+
"mid": "/m/0fpzzp",
32+
"description": "Brand2",
33+
"score": 0.5492993,
34+
"boundingPoly": {
35+
"vertices": [
36+
{
37+
"x": 310,
38+
"y": 209
39+
},
40+
{
41+
"x": 477,
42+
"y": 209
43+
},
44+
{
45+
"x": 477,
46+
"y": 282
47+
},
48+
{
49+
"x": 310,
50+
"y": 282
51+
}
52+
]
53+
}
1954
}
2055
]
2156
}

gcloud/vision/entity.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Copyright 2016 Google Inc. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Entity class for holding information returned from annotating an image."""
16+
17+
18+
from gcloud.vision.geometry import Bounds
19+
20+
21+
class EntityAnnotation(object):
22+
"""Representation of an entity returned from the Vision API."""
23+
def __init__(self, bounds, description, mid, score):
24+
self._bounds = bounds
25+
self._description = description
26+
self._mid = mid
27+
self._score = score
28+
29+
@classmethod
30+
def from_api_repr(cls, response):
31+
"""Factory: construct entity from Vision API response.
32+
33+
:type response: dict
34+
:param response: Dictionary response from Vision API with entity data.
35+
36+
:rtype: :class:`gcloud.vision.entiy.EntityAnnotation`
37+
:returns: Instance of ``EntityAnnotation``.
38+
"""
39+
bounds = Bounds.from_api_repr(response['boundingPoly'])
40+
description = response['description']
41+
mid = response['mid']
42+
score = response['score']
43+
44+
return cls(bounds, description, mid, score)
45+
46+
@property
47+
def bounds(self):
48+
"""Bounding polygon of detected image feature.
49+
50+
:rtype: :class:`gcloud.vision.geometry.Bounds`
51+
:returns: Instance of ``Bounds`` with populated vertices.
52+
"""
53+
return self._bounds
54+
55+
@property
56+
def description(self):
57+
"""Description of feature detected in image.
58+
59+
:rtype: str
60+
:returns: String description of feature detected in image.
61+
"""
62+
return self._description
63+
64+
@property
65+
def mid(self):
66+
"""MID of feature detected in image.
67+
68+
:rtype: str
69+
:returns: String MID of feature detected in image.
70+
"""
71+
return self._mid
72+
73+
@property
74+
def score(self):
75+
"""Overall score of the result. Range [0, 1].
76+
77+
:rtype: float
78+
:returns: Overall score of the result. Range [0, 1].
79+
"""
80+
return self._score

gcloud/vision/face.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"""Face class representing the Vision API's face detection response."""
1616

1717

18-
from gcloud.vision.geometry import BoundsBase
18+
from gcloud.vision.geometry import Bounds
19+
from gcloud.vision.geometry import FDBounds
1920
from gcloud.vision.likelihood import Likelihood
2021
from gcloud.vision.geometry import Position
2122

@@ -68,10 +69,6 @@ def tilt(self):
6869
return self._tilt
6970

7071

71-
class Bounds(BoundsBase):
72-
"""The bounding polygon of the entire face."""
73-
74-
7572
class Emotions(object):
7673
"""Emotions displayed by the face detected in an image."""
7774
def __init__(self, joy_likelihood, sorrow_likelihood,
@@ -348,10 +345,6 @@ class FaceLandmarkTypes(object):
348345
CHIN_RIGHT_GONION = 'CHIN_RIGHT_GONION'
349346

350347

351-
class FDBounds(BoundsBase):
352-
"""The bounding polygon of just the skin portion of the face."""
353-
354-
355348
class Landmark(object):
356349
"""A face-specific landmark (for example, a face feature, left eye)."""
357350
def __init__(self, position, landmark_type):

gcloud/vision/geometry.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ def vertices(self):
4646
return self._vertices
4747

4848

49+
class Bounds(BoundsBase):
50+
"""A polygon boundry of the detected feature."""
51+
52+
53+
class FDBounds(BoundsBase):
54+
"""The bounding polygon of just the skin portion of the face."""
55+
56+
4957
class Position(object):
5058
"""A 3D position in the image.
5159

gcloud/vision/image.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from gcloud._helpers import _to_bytes
2121
from gcloud._helpers import _bytes_to_unicode
22+
from gcloud.vision.entity import EntityAnnotation
2223
from gcloud.vision.face import Face
2324
from gcloud.vision.feature import Feature
2425
from gcloud.vision.feature import FeatureTypes
@@ -105,4 +106,13 @@ def detect_logos(self, limit=10):
105106
:param limit: The maximum number of logos to find.
106107
107108
:rtype: list
108-
:returns: List of
109+
:returns: List of :class:`gcloud.vision.entity.EntityAnnotation`.
110+
"""
111+
logos = []
112+
logo_detection_feature = Feature(FeatureTypes.LOGO_DETECTION, limit)
113+
result = self.client.annotate(self, [logo_detection_feature])
114+
for logo_response in result['logoAnnotations']:
115+
logo = EntityAnnotation.from_api_repr(logo_response)
116+
logos.append(logo)
117+
118+
return logos

gcloud/vision/test_client.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,38 @@ def test_face_detection_from_content(self):
110110
image_request['image']['content'])
111111
self.assertEqual(5, image_request['features'][0]['maxResults'])
112112

113+
def test_logo_detection_from_source(self):
114+
from gcloud.vision.entity import EntityAnnotation
115+
from gcloud.vision._fixtures import LOGO_DETECTION_RESPONSE as RETURNED
116+
credentials = _Credentials()
117+
client = self._makeOne(project=self.PROJECT, credentials=credentials)
118+
client.connection = _Connection(RETURNED)
119+
120+
image = client.image(_IMAGE_SOURCE)
121+
logos = image.detect_logos(limit=3)
122+
self.assertEqual(2, len(logos))
123+
self.assertTrue(isinstance(logos[0], EntityAnnotation))
124+
image_request = client.connection._requested[0]['data']['requests'][0]
125+
self.assertEqual(_IMAGE_SOURCE,
126+
image_request['image']['source']['gcs_image_uri'])
127+
self.assertEqual(3, image_request['features'][0]['maxResults'])
128+
129+
def test_logo_detection_from_content(self):
130+
from gcloud.vision.entity import EntityAnnotation
131+
from gcloud.vision._fixtures import LOGO_DETECTION_RESPONSE as RETURNED
132+
credentials = _Credentials()
133+
client = self._makeOne(project=self.PROJECT, credentials=credentials)
134+
client.connection = _Connection(RETURNED)
135+
136+
image = client.image(_IMAGE_CONTENT)
137+
logos = image.detect_logos(limit=5)
138+
self.assertEqual(2, len(logos))
139+
self.assertTrue(isinstance(logos[0], EntityAnnotation))
140+
image_request = client.connection._requested[0]['data']['requests'][0]
141+
self.assertEqual(self.B64_IMAGE_CONTENT,
142+
image_request['image']['content'])
143+
self.assertEqual(5, image_request['features'][0]['maxResults'])
144+
113145

114146
class TestVisionRequest(unittest.TestCase):
115147
def _getTargetClass(self):

gcloud/vision/test_entity.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright 2016 Google Inc. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import unittest
16+
17+
18+
class TestEntityAnnotation(unittest.TestCase):
19+
def _getTargetClass(self):
20+
from gcloud.vision.entity import EntityAnnotation
21+
return EntityAnnotation
22+
23+
def test_logo_annotation(self):
24+
from gcloud.vision._fixtures import LOGO_DETECTION_RESPONSE
25+
26+
LOGO = LOGO_DETECTION_RESPONSE['responses'][0]['logoAnnotations'][0]
27+
entity_class = self._getTargetClass()
28+
logo = entity_class.from_api_repr(LOGO)
29+
30+
self.assertEqual('/m/05b5c', logo.mid)
31+
self.assertEqual('Brand1', logo.description)
32+
self.assertEqual(0.63192177, logo.score)
33+
self.assertEqual(162, logo.bounds.vertices[0].y_coordinate)

0 commit comments

Comments
 (0)