diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..2531548f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,118 @@ +name: Build packages + +on: + push: + branches: + - master + - 'integration/**' + +jobs: + build: + # This section builds the distribution for all versions of Python listed in + # the python-version matrix under strategy to confirm that it builds for + # all of those versions. It then uploads the package built by Python 3.9 + # for use in later jobs. + name: Build distribution + runs-on: ubuntu-latest + outputs: + version: ${{ steps.extract_version.outputs.raw_version }} + rc_version: ${{ steps.extract_release_candidate_version.outputs.version }} + strategy: + matrix: + python-version: [3.9, 3.10, 3.11, 3.12, 3.13, 3.14] + steps: + # Pull the current branch to build the package from. + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{github.ref}} + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Extract version number from __about__.py + shell: bash + run: echo "raw_version=`sed -n 's/__version__ = \"\(.*\)\"/\1/p' < sarpy/__about__.py`" >> $GITHUB_OUTPUT + id: extract_version + - name: Set version number for release candidate + shell: bash + if: contains(github.ref, 'refs/heads/integration/') + run: | + echo ${{ steps.extract_version.outputs.raw_version }} + rc_version=${{ steps.extract_version.outputs.raw_version }}rc1 + sed -i -e "s/__version__ = \"${{ steps.extract_version.outputs.raw_version }}/__version__ = \"$rc_version/g" sarpy/__about__.py + echo "version=$rc_version" >> $GITHUB_OUTPUT + id: extract_release_candidate_version + - name: Install pypa/build + run: >- + python3 -m + pip install + build + --user + - name: Build sarpy binary wheel and a source tarball + run: python3 -m build + - name: Upload all the dists + if: matrix.python-version == 3.9 + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.extract_version.outputs.raw_version }} + path: dist/ + overwrite: true + # This job creates a GitHub release and uploads the package contents created + # in the build job to the release. + release: + name: Create release + needs: build + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{github.ref}} + - name: Extract version number from __about__.py + shell: bash + run: | + if "${{endswith(github.ref, 'master')}}" + then + echo "version=${{ needs.build.outputs.version }}" >> $GITHUB_OUTPUT + else + echo "version=${{ needs.build.outputs.rc_version }}" >> $GITHUB_OUTPUT + fi + id: extract_version + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: ${{ needs.build.outputs.version }} + path: dist + - name: Create a release + id: create_release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ steps.extract_version.outputs.version }} + generate_release_notes: true + name: Version ${{ steps.extract_version.outputs.version }} + draft: false + prerelease: false + target_commitish: ${{github.ref}} + files: dist/* + + publish-to-pypi: + name: Publish to PyPI + needs: release + runs-on: ubuntu-latest + environment: + name: development + permissions: + id-token: write + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: ${{ needs.build.outputs.version }} + path: dist + merge-multiple: true + - run: ls -R dist + - name: Publish distribution to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://pypi.org/legacy/ diff --git a/sarpy/__about__.py b/sarpy/__about__.py index 5a196794..7da772c7 100644 --- a/sarpy/__about__.py +++ b/sarpy/__about__.py @@ -27,13 +27,12 @@ '__license__', '__copyright__'] from sarpy.__details__ import __classification__, _post_identifier -_version_number = '1.3.60' -__version__ = _version_number + _post_identifier +__version__ = "1.3.61" __author__ = "National Geospatial-Intelligence Agency" __url__ = "https://github.com/ngageoint/sarpy" -__email__ = "richard.m.naething@nga.mil" +__email__ = "SARToolboxDev@nga.mil" __title__ = "sarpy" diff --git a/sarpy/io/complex/sicd.py b/sarpy/io/complex/sicd.py index ccda743c..8ba9def9 100644 --- a/sarpy/io/complex/sicd.py +++ b/sarpy/io/complex/sicd.py @@ -865,7 +865,8 @@ def __init__( sicd_meta: Optional[SICDType] = None, sicd_writing_details: Optional[SICDWritingDetails] = None, check_older_version: bool = False, - check_existence: bool = True): + check_existence: bool = True, + in_memory: bool = None): """ Parameters @@ -878,6 +879,8 @@ def __init__( NGA applications like SOCET or RemoteView check_existence : bool Should we check if the given file already exists? + in_memory : bool + If True force in-memory writing, if False force file writing. """ if sicd_meta is None and sicd_writing_details is None: @@ -885,7 +888,7 @@ def __init__( if sicd_writing_details is None: sicd_writing_details = SICDWritingDetails(sicd_meta, check_older_version=check_older_version) NITFWriter.__init__( - self, file_object, sicd_writing_details, check_existence=check_existence) + self, file_object, sicd_writing_details, check_existence=check_existence, in_memory=in_memory) @property def nitf_writing_details(self) -> SICDWritingDetails: diff --git a/sarpy/io/complex/sicd_elements/blocks.py b/sarpy/io/complex/sicd_elements/blocks.py index 33d04bad..0299776c 100644 --- a/sarpy/io/complex/sicd_elements/blocks.py +++ b/sarpy/io/complex/sicd_elements/blocks.py @@ -1090,6 +1090,12 @@ def get_array(self, dtype=numpy.float64) -> numpy.ndarray: """ return numpy.array(self._coefs, dtype=dtype) + + def __eq__(self, other): + if not isinstance(other, Poly1DType): + return TypeError('Provided object is not an Poly1DType.') + return numpy.array_equal(self.Coefs, other.Coefs) and self.order1 == other.order1 + @classmethod def from_node(cls, node, xml_ns, ns_key=None, kwargs=None): diff --git a/sarpy/io/general/data_segment.py b/sarpy/io/general/data_segment.py index 60dd3a4d..6e8980c1 100644 --- a/sarpy/io/general/data_segment.py +++ b/sarpy/io/general/data_segment.py @@ -2053,7 +2053,7 @@ def get_raw_bytes(self, warn: bool = False) -> Union[bytes, Tuple]: logger.error( 'There has been a call to `get_raw_bytes` from {},\n\t' 'but all pixels are not fully written'.format(self.__class__)) - return self.underlying_array.tobytes() + return self.underlying_array.view('B').reshape(-1) def flush(self) -> None: self._validate_closed() diff --git a/sarpy/io/general/nitf.py b/sarpy/io/general/nitf.py index 86fa75f9..d9b7443c 100644 --- a/sarpy/io/general/nitf.py +++ b/sarpy/io/general/nitf.py @@ -2535,6 +2535,8 @@ def _flatten_bytes(value: Union[bytes, Sequence]) -> bytes: return value elif isinstance(value, Sequence): return b''.join(_flatten_bytes(entry) for entry in value) + elif isinstance(value, numpy.ndarray) and value.dtype == numpy.uint8: + return value.reshape(-1) else: raise TypeError('input must be a bytes object, or a sequence with bytes objects as leaves') @@ -3516,7 +3518,8 @@ def __init__( self, file_object: Union[str, BinaryIO], writing_details: NITFWritingDetails, - check_existence: bool = True): + check_existence: bool = True, + in_memory: bool = None): """ Parameters @@ -3525,6 +3528,8 @@ def __init__( writing_details : NITFWritingDetails check_existence : bool Should we check if the given file already exists? + in_memory : bool + If True force in-memory writing, if False force file writing. Raises ------ @@ -3547,6 +3552,7 @@ def __init__( raise ValueError('file_object requires a file path or BinaryIO object') self._file_object = file_object + if is_real_file(file_object): self._file_name = file_object.name self._in_memory = False @@ -3554,6 +3560,9 @@ def __init__( self._file_name = None self._in_memory = True + if in_memory is not None: + self._in_memory = in_memory + self.nitf_writing_details = writing_details if not self.nitf_writing_details.verify_images_have_no_compression(): diff --git a/sarpy/io/product/sidd.py b/sarpy/io/product/sidd.py index b57c5972..41026e63 100644 --- a/sarpy/io/product/sidd.py +++ b/sarpy/io/product/sidd.py @@ -893,7 +893,8 @@ def __init__( Sequence[SIDDType2], Sequence[SIDDType1]]] = None, sicd_meta: Optional[Union[SICDType, Sequence[SICDType]]] = None, sidd_writing_details: Optional[SIDDWritingDetails] = None, - check_existence: bool = True): + check_existence: bool = True, + in_memory: bool = None): """ Parameters @@ -904,6 +905,8 @@ def __init__( sidd_writing_details : None|SIDDWritingDetails check_existence : bool Should we check if the given file already exists? + in_memory : bool + If True force in-memory writing, if False force file writing. """ if sidd_meta is None and sidd_writing_details is None: @@ -911,7 +914,7 @@ def __init__( if sidd_writing_details is None: sidd_writing_details = SIDDWritingDetails(sidd_meta, sicd_meta=sicd_meta) NITFWriter.__init__( - self, file_object, sidd_writing_details, check_existence=check_existence) + self, file_object, sidd_writing_details, check_existence=check_existence, in_memory=in_memory) @property def nitf_writing_details(self) -> SIDDWritingDetails: diff --git a/sarpy/processing/sidd/sidd_product_creation.py b/sarpy/processing/sidd/sidd_product_creation.py index 88a93756..82be2c90 100644 --- a/sarpy/processing/sidd/sidd_product_creation.py +++ b/sarpy/processing/sidd/sidd_product_creation.py @@ -43,7 +43,7 @@ DEFAULT_IMG_REMAP = NRL DEFAULT_CSI_REMAP = NRL -DEFAULT_DI_REMAP = NRL +DEFAULT_DI_REMAP = NRL _output_text = 'output_directory `{}`\n\t' \ 'does not exist or is not a directory' @@ -167,7 +167,7 @@ def create_detected_image_sidd( ortho_bounds = ortho_iterator.ortho_bounds sidd_structure = create_sidd_structure( ortho_helper, ortho_bounds, - product_class='Detected Image', pixel_type='MONO{}I'.format(remap_function.bit_depth), version=version) + product_class='Detected Image', pixel_type='MONO{}I'.format(remap_function.bit_depth), version=version, remap_function=remap_function) # set suggested name sidd_structure.NITF['SUGGESTED_NAME'] = ortho_helper.sicd.get_suggested_name(ortho_helper.index)+'_IMG' @@ -256,7 +256,7 @@ def create_csi_sidd( ortho_bounds = ortho_iterator.ortho_bounds sidd_structure = create_sidd_structure( ortho_helper, ortho_bounds, - product_class='Color Subaperture Image', pixel_type='RGB24I', version=version) + product_class='Color Subaperture Image', pixel_type='RGB24I', version=version, remap_function=remap_function) # set suggested name sidd_structure.NITF['SUGGESTED_NAME'] = csi_calculator.sicd.get_suggested_name(csi_calculator.index)+'_CSI' @@ -352,7 +352,7 @@ def create_dynamic_image_sidd( ortho_bounds = ortho_iterator.ortho_bounds sidd_structure = create_sidd_structure( ortho_helper, ortho_bounds, - product_class='Dynamic Image', pixel_type='MONO{}I'.format(remap_function.bit_depth), version=version) + product_class='Dynamic Image', pixel_type='MONO{}I'.format(remap_function.bit_depth), version=version, remap_function=remap_function) # set suggested name sidd_structure.NITF['SUGGESTED_NAME'] = subap_calculator.sicd.get_suggested_name(subap_calculator.index)+'__DI' the_sidds = [] diff --git a/sarpy/processing/sidd/sidd_structure_creation.py b/sarpy/processing/sidd/sidd_structure_creation.py index 79dda752..7041b858 100644 --- a/sarpy/processing/sidd/sidd_structure_creation.py +++ b/sarpy/processing/sidd/sidd_structure_creation.py @@ -209,7 +209,7 @@ def _create_plane_projection_v3(proj_helper, bounds): ######################### # Version 3 element creation -def create_sidd_structure_v3(ortho_helper, bounds, product_class, pixel_type): +def create_sidd_structure_v3(ortho_helper, bounds, product_class, pixel_type, remap_function=None ): """ Create a SIDD version 3.0 structure based on the orthorectification helper and pixel bounds. @@ -244,9 +244,9 @@ def _create_display_v3(): NonInteractiveProcessing=[NonInteractiveProcessingType3( ProductGenerationOptions=ProductGenerationOptionsType3( DataRemapping=NewLookupTableType3( - LUTName='DENSITY', + LUTName=remap_function.name.upper(), Predefined=PredefinedLookupType3( - DatabaseName='DENSITY'))), + DatabaseName=remap_function.name.upper()))), RRDS=RRDSType3(DownsamplingMethod='DECIMATE'), band=i+1) for i in range(bands)], InteractiveProcessing=[InteractiveProcessingType3( @@ -326,7 +326,7 @@ def _create_exploitation_v3(): ######################### # Version 2 element creation -def create_sidd_structure_v2(ortho_helper, bounds, product_class, pixel_type): +def create_sidd_structure_v2(ortho_helper, bounds, product_class, pixel_type, remap_function=None ): """ Create a SIDD version 2.0 structure based on the orthorectification helper and pixel bounds. @@ -361,9 +361,9 @@ def _create_display_v2(): NonInteractiveProcessing=[NonInteractiveProcessingType2( ProductGenerationOptions=ProductGenerationOptionsType2( DataRemapping=NewLookupTableType( - LUTName='DENSITY', + LUTName=remap_function.name.upper(), Predefined=PredefinedLookupType( - DatabaseName='DENSITY'))), + DatabaseName=remap_function.name.upper()))), RRDS=RRDSType2(DownsamplingMethod='DECIMATE'), band=i+1) for i in range(bands)], InteractiveProcessing=[InteractiveProcessingType2( @@ -521,7 +521,7 @@ def _create_exploitation_v1(): ########################## # Switchable version SIDD structure -def create_sidd_structure(ortho_helper, bounds, product_class, pixel_type, version=3): +def create_sidd_structure(ortho_helper, bounds, product_class, pixel_type, version=3, remap_function=None): """ Create a SIDD structure, with version specified, based on the orthorectification helper and pixel bounds. @@ -550,6 +550,6 @@ def create_sidd_structure(ortho_helper, bounds, product_class, pixel_type, versi if version == 1: return create_sidd_structure_v1(ortho_helper, bounds, product_class, pixel_type) elif version == 2: - return create_sidd_structure_v2(ortho_helper, bounds, product_class, pixel_type) + return create_sidd_structure_v2(ortho_helper, bounds, product_class, pixel_type, remap_function=remap_function ) else: - return create_sidd_structure_v3(ortho_helper, bounds, product_class, pixel_type) + return create_sidd_structure_v3(ortho_helper, bounds, product_class, pixel_type, remap_function=remap_function ) diff --git a/sarpy/utils/create_product.py b/sarpy/utils/create_product.py index 0724eeb9..e3336198 100644 --- a/sarpy/utils/create_product.py +++ b/sarpy/utils/create_product.py @@ -55,6 +55,9 @@ def main(args=None): parser.add_argument( '-m', '--method', default='nearest', choices=['nearest', ]+['spline_{}'.format(i) for i in range(1, 6)], help="The interpolation method.") + parser.add_argument( + '-b', '--bit_depth', default='8', choices=['8', '16' ], + help="SIDD product pixel bit depth.") parser.add_argument( '--version', default=2, type=int, choices=[1, 2, 3], help="The version of the SIDD standard used.") @@ -80,7 +83,7 @@ def main(args=None): ortho_helper = NearestNeighborMethod(reader, index=i) if args.type == 'detected': create_detected_image_sidd(ortho_helper, args.output_directory, - remap_function=remap.get_registered_remap(args.remap), + remap_function=remap.get_registered_remap(args.remap, bit_depth=args.bit_depth ), version=args.version, include_sicd=args.sicd) elif args.type == 'csi': create_csi_sidd(ortho_helper, args.output_directory, diff --git a/sarpy/visualization/remap.py b/sarpy/visualization/remap.py index ab0158b5..2fa925bf 100644 --- a/sarpy/visualization/remap.py +++ b/sarpy/visualization/remap.py @@ -1827,12 +1827,17 @@ def register_remap( """ if isinstance(remap_function, type) and issubclass(remap_function, RemapFunction): - remap_function = remap_function() + remap_function = remap_function( bit_depth=bit_depth) if not isinstance(remap_function, RemapFunction): raise TypeError('remap_function must be an instance of RemapFunction.') - remap_name = remap_function.name + + if remap_function.bit_depth == 16: + remap_name = remap_function.name + '_' + str( remap_function.bit_depth ) + else: + remap_name = remap_function.name + if remap_name not in _REMAP_DICT: _REMAP_DICT[remap_name] = remap_function elif overwrite: @@ -1846,14 +1851,27 @@ def _register_defaults(): global _DEFAULTS_REGISTERED if _DEFAULTS_REGISTERED: return - register_remap(NRL(bit_depth=8), overwrite=False) - register_remap(Density(bit_depth=8), overwrite=False) - register_remap(High_Contrast(bit_depth=8), overwrite=False) - register_remap(Brighter(bit_depth=8), overwrite=False) - register_remap(Darker(bit_depth=8), overwrite=False) - register_remap(Linear(bit_depth=8), overwrite=False) - register_remap(Logarithmic(bit_depth=8), overwrite=False) - register_remap(PEDF(bit_depth=8), overwrite=False) + + # register instance of class + register_remap(NRL( bit_depth=8), overwrite=False) + register_remap(Density( bit_depth=8), overwrite=False) + register_remap(High_Contrast( bit_depth=8), overwrite=False) + register_remap(Brighter( bit_depth=8), overwrite=False) + register_remap(Darker( bit_depth=8), overwrite=False) + register_remap(Linear( bit_depth=8), overwrite=False) + register_remap(Logarithmic( bit_depth=8), overwrite=False) + register_remap(PEDF( bit_depth=8), overwrite=False) + + register_remap(NRL( bit_depth=16), overwrite=False) + register_remap(Density( bit_depth=16), overwrite=False) + register_remap(High_Contrast( bit_depth=16), overwrite=False) + register_remap(Brighter( bit_depth=16), overwrite=False) + register_remap(Darker( bit_depth=16), overwrite=False) + register_remap(Linear( bit_depth=16), overwrite=False) + register_remap(Logarithmic( bit_depth=16), overwrite=False) + register_remap(PEDF( bit_depth=16), overwrite=False) + + if plt is not None: try: register_remap(LUT8bit(NRL(bit_depth=8), 'viridis', use_alpha=False), overwrite=False) @@ -1886,7 +1904,7 @@ def get_remap_names() -> List[str]: if not _DEFAULTS_REGISTERED: _register_defaults() - return list(_REMAP_DICT.keys()) + return list( _REMAP_DICT.keys()) def get_remap_list() -> List[Tuple[str, RemapFunction]]: @@ -1910,9 +1928,12 @@ def get_remap_list() -> List[Tuple[str, RemapFunction]]: def get_registered_remap( remap_name: str, - default: Optional[RemapFunction] = None) -> RemapFunction: + default: Optional[RemapFunction] = None, + bit_depth=8) -> RemapFunction: """ - Gets a remap function from it's registered name. + Gets a remap instance via its registered name. + # add 16 bit ability by newRegMap is dict of class/constructors + Parameters ---------- @@ -1921,7 +1942,7 @@ def get_registered_remap( Returns ------- - RemapFunction + RemapFunction Class Raises ------ @@ -1931,8 +1952,17 @@ def get_registered_remap( if not _DEFAULTS_REGISTERED: _register_defaults() - if remap_name in _REMAP_DICT: - return _REMAP_DICT[remap_name] + if int( bit_depth ) not in [ 8, 16 ]: + raise KeyError('Unregistered remap name `{}` with bit_depth `{}`'.format( remap_name, bit_depth )) + + if int( bit_depth ) == 16: + rm_name = remap_name + '_' + str( bit_depth ) + else: + rm_name = remap_name + + if rm_name in _REMAP_DICT: + return _REMAP_DICT[ rm_name ] + if default is not None: return default raise KeyError('Unregistered remap name `{}`'.format(remap_name)) diff --git a/setup.py b/setup.py index 5f19f1ae..da1c7e29 100644 --- a/setup.py +++ b/setup.py @@ -62,11 +62,11 @@ def my_test_suite(): url=parameters['__url__'], author=parameters['__author__'], author_email=parameters['__email__'], # The primary POC - install_requires=['numpy>=1.19.0', 'scipy'], + install_requires=['h5py', 'numpy>=1.19.0', 'pillow', 'scipy', 'matplotlib', 'shapely>=1.6.4', 'lxml>=4.1.1'], zip_safe=False, # Use of __file__ and __path__ in some code makes it unusable from zip test_suite="setup.my_test_suite", extras_require={ - "all": ['pillow', 'lxml>=4.1.1', 'matplotlib', 'h5py', 'smart_open[http]', 'pytest>=3.3.2', 'networkx>=2.5', 'shapely>=1.6.4'], + "all": ['smart_open[http]', 'pytest>=3.3.2', 'networkx>=2.5'], }, classifiers=[ 'Development Status :: 4 - Beta', diff --git a/tests/io/complex/test_SICDWriter.py b/tests/io/complex/test_SICDWriter.py new file mode 100644 index 00000000..4b86f4fe --- /dev/null +++ b/tests/io/complex/test_SICDWriter.py @@ -0,0 +1,371 @@ +import json +import numpy +import os +import pytest +import unittest + +from sarpy.io.complex.converter import conversion_utility, open_complex +from sarpy.io.complex.sicd import SICDWriter, SICDWritingDetails +from sarpy.io.complex.sicd_elements.SICD import SICDType +from sarpy.io.complex.sicd_schema import get_schema_path, \ + get_default_version_string +from sarpy.io.complex.sicd_elements.blocks import XYZPolyType + +from tests import parse_file_entry + +complex_file_types = {} +this_loc = os.path.abspath(__file__) +# specifies file locations +file_reference = os.path.join(os.path.split(this_loc)[0], \ + 'complex_file_types.json') +if os.path.isfile(file_reference): + with open(file_reference, 'r') as local_file: + test_files_list = json.load(local_file) + for test_files_type in test_files_list: + valid_entries = [] + for entry in test_files_list[test_files_type]: + the_file = parse_file_entry(entry) + if the_file is not None: + valid_entries.append(the_file) + complex_file_types[test_files_type] = valid_entries + +sicd_files = complex_file_types.get('SICD', []) + +def get_sicd_meta(): + input_file = sicd_files[0] + reader = open_complex(input_file) + return_sicd_meta = reader.sicd_meta + return return_sicd_meta + +def test_sicd_writer_init_failure_no_input(tmp_path): + with pytest.raises(TypeError, + match="missing 1 required positional argument: " + \ + "'file_object'"): + sicd_writer = SICDWriter() + +def test_sicd_writer_init_failure_file_only(tmp_path): + output_file = tmp_path / "out.sicd" + with pytest.raises(ValueError, + match="One of sicd_meta or sicd_writing_details must " + \ + "be provided."): + sicd_writer = SICDWriter(output_file) + +@unittest.skipIf(len(sicd_files) == 0, 'No sicd files found') +def test_sicd_writer_init_failure_sicd_meta_only(tmp_path): + sicd_meta = get_sicd_meta() + with pytest.raises(TypeError, + match="missing 1 required positional argument: " + \ + "'file_object'"): + sicd_writer = SICDWriter(sicd_meta=sicd_meta) + +@unittest.skipIf(len(sicd_files) == 0, 'No sicd files found') +def test_sicd_writer_init_failure_sicd_writing_details_only(tmp_path): + sicd_meta = get_sicd_meta() + sicd_writing_details = SICDWritingDetails(sicd_meta) + output_file = str(tmp_path / "out.sicd") + with pytest.raises(TypeError, + match="missing 1 required positional argument: " + \ + "'file_object'"): + sicd_writer = SICDWriter(sicd_writing_details=sicd_writing_details) + +@unittest.skipIf(len(sicd_files) == 0, 'No sicd files found') +def test_sicd_writer_init_sicd_meta(tmp_path): + sicd_meta = get_sicd_meta() + output_file = str(tmp_path / "out.sicd") + sicd_writer = SICDWriter(output_file, sicd_meta=sicd_meta) + +@unittest.skipIf(len(sicd_files) == 0, 'No sicd files found') +def test_sicd_writer_init_sicd_writing_details(tmp_path): + sicd_meta = get_sicd_meta() + sicd_writing_details = SICDWritingDetails(sicd_meta) + output_file = str(tmp_path / "out.sicd") + sicd_writer = SICDWriter(output_file, \ + sicd_writing_details=sicd_writing_details) + +@unittest.skipIf(len(sicd_files) == 0, 'No sicd files found') +def test_sicd_writer_init_failure_sicd_meta_invalid_output(tmp_path): + sicd_meta = get_sicd_meta() + output_file = str("/does_not_exist/out.sicd") + with pytest.raises(FileNotFoundError, + match="No such file or directory: " + \ + "'/does_not_exist/out.sicd'"): + sicd_writer = SICDWriter(output_file, sicd_meta=sicd_meta) + +@unittest.skipIf(len(sicd_files) == 0, 'No sicd files found') +def test_sicd_writer_init_failure_sicd_writing_details_invalid_output(tmp_path): + sicd_meta = get_sicd_meta() + sicd_writing_details = SICDWritingDetails(sicd_meta) + output_file = str("/does_not_exist/out.sicd") + with pytest.raises(FileNotFoundError, + match="No such file or directory: " + \ + "'/does_not_exist/out.sicd'"): + sicd_writer = SICDWriter(output_file, \ + sicd_writing_details=sicd_writing_details) + +@unittest.skipIf(len(sicd_files) == 0, 'No sicd files found') +def test_sicd_writer_init_failure_bad_sicd_meta(tmp_path): + sicd_meta = get_sicd_meta() + sicd_meta_bad_string = str(sicd_meta) + "})" + sicd_meta_bad_type = SICDType(sicd_meta_bad_string) + output_file = str(tmp_path / "out.sicd") + with pytest.raises(ValueError, + match="The sicd_meta has un-populated ImageData, and " + \ + "nothing useful can be inferred."): + sicd_writer = SICDWriter(output_file, sicd_meta=sicd_meta_bad_type) + +@unittest.skipIf(len(sicd_files) == 0, 'No sicd files found') +def test_sicd_writer_init_failure_bad_sicd_writing_details(tmp_path): + sicd_writing_details_bad = "Bad Data" + output_file = str(tmp_path / "out.sicd") + with pytest.raises(TypeError, + match="nitf_writing_details must be of type " + \ + ""): + sicd_writer = SICDWriter(output_file, \ + sicd_writing_details=sicd_writing_details_bad) + +@unittest.skipIf(len(sicd_files) == 0, 'No sicd files found') +def test_nitf_writing_details(tmp_path): + sicd_meta = get_sicd_meta() + sicd_writing_details = SICDWritingDetails(sicd_meta) + output_file = str(tmp_path / "out.sicd") + sicd_writer = SICDWriter(output_file, \ + sicd_writing_details=sicd_writing_details) + assert sicd_writer.nitf_writing_details == sicd_writing_details + +@unittest.skipIf(len(sicd_files) == 0, 'No sicd files found') +def test_nitf_writing_details_setter_failure(tmp_path): + sicd_meta = get_sicd_meta() + sicd_writing_details = SICDWritingDetails(sicd_meta) + output_file = str(tmp_path / "out.sicd") + sicd_writer = SICDWriter(output_file, \ + sicd_writing_details=sicd_writing_details) + with pytest.raises(ValueError, + match="nitf_writing_details is read-only"): + sicd_writer.nitf_writing_details = sicd_writing_details + +@unittest.skipIf(len(sicd_files) == 0, 'No sicd files found') +def test_get_format_function(tmp_path): + sicd_meta = get_sicd_meta() + output_file = str(tmp_path / "out.sicd") + sicd_writer = SICDWriter(output_file, sicd_meta=sicd_meta) + sicd_writer.get_format_function(raw_dtype="float", band_dimension=8, + complex_order='IQ', lut=numpy.array([1, 2])) + + +@unittest.skipIf(len(sicd_files) == 0, 'No sicd files found') +def test_get_format_function_failure_bad_band_type(tmp_path): + sicd_meta = get_sicd_meta() + output_file = str(tmp_path / "out.sicd") + sicd_writer = SICDWriter(output_file, sicd_meta=sicd_meta) + with pytest.raises(ValueError, + match="Got unsupported SICD band type definition"): + sicd_writer.get_format_function(raw_dtype="float", band_dimension=8, + complex_order='Steve', \ + lut=numpy.array([1, 2])) + + +@unittest.skipIf(len(sicd_files) == 0, 'No sicd files found') +def test_sicd_meta(tmp_path): + sicd_meta = get_sicd_meta() + output_file = str(tmp_path / "out.sicd") + sicd_writer = SICDWriter(output_file, sicd_meta=sicd_meta) + # Must confirm each piece of the SICDType is equal because there isn't an + # == operator for SICDType. + # First confirm that all CollectionInfo properties are equal. + assert sicd_meta.CollectionInfo.Classification == \ + sicd_writer.sicd_meta.CollectionInfo.Classification + assert sicd_meta.CollectionInfo.CollectorName == \ + sicd_writer.sicd_meta.CollectionInfo.CollectorName + assert sicd_meta.CollectionInfo.IlluminatorName == \ + sicd_writer.sicd_meta.CollectionInfo.IlluminatorName + assert sicd_meta.CollectionInfo.CoreName == \ + sicd_writer.sicd_meta.CollectionInfo.CoreName + assert sicd_meta.CollectionInfo.CollectType == \ + sicd_writer.sicd_meta.CollectionInfo.CollectType + assert sicd_meta.CollectionInfo.CountryCodes == \ + sicd_writer.sicd_meta.CollectionInfo.CountryCodes + assert sicd_meta.CollectionInfo.Parameters == \ + sicd_writer.sicd_meta.CollectionInfo.Parameters + assert sicd_meta.CollectionInfo.RadarMode.ModeType == \ + sicd_writer.sicd_meta.CollectionInfo.RadarMode.ModeType + assert sicd_meta.CollectionInfo.RadarMode.ModeID == \ + sicd_writer.sicd_meta.CollectionInfo.RadarMode.ModeID + # Confirm ImageCreation properties are equal. + assert sicd_meta.ImageCreation.Application == \ + sicd_writer.sicd_meta.ImageCreation.Application + assert sicd_meta.ImageCreation.DateTime == \ + sicd_writer.sicd_meta.ImageCreation.DateTime + assert sicd_meta.ImageCreation.Site == \ + sicd_writer.sicd_meta.ImageCreation.Site + # TODO: These are different because one is based on the profile from the + # file and the other is based on the profile of the version of sarpy + # we are testing. Skip for now. Determine if these should be the same + # assert sicd_meta.ImageCreation.Profile == \ + # sicd_writer.sicd_meta.ImageCreation.Profile + # Confirm ImageData properties are equal. + assert sicd_meta.ImageData.AmpTable == \ + sicd_writer.sicd_meta.ImageData.AmpTable + assert sicd_meta.ImageData.FirstCol == \ + sicd_writer.sicd_meta.ImageData.FirstCol + assert sicd_meta.ImageData.FirstRow == \ + sicd_writer.sicd_meta.ImageData.FirstRow + assert sicd_meta.ImageData.PixelType == \ + sicd_writer.sicd_meta.ImageData.PixelType + assert sicd_meta.ImageData.SCPPixel.Row == \ + sicd_writer.sicd_meta.ImageData.SCPPixel.Row + assert sicd_meta.ImageData.SCPPixel.Col == \ + sicd_writer.sicd_meta.ImageData.SCPPixel.Col + assert sicd_meta.ImageData.FullImage.NumCols == \ + sicd_writer.sicd_meta.ImageData.FullImage.NumCols + assert sicd_meta.ImageData.FullImage.NumRows == \ + sicd_writer.sicd_meta.ImageData.FullImage.NumRows + # Confirm GeoData properties are equal. + assert sicd_meta.GeoData.EarthModel == \ + sicd_writer.sicd_meta.GeoData.EarthModel + assert sicd_meta.GeoData.SCP.ECF.X == \ + sicd_writer.sicd_meta.GeoData.SCP.ECF.X + assert sicd_meta.GeoData.SCP.ECF.Y == \ + sicd_writer.sicd_meta.GeoData.SCP.ECF.Y + assert sicd_meta.GeoData.SCP.ECF.Z == \ + sicd_writer.sicd_meta.GeoData.SCP.ECF.Z + assert sicd_meta.GeoData.SCP.LLH.Lat == \ + sicd_writer.sicd_meta.GeoData.SCP.LLH.Lat + assert sicd_meta.GeoData.SCP.LLH.Lon == \ + sicd_writer.sicd_meta.GeoData.SCP.LLH.Lon + assert sicd_meta.GeoData.SCP.LLH.HAE == \ + sicd_writer.sicd_meta.GeoData.SCP.LLH.HAE + # TODO: Confirm ImageCorners match. + assert numpy.allclose(sicd_meta.GeoData.ImageCorners.FRFC, \ + sicd_writer.sicd_meta.GeoData.ImageCorners.FRFC) + assert numpy.allclose(sicd_meta.GeoData.ImageCorners.FRLC, \ + sicd_writer.sicd_meta.GeoData.ImageCorners.FRLC) + assert numpy.allclose(sicd_meta.GeoData.ImageCorners.LRFC, \ + sicd_writer.sicd_meta.GeoData.ImageCorners.LRFC) + assert numpy.allclose(sicd_meta.GeoData.ImageCorners.LRLC, \ + sicd_writer.sicd_meta.GeoData.ImageCorners.LRLC) + # Confirm Grid properties are equal. + assert sicd_meta.Grid.Col.SS == \ + sicd_writer.sicd_meta.Grid.Col.SS + assert sicd_meta.Grid.Col.UVectECF.X == \ + sicd_writer.sicd_meta.Grid.Col.UVectECF.X + assert sicd_meta.Grid.Col.UVectECF.Y == \ + sicd_writer.sicd_meta.Grid.Col.UVectECF.Y + assert sicd_meta.Grid.Col.UVectECF.Z == \ + sicd_writer.sicd_meta.Grid.Col.UVectECF.Z + assert sicd_meta.Grid.Row.SS == \ + sicd_writer.sicd_meta.Grid.Row.SS + assert pytest.approx(sicd_meta.Grid.Row.UVectECF.X) == \ + pytest.approx(sicd_writer.sicd_meta.Grid.Row.UVectECF.X) + assert pytest.approx(sicd_meta.Grid.Row.UVectECF.Y) == \ + pytest.approx(sicd_writer.sicd_meta.Grid.Row.UVectECF.Y) + assert pytest.approx(sicd_meta.Grid.Row.UVectECF.Z) == \ + pytest.approx(sicd_writer.sicd_meta.Grid.Row.UVectECF.Z) + assert sicd_meta.Grid.TimeCOAPoly.Coefs == \ + sicd_writer.sicd_meta.Grid.TimeCOAPoly.Coefs + assert sicd_meta.Grid.TimeCOAPoly.order1 == \ + sicd_writer.sicd_meta.Grid.TimeCOAPoly.order1 + assert sicd_meta.Grid.TimeCOAPoly.order2 == \ + sicd_writer.sicd_meta.Grid.TimeCOAPoly.order2 + assert sicd_meta.Grid.Type == \ + sicd_writer.sicd_meta.Grid.Type + assert sicd_meta.Grid.ImagePlane == \ + sicd_writer.sicd_meta.Grid.ImagePlane + # Confirm Timeline properties are equal. + assert sicd_meta.Timeline.IPP == \ + sicd_writer.sicd_meta.Timeline.IPP + assert sicd_meta.Timeline.CollectStart == \ + sicd_writer.sicd_meta.Timeline.CollectStart + assert sicd_meta.Timeline.CollectDuration == \ + sicd_writer.sicd_meta.Timeline.CollectDuration + # Confirm Position properties are equal. + assert pytest.approx(sicd_meta.Position.ARPPoly.X) == \ + pytest.approx(sicd_writer.sicd_meta.Position.ARPPoly.X) + assert pytest.approx(sicd_meta.Position.ARPPoly.Y) == \ + pytest.approx(sicd_writer.sicd_meta.Position.ARPPoly.Y) + assert pytest.approx(sicd_meta.Position.ARPPoly.Z) == \ + pytest.approx(sicd_writer.sicd_meta.Position.ARPPoly.Z) + assert pytest.approx(sicd_meta.Position.GRPPoly.X) == \ + pytest.approx(sicd_writer.sicd_meta.Position.GRPPoly.X) + assert pytest.approx(sicd_meta.Position.GRPPoly.Y) == \ + pytest.approx(sicd_writer.sicd_meta.Position.GRPPoly.Y) + assert pytest.approx(sicd_meta.Position.GRPPoly.Z) == \ + pytest.approx(sicd_writer.sicd_meta.Position.GRPPoly.Z) + # Confirm TxAPCPoly properties are equal. + if isinstance(sicd_meta.Position.TxAPCPoly, XYZPolyType) \ + and isinstance(sicd_meta.Position.TxAPCPoly, XYZPolyType): + assert pytest.approx(sicd_meta.Position.TxAPCPoly.X) == \ + pytest.approx(sicd_writer.sicd_meta.Position.TxAPCPoly.X) + assert pytest.approx(sicd_meta.Position.TxAPCPoly.Y) == \ + pytest.approx(sicd_writer.sicd_meta.Position.TxAPCPoly.Y) + assert pytest.approx(sicd_meta.Position.TxAPCPoly.Z) == \ + pytest.approx(sicd_writer.sicd_meta.Position.TxAPCPoly.Z) + # Confirm RcvAPC properties are equal. + if isinstance(sicd_meta.Position.RcvAPC, XYZPolyType) \ + and isinstance(sicd_meta.Position.RcvAPC, XYZPolyType): + assert pytest.approx(sicd_meta.Position.RcvAPC.X) == \ + pytest.approx(sicd_writer.sicd_meta.Position.RcvAPC.X) + assert pytest.approx(sicd_meta.Position.RcvAPC.Y) == \ + pytest.approx(sicd_writer.sicd_meta.Position.RcvAPC.Y) + assert pytest.approx(sicd_meta.Position.RcvAPC.Z) == \ + pytest.approx(sicd_writer.sicd_meta.Position.RcvAPC.Z) + # Confirm RadarCollection properties are equal. + assert numpy.allclose(sicd_meta.RadarCollection.TxFrequency.get_array(), \ + sicd_writer.sicd_meta.RadarCollection.TxFrequency.get_array()) + assert sicd_meta.RadarCollection.RefFreqIndex == \ + sicd_writer.sicd_meta.RadarCollection.RefFreqIndex + # TODO: Make this comparison work. + # assert numpy.allclose(sicd_meta.RadarCollection.Waveform.get_array(), \ + # sicd_writer.sicd_meta.RadarCollection.Waveform.get_array()) + assert sicd_meta.RadarCollection.TxPolarization == \ + sicd_writer.sicd_meta.RadarCollection.TxPolarization + + # TODO: Make tests for ImageFormation. This type requires an in depth + # comparison + # assert sicd_meta.ImageFormation.TxPolarization == \ + # sicd_writer.sicd_meta.ImageFormation.TxPolarization + # Confirm SCPCOA properties are equal. + assert pytest.approx(sicd_meta.SCPCOA.SCPTime) == \ + pytest.approx(sicd_writer.sicd_meta.SCPCOA.SCPTime) + assert pytest.approx(sicd_meta.SCPCOA.ARPPos.X) == \ + pytest.approx(sicd_writer.sicd_meta.SCPCOA.ARPPos.X) + assert pytest.approx(sicd_meta.SCPCOA.ARPPos.Y) == \ + pytest.approx(sicd_writer.sicd_meta.SCPCOA.ARPPos.Y) + assert pytest.approx(sicd_meta.SCPCOA.ARPPos.Z) == \ + pytest.approx(sicd_writer.sicd_meta.SCPCOA.ARPPos.Z) + assert pytest.approx(sicd_meta.SCPCOA.ARPVel.X) == \ + pytest.approx(sicd_writer.sicd_meta.SCPCOA.ARPVel.X) + assert pytest.approx(sicd_meta.SCPCOA.ARPVel.Y) == \ + pytest.approx(sicd_writer.sicd_meta.SCPCOA.ARPVel.Y) + assert pytest.approx(sicd_meta.SCPCOA.ARPVel.Z) == \ + pytest.approx(sicd_writer.sicd_meta.SCPCOA.ARPVel.Z) + assert pytest.approx(sicd_meta.SCPCOA.ARPAcc.X) == \ + pytest.approx(sicd_writer.sicd_meta.SCPCOA.ARPAcc.X) + assert pytest.approx(sicd_meta.SCPCOA.ARPAcc.Y) == \ + pytest.approx(sicd_writer.sicd_meta.SCPCOA.ARPAcc.Y) + assert pytest.approx(sicd_meta.SCPCOA.ARPAcc.Z) == \ + pytest.approx(sicd_writer.sicd_meta.SCPCOA.ARPAcc.Z) + assert sicd_meta.SCPCOA.SideOfTrack == \ + sicd_writer.sicd_meta.SCPCOA.SideOfTrack + assert pytest.approx(sicd_meta.SCPCOA.SlantRange) == \ + pytest.approx(sicd_writer.sicd_meta.SCPCOA.SlantRange) + assert pytest.approx(sicd_meta.SCPCOA.GroundRange) == \ + pytest.approx(sicd_writer.sicd_meta.SCPCOA.GroundRange) + assert pytest.approx(sicd_meta.SCPCOA.DopplerConeAng) == \ + pytest.approx(sicd_writer.sicd_meta.SCPCOA.DopplerConeAng) + assert pytest.approx(sicd_meta.SCPCOA.GrazeAng) == \ + pytest.approx(sicd_writer.sicd_meta.SCPCOA.GrazeAng) + assert pytest.approx(sicd_meta.SCPCOA.IncidenceAng) == \ + pytest.approx(sicd_writer.sicd_meta.SCPCOA.IncidenceAng) + assert pytest.approx(sicd_meta.SCPCOA.TwistAng) == \ + pytest.approx(sicd_writer.sicd_meta.SCPCOA.TwistAng) + assert pytest.approx(sicd_meta.SCPCOA.SlopeAng) == \ + pytest.approx(sicd_writer.sicd_meta.SCPCOA.SlopeAng) + assert pytest.approx(sicd_meta.SCPCOA.AzimAng) == \ + pytest.approx(sicd_writer.sicd_meta.SCPCOA.AzimAng) + assert pytest.approx(sicd_meta.SCPCOA.LayoverAng) == \ + pytest.approx(sicd_writer.sicd_meta.SCPCOA.LayoverAng) + + # The following sicd_meta features are optional, so skip them in the test for + # now. + # 'Radiometric', 'Antenna', 'ErrorStatistics', 'MatchInfo', 'RgAzComp', + # 'PFA', 'RMA' diff --git a/tests/io/complex/test_sicd.py b/tests/io/complex/test_sicd.py index 788bdade..d5c873d8 100644 --- a/tests/io/complex/test_sicd.py +++ b/tests/io/complex/test_sicd.py @@ -62,4 +62,4 @@ def test_sicd_creation(self): with self.subTest(msg='Test writing a single row of the sicd file {}'.format(fil)): with tempfile.TemporaryDirectory() as tmpdirname: - conversion_utility(reader, tmpdirname, row_limits=(0, 1)) + conversion_utility(reader, tmpdirname, row_limits=(0, 1)) \ No newline at end of file diff --git a/tests/io/general/test_nitf.py b/tests/io/general/test_nitf.py index 19f03921..9f7fefa5 100644 --- a/tests/io/general/test_nitf.py +++ b/tests/io/general/test_nitf.py @@ -58,3 +58,24 @@ def test_write_filehandle(tests_path, tmp_path): writer.write(data) assert not fd.closed + assert filecmp.cmp(in_nitf, out_nitf, shallow=False) + +def test_in_memory_write(tests_path, tmp_path): + in_nitf_mem = tests_path / "data/iq.nitf" + with sarpy.io.general.nitf.NITFReader(str(in_nitf_mem)) as reader_mem: + data_mem = reader_mem.read() + writer_details_mem = sarpy.io.general.nitf.NITFWritingDetails( + reader_mem.nitf_details.nitf_header, + (sarpy.io.general.nitf.ImageSubheaderManager(reader_mem.get_image_header(0)),), + reader_mem.image_segment_collections, + ) + + out_nitf_mem = tmp_path / 'output_memory.nitf' + with out_nitf_mem.open('wb') as fd_mem: + with sarpy.io.general.nitf.NITFWriter( + fd_mem, writing_details=writer_details_mem, in_memory=True + ) as writer_mem: + writer_mem.write(data_mem) + + assert not fd_mem.closed + assert filecmp.cmp(in_nitf_mem, out_nitf_mem, shallow=False) \ No newline at end of file diff --git a/tests/visualization/test_remap.py b/tests/visualization/test_remap.py index 2a23d641..b369a827 100644 --- a/tests/visualization/test_remap.py +++ b/tests/visualization/test_remap.py @@ -329,8 +329,31 @@ def test_remap_names(self): def test_get_registered_remap(self): with self.assertRaises(KeyError): remap.get_registered_remap("__fake__") - self.assertEqual(remap.get_registered_remap("__fake__", "default"), "default") + self.assertEqual(remap.get_registered_remap("__fake__", "default", 8 ), "default") + + def test_get_registered_remap_required_param_only(self): + self.assertEqual(remap.get_registered_remap("linear" ).name, "linear") + + def test_get_registered_remap_required_param_default(self): + self.assertEqual(remap.get_registered_remap("linear", "default" ).name, "linear") + + def test_get_registered_remap_required_param_default_bit_depth(self): + self.assertEqual(remap.get_registered_remap("linear" ).name, "linear") + self.assertEqual(remap.get_registered_remap("linear" ).bit_depth, 8) + + + def test_get_registered_remap_bitdepth_param(self): + self.assertEqual(remap.get_registered_remap("linear", bit_depth= 16 ).name, "linear") + self.assertEqual(remap.get_registered_remap("linear", bit_depth= 16 ).bit_depth, 16) + + def test_get_registered_remap_falure_bitdepth_param(self): + with self.assertRaises(KeyError): + remap.get_registered_remap("linear", bit_depth= 32 ) + def test_get_registered_remap_falure_not_registered(self): + with self.assertRaises(KeyError): + remap.get_registered_remap("steve" ) + def test_get_remap_list(self): remap_list = remap.get_remap_list() self.assertSetEqual(set(item[0] for item in remap_list), set(remap.get_remap_names()))