3333import numpy as np
3434from PIL import Image
3535
36+ # NOTE: image-segmentation front-end requires background pixels to be white to
37+ # render them transparent
3638BACKGROUND_RGBA = np .array ([255 , 255 , 255 , 255 ], dtype = np .uint8 )
3739BACKGROUND_RGBA .flags .writeable = False
40+
3841_HEADER_LENGTH = 6 * 4
3942
4043
41- def encode (image_in : Image , colormap : List [np .array ]):
42- """Converts a RGB `Image` to a `io.BytesIO` with LBX encoded data.
44+ def encode (image_in : Image , colormap : List [np .array ]) -> BytesIO :
45+ """Converts a RGB `Image` representing a segmentation map into the LBX data format.
46+
47+ Given a segmentation map representing an image, convert it to the LBX format.
48+ Background pixels should be represented using the `BACKGROUND_RGBA` RGBA value.
4349
4450 Args:
4551 image_in: The image to encode.
46- colormap: Ordered list of `np.array`s each of length 3 representing
47- a RGB color. The ordering of this list determines which colors
48- map to which class labels in the project ontology.
52+ colormap: Ordered list of `np.array`s each of length 3 representing a
53+ RGB color. Do not include `BACKGROUND_RGBA`; it will be automatically
54+ accounted for. Every pixel in `image_in` must match some entry in this
55+ list. The ordering of this list determines which colors map to which
56+ class labels in the project ontology.
4957
5058 Returns:
51- A `io.BytesIO` containing the LBX encoded image .
59+ The LBX encoded bytes .
5260 """
5361 image = image_in .convert ('RGBA' )
5462 pixel_words = np .array (image ).reshape (- 1 , 4 )
5563 pixel_words .flags .writeable = False
5664
57- colormap = [BACKGROUND_RGBA ] + \
58- list (map (lambda color : np .append (color , 255 ).astype (np .uint8 ), colormap ))
65+ colormap = list (map (lambda color : np .append (color , 255 ).astype (np .uint8 ), colormap ))
5966
6067 input_byte_len = len (np .array (image ).flat )
6168 buff = BytesIO (bytes ([0 ] * (len (colormap ) * 4 + input_byte_len )))
@@ -66,12 +73,12 @@ def _color_to_key(color):
6673 return hash (color .tostring ())
6774
6875 color_dict = dict ()
76+ color_dict [_color_to_key (BACKGROUND_RGBA )] = 0 # manually add bg
6977 for i , color in enumerate (colormap ):
7078 color .flags .writeable = False
7179 struct .pack_into ('<BBBB' , buff .getbuffer (), offset , * color )
72- color_dict [_color_to_key (color )] = i
80+ color_dict [_color_to_key (color )] = i + 1
7381 offset += 4
74- # color_dict[_color_to_key(BACKGROUND_RGBA)] = len(colormap)
7582
7683 count = 0
7784 pixel_ints = np .apply_along_axis (_color_to_key , 1 , pixel_words )
@@ -84,9 +91,11 @@ def _color_to_key(color):
8491 struct .pack_into ('<HH' , buff .getbuffer (), offset , color_dict [pixel_int ], count )
8592 offset += 4
8693 count = 0
87- except KeyError as e :
88- logging .error ('Could not find color {} in colormap' .format (pixel_words [i ]))
89- raise e
94+ except KeyError as exc :
95+ logging .error ('Could not find color %s in colormap' , pixel_words [i ])
96+ if np .all (pixel_words [i ] == np .array ([0 , 0 , 0 , 0 ])):
97+ logging .error ('Did you remember to set background pixels to `BACKGROUND_RGBA`?' )
98+ raise exc
9099
91100 # write header
92101 struct .pack_into (
@@ -97,35 +106,31 @@ def _color_to_key(color):
97106 return buff
98107
99108
100- def decode (lbx : BytesIO ):
101- """Decodes a `BytesIO` with LBX encoded data into a `PIL.Image` .
109+ def decode (lbx : BytesIO ) -> Image :
110+ """Decodes LBX encoded byte data into an image .
102111
103112 Args:
104113 lbx: A byte buffer containing the LBX encoded image data.
105114
106115 Returns:
107- A `PIL.Image` of the decoded data.
116+ A RGBA image of the decoded data.
108117 """
109- version , width , height , byte_length , num_colors , num_blocks = \
110- map ( lambda x : x [ 0 ], struct .iter_unpack ('<i' , lbx .read (_HEADER_LENGTH ) ))
111- assert version == 1 , 'this method only supports LBX v1 format'
118+ version , width , height , byte_length , num_colors , num_blocks = \
119+ struct .unpack ('<' + 'i' * int ( _HEADER_LENGTH / 4 ) , lbx .read (_HEADER_LENGTH ))
120+ assert version == 1 , 'only LBX v1 format is supported '
112121
113122 colormap = np .array (
114- list (_grouper (
115- map (lambda x : x [0 ],
116- struct .iter_unpack ('<B' , lbx .read (4 * num_colors ))),
117- 4 )) +
118- [BACKGROUND_RGBA ])
123+ [BACKGROUND_RGBA ] +
124+ list (_grouper (struct .unpack ('<' + 'B' * 4 * num_colors , lbx .read (4 * num_colors )), 4 )))
119125
120126 image_data = np .zeros ((width * height , 4 ), dtype = 'uint8' )
121127 offset = 0
122128 for _ in range (num_blocks ):
123- layer = struct .unpack ('<H' , lbx .read (2 ))[0 ]
124- run_length = struct .unpack ('<H' , lbx .read (2 ))[0 ]
129+ layer , run_length = struct .unpack ('<HH' , lbx .read (4 ))
125130 image_data [offset :offset + run_length , :] = colormap [layer ]
126131 offset += run_length
127132 assert 4 * offset == byte_length , \
128- 'number of bytes read does not equal numBytes in header'
133+ 'number of bytes read does not equal num bytes specified in header'
129134
130135 reshaped_image = np .reshape (image_data , (height , width , 4 ))
131136 return Image .fromarray (reshaped_image , mode = 'RGBA' )
0 commit comments