def test_profile_version_restriction(self): # A sequence with no pictures but (incorrectly) major_version 1 and # profile high quality. seq = bitstream.Sequence(data_units=[ bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.sequence_header, ), sequence_header=bitstream.SequenceHeader( parse_parameters=bitstream.ParseParameters( major_version=1, profile=tables.Profiles.high_quality, ), video_parameters=bitstream.SourceParameters( frame_size=bitstream.FrameSize( # Don't waste time on full-sized frames custom_dimensions_flag=True, frame_width=4, frame_height=4, ), clean_area=bitstream.CleanArea( custom_clean_area_flag=True, clean_width=4, clean_height=4, ), ), ), ), bitstream.DataUnit(parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.end_of_sequence, ), ), ]) populate_parse_offsets(seq) state = bytes_to_state(serialise_to_bytes(seq)) with pytest.raises(decoder.ProfileNotSupportedByVersion): decoder.parse_stream(state)
def output_decoder_test_case(output_dir, codec_features, test_case): """ Write a decoder test case to disk. Parameters ========== output_dir : str Output directory to write test cases to. codec_features : :py:class:`~vc2_conformance.codec_features.CodecFeatures` test_case : :py:class:`~vc2_conformance.test_cases.TestCase` """ # Serialise bitstream bitstream_filename = os.path.join( output_dir, "{}.vc2".format(test_case.name), ) with open(bitstream_filename, "wb") as f: autofill_and_serialise_stream(f, test_case.value) # Decode model answer model_answer_directory = os.path.join( output_dir, "{}_expected".format(test_case.name), ) makedirs(model_answer_directory, exist_ok=True) with open(bitstream_filename, "rb") as f: index = [0] def output_picture(picture, video_parameters, picture_coding_mode): file_format.write( picture, video_parameters, picture_coding_mode, os.path.join( model_answer_directory, "picture_{}.raw".format(index[0]), ), ) index[0] += 1 state = State(_output_picture_callback=output_picture) init_io(state, f) parse_stream(state) # Write metadata if test_case.metadata is not None: with open( os.path.join( output_dir, "{}_metadata.json".format(test_case.name), ), "w", ) as f: json.dump(test_case.metadata, f) logging.info( "Generated decoder test case %s for %s", test_case.name, codec_features["name"], )
def test_all_decoder_test_cases(codec_features, test_case): # Every test case for every basic video mode must produce a valid bitstream # containing pictures with the correct format. Any JSON metadata must also # be seriallisable. # Must return a Stream assert isinstance(test_case.value, Stream) # Mustn't crash! json.dumps(test_case.metadata) # Serialise f = BytesIO() autofill_and_serialise_stream(f, test_case.value) f.seek(0) # Deserialise/validate def output_picture_callback(picture, video_parameters, picture_coding_mode): assert video_parameters == codec_features["video_parameters"] assert picture_coding_mode == codec_features["picture_coding_mode"] state = State(_output_picture_callback=output_picture_callback, ) with alternative_level_1(): init_io(state, f) parse_stream(state)
def test_iter_sequence_headers(codec_features): sequence_headers = list(iter_sequence_headers(codec_features)) assert sequence_headers pi = ParseInfo( parse_code=ParseCodes.sequence_header, # An arbitrary non-zero value; this won't get picked up by the # conformance checker since it'll hit the end-of-file first next_parse_offset=999, ) for sh in sequence_headers: # Set the version to one compatible with the levels tried in this test sh.setdefault("parse_parameters", {})["major_version"] = 2 f = BytesIO() state = State() serialise(pi, parse_info, state, f) video_parameters = serialise(sh, sequence_header, state, f) # Check the encoded video parameters are as requested assert video_parameters == codec_features["video_parameters"] # Check that the header does everything that the level requires. Here we # just check we reach the end of the sequence header without a conformance # error. f.seek(0) state = State() init_io(state, f) with pytest.raises((UnexpectedEndOfStream, InconsistentNextParseOffset)): parse_stream(state) assert f.tell() == len(f.getvalue())
def test_qindex_matters(self): codec_features = deepcopy(MINIMAL_CODEC_FEATURES) codec_features["lossless"] = True codec_features["picture_bytes"] = None # Sanity check: Make sure we're outputting some kind of picture which # really does depend on quantization pictures = {False: [], True: []} for override_qindex in [False, True]: stream = lossless_quantization(codec_features) if override_qindex: for _state, _sx, _sy, hq_slice in iter_slices_in_sequence( codec_features, stream["sequences"][0], ): hq_slice["qindex"] = 0 # Serialise f = BytesIO() autofill_and_serialise_stream(f, stream) f.seek(0) # Decode def output_picture_callback(picture, video_parameters, picture_coding_mode): pictures[override_qindex].append(picture) state = State(_output_picture_callback=output_picture_callback) init_io(state, f) parse_stream(state) # Make sure that the qindex mattered by checking that decoding with # qindex clamped to 0 resulted in different pictures assert pictures[False] != pictures[True]
def test_parse_code_version_restriction(self): # A sequence with 1 HQ picture fragment but (incorrectly) major_version 2 seq = bitstream.Sequence(data_units=[ bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.sequence_header, ), sequence_header=bitstream.SequenceHeader( parse_parameters=bitstream.ParseParameters(major_version=2, ), video_parameters=bitstream.SourceParameters( frame_size=bitstream.FrameSize( # Don't waste time on full-sized frames custom_dimensions_flag=True, frame_width=4, frame_height=4, ), clean_area=bitstream.CleanArea( custom_clean_area_flag=True, clean_width=4, clean_height=4, ), ), ), ), bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.high_quality_picture_fragment, ), fragment_parse=bitstream.FragmentParse( fragment_header=bitstream.FragmentHeader( fragment_slice_count=0, ), transform_parameters=bitstream.TransformParameters( slice_parameters=bitstream.SliceParameters( slices_x=1, slices_y=1, ), ), ), ), bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.high_quality_picture_fragment, ), fragment_parse=bitstream.FragmentParse( fragment_header=bitstream.FragmentHeader( fragment_slice_count=1, ), ), ), bitstream.DataUnit(parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.end_of_sequence, ), ), ]) populate_parse_offsets(seq) state = bytes_to_state(serialise_to_bytes(seq)) with pytest.raises(decoder.ParseCodeNotSupportedByVersion): decoder.parse_stream(state)
def serialise_and_decode_pictures(self, stream): f = BytesIO() autofill_and_serialise_stream(f, stream) pictures = [] state = State( _output_picture_callback=lambda pic, vp, pcm: pictures.append(pic)) f.seek(0) decoder.init_io(state, f) decoder.parse_stream(state) return pictures
def encode_and_decode(stream): f = BytesIO() autofill_and_serialise_stream(f, stream) f.seek(0) pictures = [] state = State( _output_picture_callback=lambda p, vp, pcm: pictures.append(p)) init_io(state, f) parse_stream(state) return pictures
def test_format_parse_code_traceback(): # Capture a traceback resulting from the I/O subsystem not being # initialised try: parse_stream(State()) except KeyError: exc_type, exc_value, exc_tb = sys.exc_info() tb = traceback.extract_tb(exc_tb) assert format_pseudocode_traceback(tb) == ( "* parse_stream (10.3)\n" " * is_end_of_stream (A.2.5)" )
def test_immediate_end_of_sequence(self, sh_data_unit_bytes): state = bytes_to_state( serialise_to_bytes( bitstream.ParseInfo( parse_code=tables.ParseCodes.end_of_sequence))) with pytest.raises(decoder.GenericInvalidSequence) as exc_info: decoder.parse_stream(state) assert exc_info.value.parse_code is tables.ParseCodes.end_of_sequence assert exc_info.value.expected_parse_codes == [ tables.ParseCodes.sequence_header ] assert exc_info.value.expected_end is False
def test_minimal_major_version_requirement( self, num_pictures, major_version, expected_major_version, exp_fail, ): # A sequence with 1 (or zero) HQ pictures seq = bitstream.Sequence(data_units=[ bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.sequence_header, ), sequence_header=bitstream.SequenceHeader( parse_parameters=bitstream.ParseParameters( major_version=major_version, ), video_parameters=bitstream.SourceParameters( frame_size=bitstream.FrameSize( # Don't waste time on full-sized frames custom_dimensions_flag=True, frame_width=4, frame_height=4, ), clean_area=bitstream.CleanArea( custom_clean_area_flag=True, clean_width=4, clean_height=4, ), ), ), ), ] + [ bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.high_quality_picture, ), picture_parse=bitstream.PictureParse( picture_header=bitstream.PictureHeader(picture_number=n, ), ), ) for n in range(num_pictures) ] + [ bitstream.DataUnit(parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.end_of_sequence, ), ), ]) populate_parse_offsets(seq) state = bytes_to_state(serialise_to_bytes(seq)) if exp_fail: with pytest.raises(decoder.MajorVersionTooHigh): decoder.parse_stream(state) else: decoder.parse_stream(state) assert state["_expected_major_version"] == expected_major_version
def check_codec_features_valid(codec_feature_sets): """ Verify that the codec features requested don't themselves violate the spec (e.g. violate a level constraint). This is done by generating then validating a bitstream containing a single mid-gray frame. Prints an error to stderr and calls :py:func:`sys.exit` if a problem is encountered. """ logging.info("Checking codec feature sets are valid...") for name, codec_features in codec_feature_sets.items(): logging.info("Checking %r...", name) f = BytesIO() # Sanity-check the color format (since this won't raise a validation # error but will result in a useless gamut being available). sanity = sanity_check_video_parameters(codec_features["video_parameters"]) if not sanity: logging.warning( "Color specification for codec configuration %r be malformed: %s", name, sanity.explain(), ) # Generate a minimal bitstream try: autofill_and_serialise_stream(f, static_gray(codec_features)) except UnsatisfiableCodecFeaturesError as e: sys.stderr.write( "Error: Codec configuration {!r} is invalid:\n".format(name) ) terminal_width = get_terminal_size()[0] sys.stderr.write(wrap_paragraphs(e.explain(), terminal_width)) sys.stderr.write("\n") sys.exit(4) f.seek(0) # Validate it meets the spec state = State() init_io(state, f) try: parse_stream(state) except ConformanceError as e: sys.stderr.write( "Error: Codec configuration {!r} is invalid:\n".format(name) ) terminal_width = get_terminal_size()[0] sys.stderr.write(wrap_paragraphs(e.explain(), terminal_width)) sys.stderr.write("\n") sys.exit(4)
def test_no_sequence_header(self, sh_data_unit_bytes): state = bytes_to_state( serialise_to_bytes( bitstream.ParseInfo( parse_code=tables.ParseCodes.padding_data, next_parse_offset=tables.PARSE_INFO_HEADER_BYTES, ))) with pytest.raises(decoder.GenericInvalidSequence) as exc_info: decoder.parse_stream(state) assert exc_info.value.parse_code is tables.ParseCodes.padding_data assert exc_info.value.expected_parse_codes == [ tables.ParseCodes.sequence_header ] assert exc_info.value.expected_end is False
def test_odd_number_of_fields_disallowed(self, picture_coding_mode, num_pictures, exp_fail): # A sequence with num_pictures HQ pictures seq = bitstream.Sequence(data_units=[ bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.sequence_header, ), sequence_header=bitstream.SequenceHeader( picture_coding_mode=picture_coding_mode, parse_parameters=bitstream.ParseParameters( major_version=2), video_parameters=bitstream.SourceParameters( frame_size=bitstream.FrameSize( # Don't waste time on full-sized frames custom_dimensions_flag=True, frame_width=4, frame_height=4, ), clean_area=bitstream.CleanArea( custom_clean_area_flag=True, clean_width=4, clean_height=4, ), ), ), ), ] + [ bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.high_quality_picture, ), picture_parse=bitstream.PictureParse( picture_header=bitstream.PictureHeader(picture_number=n, ), ), ) for n in range(num_pictures) ] + [ bitstream.DataUnit(parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.end_of_sequence, ), ), ]) populate_parse_offsets(seq) state = bytes_to_state(serialise_to_bytes(seq)) if exp_fail: with pytest.raises( decoder.OddNumberOfFieldsInSequence) as exc_info: decoder.parse_stream(state) assert exc_info.value.num_fields_in_sequence == num_pictures else: decoder.parse_stream(state)
def run(self): try: self._file = open(self._filename, "rb") self._filesize_bytes = os.path.getsize(self._filename) except Exception as e: # Catch-all exception handler excuse: Catching only file-related # exceptions is challenging, particularly in a backward-compatible # manner. However, none of the above are known to produce # exceptions *except* due to file-related issues. self._print_error(str(e)) return 1 self._state = State(_output_picture_callback=self._output_picture) init_io(self._state, self._file) if self._show_status: self._update_status_line("Starting bitstream validation...") try: parse_stream(self._state) self._hide_status_line() if tell(self._state) == (0, 7): sys.stdout.flush() sys.stderr.write( "Warning: 0 bytes read, bitstream is empty.\n") print( "No errors found in bitstream. Verify decoded pictures to confirm conformance." ) return 0 except ConformanceError as e: # Bitstream failed validation exc_type, exc_value, exc_tb = sys.exc_info() self._hide_status_line() self._print_conformance_error(e, traceback.extract_tb(exc_tb)) self._print_error("non-conformant bitstream (see above)") return 2 except Exception as e: # Internal error (shouldn't happen(!)) self._hide_status_line() self._print_error("internal error in bitstream validator: {}: {} " "(probably a bug in this program)".format( type(e).__name__, str(e), )) return 3
def serialize_and_decode(sequence): # Serialise f = BytesIO() autofill_and_serialise_stream(f, Stream(sequences=[sequence])) # Setup callback to capture decoded pictures decoded_pictures = [] def output_picture_callback(picture, video_parameters, picture_coding_mode): decoded_pictures.append(picture) # Feed to conformance checking decoder f.seek(0) state = State(_output_picture_callback=output_picture_callback) init_io(state, f) parse_stream(state) return decoded_pictures
def check_for_signal_clipping(sequence): """ Given a :py:class:`vc2_conformance.bitstream.Sequence`, return True if any picture component signal was clipped during decoding. """ # NB: Internally we just check for saturated signal levels. This way we # avoid the need to modify the decoder to remove the clipper and all that # faff... # Serialise f = BytesIO() # NB: Deepcopy required due to autofill_and_serialise_stream mutating the # stream stream = Stream(sequences=[deepcopy(sequence)]) autofill_and_serialise_stream(f, stream) f.seek(0) # Decode and look for saturated pixel values state = State() may_have_clipped = [False] def output_picture_callback(picture, video_parameters, picture_coding_mode): components_and_depths = [ ("Y", state["luma_depth"]), ("C1", state["color_diff_depth"]), ("C2", state["color_diff_depth"]), ] for component, depth in components_and_depths: min_value = min(min(row) for row in picture[component]) max_value = max(max(row) for row in picture[component]) if min_value == 0: may_have_clipped[0] = True if max_value == (1 << depth) - 1: may_have_clipped[0] = True state["_output_picture_callback"] = output_picture_callback init_io(state, f) parse_stream(state) return may_have_clipped[0]
def test_picture_and_incomplete_fragment_interleaving_disallowed( self, num_slices_to_send, exp_fail): # A sequence with a 3x2 slice picture fragment with num_slices_to_send slices in # it followed by an HQ picture seq = bitstream.Sequence(data_units=[ bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.sequence_header, ), sequence_header=bitstream.SequenceHeader( video_parameters=bitstream.SourceParameters( frame_size=bitstream.FrameSize( # Don't waste time on full-sized pictures custom_dimensions_flag=True, frame_width=8, frame_height=8, ), clean_area=bitstream.CleanArea( custom_clean_area_flag=True, clean_width=8, clean_height=8, ), ), ), ), bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.high_quality_picture_fragment, ), fragment_parse=bitstream.FragmentParse( fragment_header=bitstream.FragmentHeader( picture_number=0, fragment_slice_count=0, ), transform_parameters=bitstream.TransformParameters( slice_parameters=bitstream.SliceParameters( slices_x=3, slices_y=2, ), ), ), ), bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.high_quality_picture_fragment, ), fragment_parse=bitstream.FragmentParse( fragment_header=bitstream.FragmentHeader( picture_number=0, fragment_slice_count=num_slices_to_send, ), ), ), bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.high_quality_picture, ), picture_parse=bitstream.PictureParse( picture_header=bitstream.PictureHeader(picture_number=1, ), ), ), bitstream.DataUnit(parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.end_of_sequence, ), ), ]) # Don't include second (non-header) fragment if sending no slices if num_slices_to_send == 0: del seq["data_units"][2] populate_parse_offsets(seq) state = bytes_to_state(serialise_to_bytes(seq)) if exp_fail: with pytest.raises(decoder.PictureInterleavedWithFragmentedPicture ) as exc_info: decoder.parse_stream(state) first_fragment_offset = ( seq["data_units"][0]["parse_info"]["next_parse_offset"] + tables.PARSE_INFO_HEADER_BYTES) assert exc_info.value.initial_fragment_offset == ( first_fragment_offset, 7) picture_offset = ( sum(seq["data_units"][i]["parse_info"]["next_parse_offset"] for i in range(len(seq["data_units"]) - 2)) + tables.PARSE_INFO_HEADER_BYTES) assert exc_info.value.this_offset == (picture_offset, 7) assert exc_info.value.fragment_slices_received == num_slices_to_send assert exc_info.value.fragment_slices_remaining == 6 - num_slices_to_send else: decoder.parse_stream(state)
def test_output_picture(self): # This test adds a callback for output_picture and makes sure that both # fragments and pictures call it correctly (and that sanity-checks very # loosely that decoding etc. is happening). Finally, it also checks # that two concatenated sequences are read one after another. # A sequence with a HQ picture followed by a HQ fragment (both all # zeros) seq1 = bitstream.Sequence(data_units=[ bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.sequence_header, ), sequence_header=bitstream.SequenceHeader( video_parameters=bitstream.SourceParameters( frame_size=bitstream.FrameSize( # Don't waste time on full-sized frames custom_dimensions_flag=True, frame_width=4, frame_height=2, ), clean_area=bitstream.CleanArea( custom_clean_area_flag=True, clean_width=4, clean_height=2, ), color_diff_sampling_format=bitstream. ColorDiffSamplingFormat( custom_color_diff_format_flag=True, color_diff_format_index=tables. ColorDifferenceSamplingFormats. color_4_2_2, # noqa: E501 ), # Output values will be treated as 8-bit (and thus all # decode to 128) signal_range=bitstream.SignalRange( custom_signal_range_flag=True, index=tables.PresetSignalRanges. video_8bit_full_range, ), ), picture_coding_mode=tables.PictureCodingModes. pictures_are_frames, ), ), # A HQ picture bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.high_quality_picture, ), picture_parse=bitstream.PictureParse( picture_header=bitstream.PictureHeader(picture_number=10, ), ), ), # A fragmented HQ picture (sent over two fragments to ensure the # callback only fires after a whole picture arrives) bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.high_quality_picture_fragment, ), fragment_parse=bitstream.FragmentParse( fragment_header=bitstream.FragmentHeader( picture_number=11, fragment_slice_count=0, ), transform_parameters=bitstream.TransformParameters( slice_parameters=bitstream.SliceParameters( slices_x=2, slices_y=1, ), ), ), ), bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.high_quality_picture_fragment, ), fragment_parse=bitstream.FragmentParse( fragment_header=bitstream.FragmentHeader( picture_number=11, fragment_slice_count=1, fragment_x_offset=0, fragment_y_offset=0, ), ), ), bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.high_quality_picture_fragment, ), fragment_parse=bitstream.FragmentParse( fragment_header=bitstream.FragmentHeader( picture_number=11, fragment_slice_count=1, fragment_x_offset=1, fragment_y_offset=0, ), ), ), bitstream.DataUnit(parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.end_of_sequence, ), ), ]) # Another (HQ) picture in a separate sequence seq2 = bitstream.Sequence(data_units=[ bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.sequence_header, ), sequence_header=bitstream.SequenceHeader( parse_parameters=bitstream.ParseParameters( major_version=2), video_parameters=bitstream.SourceParameters( frame_size=bitstream.FrameSize( # Don't waste time on full-sized frames custom_dimensions_flag=True, frame_width=4, frame_height=2, ), clean_area=bitstream.CleanArea( custom_clean_area_flag=True, clean_width=4, clean_height=2, ), color_diff_sampling_format=bitstream. ColorDiffSamplingFormat( custom_color_diff_format_flag=True, color_diff_format_index=tables. ColorDifferenceSamplingFormats. color_4_2_2, # noqa: E501 ), # Output values will be treated as 8-bit (and thus all # decode to 128) signal_range=bitstream.SignalRange( custom_signal_range_flag=True, index=tables.PresetSignalRanges. video_8bit_full_range, ), ), picture_coding_mode=tables.PictureCodingModes. pictures_are_frames, ), ), bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.high_quality_picture, ), picture_parse=bitstream.PictureParse( picture_header=bitstream.PictureHeader( picture_number=12), ), ), bitstream.DataUnit(parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.end_of_sequence, ), ), ]) populate_parse_offsets(seq1) populate_parse_offsets(seq2) state = bytes_to_state( serialise_to_bytes(seq1) + serialise_to_bytes(seq2)) state["_output_picture_callback"] = Mock() decoder.parse_stream(state) assert state["_output_picture_callback"].call_count == 3 for i, (args, kwargs) in enumerate( state["_output_picture_callback"].call_args_list): assert kwargs == {} # Should get a 4x2 mid-gray frame with 4:2:2 color difference sampling assert args[0] == { "pic_num": 10 + i, "Y": [[128, 128, 128, 128], [128, 128, 128, 128]], "C1": [[128, 128], [128, 128]], "C2": [[128, 128], [128, 128]], } # Just sanity check the second argument looks like a set of video parameters assert args[1]["frame_width"] == 4 assert args[1]["frame_height"] == 2 assert args[1]["luma_offset"] == 0 assert args[1]["luma_offset"] == 0 assert args[1]["luma_excursion"] == 255 assert args[1]["color_diff_offset"] == 128 assert args[1]["color_diff_excursion"] == 255 # And the picture coding mode too... assert args[2] == tables.PictureCodingModes.pictures_are_frames