본문 바로가기

공부

[Open CV] 색상 곰 탐지 알고리즘 다양한 방법으로 풀기 cv2.HoughCircles, bfs

반응형

 

컴퓨터 비전

파이썬은 배열 처리에 유리하며, numpy는 다차원 배열을 위한 모듈이다.

OpenCV는 영상을 numpy.ndarray로 표현한다.

 

설치 방법

python -m pip install opencv-python

 

색상 공 탐지 및 인식 알고리즘

1. 이미지 데이터 생성

- 그림판이나 OpenCV를 이용하여 빨간색, 파란색, 녹색의 원을 5개 이상 만든다.

- 영상의 크기는 640 x 480 으로 함

 

우리 팀원중에 한명이 아래와 같이 이미지를 만들어줬다.

 

2. 객체 탐지 및 인식

- 1번 이미지에서 생성된 원을 탐지 및 인식함

- 탐지된 원에는 사각형 박스를 씌어주고, 글자 출력하여 색상 정보를 글자로 알려줌

 

그렇다면, 어떻게 원을 탐지하고, 그 색상을 인지해서 알려줄까?

 

다양한 접근 방법

1. R, G, B 채널을 각각 따로 해서, 원을 탐지하고, 나중에 합치기

2. R, G, B의 채널을 함께 OpenCV의 HoughCircles 함수를 활용하여 원 인식 후, 색상 text 넣기

 

Python+OpenCV] 이미지에서 원형 도형 검출(Hough Circle Transform)

Hough Transform 알고리즘은, 수학적 모델링이 가능한 모든 도형을 이미지에서 검출할 수 있는 방법이다.

원형에 수학적 모델 식을 이용해 허프 변환(Hough Transform)을 적용할 수 있는데, 원에 대한 수학식이 중심점(x, y)와 반지름(r)이라는 3개의 매개변수로 구성되어 있고, 결국 3차원 배열이라는 저장소 요구 및 연산이 비효율적이다.

이에 대한 개선으로 Gradient(가장자리에서의 기울기값)을 이용하여 Hough Transform 을 적용할 수 있어 허브 그래디언트 방법을 사용한다. OpenCV에서는 cv2.HoughCircles 함수를 제공한다.

 

아래는 OpenCV에서의 cv2.HoughCircles 함수의 예시이다.

import numpy as np
import cv2 as cv
 
img = cv.imread('[이미지_주소]', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
img = cv.medianBlur(img,5)
cimg = cv.cvtColor(img,cv.COLOR_GRAY2BGR)
 
circles = cv.HoughCircles(img,cv.HOUGH_GRADIENT,1,20,
                            param1=50,param2=30,minRadius=0,maxRadius=0)
 
circles = np.uint16(np.around(circles))
for i in circles[0,:]:
    # draw the outer circle
    cv.circle(cimg,(i[0],i[1]),i[2],(0,255,0),2)
    # draw the center of the circle
    cv.circle(cimg,(i[0],i[1]),2,(0,0,255),3)
 
cv.imshow('detected circles',cimg)
cv.waitKey(0)
cv.destroyAllWindows()

 

그러면 아래와 같이 결과값이 나온다.

 

cv2.HoughCircles 함수 인자

- 첫번째 : 입력 이미지로서, 8비트 단일 채널인 Grayscale 이미지만 받는다.

=> 이미지를 처음에 색상이 있는 인자로 두고 받았더니 에러가 떴다.

Grayscale 만 가능한 것 같다.

- 두번째 : cv2.HOUGH_GRADIENT 만 지원

- 세번째 : 대부분 1을 지정하는데, 이 경우 입력 이미지와 동일한 해상도가 사용된다.

- 네번째 : 검출한 원의 중심과의 최소거리 값으로 이 최소값보다 작으면 원으로 판별이 안된다.

- param1 : Canny Edge 에 전달되는 인자값

- param2 : 검출 결과를 보며 적당히 조정해야 하는 값, 작으면 오류가 높고, 크면 검출률이 낮아짐

- minRadius : 원의 최소 반지름

- maxRadius : 원의 최대 반지

 

해당 함수를 활용하여 원을 검출하고, 그 원의 색상을 탐지해보는 코드는 아래와 같다.

 

1. 라이브러리 import

import cv2
import numpy as np

 

2. 색상 값과 딕셔너리 지정

colors_dict = { (255, 0, 0) : "Blue",
               (0, 255, 0) : "Green",
               (0, 0, 255): "Red" }

 

3. 이미지 불러오기

cv2.IMREAD_COLOR 를 불러오면, 이미지에 값에는 color가 존재한다.

그러나 cv2.HoughCircles 함수에 color_image를 인자값으로 넣었다면, 에러가 뜬다.

그래서 image를 불러올 때, cv2.IMREAD_GRAYSCALE 의 인자로 그레이스케일 값으로 처리해준다.

color_image = cv2.imread(image_path, cv2.IMREAD_COLOR)
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

 

4. cv2.HoughCircles 함수 사용하여 값을 받아온다.

int의 값으로 바꿔서 반내림 해줌

circles = np.uint16(cv2.HoughCircles(image, cv2.HOUGH_GRADIENT, 1, image.shape[0] / 8, param1=100, param2=10, minRadius=0, maxRadius=100))

 

circle의 값은 x 좌표의 중심값, y좌표의 중심값, 그리고 반지름으로 구성된 원들의 다중 리스트가 나온다.

 

5. 사각형과 텍스트를 그려주는 코드를 작성해준다.

center 값에는 x, y 좌표의 중심값을 넣어주고,

radius 반지름의 값은 3번째 인자인 i[2]의 값을 넣어준다.

 

cv2.ractangle 의 함수로, 이미지에 검정색 사각형을 그려준다.

 

색상 color는 튜플로 선언해서, color_image의 x, y좌표의 중심값의 값을 넣어준다.

그리고 해당 값을 colorstr로 치환해서 색상을 표시해주도록 한다.

 

그리고 cv2.putText로 값을 그림에 그려주면 된다.

for i in circles[0]:
    center = (i[0], i[1])
    radius = i[2]
    cv2.rectangle(color_image, (center[0]-radius , center[1]-radius, radius*2, radius*2) , black, 1)
    color = tuple(color_image[center[1], center[0]])
    colorstr = colors_dict[color]
    cv2.putText(color_image, colorstr,  (center[0]-radius , center[1]-radius) , 1 , 1, black)

 

전체 코드는 아래와 같다.

import cv2
import numpy as np

black = (0, 0, 0)

colors_dict = { (255, 0, 0) : "Blue",
               (0, 255, 0) : "Green",
               (0, 0, 255): "Red" }

image_path = r"이미지 주소"

color_image = cv2.imread(image_path, cv2.IMREAD_COLOR)
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
circles = np.uint16(cv2.HoughCircles(image, cv2.HOUGH_GRADIENT, 1, image.shape[0] / 8, param1=100, param2=10, minRadius=0, maxRadius=100))

for i in circles[0]:
    center = (i[0], i[1])
    radius = i[2]
    cv2.rectangle(color_image, (center[0]-radius , center[1]-radius, radius*2, radius*2) , black, 1)
    color = tuple(color_image[center[1], center[0]])
    colorstr = colors_dict[color]
    cv2.putText(color_image, colorstr,  (center[0]-radius , center[1]-radius) , 1 , 1, black)

cv2.imshow("ball", color_image)
cv2.waitKey(0)

 

결과값

 

 

 

3. 알고리즘을 활용하여 0, 0 부터 끝까지 진행하면서 원을 발견하면 그 원을 탐지

이 방식은 생각을 할 수는 있는데, 구현하는게 정말 어려운 방식이라서 아무도 못했을거라고 생각한 방식이다.

그러나 내 앞자리 코딩 천재 승혁님이 구상과 구현을 마무리하고 발표까지,,

그래서 코드를 받았다. (감사합니다.)

 

1. 앞의 선언은 비슷하다.

import numpy as np
import cv2

color_dict = {
    "blue":  [255, 0, 0],
    "green": [0, 255, 0],
    "red":   [0, 0, 255]
}

height, width = 640, 480
full_visited_indexs = [[False]*width for _ in range(height)]

 

2. 나는 팀원이 그림판으로 그려줬는데, 그것까지 구현해주셨다.(존경..)

height, width = 640, 480
image = np.full((height, width, 3), (255,255,255), np.uint8)

full_visited_indexs = [[False]*width for _ in range(height)]

pt1, pt2, pt3, pt4, pt5 = (120, 150), (200, 150), (120, 370), (400, 300), (340, 200)

cv2.circle(image, pt1, 30, color_dict["red"],   -1)
cv2.circle(image, pt2, 30, color_dict["blue"],  -1)
cv2.circle(image, pt3, 30, color_dict["blue"],  -1)
cv2.circle(image, pt4, 30, color_dict["red"],   -1)
cv2.circle(image, pt5, 30, color_dict["green"], -1)

 

3. 색상을 찾아주는 함수

def find_color(x, y):
    pixel_value = image[y, x].tolist()
    for key, value in color_dict.items():
        if value == pixel_value:
            return key
    return None

 

4. bfs로 진행

image를 픽셀로 생각해서, dxs, dys를 선언해주었고,

queue로 처음시작 x, y의 값을 넣어주고, 해당 index에 방문을 했다고 해준다.

 

queue를 돌면서, 해당 x, y 값을 변경시켜준다.

만약에 해당 index에 방문하지 않았고, 색상이 원하는 값과 같다면 queue에 nx, ny를 추가해서

색상 원이 그려지는 것이 끝날때까지 반복한다.

dxs = [1, 0, -1, 0]
dys = [0, 1, 0, -1]

def bfs(start_x, start_y, target_color):
    queue = [(start_x, start_y)]
    full_visited_indexs[start_y][start_x] = True
    visited_points = []

    while queue:
        x, y = queue.pop(0)
        visited_points.append((x, y))

        for dx, dy in zip(dxs, dys):
            nx, ny = x + dx, y + dy
            if 0 <= nx < width and 0 <= ny < height:
                if not full_visited_indexs[ny][nx]:
                    if find_color(nx, ny) == target_color:
                        full_visited_indexs[ny][nx] = True
                        queue.append((nx, ny))

    return visited_points

 

5. 해당 박스를 찾기

색상을 찾고, 해당 색상이 원하는 타겟 색상에 존재한다면,

추가로 index에 처음 방문하는 것이라면 bfs 함수 실행

그 return 값을 상하좌우 좌표값을 boxs 안에 넣어둔다.

def draw_box():
    count = 0
    for y in range(height):
        for x in range(width):
            color = find_color(x, y)
            if color in ["red", "blue", "green"]:
                if not full_visited_indexs[y][x]:
                    visited_indexs = bfs(x, y, color)
                    min_x = min(pt[0] for pt in visited_indexs)
                    max_x = max(pt[0] for pt in visited_indexs)
                    min_y = min(pt[1] for pt in visited_indexs)
                    max_y = max(pt[1] for pt in visited_indexs)
                    boxes.append((min_x, min_y, max_x, max_y, color))
                    count += 1
    print("총 색영역 개수:", count)

 


6. 텍스트와 네모 표시

boxes 값에 저장해두었던 것을 꺼내와서

cv2.rectangle, cv2.putText 함수를 사용해서 그려준다.


boxes = []
def draw_rec_text(boxes):
    for box in boxes:
        min_x, min_y, max_x, max_y, color = box
        if color == "red":    
            cv2.rectangle(image, (min_x, min_y), (max_x, max_y), (0,0,255), 1)
            cv2.putText(image, color, (min_x, min_y), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255), 1)
        if color == "green":
            cv2.rectangle(image, (min_x, min_y), (max_x, max_y), (0,255,0), 1)
            cv2.putText(image, color, (min_x, min_y), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,255,0), 1)
        if color == "blue":
            cv2.rectangle(image, (min_x, min_y), (max_x, max_y), (255,0,0), 1)
            cv2.putText(image, color, (min_x, min_y), cv2.FONT_HERSHEY_SIMPLEX, 1, (255,0,0), 1)

 

 

전체 코드

import numpy as np
import cv2

color_dict = {
    "blue":  [255, 0, 0],
    "green": [0, 255, 0],
    "red":   [0, 0, 255]
}

height, width = 640, 480
full_visited_indexs = [[False]*width for _ in range(height)]

image_path = r"이미지_경로"

image = cv2.imread(image_path, cv2.IMREAD_COLOR)
image = cv2.resize(image, (width, height)) 

def find_color(x, y):
    pixel_value = image[y, x].tolist()
    for key, value in color_dict.items():
        if value == pixel_value:
            return key
    return None

dxs = [1, 0, -1, 0]
dys = [0, 1, 0, -1]

def bfs(start_x, start_y, target_color):
    queue = [(start_x, start_y)]
    full_visited_indexs[start_y][start_x] = True
    visited_points = []

    while queue:
        x, y = queue.pop(0)
        visited_points.append((x, y))

        for dx, dy in zip(dxs, dys):
            nx, ny = x + dx, y + dy
            if 0 <= nx < width and 0 <= ny < height:
                if not full_visited_indexs[ny][nx]:
                    if find_color(nx, ny) == target_color:
                        full_visited_indexs[ny][nx] = True
                        queue.append((nx, ny))

    return visited_points

boxes = []
def draw_rec_text(boxes):
    for box in boxes:
        min_x, min_y, max_x, max_y, color = box
        if color == "red":    
            cv2.rectangle(image, (min_x, min_y), (max_x, max_y), (0,0,255), 1)
            cv2.putText(image, color, (min_x, min_y), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255), 1)
        if color == "green":
            cv2.rectangle(image, (min_x, min_y), (max_x, max_y), (0,255,0), 1)
            cv2.putText(image, color, (min_x, min_y), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,255,0), 1)
        if color == "blue":
            cv2.rectangle(image, (min_x, min_y), (max_x, max_y), (255,0,0), 1)
            cv2.putText(image, color, (min_x, min_y), cv2.FONT_HERSHEY_SIMPLEX, 1, (255,0,0), 1)

def draw_box():
    count = 0
    for y in range(height):
        for x in range(width):
            color = find_color(x, y)
            if color in ["red", "blue", "green"]:
                if not full_visited_indexs[y][x]:
                    visited_indexs = bfs(x, y, color)
                    min_x = min(pt[0] for pt in visited_indexs)
                    max_x = max(pt[0] for pt in visited_indexs)
                    min_y = min(pt[1] for pt in visited_indexs)
                    max_y = max(pt[1] for pt in visited_indexs)
                    boxes.append((min_x, min_y, max_x, max_y, color))
                    count += 1
    print("총 색영역 개수:", count)

if __name__ == "__main__":
    draw_box()
    draw_rec_text(boxes)
    cv2.imshow("Draw Circle", image)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

 

결과값은 아래와 같다.

 

 

하나의 문제를 해결하는데에 있어서

정말 다양하게 접근하여 문제를 풀어나갈 수 있다는 점이 재미있었다.

나 혼자 해결하고자 했으면 하나의 방법만 생각하고 넘어갔을텐데

여러 사람과 함께 동일한 문제를 풀어가려고 하니 확실히 다양한 관점으로 배울 수 있어서 좋았다

 

 

 

출처 및 참고

- OpenCV 공식 문서 : https://docs.opencv.org/4.x/da/d53/tutorial_py_houghcircles.html

- https://stackoverflow.com/questions/50568668/understanding-houghcircles-in-python-opencv-cv2

- https://docs.opencv.org/3.4/d4/d70/tutorial_hough_circle.html

- 승혁님의 코드

 

 

 

 

반응형