Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
jevans
da6d386c85 one more time 2014-11-22 10:32:01 -05:00
4 changed files with 185 additions and 34 deletions

View file

@ -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.

View file

@ -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)

View file

@ -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

View file

@ -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()