Pythonで譜面動画から譜面を作る

# はじめに
<!-- 発端や概要を記載 -->
__Tab譜が印刷したい!!__
ギターやベースなどを趣味に持つ方なら、一度はYoutubeなどでTab譜を探したことがあるのではないだろうか。そういった時に、こういう場合があると思う。
・動画形式じゃなくて純粋な画像データのTabが欲しい。
・動画のスクリーンショット撮った印刷しても譜面以外の部分が多すぎる
そういった問題を解決するため、今回、動画のスクリーンショットから譜面を作るプログラムを制作した
# 目次
<!-- タイトルとアンカー名を編集 -->
1. [環境](#実装環境)
1. [コード](#コード)
1. [使い方](#使い方)
1. [実行例](#実行例)
1. [最後に](#最後に)
1. [参考文献](#reference)

<!-- 各チャプター -->
<a id="#実装環境"></a>
# 実装環境
Windows10
Anaconda使用
python 3.10.11

またディレクトリ構成は

Tab_Maker.py
Blessing(フォルダ)
-スクリーンショット1
-スクリーンショット2...

といった形である。
使用する際にはフォルダに対象となる画像群を移しておくことを推奨する。

<a id="#コード"></a>
# コード
まずはライブラリのインポート
``` python
import pathlib
import cv2
import numpy as np
```
次にTab_Makerクラス（ほぼすべて）
``` python
class tab_maker():
    
    #画像保存先を受け取り起動。画像リストを生成
    def  __init__(self,input_dir):
        self.input_dir = input_dir
        self.input_list = list(pathlib.Path(self.input_dir).glob('**/*.png'))
        self.img_list = []
        print("Find " + str(len(self.input_list)) + " img files.")
        
    #画像ファイルの再設定
    def set_inputdir(self,new_input_dir):
        self.input_dir = new_input_dir
        self.input_list = list(pathlib.Path(self.input_dir).glob('**/*.jpg'))
        print("Find " + str(len(self.input_list)) + " img files.")
    
    #画像リストから画像を全取得
    def get_images(self,printname=False):
        #リストの初期化
        self.img_list = []
        
        #画像を一枚ずつ読み込み
        for i in range(len(self.input_list)):
            img_file_name = str(self.input_list[i])
            img = self.imread(img_file_name,printname=printname)
            height,width = img.shape[:2]
            
            #画像サイズが同じかどうかをチェック
            if i==0:
                self.height = height
                self.width = width
                self.img_list.append(img)
            else:
                if(self.height == height and self.width == width):
                    self.img_list.append(img)
                else:
                    print("This image was excluded because it had a different size from the first image.")
                    print("異なるサイズの画像があったため除外しました")
                    
    #画像を指定の二点をもとに切り取る
    def trimming_img(self,img,left_up,right_down):
        return img[left_up[1]:right_down[1],left_up[0]:right_down[0]]
    
    #保持しているすべての画像を指定の二点をもとに切り取る
    def trimming_img_all(self,left_up,right_down):
        new_img_list = []
        for img in self.img_list:
            trim_img = self.trimming_img(img,left_up, right_down)
            new_img_list.append(trim_img)
        self.img_list = new_img_list
    
    #画像を読み込み（日本語対応）　ただし倍率を50%で読み込む（座標確認用）
    def imread(self,filename, flags=cv2.IMREAD_COLOR, dtype=np.uint8,printname=False):
        try:
            n = np.fromfile(filename, dtype)
            img = cv2.imdecode(n, flags)
            height,width = img.shape[:2]
            
            #読み込み画像名を表示
            if printname == True:
                print(str(filename) + ":Image Size:(" + str(height)+","+str(width)+")")
                
            img = cv2.resize(img,(int(width / 2), int(height / 2)))
            return img
        except Exception as e:
            print(e)
            return None
    
    #保持している画像をすべて表示
    def show_all(self):
        for i,img in enumerate(self.img_list):
            cv2.imshow(str(i),img)
        cv2.waitKey()
        cv2.destroyAllWindows()
    
    #保持している画像を一枚表示
    def show_one(self):
        cv2.imshow("number1",self.img_list[0])
        cv2.waitKey()
        cv2.destroyAllWindows()
    
    #切り取り対象座標確認用
    def show_one_getpos(self):
        #保持画像のうち、一枚目を表示
        cv2.imshow("sample",self.img_list[0])
        #sampleWindow内でクリックが発生したとき、click_pos関数を呼び出す
        cv2.setMouseCallback("sample",self.click_pos)
        cv2.waitKey()
        cv2.destroyAllWindows()
    
    #クリック時に動作する関数
    def click_pos(self,event, x, y, flags, params):
        if event == cv2.EVENT_LBUTTONDOWN:
            pos_str='(x,y)=('+str(x)+','+str(y)+')'
            print(pos_str)
            
    #画像の結合を行う
    #vseparate:何枚づつで縦結合するか
    #vspacing:縦方向の余白
    #hspacing:横方向の余白
    def concat(self,vseparate=6,vspacing=5,hspacing=0):
        img_list_copy = self.img_list.copy()

        #足りない枚数
        append_num = len(img_list_copy)%vseparate
        
        #足りない枚数分白紙画像を追加
        for i in range(vseparate - append_num):
            white_img=np.full((img_list_copy[0].shape[0],img_list_copy[0].shape[1],img_list_copy[0].shape[2]),255,np.uint8)
            img_list_copy.append(white_img)
    
        #余白の追加
        if vspacing > 0:
            for i,img in enumerate(img_list_copy):
                img_list_copy[i]=cv2.copyMakeBorder(img, vspacing, vspacing, hspacing, hspacing,cv2.BORDER_CONSTANT,value=[255,255,255])
        
        #画像の結合と保存
        number = 1
        while len(img_list_copy) >= vseparate:
            self.concat_img = cv2.vconcat(img_list_copy[:vseparate])
            del img_list_copy[:vseparate]
            cv2.imwrite("result"+self.input_dir+str(number)+".png",self.concat_img)
            number=number+1       
    
    #実行部
    def main(self):
        
        #画像が一つも見つからなければ終了
        if len(self.input_list) == 0:
            print("No image")
            return 0
        
        #画像を取得
        self.get_images()
        
        #座標確認と座標入力
        print("切り取りたい範囲の左上座標と、右下座標をメモしてください。画像内をクリックするとその位置での座標が出力されます。\n画像内でいずれかのキーを押すと座標確認モード終了")
        self.show_one_getpos()

        left_up_pos_x = int(input("left_up_x:"))
        left_up_pos_y = int(input("left_up_y:"))
        right_down_pos_x = int(input("right_down_x:"))
        right_down_pos_y = int(input("right_down_y:"))
        
        #トリミングの実行と画像結合
        self.trimming_img_all([left_up_pos_x,left_up_pos_y], [right_down_pos_x,right_down_pos_y])
        self.concat()
        
```

実行用コード
``` python
if __name__ == "__main__":
    TMaker=tab_maker("Blessing")
    TMaker.main()
```
<a id="#使い方"></a>
# 使い方
　製作時に必要だと思った機能はおおむね入れてある。
関数の組み合わせを行っているのはmain部であるから、好きなように組み合わせてもらうといいと思うし、好みの機能を追加していただいても良い。
以下使い方。

1. tab_makerクラスのインスタンス生成時に、対象となる画像の保存されたディレクトリを指定。
2. main関数を起動
3. 対象画像のうち一枚だけ表示されるので、切り取りたい部分の左上と右下をクリックして座標をメモる。
4. 座標の数値を入力
5. 終わり

結果はresult~から始まる画像で保存されるはず。

<a id="#実行例"></a>
# 実行例
実行例というかこれにしか使ってないというか...
まずはTab付の演奏動画から、譜面が切り替わるごとにスクリーンショットを撮る。
今回は
https://youtu.be/stuq_nAU2pQ?si=y_Jx_hbjsjNyuBHc
の動画を対象にさせていただいた。普段はTab譜を売るか、公開するかしていただいているのだが、この動画だけはどちらもされていなかった。これが動機。
アニソンのソロギターを多く投稿されている方なのでおすすめです。

![スクリーンショット (325).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3537209/53340340-9b43-b8cd-03c0-0d6545105296.png)

次にプログラムを実行する。
するとこのように画像のうちの一つが表示される。
ここでは著作権保護のため黒塗りにしている。
わかりにくいがこれはウィンドウで開かれており、ウィンドウ内でクリックするとその場所での座標がコンソールに表示される。ここで切り取りたいエリアの左上と右下の座標をメモしておく

![名称未設定のデザイン.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3537209/a01220cb-b7a7-fa21-ca1b-0fa6bce6c726.png)


座標をメモし終えたら、ウィンドウ内でなんでもいいのでキーを押す。
次に、メモした座標を入力していく。
入力し終えたら勝手に画像の切り取り、結合が始まる。
以下は実行時のコンソールのスクショ。
(x,y)=(120,316)
(x,y)=(518,411)
が画像内をクリックしたときのログ。
left_up_x:120
left_up_y:316
right_down_x:518
right_down_y:411
これが入力のログ。
左上、右下の順にクリックした後、順番通りに座標を入力すれば問題ないはず。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3537209/ff0cbebb-9806-6c9f-de72-db6ef76ddf33.png)

結果はこんな感じ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3537209/87264eb0-4657-fb86-61d6-ab3257770adf.png)
著作権保護のためぼかし。
楽譜として使える範囲の見た目にはなってると思う。

<a id="#最後に"></a>
# 最後に
作ってる途中はすごい便利だと思ってたけど、説明してみるとわりとだるいかもしれない。
やっぱGUIが欲しいよね...
このコードが誰かの役に立つことを祈ります！
もっと使いやすいのあったり、作れたら教えてね！
あと質問とか文句とかアドバイスとかあったら言ってほしいな！
本職のプログラマーじゃないから難しすぎると困るけど！

<a id="#reference"></a>
# 参考文献
https://qiita.com/tkrtmy1031/items/80c292fab5d631c6d4ba