> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trainy.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Images

> Log and view image data in Pluto

## Logging

Image logging supports the following formats:

* [Numpy](https://numpy.org/doc/stable/reference/arrays.html) arrays
* [PIL/Pillow](https://pillow.readthedocs.io/en/stable/reference/Image.html) images
* [PyTorch](https://pytorch.org/docs/stable/tensors.html) tensors
* [Matplotlib](https://matplotlib.org/stable/api/figure_api.html) plots or figures

To log an image, instantiate the `pluto.Image` class.

```python theme={null}
img = pluto.Image(
    data: Union[str, PILImage.Image, matplotlib.figure.Figure, torch.Tensor, np.ndarray],
    caption: str | None = None,
)
pluto.log({"image/file/0-0": img}, step=step)
```

| Parameter | Type | Description |
| - | - | - |
| `data` | `Union[str, PILImage.Image, matplotlib.figure.Figure, torch.Tensor, np.ndarray]` | The image data to log. Can be a path to an image file, a PIL image, a Matplotlib figure, a PyTorch tensor, or a NumPy array. |
| `caption` | `str` | A caption for the image. |

## Viewing

Logged images appear as gallery cards, grouped by their metric label prefix. When comparing multiple runs, images from each run are shown side-by-side.

<Frame caption="Comparing multi-sample image logs across runs, with per-card step pinning.">
  <video autoPlay loop muted playsInline controls className="w-full rounded-xl" src="https://mintcdn.com/trainy/RSBzmM6c9FBJXx6L/images/pluto/images-multisample-pinning.mp4?fit=max&auto=format&n=RSBzmM6c9FBJXx6L&q=85&s=f6b88525cf50b4f3a7ba6fccfd378a6a" data-path="images/pluto/images-multisample-pinning.mp4" />
</Frame>

When multiple runs are selected in the Compare view, images logged with the same metric label are displayed together, with each run's image labeled with the run name and ID. This makes it easy to visually compare outputs across experiments.

### Adding to a dashboard

Images show up automatically in the Compare view. To place an image gallery on a [custom dashboard](/pluto/dashboards), open **Add Widget**, choose the **Files** tab, and search for your image log name.

<img src="https://mintcdn.com/trainy/ck7WI5896eYTYADa/images/pluto/add-widget-files-image.png?fit=max&auto=format&n=ck7WI5896eYTYADa&q=85&s=293b26c7d5c2f5431b81a9435ccf723e" alt="Adding a Files widget from the Add Widget modal" width="1352" height="1656" data-path="images/pluto/add-widget-files-image.png" />

### Step Navigation

If you log images at multiple training steps, use the step slider below the images to browse through different steps. This is useful for tracking how generated outputs (e.g., sample images, attention maps) evolve over the course of training.

When multiple image groups are displayed in the same section, their step sliders can be **linked** so that changing the step on one group changes all of them simultaneously. Click the **lock icon** on the step navigator to toggle sync on or off.

### Multi-Sample Logging

Log **several images at the same step under one log name** by passing a list. Every sample renders inside the same card with a per-cell `◀  i / N  ▶` nav row above the image:

```python theme={null}
pluto.log({
    "gens": [pluto.Image(a), pluto.Image(b), pluto.Image(c)],
}, step=10)
```

<img src="https://mintcdn.com/trainy/RSBzmM6c9FBJXx6L/images/pluto/multi-index-nav.png?fit=max&auto=format&n=RSBzmM6c9FBJXx6L&q=85&s=5dd0d989d7dccdd8966b4a0a88d80a37" alt="Multi-sample image cards across runs with 1/1, 3/5, and 2/2 nav rows" style={{ maxWidth: '85%' }} width="1100" height="1012" data-path="images/pluto/multi-index-nav.png" />

#### Sync Sample Indices

A **Sync Sample Indices** selector in the media settings popover controls how the sample index is shared:

* **Off** — each run steps through its samples independently.
* **Across runs** — every run in the same widget shares the sample index.
* **Across widgets** (default) — every run on every media widget (image, video, and audio) advances the sample index in tandem.

The selector is always available, so you can opt a single widget out of cross-widget sync. Where a cell has fewer samples than the shared index, it clamps to its own last sample, and single-sample cells show no nav row.

### Pinning Runs at a Step

The global step slider forces every run to be viewed at the same step, which makes it hard to compare runs at their respective best-performing checkpoints — different runs converge at different steps, and the artifacts you actually want side-by-side don't all live at the same `step`.

Pinning solves this by letting you freeze individual runs at a specific step while the global slider keeps driving everything else. Common workflows:

* Lock a baseline run at its best step, then sweep the slider on the rest to see how the variants compare against that fixed reference.
* Pin every run on the page at its own argmin/argmax of a metric so the gallery shows each run's best checkpoint regardless of when it landed (see [Best-Step Pinning](#best-step-pinning) below).
* Hold a run frozen at a known-bad step to inspect failure modes while other runs continue advancing.

Each pinned card shows a colored badge with the pinned step. Click the pin button on an image card to choose the pin scope:

| Symbol | Scope | Effect |
| - | - | - |
| ◇ | **Pin in this panel** | Freezes only this card's run inside this image widget. Other widgets for the same run still follow the global stepper. |
| ◈ | **Pin across all panels** | Freezes this run at the chosen step in every image widget on the page that shows it. |
| ★ | **Pin to best step** | Pinned by a column-header action (see below). The step is computed per run from a metric's argmin/argmax. |

<img src="https://mintcdn.com/trainy/RSBzmM6c9FBJXx6L/images/pluto/pin-menu-image.png?fit=max&auto=format&n=RSBzmM6c9FBJXx6L&q=85&s=9a9ede31be512eb1c1c3ccd270355b8d" alt="Pin scope menu on an image card" style={{ maxWidth: '85%' }} width="804" height="378" data-path="images/pluto/pin-menu-image.png" />

#### Unpinning

Cross-panel and best-step pins offer two unpin options in a dropdown:

* **Unpin this image** — removes the pin only from this widget. Other widgets that still hold the same cross-panel pin keep it.
* **Unpin across all panels** — removes the pin everywhere it was applied.

Local (single-panel) pins keep the simpler one-click unpin action — there's nothing else to scope.

<img src="https://mintcdn.com/trainy/RSBzmM6c9FBJXx6L/images/pluto/unpin-menu-image.png?fit=max&auto=format&n=RSBzmM6c9FBJXx6L&q=85&s=4767718a283992104e7bdec873ab4713" alt="Unpin menu on a pinned image card" style={{ maxWidth: '85%' }} width="716" height="378" data-path="images/pluto/unpin-menu-image.png" />

#### Clear All Pins

Open the **Image Settings** popover and click **Clear All Pins** to remove every pin (local, cross-panel, and best-step) at once for the current page.

### Best-Step Pinning

For metric-driven exploration (ex. "show me each run at its best loss"), pin every run on the page to its own argmin/argmax step in one click. Open a metric column's header menu in the runs table, then choose **Find best step** → one of:

| Action | What it pins to |
| - | - |
| **Pin steppers at min value** | Each run pinned at the step where the metric is lowest |
| **Pin steppers at max value** | Each run pinned at the step where the metric is highest |
| **Pin steppers at min (with media)** | Lowest metric value among steps that *also* have an image for that run |
| **Pin steppers at max (with media)** | Highest metric value among steps that *also* have an image for that run |

The **(with media)** variants (grouped under *Pin to nearest media step*) are useful when your metric and image cadences don't perfectly overlap — without them, the chosen step might land on a step that has no image, so the card falls back to the **No image at step N** placeholder.

<img src="https://mintcdn.com/trainy/ck7WI5896eYTYADa/images/pluto/find-best-step-image.png?fit=max&auto=format&n=ck7WI5896eYTYADa&q=85&s=f692aa19fa1f0440c32bfaa350ab6210" alt="Find best step menu with the with-media variants and tolerance" style={{ maxWidth: '85%' }} width="974" height="794" data-path="images/pluto/find-best-step-image.png" />

#### Tolerance Window

Even with the **(with media)** variants, your metric and image cadences may not align exactly (e.g. metric every 10 steps, images every 10 steps offset by 5). The best-step picker handles this with a **nearest-snap with tolerance**: each metric step snaps to its closest image step within a step-distance limit `K` (default `K = 20`).

You can edit `K` directly in the **Find best step** submenu — type a value, press Enter or click any of the four pin buttons.

**Widen the window if pins come up empty.** In the `tol-demo-gap` run below, the metric is logged at steps 0/50/100/150/200 and images only at 25/75/125/175, so every metric step sits exactly 25 steps from the nearest image. At the default `K = 20` nothing is within tolerance and the run stays **unpinned** (plain pin icon, no step badge); raise `K` to 25 and the min-loss step (100) snaps to image step 125 — the provenance even notes the tie with step 75, broken toward the later step.

<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap', alignItems: 'flex-start' }}>
  <img src="https://mintcdn.com/trainy/RSBzmM6c9FBJXx6L/images/pluto/tolerance-image-nopin.png?fit=max&auto=format&n=RSBzmM6c9FBJXx6L&q=85&s=eeaa6fe151fe5cc29e9b4e597be355bf" alt="tol-demo-gap unpinned at K=20" style={{ width: '48%', minWidth: '260px', borderRadius: '8px' }} width="670" height="526" data-path="images/pluto/tolerance-image-nopin.png" />

  <img src="https://mintcdn.com/trainy/RSBzmM6c9FBJXx6L/images/pluto/tolerance-image-pinned.png?fit=max&auto=format&n=RSBzmM6c9FBJXx6L&q=85&s=ee47183e795591b0db607eb6b4826da8" alt="tol-demo-gap pinned at K=25" style={{ width: '48%', minWidth: '260px', borderRadius: '8px' }} width="894" height="698" data-path="images/pluto/tolerance-image-pinned.png" />
</div>

**A wider window can also find a *better* step.** When the metric keeps improving past the nearest media step, a tight window snaps to a worse value. In `tol-demo-sparse` below, `K = 10` only reaches metric step 40 (loss 0.85), while `K = 25` reaches metric step 55 — the true minimum (loss 0.1). The pinned image is the same in both (step 30); only the provenance popover reveals which metric step drove the pick.

<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap', alignItems: 'flex-start' }}>
  <img src="https://mintcdn.com/trainy/RSBzmM6c9FBJXx6L/images/pluto/tolerance-image-bad.png?fit=max&auto=format&n=RSBzmM6c9FBJXx6L&q=85&s=e8edcf464b87083eb6077bd99d841f2c" alt="tol-demo-sparse at K=10 reaching loss 0.85" style={{ width: '48%', minWidth: '260px', borderRadius: '8px' }} width="724" height="662" data-path="images/pluto/tolerance-image-bad.png" />

  <img src="https://mintcdn.com/trainy/RSBzmM6c9FBJXx6L/images/pluto/tolerance-image-good.png?fit=max&auto=format&n=RSBzmM6c9FBJXx6L&q=85&s=c4dc284fe3e95f46ce8a0e88d4ad176b" alt="tol-demo-sparse at K=25 reaching loss 0.1" style={{ width: '48%', minWidth: '260px', borderRadius: '8px' }} width="724" height="662" data-path="images/pluto/tolerance-image-good.png" />
</div>

#### Pin Provenance

Every best-step pinned card shows an **info icon (ⓘ)** next to the run name. Hover for a popover that explains why this step was chosen:

* A headline with the metric and extreme that drove the pick (e.g. *Pinned at max train/loss = 0.9023*)
* The metric step that drove the pick
* How many steps away the chosen image is from that metric step
* A tied-alternative line when more than one image step matched within tolerance

<img src="https://mintcdn.com/trainy/ck7WI5896eYTYADa/images/pluto/best-step-example-image.png?fit=max&auto=format&n=ck7WI5896eYTYADa&q=85&s=a2f806810f492dba3d38797fd04aafa8" alt="A best-step pinned image card with its provenance popover" style={{ maxWidth: '85%' }} width="580" height="470" data-path="images/pluto/best-step-example-image.png" />

### Sync Zoom

Click the **settings** icon on an image card to open the Image Settings popover. Enable **Sync Zoom** to persist the zoom level across image fullscreen views within the same group. When enabled, zooming in on one image carries over when you navigate to the next step. Sync Zoom is off by default.

<img src="https://mintcdn.com/trainy/110C0XtRepKYeipL/images/pluto/sync-zoom-popover.png?fit=max&auto=format&n=110C0XtRepKYeipL&q=85&s=d4244c80a52cec6aa594c2548e94261c" alt="Sync Zoom popover" style={{ maxWidth: '50%' }} width="526" height="306" data-path="images/pluto/sync-zoom-popover.png" />

## Annotations

`pluto.Image` accepts **bounding boxes** and **segmentation masks**, drawn as overlays on the image.

```python theme={null}
run.log({"detections": pluto.Image(
    img,
    boxes={
        "predictions": {
            "box_data": [
                {
                    "position": {"minX": 10, "minY": 20, "maxX": 80, "maxY": 90},
                    "domain": "pixel",
                    "class_id": 3,
                    "box_caption": "car 0.91",
                }
            ],
            "class_labels": {3: "car"},
        }
    },
    masks={
        "predictions": {
            "mask_data": mask_2d,              # HxW array of class ids
            "class_labels": {1: "road", 3: "car"},
        }
    },
)})
```

Both take a dict keyed by **layer name**. A box layer and a mask layer sharing a name become one toggle, which is how you overlay a prediction's boxes and mask together.

### Boxes

Each box needs a `position` with `minX`, `minY`, `maxX` and `maxY`. Everything else is optional:

| Key | Purpose |
| - | - |
| `position` | The rectangle. **Required.** |
| `domain` | How to read the coordinates — see below |
| `class_id` | Picks the color, and the caption when `box_caption` is absent |
| `box_caption` | The label drawn on the box |

There's no limit on how many boxes a layer can hold, and a box drawn outside the image is clipped rather than rejected.

<img src="https://mintcdn.com/trainy/pLtLJdYKyksDaHCH/images/pluto/annot-boxes.png?fit=max&auto=format&n=pLtLJdYKyksDaHCH&q=85&s=ad06ecf434a7562c5ca518a75272f5a7" alt="Boxes drawn over an image, each labelled with its class and confidence" width="1022" height="768" data-path="images/pluto/annot-boxes.png" />

### Pixel vs fractional coordinates

`pluto.Image` stamps every box with `domain: "pixel"` unless you set one yourself, so **pixel coordinates work whether or not you pass `domain`** — that's what the boxes above are using.

To use fractional (0–1) coordinates you have to set `domain` to something else explicitly. Passing 0–1 values on their own means they're read as pixels — a box a fraction of a pixel wide, which draws nothing at all:

<img src="https://mintcdn.com/trainy/pLtLJdYKyksDaHCH/images/pluto/annot-domain-fractional.png?fit=max&auto=format&n=pLtLJdYKyksDaHCH&q=85&s=2a17ffa0685456109b617bbc3a9ff33a" alt="The same boxes given as 0-1 values with no domain: the image renders bare, with no boxes" width="1022" height="768" data-path="images/pluto/annot-domain-fractional.png" />

<Warning>
  This is the opposite of wandb, where **omitting** `domain` means fractional. Porting a wandb config that relied on that will silently draw nothing — no error, no warning, just an unannotated image.
</Warning>

Reversed corners are tolerated, and a box outside the image is clipped rather than rejected.

### Masks

`mask_data` is an array of class ids, one per pixel, uploaded as a separate PNG alongside the image.

* **Class ids must be 0–255.** Higher values wrap.
* **Mask dimensions aren't checked** against the image — a mismatched mask is stretched to fit, so a different aspect ratio distorts.
* Multiple mask layers composite in name order at a fixed opacity.

<img src="https://mintcdn.com/trainy/pLtLJdYKyksDaHCH/images/pluto/annot-masks.png?fit=max&auto=format&n=pLtLJdYKyksDaHCH&q=85&s=535b5dd54aac37e5537a434f488f628a" alt="Segmentation masks tinting the regions of an image" width="1022" height="768" data-path="images/pluto/annot-masks.png" />

<Warning>
  **A mask layer with no `class_labels` renders nothing at all.** Only class ids present in `class_labels` are tinted. Boxes behave differently — a box without a matching label still draws, falling back to the raw class id as its caption.
</Warning>

<Note>
  **Self-hosted:** masks are recolored in the browser by reading pixel data, which requires CORS headers on your object store. MinIO sends them by default; a bare S3 bucket does not, and masks will silently fail to draw.
</Note>

### Colors and captions

Class colors come from a fixed 16-color palette indexed by `class_id`, so classes 16 apart share a color. `box_caption` overrides the class label on screen.

<img src="https://mintcdn.com/trainy/pLtLJdYKyksDaHCH/images/pluto/annot-both.png?fit=max&auto=format&n=pLtLJdYKyksDaHCH&q=85&s=164d6aa931159787c3514e278989a812" alt="Boxes and masks together, each class in its own colour with captions" width="1022" height="768" data-path="images/pluto/annot-both.png" />

Boxes and masks sharing a layer name draw together, as above.

<Note>
  A `scores` dict is accepted on a box but **never displayed**. Put the confidence in `box_caption` if you want it visible.
</Note>

### Where overlays appear

Overlays render everywhere an image does — the run page grid, the all-runs comparison, and dashboard file widgets.

**Per-layer show/hide toggles** appear in only three places: a media widget's fullscreen dialog, a card's zoom dialog, and the **Files** tab. There isn't room on inline grid tiles. Toggles are per-view and reset when you move to another image; all layers start visible. There's no opacity control and no filter-by-class.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.