[Git][debian-gis-team/glymur][upstream] New upstream version 0.9.4
Antonio Valentino (@antonio.valentino)
gitlab at salsa.debian.org
Sat Sep 4 18:04:56 BST 2021
Antonio Valentino pushed to branch upstream at Debian GIS Project / glymur
Commits:
ab1774db by Antonio Valentino at 2021-09-04T11:08:53+00:00
New upstream version 0.9.4
- - - - -
30 changed files:
- .travis.yml
- CHANGES.txt
- README.md
- appveyor.yml
- − ci/travis-36.yaml
- ci/travis-36-no-opj.yaml → ci/travis-37-no-opj.yaml
- ci/travis-39-opj2p4.yaml
- docs/source/conf.py
- docs/source/how_do_i.rst
- docs/source/introduction.rst
- docs/source/whatsnew/0.9.rst
- glymur/data/__init__.py
- glymur/jp2box.py
- glymur/jp2k.py
- glymur/lib/openjp2.py
- glymur/version.py
- setup.py
- tests/test_callbacks.py
- tests/test_codestream.py
- tests/test_colour_specification_box.py
- tests/test_jp2box.py
- tests/test_jp2box_jpx.py
- tests/test_jp2box_uuid.py
- tests/test_jp2box_xml.py
- tests/test_jp2k.py
- tests/test_openjp2.py
- tests/test_printing.py
- + tests/test_set_decoded_components.py
- tests/test_warnings.py
- + tests/test_writing_tiles.py
Changes:
=====================================
.travis.yml
=====================================
@@ -2,10 +2,10 @@ language: python
matrix:
fast_finish: true
include:
- - python: 3.9-dev
+ - python: 3.9
env:
- JOB="3.9" ENV_FILE="ci/travis-39.yaml"
- - python: 3.9-dev
+ - python: 3.9
env:
- JOB="3.9 opj 2.4" ENV_FILE="ci/travis-39-opj2p4.yaml"
- python: 3.8
@@ -17,12 +17,9 @@ matrix:
- python: 3.7
env:
- JOB="3.7" ENV_FILE="ci/travis-37-no-gdal.yaml"
- - python: 3.6
- env:
- - JOB="3.6 No OPENJPEG" ENV_FILE="ci/travis-36-no-opj.yaml"
- - python: 3.6
+ - python: 3.7
env:
- - JOB="3.6" ENV_FILE="ci/travis-36.yaml"
+ - JOB="3.7 No OPENJPEG" ENV_FILE="ci/travis-37-no-opj.yaml"
before_install:
- echo "before_install"
- sudo apt-get update
=====================================
CHANGES.txt
=====================================
@@ -1,3 +1,16 @@
+September 01, 2021 - v0.9.4
+ Add support for writing images tile-by-tile.
+ Add support for opj_set_decoded_components.
+ Remove support for Python 3.6.
+
+December 31, 2020 - v0.9.3
+ Qualify support on Python 3.9, OpenJPEG 2.4
+ Add support for multithreaded writes
+
+June 30, 2020 - v0.9.2
+ Update setup.py to include tests.
+ Update gdal imports to stop DeprecationWarning.
+
June 30, 2020 - v0.9.2
Update setup.py to include tests.
Update gdal imports to stop DeprecationWarning.
=====================================
README.md
=====================================
@@ -3,6 +3,6 @@ glymur: a Python interface for JPEG 2000
**glymur** contains a Python interface to the OpenJPEG library which
allows one to read and write JPEG 2000 files. **glymur** works on
-Python 3.6 and 3.7.
+Python 3.7, 3.8, and 3.9.
Please read the docs, https://glymur.readthedocs.org/en/latest/
=====================================
appveyor.yml
=====================================
@@ -12,23 +12,23 @@ environment:
matrix:
- - CONDA_ROOT: "C:\\Miniconda3_64"
- PYTHON_VERSION: "3.6"
- PYTHON_ARCH: "64"
- CONDA_PY: "36"
- CONDA_NPY: "117"
-
- CONDA_ROOT: "C:\\Miniconda3_64"
PYTHON_VERSION: "3.7"
PYTHON_ARCH: "64"
CONDA_PY: "37"
- CONDA_NPY: "117"
+ CONDA_NPY: "21"
- CONDA_ROOT: "C:\\Miniconda3_64"
PYTHON_VERSION: "3.8"
PYTHON_ARCH: "64"
CONDA_PY: "38"
- CONDA_NPY: "117"
+ CONDA_NPY: "21"
+
+ - CONDA_ROOT: "C:\\Miniconda3_64"
+ PYTHON_VERSION: "3.9"
+ PYTHON_ARCH: "64"
+ CONDA_PY: "39"
+ CONDA_NPY: "21"
# We always use a 64-bit machine, but can build x86 distributions
# with the PYTHON_ARCH variable (which is used by CMD_IN_ENV).
=====================================
ci/travis-36.yaml deleted
=====================================
@@ -1,14 +0,0 @@
-name: glymur
-channels:
- - defaults
-dependencies:
- - python=3.6.*
- - gdal
- - lxml
- - numpy
- - openjpeg
- - pip
- - scikit-image
- - pip:
- - importlib_resources
-
=====================================
ci/travis-36-no-opj.yaml → ci/travis-37-no-opj.yaml
=====================================
@@ -2,13 +2,9 @@ name: glymur
channels:
- defaults
dependencies:
- - python=3.6.*
+ - python=3.7.*
- gdal
- lxml
- numpy
- pip
- scikit-image
- - pip:
- - importlib_resources
-
-
=====================================
ci/travis-39-opj2p4.yaml
=====================================
@@ -6,3 +6,4 @@ dependencies:
- lxml
- numpy
- openjpeg >= 2.4.0
+ - scikit-image
=====================================
docs/source/conf.py
=====================================
@@ -78,7 +78,7 @@ copyright = '2013-2020, John Evans'
# The short X.Y version.
version = '0.9'
# The full version, including alpha/beta/rc tags.
-release = '0.9.3'
+release = '0.9.4'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
=====================================
docs/source/how_do_i.rst
=====================================
@@ -41,6 +41,39 @@ you can make use of OpenJPEG's thread support to speed-up read operations. ::
>>> t1 - t0
0.4060473537445068
+**************************************************
+... efficiently read just one band of a big image?
+**************************************************
+For really large images, before v0.9.4 you had to read in all bands of an
+image, even if you were only interested in just one of those bands. With
+v0.9.4 or higher, you can make use of the :py:meth:`decoded_components`
+property, which will inform the openjpeg library to just decode the
+specified component(s), which can significantly speed up read operations
+on large images. Be aware, however, that the openjpeg library will not
+employ the MCT when decoding these components.
+
+You can set the property to None to restore the behavior of decoding all
+bands.
+
+ >>> import glymur
+ >>> jp2file = glymur.data.nemo()
+ >>> jp2 = glymur.Jp2k(jp2file)
+ >>> data = jp2[:]
+ >>> data.shape
+ (1456, 2592, 3)
+ >>> jp2.decoded_components = 1
+ >>> data = jp2[:]
+ >>> data.shape
+ (1456, 2592)
+ >>> jp2.decoded_components = [0, 2]
+ >>> data = jp2[:]
+ >>> data.shape
+ (1456, 2592, 2)
+ >>> jp2.decoded_components = None
+ >>> data = jp2[:]
+ >>> data.shape
+ (1456, 2592, 3)
+
*****************
... write images?
*****************
@@ -60,7 +93,7 @@ or::
**********************************************
... write images using multithreaded encoding?
**********************************************
-If you have glymur 0.9.2 or higher
+If you have glymur 0.9.3 or higher
and OpenJPEG 2.4.0 or higher,
you can make use of OpenJPEG's thread support to speed-up read operations.
With a puny 2015 macbook, just two cores, and a 5824x10368x3 image, we get::
@@ -81,6 +114,30 @@ With a puny 2015 macbook, just two cores, and a 5824x10368x3 image, we get::
7.24 seconds
+*********************************************
+... write images that cannot fit into memory?
+*********************************************
+If you have glymur 0.9.4 or higher, you can write out an image tile-by-tile.
+In this example, we take a 512x512x3 image and tile it into a 20x20 grid,
+resulting in a 10240x10240x3 image.
+
+ >>> import skimage.data
+ >>> from glymur import Jp2k
+ >>> img = skimage.data.astronaut()
+ >>> print(img.shape)
+ (512, 512, 3)
+ >>> shape = img.shape[0] * 20, img.shape[1] * 20, 3
+ >>> tilesize = (img.shape[0], img.shape[1])
+ >>> j = Jp2k('400astronauts.jp2', shape=shape, tilesize=tilesize, verbose=True)
+ >>> for tw in j.get_tilewriters():
+ tw[:] = img
+ >>> j = Jp2k('400astronauts.jp2')
+ >>> print(j.shape)
+ (10240, 10240, 3)
+
+Note that the tiles are written out left-to-right, tile-row-by-tile-row. You must
+have image data ready to feed each tile writer, you cannot skip a tile.
+
************************************************************************
... write images with different compression ratios for different layers?
************************************************************************
=====================================
docs/source/introduction.rst
=====================================
@@ -15,7 +15,7 @@ Glymur will look to use **lxml** when processing boxes with XML content, but can
fall back upon the standard library's **ElementTree** if **lxml** is not
available.
-The current version of glymur works on Python versions 3.6, 3.7, 3.8, and 3.9.
+The current version of glymur works on Python versions 3.7, 3.8, and 3.9..
For more information about OpenJPEG, please consult http://www.openjpeg.org.
=====================================
docs/source/whatsnew/0.9.rst
=====================================
@@ -2,6 +2,14 @@
Changes in glymur 0.9
#####################
+****************
+Changes in 0.9.4
+****************
+
+ * Remove support for Python 3.6.
+ * Add support for writing images tile-by-tile.
+ * Add support for opj_set_decoded_components.
+
****************
Changes in 0.9.3
****************
=====================================
glymur/data/__init__.py
=====================================
@@ -6,7 +6,7 @@ These include:
goodstuff.j2k: my favorite bevorage.
"""
-import pkg_resources
+import importlib.resources as ir
def nemo():
@@ -17,8 +17,8 @@ def nemo():
file : str
Platform-independent path to nemo.jp2.
"""
- filename = pkg_resources.resource_filename(__name__, "nemo.jp2")
- return filename
+ with ir.path('glymur.data', 'nemo.jp2') as filename:
+ return str(filename)
def goodstuff():
@@ -29,8 +29,8 @@ def goodstuff():
file : str
Platform-independent path to goodstuff.j2k.
"""
- filename = pkg_resources.resource_filename(__name__, "goodstuff.j2k")
- return filename
+ with ir.path('glymur.data', 'goodstuff.j2k') as filename:
+ return str(filename)
def jpxfile():
@@ -41,5 +41,5 @@ def jpxfile():
file : str
Platform-independent path to 12-v6.4.jpx
"""
- filename = pkg_resources.resource_filename(__name__, "heliov.jpx")
- return filename
+ with ir.path('glymur.data', 'heliov.jpx') as filename:
+ return str(filename)
=====================================
glymur/jp2box.py
=====================================
@@ -191,9 +191,12 @@ class Jp2kBox(object):
f'The original error message was "{str(err)}".'
)
warnings.warn(msg, UserWarning)
- box = UnknownBox(box_id.decode('utf-8'),
- length=num_bytes,
- offset=start, longname='Unknown')
+ box = UnknownBox(
+ box_id.decode('utf-8'),
+ length=num_bytes,
+ offset=start,
+ longname='Unknown'
+ )
return box
@@ -1003,8 +1006,9 @@ class ContiguousCodestreamBox(Jp2kBox):
if self._filename is not None:
with open(self._filename, 'rb') as fptr:
fptr.seek(self.main_header_offset)
- codestream = Codestream(fptr, self._length,
- header_only=header_only)
+ codestream = Codestream(
+ fptr, self._length, header_only=header_only
+ )
self._codestream = codestream
return self._codestream
@@ -1052,8 +1056,12 @@ class ContiguousCodestreamBox(Jp2kBox):
codestream = Codestream(fptr, length, header_only=False)
else:
codestream = None
- box = cls(codestream, main_header_offset=main_header_offset,
- length=length, offset=offset)
+ box = cls(
+ codestream,
+ main_header_offset=main_header_offset,
+ length=length,
+ offset=offset
+ )
box._filename = fptr.name
box._length = length
return box
@@ -1333,9 +1341,13 @@ class FileTypeBox(Jp2kBox):
compatibility_list.append(entry)
- return cls(brand=brand, minor_version=minor_version,
- compatibility_list=compatibility_list,
- length=length, offset=offset)
+ return cls(
+ brand=brand,
+ minor_version=minor_version,
+ compatibility_list=compatibility_list,
+ length=length,
+ offset=offset
+ )
class FragmentListBox(Jp2kBox):
@@ -1355,8 +1367,10 @@ class FragmentListBox(Jp2kBox):
box_id = 'flst'
longname = 'Fragment List'
- def __init__(self, fragment_offset, fragment_length, data_reference,
- length=0, offset=-1):
+ def __init__(
+ self, fragment_offset, fragment_length, data_reference, length=0,
+ offset=-1
+ ):
super().__init__()
self.fragment_offset = fragment_offset
self.fragment_length = fragment_length
@@ -1618,9 +1632,11 @@ class ImageHeaderBox(Jp2kBox):
box_id = 'ihdr'
longname = 'Image Header'
- def __init__(self, height, width, num_components=1, signed=False,
- bits_per_component=8, compression=7, colorspace_unknown=False,
- ip_provided=False, length=0, offset=-1):
+ def __init__(
+ self, height, width, num_components=1, signed=False,
+ bits_per_component=8, compression=7, colorspace_unknown=False,
+ ip_provided=False, length=0, offset=-1
+ ):
"""
Examples
--------
@@ -2041,8 +2057,10 @@ class PaletteBox(Jp2kBox):
"""Verify that the box obeys the specifications."""
if (((len(self.bits_per_component) != len(self.signed))
or (len(self.signed) != self.palette.shape[1]))):
- msg = ("The length of the 'bits_per_component' and the 'signed' "
- "members must equal the number of columns of the palette.")
+ msg = (
+ "The length of the 'bits_per_component' and the 'signed' "
+ "members must equal the number of columns of the palette."
+ )
self._dispatch_validation_error(msg, writing=writing)
bps = self.bits_per_component
if writing and not all(b == bps[0] for b in bps):
@@ -2127,8 +2145,10 @@ class PaletteBox(Jp2kBox):
# Are any components signed or differently sized? We don't handle
# that.
if any(signed) or len(set(bps)) != 1:
- msg = ("Palettes with signed components or differently sized "
- "components are not supported.")
+ msg = (
+ "Palettes with signed components or differently sized "
+ "components are not supported."
+ )
raise InvalidJp2kError(msg)
# The palette is unsigned and all components have the same width..
@@ -2861,8 +2881,10 @@ class XMLBox(Jp2kBox):
"""
super().__init__()
if filename is not None and xml is not None:
- msg = ("Only one of either a filename or an ElementTree instance "
- "should be provided.")
+ msg = (
+ "Only one of either a filename or an ElementTree instance "
+ "should be provided."
+ )
raise RuntimeError(msg)
if filename is not None:
self.xml = ET.parse(str(filename))
@@ -3436,32 +3458,46 @@ class UUIDBox(Jp2kBox):
# report corners
uleft = self.GDALInfoReportCorner(gtif, hTransform, "Upper Left", 0, 0)
- lleft = self.GDALInfoReportCorner(gtif, hTransform, "Lower Left",
- 0, gtif.RasterYSize)
- uright = self.GDALInfoReportCorner(gtif, hTransform, "Upper Right",
- gtif.RasterXSize, 0)
- lright = self.GDALInfoReportCorner(gtif, hTransform, "Lower Right",
- gtif.RasterXSize, gtif.RasterYSize)
- center = self.GDALInfoReportCorner(gtif, hTransform, "Center",
- gtif.RasterXSize / 2.0,
- gtif.RasterYSize / 2.0)
+ lleft = self.GDALInfoReportCorner(
+ gtif, hTransform, "Lower Left", 0, gtif.RasterYSize
+ )
+ uright = self.GDALInfoReportCorner(
+ gtif, hTransform, "Upper Right", gtif.RasterXSize, 0
+ )
+ lright = self.GDALInfoReportCorner(
+ gtif,
+ hTransform,
+ "Lower Right",
+ gtif.RasterXSize, gtif.RasterYSize
+ )
+ center = self.GDALInfoReportCorner(
+ gtif,
+ hTransform,
+ "Center",
+ gtif.RasterXSize / 2.0, gtif.RasterYSize / 2.0
+ )
gdal.Unlink(in_mem_name)
- fmt = ("Coordinate System =\n"
- "{coordinate_system}\n"
- "{geotransform}\n"
- "Corner Coordinates:\n"
- "{upper_left}\n"
- "{lower_left}\n"
- "{upper_right}\n"
- "{lower_right}\n"
- "{center}")
+ fmt = (
+ "Coordinate System =\n"
+ "{coordinate_system}\n"
+ "{geotransform}\n"
+ "Corner Coordinates:\n"
+ "{upper_left}\n"
+ "{lower_left}\n"
+ "{upper_right}\n"
+ "{lower_right}\n"
+ "{center}"
+ )
coordinate_system = textwrap.indent(psz_pretty_wkt, ' ' * 4)
- msg = fmt.format(coordinate_system=coordinate_system,
- geotransform=geotransform_str,
- upper_left=uleft, upper_right=uright,
- lower_left=lleft, lower_right=lright, center=center)
+ msg = fmt.format(
+ coordinate_system=coordinate_system,
+ geotransform=geotransform_str,
+ upper_left=uleft, upper_right=uright,
+ lower_left=lleft, lower_right=lright,
+ center=center
+ )
return msg
def GDALInfoReportCorner(self, hDataset, hTransform, corner_name, x, y):
@@ -3481,8 +3517,10 @@ class UUIDBox(Jp2kBox):
if hTransform is not None:
point = hTransform.TransformPoint(dfGeoX, dfGeoY, 0)
if point is not None:
- line += '({},{})'.format(gdal.DecToDMS(point[0], 'Long', 2),
- gdal.DecToDMS(point[1], 'Lat', 2))
+ line += '({},{})'.format(
+ gdal.DecToDMS(point[0], 'Long', 2),
+ gdal.DecToDMS(point[1], 'Lat', 2)
+ )
return line
=====================================
glymur/jp2k.py
=====================================
@@ -21,6 +21,7 @@ import warnings
import numpy as np
# Local imports...
+import glymur
from .codestream import Codestream
from . import core, version, get_option
from .jp2box import (
@@ -49,6 +50,8 @@ class Jp2k(Jp2kBox):
----------
codestream : glymur.codestream.Codestream
JP2 or J2K codestream object.
+ decoded_components : sequence or None
+ If set, decode only these components. The MCT will not be used.
ignore_pclr_cmap_cdef : bool
Whether or not to ignore the pclr, cmap, or cdef boxes during any
color transformation, defaults to False.
@@ -91,7 +94,13 @@ class Jp2k(Jp2kBox):
0.4060473537445068
"""
- def __init__(self, filename, data=None, shape=None, **kwargs):
+ def __init__(
+ self, filename, data=None, shape=None, tilesize=None, verbose=False,
+ cbsize=None, cinema2k=None, cinema4k=None, colorspace=None,
+ cratios=None, eph=None, grid_offset=None, irreversible=None, mct=None,
+ modesw=None, numres=None, prog=None, psizes=None, psnr=None, sop=None,
+ subsam=None,
+ ):
"""
Parameters
----------
@@ -120,8 +129,8 @@ class Jp2k(Jp2kBox):
irreversible : bool, optional
If true, use the irreversible DWT 9-7 transform.
mct : bool, optional
- Usage of the multi component transform. If not specified, defaults
- to True if the color space is RGB.
+ Usage of the multi component transform to write an image. If not
+ specified, defaults to True if the color space is RGB.
modesw : int, optional
mode switch
1 = BYPASS(LAZY)
@@ -156,21 +165,51 @@ class Jp2k(Jp2kBox):
self.box = []
self._codec_format = None
- self._colorspace = None
self._layer = 0
self._codestream = None
+ self._decoded_components = None
+
+ self._cbsize = cbsize
+ self._cinema2k = cinema2k
+ self._cinema4k = cinema4k
+ self._colorspace = colorspace
+ self._cratios = cratios
+ self._eph = eph
+ self._grid_offset = grid_offset
+ self._irreversible = irreversible
+ self._mct = mct
+ self._modesw = modesw
+ self._numres = numres
+ self._prog = prog
+ self._psizes = psizes
+ self._psnr = psnr
+ self._sop = sop
+ self._subsam = subsam
+ self._tilesize = tilesize
self._ignore_pclr_cmap_cdef = False
- self._verbose = False
+ self._verbose = verbose
+
+ if self.filename[-4:].endswith(('.jp2', '.JP2')):
+ self._codec_format = opj2.CODEC_JP2
+ else:
+ self._codec_format = opj2.CODEC_J2K
+
+ if self._codec_format == opj2.CODEC_J2K and colorspace is not None:
+ msg = 'Do not specify a colorspace when writing a raw codestream.'
+ raise InvalidJp2kError(msg)
if data is not None:
- # We are writing a JP2/J2K/JPX file.
+ # We are writing a JP2/J2K/JPX file where the image is
+ # contained in memory.
self._shape = data.shape
- self._write(data, **kwargs)
- elif shape is not None:
- # Only if J2X?
+ self._write(data)
+ elif data is None and shape is not None:
+ # We are writing an entire image via the slice protocol, or we are
+ # writing an image tile-by-tile. A future course of action will
+ # determine that.
self._shape = shape
- if data is None and shape is None:
+ elif data is None and shape is None:
# We must be just reading a JP2/J2K/JPX file. Parse its
# contents, then determine "shape".
self.parse()
@@ -219,6 +258,32 @@ class Jp2k(Jp2kBox):
def ignore_pclr_cmap_cdef(self, ignore_pclr_cmap_cdef):
self._ignore_pclr_cmap_cdef = ignore_pclr_cmap_cdef
+ @property
+ def decoded_components(self):
+ return self._decoded_components
+
+ @decoded_components.setter
+ def decoded_components(self, components):
+
+ if components is None:
+ # this is a special case where we are restoring the original
+ # behavior of reading all bands
+ self._decoded_components = components
+ return
+
+ if np.isscalar(components):
+ components = [components]
+
+ if any(x > len(self.codestream.segment[1].xrsiz) for x in components):
+ msg = (
+ f"{components} has at least one invalid component, "
+ f"cannot be greater than "
+ f"{len(self.codestream.segment[1].xrsiz)}."
+ )
+ raise ValueError(msg)
+
+ self._decoded_components = components
+
@property
def layer(self):
return self._layer
@@ -242,6 +307,10 @@ class Jp2k(Jp2kBox):
self._codestream = self.get_codestream(header_only=True)
return self._codestream
+ @property
+ def tilesize(self):
+ return self._tilesize
+
@property
def verbose(self):
return self._verbose
@@ -280,6 +349,13 @@ class Jp2k(Jp2kBox):
metadata.append(str(self.codestream))
return '\n'.join(metadata)
+ def get_tilewriters(self):
+ """
+ Return an object that facilitates writing tile by tile.
+ """
+
+ return _TileWriter(self)
+
def parse(self):
"""Parses the JPEG 2000 file.
@@ -421,11 +497,7 @@ class Jp2k(Jp2kBox):
# cinema4k
self._cparams.rsiz = core.OPJ_PROFILE_CINEMA_4K
- def _populate_cparams(self, img_array, mct=None, cratios=None, psnr=None,
- cinema2k=None, cinema4k=None, irreversible=None,
- cbsize=None, eph=None, grid_offset=None, modesw=None,
- numres=None, prog=None, psizes=None, sop=None,
- subsam=None, tilesize=None, colorspace=None):
+ def _populate_cparams(self, img_array):
"""Directs processing of write method arguments.
Parameters
@@ -435,33 +507,40 @@ class Jp2k(Jp2kBox):
kwargs : dictionary
Non-image keyword inputs provided to write method.
"""
- other_args = (mct, cratios, psnr, irreversible, cbsize, eph,
- grid_offset, modesw, numres, prog, psizes, sop, subsam)
+ other_args = (
+ self._mct, self._cratios, self._psnr, self._irreversible,
+ self._cbsize, self._eph, self._grid_offset, self._modesw,
+ self._numres, self._prog, self._psizes, self._sop, self._subsam
+ )
if (
- (cinema2k is not None or cinema4k is not None)
+ (
+ self._cinema2k is not None or self._cinema4k is not None
+ )
and (not all([arg is None for arg in other_args]))
):
msg = ("Cannot specify cinema2k/cinema4k along with any other "
"options.")
raise InvalidJp2kError(msg)
- if psnr is not None:
- if cratios is not None:
+ if self._psnr is not None:
+ if self._cratios is not None:
msg = "Cannot specify cratios and psnr options together."
raise InvalidJp2kError(msg)
- if 0 in psnr and psnr[-1] != 0:
+ if 0 in self._psnr and self._psnr[-1] != 0:
msg = ("If a zero value is supplied in the PSNR keyword "
"argument, it must be in the final position.")
raise InvalidJp2kError(msg)
if (
- (0 in psnr and np.any(np.diff(psnr[:-1]) < 0))
- or (0 not in psnr and np.any(np.diff(psnr) < 0))
+ (0 in self._psnr and np.any(np.diff(self._psnr[:-1]) < 0))
+ or (0 not in self._psnr and np.any(np.diff(self._psnr) < 0))
):
- msg = ("PSNR values must be increasing, with one exception - "
- "zero may be in the final position to indicate a "
- "lossless layer.")
+ msg = (
+ "PSNR values must be increasing, with one exception - "
+ "zero may be in the final position to indicate a lossless "
+ "layer."
+ )
raise InvalidJp2kError(msg)
cparams = opj2.set_default_encoder_parameters()
@@ -471,82 +550,81 @@ class Jp2k(Jp2kBox):
outfile += b'0' * num_pad_bytes
cparams.outfile = outfile
- if self.filename[-4:].endswith(('.jp2', '.JP2')):
- cparams.codec_fmt = opj2.CODEC_JP2
- else:
- cparams.codec_fmt = opj2.CODEC_J2K
+ cparams.codec_fmt = self._codec_format
- cparams.irreversible = 1 if irreversible else 0
+ cparams.irreversible = 1 if self._irreversible else 0
- if cinema2k is not None:
+ if self._cinema2k is not None:
self._cparams = cparams
- self._set_cinema_params('cinema2k', cinema2k)
+ self._set_cinema_params('cinema2k', self._cinema2k)
- if cinema4k is not None:
+ if self._cinema4k is not None:
self._cparams = cparams
- self._set_cinema_params('cinema4k', cinema4k)
+ self._set_cinema_params('cinema4k', self._cinema4k)
- if cbsize is not None:
- cparams.cblockw_init = cbsize[1]
- cparams.cblockh_init = cbsize[0]
+ if self._cbsize is not None:
+ cparams.cblockw_init = self._cbsize[1]
+ cparams.cblockh_init = self._cbsize[0]
- if cratios is not None:
- cparams.tcp_numlayers = len(cratios)
- for j, cratio in enumerate(cratios):
+ if self._cratios is not None:
+ cparams.tcp_numlayers = len(self._cratios)
+ for j, cratio in enumerate(self._cratios):
cparams.tcp_rates[j] = cratio
cparams.cp_disto_alloc = 1
- cparams.csty |= 0x02 if sop else 0
- cparams.csty |= 0x04 if eph else 0
+ cparams.csty |= 0x02 if self._sop else 0
+ cparams.csty |= 0x04 if self._eph else 0
- if grid_offset is not None:
- cparams.image_offset_x0 = grid_offset[1]
- cparams.image_offset_y0 = grid_offset[0]
+ if self._grid_offset is not None:
+ cparams.image_offset_x0 = self._grid_offset[1]
+ cparams.image_offset_y0 = self._grid_offset[0]
- if modesw is not None:
+ if self._modesw is not None:
for shift in range(6):
power_of_two = 1 << shift
- if modesw & power_of_two:
+ if self._modesw & power_of_two:
cparams.mode |= power_of_two
- if numres is not None:
- cparams.numresolution = numres
+ if self._numres is not None:
+ cparams.numresolution = self._numres
- if prog is not None:
- cparams.prog_order = core.PROGRESSION_ORDER[prog.upper()]
+ if self._prog is not None:
+ cparams.prog_order = core.PROGRESSION_ORDER[self._prog.upper()]
- if psnr is not None:
- cparams.tcp_numlayers = len(psnr)
- for j, snr_layer in enumerate(psnr):
+ if self._psnr is not None:
+ cparams.tcp_numlayers = len(self._psnr)
+ for j, snr_layer in enumerate(self._psnr):
cparams.tcp_distoratio[j] = snr_layer
cparams.cp_fixed_quality = 1
- if psizes is not None:
- for j, (prch, prcw) in enumerate(psizes):
+ if self._psizes is not None:
+ for j, (prch, prcw) in enumerate(self._psizes):
cparams.prcw_init[j] = prcw
cparams.prch_init[j] = prch
cparams.csty |= 0x01
- cparams.res_spec = len(psizes)
+ cparams.res_spec = len(self._psizes)
- if subsam is not None:
- cparams.subsampling_dy = subsam[0]
- cparams.subsampling_dx = subsam[1]
+ if self._subsam is not None:
+ cparams.subsampling_dy = self._subsam[0]
+ cparams.subsampling_dx = self._subsam[1]
- if tilesize is not None:
- cparams.cp_tdx = tilesize[1]
- cparams.cp_tdy = tilesize[0]
+ if self._tilesize is not None:
+ cparams.cp_tdx = self._tilesize[1]
+ cparams.cp_tdy = self._tilesize[0]
cparams.tile_size_on = opj2.TRUE
- if mct is None:
+ if self._mct is None:
# If the multi component transform was not specified, we infer
# that it should be used if the color space is RGB.
cparams.tcp_mct = 1 if self._colorspace == opj2.CLRSPC_SRGB else 0
else:
if self._colorspace == opj2.CLRSPC_GRAY:
- msg = ("Cannot specify usage of the multi component transform "
- "if the colorspace is gray.")
+ msg = (
+ "Cannot specify usage of the multi component transform "
+ "if the colorspace is gray."
+ )
raise InvalidJp2kError(msg)
- cparams.tcp_mct = 1 if mct else 0
+ cparams.tcp_mct = 1 if self._mct else 0
# Set defaults to lossless to begin.
if cparams.tcp_numlayers == 0:
@@ -554,11 +632,11 @@ class Jp2k(Jp2kBox):
cparams.tcp_numlayers += 1
cparams.cp_disto_alloc = 1
- self._validate_compression_params(img_array, cparams, colorspace)
+ self._validate_compression_params(img_array, cparams)
self._cparams = cparams
- def _write(self, img_array, verbose=False, **kwargs):
+ def _write(self, img_array):
"""Write image data to a JP2/JPX/J2k file. Intended usage of the
various parameters follows that of OpenJPEG's opj_compress utility.
@@ -570,18 +648,18 @@ class Jp2k(Jp2kBox):
"in order to write images.")
raise RuntimeError(msg)
- self._determine_colorspace(**kwargs)
- self._populate_cparams(img_array, **kwargs)
+ self._determine_colorspace()
+ self._populate_cparams(img_array)
- self._write_openjp2(img_array, verbose=verbose)
+ if img_array.ndim == 2:
+ # Force the image to be 3D. This makes it easier to copy the
+ # image data later on.
+ numrows, numcols = img_array.shape
+ img_array = img_array.reshape(numrows, numcols, 1)
- def _validate_j2k_colorspace(self, cparams, colorspace):
- """
- Cannot specify a colorspace with J2K.
- """
- if cparams.codec_fmt == opj2.CODEC_J2K and colorspace is not None:
- msg = 'Do not specify a colorspace when writing a raw codestream.'
- raise InvalidJp2kError(msg)
+ self._populate_comptparms(img_array)
+
+ self._write_openjp2(img_array)
def _validate_codeblock_size(self, cparams):
"""
@@ -660,11 +738,13 @@ class Jp2k(Jp2kBox):
Only uint8 and uint16 images are currently supported.
"""
if img_array.dtype != np.uint8 and img_array.dtype != np.uint16:
- msg = ("Only uint8 and uint16 datatypes are currently supported "
- "when writing.")
+ msg = (
+ "Only uint8 and uint16 datatypes are currently supported when "
+ "writing."
+ )
raise InvalidJp2kError(msg)
- def _validate_compression_params(self, img_array, cparams, colorspace):
+ def _validate_compression_params(self, img_array, cparams):
"""Check that the compression parameters are valid.
Parameters
@@ -674,21 +754,15 @@ class Jp2k(Jp2kBox):
cparams : CompressionParametersType(ctypes.Structure)
Corresponds to cparameters_t type in openjp2 headers.
"""
- self._validate_j2k_colorspace(cparams, colorspace)
self._validate_codeblock_size(cparams)
self._validate_precinct_size(cparams)
self._validate_image_rank(img_array)
self._validate_image_datatype(img_array)
- def _determine_colorspace(self, colorspace=None, **kwargs):
+ def _determine_colorspace(self):
"""Determine the colorspace from the supplied inputs.
-
- Parameters
- ----------
- colorspace : str, optional
- Either 'rgb' or 'gray'.
"""
- if colorspace is None:
+ if self._colorspace is None:
# Must infer the colorspace from the image dimensions.
if len(self.shape) < 3:
# A single channel image is grayscale.
@@ -701,33 +775,28 @@ class Jp2k(Jp2kBox):
# Anything else must be RGB, right?
self._colorspace = opj2.CLRSPC_SRGB
else:
- if colorspace.lower() not in ('rgb', 'grey', 'gray'):
- msg = f'Invalid colorspace "{colorspace}".'
+ if self._colorspace.lower() not in ('rgb', 'grey', 'gray'):
+ msg = f'Invalid colorspace "{self._colorspace}".'
raise InvalidJp2kError(msg)
- elif colorspace.lower() == 'rgb' and self.shape[2] < 3:
+ elif self._colorspace.lower() == 'rgb' and self.shape[2] < 3:
msg = 'RGB colorspace requires at least 3 components.'
raise InvalidJp2kError(msg)
# Turn the colorspace from a string to the enumerated value that
# the library expects.
- COLORSPACE_MAP = {'rgb': opj2.CLRSPC_SRGB,
- 'gray': opj2.CLRSPC_GRAY,
- 'grey': opj2.CLRSPC_GRAY,
- 'ycc': opj2.CLRSPC_YCC}
+ COLORSPACE_MAP = {
+ 'rgb': opj2.CLRSPC_SRGB,
+ 'gray': opj2.CLRSPC_GRAY,
+ 'grey': opj2.CLRSPC_GRAY,
+ 'ycc': opj2.CLRSPC_YCC
+ }
- self._colorspace = COLORSPACE_MAP[colorspace.lower()]
+ self._colorspace = COLORSPACE_MAP[self._colorspace.lower()]
- def _write_openjp2(self, img_array, verbose=False):
+ def _write_openjp2(self, img_array):
"""
Write JPEG 2000 file using OpenJPEG 2.x interface.
"""
- if img_array.ndim == 2:
- # Force the image to be 3D. Just makes things easier later on.
- numrows, numcols = img_array.shape
- img_array = img_array.reshape(numrows, numcols, 1)
-
- self._populate_comptparms(img_array)
-
with ExitStack() as stack:
image = opj2.image_create(self._comptparms, self._colorspace)
stack.callback(opj2.image_destroy, image)
@@ -737,7 +806,7 @@ class Jp2k(Jp2kBox):
codec = opj2.create_compress(self._cparams.codec_fmt)
stack.callback(opj2.destroy_codec, codec)
- if self._verbose or verbose:
+ if self._verbose:
info_handler = _INFO_CALLBACK
else:
info_handler = None
@@ -748,8 +817,8 @@ class Jp2k(Jp2kBox):
opj2.setup_encoder(codec, self._cparams, image)
- strm = opj2.stream_create_default_file_stream(self.filename,
- False)
+ strm = opj2.stream_create_default_file_stream(self.filename, False)
+
num_threads = get_option('lib.num_threads')
if version.openjpeg_version >= '2.4.0':
opj2.codec_set_threads(codec, num_threads)
@@ -789,8 +858,9 @@ class Jp2k(Jp2kBox):
and box.uuid == UUID('be7acfcb-97a9-42e8-9c71-999491e3afac')
)
if not (box_is_xml or box_is_xmp):
- msg = ("Only XML boxes and XMP UUID boxes can currently be "
- "appended.")
+ msg = (
+ "Only XML boxes and XMP UUID boxes can currently be appended."
+ )
raise RuntimeError(msg)
# Check the last box. If the length field is zero, then rewrite
@@ -1081,13 +1151,14 @@ class Jp2k(Jp2kBox):
if np.abs(np.log2(step) - np.round(np.log2(step))) > 1e-6:
msg = "Row and column strides must be powers of 2."
raise ValueError(msg)
- rlevel = np.int(np.round(np.log2(step)))
+ rlevel = int(np.round(np.log2(step)))
- area = (0 if rows.start is None else rows.start,
- 0 if cols.start is None else cols.start,
- numrows if rows.stop is None else rows.stop,
- numcols if cols.stop is None else cols.stop
- )
+ area = (
+ 0 if rows.start is None else rows.start,
+ 0 if cols.start is None else cols.start,
+ numrows if rows.stop is None else rows.stop,
+ numcols if cols.stop is None else cols.stop
+ )
data = self._read(area=area, rlevel=rlevel)
if len(pargs) == 2:
return data
@@ -1155,8 +1226,19 @@ class Jp2k(Jp2kBox):
def _subsampling_sanity_check(self):
"""Check for differing subsample factors.
"""
- dxs = np.array(self.codestream.segment[1].xrsiz)
- dys = np.array(self.codestream.segment[1].yrsiz)
+ if self._decoded_components is None:
+ dxs = np.array(self.codestream.segment[1].xrsiz)
+ dys = np.array(self.codestream.segment[1].yrsiz)
+ else:
+ dxs = np.array([
+ self.codestream.segment[1].xrsiz[i]
+ for i in self._decoded_components
+ ])
+ dys = np.array([
+ self.codestream.segment[1].yrsiz[i]
+ for i in self._decoded_components
+ ])
+
if np.any(dxs - dxs[0]) or np.any(dys - dys[0]):
msg = (
f"The read_bands method should be used when the subsampling "
@@ -1232,13 +1314,18 @@ class Jp2k(Jp2kBox):
raw_image = opj2.read_header(stream, codec)
stack.callback(opj2.image_destroy, raw_image)
+ if self._decoded_components is not None:
+ opj2.set_decoded_components(codec, self._decoded_components)
+
if self._dparams.nb_tile_to_decode:
opj2.get_decoded_tile(codec, stream, raw_image,
self._dparams.tile_index)
else:
- opj2.set_decode_area(codec, raw_image,
- self._dparams.DA_x0, self._dparams.DA_y0,
- self._dparams.DA_x1, self._dparams.DA_y1)
+ opj2.set_decode_area(
+ codec, raw_image,
+ self._dparams.DA_x0, self._dparams.DA_y0,
+ self._dparams.DA_x1, self._dparams.DA_y1
+ )
opj2.decode(codec, stream, raw_image)
opj2.end_decompress(codec, stream)
@@ -1350,9 +1437,11 @@ class Jp2k(Jp2kBox):
>>> components_lst = jp.read_bands(rlevel=1)
"""
if version.openjpeg_version < '2.3.0':
- msg = ("You must have at least version 2.3.0 of OpenJPEG "
- "installed before using this method. Your version of "
- "OpenJPEG is {version.openjpeg_version}.")
+ msg = (
+ f"You must have at least version 2.3.0 of OpenJPEG installed "
+ f"before using this method. Your version of OpenJPEG is "
+ f"{version.openjpeg_version}."
+ )
raise RuntimeError(msg)
self.ignore_pclr_cmap_cdef = ignore_pclr_cmap_cdef
@@ -1387,8 +1476,10 @@ class Jp2k(Jp2kBox):
dtypes.append(self._component2dtype(component))
nrows.append(component.h)
ncols.append(component.w)
- is_cube = all(r == nrows[0] and c == ncols[0] and d == dtypes[0]
- for r, c, d in zip(nrows, ncols, dtypes))
+ is_cube = all(
+ r == nrows[0] and c == ncols[0] and d == dtypes[0]
+ for r, c, d in zip(nrows, ncols, dtypes)
+ )
if is_cube:
image = np.zeros((nrows[0], ncols[0], ncomps), dtypes[0])
@@ -1503,18 +1594,27 @@ class Jp2k(Jp2kBox):
return codestream
- def _populate_image_struct(self, image, imgdata):
+ def _populate_image_struct(
+ self, image, imgdata, tile_x_factor=1, tile_y_factor=1
+ ):
"""Populates image struct needed for compression.
Parameters
----------
image : ImageType(ctypes.Structure)
Corresponds to image_t type in openjp2 headers.
- img_array : ndarray
+ imgdata : ndarray
Image data to be written to file.
+ tile_x_factor, tile_y_factor: int
+ Used only when writing tile-by-tile. In this case, the image data
+ that we have is only the size of a single tile.
"""
- numrows, numcols, num_comps = imgdata.shape
+ if len(self.shape) < 3:
+ (numrows, numcols), num_comps = self.shape, 1
+ else:
+ numrows, numcols, num_comps = self.shape
+
for k in range(num_comps):
self._validate_nonzero_image_size(numrows, numcols, k)
@@ -1523,15 +1623,19 @@ class Jp2k(Jp2kBox):
image.contents.y0 = self._cparams.image_offset_y0
image.contents.x1 = (
image.contents.x0
- + (numcols - 1) * self._cparams.subsampling_dx
+ + (numcols - 1) * self._cparams.subsampling_dx * tile_x_factor
+ 1
)
image.contents.y1 = (
image.contents.y0
- + (numrows - 1) * self._cparams.subsampling_dy
+ + (numrows - 1) * self._cparams.subsampling_dy * tile_y_factor
+ 1
)
+ if tile_x_factor != 1 or tile_y_factor != 1:
+ # don't stage the data if writing tiles
+ return image
+
# Stage the image data to the openjpeg data structure.
for k in range(0, num_comps):
if self._cparams.rsiz in (core.OPJ_PROFILE_CINEMA_2K,
@@ -1562,7 +1666,11 @@ class Jp2k(Jp2kBox):
else:
comp_prec = 16
- numrows, numcols, num_comps = img_array.shape
+ if len(self.shape) < 3:
+ (numrows, numcols), num_comps = self.shape, 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
@@ -1593,9 +1701,11 @@ class Jp2k(Jp2kBox):
This is non-exhaustive.
"""
- JP2_IDS = ['colr', 'cdef', 'cmap', 'jp2c', 'ftyp', 'ihdr', 'jp2h',
- 'jP ', 'pclr', 'res ', 'resc', 'resd', 'xml ', 'ulst',
- 'uinf', 'url ', 'uuid']
+ JP2_IDS = [
+ 'colr', 'cdef', 'cmap', 'jp2c', 'ftyp', 'ihdr', 'jp2h', 'jP ',
+ 'pclr', 'res ', 'resc', 'resd', 'xml ', 'ulst', 'uinf', 'url ',
+ 'uuid'
+ ]
self._validate_signature_compatibility(boxes)
self._validate_jp2h(boxes)
@@ -1623,8 +1733,10 @@ class Jp2k(Jp2kBox):
jp2h = lst[0]
for colr in [box for box in jp2h.box if box.box_id == 'colr']:
if colr.approximation != 0:
- msg = ("A JP2 colr box cannot have a non-zero approximation "
- "field.")
+ msg = (
+ "A JP2 colr box cannot have a non-zero approximation "
+ "field."
+ )
raise InvalidJp2kError(msg)
def _validate_jpx_box_sequence(self, boxes):
@@ -1639,8 +1751,10 @@ class Jp2k(Jp2kBox):
# Check for a bad sequence of boxes.
# 1st two boxes must be 'jP ' and 'ftyp'
if boxes[0].box_id != 'jP ' or boxes[1].box_id != 'ftyp':
- msg = ("The first box must be the signature box and the second "
- "must be the file type box.")
+ msg = (
+ "The first box must be the signature box and the second must "
+ "be the file type box."
+ )
raise InvalidJp2kError(msg)
# The compatibility list must contain at a minimum 'jp2 '.
@@ -1657,8 +1771,10 @@ class Jp2k(Jp2kBox):
jp2c_lst = [idx for (idx, box) in enumerate(boxes)
if box.box_id == 'jp2c']
if len(jp2c_lst) == 0:
- msg = ("A codestream box must be defined in the outermost "
- "list of boxes.")
+ msg = (
+ "A codestream box must be defined in the outermost list of "
+ "boxes."
+ )
raise InvalidJp2kError(msg)
jp2c_idx = jp2c_lst[0]
@@ -1680,13 +1796,16 @@ class Jp2k(Jp2kBox):
# 1st jp2 header box must be ihdr
if jp2h.box[0].box_id != 'ihdr':
- msg = ("The first box in the jp2 header box must be the image "
- "header box.")
+ msg = (
+ "The first box in the jp2 header box must be the image header "
+ "box."
+ )
raise InvalidJp2kError(msg)
# colr must be present in jp2 header box.
- colr_lst = [j for (j, box) in enumerate(jp2h.box)
- if box.box_id == 'colr']
+ colr_lst = [
+ j for (j, box) in enumerate(jp2h.box) if box.box_id == 'colr'
+ ]
if len(colr_lst) == 0:
msg = "The jp2 header box must contain a color definition box."
raise InvalidJp2kError(msg)
@@ -1800,8 +1919,10 @@ class Jp2k(Jp2kBox):
for box in boxes:
if box.box_id in JPX_IDS:
if len(set(['jpx ', 'jpxb']).intersection(jpx_cl)) == 0:
- msg = ("A JPX box requires that either 'jpx ' or 'jpxb' "
- "be present in the ftype compatibility list.")
+ msg = (
+ "A JPX box requires that either 'jpx ' or 'jpxb' be "
+ "present in the ftype compatibility list."
+ )
raise InvalidJp2kError(msg)
if hasattr(box, 'box') != 0:
# Same set of checks on any child boxes.
@@ -1817,13 +1938,162 @@ class Jp2k(Jp2kBox):
if hasattr(box, 'box'):
for boxi in box.box:
if boxi.box_id == 'lbl ':
- msg = (f"A label box cannot be nested inside a "
- f"{box.box_id} box.")
+ msg = (
+ f"A label box cannot be nested inside a "
+ f"{box.box_id} box."
+ )
raise InvalidJp2kError(msg)
# Same set of checks on any child boxes.
self._validate_label(box.box)
+class _TileWriter(object):
+ """
+ Writes tiles to file, one by one.
+
+ Attributes
+ ----------
+ jp2k : glymur.Jp2k
+ Object wrapping the JPEG2000 file.
+ num_tile_rows, num_tile_cols : int
+ Dimensions of the image in terms of tiles.
+ number_of_tiles : int
+ This many tiles in the image.
+ tile_index : int
+ Each time the iteration protocol fires, this index will increase by
+ one, as the openjpeg library requires the tiles to be processed
+ sequentially.
+ """
+
+ def __init__(self, jp2k):
+ self.jp2k = jp2k
+
+ self.num_tile_rows = int(self.jp2k.shape[0] / self.jp2k.tilesize[0])
+ self.num_tile_cols = int(self.jp2k.shape[1] / self.jp2k.tilesize[1])
+ self.number_of_tiles = self.num_tile_rows * self.num_tile_cols
+
+ def __iter__(self):
+ self.tile_index = -1
+ return self
+
+ def __next__(self):
+ if self.tile_index < self.number_of_tiles - 1:
+ self.tile_index += 1
+ return self
+ else:
+ # We've gone thru all the tiles by this point.
+ raise StopIteration
+
+ def __setitem__(self, index, img_array):
+ """Write image data to a JP2/JPX/J2k file. Intended usage of the
+ various parameters follows that of OpenJPEG's opj_compress utility.
+ """
+ if version.openjpeg_version < '2.3.0':
+ msg = ("You must have at least version 2.3.0 of OpenJPEG "
+ "in order to write images.")
+ raise RuntimeError(msg)
+
+ if not isinstance(index, slice):
+ msg = (
+ "When writing tiles, the tile slice arguments must be just"
+ "a single slice(None, None, None), i.e. [:]."
+ )
+ raise RuntimeError(msg)
+
+ if self.tile_index == 0:
+ self.setup_first_tile(img_array)
+
+ try:
+ opj2.write_tile(
+ self.codec,
+ self.tile_index,
+ _set_planar_pixel_order(img_array),
+ self.stream
+ )
+ except glymur.lib.openjp2.OpenJPEGLibraryError as e:
+ # properly dispose of these resources
+ opj2.end_compress(self.codec, self.stream)
+ opj2.stream_destroy(self.stream)
+ opj2.image_destroy(self.image)
+ opj2.destroy_codec(self.codec)
+ raise(e)
+
+ if self.tile_index == self.number_of_tiles - 1:
+ # properly dispose of these resources
+ opj2.end_compress(self.codec, self.stream)
+ opj2.stream_destroy(self.stream)
+ opj2.image_destroy(self.image)
+ opj2.destroy_codec(self.codec)
+
+ # ... and reparse the newly created file to get all the metadata
+ self.jp2k.parse()
+
+ def setup_first_tile(self, img_array):
+ """
+ Only do these things for the first tile.
+ """
+ self.jp2k._determine_colorspace()
+ self.jp2k._populate_cparams(img_array)
+ self.jp2k._populate_comptparms(img_array)
+
+ self.codec = opj2.create_compress(self.jp2k._cparams.codec_fmt)
+
+ if self.jp2k.verbose:
+ info_handler = _INFO_CALLBACK
+ else:
+ info_handler = None
+
+ opj2.set_info_handler(self.codec, info_handler)
+ opj2.set_warning_handler(self.codec, _WARNING_CALLBACK)
+ opj2.set_error_handler(self.codec, _ERROR_CALLBACK)
+
+ self.image = opj2.image_tile_create(
+ self.jp2k._comptparms, self.jp2k._colorspace
+ )
+
+ self.jp2k._populate_image_struct(
+ self.image, img_array,
+ tile_x_factor=self.num_tile_cols,
+ tile_y_factor=self.num_tile_rows
+ )
+ self.image.contents.x1 = self.jp2k.shape[1]
+ self.image.contents.y1 = self.jp2k.shape[0]
+
+ opj2.setup_encoder(self.codec, self.jp2k._cparams, self.image)
+
+ self.stream = opj2.stream_create_default_file_stream(
+ self.jp2k.filename, False
+ )
+
+ num_threads = get_option('lib.num_threads')
+ if version.openjpeg_version >= '2.4.0':
+ opj2.codec_set_threads(self.codec, num_threads)
+ elif num_threads > 1:
+ msg = (
+ f'Threaded encoding is not supported in library versions '
+ f'prior to 2.4.0. Your version is '
+ f'{version.openjpeg_version}.'
+ )
+ warnings.warn(msg, UserWarning)
+
+ opj2.start_compress(self.codec, self.image, self.stream)
+
+
+def _set_planar_pixel_order(img):
+ """
+ Reorder the image pixels so that plane-0 comes first, then plane-1, etc.
+ This is a requirement for using opj_write_tile.
+ """
+ if img.ndim == 3:
+ # C-order increments along the y-axis slowest (0), then x-axis (1),
+ # then z-axis (2). We want it to go along the z-axis slowest, then
+ # y-axis, then x-axis.
+ img = np.swapaxes(img, 1, 2)
+ img = np.swapaxes(img, 0, 1)
+
+ return img.copy()
+
+
# Setup the default callback handlers. See the callback functions subsection
# in the ctypes section of the Python documentation for a solid explanation of
# what's going on here.
=====================================
glymur/lib/openjp2.py
=====================================
@@ -7,6 +7,9 @@ import ctypes
import queue
import textwrap
+# 3rd party library imports
+import numpy as np
+
# Local imports
from ..config import glymur_config
@@ -661,8 +664,11 @@ def decode(codec, stream, image):
RuntimeError
If the OpenJPEG library routine opj_decode fails.
"""
- OPENJP2.opj_decode.argtypes = [CODEC_TYPE, STREAM_TYPE_P,
- ctypes.POINTER(ImageType)]
+ OPENJP2.opj_decode.argtypes = [
+ CODEC_TYPE,
+ STREAM_TYPE_P,
+ ctypes.POINTER(ImageType)
+ ]
OPENJP2.opj_decode.restype = check_error
OPENJP2.opj_decode(codec, stream, image)
@@ -691,19 +697,23 @@ def decode_tile_data(codec, tidx, data, data_size, stream):
RuntimeError
If the OpenJPEG library routine opj_decode fails.
"""
- OPENJP2.opj_decode_tile_data.argtypes = [CODEC_TYPE,
- ctypes.c_uint32,
- ctypes.POINTER(ctypes.c_uint8),
- ctypes.c_uint32,
- STREAM_TYPE_P]
+ OPENJP2.opj_decode_tile_data.argtypes = [
+ CODEC_TYPE,
+ ctypes.c_uint32,
+ ctypes.POINTER(ctypes.c_uint8),
+ ctypes.c_uint32,
+ STREAM_TYPE_P
+ ]
OPENJP2.opj_decode_tile_data.restype = check_error
datap = data.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8))
- OPENJP2.opj_decode_tile_data(codec,
- ctypes.c_uint32(tidx),
- datap,
- ctypes.c_uint32(data_size),
- stream)
+ OPENJP2.opj_decode_tile_data(
+ codec,
+ ctypes.c_uint32(tidx),
+ datap,
+ ctypes.c_uint32(data_size),
+ stream
+ )
def create_decompress(codec_format):
@@ -786,10 +796,12 @@ def get_decoded_tile(codec, stream, imagep, tile_index):
RuntimeError
If the OpenJPEG library routine opj_get_decoded_tile fails.
"""
- OPENJP2.opj_get_decoded_tile.argtypes = [CODEC_TYPE,
- STREAM_TYPE_P,
- ctypes.POINTER(ImageType),
- ctypes.c_uint32]
+ OPENJP2.opj_get_decoded_tile.argtypes = [
+ CODEC_TYPE,
+ STREAM_TYPE_P,
+ ctypes.POINTER(ImageType),
+ ctypes.c_uint32
+ ]
OPENJP2.opj_get_decoded_tile.restype = check_error
OPENJP2.opj_get_decoded_tile(codec, stream, imagep, tile_index)
@@ -899,14 +911,18 @@ def image_create(comptparms, clrspc):
image : ImageType
Reference to ImageType instance.
"""
- OPENJP2.opj_image_create.argtypes = [ctypes.c_uint32,
- ctypes.POINTER(ImageComptParmType),
- COLOR_SPACE_TYPE]
+ OPENJP2.opj_image_create.argtypes = [
+ ctypes.c_uint32,
+ ctypes.POINTER(ImageComptParmType),
+ COLOR_SPACE_TYPE
+ ]
OPENJP2.opj_image_create.restype = ctypes.POINTER(ImageType)
- image = OPENJP2.opj_image_create(len(comptparms),
- comptparms,
- clrspc)
+ image = OPENJP2.opj_image_create(
+ len(comptparms),
+ comptparms,
+ clrspc
+ )
return image
@@ -927,15 +943,19 @@ def image_tile_create(comptparms, clrspc):
image : ImageType
Reference to ImageType instance.
"""
- ARGTYPES = [ctypes.c_uint32,
- ctypes.POINTER(ImageComptParmType),
- COLOR_SPACE_TYPE]
+ ARGTYPES = [
+ ctypes.c_uint32,
+ ctypes.POINTER(ImageComptParmType),
+ COLOR_SPACE_TYPE
+ ]
OPENJP2.opj_image_tile_create.argtypes = ARGTYPES
OPENJP2.opj_image_tile_create.restype = ctypes.POINTER(ImageType)
- image = OPENJP2.opj_image_tile_create(len(comptparms),
- comptparms,
- clrspc)
+ image = OPENJP2.opj_image_tile_create(
+ len(comptparms),
+ comptparms,
+ clrspc
+ )
return image
@@ -961,8 +981,11 @@ def read_header(stream, codec):
RuntimeError
If the OpenJPEG library routine opj_read_header fails.
"""
- ARGTYPES = [STREAM_TYPE_P, CODEC_TYPE,
- ctypes.POINTER(ctypes.POINTER(ImageType))]
+ ARGTYPES = [
+ STREAM_TYPE_P,
+ CODEC_TYPE,
+ ctypes.POINTER(ctypes.POINTER(ImageType))
+ ]
OPENJP2.opj_read_header.argtypes = ARGTYPES
OPENJP2.opj_read_header.restype = check_error
@@ -1003,16 +1026,18 @@ def read_tile_header(codec, stream):
RuntimeError
If the OpenJPEG library routine opj_read_tile_header fails.
"""
- ARGTYPES = [CODEC_TYPE,
- STREAM_TYPE_P,
- ctypes.POINTER(ctypes.c_uint32),
- ctypes.POINTER(ctypes.c_uint32),
- ctypes.POINTER(ctypes.c_int32),
- ctypes.POINTER(ctypes.c_int32),
- ctypes.POINTER(ctypes.c_int32),
- ctypes.POINTER(ctypes.c_int32),
- ctypes.POINTER(ctypes.c_uint32),
- ctypes.POINTER(BOOL_TYPE)]
+ ARGTYPES = [
+ CODEC_TYPE,
+ STREAM_TYPE_P,
+ ctypes.POINTER(ctypes.c_uint32),
+ ctypes.POINTER(ctypes.c_uint32),
+ ctypes.POINTER(ctypes.c_int32),
+ ctypes.POINTER(ctypes.c_int32),
+ ctypes.POINTER(ctypes.c_int32),
+ ctypes.POINTER(ctypes.c_int32),
+ ctypes.POINTER(ctypes.c_uint32),
+ ctypes.POINTER(BOOL_TYPE)
+ ]
OPENJP2.opj_read_tile_header.argtypes = ARGTYPES
OPENJP2.opj_read_tile_header.restype = check_error
@@ -1024,25 +1049,29 @@ def read_tile_header(codec, stream):
row1 = ctypes.c_int32()
ncomps = ctypes.c_uint32()
go_on = BOOL_TYPE()
- OPENJP2.opj_read_tile_header(codec,
- stream,
- ctypes.byref(tile_index),
- ctypes.byref(data_size),
- ctypes.byref(col0),
- ctypes.byref(row0),
- ctypes.byref(col1),
- ctypes.byref(row1),
- ctypes.byref(ncomps),
- ctypes.byref(go_on))
+ OPENJP2.opj_read_tile_header(
+ codec,
+ stream,
+ ctypes.byref(tile_index),
+ ctypes.byref(data_size),
+ ctypes.byref(col0),
+ ctypes.byref(row0),
+ ctypes.byref(col1),
+ ctypes.byref(row1),
+ ctypes.byref(ncomps),
+ ctypes.byref(go_on)
+ )
go_on = bool(go_on.value)
- return (tile_index.value,
- data_size.value,
- col0.value,
- row0.value,
- col1.value,
- row1.value,
- ncomps.value,
- go_on)
+ return (
+ tile_index.value,
+ data_size.value,
+ col0.value,
+ row0.value,
+ col1.value,
+ row1.value,
+ ncomps.value,
+ go_on
+ )
def set_decode_area(codec, image, start_x=0, start_y=0, end_x=0, end_y=0):
@@ -1067,19 +1096,62 @@ def set_decode_area(codec, image, start_x=0, start_y=0, end_x=0, end_y=0):
RuntimeError
If the OpenJPEG library routine opj_set_decode_area fails.
"""
- OPENJP2.opj_set_decode_area.argtypes = [CODEC_TYPE,
- ctypes.POINTER(ImageType),
- ctypes.c_int32,
- ctypes.c_int32,
- ctypes.c_int32,
- ctypes.c_int32]
+ OPENJP2.opj_set_decode_area.argtypes = [
+ CODEC_TYPE,
+ ctypes.POINTER(ImageType),
+ ctypes.c_int32,
+ ctypes.c_int32,
+ ctypes.c_int32,
+ ctypes.c_int32
+ ]
OPENJP2.opj_set_decode_area.restype = check_error
- OPENJP2.opj_set_decode_area(codec, image,
- ctypes.c_int32(start_x),
- ctypes.c_int32(start_y),
- ctypes.c_int32(end_x),
- ctypes.c_int32(end_y))
+ OPENJP2.opj_set_decode_area(
+ codec, image,
+ ctypes.c_int32(start_x),
+ ctypes.c_int32(start_y),
+ ctypes.c_int32(end_x),
+ ctypes.c_int32(end_y)
+ )
+
+
+def set_decoded_components(codec, comp_indices):
+ """Wraps openjp2 library function opj_set_decoded_components.
+
+ Restrict the number of components to decode. This function should be
+ called right after read_header.
+
+ Parameters
+ ----------
+ codec : CODEC_TYPE
+ Codec initialized by create_decompress function.
+ comp_indices : list-like
+ The indices of the components to decode (relative to the codestream,
+ starting at 0).
+
+ Raises
+ ------
+ RuntimeError
+ If the OpenJPEG library routine opj_set_decoded_components fails..
+ """
+ comp_indices = np.uint32(comp_indices)
+ OPENJP2.opj_set_decoded_components.argtypes = [
+ CODEC_TYPE,
+ ctypes.c_uint32,
+ ctypes.POINTER(ctypes.c_uint32),
+ ctypes.c_int32
+ ]
+ OPENJP2.opj_set_decoded_components.restype = check_error
+
+ ncomps = len(comp_indices)
+ indices_p = comp_indices.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32))
+
+ # This is always False (0) for now.
+ apply_color_xforms = ctypes.c_int32(0)
+
+ OPENJP2.opj_set_decoded_components(
+ codec, ncomps, indices_p, apply_color_xforms
+ )
def set_default_decoder_parameters():
@@ -1159,9 +1231,11 @@ def set_error_handler(codec, handler, data=None):
RuntimeError
If the OpenJPEG library routine opj_set_error_handler fails.
"""
- OPENJP2.opj_set_error_handler.argtypes = [CODEC_TYPE,
- ctypes.c_void_p,
- ctypes.c_void_p]
+ OPENJP2.opj_set_error_handler.argtypes = [
+ CODEC_TYPE,
+ ctypes.c_void_p,
+ ctypes.c_void_p
+ ]
OPENJP2.opj_set_error_handler.restype = check_error
OPENJP2.opj_set_error_handler(codec, handler, data)
@@ -1185,9 +1259,9 @@ def set_info_handler(codec, handler, data=None):
RuntimeError
If the OpenJPEG library routine opj_set_info_handler fails.
"""
- OPENJP2.opj_set_info_handler.argtypes = [CODEC_TYPE,
- ctypes.c_void_p,
- ctypes.c_void_p]
+ OPENJP2.opj_set_info_handler.argtypes = [
+ CODEC_TYPE, ctypes.c_void_p, ctypes.c_void_p
+ ]
OPENJP2.opj_set_info_handler.restype = check_error
OPENJP2.opj_set_info_handler(codec, handler, data)
@@ -1211,9 +1285,9 @@ def set_warning_handler(codec, handler, data=None):
RuntimeError
If the OpenJPEG library routine opj_set_warning_handler fails.
"""
- OPENJP2.opj_set_warning_handler.argtypes = [CODEC_TYPE,
- ctypes.c_void_p,
- ctypes.c_void_p]
+ OPENJP2.opj_set_warning_handler.argtypes = [
+ CODEC_TYPE, ctypes.c_void_p, ctypes.c_void_p
+ ]
OPENJP2.opj_set_warning_handler.restype = check_error
OPENJP2.opj_set_warning_handler(codec, handler, data)
@@ -1263,9 +1337,11 @@ def setup_encoder(codec, cparams, image):
RuntimeError
If the OpenJPEG library routine opj_setup_encoder fails.
"""
- ARGTYPES = [CODEC_TYPE,
- ctypes.POINTER(CompressionParametersType),
- ctypes.POINTER(ImageType)]
+ ARGTYPES = [
+ CODEC_TYPE,
+ ctypes.POINTER(CompressionParametersType),
+ ctypes.POINTER(ImageType)
+ ]
OPENJP2.opj_setup_encoder.argtypes = ARGTYPES
OPENJP2.opj_setup_encoder.restype = check_error
OPENJP2.opj_setup_encoder(codec, ctypes.byref(cparams), image)
@@ -1290,9 +1366,9 @@ def start_compress(codec, image, stream):
RuntimeError
If the OpenJPEG library routine opj_start_compress fails.
"""
- OPENJP2.opj_start_compress.argtypes = [CODEC_TYPE,
- ctypes.POINTER(ImageType),
- STREAM_TYPE_P]
+ OPENJP2.opj_start_compress.argtypes = [
+ CODEC_TYPE, ctypes.POINTER(ImageType), STREAM_TYPE_P
+ ]
OPENJP2.opj_start_compress.restype = check_error
OPENJP2.opj_start_compress(codec, image, stream)
@@ -1353,7 +1429,7 @@ def write_tile(codec, tile_index, data, *pargs):
tile_index : int
The index of the tile to write, zero-indexing assumed
data : array
- Image data arranged in usual C-order
+ Image data. The memory layout is planar, not the usual C-order.
data_size : int, optional
Size of a tile in bytes. If not provided, it will be inferred.
stream : STREAM_TYPE_P
@@ -1372,19 +1448,23 @@ def write_tile(codec, tile_index, data, *pargs):
data_size = data.nbytes
stream = pargs[0]
- OPENJP2.opj_write_tile.argtypes = [CODEC_TYPE,
- ctypes.c_uint32,
- ctypes.POINTER(ctypes.c_uint8),
- ctypes.c_uint32,
- STREAM_TYPE_P]
+ OPENJP2.opj_write_tile.argtypes = [
+ CODEC_TYPE,
+ ctypes.c_uint32,
+ ctypes.POINTER(ctypes.c_uint8),
+ ctypes.c_uint32,
+ STREAM_TYPE_P
+ ]
OPENJP2.opj_write_tile.restype = check_error
datap = data.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8))
- OPENJP2.opj_write_tile(codec,
- ctypes.c_uint32(int(tile_index)),
- datap,
- ctypes.c_uint32(int(data_size)),
- stream)
+ OPENJP2.opj_write_tile(
+ codec,
+ ctypes.c_uint32(int(tile_index)),
+ datap,
+ ctypes.c_uint32(int(data_size)),
+ stream
+ )
def set_error_message(msg):
=====================================
glymur/version.py
=====================================
@@ -20,7 +20,7 @@ from .lib import openjp2 as opj2
# Do not change the format of this next line! Doing so risks breaking
# setup.py
-version = "0.9.3"
+version = "0.9.4"
_sv = LooseVersion(version)
version_tuple = _sv.version
=====================================
setup.py
=====================================
@@ -24,7 +24,6 @@ kwargs = {
kwargs['classifiers'] = [
"Programming Language :: Python",
- "Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
=====================================
tests/test_callbacks.py
=====================================
@@ -7,6 +7,9 @@ import warnings
import unittest
from unittest.mock import patch
+# 3rd party library imports
+import skimage.data
+
# Local imports ...
import glymur
from . import fixtures
@@ -66,3 +69,26 @@ class TestSuite(fixtures.TestCommon):
actual = fake_out.getvalue().strip()
self.assertIn('[INFO]', actual)
+
+ def test_info_callbacks_on_writing_tiles(self):
+ """
+ SCENARIO: the verbose attribute is set to True
+
+ EXPECTED RESULT: The info callback handler should be enabled. There
+ should be [INFO] output present in sys.stdout.
+ """
+ jp2_data = skimage.data.moon()
+
+ shape = jp2_data.shape[0] * 3, jp2_data.shape[1] * 2
+ tilesize = (jp2_data.shape[0], jp2_data.shape[1])
+
+ j = glymur.Jp2k(
+ self.temp_jp2_filename, shape=shape, tilesize=tilesize,
+ verbose=True
+ )
+ with patch('sys.stdout', new=StringIO()) as fake_out:
+ for tw in j.get_tilewriters():
+ tw[:] = jp2_data
+ actual = fake_out.getvalue().strip()
+
+ self.assertIn('[INFO] tile number', actual)
=====================================
tests/test_codestream.py
=====================================
@@ -4,11 +4,7 @@ Test suite for codestream oddities
"""
# Standard library imports ...
-try:
- import importlib.resources as ir
-except ImportError: # pragma: no cover
- # before 3.7
- import importlib_resources as ir
+import importlib.resources as ir
from io import BytesIO
import struct
import unittest
=====================================
tests/test_colour_specification_box.py
=====================================
@@ -5,11 +5,7 @@ Test suite specifically targeting ICC profiles
# Standard library imports ...
from datetime import datetime
-try:
- import importlib.resources as ir
-except ImportError: # pragma: no cover
- # before 3.7
- import importlib_resources as ir
+import importlib.resources as ir
import struct
import tempfile
import unittest
=====================================
tests/test_jp2box.py
=====================================
@@ -2,11 +2,7 @@
"""
# Standard library imports ...
import doctest
-try:
- import importlib.resources as ir
-except ImportError: # pragma: no cover
- # before 3.7
- import importlib_resources as ir
+import importlib.resources as ir
from io import BytesIO
import os
import pathlib
=====================================
tests/test_jp2box_jpx.py
=====================================
@@ -4,11 +4,7 @@ Test suite specifically targeting JPX box layout.
"""
# Standard library imports ...
import ctypes
-try:
- import importlib.resources as ir
-except ImportError: # pragma: no cover
- # before 3.7
- import importlib_resources as ir
+import importlib.resources as ir
from io import BytesIO
import shutil
import struct
=====================================
tests/test_jp2box_uuid.py
=====================================
@@ -2,11 +2,7 @@
"""Test suite for printing.
"""
# Standard library imports
-try:
- import importlib.resources as ir
-except ImportError: # pragma: no cover
- # before 3.7
- import importlib_resources as ir
+import importlib.resources as ir
import io
import shutil
import struct
=====================================
tests/test_jp2box_xml.py
=====================================
@@ -3,11 +3,7 @@
Test suite specifically targeting the JP2 XML box layout.
"""
# Standard library imports
-try:
- import importlib.resources as ir
-except ImportError: # pragma: no cover
- # before 3.7
- import importlib_resources as ir
+import importlib.resources as ir
from io import BytesIO
import pathlib
import struct
=====================================
tests/test_jp2k.py
=====================================
@@ -5,11 +5,7 @@ Tests for general glymur functionality.
import collections
import datetime
import doctest
-try:
- import importlib.resources as ir
-except ImportError: # pragma: no cover
- # before 3.7
- import importlib_resources as ir
+import importlib.resources as ir
from io import BytesIO
import os
import pathlib
@@ -95,7 +91,7 @@ class TestSliceProtocolBaseWrite(SliceProtocolBase):
def test_basic_write(self):
"""
- SCENARIO: write image by specifying image data in constructor
+ SCENARIO: write image by specifying image data with slice protocol
EXPECTED RESULT: image is validated
"""
=====================================
tests/test_openjp2.py
=====================================
@@ -2,16 +2,19 @@
Tests for libopenjp2 wrapping functions.
"""
# Standard library imports ...
+from contextlib import ExitStack
from io import StringIO
import unittest
from unittest.mock import patch
# Third party library imports ...
import numpy as np
+import skimage.data
# Local imports ...
import glymur
from glymur.lib import openjp2
+from glymur.jp2k import _INFO_CALLBACK, _WARNING_CALLBACK, _ERROR_CALLBACK
from . import fixtures
from .fixtures import OPENJPEG_NOT_AVAILABLE, OPENJPEG_NOT_AVAILABLE_MSG
@@ -181,6 +184,207 @@ class TestOpenJP2(fixtures.TestCommon):
xtx5_setup(filename, short_sig=True)
self.assertTrue(True)
+ def test_tile_write_moon(self):
+ """
+ Test writing tiles for a 2D image.
+ """
+ img = skimage.data.moon()
+
+ num_comps = 1
+ image_height, image_width = img.shape
+
+ tile_height, tile_width = 256, 256
+
+ comp_prec = 8
+ irreversible = True
+
+ cblockh_init, cblockw_init = 64, 64
+
+ numresolution = 6
+
+ cparams = openjp2.set_default_encoder_parameters()
+
+ cparams.tile_size_on = openjp2.TRUE
+ cparams.cp_tdx = tile_width
+ cparams.cp_tdy = tile_height
+
+ cparams.cblockw_init, cparams.cblockh_init = cblockw_init, cblockh_init
+
+ cparams.irreversible = 1 if irreversible else 0
+
+ cparams.numresolution = numresolution
+ cparams.prog_order = glymur.core.PROGRESSION_ORDER['LRCP']
+
+ # greyscale so no mct
+ cparams.tcp_mct = 0
+
+ # comptparms == l_params
+ comptparms = (openjp2.ImageComptParmType * num_comps)()
+ for j in range(num_comps):
+ comptparms[j].dx = 1
+ comptparms[j].dy = 1
+ comptparms[j].w = tile_width
+ comptparms[j].h = tile_height
+ comptparms[j].x0 = 0
+ comptparms[j].y0 = 0
+ comptparms[j].prec = comp_prec
+ comptparms[j].bpp = comp_prec
+ comptparms[j].sgnd = 0
+
+ with ExitStack() as stack:
+
+ codec = openjp2.create_compress(openjp2.CODEC_J2K)
+ stack.callback(openjp2.destroy_codec, codec)
+
+ info_handler = _INFO_CALLBACK
+
+ openjp2.set_info_handler(codec, info_handler)
+ openjp2.set_warning_handler(codec, _WARNING_CALLBACK)
+ openjp2.set_error_handler(codec, _ERROR_CALLBACK)
+
+ # l_params == comptparms
+ # l_image == tile
+ image = openjp2.image_tile_create(comptparms, openjp2.CLRSPC_GRAY)
+ stack.callback(openjp2.image_destroy, image)
+
+ image.contents.x0, image.contents.y0 = 0, 0
+ image.contents.x1, image.contents.y1 = image_width, image_height
+ image.contents.color_space = openjp2.CLRSPC_GRAY
+
+ openjp2.setup_encoder(codec, cparams, image)
+
+ filename = str(self.temp_j2k_filename)
+ strm = openjp2.stream_create_default_file_stream(filename, False)
+ stack.callback(openjp2.stream_destroy, strm)
+
+ openjp2.start_compress(codec, image, strm)
+
+ openjp2.write_tile(codec, 0, img[0:256, 0:256].copy(), strm)
+ openjp2.write_tile(codec, 1, img[0:256, 256:512].copy(), strm)
+ openjp2.write_tile(codec, 2, img[256:512, 0:256].copy(), strm)
+ openjp2.write_tile(codec, 3, img[256:512, 256:512].copy(), strm)
+
+ openjp2.end_compress(codec, strm)
+
+ def test_write_tiles_3D(self):
+ """
+ Test writing tiles for an RGB image.
+ """
+
+ img = skimage.data.astronaut()
+
+ image_height, image_width, num_comps = img.shape
+
+ tile_height, tile_width = 256, 256
+
+ comp_prec = 8
+ irreversible = False
+
+ cblockh_init, cblockw_init = 64, 64
+
+ numresolution = 6
+
+ cparams = openjp2.set_default_encoder_parameters()
+
+ outfile = str(self.temp_j2k_filename).encode()
+ num_pad_bytes = openjp2.PATH_LEN - len(outfile)
+ outfile += b'0' * num_pad_bytes
+ cparams.outfile = outfile
+
+ # not from openjpeg test file
+ cparams.cp_disto_alloc = 1
+
+ cparams.tile_size_on = openjp2.TRUE
+ cparams.cp_tdx = tile_width
+ cparams.cp_tdy = tile_height
+
+ cparams.cblockw_init, cparams.cblockh_init = cblockw_init, cblockh_init
+
+ # not from openjpeg test file
+ cparams.mode = 0
+
+ cparams.irreversible = 1 if irreversible else 0
+
+ cparams.numresolution = numresolution
+ cparams.prog_order = glymur.core.PROGRESSION_ORDER['LRCP']
+
+ cparams.tcp_mct = 1
+
+ cparams.tcp_numlayers = 1
+ cparams.tcp_rates[0] = 0
+ cparams.tcp_distoratio[0] = 0
+
+ # comptparms == l_params
+ comptparms = (openjp2.ImageComptParmType * num_comps)()
+ for j in range(num_comps):
+ comptparms[j].dx = 1
+ comptparms[j].dy = 1
+ comptparms[j].w = image_width
+ comptparms[j].h = image_height
+ comptparms[j].x0 = 0
+ comptparms[j].y0 = 0
+ comptparms[j].prec = comp_prec
+ comptparms[j].bpp = comp_prec
+ comptparms[j].sgnd = 0
+
+ with ExitStack() as stack:
+ codec = openjp2.create_compress(openjp2.CODEC_J2K)
+ stack.callback(openjp2.destroy_codec, codec)
+
+ info_handler = _INFO_CALLBACK
+
+ openjp2.set_info_handler(codec, info_handler)
+ openjp2.set_warning_handler(codec, _WARNING_CALLBACK)
+ openjp2.set_error_handler(codec, _ERROR_CALLBACK)
+
+ image = openjp2.image_tile_create(comptparms, openjp2.CLRSPC_SRGB)
+ stack.callback(openjp2.image_destroy, image)
+
+ image.contents.x0, image.contents.y0 = 0, 0
+ image.contents.x1, image.contents.y1 = image_width, image_height
+ image.contents.color_space = openjp2.CLRSPC_SRGB
+
+ openjp2.setup_encoder(codec, cparams, image)
+
+ filename = str(self.temp_j2k_filename)
+ strm = openjp2.stream_create_default_file_stream(filename, False)
+ stack.callback(openjp2.stream_destroy, strm)
+
+ openjp2.start_compress(codec, image, strm)
+
+ # have to change the memory layout of 3D images in order to use
+ # opj_write_tile
+ openjp2.write_tile(
+ codec, 0, _set_planar_pixel_order(img[0:256, 0:256, :]), strm
+ )
+ openjp2.write_tile(
+ codec, 1, _set_planar_pixel_order(img[0:256, 256:512, :]), strm
+ )
+ openjp2.write_tile(
+ codec, 2, _set_planar_pixel_order(img[256:512, 0:256, :]), strm
+ )
+ openjp2.write_tile(
+ codec, 3, _set_planar_pixel_order(img[256:512, 256:512, :]),
+ strm
+ )
+
+ openjp2.end_compress(codec, strm)
+
+
+def _set_planar_pixel_order(img):
+ """
+ Reorder the image pixels so that plane-0 comes first, then plane-1, etc.
+ This is a requirement for using opj_write_tile.
+ """
+ if img.ndim == 3:
+ # C-order increments along the y-axis slowest (0), then x-axis (1),
+ # then z-axis (2). We want it to go along the z-axis slowest, then
+ # y-axis, then x-axis.
+ img = np.swapaxes(img, 1, 2)
+ img = np.swapaxes(img, 0, 1)
+
+ return img.copy()
+
def tile_encoder(**kwargs):
"""Fixture used by many tests."""
=====================================
tests/test_printing.py
=====================================
@@ -3,11 +3,7 @@
Test suite for printing.
"""
# Standard library imports ...
-try:
- import importlib.resources as ir
-except ImportError: # pragma: no cover
- # before 3.7
- import importlib_resources as ir
+import importlib.resources as ir
from io import BytesIO, StringIO
import struct
import sys
=====================================
tests/test_set_decoded_components.py
=====================================
@@ -0,0 +1,196 @@
+# standard library imports
+import importlib.resources as ir
+import pathlib
+import shutil
+import tempfile
+import unittest
+
+# 3rd party library imports
+import numpy as np
+
+# local imports
+import glymur
+from glymur import Jp2k
+
+
+class TestSuite(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(self):
+ """
+ We need a test image without the MCT and with at least 2 quality
+ layers.
+ """
+ self.testdir = tempfile.mkdtemp()
+
+ # windows won't like it if we try using tempfile.NamedTemporaryFile
+ # here
+ self.j2kfile = pathlib.Path(self.testdir) / 'tmp.j2k'
+
+ data = Jp2k(glymur.data.goodstuff())[:]
+ Jp2k(self.j2kfile.name, data=data, mct=False, cratios=[200, 100, 50])
+
+ @classmethod
+ def tearDownClass(self):
+ shutil.rmtree(self.testdir)
+
+ def test_one_component(self):
+ """
+ SCENARIO: Decode the 1st component of an RGB image. Then restore
+ the original configuration of reading all bands.
+
+ EXPECTED RESULT: the data matches what we get from the regular way.
+ """
+ j2k = Jp2k(self.j2kfile.name)
+ expected = j2k[:, :, 0]
+
+ j2k.decoded_components = 0
+ actual = j2k[:]
+
+ np.testing.assert_array_equal(actual, expected)
+
+ # restore the original configuration
+ j2k.decoded_components = None
+ actual = j2k[:]
+ self.assertEqual(actual.shape, (800, 480, 3))
+
+ def test_second_component(self):
+ """
+ SCENARIO: Decode the 2nd component of a non-MCT image.
+
+ EXPECTED RESULT: Match the 2nd component read in the regular way.
+ """
+ j2k = Jp2k(self.j2kfile.name)
+ expected = j2k[:, :, 1]
+
+ j2k.decoded_components = 1
+ actual = j2k[:]
+
+ np.testing.assert_array_equal(actual, expected)
+
+ def test_three_components_without_MCT(self):
+ """
+ SCENARIO: Decode three components without using the MCT.
+
+ EXPECTED RESULT: Match the results the regular way.
+ """
+
+ j2k = Jp2k(self.j2kfile.name)
+
+ expected = j2k[:]
+
+ j2k.decoded_components = [0, 1, 2]
+ actual = j2k[:]
+
+ np.testing.assert_array_equal(actual, expected)
+
+ def test_partial_component_decoding_with_area(self):
+ """
+ SCENARIO: Decode one component with a specific area.
+
+ EXPECTED RESULT: Match the results the regular way.
+ """
+ j2k = Jp2k(self.j2kfile.name)
+
+ expected = j2k[20:40, 10:30, 0]
+
+ j2k.decoded_components = 0
+ actual = j2k[20:40, 10:30]
+
+ np.testing.assert_array_equal(actual, expected)
+
+ def test_layer(self):
+ """
+ SCENARIO: Decode one component with a particular layer
+
+ EXPECTED RESULT: Match the results the regular way.
+ """
+
+ j2k = Jp2k(self.j2kfile.name)
+ j2k.layer = 1
+
+ expected = j2k[:, :, 0]
+
+ j2k.decoded_components = 0
+ actual = j2k[:]
+
+ np.testing.assert_array_equal(actual, expected)
+
+ def test_reduced_resolution(self):
+ """
+ SCENARIO: Decode one component with reduced resolution.
+
+ EXPECTED RESULT: Match the results the regular way.
+ """
+
+ j2k = Jp2k(self.j2kfile.name)
+
+ expected = j2k[::2, ::2, 0]
+
+ j2k.decoded_components = [0]
+ actual = j2k[::2, ::2]
+
+ np.testing.assert_array_equal(actual, expected)
+
+ def test_negative_component(self):
+ """
+ SCENARIO: Provide a negative component.
+
+ EXPECTED RESULT: exception
+ """
+
+ j2k = Jp2k(self.j2kfile.name)
+
+ with self.assertRaises(glymur.lib.openjp2.OpenJPEGLibraryError):
+ j2k.decoded_components = -1
+ j2k[:]
+
+ def test_same_component_several_times(self):
+ """
+ SCENARIO: Decode one component multiple times.
+
+ EXPECTED RESULT: exception
+ """
+
+ j2k = Jp2k(self.j2kfile.name)
+
+ with self.assertRaises(glymur.lib.openjp2.OpenJPEGLibraryError):
+ j2k.decoded_components = [0, 0]
+ j2k[:]
+
+ def test_invalid_component(self):
+ """
+ SCENARIO: Decode an invalid component.
+
+ EXPECTED RESULT: exception
+ """
+ j2k = Jp2k(self.j2kfile.name)
+
+ with self.assertRaises(ValueError):
+ j2k.decoded_components = 10
+
+ def test_differing_subsamples(self):
+ """
+ SCENARIO: Decode a component where other components have different
+ subsamples.
+
+ EXPECTED RESULT: success, trying to read that component without
+ setting decoded_components would require us to use the read_bands
+ method.
+ """
+ with ir.path('tests.data', 'p0_06.j2k') as path:
+ j2k = Jp2k(path)
+
+ expected = j2k.read_bands()[0]
+
+ j2k.decoded_components = 0
+ actual = j2k[:]
+
+ np.testing.assert_array_equal(actual, expected)
+
+ # verify that without using decoded components, we cannot read the
+ # image using the slice protocol
+ with ir.path('tests.data', 'p0_06.j2k') as path:
+ j2k = Jp2k(path)
+ with self.assertRaises(RuntimeError):
+ j2k[:, :, 0]
=====================================
tests/test_warnings.py
=====================================
@@ -3,11 +3,7 @@ Test suite for warnings issued by glymur.
"""
# Standard library imports
import codecs
-try:
- import importlib.resources as ir
-except ImportError: # pragma: no cover
- # before 3.7
- import importlib_resources as ir
+import importlib.resources as ir
from io import BytesIO
import struct
import unittest
=====================================
tests/test_writing_tiles.py
=====================================
@@ -0,0 +1,154 @@
+# 3rd party library imports
+import skimage.io
+import numpy as np
+
+# local imports
+import glymur
+from glymur import Jp2k
+from . import fixtures
+
+
+class TestSuite(fixtures.TestCommon):
+ """
+ Test suite for writing with tiles.
+ """
+ def test_astronaut(self):
+ """
+ SCENARIO: construct a j2k file by tiling an image in a 2x2 grid..
+
+ EXPECTED RESULT: the written image validates
+ """
+ j2k_data = skimage.data.astronaut()
+ data = [
+ j2k_data[:256, :256, :],
+ j2k_data[:256, 256:512, :],
+ j2k_data[256:512, :256, :],
+ j2k_data[256:512, 256:512, :],
+ ]
+
+ shape = j2k_data.shape
+ tilesize = 256, 256
+
+ j = Jp2k(self.temp_j2k_filename, shape=shape, tilesize=tilesize)
+ for idx, tw in enumerate(j.get_tilewriters()):
+ tw[:] = data[idx]
+
+ new_j = Jp2k(self.temp_j2k_filename)
+ actual = new_j[:]
+ expected = j2k_data
+ np.testing.assert_array_equal(actual, expected)
+
+ def test_smoke(self):
+ """
+ SCENARIO: construct a j2k file by repeating a 3D image in a 2x2 grid.
+
+ EXPECTED RESULT: the written image matches the 2x2 grid
+ """
+ j2k_data = skimage.data.astronaut()
+
+ shape = (
+ j2k_data.shape[0] * 2, j2k_data.shape[1] * 2, j2k_data.shape[2]
+ )
+ tilesize = (j2k_data.shape[0], j2k_data.shape[1])
+
+ j = Jp2k(self.temp_j2k_filename, shape=shape, tilesize=tilesize)
+ for tw in j.get_tilewriters():
+ tw[:] = j2k_data
+
+ new_j = Jp2k(self.temp_j2k_filename)
+ actual = new_j[:]
+ expected = np.tile(j2k_data, (2, 2, 1))
+ np.testing.assert_array_equal(actual, expected)
+
+ def test_moon(self):
+ """
+ SCENARIO: construct a jp2 file by repeating a 2D image in a 3x2 grid.
+
+ EXPECTED RESULT: the written image matches the 3x2 grid
+ """
+ jp2_data = skimage.data.moon()
+
+ shape = jp2_data.shape[0] * 3, jp2_data.shape[1] * 2
+ tilesize = (jp2_data.shape[0], jp2_data.shape[1])
+
+ j = Jp2k(self.temp_jp2_filename, shape=shape, tilesize=tilesize)
+ for tw in j.get_tilewriters():
+ tw[:] = jp2_data
+
+ new_j = Jp2k(self.temp_jp2_filename)
+ actual = new_j[:]
+ expected = np.tile(jp2_data, (3, 2))
+ np.testing.assert_array_equal(actual, expected)
+
+ def test_tile_slice_has_non_none_elements(self):
+ """
+ SCENARIO: construct a jp2 file by repeating a 2D image in a 2x2 grid,
+ but the tile writer does not receive a degenerate slice object.
+
+ EXPECTED RESULT: RuntimeError
+ """
+ jp2_data = skimage.data.moon()
+
+ shape = jp2_data.shape[0] * 2, jp2_data.shape[1] * 2
+ tilesize = (jp2_data.shape[0], jp2_data.shape[1])
+
+ j = Jp2k(self.temp_jp2_filename, shape=shape, tilesize=tilesize)
+ with self.assertRaises(RuntimeError):
+ for tw in j.get_tilewriters():
+ tw[:256, :256] = jp2_data
+
+ def test_tile_slice_is_ellipsis(self):
+ """
+ SCENARIO: construct a jp2 file by repeating a 2D image in a 2x2 grid,
+ but the tile writer does not receive a degenerate slice object.
+
+ EXPECTED RESULT: RuntimeError
+ """
+ jp2_data = skimage.data.moon()
+
+ shape = jp2_data.shape[0] * 2, jp2_data.shape[1] * 2
+ tilesize = (jp2_data.shape[0], jp2_data.shape[1])
+
+ j = Jp2k(self.temp_jp2_filename, shape=shape, tilesize=tilesize)
+ with self.assertRaises(RuntimeError):
+ for tw in j.get_tilewriters():
+ tw[...] = jp2_data
+
+ def test_too_much_data_for_slice(self):
+ """
+ SCENARIO: construct a jp2 file by repeating a 2D image in a 2x2 grid,
+ but the tile writer does not receive a degenerate slice object.
+
+ EXPECTED RESULT: RuntimeError
+ """
+ jp2_data = skimage.data.moon()
+
+ shape = jp2_data.shape[0] * 2, jp2_data.shape[1] * 2
+ tilesize = (jp2_data.shape[0], jp2_data.shape[1])
+
+ j = Jp2k(self.temp_jp2_filename, shape=shape, tilesize=tilesize)
+ with self.assertRaises(glymur.lib.openjp2.OpenJPEGLibraryError):
+ for tw in j.get_tilewriters():
+ tw[:] = np.tile(jp2_data, (2, 2))
+
+ def test_write_with_different_compression_ratios(self):
+ """
+ SCENARIO: construct a jp2 file by repeating a 2D image in a 2x2 grid.
+
+ EXPECTED RESULT: There are three layers.
+ """
+ jp2_data = skimage.data.moon()
+
+ shape = jp2_data.shape[0] * 2, jp2_data.shape[1] * 2
+ tilesize = (jp2_data.shape[0], jp2_data.shape[1])
+
+ j = Jp2k(
+ self.temp_jp2_filename, shape=shape, tilesize=tilesize,
+ cratios=[20, 5, 1]
+ )
+
+ for tw in j.get_tilewriters():
+ tw[:] = jp2_data
+
+ codestream = j.get_codestream()
+ self.assertEqual(codestream.segment[2].layers, 3) # layers = 3
View it on GitLab: https://salsa.debian.org/debian-gis-team/glymur/-/commit/ab1774db59e4ae7ed02768e77a09d8fd9d259e0e
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/glymur/-/commit/ab1774db59e4ae7ed02768e77a09d8fd9d259e0e
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20210904/dd27ebd6/attachment-0001.htm>
More information about the Pkg-grass-devel
mailing list