mirror of
https://github.com/introlab/rtabmap_ros.git
synced 2026-10-06 01:37:46 +08:00
179 lines
6.4 KiB
Python
179 lines
6.4 KiB
Python
# Copyright 2025 matlabbe
|
|||
|
|
#
|
||
|
|
# Redistribution and use in source and binary forms, with or without
|
||
|
|
# modification, are permitted provided that the following conditions are met:
|
||
|
|
#
|
||
|
|
# * Redistributions of source code must retain the above copyright
|
||
|
|
# notice, this list of conditions and the following disclaimer.
|
||
|
|
#
|
||
|
|
# * Redistributions in binary form must reproduce the above copyright
|
||
|
|
# notice, this list of conditions and the following disclaimer in the
|
||
|
|
# documentation and/or other materials provided with the distribution.
|
||
|
|
#
|
||
|
|
# * Neither the name of the matlabbe nor the names of its
|
||
|
|
# contributors may be used to endorse or promote products derived from
|
||
|
|
# this software without specific prior written permission.
|
||
|
|
#
|
||
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||
|
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||
|
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||
|
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||
|
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||
|
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||
|
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||
|
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||
|
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||
|
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||
|
|
# POSSIBILITY OF SUCH DAMAGE.
|
||
|
|
|
||
|
|
|
||
|
|
"""Tests for :mod:`rtabmap_python.cv_compression`."""
|
||
|
|
|
||
|
|
import struct
|
||
|
|
import zlib
|
||
|
|
|
||
|
|
import numpy as np
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from rtabmap_python.cv_compression import compress, uncompress
|
||
|
|
|
||
|
|
|
||
|
|
# The single-channel depths the format encodes, with the codes RTAB-Map's
|
||
|
|
# serializeMatType() gives them: CV_8U, CV_8S, CV_16U, CV_16S, CV_32S, CV_32F, CV_64F.
|
||
|
|
SUPPORTED_TYPES = [
|
||
|
|
('uint8', 0),
|
||
|
|
('int8', 1),
|
||
|
|
('uint16', 2),
|
||
|
|
('int16', 3),
|
||
|
|
('int32', 4),
|
||
|
|
('float32', 5),
|
||
|
|
('float64', 6),
|
||
|
|
]
|
||
|
|
|
||
|
|
# Three int32: rows, cols, type code.
|
||
|
|
TRAILER_SIZE = 3 * 4
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize('dtype,code', SUPPORTED_TYPES)
|
||
|
|
def test_roundtrip_preserves_shape_dtype_and_values(dtype, code):
|
||
|
|
"""Every supported depth survives a compress/uncompress cycle unchanged."""
|
||
|
|
data = np.arange(12, dtype=dtype).reshape(3, 4)
|
||
|
|
|
||
|
|
result = uncompress(compress(data))
|
||
|
|
|
||
|
|
assert result.shape == (3, 4)
|
||
|
|
assert result.dtype == np.dtype(dtype)
|
||
|
|
assert np.array_equal(result, data)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize('dtype,code', SUPPORTED_TYPES)
|
||
|
|
def test_trailer_records_rows_cols_and_type_code(dtype, code):
|
||
|
|
"""The last 12 bytes are rows, cols and the type code, as the C++ side writes them."""
|
||
|
|
data = np.zeros((3, 4), dtype=dtype)
|
||
|
|
|
||
|
|
trailer = bytes(compress(data)[-TRAILER_SIZE:])
|
||
|
|
|
||
|
|
assert trailer == struct.pack('iii', 3, 4, code)
|
||
|
|
|
||
|
|
|
||
|
|
def test_payload_is_plain_zlib_of_the_c_order_bytes():
|
||
|
|
"""Everything before the trailer is a zlib stream, so the C++ side can inflate it."""
|
||
|
|
data = np.arange(6, dtype=np.uint8)
|
||
|
|
|
||
|
|
payload = bytes(compress(data)[:-TRAILER_SIZE])
|
||
|
|
|
||
|
|
assert zlib.decompress(payload) == data.tobytes()
|
||
|
|
|
||
|
|
|
||
|
|
def test_compress_returns_a_bytearray():
|
||
|
|
"""The return type is a bytearray, which is what the message fields expect."""
|
||
|
|
assert isinstance(compress(np.zeros(4, dtype=np.uint8)), bytearray)
|
||
|
|
|
||
|
|
|
||
|
|
def test_one_dimensional_input_comes_back_as_a_single_row():
|
||
|
|
"""A 1-D array is recorded as 1-by-n, so the roundtrip is not shape-preserving."""
|
||
|
|
data = np.arange(5, dtype=np.float32)
|
||
|
|
|
||
|
|
result = uncompress(compress(data))
|
||
|
|
|
||
|
|
assert result.shape == (1, 5)
|
||
|
|
assert np.array_equal(result.ravel(), data)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize('shape', [(1, 5), (5, 1), (2, 3)])
|
||
|
|
def test_two_dimensional_shapes_are_preserved_exactly(shape):
|
||
|
|
"""Rows and cols are recorded separately, so no 2-D shape is transposed or flattened."""
|
||
|
|
data = np.arange(5 if 1 in shape else 6, dtype=np.uint8).reshape(shape)
|
||
|
|
|
||
|
|
assert uncompress(compress(data)).shape == shape
|
||
|
|
|
||
|
|
|
||
|
|
def test_non_contiguous_input_roundtrips():
|
||
|
|
"""A transposed view is written in C order, so it reads back as the same matrix."""
|
||
|
|
data = np.arange(12, dtype=np.int16).reshape(3, 4).T
|
||
|
|
assert not data.flags.c_contiguous
|
||
|
|
|
||
|
|
result = uncompress(compress(data))
|
||
|
|
|
||
|
|
assert result.shape == (4, 3)
|
||
|
|
assert np.array_equal(result, data)
|
||
|
|
|
||
|
|
|
||
|
|
def test_empty_array_roundtrips_as_an_empty_row():
|
||
|
|
"""An empty array is not a special case; it comes back as a 1-by-0 matrix."""
|
||
|
|
result = uncompress(compress(np.array([], dtype=np.uint8)))
|
||
|
|
|
||
|
|
assert result.shape == (1, 0)
|
||
|
|
assert result.dtype == np.uint8
|
||
|
|
|
||
|
|
|
||
|
|
def test_uncompressed_array_is_read_only():
|
||
|
|
"""uncompress() views the decompressed buffer rather than copying it."""
|
||
|
|
result = uncompress(compress(np.arange(4, dtype=np.uint8)))
|
||
|
|
|
||
|
|
assert not result.flags.writeable
|
||
|
|
with pytest.raises(ValueError):
|
||
|
|
result[0, 0] = 1
|
||
|
|
# .copy() is the way out, as the docstring says.
|
||
|
|
assert result.copy().flags.writeable
|
||
|
|
|
||
|
|
|
||
|
|
def test_accepts_bytes_as_well_as_bytearray():
|
||
|
|
"""uncompress() reads whatever compress() produced, converted or not."""
|
||
|
|
data = np.arange(8, dtype=np.uint16).reshape(2, 4)
|
||
|
|
|
||
|
|
result = uncompress(bytes(compress(data)))
|
||
|
|
|
||
|
|
assert np.array_equal(result, data)
|
||
|
|
|
||
|
|
|
||
|
|
def test_larger_matrix_roundtrips():
|
||
|
|
"""A matrix big enough to actually exercise zlib, with non-trivial content."""
|
||
|
|
rng = np.random.default_rng(42)
|
||
|
|
data = rng.integers(0, 255, size=(120, 160), dtype=np.uint8)
|
||
|
|
|
||
|
|
assert np.array_equal(uncompress(compress(data)), data)
|
||
|
|
|
||
|
|
|
||
|
|
def test_rejects_more_than_two_dimensions():
|
||
|
|
"""The format has no encoding for a third dimension, so compress() refuses one."""
|
||
|
|
with pytest.raises(AssertionError):
|
||
|
|
compress(np.zeros((2, 2, 2), dtype=np.uint8))
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize('dtype', ['int64', 'uint64', 'float16'])
|
||
|
|
def test_rejects_unsupported_dtype(dtype):
|
||
|
|
"""Depths outside CV_8U..CV_64F have no type code and raise rather than truncate."""
|
||
|
|
with pytest.raises(KeyError):
|
||
|
|
compress(np.zeros(4, dtype=dtype))
|
||
|
|
|
||
|
|
|
||
|
|
def test_rejects_unknown_type_code():
|
||
|
|
"""A trailer from a multi-channel C++ matrix encodes a code this table lacks."""
|
||
|
|
payload = bytes(compress(np.zeros((2, 2), dtype=np.uint8))[:-TRAILER_SIZE])
|
||
|
|
# What serializeMatType() returns for a 3-channel CV_8U matrix: depth + ((cn - 1) << 3).
|
||
|
|
forged = bytearray(payload) + struct.pack('iii', 2, 2, 0 + ((3 - 1) << 3))
|
||
|
|
|
||
|
|
with pytest.raises(KeyError):
|
||
|
|
uncompress(forged)
|