Ultralytics YOLO27: 대기자 명단에 등록하세요.
Get Started
강의중급

코드에서 마스크 사용하기: 면적, 겹침, 윤곽, 분리 이미지

masks.data와 masks.xy를 올바르게 읽고, 객체 면적과 채움 비율을 측정하며, 박스 대신 마스크로 겹침을 테스트하고 프레임에서 객체를 분리합니다.

세그멘테이션 모델은 코드가 마스크를 의사 결정에 활용해야 비로소 가치를 발휘합니다. 버스 사진에서 보행자 각각의 박스는 버스 박스와 56~74% 겹치지만, 보행자 마스크와 버스 마스크는 전혀 겹치지 않습니다. 박스를 기반으로 "차량 구역 안의 사람" 알림을 만들면 보도에 서 있는 네 사람 모두에 대해 알림이 울립니다. 마스크는 이 질문에 올바르게 답하지만, 기본 마스크 뷰에는 그럴듯하지만 잘못된 수치를 만드는 크기 및 형식 관련 함정이 있습니다.

학습 성과

각 객체의 마스크 면적, 채움 비율, 윤곽 크기, 다른 객체와의 겹침을 보고하고 객체별 투명 분리 이미지를 저장하는 스크립트를 실행합니다. 각 작업에 맞는 마스크 뷰를 사용하세요.

속성 과정
이미 사용법을 알고 계신다면, 간단히 요약해 드리겠습니다.
  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입니다.

실습형

동일한 마스크를 나타내는 두 가지 뷰#

Ultralytics YOLO 세그멘테이션 마스크로 분리한 사람. 검은 배경의 전체 프레임에서 원래 위치에 표시되어 있습니다

result.masks에는 각 인스턴스 마스크가 두 가지 표현으로 저장됩니다. 다음 코드 줄의 작업에 맞는 표현을 선택하세요. 모든 속성은 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%를 채우지만, 프레임 오른쪽 가장자리에서 잘리고 버스에 붙어 서 있는 두 번째 사람은 31%만 채웁니다.
  • 분리 이미지는 사람이나 버스만 포함하고 다른 것은 포함하지 않는 BGRA PNG이며, 카탈로그, 두 번째 모델 또는 검토 대기열에서 바로 사용할 수 있습니다.

분리 가이드의 자르기 변형은 투명 배경 대신 검은색을 사용합니다.

같은 사람을 경계 박스에 맞춰 자르고 마스크 바깥의 모든 픽셀을 검은색으로 설정한 이미지

겹침: 박스가 놓인 위치#

"사람이 제한 구역 안에 있다" 또는 "제품이 컨베이어를 덮는다"와 같은 규칙은 겹침 테스트입니다. 박스와 마스크를 사용해 같은 테스트를 실행하세요.

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을 사용해 0으로 채운 배열에 폴리곤을 채운 다음, 버스 마스크 대신 사용하세요.

선택 기준

내부에 있는지, 접촉하는지, 덮고 있는지, 가로막는지로 표현되는 규칙은 모두 중첩 검사입니다. 이 검사는 마스크로 실행하세요. 몇 픽셀의 여유가 결과를 바꾸지 않는 근처에 있는지나 왼쪽에 있는지를 판단할 때는 박스로 충분합니다.

픽셀에서 실제 답변으로#

질문연산주의할 점
크기는 얼마나 될까요?mask.sum() × (픽셀당 cm²)알려진 물체 하나를 사용해 픽셀당 cm² 값을 한 번 보정합니다. 카메라가 기울어져 있으면 거리에 따라 면적이 달라집니다. 구역별로 보정하거나 한 영역에서만 측정하세요.
영역에서 차지하는 비율은 얼마나 될까요?클래스 마스크 픽셀 수 ÷ 영역 픽셀 수겹치는 인스턴스를 한 번 또는 두 번 계산할지 결정합니다.
구역 안에 있나요?(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은 모든 윤곽선을 하나의 배열로 합칩니다. 이 배열에 cv2.contourArea을 적용하면 실제로 존재하지 않는 형태의 면적을 측정하게 됩니다. 해결 방법: masks.data의 픽셀 개수로 면적을 계산하고, 폴리곤은 그리기에 사용하세요.

해상도 혼용

증상: 두 번의 실행에서 면적이 약 2.85배 차이 납니다. 원인: 한 번의 실행에서 retina_masks=False을 사용해 면적이 추론 픽셀 단위로 계산되었습니다. 해결 방법: 모든 경우에 retina_masks=True을 사용해 측정하고, CUDA에서는 .numpy() 전에 .cpu()를 호출하세요.

사용해 보기

객체가 세 개 이상 있는 본인 이미지를 사용해 masks.py을 실행하고, 채움 비율이 가장 낮은 객체와 해당 객체의 박스가 크기를 과대평가하는 이유를 설명하세요. retina_masks=True 없이 다시 실행하고 masks.data.shape와 면적이 어떻게 달라지는지 기록하세요. 그런 다음 overlap.py을 수정해 프레임 아래쪽 3분의 1에 그린 사각형과 겹치는 객체가 있는지 테스트하세요.

완료 조건
다음 조건을 모두 충족하면 레슨을 완료한 것입니다.
  • 이미지에서 검출된 객체마다 투명한 PNG 크롭 이미지가 하나씩 있습니다.

  • 채움 비율이 가장 낮은 값을 제시하고, retina_masks=True의 유무에 따라 masks.data이 어떻게 달라지는지 설명할 수 있습니다.

  • 구역 테스트는 모든 객체에 대해 박스 기반 및 마스크 기반 겹침 값을 출력하며, 값이 서로 다른 객체가 있다면 그 이유를 설명할 수 있습니다.

정답 보기

retina_masks=True 없이 버스 이미지에 실행하면 masks.data이 (5, 640, 480) 형태로 반환됩니다. 모든 픽셀 면적은 (1080 × 810) / (640 × 480) ≈ 2.85배 줄어들며, 크롭 이미지의 경우 마스크가 orig_img과 더 이상 정렬되지 않습니다. 이미지의 종횡비가 추론 크기와 다르면 레터박스 패딩으로 인해 정렬이 추가로 어긋납니다.

버스 이미지에서 채움 비율이 가장 낮은 것은 두 번째 사람(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%p 이내로 일치합니다. 버스는 오탐입니다. 박스는 구역에 4% 들어가지만 마스크는 전혀 들어가지 않습니다. "구역 안에 0% 초과" 규칙을 적용하면 박스는 버스를 표시하지만 마스크는 표시하지 않습니다.

다음 단계

다음으로 인스턴스를 완전히 제외하고 시맨틱 세그멘테이션으로 모든 픽셀에 레이블을 지정합니다.