Beispiel #1
0
 def test_bitmaps(self):
     """ Bitmap and bitmaptools operations tests
     """
     ctx = beatmup.Context()
     w, h, c = 320, 240, 11
     bitmap = beatmup.bitmaptools.chessboard(ctx, w, h, c)
     ctx2 = beatmup.Context()
     copy = beatmup.bitmaptools.make_copy(bitmap, ctx2,
                                          beatmup.PixelFormat.SINGLE_BYTE)
     assert copy.get_width() == w
     assert copy.get_height() == h
     assert copy.get_pixel_format() == beatmup.PixelFormat.SINGLE_BYTE
     assert copy.get_memory_size() == w * h
Beispiel #2
0
    def test_color_matrix_transform(self):
        """ Color matrix transformation tests
        """
        ctx = beatmup.Context()
        image = beatmup.bitmaptools.chessboard(ctx, 320, 240, 16,
                                               beatmup.PixelFormat.TRIPLE_BYTE)

        matrix = beatmup.filters.ColorMatrix()
        matrix.set_coefficients(0, 0, (1, 0, 0, 0))
        matrix.set_coefficients(1, 0, (0, 0.5, 0, 0))
        matrix.set_coefficients(2, 0, (0, 0, 0.5, 0))
        matrix.input = image
        matrix.output = beatmup.InternalBitmap(ctx,
                                               beatmup.PixelFormat.TRIPLE_BYTE,
                                               image.get_width(),
                                               image.get_height())
        ctx.perform_task(matrix)

        matrix.input = matrix.output
        matrix.output = beatmup.InternalBitmap(ctx,
                                               beatmup.PixelFormat.TRIPLE_BYTE,
                                               image.get_width(),
                                               image.get_height())
        matrix.set_hsv_correction(-90, 1, 1)

        ctx.perform_task(matrix)
        if SAVE_BITMAPS:
            matrix.output.save_bmp("test_colormatrix.bmp")
Beispiel #3
0
    def test_metric(self):
        """ Metric test
        """
        ctx = beatmup.Context()
        metric = beatmup.Metric()
        import numpy

        # generate random inputs
        array1 = numpy.random.uniform(-0.5, 0.5,
                                      (256, 256, 3)).astype(numpy.float32)
        array2 = numpy.random.uniform(-0.5, 0.5,
                                      (256, 256, 3)).astype(numpy.float32)
        diff = (array1 - array2).reshape(-1)
        img1 = beatmup.Bitmap(ctx, array1)
        img2 = beatmup.Bitmap(ctx, array2)
        metric.set_bitmaps(img1, img2)

        # check L1
        metric.set_norm(metric.Norm.L1)
        ctx.perform_task(metric)
        self.assertAlmostEqual(numpy.linalg.norm(diff, 1), metric.get_result(),
                               1)

        # check L2
        metric.set_norm(metric.Norm.L2)
        ctx.perform_task(metric)
        self.assertAlmostEqual(numpy.linalg.norm(diff, 2), metric.get_result(),
                               2)

        # check PSNR
        psnr = 10 * numpy.log10(1 / numpy.mean(diff**2))
        self.assertAlmostEqual(beatmup.Metric.psnr(img1, img2), psnr, 5)
Beispiel #4
0
    def test_floodfill(self):
        """ FloodFill test
        """
        ctx = beatmup.Context()

        # make bitmaps
        cell_step = 16
        input = beatmup.bitmaptools.chessboard(ctx, 320, 240, cell_step)
        output = beatmup.InternalBitmap(ctx, beatmup.PixelFormat.BINARY_MASK,
                                        320, 240)
        output.zero()

        # make a FloodFill instance
        ff = beatmup.FloodFill()
        ff.set_compute_contours(True)
        ff.tolerance = 0.01
        ff.input = input
        ff.output = output
        dilation = 3
        ff.set_border_postprocessing(beatmup.FloodFill.BorderMorphology.DILATE,
                                     dilation, 0)

        # plant 3 seeds, one in a middle square
        ff.set_seeds([(12, 34), (123, 234), (155, 118)])

        # run
        ctx.perform_task(ff)

        # check: 3 components expected, all of the same length
        self.assertEqual(ff.get_contour_count(), 3)
        for i in range(3):
            self.assertEqual(ff.get_contour(i).get_length(), cell_step * 4 - 1)

        if SAVE_BITMAPS:
            output.save_bmp("test_floodfill.bmp")
Beispiel #5
0
    def test_multiply_adds_and_texel_fetches(self):
        """ Tests multiply-adds and texel fetches counting
        """
        # generate input image
        input_image = make_random_image((32, 32))

        # set up a test model
        model = tf.keras.models.Sequential([
            tf.keras.layers.Input((32, 32, 3)),
            tf.keras.layers.Conv2D(16, kernel_size=3, use_bias=False),
            tf.keras.layers.Activation(beatmup_keras.brelu6),
            tf.keras.layers.Conv2D(32, kernel_size=3, groups=4, use_bias=False),
            tf.keras.layers.Activation(beatmup_keras.brelu6),
            tf.keras.layers.MaxPooling2D(3, strides=1)
        ])

        # prepare model
        ctx = beatmup.Context()
        test_model, test_data = beatmup_keras.export_model(model, ctx)
        inference = beatmup.nnets.InferenceTask(test_model, test_data)
        inference.connect(beatmup.Bitmap(ctx, input_image), test_model.get_first_operation())
        test_model.add_output(test_model.get_last_operation())
        ctx.perform_task(inference)

        # check
        self.assertEqual(test_model.count_multiply_adds(), 30*30*16*3*3*3 + 28*28*32*3*3*4 + 0)
        self.assertEqual(test_model.count_texel_fetches(), 30*30*16*3*3//4 + 28*28*32*3*3//4 + 26*26*32*3*3//4)
Beispiel #6
0
def replay_tests():
    """ Reads a file with test data and replays the tests
    """
    # open file
    datafile = beatmup.ChunkFile(test_export_filename)
    datafile.open()

    # prepare things
    ctx = beatmup.Context()

    # loop till tests
    i = 1
    prefix = lambda i: 'test' + str(i)
    while datafile.chunk_exists(prefix(i)):
        # get data
        datafile.open()
        test_title = bytes.decode(datafile[prefix(i)])
        model_code = bytes.decode(datafile[prefix(i) + ':model'])
        input_image_shape = numpy.frombuffer(datafile[prefix(i) + ':input_shape'], dtype=numpy.int32)
        input_image = numpy.frombuffer(datafile[prefix(i) + ':input'], dtype=numpy.uint8).reshape(input_image_shape)
        ref_output = numpy.frombuffer(datafile[prefix(i) + ':gt'], dtype=numpy.float32)
        threshold, = struct.unpack('f', datafile[prefix(i) + ':threshold'])
        datafile.close()
        print('#%02d: %s' % (i, test_title))

        # restore model
        model = beatmup.nnets.DeserializedModel(ctx, model_code)

        # build inference task
        inference = beatmup.nnets.InferenceTask(model, datafile)

        # connect input
        inference.connect(beatmup.Bitmap(ctx, input_image), model.get_first_operation())

        # connect output if not softmax; softmax has no outputs
        head = model.get_last_operation()
        softmax = 'softmax' in head.name
        if not softmax:
            model.add_output(head)

        # run inference
        ctx.perform_task(inference)

        # get data
        test_output = numpy.asarray(head.get_probabilities()) if softmax else model.get_output_data(head)

        # get ground truth data
        ref_output = ref_output.reshape(test_output.shape)

        # check error and print things
        error = numpy.max(numpy.abs(test_output - ref_output))
        print('  Error: %0.4f for mapping %s to %s' % (error, input_image.shape, test_output.shape))
        assert error < threshold

        i += 1
Beispiel #7
0
    def test_image_sampler(self):
        """ ImageSampler test
        """
        if VERBOSE: print('---- ImageSampler test...')

        # generate input image
        input_image = make_random_image((48, 32))
        center_crop = input_image[8:-8,:,:]

        # set up a test model
        ref_model = tf.keras.models.Sequential([
            tf.keras.layers.Input(input_image.shape),
            tf.keras.layers.Conv2D(8, 1,
                name='conv',
                strides=1,
                kernel_initializer='random_normal',
                bias_initializer='random_normal',
                use_bias=False),
            tf.keras.layers.Activation(beatmup_keras.brelu1)
        ])

        # get "test id" to add as prefix to model layers
        global model_ctr
        test_id = 'test' + str(model_ctr)

        # convert model
        ctx = beatmup.Context()
        model, model_data = beatmup_keras.export_model(ref_model, ctx, prefix=test_id + '__')

        # run inference on a cropped input
        inference = beatmup.nnets.InferenceTask(model, model_data)
        inference.connect(beatmup.Bitmap(ctx, center_crop), model.get_first_operation())
        model.add_output(model.get_last_operation())
        ctx.perform_task(inference)
        ref_output = model.get_output_data(model.get_last_operation())

        # add a preprocessing layer
        model, model_data = beatmup_keras.export_model(ref_model, ctx, prefix=test_id + '__')
        image_sampler = beatmup.nnets.ImageSampler(test_id + "__preprocessing", center_crop.shape[:2])
        first_op = model.get_first_operation()
        model.add_operation(first_op.name, image_sampler)
        model.add_connection(image_sampler.name, first_op.name)

        # run on full input
        inference = beatmup.nnets.InferenceTask(model, model_data)
        inference.connect(beatmup.Bitmap(ctx, input_image), model.get_first_operation())
        model.add_output(model.get_last_operation())
        ctx.perform_task(inference)
        test_output = model.get_output_data(model.get_last_operation())

        self.assertTrue(numpy.all(ref_output == test_output))

        # export the test
        export_test(test_id, self.test_image_sampler.__doc__, model_data, model, input_image, ref_output, 0.004)
Beispiel #8
0
 def test_x2(self):
     """ x2 neural resampler test
     """
     ctx = beatmup.Context()
     resampler = beatmup.BitmapResampler(ctx)
     resampler.mode = beatmup.BitmapResampler.CONVNET
     input = beatmup.InternalBitmap(ctx, "../../images/fecamp.bmp")
     output = beatmup.InternalBitmap(ctx, beatmup.PixelFormat.TRIPLE_BYTE,
                                     input.get_width() * 2,
                                     input.get_height() * 2)
     resampler.input = input
     resampler.output = output
     ctx.submit_task(resampler)
     ctx.wait()
     if SAVE_BITMAPS:
         output.save_bmp("test_x2.bmp")
Beispiel #9
0
    def test_sepia(self):
        """ Sepia filter test
        """
        ctx = beatmup.Context()
        image = beatmup.bitmaptools.chessboard(ctx, 320, 240, 16,
                                               beatmup.PixelFormat.TRIPLE_BYTE)

        sepia = beatmup.filters.Sepia()
        sepia.input = image
        sepia.output = beatmup.InternalBitmap(ctx,
                                              beatmup.PixelFormat.TRIPLE_BYTE,
                                              image.get_width(),
                                              image.get_height())
        ctx.perform_task(sepia)
        if SAVE_BITMAPS:
            sepia.output.save_bmp("test_sepia.bmp")
Beispiel #10
0
    def test_context(self):
        """ Beatmup.Context API tests
        """
        ctx = beatmup.Context()

        assert not ctx.busy()
        assert not ctx.is_gpu_queried()
        assert not ctx.is_gpu_ready()

        ctx.warm_up_gpu()
        vendor, renderer = ctx.query_gpu_info()

        assert ctx.is_gpu_ready()
        assert ctx.is_gpu_queried()

        count = 3
        ctx.limit_worker_count(count)
        assert ctx.max_allowed_worker_count() == count
Beispiel #11
0
    def test_rotation(self):
        """ ImageSampler rotation test
        """
        if VERBOSE: print('---- ImageSampler rotation test...')

        # generate input image
        input_image = make_random_image((48, 32))
        center_crop = input_image[8:-8,:,:]

        # set up a test model
        ref_model = tf.keras.models.Sequential([
            tf.keras.layers.Input(input_image.shape),
            tf.keras.layers.Conv2D(4, 1,
                name='conv',
                strides=1,
                kernel_initializer='random_normal',
                bias_initializer='random_normal',
                use_bias=False),
            tf.keras.layers.Activation(beatmup_keras.brelu1)
        ])

        # convert model
        ctx = beatmup.Context()
        model, model_data = beatmup_keras.export_model(ref_model, ctx)

        # add a preprocessing layer
        image_sampler = beatmup.nnets.ImageSampler('sampler', center_crop.shape[:2])
        first_op = model.get_first_operation()
        model.add_operation(first_op.name, image_sampler)
        model.add_connection(image_sampler.name, first_op.name)

        # run inference on a cropped input
        inference = beatmup.nnets.InferenceTask(model, model_data)
        inference.connect(beatmup.Bitmap(ctx, center_crop), model.get_first_operation())
        model.add_output(model.get_last_operation())
        ctx.perform_task(inference)
        ref_output = model.get_output_data(model.get_last_operation())

        # rotate and test
        for i in range(4):
            image_sampler.rotation = i
            ctx.perform_task(inference)
            test_output = model.get_output_data(model.get_last_operation())
            self.assertTrue(numpy.all(ref_output == numpy.rot90(test_output, i)))
Beispiel #12
0
        def wrapped(self, *args, **kwargs):
            input_image, ref_model = func(self, *args, **kwargs)

            # get "test id" to add as prefix to model layers
            global model_ctr
            test_id = 'test' + str(model_ctr)

            # compute reference output
            ref_model.compile()
            ref_output = ref_model.predict(make_tensorflow_batch(input_image))[0]

            # convert model
            ctx = beatmup.Context()
            test_model, model_data = beatmup_keras.export_model(ref_model, ctx, prefix=test_id + '__')

            # init inference task
            inference = beatmup.nnets.InferenceTask(test_model, model_data)

            # connect input
            inference.connect(beatmup.Bitmap(ctx, input_image), test_model.get_first_operation())

            # connect output if not softmax; softmax has no outputs
            head = test_model.get_last_operation()
            softmax = 'softmax' in head.name
            if not softmax:
                test_model.add_output(head)

            # run inference
            ctx.perform_task(inference)

            # get data
            test_output = numpy.asarray(head.get_probabilities()) if softmax else test_model.get_output_data(head)

            # compare
            error = numpy.max(numpy.abs(test_output - ref_output))
            if VERBOSE: print('Error: %0.4f for %d layers mapping %s to %s' % (error, len(ref_model.layers), input_image.shape, test_output.shape))
            del ctx

            # export
            export_test(test_id, func.__doc__, model_data, test_model, input_image, ref_output, error_threshold)

            # assert
            self.assertLess(error, error_threshold)
Beispiel #13
0
    def test_shader_applicator(self):
        """ ShaderApplicator test
        """
        ctx = beatmup.Context()
        applicator = beatmup.ShaderApplicator()
        applicator.output_bitmap = beatmup.InternalBitmap(
            ctx, beatmup.PixelFormat.TRIPLE_BYTE, 640, 480)
        applicator.add_sampler(
            beatmup.bitmaptools.chessboard(ctx, 320, 240, 32,
                                           beatmup.PixelFormat.TRIPLE_BYTE))
        self.assertFalse(applicator.remove_sampler("testSampler"))
        applicator.add_sampler(
            beatmup.bitmaptools.chessboard(ctx, 100, 100, 1,
                                           beatmup.PixelFormat.SINGLE_BYTE),
            "testSampler")
        self.assertTrue(applicator.remove_sampler("testSampler"))
        applicator.add_sampler(
            beatmup.bitmaptools.chessboard(ctx, 100, 100, 1,
                                           beatmup.PixelFormat.SINGLE_BYTE),
            "testSampler")

        applicator.shader = beatmup.ImageShader(ctx)
        applicator.shader.set_source_code(beatmup.ImageShader.CODE_HEADER + """
            uniform sampler2D testSampler;
            highp vec2 distort(highp vec2 xy) {
                highp vec2 r = xy - vec2(0.5, 0.5);
                highp float t = length(r);
                return (-0.5 * t * t + 0.9) * r + vec2(0.5, 0.5);
            }
            void main() {
                gl_FragColor = texture2D(image, distort(texCoord)) * texture2D(testSampler, texCoord).r;
            }
            """)

        ctx.perform_task(applicator)

        if SAVE_BITMAPS:
            applicator.output_bitmap.save_bmp("test_shader_applicator.bmp")
Beispiel #14
0
# get input and output filenames
input_filename = args.input[0]
output_filename = "%s_x2%s" % os.path.splitext(
    input_filename) if args.output is None else args.output

# try to read the input image
image = None
try:
    image = cv2.imread(input_filename)
except:
    print(f"Cannot read or decode {input_filename}")
    exit(-1)

# initialize context
ctx = beatmup.Context()

# crete a resampler
resampler = beatmup.BitmapResampler(ctx)

# choose resampling algorithm
resampler.mode = beatmup.BitmapResampler.CONVNET

# get the input shape
h, w, c = image.shape
assert c == 3, "A three-channel input image is expected"

# set up output bitmap
output = beatmup.Bitmap(ctx, numpy.zeros((2 * h, 2 * w, c), numpy.uint8))

# feed the resampler with input and output
Beispiel #15
0
    def test_serialization(self):
        """ Tests model serialization and reconstruction
        """
        if VERBOSE: print('---- Serialization...')

        # generate input image
        input_image = make_random_image((32, 32))

        # set up a test model
        input = tf.keras.layers.Input(input_image.shape)
        x = tf.keras.layers.Conv2D(32, 3,
                    name='conv_1',
                    strides=2,
                    kernel_initializer='random_normal',
                    bias_initializer='random_normal',
                    use_bias=False)(input)
        x = residual = tf.keras.layers.Activation(beatmup_keras.brelu6)(x)
        x = tf.keras.layers.DepthwiseConv2D(3,
                    name='depthwise_conv_2',
                    strides=1,
                    kernel_initializer='random_normal',
                    bias_initializer='random_normal',
                    padding='same',
                    use_bias=True)(x)
        x = tf.keras.layers.Activation(beatmup_keras.brelu6)(x)
        x = tf.keras.layers.Conv2D(32, 1,
                    name='pointwise_conv_2',
                    strides=1,
                    kernel_initializer='random_normal',
                    bias_initializer='random_normal',
                    use_bias=True)(x)
        x = tf.keras.layers.Add(name="add_residual_1")([x, residual])
        x = tf.keras.layers.Activation(beatmup_keras.brelu1)(x)

        x = tf.keras.layers.MaxPooling2D(2)(x)
        x = tf.keras.layers.Conv2D(64, 1,
                    name='pointwise_conv',
                    kernel_initializer='random_normal',
                    bias_initializer='random_normal',
                    use_bias=True)(x)
        x = tf.keras.layers.ReLU(max_value=2.0)(x)
        x = residual = beatmup_keras.Shuffle(2)(x)

        x = tf.keras.layers.DepthwiseConv2D(3,
                    name='depthwise_conv_3',
                    strides=1,
                    kernel_initializer='random_normal',
                    bias_initializer='random_normal',
                    padding='same',
                    use_bias=True)(x)
        x = tf.keras.layers.Activation(beatmup_keras.brelu6)(x)
        x = tf.keras.layers.Conv2D(64, 1,
                    name='pointwise_conv_3',
                    strides=1,
                    kernel_initializer='random_normal',
                    bias_initializer='random_normal',
                    use_bias=True)(x)
        x = tf.keras.layers.Add(name="add_residual_2")([x, residual])
        x = tf.keras.layers.ReLU(max_value=2.0)(x)

        x = tf.keras.layers.GlobalAveragePooling2D()(x)
        x = tf.keras.layers.Dense(40)(x)
        x = tf.keras.layers.Softmax()(x)

        # make a model
        ref_model = tf.keras.models.Model(inputs=input, outputs=x)
        ref_model.compile()
        ref_output = ref_model.predict(make_tensorflow_batch(input_image))[0]

        # convert model
        ctx = beatmup.Context()
        model, model_data = beatmup_keras.export_model(ref_model, ctx)

        # run inference
        inference = beatmup.nnets.InferenceTask(model, model_data)
        inference.connect(beatmup.Bitmap(ctx, input_image), model.get_first_operation())
        ctx.perform_task(inference)
        output = model.get_last_operation().get_probabilities()

        # print stuff
        error = numpy.max(numpy.abs(output- ref_output))
        if VERBOSE: print("Error: %0.4f for %d layers mapping %s to %s" % (error, len(ref_model.layers), input_image.shape, len(output)))

        # serialize
        serial = model.serialize()

        # reconstruct
        reconstructed_model = beatmup.nnets.DeserializedModel(ctx, serial)

        # run inference of the reconstructed model
        inference_rec = beatmup.nnets.InferenceTask(reconstructed_model, model_data)
        inference_rec.connect(beatmup.Bitmap(ctx, input_image), reconstructed_model.get_first_operation())
        ctx.perform_task(inference_rec)
        output_rec = model.get_last_operation().get_probabilities()

        # compare
        self.assertEqual(output, output_rec)
Beispiel #16
0
    def test_rendering(self):
        """ SceneRenderer test
        """
        ctx = beatmup.Context()

        # make bitmaps
        input = beatmup.bitmaptools.chessboard(ctx, 1024, 768, 32,
                                               beatmup.PixelFormat.TRIPLE_BYTE)
        output = beatmup.InternalBitmap(ctx, beatmup.PixelFormat.TRIPLE_BYTE,
                                        1024, 1024)

        # create shaders
        distortion = beatmup.ImageShader(ctx)
        distortion.set_source_code(distortion.CODE_HEADER + """
            highp vec2 distort(highp vec2 xy) {
                highp vec2 r = xy - vec2(0.5, 0.5);
                highp float t = length(r);
                return (-0.5 * t * t + 0.9) * r + vec2(0.5, 0.5);
            }
            void main() {
                gl_FragColor = texture2D(image, distort(texCoord));
            }
            """)

        gray_shift = beatmup.ImageShader(ctx)
        gray_shift.set_source_code(gray_shift.CODE_HEADER + """
            highp float gray(highp vec2 pos) {
                highp vec4 clr = texture2D(image, pos);
                return 0.333 * (clr.r + clr.g + clr.b);
            }
            void main() {
                gl_FragColor = vec4(
                    gray(texCoord + vec2(0.01, 0.01)),
                    gray(texCoord),
                    gray(texCoord - vec2(0.01, 0.01)),
                    1.0
                );
            }
            """)

        # create a scene
        scene = beatmup.Scene()
        layer = scene.new_shaped_bitmap_layer()
        layer.bitmap = input
        layer.mapping.scale(0.48)
        layer.mapping.rotate_degrees(1)
        layer.mapping.set_center_position((0.25, 0.75))
        layer.corner_radius = 0.05
        layer.slope_width = 0.01
        layer.in_pixels = False

        layer = scene.new_shaded_bitmap_layer()
        layer.bitmap = input
        layer.mapping.scale(0.48)
        layer.mapping.rotate_degrees(-1)
        layer.mapping.set_center_position((0.75, 0.25))
        layer.shader = distortion

        layer = scene.new_shaded_bitmap_layer()
        layer.bitmap = input
        layer.mapping.scale(0.48)
        layer.mapping.rotate_degrees(-2)
        layer.mapping.set_center_position((0.75, 0.75))
        layer.shader = gray_shift

        subscene = beatmup.Scene()
        scene_layer = scene.add_scene(subscene)
        scene_layer.mapping.scale(0.45)
        scene_layer.mapping.rotate_degrees(-3)
        scene_layer.mapping.set_center_position((0.25, 0.25))
        layer = subscene.new_masked_bitmap_layer()
        layer.bitmap = input
        layer.modulation_color = (255, 0, 0, 255)
        layer.mask = beatmup.bitmaptools.chessboard(ctx, 320, 240, 32)

        layer = subscene.new_masked_bitmap_layer()
        layer.bitmap = input
        layer.modulation_color = (255, 255, 0, 128)
        layer.mask = beatmup.bitmaptools.chessboard(ctx, 320, 240, 32)
        beatmup.bitmaptools.invert(layer.mask, layer.mask)

        # setup renderer
        renderer = beatmup.SceneRenderer()
        renderer.scene = scene
        renderer.output = output
        renderer.output_pixels_fetching = True
        renderer.background_image = beatmup.bitmaptools.chessboard(
            ctx, 16, 16, 8, beatmup.PixelFormat.TRIPLE_BYTE)

        ctx.perform_task(renderer)
        ctx.perform_task(renderer)
        if SAVE_BITMAPS:
            output.save_bmp("test_rendering.bmp")
Beispiel #17
0
    def test_multitask(self):
        """ Multitask test
        """
        ctx = beatmup.Context()
        applicator = beatmup.ShaderApplicator()
        applicator.output_bitmap = beatmup.InternalBitmap(
            ctx, beatmup.PixelFormat.TRIPLE_BYTE, 640, 480)
        applicator.add_sampler(
            beatmup.bitmaptools.chessboard(ctx, 320, 240, 32,
                                           beatmup.PixelFormat.TRIPLE_BYTE))

        applicator.shader = beatmup.ImageShader(ctx)
        applicator.shader.set_source_code(beatmup.ImageShader.CODE_HEADER + """
            uniform highp float factor;
            highp vec2 distort(highp vec2 xy) {
                highp vec2 r = xy - vec2(0.5, 0.5);
                highp float t = length(r);
                return (-factor * t * t + 0.9) * r + vec2(0.5, 0.5);
            }
            void main() {
                gl_FragColor = texture2D(image, distort(texCoord));
            }
            """)
        applicator.shader.set_float('factor', 0.9)

        sepia = beatmup.filters.Sepia()
        sepia.input = applicator.output_bitmap
        sepia.output = beatmup.InternalBitmap(ctx,
                                              beatmup.PixelFormat.TRIPLE_BYTE,
                                              sepia.input.get_width(),
                                              sepia.input.get_height())

        # create a scene
        scene = beatmup.Scene()
        layer = scene.new_bitmap_layer()
        layer.bitmap = sepia.output
        layer.mapping.scale(0.95)
        layer.mapping.rotate_degrees(1)
        layer.mapping.set_center_position((0.5, 0.5))

        # setup renderer
        renderer = beatmup.SceneRenderer()
        renderer.scene = scene
        renderer.output = beatmup.InternalBitmap(
            ctx, beatmup.PixelFormat.TRIPLE_BYTE, 640, 480)
        renderer.output_pixels_fetching = True
        renderer.background_image = beatmup.bitmaptools.chessboard(
            ctx, 16, 16, 8, beatmup.PixelFormat.TRIPLE_BYTE)

        # construct multitask and test its API
        multitask = beatmup.Multitask()
        holder = multitask.add_task(renderer)
        self.assertEqual(multitask.get_task_count(), 1)
        holder = multitask.insert_task(applicator, holder)
        self.assertEqual(multitask.get_task_count(), 2)
        self.assertEqual(multitask.remove_task(holder), True)
        self.assertEqual(multitask.remove_task(holder), False)
        multitask.insert_task(sepia, multitask.get_task(0))
        self.assertEqual(multitask.get_task_count(), 2)
        distort_holder = multitask.insert_task(applicator,
                                               multitask.get_task(0))
        self.assertEqual(multitask.get_task_index(multitask.get_task(2)), 2)
        multitask.measure()

        # run multitask twice for different parameters: expecting different results
        ctx.perform_task(multitask)
        ref_output = renderer.output
        renderer.output = beatmup.InternalBitmap(
            ctx, beatmup.PixelFormat.TRIPLE_BYTE, 640, 480)
        applicator.shader.set_float('factor', 0.5)
        multitask.set_repetition_policy(
            distort_holder, beatmup.Multitask.RepetitionPolicy.REPEAT_UPDATE)
        ctx.perform_task(multitask)
        self.assertLess(beatmup.Metric.psnr(renderer.output, ref_output), 40)

        # run again after setting REPEAT_UPDATE: expecting same result
        ref_output = renderer.output
        renderer.output = beatmup.InternalBitmap(
            ctx, beatmup.PixelFormat.TRIPLE_BYTE, 640, 480)
        applicator.shader.set_float('factor', 0.9)
        ctx.perform_task(multitask)
        self.assertTrue(beatmup.Metric.psnr(renderer.output, ref_output) > 40)

        # run again after reseting the policy: expecting different results
        multitask.set_repetition_policy(
            distort_holder, beatmup.Multitask.RepetitionPolicy.REPEAT_UPDATE)
        ctx.perform_task(multitask)
        self.assertTrue(beatmup.Metric.psnr(renderer.output, ref_output) < 40)

        if SAVE_BITMAPS:
            renderer.output.save_bmp("test_multitask.bmp")