OpenCV 비전 실무3장 · 디지털 영상과 색공간 변환19 / 21← 이전목차다음 →MetaContents

3장. 디지털 영상과 색공간 변환

프로젝트 1 — 색으로 과일 고르고 넓이 비율 재기

과일 가게(가상)의 직원이 진열용 바구니 사진을 보고, 빨강·노랑·초록·보라 과일이 그림에서 각각 몇 %를 차지하는지 알고 싶어 합니다. "빨간 과일이 너무 많으니 다른 색을 더 담자" 같은 판단에 쓰려는 것입니다. 바구니 그림에서 색마다 HSV 마스크를 만들어 넓이 비율을 재고, 표로 정리해 보세요. 수업을 준비하는 강사라면 이 코드를 "색 고르기" 시연 자료로 그대로 쓸 수 있습니다.

쓰는 그림은 fruit_basket.png 입니다. 가게와 그림은 모두 연습용으로 지어낸 것입니다.

색과일H 범위S 범위V 범위
빨강사과 셋0~10, 170~179100~25580~255
노랑바나나 둘20~35100~255100~255
초록라임 둘40~8580~25560~255
보라포도 한 송이125~16060~25550~255

요구사항

  1. 색마다 범위를 사전(색 이름 → 범위 목록)으로 적습니다. 빨강처럼 범위가 둘인 색도 같은 코드로 처리합니다.
  2. 색마다 마스크를 만들고, 흰 칸 수와 그림 전체에서의 비율(%)을 표로 찍습니다. 비율이 큰 색부터 늘어놓습니다.
  3. 네 색을 합친 칸 수와 비율, 어느 색에도 들지 않은 칸의 비율을 함께 찍습니다. 두 색에 겹쳐 잡힌 칸이 있는지도 확인합니다.
  4. 원본, 마스크 네 장, 네 색만 남긴 그림을 2줄 × 3칸으로 이어 붙여 out_ch03_masks.png 로 저장하고 화면에 띄웁니다.

힌트

  1. 범위 사전은 {"빨강": [((0, 100, 80), (10, 255, 255)), ((170, 100, 80), (179, 255, 255))], "노랑": [(...)]} 처럼 색마다 (아래 값, 위 값) 짝의 목록으로 둡니다.
  2. 마스크는 np.zeros(hsv.shape[:2], np.uint8) 로 빈 마스크를 만든 뒤, 범위마다 cv2.inRange 결과를 cv2.bitwise_or 로 더해 갑니다.
  3. 비율은 (mask > 0).mean() * 100 입니다. True 를 1, False 를 0 으로 세어 평균을 내면 흰 칸의 비율이 됩니다.
  4. 겹침은 "색마다 센 칸 수의 합"과 "네 마스크를 OR 로 합친 마스크의 칸 수"를 견주면 알 수 있습니다. 둘이 같으면 겹친 칸이 없습니다.
  5. 흑백 마스크는 cv2.COLOR_GRAY2BGR 로 채널을 셋으로 맞춰야 컬러 그림과 이어 붙일 수 있습니다. 가로로는 cv2.hconcat, 세로로는 cv2.vconcat 으로 붙입니다.

채점 기준

항목기준점수
범위 사전네 색의 범위를 사전으로 적고, 빨강의 두 구간을 같은 코드로 처리한다20
마스크색마다 inRange 와 bitwise_or 로 마스크를 만든다20
비율 표칸 수와 비율을 큰 순서로 표에 찍는다25
합계와 겹침네 색 합계, 나머지 비율, 겹침 여부를 찍는다15
저장과 표시여섯 칸을 한 장으로 붙여 저장하고 띄운다20

코드의 흐름

읽기, 범위 표, 마스크, 비율 표, 저장의 다섯 단계를 이은 흐름도

그림의 다섯 단계를 예시 풀이의 단계와 같은 순서로 짰습니다. 둘째 단계의 범위 표만 바꾸면 다른 색의 과일이나 물건에도 셋째 단계부터의 코드를 그대로 쓸 수 있습니다.

실제 데이터로 해 보려면

실제 과일 사진으로 해 보려면 공개 이미지 자료를 내려받아 씁니다. 예를 들어 AI Hub(aihub.or.kr)에는 농산물 품질이나 과일 생육을 찍은 이미지 데이터가 공개돼 있습니다. 내려받기 전에 데이터마다 이용 약관(상업적 이용 가능 여부, 재배포 금지 등)을 확인하고, 사람이나 가게 간판이 찍힌 사진은 개인정보와 초상권 기준을 먼저 확인합니다. 실제 사진은 조명과 그늘 때문에 같은 과일도 HSV 값이 넓게 퍼지므로, 실습 4처럼 여러 칸의 값을 먼저 읽어 범위를 다시 정합니다.

결과 보기 (예시 풀이)

1단계. 그림을 읽고 범위 사전 적기

Python
import cv2
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False

img = cv2.imread("fruit_basket.png")
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
total = img.shape[0] * img.shape[1]      # 전체 칸 수(세로 × 가로)

# 요구사항 1: 색 이름 → [(아래 [H, S, V], 위 [H, S, V]), ...]. 빨강만 범위가 둘이다
RANGES = {
    "빨강": [((0, 100, 80), (10, 255, 255)), ((170, 100, 80), (179, 255, 255))],
    "노랑": [((20, 100, 100), (35, 255, 255))],
    "초록": [((40, 80, 60), (85, 255, 255))],
    "보라": [((125, 60, 50), (160, 255, 255))],
}
print("전체 칸 수:", f"{total:,}")
for name, ranges in RANGES.items():
    print(f"{name}: 범위 {len(ranges)}개")
출력
전체 칸 수: 76,800
빨강: 범위 2개
노랑: 범위 1개
초록: 범위 1개
보라: 범위 1개

2단계. 색마다 마스크 만들기

Python
def make_mask(hsv, ranges):
    """범위 목록의 inRange 결과를 모두 OR 로 합친 마스크를 돌려준다."""
    mask = np.zeros(hsv.shape[:2], np.uint8)          # 모두 0(검정)인 빈 마스크
    for lo, hi in ranges:
        mask = cv2.bitwise_or(mask, cv2.inRange(hsv, np.array(lo), np.array(hi)))
    return mask

# 요구사항 1, 2: 범위가 하나든 둘이든 같은 함수로 만든다
masks = {name: make_mask(hsv, ranges) for name, ranges in RANGES.items()}
for name, m in masks.items():
    print(f"{name}: 모양 {m.shape}, 값 {[int(v) for v in np.unique(m)]}")
출력
빨강: 모양 (240, 320), 값 [0, 255]
노랑: 모양 (240, 320), 값 [0, 255]
초록: 모양 (240, 320), 값 [0, 255]
보라: 모양 (240, 320), 값 [0, 255]

3단계. 넓이 비율 표 만들기

Python
# 요구사항 2: 색마다 흰 칸 수와 비율(%)
rows = []
for name, m in masks.items():
    count = int((m > 0).sum())
    rows.append({"색": name, "칸 수": count, "비율(%)": round(count / total * 100, 2)})
table = pd.DataFrame(rows).sort_values("비율(%)", ascending=False)
print(table.to_string(index=False))

# 요구사항 3: 네 색 합계, 나머지, 겹침
union = np.zeros(hsv.shape[:2], np.uint8)
for m in masks.values():
    union = cv2.bitwise_or(union, m)
union_count = int((union > 0).sum())
print(f"\n네 색을 합친 칸: {union_count:,}칸 ({round(union_count / total * 100, 2)}%)")
print(f"어느 색에도 들지 않은 칸: {round((total - union_count) / total * 100, 2)}%")
print("색마다 센 칸 수의 합:", int(table["칸 수"].sum()), "→ 겹친 칸:", int(table["칸 수"].sum()) - union_count)
출력
 색  칸 수  비율(%)
빨강 5448   7.09
보라 3513   4.57
초록 2892   3.77
노랑 1980   2.58

네 색을 합친 칸: 13,833칸 (18.01%)
어느 색에도 들지 않은 칸: 81.99%
색마다 센 칸 수의 합: 13833 → 겹친 칸: 0

4단계. 여섯 칸을 한 장으로 붙여 저장하기

Python
def labeled(tile, text):
    """그림 왼쪽 위에 흰 띠와 영어 이름을 적은 복사본을 돌려준다(putText 는 한글을 못 쓴다)."""
    out = tile.copy()
    cv2.rectangle(out, (0, 0), (120, 26), (255, 255, 255), -1)
    cv2.putText(out, text, (6, 19), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 1, cv2.LINE_AA)
    return out

english = {"빨강": "red", "노랑": "yellow", "초록": "green", "보라": "purple"}
# 흑백 마스크는 채널을 셋으로 맞춰야 컬러 그림과 이어 붙일 수 있다
tiles = [labeled(img, "original")]
tiles += [labeled(cv2.cvtColor(m, cv2.COLOR_GRAY2BGR), english[name]) for name, m in masks.items()]
tiles.append(labeled(cv2.bitwise_and(img, img, mask=union), "all four"))

# 요구사항 4: 위 줄 3칸, 아래 줄 3칸을 가로로 붙인 뒤 두 줄을 세로로 붙인다
sheet = cv2.vconcat([cv2.hconcat(tiles[:3]), cv2.hconcat(tiles[3:])])
print("모아 붙인 그림의 모양:", sheet.shape)
print("저장:", cv2.imwrite("out_ch03_masks.png", sheet))
출력
모아 붙인 그림의 모양: (480, 960, 3)
저장: True

5단계. 화면에 띄우기

Python
plt.figure(figsize=(10, 5.2))
plt.imshow(cv2.cvtColor(sheet, cv2.COLOR_BGR2RGB))   # 띄울 때는 RGB 로 바꾼다
plt.title("원본, 색마다의 마스크, 네 색만 남긴 그림")
plt.axis("off")
plt.show()
원본, 빨강·노랑·초록·보라 마스크, 네 색만 남긴 그림을 2줄 3칸으로 붙인 결과

표를 보면 빨강(사과)이 5,448칸으로 그림의 7.09%를 차지해 가장 넓고, 보라(포도) 4.57%, 초록(라임) 3.77%, 노랑(바나나) 2.58% 순입니다. 네 색을 합쳐도 13,833칸, 18.01%이고, 나머지 81.99%는 벽, 탁자, 바구니입니다. 색마다 센 칸 수의 합(13,833)과 네 마스크를 합친 칸 수가 같으므로 두 색에 겹쳐 잡힌 칸은 없습니다. 빨강은 실습 5에서 본 것처럼 0~10 구간 3,667칸과 170~179 구간 1,781칸을 합친 값입니다. 바나나는 사과에 가려 보이는 부분이 적어 가장 좁게 나왔습니다. 이 비율은 그림에서 보이는 넓이이지 과일의 개수나 무게가 아니라는 점을 함께 적어 둡니다. 마스크를 자세히 보면 작은 점도 섞여 있습니다. 포도와 바구니 테두리가 맞닿은 칸 몇 개가 빨강으로 잡혔고, 포도 줄기는 초록으로 잡혔습니다. 두 색이 섞인 테두리 칸은 H 가 그 사이 색이 되기 때문입니다. 이런 점은 비율을 조금 부풀리므로, 9장에서 배우는 방법으로 지우면 더 정확해집니다. 직원은 이 표를 보고 빨간 과일이 노란 과일의 두 배를 넘으니 노란 과일을 더 보이게 담자고 판단할 수 있습니다.

← 이전 절3장 · 실습 5 — inRange 로 빨간 과일 고르기다음 절 →3장 · 정리