Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da6d386c85 |
4 changed files with 185 additions and 34 deletions
|
|
@ -7,4 +7,5 @@ Changes in 0.8.0
|
||||||
|
|
||||||
* Simplified writing images by moving data and options into the
|
* Simplified writing images by moving data and options into the
|
||||||
constructor. This is backwards-incompatible with 0.7.x.
|
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.
|
* Deprecated :py:meth:`read` method in favor of array-style slicing.
|
||||||
|
|
|
||||||
196
glymur/jp2k.py
196
glymur/jp2k.py
|
|
@ -80,7 +80,8 @@ class Jp2k(Jp2kBox):
|
||||||
(728, 1296, 3)
|
(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
|
Only the filename parameter is required in order to read a JPEG 2000
|
||||||
file.
|
file.
|
||||||
|
|
@ -93,6 +94,8 @@ class Jp2k(Jp2kBox):
|
||||||
image data to be written
|
image data to be written
|
||||||
shape : tuple
|
shape : tuple
|
||||||
size of image data, only required when image_data is not provided
|
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
|
cbsize : tuple, optional
|
||||||
code block size (DY, DX)
|
code block size (DY, DX)
|
||||||
cinema2k : int, optional
|
cinema2k : int, optional
|
||||||
|
|
@ -133,9 +136,6 @@ class Jp2k(Jp2kBox):
|
||||||
if true, write SOP marker before each packet
|
if true, write SOP marker before each packet
|
||||||
subsam : tuple, optional
|
subsam : tuple, optional
|
||||||
subsampling factors (dy, dx)
|
subsampling factors (dy, dx)
|
||||||
tilesize : tuple, optional
|
|
||||||
numeric tuple specifying tile size in terms of (numrows, numcols),
|
|
||||||
not (X, Y)
|
|
||||||
verbose : bool, optional
|
verbose : bool, optional
|
||||||
print informational messages produced by the OpenJPEG library
|
print informational messages produced by the OpenJPEG library
|
||||||
"""
|
"""
|
||||||
|
|
@ -151,6 +151,11 @@ class Jp2k(Jp2kBox):
|
||||||
else:
|
else:
|
||||||
self._shape = shape
|
self._shape = shape
|
||||||
|
|
||||||
|
if tileshape is not None:
|
||||||
|
self._tileshape = tileshape
|
||||||
|
else:
|
||||||
|
self._tileshape = shape
|
||||||
|
|
||||||
self._ignore_pclr_cmap_cdef = False
|
self._ignore_pclr_cmap_cdef = False
|
||||||
self._verbose = False
|
self._verbose = False
|
||||||
|
|
||||||
|
|
@ -238,6 +243,89 @@ class Jp2k(Jp2kBox):
|
||||||
metadata.append(str(codestream))
|
metadata.append(str(codestream))
|
||||||
return '\n'.join(metadata)
|
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):
|
def parse(self):
|
||||||
"""Parses the JPEG 2000 file.
|
"""Parses the JPEG 2000 file.
|
||||||
|
|
||||||
|
|
@ -350,7 +438,7 @@ class Jp2k(Jp2kBox):
|
||||||
# 2.1 API
|
# 2.1 API
|
||||||
self._cparams.rsiz = core.OPJ_PROFILE_CINEMA_4K
|
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.
|
"""Directs processing of write method arguments.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
|
@ -452,9 +540,9 @@ class Jp2k(Jp2kBox):
|
||||||
cparams.subsampling_dy = kwargs['subsam'][0]
|
cparams.subsampling_dy = kwargs['subsam'][0]
|
||||||
cparams.subsampling_dx = kwargs['subsam'][1]
|
cparams.subsampling_dx = kwargs['subsam'][1]
|
||||||
|
|
||||||
if 'tilesize' in kwargs:
|
if self._tileshape is not None:
|
||||||
cparams.cp_tdx = kwargs['tilesize'][1]
|
cparams.cp_tdx = self._tileshape[1]
|
||||||
cparams.cp_tdy = kwargs['tilesize'][0]
|
cparams.cp_tdy = self._tileshape[0]
|
||||||
cparams.tile_size_on = opj2.TRUE
|
cparams.tile_size_on = opj2.TRUE
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -472,7 +560,7 @@ class Jp2k(Jp2kBox):
|
||||||
else:
|
else:
|
||||||
cparams.tcp_mct = 0
|
cparams.tcp_mct = 0
|
||||||
|
|
||||||
self._validate_compression_params(img_array, cparams, **kwargs)
|
self._validate_compression_params(cparams, **kwargs)
|
||||||
|
|
||||||
self._cparams = cparams
|
self._cparams = cparams
|
||||||
|
|
||||||
|
|
@ -488,8 +576,27 @@ class Jp2k(Jp2kBox):
|
||||||
msg += "in order to write images."
|
msg += "in order to write images."
|
||||||
raise RuntimeError(msg)
|
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._determine_colorspace(**kwargs)
|
||||||
self._populate_cparams(img_array, **kwargs)
|
self._populate_cparams(**kwargs)
|
||||||
|
|
||||||
if opj2.OPENJP2 is not None:
|
if opj2.OPENJP2 is not None:
|
||||||
self._write_openjp2(img_array, verbose=verbose)
|
self._write_openjp2(img_array, verbose=verbose)
|
||||||
|
|
@ -563,13 +670,11 @@ class Jp2k(Jp2kBox):
|
||||||
|
|
||||||
self.parse()
|
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.
|
"""Check that the compression parameters are valid.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
img_array : ndarray
|
|
||||||
Image data to be written to file.
|
|
||||||
cparams : CompressionParametersType(ctypes.Structure)
|
cparams : CompressionParametersType(ctypes.Structure)
|
||||||
Corresponds to cparameters_t type in openjp2 headers.
|
Corresponds to cparameters_t type in openjp2 headers.
|
||||||
"""
|
"""
|
||||||
|
|
@ -614,25 +719,6 @@ class Jp2k(Jp2kBox):
|
||||||
msg += "must be powers of 2."
|
msg += "must be powers of 2."
|
||||||
raise IOError(msg.format(prch, prcw))
|
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):
|
def _determine_colorspace(self, colorspace=None, **kwargs):
|
||||||
"""Determine the colorspace from the supplied inputs.
|
"""Determine the colorspace from the supplied inputs.
|
||||||
|
|
||||||
|
|
@ -900,6 +986,52 @@ class Jp2k(Jp2kBox):
|
||||||
#
|
#
|
||||||
# Should have a slice object where start = stop = step = None
|
# Should have a slice object where start = stop = step = None
|
||||||
self._write(data)
|
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:
|
else:
|
||||||
msg = "Partial write operations are currently not allowed."
|
msg = "Partial write operations are currently not allowed."
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,24 @@ class SliceProtocolBase(unittest.TestCase):
|
||||||
@unittest.skipIf(os.name == "nt", fixtures.WINDOWS_TMP_FILE_MSG)
|
@unittest.skipIf(os.name == "nt", fixtures.WINDOWS_TMP_FILE_MSG)
|
||||||
class TestSliceProtocolBaseWrite(SliceProtocolBase):
|
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):
|
def test_write_ellipsis(self):
|
||||||
expected = self.j2k_data
|
expected = self.j2k_data
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -355,7 +355,7 @@ class TestSuiteWrite(fixtures.MetadataBase):
|
||||||
data=data,
|
data=data,
|
||||||
psizes=[(128, 128)] * 3,
|
psizes=[(128, 128)] * 3,
|
||||||
cratios=[100, 20, 2],
|
cratios=[100, 20, 2],
|
||||||
tilesize=(480, 640),
|
tileshape=(480, 640),
|
||||||
cbsize=(32, 32))
|
cbsize=(32, 32))
|
||||||
|
|
||||||
# Should be three layers.
|
# Should be three layers.
|
||||||
|
|
@ -388,7 +388,7 @@ class TestSuiteWrite(fixtures.MetadataBase):
|
||||||
infile = opj_data_file('input/nonregression/Bretagne2.ppm')
|
infile = opj_data_file('input/nonregression/Bretagne2.ppm')
|
||||||
data = read_image(infile)
|
data = read_image(infile)
|
||||||
with tempfile.NamedTemporaryFile(suffix='.j2k') as tfile:
|
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()
|
codestream = j.get_codestream()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue