Skip to content

Commit 5668986

Browse files
committed
Add Vision landmark detection.
1 parent f834020 commit 5668986

File tree

5 files changed

+217
-10
lines changed

5 files changed

+217
-10
lines changed

google/cloud/vision/entity.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717

1818
from google.cloud.vision.geometry import Bounds
19+
from google.cloud.vision.geometry import LocationInformation
1920

2021

2122
class EntityAnnotation(object):
@@ -27,15 +28,20 @@ class EntityAnnotation(object):
2728
:type description: str
2829
:param description: Description of entity detected in an image.
2930
31+
:type locations: list of
32+
:class:`~google.cloud.vision.geometry.LocationInformation`.
33+
:param locations: List of ``LocationInformation`` instances.
34+
3035
:type mid: str
3136
:param mid: Opaque entity ID.
3237
3338
:type score: float
3439
:param score: Overall score of the result. Range [0, 1].
3540
"""
36-
def __init__(self, bounds, description, mid, score):
41+
def __init__(self, bounds, description, locations, mid, score):
3742
self._bounds = bounds
3843
self._description = description
44+
self._locations = locations
3945
self._mid = mid
4046
self._score = score
4147

@@ -51,10 +57,12 @@ def from_api_repr(cls, response):
5157
"""
5258
bounds = Bounds.from_api_repr(response['boundingPoly'])
5359
description = response['description']
60+
locations = [LocationInformation.from_api_repr(location)
61+
for location in response.get('locations', [])]
5462
mid = response['mid']
5563
score = response['score']
5664

57-
return cls(bounds, description, mid, score)
65+
return cls(bounds, description, locations, mid, score)
5866

5967
@property
6068
def bounds(self):
@@ -74,6 +82,16 @@ def description(self):
7482
"""
7583
return self._description
7684

85+
@property
86+
def locations(self):
87+
"""Location coordinates landmarks detected.
88+
89+
:rtype: :class:`google.cloud.vision.geometry.LocationInformation`
90+
:returns: ``LocationInformation`` populated with latitude and longitude
91+
of object detected in an image.
92+
"""
93+
return self._locations
94+
7795
@property
7896
def mid(self):
7997
"""MID of feature detected in image.

google/cloud/vision/geometry.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,53 @@ class FDBounds(BoundsBase):
5858
"""The bounding polygon of just the skin portion of the face."""
5959

6060

61+
class LocationInformation(object):
62+
"""Representation of location information returned by the Vision API.
63+
64+
:type latitude: float
65+
:param latitude: Latitude coordinate of geographical location.
66+
67+
:type longitude: float
68+
:param longitude: Longitude coordinate of geographical location.
69+
"""
70+
def __init__(self, latitude, longitude):
71+
self._latitude = latitude
72+
self._longitude = longitude
73+
74+
@classmethod
75+
def from_api_repr(cls, response):
76+
"""Factory: construct location information from Vision API response.
77+
78+
:type response: dict
79+
:param response: Dictionary response of locations.
80+
81+
:rtype: :class:`~google.cloud.vision.geometry.LocationInformation`
82+
:returns: ``LocationInformation`` with populated latitude and
83+
longitude.
84+
"""
85+
latitude = response['latLng']['latitude']
86+
longitude = response['latLng']['longitude']
87+
return cls(latitude, longitude)
88+
89+
@property
90+
def latitude(self):
91+
"""Latitude coordinate.
92+
93+
:rtype: float
94+
:returns: Latitude coordinate of location.
95+
"""
96+
return self._latitude
97+
98+
@property
99+
def longitude(self):
100+
"""Longitude coordinate.
101+
102+
:rtype: float
103+
:returns: Longitude coordinate of location.
104+
"""
105+
return self._longitude
106+
107+
61108
class Position(object):
62109
"""A 3D position in the image.
63110

google/cloud/vision/image.py

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,28 @@ def source(self):
8282
"""
8383
return self._source
8484

85+
def _detect_annotation(self, feature):
86+
"""Generic method for detecting a single annotation.
87+
88+
:type feature: :class:`~google.cloud.vision.feature.Feature`
89+
:param feature: The ``Feature`` indication the type of annotation to
90+
perform.
91+
92+
:rtype: list
93+
:returns: List of
94+
:class:`~google.cloud.vision.entity.EntityAnnotation`.
95+
"""
96+
reverse_types = {
97+
'LANDMARK_DETECTION': 'landmarkAnnotations',
98+
'LOGO_DETECTION': 'logoAnnotations',
99+
}
100+
detected_objects = []
101+
result = self.client.annotate(self, [feature])
102+
for response in result[reverse_types[feature.feature_type]]:
103+
detected_object = EntityAnnotation.from_api_repr(response)
104+
detected_objects.append(detected_object)
105+
return detected_objects
106+
85107
def detect_faces(self, limit=10):
86108
"""Detect faces in image.
87109
@@ -100,6 +122,19 @@ def detect_faces(self, limit=10):
100122

101123
return faces
102124

125+
def detect_landmarks(self, limit=10):
126+
"""Detect landmarks in an image.
127+
128+
:type limit: int
129+
:param limit: The maximum number of landmarks to find.
130+
131+
:rtype: list
132+
:returns: List of
133+
:class:`~google.cloud.vision.entity.EntityAnnotation`.
134+
"""
135+
feature = Feature(FeatureTypes.LANDMARK_DETECTION, limit)
136+
return self._detect_annotation(feature)
137+
103138
def detect_logos(self, limit=10):
104139
"""Detect logos in an image.
105140
@@ -110,11 +145,5 @@ def detect_logos(self, limit=10):
110145
:returns: List of
111146
:class:`~google.cloud.vision.entity.EntityAnnotation`.
112147
"""
113-
logos = []
114-
logo_detection_feature = Feature(FeatureTypes.LOGO_DETECTION, limit)
115-
result = self.client.annotate(self, [logo_detection_feature])
116-
for logo_response in result['logoAnnotations']:
117-
logo = EntityAnnotation.from_api_repr(logo_response)
118-
logos.append(logo)
119-
120-
return logos
148+
feature = Feature(FeatureTypes.LOGO_DETECTION, limit)
149+
return self._detect_annotation(feature)

unit_tests/vision/_fixtures.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,78 @@
1+
LANDMARK_DETECTION_RESPONSE = {
2+
'responses': [
3+
{
4+
'landmarkAnnotations': [
5+
{
6+
'mid': '/m/04gdr',
7+
'description': 'Louvre',
8+
'score': 0.67257267,
9+
'boundingPoly': {
10+
'vertices': [
11+
{
12+
'x': 1075,
13+
'y': 49
14+
},
15+
{
16+
'x': 1494,
17+
'y': 49
18+
},
19+
{
20+
'x': 1494,
21+
'y': 307
22+
},
23+
{
24+
'x': 1075,
25+
'y': 307
26+
}
27+
]
28+
},
29+
'locations': [
30+
{
31+
'latLng': {
32+
'latitude': 48.861013,
33+
'longitude': 2.335818
34+
}
35+
}
36+
]
37+
},
38+
{
39+
'mid': '/m/094llg',
40+
'description': 'Louvre Pyramid',
41+
'score': 0.53734678,
42+
'boundingPoly': {
43+
'vertices': [
44+
{
45+
'x': 227,
46+
'y': 274
47+
},
48+
{
49+
'x': 1471,
50+
'y': 274
51+
},
52+
{
53+
'x': 1471,
54+
'y': 624
55+
},
56+
{
57+
'x': 227,
58+
'y': 624
59+
}
60+
]
61+
},
62+
'locations': [
63+
{
64+
'latLng': {
65+
'latitude': 48.860749,
66+
'longitude': 2.336312
67+
}
68+
}
69+
]
70+
}
71+
]
72+
}
73+
]
74+
}
75+
176
LOGO_DETECTION_RESPONSE = {
277
'responses': [
378
{

unit_tests/vision/test_client.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,44 @@ def test_face_detection_from_content(self):
114114
image_request['image']['content'])
115115
self.assertEqual(5, image_request['features'][0]['maxResults'])
116116

117+
def test_landmark_detection_from_source(self):
118+
from google.cloud.vision.entity import EntityAnnotation
119+
from unit_tests.vision._fixtures import (LANDMARK_DETECTION_RESPONSE as
120+
RETURNED)
121+
credentials = _Credentials()
122+
client = self._makeOne(project=self.PROJECT, credentials=credentials)
123+
client.connection = _Connection(RETURNED)
124+
125+
image = client.image(source_uri=_IMAGE_SOURCE)
126+
landmarks = image.detect_landmarks(limit=3)
127+
self.assertEqual(2, len(landmarks))
128+
self.assertTrue(isinstance(landmarks[0], EntityAnnotation))
129+
image_request = client.connection._requested[0]['data']['requests'][0]
130+
self.assertEqual(_IMAGE_SOURCE,
131+
image_request['image']['source']['gcs_image_uri'])
132+
self.assertEqual(3, image_request['features'][0]['maxResults'])
133+
self.assertEqual(48.861013, landmarks[0].locations[0].latitude)
134+
self.assertEqual(2.335818, landmarks[0].locations[0].longitude)
135+
self.assertEqual('/m/04gdr', landmarks[0].mid)
136+
self.assertEqual('/m/094llg', landmarks[1].mid)
137+
138+
def test_landmark_detection_from_content(self):
139+
from google.cloud.vision.entity import EntityAnnotation
140+
from unit_tests.vision._fixtures import (LANDMARK_DETECTION_RESPONSE as
141+
RETURNED)
142+
credentials = _Credentials()
143+
client = self._makeOne(project=self.PROJECT, credentials=credentials)
144+
client.connection = _Connection(RETURNED)
145+
146+
image = client.image(content=_IMAGE_CONTENT)
147+
landmarks = image.detect_landmarks(limit=5)
148+
self.assertEqual(2, len(landmarks))
149+
self.assertTrue(isinstance(landmarks[0], EntityAnnotation))
150+
image_request = client.connection._requested[0]['data']['requests'][0]
151+
self.assertEqual(self.B64_IMAGE_CONTENT,
152+
image_request['image']['content'])
153+
self.assertEqual(5, image_request['features'][0]['maxResults'])
154+
117155
def test_logo_detection_from_source(self):
118156
from google.cloud.vision.entity import EntityAnnotation
119157
from unit_tests.vision._fixtures import LOGO_DETECTION_RESPONSE

0 commit comments

Comments
 (0)