画像認識で楽譜を読み取ってMIDIデータに変換するbotを作ろうとしているお話
鳥です
まだ制作途中ですが楽譜をMIDIデータに変換するプログラムを作っているのでここまでの過程を書きます
作ることになった経緯
ことのきっかけは、中学時代の友達に頼まれたことです それで色々あってdiscordのbotという形で作ることになりました
制作準備
discordのbotで作るなら色々機能が充実しているpythonで作るか〜、画像認識を使うのでopenCVも使うか〜ってことになり、早速openCVをインストールしようとしますが早くもここで躓きました
この時
pip install opencv-python
でインストールした(つもり)だったのですがなんかインストールされてなかったみたいで全然プログラムが動きませんでした
(ここで心が折れかける)
色々調べまくった結果、anacondaからopenCVをインストールすればいいらしく、下の記事を参考にようやくインストールすることができました(ここまで半日くらいかかった)
Anaconda に「OpenCV」をインストール-スケ郎のお話
ようやく制作段階へ
五線の検出
とりあえず楽譜の画像認識をしないとだめなので色々ネットで調べてみるといい感じの記事がありました(下)
pythonで楽譜画像認識 | inglow:東京・大阪・名古屋のマーケティングオートメーション・Webプロモーション
ここのコードを参考に(というか丸パクリ)してコードをとりあえず書いてみて、楽譜を投げてみると
こわいこわいこわい
五線の検出も音符の検出も何もかも上手くいってないですね
色々コードを見ていると、白黒の反転をしていなかったことがわかりました(どうやら白黒反転していなかったらopenCVの直線検出関数はクソ大量の検出結果を返してくるみたいです)
白黒反転のコードをつけて閾値もいい感じに調節して再度楽譜を投げてみます
五線はいい感じに検出できました
....と思っていました
コンソールに検出した五線の情報を出力させると、なぜか本数が89本もあり困惑しました
原因は簡単で、同じ直線を何度も検出していたからです
なので、一定距離以下しか離れていないの直線どうしは同じものとして、検出結果から削除しました
音符の検出
五線が検出できたところで次は音符の検出です
原型は既にあるのでちょっと改良するだけです
手順としては
画像をぼかす(五線を消す)→輪郭の検出→領域の面積の最大値、最小値を決め、それ以上、以下の領域は検出結果から削除
です(後にこれで苦しめられる)
色々とぼかす時の重みとかを調節した結果、うまく検出できました
ここまでできたところでとりあえず他の楽譜でも試してみます(著作権の関係で画像無し)
すると、和音の部分が複数の音符として検出されるのではなく、一つの音符として検出されてしまいました
これの解決策として五線をぼかして消すのではなく、五線の部分を上から白で塗って消す方法を取ることにしましたが、この方法だと五線上にある音符は二つの領域に分けられてしまうのでうまく行きません(これはまだ解決できてないです)
現状
今のところ、ここまでできています(閾値とか、同じ直線と見做す時の線同士の距離とかは引数で設定できるようにしています)
import discord import sys import numpy import cv2 import os import requests import shutil import urllib.request import datetime import subprocess import math client = discord.Client() TOKEN = '*******' def download_img(url, file_name): r = requests.get(url, stream=True) if r.status_code == 200: with open(file_name, 'wb') as f: f.write(r.content) def numpy_mat_sort(mat): change = True while change: change = False for i in range(len(mat) - 1): if mat[i][0][1] > mat[i + 1][0][1]: mat[i], mat[i + 1] = mat[i + 1], mat[i].copy() change = True @client.event async def on_ready(): print('We have logged in') @client.event async def on_message(message): if message.author == client.user: return if message.content == 'stopscorebot': sys.exit(1) if message.content.startswith('!s'): ctx = message.content augv = ctx.split() print(augv) maxlinegap = 5 do_blur = True same_line_dis = 5 minlinelength = 100 for aug in augv: index_linegap = aug.find('maxlinegap') if index_linegap != -1: maxlinegap = int(aug[index_linegap + len('maxlinegap') + 1:]) index_is_blur = aug.find('do_blur') if index_is_blur != -1: if int(aug[index_is_blur + len('do_blur') + 1:]) == 0: do_blur = False index_same_line_dis = aug.find('same_line_dis') if index_same_line_dis != -1: same_line_dis = int(aug[index_same_line_dis + len('same_line_dis') + 1:]) index_minlinelength = aug.find('minlinelength') if index_minlinelength != -1: minlinelength = int(aug[index_minlinelength + len('minlinelength') + 1:]) download_img(message.attachments[0].url, "image.png") print(maxlinegap) print(do_blur) print(same_line_dis) print(minlinelength) #パスのベースを作成 DS = os.sep BASE_PATH = os.path.dirname(__file__) + DS #楽譜画像のパスを生成 scor_img = 'image.png' #指定したデータを指定したファイル名で出力 def debug_image(img, imgname = 'result.png'): #画像を出力 cv2.imwrite(imgname, img) result_img = cv2.imread(scor_img, cv2.IMREAD_COLOR) #五線を認識する scr = cv2.imread(scor_img) scr_gray = cv2.cvtColor(scr, cv2.COLOR_RGB2GRAY) #途切れてるところがつながるようにぼかしてみる if do_blur: kval = 2 kernel = numpy.ones((kval,kval),numpy.float32)/(kval*kval) scr_gray = cv2.filter2D(scr_gray,-1,kernel) #白黒反転 line_dst = cv2.bitwise_not(scr_gray) #2値化 retval_line, line_dst = cv2.threshold(line_dst, 30, 255, cv2.THRESH_BINARY) debug_image(line_dst, 'line_dst.png') #線を検出 lines = cv2.HoughLinesP(line_dst, rho=1, theta=numpy.pi/360, threshold=100, minLineLength=minlinelength, maxLineGap=maxlinegap) if lines is None: await message.channel.send('五線を検出できませんでした') return dis = 9999 numpy_mat_sort(lines) dellist = [] for line1 in range(len(lines)): for line2 in range(line1, len(lines)): if line1 >= len(lines) or line2 >= len(lines): break if line1 == line2: continue if min(abs(lines[line1][0][3] - lines[line2][0][3]), abs(lines[line1][0][1] - lines[line2][0][1])) < same_line_dis: dellist = dellist + [line2] lines = numpy.delete(lines, dellist, 0) print(lines) print(len(lines)) for line1 in range(len(lines) - 1): dis = min(dis, abs(lines[line1][0][3] - lines[line1 + 1][0][3])) for line in lines: x1, y1, x2, y2 = line[0] #赤線 result_img = cv2.line(result_img, (x1, y1), (x2, y2), (0,0,255), 1) #五線認識ここまで #音符のたま認識 #楽譜データを読み込む scr = cv2.imread(scor_img, cv2.IMREAD_COLOR) #画像のサイズを取得 height, width, channels = scr.shape image_size = height * width #グレースケール化 ① scr_filled = cv2.cvtColor(scr, cv2.COLOR_RGB2GRAY) #細い線とかをぼかす ぼかし処理 scr_filled = cv2.bilateralFilter(scr_filled, 15, 140, 10) #白黒反転 ③ dst = cv2.bitwise_not(scr_filled) debug_image(dst, 'dst.png') #2値化 retval, dst = cv2.threshold(dst, 240, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) debug_image(dst, 'dst2.png') #五線の消去 for line in lines: x1, y1, x2, y2 = line[0] for x in range(x1, x2): for y in range(y1, y2): pixcel_value_up = scr_filled[y - 2, x] pixcel_value_bottom = scr_filled[y + 2, x] if (pixcel_value_up == [0, 0, 0] or pixcel_value_bottom == [0, 0, 0]) and ((scr_filled[y - 2, x - 2] == [0, 0, 0] and scr_filled[y - 2, x + 2] == [0, 0, 0]) or (scr_filled[y + 2, x - 2] == [0, 0, 0] and scr_filled[y + 2, x + 2] == [0, 0, 0])): continue #白線 dst = cv2.line(dst, (x - 3, y - 1), (x + 3, y + 1), (255,255,255), 1) debug_image(dst, 'dst_deletedlines.png') #輪郭を抽出 cnt, hierarchy = cv2.findContours(dst, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) #抽出した領域を出力 抽出した領域に境界線を引いた画像を出力 dst = cv2.imread(scor_img, cv2.IMREAD_COLOR) dst = cv2.drawContours(dst, cnt, -1, (0, 0, 255, 255), 2, cv2.LINE_AA) debug_image(dst, 'dst3.png') #認識させるサイズを指定する minsize = dis ** 2 - 6 print(minsize) #20→100→500 maxsize = minsize + 30 print(maxsize) #元元3000とか #大きいor小さい領域は削除 notes = [] for i, count in enumerate(cnt): #小さい領域の場合は無視 area = cv2.contourArea(count) if area < minsize: continue #最大値の指定を追加 if area > maxsize: continue #画像全体を占める領域を除外 if image_size * 0.50 < area: continue #囲う線を描画する x,y,w,h = cv2.boundingRect(count) if h >= w * 2 or w >= h * 2: continue if y < lines[0][0][1]: continue result_img = cv2.rectangle(result_img, (x, y), (x + w, y + h), (0, 255, 0), 3) notes.append([x + w / 2, y + h / 2]) #音符認識ここまで #検出結果を表示 debug_image(result_img, 'result.png') print(notes) await message.channel.send(file=discord.File('result.png')) client.run(TOKEN)
ここからさらに
の手順が待っています
心が折れそう...
これやってると作曲したくなってきました誰かMIDIキーボードください
AtCoder緑になった話
こんにちは、鳥です
ABC252で入緑したので入緑するまでにやったことなどをまとめます
以下、入緑までのみちのり
- 勉強したこと
- どこまで解けばいいか
- 思ったこと
勉強したこと
入緑するまでに基本的なアルゴリズムとかデータ構造は勉強しました 具体的にはこんな感じです
- DP(特にbitDP)
- DFS BFS
- 二分探索
データ構造
- キュー スタック
このほかにも色々勉強したものがありましたが、入緑するまでは使いませんでした
どこまで解けばいいか
ABCのDまでを開始1時間くらいで解ければ緑perfは出るので入緑できます
思ったこと
ABC-Dを解くためにはどちらかと言うとアルゴリズムとかデータ構造よりも数学的な考察能力の方が必要に感じました なので高校初級程度までの数学の知識は必須だと思います
早く入緑するためには過去問を解くよりも数学の知識をつける方が効果的かもしれません
続きを読む