にっき

コンテスト参加記録とかいろいろ

【競プロ】AtCoder水色になった話

AtCoder水色になりました。

だいぶ前の話ですが思い出して書きます。

 

目次

  • 1.入水までの経緯、その後
  • 2.勉強したこと・やったこと
  • 3.なんか最近の参加者のレベル上がってるよね?
  • 4.おしまい

 

続きを読む

画像認識で楽譜を読み取って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データに変換

の手順が待っています

心が折れそう...

これやってると作曲したくなってきました誰かMIDIキーボードください

AtCoder緑になった話

こんにちは、鳥です

ABC252で入緑したので入緑するまでにやったことなどをまとめます

以下、入緑までのみちのり

  • 勉強したこと
  • どこまで解けばいいか
  • 思ったこと

勉強したこと

入緑するまでに基本的なアルゴリズムとかデータ構造は勉強しました 具体的にはこんな感じです

アルゴリズム

  • DP(特にbitDP)
  • DFS BFS
  • 二分探索

データ構造

  • キュー スタック

このほかにも色々勉強したものがありましたが、入緑するまでは使いませんでした

どこまで解けばいいか

ABCのDまでを開始1時間くらいで解ければ緑perfは出るので入緑できます

思ったこと

ABC-Dを解くためにはどちらかと言うとアルゴリズムとかデータ構造よりも数学的な考察能力の方が必要に感じました なので高校初級程度までの数学の知識は必須だと思います

早く入緑するためには過去問を解くよりも数学の知識をつける方が効果的かもしれません

続きを読む

JOI 2008 本選4 ぴょんぴょん川渡り - 解説

アルゴリズム

続きを読む

JOI 2007 本選4 最悪の記者 - 解説

  • アルゴリズム
  • 問題
  • グラフで捉える
  • トポロジカルソート
  • 複数解がある場合の判定
  • コード

アルゴリズム

続きを読む