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
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")
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)
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")
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)
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
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)
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")
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")
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
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)))
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)
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")
# 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
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)
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")
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")