Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added ML/model/note_head_template.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified ML/model/weights.pt
Binary file not shown.
4 changes: 4 additions & 0 deletions ML/src/exception/EmptyDataFrameError.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class EmptyDataFrameError(Exception):
def __init__(self, message):
self.message = message
super().__init__(f"{message}")
4 changes: 4 additions & 0 deletions ML/src/exception/EmptyImageError.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class EmptyImageError(Exception):
def __init__(self, message):
self.message = message
super().__init__(f"{message}")
7 changes: 7 additions & 0 deletions ML/src/exception/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .EmptyDataFrameError import EmptyDataFrameError
from .EmptyImageError import EmptyImageError

__all__ = [
"EmptyDataFrameError",
"EmptyImageError"
]
2 changes: 1 addition & 1 deletion ML/src/makexml/IntervalPreset.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

class IntervalPreset:
# 키 종류
KEY_ORDER = ['C', 'G', 'D', 'A', 'E', 'B', 'Fs', 'Gb', 'Db', 'Ab', 'Eb', 'Bb', 'F']
KEY_ORDER = ['C', 'G', 'D', 'A', 'E', 'B', 'Fsharp', 'Gb', 'Db', 'Ab', 'Eb', 'Bb', 'F']
# 각 키에 해당하는 숫자
KEY_PITCH_ORDER = [0, 5, 0, 7, 2, 9, 4, 0, 7, 2, 9, 4, 11]
# 샾이 붙는 순서
Expand Down
288 changes: 257 additions & 31 deletions ML/src/makexml/MakeScore.py

Large diffs are not rendered by default.

77 changes: 72 additions & 5 deletions ML/src/makexml/MeasureIterator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,74 @@
from .ScoreIterator import ScoreIterator
from fractions import Fraction
from .IntervalPreset import IntervalPreset
class MeasureIterator:
def __init__(self):
self.cur_keysig = 0
self.measure_length = -1
self.remain_measure_length = -1
self.interval_list = []
self.cur_clef = -1
self.__cur_keysig = 0 # 기본은 C로 가정
self.__measure_length = -1 # 기본은 없음
self.__remain_measure_length = -1 # 기본은 없음
self.__interval_list = []
self.__cur_clef = -1

def set_cur_measinfo(self, keysig, timesig, interval_list, clef):
self.__cur_keysig = keysig
self.__measure_length = MeasureIterator.calc_measure_length(timesig)
self.__remain_measure_length = self.measure_length
self.__interval_list = interval_list
self.__cur_clef = clef

def set_cur_keysig(self, keysig):
self.__cur_keysig = keysig

def set_cur_measure_length(self, timesig):
self.__measure_length = MeasureIterator.calc_measure_length(timesig)
self.__remain_measure_length = self.__measure_length

def set_cur_interval_list(self, interval_list):
self.__interval_list = interval_list

def set_cur_clef(self, clef):
self.__cur_clef = clef

def subtract_remain_measure_length(self, duration):
if self.__remain_measure_length > 0:
self.__remain_measure_length -= duration
else:
return -1

return self.__remain_measure_length

def set_measiter_from_scoiter(self, scoiter):
clef = scoiter.get_cur_clef()
keysig = scoiter.get_cur_keysig()
timesig = scoiter.get_cur_timesig()

self.__cur_clef = clef
self.__cur_keysig = keysig
self.__measure_length = MeasureIterator.calc_measure_length(timesig)
self.__remain_measure_length = self.__measure_length
self.__interval_list = IntervalPreset.get_interval_list(clef, keysig)

def get_cur_keysig(self):
return self.__cur_keysig

def get_cur_remain_measure_length(self):
return self.__remain_measure_length

def get_interval_list(self):
return self.__interval_list

def get_cur_clef(self):
return self.__cur_clef

def calc_interval_list(self):
self.__interval_list = IntervalPreset.get_interval_list(self.__cur_clef, self.__cur_keysig)

@staticmethod
def calc_measure_length(timesig):
num, note = timesig[0], timesig[1]

length = Fraction(num) * Fraction(4, note)

return length


11 changes: 6 additions & 5 deletions ML/src/makexml/Pitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pandas as pd
import numpy as np
from .IntervalPreset import IntervalPreset
from music21.pitch import Accidental

class Pitch:

Expand All @@ -23,7 +24,7 @@ def find_pitch_from_y(staff_df, head, staff_lines, measiter, margin_ratio=0.35):

return: 해당 위치의 MIDI pitch (int), or None
"""
interval_list = measiter.interval_list
interval_list = measiter.get_interval_list()

if len(staff_lines) != 5 or len(interval_list) != 19:
return None
Expand Down Expand Up @@ -82,16 +83,16 @@ def find_pitch_from_y(staff_df, head, staff_lines, measiter, margin_ratio=0.35):
if adjust == 1:
interval_list[pitch_idx] += 1
n.pitch.midi = interval_list[pitch_idx]
n.accidental = note.Accidental('sharp')
n.accidental = Accidental('sharp')
elif adjust == -1:
interval_list[pitch_idx] -= 1
n.pitch.midi = interval_list[pitch_idx]
n.accidental = note.Accidental('flat')
n.accidental = Accidental('flat')
else:
temp_interval = IntervalPreset.get_interval_list(measiter.cur_clef, 0)
temp_interval = IntervalPreset.get_interval_list(measiter.get_cur_clef(), 0)
interval_list[pitch_idx] = temp_interval[pitch_idx]
n.pitch.midi = interval_list[pitch_idx]
n.accidental = note.Accidental('natural')
n.accidental = Accidental('natural')
return n

n.pitch.midi = interval_list[pitch_idx]
Expand Down
19 changes: 17 additions & 2 deletions ML/src/makexml/ScoreInfo.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
class ScoreInfo:
def __init__(self):
self.keysig_list = []
self.timesig_list = []
self.__keysig_list = []
self.__timesig_list = []

def add_keysig(self, keysig):
self.__keysig_list.append(keysig)

def add_timesig(self, timesig):
self.__timesig_list.append(timesig)

def is_keysig_empty(self):
if not self.__keysig_list:
return True
else:
return False

def get_keysig_list(self):
return self.__keysig_list
43 changes: 40 additions & 3 deletions ML/src/makexml/ScoreIterator.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,42 @@
class ScoreIterator:
def __init__(self):
self.cur_keysig = 0
self.cur_timesig = [0,0]
self.clef = -1
self.__cur_keysig = 0 # 기본 C키 가정
self.__cur_timesig = [0,0] # 기본은 없음
self.__clef = -1 # 기본은 -1로 없앰앰

# 멤버변수값 초기화
def set_cur_score_info(self, keysig, timesig, clef):
self.__cur_keysig = keysig
self.__cur_timesig = timesig
self.__clef = clef

# 현재 키 설정
def set_cur_keysig(self, keysig):
self.__cur_keysig = keysig

# 현재 박자표 설정
def set_cur_timesig(self, timesig):
self.__cur_timesig = timesig

# 현재 음자리표 설정
def set_cur_clef(self, clef):
self.__clef = clef

# 음자리표 반환
def get_cur_clef(self):
return self.__clef

# 박자표 반환
def get_cur_timesig(self):
return self.__cur_timesig

# 조표 반환
def get_cur_keysig(self):
return self.__cur_keysig

# 박자표 비교
def compare_timesig(self, timesig):
if self.__cur_timesig == timesig:
return True
else:
return False
118 changes: 117 additions & 1 deletion ML/src/makexml/StafflineUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,120 @@ def get_objects_in_staff_area(staff_df, staff_x1, staff_x2, pitch_y_top, pitch_y
filtered_df.sort_values(by=["x_center", "y_center"], inplace=True)
filtered_df.reset_index(drop=True, inplace=True)

return filtered_df
return filtered_df

#fallback 로직 (음자리표로 staff_line 추정)
@staticmethod
def fallback_staffline_from_clef(clef_row, image):
h, w = image.shape[:2]

clef_y1 = int(clef_row["y1"])
clef_y2 = int(clef_row["y2"])
clef_height = clef_y2 - clef_y1

# Clef type에 따른 확장 비율 설정
if clef_row["class_name"] == "clef_F": # 낮은 음자리표 (Bass Clef)
up_ratio = 0.05 # y1 기준 위쪽으로 5%
down_ratio = 0.30 # y2 기준 아래쪽으로 30%
elif clef_row["class_name"] == "clef_G": # 높은 음자리표 (Treble Clef)
up_ratio = 0.05 # y1 기준 위쪽으로 15%
down_ratio = 0.05 # y2 기준 아래쪽으로 15%
else:
# 기본값 (예비 처리용)
up_ratio = 0.10
down_ratio = 0.10

# 확장된 crop 영역 계산
y1_pad = max(0, int(clef_y1 - clef_height * up_ratio))
y2_pad = min(h, int(clef_y2 + clef_height * down_ratio))

# 이미지 crop
staff_crop = image[y1_pad:y2_pad, 0:w]

# 오선 감지
local_staff_lines = StafflineUtils.extract_5lines(staff_crop)

# 성공 시, 원본 이미지 좌표로 보정해서 반환
if len(local_staff_lines) == 5:
staff_lines_global = [y + y1_pad for y in local_staff_lines]
print(f"[🟡 fallback] Clef 기반 오선 Y좌표: {staff_lines_global}")
return staff_lines_global
else:
print("[❌ fallback] Clef 기반 오선 감지 실패")
return []

@staticmethod
def find_note_head_in_box(image, bbox):
"""
Bounding box 내부에서 OpenCV 기반으로 note_head 중심 좌표들을 찾아 리턴
- HoughCircle + contour 기반 detect_note_head_opencv 사용
- staff_gap은 bbox 세로 길이 기준으로 추정
"""
x1, y1, x2, y2 = map(int, bbox)
staff_gap = max(4, (y2 - y1) / 5)

centers = StafflineUtils.detect_note_head_opencv(image, bbox, staff_gap)

if not centers:
print("[❌ fallback 실패] note_head 탐지 불가")
return []

print(f"[✅ fallback 성공] note_head {len(centers)}개 탐지")
return centers # [(cx, cy), ...]

@staticmethod
def detect_note_head_opencv(image, bbox, debug=False):
x1, y1, x2, y2 = map(int, bbox)

roi = image[y1:y2, x1:x2]
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

# 오선 제거
proj = np.sum(binary == 255, axis=1)
W = binary.shape[1]
staff_y = np.where(proj > 0.8 * W)[0]
splits = np.where(np.diff(staff_y) > 1)[0] + 1
groups = np.split(staff_y, splits)

mask = np.ones_like(binary, dtype=np.uint8) * 255
max_thick = 0
for g in groups:
start, end = g[0], g[-1]
mask[start:end + 1, :] = 0
max_thick = max(max_thick, end - start + 1)

no_staff = cv2.bitwise_and(binary, mask)

# 스템 제거
v_open = cv2.getStructuringElement(cv2.MORPH_RECT, (1, binary.shape[0] // 8))
stems = cv2.morphologyEx(no_staff, cv2.MORPH_OPEN, v_open, iterations=2)
clean = cv2.subtract(no_staff, stems)

# 노이즈 정리
round_k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
clean = cv2.morphologyEx(clean, cv2.MORPH_OPEN, round_k)
clean = cv2.dilate(clean, round_k)

contours, _ = cv2.findContours(clean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
centers = []
for cnt in contours:
if cv2.contourArea(cnt) < 50:
continue
x_, y_, w_, h_ = cv2.boundingRect(cnt)
if h_ > 2 * w_ or w_ / h_ > 3:
continue
if len(cnt) >= 5:
_, axes, _ = cv2.fitEllipse(cnt)
MA, ma = max(axes), min(axes)
if ma / MA < 0.5:
continue
cx = x_ + w_ // 2 + x1
cy = y_ + h_ // 2 + y1
centers.append((cx, cy))

if debug:
cv2.drawContours(image, [cnt + [x1, y1]], -1, (0, 0, 255), 1)
cv2.circle(image, (cx, cy), 3, (255, 0, 0), -1)

return centers
3 changes: 2 additions & 1 deletion ML/src/makexml/TextProcesser.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""
import easyocr
import json
import numpy as np
Expand Down Expand Up @@ -66,5 +67,5 @@ def get_lyrics_json_from_mxl(mxl_path):

#print(lyrics_by_verse)
return dict(lyrics_by_verse)

"""