Skip to content

Commit 42a64a7

Browse files
authored
Merge branch 'develop' into al-1284
2 parents e0e29dd + a311e65 commit 42a64a7

File tree

10 files changed

+124
-55
lines changed

10 files changed

+124
-55
lines changed

README.md

Lines changed: 19 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,16 @@ The Labelbox Python API offers a simple, user-friendly way to interact with the
1818

1919
## Requirements
2020

21-
* Use Python 3.6, 3.7 or 3.8
22-
* [Create an account](http://app.labelbox.com/)
23-
* [Generate an API key](https://labelbox.com/docs/api/getting-started#create_api_key)
21+
- Use Python 3.6, 3.7 or 3.8
22+
- [Create an account](http://app.labelbox.com/)
23+
- [Generate an API key](https://labelbox.com/docs/api/getting-started#create_api_key)
2424

2525
## Installation
2626

2727
Prerequisite: Install pip
2828

2929
`pip` is a package manager for Python. **On macOS**, you can set it up to use the default python3 install via -
30+
3031
```
3132
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
3233
python3 get-pip.py
@@ -39,55 +40,28 @@ export PATH=/Users/<your-macOS-username>/Library/Python/3.8/bin:$PATH
3940
```
4041

4142
Install SDK locally, using Python's Pip manager
43+
4244
```
4345
pip3 install -e .
4446
```
4547

4648
Install dependencies
47-
```
48-
pip3 install -r requirements.txt
49-
```
50-
To install dependencies required for data processing modules use:
51-
```
52-
pip install labelbox[data]
53-
```
54-
### Note for Windows users
55-
The package `rasterio` installed by `labelbox[data]` relies on GDAL which could be difficult to install on Microsoft Windows.
56-
57-
You may see the following error message:
5849

5950
```
60-
INFO:root:Building on Windows requires extra options to setup.py to locate needed GDAL files. More information is available in the README.
61-
62-
ERROR: A GDAL API version must be specified. Provide a path to gdal-config using a GDAL_CONFIG environment variable or use a GDAL_VERSION environment variable.
51+
pip3 install -r requirements.txt
6352
```
6453

65-
As a workaround:
66-
67-
1. Download the binary files for GDAL and rasterio:
68-
69-
a. From https://www.lfd.uci.edu/~gohlke/pythonlibs/#gdal, download `GDAL‑3.3.2‑cp38‑cp38‑win_amd64.wh`
70-
71-
b. From https://www.lfd.uci.edu/~gohlke/pythonlibs/#rasterio, download `rasterio‑1.2.9‑cp38‑cp38‑win_amd64.whl`
72-
73-
Note: You need to download the right files for your Python version. In the files above `cp38` means CPython 3.8.
74-
75-
2. After downloading the files, please run the following commands, in this particular order.
54+
To install dependencies required for data processing modules use:
7655

7756
```
78-
pip install GDAL‑3.3.2‑cp38‑cp38‑win_amd64.wh
79-
pip install rasterio‑1.2.9‑cp38‑cp38‑win_amd64.whl
8057
pip install labelbox[data]
8158
```
8259

83-
This should resolve the error message.
84-
85-
8660
## Documentation
8761

88-
* [Visit our docs](https://labelbox.com/docs/python-api) to learn how the SDK works
89-
* Checkout our [notebook examples](examples/) to follow along with interactive tutorials
90-
* view our [API reference](https://labelbox.com/docs/python-api/api-reference).
62+
- [Visit our docs](https://labelbox.com/docs/python-api) to learn how the SDK works
63+
- Checkout our [notebook examples](examples/) to follow along with interactive tutorials
64+
- view our [API reference](https://labelbox.com/docs/python-api/api-reference).
9165

9266
## Authentication
9367

@@ -100,7 +74,9 @@ user@machine:~$ python3
10074
from labelbox import Client
10175
client = Client()
10276
```
103-
* Update api_key and endpoint if not using the production cloud deployment
77+
78+
- Update api_key and endpoint if not using the production cloud deployment
79+
10480
```
10581
# On prem
10682
client = Client( endpoint = "<local deployment>")
@@ -113,29 +89,35 @@ client = Client(api_key=os.environ['LABELBOX_TEST_API_KEY_LOCAL'], endpoint="htt
11389
```
11490

11591
## Contribution
92+
11693
Please consult `CONTRIB.md`
11794

11895
## Testing
96+
11997
1. Update the `Makefile` with your `local`, `staging`, `prod` API key. Ensure that docker has been installed on your system. Make sure the key is not from a free tier account.
12098
2. To test on `local`:
99+
121100
```
122101
user@machine:~$ export LABELBOX_TEST_API_KEY_LOCAL="<your local api key here>"
123102
make test-local # with an optional flag: PATH_TO_TEST=tests/integration/...etc LABELBOX_TEST_API_KEY_LOCAL=specify_here_or_export_me
124103
```
125104

126105
3. To test on `staging`:
106+
127107
```
128108
user@machine:~$ export LABELBOX_TEST_API_KEY_STAGING="<your staging api key here>"
129109
make test-staging # with an optional flag: PATH_TO_TEST=tests/integration/...etc LABELBOX_TEST_API_KEY_STAGING=specify_here_or_export_me
130110
```
131111

132112
4. To test on `prod`:
113+
133114
```
134115
user@machine:~$ export LABELBOX_TEST_API_KEY_PROD="<your prod api key here>"
135116
make test-prod # with an optional flag: PATH_TO_TEST=tests/integration/...etc LABELBOX_TEST_API_KEY_PROD=specify_here_or_export_me
136117
```
137118

138119
5. If you make any changes and need to rebuild the image used for testing, force a rebuild with the `-B` flag
120+
139121
```
140122
make -B {build|test-staging|test-prod}
141123
```

labelbox/data/annotation_types/data/tiled_image.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,7 @@
1515
from pydantic import BaseModel, validator
1616
from pydantic.class_validators import root_validator
1717

18-
from labelbox.data.annotation_types.geometry.polygon import Polygon
19-
from labelbox.data.annotation_types.geometry.point import Point
20-
from labelbox.data.annotation_types.geometry.line import Line
21-
from labelbox.data.annotation_types.geometry.rectangle import Rectangle
22-
from ..geometry import Point
18+
from labelbox.data.annotation_types import Rectangle, Point, Line, Polygon
2319
from .base_data import BaseData
2420
from .raster import RasterData
2521

labelbox/data/annotation_types/geometry/line.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import numpy as np
55
import cv2
66
from pydantic import validator
7+
from shapely.geometry import LineString as SLineString
78

89
from .point import Point
910
from .geometry import Geometry
@@ -25,6 +26,17 @@ def geometry(self) -> geojson.MultiLineString:
2526
return geojson.MultiLineString(
2627
[[[point.x, point.y] for point in self.points]])
2728

29+
@classmethod
30+
def from_shapely(cls, shapely_obj: SLineString) -> "Line":
31+
"""Transforms a shapely object."""
32+
if not isinstance(shapely_obj, SLineString):
33+
raise TypeError(
34+
f"Expected Shapely Line. Got {shapely_obj.geom_type}")
35+
36+
obj_coords = shapely_obj.__geo_interface__['coordinates']
37+
return Line(
38+
points=[Point(x=coords[0], y=coords[1]) for coords in obj_coords])
39+
2840
def draw(self,
2941
height: Optional[int] = None,
3042
width: Optional[int] = None,

labelbox/data/annotation_types/geometry/mask.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
import numpy as np
44
from pydantic.class_validators import validator
5-
from rasterio.features import shapes
6-
from shapely.geometry import MultiPolygon, shape
5+
from shapely.geometry import MultiPolygon, Polygon
76
import cv2
87

98
from ..data import MaskData
@@ -39,12 +38,23 @@ class Mask(Geometry):
3938
@property
4039
def geometry(self) -> Dict[str, Tuple[int, int, int]]:
4140
mask = self.draw(color=1)
42-
polygons = (
43-
shape(shp)
44-
for shp, val in shapes(mask, mask=None)
45-
# ignore if shape is area of smaller than 1 pixel
46-
if val >= 1)
47-
return MultiPolygon(polygons).__geo_interface__
41+
contours, hierarchy = cv2.findContours(image=mask,
42+
mode=cv2.RETR_TREE,
43+
method=cv2.CHAIN_APPROX_NONE)
44+
45+
holes = []
46+
external_contours = []
47+
for i in range(len(contours)):
48+
if hierarchy[0, i, 3] != -1:
49+
#determined to be a hole based on contour hierarchy
50+
holes.append(contours[i])
51+
else:
52+
external_contours.append(contours[i])
53+
54+
external_polygons = self._extract_polygons_from_contours(
55+
external_contours)
56+
holes = self._extract_polygons_from_contours(holes)
57+
return external_polygons.difference(holes).__geo_interface__
4858

4959
def draw(self,
5060
height: Optional[int] = None,
@@ -86,6 +96,12 @@ def draw(self,
8696
canvas[mask.astype(np.bool)] = color
8797
return canvas
8898

99+
def _extract_polygons_from_contours(self, contours: List) -> MultiPolygon:
100+
contours = map(np.squeeze, contours)
101+
filtered_contours = filter(lambda contour: len(contour) > 2, contours)
102+
polygons = map(Polygon, filtered_contours)
103+
return MultiPolygon(polygons)
104+
89105
def create_url(self, signer: Callable[[bytes], str]) -> str:
90106
"""
91107
Update the segmentation mask to have a url.

labelbox/data/annotation_types/geometry/point.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import geojson
44
import numpy as np
55
import cv2
6+
from shapely.geometry import Point as SPoint
67

78
from .geometry import Geometry
89

@@ -24,6 +25,16 @@ class Point(Geometry):
2425
def geometry(self) -> geojson.Point:
2526
return geojson.Point((self.x, self.y))
2627

28+
@classmethod
29+
def from_shapely(cls, shapely_obj: SPoint) -> "Point":
30+
"""Transforms a shapely object."""
31+
if not isinstance(shapely_obj, SPoint):
32+
raise TypeError(
33+
f"Expected Shapely Point. Got {shapely_obj.geom_type}")
34+
35+
obj_coords = shapely_obj.__geo_interface__['coordinates']
36+
return Point(x=obj_coords[0], y=obj_coords[1])
37+
2738
def draw(self,
2839
height: Optional[int] = None,
2940
width: Optional[int] = None,

labelbox/data/annotation_types/geometry/polygon.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import geojson
55
import numpy as np
66
from pydantic import validator
7+
from shapely.geometry import Polygon as SPolygon
78

89
from .geometry import Geometry
910
from .point import Point
@@ -30,6 +31,17 @@ def geometry(self) -> geojson.Polygon:
3031
self.points.append(self.points[0])
3132
return geojson.Polygon([[(point.x, point.y) for point in self.points]])
3233

34+
@classmethod
35+
def from_shapely(cls, shapely_obj: SPolygon) -> "Polygon":
36+
"""Transforms a shapely object."""
37+
#we only consider 0th index because we only allow for filled polygons
38+
if not isinstance(shapely_obj, SPolygon):
39+
raise TypeError(
40+
f"Expected Shapely Polygon. Got {shapely_obj.geom_type}")
41+
obj_coords = shapely_obj.__geo_interface__['coordinates'][0]
42+
return Polygon(
43+
points=[Point(x=coords[0], y=coords[1]) for coords in obj_coords])
44+
3345
def draw(self,
3446
height: Optional[int] = None,
3547
width: Optional[int] = None,

labelbox/data/annotation_types/geometry/rectangle.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import cv2
44
import geojson
55
import numpy as np
6+
from shapely.geometry import Polygon as SPolygon
67

78
from .geometry import Geometry
89
from .point import Point
@@ -30,6 +31,24 @@ def geometry(self) -> geojson.geometry.Geometry:
3031
[self.start.x, self.start.y],
3132
]])
3233

34+
@classmethod
35+
def from_shapely(cls, shapely_obj: SPolygon) -> "Rectangle":
36+
"""Transforms a shapely object.
37+
38+
If the provided shape is a non-rectangular polygon, a rectangle will be
39+
returned based on the min and max x,y values."""
40+
if not isinstance(shapely_obj, SPolygon):
41+
raise TypeError(
42+
f"Expected Shapely Polygon. Got {shapely_obj.geom_type}")
43+
44+
min_x, min_y, max_x, max_y = shapely_obj.bounds
45+
46+
start = [min_x, min_y]
47+
end = [max_x, max_y]
48+
49+
return Rectangle(start=Point(x=start[0], y=start[1]),
50+
end=Point(x=end[0], y=end[1]))
51+
3352
def draw(self,
3453
height: Optional[int] = None,
3554
width: Optional[int] = None,

labelbox/data/serialization/coco/instance_dataset.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ def mask_to_coco_object_annotation(annotation: ObjectAnnotation, annot_idx: int,
2121
# This is going to fill any holes into the multipolygon
2222
# If you need to support holes use the panoptic data format
2323
shapely = annotation.value.shapely.simplify(1).buffer(0)
24+
if shapely.is_empty:
25+
shapely = annotation.value.shapely.simplify(1).buffer(0.01)
2426
xmin, ymin, xmax, ymax = shapely.bounds
2527
# Iterate over polygon once or multiple polygon for each item
2628
area = shapely.area

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ shapely
77
tqdm
88
geojson
99
numpy
10-
rasterio
1110
PILLOW
1211
opencv-python
1312
typeguard

tests/data/annotation_types/geometry/test_mask.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,17 @@ def test_mask():
2121
expected1 = {
2222
'type':
2323
'MultiPolygon',
24-
'coordinates': [(((0.0, 0.0), (0.0, 11.0), (11.0, 11.0), (11.0, 0.0),
25-
(0.0, 0.0)),)]
24+
'coordinates': [
25+
(((0.0, 0.0), (0.0, 1.0), (0.0, 2.0), (0.0, 3.0), (0.0, 4.0), (0.0,
26+
5.0),
27+
(0.0, 6.0), (0.0, 7.0), (0.0, 8.0), (0.0, 9.0), (0.0, 10.0),
28+
(1.0, 10.0), (2.0, 10.0), (3.0, 10.0), (4.0, 10.0), (5.0, 10.0),
29+
(6.0, 10.0), (7.0, 10.0), (8.0, 10.0), (9.0, 10.0), (10.0, 10.0),
30+
(10.0, 9.0), (10.0, 8.0), (10.0, 7.0), (10.0, 6.0), (10.0, 5.0),
31+
(10.0, 4.0), (10.0, 3.0), (10.0, 2.0), (10.0, 1.0), (10.0, 0.0),
32+
(9.0, 0.0), (8.0, 0.0), (7.0, 0.0), (6.0, 0.0), (5.0, 0.0),
33+
(4.0, 0.0), (3.0, 0.0), (2.0, 0.0), (1.0, 0.0), (0.0, 0.0)),)
34+
]
2635
}
2736
assert mask1.geometry == expected1
2837
assert mask1.shapely.__geo_interface__ == expected1
@@ -31,8 +40,19 @@ def test_mask():
3140
expected2 = {
3241
'type':
3342
'MultiPolygon',
34-
'coordinates': [(((20.0, 20.0), (20.0, 31.0), (31.0, 31.0),
35-
(31.0, 20.0), (20.0, 20.0)),)]
43+
'coordinates': [
44+
(((20.0, 20.0), (20.0, 21.0), (20.0, 22.0), (20.0, 23.0),
45+
(20.0, 24.0), (20.0, 25.0), (20.0, 26.0), (20.0, 27.0),
46+
(20.0, 28.0), (20.0, 29.0), (20.0, 30.0), (21.0, 30.0),
47+
(22.0, 30.0), (23.0, 30.0), (24.0, 30.0), (25.0, 30.0),
48+
(26.0, 30.0), (27.0, 30.0), (28.0, 30.0), (29.0, 30.0),
49+
(30.0, 30.0), (30.0, 29.0), (30.0, 28.0), (30.0, 27.0),
50+
(30.0, 26.0), (30.0, 25.0), (30.0, 24.0), (30.0, 23.0),
51+
(30.0, 22.0), (30.0, 21.0), (30.0, 20.0), (29.0, 20.0),
52+
(28.0, 20.0), (27.0, 20.0), (26.0, 20.0), (25.0, 20.0),
53+
(24.0, 20.0), (23.0, 20.0), (22.0, 20.0), (21.0, 20.0), (20.0,
54+
20.0)),)
55+
]
3656
}
3757
assert mask2.geometry == expected2
3858
assert mask2.shapely.__geo_interface__ == expected2

0 commit comments

Comments
 (0)