jbpy.image_data

Functions for handling image segment data

  1"""Functions for handling image segment data"""
  2
  3import copy
  4import itertools
  5import math
  6import os
  7import typing
  8
  9import jbpy.core
 10
 11BLOCK_NOT_RECORDED = 0xFFFFFFFF
 12
 13_PVTYPE_TO_AP_TYPE_STRING = {"INT": "u", "SI": "i", "R": "f", "C": "c"}
 14
 15
 16def array_protocol_typestr(pvtype: str, nbpp: int) -> str:
 17    """Generate a NumPy array interface protocol typestr describing a NITF pixel
 18
 19    Parameters
 20    ----------
 21    pvtype : str
 22        Image subheader Pixel Value Type (PVTYPE)
 23    nbpp : int
 24        Image subheader Number of Bits Per Pixel Per Band (NBPP)
 25
 26    Notes
 27    -----
 28    The resulting typestr is sutable for storing the pixel value.
 29    Additional transforms of the pixel values may be necessary to account
 30    for PJUST and ABPP.
 31    """
 32    assert nbpp % 8 == 0  # 12bit not implemented
 33    dtype_str = ">"
 34    dtype_str += _PVTYPE_TO_AP_TYPE_STRING[pvtype]
 35    dtype_str += str(int(nbpp // 8))
 36    return dtype_str
 37
 38
 39class BinaryUnsignedInteger(jbpy.core.PythonConverter):
 40    """convert to/from a binary integer"""
 41
 42    def to_bytes_impl(self, decoded_value: int, size: int) -> bytes:
 43        decoded_value = int(decoded_value)
 44        return decoded_value.to_bytes(size, byteorder="big", signed=False)
 45
 46    def from_bytes_impl(self, encoded_value: bytes) -> int:
 47        return int.from_bytes(encoded_value, byteorder="big", signed=False)
 48
 49
 50# MaskTable is defined here rather than in jbpy.core because jbpy.core's existing callback
 51# support makes it difficult to keep MaskTable updated when NBPR, NBPC, NBANDS, and XBANDS change.
 52# As a result it doesn't behave quite like the other Groups.
 53class MaskTable(jbpy.core.Group):
 54    """JBP Image Data Mask Table
 55
 56    Parameters
 57    ----------
 58    name : str
 59        Name to give this group
 60    image_subheader : jbpy.core.ImageSubheader
 61        Subheader for the image segment containing the mask table
 62
 63    Notes
 64    -----
 65    image_subheader must not change after initializing this class.
 66    """
 67
 68    def __init__(self, name: str, image_subheader: jbpy.core.ImageSubheader):
 69        super().__init__(name)
 70        self._num_blocks = image_subheader["NBPC"].value * image_subheader["NBPR"].value
 71
 72        if image_subheader["IMODE"].value == "S":
 73            # Each band is stored as a separate block
 74            self._num_bands = image_subheader.get(
 75                "XBANDS", image_subheader["NBANDS"]
 76            ).value
 77        else:
 78            self._num_bands = 1
 79
 80        self._append(
 81            jbpy.core.Field(
 82                "IMDATOFF",
 83                "Blocked Image Data Offset",
 84                4,
 85                converter=BinaryUnsignedInteger(),
 86                default=0,
 87            )
 88        )
 89        self._append(
 90            jbpy.core.Field(
 91                "BMRLNTH",
 92                "Block Mask Record Length",
 93                2,
 94                decoded_range=jbpy.core.Enum([0, 4]),
 95                converter=BinaryUnsignedInteger(),
 96                default=0,
 97                setter_callback=self._handle_bmrlnth,
 98            )
 99        )
100        self._append(
101            jbpy.core.Field(
102                "TMRLNTH",
103                "Pad Pixel Mask Record Length",
104                2,
105                decoded_range=jbpy.core.Enum([0, 4]),
106                converter=BinaryUnsignedInteger(),
107                default=0,
108                setter_callback=self._handle_tmrlnth,
109            )
110        )
111        self._append(
112            jbpy.core.Field(
113                "TPXCDLNTH",
114                "Pad Output Pixel Code Length",
115                2,
116                converter=BinaryUnsignedInteger(),
117                default=0,
118                setter_callback=self._handle_tpxcdlnth,
119            )
120        )
121
122    def _handle_bmrlnth(self, field):
123        self._remove_all("BMR\\d+BND\\d+")
124        if field.value == 0:
125            return
126
127        after = self["TPXCDLNTH"]
128        if "TPXCD" in self:
129            after = self["TPXCD"]
130
131        for band_idx in range(self._num_bands):
132            for block_idx in range(self._num_blocks):
133                name = self.bmr_name(block_idx, band_idx)
134                after = self._insert_after(
135                    after,
136                    jbpy.core.Field(
137                        name,
138                        f"Block {block_idx}, Band {band_idx} Offset",
139                        4,
140                        converter=BinaryUnsignedInteger(),
141                        default=0,
142                    ),
143                )
144
145    def _handle_tmrlnth(self, field):
146        self._remove_all("TMR\\d+BND\\d+")
147        if field.value == 0:
148            return
149
150        after = self["TPXCDLNTH"]
151        if "TPXCD" in self:
152            after = self["TPXCD"]
153
154        for bmr_field in self.find_all("BMR\\d+BND\\d+"):
155            if bmr_field.get_offset() > after.get_offset():
156                after = bmr_field
157
158        for band_idx in range(self._num_bands):
159            for block_idx in range(self._num_blocks):
160                name = self.tmr_name(block_idx, band_idx)
161                after = self._insert_after(
162                    after,
163                    jbpy.core.Field(
164                        name,
165                        f"Pad Pixel {block_idx}, Band {band_idx}",
166                        4,
167                        converter=BinaryUnsignedInteger(),
168                        default=0,
169                    ),
170                )
171
172    def _handle_tpxcdlnth(self, field):
173        self._remove_all("TPXCD")
174        tpxcd_length = int(math.ceil(field.value / 8))
175        if tpxcd_length > 0:
176            self._insert_after(
177                field,
178                jbpy.core.Field(
179                    "TPXCD",
180                    "Pad Output Pixel Code",
181                    tpxcd_length,
182                    converter=jbpy.core.Bytes(),
183                    default=b"\x00" * tpxcd_length,
184                ),
185            )
186
187    @staticmethod
188    def bmr_name(block_index: int, band_index: int) -> str:
189        """Generate the expected name for BMRnBNDm given indices
190
191        Parameters
192        ----------
193        block_index : int
194            Linear index of the block (zero-based).  "n"
195        band_index : int
196            Index of the band (zero-based).  "m"
197
198        Returns
199        -------
200        str
201            Field name
202        """
203        return f"BMR{block_index:08d}BND{band_index:05d}"
204
205    @staticmethod
206    def tmr_name(block_index: int, band_index: int) -> str:
207        """Generate the expected name for TMRnBNDm given indices
208
209        Parameters
210        ----------
211        block_index : int
212            Linear index of the block (one-based).  "n"
213        band_index : int
214            Index of the band (one-based).  "m"
215
216        Returns
217        -------
218        str
219            Field name
220        """
221        return f"TMR{block_index:08d}BND{band_index:05d}"
222
223
224def read_mask_table(
225    image_segment: jbpy.core.ImageSegment, file: jbpy.core.BinaryFile_R
226) -> MaskTable:
227    """Read an image segment's mask table
228
229    Parameters
230    ----------
231    image_segment : ImageSegment
232        Which image segment's mask table to read
233    file : file-like
234        JBP file containing the image_segment
235
236    Returns
237    -------
238    dictionary containing the mask table values or None if there is no mask table
239    """
240    file.seek(image_segment["Data"].get_offset(), os.SEEK_SET)
241    mt = MaskTable("MaskTable", image_segment["subheader"])
242    mt.load(file)
243    return mt
244
245
246IMPLEMENTED_PIXEL_TYPES = [  # (PVTYPE, NBPP)
247    ("INT", 8),
248    # ('INT', 12),  # 12-bit not implemented
249    ("INT", 16),
250    ("INT", 32),
251    ("INT", 64),
252    ("SI", 8),
253    # ('SI', 12),  # 12-bit not implemented
254    ("SI", 16),
255    ("SI", 32),
256    ("SI", 64),
257    ("R", 32),
258    ("R", 64),
259    ("C", 64),
260]
261
262
263def image_array_description(
264    image_segment: jbpy.core.ImageSegment,
265) -> tuple[tuple[int, int, int], int, str]:
266    """Shape of image described by the image segment
267
268    Always describes a 3D shape with one axis being the bands.
269    Axis containing the bands is determined by the IMODE field.
270
271    Parameters
272    ----------
273    image_segment : jbpy.core.ImageSegment
274        The image segment to describe
275
276    Returns
277    -------
278    shape : tuple
279        Shape of the full image
280    band_axis : int
281        Which axis contains the bands.  Other two axes will be rows and cols, respectively.
282    typestr : str
283        Array interface protocol typestr describing the pixel type
284    """
285    subhdr = image_segment["subheader"]
286    num_bands = subhdr.get("XBANDS", subhdr["NBANDS"]).value
287    nrows = subhdr["NROWS"].value
288    ncols = subhdr["NCOLS"].value
289    imode = subhdr["IMODE"].value
290    pvtype = subhdr["PVTYPE"].value
291    nbpp = subhdr["NBPP"].value
292
293    if imode == "B":
294        shape = (num_bands, nrows, ncols)
295        band_axis = 0
296    elif imode == "P":
297        shape = (nrows, ncols, num_bands)
298        band_axis = 2
299    elif imode == "R":
300        shape = (nrows, num_bands, ncols)
301        band_axis = 1
302    elif imode == "S":
303        shape = (num_bands, nrows, ncols)
304        band_axis = 0
305
306    typestr = array_protocol_typestr(pvtype, nbpp)
307
308    return shape, band_axis, typestr
309
310
311Slice3DType = tuple[slice | int, slice | int, slice | int]
312
313
314class BlockInfo(typing.TypedDict):
315    """Information describing a single image data block"""
316
317    #: band index of this block.  Zero unless IMODE == S
318    block_band_index: int
319
320    #: row index of this block
321    block_row_index: int
322
323    #: col index of this block
324    block_col_index: int
325
326    #: Offset to first byte of the block relative to the start of the image data
327    offset: int | None
328
329    #: Size of the block in bytes (including fill pixels)
330    nbytes: int
331
332    #: Shape of the block
333    shape: tuple[int, int, int]
334
335    #: Which axis of the block's shape contains the bands
336    band_axis: int
337
338    #: Array interface protocol typestr for the block's pixels
339    typestr: str
340
341    #: 3D slice of full image describing this blocks' non-fill pixels
342    image_slicing: Slice3DType
343
344    #: 3D slice of this block describing the non-fill pixels
345    block_slicing: Slice3DType
346
347    #: How many rows of fill are contained in this block
348    fill_rows: int
349
350    #: How many columns of fill are contained in this block
351    fill_cols: int
352
353    #: Does this block contain pad pixels
354    has_pad: bool
355
356    #: pixel bit pattern identifiying pad pixels
357    pad_value: bytes | None
358
359
360def block_info_uncompressed(
361    image_segment: jbpy.core.ImageSegment, file: jbpy.core.BinaryFile_R | None = None
362) -> list[BlockInfo]:
363    """
364    Describe the blocks comprising an uncompressed image segment
365
366    Parameters
367    ----------
368    image_segment : ImageSegment
369        Which image segment to describe
370    file : file-like
371        JBP file containing the image_segment.  Required if image segment contains Mask Table. (IC field contains "M")
372
373    Returns
374    -------
375    list of BlockInfo dictionaries
376    """
377    subhdr = image_segment["subheader"]
378    assert subhdr["IC"].value in ("NC", "NM")
379
380    block_info = nominal_block_info(subhdr)
381
382    mask_table = None
383    if "M" in subhdr["IC"].value:
384        assert file is not None
385        mask_table = read_mask_table(image_segment, file)
386        block_info = apply_mask_table_to_block_info(subhdr, block_info, mask_table)
387
388    return block_info
389
390
391def nominal_block_info(image_subheader: jbpy.core.ImageSubheader) -> list[BlockInfo]:
392    """Create a list of block information assuming an image is uncompressed and unmasked (IC=NC)
393
394    Parameters
395    ----------
396    image_subheader : jbpy.core.ImageSubheader
397        Subheader of the image to describe
398
399    Returns
400    -------
401    list of BlockInfo dictionaries
402    """
403
404    assert (
405        image_subheader["PVTYPE"].value,
406        image_subheader["NBPP"].value,
407    ) in IMPLEMENTED_PIXEL_TYPES
408
409    num_image_bands = image_subheader.get("XBANDS", image_subheader["NBANDS"]).value
410    if image_subheader["IMODE"].value == "S":
411        # Each band is stored as a separate block
412        num_bands_in_block = 1
413        num_block_bands = num_image_bands
414    else:
415        num_bands_in_block = num_image_bands
416        num_block_bands = 1
417
418    rows_per_block = image_subheader["NPPBV"].value or image_subheader["NROWS"].value
419    cols_per_block = image_subheader["NPPBH"].value or image_subheader["NCOLS"].value
420    expected_blocks_per_col = int(
421        math.ceil(image_subheader["NROWS"].value / rows_per_block)
422    )
423    expected_blocks_per_row = int(
424        math.ceil(image_subheader["NCOLS"].value / cols_per_block)
425    )
426
427    if expected_blocks_per_col != image_subheader["NBPC"].value:
428        raise RuntimeError(
429            f"Image segment has {image_subheader['NBPC'].value} vertical blocks, expected {expected_blocks_per_col}"
430        )
431    if expected_blocks_per_row != image_subheader["NBPR"].value:
432        raise RuntimeError(
433            f"Image segment has {image_subheader['NBPR'].value} horizontal blocks, expected {expected_blocks_per_row}"
434        )
435
436    num_fill_rows = (rows_per_block * expected_blocks_per_col) - image_subheader[
437        "NROWS"
438    ].value
439    num_fill_cols = (cols_per_block * expected_blocks_per_row) - image_subheader[
440        "NCOLS"
441    ].value
442
443    if num_fill_rows < 0 or num_fill_cols < 0:
444        raise RuntimeError("Image segment is missing blocks")
445
446    # Will not work for NBPP == 12
447    block_nbytes = (
448        num_bands_in_block
449        * rows_per_block
450        * cols_per_block
451        * image_subheader["NBPP"].value
452        // 8
453    )
454
455    blocks = []
456    for block_counter, block_indices in enumerate(
457        itertools.product(
458            range(num_block_bands),
459            range(image_subheader["NBPC"].value),
460            range(image_subheader["NBPR"].value),
461        )
462    ):
463        block_band_index, block_row_index, block_col_index = block_indices
464        start_row = block_row_index * rows_per_block
465        start_col = block_col_index * cols_per_block
466
467        # how much fill is in this block
468        fill_rows = (
469            num_fill_rows if block_row_index == image_subheader["NBPC"].value - 1 else 0
470        )
471        fill_cols = (
472            num_fill_cols if block_col_index == image_subheader["NBPR"].value - 1 else 0
473        )
474        image_slice_rows = slice(start_row, start_row + rows_per_block - fill_rows)
475        image_slice_cols = slice(start_col, start_col + cols_per_block - fill_cols)
476        block_slice_rows = slice(0, rows_per_block - fill_rows)
477        block_slice_cols = slice(0, cols_per_block - fill_cols)
478
479        image_slicing: Slice3DType
480        block_slicing: Slice3DType
481        if image_subheader["IMODE"].value == "P":
482            shape = (rows_per_block, cols_per_block, num_image_bands)
483            image_slicing = (
484                image_slice_rows,
485                image_slice_cols,
486                slice(None, None),  # all bands
487            )
488            block_slicing = (
489                block_slice_rows,
490                block_slice_cols,
491                slice(None, None),  # all bands
492            )
493            band_axis = 2
494        elif image_subheader["IMODE"].value == "B":
495            shape = (num_image_bands, rows_per_block, cols_per_block)
496            image_slicing = (
497                slice(None, None),  # all bands
498                image_slice_rows,
499                image_slice_cols,
500            )
501            block_slicing = (
502                slice(None, None),  # all bands
503                block_slice_rows,
504                block_slice_cols,
505            )
506            band_axis = 0
507        elif image_subheader["IMODE"].value == "R":
508            shape = (rows_per_block, num_image_bands, cols_per_block)
509            image_slicing = (
510                image_slice_rows,
511                slice(None, None),  # all bands
512                image_slice_cols,
513            )
514            block_slicing = (
515                block_slice_rows,
516                slice(None, None),  # all bands
517                block_slice_cols,
518            )
519            band_axis = 1
520        elif image_subheader["IMODE"].value == "S":
521            shape = (1, rows_per_block, cols_per_block)
522            image_slicing = (
523                block_band_index,  # single band
524                image_slice_rows,
525                image_slice_cols,
526            )
527            block_slicing = (
528                0,  # each block contains only a single band
529                block_slice_rows,
530                block_slice_cols,
531            )
532            band_axis = 0
533
534        # Nominal description for unmasked/unpadded data, "NC"
535        info: BlockInfo = {
536            "block_band_index": block_band_index,
537            "block_row_index": block_row_index,
538            "block_col_index": block_col_index,
539            "offset": block_nbytes * block_counter,
540            "nbytes": block_nbytes,
541            "shape": shape,
542            "typestr": array_protocol_typestr(
543                image_subheader["PVTYPE"].value, image_subheader["NBPP"].value
544            ),
545            "image_slicing": image_slicing,
546            "block_slicing": block_slicing,
547            "fill_rows": fill_rows,
548            "fill_cols": fill_cols,
549            "has_pad": False,
550            "pad_value": None,
551            "band_axis": band_axis,
552        }
553        blocks.append(info)
554
555    return blocks
556
557
558def apply_mask_table_to_block_info(
559    image_subheader: jbpy.core.ImageSubheader,
560    block_info: list[BlockInfo],
561    mask_table: MaskTable,
562) -> list[BlockInfo]:
563    """Return a copy of a block_info list with information from a mask table applied
564
565    Parameters
566    ----------
567    image_subheader : jbpy.core.ImageSubheader
568        Subheader of the image to describe
569    block_info : list of BlockInfo
570        Input BlockInfo dictionaries
571    mask_table : MaskTable
572        Mask Table from the image segment
573
574    Returns
575    -------
576    list of BlockInfo dictionaries
577    """
578    assert "M" in image_subheader["IC"].value
579
580    block_info = copy.deepcopy(block_info)
581
582    # Update description for masked data.  "NM"
583    for info in block_info:
584        # mask tables are inserted immediately before the pixel data
585        assert info["offset"] is not None
586        info["offset"] += mask_table["IMDATOFF"].value
587
588        n = (
589            info["block_row_index"] * image_subheader["NBPR"].value
590            + info["block_col_index"]
591        )
592        m = info["block_band_index"]
593        if "TPXCD" in mask_table:
594            info["pad_value"] = mask_table["TPXCD"].value
595
596        bmr_name = mask_table.bmr_name(n, m)
597        if bmr_name in mask_table:
598            if (
599                mask_table[bmr_name].value == BLOCK_NOT_RECORDED
600            ):  # block is omitted from file
601                info["offset"] = None
602                info["nbytes"] = 0
603            else:
604                info["offset"] = (
605                    +mask_table["IMDATOFF"].value + mask_table[bmr_name].value
606                )
607        tmr_name = mask_table.tmr_name(n, m)
608        if tmr_name in mask_table:
609            info["has_pad"] = mask_table[tmr_name].value != BLOCK_NOT_RECORDED
610
611    return block_info
BLOCK_NOT_RECORDED = 4294967295
def array_protocol_typestr(pvtype: str, nbpp: int) -> str:
17def array_protocol_typestr(pvtype: str, nbpp: int) -> str:
18    """Generate a NumPy array interface protocol typestr describing a NITF pixel
19
20    Parameters
21    ----------
22    pvtype : str
23        Image subheader Pixel Value Type (PVTYPE)
24    nbpp : int
25        Image subheader Number of Bits Per Pixel Per Band (NBPP)
26
27    Notes
28    -----
29    The resulting typestr is sutable for storing the pixel value.
30    Additional transforms of the pixel values may be necessary to account
31    for PJUST and ABPP.
32    """
33    assert nbpp % 8 == 0  # 12bit not implemented
34    dtype_str = ">"
35    dtype_str += _PVTYPE_TO_AP_TYPE_STRING[pvtype]
36    dtype_str += str(int(nbpp // 8))
37    return dtype_str

Generate a NumPy array interface protocol typestr describing a NITF pixel

Parameters
  • pvtype (str): Image subheader Pixel Value Type (PVTYPE)
  • nbpp (int): Image subheader Number of Bits Per Pixel Per Band (NBPP)
Notes

The resulting typestr is sutable for storing the pixel value. Additional transforms of the pixel values may be necessary to account for PJUST and ABPP.

class BinaryUnsignedInteger(jbpy.core.PythonConverter):
40class BinaryUnsignedInteger(jbpy.core.PythonConverter):
41    """convert to/from a binary integer"""
42
43    def to_bytes_impl(self, decoded_value: int, size: int) -> bytes:
44        decoded_value = int(decoded_value)
45        return decoded_value.to_bytes(size, byteorder="big", signed=False)
46
47    def from_bytes_impl(self, encoded_value: bytes) -> int:
48        return int.from_bytes(encoded_value, byteorder="big", signed=False)

convert to/from a binary integer

def to_bytes_impl(self, decoded_value: int, size: int) -> bytes:
43    def to_bytes_impl(self, decoded_value: int, size: int) -> bytes:
44        decoded_value = int(decoded_value)
45        return decoded_value.to_bytes(size, byteorder="big", signed=False)

Convert python type to bytes

def from_bytes_impl(self, encoded_value: bytes) -> int:
47    def from_bytes_impl(self, encoded_value: bytes) -> int:
48        return int.from_bytes(encoded_value, byteorder="big", signed=False)

Convert bytes to python type

class MaskTable(jbpy.core.Group):
 54class MaskTable(jbpy.core.Group):
 55    """JBP Image Data Mask Table
 56
 57    Parameters
 58    ----------
 59    name : str
 60        Name to give this group
 61    image_subheader : jbpy.core.ImageSubheader
 62        Subheader for the image segment containing the mask table
 63
 64    Notes
 65    -----
 66    image_subheader must not change after initializing this class.
 67    """
 68
 69    def __init__(self, name: str, image_subheader: jbpy.core.ImageSubheader):
 70        super().__init__(name)
 71        self._num_blocks = image_subheader["NBPC"].value * image_subheader["NBPR"].value
 72
 73        if image_subheader["IMODE"].value == "S":
 74            # Each band is stored as a separate block
 75            self._num_bands = image_subheader.get(
 76                "XBANDS", image_subheader["NBANDS"]
 77            ).value
 78        else:
 79            self._num_bands = 1
 80
 81        self._append(
 82            jbpy.core.Field(
 83                "IMDATOFF",
 84                "Blocked Image Data Offset",
 85                4,
 86                converter=BinaryUnsignedInteger(),
 87                default=0,
 88            )
 89        )
 90        self._append(
 91            jbpy.core.Field(
 92                "BMRLNTH",
 93                "Block Mask Record Length",
 94                2,
 95                decoded_range=jbpy.core.Enum([0, 4]),
 96                converter=BinaryUnsignedInteger(),
 97                default=0,
 98                setter_callback=self._handle_bmrlnth,
 99            )
100        )
101        self._append(
102            jbpy.core.Field(
103                "TMRLNTH",
104                "Pad Pixel Mask Record Length",
105                2,
106                decoded_range=jbpy.core.Enum([0, 4]),
107                converter=BinaryUnsignedInteger(),
108                default=0,
109                setter_callback=self._handle_tmrlnth,
110            )
111        )
112        self._append(
113            jbpy.core.Field(
114                "TPXCDLNTH",
115                "Pad Output Pixel Code Length",
116                2,
117                converter=BinaryUnsignedInteger(),
118                default=0,
119                setter_callback=self._handle_tpxcdlnth,
120            )
121        )
122
123    def _handle_bmrlnth(self, field):
124        self._remove_all("BMR\\d+BND\\d+")
125        if field.value == 0:
126            return
127
128        after = self["TPXCDLNTH"]
129        if "TPXCD" in self:
130            after = self["TPXCD"]
131
132        for band_idx in range(self._num_bands):
133            for block_idx in range(self._num_blocks):
134                name = self.bmr_name(block_idx, band_idx)
135                after = self._insert_after(
136                    after,
137                    jbpy.core.Field(
138                        name,
139                        f"Block {block_idx}, Band {band_idx} Offset",
140                        4,
141                        converter=BinaryUnsignedInteger(),
142                        default=0,
143                    ),
144                )
145
146    def _handle_tmrlnth(self, field):
147        self._remove_all("TMR\\d+BND\\d+")
148        if field.value == 0:
149            return
150
151        after = self["TPXCDLNTH"]
152        if "TPXCD" in self:
153            after = self["TPXCD"]
154
155        for bmr_field in self.find_all("BMR\\d+BND\\d+"):
156            if bmr_field.get_offset() > after.get_offset():
157                after = bmr_field
158
159        for band_idx in range(self._num_bands):
160            for block_idx in range(self._num_blocks):
161                name = self.tmr_name(block_idx, band_idx)
162                after = self._insert_after(
163                    after,
164                    jbpy.core.Field(
165                        name,
166                        f"Pad Pixel {block_idx}, Band {band_idx}",
167                        4,
168                        converter=BinaryUnsignedInteger(),
169                        default=0,
170                    ),
171                )
172
173    def _handle_tpxcdlnth(self, field):
174        self._remove_all("TPXCD")
175        tpxcd_length = int(math.ceil(field.value / 8))
176        if tpxcd_length > 0:
177            self._insert_after(
178                field,
179                jbpy.core.Field(
180                    "TPXCD",
181                    "Pad Output Pixel Code",
182                    tpxcd_length,
183                    converter=jbpy.core.Bytes(),
184                    default=b"\x00" * tpxcd_length,
185                ),
186            )
187
188    @staticmethod
189    def bmr_name(block_index: int, band_index: int) -> str:
190        """Generate the expected name for BMRnBNDm given indices
191
192        Parameters
193        ----------
194        block_index : int
195            Linear index of the block (zero-based).  "n"
196        band_index : int
197            Index of the band (zero-based).  "m"
198
199        Returns
200        -------
201        str
202            Field name
203        """
204        return f"BMR{block_index:08d}BND{band_index:05d}"
205
206    @staticmethod
207    def tmr_name(block_index: int, band_index: int) -> str:
208        """Generate the expected name for TMRnBNDm given indices
209
210        Parameters
211        ----------
212        block_index : int
213            Linear index of the block (one-based).  "n"
214        band_index : int
215            Index of the band (one-based).  "m"
216
217        Returns
218        -------
219        str
220            Field name
221        """
222        return f"TMR{block_index:08d}BND{band_index:05d}"

JBP Image Data Mask Table

Parameters
  • name (str): Name to give this group
  • image_subheader (jbpy.core.ImageSubheader): Subheader for the image segment containing the mask table
Notes

image_subheader must not change after initializing this class.

MaskTable(name: str, image_subheader: jbpy.core.ImageSubheader)
 69    def __init__(self, name: str, image_subheader: jbpy.core.ImageSubheader):
 70        super().__init__(name)
 71        self._num_blocks = image_subheader["NBPC"].value * image_subheader["NBPR"].value
 72
 73        if image_subheader["IMODE"].value == "S":
 74            # Each band is stored as a separate block
 75            self._num_bands = image_subheader.get(
 76                "XBANDS", image_subheader["NBANDS"]
 77            ).value
 78        else:
 79            self._num_bands = 1
 80
 81        self._append(
 82            jbpy.core.Field(
 83                "IMDATOFF",
 84                "Blocked Image Data Offset",
 85                4,
 86                converter=BinaryUnsignedInteger(),
 87                default=0,
 88            )
 89        )
 90        self._append(
 91            jbpy.core.Field(
 92                "BMRLNTH",
 93                "Block Mask Record Length",
 94                2,
 95                decoded_range=jbpy.core.Enum([0, 4]),
 96                converter=BinaryUnsignedInteger(),
 97                default=0,
 98                setter_callback=self._handle_bmrlnth,
 99            )
100        )
101        self._append(
102            jbpy.core.Field(
103                "TMRLNTH",
104                "Pad Pixel Mask Record Length",
105                2,
106                decoded_range=jbpy.core.Enum([0, 4]),
107                converter=BinaryUnsignedInteger(),
108                default=0,
109                setter_callback=self._handle_tmrlnth,
110            )
111        )
112        self._append(
113            jbpy.core.Field(
114                "TPXCDLNTH",
115                "Pad Output Pixel Code Length",
116                2,
117                converter=BinaryUnsignedInteger(),
118                default=0,
119                setter_callback=self._handle_tpxcdlnth,
120            )
121        )
@staticmethod
def bmr_name(block_index: int, band_index: int) -> str:
188    @staticmethod
189    def bmr_name(block_index: int, band_index: int) -> str:
190        """Generate the expected name for BMRnBNDm given indices
191
192        Parameters
193        ----------
194        block_index : int
195            Linear index of the block (zero-based).  "n"
196        band_index : int
197            Index of the band (zero-based).  "m"
198
199        Returns
200        -------
201        str
202            Field name
203        """
204        return f"BMR{block_index:08d}BND{band_index:05d}"

Generate the expected name for BMRnBNDm given indices

Parameters
  • block_index (int): Linear index of the block (zero-based). "n"
  • band_index (int): Index of the band (zero-based). "m"
Returns
  • str: Field name
@staticmethod
def tmr_name(block_index: int, band_index: int) -> str:
206    @staticmethod
207    def tmr_name(block_index: int, band_index: int) -> str:
208        """Generate the expected name for TMRnBNDm given indices
209
210        Parameters
211        ----------
212        block_index : int
213            Linear index of the block (one-based).  "n"
214        band_index : int
215            Index of the band (one-based).  "m"
216
217        Returns
218        -------
219        str
220            Field name
221        """
222        return f"TMR{block_index:08d}BND{band_index:05d}"

Generate the expected name for TMRnBNDm given indices

Parameters
  • block_index (int): Linear index of the block (one-based). "n"
  • band_index (int): Index of the band (one-based). "m"
Returns
  • str: Field name
def read_mask_table( image_segment: jbpy.core.ImageSegment, file: jbpy.core.BinaryFile_R) -> MaskTable:
225def read_mask_table(
226    image_segment: jbpy.core.ImageSegment, file: jbpy.core.BinaryFile_R
227) -> MaskTable:
228    """Read an image segment's mask table
229
230    Parameters
231    ----------
232    image_segment : ImageSegment
233        Which image segment's mask table to read
234    file : file-like
235        JBP file containing the image_segment
236
237    Returns
238    -------
239    dictionary containing the mask table values or None if there is no mask table
240    """
241    file.seek(image_segment["Data"].get_offset(), os.SEEK_SET)
242    mt = MaskTable("MaskTable", image_segment["subheader"])
243    mt.load(file)
244    return mt

Read an image segment's mask table

Parameters
  • image_segment (ImageSegment): Which image segment's mask table to read
  • file (file-like): JBP file containing the image_segment
Returns
  • dictionary containing the mask table values or None if there is no mask table
IMPLEMENTED_PIXEL_TYPES = [('INT', 8), ('INT', 16), ('INT', 32), ('INT', 64), ('SI', 8), ('SI', 16), ('SI', 32), ('SI', 64), ('R', 32), ('R', 64), ('C', 64)]
def image_array_description( image_segment: jbpy.core.ImageSegment) -> tuple[tuple[int, int, int], int, str]:
264def image_array_description(
265    image_segment: jbpy.core.ImageSegment,
266) -> tuple[tuple[int, int, int], int, str]:
267    """Shape of image described by the image segment
268
269    Always describes a 3D shape with one axis being the bands.
270    Axis containing the bands is determined by the IMODE field.
271
272    Parameters
273    ----------
274    image_segment : jbpy.core.ImageSegment
275        The image segment to describe
276
277    Returns
278    -------
279    shape : tuple
280        Shape of the full image
281    band_axis : int
282        Which axis contains the bands.  Other two axes will be rows and cols, respectively.
283    typestr : str
284        Array interface protocol typestr describing the pixel type
285    """
286    subhdr = image_segment["subheader"]
287    num_bands = subhdr.get("XBANDS", subhdr["NBANDS"]).value
288    nrows = subhdr["NROWS"].value
289    ncols = subhdr["NCOLS"].value
290    imode = subhdr["IMODE"].value
291    pvtype = subhdr["PVTYPE"].value
292    nbpp = subhdr["NBPP"].value
293
294    if imode == "B":
295        shape = (num_bands, nrows, ncols)
296        band_axis = 0
297    elif imode == "P":
298        shape = (nrows, ncols, num_bands)
299        band_axis = 2
300    elif imode == "R":
301        shape = (nrows, num_bands, ncols)
302        band_axis = 1
303    elif imode == "S":
304        shape = (num_bands, nrows, ncols)
305        band_axis = 0
306
307    typestr = array_protocol_typestr(pvtype, nbpp)
308
309    return shape, band_axis, typestr

Shape of image described by the image segment

Always describes a 3D shape with one axis being the bands. Axis containing the bands is determined by the IMODE field.

Parameters
Returns
  • shape (tuple): Shape of the full image
  • band_axis (int): Which axis contains the bands. Other two axes will be rows and cols, respectively.
  • typestr (str): Array interface protocol typestr describing the pixel type
Slice3DType = tuple[slice | int, slice | int, slice | int]
class BlockInfo(typing.TypedDict):
315class BlockInfo(typing.TypedDict):
316    """Information describing a single image data block"""
317
318    #: band index of this block.  Zero unless IMODE == S
319    block_band_index: int
320
321    #: row index of this block
322    block_row_index: int
323
324    #: col index of this block
325    block_col_index: int
326
327    #: Offset to first byte of the block relative to the start of the image data
328    offset: int | None
329
330    #: Size of the block in bytes (including fill pixels)
331    nbytes: int
332
333    #: Shape of the block
334    shape: tuple[int, int, int]
335
336    #: Which axis of the block's shape contains the bands
337    band_axis: int
338
339    #: Array interface protocol typestr for the block's pixels
340    typestr: str
341
342    #: 3D slice of full image describing this blocks' non-fill pixels
343    image_slicing: Slice3DType
344
345    #: 3D slice of this block describing the non-fill pixels
346    block_slicing: Slice3DType
347
348    #: How many rows of fill are contained in this block
349    fill_rows: int
350
351    #: How many columns of fill are contained in this block
352    fill_cols: int
353
354    #: Does this block contain pad pixels
355    has_pad: bool
356
357    #: pixel bit pattern identifiying pad pixels
358    pad_value: bytes | None

Information describing a single image data block

block_band_index: int
block_row_index: int
block_col_index: int
offset: int | None
nbytes: int
shape: tuple[int, int, int]
band_axis: int
typestr: str
image_slicing: tuple[slice | int, slice | int, slice | int]
block_slicing: tuple[slice | int, slice | int, slice | int]
fill_rows: int
fill_cols: int
has_pad: bool
pad_value: bytes | None
def block_info_uncompressed( image_segment: jbpy.core.ImageSegment, file: jbpy.core.BinaryFile_R | None = None) -> list[BlockInfo]:
361def block_info_uncompressed(
362    image_segment: jbpy.core.ImageSegment, file: jbpy.core.BinaryFile_R | None = None
363) -> list[BlockInfo]:
364    """
365    Describe the blocks comprising an uncompressed image segment
366
367    Parameters
368    ----------
369    image_segment : ImageSegment
370        Which image segment to describe
371    file : file-like
372        JBP file containing the image_segment.  Required if image segment contains Mask Table. (IC field contains "M")
373
374    Returns
375    -------
376    list of BlockInfo dictionaries
377    """
378    subhdr = image_segment["subheader"]
379    assert subhdr["IC"].value in ("NC", "NM")
380
381    block_info = nominal_block_info(subhdr)
382
383    mask_table = None
384    if "M" in subhdr["IC"].value:
385        assert file is not None
386        mask_table = read_mask_table(image_segment, file)
387        block_info = apply_mask_table_to_block_info(subhdr, block_info, mask_table)
388
389    return block_info

Describe the blocks comprising an uncompressed image segment

Parameters
  • image_segment (ImageSegment): Which image segment to describe
  • file (file-like): JBP file containing the image_segment. Required if image segment contains Mask Table. (IC field contains "M")
Returns
  • list of BlockInfo dictionaries
def nominal_block_info( image_subheader: jbpy.core.ImageSubheader) -> list[BlockInfo]:
392def nominal_block_info(image_subheader: jbpy.core.ImageSubheader) -> list[BlockInfo]:
393    """Create a list of block information assuming an image is uncompressed and unmasked (IC=NC)
394
395    Parameters
396    ----------
397    image_subheader : jbpy.core.ImageSubheader
398        Subheader of the image to describe
399
400    Returns
401    -------
402    list of BlockInfo dictionaries
403    """
404
405    assert (
406        image_subheader["PVTYPE"].value,
407        image_subheader["NBPP"].value,
408    ) in IMPLEMENTED_PIXEL_TYPES
409
410    num_image_bands = image_subheader.get("XBANDS", image_subheader["NBANDS"]).value
411    if image_subheader["IMODE"].value == "S":
412        # Each band is stored as a separate block
413        num_bands_in_block = 1
414        num_block_bands = num_image_bands
415    else:
416        num_bands_in_block = num_image_bands
417        num_block_bands = 1
418
419    rows_per_block = image_subheader["NPPBV"].value or image_subheader["NROWS"].value
420    cols_per_block = image_subheader["NPPBH"].value or image_subheader["NCOLS"].value
421    expected_blocks_per_col = int(
422        math.ceil(image_subheader["NROWS"].value / rows_per_block)
423    )
424    expected_blocks_per_row = int(
425        math.ceil(image_subheader["NCOLS"].value / cols_per_block)
426    )
427
428    if expected_blocks_per_col != image_subheader["NBPC"].value:
429        raise RuntimeError(
430            f"Image segment has {image_subheader['NBPC'].value} vertical blocks, expected {expected_blocks_per_col}"
431        )
432    if expected_blocks_per_row != image_subheader["NBPR"].value:
433        raise RuntimeError(
434            f"Image segment has {image_subheader['NBPR'].value} horizontal blocks, expected {expected_blocks_per_row}"
435        )
436
437    num_fill_rows = (rows_per_block * expected_blocks_per_col) - image_subheader[
438        "NROWS"
439    ].value
440    num_fill_cols = (cols_per_block * expected_blocks_per_row) - image_subheader[
441        "NCOLS"
442    ].value
443
444    if num_fill_rows < 0 or num_fill_cols < 0:
445        raise RuntimeError("Image segment is missing blocks")
446
447    # Will not work for NBPP == 12
448    block_nbytes = (
449        num_bands_in_block
450        * rows_per_block
451        * cols_per_block
452        * image_subheader["NBPP"].value
453        // 8
454    )
455
456    blocks = []
457    for block_counter, block_indices in enumerate(
458        itertools.product(
459            range(num_block_bands),
460            range(image_subheader["NBPC"].value),
461            range(image_subheader["NBPR"].value),
462        )
463    ):
464        block_band_index, block_row_index, block_col_index = block_indices
465        start_row = block_row_index * rows_per_block
466        start_col = block_col_index * cols_per_block
467
468        # how much fill is in this block
469        fill_rows = (
470            num_fill_rows if block_row_index == image_subheader["NBPC"].value - 1 else 0
471        )
472        fill_cols = (
473            num_fill_cols if block_col_index == image_subheader["NBPR"].value - 1 else 0
474        )
475        image_slice_rows = slice(start_row, start_row + rows_per_block - fill_rows)
476        image_slice_cols = slice(start_col, start_col + cols_per_block - fill_cols)
477        block_slice_rows = slice(0, rows_per_block - fill_rows)
478        block_slice_cols = slice(0, cols_per_block - fill_cols)
479
480        image_slicing: Slice3DType
481        block_slicing: Slice3DType
482        if image_subheader["IMODE"].value == "P":
483            shape = (rows_per_block, cols_per_block, num_image_bands)
484            image_slicing = (
485                image_slice_rows,
486                image_slice_cols,
487                slice(None, None),  # all bands
488            )
489            block_slicing = (
490                block_slice_rows,
491                block_slice_cols,
492                slice(None, None),  # all bands
493            )
494            band_axis = 2
495        elif image_subheader["IMODE"].value == "B":
496            shape = (num_image_bands, rows_per_block, cols_per_block)
497            image_slicing = (
498                slice(None, None),  # all bands
499                image_slice_rows,
500                image_slice_cols,
501            )
502            block_slicing = (
503                slice(None, None),  # all bands
504                block_slice_rows,
505                block_slice_cols,
506            )
507            band_axis = 0
508        elif image_subheader["IMODE"].value == "R":
509            shape = (rows_per_block, num_image_bands, cols_per_block)
510            image_slicing = (
511                image_slice_rows,
512                slice(None, None),  # all bands
513                image_slice_cols,
514            )
515            block_slicing = (
516                block_slice_rows,
517                slice(None, None),  # all bands
518                block_slice_cols,
519            )
520            band_axis = 1
521        elif image_subheader["IMODE"].value == "S":
522            shape = (1, rows_per_block, cols_per_block)
523            image_slicing = (
524                block_band_index,  # single band
525                image_slice_rows,
526                image_slice_cols,
527            )
528            block_slicing = (
529                0,  # each block contains only a single band
530                block_slice_rows,
531                block_slice_cols,
532            )
533            band_axis = 0
534
535        # Nominal description for unmasked/unpadded data, "NC"
536        info: BlockInfo = {
537            "block_band_index": block_band_index,
538            "block_row_index": block_row_index,
539            "block_col_index": block_col_index,
540            "offset": block_nbytes * block_counter,
541            "nbytes": block_nbytes,
542            "shape": shape,
543            "typestr": array_protocol_typestr(
544                image_subheader["PVTYPE"].value, image_subheader["NBPP"].value
545            ),
546            "image_slicing": image_slicing,
547            "block_slicing": block_slicing,
548            "fill_rows": fill_rows,
549            "fill_cols": fill_cols,
550            "has_pad": False,
551            "pad_value": None,
552            "band_axis": band_axis,
553        }
554        blocks.append(info)
555
556    return blocks

Create a list of block information assuming an image is uncompressed and unmasked (IC=NC)

Parameters
Returns
  • list of BlockInfo dictionaries
def apply_mask_table_to_block_info( image_subheader: jbpy.core.ImageSubheader, block_info: list[BlockInfo], mask_table: MaskTable) -> list[BlockInfo]:
559def apply_mask_table_to_block_info(
560    image_subheader: jbpy.core.ImageSubheader,
561    block_info: list[BlockInfo],
562    mask_table: MaskTable,
563) -> list[BlockInfo]:
564    """Return a copy of a block_info list with information from a mask table applied
565
566    Parameters
567    ----------
568    image_subheader : jbpy.core.ImageSubheader
569        Subheader of the image to describe
570    block_info : list of BlockInfo
571        Input BlockInfo dictionaries
572    mask_table : MaskTable
573        Mask Table from the image segment
574
575    Returns
576    -------
577    list of BlockInfo dictionaries
578    """
579    assert "M" in image_subheader["IC"].value
580
581    block_info = copy.deepcopy(block_info)
582
583    # Update description for masked data.  "NM"
584    for info in block_info:
585        # mask tables are inserted immediately before the pixel data
586        assert info["offset"] is not None
587        info["offset"] += mask_table["IMDATOFF"].value
588
589        n = (
590            info["block_row_index"] * image_subheader["NBPR"].value
591            + info["block_col_index"]
592        )
593        m = info["block_band_index"]
594        if "TPXCD" in mask_table:
595            info["pad_value"] = mask_table["TPXCD"].value
596
597        bmr_name = mask_table.bmr_name(n, m)
598        if bmr_name in mask_table:
599            if (
600                mask_table[bmr_name].value == BLOCK_NOT_RECORDED
601            ):  # block is omitted from file
602                info["offset"] = None
603                info["nbytes"] = 0
604            else:
605                info["offset"] = (
606                    +mask_table["IMDATOFF"].value + mask_table[bmr_name].value
607                )
608        tmr_name = mask_table.tmr_name(n, m)
609        if tmr_name in mask_table:
610            info["has_pad"] = mask_table[tmr_name].value != BLOCK_NOT_RECORDED
611
612    return block_info

Return a copy of a block_info list with information from a mask table applied

Parameters
  • image_subheader (jbpy.core.ImageSubheader): Subheader of the image to describe
  • block_info (list of BlockInfo): Input BlockInfo dictionaries
  • mask_table (MaskTable): Mask Table from the image segment
Returns
  • list of BlockInfo dictionaries