示例#1
0
def test_recursive_stage_intensity(stage_factory: StageFactoryBase):
    root, g1, g2, s1, s2, s3, s4, s5, s6 = create_recursive_stages(
        stage_factory)

    from ceed.function.plugin import LinearFunc
    for i, stage in enumerate((s1, s2)):
        stage.stage.add_func(LinearFunc(
            function_factory=stage_factory.function_factory, b=0, m=.1,
            duration=(i + 1) * 5))

    shape = CircleShapeP1(
        app=None, painter=stage_factory.shape_factory, show_in_gui=False,
        create_add_shape=True)

    shape2 = CircleShapeP1Internal(
        app=None, painter=stage_factory.shape_factory, show_in_gui=False,
        create_add_shape=True)
    s1.stage.add_shape(shape.shape)
    s2.stage.add_shape(shape2.shape)

    values, n = get_stage_time_intensity(stage_factory, root.name, 10)
    assert n == 10 * 10
    assert len(values) == 2
    colors = values[shape.name]
    colors2 = values[shape2.name]
    assert len(colors) == n

    for i, (r, g, b, a) in enumerate(colors):
        if i < 5 * 10:
            assert math.isclose(r, i / 10 * .1) if s2.color_r else r == 0
            assert math.isclose(g, i / 10 * .1) if s2.color_g else g == 0
            assert math.isclose(b, i / 10 * .1) if s2.color_b else b == 0
        else:
            assert r == 0
            assert g == 0
            assert b == 0

    for i, (r, g, b, a) in enumerate(colors2):
        assert math.isclose(r, i / 10 * .1) if s2.color_r else r == 0
        assert math.isclose(g, i / 10 * .1) if s2.color_g else g == 0
        assert math.isclose(b, i / 10 * .1) if s2.color_b else b == 0
示例#2
0
def test_register_funcs():
    from ceed.function.plugin import ConstFunc, LinearFunc
    function_factory = FunctionFactoryBase()
    count = 0

    def count_changes(*largs):
        nonlocal count
        count += 1

    function_factory.fbind('on_changed', count_changes)

    assert not function_factory.funcs_cls
    assert not function_factory.funcs_user
    assert not function_factory.funcs_inst
    assert not function_factory.funcs_inst_default
    assert not function_factory.get_classes()
    assert not function_factory.get_names()

    function_factory.register(ConstFunc)
    assert count
    assert function_factory.funcs_cls['ConstFunc'] is ConstFunc
    assert isinstance(function_factory.funcs_inst['Const'], ConstFunc)
    assert isinstance(function_factory.funcs_inst_default['Const'], ConstFunc)
    assert ConstFunc in function_factory.get_classes()
    assert 'ConstFunc' in function_factory.get_names()

    f = LinearFunc(function_factory=function_factory)
    count = 0
    function_factory.register(LinearFunc, instance=f)
    assert count
    assert function_factory.funcs_cls['LinearFunc'] is LinearFunc
    assert function_factory.funcs_inst['Linear'] is f
    assert function_factory.funcs_inst_default['Linear'] is f
    assert LinearFunc in function_factory.get_classes()
    assert 'LinearFunc' in function_factory.get_names()
    assert not function_factory.funcs_user
示例#3
0
def test_simple_stage_intensity(stage_factory: StageFactoryBase):
    from ceed.function.plugin import LinearFunc
    shape = EllipseShapeP1(
        app=None, painter=stage_factory.shape_factory, show_in_gui=False,
        create_add_shape=True)

    shape2 = EllipseShapeP2(
        app=None, painter=stage_factory.shape_factory, show_in_gui=False,
        create_add_shape=True)

    f: LinearFunc = LinearFunc(
        function_factory=stage_factory.function_factory, b=0, m=.1, duration=5)

    stage = make_stage(
        stage_factory, color_r=True, color_g=False, color_b=True)
    stage_factory.add_stage(stage)
    stage.add_func(f)
    stage.add_shape(shape.shape)
    stage.add_shape(shape2.shape)

    values, n = get_stage_time_intensity(stage_factory, stage.name, 10)
    assert n == 5 * 10
    assert len(values) == 2
    colors = values[shape.name]
    colors2 = values[shape2.name]
    assert len(colors) == 10 * 5

    for i, (r, g, b, a) in enumerate(colors):
        assert math.isclose(r, i / 10 * .1)
        assert math.isclose(b, i / 10 * .1)
        assert g == 0

    for i, (r, g, b, a) in enumerate(colors2):
        assert math.isclose(r, i / 10 * .1)
        assert math.isclose(b, i / 10 * .1)
        assert g == 0
示例#4
0
async def run_data_experiment(stage_app: CeedTestApp):
    from ..test_stages import create_2_shape_stage
    from ceed.function.plugin import LinearFunc

    root, s1, s2, shape1, shape2 = create_2_shape_stage(
        stage_app.app.stage_factory, show_in_gui=True, app=stage_app)
    await stage_app.wait_clock_frames(2)

    # 30 frames
    f1 = LinearFunc(function_factory=stage_app.app.function_factory,
                    duration=.25,
                    loop=8,
                    m=2.4)
    f2 = LinearFunc(function_factory=stage_app.app.function_factory,
                    duration=.25,
                    loop=8,
                    m=2.4)
    s1.stage.add_func(f1)
    s2.stage.add_func(f2)
    await stage_app.wait_clock_frames(2)

    stage_app.app.view_controller.frame_rate = 120
    # count frames
    stage_app.app.view_controller.use_software_frame_rate = False
    stage_app.app.view_controller.pad_to_stage_handshake = True

    for image, (b1, b2) in zip(stored_images, stored_b_values):
        f1.b = b1
        f2.b = b2

        # set background image
        stage_app.app.central_display.update_img(image)
        stage_app.app.player.last_image = image
        await stage_app.wait_clock_frames(2)

        stage_app.app.view_controller.request_stage_start(root.name)
        await wait_experiment_done(stage_app, timeout=10 * 60)

    for i, image in enumerate(stored_images[2:]):
        stage_app.app.ceed_data.add_image_to_file(image, f'image {i}')
        await stage_app.wait_clock_frames(2)
示例#5
0
文件: test_stage.py 项目: matham/ceed
async def test_recursive_play_stage_intensity(stage_app: CeedTestApp, tmp_path,
                                              flip, skip, video_mode):
    """Checks that proper frame rendering happens in all these modes.
    In skip mode, some frames are skipped if GPU/CPU is too slow.
    """
    from ..test_stages import create_recursive_stages
    from .examples.shapes import CircleShapeP1, CircleShapeP2
    from kivy.clock import Clock
    from ceed.analysis import CeedDataReader

    root, g1, g2, s1, s2, s3, s4, s5, s6 = create_recursive_stages(
        stage_app.app.stage_factory, app=stage_app)

    from ceed.function.plugin import LinearFunc
    for i, stage in enumerate((s1, s2, s3, s4, s5, s6)):
        stage.stage.add_func(
            LinearFunc(function_factory=stage_app.app.function_factory,
                       b=0,
                       m=.5,
                       duration=(i % 2 + 1) * 1))

    shape = CircleShapeP1(app=None,
                          painter=stage_app.app.shape_factory,
                          show_in_gui=True)

    shape2 = CircleShapeP2(app=None,
                           painter=stage_app.app.shape_factory,
                           show_in_gui=True)
    s1.stage.add_shape(shape.shape)
    s4.stage.add_shape(shape.shape)
    s5.stage.add_shape(shape.shape)
    s2.stage.add_shape(shape2.shape)
    s3.stage.add_shape(shape2.shape)
    s6.stage.add_shape(shape2.shape)

    root.show_in_gui()
    await stage_app.wait_clock_frames(2)

    frame = 0
    event = None
    # make GPU too slow to force skipping frames, when enabled
    fps = await measure_fps(stage_app) + 10
    rate = stage_app.app.view_controller.frame_rate = fps
    stage_app.app.view_controller.use_software_frame_rate = False
    stage_app.app.view_controller.flip_projector = flip
    stage_app.app.view_controller.skip_estimated_missed_frames = skip
    stage_app.app.view_controller.video_mode = video_mode
    stage_app.app.view_controller.pad_to_stage_handshake = False

    n_sub_frames = 1
    if video_mode == 'QUAD4X':
        n_sub_frames = 4
    elif video_mode == 'QUAD12X':
        n_sub_frames = 12

    centers = shape.center, shape2.center
    num_frames = rate * n_sub_frames * (2 + 1 + 2 + 1)
    shape_color = [
        (False, False, False, 0.),
    ] * num_frames
    shape2_color = [
        (False, False, False, 0.),
    ] * num_frames
    skipped_frame_indices = set()
    n_missed_frames = 0

    for s, start, e in [(s1, 0, 1), (s4, 3, 5), (s5, 5, 6)]:
        for i in range(start * rate * n_sub_frames, e * rate * n_sub_frames):
            val = (i - start * rate * n_sub_frames) / (rate *
                                                       n_sub_frames) * .5
            shape_color[i] = s.color_r, s.color_g, s.color_b, val

    for s, start, e in [(s2, 0, 2), (s3, 2, 3), (s6, 5, 6)]:
        for i in range(start * rate * n_sub_frames, e * rate * n_sub_frames):
            val = (i - start * rate * n_sub_frames) / (rate *
                                                       n_sub_frames) * .5
            shape2_color[i] = s.color_r, s.color_g, s.color_b, val

    def verify_intensity(*largs):
        nonlocal frame, n_missed_frames
        # total frames is a multiple of n_sub_frames
        if not stage_app.app.view_controller.stage_active:
            assert stage_app.app.view_controller.count - 1 == num_frames
            if skip:
                # last frame could be passed actual frames
                assert frame - n_missed_frames * n_sub_frames <= num_frames
            else:
                assert frame == num_frames
            event.cancel()
            return
        # not yet started
        if not stage_app.app.view_controller.count:
            return

        # some frame may have been skipped, but num_frames is max frames
        # This callback happens after frame callback and after the frame flip.
        # This also means we record even the last skipped frames (if skipped)
        assert frame < num_frames

        frame = verify_color(stage_app, shape_color, shape2_color, frame,
                             centers, flip, video_mode)
        assert stage_app.app.view_controller.count == frame

        if skip:
            # some frames may have been dropped for next frame
            n_missed_frames = stage_app.app.view_controller._n_missed_frames
            for k in range(n_missed_frames * n_sub_frames):
                # frame is next frame index, next frame is skipped
                skipped_frame_indices.add(frame)
                frame += 1
        else:
            assert not stage_app.app.view_controller._n_missed_frames

    event = Clock.create_trigger(verify_intensity, timeout=0, interval=True)
    event()
    stage_app.app.view_controller.request_stage_start(root.name)

    await wait_experiment_done(stage_app, timeout=num_frames / rate * 50)

    filename = str(tmp_path / 'recursive_play_stage_intensity.h5')
    stage_app.app.ceed_data.save(filename=filename)

    f = CeedDataReader(filename)
    f.open_h5()
    assert f.experiments_in_file == ['0']
    assert not f.num_images_in_file
    f.load_experiment(0)

    shape_data = f.shapes_intensity[shape.name]
    shape_data_rendered = f.shapes_intensity_rendered[shape.name]
    shape2_data = f.shapes_intensity[shape2.name]
    shape2_data_rendered = f.shapes_intensity_rendered[shape2.name]
    recorded_rendered_frames = f.rendered_frames

    # even when skipping, skipped frames are still logged but they are removed
    # in xxx_rendered arrays
    if skip:
        # because frame rate is high, we'll definitely drop frames
        assert skipped_frame_indices
    else:
        assert not skipped_frame_indices

    assert shape_data.shape[0] == num_frames
    assert shape2_data.shape[0] == num_frames

    n_skipped = len(skipped_frame_indices)
    if skip:
        # last frame may be recorded as skipped, but if stage is done frame is
        # not real. n_missed_frames is the n_missed_frames from last frame
        assert num_frames - n_skipped <= shape_data_rendered.shape[0] \
            <= num_frames - n_skipped + n_sub_frames * n_missed_frames
        assert num_frames - n_skipped <= shape2_data_rendered.shape[0] \
            <= num_frames - n_skipped + n_sub_frames * n_missed_frames
    else:
        assert shape_data_rendered.shape[0] == num_frames
        assert shape2_data_rendered.shape[0] == num_frames

    # in QUAD12X mode, all 3 channels have same value in the data (because we
    # show gray). But the projector outputs different values for each channel,
    # for each sub-frame
    gray = video_mode == 'QUAD12X'
    i = 0
    k = 0
    for (r, g, b, val), (r1, g1, b1, _) in zip(shape_color, shape_data):
        assert isclose(val, r1, abs_tol=2 / 255) if r or gray else r1 == 0
        assert isclose(val, g1, abs_tol=2 / 255) if g or gray else g1 == 0
        assert isclose(val, b1, abs_tol=2 / 255) if b or gray else b1 == 0

        if skip:
            assert recorded_rendered_frames[k] \
                == (k not in skipped_frame_indices)
        else:
            assert recorded_rendered_frames[k]

        if k not in skipped_frame_indices:
            r1, g1, b1, _ = shape_data_rendered[i, :]
            assert isclose(val, r1, abs_tol=2 / 255) if r or gray else r1 == 0
            assert isclose(val, g1, abs_tol=2 / 255) if g or gray else g1 == 0
            assert isclose(val, b1, abs_tol=2 / 255) if b or gray else b1 == 0
            i += 1
        k += 1

    i = 0
    k = 0
    for (r, g, b, val), (r1, g1, b1, _) in zip(shape2_color, shape2_data):
        assert isclose(val, r1, abs_tol=2 / 255) if r or gray else r1 == 0
        assert isclose(val, g1, abs_tol=2 / 255) if g or gray else g1 == 0
        assert isclose(val, b1, abs_tol=2 / 255) if b or gray else b1 == 0

        if skip:
            assert recorded_rendered_frames[k] \
                == (k not in skipped_frame_indices)
        else:
            assert recorded_rendered_frames[k]

        if k not in skipped_frame_indices:
            r1, g1, b1, _ = shape2_data_rendered[i, :]
            assert isclose(val, r1, abs_tol=2 / 255) if r or gray else r1 == 0
            assert isclose(val, g1, abs_tol=2 / 255) if g or gray else g1 == 0
            assert isclose(val, b1, abs_tol=2 / 255) if b or gray else b1 == 0
            i += 1
        k += 1

    f.close_h5()
示例#6
0
文件: test_stage.py 项目: matham/ceed
async def test_short_stage(stage_app: CeedTestApp, tmp_path, quad, sub_frames,
                           main_frames):
    from ceed.analysis import CeedDataReader
    from ceed.function.plugin import LinearFunc
    from kivy.clock import Clock

    num_frames = int(math.ceil(main_frames * sub_frames))
    rate = main_frames

    root = SerialAllStage(stage_factory=stage_app.app.stage_factory,
                          show_in_gui=False,
                          app=stage_app,
                          create_add_to_parent=True)
    shape = CircleShapeP1(app=None,
                          painter=stage_app.app.shape_factory,
                          show_in_gui=True)
    root.stage.add_shape(shape.shape)
    root.stage.add_func(
        LinearFunc(function_factory=stage_app.app.function_factory,
                   b=0,
                   m=1,
                   duration=1))
    root.show_in_gui()
    await stage_app.wait_clock_frames(2)

    # use a larger frame rate so we have to drop frames
    stage_app.app.view_controller.frame_rate = rate
    stage_app.app.view_controller.use_software_frame_rate = False
    stage_app.app.view_controller.video_mode = quad
    stage_app.app.view_controller.pad_to_stage_handshake = False
    stage_app.app.view_controller.flip_projector = False

    frame = 0
    event = None
    cx, cy = shape.shape.centroid
    if sub_frames == 1:
        centers = [(cx, cy)]
    else:
        cx1, cy1 = cx // 2, cy // 2
        corners = ((0, 540), (960, 540), (0, 0), (960, 0))
        centers = [(cx1 + x, cy1 + y) for x, y in corners]
    intensity = []
    total_rounded_frames = math.ceil(main_frames) * sub_frames

    def verify_intensity(*largs):
        nonlocal frame
        if not stage_app.app.view_controller.stage_active:
            event.cancel()
            return
        # not yet started
        if not stage_app.app.view_controller.count:
            return

        assert frame < num_frames

        rgb = stage_app.get_widget_pos_pixel(stage_app.app.shape_factory,
                                             centers)
        rgb = [[c / 255 for c in p] for p in rgb]
        if sub_frames == 12:
            for plane in range(3):
                for point in rgb:
                    value = point[plane]
                    intensity.append((value, value, value, 1))
        else:
            intensity.extend(rgb)
        frame += sub_frames

        assert frame in (stage_app.app.view_controller.count,
                         total_rounded_frames)
        assert not stage_app.app.view_controller._n_missed_frames

    event = Clock.create_trigger(verify_intensity, timeout=0, interval=True)
    event()
    stage_app.app.view_controller.request_stage_start(root.name)

    await wait_experiment_done(stage_app, timeout=50)

    assert stage_app.app.view_controller.count == num_frames + 1
    # only counts whole frames
    assert frame == total_rounded_frames
    # have data for blank frames at end
    assert len(intensity) == total_rounded_frames
    assert total_rounded_frames >= num_frames

    filename = str(tmp_path / 'short_stage.h5')
    stage_app.app.ceed_data.save(filename=filename)
    with CeedDataReader(filename) as f:
        f.load_experiment(0)

        shape_data = f.shapes_intensity[shape.name]
        shape_data_rendered = f.shapes_intensity_rendered[shape.name]
        recorded_rendered_frames = f.rendered_frames

        assert shape_data.shape[0] == num_frames
        assert shape_data_rendered.shape[0] == num_frames
        assert len(recorded_rendered_frames) == num_frames

        # for each sub-frame
        gray = quad == 'QUAD12X'
        r, g, b = root.color_r, root.color_g, root.color_b
        for i, ((v1, v2, v3, _),
                (r1, g1, b1,
                 _)) in enumerate(zip(intensity[:num_frames], shape_data)):
            # we saw the intensity we expect
            val = i / (main_frames * sub_frames)
            assert isclose(val, v1, abs_tol=2 / 255) if r or gray else v1 == 0
            assert isclose(val, v2, abs_tol=2 / 255) if g or gray else v2 == 0
            assert isclose(val, v3, abs_tol=2 / 255) if b or gray else v3 == 0

            # what we saw is what is recorded
            assert isclose(v1, r1, abs_tol=2 / 255)
            assert isclose(v2, g1, abs_tol=2 / 255)
            assert isclose(v3, b1, abs_tol=2 / 255)

            assert recorded_rendered_frames[i]
            assert shape_data_rendered[i, 0] == r1
            assert shape_data_rendered[i, 1] == g1
            assert shape_data_rendered[i, 2] == b1

        # remaining frames are blank in quad mode
        for r, g, b, _ in intensity[num_frames:]:
            assert not r
            assert not g
            assert not b
示例#7
0
def test_recursive_full_stage_intensity(stage_factory: StageFactoryBase):
    root, g1, g2, s1, s2, s3, s4, s5, s6 = create_recursive_stages(
        stage_factory)

    from ceed.function.plugin import LinearFunc
    for i, stage in enumerate((s1, s2, s3, s4, s5, s6)):
        stage.stage.add_func(LinearFunc(
            function_factory=stage_factory.function_factory, b=0, m=.1,
            duration=(i % 2 + 1) * 5))

    shape = CircleShapeP1(
        app=None, painter=stage_factory.shape_factory, show_in_gui=False,
        create_add_shape=True)

    shape2 = CircleShapeP1Internal(
        app=None, painter=stage_factory.shape_factory, show_in_gui=False,
        create_add_shape=True)
    s1.stage.add_shape(shape.shape)
    s4.stage.add_shape(shape.shape)
    s5.stage.add_shape(shape.shape)
    s2.stage.add_shape(shape2.shape)
    s3.stage.add_shape(shape2.shape)
    s6.stage.add_shape(shape2.shape)

    values, n = get_stage_time_intensity(stage_factory, root.name, 10)
    assert n == 10 * (10 + 5 + 10 + 5)
    assert len(values) == 2
    colors = values[shape.name]
    colors2 = values[shape2.name]
    assert len(colors) == n

    for s, start, e in [(s1, 0, 5), (s4, 15, 25), (s5, 25, 30)]:
        for i in range(start * 10, e * 10):
            r, g, b, a = colors[i]

            i -= start * 10
            assert math.isclose(r, i / 10 * .1) if s.color_r else r == 0
            assert math.isclose(g, i / 10 * .1) if s.color_g else g == 0
            assert math.isclose(b, i / 10 * .1) if s.color_b else b == 0

    for start, e in [(5, 15), ]:
        for i in range(start * 10, e * 10):
            r, g, b, a = colors[i]
            assert r == 0
            assert g == 0
            assert b == 0

    for s, start, e in [(s2, 0, 10), (s3, 10, 15), (s6, 25, 30)]:
        for i in range(start * 10, e * 10):
            r, g, b, a = colors2[i]

            i -= start * 10
            assert math.isclose(r, i / 10 * .1) if s.color_r else r == 0
            assert math.isclose(g, i / 10 * .1) if s.color_g else g == 0
            assert math.isclose(b, i / 10 * .1) if s.color_b else b == 0

    for start, e in [(15, 25), ]:
        for i in range(start * 10, e * 10):
            r, g, b, a = colors2[i]
            assert r == 0
            assert g == 0
            assert b == 0
示例#8
0
async def test_recursive_play_stage_intensity(stage_app: CeedTestApp,
                                              tmp_path):
    from ..test_stages import create_recursive_stages
    from .examples.shapes import CircleShapeP1, CircleShapeP2
    from kivy.clock import Clock
    from ceed.analysis import CeedDataReader
    root, g1, g2, s1, s2, s3, s4, s5, s6 = create_recursive_stages(
        stage_app.stage_factory, app=stage_app)

    from ceed.function.plugin import LinearFunc
    for i, stage in enumerate((s1, s2, s3, s4, s5, s6)):
        stage.stage.add_func(
            LinearFunc(function_factory=stage_app.function_factory,
                       b=0,
                       m=.1,
                       duration=(i % 2 + 1) * 5))

    shape = CircleShapeP1(app=None,
                          painter=stage_app.shape_factory,
                          show_in_gui=True)

    shape2 = CircleShapeP2(app=None,
                           painter=stage_app.shape_factory,
                           show_in_gui=True)
    s1.stage.add_shape(shape.shape)
    s4.stage.add_shape(shape.shape)
    s5.stage.add_shape(shape.shape)
    s2.stage.add_shape(shape2.shape)
    s3.stage.add_shape(shape2.shape)
    s6.stage.add_shape(shape2.shape)

    root.show_in_gui()
    await stage_app.wait_clock_frames(2)

    frame = 0
    event = None
    trio_event = trio.Event()
    rate = stage_app.view_controller.frame_rate = 10
    num_frames = rate * (10 + 5 + 10 + 5)
    initial_frames = Clock.frames_displayed
    stage_app.view_controller.use_software_frame_rate = False
    stage_app.view_controller.flip_projector = False

    shape_color = [
        (0., 0., 0.),
    ] * num_frames
    shape2_color = [
        (0., 0., 0.),
    ] * num_frames

    for s, start, e in [(s1, 0, 5), (s4, 15, 25), (s5, 25, 30)]:
        for i in range(start * rate, e * rate):
            val = (i - start * rate) / rate * .1
            shape_color[i] = (int(s.color_r) * val, int(s.color_g) * val,
                              int(s.color_b) * val)

    for s, start, e in [(s2, 0, 10), (s3, 10, 15), (s6, 25, 30)]:
        for i in range(start * rate, e * rate):
            val = (i - start * rate) / rate * .1
            shape2_color[i] = (int(s.color_r) * val, int(s.color_g) * val,
                               int(s.color_b) * val)

    def verify_intensity(*largs):
        nonlocal frame
        if Clock.frames_displayed <= initial_frames + 1:
            return

        if not stage_app.view_controller.stage_active:
            event.cancel()
            trio_event.set()
            assert frame == num_frames
            return

        assert frame < num_frames
        points = stage_app.get_widget_pos_pixel(stage_app.shape_factory,
                                                [shape.center, shape2.center])
        points = [[c / 255 for c in p] for p in points]
        (r1, g1, b1, _), (r2, g2, b2, _) = points

        val = shape_color[frame]
        assert math.isclose(r1, val[0], abs_tol=2 / 255) if val[0] else r1 == 0
        assert math.isclose(g1, val[1], abs_tol=2 / 255) if val[1] else g1 == 0
        assert math.isclose(b1, val[2], abs_tol=2 / 255) if val[2] else b1 == 0
        val = shape2_color[frame]
        assert math.isclose(r2, val[0], abs_tol=2 / 255) if val[0] else r2 == 0
        assert math.isclose(g2, val[1], abs_tol=2 / 255) if val[1] else g2 == 0
        assert math.isclose(b2, val[2], abs_tol=2 / 255) if val[2] else b2 == 0

        frame += 1

    event = Clock.create_trigger(verify_intensity, timeout=0, interval=True)
    event()
    stage_app.view_controller.request_stage_start(root.name)

    await trio_event.wait()

    stage_app.view_controller.request_stage_end()
    event.cancel()

    filename = str(tmp_path / 'recursive_play_stage_intensity.h5')
    stage_app.ceed_data.save(filename=filename)

    f = CeedDataReader(filename)
    f.open_h5()
    assert f.experiments_in_file == ['0']
    assert not f.num_images_in_file
    f.load_experiment(0)

    shape_data = np.array(f.shapes_intensity[shape.name])
    shape2_data = np.array(f.shapes_intensity[shape2.name])
    assert len(shape_data) == num_frames
    assert len(shape2_data) == num_frames
    for (r, g, b), (r1, g1, b1, _) in zip(shape_color, shape_data):
        assert math.isclose(r, r1, abs_tol=2 / 255) if r else r1 == 0
        assert math.isclose(g, g1, abs_tol=2 / 255) if g else g1 == 0
        assert math.isclose(b, b1, abs_tol=2 / 255) if b else b1 == 0
    for (r, g, b), (r1, g1, b1, _) in zip(shape2_color, shape2_data):
        assert math.isclose(r, r1, abs_tol=2 / 255) if r else r1 == 0
        assert math.isclose(g, g1, abs_tol=2 / 255) if g else g1 == 0
        assert math.isclose(b, b1, abs_tol=2 / 255) if b else b1 == 0

    f.close_h5()