From 6e1637e470b245a2bbf3b74d2dc95c49359e4838 Mon Sep 17 00:00:00 2001 From: John Evans Date: Wed, 19 Nov 2014 14:25:44 -0500 Subject: [PATCH] starting on incompatible changes to writing --- glymur/jp2k.py | 112 +++++++++++++++++++++------- glymur/test/test_jp2k.py | 103 +++++++++++-------------- glymur/test/test_opj_suite_write.py | 3 +- 3 files changed, 128 insertions(+), 90 deletions(-) diff --git a/glymur/jp2k.py b/glymur/jp2k.py index 9e2e120..2126d89 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -37,16 +37,6 @@ from .jp2box import ( ) from .lib import openjpeg as opj, openjp2 as opj2, c as libc -JP2_IDS = ['colr', 'cdef', 'cmap', 'jp2c', 'ftyp', 'ihdr', 'jp2h', 'jP ', - 'pclr', 'res ', 'resc', 'resd', 'xml ', 'ulst', 'uinf', 'url ', - 'uuid'] -JPX_IDS = ['asoc', 'nlst'] - -_COLORSPACE_MAP = {'rgb': opj2.CLRSPC_SRGB, - 'gray': opj2.CLRSPC_GRAY, - 'grey': opj2.CLRSPC_GRAY, - 'ycc': opj2.CLRSPC_YCC} - class Jp2k(Jp2kBox): """JPEG 2000 file. @@ -54,8 +44,6 @@ class Jp2k(Jp2kBox): ---------- filename : str The path to the JPEG 2000 file. - mode : str - The mode used to open the file. box : sequence List of top-level boxes in the file. Each box may in turn contain its own list of boxes. Will be empty if the file consists only of a @@ -92,18 +80,16 @@ class Jp2k(Jp2kBox): (728, 1296, 3) """ - def __init__(self, filename, mode='rb'): + def __init__(self, filename, data=None, shape=None, **kwargs): """ Parameters ---------- filename : str or file The path to JPEG 2000 file. - mode : str, optional - The mode used to open the file. """ Jp2kBox.__init__(self) self.filename = filename - self.mode = mode + self.box = [] self._codec_format = None self._colorspace = None @@ -114,8 +100,10 @@ class Jp2k(Jp2kBox): self._verbose = False # Parse the file for JP2/JPX contents only if we are reading it. - if mode == 'rb': + if data is None and shape is None: self.parse() + else: + self._write(data, **kwargs) @property def ignore_pclr_cmap_cdef(self): @@ -433,6 +421,77 @@ class Jp2k(Jp2kBox): self._cparams = cparams + def _write(self, img_array, verbose=False, **kwargs): + """Write image data to a JP2/JPX/J2k file. Intended usage of the + various parameters follows that of OpenJPEG's opj_compress utility. + + This method can only be used to create JPEG 2000 images that can fit + in memory. + + Parameters + ---------- + img_array : ndarray + Image data to be written to file. + cbsize : tuple, optional + Code block size (DY, DX). + cinema2k : int, optional + frames per second, either 24 or 48 + cinema4k : bool, optional + Set to True to specify Cinema4K mode, defaults to false. + colorspace : str, optional + Either 'rgb' or 'gray'. + cratios : iterable + Compression ratios for successive layers. + eph : bool, optional + If true, write SOP marker after each header packet. + grid_offset : tuple, optional + Offset (DY, DX) of the origin of the image in the reference grid. + irreversible : bool, optional + If true, use the irreversible DWT 9-7 transform. + mct : bool, optional + Specifies usage of the multi component transform. If not + specified, defaults to True if the colorspace is RGB. + modesw : int, optional + Mode switch. + 1 = BYPASS(LAZY) + 2 = RESET + 4 = RESTART(TERMALL) + 8 = VSC + 16 = ERTERM(SEGTERM) + 32 = SEGMARK(SEGSYM) + numres : int, optional + Number of resolutions. + prog : str, optional + Progression order, one of "LRCP" "RLCP", "RPCL", "PCRL", "CPRL". + psnr : iterable, optional + Different PSNR for successive layers. + psizes : list, optional + List of precinct sizes. Each precinct size tuple is defined in + (height x width). + sop : bool, optional + If true, write SOP marker before each packet. + subsam : tuple, optional + Subsampling factors (dy, dx). + tilesize : tuple, optional + Numeric tuple specifying tile size in terms of (numrows, numcols), + not (X, Y). + verbose : bool, optional + Print informational messages produced by the OpenJPEG library. + + """ + if re.match("1.[0-4]", version.openjpeg_version) is not None: + raise RuntimeError("You must have at least version 1.5 of OpenJPEG " + "in order to write images.") + + self._shape = img_array.shape + self._determine_colorspace(**kwargs) + self._populate_cparams(img_array, **kwargs) + + if opj2.OPENJP2 is not None: + self._write_openjp2(img_array, verbose=verbose) + else: + self._write_openjpeg(img_array, verbose=verbose) + def write(self, img_array, verbose=False, **kwargs): """Write image data to a JP2/JPX/J2k file. Intended usage of the various parameters follows that of OpenJPEG's opj_compress utility. @@ -490,16 +549,6 @@ class Jp2k(Jp2kBox): verbose : bool, optional Print informational messages produced by the OpenJPEG library. - Examples - -------- - >>> import glymur - >>> jfile = glymur.data.nemo() - >>> jp2 = glymur.Jp2k(jfile) - >>> data = jp2.read(rlevel=1) - >>> from tempfile import NamedTemporaryFile - >>> tfile = NamedTemporaryFile(suffix='.jp2', delete=False) - >>> j = Jp2k(tfile.name, mode='wb') - >>> j.write(data.astype(np.uint8)) """ if re.match("1.[0-4]", version.openjpeg_version) is not None: raise RuntimeError("You must have at least version 1.5 of OpenJPEG " @@ -679,7 +728,12 @@ class Jp2k(Jp2kBox): # Turn the colorspace from a string to the enumerated value that # the library expects. - self._colorspace = _COLORSPACE_MAP[colorspace.lower()] + COLORSPACE_MAP = {'rgb': opj2.CLRSPC_SRGB, + 'gray': opj2.CLRSPC_GRAY, + 'grey': opj2.CLRSPC_GRAY, + 'ycc': opj2.CLRSPC_YCC} + + self._colorspace = COLORSPACE_MAP[colorspace.lower()] def _write_openjp2(self, img_array, verbose=False): @@ -1780,6 +1834,8 @@ def _validate_singletons(boxes): if 'dtbl' in multiples: raise IOError('There can only be one dtbl box in a file.') +JPX_IDS = ['asoc', 'nlst'] + def _validate_jpx_brand(boxes, brand): """ If there is a JPX box then the brand must be 'jpx '. diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index 5158520..257688f 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -81,6 +81,7 @@ class SliceProtocolBase(unittest.TestCase): @unittest.skipIf(os.name == "nt", fixtures.WINDOWS_TMP_FILE_MSG) class TestSliceProtocolBaseWrite(SliceProtocolBase): + @unittest.skip('requires shape implementation') def test_write_ellipsis(self): expected = self.j2k_data @@ -95,12 +96,12 @@ class TestSliceProtocolBaseWrite(SliceProtocolBase): expected = self.j2k_data with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: - j = Jp2k(tfile.name, 'wb') - j[:] = self.j2k_data + j = Jp2k(tfile.name, data=self.j2k_data) actual = j[:] np.testing.assert_array_equal(actual, expected) + @unittest.skip('requires shape implementation') def test_cannot_write_with_non_default_single_slice(self): with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: j = Jp2k(tfile.name, 'wb') @@ -113,30 +114,35 @@ class TestSliceProtocolBaseWrite(SliceProtocolBase): with self.assertRaises(TypeError): j[slice(0, 640)] = self.j2k_data + @unittest.skip('requires shape implementation') def test_cannot_write_a_row(self): with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: j = Jp2k(tfile.name, 'wb') with self.assertRaises(TypeError): j[5] = self.j2k_data + @unittest.skip('requires shape implementation') def test_cannot_write_a_pixel(self): with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: j = Jp2k(tfile.name, 'wb') with self.assertRaises(TypeError): j[25, 35] = self.j2k_data[25, 35] + @unittest.skip('requires shape implementation') def test_cannot_write_a_column(self): with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: j = Jp2k(tfile.name, 'wb') with self.assertRaises(TypeError): j[:, 25, :] = self.j2k_data[:, :25, :] + @unittest.skip('requires shape implementation') def test_cannot_write_a_band(self): with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: j = Jp2k(tfile.name, 'wb') with self.assertRaises(TypeError): j[:, :, 0] = self.j2k_data[:, :, 0] + @unittest.skip('requires shape implementation') def test_cannot_write_a_subarray(self): with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: j = Jp2k(tfile.name, 'wb') @@ -297,8 +303,7 @@ class TestJp2k(unittest.TestCase): j = Jp2k(self.jp2file) expdata = j[:] with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: - j2 = Jp2k(tfile.name, 'wb') - j2.write(expdata, irreversible=True) + j2 = Jp2k(tfile.name, data=expdata, irreversible=True) codestream = j2.get_codestream() self.assertEqual(codestream.segment[2].spcod[8], @@ -337,7 +342,6 @@ class TestJp2k(unittest.TestCase): newjp2 = eval(repr(j)) self.assertEqual(newjp2.filename, self.j2kfile) - self.assertEqual(newjp2.mode, 'rb') self.assertEqual(len(newjp2.box), 0) @unittest.skip("see issue 302") @@ -664,32 +668,30 @@ class TestJp2k_write(unittest.TestCase): data = np.zeros((640, 480), dtype=np.uint8) with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: with self.assertRaises(IOError): - j = Jp2k(tfile.name, 'wb') - j.write(data, cbsize=(16, 16), psizes=[(16, 16)]) + j = Jp2k(tfile.name, data=data, + cbsize=(16, 16), psizes=[(16, 16)]) def test_precinct_size_not_power_of_two(self): """must be power of two""" data = np.zeros((640, 480), dtype=np.uint8) with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: with self.assertRaises(IOError): - j = Jp2k(tfile.name, 'wb') - j.write(data, cbsize=(16, 16), psizes=[(48, 48)]) + j = Jp2k(tfile.name, data=data, + cbsize=(16, 16), psizes=[(48, 48)]) def test_unsupported_int32(self): """Should raise a runtime error if trying to write int32""" data = np.zeros((128, 128), dtype=np.int32) with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: with self.assertRaises(RuntimeError): - j = Jp2k(tfile.name, 'wb') - j.write(data) + j = Jp2k(tfile.name, data=data) def test_unsupported_uint32(self): """Should raise a runtime error if trying to write uint32""" data = np.zeros((128, 128), dtype=np.uint32) with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: with self.assertRaises(RuntimeError): - j = Jp2k(tfile.name, 'wb') - j.write(data) + j = Jp2k(tfile.name, data=data) def test_write_with_version_too_early(self): """Should raise a runtime error if trying to write with version 1.3""" @@ -699,8 +701,7 @@ class TestJp2k_write(unittest.TestCase): with patch('glymur.version.openjpeg_version', new=version): with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: with self.assertRaises(RuntimeError): - j = Jp2k(tfile.name, 'wb') - j.write(data) + j = Jp2k(tfile.name, data=data) def test_cblkh_different_than_width(self): """Verify that we can set a code block size where height does not equal @@ -708,11 +709,8 @@ class TestJp2k_write(unittest.TestCase): """ data = np.zeros((128, 128), dtype=np.uint8) with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: - j = Jp2k(tfile.name, 'wb') - # The code block dimensions are given as rows x columns. - j.write(data, cbsize=(16, 32)) - + j = Jp2k(tfile.name, data=data, cbsize=(16, 32)) codestream = j.get_codestream() # Code block size is reported as XY in the codestream. @@ -721,59 +719,55 @@ class TestJp2k_write(unittest.TestCase): def test_too_many_dimensions(self): """OpenJP2 only allows 2D or 3D images.""" with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: - j = Jp2k(tfile.name, 'wb') with self.assertRaises(IOError): - data = np.zeros((128, 128, 2, 2), dtype=np.uint8) - j.write(data) + j = Jp2k(tfile.name, + data=np.zeros((128, 128, 2, 2), dtype=np.uint8)) def test_2d_rgb(self): """RGB must have at least 3 components.""" with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - j = Jp2k(tfile.name, 'wb') with self.assertRaises(IOError): - data = np.zeros((128, 128, 2), dtype=np.uint8) - j.write(data, colorspace='rgb') + j = Jp2k(tfile.name, + data=np.zeros((128, 128, 2), dtype=np.uint8), + colorspace='rgb') def test_colorspace_with_j2k(self): """Specifying a colorspace with J2K does not make sense""" with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: - j = Jp2k(tfile.name, 'wb') with self.assertRaises(IOError): - data = np.zeros((128, 128, 3), dtype=np.uint8) - j.write(data, colorspace='rgb') + j = Jp2k(tfile.name, + data=np.zeros((128, 128, 3), dtype=np.uint8), + colorspace='rgb') def test_specify_rgb(self): """specify RGB explicitly""" with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - j = Jp2k(tfile.name, 'wb') - data = np.zeros((128, 128, 3), dtype=np.uint8) - j.write(data, colorspace='rgb') + j = Jp2k(tfile.name, + data=np.zeros((128, 128, 3), dtype=np.uint8), + colorspace='rgb') self.assertEqual(j.box[2].box[1].colorspace, glymur.core.SRGB) def test_specify_gray(self): """test gray explicitly specified (that's GRAY, not GREY)""" with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - j = Jp2k(tfile.name, 'wb') data = np.zeros((128, 128), dtype=np.uint8) - j.write(data, colorspace='gray') + j = Jp2k(tfile.name, data=data, colorspace='gray') self.assertEqual(j.box[2].box[1].colorspace, glymur.core.GREYSCALE) def test_specify_grey(self): """test grey explicitly specified""" with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - j = Jp2k(tfile.name, 'wb') data = np.zeros((128, 128), dtype=np.uint8) - j.write(data, colorspace='grey') + j = Jp2k(tfile.name, data=data, colorspace='grey') self.assertEqual(j.box[2].box[1].colorspace, glymur.core.GREYSCALE) def test_grey_with_two_extra_comps(self): """should be able to write gray + two extra components""" with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - j = Jp2k(tfile.name, 'wb') data = np.zeros((128, 128, 3), dtype=np.uint8) - j.write(data, colorspace='gray') + j = Jp2k(tfile.name, data=data, colorspace='gray') self.assertEqual(j.box[2].box[0].height, 128) self.assertEqual(j.box[2].box[0].width, 128) self.assertEqual(j.box[2].box[0].num_components, 3) @@ -783,18 +777,16 @@ class TestJp2k_write(unittest.TestCase): def test_specify_ycc(self): """Should reject YCC""" with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - j = Jp2k(tfile.name, 'wb') with self.assertRaises(IOError): data = np.zeros((128, 128, 3), dtype=np.uint8) - j.write(data, colorspace='ycc') + j = Jp2k(tfile.name, data=data, colorspace='ycc') def test_write_with_jp2_in_caps(self): """should be able to write with JP2 suffix.""" j2k = Jp2k(self.j2kfile) expdata = j2k[:] with tempfile.NamedTemporaryFile(suffix='.JP2') as tfile: - ofile = Jp2k(tfile.name, 'wb') - ofile[:] = expdata + ofile = Jp2k(tfile.name, data=expdata) actdata = ofile[:] np.testing.assert_array_equal(actdata, expdata) @@ -803,8 +795,7 @@ class TestJp2k_write(unittest.TestCase): j2k = Jp2k(self.j2kfile) expdata = j2k[:] with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - ofile = Jp2k(tfile.name, 'wb') - ofile.write(expdata, mct=False) + ofile = Jp2k(tfile.name, data=expdata, mct=False) actdata = ofile[:] np.testing.assert_array_equal(actdata, expdata) @@ -816,9 +807,8 @@ class TestJp2k_write(unittest.TestCase): j2k = Jp2k(self.j2kfile) expdata = j2k[:] with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - ofile = Jp2k(tfile.name, 'wb') with self.assertRaises(IOError): - ofile.write(expdata[:, :, 0], mct=True) + ofile = Jp2k(tfile.name, data=expdata[:, :, 0], mct=True) def test_write_cprl(self): """Must be able to write a CPRL progression order file""" @@ -826,8 +816,7 @@ class TestJp2k_write(unittest.TestCase): j = Jp2k(self.jp2file) expdata = j[::2, ::2] with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - ofile = Jp2k(tfile.name, 'wb') - ofile.write(expdata, prog='CPRL') + ofile = Jp2k(tfile.name, data=expdata, prog='CPRL') actdata = ofile[:] np.testing.assert_array_equal(actdata, expdata) @@ -873,10 +862,9 @@ class Test_2p0_official(unittest.TestCase): """Can only write 4 components on 2.0+, should error out otherwise.""" with patch('glymur.version.openjpeg_version', new="2.0.0"): with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - j = Jp2k(tfile.name, 'wb') data = np.zeros((128, 128, 4), dtype=np.uint8) with self.assertRaises(IOError): - j.write(data) + Jp2k(tfile.name, data=data) @unittest.skipIf(glymur.version.openjpeg_version_tuple[0] < 2, @@ -908,10 +896,9 @@ class TestJp2k_2_0(unittest.TestCase): def test_unrecognized_jp2_clrspace(self): """We only allow RGB and GRAYSCALE. Should error out with others""" with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - j = Jp2k(tfile.name, 'wb') + data = np.zeros((128, 128, 3), dtype=np.uint8) with self.assertRaises(IOError): - data = np.zeros((128, 128, 3), dtype=np.uint8) - j.write(data, colorspace='cmyk') + j = Jp2k(tfile.name, data=data, colorspace='cmyk') @unittest.skipIf(os.name == "nt", fixtures.WINDOWS_TMP_FILE_MSG) def test_asoc_label_box(self): @@ -920,8 +907,7 @@ class TestJp2k_2_0(unittest.TestCase): # OpenJPEG doesn't have such a file. data = Jp2k(self.jp2file)[::2, ::2] with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - j = Jp2k(tfile.name, 'wb') - j.write(data) + j = Jp2k(tfile.name, data=data) with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile2: @@ -979,9 +965,8 @@ class TestJp2k_2_1(unittest.TestCase): def test_grey_with_extra_component(self): """version 2.0 cannot write gray + extra""" with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - j = Jp2k(tfile.name, 'wb') data = np.zeros((128, 128, 2), dtype=np.uint8) - j.write(data) + j = Jp2k(tfile.name, data=data) self.assertEqual(j.box[2].box[0].height, 128) self.assertEqual(j.box[2].box[0].width, 128) self.assertEqual(j.box[2].box[0].num_components, 2) @@ -992,9 +977,8 @@ class TestJp2k_2_1(unittest.TestCase): def test_rgb_with_extra_component(self): """v2.0+ should be able to write extra components""" with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: - j = Jp2k(tfile.name, 'wb') data = np.zeros((128, 128, 4), dtype=np.uint8) - j.write(data) + j = Jp2k(tfile.name, data=data) self.assertEqual(j.box[2].box[0].height, 128) self.assertEqual(j.box[2].box[0].width, 128) self.assertEqual(j.box[2].box[0].num_components, 4) @@ -1130,8 +1114,7 @@ class TestJp2kOpjDataRoot(unittest.TestCase): expdata = np.fromfile(filename, dtype=np.uint16) expdata.resize((2816, 2048)) with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: - j = Jp2k(tfile.name, 'wb') - j.write(expdata, irreversible=True) + j = Jp2k(tfile.name, data=expdata, irreversible=True) codestream = j.get_codestream() self.assertEqual(codestream.segment[2].spcod[8], diff --git a/glymur/test/test_opj_suite_write.py b/glymur/test/test_opj_suite_write.py index 8be1cef..b394d72 100644 --- a/glymur/test/test_opj_suite_write.py +++ b/glymur/test/test_opj_suite_write.py @@ -221,9 +221,8 @@ class TestNegative2pointzero(unittest.TestCase): for version in versions: with patch('glymur.version.openjpeg_version', new=version): with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: - j = Jp2k(tfile.name, 'wb') with self.assertRaises(IOError): - j.write(data, cinema2k=48) + j = Jp2k(tfile.name, data=data, cinema2k=48) @unittest.skipIf(re.match(r'''1.[0-4]''', openjpeg_version) is not None,