def test_pictures(self, parse_code): stream = Stream( sequences=[ Sequence( data_units=[ # First in sequence should be auto-numbered to expected start # offset DataUnit( parse_info=ParseInfo(parse_code=parse_code), picture_parse=PictureParse( picture_header=PictureHeader(picture_number=AUTO) ), ), # If picture number not mentioned, it should be autofilled DataUnit( parse_info=ParseInfo(parse_code=parse_code), picture_parse=PictureParse(picture_header=PictureHeader()), ), # If explicit picture number given, should be used DataUnit( parse_info=ParseInfo(parse_code=parse_code), picture_parse=PictureParse( picture_header=PictureHeader(picture_number=0xFFFFFFFE) ), ), # Should continue from last explicit number if given DataUnit( parse_info=ParseInfo(parse_code=parse_code), picture_parse=PictureParse( picture_header=PictureHeader(picture_number=AUTO) ), ), # Should wrap-around DataUnit( parse_info=ParseInfo(parse_code=parse_code), picture_parse=PictureParse( picture_header=PictureHeader(picture_number=AUTO) ), ), ] ) ] ) autofill_picture_number(stream, 1234) picture_numbers = [ data_unit["picture_parse"]["picture_header"]["picture_number"] for seq in stream["sequences"] for data_unit in seq["data_units"] ] assert picture_numbers == [ 1234, 1235, 0xFFFFFFFE, 0xFFFFFFFF, 0x0, ]
def test_picture_tp_already_exists(self, parse_code, existing_tp): data_unit = DataUnit( parse_info=ParseInfo(parse_code=parse_code), picture_parse=PictureParse( wavelet_transform=WaveletTransform( transform_parameters=existing_tp, ), ), ) assert get_transform_parameters(data_unit) is existing_tp
def test_picture_tp_created(self, parse_code): data_unit = DataUnit( parse_info=ParseInfo(parse_code=parse_code), ) tp = get_transform_parameters(data_unit) assert tp == TransformParameters() tp["dwt_depth"] = 4 # Transform parameters (and parent structures) created assert data_unit == DataUnit( parse_info=ParseInfo(parse_code=parse_code), picture_parse=PictureParse( wavelet_transform=WaveletTransform( transform_parameters=TransformParameters( dwt_depth=4, ), ), ), )
def test_finalizer_works(self): f = BytesIO() w = BitstreamWriter(f) # Sequence with every data unit type and fully automatic numbers stream = Stream( sequences=[ Sequence( data_units=[ DataUnit( parse_info=ParseInfo( parse_code=tables.ParseCodes.sequence_header ), sequence_header=SequenceHeader( parse_parameters=ParseParameters(major_version=3), video_parameters=SourceParameters( # Tiny custom frame-size used to reduce test suite # runtime frame_size=FrameSize( custom_dimensions_flag=True, frame_width=4, frame_height=4, ) ), ), ), DataUnit( parse_info=ParseInfo( parse_code=tables.ParseCodes.high_quality_picture ), picture_parse=PictureParse( picture_header=PictureHeader(picture_number=0) ), ), DataUnit( parse_info=ParseInfo( parse_code=tables.ParseCodes.low_delay_picture ), picture_parse=PictureParse( picture_header=PictureHeader(picture_number=0) ), ), DataUnit( parse_info=ParseInfo( parse_code=tables.ParseCodes.high_quality_picture_fragment ), fragment_parse=FragmentParse( fragment_header=FragmentHeader(picture_number=0) ), ), DataUnit( parse_info=ParseInfo( parse_code=tables.ParseCodes.high_quality_picture_fragment ), fragment_parse=FragmentParse( fragment_header=FragmentHeader(picture_number=0) ), ), DataUnit( parse_info=ParseInfo( parse_code=tables.ParseCodes.padding_data ), padding=Padding(bytes=b"123"), ), DataUnit( parse_info=ParseInfo( parse_code=tables.ParseCodes.auxiliary_data ), auxiliary_data=AuxiliaryData(bytes=b"123"), ), DataUnit( parse_info=ParseInfo( parse_code=tables.ParseCodes.end_of_sequence ), ), ] ) ] ) ( next_parse_offsets_to_autofill, previous_parse_offsets_to_autofill, ) = autofill_parse_offsets(stream) with Serialiser(w, stream, vc2_default_values_with_auto) as serdes: vc2.parse_stream(serdes, State()) w.flush() offset_before = w.tell() autofill_parse_offsets_finalize( w, serdes.context, next_parse_offsets_to_autofill, previous_parse_offsets_to_autofill, ) assert w.tell() == offset_before f.seek(0) r = BitstreamReader(f) with Deserialiser(r) as serdes: vc2.parse_stream(serdes, State()) parse_infos = [ data_unit["parse_info"] for sequence in serdes.context["sequences"] for data_unit in sequence["data_units"] ] # Check for start/end offsets being zero assert parse_infos[0]["previous_parse_offset"] == 0 assert parse_infos[-1]["next_parse_offset"] == 0 # Check for consistency and plusibility of offsets for pi1, pi2 in zip(parse_infos, parse_infos[1:]): assert pi1["next_parse_offset"] > 13 assert pi2["previous_parse_offset"] > 13 assert pi1["next_parse_offset"] == pi2["previous_parse_offset"]
def test_removal_of_extended_transform_parameters(self): # NB: The stream specified below is actually compatible with version 2 # so the extended transform parameters field should be removed if and # only if the major version was set to AUTO in the proceeding sequence # header. stream = Stream( sequences=[ Sequence( data_units=[ DataUnit( parse_info=ParseInfo( parse_code=tables.ParseCodes.sequence_header, ), ), DataUnit( parse_info=ParseInfo( parse_code=tables.ParseCodes.high_quality_picture, ), picture_parse=PictureParse( wavelet_transform=WaveletTransform( transform_parameters=TransformParameters( wavelet_index=tables.WaveletFilters.haar_no_shift, dwt_depth=2, extended_transform_parameters=ExtendedTransformParameters( asym_transform_index_flag=True, wavelet_index_ho=tables.WaveletFilters.haar_no_shift, asym_transform_flag=True, dwt_depth_ho=0, ), ), ), ), ), ] ) ] ) autofill_major_version(stream) assert stream == Stream( sequences=[ Sequence( data_units=[ DataUnit( parse_info=ParseInfo( parse_code=tables.ParseCodes.sequence_header, ), sequence_header=SequenceHeader( parse_parameters=ParseParameters( major_version=2, ), ), ), DataUnit( parse_info=ParseInfo( parse_code=tables.ParseCodes.high_quality_picture, ), picture_parse=PictureParse( wavelet_transform=WaveletTransform( transform_parameters=TransformParameters( wavelet_index=tables.WaveletFilters.haar_no_shift, dwt_depth=2, ), ), ), ), ] ) ] )
def test_version_selection(self, parameters, exp_version): profile = parameters.get("profile", tables.Profiles.low_delay) frame_rate_index = parameters.get("frame_rate_index") signal_range_index = parameters.get("signal_range_index") color_spec_index = parameters.get("color_spec_index", 0) color_primaries_index = parameters.get("color_primaries_index", 0) color_matrix_index = parameters.get("color_matrix_index", 0) transfer_function_index = parameters.get("transfer_function_index", 0) wavelet_index = parameters.get( "wavelet_index", tables.WaveletFilters.haar_no_shift ) wavelet_index_ho = parameters.get("wavelet_index_ho") dwt_depth_ho = parameters.get("dwt_depth_ho", None) parse_code = parameters.get("parse_code", tables.ParseCodes.low_delay_picture) # Kept separate to allow later checking of the version chosen pp = ParseParameters(major_version=AUTO, profile=profile) # Repeated in the appropriate place for fragments and pictures tp = TransformParameters( wavelet_index=wavelet_index, dwt_depth=2, extended_transform_parameters=ExtendedTransformParameters( asym_transform_index_flag=wavelet_index_ho is not None, wavelet_index_ho=wavelet_index_ho, asym_transform_flag=dwt_depth_ho is not None, dwt_depth_ho=dwt_depth_ho, ), ) stream = Stream( sequences=[ Sequence( data_units=[ DataUnit( parse_info=ParseInfo( parse_code=tables.ParseCodes.sequence_header ), sequence_header=SequenceHeader( parse_parameters=pp, video_parameters=SourceParameters( frame_rate=FrameRate( custom_frame_rate_flag=frame_rate_index is not None, index=frame_rate_index, ), signal_range=SignalRange( custom_signal_range_flag=signal_range_index is not None, index=signal_range_index, ), color_spec=ColorSpec( custom_color_spec_flag=True, index=color_spec_index, color_primaries=ColorPrimaries( custom_color_primaries_flag=color_primaries_index is not None, index=color_primaries_index, ), color_matrix=ColorMatrix( custom_color_matrix_flag=color_matrix_index is not None, index=color_matrix_index, ), transfer_function=TransferFunction( custom_transfer_function_flag=transfer_function_index is not None, index=transfer_function_index, ), ), ), ), ), DataUnit( parse_info=ParseInfo( parse_code=parse_code, ), picture_parse=PictureParse( wavelet_transform=WaveletTransform( transform_parameters=tp, ) ), fragment_parse=FragmentParse( transform_parameters=tp, ), ), ] ) ] ) autofill_major_version(stream) assert pp["major_version"] == exp_version if pp["major_version"] == 3: assert "extended_transform_parameters" in tp else: assert "extended_transform_parameters" not in tp
class TestAutofillPictureNumber(object): @pytest.mark.parametrize( "seq", [ # Empty dictionary Sequence(), # Empty sequence Sequence(data_units=[]), # Sequence with immediate end-of-sequence Sequence( data_units=[ DataUnit( parse_info=ParseInfo( parse_code=tables.ParseCodes.end_of_sequence ), ) ] ), # Sequence with no pictures Sequence( data_units=[ DataUnit( parse_info=ParseInfo( parse_code=tables.ParseCodes.sequence_header ) ), DataUnit( parse_info=ParseInfo(parse_code=tables.ParseCodes.padding_data) ), DataUnit( parse_info=ParseInfo( parse_code=tables.ParseCodes.auxiliary_data ) ), DataUnit( parse_info=ParseInfo( parse_code=tables.ParseCodes.end_of_sequence ) ), ] ), ], ) def test_non_picture_sequence(self, seq): # Shouldn't crash or make any changes stream = Stream(sequences=[seq]) stream_orig = deepcopy(stream) autofill_picture_number(stream) assert stream == stream_orig @pytest.mark.parametrize( "seq", ( [ # Pictures Sequence( data_units=[ DataUnit( parse_info=ParseInfo(parse_code=parse_code), picture_parse=PictureParse( picture_header=PictureHeader( picture_number=1234, ) ), ) ] ) for parse_code in [ tables.ParseCodes.high_quality_picture, tables.ParseCodes.low_delay_picture, ] ] + [ # Fragments Sequence( data_units=[ DataUnit( parse_info=ParseInfo(parse_code=parse_code), fragment_parse=FragmentParse( fragment_header=FragmentHeader( picture_number=1234, fragment_slice_count=fragment_slice_count, ) ), ) ] ) for parse_code in [ tables.ParseCodes.high_quality_picture_fragment, tables.ParseCodes.low_delay_picture_fragment, ] for fragment_slice_count in [0, 1] ] ), ) def test_dont_change_non_auto_picture_numbers(self, seq): # Shouldn't crash or make any changes stream = Stream(sequences=[seq]) stream_orig = deepcopy(stream) autofill_picture_number(stream) assert stream == stream_orig @pytest.mark.parametrize( "parse_code", [tables.ParseCodes.high_quality_picture, tables.ParseCodes.low_delay_picture], ) def test_pictures(self, parse_code): stream = Stream( sequences=[ Sequence( data_units=[ # First in sequence should be auto-numbered to expected start # offset DataUnit( parse_info=ParseInfo(parse_code=parse_code), picture_parse=PictureParse( picture_header=PictureHeader(picture_number=AUTO) ), ), # If picture number not mentioned, it should be autofilled DataUnit( parse_info=ParseInfo(parse_code=parse_code), picture_parse=PictureParse(picture_header=PictureHeader()), ), # If explicit picture number given, should be used DataUnit( parse_info=ParseInfo(parse_code=parse_code), picture_parse=PictureParse( picture_header=PictureHeader(picture_number=0xFFFFFFFE) ), ), # Should continue from last explicit number if given DataUnit( parse_info=ParseInfo(parse_code=parse_code), picture_parse=PictureParse( picture_header=PictureHeader(picture_number=AUTO) ), ), # Should wrap-around DataUnit( parse_info=ParseInfo(parse_code=parse_code), picture_parse=PictureParse( picture_header=PictureHeader(picture_number=AUTO) ), ), ] ) ] ) autofill_picture_number(stream, 1234) picture_numbers = [ data_unit["picture_parse"]["picture_header"]["picture_number"] for seq in stream["sequences"] for data_unit in seq["data_units"] ] assert picture_numbers == [ 1234, 1235, 0xFFFFFFFE, 0xFFFFFFFF, 0x0, ] @pytest.mark.parametrize( "parse_code", [ tables.ParseCodes.high_quality_picture_fragment, tables.ParseCodes.low_delay_picture_fragment, ], ) def test_fragments(self, parse_code): stream = Stream( sequences=[ Sequence( data_units=[ # First in sequence should be auto-numbered to expected start # offset DataUnit( parse_info=ParseInfo(parse_code=parse_code), fragment_parse=FragmentParse( fragment_header=FragmentHeader( picture_number=AUTO, fragment_slice_count=0, ) ), ), # If not the first fragment in the picture, the picture number # should not be incremented DataUnit( parse_info=ParseInfo(parse_code=parse_code), fragment_parse=FragmentParse( fragment_header=FragmentHeader( picture_number=AUTO, fragment_slice_count=1, ) ), ), # If picture number not mentioned, it should still be autofilled DataUnit( parse_info=ParseInfo(parse_code=parse_code), fragment_parse=FragmentParse( fragment_header=FragmentHeader( fragment_slice_count=1, ) ), ), # Should auto increment on new picture started DataUnit( parse_info=ParseInfo(parse_code=parse_code), fragment_parse=FragmentParse( fragment_header=FragmentHeader( picture_number=AUTO, fragment_slice_count=0, ) ), ), # If explicit picture number when given, should be used DataUnit( parse_info=ParseInfo(parse_code=parse_code), fragment_parse=FragmentParse( fragment_header=FragmentHeader( picture_number=4321, fragment_slice_count=0, ) ), ), # ...even if that changes the picture number mid picture DataUnit( parse_info=ParseInfo(parse_code=parse_code), fragment_parse=FragmentParse( fragment_header=FragmentHeader( picture_number=0xFFFFFFFE, fragment_slice_count=1, ) ), ), # Should continue on from last explicit number DataUnit( parse_info=ParseInfo(parse_code=parse_code), fragment_parse=FragmentParse( fragment_header=FragmentHeader( picture_number=AUTO, fragment_slice_count=0, ) ), ), # Should wrap-around DataUnit( parse_info=ParseInfo(parse_code=parse_code), fragment_parse=FragmentParse( fragment_header=FragmentHeader( picture_number=AUTO, fragment_slice_count=0, ) ), ), ] ) ] ) autofill_picture_number(stream, 1234) picture_numbers = [ data_unit["fragment_parse"]["fragment_header"]["picture_number"] for seq in stream["sequences"] for data_unit in seq["data_units"] ] assert picture_numbers == [ 1234, 1234, 1234, 1235, 4321, 0xFFFFFFFE, 0xFFFFFFFF, 0x0, ] @pytest.mark.parametrize( "parse_code", [tables.ParseCodes.high_quality_picture, tables.ParseCodes.low_delay_picture], ) def test_multiple_sequences(self, parse_code): stream = Stream( sequences=[ Sequence( data_units=[ DataUnit(parse_info=ParseInfo(parse_code=parse_code)), DataUnit(parse_info=ParseInfo(parse_code=parse_code)), DataUnit(parse_info=ParseInfo(parse_code=parse_code)), ] ), Sequence( data_units=[ DataUnit(parse_info=ParseInfo(parse_code=parse_code)), DataUnit(parse_info=ParseInfo(parse_code=parse_code)), DataUnit(parse_info=ParseInfo(parse_code=parse_code)), ] ), ] ) autofill_picture_number(stream, 1234) picture_numbers = [ data_unit["picture_parse"]["picture_header"]["picture_number"] for seq in stream["sequences"] for data_unit in seq["data_units"] ] assert picture_numbers == [ 1234, 1235, 1236, # Restarts in second sequence 1234, 1235, 1236, ]
def make_picture_parse(codec_features, picture, minimum_qindex=0, minimum_slice_size_scaler=1): """ Compress a picture. Raises :py:exc:`PictureBytesSpecifiedForLosslessModeError` if ``picture_bytes`` is specifiied for a lossless coding mode. Raises :py:exc:`InsufficientLDPictureBytesError` :py:exc:`InsufficientHQPictureBytesError` if ``picture_bytes`` is too small for the coding mode used. Parameters ========== codec_features : :py:class:`~vc2_conformance.codec_features.CodecFeatures` picture : {"Y": [[s, ...], ...], "C1": ..., "C2": ..., "pic_num": int} The picture to be encoded. This picture will be compressed using a simple VC-2 encoder implementation. It does not necessarily produce the most high-quality encodings. If ``pic_num`` is omitted, ``picture_number`` fields will be omitted in the output. minimum_qindex : int Specifies the minimum quantization index to be used. Must be 0 for lossless codecs. minimum_slice_size_scaler : int Specifies the minimum slice_size_scaler to be used for high quality pictures. Ignored in low delay mode. Returns ======= transform_data : :py:class:`vc2_conformance.bitstream.TransformData` The transform data, ready for serialization. """ # Apply transform and split into slices transform_coeffs = transform_and_slice_picture(codec_features, picture) picture_header = PictureHeader() if "pic_num" in picture: picture_header["picture_number"] = picture["pic_num"] slice_parameters = SliceParameters( slices_x=codec_features["slices_x"], slices_y=codec_features["slices_y"], ) # Quantize coefficients as required and set slice_size_scaler (HQ Only) and # slice_bytes (LD only) if codec_features["profile"] == Profiles.high_quality: if codec_features["lossless"]: assert minimum_qindex == 0 if codec_features["picture_bytes"] is not None: raise PictureBytesSpecifiedForLosslessModeError(codec_features) slice_size_scaler, transform_data = make_transform_data_hq_lossless( transform_coeffs, minimum_slice_size_scaler, ) else: try: slice_size_scaler, transform_data = make_transform_data_hq_lossy( codec_features["picture_bytes"], transform_coeffs, minimum_qindex, minimum_slice_size_scaler, ) except InsufficientHQPictureBytesError: # Re-raise with codec features dict raise InsufficientHQPictureBytesError(codec_features) # NB: For simplicity, this implementation currently does not support # setting the slice prefix bytes to anything except zero since this is # not required by any existing VC-2 level. The assumption that this is # OK is verified in # ``tests/encoder/test_level_constraints_assumptions.py``. # # In addition, the `codec_features_to_trivial_level_constraints` # function assumes that this value is always 0 too. slice_parameters["slice_prefix_bytes"] = 0 slice_parameters["slice_size_scaler"] = slice_size_scaler elif codec_features["profile"] == Profiles.low_delay: if codec_features["lossless"]: raise LosslessUnsupportedByLowDelayError(codec_features) try: transform_data = make_transform_data_ld_lossy( codec_features["picture_bytes"], transform_coeffs, minimum_qindex, ) except InsufficientLDPictureBytesError: # Re-raise with codec features dict raise InsufficientLDPictureBytesError(codec_features) slice_bytes_fraction = Fraction( codec_features["picture_bytes"], codec_features["slices_x"] * codec_features["slices_y"], ) slice_parameters[ "slice_bytes_numerator"] = slice_bytes_fraction.numerator slice_parameters[ "slice_bytes_denominator"] = slice_bytes_fraction.denominator transform_parameters = TransformParameters( wavelet_index=codec_features["wavelet_index"], dwt_depth=codec_features["dwt_depth"], # NB: We *always* include an ExtendedTransformParameters field. This # field will later be removed (if not supported by the major_version # chosen for the stream) using the # :py:func:`vc2_conformance.bitstream.vc2_autofill.autofill_major_version` # function. extended_transform_parameters=make_extended_transform_parameters( codec_features), slice_parameters=slice_parameters, quant_matrix=make_quant_matrix(codec_features), ) wavelet_transform = WaveletTransform( transform_parameters=transform_parameters, transform_data=transform_data, ) return PictureParse( picture_header=picture_header, wavelet_transform=wavelet_transform, )