Glymur 0.12.9.post1__py3-none-any.whl → 0.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
glymur/jp2kr.py ADDED
@@ -0,0 +1,982 @@
1
+ """This file is part of glymur, a Python interface for accessing JPEG 2000.
2
+
3
+ http://glymur.readthedocs.org
4
+
5
+ Copyright 2013 John Evans
6
+
7
+ License: MIT
8
+ """
9
+ # Standard library imports...
10
+ from __future__ import annotations
11
+ from contextlib import ExitStack
12
+ from itertools import filterfalse
13
+ import ctypes
14
+ import pathlib
15
+ import re
16
+ import struct
17
+ import warnings
18
+
19
+ # Third party library imports
20
+ import numpy as np
21
+
22
+ # Local imports...
23
+ from .codestream import Codestream
24
+ from . import core, version, get_option
25
+ from .jp2box import Jp2kBox, FileTypeBox, InvalidJp2kError
26
+ from .lib import openjp2 as opj2
27
+
28
+
29
+ class Jp2kr(Jp2kBox):
30
+ """Read JPEG 2000 files.
31
+
32
+ Attributes
33
+ ----------
34
+ box : sequence
35
+ List of top-level boxes in the file. Each box may in turn contain
36
+ its own list of boxes. Will be empty if the file consists only of a
37
+ raw codestream.
38
+ filename : str
39
+ The path to the JPEG 2000 file.
40
+ path : Path
41
+ The path to the JPEG 2000 file.
42
+
43
+ Examples
44
+ --------
45
+ >>> import glymur
46
+ >>> jfile = glymur.data.nemo()
47
+ >>> jp2 = glymur.Jp2kr(jfile)
48
+ >>> jp2.shape
49
+ (1456, 2592, 3)
50
+ >>> image = jp2[:]
51
+ >>> image.shape
52
+ (1456, 2592, 3)
53
+
54
+ Read a lower resolution thumbnail.
55
+
56
+ >>> thumbnail = jp2[::2, ::2]
57
+ >>> thumbnail.shape
58
+ (728, 1296, 3)
59
+ """
60
+
61
+ def __init__(
62
+ self,
63
+ filename: str | pathlib.Path,
64
+ verbose: bool = False,
65
+ **kwargs
66
+ ):
67
+ """
68
+ Parameters
69
+ ----------
70
+ filename : str or Path
71
+ Interpreted as a path to a JPEG 2000 file.
72
+ verbose : bool
73
+ If true, print informational messages produced by the OpenJPEG
74
+ library.
75
+ """
76
+ super().__init__()
77
+
78
+ # In case of pathlib.Paths...
79
+ self.filename = str(filename)
80
+ self.path = pathlib.Path(self.filename)
81
+
82
+ # Setup some default attributes
83
+ self.box = []
84
+ self._codestream = None
85
+ self._decoded_components = None
86
+ self._dtype = None
87
+ self._ignore_pclr_cmap_cdef = False
88
+ self._layer = 0
89
+ self._ndim = None
90
+ self._parse_count = 0
91
+ self._verbose = verbose
92
+
93
+ if not self.path.exists():
94
+ raise FileNotFoundError(f"{self.filename} does not exist.")
95
+
96
+ self.parse()
97
+ self._initialize_shape()
98
+
99
+ def _initialize_shape(self):
100
+ """If there was no image data provided and if no shape was
101
+ initially provisioned, then shape must be computed AFTER we
102
+ have parsed the input file.
103
+ """
104
+ if self._codec_format == opj2.CODEC_J2K:
105
+ # get the image size from the codestream
106
+ cstr = self.codestream
107
+ height = cstr.segment[1].ysiz
108
+ width = cstr.segment[1].xsiz
109
+ num_components = len(cstr.segment[1].xrsiz)
110
+ else:
111
+ # try to get the image size from the IHDR box
112
+ jp2h = [box for box in self.box if box.box_id == 'jp2h'][0]
113
+ ihdr = [box for box in jp2h.box if box.box_id == 'ihdr'][0]
114
+
115
+ height, width = ihdr.height, ihdr.width
116
+ num_components = ihdr.num_components
117
+
118
+ if num_components == 1:
119
+ # but if there is a PCLR box, then we need to check
120
+ # that as well, as that turns a single-channel image
121
+ # into a multi-channel image
122
+ pclr = [box for box in jp2h.box if box.box_id == 'pclr']
123
+ if len(pclr) > 0:
124
+ num_components = len(pclr[0].signed)
125
+
126
+ if num_components == 1:
127
+ self.shape = (height, width)
128
+ else:
129
+ self.shape = (height, width, num_components)
130
+
131
+ return self._shape
132
+
133
+ @property
134
+ def ignore_pclr_cmap_cdef(self):
135
+ """If true, ignore the pclr, cmap, or cdef boxes during any
136
+ color transformation. Defaults to false.
137
+ """
138
+ return self._ignore_pclr_cmap_cdef
139
+
140
+ @ignore_pclr_cmap_cdef.setter
141
+ def ignore_pclr_cmap_cdef(self, ignore_pclr_cmap_cdef):
142
+ self._ignore_pclr_cmap_cdef = ignore_pclr_cmap_cdef
143
+
144
+ @property
145
+ def decoded_components(self):
146
+ """If true, decode only these components. The MCT will not be used.
147
+ List or scalar or None (default).
148
+ """
149
+ return self._decoded_components
150
+
151
+ @decoded_components.setter
152
+ def decoded_components(self, components):
153
+
154
+ if components is None:
155
+ # This is ok. It is a special case where we are restoring the
156
+ # original behavior of reading all bands.
157
+ self._decoded_components = components
158
+ return
159
+
160
+ if np.isscalar(components):
161
+ # turn it into a list to be general
162
+ components = [components]
163
+
164
+ if any(x > len(self.codestream.segment[1].xrsiz) for x in components):
165
+
166
+ msg = (
167
+ f"{components} has at least one invalid component, "
168
+ f"cannot be greater than "
169
+ f"{len(self.codestream.segment[1].xrsiz)}."
170
+ )
171
+ raise ValueError(msg)
172
+
173
+ elif any(x < 0 for x in components):
174
+
175
+ msg = (
176
+ f"{components} has at least one invalid component, "
177
+ f"components cannot be negative."
178
+ )
179
+ raise ValueError(msg)
180
+
181
+ self._decoded_components = components
182
+
183
+ @property
184
+ def layer(self):
185
+ """Zero-based number of quality layer to decode. Defaults to 0, the
186
+ highest quality layer.
187
+ """
188
+ return self._layer
189
+
190
+ @layer.setter
191
+ def layer(self, layer):
192
+ # Set to the indicated value so long as it is valid.
193
+ cod = [
194
+ segment for segment in self.codestream.segment
195
+ if segment.marker_id == 'COD'
196
+ ][0]
197
+ if layer < 0 or layer >= cod.layers:
198
+ msg = f"Invalid layer number, must be in range [0, {cod.layers})."
199
+ raise ValueError(msg)
200
+
201
+ self._layer = layer
202
+
203
+ @property
204
+ def dtype(self):
205
+ """Datatype of the image."""
206
+ if self._dtype is None:
207
+ c = self.get_codestream()
208
+ bps0 = c.segment[1].bitdepth[0]
209
+ sgnd0 = c.segment[1].signed[0]
210
+
211
+ if (
212
+ not all(bitdepth == bps0 for bitdepth in c.segment[1].bitdepth)
213
+ or not all(signed == sgnd0 for signed in c.segment[1].signed)
214
+ ):
215
+ msg = (
216
+ "The dtype property is only valid when all components "
217
+ "have the same bitdepth and sign. "
218
+ "\n\n"
219
+ f"{c.segment[1]}"
220
+ )
221
+ raise TypeError(msg)
222
+
223
+ if bps0 <= 8:
224
+ self._dtype = np.int8 if sgnd0 else np.uint8
225
+ else:
226
+ self._dtype = np.int16 if sgnd0 else np.uint16
227
+
228
+ return self._dtype
229
+
230
+ @property
231
+ def ndim(self):
232
+ """Number of image dimensions."""
233
+ return len(self.shape)
234
+
235
+ @property
236
+ def codestream(self):
237
+ """Metadata for JP2 or J2K codestream (header only)."""
238
+ if self._codestream is None:
239
+ self._codestream = self.get_codestream(header_only=True)
240
+ return self._codestream
241
+
242
+ @property
243
+ def tilesize(self):
244
+ """Height and width of the image tiles."""
245
+ return self._tilesize
246
+
247
+ @property
248
+ def verbose(self):
249
+ """If true, print informational messages produced by the
250
+ OpenJPEG library. Defaults to false.
251
+ """
252
+ return self._verbose
253
+
254
+ @verbose.setter
255
+ def verbose(self, verbose):
256
+ self._verbose = verbose
257
+
258
+ @property
259
+ def shape(self):
260
+ """Dimensions of full resolution image."""
261
+ return self._shape
262
+
263
+ @shape.setter
264
+ def shape(self, shape):
265
+ self._shape = shape
266
+
267
+ def __repr__(self):
268
+ msg = f"glymur.Jp2k('{self.path}')"
269
+ return msg
270
+
271
+ def __str__(self):
272
+ metadata = [f'File: {self.path.name}']
273
+ if len(self.box) > 0:
274
+ for box in self.box:
275
+ metadata.append(str(box))
276
+ elif self._codestream is None and not self.path.exists():
277
+ # No codestream either. Empty file? We are done.
278
+ return metadata[0]
279
+ else:
280
+ metadata.append(str(self.codestream))
281
+ return '\n'.join(metadata)
282
+
283
+ def parse(self, force=False):
284
+ """Parses the JPEG 2000 file.
285
+
286
+ Parameters
287
+ ----------
288
+ force : bool
289
+ If true, parse the file even if it has already been parsed once.
290
+
291
+ Raises
292
+ ------
293
+ RuntimeError
294
+ The file was not JPEG 2000.
295
+ """
296
+ if self._parse_count > 0 and not force:
297
+ # don't parse more than once if we can help it
298
+ return
299
+
300
+ self.length = self.path.stat().st_size
301
+
302
+ with self.path.open('rb') as fptr:
303
+
304
+ # Make sure we have a JPEG2000 file. It could be either JP2 or
305
+ # J2C. Check for J2C first, single box in that case.
306
+ read_buffer = fptr.read(2)
307
+ signature, = struct.unpack('>H', read_buffer)
308
+ if signature == 0xff4f:
309
+ self._codec_format = opj2.CODEC_J2K
310
+ # That's it, we're done. The codestream object is only
311
+ # produced upon explicit request.
312
+ return
313
+
314
+ self._codec_format = opj2.CODEC_JP2
315
+
316
+ # Should be JP2.
317
+ # First 4 bytes should be 12, the length of the 'jP ' box.
318
+ # 2nd 4 bytes should be the box ID ('jP ').
319
+ # 3rd 4 bytes should be the box signature (13, 10, 135, 10).
320
+ fptr.seek(0)
321
+ read_buffer = fptr.read(12)
322
+ values = struct.unpack('>I4s4B', read_buffer)
323
+ box_length = values[0]
324
+ box_id = values[1]
325
+ signature = values[2:]
326
+
327
+ if (
328
+ box_length != 12
329
+ or box_id != b'jP '
330
+ or signature != (13, 10, 135, 10)
331
+ ):
332
+ msg = f'{self.filename} is not a JPEG 2000 file.'
333
+ raise InvalidJp2kError(msg)
334
+
335
+ # Back up and start again, we know we have a superbox (box of
336
+ # boxes) here.
337
+ fptr.seek(0)
338
+ self.box = self.parse_superbox(fptr)
339
+ self._validate()
340
+
341
+ self._parse_count += 1
342
+
343
+ def _validate(self):
344
+ """Validate the JPEG 2000 outermost superbox. These checks must be
345
+ done at a file level.
346
+ """
347
+ # A JP2 file must contain certain boxes. The 2nd box must be a file
348
+ # type box.
349
+ if not isinstance(self.box[1], FileTypeBox):
350
+ msg = f"{self.filename} does not contain a valid File Type box."
351
+ raise InvalidJp2kError(msg)
352
+
353
+ ftyp = self.box[1]
354
+ if ftyp.brand != 'jp2 ':
355
+ # Don't bother trying to validate JPX.
356
+ return
357
+
358
+ jp2h = [box for box in self.box if box.box_id == 'jp2h'][0]
359
+
360
+ # An IHDR box is required as the first child box of the JP2H box.
361
+ if jp2h.box[0].box_id != 'ihdr':
362
+ msg = "A valid IHDR box was not found. The JP2 file is invalid."
363
+ raise InvalidJp2kError(msg)
364
+
365
+ # A jp2-branded file cannot contain an "any ICC profile
366
+ colrs = [box for box in jp2h.box if box.box_id == 'colr']
367
+ for colr in colrs:
368
+ if colr.method not in (
369
+ core.ENUMERATED_COLORSPACE, core.RESTRICTED_ICC_PROFILE
370
+ ):
371
+ msg = (
372
+ "Color Specification box method must specify either an "
373
+ "enumerated colorspace or a restricted ICC profile if the "
374
+ "file type box brand is 'jp2 '."
375
+ )
376
+ warnings.warn(msg, UserWarning)
377
+
378
+ # We need to have one and only one JP2H box if we have a JP2 file.
379
+ num_jp2h_boxes = len([box for box in self.box if box.box_id == 'jp2h'])
380
+ if num_jp2h_boxes > 1:
381
+ msg = (
382
+ f"This file has {num_jp2h_boxes} JP2H boxes in the outermost "
383
+ "layer of boxes. There should only be one."
384
+ )
385
+ warnings.warn(msg)
386
+
387
+ # We should have one and only one JP2C box if we have a JP2 file.
388
+ num_jp2c_boxes = len([box for box in self.box if box.box_id == 'jp2c'])
389
+ if num_jp2c_boxes > 1 and self.box[1].brand == 'jp2 ':
390
+ msg = (
391
+ f"This file has {num_jp2c_boxes} JP2C boxes (images) in the "
392
+ "outermost layer of boxes. All JP2C boxes after the first "
393
+ "will be ignored."
394
+ )
395
+ warnings.warn(msg)
396
+ elif num_jp2c_boxes == 0:
397
+ msg = (
398
+ "A valid JP2C box was not found in the outermost level of JP2 "
399
+ "boxes. The JP2 file is invalid."
400
+ )
401
+ raise InvalidJp2kError(msg)
402
+
403
+ # Make sure that IHDR and SIZ conform on the dimensions.
404
+ ihdr = jp2h.box[0]
405
+ ihdr_dims = ihdr.height, ihdr.width, ihdr.num_components
406
+
407
+ siz = [
408
+ segment for segment in self.codestream.segment
409
+ if segment.marker_id == 'SIZ'
410
+ ][0]
411
+
412
+ siz_dims = (siz.ysiz, siz.xsiz, len(siz.bitdepth))
413
+ if ihdr_dims != siz_dims:
414
+ msg = (
415
+ f"The IHDR dimensions {ihdr_dims} do not match the codestream "
416
+ f"dimensions {siz_dims}."
417
+ )
418
+ warnings.warn(msg, UserWarning)
419
+
420
+ def __getitem__(self, pargs):
421
+ """Slicing protocol."""
422
+ if not self.path.exists():
423
+ msg = f"Cannot read from {self.filename}, it does not yet exist."
424
+ raise FileNotFoundError(msg)
425
+ if len(self.shape) == 2:
426
+ numrows, numcols = self.shape
427
+ numbands = 1
428
+ else:
429
+ numrows, numcols, numbands = self.shape
430
+
431
+ if isinstance(pargs, int):
432
+ # Not a very good use of this protocol, but technically legal.
433
+ # This retrieves a single row.
434
+ row = pargs
435
+ area = (row, 0, row + 1, numcols)
436
+ return self._read(area=area).squeeze()
437
+
438
+ if pargs is Ellipsis:
439
+ # Case of jp2[...]
440
+ return self._read()
441
+
442
+ if isinstance(pargs, slice):
443
+ if (
444
+ pargs.start is None
445
+ and pargs.stop is None
446
+ and pargs.step is None
447
+ ):
448
+ # Case of jp2[:]
449
+ return self._read()
450
+
451
+ # Corner case of jp2[x] where x is a slice object with non-null
452
+ # members. Just augment it with an ellipsis and let the code
453
+ # below handle it.
454
+ pargs = (pargs, Ellipsis)
455
+
456
+ if isinstance(pargs, tuple) and any(x is Ellipsis for x in pargs):
457
+ newindex = self._remove_ellipsis(pargs, numrows, numcols, numbands)
458
+
459
+ # Run once again because it is possible that there's another
460
+ # Ellipsis object in the 2nd or 3rd position.
461
+ return self.__getitem__(newindex)
462
+
463
+ if isinstance(pargs, tuple) and any(isinstance(x, int) for x in pargs):
464
+ # Replace the first such integer argument, replace it with a slice.
465
+ lst = list(pargs)
466
+ g = filterfalse(lambda x: not isinstance(x[1], int),
467
+ enumerate(pargs))
468
+ idx = next(g)[0]
469
+ lst[idx] = slice(pargs[idx], pargs[idx] + 1)
470
+ newindex = tuple(lst)
471
+
472
+ # Invoke array-based slicing again, as there may be additional
473
+ # integer argument remaining.
474
+ data = self.__getitem__(newindex)
475
+
476
+ # Reduce dimensionality in the scalar dimension.
477
+ return np.squeeze(data, axis=idx)
478
+
479
+ # Assuming pargs is a tuple of slices from now on.
480
+ rows = pargs[0]
481
+ cols = pargs[1]
482
+ if len(pargs) == 2:
483
+ bands = slice(None, None, None)
484
+ else:
485
+ bands = pargs[2]
486
+
487
+ rows_step = 1 if rows.step is None else rows.step
488
+ cols_step = 1 if cols.step is None else cols.step
489
+ if rows_step != cols_step:
490
+ msg = "Row and column strides must be the same."
491
+ raise ValueError(msg)
492
+
493
+ # Ok, reduce layer step is the same in both xy directions, so just take
494
+ # one of them.
495
+ step = rows_step
496
+ if step == -1:
497
+ # This is a shortcut for the last decomposition (or reduce layer
498
+ # step).
499
+ step = 2 ** self.codestream.segment[2].num_res
500
+
501
+ # Check if the step size is a power of 2.
502
+ if np.abs(np.log2(step) - np.round(np.log2(step))) > 1e-6:
503
+ msg = "Row and column strides must be powers of 2."
504
+ raise ValueError(msg)
505
+ rlevel = int(np.round(np.log2(step)))
506
+
507
+ area = (
508
+ 0 if rows.start is None else rows.start,
509
+ 0 if cols.start is None else cols.start,
510
+ numrows if rows.stop is None else rows.stop,
511
+ numcols if cols.stop is None else cols.stop
512
+ )
513
+ data = self._read(area=area, rlevel=rlevel)
514
+ if len(pargs) == 2:
515
+ return data
516
+
517
+ # Ok, 3 arguments in pargs.
518
+ return data[:, :, bands]
519
+
520
+ def read(self, **kwargs):
521
+ """Read a JPEG 2000 image.
522
+
523
+ Returns
524
+ -------
525
+ img_array : ndarray
526
+ The image data.
527
+ """
528
+
529
+ if 'ignore_pclr_cmap_cdef' in kwargs:
530
+ self.ignore_pclr_cmap_cdef = kwargs['ignore_pclr_cmap_cdef']
531
+ kwargs.pop('ignore_pclr_cmap_cdef')
532
+ warnings.warn("Use array-style slicing instead.", DeprecationWarning)
533
+ img = self._read(**kwargs)
534
+ return img
535
+
536
+ def _subsampling_sanity_check(self):
537
+ """Check for differing subsample factors."""
538
+ if self._decoded_components is None:
539
+ dxs = np.array(self.codestream.segment[1].xrsiz)
540
+ dys = np.array(self.codestream.segment[1].yrsiz)
541
+ else:
542
+ dxs = np.array([
543
+ self.codestream.segment[1].xrsiz[i]
544
+ for i in self._decoded_components
545
+ ])
546
+ dys = np.array([
547
+ self.codestream.segment[1].yrsiz[i]
548
+ for i in self._decoded_components
549
+ ])
550
+
551
+ if np.any(dxs - dxs[0]) or np.any(dys - dys[0]):
552
+ msg = (
553
+ f"The read_bands method should be used when the subsampling "
554
+ f"factors are different."
555
+ f"\n\n"
556
+ f"{self.codestream.segment[1]}"
557
+ )
558
+ raise RuntimeError(msg)
559
+
560
+ def _read(self, rlevel=0, layer=None, area=None, tile=None, verbose=False):
561
+ """Read a JPEG 2000 image using libopenjp2.
562
+
563
+ Parameters
564
+ ----------
565
+ layer : int, optional
566
+ Number of quality layer to decode.
567
+ rlevel : int, optional
568
+ Factor by which to rlevel output resolution. Use -1 to get the
569
+ lowest resolution thumbnail.
570
+ area : tuple, optional
571
+ Specifies decoding image area,
572
+ (first_row, first_col, last_row, last_col)
573
+ tile : int, optional
574
+ Number of tile to decode.
575
+ verbose : bool, optional
576
+ Print informational messages produced by the OpenJPEG library.
577
+
578
+ Returns
579
+ -------
580
+ ndarray
581
+ The image data.
582
+
583
+ Raises
584
+ ------
585
+ RuntimeError
586
+ If the image has differing subsample factors.
587
+ """
588
+ if re.match("0|1|2.[012]", version.openjpeg_version):
589
+ msg = (
590
+ f"You must have a version of OpenJPEG at least as high as "
591
+ f"2.3.0 before you can read JPEG2000 images with glymur. "
592
+ f"Your version is {version.openjpeg_version}"
593
+ )
594
+ raise RuntimeError(msg)
595
+
596
+ self._subsampling_sanity_check()
597
+ self._populate_dparams(rlevel, tile=tile, area=area)
598
+ image = self._read_openjp2()
599
+ return image
600
+
601
+ def _read_openjp2(self):
602
+ """Read a JPEG 2000 image using libopenjp2.
603
+
604
+ Returns
605
+ -------
606
+ ndarray or lst
607
+ Either the image as an ndarray or a list of ndarrays, each item
608
+ corresponding to one band.
609
+ """
610
+ with ExitStack() as stack:
611
+ filename = self.filename
612
+ stream = opj2.stream_create_default_file_stream(filename, True)
613
+ stack.callback(opj2.stream_destroy, stream)
614
+ codec = opj2.create_decompress(self._codec_format)
615
+ stack.callback(opj2.destroy_codec, codec)
616
+
617
+ opj2.set_error_handler(codec, opj2._ERROR_CALLBACK)
618
+ opj2.set_warning_handler(codec, opj2._WARNING_CALLBACK)
619
+
620
+ if self._verbose:
621
+ opj2.set_info_handler(codec, opj2._INFO_CALLBACK)
622
+ else:
623
+ opj2.set_info_handler(codec, None)
624
+
625
+ opj2.setup_decoder(codec, self._dparams)
626
+ if version.openjpeg_version >= '2.2.0':
627
+ opj2.codec_set_threads(codec, get_option('lib.num_threads'))
628
+
629
+ raw_image = opj2.read_header(stream, codec)
630
+ stack.callback(opj2.image_destroy, raw_image)
631
+
632
+ if self._decoded_components is not None:
633
+ opj2.set_decoded_components(codec, self._decoded_components)
634
+
635
+ if self._dparams.nb_tile_to_decode:
636
+ opj2.get_decoded_tile(codec, stream, raw_image,
637
+ self._dparams.tile_index)
638
+ else:
639
+ opj2.set_decode_area(
640
+ codec, raw_image,
641
+ self._dparams.DA_x0, self._dparams.DA_y0,
642
+ self._dparams.DA_x1, self._dparams.DA_y1
643
+ )
644
+ opj2.decode(codec, stream, raw_image)
645
+
646
+ opj2.end_decompress(codec, stream)
647
+
648
+ image = self._extract_image(raw_image)
649
+
650
+ return image
651
+
652
+ def _populate_dparams(self, rlevel, tile=None, area=None):
653
+ """Populate decompression structure with appropriate input parameters.
654
+
655
+ Parameters
656
+ ----------
657
+ rlevel : int
658
+ Factor by which to rlevel output resolution.
659
+ area : tuple
660
+ Specifies decoding image area,
661
+ (first_row, first_col, last_row, last_col)
662
+ tile : int
663
+ Number of tile to decode.
664
+ """
665
+ dparam = opj2.set_default_decoder_parameters()
666
+
667
+ infile = self.filename.encode()
668
+ nelts = opj2.PATH_LEN - len(infile)
669
+ infile += b'0' * nelts
670
+ dparam.infile = infile
671
+
672
+ # Return raw codestream components instead of "interpolating" the
673
+ # colormap?
674
+ dparam.flags |= 1 if self.ignore_pclr_cmap_cdef else 0
675
+
676
+ dparam.decod_format = self._codec_format
677
+ dparam.cp_layer = self.layer
678
+
679
+ # Must check the specified rlevel against the maximum.
680
+ if rlevel != 0:
681
+ # Must check the specified rlevel against the maximum.
682
+ cod_seg = [
683
+ segment for segment in self.codestream.segment
684
+ if segment.marker_id == 'COD'
685
+ ][0]
686
+ max_rlevel = cod_seg.num_res
687
+ if rlevel == -1:
688
+ # -1 is shorthand for the largest rlevel
689
+ rlevel = max_rlevel
690
+ elif rlevel < -1 or rlevel > max_rlevel:
691
+ msg = (f"rlevel must be in the range [-1, {max_rlevel}] "
692
+ "for this image.")
693
+ raise ValueError(msg)
694
+
695
+ dparam.cp_reduce = rlevel
696
+
697
+ if area is not None:
698
+ if area[0] < 0 or area[1] < 0 or area[2] <= 0 or area[3] <= 0:
699
+ msg = (
700
+ f"The upper left corner coordinates must be nonnegative "
701
+ f"and the lower right corner coordinates must be positive."
702
+ f" The specified upper left and lower right coordinates "
703
+ f"are ({area[0]}, {area[1]}) and ({area[2]}, {area[3]})."
704
+ )
705
+ raise ValueError(msg)
706
+ dparam.DA_y0 = area[0]
707
+ dparam.DA_x0 = area[1]
708
+ dparam.DA_y1 = area[2]
709
+ dparam.DA_x1 = area[3]
710
+
711
+ if tile is not None:
712
+ dparam.tile_index = tile
713
+ dparam.nb_tile_to_decode = 1
714
+
715
+ self._dparams = dparam
716
+
717
+ def read_bands(self, rlevel=0, layer=0, area=None, tile=None,
718
+ verbose=False, ignore_pclr_cmap_cdef=False):
719
+ """Read a JPEG 2000 image.
720
+
721
+ The only time you should use this method is when the image has
722
+ different subsampling factors across components. Otherwise you should
723
+ use the read method.
724
+
725
+ Parameters
726
+ ----------
727
+ layer : int, optional
728
+ Number of quality layer to decode.
729
+ rlevel : int, optional
730
+ Factor by which to rlevel output resolution.
731
+ area : tuple, optional
732
+ Specifies decoding image area,
733
+ (first_row, first_col, last_row, last_col)
734
+ tile : int, optional
735
+ Number of tile to decode.
736
+ ignore_pclr_cmap_cdef : bool
737
+ Whether or not to ignore the pclr, cmap, or cdef boxes during any
738
+ color transformation. Defaults to False.
739
+ verbose : bool, optional
740
+ Print informational messages produced by the OpenJPEG library.
741
+
742
+ Returns
743
+ -------
744
+ list
745
+ List of the individual image components.
746
+
747
+ See also
748
+ --------
749
+ read : read JPEG 2000 image
750
+
751
+ Examples
752
+ --------
753
+ >>> import glymur
754
+ >>> jfile = glymur.data.nemo()
755
+ >>> jp = glymur.Jp2k(jfile)
756
+ >>> components_lst = jp.read_bands(rlevel=1)
757
+ """
758
+ if version.openjpeg_version < '2.3.0':
759
+ msg = (
760
+ f"You must have at least version 2.3.0 of OpenJPEG installed "
761
+ f"before using this method. Your version of OpenJPEG is "
762
+ f"{version.openjpeg_version}."
763
+ )
764
+ raise RuntimeError(msg)
765
+
766
+ self.ignore_pclr_cmap_cdef = ignore_pclr_cmap_cdef
767
+ self.layer = layer
768
+ self._populate_dparams(rlevel, tile=tile, area=area)
769
+ lst = self._read_openjp2()
770
+ return lst
771
+
772
+ def _extract_image(self, raw_image):
773
+ """Extract unequally-sized image bands.
774
+
775
+ Parameters
776
+ ----------
777
+ raw_image : reference to openjpeg ImageType instance
778
+ The image structure initialized with image characteristics.
779
+
780
+ Returns
781
+ -------
782
+ list or ndarray
783
+ If the JPEG 2000 image has unequally-sized components, they are
784
+ extracted into a list, otherwise a numpy array.
785
+
786
+ """
787
+ ncomps = raw_image.contents.numcomps
788
+
789
+ # Make a pass thru the image, see if any of the band datatypes or
790
+ # dimensions differ.
791
+ dtypes, nrows, ncols = [], [], []
792
+ for k in range(raw_image.contents.numcomps):
793
+ component = raw_image.contents.comps[k]
794
+ dtypes.append(self._component2dtype(component))
795
+ nrows.append(component.h)
796
+ ncols.append(component.w)
797
+ is_cube = all(
798
+ r == nrows[0] and c == ncols[0] and d == dtypes[0]
799
+ for r, c, d in zip(nrows, ncols, dtypes)
800
+ )
801
+
802
+ if is_cube:
803
+ image = np.zeros((nrows[0], ncols[0], ncomps), dtypes[0])
804
+ else:
805
+ image = []
806
+
807
+ for k in range(raw_image.contents.numcomps):
808
+ component = raw_image.contents.comps[k]
809
+
810
+ self._validate_nonzero_image_size(nrows[k], ncols[k], k)
811
+
812
+ addr = ctypes.addressof(component.data.contents)
813
+ with warnings.catch_warnings():
814
+ warnings.simplefilter("ignore")
815
+
816
+ band_i32 = np.ctypeslib.as_array(
817
+ (ctypes.c_int32 * nrows[k] * ncols[k]).from_address(addr)
818
+ )
819
+ band = np.reshape(
820
+ band_i32.astype(dtypes[k]), (nrows[k], ncols[k])
821
+ )
822
+
823
+ if is_cube:
824
+ image[:, :, k] = band
825
+ else:
826
+ image.append(band)
827
+
828
+ if is_cube and image.shape[2] == 1:
829
+ # The third dimension has just a single layer. Make the image
830
+ # data 2D instead of 3D.
831
+ image.shape = image.shape[0:2]
832
+
833
+ return image
834
+
835
+ def _component2dtype(self, component):
836
+ """Determin the appropriate numpy datatype for an OpenJPEG component.
837
+
838
+ Parameters
839
+ ----------
840
+ component : ctypes pointer to ImageCompType (image_comp_t)
841
+ single image component structure.
842
+
843
+ Returns
844
+ -------
845
+ builtins.type
846
+ numpy datatype to be used to construct an image array
847
+ """
848
+ if component.prec > 16:
849
+ msg = f"Unhandled precision: {component.prec} bits."
850
+ raise ValueError(msg)
851
+
852
+ if component.sgnd:
853
+ if component.prec <= 8:
854
+ dtype = np.int8
855
+ else:
856
+ dtype = np.int16
857
+ else:
858
+ if component.prec <= 8:
859
+ dtype = np.uint8
860
+ else:
861
+ dtype = np.uint16
862
+
863
+ return dtype
864
+
865
+ def get_codestream(self, header_only=True):
866
+ """Retrieve codestream.
867
+
868
+ Parameters
869
+ ----------
870
+ header_only : bool, optional
871
+ If True, only marker segments in the main header are parsed.
872
+ Supplying False may impose a large performance penalty.
873
+
874
+ Returns
875
+ -------
876
+ Codestream
877
+ Object describing the codestream syntax.
878
+
879
+ Examples
880
+ --------
881
+ >>> import glymur
882
+ >>> jfile = glymur.data.nemo()
883
+ >>> jp2 = glymur.Jp2k(jfile)
884
+ >>> codestream = jp2.get_codestream()
885
+ >>> print(codestream.segment[1])
886
+ SIZ marker segment @ (3233, 47)
887
+ Profile: no profile
888
+ Reference Grid Height, Width: (1456 x 2592)
889
+ Vertical, Horizontal Reference Grid Offset: (0 x 0)
890
+ Reference Tile Height, Width: (1456 x 2592)
891
+ Vertical, Horizontal Reference Tile Offset: (0 x 0)
892
+ Bitdepth: (8, 8, 8)
893
+ Signed: (False, False, False)
894
+ Vertical, Horizontal Subsampling: ((1, 1), (1, 1), (1, 1))
895
+ """
896
+ with self.path.open('rb') as fptr:
897
+ if self._codec_format == opj2.CODEC_J2K:
898
+ codestream = Codestream(fptr, self.length,
899
+ header_only=header_only)
900
+ else:
901
+ box = [x for x in self.box if x.box_id == 'jp2c']
902
+ fptr.seek(box[0].offset)
903
+ read_buffer = fptr.read(8)
904
+ (box_length, _) = struct.unpack('>I4s', read_buffer)
905
+ if box_length == 0:
906
+ # The length of the box is presumed to last until the end
907
+ # of the file. Compute the effective length of the box.
908
+ box_length = self.path.stat().st_size - fptr.tell() + 8
909
+ elif box_length == 1:
910
+ # Seek past the XL field.
911
+ read_buffer = fptr.read(8)
912
+ box_length, = struct.unpack('>Q', read_buffer)
913
+ codestream = Codestream(fptr, box_length - 8,
914
+ header_only=header_only)
915
+
916
+ return codestream
917
+
918
+ def _populate_image_struct(
919
+ self, image, imgdata, tile_x_factor=1, tile_y_factor=1
920
+ ):
921
+ """Populates image struct needed for compression.
922
+
923
+ Parameters
924
+ ----------
925
+ image : ImageType(ctypes.Structure)
926
+ Corresponds to image_t type in openjp2 headers.
927
+ imgdata : ndarray
928
+ Image data to be written to file.
929
+ tile_x_factor, tile_y_factor: int
930
+ Used only when writing tile-by-tile. In this case, the image data
931
+ that we have is only the size of a single tile.
932
+ """
933
+
934
+ if len(self.shape) < 3:
935
+ (numrows, numcols), num_comps = self.shape, 1
936
+ else:
937
+ numrows, numcols, num_comps = self.shape
938
+
939
+ for k in range(num_comps):
940
+ self._validate_nonzero_image_size(numrows, numcols, k)
941
+
942
+ # set image offset and reference grid
943
+ image.contents.x0 = self._cparams.image_offset_x0
944
+ image.contents.y0 = self._cparams.image_offset_y0
945
+ image.contents.x1 = (
946
+ image.contents.x0
947
+ + (numcols - 1) * self._cparams.subsampling_dx * tile_x_factor
948
+ + 1
949
+ )
950
+ image.contents.y1 = (
951
+ image.contents.y0
952
+ + (numrows - 1) * self._cparams.subsampling_dy * tile_y_factor
953
+ + 1
954
+ )
955
+
956
+ if tile_x_factor != 1 or tile_y_factor != 1:
957
+ # don't stage the data if writing tiles
958
+ return image
959
+
960
+ # Stage the image data to the openjpeg data structure.
961
+ for k in range(0, num_comps):
962
+ if self._cparams.rsiz in (core.OPJ_PROFILE_CINEMA_2K,
963
+ core.OPJ_PROFILE_CINEMA_4K):
964
+ image.contents.comps[k].prec = 12
965
+ image.contents.comps[k].bpp = 12
966
+
967
+ layer = np.ascontiguousarray(imgdata[:, :, k], dtype=np.int32)
968
+ dest = image.contents.comps[k].data
969
+ src = layer.ctypes.data
970
+ ctypes.memmove(dest, src, layer.nbytes)
971
+
972
+ return image
973
+
974
+ def _validate_nonzero_image_size(self, nrows, ncols, component_index):
975
+ """The image cannot have area of zero."""
976
+ if nrows == 0 or ncols == 0:
977
+ # Letting this situation continue would segfault openjpeg.
978
+ msg = (
979
+ f"Component {component_index} has dimensions "
980
+ f"{nrows} x {ncols}"
981
+ )
982
+ raise InvalidJp2kError(msg)