【OpenCV + Pretty_midi】画像をMIDI出力してみた！

本エントリーは「チームうどん」の皆様が主催の「[初めてのアドベントカレンダー](https://qiita.com/advent-calendar/2021/first-try)」22日目の記事です！

「本当に初めて、、？」ってレベルの面白い記事が目白押しなので、他のエントリもぜひご覧ください〜〜！

# はじめに

一年ほど前にリリースするか悩んだ末リリースしなかったアプリのコア機能である**画像から音楽を生成する機能**の実装をこちらで供養することにしました。

### この記事の対象読者

- 変わった技術に興味のある方
- MIDI画像を作ってみたい！と思っている方
- プログラミングを用いた音楽生成に興味のある方

# 使用環境

- Python 3.9.0
- OpenCV 4.5.4
- NumPy 1.21.5
- Pretty_midi 0.2.9

# 使用する画像
~~みんな大好き~~画像処理のデモでお馴染みのLennaさん(`Lenna.png`)を使用します。

![Lennaさん](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091686/03853d4a-dfdf-97d1-c849-ebf49ff66f57.png)


# ソースコード

```python:create_music.py
import numpy as np
import cv2
import pretty_midi as pm

def get_edge_image(image_path: str) -> np.ndarray:
    origin_img = cv2.imread(image_path, 0)
    if(origin_img is None):
        print('画像が読み込めませんでした')
        exit()
    return cv2.Canny(origin_img, 50, 200)

def get_resized_image(image: np.ndarray) -> np.ndarray:
    return cv2.resize(image,dsize = (round(128 / image.shape[0] * image.shape[1]),128))

def image2score(image_path: str) -> None:
    image_score = pm.PrettyMIDI()
    piano = pm.Instrument(0)
    img = get_resized_image(get_edge_image(image_path))
    start_second = 0
    end_second = 0
    for i in range(img.shape[0]):
        for j in range(img.shape[1]):
            if img.item(i,j) != 0:
                piano.notes.append(pm.Note(100, j, start_second, end_second))
        start_second += 0.5
        end_second += 0.5
    image_score.instruments.append(piano)
    image_score.write('output.mid')

if __name__ == '__main__':
    image_path = input('ファイル名を入力してください>>')
    image2score(image_path)

```

それぞれの関数について動作を説明します！

### get_edge_image

```python:get_edge_image
def get_edge_image(image_path: str) -> np.ndarray:
    origin_img = cv2.imread(image_path, 0)
    if(origin_img is None):
        print('画像が読み込めませんでした')
        exit()
    return cv2.Canny(origin_img, 50, 200)
```

OpenCVで画像をグレースケールとして読み込んだ後、[Canny関数](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_canny/py_canny.html)を用いてエッジ検出をした結果を返しています。
戻り値の中身はこんな感じです。
![白黒Lennaさん](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091686/31d392d4-f265-1b3e-b363-7d77fd156152.png)

### get_resized_image

```python:get_resized_image
def get_resized_image(image: np.ndarray) -> np.ndarray:
    return cv2.resize(image,dsize = (round(128 / image.shape[0] * image.shape[1]),128))
```

引数で受け取った画像のサイズを`高さ127px`になるように縦横の比率が変わらないようにリサイズした結果、つまりはMIDI画像を返します。
戻り値の中身はこんな感じです。
![サイズ変更白黒Lennaさん](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091686/0c0d3213-ad11-5613-a29d-29ef572481ec.png)

### image2score

```python:image2score
def image2score(image_path: str) -> None:
    image_score = pm.PrettyMIDI()
    piano = pm.Instrument(0)
    img = get_resized_image(get_edge_image(image_path))

    start_second = 0
    end_second = 0
    for i in range(img.shape[0]):
        for j in range(img.shape[1]):
            if img.item(i,j) != 0:
                piano.notes.append(pm.Note(100, j, start_second, end_second))
        start_second += 0.5
        end_second += 0.5
    image_score.instruments.append(piano)
    image_score.write('output.mid')
```

先ほどリサイズした白黒画像の`縦方向のピクセルをノートナンバー(0~127)`、`横方向のピクセルをノート(音)`としてMIDIに変換し、出力しています。

この関数で用いているモジュールの一つである[Pretty_midi](https://github.com/craffel/pretty-midi)は`Pretty_midiインスタンス生成`->`楽器インスタンス生成`->`楽器にノート(音)を追加`->`Pretty_midiインスタンスの楽器リストに楽器インスタンスを追加`->`Pretty_midiインスタンスをMIDIとして出力`の順に記述することでMIDIファイルを簡単に生成できる外部モジュールです。
Noteインスタンスを作成する際(`pm.Note(100, j, start_second, end_second)`の部分)、引数に`(大きさ,ノートナンバー,開始(s),終わり(s))`と指定することで、音の強弱・音階・長さを設定できます。

この関数で出力したMIDIファイルをGarage Bandで演奏した結果は[こちら](https://drive.google.com/file/d/16r4jr6nf8SG3DyGI0dacIAe-ykUilO3d/view?usp=sharing)(Qiitaに音楽をアップロードすることができないため、Google Driveの共有用リンクを貼っています)

あまり夜中に聞かない方が良さそうな音楽が出来ました(笑)

# まとめ
今回は画像をMIDI出力してみました！
今回はLennaさんをお借りしましたが、入力する画像によって様々な結果が得られるので、この記事を読んで少しでも気になった方は是非お手元の画像でも試してみてください！