본문 바로가기

Previous (20-22)/Development

(Python) 그래픽 이퀄라이저(Equalizer) 만들기

안녕하세요. 

아직 풍코딩과 관련된 강좌는 유튜브에 올라와 있지는 않지만,
강좌를 준비하면서 개발되는 프로그램은 여기 블로그에 공유를 할 예정이니,
풍코딩에 관심있으신 분 또는 여러 유용한 소스코드를 얻어가실 분은 이 곳에서 얻어가시면 될 것 같습니다.

이번에 쓸 글은 그래픽 이퀄라이저(Graphic Equalizer) 만들기를 해 보겠습니다.

강좌를 쓸 사람인데, 어디 다른데서 있는거 퍼오기나 하는건 존심이 상하기도 하고 그래서,
한번 직접 만들어보게 되었습니다.

먼저 화면 출력 결과는 위와 같습니다.
실시간 그래픽 이퀄라이저 표현이 필요하다면 활용하면 좋겠죠?

 

그러면 이제 저걸 어떻게 만드는지를 하나씩 살펴보도록 하겠습니다.

 

이퀄라이저 구현 필수 요소

  1. 배경 구현
  2. 막대 화면 배치
  3. 막대 색상 구현
  4. 막대 개수 랜덤 구현

위와 같이 네 가지로 구분할 수 있으며, 이에 기반하여 하나씩 구현해 보겠습니다.

 

(1) 배경 구현

먼저 이퀄라이저 구현에 필요한 모듈로 다음의 모듈을 사용합니다.

import numpy as np		# 다차원 배열 처리 모듈
import cv2 as cv		# OpenCV(그래픽 처리) 모듈
import colorsys as cs		# 색상변환 모듈
import random as rd		# Random 숫자 모듈

 

다음은 화면 표시를 위한 규격을 설정합니다.

width = 1024		# 너비
height= 600		# 높이
bpp = 3			# 표시 채널(grayscale:1, bgr:3, transparent: 4)

img = np.zeros((height, width, bpp), np.uint8)	# 빈 화면 표시

cv.imshow("drawimg",img)	# drawing이라는 이름으로 화면 표시
cv.waitKey(0)			# 화면 표시 시간을 무제한(0)으로 함

여기서 img 변수의 값이 무슨 뜻인지 이해가 안되시는 분들이 있어서 설명드리면,

TV나 모니터에서 화상을 표시할 때 보면 가로x세로 너비에 픽셀 단위로 점을 찍어서 표시하며, 이 때 np.zeros() 메소드를 사용하여 각 점을 '0'으로 찍는다, 즉 검은색으로 표시하는 것으로 보시면 됩니다. (검은색에 대한 색상 코드 값은 (0,0,0), 즉 '0' 입니다)

그리고 맨 아래 두 줄은 실제 화면에 표시하기 위한 명령어로, cv.imshow() 메소드를 사용해서 img 변수의 값을 "drawing"이라는 이름으로 그리고, cv.waitKey(0) 메소드를 사용하여 화면 표시 시간을 결정합니다.

여기에서 waitKey() 메소드의 파라미터가 1 이상의 숫자일 경우에는 ms 를 단위로 해서 보여주고, 0으로 하면 무제한으로 보여준다는 것을 의미합니다.

위와 같이 img를 설정하고 화면에 표시했을 때 결과는 다음과 같습니다.

만약에 img 값이 '0'이 아니라 '255'로 채워진다면 결과는 다음과 같겠죠?

그러면 어떻게 255로 채우느냐. 다음과 같이 하면 간단합니다.

width = 1024		# 너비
height= 600		# 높이
bpp = 3			# 표시 채널(grayscale:1, bgr:3, transparent: 4)

img = np.full((height, width, bpp), 255, np.uint8)	# 빈 화면 표시

cv.imshow("drawimg",img)
cv.waitKey(0)

맨 아래 구문과 같이, np.add() 메소드를 사용하여 255만 더해주면 간단하며, 배경 색상을 어떤 색으로 할 것인지는 위 사항을 참고해서 결정하면 될 것으로 생각됩니다.

 

(2) 막대 화면 배치

다음은 막대 화면 배치를 해야겠죠.

막대 화면 배치에 필요한 요소는 무엇일까요.

  1. 어떤 모양으로 
  2. 어느 위치에
  3. 어떤 크기로

배치하는 것이 중요하겠죠.

먼저 어떤 모양. 바로 사각형입니다.

맨 위와 같이 그래픽 이퀄라이저를 표현하기 위해서는 사각형 형태의 구성요소가 필요하기 때문입니다.

다음은 어떤 위치에 배치할 것인가. 하나의 표를 만들어 보겠습니다.

위와 같은 형식으로 15x6 형태의 표를 배치할 위치로 구상하면 될 것입니다.

먼저 화면을 표시할 크기는 앞서 언급한 것처럼 1024x600 형태로 구성되어 있고, 이퀄라이저는 맨 아래를 기준으로 위로 올라갔다 내려갔다 하는 형태이므로 표의 최상단이 아닌 최하단을 기준위치로 결정해야 합니다.

이 글에서는 맨 아래에서 100픽셀 위에 있는 곳을 최하단으로 결정할 예정이므로, 화면 크기인 600-100=500을 y축 기준선으로 하고, 또한 가장 좌측으로부터 100픽셀에 있는 곳을 첫 번째 위치로 할 예정이므로, 100을 x축 기준선으로 하겠습니다.

  • x축 기준선: 100
  • y축 기준선: 500

세번째로는 도형의 크기입니다.

도형의 크기를 결정할 때에는 고려해야 할 요소로, 사각형의 width, height뿐만 아니라, 사각형과 사각형 사이의 거리까지도 감안해야 합니다. 이에 따라 사각형 크기 및 거리를 고려하여 다음과 같이 결정합니다.

  • 사각형 크기: (50, 20)
  • 사각형 간 거리: (55, 22)

위 크기를 감안했을 때, 사각형 간 공백은 50 * 1.1 = 55, 20 * 1.1 = 22, 즉 10% 사이의 간격이 형성된다는 것을 알 수 있습니다.

위 사항을 감안하여 막대를 화면에 배치하기 위한 초기값을 설정하는 코드는 다음과 같습니다.

thickness = -1				# 도형의 형태
rect_w = 50				# 도형 너비
rect_h = 20				# 도형 높이
int_x = 55				# 도형 간 좌우 거리
int_y = 22				# 도형 간 상하 거리
screen_start_x = 100			# 화면 시작 x축
screen_start_y = 500			# 화면 시작 y축

여기서 하나 추가된 것은, thickness 변수입니다.

Python에서는 도형을 그릴 때 사용하는 cv.rectangle() 메소드를 사용하여 그리는데, 이 때 5번째 파라미터는 사각형의 선 굵기를 나타내는 부분으로, 0 이상의 값을 할 경우에는 해당 숫자만큼의 선이 그려지는 반면, -1로 하면 선이 없는 대신 특정 도형으로 채워지게 됩니다. 그래픽 이퀄라이저의 막대는 선 없이 채우기 형태로 나타내므로, thickness 변수의 값도 -1로 설정합니다.

위 값을 설정하였을 때 사각형을 표시하는 예제입니다.

rect1_point1 = (screen_start_x, screen_start_y)
rect1_point2 = (screen_start_x + rect_w, screen_start_y + rect_h)

rect2_point1 = (screen_start_x + int_x, screen_start_y - int_y)
rect2_point2 = (screen_start_x + rect_w + int_x, screen_start_y + rect_h - int_y)

cv.rectangle(img, rect1_point1, rect1_point2, (0,0,0), thickness)
cv.rectangle(img, rect2_point1, rect2_point2, (0,0,0), thickness)

cv.imshow("drawimg",img)
cv.waitKey(0)

사각형을 그리는 메소드는 cv.rectangle()로, (표시할 이미지, 시작점, 종료점, 색상, 굵기) 형태의 파라미터로 입력됩니다.그렇기 때문에 시작점과 종료점을 설정하기 위해서 rect1_point1, rect1_point2, rect2_point1, rect2_point2 변수를 사용한 것으로 보시면 됩니다.

사실 위 예제는 이퀄라이저를 실제로 그릴 때 사용되는 소스코드는 아닙니다. 하지만 사각형 2개를 그렸을 때 어떤 식으로 배치되는 지를 나타내기 위한 예제이며, 이를 바탕으로 했을 때 그려진 결과는 아래와 같습니다.

 

(3) 막대 색상 구현

다음은 막대 색상을 구현하겠습니다.

맨 첫 번째 이미지에서와 같이 15개의 색상은 아래와 같이 표시됩니다.

짙은 빨간색을 시작으로 하늘색까지 나누어져서 표시되는데, 이러한 부분은 어떻게 구현했는지를 살펴보겠습니다.

색상코드를 표시하는 방법은 다양하게 존재하지만, PC에서 색상을 표현하는 코드는 rgb코드를 가장 많이 사용합니다. 
cv.rectangle() 메소드에서 표시되는 색상 역시 rgb코드 형태로, (blue, green, red)의 튜플 값이 입력됩니다.

그러나 RGB코드만 가지고 위와 같이 15개 색상을 아름답게 분류한다는 것은 사실 매우 어려운 일로, 위와 같은 색상을 도출해 내기 위해서는 HSV코드 또는 HSL 코드 등을 통해서 도출이 가능합니다.

사진 출처: www.depositphotos.com

HSV는 Hue(색상), Saturation(채도), Value(명도)로 구분되며, 위 사진은 그중에서도 Hue(색상)을 나타낸 부분입니다.

즉 빨간색부터 하늘색까지의 색상을 표현하기 위해서는 Hue의 각도를 조정하여 표현할 수 있으며, 시작 색상과 종료 색상에 대한 Hue 값은 다음과 같습니다.

  • 빨간색: 0º
  • 하늘색: 180º

그리고 Hue를 기준으로 고루 분리되어 나타내기 위해서는 Saturation과 Value의 값은 고정된 값으로 표시가 되어야 하므로, 원색 그대로가 아닌 채도와 명도가 들어가 있는 식으로 이 글에서는 다음 값으로 설정합니다.

  • 채도(Saturation): 98%
  • 명도(Value): 46%

물론 색상 배열은 원하시는 형태로 직접 해도 되지만, HSV 색상코드를 사용해서 배치했을 때 어떤 식으로 되는지를 알려주는 것이니 참고하시면 될 것 같습니다.

Python에서는 HSV 색상코드를 RGB로 변환하기 위한 모듈을 제공하고 있으며, 서두에 언급한 'colorsys'를 사용합니다.

colorsys 모듈에서는 cs.hsv_to_rgb() 메소드를 사용하며, 파라미터는 (hue, saturation, value)의 값이 들어갑니다.

하지만 hsv_to_rgb() 메소드를 사용할 때 유의점은 입력되는 파라미터와 반환되는 RGB의 값은 모두 0에서 1 사이의 값이 이 됩니다.

  • 입력 파라미터(hue, saturation, value)
    • hue - 입력값: 0~1 // 실제 코드값: 0º~360º
    • saturation - 입력값: 0~1 // 실제 코드값: 0% ~ 100%
    • value - 입력값: 0~1 // 실제 코드값: 0% ~ 100%
  • 출력 반환값(red, green, blue)
    • red / green / blue - 입력값: 0~1 // 실제 코드값: 0~255

그러므로 위의 빨간색부터 하늘색까지를 나타내기 위한 hsv_to_rgb() 메소드의 파라미터는 다음과 같습니다.

  • 빨간색에 대한 파라미터: (0, 0.98, 0.46)
  • 하늘색에 대한 파라미터: (0.5, 0.98, 0.46) 

여기까지에 대한 설정값은 다음과 같이 나타냅니다.

hue_start = 0
hue_stop = 0.5
saturation = 0.98
value = 0.46
count = 15

다음은 위의 15가지 색상을 어떻게 나누는지를 나타내는 코드입니다. 코드는 하늘색(0.5)에서 빨간색(0) 사이의 코드를 15개로 나누는 부분으로, 이를 수치화해서 표현하는 부분입니다.

rgb_list = []
hue_size = hue_stop - hue_start
hue_diff = hue_size / count

for i in range(count):
    hue = hue_start + hue_diff * i
    rgbcode = np.multiply(cs.hsv_to_rgb(hue, saturation, value), 256)

여기서 np.multiply() 메소드는 256을 곱하는 역할을 수행하며, 이는 반환값이 0~1 사이의 값인 관계로 실제 RGB 코드값으로 변경하기 위한 과정으로 볼 수 있습니다.

이제 위의 과정을 토대로 해서 구현된 15가지 색상에 대한 막대를 그리는 코드를 구현합니다. 여기서 중요한 것은 cv.rectangle() 메소드의 색상코드는 RGB 값을 가진 Tuple로, 일반적으로 알고 있는 (red, green, blue)가 아닌 (blue, green, red)의 순서로 배치되므로, 배열 값을 뒤바꾸기 위한 메소드인 np.flip() 메소드를 사용하여 값을 변경한 후 rectangle에 입력하는 식으로 합니다.

이번 예제는 모듈을 불러오는 부분부터 모든 과정이 포함된 코드로 나타냅니다.

import numpy as np
import cv2 as cv
import colorsys as cs
import random as rd

width = 1024
height= 600
bpp = 3

img = np.zeros((height, width, bpp), np.uint8)

thickness = -1
rect_w = 50
rect_h = 20
int_x = 55
int_y = 22
screen_start_x = 100
screen_start_y = 500

hue_start = 0
hue_stop = 0.5
saturation = 0.98
value = 0.46
count = 15

rgb_list = []
hue_size = hue_stop - hue_start
hue_diff = hue_size / count

for i in range(count):
    hue = hue_start + hue_diff * i
    rgbcode = np.flip(np.multiply(cs.hsv_to_rgb(hue, saturation, value), 256))

    startx = screen_start_x + i * int_x
    cv.rectangle(img, (startx, screen_start_y), (startx + rect_w, screen_start_y + rect_h), rgbcode, thickness)


cv.imshow("drawimg",img)
cv.waitKey(0)

 

(4) 막대 개수 랜덤 구현

마지막으로 막대 개수를 랜덤하게 구현하는 부분을 나타내겠습니다.

앞서 불러온 모듈 중에서 랜덤을 구현하기 위한 모듈로 random 모듈을 불러옵니다.

여기에서 사용하는 메소드는 여러가지가 있지만, 앞서 나타낼 그래픽 이퀄라이저는 총 6줄 형태로 나타낼 예정이므로, 1개에서 6개 사이의 랜덤 숫자를 호출하는 부분으로 구현하도록 합니다.

eqbar_cnt = rd.randint(1,6)

eqbar_cnt 변수는 1~6 사이의 숫자가 입력되며, 입력된 수를 반복하여 나타내도록 합니다.

앞서 막대 배치를 할 때에는 가장 아래에 있는 막대를 기준으로 하여 나타내므로, eqbar_cnt 변수의 값이 특정 값을 나타낸다면 막대가 그려질 때마다 위로 배치되는 형태가 되어야 합니다. 반복되는 수는 아래와 같습니다.

앞서 나타낸 예제에서는 screen_start_x, screen_start_y의 값을 각각 100, 500으로 하였고, 15개의 막대를 가로로 표시하기 위해서 막대 개수(count)만큼을 반복해서 아래와 같이 나타냈었으며, 이 예제에서는 screen_start_x (100)을 x축 시작점으로 하여, 개수만큼 int_x (55)씩 더해서 막대의 x축을 배치시켰습니다.

...

rect_w = 50
rect_h = 20
int_x = 55
int_y = 22
screen_start_x = 100
screen_start_y = 500

...

for i in range(count):
	...
    
    startx = screen_start_x + i * int_x
    cv.rectangle(img, (startx, screen_start_y), (startx + rect_w, screen_start_y + rect_h), rgbcode, thickness)

...

y축 또한 마찬가지입니다. eqbar_cnt 변수의 값만큼 반복을 한 다음에, screen_start_y (500) 시작점에서 반대로 int_y (22) 만큼을 빼서 막대를 위로 반복하여 쌓도록 하며, 이를 반영한 코드는 다음과 같습니다.

...

rect_w = 50
rect_h = 20
int_x = 55
int_y = 22
screen_start_x = 100
screen_start_y = 500

...

for i in range(count):
	...
    
        eqbar_cnt = rd.randint(1,6)

        for j in range(eqbar_cnt):
            startx = screen_start_x + i * int_x
            starty = screen_start_y - j * int_y
            cv.rectangle(img, (startx, starty), (startx + rect_w, starty + rect_h), rgbcode, thickness)

...

eqbar_cnt 을 기준으로 2차 반복문을 수행하도록 한 후, startx 변수는 위 예제와 동일한 형태로 하되, starty 변수를 추가로 생성하여, int_y 만큼을 빼도록 한 후, cv.rectangle() 메소드를 실행해서 그립니다. eqbar_cnt는 rd.randint() 메소드의 임의의 수를 반환하므로, x축을 기준으로 한 막대를 그릴 때마다 막대의 개수는 1에서 6 사이의 랜덤한 값으로 생성하게 됩니다.

랜덤 개수 구현까지 완료되었으면, 이퀄라이저를 표시하는 일만 남았습니다.

이퀄라이저는 0.25초, 즉 250ms 에 한번씩 표시하도록 하며, 한 화면을 반복할 때마다 계속해서 다른 값이 나타나야 하므로, 화면을 250ms에 한번씩 초기화를 시켜줍니다. 이를 반영한 전체 코드는 다음과 같습니다.

import numpy as np
import cv2 as cv
import colorsys as cs
import random as rd

width = 1024
height= 600
bpp = 3

img = np.zeros((height, width, bpp), np.uint8)

thickness = -1
rect_w = 50
rect_h = 20
int_x = 55
int_y = 22
screen_start_x = 100
screen_start_y = 500

hue_start = 0
hue_stop = 0.5
saturation = 0.98
value = 0.46
count = 15

rgb_list = []
hue_size = hue_stop - hue_start
hue_diff = hue_size / count

while True:
    img = np.zeros((height, width, bpp), np.uint8)

    for i in range(count):
        hue = hue_start + hue_diff * i
        rgbcode = np.flip(np.multiply(cs.hsv_to_rgb(hue, saturation, value), 256))
        eqbar_cnt = rd.randint(1,6)

        for j in range(eqbar_cnt):
            startx = screen_start_x + i * int_x
            starty = screen_start_y - j * int_y
            cv.rectangle(img, (startx, starty), (startx + rect_w, starty + rect_h), rgbcode, thickness)


    cv.imshow("drawimg",img)
    cv.waitKey(250)

 

글 서두에 나타냈던 결과를 다시 한번 표시하도록 하겠습니다.

그래픽 이퀄라이저는 위 방식을 사용하여 여러 가지 코드를 조합해서 표현이 가능하며, 이 글에서는 단순히 0.25초에 한번씩 랜덤하게 표시하는 정도에 그쳤지만, 이를 좀 더 확장해서 여러 기능을 추가하는 것도 한번 검토해 보도록 하겠습니다.

이상 글 마치겠습니다.