diff --git a/MANIFEST.in b/MANIFEST.in index 4bfa6f178..8c78ad091 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -13,5 +13,6 @@ recursive-include docs *.rst recursive-include labelbox *.py include labelbox/exporters/pascal_voc_writer/templates/annotation.xml recursive-include tests *.json +recursive-include tests *.lbx recursive-include tests *.png recursive-include tests *.py diff --git a/Pipfile b/Pipfile index 768bd5045..3f9616d3a 100644 --- a/Pipfile +++ b/Pipfile @@ -26,6 +26,7 @@ sphinxcontrib-napoleon = "*" setuptools = ">=40.2.0" wheel = "*" sphinx-rtd-theme = "*" +twine = "*" [requires] python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock index fe747615f..f93b4f904 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "783555ed63d4d1381b0184241d346046c945fdca2072fce46cfe36728daff77b" + "sha256": "3e757f363a6d88ddaca0941bdfefacf37da62a9d2fbad8e412bfbe229f201de0" }, "pipfile-spec": 6, "requires": { @@ -53,9 +53,10 @@ }, "click-plugins": { "hashes": [ - "sha256:7acc5e7eedd2dfd719714e8d53ae99030b5357aed661d0b06dacd6c2d583d7c5" + "sha256:b1ee1ccc9421c73007fe290680d97984eb6eaf5f4512b7620c6aa46031d6cb6b", + "sha256:dfed74b5063546a137de99baaaf742b4de4337ad2b3e1df5ec7c8a256adc0847" ], - "version": "==1.0.3" + "version": "==1.0.4" }, "cligj": { "hashes": [ @@ -117,7 +118,7 @@ "sha256:e3660744cda0d94b90141cdd0db9308b958a372cfeee8d7188fdf5ad9108ea82", "sha256:f2362d0ca3e16c37782c1054d7972b8ad2729169567e3f0f4e5dd3cdf85f188e" ], - "markers": "python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.3.*' and python_version != '3.1.*' and python_version >= '2.7'", + "markers": "python_version != '3.0.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.3.*' and python_version != '3.1.*'", "version": "==1.15.1" }, "pillow": { @@ -158,26 +159,27 @@ }, "pyparsing": { "hashes": [ - "sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04", - "sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010" + "sha256:905d8090c335314568b5faee0025b1829f27bb974604a5762a6cdef3a7dfc3b7", + "sha256:f493ee323be1e94929416b3585eefcc04943115cecbaaa35a8c86d1a2368af19" ], - "version": "==2.2.0" + "markers": "python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.6'", + "version": "==2.2.1" }, "rasterio": { "hashes": [ - "sha256:2dbed0296f3982b142d1fa66d44248ee862404aaa3dcf95c7f81ff240a70d139", - "sha256:31390423afaa651f420711e65470d01f6788238d8056a8689f0a9ce2b259809c", - "sha256:77b775028c2c745e047f9950f2c6305d9f827249b1114662714fe8c47a3b5dcd", - "sha256:7aaa539785c00f59f8dfe3e73a901215ef6bed67b4f18ff8d3bab588340ff45e", - "sha256:949cd9ce0143e3e71171e5833c98713cb2fbda1e64aaf27d9e795ac8225a6319", - "sha256:95465b8767f1b40865005423431444747aaf1c4230eb7e45e89bfd9d03a5b89e", - "sha256:a0e678479991e357df9a6967d3f93c64ccd1ac4ad41c6637fa9726f5cba91059", - "sha256:a6e851e3e0592b9e94c2c5fdb198dcedd8a18c5413ca634d1efeead8f8ea90a6", - "sha256:b494a0a16f8ec798192e452e5ea30232fccdc86e01593a88ed0753768501bb9e", - "sha256:c41d9445bf65d9261200097061d154fd568378bfe51f8c98ba4bc33bfb76a16f" + "sha256:22f84ddd39bd2ea08423030d44205fcc17b784978693a21e06a28e66a2e839a0", + "sha256:3397b3f290b668658ce1a98da1827d2761b849a3633f2bc7173d21ee1d2f2a83", + "sha256:55510bd2d0fba3f67cb2c1655295085777a6f6fa00c5ed93149c07ec47744427", + "sha256:60ed7cd3eec0134fa8d586845fd698eb41451eb9c8f84f50fa0656d76788bb50", + "sha256:6e5812855157b4d3bd9cdae92f49b9bbfa88f897850e9b5a3cc2d71a6ed6394e", + "sha256:c4b8c4643f8de20b4fd02836b951fada0bb50625423bef8fcbd1ce658d91116c", + "sha256:dc522623b62c423de8e640ba5b39ca614a52f25f62d648c9682319e185f2db12", + "sha256:ed018f8c64b06cca0e9a89967862f6898be2cf91038fcff7a7ae598b18f194a7", + "sha256:f001ea0f081f605cac36201886c4a1550e554a19c29632662317bfe5271c2d53", + "sha256:f2b8ff4714e7b7de54c39ce8c10747e66ca374a5b2516e0f2ee8a3f121cfc4e3" ], "index": "pypi", - "version": "==1.0.3.post1" + "version": "==1.0.4" }, "requests": { "hashes": [ @@ -232,6 +234,7 @@ "sha256:0ae01783adeaa6948352fc474605da78d277da545b2315f6d2febe0f065bab02", "sha256:8d56182fc83e1a1893284f69abd35751ce30e8f0a33794c2802e7e5d6547e1f1" ], + "markers": "python_version != '3.0.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.3.*' and python_version != '3.1.*'", "version": "==1.4.2" }, "urllib3": { @@ -239,7 +242,7 @@ "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" ], - "markers": "python_version != '3.3.*' and python_version < '4' and python_version != '3.0.*' and python_version != '3.2.*' and python_version >= '2.6' and python_version != '3.1.*'", + "markers": "python_version != '3.0.*' and python_version >= '2.6' and python_version != '3.2.*' and python_version != '3.3.*' and python_version < '4' and python_version != '3.1.*'", "version": "==1.23" } }, @@ -263,7 +266,7 @@ "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" ], - "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*'", + "markers": "python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.0.*' and python_version >= '2.7' and python_version != '3.3.*'", "version": "==1.2.1" }, "attrs": { @@ -336,7 +339,7 @@ "sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6", "sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80" ], - "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.2.*' and python_version < '4' and python_version >= '2.6'", + "markers": "python_version != '3.2.*' and python_version < '4' and python_version != '3.1.*' and python_version != '3.0.*' and python_version >= '2.6'", "version": "==4.5.1" }, "docutils": { @@ -368,7 +371,7 @@ "sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8", "sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5" ], - "markers": "python_version != '3.1.*' and python_version != '3.2.*' and python_version != '3.3.*' and python_version != '3.0.*' and python_version >= '2.7'", + "markers": "python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.0.*' and python_version >= '2.7' and python_version != '3.3.*'", "version": "==1.1.0" }, "isort": { @@ -377,7 +380,7 @@ "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" ], - "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.2.*'", + "markers": "python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.0.*' and python_version >= '2.7' and python_version != '3.3.*'", "version": "==4.3.4" }, "jinja2": { @@ -445,11 +448,18 @@ }, "mypy": { "hashes": [ - "sha256:673ea75fb750289b7d1da1331c125dc62fc1c3a8db9129bb372ae7b7d5bf300a", - "sha256:c770605a579fdd4a014e9f0a34b6c7a36ce69b08100ff728e96e27445cef3b3c" + "sha256:00b95bfdc0d5b9aa53c906e56fb91937743f2121d66684db5f947ec5d75f565d", + "sha256:6704586b4c2bf7dfa5e87a422be9ca57db622bab65008245759f3d4baeb219dd" ], "index": "pypi", - "version": "==0.620" + "version": "==0.630" + }, + "mypy-extensions": { + "hashes": [ + "sha256:37e0e956f41369209a3d5f34580150bcacfabaa57b33a15c0b25f4b5725e0812", + "sha256:b16cabe759f55e3409a7d231ebd2841378fb0c27a5d1994719e340e4f429ac3e" + ], + "version": "==0.4.1" }, "packaging": { "hashes": [ @@ -458,12 +468,19 @@ ], "version": "==17.1" }, + "pkginfo": { + "hashes": [ + "sha256:5878d542a4b3f237e359926384f1dde4e099c9f5525d236b1840cf704fa8d474", + "sha256:a39076cb3eb34c333a0dd390b568e9e1e881c7bf2cc0aee12120636816f55aee" + ], + "version": "==1.4.2" + }, "pluggy": { "hashes": [ "sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1", "sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1" ], - "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*'", + "markers": "python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.0.*' and python_version >= '2.7' and python_version != '3.3.*'", "version": "==0.7.1" }, "pockets": { @@ -478,7 +495,7 @@ "sha256:06a30435d058473046be836d3fc4f27167fd84c45b99704f2fb5509ef61f9af1", "sha256:50402e9d1c9005d759426988a492e0edaadb7f4e68bcddfea586bc7432d009c6" ], - "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*'", + "markers": "python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.0.*' and python_version >= '2.7' and python_version != '3.3.*'", "version": "==1.6.0" }, "pycodestyle": { @@ -512,10 +529,11 @@ }, "pyparsing": { "hashes": [ - "sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04", - "sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010" + "sha256:905d8090c335314568b5faee0025b1829f27bb974604a5762a6cdef3a7dfc3b7", + "sha256:f493ee323be1e94929416b3585eefcc04943115cecbaaa35a8c86d1a2368af19" ], - "version": "==2.2.0" + "markers": "python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.6'", + "version": "==2.2.1" }, "pytest": { "hashes": [ @@ -548,6 +566,13 @@ "index": "pypi", "version": "==2.19.1" }, + "requests-toolbelt": { + "hashes": [ + "sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237", + "sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5" + ], + "version": "==0.8.0" + }, "six": { "hashes": [ "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", @@ -564,11 +589,11 @@ }, "sphinx": { "hashes": [ - "sha256:217a7705adcb573da5bbe1e0f5cab4fa0bd89fd9342c9159121746f593c2d5a4", - "sha256:a602513f385f1d5785ff1ca420d9c7eb1a1b63381733b2f0ea8188a391314a86" + "sha256:95acd6648902333647a0e0564abdb28a74b0a76d2333148aa35e5ed1f56d3c4b", + "sha256:c091dbdd5cc5aac6eb95d591a819fd18bccec90ffb048ec465b165a48b839b45" ], "index": "pypi", - "version": "==1.7.9" + "version": "==1.8.0" }, "sphinx-rtd-theme": { "hashes": [ @@ -591,16 +616,39 @@ "sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd", "sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9" ], - "markers": "python_version != '3.1.*' and python_version != '3.2.*' and python_version != '3.3.*' and python_version != '3.0.*' and python_version >= '2.7'", + "markers": "python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.0.*' and python_version >= '2.7' and python_version != '3.3.*'", "version": "==1.1.0" }, + "toml": { + "hashes": [ + "sha256:380178cde50a6a79f9d2cf6f42a62a5174febe5eea4126fe4038785f1d888d42", + "sha256:a7901919d3e4f92ffba7ff40a9d697e35bbbc8a8049fe8da742f34c83606d957" + ], + "version": "==0.9.6" + }, "tox": { "hashes": [ - "sha256:37cf240781b662fb790710c6998527e65ca6851eace84d1595ee71f7af4e85f7", - "sha256:eb61aa5bcce65325538686f09848f04ef679b5cd9b83cc491272099b28739600" + "sha256:433bb93c57edae263150767e672a0d468ab4fefcc1958eb4013e56a670bb851e", + "sha256:bfb4e4efb7c61a54bc010a5c00fdbe0973bc4bdf04090bfcd3c93c901006177c" ], "index": "pypi", - "version": "==3.2.1" + "version": "==3.3.0" + }, + "tqdm": { + "hashes": [ + "sha256:18f1818ce951aeb9ea162ae1098b43f583f7d057b34d706f66939353d1208889", + "sha256:df02c0650160986bac0218bb07952245fc6960d23654648b5d5526ad5a4128c9" + ], + "markers": "python_version != '3.1.*' and python_version >= '2.6' and python_version != '3.0.*'", + "version": "==4.26.0" + }, + "twine": { + "hashes": [ + "sha256:08eb132bbaec40c6d25b358f546ec1dc96ebd2638a86eea68769d9e67fe2b129", + "sha256:2fd9a4d9ff0bcacf41fdc40c8cb0cfaef1f1859457c9653fd1b92237cc4e9f25" + ], + "index": "pypi", + "version": "==1.11.0" }, "typed-ast": { "hashes": [ @@ -636,7 +684,7 @@ "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" ], - "markers": "python_version != '3.3.*' and python_version < '4' and python_version != '3.0.*' and python_version != '3.2.*' and python_version >= '2.6' and python_version != '3.1.*'", + "markers": "python_version != '3.0.*' and python_version >= '2.6' and python_version != '3.2.*' and python_version != '3.3.*' and python_version < '4' and python_version != '3.1.*'", "version": "==1.23" }, "virtualenv": { @@ -644,7 +692,7 @@ "sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669", "sha256:ca07b4c0b54e14a91af9f34d0919790b016923d157afda5efdde55c96718f752" ], - "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*'", + "markers": "python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7'", "version": "==16.0.0" }, "wheel": { diff --git a/labelbox/__init__.py b/labelbox/__init__.py index 6f28194a6..db6688587 100644 --- a/labelbox/__init__.py +++ b/labelbox/__init__.py @@ -1,3 +1,3 @@ "The Labelbox python package." -__version__ = '0.0.4' +__version__ = '0.0.5' diff --git a/labelbox/lbx.py b/labelbox/lbx.py new file mode 100644 index 000000000..69d3220ff --- /dev/null +++ b/labelbox/lbx.py @@ -0,0 +1,143 @@ +"""Module for interacting with the LBX file format. + +LBX File Format - lossless compression for segmented images + +HEADERS +bytes 0-3 -> LBX version +bytes 4-7 -> pixel width +bytes 8-11 -> pixel height +bytes 12-15 -> decompressed bytes +bytes 16-19 -> # colors +bytes 20-23 -> # blocks + +BODY +- COLORS - +- BLOCKS - + +COLOR +byte 0 -> red +byte 1 -> blue +byte 2 -> green +byte 3 -> alpha (always 255) + +BLOCK +bytes 0-1 -> R | G value of pixel (superpixel layer #) +bytes 2-3 -> # consecutive occurences +""" +from io import BytesIO +import itertools +import logging +import struct +from typing import List + +import numpy as np +from PIL import Image + +# NOTE: image-segmentation front-end requires background pixels to be white to +# render them transparent +BACKGROUND_RGBA = np.array([255, 255, 255, 255], dtype=np.uint8) +BACKGROUND_RGBA.flags.writeable = False + +_HEADER_LENGTH = 6 * 4 + + +def encode(image_in: Image, colormap: List[np.array]) -> BytesIO: + """Converts a RGB `Image` representing a segmentation map into the LBX data format. + + Given a segmentation map representing an image, convert it to the LBX format. + Background pixels should be represented using the `BACKGROUND_RGBA` RGBA value. + + Args: + image_in: The image to encode. + colormap: Ordered list of `np.array`s each of length 3 representing a + RGB color. Do not include `BACKGROUND_RGBA`; it will be automatically + accounted for. Every pixel in `image_in` must match some entry in this + list. The ordering of this list determines which colors map to which + class labels in the project ontology. + + Returns: + The LBX encoded bytes. + """ + image = image_in.convert('RGBA') + pixel_words = np.array(image).reshape(-1, 4) + pixel_words.flags.writeable = False + + colormap = list(map(lambda color: np.append(color, 255).astype(np.uint8), colormap)) + + input_byte_len = len(np.array(image).flat) + buff = BytesIO(bytes([0] * (len(colormap) * 4 + input_byte_len))) + + offset = _HEADER_LENGTH # make room for header + + def _color_to_key(color): + return hash(color.tostring()) + + color_dict = dict() + color_dict[_color_to_key(BACKGROUND_RGBA)] = 0 # manually add bg + for i, color in enumerate(colormap): + color.flags.writeable = False + struct.pack_into(' 65534: + try: + struct.pack_into(' Image: + """Decodes LBX encoded byte data into an image. + + Args: + lbx: A byte buffer containing the LBX encoded image data. + + Returns: + A RGBA image of the decoded data. + """ + version, width, height, byte_length, num_colors, num_blocks =\ + struct.unpack('<' + 'i'*int(_HEADER_LENGTH/4), lbx.read(_HEADER_LENGTH)) + assert version == 1, 'only LBX v1 format is supported' + + colormap = np.array( + [BACKGROUND_RGBA] + + list(_grouper(struct.unpack('<' + 'B'*4*num_colors, lbx.read(4 * num_colors)), 4))) + + image_data = np.zeros((width * height, 4), dtype='uint8') + offset = 0 + for _ in range(num_blocks): + layer, run_length = struct.unpack(' ABC DEF Gxx" + args = [iter(iterable)] * group_size + return itertools.zip_longest(*args, fillvalue=fillvalue) diff --git a/labelbox/predictions/__init__.py b/labelbox/predictions.py similarity index 100% rename from labelbox/predictions/__init__.py rename to labelbox/predictions.py diff --git a/tests/test_lbx.py b/tests/test_lbx.py new file mode 100644 index 000000000..a797896ea --- /dev/null +++ b/tests/test_lbx.py @@ -0,0 +1,49 @@ +from io import BytesIO + +import labelbox.lbx as lbx +import numpy as np +from PIL import Image +import pytest +import struct + + +@pytest.fixture +def im_png(datadir): + with open(datadir.join('sample.png'), 'rb') as f: + image = np.array(Image.open(BytesIO(f.read()))) + + # convert black to BG + image[np.apply_along_axis(np.all, 2, image[:, :, :3] == [0, 0, 0])] = [255, 255, 255, 255] + image = Image.fromarray(image) + yield image + + +@pytest.fixture +def lbx_sample(datadir): + with open(datadir.join('sample.lbx'), 'rb') as f: + yield f + + +def test_lbx_decode(lbx_sample): + im = lbx.decode(lbx_sample) + assert im.size == (500, 375) + + +def test_lbx_encode(im_png): + colormap = [ + np.array([0, 0, 128]), + np.array([0, 128, 0]), + ] + lbx_encoded = lbx.encode(im_png, colormap) + version, width, height = struct.unpack('