diff --git a/docs/source/whatsnew/0.8.rst b/docs/source/whatsnew/0.8.rst index f36b593..1f2c88b 100644 --- a/docs/source/whatsnew/0.8.rst +++ b/docs/source/whatsnew/0.8.rst @@ -7,4 +7,5 @@ Changes in 0.8.0 * Simplified writing images by moving data and options into the constructor. This is backwards-incompatible with 0.7.x. + * Tilesize parameter is now called tileshape. * Deprecated :py:meth:`read` method in favor of array-style slicing. diff --git a/glymur/jp2k.py b/glymur/jp2k.py index b1e5c60..e2764a1 100644 --- a/glymur/jp2k.py +++ b/glymur/jp2k.py @@ -80,7 +80,8 @@ class Jp2k(Jp2kBox): (728, 1296, 3) """ - def __init__(self, filename, data=None, shape=None, **kwargs): + def __init__(self, filename, data=None, shape=None, tileshape=None, + **kwargs): """ Only the filename parameter is required in order to read a JPEG 2000 file. @@ -93,6 +94,8 @@ class Jp2k(Jp2kBox): image data to be written shape : tuple size of image data, only required when image_data is not provided + tileshape : tuple, optional, default is same as shape + tile size cbsize : tuple, optional code block size (DY, DX) cinema2k : int, optional @@ -133,9 +136,6 @@ class Jp2k(Jp2kBox): 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 """ @@ -151,6 +151,11 @@ class Jp2k(Jp2kBox): else: self._shape = shape + if tileshape is not None: + self._tileshape = tileshape + else: + self._tileshape = shape + self._ignore_pclr_cmap_cdef = False self._verbose = False @@ -238,6 +243,89 @@ class Jp2k(Jp2kBox): metadata.append(str(codestream)) return '\n'.join(metadata) + def __enter__(self): + """ + """ + self._determine_colorspace() + self._populate_cparams() + + #self._cparams.cp_fixed_quality = 1 + #self._cparams.tcp_distoratio[0] = 20 + # set cp_fixed_quality = 1 ?? + + self._num_tiles_per_row = np.ceil(self.shape[1] / self._tileshape[1]) + + if len(self.shape) == 2: + num_comps = 1 + else: + num_comps = self.shape[2] + + # Populate comptparms + # Only two precisions are possible. + #if img_array.dtype == np.uint8: + comp_prec = 8 + #else: + #comp_prec = 16 + + if len(self.shape) == 2: + numrows, numcols = self.shape + numcops = 1 + else: + numrows, numcols, num_comps = self.shape + comptparms = (opj2.ImageComptParmType * num_comps)() + + for j in range(num_comps): + comptparms[j].dx = self._cparams.subsampling_dx + comptparms[j].dy = self._cparams.subsampling_dy + comptparms[j].w = numcols + comptparms[j].h = numrows + comptparms[j].x0 = self._cparams.image_offset_x0 + comptparms[j].y0 = self._cparams.image_offset_y0 + comptparms[j].prec = comp_prec + comptparms[j].bpp = comp_prec + comptparms[j].sgnd = 0 + self._comptparms = comptparms + + self._codec = opj2.create_compress(self._cparams.codec_fmt) + + num_tile_pixels = self._cparams.cp_tdx * self._cparams.cp_tdy + self._tile_size = num_tile_pixels * num_comps * comptparms[j].prec / 8 + + #self._image = opj2.image_tile_create(self._comptparms, self._colorspace) + self._image = opj2.image_create(self._comptparms, self._colorspace) + + self._image.contents.x0 = 0 + self._image.contents.y0 = 0 + self._image.contents.x1 = self.shape[1] + self._image.contents.y1 = self.shape[0] + + # Is this needed? + self._image.contents.color_space = self._colorspace + + opj2.setup_encoder(self._codec, self._cparams, self._image) + self._stream = opj2.stream_create_default_file_stream(self.filename, + False) + opj2.start_compress(self._codec, self._image, self._stream) + + #print('cparams') + #print(self._cparams) + #print('comptparms[0]') + #print(self._comptparms[0]) + #print('comptparms[1]') + #print(self._comptparms[1]) + #print('comptparms[2]') + #print(self._comptparms[2]) + #print('image') + #print(self._image[0]) + + return self + + def __exit__(self, *pargs): + opj2.end_compress(self._codec, self._stream) + opj2.stream_destroy(self._stream) + opj2.destroy_codec(self._codec) + opj2.image_destroy(self._image) + def parse(self): """Parses the JPEG 2000 file. @@ -350,7 +438,7 @@ class Jp2k(Jp2kBox): # 2.1 API self._cparams.rsiz = core.OPJ_PROFILE_CINEMA_4K - def _populate_cparams(self, img_array, **kwargs): + def _populate_cparams(self, **kwargs): """Directs processing of write method arguments. Parameters @@ -452,9 +540,9 @@ class Jp2k(Jp2kBox): cparams.subsampling_dy = kwargs['subsam'][0] cparams.subsampling_dx = kwargs['subsam'][1] - if 'tilesize' in kwargs: - cparams.cp_tdx = kwargs['tilesize'][1] - cparams.cp_tdy = kwargs['tilesize'][0] + if self._tileshape is not None: + cparams.cp_tdx = self._tileshape[1] + cparams.cp_tdy = self._tileshape[0] cparams.tile_size_on = opj2.TRUE try: @@ -472,7 +560,7 @@ class Jp2k(Jp2kBox): else: cparams.tcp_mct = 0 - self._validate_compression_params(img_array, cparams, **kwargs) + self._validate_compression_params(cparams, **kwargs) self._cparams = cparams @@ -488,8 +576,27 @@ class Jp2k(Jp2kBox): msg += "in order to write images." raise RuntimeError(msg) + # What would the point of 1D images be? + if img_array.ndim == 1 or img_array.ndim > 3: + msg = "{0}D imagery is not allowed.".format(img_array.ndim) + raise IOError(msg) + + if re.match("2.0.0", version.openjpeg_version) is not None: + if (((img_array.ndim != 2) and + (img_array.shape[2] != 1 and img_array.shape[2] != 3))): + msg = "Writing images is restricted to single-channel " + msg += "greyscale images or three-channel RGB images when " + msg += "the OpenJPEG library version is the official 2.0.0 " + msg += "release." + raise IOError(msg) + + if img_array.dtype != np.uint8 and img_array.dtype != np.uint16: + msg = "Only uint8 and uint16 datatypes are currently supported " + msg += "when writing." + raise RuntimeError(msg) + self._determine_colorspace(**kwargs) - self._populate_cparams(img_array, **kwargs) + self._populate_cparams(**kwargs) if opj2.OPENJP2 is not None: self._write_openjp2(img_array, verbose=verbose) @@ -563,13 +670,11 @@ class Jp2k(Jp2kBox): self.parse() - def _validate_compression_params(self, img_array, cparams, **kwargs): + def _validate_compression_params(self, cparams, **kwargs): """Check that the compression parameters are valid. Parameters ---------- - img_array : ndarray - Image data to be written to file. cparams : CompressionParametersType(ctypes.Structure) Corresponds to cparameters_t type in openjp2 headers. """ @@ -614,25 +719,6 @@ class Jp2k(Jp2kBox): msg += "must be powers of 2." raise IOError(msg.format(prch, prcw)) - # What would the point of 1D images be? - if img_array.ndim == 1 or img_array.ndim > 3: - msg = "{0}D imagery is not allowed.".format(img_array.ndim) - raise IOError(msg) - - if re.match("2.0.0", version.openjpeg_version) is not None: - if (((img_array.ndim != 2) and - (img_array.shape[2] != 1 and img_array.shape[2] != 3))): - msg = "Writing images is restricted to single-channel " - msg += "greyscale images or three-channel RGB images when " - msg += "the OpenJPEG library version is the official 2.0.0 " - msg += "release." - raise IOError(msg) - - if img_array.dtype != np.uint8 and img_array.dtype != np.uint16: - msg = "Only uint8 and uint16 datatypes are currently supported " - msg += "when writing." - raise RuntimeError(msg) - def _determine_colorspace(self, colorspace=None, **kwargs): """Determine the colorspace from the supplied inputs. @@ -900,6 +986,52 @@ class Jp2k(Jp2kBox): # # Should have a slice object where start = stop = step = None self._write(data) + + elif isinstance(index, tuple): + + # determine what tile number to write to + rows, cols = index + if rows.start is None: + tr1 = 0 + else: + tr1 = np.floor(rows.start / self._tileshape[0]) + if rows.stop is None: + tr2 = np.floor(self._shape[0] / self._tileshape[0]) + else: + tr2 = np.floor((rows.stop - 1) / self._tileshape[0]) + + if tr1 == tr2: + tile_row = tr1 + else: + msg = "Slice arguments cannot cross tile boundaries." + raise IOError(msg) + + if cols.start is None: + tc1 = 0 + else: + tc1 = np.floor(cols.start / self._tileshape[1]) + if rows.stop is None: + tc2 = np.floor(self._shape[1] / self._tileshape[1]) + else: + tc2 = np.floor((cols.stop - 1) / self._tileshape[1]) + + if tc1 == tc2: + tile_col = tc1 + else: + msg = "Slice arguments cannot cross tile boundaries." + raise IOError(msg) + + num_tile_pixels = self._cparams.cp_tdx * self._cparams.cp_tdy + num_comps = len(self._shape) + if data.dtype == np.uint8: + nbytes = num_tile_pixels * num_comps + else: + nbytes = num_tile_pixels * num_comps * 2 + + tile_no = tile_row * self._num_tiles_per_row + tile_col + opj2.write_tile(self._codec, tile_no, data, nbytes, self._stream) + return + else: msg = "Partial write operations are currently not allowed." raise TypeError(msg) diff --git a/glymur/test/test_jp2k.py b/glymur/test/test_jp2k.py index edb19a1..f311c5c 100644 --- a/glymur/test/test_jp2k.py +++ b/glymur/test/test_jp2k.py @@ -83,6 +83,24 @@ class SliceProtocolBase(unittest.TestCase): @unittest.skipIf(os.name == "nt", fixtures.WINDOWS_TMP_FILE_MSG) class TestSliceProtocolBaseWrite(SliceProtocolBase): + def test_basic_write_by_tile_2d(self): + data = self.j2k_data[:, :, 0].copy() + with tempfile.NamedTemporaryFile(suffix='.jp2') as tfile: + kwargs = { + 'shape': (800, 480), + 'tileshape': (400, 240) + } + with Jp2k(tfile.name, **kwargs) as jp2: + jp2[:400, :240] = data[:400, :240] + jp2[:400, 240:480] = data[:400, 240:480] + jp2[400:800, :240] = data[400:800, :240] + jp2[400:800, 240:480] = data[400:800, 240:480] + + actual = Jp2k(tfile.name).read() + expected = data + import shutil; shutil.copyfile(tfile.name, '/Users/jevans/aa.jp2') + np.testing.assert_array_equal(actual, expected) + def test_write_ellipsis(self): expected = self.j2k_data diff --git a/glymur/test/test_opj_suite_write.py b/glymur/test/test_opj_suite_write.py index ecb7a7d..01ffa8c 100644 --- a/glymur/test/test_opj_suite_write.py +++ b/glymur/test/test_opj_suite_write.py @@ -355,7 +355,7 @@ class TestSuiteWrite(fixtures.MetadataBase): data=data, psizes=[(128, 128)] * 3, cratios=[100, 20, 2], - tilesize=(480, 640), + tileshape=(480, 640), cbsize=(32, 32)) # Should be three layers. @@ -388,7 +388,7 @@ class TestSuiteWrite(fixtures.MetadataBase): infile = opj_data_file('input/nonregression/Bretagne2.ppm') data = read_image(infile) with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile: - j = Jp2k(tfile.name, data=data, tilesize=(127, 127), prog="PCRL") + j = Jp2k(tfile.name, data=data, tileshape=(127, 127), prog="PCRL") codestream = j.get_codestream()