コードでマスクを使う:面積、重なり、輪郭、切り抜き
masks.dataとmasks.xyを正しく読み取り、オブジェクトの面積と塗りつぶし率を測定し、ボックスではなくマスクで重なりを確認して、フレームからオブジェクトを切り抜きます。
セグメンテーションモデルの価値を引き出せるのは、コードでマスクを判断材料に変えたときです。バスの写真では、各歩行者のボックスはバスのボックスと56~74%重なりますが、歩行者のマスクとバスのマスクの重なりは0%です。ボックスを使って「車両ゾーン内に人がいる」というアラートを作ると、歩道に立つ4人全員に反応してしまいます。マスクなら正しい結果を得られますが、標準のビューにはサイズと形式に関する落とし穴があり、もっともらしく見える誤った数値につながります。
実践的
同じマスクの2つのビュー#

result.masksには、すべてのインスタンスマスクが2つの形式で格納されています。次のコード行で行う処理に応じて選択してください。Resultsリファレンスには、すべての属性が記載されています。
| 属性 | 形状 | 座標 | 用途 |
|---|---|---|---|
masks.data | 0/1のN × H × Wテンソル | デフォルトは推論サイズ、retina_masks=Trueを指定すると元のサイズ | ピクセル面積、マスク間の重なり、切り抜き |
masks.xy | それぞれがP × 2のN個の配列からなるリスト | 元画像のピクセル | 輪郭の描画、ポリゴンの書き出し |
masks.xyn | それぞれがP × 2のN個の配列からなるリスト | 0~1に正規化 | YOLOポリゴンラベルの書き戻し |
1080 × 810のバス画像では、masks.data はレターボックス処理後の推論サイズである (5, 640, 480) として返されるため、ピクセル数は約2.85倍小さくなり、マスクを元画像に重ねられません。retina_masks=True は、わずかな後処理コストでマスクを元の解像度にアップサンプリングします。測定や切り抜きを行う場合は必ず使用してください。
すべてのオブジェクトを測定して切り抜く#
このスクリプトは各オブジェクトについて両方のビューを読み取ります。masks.dataからピクセル面積を、masks.xyから輪郭サイズを取得し、マスクをアルファチャンネルとして使った透明なPNGの切り抜きを作成します。切り抜きのバリエーションについては、セグメンテーションオブジェクトの分離ガイドをご覧ください。
import cv2
import numpy as np
from ultralytics import YOLO
model = YOLO("yolo26n-seg.pt")
result = model("https://ultralytics.com/images/bus.jpg", retina_masks=True)[0] # masks at original resolution
print("masks.data:", tuple(result.masks.data.shape), "orig_shape:", result.orig_shape)
for i, box in enumerate(result.boxes):
name = result.names[int(box.cls)]
mask = result.masks.data[i].cpu().numpy().astype(np.uint8) # H x W, 1 = object
polygon = result.masks.xy[i] # P x 2 outline in original pixels
x1, y1, x2, y2 = box.xyxy[0].int().tolist()
mask_area = int(mask.sum())
box_area = (x2 - x1) * (y2 - y1)
print(f"{name:6s} conf={float(box.conf):.2f} mask={mask_area:7d}px box={box_area:7d}px fill={mask_area / box_area:.2f} polygon={len(polygon)} pts")
# Cut the object out on a transparent background
cutout = np.dstack([result.orig_img, mask * 255])[y1:y2, x1:x2]
cv2.imwrite(f"{i}_{name}.png", cutout)masks.data: (5, 1080, 810) orig_shape: (1080, 810)
person conf=0.88 mask= 49331px box= 97219px fill=0.51 polygon=575 pts
person conf=0.86 mask= 21316px box= 67760px fill=0.31 polygon=408 pts
bus conf=0.86 mask= 255265px box= 409822px fill=0.62 polygon=1172 pts
person conf=0.81 mask= 33524px box= 55842px fill=0.60 polygon=350 pts
person conf=0.64 mask= 11109px box= 23828px fill=0.47 polygon=211 pts出力の確認ポイント:
masks.data: (5, 1080, 810)はorig_shapeと一致しているため、retina_masks=Trueは正しく機能しており、すべてのピクセル数は元画像のピクセル単位です。- インデックス
iが両者を対応付けます。result.masks.data[i]はresult.boxes[i]に属するため、名前と信頼度はボックスから、ピクセルはマスクから取得します。 - 塗りつぶし率から、検出器だけでは分からないことが見えてきます。バスはボックスの62%を占めますが、フレーム右端で切れてバスの前に立つ2人目の人物は31%しか占めません。
- 切り抜き画像は、人物またはバスだけを含み、それ以外のものは含まないBGRA PNGです。カタログ、2つ目のモデル、レビューキューですぐに使えます。
分離ガイドの切り抜きバリエーションでは、透明な背景の代わりに黒い背景を使います。

重なり:ボックスの場合#
「人が制限区域内にいる」や「製品がコンベヤーを覆っている」といったルールは、重なりの判定です。同じ判定をボックスとマスクの両方で実行します。
import numpy as np
from ultralytics import YOLO
result = YOLO("yolo26n-seg.pt")("https://ultralytics.com/images/bus.jpg", retina_masks=True, verbose=False)[0]
names = [result.names[int(c)] for c in result.boxes.cls]
masks = result.masks.data.cpu().numpy().astype(bool) # N x H x W at original resolution
boxes = np.zeros_like(masks)
for i, (x1, y1, x2, y2) in enumerate(result.boxes.xyxy.int().tolist()):
boxes[i, y1:y2, x1:x2] = True # the same objects as filled rectangles
bus = names.index("bus")
for i, name in enumerate(names):
if name == "person":
by_box = (boxes[i] & boxes[bus]).sum() / boxes[i].sum()
by_mask = (masks[i] & masks[bus]).sum() / masks[i].sum()
print(f"person {i}: {by_box:4.0%} of box overlaps the bus box, {by_mask:4.0%} of mask overlaps the bus mask")person 0: 67% of box overlaps the bus box, 0% of mask overlaps the bus mask
person 1: 69% of box overlaps the bus box, 0% of mask overlaps the bus mask
person 3: 74% of box overlaps the bus box, 0% of mask overlaps the bus mask
person 4: 56% of box overlaps the bus box, 0% of mask overlaps the bus mask人物はバスの前に立っているため、バスのマスクは各人物の輪郭で止まり、マスク同士に共有ピクセルはありません。ボックスは両者を囲む長方形なので、大きく重なります。同じ計算は、独自に描画したあらゆる領域にも使えます。cv2.fillPolyでゼロ配列にポリゴンを塗りつぶし、バスマスクの代わりに使用します。
内部にある、触れている、覆っている、ふさいでいるという表現を使う判定ルールは、すべて重なりの判定です。マスクを使ってください。数ピクセルのずれで答えが変わらない近くにあるや左にあるの判定には、ボックスで十分です。
ピクセルから実世界の答えへ#
| 質問 | 計算 | 注意点 |
|---|---|---|
| どれくらいの大きさですか? | mask.sum() ×(ピクセルあたりの cm²) | 既知の物体を使って、ピクセルあたりの cm² を一度校正します。カメラが斜めの場合、面積は距離によって変化するため、ゾーンごとに校正するか、1つの領域だけで測定します。 |
| 領域のどのくらいが覆われていますか? | クラスマスクのピクセル数 ÷ 領域のピクセル数 | 重なり合うインスタンスを1回または2回カウントするかを決めます。 |
| ゾーン内にありますか? | (mask & zone).sum() / mask.sum() | 元の画像サイズでゾーンを描画します。 |
| 輪郭はどのような形状ですか? | masks.xy[i] | 面積を計算する前に、以下の分割オブジェクトに関する警告を確認してください。 |
よくある落とし穴#
症状: オブジェクトのないフレームで AttributeError: 'NoneType' object has no attribute 'xy' が発生します。原因: モデルが何も検出しない場合、result.masks は空のオブジェクトではなく None になります。対処: result.boxes をループしてください(セグメンテーションモデルでは空であり、None にはなりません)。ループ内でマスクをインデックス指定する方法は masks.py のとおりです。または、先に result.masks is None を確認してください。
症状: 遮蔽されたオブジェクトの面積が大幅に誤っています。原因: 遮蔽物がオブジェクトを分断すると、masks.xy はすべての輪郭を1つの配列にまとめます。その配列に対する cv2.contourArea は、実在しない形状を測定します。対処: 面積は masks.data のピクセル数から計算し、ポリゴンは描画に使ってください。
症状: 2回の実行で面積が約2.85倍異なります。原因: 片方の実行では retina_masks=False が使われ、面積が推論時のピクセル単位になっています。対処: 常に retina_masks=True で測定し、CUDAでは .numpy() の前に .cpu() を呼び出してください。
解答を表示
retina_masks=True を指定しない場合、バスの画像では形状が (5, 640, 480) の masks.data が返されます。ピクセル面積はすべて (1080 × 810) / (640 × 480) ≈ 2.85 分の1に縮小し、切り抜き画像用の orig_img とマスクの位置が合わなくなります。画像のアスペクト比が推論時の形状と異なる場合は、レターボックスのパディングによってさらに位置ずれが発生します。
バスの画像で塗りつぶし率が最も低いのは2人目の人物(0.31)です。フレームの右端で切れており、バスにもたれかかっているため、バウンディングボックスの大半が背景とバスを占めています。
ゾーンについては、元のサイズで一度作成し、マスクテストを再利用します。
import cv2
h, w = result.orig_shape
zone = np.zeros((h, w), np.uint8)
cv2.fillPoly(zone, [np.array([[0, 2 * h // 3], [w, 2 * h // 3], [w, h], [0, h]])], 1)
zone = zone.astype(bool)
for i, name in enumerate(names):
print(f"{name} {i}: box {(boxes[i] & zone).sum() / boxes[i].sum():.0%}, mask {(masks[i] & zone).sum() / masks[i].sum():.0%}")バスの画像では:
person 0: box 36%, mask 29%
person 1: box 33%, mask 37%
bus 2: box 4%, mask 0%
person 3: box 31%, mask 21%
person 4: box 47%, mask 53%人物の脚は実際に下部3分の1にあるため、バウンディングボックスとマスクの結果は約10ポイント以内で一致します。誤検出となるのはバスです。バウンディングボックスはゾーンに4%入り込みますが、マスクはまったく入りません。「0%より多くが内側」というルールでは、バウンディングボックスはバスを検出し、マスクは検出しません。