diff --git a/docs/source/how_do_i.rst b/docs/source/how_do_i.rst index 5fcee3a..85b9ac9 100644 --- a/docs/source/how_do_i.rst +++ b/docs/source/how_do_i.rst @@ -119,6 +119,54 @@ and add it after the JP2 header box, but before the codestream box :: . (truncated) . +Create an image with an alpha layer? +==================================== + +OpenJPEG can create JP2 files with more than 3 components, but by default, any +extra components are not described. In order to specify an alpha layer as such, +we need to rewrap such an image in a set of boxes that includes a channel +definition box. + +This example is based on SciPy example code found at +http://scipy-lectures.github.io/advanced/image_processing/#basic-manipulations . :: + + >>> import numpy as np + >>> import glymur + >>> from glymur import Jp2k + >>> rgb = Jp2k(glymur.data.goodstuff()).read() + >>> lx, ly = rgb.shape[0:2] + >>> X, Y = np.ogrid[0:lx, 0:ly] + >>> mask = ly**2*(X - lx / 2) ** 2 + lx**2*(Y - ly / 2) ** 2 > (lx * ly / 2)**2 + >>> alpha = 255 * np.ones((lx, ly, 1), dtype=np.uint8) + >>> alpha[mask] = 0 + >>> rgba = np.concatenate((rgb, alpha), axis=2) + >>> jp2 = Jp2k('tmp.jp2', 'wb') + >>> jp2.write(rgba) + +The first three channels are color channels, but the fourth will be identified +as an alpha channel:: + + >>> from glymur.core import COLOR, OPACITY + >>> ctype = [COLOR, COLOR, COLOR, OPACITY] + +And finally we have to specify just exactly how each channel is to be +interpreted. The color channels are straightforward, but the alpha channel +in this case is to be applied against the entire image (it is possible to +apply an alpha channel to a single color channel, but we aren't doing that). :: + + >>> from glymur.core import RED, GREEN, BLUE, WHOLE_IMAGE + >>> asoc = [RED, GREEN, BLUE, WHOLE_IMAGE] + >>> cdef = glymur.jp2box.ChannelDefinitionBox(channel_type=ctype, association=asoc) + +It's easiest to take the existing jp2 jacket and just add the channel +definition box in the appropriate spot. The channel definition box **must** +go into the jp2 header box, and then we can rewrap the image. :: + + >>> boxes = jp2.box # The box attribute is the list of JP2 boxes + >>> boxes[2].box.append(cdef) + >>> jp2_rgba = jp2.wrap("goodstuff_rgba.jp2", boxes=boxes) + + Work with XMP UUIDs? ==================== The example JP2 file shipped with glymur has an XMP UUID. :: diff --git a/glymur/core.py b/glymur/core.py index 3959dd0..22b5a19 100644 --- a/glymur/core.py +++ b/glymur/core.py @@ -43,14 +43,14 @@ _COLORSPACE_MAP_DISPLAY = { ROMM_RGB: 'ROMM-RGB'} # enumerated color channel types -_COLOR = 0 -_OPACITY = 1 -_PRE_MULTIPLIED_OPACITY = 2 +COLOR = 0 +OPACITY = 1 +PRE_MULTIPLIED_OPACITY = 2 _UNSPECIFIED = 65535 _COLOR_TYPE_MAP_DISPLAY = { - _COLOR: 'color', - _OPACITY: 'opacity', - _PRE_MULTIPLIED_OPACITY: 'pre-multiplied opacity', + COLOR: 'color', + OPACITY: 'opacity', + PRE_MULTIPLIED_OPACITY: 'pre-multiplied opacity', _UNSPECIFIED: 'unspecified'} # color channel definitions. @@ -58,6 +58,7 @@ RED = 1 GREEN = 2 BLUE = 3 GREY = 1 +WHOLE_IMAGE = 0 # enumerated color channel associations _COLORSPACE = {SRGB: {"R": 1, "G": 2, "B": 3}, diff --git a/glymur/jp2box.py b/glymur/jp2box.py index fe9ee00..39f2f16 100644 --- a/glymur/jp2box.py +++ b/glymur/jp2box.py @@ -426,14 +426,23 @@ class ChannelDefinitionBox(Jp2kBox): longname : str more verbose description of the box. index : int - number of the channel + number of the channel. Defaults to monotonically increasing sequence, + i.e. [0, 1, 2, ...] channel_type : int type of the channel association : int index of the associated color """ - def __init__(self, index, channel_type, association, **kwargs): + def __init__(self, index=None, channel_type=None, association=None, **kwargs): Jp2kBox.__init__(self, box_id='cdef', longname='Channel Definition') + + # channel type and association must be specified. + if channel_type is None or association is None: + raise IOError("channel_type and association must be specified.") + + if index is None: + index = list(range(len(channel_type))) + if len(index) != len(channel_type) or len(index) != len(association): msg = "Length of channel definition box inputs must be the same." raise IOError(msg) @@ -504,8 +513,9 @@ class ChannelDefinitionBox(Jp2kBox): channel_type = data[1:num_components * 6:3] association = data[2:num_components * 6:3] - box = ChannelDefinitionBox(index, channel_type, association, - length=length, offset=offset) + box = ChannelDefinitionBox(index=index, channel_type=channel_type, + association=association, length=length, + offset=offset) return box diff --git a/glymur/test/test_jp2box.py b/glymur/test/test_jp2box.py index 72c1f1e..deea9d3 100644 --- a/glymur/test/test_jp2box.py +++ b/glymur/test/test_jp2box.py @@ -16,6 +16,8 @@ import pkg_resources import glymur from glymur import Jp2k from glymur.jp2box import * +from glymur.core import COLOR, OPACITY +from glymur.core import RED, GREEN, BLUE, GREY, WHOLE_IMAGE # Doc tests should be run as well. @@ -84,12 +86,19 @@ class TestChannelDefinition(unittest.TestCase): def tearDown(self): pass - def test_rgb(self): + def test_cdef_no_inputs(self): + """channel_type and association are required inputs.""" + with self.assertRaises(IOError): + glymur.jp2box.ChannelDefinitionBox() + + def test_rgb_with_index(self): """Just regular RGB.""" j2k = Jp2k(self.j2kfile) + channel_type = [COLOR, COLOR, COLOR] + association = [RED, GREEN, BLUE] cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2], - channel_type=[0, 0, 0], - association=[1, 2, 3]) + channel_type=channel_type, + association=association) boxes = [self.ihdr, self.colr_rgb, cdef] self.jp2h.box = boxes boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] @@ -101,15 +110,41 @@ class TestChannelDefinition(unittest.TestCase): boxes = [box.box_id for box in jp2h.box] self.assertEqual(boxes, ['ihdr', 'colr', 'cdef']) self.assertEqual(jp2h.box[2].index, (0, 1, 2)) - self.assertEqual(jp2h.box[2].channel_type, (0, 0, 0)) - self.assertEqual(jp2h.box[2].association, (1, 2, 3)) + self.assertEqual(jp2h.box[2].channel_type, + (COLOR, COLOR, COLOR)) + self.assertEqual(jp2h.box[2].association, + (RED, GREEN, BLUE)) + + def test_rgb(self): + """Just regular RGB, but don't supply the optional index.""" + j2k = Jp2k(self.j2kfile) + channel_type = [COLOR, COLOR, COLOR] + association = [RED, GREEN, BLUE] + cdef = glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, + association=association) + boxes = [self.ihdr, self.colr_rgb, cdef] + self.jp2h.box = boxes + boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] + with tempfile.NamedTemporaryFile(suffix=".jp2") as tfile: + j2k.wrap(tfile.name, boxes=boxes) + + jp2 = Jp2k(tfile.name) + jp2h = jp2.box[2] + boxes = [box.box_id for box in jp2h.box] + self.assertEqual(boxes, ['ihdr', 'colr', 'cdef']) + self.assertEqual(jp2h.box[2].index, (0, 1, 2)) + self.assertEqual(jp2h.box[2].channel_type, + (COLOR, COLOR, COLOR)) + self.assertEqual(jp2h.box[2].association, + (RED, GREEN, BLUE)) def test_rgba(self): """Just regular RGBA.""" j2k = Jp2k(self.four_planes) - cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2, 3], - channel_type=[0, 0, 0, 1], - association=[1, 2, 3, 0]) + channel_type = (COLOR, COLOR, COLOR, OPACITY) + association = (RED, GREEN, BLUE, WHOLE_IMAGE) + cdef = glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, + association=association) boxes = [self.ihdr, self.colr_rgb, cdef] self.jp2h.box = boxes boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] @@ -121,15 +156,16 @@ class TestChannelDefinition(unittest.TestCase): boxes = [box.box_id for box in jp2h.box] self.assertEqual(boxes, ['ihdr', 'colr', 'cdef']) self.assertEqual(jp2h.box[2].index, (0, 1, 2, 3)) - self.assertEqual(jp2h.box[2].channel_type, (0, 0, 0, 1)) - self.assertEqual(jp2h.box[2].association, (1, 2, 3, 0)) + self.assertEqual(jp2h.box[2].channel_type, channel_type) + self.assertEqual(jp2h.box[2].association, association) def test_bad_rgba(self): """R, G, and B must be specified.""" j2k = Jp2k(self.four_planes) - cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2, 3], - channel_type=[0, 0, 1, 1], - association=[1, 2, 3, 0]) + channel_type = (COLOR, COLOR, OPACITY, OPACITY) + association = (RED, GREEN, BLUE, WHOLE_IMAGE) + cdef = glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, + association=association) boxes = [self.ihdr, self.colr_rgb, cdef] self.jp2h.box = boxes boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] @@ -140,9 +176,10 @@ class TestChannelDefinition(unittest.TestCase): def test_grey(self): """Just regular greyscale.""" j2k = Jp2k(self.one_plane) - cdef = glymur.jp2box.ChannelDefinitionBox(index=[0], - channel_type=[0], - association=[1]) + channel_type = (COLOR,) + association = (GREY,) + cdef = glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, + association=association) boxes = [self.ihdr, self.colr_gr, cdef] self.jp2h.box = boxes boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] @@ -154,15 +191,16 @@ class TestChannelDefinition(unittest.TestCase): boxes = [box.box_id for box in jp2h.box] self.assertEqual(boxes, ['ihdr', 'colr', 'cdef']) self.assertEqual(jp2h.box[2].index, (0,)) - self.assertEqual(jp2h.box[2].channel_type, (0,)) - self.assertEqual(jp2h.box[2].association, (1,)) + self.assertEqual(jp2h.box[2].channel_type, channel_type) + self.assertEqual(jp2h.box[2].association, association) def test_grey_alpha(self): """Just regular greyscale plus alpha.""" j2k = Jp2k(self.two_planes) - cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1], - channel_type=[0, 1], - association=[1, 0]) + channel_type = (COLOR, OPACITY) + association = (GREY, WHOLE_IMAGE) + cdef = glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, + association=association) boxes = [self.ihdr, self.colr_gr, cdef] self.jp2h.box = boxes boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] @@ -174,17 +212,19 @@ class TestChannelDefinition(unittest.TestCase): boxes = [box.box_id for box in jp2h.box] self.assertEqual(boxes, ['ihdr', 'colr', 'cdef']) self.assertEqual(jp2h.box[2].index, (0, 1)) - self.assertEqual(jp2h.box[2].channel_type, (0, 1)) - self.assertEqual(jp2h.box[2].association, (1, 0)) + self.assertEqual(jp2h.box[2].channel_type, channel_type) + self.assertEqual(jp2h.box[2].association, association) def test_bad_grey_alpha(self): - """A greyscale image with alpha layer must specify Y""" + """A greyscale image with alpha layer must specify a color channel""" j2k = Jp2k(self.two_planes) + channel_type = (OPACITY, OPACITY) + association = (GREY, WHOLE_IMAGE) + # This cdef box - cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1], - channel_type=[1, 1], - association=[0, 1]) + cdef = glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, + association=association) boxes = [self.ihdr, self.colr_gr, cdef] self.jp2h.box = boxes boxes = [self.jP, self.ftyp, self.jp2h, self.jp2c] @@ -196,9 +236,10 @@ class TestChannelDefinition(unittest.TestCase): """There can only be one channel definition box in the jp2 header.""" j2k = Jp2k(self.j2kfile) - cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2], - channel_type=[0, 0, 0], - association=[1, 2, 3]) + channel_type = (COLOR, COLOR, COLOR) + association = (RED, GREEN, BLUE) + cdef = glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, + association=association) boxes = [self.ihdr, cdef, self.colr_rgb, cdef] self.jp2h.box = boxes @@ -214,9 +255,10 @@ class TestChannelDefinition(unittest.TestCase): boxes = [self.ihdr, self.colr_rgb] self.jp2h.box = boxes - cdef = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2], - channel_type=[0, 0, 0], - association=[1, 2, 3]) + channel_type = (COLOR, COLOR, COLOR) + association = (RED, GREEN, BLUE) + cdef = glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, + association=association) boxes = [self.jP, self.ftyp, self.jp2h, cdef, self.jp2c] @@ -228,18 +270,20 @@ class TestChannelDefinition(unittest.TestCase): # Channel types are limited to 0, 1, 2, 65535 # Should reject if not all of index, channel_type, association the # same length. + channel_type = (COLOR, COLOR, 3) + association = (RED, GREEN, BLUE) with self.assertRaises(IOError): - box = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2], - channel_type=[0, 0, 3], - association=[1, 2, 3]) + box = glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, + association=association) def test_wrong_lengths(self): # Should reject if not all of index, channel_type, association the # same length. + channel_type = (COLOR, COLOR) + association = (RED, GREEN, BLUE) with self.assertRaises(IOError): - box = glymur.jp2box.ChannelDefinitionBox(index=[0, 1, 2], - channel_type=[0, 0], - association=[1, 2, 3]) + box = glymur.jp2box.ChannelDefinitionBox(channel_type=channel_type, + association=association) @unittest.skipIf(os.name == "nt", "Temporary file issue on window.")