본문 바로가기
인공지능/컴퓨터비전

[OpenCV/Python] 악보 인식(디지털 악보 인식) - 2

by 이민훈 2021. 8. 4.

전처리 과정 - 2. 오선 제거

악보 영상의 구성요소들을 수월하게 인식하기 위해 오선을 제거합니다.

 

악보 인식에서 오선을 제거하는 것은 중요한 과제로 여겨지는데

 

현재 디지털 악보만을 대상으로 알고리즘을 구현하고 있기 때문에

 

수평 히스토그램을 통해 손쉽게 오선을 제거할 수 있습니다.

 

height, width = image_1.shape

for row in range(height):
    pixels = 0
    for col in range(width):
        pixels += (image_1[row][col] == 255)  # 한 행에 존재하는 픽셀의 개수를 셈

 

위 코드는 이미지를 탐색하여 각 행에 있는 흰색 픽셀의 개수를 세는 코드입니다.

 

배경은 검은색이고 물체는 흰색이기 때문에 해당 좌표의 픽셀값이 흰색(255)이면 변수의 값을 증가시켜주는 거죠.

 

# Main.py
import cv2
import os
import numpy as np
import functions as fs
import modules

# 이미지 불러오기
resource_path = os.getcwd() + "/resource/"
image_0 = cv2.imread(resource_path + "music.jpg")

# 1. 보표 영역 추출 및 그 외 노이즈 제거
image_1 = modules.remove_noise(image_0)

# 2. 오선 제거
height, width = image_1.shape
histogram = np.zeros(image_1.shape, np.uint8)

for row in range(height):
    pixels = 0
    for col in range(width):
        pixels += (image_1[row][col] == 255)  # 한 행에 존재하는 픽셀의 개수를 셈
    for pixel in range(pixels):
        histogram[row][pixel] = 255

# 이미지 띄우기
cv2.imshow('image', histogram)
k = cv2.waitKey(0)
if k == 27:
    cv2.destroyAllWindows()

 

히스토그램이라는 이미지를 새로 만들고 각 행의 흰색 픽셀만큼 좌에서 우로 그려보겠습니다.

 

 

오선이 존재하는 행은 흰색 픽셀이 아주 길게 존재합니다.

 

이를 이용해 오선을 제거할 수 있습니다.

 

# Main.py
import cv2
import os
import numpy as np
import functions as fs
import modules

# 이미지 불러오기
resource_path = os.getcwd() + "/resource/"
image_0 = cv2.imread(resource_path + "music.jpg")

# 1. 보표 영역 추출 및 그 외 노이즈 제거
image_1 = modules.remove_noise(image_0)

# 2. 오선 제거
height, width = image_1.shape
histogram = np.zeros(image_1.shape, np.uint8)

for row in range(height):
    pixels = 0
    for col in range(width):
        pixels += (image_1[row][col] == 255)  # 한 행에 존재하는 흰색 픽셀의 개수를 셈
    if pixels >= width * 0.5:  # 이미지 넓이의 50% 이상이라면
        for col in range(width):
            image_1[row][col] = 0  # 제거

# 이미지 띄우기
cv2.imshow('image', image_1)
k = cv2.waitKey(0)
if k == 27:
    cv2.destroyAllWindows()

 

 

하지만 한가지 문제가 생기는데 바로 인식해야 할 구성요소(음표, 쉼표 등)들이 변형된다는 겁니다.

 

 

이렇게 될 경우 추후 객체를 검출하는 데 있어서 어려움이 생길 수 있습니다.

 

그래서 오선을 제거할 때 오선의 위아래로 픽셀이 존재하는지 검사한 후 픽셀을 제거해야 합니다.

 

그러기 위해선 오선의 높이를 알아야 합니다.

 

오선 한 줄이 1픽셀로 이루어져 있을 수도 있지만 2픽셀 이상으로 이루어져 있을 수도 있습니다.

 

해상도가 높은 악보일수록 그럴 확률이 더 높겠죠?

 

 

어떤 악보의 오선을 캡처한 그림이라고 생각해봅시다.

 

해당 악보의 오선을 확대해보면 오선 하나는 5픽셀로 이루어져 있다는 걸 알 수 있습니다.

 

아래 코드는 오선의 좌표와 높이를 리스트에 저장해두는 코드입니다.

 

# Main.py
import cv2
import os
import numpy as np
import functions as fs
import modules

# 이미지 불러오기
resource_path = os.getcwd() + "/resource/"
image_0 = cv2.imread(resource_path + "music.jpg")

# 1. 보표 영역 추출 및 그 외 노이즈 제거
image_1 = modules.remove_noise(image_0)

# 2. 오선 제거
height, width = image_1.shape
staves = []  # 오선의 좌표, 높이들이 저장될 리스트

for row in range(height):
    pixels = 0
    for col in range(width):
        pixels += (image_1[row][col] == 255)  # 한 행에 존재하는 흰색 픽셀의 개수를 셈
    if pixels >= width * 0.5:  # 이미지 넓이의 50% 이상이라면
        if len(staves) == 0 or abs(staves[-1][0] + staves[-1][1] - row) >= 1:  # 첫 오선이거나 이전에 검출된 오선과 다른 오선
            staves.append([row, 1])  # 오선 추가 [오선의 y 좌표][오선 높이]
        else:  # 이전에 검출된 오선과 같은 오선
            staves[-1][1] += 1  # 높이 업데이트

print(staves)

# 이미지 띄우기
cv2.imshow('image', image_1)
k = cv2.waitKey(0)
if k == 27:
    cv2.destroyAllWindows()

 

새로운 오선을 추가하는 데 있어 두 번째 조건인 abs(staves[-1][0] + staves[-1][1] - row) > 1 에 대해 설명드리겠습니다.

 

이전에 탐색 된 오선의 좌표가 760이고 높이가 1인 상태입니다.

 

바로 다음 행에서 탐색 된 오선이 새로운 오선이 되기 위해선 이전에 탐색 된 오선과 떨어져 있어야 합니다.

 

이전의 오선 좌표 + 이전의 오선 높이가 761이고 현재 행인 761을 뺐을 때 0이므로 같은 오선이라 볼 수 있습니다.

 

print(staves)로 탐색 후 리스트를 찍어보면

 

[[221, 1], [231, 1], [241, 1], [251, 1], [261, 1], [410, 1], [420, 1], [429, 2], [439, 2], [449, 2], [598, 1], [608, 1], [618, 1], [628, 1], [638, 1], [787, 1], [797, 1], [807, 1], [816, 2], [826, 2]]이 출력됩니다.

 

총 20개의 오선이 존재하고 20개의 오선은 모두 1~2픽셀로 이루어져 있다는 걸 알 수 있습니다.

 

for staff in range(len(staves)):
    top_pixel = staves[staff][0]  # 오선의 최상단 y 좌표
    bot_pixel = staves[staff][0] + staves[staff][1] - 1  # 오선의 최하단 y 좌표 (오선의 최상단 y 좌표 + 오선 높이)
    for col in range(width):
        if image_1[top_pixel - 1][col] == 0 and image_1[bot_pixel + 1][col] == 0:  # 오선 위, 아래로 픽셀이 있는지 탐색
            for row in range(top_pixel, bot_pixel + 1):
                image_1[row][col] = 0  # 오선을 지움

 

위 코드는 오선의 위, 아래를 탐색한 후 픽셀이 존재하지 않으면 오선을 지우는 코드입니다.

 

현재 첫 번째 오선인 145, 0을 예로 들면 x좌표를 이동하여 y좌표인 144, 146을 탐색 후 픽셀이 존재하지 않으면

 

해당 x좌표의 오선을 모두 제거한다는 의미입니다.

 

 

그림을 보시면 구성요소들의 모양이 변형되지 않고 오선이 깔끔하게 제거된 것을 볼 수 있습니다.

 

역시나 modules.py에 함수로 구현한 후 메인 파일에서 import해 사용하시면 됩니다.

 

# modules.py
import cv2
import numpy as np
import functions as fs

def remove_staves(image):
    height, width = image.shape
    staves = []  # 오선의 좌표들이 저장될 리스트

    for row in range(height):
        pixels = 0
        for col in range(width):
            pixels += (image[row][col] == 255)  # 한 행에 존재하는 흰색 픽셀의 개수를 셈
        if pixels >= width * 0.5:  # 이미지 넓이의 50% 이상이라면
            if len(staves) == 0 or abs(staves[-1][0] + staves[-1][1] - row) > 1:  # 첫 오선이거나 이전에 검출된 오선과 다른 오선
                staves.append([row, 0])  # 오선 추가 [오선의 y 좌표][오선 높이]
            else:  # 이전에 검출된 오선과 같은 오선
                staves[-1][1] += 1  # 높이 업데이트

    for staff in range(len(staves)):
        top_pixel = staves[staff][0]  # 오선의 최상단 y 좌표
        bot_pixel = staves[staff][0] + staves[staff][1]  # 오선의 최하단 y 좌표 (오선의 최상단 y 좌표 + 오선 높이)
        for col in range(width):
            if image[top_pixel - 1][col] == 0 and image[bot_pixel + 1][col] == 0:  # 오선 위, 아래로 픽셀이 있는지 탐색
                for row in range(top_pixel, bot_pixel + 1):
                    image[row][col] = 0  # 오선을 지움

    return image, [x[0] for x in staves]

 

# Main.py
import cv2
import os
import numpy as np
import functions as fs
import modules

# 이미지 불러오기
resource_path = os.getcwd() + "/resource/"
image_0 = cv2.imread(resource_path + "music.jpg")

# 1. 보표 영역 추출 및 그 외 노이즈 제거
image_1 = modules.remove_noise(image_0)

# 2. 오선 제거
image_2, staves = modules.remove_staves(image_1)

# 이미지 띄우기
cv2.imshow('image', image_2)
k = cv2.waitKey(0)
if k == 27:
    cv2.destroyAllWindows()

댓글