Ultralytics YOLO27: ウェイトリストに登録する。
Get Started
レッスン中級

コードでマスクを使う:面積、重なり、輪郭、切り抜き

masks.dataとmasks.xyを正しく読み取り、オブジェクトの面積と塗りつぶし率を測定し、ボックスではなくマスクで重なりを確認して、フレームからオブジェクトを切り抜きます。

セグメンテーションモデルの価値を引き出せるのは、コードでマスクを判断材料に変えたときです。バスの写真では、各歩行者のボックスはバスのボックスと56~74%重なりますが、歩行者のマスクとバスのマスクの重なりは0%です。ボックスを使って「車両ゾーン内に人がいる」というアラートを作ると、歩道に立つ4人全員に反応してしまいます。マスクなら正しい結果を得られますが、標準のビューにはサイズと形式に関する落とし穴があり、もっともらしく見える誤った数値につながります。

成果

各オブジェクトのマスク面積、塗りつぶし率、輪郭サイズ、別のオブジェクトとの重なりを出力するスクリプトを実行し、それぞれを透明な背景で切り抜いて保存します。それぞれの用途に適したマスクビューを使います。

短期集中コース
すでに使い方をご存じの方は、こちらをご覧ください。
  1. 測定や切り抜きを行うときは、マスクを元画像のサイズに合わせるため、retina_masks=Trueで推論します。

  2. 面積、重なり、切り抜きにはmasks.data(N × H × W)を使い、輪郭の描画や書き出しにはmasks.xy(ピクセル単位のポリゴン)を使います。

  3. 面積はmask.sum()、塗りつぶし率はマスク面積 ÷ ボックス面積、重なりは(mask_a & mask_b).sum()です。

  4. マスクを読み取る前にresult.masks is Noneを確認してください。何も検出されなかった場合はNoneです。

実践的

同じマスクの2つのビュー#

Ultralytics YOLOのセグメンテーションマスクで人物を分離し、黒い背景のフルフレーム内に配置した例

result.masksには、すべてのインスタンスマスクが2つの形式で格納されています。次のコード行で行う処理に応じて選択してください。Resultsリファレンスには、すべての属性が記載されています。

属性形状座標用途
masks.data0/1のN × H × Wテンソルデフォルトは推論サイズ、retina_masks=Trueを指定すると元のサイズピクセル面積、マスク間の重なり、切り抜き
masks.xyそれぞれがP × 2のN個の配列からなるリスト元画像のピクセル輪郭の描画、ポリゴンの書き出し
masks.xynそれぞれがP × 2のN個の配列からなるリスト0~1に正規化YOLOポリゴンラベルの書き戻し
masks.dataはデフォルトで推論時のサイズになる

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() を呼び出してください。

試してみる

masks.py を、少なくとも3つのオブジェクトが写った自分の画像で実行し、塗りつぶし率が最も低いオブジェクトと、そのバウンディングボックスがサイズを過大評価する理由を報告します。retina_masks=True を使わずに再実行し、masks.data.shape と面積がどのように変わるかを記録します。次に、overlap.py を調整して、画像内のオブジェクトがフレーム下部3分の1に描いた長方形と重なるかどうかをテストします。

完了条件
以下のすべてを満たすと、レッスンは完了です。
  • 画像内の検出された各オブジェクトについて、背景が透明なPNGの切り抜き画像が1枚ずつあります。

  • 塗りつぶし率が最も低い値を示し、masks.data と retina_masks=True の有無による違いを説明できます。

  • ゾーンテストでは、すべてのオブジェクトについてバウンディングボックスベースとマスクベースの重なりが出力され、結果が異なるオブジェクトについて説明できます。

解答を表示

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%より多くが内側」というルールでは、バウンディングボックスはバスを検出し、マスクは検出しません。

次のステップ

次に、インスタンスを完全に扱わず、セマンティックセグメンテーションですべてのピクセルにラベルを付けます。