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

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

by 이민훈 2021. 8. 5.

6. 인식 과정 - 조표

이제 대망의 인식 과정입니다.

 

먼저 지금 갖고 있는 정보들을 확인해보겠습니다.

 

for i in range(len(objects)):
    obj = objects[i]
    line = obj[0]
    stats = obj[1]
    stems = obj[2]
    direction = obj[3]
    staff = staves[line * 5: (line + 1) * 5]
    print(line, stats, stems, direction)

 

하나하나의 객체에는 현재 몇 번 보표에 속해있는지, 객체의 정보(x, y, w, h, area),

 

직선들의 정보, 음표라고 가정한다면 방향들의 정보가 있습니다.

 

아래는 첫 번째 음표의 정보인데, 정보를 해석해보겠습니다.

 

0 (321, 221, 13, 39, 139) [[333, 221, 1, 34]] True

 

0번째 보표에 속해있고 321, 221 좌표에 위치해있습니다.

 

넓이와 높이는 각각 13, 39이고 객체에 존재하는 흰색 픽셀은 139개네요.

 

stems 리스트엔 하나의 리스트가 있으니 1개의 직선이 존재하는 것이고

 

그 직선은 333, 221 좌표에, 넓이와 높이는 각 1, 34입니다.

 

이 객체가 음표라면 정방향(True) 음표일 겁니다.

 

staff는 보표 번호에 따라 보표에 속한 오선들의 좌표를 들고 옵니다.

 

 

이제 인식하는데 필요한 정보들을 얻었으니 구성요소들을 인식하는 알고리즘을 만들어보겠습니다.

 

먼저 조표를 인식하는 알고리즘을 만들어보겠습니다.

 

음자리표 또한 인식해야 할 객체지만, 어려운 악보의 인식은 제외하고

 

동요 디지털 악보를 인식하는 것으로 대상을 제한하고 있으니

 

높은 음자리표일 것으로 가정하고, 음자리표 인식은 제외하도록 하겠습니다.

 

해당 과정을 다 이해하신다면 음자리표는 1번째로 오는 객체이고 위치가 정해져 있기 때문에

 

얼마든지 수월하게 알고리즘을 짜실 수 있을 겁니다.

 

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

def recognition(image, staves, objects):
    key = 0
    time_signature = False
    beats = []  # 박자 리스트
    pitches = []  # 음이름 리스트

    for i in range(1, len(objects)):
        obj = objects[i]
        line = obj[0]
        stats = obj[1]
        stems = obj[2]
        direction = obj[3]
        (x, y, w, h, area) = stats
        staff = staves[line * 5: (line + 1) * 5]
        if not time_signature:  # 조표가 완전히 탐색되지 않음 (아직 박자표를 찾지 못함)
            ts, temp_key = rs.recognize_key(image, staff, stats)
            time_signature = ts
            key += temp_key
        else:  # 조표가 완전히 탐색되었음
            pass

        cv2.rectangle(image, (x, y, w, h), (255, 0, 0), 1)
        fs.put_text(image, i, (x, y - fs.weighted(30)))

    return image, key, beats, pitches

 

modules.py에 recognition이라는 이름으로 인식 함수를 하나 만들겠습니다.

 

파라미터로 image, staves, objects 각각 이미지, 오선 정보, 객체 정보를 받아옵니다.

 

박자와 음이름들은 음표, 쉼표 인식에서 처리할 정보들이고

 

조표는 객체들 중 음자리표 다음, 박자표 이전에 위치해 있습니다.

 

다장조라면 음자리표 이후 박자표가 나오고 다장조가 아니면 음자리표 이후 조표가 나옵니다.

 

그래서 박자표가 아직 검출되지 않았다면 조표 인식 함수를 호출하고

 

아니라면 음표, 쉼표 인식 함수를 호출하도록 하겠습니다.

 

인식 알고리즘들은 recognition_modules.py라는 파일을 만들어 따로 두도록 하겠습니다.

 

# recognition_modules.py
import functions as fs

def recognize_key(image, staves, stats):
	pass

    return key

 

이미지, 오선 정보, 객체 정보를 받아 키를 반환하는 함수입니다.

 

이제 조표의 특징을 잘 생각해봅시다.

 

조표는 플랫 또는 샾일 수있고, 몇개가 있을지 알 수 없습니다. 1개에서 최대 7개까지 붙을수 있죠..

 

아예 없는 경우도 있습니다. 다장조의 경우 음자리표다음 박자표가 옵니다.

 

하지만 박자표는 언제나 일정한 위치에 일정한 크기를 가지고 있습니다.

그렇다는 얘기는 박자표를 찾는 알고리즘은 조표를 찾는 알고리즘보단 비교적 정확하게 작동할 것이고,

 

박자표가 나오기전 객체들은 조표라고 가정할 수 있다는 것입니다.

 

 

박자표는 정확히 오선의 중간에 위치하고 있고, 높이는 오선 4칸을 차지하고 있습니다.

 

챕터4에서 확인한 결과 넓이는 12 즉, 오선 한칸정도 되는 넓이입니다.

 

# recognition_modules.py
import functions as fs

def recognize_key(image, staves, stats):
    (x, y, w, h, area) = stats
    ts_conditions = (
        staves[0] + fs.weighted(5) >= y >= staves[0] - fs.weighted(5) and  # 상단 위치 조건
        staves[4] + fs.weighted(5) >= y + h >= staves[4] - fs.weighted(5) and  # 하단 위치 조건
        staves[2] + fs.weighted(5) >= fs.get_center(y, h) >= staves[2] - fs.weighted(5) and  # 중단 위치 조건
        fs.weighted(18) >= w >= fs.weighted(10) and  # 넓이 조건
        fs.weighted(45) >= h >= fs.weighted(35)  # 높이 조건
    )

    return key

 

조건을 적어본다면 위와 같습니다.

 

객체의 상단은 첫번째 오선 부근에 존재해야하고, 하단은 마지막 오선 부근에 존재해야합니다.

 

중단은 3번째 오선 부근에 존재해야하고, 넓이와 높이는 적당히 여유롭게 조건을 적어주었습니다.

 

이제 박자표조건에 부합할 경우 True를 리턴해 recognition 함수에 박자표를 찾았다고 알려주면 될거고,

 

박자표가 아닐경우 조표이기에 조표를 인식할수 있어야 합니다.

 

플랫과 샾을 구분하는 법은, 앞서 정방향 음표와 역방향 음표를 구분할 때 썼던 방법인데,

 

플랫은 직선 성분이 객체의 앞쪽에 존재하고, 샾은 조금 더 뒤에 위치해 있습니다.

 

조표로 가정한 객체에서 직선 성분이 빨리 검출된다면 플랫으로 볼 수가 있습니다.

 

# recognition_modules.py
import functions as fs

def recognize_key(image, staves, stats):
    (x, y, w, h, area) = stats
    ts_conditions = (
        staves[0] + fs.weighted(5) >= y >= staves[0] - fs.weighted(5) and  # 상단 위치 조건
        staves[4] + fs.weighted(5) >= y + h >= staves[4] - fs.weighted(5) and  # 하단 위치 조건
        staves[2] + fs.weighted(5) >= fs.get_center(y, h) >= staves[2] - fs.weighted(5) and  # 중단 위치 조건
        fs.weighted(18) >= w >= fs.weighted(10) and  # 넓이 조건
        fs.weighted(45) >= h >= fs.weighted(35)  # 높이 조건
    )
    if ts_conditions:
        return True, 0
    else:  # 조표가 있을 경우 (다장조를 제외한 모든 조)
        stems = fs.stem_detection(image, stats, 20)
        if stems[0][0] - x >= fs.weighted(3):  # 직선이 나중에 발견되면
            key = int(10 * len(stems) / 2)  # 샾
        else:  # 직선이 일찍 발견되면
            key = 100 * len(stems)  # 플랫

    return False, key

 

코드로 짜본다면 위와 같습니다.

 

미리 만들어둔 stem_detection 함수로 직선성분을 모두 검출하고

 

첫번째 직선의 x좌표가 객체안에서 나중에 발견된다면 샾, 아니면 플랫으로 분류할 수 있습니다.

 

샾은 한개당 10, 플랫은 한개당 100으로 계산하여 반환하도록 하겠습니다.

 

샾은 직선이 2개이므로 2로 나눠준 것입니다.

 

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

def recognition(image, staves, objects):
    key = 0
    time_signature = False
    beats = []  # 박자 리스트
    pitches = []  # 음이름 리스트

    for i in range(1, len(objects)):
        obj = objects[i]
        line = obj[0]
        stats = obj[1]
        stems = obj[2]
        direction = obj[3]
        (x, y, w, h, area) = stats
        staff = staves[line * 5: (line + 1) * 5]
        if not time_signature:  # 조표가 완전히 탐색되지 않음 (아직 박자표를 찾지 못함)
            ts, temp_key = rs.recognize_key(image, staff, stats)
            time_signature = ts
            key += temp_key
            if time_signature:
                fs.put_text(image, key, (x, y + h + fs.weighted(20)))
        else:  # 조표가 완전히 탐색되었음
            pass

        cv2.rectangle(image, (x, y, w, h), (255, 0, 0), 1)
        fs.put_text(image, i, (x, y - fs.weighted(30)))

    return image, key, beats, pitches

 

이제 박자표를 찾는순간 put_text 함수를 이용해 값을 이미지에 찍어보도록 하겠습니다.

 

박자표밑에 20 또는 300과 같은 값들이 찍힐겁니다.

 

# 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)

# 3. 악보 이미지 정규화
image_3, staves = modules.normalization(image_2, staves, 10)

# 4. 객체 검출 과정
image_4, objects = modules.object_detection(image_3, staves)

# 5. 객체 분석 과정
image_5, objects = modules.object_analysis(image_4, objects)

# 6. 인식 과정
image_6, key, beats, pitches = modules.recognition(image_5, staves, objects)

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

 

 

해당 악곡은 플랫이 3개이므로 300이 찍힌 것을 볼 수 있습니다.

 

다른 악보로도 테스트 해보겠습니다.

 

 

플랫이 3개일때 300, 샾이 1개일때 10으로 값이 잘 나오는것을 확인했습니다.

 

다음챕터부턴 음표 인식 알고리즘을 구현하도록 하겠습니다.

댓글