popn music のプレイ動画から、譜面エディタで読める json ファイルを生成する

# やりたいこと

ポップンミュージックの録画台で録画したプレイ動画から、譜面エディタ（ぽぷどろ）で読み込める譜面データ (json) を生成します。（注：ぽぷどろを作られている方と筆者は無関係です。ただ使わせていただいているだけです（感謝））

<img src="http://www.bbled.org/wp-content/uploads/2020/01/h1.png"/>


こんな感じの動画を入力にして、

```sample.json
{
	"version": 3,
	"bpm": 111,
	"hsp": 5.8,
	"notes": [
		{"frame": 0, "pixel": 34, "color": 5, "timing": 0, "measure": 1},
		{"frame": 1, "pixel": 11, "color": 5},
		{"frame": 2, "pixel": 21, "color": 5},
		{"frame": 3, "pixel": 31, "color": 5},
		{"frame": 4, "pixel": 42, "color": 5},
                ...
        ]

}
```

こんな感じの json ファイルを出力して、これを[ぽぷどろ](http://popudoro.net/)に読ませると、

<img src="http://www.bbled.org/wp-content/uploads/2020/01/heaven2.png"/>


こんな感じで譜面の画像が作れます。ちなみに上の譜面や画面は、サヨナラ・ヘブン H のものです。

まあまあできてるように見えますが、ポップ君の取りこぼしが多少あったり、譜面がズレる部分が多少あったり、微調整の手順が激面倒だったりと、課題はいろいろあります。今のところ、解析可能なのは 4/4 かつソフランなしの譜面のみです。といっても画像認識自体は十分できてるので、取得できているポップ君の位置と時刻情報を頑張って処理すれば、ソフランも変拍子もいけるはず（多分）。

ちなみに、本当にやりたいことはこれではなくて、プレイ動画から COOL/GREAT/GOOD/BAD がどのポップ君で出ているかを判定して、自分のスコアというかスキル管理をすることなんですが、先にウォーミングアップ的にやってみました。

# 方針

OpenCV + Python3 で画像処理をしてポップ君の降ってくるフレームおよび位置（y座標）を取得してファイルに出力するプログラムと、出力されたフレーム情報とy座標の情報を使って、ぽぷどろで読み込める json データを生成するプログラムの二つを作ります。

## ポップン動画の基礎知識

- 旧筐体の画面は縦横比 4:3、新筐体は 16:9
- 旧筐体の VGA (640x480) 録画台の場合、ポップ君が降る領域のサイズは 320x320
- スピード表記は、画面が VGA 相当の場合に 1秒間にポップ君が進む pixel 数を表す。
 - たとえばスピード bpm 160 でハイスピ 4 倍 (=640) なら 1 秒間に 640 pixel 進むということ。

録画している動画のフレームレートが fps (frame/sec) の場合、1 フレームの間にポップ君が進む pixel 数 x は、bpm, hsp (ハイスピード倍率）とすると、下記の式で表せます。

```
x = hsp * bpm / fps
```

## 拍の長さとフレーム数の相互変換

時間（時間長）の単位について、曲の1拍の長さを 1 beat と定義し、各ポップ君の譜面上での位置（タイミング）を、曲の最初に降ってくるポップ君からの拍の長さ (beat) で表すことにします。曲が 4/4 で bpm が 4 分音符に対する表記であるものとします。

- 曲の先頭に降ってくるポップ君のタイミングは 0 beat。
- 2拍目に降ってくるポップ君のタイミングは 1 beat。
- 4/4 拍子の曲なら、2小節目の1泊目のポップ君のタイミングは 4 beat。
- 最初の小説の 0.5 拍目にあるポップ君は 1/2 beat

曲に含まれるすべてのポップ君が画面に一度に表示されていれば、すべてのポップ君を画像認識して位置を検出すればよいだけなのですが、実際にはある瞬間の画面には、曲に含まれている一部のポップ君しか表示されていません。そのため、ポップ君が表示されている位置だけでなく、ポップ君が「曲の先頭のポップ君が表示されたフレームから経過したフレーム数」も計測する必要があります（あたりまえ）。

曲の先頭のフレームを 0 としたとき、ポップ君が表示されるまでに経過したフレーム数 f と、ポップ君の画面上からの距離 h と、先に定義した定数から、ポップ君のタイミング t は以下の式で求まります。

```
t = (f * bpm * hsp / fps + h ) / (hsp * fps) 
```

ぽぷどろでは、1 拍のポップ君の時間分解能を 12 としているようなので、それに合わせるには t * 12 とする必要があります。 つまり、下記になります。

```
t' = (f * bpm * hsp / fps + h ) / (hsp * fps) * 12
```

bpm, hsp, fps は既知なので、f と h を動画から求めればよいことになります。

…しかし、実際に処理してみると結構誤差が出るので、どこかの仮定が間違っているかもしれません。いろいろ試してみたところ、fps を手作業で調整すると、かなりの精度で譜面が作れるようなので、fps 周りは何かありそうです。このへんの原因究明については後日もしくは他力本願ということで…

## ぽぷどろの json ファイル

version 3.1 で読めるファイルは、以下のようになっているようです。

```score.json
{
    "version": 3,
    "notes": [
        {
            "measure": 1,
            "timing": 0,
            "color": 1
        },
        {
            "measure": 1,
            "timing": 6,
            "color": 2
        },
        {
            "measure": 1,
            "timing": 12,
            "color": 3
        },
        {
            "measure": 1,
            "timing": 18,
            "color": 4
        },
        {
            "measure": 1,
            "timing": 24,
            "color": 5
        },
        {
            "measure": 1,
            "timing": 30,
            "color": 6
        },
        {
            "measure": 1,
            "timing": 36,
            "color": 7
        },
        {
            "measure": 1,
            "timing": 42,
            "color": 8
        },
        {
            "measure": 1,
            "timing": 24,
            "color": 9
        },
        {
            "measure": 1,
            "timing": 48,
            "color": 1
        }
    ],
    "measures": [
        {
            "split": 0,
            "x": 0,
            "y": -125,
            "w": -125,
            "h": 0
        },
        {
            "split": 16,
            "x": 21,
            "y": 24,
            "w": 117,
            "h": 128
        }
    ],
    "soflans": [],
    "startMeasureNum": 1
}
```

これで、下記のような画像が生成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266247/99247504-8f4f-0fdd-0ac0-63c5c2019663.png)

とりあえず version, notes, measures, soflans, startMeasureNum の 5 つが存在しないと、読み込んでくれないようです。notes 以外は触らなくても、譜面生成を行う上では問題ないようなので、ぽぷどろで生成されたコードをそのまま貼ることにしました。

notes の中身については、ポップ君が measure, timing, color の三つの要素で表現されています。

- measure: ポップ君の存在している小説番号
- timing: 1拍の時間分解能を 12 としたときの、ポップ君の出現位置
- color: ポップ君の色（１が右の白、５が中央の赤、９が左の白）

これらを出力してやればよさそうです。

# 実装

## 環境

- python 3.7.4
- opencv 3.?

メインは Windows 10 で anaconda を使って仮想環境を作ってやってます。mac でも同じ環境で動いてます。

- 4/4 拍子の譜面のみです
- 長押し譜面は対応してません
- ソフラン譜面は対応してません

コード全体は下記においてます：https://github.com/royalcrab/popscore

1. analyze_pop_mov.py: 録画した mp4 から、ポップ君の降ってくるレーン (color)、フレーム番号 (frame) と位置 (pixel）を取得して json で書き出します
2. generate_pop_score.py：上記プログラムで出力された json ファイルを解析して、measure, timing を計算し、それらの情報を付与してぽぷどろが読める json を出力します。

なお、現状では 1. にも 2 の機能が含まれているため、1 だけで機能します。 ただし、1は画像解析をするため、かなり重いです。2はほぼ一瞬で終わるため、1 で画像解析を1回だけ行っておいて、あとは2でパラメータをいじって調整していく想定です。

ちなみに、録画台3か所の動画で試しましたが、いずれの動画でも画像処理に関してはパラメータの調整だけでいけてました。

## 画像処理用パラメータ調整

analyze_pop_mov.py にある下記の変数を、動画にあわせて変更する必要があります。

- bpm: 曲の bpm
- hsp: ハイスピードオプションで選択した倍率
- fps: 動画のフレームレート (fps)。
- src: 動画ファイル名。動画サイズは 480x640 固定です。
- reference_frame: 背景差分の拝啓として使う動画のフレーム番号（曲開始直後の are you ready? と出る直前くらいのフレームを指定すると良い）
- start_frame: 最初のポップ君が降る直前のフレーム。ポップ君が落ちてくる前なら、どのフレームでもいいです。
- end_frame: 最後のポップ君が降った後の、レーンに何も降ってない状態のフレーム。

start_frame と end_frame は画像処理を行うフレーム数を減らすために、処理範囲を指定するためのものです。

このプログラムでは rects.josn というファイルを出力します。画像処理の途中経過を見たければ、out と gray というディレクトリを作って、imwrite している行のコメントを外してください。

## 譜面生成用パラメータ調整

generate_pop_score.py では、下記を変更する必要があります。結果は標準出力に出ます。

- fps: 動画のfps
- offset: 最初のポップ君の拍（先頭の拍は0とする）。

fps の指定が曲者で、60fps で撮影されているはずの動画で 60 と指定しても、だいたいうまくいきません。出力された譜面をぽぷどろに入れてみて、fps の数値を何度か調整する必要があります。経験的には、小数点以下2桁の精度で数字を指定しないと合わない気がします。

この操作が結構面倒なので、譜面を表示した状態で、数値を変更ながら調整できるようにしたいとは思っています。

## 実装の詳細について

画像処理は、背景差分をとって、閾値以上の色の領域を findContours で取得して、取得した領域の底面の部分の y 座標を取る、みたいなことをしてます。当初は DNN とか使って学習して…みたいなことも考えたのですが、そこまでやらなくてもいけました。

ポップ君の位置の計算は、上のほうに書いた式をベースに計算してるだけです。

詳しくは github か別ブログに書くかも。

## 長押し譜面について

長押しポップ君て、途中でボタンを離すと、長押し部分の表示は消えるんですね…。先頭部分のポップ君は通常のものと一緒なので、少なくとも先頭部分はとれそうです。区間全体を綺麗に取ろうと思ったら、別の実装が必要そう。

## ソフランについて

速度が変化するフレームと、変化した bpm を正確に指定できれば、問題なく取得できそうです。画面上の bpm 表示が正確なタイミングで切りかわるようなので、少なくとも手動でなら対応はできそうな感じ。自動でやるには、bpm の数字を画像認識する方向ですかね…

## 変拍子などについて

これは、単純に measure の切れ目をどこにするかというだけの問題なので、個々の小節の長さを手動で指定してやれば対処は可能です。小節の情報って画面には全く出ないので、こればっかりは自動では無理っぽい。

# 追記

## 新筐体

新筐体の画面サイズは 1280x760 でした。ポップ君が降るレーンの縦幅は 512 pixel ぽいです。ポップ君の落下速度は、単純に 480/720 = 1.5 倍になっているようです。計算式を少し変えれば、容易に対応できそうです。

## さらなる追記

ドラマニと IIDX の録画もできそうなので、近いうちにそのへんもやってみます。

## e-amusement ベーシックコース

とりあえず popn はプレミアムコース対象外なんですよね…。popn もベーシックコースでスコアは見れますが、bad や good とかの数が分かるだけで、どこで bad 出てるとかまでは分かりません。新筐体では、以前はボタンごとの bad/good/great 比率を見ることができた気がしますが、いつのまにか無くなりました? プレミアムコースでも、jubeat だと過去のスコア履歴がフラフで見られるくらいで(それでも無いよりは良いですが)、分析できるほどのデーは取得できなかったりするので、もう少し何とかしてほしいなあと思いつづけて早 20 年。

プレイデータをサーバに貯めるのがしんどいなら、例えばプレイ直後にQRコードで表示してアプリで読ませるとかでもいいと思うんですよね。QR コード version 40 / M で 177x177 pixel で 2KB 程度いけるので、ノート数 2000 くらいなら VGA の画面にも表示できるし、データをアプリに読ませられるよね（譜面データをアプリに読ませる必要はありますが）。 

じゃあおまえやれよ！というならやるので、呼んでください。画像処理するよりはマシな気がするので（誰が呼ぶんだろう？）

# Popn Music my BEST 10 Selection

本題です。POPN専用曲じゃないのとか、初出は他ゲーのとか入ってますが、気にしないでください。順不同です。ちなみに、書いてる人が押せる上限は 44-45 程度です。

1. High Shool Love
2. キルト
3. DAWN
4. attack in the minor key
5. キセキはじまり☆
6. サヨナラ・ヘブン
7. ピアノ協奏曲第1番"蠍火"
8. Harmonia
9. チョコレートスマイル
10. Love Is Strong To The Sky

TERRA、FLYING DUO、ウッチーズとかいろいろ漏れてますが、BEST 10 というと難しい…