blob: be1cc14081c9b204afd4eb81d03fc306ea00eaca [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Tests for reading rasters whose sample models use the less common layouts.
The expected samples of every test were produced by handing the same sample model and data
buffer to java.awt.image, so they record what Java AWT reads for these layouts. Each test
names the Java layout it covers, which is enough to reproduce it.
"""
import warnings
from typing import List
import numpy as np
import pytest
from sedona.spark.raster.data_buffer import DataBuffer
from sedona.spark.raster.sample_model import (
ComponentSampleModel,
MultiPixelPackedSampleModel,
PixelInterleavedSampleModel,
SinglePixelPackedSampleModel,
)
def ramp(start: int, length: int, dtype=np.int32) -> np.ndarray:
"""A bank of consecutive values, so that a sample identifies its own position."""
return np.arange(start, start + length, dtype=dtype)
def signed_int32(mask: int) -> int:
"""Turn a bit mask written in its unsigned form into the value the deserializer
produces for it, which is negative for masks covering the top bit."""
return mask - (1 << 32) if mask >= (1 << 31) else mask
def int_bank(values: List[int]) -> np.ndarray:
"""An int32 bank holding values written in their unsigned form."""
return np.array(values, dtype=np.uint32).astype(np.int32)
def test_component_fast_path_pairs_band_offsets_with_bands() -> None:
# ComponentSampleModel(TYPE_INT, 2, 2, 1, 2, {1, 0}, {3, 1})
sample_model = ComponentSampleModel(DataBuffer.TYPE_INT, 2, 2, 1, 2, [1, 0], [3, 1])
data_buffer = DataBuffer(DataBuffer.TYPE_INT, [ramp(10, 7), ramp(20, 7)], 7, [0, 0])
np.testing.assert_array_equal(
sample_model.as_numpy(data_buffer),
np.array([[[23, 24], [25, 26]], [[11, 12], [13, 14]]]),
)
def test_component_fast_path_applies_bank_offsets() -> None:
# ComponentSampleModel(TYPE_INT, 2, 2, 1, 2, {1, 0}, {3, 1})
# over DataBufferInt(banks, 7, {2, 1})
sample_model = ComponentSampleModel(DataBuffer.TYPE_INT, 2, 2, 1, 2, [1, 0], [3, 1])
data_buffer = DataBuffer(DataBuffer.TYPE_INT, [ramp(10, 9), ramp(20, 9)], 7, [2, 1])
np.testing.assert_array_equal(
sample_model.as_numpy(data_buffer),
np.array([[[24, 25], [26, 27]], [[13, 14], [15, 16]]]),
)
def test_component_fast_path_reads_a_bounded_window_of_padded_banks() -> None:
# ComponentSampleModel(TYPE_INT, 2, 2, 1, 2, {0, 1}, {0, 0}) over banks of 6 samples,
# so the band offsets alone give no reason to narrow a bank down to 4 samples
sample_model = ComponentSampleModel(DataBuffer.TYPE_INT, 2, 2, 1, 2, [0, 1], [0, 0])
data_buffer = DataBuffer(DataBuffer.TYPE_INT, [ramp(10, 6), ramp(20, 6)], 6, [0, 0])
np.testing.assert_array_equal(
sample_model.as_numpy(data_buffer),
np.array([[[10, 11], [12, 13]], [[20, 21], [22, 23]]]),
)
def test_banded_sample_model_over_reordered_banks() -> None:
# BandedSampleModel(TYPE_INT, 2, 2, 2, {1, 0}, {1, 2}) over DataBufferInt(banks, 6, {1, 2}),
# which is deserialized as a component model with a unit pixel stride
sample_model = ComponentSampleModel(DataBuffer.TYPE_INT, 2, 2, 1, 2, [1, 0], [1, 2])
data_buffer = DataBuffer(DataBuffer.TYPE_INT, [ramp(10, 8), ramp(20, 8)], 6, [1, 2])
np.testing.assert_array_equal(
sample_model.as_numpy(data_buffer),
np.array([[[23, 24], [25, 26]], [[13, 14], [15, 16]]]),
)
def test_component_slow_path_applies_bank_offsets() -> None:
# ComponentSampleModel(TYPE_INT, 3, 2, 2, 8, {1, 1, 0}, {0, 1, 4})
# over DataBufferInt(banks, 20, {3, 2}): two bands share a bank, and there are gaps
# both between the scanlines and between the pixels within them
sample_model = ComponentSampleModel(
DataBuffer.TYPE_INT, 3, 2, 2, 8, [1, 1, 0], [0, 1, 4]
)
data_buffer = DataBuffer(
DataBuffer.TYPE_INT, [ramp(100, 24), ramp(200, 24)], 20, [3, 2]
)
np.testing.assert_array_equal(
sample_model.as_numpy(data_buffer),
np.array(
[
[[202, 204, 206], [210, 212, 214]],
[[203, 205, 207], [211, 213, 215]],
[[107, 109, 111], [115, 117, 119]],
]
),
)
def test_pixel_interleaved_fast_path_applies_bank_offset() -> None:
# PixelInterleavedSampleModel(TYPE_INT, 2, 2, 3, 6, {0, 1, 2})
# over DataBufferInt(bank of 20 samples, 17, {3})
sample_model = PixelInterleavedSampleModel(
DataBuffer.TYPE_INT, 2, 2, 3, 6, [0, 1, 2]
)
data_buffer = DataBuffer(DataBuffer.TYPE_INT, [ramp(10, 20)], 17, [3])
np.testing.assert_array_equal(
sample_model.as_numpy(data_buffer),
np.array([[[13, 16], [19, 22]], [[14, 17], [20, 23]], [[15, 18], [21, 24]]]),
)
def test_pixel_interleaved_slow_path_band_offsets_may_exceed_pixel_stride() -> None:
# PixelInterleavedSampleModel(TYPE_INT, 2, 2, 4, 10, {3, 1, 0})
# over DataBufferInt(bank, 20, {2}): band 0 sits past the window the three bands of a
# single pixel would occupy
sample_model = PixelInterleavedSampleModel(
DataBuffer.TYPE_INT, 2, 2, 4, 10, [3, 1, 0]
)
data_buffer = DataBuffer(DataBuffer.TYPE_INT, [ramp(10, 24)], 20, [2])
np.testing.assert_array_equal(
sample_model.as_numpy(data_buffer),
np.array([[[15, 19], [25, 29]], [[13, 17], [23, 27]], [[12, 16], [22, 26]]]),
)
def test_single_pixel_packed_argb_does_not_sign_extend() -> None:
# SinglePixelPackedSampleModel(TYPE_INT, 2, 2, 3, {0xFF000000, 0xFF0000, 0xFF00, 0xFF})
# over DataBufferInt(bank, 7, {1}): the alpha mask covers the sign bit of the samples
sample_model = SinglePixelPackedSampleModel(
DataBuffer.TYPE_INT,
2,
2,
3,
[signed_int32(0xFF000000), 0x00FF0000, 0x0000FF00, 0x000000FF],
)
data_buffer = DataBuffer(
DataBuffer.TYPE_INT,
[int_bank([0, 0x8090A0B0, 0x102030F0, 0, 0xFFFFFFFF, 0x01020304, 0, 0])],
7,
[1],
)
np.testing.assert_array_equal(
sample_model.as_numpy(data_buffer),
np.array(
[
[[128, 16], [255, 1]],
[[144, 32], [255, 2]],
[[160, 48], [255, 3]],
[[176, 240], [255, 4]],
]
),
)
def test_single_pixel_packed_zero_mask_reads_zero() -> None:
# SinglePixelPackedSampleModel(TYPE_INT, 2, 1, 2, {0xFF0000, 0xFF00, 0xFF, 0}): Java
# accepts a zero mask, leaves its bit offset at zero and reads the band as zero
sample_model = SinglePixelPackedSampleModel(
DataBuffer.TYPE_INT, 2, 1, 2, [0x00FF0000, 0x0000FF00, 0x000000FF, 0]
)
assert sample_model.bit_offsets == [16, 8, 0, 0]
data_buffer = DataBuffer(
DataBuffer.TYPE_INT, [int_bank([0x8090A0B0, 0xFFFFFFFF])], 2, [0]
)
np.testing.assert_array_equal(
sample_model.as_numpy(data_buffer),
np.array([[[144, 255]], [[160, 255]], [[176, 255]], [[0, 0]]]),
)
def test_single_pixel_packed_applies_bank_offset() -> None:
# SinglePixelPackedSampleModel(TYPE_USHORT, 2, 2, 4, {0xF800, 0x7E0, 0x1F}) over
# DataBufferUShort(bank, 8, {2}): RGB 565 pixels starting two samples into the bank
sample_model = SinglePixelPackedSampleModel(
DataBuffer.TYPE_USHORT, 2, 2, 4, [0xF800, 0x07E0, 0x001F]
)
data_buffer = DataBuffer(
DataBuffer.TYPE_USHORT,
[np.array([0, 0xF81F, 0x07E0, 0, 0, 0x1234, 0xFFFF, 0], dtype=np.uint16)],
8,
[2],
)
np.testing.assert_array_equal(
sample_model.as_numpy(data_buffer),
np.array([[[0, 0], [31, 0]], [[63, 0], [63, 0]], [[0, 0], [31, 0]]]),
)
def test_multi_pixel_packed_applies_bit_and_bank_offsets() -> None:
# MultiPixelPackedSampleModel(TYPE_BYTE, 5, 2, 4, 4, 4) over DataBufferByte(bank, 9, {1}):
# four bits per pixel, with both the bank and the first scanline starting half a byte in
sample_model = MultiPixelPackedSampleModel(DataBuffer.TYPE_BYTE, 5, 2, 4, 4, 4)
data_buffer = DataBuffer(
DataBuffer.TYPE_BYTE,
[
np.array(
[0xFF, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x00],
dtype=np.uint8,
)
],
9,
[1],
)
# an ordinary packed layout, so reading it must not warn about Java's shift quirks
with warnings.catch_warnings():
warnings.simplefilter("error")
arr = sample_model.as_numpy(data_buffer)
np.testing.assert_array_equal(
arr, np.array([[[1, 2, 3, 4, 5], [9, 10, 11, 12, 13]]])
)
def test_multi_pixel_packed_stays_within_the_bank() -> None:
# MultiPixelPackedSampleModel(TYPE_BYTE, 8, 1, 1, 1, 0) over DataBufferByte(bank, 1, {0}):
# the whole image is held in a single byte, so reading beyond it is out of bounds
sample_model = MultiPixelPackedSampleModel(DataBuffer.TYPE_BYTE, 8, 1, 1, 1, 0)
data_buffer = DataBuffer(
DataBuffer.TYPE_BYTE, [np.array([0xA9], dtype=np.uint8)], 1, [0]
)
np.testing.assert_array_equal(
sample_model.as_numpy(data_buffer),
np.array([[[1, 0, 1, 0, 1, 0, 0, 1]]]),
)
def test_multi_pixel_packed_scanline_spanning_several_samples() -> None:
# MultiPixelPackedSampleModel(TYPE_BYTE, 5, 2, 2, 3, 6) over DataBufferByte(bank, 7, {1}):
# scanlines of two bit pixels that start near the end of a byte
sample_model = MultiPixelPackedSampleModel(DataBuffer.TYPE_BYTE, 5, 2, 2, 3, 6)
data_buffer = DataBuffer(
DataBuffer.TYPE_BYTE,
[np.array([0x00, 0x1B, 0xE4, 0x39, 0x00, 0x4E, 0x93, 0x00], dtype=np.uint8)],
7,
[1],
)
np.testing.assert_array_equal(
sample_model.as_numpy(data_buffer),
np.array([[[3, 3, 2, 1, 0], [0, 1, 0, 3, 2]]]),
)
@pytest.mark.parametrize(
"bank,expected",
[
# the top sample has its sign bit set, so Java's arithmetic shift extends it
([0x89ABCDEF, 0x12345678], [154, 188, 222, 248]),
# and a positive sample, where the shifted-down top bits are not sign bits
([0x12345678, 0], [35, 69, 103, 1]),
],
)
def test_multi_pixel_packed_pixel_straddling_two_samples(
bank: List[int], expected: List[int]
) -> None:
# MultiPixelPackedSampleModel(TYPE_INT, 4, 1, 8, 1, 4): a data bit offset that is not a
# multiple of num_bits leaves the last pixel straddling two samples, and Java's shift
# distance for it goes negative. Java takes an int shift distance modulo 32, so -4 shifts
# right by 28 and reads the top bits of the sample rather than shifting it all out.
sample_model = MultiPixelPackedSampleModel(DataBuffer.TYPE_INT, 4, 1, 8, 1, 4)
data_buffer = DataBuffer(DataBuffer.TYPE_INT, [int_bank(bank)], 2, [0])
with pytest.warns(UserWarning, match="straddle two samples"):
arr = sample_model.as_numpy(data_buffer)
np.testing.assert_array_equal(arr, np.array([[expected]]))
def test_multi_pixel_packed_whole_sample_pixels_read_zero() -> None:
# MultiPixelPackedSampleModel(TYPE_INT, 3, 2, 32, 4, 0): Java accepts a pixel that
# occupies a whole 32 bit sample, but derives a zero bit mask for it from
# `(1 << 32) - 1`, so it reads every pixel of such a raster as zero
sample_model = MultiPixelPackedSampleModel(DataBuffer.TYPE_INT, 3, 2, 32, 4, 0)
data_buffer = DataBuffer(
DataBuffer.TYPE_INT,
[
int_bank(
[
0x11223344,
0x55667788,
0x99AABBCC,
0,
0xDEADBEEF,
0x0F0F0F0F,
0x12345678,
0,
]
)
],
8,
[0],
)
with pytest.warns(UserWarning, match="one pixel per 32 bit sample"):
arr = sample_model.as_numpy(data_buffer)
np.testing.assert_array_equal(arr, np.array([[[0, 0, 0], [0, 0, 0]]]))
@pytest.mark.parametrize(
"data_type,dtype",
[
(DataBuffer.TYPE_BYTE, np.uint8),
(DataBuffer.TYPE_SHORT, np.int16),
(DataBuffer.TYPE_USHORT, np.uint16),
(DataBuffer.TYPE_INT, np.int32),
(DataBuffer.TYPE_FLOAT, np.float32),
(DataBuffer.TYPE_DOUBLE, np.float64),
],
)
def test_component_sample_model_keeps_the_data_type(data_type: int, dtype) -> None:
contiguous = ComponentSampleModel(data_type, 2, 2, 1, 2, [0], [1])
strided = ComponentSampleModel(data_type, 2, 2, 2, 5, [0], [1])
arr = contiguous.as_numpy(DataBuffer(data_type, [ramp(1, 6, dtype)], 5, [1]))
assert arr.dtype == dtype
np.testing.assert_array_equal(arr, np.array([[[3, 4], [5, 6]]], dtype=dtype))
arr = strided.as_numpy(DataBuffer(data_type, [ramp(1, 12, dtype)], 11, [1]))
assert arr.dtype == dtype
np.testing.assert_array_equal(arr, np.array([[[3, 5], [8, 10]]], dtype=dtype))