view render.py @ 4:a9f10dceb17e draft default tip

planemo upload for repository https://github.com/BMCV/galaxy-image-analysis/tree/master/tools/libcarna/ commit 99d6ebe67515f1362ea4412b143ab2a140c8d631
author imgteam
date Tue, 13 Jan 2026 12:21:55 +0000
parents 31a2e1909ae5
children
line wrap: on
line source

import giatools
import libcarna
import libcarna._imshow
import pandas as pd

# Fail early if an optional backend is not available
giatools.require_backend('omezarr')

# Patch `libcarna._imshow` to return plain HTML
libcarna._imshow.IPythonHTML = lambda html: html


GEOMETRY_TYPE_INTENSITIES = 0
GEOMETRY_TYPE_MASK = 1


def wrap_color(params: dict) -> dict:
    """
    Return the `params` dictionary but wrap values for `color` with `libcarna.color`.
    """
    result = dict()
    for key, value in params.items():
        if key == 'color':
            value = libcarna.color(value)
        result[key] = value
    return result


if __name__ == "__main__":
    tool = giatools.ToolBaseplate()
    tool.add_input_image('intensities')
    tool.add_input_image('mask', required=False)
    tool.parser.add_argument('--colormap', type=str)
    tool.parser.add_argument('--html', type=str)
    tool.parse_args()

    # Load custom colormap
    if tool.args.raw_args.colormap:
        df_colormap = pd.read_csv(tool.args.raw_args.colormap, delimiter='\t')

    # Validate the input image(s)
    try:
        for image in tool.args.input_images.values():
            if any(image.shape[image.axes.index(axis)] > 1 for axis in image.axes if axis not in 'ZYX'):
                raise ValueError(f'This tool is not applicable to images with {image.original_axes} axes.')

        # Create and configure frame renderer
        print('Sample rate:', tool.args.params['sample_rate'])
        mode = getattr(libcarna, tool.args.params['mode'])(
            GEOMETRY_TYPE_INTENSITIES,
            sr=tool.args.params['sample_rate'],
            **tool.args.params['mode_kwargs']
        )
        mask_renderer = libcarna.mask_renderer(
            GEOMETRY_TYPE_MASK,
            sr=tool.args.params['sample_rate'],
            **wrap_color(tool.args.params['mask_renderer_kwargs']),
        )
        r = libcarna.renderer(
            tool.args.params['width'],
            tool.args.params['height'],
            [mode, mask_renderer],
        )
        print('EGL Vendor:', r.gl_context.vendor)

        # Build the scene graph
        root = libcarna.node()
        intensities = tool.args.input_images['intensities']
        intensities_volume = libcarna.volume(
            GEOMETRY_TYPE_INTENSITIES,
            intensities.normalize_axes_like(tool.args.params['axes']).data,
            parent=root,
            units=tool.args.params['units'],
            spacing=[
                {
                    'X': intensities.metadata.pixel_size[0] or 1,
                    'Y': intensities.metadata.pixel_size[1] or 1,
                    'Z': intensities.metadata.z_spacing or 1,
                }
                [axis] for axis in tool.args.params['axes']
            ],
            normals=(tool.args.params['mode'] == 'dvr'),
        )
        camera = libcarna.camera(
            parent=root,
        ).frustum(
            **tool.args.params['camera']['kwargs'],
        ).translate(
            z=tool.args.params['camera']['distance'],
        )
        if (mask := tool.args.input_images.get('mask')):
            libcarna.volume(
                GEOMETRY_TYPE_MASK,
                mask.normalize_axes_like(tool.args.params['axes']).data,
                parent=intensities_volume,
                spacing=intensities_volume.spacing,
            )

        # Apply colormap
        if tool.args.params['colormap'] == 'custom':
            mode.cmap.clear()
            i0, color0 = None, None
            for row in df_colormap.to_dict(orient='records'):
                match row['type']:
                    case 'relative':
                        i1 = row['intensity']
                    case 'absolute':
                        i1 = intensities_volume.normalized(row['intensity'])
                    case _:
                        raise ValueError('Unknown intensity type: "{}"'.format(row['type']))
                color1 = libcarna.color(row['color'])
                if i0 is not None:
                    mode.cmap.linear_segment(i0, i1, color0, color1)
                i0, color0 = i1, color1
        else:
            cmap_kwargs = dict()
            if (ramp_params := tool.args.params['ramp']):
                ramp_values = list()
                for val_type, value in (
                    (ramp_params['start_type'], ramp_params['start_value']),
                    (ramp_params['end_type'], ramp_params['end_value']),
                ):
                    ramp_values.append(
                        value if val_type == 'relative' else intensities_volume.normalized(value),
                    )
                cmap_kwargs['ramp'] = tuple(ramp_values)
                cmap_kwargs['n_samples'] = 1000  # high precision is required when sampling ramps to avoid "soft" corners
            mode.cmap(tool.args.params['colormap'], **cmap_kwargs)

        # Render
        colorbars = list()
        if tool.args.params['colorbar']:
            colorbars.append(mode.cmap.bar(intensities_volume))
        html = libcarna.imshow(
            libcarna.animate(
                libcarna.animate.rotate_local(camera),
                n_frames=tool.args.params['video']['frames'],
            ).render(r, camera),
            *colorbars,
            fps=tool.args.params['video']['fps'],
        )

        # Write the result
        with open(tool.args.raw_args.html, 'w') as fhtml:
            fhtml.write(html)

    except ValueError as err:
        exit(err.args[0])