13장. Shape 분석과 ROI 처리
실습 5 — 마스크 ROI 와 ROI 안에서만 처리하기
택배 사진에서 둥근 스티커를 동그라미 마스크와 윤곽선 마스크로 오려 내고, 마스크 안만의 평균 색을 잽니다. 그다음 전화번호 줄만 흐리게 가리고, 송장만 이진화해 원본에 다시 붙입니다. 결과는 out_ch13_processed.png 로 저장합니다.
1단계. 동그라미 마스크와 윤곽선 마스크
import cv2
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
img = cv2.imread("ch13_parcel.png")
# (가) 동그라미 마스크: 스티커의 가운데 (246, 178), 반지름 26 을 흰색으로 채운다
circle_mask = np.zeros(img.shape[:2], dtype=np.uint8)
cv2.circle(circle_mask, (246, 178), 26, 255, -1) # 두께 -1: 속까지 채운다
# (나) 윤곽선 마스크: 주황 스티커를 HSV 로 골라 윤곽선을 찾고 속까지 칠한다
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
orange = cv2.inRange(hsv, (5, 150, 150), (20, 255, 255)) # 3차시의 inRange: 주황 범위
found, _ = cv2.findContours(orange, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
sticker = max(found, key=cv2.contourArea)
contour_mask = np.zeros(img.shape[:2], dtype=np.uint8)
# drawContours(판, [윤곽선], 몇 번째(-1 은 모두), 색, 두께 -1 은 채우기)
cv2.drawContours(contour_mask, [sticker], -1, 255, -1)
for name, m in [("동그라미", circle_mask), ("윤곽선", contour_mask)]:
print(f"{name} 마스크: 흰 칸 {int((m == 255).sum())}")
# cv2.mean 에 mask 를 주면 흰 칸만의 B, G, R 평균
print(" 마스크 안의 평균(B, G, R):", [round(v, 1) for v in cv2.mean(img, mask=m)[:3]])동그라미 마스크: 흰 칸 2121
마스크 안의 평균(B, G, R): [60.5, 132.9, 241.4]
윤곽선 마스크: 흰 칸 2121
마스크 안의 평균(B, G, R): [60.5, 132.9, 241.4]2단계. 마스크를 씌워 오려 내기
fig, axes = plt.subplots(2, 2, figsize=(8, 6))
for col, (name, m) in enumerate([("동그라미 마스크", circle_mask), ("윤곽선 마스크", contour_mask)]):
# bitwise_and(그림, 그림, mask=마스크): 흰 칸만 원래 색, 나머지는 검정
cut = cv2.bitwise_and(img, img, mask=m)
axes[0, col].imshow(m, cmap="gray")
axes[0, col].set_title(name)
axes[1, col].imshow(cv2.cvtColor(cut, cv2.COLOR_BGR2RGB))
axes[1, col].set_title("마스크를 씌운 결과")
for ax in axes.flat:
ax.axis("off")
plt.tight_layout()
plt.show()
3단계. 전화번호 줄만 흐리게, 송장만 이진화해 붙이기
out = img.copy() # 원본은 견주려고 남겨 두고 복사본을 고친다
# (가) 전화번호 줄: 세로 88~107, 가로 64~189 만 크게 흐린다
y0, y1, x0, x1 = 88, 108, 64, 190
out[y0:y1, x0:x1] = cv2.GaussianBlur(out[y0:y1, x0:x1], (15, 15), 0)
# (나) 송장 ROI(세로 40~152, 가로 60~204)만 흑백으로 바꿔 Otsu 이진화(8차시)
ly0, ly1, lx0, lx1 = 40, 153, 60, 205
label_gray = cv2.cvtColor(out[ly0:ly1, lx0:lx1], cv2.COLOR_BGR2GRAY)
t, label_bin = cv2.threshold(label_gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print("송장 ROI 의 Otsu 기준값:", int(t))
print("이진 조각의 모양:", label_bin.shape, "→ 컬러 자리:", out[ly0:ly1, lx0:lx1].shape)
# 흑백 조각(2차원)을 3채널로 바꿔야 컬러 그림의 자리에 붙일 수 있다
out[ly0:ly1, lx0:lx1] = cv2.cvtColor(label_bin, cv2.COLOR_GRAY2BGR)
# 바뀐 칸이 모두 송장 ROI 안에 있는지 확인한다(전화번호 줄도 송장 안에 있다)
changed = np.any(out != img, axis=2)
ys, xs = np.nonzero(changed)
print("바뀐 칸 수:", int(changed.sum()))
print("바뀐 칸의 범위: 세로", int(ys.min()), "~", int(ys.max()), "/ 가로", int(xs.min()), "~", int(xs.max()))송장 ROI 의 Otsu 기준값: 138
이진 조각의 모양: (113, 145) → 컬러 자리: (113, 145, 3)
바뀐 칸 수: 16385
바뀐 칸의 범위: 세로 40 ~ 152 / 가로 60 ~ 2044단계. 견주어 보고 저장하기
print("저장:", cv2.imwrite("out_ch13_processed.png", out))
fig, axes = plt.subplots(1, 2, figsize=(10, 3.8))
for ax, (title, arr) in zip(axes, [("원본", img), ("ROI 안에서만 처리", out)]):
ax.imshow(cv2.cvtColor(arr, cv2.COLOR_BGR2RGB))
ax.set_title(title)
ax.axis("off")
plt.tight_layout()
plt.show()저장: True
코드 설명
cv2.drawContours(판, [윤곽선], -1, 255, -1)은 윤곽선의 속까지 흰색으로 칠해 물체 모양 그대로의 마스크를 만듭니다. 셋째 인자 -1 은 목록의 윤곽선을 모두, 다섯째 인자 -1 은 속까지 채우라는 뜻입니다.- 스티커는 원으로 그렸으므로 동그라미 마스크와 윤곽선 마스크의 흰 칸이 2,121칸으로 같고, 마스크 안의 평균도 B 60.5, G 132.9, R 241.4 로 같습니다. R 이 크고 B 가 작은 주황입니다. 원이 아닌 물체라면 손으로 정한 동그라미는 어긋나고, 윤곽선 마스크는 모양을 그대로 따라갑니다.
out[y0:y1, x0:x1] = cv2.GaussianBlur(...)처럼 원본 쪽 슬라이싱에 대입해야 붙여 넣어집니다. 블러는 크기를 바꾸지 않으므로 바로 붙일 수 있습니다.- 송장 ROI 의 Otsu 기준값은 138 입니다. 앞에서 흐리게 한 전화번호 줄은 글자와 종이가 섞여 이 값보다 밝아져서, 이진화 뒤에는 흰색으로 아예 사라졌습니다. 바뀐 칸은 16,385칸으로 송장 ROI 의 칸 수(145×113)와 같고, 범위도 세로 40~152, 가로 60~204 안이라 상자와 스티커는 원본 그대로입니다.