본문 바로가기

IT/OPENCV

[OPENCV] 11. 히스토그램(Histogram) - calcHist

반응형

☞ 운영 체제 : Linux Ubuntu 20.04.5 LTS - focal
☞ CPU : AMD Ryzen 7 5800H with Radeon Graphics (16 CPUs), ~3.2GHz
☞ 그래픽카드 : NVIDIA GeForce RTX3070 Laptop GPU

☞ IDE : Visual Studio Code

☞ 언어 : Python

 

 

 

 


목차

1. 히스토그램이란?

2. 객체 감지 & calcHist 함수 적용하기

3. 실행 결과

 

 

 


 

 

 

 

이전부터 YOLO라는 객체 탐지 알고리즘으로 이미지의 물체를 찾아내거나(Detection) 분류하거나(Classfiy) 추적하거나(Tracking) 분할 (Segmentation) 그리고 포즈 추정까지(Pose estimation) 전반적인 모델을 지원하고 사용자가 원하는대로 직접 학습을 시킬 수 있게까지 됐다. 처음 접한 것은 3버전이였고 현재는 설치과정이 간단해주지고 Python 모듈로 사용할 수 있는 8버전까지 있다.

 

사실 YOLO 같이 (이미 우리가 생각한 것들을 코드로 작성한) 오픈소스가 있어 OPENCV를 이용한 고전 알고리즘을 구현할 필요가 없어진 것이 사실이다. 그러나 간단히 구현하거나 모델을 만들어서 하지 않아도 되는 정도의 정확도라면 굳이 모델을 만드는 귀찮은 일을 할 필요가 없다.

 

그중에서 모델을 사용하지 않고 사용했던 히스토그램 함수가 생각나서 소개해보려 한다.

 

 

 

① 히스토그램이란?

OPENCV는 카메라 렌즈 혹은 그래픽으로 얻을 수 있는 정보를 바탕으로 픽셀의 수(해상도), 픽셀의 색상, 픽셀의 패턴 등을 통해 다양한 관점에서 분석할 수 있도록 다양한 기능을 제공한다. 이러한 기능 중에서 히스토그램은 그램으로 끝나는 말로 스펙트로그램, 밴 다이어 그램, 인스타 그램(?) 등 시각화(혹은 그래픽) 기법 중 하나이다. 또한, 이미지의 각 픽셀 값 분포를 시각화하여 보여주고 밝기, 색상, 대비와 같은 정보를 제공해준다.

 

대표적으로 평활화(Equalization), 매칭(Matching), 역투영(Backprojection)이 있는데 오늘은 어떠한 기법이라기보다는 calcHist라는 함수를 사용해서 이미지로 부터 얻은 색상 분포를 통해 분류하는 것을 해보려한다.

 

 


② 객체 감지 & calcHist 함수 적용하기

시작하기 앞서 간단히 본 작업 내용에 대해서 미리 설명하고 가려고 한다.

 

이미지는 풋살장에서 풋살을 하고 있는 선수들을 카메라로 촬영했고, 팀별로 색이 다른 조끼를 착용하고 있다. 여기서 먼저 YOLOv8을 사용하여 객체를 감지한다. 이때, 감지해서 나온 bounding box를 크롭하고 변형해서 그 박스 안의 색 정보를 알아낸다. calHist 함수를 통해 색상 분포 값을 얻어내고 해당 범위를 통해 조끼 색이 어떤 색인지 유추해낸다. 이를 통해 해당 선수가 어떤 팀인지 알아낼 수 있다.

 

오늘 사용할 이미지다. (파노라마 1080p 이미지)

참가자의 소중한 데이터 사용으로 모자이크 적용

 

 

 

YOLOv8 모델

보통은 감지할 물체를 정해서 이미지를 학습하여 모델을 사용하지만, YOLOv8에서 제공하는 모델로도 충분히 감지를 할 수가 있다. 먼저 YOLOv8 모델을 불러오고 bounding box를 만들기 위한 좌표를 얻어내보자.

 

 

import os
import cv2

from ultralytics import YOLO

# 필자의 경로 -> 본인에게 맞게 수정하여 사용바람
WORKSPACE_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
YOLO_ROOT = os.path.join(WORKSPACE_ROOT, 'ultralytics')

# 오늘 사용하는 이미지 경로가 담긴 경로
image = os.path.join('images', 'frame.png')

# m 모델을 사용
DETECT_MODEL = os.path.join(YOLO_ROOT, 'yolov8m.pt')

# 모델 로드
detect_model = YOLO(DETECT_MODEL)

# bounding box를 그리기 위한 이미지 객체
image = cv2.imread(image)

detect_results = detect_model.predict(image, verbose=False, save=False, imgsz=1088, conf=0.3, device='cuda:0')
        
detect_x1 = detect_results[0].boxes.data[:, 0].int().cpu().tolist()
detect_y1 = detect_results[0].boxes.data[:, 1].int().cpu().tolist()
detect_x2 = detect_results[0].boxes.data[:, 2].int().cpu().tolist()
detect_y2 = detect_results[0].boxes.data[:, 3].int().cpu().tolist()
detect_conf = detect_results[0].boxes.data[:, 4].float().cpu().tolist()
detect_cls = detect_results[0].boxes.data[:, 5].int().cpu().tolist()

# 감지된 물체들의 정보를 출력
print(detect_results[0].boxes.data.int().cpu().tolist())

# 감지된 물체의 좌표를 이용하여 bounding box를 그리기
for i in range(len(detect_x1)):
    cv2.rectangle(image, (detect_x1[i],detect_y1[i]), (detect_x2[i],detect_y2[i]), (255,0,0), 2)

# 물체에 bounding box가 그려진 이미지를 확인할 수 있음
cv2.imshow("result", image)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

 

 

☞ 코드 실행 결과

 

 

기존 m 감지 모델을 사용했을 때 사람, 공이 잡히는 것을 볼 수 있다. 

 

print(detect_results[0].boxes.data.int().cpu().tolist()) 출력 결과로는 다음과 같은 2차원 리스트를 얻을 수 있다. 감지된 수 만큼 리스트가 생기며 이는 순서대로 이미지 내의 픽셀 값인 x1, y1, x2, y2, conf, cls 값이다.

 

[[477, 436, 562, 529, 0, 0], [508, 586, 622, 697, 0, 0], [1414, 632, 1522, 764, 0, 0], [1461, 345, 1517, 408, 0, 0], [1017, 260, 1055, 342, 0, 0], [816, 213, 862, 288, 0, 0], [572, 285, 627, 369, 0, 0], [970, 244, 1008, 326, 0, 0], [1181, 242, 1215, 323, 0, 0], [1332, 263, 1368, 332, 0, 0], [1002, 168, 1027, 240, 0, 0], [1121, 415, 1137, 433, 0, 32], [450, 220, 499, 280, 0, 0], [319, 366, 369, 432, 0, 0], [1408, 741, 1450, 768, 0, 36], [284, 378, 322, 435, 0, 0]]

 

 

conf 값의 경우 0과 1사이의 값을 가지므로 float 타입으로 출력해야 값을 확인할 수 있다. 마지막 인덱스의 0은 사람, 32는 공을 의미한다. 여기서 공은 필요하지 않으니 사람 cls값인 0만 받는 것으로 하려면, detect_cls 값이 0일 때만 출력하도록 조건문을 추가해주면 된다.

 

 

 

bounding box값 사용

이제 이미지에서 얻은 사람들의 bounding box를 크롭해오자. 그리고 사람의 좌표만을 가져올 수 있도록 위해서 얘기한 detect_cls 값이 0일 때의 조건문을 추가하자.

 

 

 

☞ 사람만 감지하기

 

 

for문에 조건문 하나만 넣어주면 0번(사람) 클래스 외에는 bounding box가 그려지지 않는다.

 

 

for i in range(len(detect_x1)):
    if detect_cls[i] == 0:
        cv2.rectangle(image, (detect_x1[i],detect_y1[i]), (detect_x2[i],detect_y2[i]), (255,0,0), 2)

 

 

 

☞ bounding box 크롭

for i in range(len(detect_x1)):
    if detect_cls[i] == 0:
        # 주석 처리 - 히스토그램에 영향을 줄 수 있음
        # cv2.rectangle(image, (detect_x1[i],detect_y1[i]), (detect_x2[i],detect_y2[i]), (255,0,0), 2)
		
        # 이미지 크롭
        cropped_image = image[detect_y1[i]:detect_y2[i], detect_x1[i]:detect_x2[i]]

		# 이미지 출력 + 번호별로 저장
        cv2.imshow("cropped image", cropped_image)
        cv2.imwrite(f"cropped_image_{i}.png", cropped_image)
        cv2.waitKey(0)

 

 

cropped_image를 통해 크롭한 bounding box의 이미지를 확인하고 이를 저장할 수 있다. 저장한 이미지는 본인이 작성한 코드와 같은 디렉터리에 있다.

 

이 다음에 조끼의 색상 분포를 얻기 전에 HSV 색공간 모델을 적용하고, 색상의 범위를 나눠 어떤 색상의 분포가 더 큰지 알아낼 것 인데 크롭한 이미지 내에서도 조끼의 픽셀 수가 많은 편이 아니기 때문에 썩 좋은 방법은 아니지만 이미지에 약간의 변형을 주고 진행한다.

 

 

cropped_image = image[detect_y1[i]:(detect_y1[i]+detect_y2[i])//2, detect_x1[i]:detect_x2[i]]

 

 

크롭해주는 영역을 수정하자. 선수의 상체와 하체를 생각하여 조끼와 무관한 하체 부분을 날려주기 위해 (detect_y1[i]+detect_y2[i])//2 값을 적용하자. 물론 이렇게 극단적으로 반을 잘라버린다고 해서 좋은 것은 아니나 카메라 특성상 이미지의 가운데 위치한 사람의 bounding box는 상체만 보일 것이다.

 

 

 

 

비교적 잘 나온 이 두 분의 사진을 사용해보자.

 

 

이미지가 아닌 동영상에서는 이전 프레임을 참고하는 MOG, KNN 모델을 통해 배경과 객체를 분리할 수 있는 알고리즘을 사용하여 선수와 배경을 분리해내면 조금 더 좋은 결과를 얻을 수 있다.

 

 

 

 

 

☞ HSV 색공간 모델 적용

HSV에 대한 설명은 내가 작성한 이전 포스팅을 보면 도움이 될 것이라 생각한다.

 

[OPENCV] 5. 색 공간 모델 HSV

☞ 메인보드 : Jetson Nano Developer Kit ☞ 운영 체제 : Ubuntu 18.04 - JetPack 4.4.1 ☞ OpenCV 버전 : 4.2.0 ☞ IDE : Visual Studio Code ☞ 언어 : C++ 목차 ○ 1. HSV란? ○ 2. OPENCV의 HSV함수 ○ 3. 코드 작성 ○ 4. HSV 활용

95mkr.tistory.com

 

 

이제 색 분포를 알기위해 크롭한 이미지에 HSV 색공간 모델을 적용기 전에 빨간색, 파란색의 색상 범위를 정하자.

 

 

import numpy as np

lower_red = np.array([160,50,50])
upper_red = np.array([180,255,255])

lower_blue = np.array([100, 50, 50])
upper_blue = np.array([120, 255, 255])

 

 

위의 빨간색, 파란색 범위를 코드에 추가해주면 되고, HSV 색공간 모델을 적용시킴과 동시에 calcHist 함수를 사용하여 해당 분포을 알아내보자.

 

 

for i in range(len(detect_x1)):
    if detect_cls[i] == 0:
        # cv2.rectangle(image, (detect_x1[i],detect_y1[i]), (detect_x2[i],detect_y2[i]), (255,0,0), 2)

		# 상체 부분만 얻기위해 크롭한 이미지의 높이를 반으로 줄인다.
        cropped_image = image[detect_y1[i]:(detect_y1[i]+detect_y2[i])//2, detect_x1[i]:detect_x2[i]]
        cv2.imshow("cropped image", cropped_image)

		# HSV 색공간 모델 적용
        hsv_image = cv2.cvtColor(cropped_image, cv2.COLOR_BGR2HSV)
        cv2.imshow("hsv" , hsv_image)

        hist = cv2.calcHist([hsv_image], [0], None, [180], [0,180])

        red_sum = np.sum(hist[lower_red[0]:upper_red[0]])
        blue_sum = np.sum(hist[lower_blue[0]:upper_blue[0]])

        print(red_sum, blue_sum)

 

 

본래 HSV의 값은 원기둥으로 표현하므로 360도인 0~360의 값의 범위를 갖지만 OPENCV에서는 HSV의 Hue 값의 범위를 0~180의 정수로 제한하기 때문에 calcHist에서 범위를 0~180으로 잡아야 정확한 값을 얻어낼 수 있다.

 

 

 

 

무튼 위와 같이 코드를 작성하여 이미지를 얻어보면 위와 같이 만화경 사륜안에 걸린 이미지를 얻을 수 있다.

 

설정해놓은 빨간, 파란색의 범위와 calcHist를 통해 출력된 값은 각각 0.0 384.0(파란팀 선수) 397.0 0.0(빨간팀 선수)이었다. 잔디의 색상 때문에 초록색까지 껴서 값을 냈다면 결과가 달라졌겠지만, lower, upper red, blue와 같이 빨간색, 파란색이란 특정 색만 추출하면 되는 경우에는 위와 같은 비교를 통해 어떤 색상의 분포가 가장 많은지 알아낼 수 있다.

 

 


③ 실행 결과

이렇게 내가 알고 싶은 색의 범위를 정하고 calcHist 함수를 사용하여 해당 픽셀의 분포를 통해 어떤 색이 많은지 알아내는 내용에 대해서 소개해보았다. 위 코드에서 팀의 결과에 따라 이미지에 텍스트를 추가해보면 다음과 같은 이미지를 얻을 수 있다.


전체 코드 보기

더보기
import os
import cv2
import numpy as np

 

from ultralytics import YOLO

 

WORKSPACE_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
YOLO_ROOT = os.path.join(WORKSPACE_ROOT, 'ultralytics')

 

image = os.path.join('images', 'frame.png')

 

DETECT_MODEL = os.path.join(YOLO_ROOT, 'yolov8m.pt')

 

detect_model = YOLO(DETECT_MODEL)

 

lower_red = np.array([160,50,50])
upper_red = np.array([180,255,255])

 

lower_blue = np.array([100, 50, 50])
upper_blue = np.array([120, 255, 255])

 

image = cv2.imread(image)

 

detect_results = detect_model.predict(image, verbose=False, save=False, imgsz=1088, conf=0.3, device='cuda:0')
 
detect_x1 = detect_results[0].boxes.data[:, 0].int().cpu().tolist()
detect_y1 = detect_results[0].boxes.data[:, 1].int().cpu().tolist()
detect_x2 = detect_results[0].boxes.data[:, 2].int().cpu().tolist()
detect_y2 = detect_results[0].boxes.data[:, 3].int().cpu().tolist()
detect_conf = detect_results[0].boxes.data[:, 4].float().cpu().tolist()
detect_cls = detect_results[0].boxes.data[:, 5].int().cpu().tolist()

 

print(detect_results[0].boxes.data.int().cpu().tolist())



for i in range(len(detect_x1)):
if detect_cls[i] == 0:
# cv2.rectangle(image, (detect_x1[i],detect_y1[i]), (detect_x2[i],detect_y2[i]), (255,0,0), 2)

 

cropped_image = image[detect_y1[i]:(detect_y1[i]+detect_y2[i])//2, detect_x1[i]:detect_x2[i]]
# cv2.imshow("cropped image", cropped_image)

 

hsv_image = cv2.cvtColor(cropped_image, cv2.COLOR_BGR2HSV)
# cv2.imshow("hsv" , hsv_image)

 

hist = cv2.calcHist([hsv_image], [0], None, [180], [0,180])

 

red_sum = np.sum(hist[lower_red[0]:upper_red[0]])
blue_sum = np.sum(hist[lower_blue[0]:upper_blue[0]])

 

print(red_sum, blue_sum)

 

if red_sum > blue_sum:
cv2.putText(image, "Red Team", (detect_x1[i], detect_y1[i] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
else:
cv2.putText(image, "Blue Team", (detect_x1[i], detect_y1[i] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 0, 0), 2)



# cv2.imwrite(f"cropped_image_{i}.png", cropped_image)
# cv2.imwrite(f"cropped_image_half_{i}.png", cropped_image)
# cv2.imwrite(f"hsv_half_{i}.png", hsv_image)
# cv2.waitKey(0)

 

cv2.imshow("result", image)
# cv2.imwrite("all_detect.png", image)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

☞ 결과 이미지

 

 

포스팅을 하면서도 이전에 사용했던? 함수들이나 다른 방법 생각이 많이 났다. 이전에는 조끼를 감지하기 위해 이미지를 반으로 잘라버리는 무식한 짓을 하지 않고 MOG, KNN과 같은 배경 객체 분리 알고리즘을 통해(물론 동영상일 때) 잔디의 색이 보이지 않아 색상 감지가 더 쉬웠다. 그리고 처음부터 조끼를 학습시켜서 조끼만 크롭해오는 감지 모델을 직접 만드는 방법도 있다.

 

이미 YOLO와 같은 다른 객체 탐지 오픈소스에서 적용했을 것이기 때문에 OPENCV 함수들만 사용하게 되면 고전 알고리즘이 되어버린 상황이다.

 

원래는 히스토그램이라하니 눈으로 보여주는 matplotlib를 사용하려다가 말았다....

심심해서 이전에 사용했던 calcHist 함수를 소개해보았다. 다음에도 생각나는게 있으면 하나씩 들고와서 소개해보도록 하겠다.

 

 

 

 

 

반응형