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
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.
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
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
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
Inherited Members
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.
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 )
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
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
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
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
- image_segment (jbpy.core.ImageSegment): The image segment to describe
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
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
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
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
- image_subheader (jbpy.core.ImageSubheader): Subheader of the image to describe
Returns
- list of BlockInfo dictionaries
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