Ultralytics YOLO27:
Lessonintermediate

Label and Train Instance Segmentation

Write polygon labels, check them in code, train yolo26n-seg.pt on COCO8-seg, and diagnose a model from the gap between its box and mask metrics.

Instance segmentation gives every object its own outline, which is what you need when area, overlap, or a boundary drives the decision. Polygons are the most expensive labels most teams buy, and a malformed row does not crash training — it quietly trains on the wrong shape. This lesson gets the row format right, checks a label set in code, and trains a first model so you can see what the extra labeling buys: a second metric that scores outlines, not just positions.

Outcome

Check a polygon label set for malformed rows, train yolo26n-seg.pt on coco8-seg.yaml, and decide what to fix from the gap between box and mask mAP50-95.

Fast Track
If you already know your way around, here's the short version.
  1. Write one row per object: class index, then normalized x y vertex pairs, at least three vertices.

  2. Run a label check for odd value counts, out-of-range coordinates, and suspiciously low polygon fill.

  3. yolo segment train data=coco8-seg.yaml model=yolo26n-seg.pt epochs=1 imgsz=320.

  4. Gate on mask mAP50-95 (metrics.seg.map) and use its gap to box mAP50-95 (metrics.box.map) as the diagnostic.

Hands-on

The polygon label row#

Ultralytics YOLO instance segmentation of a city crosswalk, with a separate mask for each car, the bus, and every pedestrian

A segmentation label file has one row per object, like a detection label, but instead of a box it lists the object's outline as normalized vertex pairs. A real row from COCO8-seg, trimmed:

45 0.782016 0.986521 0.937078 0.874167 0.957297 0.782021 0.950562 0.739333 0.825844 0.561792 ...
PositionDetection rowSegmentation row
1class index (45 is bowl)class index
2–5x_center y_center width heightthe first two vertices, x1 y1 x2 y2
6+—more vertices, as many as the outline needs
Unitsnormalized 0–1normalized 0–1 (pixel x ÷ image width, pixel y ÷ image height)

Three rules carry most of the weight: at least three vertices per object, coordinates divided by the image size, and one row per instance even when two objects of the same class touch. The segmentation dataset guide has the full specification.

Polygon labels also train boxes and class maps

Polygons are a superset of box labels. The same label folder trains yolo26n.pt (Ultralytics derives each box from its polygon) and yolo26n-sem.pt (it rasterizes the polygons into class maps) — we trained both on coco8-seg.yaml. If the plan changes after labeling, the budget is not lost.

Check labels before you train#

A row with an odd number of values, a coordinate written in pixels, or an outline traced around the wrong thing will not stop training. This checker reads the dataset through Ultralytics' own loader, rejects malformed rows, and prints each polygon's area next to the box Ultralytics will derive from it:

from pathlib import Path

import numpy as np

from ultralytics.data.utils import check_det_dataset

data = check_det_dataset("coco8-seg.yaml")  # resolves paths, downloads if needed
for label in sorted(Path(data["path"], "labels", "train").glob("*.txt"))[:2]:
    for n, row in enumerate(label.read_text().splitlines(), 1):
        cls, *coords = row.split()
        if len(coords) % 2 or len(coords) < 6:
            print(f"{label.name}:{n} BAD: needs an even number of values and at least 3 vertices")
            continue
        xy = np.array(coords, dtype=float).reshape(-1, 2)
        if xy.min() < 0 or xy.max() > 1:
            print(f"{label.name}:{n} BAD: coordinates must be normalized to 0-1")
            continue
        x, y = xy[:, 0], xy[:, 1]
        area = 0.5 * abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))  # shoelace formula
        w, h = xy.max(0) - xy.min(0)  # the box Ultralytics derives from the polygon
        name = data["names"][int(cls)]
        print(f"{label.stem} {name:9s} {len(xy):3d} vertices  polygon {area:.3f}  box {w * h:.3f}  fill {area / (w * h):.2f}")
000000000009 bowl       12 vertices  polygon 0.391  box 0.569  fill 0.69
000000000009 bowl       14 vertices  polygon 0.145  box 0.238  fill 0.61
000000000009 broccoli   50 vertices  polygon 0.161  box 0.252  fill 0.64
000000000009 bowl       55 vertices  polygon 0.079  box 0.531  fill 0.15
000000000009 orange     13 vertices  polygon 0.007  box 0.011  fill 0.64
000000000009 orange     13 vertices  polygon 0.005  box 0.009  fill 0.61
000000000009 orange     18 vertices  polygon 0.012  box 0.019  fill 0.61
000000000009 orange     12 vertices  polygon 0.010  box 0.022  fill 0.44
000000000025 giraffe    82 vertices  polygon 0.072  box 0.234  fill 0.31
000000000025 giraffe    19 vertices  polygon 0.010  box 0.027  fill 0.38

Areas are fractions of the image, so the first bowl covers 39% of its photo. Fill is polygon area divided by box area, and it is the column to scan:

  • Most compact objects (bowls, oranges, broccoli) land around 0.6–0.7.
  • The giraffe's 0.31 is genuine: long legs and neck leave most of its box empty — the reason this object needs a mask at all.
  • The third bowl's 0.15 is the row a reviewer should open. Only a thin rim of that bowl is visible around its food, so its outline is a narrow band inside a large box. That is correct here; on your data, a fill that low is just as often an outline traced around the wrong object.
Decision rule

Fix every BAD row before training. Then sort by fill and open the lowest few percent of rows per class in an annotation tool: a low fill is either a thin or occluded object (keep) or a bad trace (fix).

Get polygons without tracing every edge#

Hand-tracing outlines is the expensive part of data labeling. Check which shortcut applies before anyone starts:

You already haveTool
COCO JSON with segmentationconvert_coco(labels_dir, use_segments=True) from ultralytics.data.converter
PNG masks (pixel value = class index + 1, 0 = background)convert_segment_masks_to_yolo_seg(masks_dir, output_dir, classes) — touching objects of one class come out as one polygon, so split them in review
Box labels onlyyolo_bbox2segment(im_dir) — prompts SAM with each box to generate a polygon
Nothing yetSmart Annotation and Batch Labeling — click once, review the polygon

On Ultralytics Platform, Smart mode runs SAM: a positive click on the object and a negative click on what it should exclude produce a mask you then review.

Ultralytics Platform Smart mode with SAM 2.1 Tiny selected: a positive click and a negative click on a warehouse robot produce its mask

Review every machine-generated polygon

A mask that bleeds into the background trains the model to do the same. Review machine polygons against the quality bar in Boxes vs Masks, then rerun the label check above.

Train on COCO8-seg#

COCO8-seg training mosaic: zebras, a giraffe, lunch boxes of broccoli and oranges, and a vase of flowers, each object with its own colored mask and class index

Training is the detection workflow with a -seg model and a segmentation dataset. One epoch at imgsz=320 on a laptop CPU finishes in seconds, enough to prove the pipeline end to end:

from ultralytics import YOLO

model = YOLO("yolo26n-seg.pt")
model.train(data="coco8-seg.yaml", epochs=1, imgsz=320, device="cpu")
metrics = model.val()
print(f"box  mAP50-95: {metrics.box.map:.3f}")
print(f"mask mAP50-95: {metrics.seg.map:.3f}")
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95)     Mask(P          R      mAP50  mAP50-95)
                   all          4         17      0.909      0.449      0.661      0.482      0.685      0.409      0.557      0.377
                 horse          1          2          1      0.993      0.995      0.564      0.619      0.857      0.663      0.199
box  mAP50-95: 0.482
mask mAP50-95: 0.377

Four validation images make these numbers noise; what matters is that the run completes and writes weights/best.pt under runs/segment/. For real data, follow the Segment task guide and train the full schedule on a GPU.

Read box and mask metrics side by side#

The validation table has two blocks. Box scores each predicted box against the box implied by the label; Mask scores the predicted pixels against the labeled polygon. Both run the same mAP machinery; only the IoU changes.

Mask mAP50-95 is almost always the lower of the two, because pixel IoU is stricter: a box a few pixels off still overlaps well, while a mask that misses an arm or bleeds into the road loses IoU fast at the 0.75–0.95 thresholds. The horse row shows it at its most extreme — box mAP50-95 0.564, mask 0.199. The model found both horses and boxed them well; it did not outline legs and tails. Use the gap to decide what to fix:

  • Both low: the problem is finding objects, not outlining them. Add data for the missed conditions first.
  • Box good, mask far lower: the model finds objects but outlines them poorly. Run the label check, try a larger model, or train and validate at a higher imgsz.
  • Mask improving while box stalls: fine — for a segmentation product, mask mAP50-95 is the release gate.
Two knobs that do not change mask resolution

overlap_mask=True merges instance masks into one image during training, and mask_ratio=4 downsamples training masks. Neither changes the resolution of predicted masks — that is retina_masks, in the next lesson. Leave both at their defaults unless you have measured a reason to change them.

Try It

Run check_seg_labels.py on all of COCO8-seg (train and val) and list the three rows with the lowest fill. Then train yolo26n-seg.pt on coco8-seg.yaml for one epoch at imgsz=320, record box mAP50-95, mask mAP50-95, and the class with the largest gap, and open val_batch0_pred.jpg in the validation folder to find an object whose mask is visibly worse than its box.

Done When
You've finished the lesson when all of these are true.
  • Your label check reports zero BAD rows and you named the three lowest-fill rows with a keep-or-fix verdict for each.

  • The training run finished, best.pt exists under runs/segment/, and you recorded both mAP50-95 values.

  • You named the class with the largest box-versus-mask gap and the next step the decision chart gives you.

Show solution

To check every split, replace the folder in the loop with Path(data["path"], "labels").rglob("*.txt"). Then train:

from ultralytics import YOLO

model = YOLO("yolo26n-seg.pt")
model.train(data="coco8-seg.yaml", epochs=1, imgsz=320, device="cpu")
metrics = model.val()
print(f"box  mAP50-95: {metrics.box.map:.3f}")   # 0.482 in our run
print(f"mask mAP50-95: {metrics.seg.map:.3f}")   # 0.377 in our run

The 0.1 gap is expected: mask IoU penalizes every boundary pixel, box IoU only the four edges. In our run the largest gap is horse (box 0.564, mask 0.199). Box mAP is good and mask is far below it, so the chart points at outline quality: check those polygons first, then try a larger model or a higher imgsz.

What's next

Next, we turn predicted masks into numbers: areas, outlines, overlaps, and transparent cutouts.