def test_plausible_maximisation_no_quantisation(
            self, analysis_input_array, analysis_transform_coeff_arrays,
            synthesis_output_array, synthesis_intermediate_arrays,
            wavelet_index, wavelet_index_ho, dwt_depth, dwt_depth_ho):
        # Check that the test patterns do, in fact, appear to maximise the
        # filter output for quantisation-free filtering
        value_min = -512
        value_max = 255

        for tx in range(synthesis_output_array.period[0]):
            for ty in range(synthesis_output_array.period[1]):
                # Produce a test pattern
                ts = make_synthesis_maximising_pattern(
                    analysis_input_array,
                    analysis_transform_coeff_arrays,
                    synthesis_output_array,
                    synthesis_output_array,
                    tx,
                    ty,
                )

                # Create a test picture and encode/decode it with the VC-2
                # pseudocode (with no quantisaton)
                test_pattern_picture, _ = ts.pattern.as_picture_and_slice(
                    value_min, value_max)
                height, width = test_pattern_picture.shape
                test_pattern_picture = test_pattern_picture.tolist()

                new_tx, new_ty = ts.target
                kwargs = {
                    "width": width,
                    "height": height,
                    "wavelet_index": wavelet_index,
                    "wavelet_index_ho": wavelet_index_ho,
                    "dwt_depth": dwt_depth,
                    "dwt_depth_ho": dwt_depth_ho,
                }
                value_maximised = decode_with_vc2(
                    encode_with_vc2(test_pattern_picture, **kwargs),
                    **kwargs)[new_ty][new_tx]

                # Check the value was in fact maximised
                assert value_maximised == value_max
    def test_always_positive_pixel_coords(
        self,
        analysis_input_array,
        analysis_transform_coeff_arrays,
        synthesis_output_array,
        synthesis_intermediate_arrays,
    ):
        for (
                level, name
        ), synthesis_target_array in synthesis_intermediate_arrays.items():
            for tx in range(synthesis_target_array.period[0]):
                for ty in range(synthesis_target_array.period[1]):
                    ts = make_synthesis_maximising_pattern(
                        analysis_input_array,
                        analysis_transform_coeff_arrays,
                        synthesis_target_array,
                        synthesis_output_array,
                        tx,
                        ty,
                    )
                    new_tx, new_ty = ts.target
                    assert new_tx >= 0
                    assert new_ty >= 0

                    # Ensure that, as promised, the returned test patterns
                    # don't use any negative pixel coordinates
                    assert ts.pattern.origin[0] >= 0
                    assert ts.pattern.origin[1] >= 0

                    # Also ensure that the returned test pattern is as close to
                    # the edge of the picture as possible (that is, moving one
                    # multiple left or up moves us off the edge of the picture)
                    # or if not, that the target value is as close as possible
                    # to the edge in that dimension.
                    mx, my = ts.pattern_translation_multiple
                    tmx, tmy = ts.target_translation_multiple
                    assert ts.pattern.origin[0] < my or new_tx < my
                    assert ts.pattern.origin[1] < mx or new_ty < mx
def test_evaluate_synthesis_test_pattern_output():
    # In this test we simply check that the decoded values match those
    # computed by the optimise_synthesis_maximising_test_pattern function

    wavelet_index = WaveletFilters.haar_with_shift
    wavelet_index_ho = WaveletFilters.le_gall_5_3
    dwt_depth = 1
    dwt_depth_ho = 0

    picture_bit_width = 10

    max_quantisation_index = 64

    quantisation_matrix = {
        0: {
            "LL": 0
        },
        1: {
            "LH": 1,
            "HL": 2,
            "HH": 3
        },
    }

    h_filter_params = LIFTING_FILTERS[wavelet_index_ho]
    v_filter_params = LIFTING_FILTERS[wavelet_index]

    input_min, input_max = signed_integer_range(picture_bit_width)

    input_array = SymbolArray(2)
    analysis_transform_coeff_arrays, _ = analysis_transform(
        h_filter_params,
        v_filter_params,
        dwt_depth,
        dwt_depth_ho,
        input_array,
    )

    symbolic_coeff_arrays = make_symbol_coeff_arrays(dwt_depth, dwt_depth_ho)
    symbolic_output_array, symbolic_intermediate_arrays = synthesis_transform(
        h_filter_params,
        v_filter_params,
        dwt_depth,
        dwt_depth_ho,
        symbolic_coeff_arrays,
    )

    pyexp_coeff_arrays = make_variable_coeff_arrays(dwt_depth, dwt_depth_ho)
    _, pyexp_intermediate_arrays = synthesis_transform(
        h_filter_params,
        v_filter_params,
        dwt_depth,
        dwt_depth_ho,
        pyexp_coeff_arrays,
    )

    for (level,
         array_name), target_array in symbolic_intermediate_arrays.items():
        for x in range(target_array.period[0]):
            for y in range(target_array.period[1]):
                # Create a test pattern
                test_pattern = make_synthesis_maximising_pattern(
                    input_array,
                    analysis_transform_coeff_arrays,
                    target_array,
                    symbolic_output_array,
                    x,
                    y,
                )

                synthesis_pyexp = pyexp_intermediate_arrays[(level,
                                                             array_name)][x, y]
                # Run with no-optimisation iterations but, as a side effect,
                # compute the actual decoded value to compare with
                test_pattern = optimise_synthesis_maximising_test_pattern(
                    h_filter_params,
                    v_filter_params,
                    dwt_depth,
                    dwt_depth_ho,
                    quantisation_matrix,
                    synthesis_pyexp,
                    test_pattern,
                    input_min,
                    input_max,
                    max_quantisation_index,
                    None,
                    1,
                    None,
                    0.0,
                    0.0,
                    0,
                    0,
                )

                # Find the actual values
                lower_value, upper_value = evaluate_synthesis_test_pattern_output(
                    h_filter_params,
                    v_filter_params,
                    dwt_depth,
                    dwt_depth_ho,
                    quantisation_matrix,
                    synthesis_pyexp,
                    test_pattern,
                    input_min,
                    input_max,
                    max_quantisation_index,
                )

                assert upper_value[0] == test_pattern.decoded_value
                assert upper_value[1] == test_pattern.quantisation_index
Example #4
0
def static_filter_analysis(
    wavelet_index,
    wavelet_index_ho,
    dwt_depth,
    dwt_depth_ho,
    num_batches=1,
    batch_num=0,
):
    r"""
    Performs a complete static analysis of a VC-2 filter configuration,
    computing theoretical upper- and lower-bounds for signal values (see
    :ref:`theory-affine-arithmetic`) and heuristic test patterns (see
    :ref:`theory-test-patterns`) for all intermediate and final analysis and
    synthesis filter values.
    
    Parameters
    ==========
    wavelet_index : :py:class:`vc2_data_tables.WaveletFilters` or int
    wavelet_index_ho : :py:class:`vc2_data_tables.WaveletFilters` or int
    dwt_depth : int
    dwt_depth_ho : int
        The filter parameters.
    
    num_batches : int
    batch_num : int
        Though for most filters this function runs either instantaneously or at
        worst in the space of a couple of hours, unusually large filters can
        take an extremely long time to run. For example, a 4-level Fidelity
        transform may take around a month to evaluate.
        
        These arguments may be used to split this job into separate batches
        which may be computed separately (and in parallel) and later combined.
        For example, setting ``num_batches`` to 3 results in only analysing
        every third filter phase. The ``batch_num`` parameter should then be
        set to either 0, 1 or 2 to specify which third.
        
        The skipped phases are simply omitted from the returned dictionaries.
        The dictionaries returned for each batch should be unified to produce
        the complete analysis.
    
    Returns
    =======
    analysis_signal_bounds : {(level, array_name, x, y): (lower_bound_exp, upper_bound_exp), ...}
    synthesis_signal_bounds : {(level, array_name, x, y): (lower_bound_exp, upper_bound_exp), ...}
        Expressions defining the upper and lower bounds for all intermediate
        and final analysis and synthesis filter values.
        
        The keys of the returned dictionaries give the level, array name and
        filter phase for which each pair of bounds corresponds (see
        :ref:`terminology`). The naming
        conventions used are those defined by
        :py:func:`vc2_bit_widths.vc2_filters.analysis_transform` and
        :py:func:`vc2_bit_widths.vc2_filters.synthesis_transform`. Arrays which
        are just interleavings, subsamplings or renamings of other arrays are
        omitted.
        
        The lower and upper bounds are given algebraically as
        :py:class:`~vc2_bit_widths.linexp.LinExp`\ s.
        
        For the analysis filter bounds, the expressions are defined in terms of
        the variables ``LinExp("signal_min")`` and ``LinExp("signal_max")``.
        These should be substituted for the minimum and maximum picture signal
        level to find the upper and lower bounds for a particular picture bit
        width.
        
        For the synthesis filter bounds, the expressions are defined in terms
        of variables of the form ``LinExp("coeff_LEVEL_ORIENT_min")`` and
        ``LinExp("coeff_LEVEL_ORIENT_max")`` which give lower and upper bounds
        for the transform coefficients with the named level and orientation.
        
        The :py:func:`~vc2_bit_widths.helpers.evaluate_filter_bounds` function
        may be used to substitute concrete values into these expressions for a
        particular picture bit width.
        
    analysis_test_patterns: {(level, array_name, x, y): :py:class:`~vc2_bit_widths.patterns.TestPatternSpecification`, ...}
    synthesis_test_patterns: {(level, array_name, x, y): :py:class:`~vc2_bit_widths.patterns.TestPatternSpecification`, ...}
        Heuristic test patterns which are designed to maximise a particular
        intermediate or final filter value. For a minimising test pattern,
        invert the polarities of the pixels.
        
        The keys of the returned dictionaries give the level, array name and
        filter phase for which each set of bounds corresponds (see
        :ref:`terminology`). Arrays which are just interleavings, subsamplings
        or renamings of other arrays are omitted.
    """
    v_filter_params = LIFTING_FILTERS[wavelet_index]
    h_filter_params = LIFTING_FILTERS[wavelet_index_ho]

    # Create the algebraic representation of the analysis transform
    picture_array = SymbolArray(2)
    analysis_coeff_arrays, intermediate_analysis_arrays = analysis_transform(
        h_filter_params,
        v_filter_params,
        dwt_depth,
        dwt_depth_ho,
        picture_array,
    )

    # Count the total number of arrays for use in logging messages
    num_arrays = sum(array.period[0] * array.period[1]
                     for array in intermediate_analysis_arrays.values()
                     if not array.nop)
    array_num = 0

    # Compute bounds/test pattern for every intermediate/output analysis value
    analysis_signal_bounds = OrderedDict()
    analysis_test_patterns = OrderedDict()
    for (level,
         array_name), target_array in intermediate_analysis_arrays.items():
        # Skip arrays which are just views of other arrays
        if target_array.nop:
            continue

        for x in range(target_array.period[0]):
            for y in range(target_array.period[1]):
                array_num += 1
                if (array_num - 1) % num_batches != batch_num:
                    continue

                logger.info(
                    "Analysing analysis filter %d of %d (level %d, %s[%d, %d])",
                    array_num,
                    num_arrays,
                    level,
                    array_name,
                    x,
                    y,
                )

                # Compute signal bounds
                analysis_signal_bounds[(level, array_name, x,
                                        y)] = analysis_filter_bounds(
                                            target_array[x, y])

                # Generate test pattern
                analysis_test_patterns[(level, array_name, x,
                                        y)] = make_analysis_maximising_pattern(
                                            picture_array,
                                            target_array,
                                            x,
                                            y,
                                        )

    # Create the algebraic representation of the synthesis transform
    coeff_arrays = make_symbol_coeff_arrays(dwt_depth, dwt_depth_ho)
    synthesis_output_array, intermediate_synthesis_arrays = synthesis_transform(
        h_filter_params,
        v_filter_params,
        dwt_depth,
        dwt_depth_ho,
        coeff_arrays,
    )

    # Create a view of the analysis coefficient arrays which avoids recomputing
    # already-known analysis filter phases
    cached_analysis_coeff_arrays = {
        level: {
            orient: SymbolicPeriodicCachingArray(array, picture_array)
            for orient, array in orients.items()
        }
        for level, orients in analysis_coeff_arrays.items()
    }

    # Count the total number of arrays for use in logging messages
    num_arrays = sum(array.period[0] * array.period[1]
                     for array in intermediate_synthesis_arrays.values()
                     if not array.nop)
    array_num = 0

    # Compute bounds/test pattern for every intermediate/output analysis value
    synthesis_signal_bounds = OrderedDict()
    synthesis_test_patterns = OrderedDict()
    for (level,
         array_name), target_array in intermediate_synthesis_arrays.items():
        # Skip arrays which are just views of other arrays
        if target_array.nop:
            continue

        for x in range(target_array.period[0]):
            for y in range(target_array.period[1]):
                array_num += 1
                if (array_num - 1) % num_batches != batch_num:
                    continue

                logger.info(
                    "Analysing synthesis filter %d of %d (level %d, %s[%d, %d])",
                    array_num,
                    num_arrays,
                    level,
                    array_name,
                    x,
                    y,
                )

                # Compute signal bounds
                synthesis_signal_bounds[(level, array_name, x,
                                         y)] = synthesis_filter_bounds(
                                             target_array[x, y])

                # Compute test pattern
                synthesis_test_patterns[(
                    level, array_name, x,
                    y)] = make_synthesis_maximising_pattern(
                        picture_array,
                        cached_analysis_coeff_arrays,
                        target_array,
                        synthesis_output_array,
                        x,
                        y,
                    )

                # For extremely large filters, a noteworthy amount of overall
                # RAM can be saved by not caching synthesis filters. These
                # filters generally don't benefit much in terms of runtime from
                # caching so this has essentially no impact on runtime.
                for a in intermediate_synthesis_arrays.values():
                    a.clear_cache()

    return (
        analysis_signal_bounds,
        synthesis_signal_bounds,
        analysis_test_patterns,
        synthesis_test_patterns,
    )
    def test_plausible_maximisation_with_quantisation(
            self, analysis_input_array, analysis_transform_coeff_arrays,
            synthesis_output_array, synthesis_intermediate_arrays,
            wavelet_index, wavelet_index_ho, dwt_depth, dwt_depth_ho):
        # Check that the test patterns do, in fact, appear to make larger values
        # post-quantisation than would appear with a more straight-forward
        # scheme
        value_min = -512
        value_max = 511

        num_periods_made_larger_by_test_pattern = 0
        for tx in range(synthesis_output_array.period[0]):
            for ty in range(synthesis_output_array.period[1]):
                # Produce a test pattern
                ts = make_synthesis_maximising_pattern(
                    analysis_input_array,
                    analysis_transform_coeff_arrays,
                    synthesis_output_array,
                    synthesis_output_array,
                    tx,
                    ty,
                )

                test_pattern_picture, _ = ts.pattern.as_picture_and_slice(
                    value_min, value_max)
                height, width = test_pattern_picture.shape
                test_pattern_picture = test_pattern_picture.tolist()

                # Create picture where just the target pixel is set
                new_tx, new_ty = ts.target
                just_target_picture = [[
                    int((x, y) == (new_tx, new_ty)) * value_max
                    for x in range(width)
                ] for y in range(height)]

                kwargs = {
                    "width": width,
                    "height": height,
                    "wavelet_index": wavelet_index,
                    "wavelet_index_ho": wavelet_index_ho,
                    "dwt_depth": dwt_depth,
                    "dwt_depth_ho": dwt_depth_ho,
                }

                # Encode with VC-2 pseudocode
                test_pattern_coeffs = encode_with_vc2(test_pattern_picture,
                                                      **kwargs)
                just_target_coeffs = encode_with_vc2(just_target_picture,
                                                     **kwargs)

                # Try different quantisation levels and record the worst-case
                # effect on the target pixel
                test_pattern_value_worst_case = 0
                just_target_value_worst_case = 0
                for qi in range(64):
                    test_pattern_value = decode_with_vc2(
                        quantise_coeffs(test_pattern_coeffs, qi),
                        **kwargs)[new_ty][new_tx]
                    if abs(test_pattern_value) > abs(
                            test_pattern_value_worst_case):
                        test_pattern_value_worst_case = test_pattern_value

                    just_target_value = decode_with_vc2(
                        quantise_coeffs(just_target_coeffs, qi),
                        **kwargs)[new_ty][new_tx]
                    if abs(just_target_value) > abs(
                            just_target_value_worst_case):
                        just_target_value_worst_case = just_target_value

                # Check the value was in fact made bigger in the worst case by
                # the new test pattern
                if abs(test_pattern_value_worst_case) > abs(
                        just_target_value_worst_case):
                    num_periods_made_larger_by_test_pattern += 1

        # Check that, in the common case, make the worst-case values worse with
        # the test pattern than a single hot pixel would.
        num_periods = synthesis_output_array.period[
            0] * synthesis_output_array.period[1]
        assert num_periods_made_larger_by_test_pattern > (num_periods / 2.0)
    def test_translation_is_valid(
        self,
        analysis_input_array,
        analysis_transform_coeff_arrays,
        synthesis_output_array,
        synthesis_intermediate_arrays,
    ):
        for (
                level, name
        ), synthesis_target_array in synthesis_intermediate_arrays.items():
            for tx in range(synthesis_target_array.period[0]):
                for ty in range(synthesis_target_array.period[1]):
                    ts = make_synthesis_maximising_pattern(
                        analysis_input_array,
                        analysis_transform_coeff_arrays,
                        synthesis_target_array,
                        synthesis_output_array,
                        tx,
                        ty,
                    )
                    new_tx, new_ty = ts.target
                    mx, my = ts.pattern_translation_multiple
                    tmx, tmy = ts.target_translation_multiple

                    def full_filter(synthesis_expression):
                        """
                        Given a synthesis filter expression in terms of
                        transform coefficients, substitute in the analysis
                        transform to return the filter expression in terms of
                        the input pixels.
                        """
                        return synthesis_expression.subs({
                            ((prefix, l, o), x, y):
                            analysis_transform_coeff_arrays[l][o][x, y]
                            for (prefix, l, o), x, y in get_maximising_inputs(
                                synthesis_expression)
                        })

                    # Check that the translated values really are processed by
                    # an equivalent filter to the offset passed in
                    dx = (new_tx - tx) * mx // tmx
                    dy = (new_ty - ty) * my // tmy
                    full_target_value = full_filter(synthesis_target_array[tx,
                                                                           ty])
                    specified_filter_translated = full_target_value.subs({
                        (prefix, x, y): (prefix, x + dx, y + dy)
                        for prefix, x, y in get_maximising_inputs(
                            full_target_value)
                    })
                    returned_filter = full_filter(
                        synthesis_target_array[new_tx, new_ty])

                    delta = strip_affine_errors(specified_filter_translated -
                                                returned_filter)
                    assert delta == 0

                    # Check that an offset of (tmx, tmy) offsets the input
                    # array by (mx, my)
                    target_filter = full_filter(
                        synthesis_target_array[tx + 2 * tmx, ty + 3 * tmy])
                    full_translated_value = full_filter(
                        synthesis_target_array[tx, ty])
                    translated_filter = full_translated_value.subs({
                        (prefix, x, y): (prefix, x + 2 * mx, y + 3 * my)
                        for prefix, x, y in get_maximising_inputs(
                            full_translated_value)
                    })

                    delta = strip_affine_errors(target_filter -
                                                translated_filter)
                    assert delta == 0
    def test_correctness(
        self,
        wavelet_index,
        filter_params,
        dwt_depth,
        dwt_depth_ho,
        quantisation_matrix,
        analysis_input_linexp_array,
        analysis_coeff_linexp_arrays,
        synthesis_output_linexp_array,
        synthesis_output_pyexp_array,
    ):
        # This test will simply attempt to maximise a real test pattern and verify
        # that the procedure appears to produce an improved result.

        max_quantisation_index = 63

        input_min = -512
        input_max = 511

        # Make an arbitrary choice of target to maximise
        synthesis_target_linexp_array = synthesis_output_linexp_array
        synthesis_target_pyexp_array = synthesis_output_pyexp_array

        # Run against all filter phases as some phases may happen to be maximised
        # by the test pattern anyway
        num_improved_phases = 0
        for tx in range(synthesis_target_linexp_array.period[0]):
            for ty in range(synthesis_target_linexp_array.period[1]):
                # Produce test pattern
                ts = make_synthesis_maximising_pattern(
                    analysis_input_linexp_array,
                    analysis_coeff_linexp_arrays,
                    synthesis_target_linexp_array,
                    synthesis_output_linexp_array,
                    tx,
                    ty,
                )

                new_tx, new_ty = ts.target
                synthesis_pyexp = synthesis_target_pyexp_array[new_tx, new_ty]

                kwargs = {
                    "h_filter_params": filter_params,
                    "v_filter_params": filter_params,
                    "dwt_depth": dwt_depth,
                    "dwt_depth_ho": dwt_depth_ho,
                    "quantisation_matrix": quantisation_matrix,
                    "synthesis_pyexp": synthesis_pyexp,
                    "test_pattern_specification": ts,
                    "input_min": input_min,
                    "input_max": input_max,
                    "max_quantisation_index": max_quantisation_index,
                    "random_state": np.random.RandomState(1),
                    "added_corruptions_per_iteration":
                    (len(ts.pattern) + 19) // 20,  # 5%
                    "removed_corruptions_per_iteration":
                    (len(ts.pattern) + 8) // 5,  # 20%
                    "added_iterations_per_improvement": 50,
                    "terminate_early": None,
                }

                # Run without any greedy searches to get 'baseline' figure
                base_ts = optimise_synthesis_maximising_test_pattern(
                    number_of_searches=1, base_iterations=0, **kwargs)

                # Verify unrelated test pattern parameters passed through
                assert base_ts.target == ts.target
                assert base_ts.pattern_translation_multiple == ts.pattern_translation_multiple
                assert base_ts.target_translation_multiple == ts.target_translation_multiple

                # Run with greedy search to verify better result
                imp_ts = optimise_synthesis_maximising_test_pattern(
                    number_of_searches=3, base_iterations=100, **kwargs)
                assert imp_ts.num_search_iterations >= 3 * 100

                # Ensure new test pattern is normalised to polarities
                assert np.all(np.isin(imp_ts.pattern.polarities, (-1, +1)))

                # Should have improved over the test pattern alone
                if abs(imp_ts.decoded_value) > abs(base_ts.decoded_value):
                    num_improved_phases += 1

                # Check to see if decoded value matches what the pseudocode decoder
                # would produce
                imp_test_pattern_picture, _ = imp_ts.pattern.as_picture_and_slice(
                    input_min, input_max)
                height, width = imp_test_pattern_picture.shape
                imp_test_pattern_picture = imp_test_pattern_picture.tolist()
                kwargs = {
                    "width": width,
                    "height": height,
                    "wavelet_index": wavelet_index,
                    "wavelet_index_ho": wavelet_index,
                    "dwt_depth": dwt_depth,
                    "dwt_depth_ho": dwt_depth_ho,
                }
                actual_decoded_value = decode_with_vc2(
                    quantise_coeffs(
                        encode_with_vc2(imp_test_pattern_picture, **kwargs),
                        imp_ts.quantisation_index,
                        quantisation_matrix,
                    ), **kwargs)[new_ty][new_tx]

                assert imp_ts.decoded_value == actual_decoded_value

        # Consider the procedure to work if some of the phases are improved
        assert num_improved_phases >= 1