박스 개수 세기

Jiyoon·2022년 3월 14일

Machine Learning

목록 보기
2/2

오늘 짤 로직은 주어진 사진에서 박스를 색깔별로 구분해 낼 수 있는 프로그램!
주어진 사진



어떤 기준인지는 모르겠지만 색깔별로 박스가 쳐져있고 색깔 별로 박스 갯수를 세는 프로그램을 짜면 된다!
  • YOLO 모델을 활용한다 하더라도 박스에 대한 정보가 없을 것인데,,, 내가 직접 모델을 학습시켜야 하나,,? → 그러기엔 충분한 데이터 셋이 없음
  • Opencv의 색상 영역 추출, 라벨링을 통해 찾아낼 수 있다



선수 지식 - 흑백 이미지에서의 라벨링

Image grayscale

RGB와 같은 3개의 기준이 아닌 0~255의 흑백 픽셀값, 하나의 기준으로 객체를 분류해야 하기 때문에 이미지를 하나의 기준으로 판단할 수 있게 흑백이미지로 변환한다.

컬러 이미지를 사용하면 비효율적이다

# 이미지 읽어올 때 흑백으로 불러오기
src = cv2.imread('파일명.jpg', cv2.IMREAD_GRAYSCALE)

# 컬러 이미지(RGB)를 흑백이미지(GRAY)로 변환
gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)

Image 이진화(binarization) - 스레시 홀딩(Thresholding)

임계값을 정해서 임계값보다 낮거나 높냐에 따라 0(흑) or 255(백)의 픽셀 값으로 만든다. 말 그래도 이진화해서 바이너리 이미지(binary image)로 만든다

전역 스레시 홀딩

  • numpy로 연산
# --- ① NumPy API로 바이너리 이미지 만들기
thresh_np = np.zeros_like(img)   # 원본과 동일한 크기의 0으로 채워진 이미지
thresh_np[ img > 127] = 255      # 127 보다 큰 값만 255로 변경
  • OpenCV에서 cv2.threshold()함수
# ---② OpenCV API로 바이너리 이미지 만들기
ret, thresh_cv = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY) 
print(ret)  # 127.0, 바이너리 이미지에 사용된 문턱 값 반환
threshold()

ret, out = cv2.threshold(img, threshold, value, type_flag)

  • img: 변환할 이미지
  • threshold: 스레시홀딩 임계값
  • value: 임계값 기준에 만족하는 픽셀에 적용할 값
  • type_flag: 스레시홀딩 적용

  • ret: 스레시홀딩에 사용한 임계값 → threshold 파라미터로 전달한 값과 동일
  • out: 스레시홀딩이 적용된 바이너리 이미지
type_flag
  • cv2.THRESH_BINARY: 픽셀 값이 임계값을 넘으면 value로 지정하고, 넘지 못하면 0으로 지정
  • cv2.THRESH_BINARY_INV: cv.THRESH_BINARY의 반대
  • cv2.THRESH_TRUNC: 픽셀 값이 임계값을 넘으면 value로 지정하고, 넘지 못하면 원래 값 유지
  • cv2.THRESH_TOZERO: 픽셀 값이 임계값을 넘으면 원래 값 유지, 넘지 못하면 0으로 지정
  • cv2.THRESH_TOZERO_INV: cv2.THRESH_TOZERO의 반대

오츠의 이진화 알고리즘

한 번에 임계값을 찾을 수 있는 방법

임계값을 임의로 정해 픽셀을 두 부류로 나누고 두 부류의 명암 분포를 구하는 작업 반복 → 모든 경우의 수 중에서 두 부류의 명암 분포가 가장 균일할 때의 임계값 선택한다.

# cv2.THRESH_OTSU는 임계값을 자동으로 구하는 알고리즘
_, src_bin = cv2.threshold(src, 0, 255, cv2.THRESH_OTSU)

threshold 파라미터는 아무 값이어도 괜찮음

적응형 스레시 홀딩

원본 이미지에서 조명이 일정하지 않거나 배경색이 여러 개인 경우 → 여러개의 임계값이 필요람

addaptiveThreshold()

cv2.adaptiveThreshold(img, value, method, type_flag, block_size, C)

  • img: 입력영상
  • value: 임계값을 만족하는 픽셀에 적용할 값
  • method: 임계값 결정 방법
  • type_flag: 스레시홀딩 적용 방법 (cv2.threshod()와 동일)
  • block_size: 영역으로 나눌 이웃의 크기(n x n), 홀수
  • C: 계산된 임계값 결과에서 가감할 상수(음수 가능)
method
  • cv2.ADAPTIVE_THRESH_MEAN_C: 이웃 픽셀의 평균으로 결정
  • cv2.ADAPTIVE_THRESH_GAUSSIAN_C: 가우시안 분포에 따른 가중치의 합으로 결정

전체 이미지에 총 9개(block_size)의 블록을 설정한다, 이미지를 9등분한다 → 블록별로 임계값 정한다



색상 영역 추출

색상 HSV 형태로 바꾸기

import cv2
import numpy as np

img_color = cv2.imread('test_img.png')
img_hsv = cv2.cvtColor(img_color, cv2.COLOR_BGR2HSV) # 이미지 hsv 색상으로 변환

color_list = [[41, 48, 151],[101, 140, 55], [184, 152, 69]] # BGR 순서로 기입
color_name = ["red", "green", "blue"]

for i, color in enumerate(color_list):

    pixel = np.uint8([[color]]) # 한 픽셀로 구성된 이미지로 변환

    hsv = cv2.cvtColor(pixel, cv2.COLOR_BGR2HSV) 
    hsv_h = int(hsv[0][0][0]) # hue값 -> 색깔 구별(0~360) + numpy.unit8값 Int로 변환
HSV

RGB색 공간보다 좀 더 우리들이 색을 판단하는 과정과 유사한 것이 HSV 색 공간
색 끼리의 조합이 아니라 색깔 자체를 알려주므로 직관성이 좋음 → 색깔을 통해 이미지에서 물체를 검출하고 싶으면 HSV공간이 적합하다

  • Hue(0~360)
    우리가 보는 그대로의 색을 Hue 채널로 나타낸 것
  • Saturation(0~100)
    색의 진하고 연한 정도를 채널로 표현한 것
  • Value(0~100)
    밝기

한 픽셀로 구성된 이미지를 기준으로 각각의 색상을 hsv값으로 바꿔준다

주의할 점은 hsv를 기준으로 색상을 체크할 것이므로 이미지 색상도 cvtColor() 함수를 써서 hsv로 바꿔줘야 한다는 점!

hsv 색상을 기준으로 inRange()함수 parameter 결정

lower = (hsv_h_low, 30, 30) # hsv 이미지에서 바이너리 이미지로 생성 , 적당한 값 30
upper = (hsv_h_high, 255, 255)

hsv로 색상을 변경하면 가장 좋은 점이 inRange()함수를 사용하기 편리하다

이미지를 볼 때 검출하고자 하는 객체희 모든 테두리가 동일한 색상을 가진 픽셀이 아니다

따라서 색상에 대한 범위가 필요한데, hsv를 사용하면 밝기와 진함 정도의 최대 최소는 적당한 값으로 고정시키고 색상(Hue)만 변경해준다. 그러면 그 배경과 객체의 경계점에 있는 흐릿한 픽셀의 색상 검출이 가능하다😀

S와 V값은 고정시키고 H값만 변경

마스크 이미지 만들기

img_mask = cv2.inRange(img_hsv, lower, upper)
img_result = cv2.bitwise_and(img_color, img_color, mask = img_mask)

라벨링을 해주기 위해 방금 정한 범위 내의 픽셀들은 흰색, 나머지는 검은색으로 만들어준다

바이너리 이미지(img_mask)를 마스크로 사용하여 원본이미지에서 범위값에 해당하는 영상부분을 획득한다



라벨링

grassfire algorithm

영어를 그대로 해석하면 잔디 + 불 알고리즘이다, 잔디에 불을 붙였을 때 불이 퍼져나가는 형태로 로직이 처리되기 때문이다.

불을 지핀 곳에서 부터 출발하여 상하좌우를 살핀 후, 원하는 픽셀값을 가진 곳으로 확장해나간다

# return
# cnt : 객체 총 갯수 - 1(배경 제외)
# labels : 각 객체 번호
# stats : x, y, w ,h
# centroids : 각 객체 중심 좌표
cnt, labels, stats, centroids = cv2.connectedComponentsWithStats(src_bin)

for s in range(1, cnt):
        (x, y, w, h, area) = stats[s]

        if area < 30:
            continue

        num += 1
connectedComponentsWithStats()

cv2.connectedComponentsWithStats(image, labels=None, stats=None, centroids=None, connectivity=None, ltype=None) -> retval, labels, stats, centroids

  • image: 8비트 1채널 영상
  • labels: 레이블 맵 행렬. 입력 영상과 같은 크기. numpy.ndarray.
  • stats: 각 객체의 바운딩 박스, 픽셀 개수 정보를 담은 행렬. numpy.ndarray. shape=(N, 5), dtype=numpy.int32.
  • centroids: 각 객체의 무게 중심 위치 정보를 담은 행렬 numpy.ndarray. shape=(N, 2), dtype=numpy.float64.
  • ltype: labels 행렬 타입. cv2.CV_32S 또는 cv2.CV_16S. 기본값은 cv2.CV_32S

  • retval: 객체 수 + 1(배경 포함)
  • labels: 객체에 번호가 지정된 레이블 맵
  • stats: N행 5열, N은 객체 수 + 1이며 각각의 행은 번호가 지정된 객체를 의미, 5열에는 x, y, width, height, area 순으로 정보가 담겨 있다
    x,y 는 좌측 상단 좌표를 의미하며 area는 면적, 픽셀의 수를 의미

해당 메서드로 구한 cnt값을 통해 객체를 둘러보며 area가 일정 수준 이상이면 num값을 올린다

어떤 블로그가 그렇게 하길래,, 한 객체를 이루기 위한 최소한의 픽셀 개수를 지정해줘서 ‘일정 수준 이상의 정확도를 가져야 객체로 사용한다’라고 해석하면 될듯



전체코드

import cv2
import numpy as np

img_color = cv2.imread('test_img.png')
img_hsv = cv2.cvtColor(img_color, cv2.COLOR_BGR2HSV) # 이미지 hsv 색상으로 변환

color_list = [[41, 48, 151],[101, 140, 55], [184, 152, 69]] # BGR 순서로 기입
color_name = ["red", "green", "blue"]

for i, color in enumerate(color_list):

    pixel = np.uint8([[color]]) # 한픽셀로 구성된 이미지로 변환

    hsv = cv2.cvtColor(pixel, cv2.COLOR_BGR2HSV) 
    hsv_h = int(hsv[0][0][0]) # hue값 -> 색깔 구별(0~360) + numpy.unit8값 Int로 변환
    # print(hsv_h)

    if hsv_h >= 10:
        hsv_h_low = hsv_h - 5
    else:
        hsv_h_low = 0

    if hsv_h <= 350:
        hsv_h_high = hsv_h + 5
    else:
        hsv_h_high = 360

    lower = (hsv_h_low, 30, 30) # hsv 이미지에서 바이너리 이미지로 생성 , 적당한 값 30
    upper = (hsv_h_high, 255, 255)

    img_mask = cv2.inRange(img_hsv, lower, upper)
    

    # 라벨링 -> cnt 개수가 객체 개수
    cnt, labels, stats, centroids = cv2.connectedComponentsWithStats(img_mask)

    # print(cnt)
    num = 0

    for s in range(1, cnt):
        (x, y, w, h, area) = stats[s]

        if area < 30:
            continue

        num += 1

    img_result = cv2.bitwise_and(img_color, img_color, mask = img_mask) 

    cv2.imshow(color_name[i] + ' mask', img_mask)
    cv2.imshow(color_name[i] + ' result', img_result)
    print(color_name[i], num)
    

cv2.waitKey(0)
cv2.destroyAllWindows()

이와 같이 잘 검출이 되는 것을 볼 수 있다😊


사실 각각의 색상에서 hue 값을 +/-5 값으로 하고 싶었는데 빨간색의 hue값이 1이라,,, 기준점이 음수가 될 수 없기에,,,이런 특수한 경우에는 따로 hue값을 지정해주었다! 그래서 그런지 빨간색 검출이 유독 문제였음,,,

어쨌든 우여곡절 끝에 완성!

0개의 댓글