| """Test proto_helpers wrapper classes.""" | |
| import os | |
| import pathlib | |
| import unittest | |
| from avm_stats import avm_frame_pb2 | |
| from avm_stats import proto_helpers | |
| from avm_stats import yuv_tools | |
| import numpy as np | |
| TESTDATA_ENV_VAR = "LIBAOM_TEST_DATA_PATH" | |
| TEST_SEQUENCE_NAME = "park_joy_90p_8_420" | |
| STREAM_FILENAME = f"{TEST_SEQUENCE_NAME}.ivf" | |
| PROTO_FILENAME = f"{TEST_SEQUENCE_NAME}_frame_0000.pb" | |
| TEST_STREAM_WIDTH = 160 | |
| TEST_STREAM_HEIGHT = 90 | |
| class ProtoHelpersTest(unittest.TestCase): | |
| def setUp(self): | |
| super().setUp() | |
| if TESTDATA_ENV_VAR not in os.environ: | |
| raise RuntimeError( | |
| f"{TESTDATA_ENV_VAR} environment variable must be set before running" | |
| " pytest!" | |
| ) | |
| self.testdata_path = pathlib.Path(os.environ[TESTDATA_ENV_VAR]) | |
| proto_path = self.testdata_path / PROTO_FILENAME | |
| with open(proto_path, "rb") as f: | |
| self.frame_proto = avm_frame_pb2.Frame.FromString(f.read()) | |
| self.frame = proto_helpers.Frame(self.frame_proto) | |
| stream_path = self.testdata_path / STREAM_FILENAME | |
| self.stream_size_bytes = stream_path.stat().st_size | |
| def test_frame_dimensions(self): | |
| self.assertEqual(self.frame.width, TEST_STREAM_WIDTH) | |
| self.assertEqual(self.frame.height, TEST_STREAM_HEIGHT) | |
| self.assertEqual( | |
| self.frame.reconstruction_rgb.shape, | |
| (TEST_STREAM_HEIGHT, TEST_STREAM_WIDTH, 3), | |
| ) | |
| def test_clip_rect(self): | |
| rect = proto_helpers.Rectangle( | |
| left_x=50.0, top_y=50.0, width=500.0, height=500.0 | |
| ) | |
| clipped = self.frame.clip_rect(rect) | |
| self.assertEqual(clipped.width, TEST_STREAM_WIDTH - 50.0) | |
| self.assertEqual(clipped.height, TEST_STREAM_HEIGHT - 50.0) | |
| def test_plane_subsample(self): | |
| even_width = 100 | |
| odd_width = 101 | |
| y_plane = proto_helpers.Plane.Y | |
| u_plane = proto_helpers.Plane.U | |
| self.assertEqual( | |
| proto_helpers.subsample_dimension(even_width, y_plane), 100 | |
| ) | |
| self.assertEqual(proto_helpers.subsample_dimension(odd_width, y_plane), 101) | |
| self.assertEqual(proto_helpers.subsample_dimension(even_width, u_plane), 50) | |
| self.assertEqual(proto_helpers.subsample_dimension(odd_width, u_plane), 51) | |
| def compare_yuv_planes( | |
| self, | |
| luma: np.ndarray, | |
| chroma_u: np.ndarray, | |
| chroma_v: np.ndarray, | |
| expected_yuv_path: str, | |
| ): | |
| """Compares luma and chroma planes to a golden YUV. | |
| Args: | |
| luma: Luma plane to test. | |
| chroma_u: Chroma U plane to test. | |
| chroma_v: Chroma V plane to test. | |
| expected_yuv_path: Path to the golden YUV to load and compare against. | |
| """ | |
| expected_yuv = yuv_tools.parse_raw_yuv( | |
| expected_yuv_path, TEST_STREAM_WIDTH, TEST_STREAM_HEIGHT, 1, bit_depth=8 | |
| ).yuvs[0] | |
| self.assertTrue(np.array_equal(luma, expected_yuv.y)) | |
| self.assertTrue(np.array_equal(chroma_u, expected_yuv.u)) | |
| self.assertTrue(np.array_equal(chroma_v, expected_yuv.v)) | |
| def compare_luma_diff( | |
| self, | |
| luma_diff: np.ndarray, | |
| first_yuv_path: str, | |
| second_yuv_path: str, | |
| ): | |
| """Compares a luma difference against the difference of two golden YUVs. | |
| Args: | |
| luma_diff: Difference between two luma planes to test, e.g. the distortion | |
| (reconstruction - original). | |
| first_yuv_path: Path to the first golden YUV. | |
| second_yuv_path: Path to the first second YUV. This will be subtracted | |
| from the first and compared to luma_diff. | |
| """ | |
| first_yuv = yuv_tools.parse_raw_yuv( | |
| first_yuv_path, TEST_STREAM_WIDTH, TEST_STREAM_HEIGHT, 1, bit_depth=8 | |
| ).yuvs[0] | |
| second_yuv = yuv_tools.parse_raw_yuv( | |
| second_yuv_path, TEST_STREAM_WIDTH, TEST_STREAM_HEIGHT, 1, bit_depth=8 | |
| ).yuvs[0] | |
| expected_luma_diff = first_yuv.y.astype(np.int16) - second_yuv.y.astype( | |
| np.int16 | |
| ) | |
| np.testing.assert_equal( | |
| luma_diff, | |
| expected_luma_diff, | |
| ) | |
| def test_pixel_data_original(self): | |
| luma = self.frame.pixels[0].original | |
| chroma_u = self.frame.pixels[1].original | |
| chroma_v = self.frame.pixels[2].original | |
| orig_yuv_path = ( | |
| self.testdata_path / f"{TEST_SEQUENCE_NAME}_frame_0000_original.yuv" | |
| ) | |
| self.compare_yuv_planes(luma, chroma_u, chroma_v, orig_yuv_path) | |
| def remove_original_data_from_proto(self): | |
| for superblock in self.frame_proto.superblocks: | |
| for plane in superblock.pixel_data: | |
| plane.ClearField("original") | |
| self.frame = proto_helpers.Frame(self.frame_proto) | |
| def test_pixel_data_original_missing(self): | |
| self.remove_original_data_from_proto() | |
| self.assertIsNone(self.frame.pixels[0].original) | |
| self.assertIsNone(self.frame.pixels[1].original) | |
| self.assertIsNone(self.frame.pixels[2].original) | |
| self.assertIsNone(self.frame.pixels[0].distortion) | |
| self.assertIsNone(self.frame.pixels[1].distortion) | |
| self.assertIsNone(self.frame.pixels[2].distortion) | |
| self.assertIsNone(self.frame.original_rgb) | |
| def test_pixel_data_reconstruction(self): | |
| luma = self.frame.pixels[0].reconstruction | |
| chroma_u = self.frame.pixels[1].reconstruction | |
| chroma_v = self.frame.pixels[2].reconstruction | |
| orig_yuv_path = ( | |
| self.testdata_path | |
| / f"{TEST_SEQUENCE_NAME}_frame_0000_reconstruction.yuv" | |
| ) | |
| self.compare_yuv_planes(luma, chroma_u, chroma_v, orig_yuv_path) | |
| def test_pixel_data_pre_filtered(self): | |
| luma = self.frame.pixels[0].pre_filtered | |
| chroma_u = self.frame.pixels[1].pre_filtered | |
| chroma_v = self.frame.pixels[2].pre_filtered | |
| orig_yuv_path = ( | |
| self.testdata_path / f"{TEST_SEQUENCE_NAME}_frame_0000_pre_filtered.yuv" | |
| ) | |
| self.compare_yuv_planes(luma, chroma_u, chroma_v, orig_yuv_path) | |
| def test_luma_distortion(self): | |
| luma_distortion = self.frame.pixels[0].distortion | |
| orig_yuv_path = ( | |
| self.testdata_path / f"{TEST_SEQUENCE_NAME}_frame_0000_original.yuv" | |
| ) | |
| recon_yuv_path = ( | |
| self.testdata_path | |
| / f"{TEST_SEQUENCE_NAME}_frame_0000_reconstruction.yuv" | |
| ) | |
| self.compare_luma_diff(luma_distortion, orig_yuv_path, recon_yuv_path) | |
| def test_filter_delta(self): | |
| luma_filter_delta = self.frame.pixels[0].filter_delta | |
| recon_yuv_path = ( | |
| self.testdata_path | |
| / f"{TEST_SEQUENCE_NAME}_frame_0000_reconstruction.yuv" | |
| ) | |
| pre_filtered_yuv_path = ( | |
| self.testdata_path / f"{TEST_SEQUENCE_NAME}_frame_0000_pre_filtered.yuv" | |
| ) | |
| self.compare_luma_diff( | |
| luma_filter_delta, recon_yuv_path, pre_filtered_yuv_path | |
| ) | |
| def test_pixel_data_prediction(self): | |
| luma = self.frame.pixels[0].prediction | |
| chroma_u = self.frame.pixels[1].prediction | |
| chroma_v = self.frame.pixels[2].prediction | |
| orig_yuv_path = ( | |
| self.testdata_path / f"{TEST_SEQUENCE_NAME}_frame_0000_prediction.yuv" | |
| ) | |
| self.compare_yuv_planes(luma, chroma_u, chroma_v, orig_yuv_path) | |
| def test_coding_unit_count(self): | |
| first_sb = self.frame.superblocks[0] | |
| # Since the test stream (Park Joy) is moderately complex and was encoded at | |
| # a reasonable quality (qp=185), we expect a decent number of coding units | |
| # to be present in the partition tree. 50 is a reasonable lower bound to | |
| # use as a quick sanity check (although in practice it's closer to 150). | |
| # Refer to generate_testdata.sh for exact encoder args for this test stream. | |
| minimum_num_coding_units = 50 | |
| # For an upper bound, assume a 128x128 superblock was partitioned entirely | |
| # into 4x4 coding units. | |
| maximum_num_coding_units = (128 * 128) // (4 * 4) | |
| num_coding_units = len(list(first_sb.get_coding_units())) | |
| self.assertLessEqual(num_coding_units, maximum_num_coding_units) | |
| self.assertGreaterEqual(num_coding_units, minimum_num_coding_units) | |
| def test_transform_unit_count(self): | |
| first_sb = self.frame.superblocks[0] | |
| # As a lower bound, there will be at least one transform unit per coding | |
| # unit. | |
| num_coding_units = len(list(first_sb.get_coding_units())) | |
| num_transform_units = len(list(first_sb.get_transform_rects())) | |
| minimum_num_transform_units = num_coding_units | |
| # Similar to the coding unit upper bound, assume a 128x128 superblock made | |
| # entirely of 4x4 transform units. | |
| maximum_num_transform_units = (128 * 128) // (4 * 4) | |
| self.assertLessEqual(num_transform_units, maximum_num_transform_units) | |
| self.assertGreaterEqual(num_transform_units, minimum_num_transform_units) | |
| def test_partition_rects_cover_whole_frame_without_overlap(self): | |
| pixel_hit_count = np.zeros( | |
| (TEST_STREAM_HEIGHT, TEST_STREAM_WIDTH), dtype=np.int32 | |
| ) | |
| for superblock in self.frame.superblocks: | |
| for rect in superblock.get_partition_rects(): | |
| pixel_hit_count[ | |
| rect.top_y : rect.top_y + rect.height, | |
| rect.left_x : rect.left_x + rect.width, | |
| ] += 1 | |
| np.testing.assert_equal( | |
| pixel_hit_count, | |
| np.ones((TEST_STREAM_HEIGHT, TEST_STREAM_WIDTH), dtype=np.int32), | |
| ) | |
| def test_total_bits_of_first_superblock(self): | |
| first_sb = self.frame.superblocks[0] | |
| # The majority of the bits in this stream will be spent in the first SB of | |
| # the first frame, so half the total size of the stream is a reasonable | |
| # lower bound. | |
| minimum_expected_bits = 0.5 * self.stream_size_bytes * 8 | |
| # In the extreme case, ALL of the bits in the stream are used to encode | |
| # this SB, so this value is the upper bound. | |
| maximum_expected_bits = self.stream_size_bytes * 8 | |
| total_bits = first_sb.get_total_bits() | |
| self.assertLessEqual(total_bits, maximum_expected_bits) | |
| self.assertGreaterEqual(total_bits, minimum_expected_bits) | |
| def test_total_intra_mode_bits_of_first_superblock(self): | |
| first_sb = self.frame.superblocks[0] | |
| num_coding_units = len(list(first_sb.get_coding_units())) | |
| # As a lower bound, assume half a bit is used per coding unit to encode | |
| # the luma prediction mode (in practice it's closer to 4 bits). | |
| minimum_expected_bits = num_coding_units * 0.5 | |
| # In the extreme case, ALL of the bits in the first SB are used to encode | |
| # the luma prediction mode. | |
| maximum_expected_bits = first_sb.get_total_bits() | |
| def intra_mode_filter(sym: proto_helpers.Symbol): | |
| return sym.source_function == "read_intra_luma_mode" | |
| total_bits = first_sb.get_total_bits(filt=intra_mode_filter) | |
| self.assertLessEqual(total_bits, maximum_expected_bits) | |
| self.assertGreaterEqual(total_bits, minimum_expected_bits) | |
| def test_first_coding_unit_prediction_mode(self): | |
| first_superblock = self.frame.superblocks[0] | |
| first_luma_coding_unit = list( | |
| first_superblock.get_coding_units(use_chroma=False) | |
| )[0] | |
| first_chroma_coding_unit = list( | |
| first_superblock.get_coding_units(use_chroma=True) | |
| )[0] | |
| # All intra prediction modes end with this suffix. | |
| self.assertTrue( | |
| first_luma_coding_unit.get_prediction_mode().endswith("_PRED") | |
| ) | |
| # All chroma prediction modes start with this prefix. | |
| self.assertTrue( | |
| first_chroma_coding_unit.get_prediction_mode().startswith("UV_") | |
| ) |