Skip to content

montage_visualizer

tit.tools.montage_visualizer

Render PNG visualizations of electrode montage placements.

Overlays coloured rings and arc connections on a template EEG cap image using ImageMagick convert. Called automatically by the simulation pipeline to document the active montage.

Public API

visualize_montage Render a PNG for a single montage or a combined multi-montage image.

See Also

tit.tools.map_electrodes : Map optimised positions to net labels. tit.sim : Simulation pipeline that invokes the visualiser.

get_expected_output_filename

get_expected_output_filename(montage_name: str, sim_mode: str = 'U') -> str

Return the expected montage visualization PNG filename.

Source code in tit/tools/montage_visualizer.py
def get_expected_output_filename(montage_name: str, sim_mode: str = "U") -> str:
    """Return the expected montage visualization PNG filename."""
    if sim_mode == "U":
        return f"{montage_name}_highlighted_visualization.png"
    return "combined_montage_visualization.png"

is_skipped_net

is_skipped_net(eeg_net: str) -> bool

Return True when montage visualization is intentionally skipped.

Source code in tit/tools/montage_visualizer.py
def is_skipped_net(eeg_net: str) -> bool:
    """Return True when montage visualization is intentionally skipped."""
    return eeg_net in _SKIP_NETS

is_supported_net

is_supported_net(eeg_net: str) -> bool

Return True when a coordinate map exists for eeg_net.

Source code in tit/tools/montage_visualizer.py
def is_supported_net(eeg_net: str) -> bool:
    """Return True when a coordinate map exists for *eeg_net*."""
    return eeg_net in _COORD_FILES

visualize_montage

visualize_montage(montage_name: str, electrode_pairs: list[list[str]], eeg_net: str, output_dir: str, sim_mode: str = 'U', logger=None) -> None

Render a PNG showing electrode positions and connection arcs.

Parameters

montage_name : str Used as the output filename base (unipolar) or "combined" (multipolar). electrode_pairs : list of list of str Electrode pairs, e.g. [["E030", "E020"], ["E095", "E070"]]. eeg_net : str EEG cap name, e.g. "GSN-HydroCel-185.csv". output_dir : str Directory to write the output PNG(s) into. sim_mode : str, optional "U" produces one image per montage; "M" produces a single combined image. Default "U".

Source code in tit/tools/montage_visualizer.py
def visualize_montage(
    montage_name: str,
    electrode_pairs: list[list[str]],
    eeg_net: str,
    output_dir: str,
    sim_mode: str = "U",
    logger=None,
) -> None:
    """Render a PNG showing electrode positions and connection arcs.

    Parameters
    ----------
    montage_name : str
        Used as the output filename base (unipolar) or ``"combined"``
        (multipolar).
    electrode_pairs : list of list of str
        Electrode pairs, e.g.
        ``[["E030", "E020"], ["E095", "E070"]]``.
    eeg_net : str
        EEG cap name, e.g. ``"GSN-HydroCel-185.csv"``.
    output_dir : str
        Directory to write the output PNG(s) into.
    sim_mode : str, optional
        ``"U"`` produces one image per montage; ``"M"`` produces a
        single combined image.  Default ``"U"``.
    """
    if is_skipped_net(eeg_net):
        expected = get_expected_output_filename(montage_name, sim_mode)
        if logger is not None:
            logger.warning(
                "Montage visualization unavailable for EEG net '%s'; "
                "skipping render. Expected output would be %s in %s.",
                eeg_net,
                expected,
                output_dir,
            )
        return

    coords = _load_coordinates(eeg_net)

    if not electrode_pairs:
        raise ValueError("No electrode pairs provided for montage visualization")
    for pair in electrode_pairs:
        if len(pair) != 2 or any(label not in coords for label in pair):
            raise ValueError(
                f"Electrode pair {pair!r} is not in the {eeg_net} coordinate map"
            )

    # Cap centre: fallback arc target when a pair has no "other" channels.
    center = (
        sum(x for x, _ in coords.values()) / len(coords),
        sum(y for _, y in coords.values()) / len(coords),
    )

    # Consecutive pairs form one TI unit: (0,1) work together, (2,3) work
    # together, ... so each pair's arc bulges toward its *partner* pair --
    # 1A faces 1B, 2A faces 2B -- rather than the whole montage.
    def _partner_target(idx: int) -> tuple[float, float]:
        partner = idx + 1 if idx % 2 == 0 else idx - 1
        pts = (
            [coords[e] for e in electrode_pairs[partner] if e in coords]
            if 0 <= partner < len(electrode_pairs)
            else []
        )
        if not pts:
            return center
        return (
            sum(p[0] for p in pts) / len(pts),
            sum(p[1] for p in pts) / len(pts),
        )

    template = os.path.join(_RESOURCES_DIR, "GSN-256.png")
    os.makedirs(output_dir, exist_ok=True)

    final_image = os.path.join(
        output_dir, get_expected_output_filename(montage_name, sim_mode)
    )
    # Publish only a fully annotated image. A missing renderer or failed drawing command must
    # never leave a bare template at the successful-output path (or damage an existing image).
    with TemporaryDirectory(prefix=".montage-", dir=output_dir) as staging:
        out_image = os.path.join(staging, "montage.png")
        source = (
            final_image if sim_mode != "U" and os.path.exists(final_image) else template
        )
        shutil.copy2(source, out_image)
        for i, pair in enumerate(electrode_pairs):
            e1, e2 = pair
            color = _COLORS[i % len(_COLORS)]
            ring = os.path.join(_RESOURCES_DIR, _RINGS[i % len(_RINGS)])
            _overlay_ring(out_image, *coords[e1], color, ring)
            _overlay_ring(out_image, *coords[e2], color, ring)
            _draw_arc(out_image, *coords[e1], *coords[e2], color, _partner_target(i))
        _draw_legend(out_image, len(electrode_pairs))
        os.replace(out_image, final_image)

montage_webp

montage_webp(electrode_pairs: list[list[str]], eeg_net: str, width: int = 720) -> bytes | None

The image :func:visualize_montage draws, as WebP bytes for a report.

None when the net has no cap template, a label is not on it, or ImageMagick is missing: a report then shows the electrodes as text only.

Source code in tit/tools/montage_visualizer.py
def montage_webp(
    electrode_pairs: list[list[str]], eeg_net: str, width: int = 720
) -> bytes | None:
    """The image :func:`visualize_montage` draws, as WebP bytes for a report.

    ``None`` when the net has no cap template, a label is not on it, or ImageMagick is
    missing: a report then shows the electrodes as text only.
    """
    import io

    from PIL import Image

    net = eeg_net if eeg_net in _COORD_FILES else f"{eeg_net}.csv"
    if not is_supported_net(net):
        return None
    with TemporaryDirectory(prefix="tit-montage-") as tmp:
        try:
            visualize_montage("report", electrode_pairs, net, tmp)
        except (OSError, subprocess.CalledProcessError, ValueError):
            return None
        image = Image.open(os.path.join(tmp, get_expected_output_filename("report")))
        image = image.convert("RGB")
        image.thumbnail((width, width))
        out = io.BytesIO()
        image.save(out, "WEBP", quality=80, method=6)
    return out.getvalue()