SSBUFrameAnalyzer v3

History

GitHub - kanomiya/SSBUFrameAnalyzer

What's New

  • 「ストック」を取得できるようにした(精度イマイチ)
    • 3ストックまで想定(4ストック以上は未実装)
    • 一応団体戦考慮してストックごとにキャラクター推定
    • 精度イマイチな原因かも:背景、ファイター順によってちょっと決め打ち座標がズレてる、画像が小さすぎる→大きく補間してからHOG
  • 勇者(Hero)のデータを追加(v3のdictionary)
    • コマンド出てるときは(顔に被るから)邪魔で難しいと思う...

f:id:kanomiya:20190805195125p:plain f:id:kanomiya:20190805195144p:plain

GitHub - kanomiya/SSBUFrameAnalyzer at v3

TODO

  • ファイター数の推定
  • bboxのズレ解消
  • bboxの自動算出
  • 残タイムの推定
  • 画面中のファイター位置の推定
    • 深層学習(YOLO/R-CNN系かセグメンテーション)になりそう、データがない...
  • 精度向上
    • normalizeしたり、コントラスト上げたり、周辺ぼかすフィルタ掛けたりしたら精度上がらないかな
  • チャージ・残量系キャラ(ルフレクラウドリトル・マックインクリング、ジョーカー、勇者他?)のチャージ・残量推定
    • f:id:kanomiya:20190805195955p:plain f:id:kanomiya:20190805195555p:plain f:id:kanomiya:20190805200110p:plain f:id:kanomiya:20190805195453p:plain f:id:kanomiya:20190805200005p:plain f:id:kanomiya:20190805195717p:plain
  • ステージの推定(データ...)

SSBUFrameAnalyzer v2

About

GitHub - kanomiya/SSBUFrameAnalyzer

いまのところ、スマブラSPスクリーンショットから「ダメージ値」・「おなまえ」・「キャラ名」を取得できる(ただし2,3,4人対戦のみ)。

「おなまえ」の取得精度は微妙。アルファベットだったら少し精度いいかも?

「キャラ名」の取得

ダメージ値と同じくHOG特徴量で最近傍(knnに拡張できる)。データには対戦中のキャラ顔画像を使用。

一応(大きく)モデル違うやつ(色変えただけ以外、ex. むらびとポケモントレーナールフレ♂♀、クッパJr.など)はデータ用意してるけど、確認はしてない。漏れあるかも。

Example 1

f:id:kanomiya:20190721205103p:plain

['マリオ', 'マリオ', 'マリオ'] [5.536537823295048, 6.301248636752482, 6.390935400999263]
['ドンキーコング', 'ディディーコング', 'ルキナ'] [5.539215799855154, 9.311677857638966, 9.606319879292924]
{'fighters': {0: {'chara_name': 'マリオ', 'name': 'ぷれしいやー', 'damage': 0.0}, 1: {'chara_name': 'ドンキーコング', 'name': 'DONKEY KONG', 'damage': 0.0}}}
FPS: 2.286091 (0.437428 s)

Example 2

f:id:kanomiya:20190721204831p:plain

['マリオ', 'マリオ', 'マリオ'] [5.971331723619266, 6.220896989804241, 6.286527104504024]
['ドンキーコング', 'ディディーコング', 'ロボット'] [5.520510947750764, 9.599002436376757, 9.670321221911154]
['リンク', 'ルキナ', 'シュルク'] [5.455176600531569, 8.748479515310487, 8.780689053687928]
{'fighters': {0: {'chara_name': 'マリオ', 'name': 'tbここ', 'damage': 0.0}, 1: {'chara_name': 'ドンキーコング', 'name': 'DONKEY KONG', 'damage': 0.0}, 2: {'chara_name': 'リンク', 'name': '1旧い|', 'damage': 0.0}}}
FPS: 1.381940 (0.723621 s)

Example 3

f:id:kanomiya:20190721204855p:plain

['マリオ', 'マリオ', 'マリオ'] [5.5006358082686555, 5.74951932443511, 5.7915505642106995]
['ドンキーコング', 'ディディーコング', 'ルキナ'] [4.692127133850047, 9.475091942436732, 9.545288243557097]
['リンク', 'シュルク', 'ルキナ'] [4.3889363397440455, 8.67419528745303, 8.707431208635597]
['サムス', 'ダークサムス', 'ケン'] [4.181132231378159, 8.547772471042341, 8.683606629158557]
{'fighters': {0: {'chara_name': 'マリオ', 'name': '1に', 'damage': 0.0}, 1: {'chara_name': 'ドンキーコング', 'name': 'IDONKEY KONG', 'damage': 0.0}, 2: {'chara_name': 'リンク', 'name': 'LINK', 'damage': 0.0}, 3: {'chara_name': 'サムス', 'name': 'SAMUS', 'damage': 0.0}}}
FPS: 1.227384 (0.814741 s)

SSBUFrameAnalyzer

About

GitHub - kanomiya/SSBUFrameAnalyzer

いまのところ、スマブラSPスクリーンショットから「ダメージ値」・「おなまえ」を取得できる(ただし2人対戦のみ)。フルで毎フレーム処理できるほどのFPS出ないと思う。

適当なSSで動かした目安FPS(ノートPC): 2.766284 (0.361496 s)

基本的にターゲットの場所を(座標決め打ちで)切り抜いて処理にかけてる。

ダメージ値の取得

HOG特徴量使ったk-nnにしたけど、適当に確認して(写真と違ってノイズ入らないし、バリエーションも少ない)問題なさそうだったのでとりあえずTop1しか使ってない。すごい。

(被ダメージ・ふっとびで)エフェクトかかると取れなくなるだろうけど、自動化するなら前フレーム維持/スキップでいいと思って考慮してない。

参照データ数減らせばちょっと速くなるだろうけど、OCRの方が負荷大では(試合中にほぼおなまえ変わらないし、別にいいのか? 団体戦?)。

おなまえの取得

Tesseract使ってOCR。たまに精度あやしいけどだいたいOK。すごい。

か→が、ぷ→ふ、みたいに濁点/半濁点に弱そう。

おなまえの長さ上限10文字までいけると思う(ひらがな10文字はためしてOK)。

TODO

  • ファイター数の推定
    • どうすればいいんだ。(自動化して連続的にとるなら)一回推定すればいい気がするから、遅延気にせずHOG系の枠を一通り試行してうまくいった組み合わせを採用?
  • 各種bboxの自動算出
  • ファイター種類(キャラクター)の推定
    • HOGでいけるだろうけど、データ集めが面倒
  • ストック数の推定
    • ホカホカ(高被ダメージ)のときエフェクトが重なるのがあぶない
  • 残タイムの推定
    • タイム∞のとき表示されない例外
    • 背景ころころ変わるので精度出そうと思ったらknnな気がする......

Example

f:id:kanomiya:20190721053044p:plain

{'fighters': {0: {'name': 'ふれいやー', 'damage': 141.3}, 1: {'name': 'KOOPA', 'damage': 101.5}}}
FPS: 2.499149 (0.400136 s)

濁点/半濁点は苦手...。プレイヤー→フレイヤーさん。

ログ監視 Python watchdog(ログローテーション未完成)

アプリケーションのログファイルを監視するシステムをつくる。

上の2つをがっちゃんこしたやつを作った。ファイルの変更監視はwatchdog、読み取りはふつうのIO。

※ うーん、ログローテーション対応が微妙かも

from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
import time
import os
from stat import *

class FileWatchHandler(FileSystemEventHandler):
    def __init__(self, path):
        self.path = os.path.realpath(path)
        self.fp = None
        self.fpos = None

        self.init()

        filesize = os.stat(self.path)[ST_SIZE]
        self.fp.seek(filesize) # ファイル末尾
        self.fpos = filesize

    def init(self):
        fp = self.fp
        if fp:
            fp.close()
        fp = open(self.path, 'r')

        self.fp = fp
        self.fpos = None

    def on_created(self, event):
        if event.src_path == self.path:
            print('reset')
            self.init()

    def on_modified(self, event):
        if event.src_path == self.path:
            print('modified')
            self.tail_f()

    def tail_f(self):
        if self.fpos:
            self.fp.seek(self.fpos)

        while True:
            self.fpos = self.fp.tell()
            line = self.fp.readline()

            if not line:
                break
            self.analyze(line)

    def analyze(self, line):
        # TODO:
        print('!', line)

    def close(self):
        self.fp.close()

path = 'test.txt'

handler = FileWatchHandler(path)
observer = Observer()
observer.schedule(handler, os.path.dirname(os.path.realpath(path)))
observer.start()

try:
    observer.join()
except KeyboardInterrupt:
    pass

observer.stop()
handler.close()

print('closed')

※ ちょっと修正してみたけど、やっぱり微妙

    def tail_f(self):
        filesize = os.stat(self.path)[ST_SIZE]
        if self.fpos:
            if self.fpos <= filesize:
                self.fp.seek(self.fpos)
            else:
                self.init()

        while True:
            self.fpos = self.fp.tell()
            line = self.fp.readline()
            
            if not line:
                break
            self.analyze(line)

(ログローテーションはともかく、)あとはanalyze関数に新しくappendされた行が入ってくるので、それぞれ解析すればOK。

ログは定期的に分割されて別ファイルに移動、新しいログが入ってきたら新規ファイルになるので、on_createdで開き直し(実はseekだけすればいいのかな?)。このとき、作成と同時に内容が書き込まれるとon_modifiedが落としちゃったのでファイル末尾への移動は入れてない。それからリネームでファイルがやってきても動かない(on_movedでいけるけど)。

observer.joinって、joinだし別スレッドの終了待ちだと思うので、これでいいかな、と思ったけど公式サンプル(GitHub - gorakhargosh/watchdog: Python library and shell utilities to monitor filesystem events.)は違う...。まあプロセス死んだらなんにせよ止まると願って。

末尾追従動作がけっこう面倒くさいので、すなおにtail -fをPopenすればよかったかな...

diffもそのうち使うかも? 今回は関係ないけど

Open JTalk mpg123

Open JTalkの出力したwavをmpg123で再生したらエラー出た。

[src/libmpg123/layer1.c:30] error: Illegal bit allocation value.
[src/libmpg123/layer1.c:171] error: Aborting layer I decoding after step one.

ffmpegでmp3に変換して再生するとok。mpg123では同じくエラーで変換できなかった。

ffmpeg -i b.wav b.mp3
mpg123 b.mp3

ffmpegのinfo

Guessed Channel Layout for Input Stream #0.0 : mono
Input #0, wav, from 'b.wav':
  Duration: 00:00:01.17, bitrate: 768 kb/s
    Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 48000 Hz, mono, s16, 768 kb/s
Input #0, mp3, from 'b.mp3':
  Metadata:
    encoder         : Lavf57.56.101
  Duration: 00:00:01.20, start: 0.023021, bitrate: 65 kb/s
    Stream #0:0: Audio: mp3, 48000 Hz, mono, s16p, 64 kb/s
ffmpeg -i b.wav c.wav

これもダメ

Guessed Channel Layout for Input Stream #0.0 : mono
Input #0, wav, from 'c.wav':
  Metadata:
    encoder         : Lavf57.56.101
  Duration: 00:00:01.17, bitrate: 768 kb/s
    Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 48000 Hz, mono, s16, 768 kb/s
ffmpeg -i b.mp3 d.wav

これもダメ

Guessed Channel Layout for Input Stream #0.0 : mono
Input #0, wav, from 'd.wav':
  Metadata:
    encoder         : Lavf57.56.101
  Duration: 00:00:01.17, bitrate: 768 kb/s
    Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 48000 Hz, mono, s16, 768 kb/s
ffmpeg -i b.wav -ac 2 e.wav

ステレオにしてみたけどこれもダメ

Guessed Channel Layout for Input Stream #0.0 : stereo
Input #0, wav, from 'e.wav':
  Metadata:
    encoder         : Lavf57.56.101
  Duration: 00:00:01.17, bitrate: 1536 kb/s
    Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 48000 Hz, stereo, s16, 1536 kb/s

うーん...

import subprocess

p = subprocess.Popen('open_jtalk -x /var/lib/mecab/dic/open-jtalk/naist-jdic -m mei/mei_normal.htsvoice -r 1.0 -ow /dev/stdout', stdin=subprocess.PIPE, stdout=subprocess.PIPE, shell=True)

text = msg
out, _ = p.communicate(text.encode('utf-8'))
wav = out

p = subprocess.Popen('ffmpeg -i pipe:0 -f mp3 pipe:1', stdin=subprocess.PIPE, stdout=subprocess.PIPE, shell=True)
out, _ = p.communicate(wav)

sound = out

p = subprocess.Popen('mpg123 -', stdin=subprocess.PIPE, shell=True)
p.communicate(sound)

どう考えても遅くなるから、別の再生プログラムを試した方がいいのかな

Open JTalk

Open JTalk

# Open JTalk Test
# python3
# exec `apt install open-jtalk open-jtalk-mecab-naist-jdic`
# get `mei_normal.htsvoice` from http://www.mmdagent.jp/

import subprocess

p = subprocess.Popen('open_jtalk -x /var/lib/mecab/dic/open-jtalk/naist-jdic -m mei/mei_normal.htsvoice -r 1.0 -ow /dev/stdout', stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)

text = 'こんにちは'
out, err = p.communicate(text.encode('utf-8'))

print(text)
print(err)

# outにwavのバイナリが入ってる。必要に応じてここ変えてね
with open('b.wav', 'wb') as fp:
    fp.write(out)

See

  • mpg123

Java 11

あんまり分かってないけど、とりあえずJRE付きでエクスポートまではできた。

Java 9以降のこと

これまでの実行可能Jar

  • Javaランタイム(JRE)を同梱しなくても、システムにインストールされたPublic JREで実行できた

これから

  • システムにJREがない
  • 同様に実行できるようにするには、JREをアプリケーションに同梱する必要がある(?)
    • これまでもrt.jarJREの同梱された形としてあったはず

Jigsawモジュールシステム

Java 9以降導入されたJavaの依存関係システム。

依存関係はmodule-info.javaファイルに記述する。

module sample.module {
    requires java.base; // 不要(省略可)
}

実行可能Jarのエクスポート(MavenJREなし)

Java 8まで、こんな感じだった。

Maven pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example.sample</groupId>
    <artifactId>Sample</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>8</java.version>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.1</version>

                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <mainClass>com.example.sample.Main</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

JREの生成(手動、module-info使用)

コンパイルしたクラスファイルのディレクトリを指定

jlink --module-path './classes;JDK_DIRECTORY/jmods' --add-modules sample.module --output jre

Jarを指定

jlink --module-path './sample.jar;JDK_DIRECTORY/jmods' --add-modules sample.module --output jre

圧縮

-c 2

起動用スクリプトも生成してくれる。

--launcher mycmd=sample.module/com.example.sample.Main

OpenJDK 11.0.1 (Windows)で実行すると、dll, exe, ランチャーシェルスクリプト, bat他が生成された。OpenJDKの中身を絞って移してきてる? 複数OS想定する場合、JREはOS別に作る必要があるのか? それとも別の方法があるのか。

実行可能Jarのエクスポート(MavenJREあり、ランチャー)

上の処理を自動化してくれるプラグインを使う。

ant-runの部分はjlinkツールが出力ディレクトリを上書きしてくれないので、packageを実行するごとに削除させるため。手動で消すかcleanすれば不要。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example.sample</groupId>
    <artifactId>Sample</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>11</java.version>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
    </properties>

    <build>
        <plugins>
            <!-- https://mvnrepository.com/artifact/org.moditect/moditect-maven-plugin -->
            <plugin>
                <groupId>org.moditect</groupId>
                <artifactId>moditect-maven-plugin</artifactId>
                <version>1.0.0.Beta2</version>
                <executions>
                    <execution>
                        <id>create-runtime-image</id>
                        <phase>package</phase>
                        <goals>
                            <goal>create-runtime-image</goal>
                        </goals>
                        <configuration>
                            <compression>2</compression>
                            <modulePath>
                                <path>${project.build.outputDirectory}</path>
                            </modulePath>
                            <modules>
                                <module>sample.module</module>
                            </modules>
                            <launcher>
                                <name>Sample</name>
                                <module>sample.module/com.example.sample.Main</module>
                            </launcher>
                            <outputDirectory>
                                ${project.build.directory}/jlink
                            </outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

            <!-- https://mvnrepository.com/artifact/org.apache.maven.plugins/maven-antrun-plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-antrun-plugin</artifactId>
                <version>1.8</version>
                <executions>
                    <execution>
                        <phase>initialize</phase>
                        <goals>
                            <goal>run</goal>
                        </goals>
                        <configuration>
                            <target>
                                <delete dir="${project.build.directory}/jlink" quiet="true" />
                            </target>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

ただ、この方法だとjarファイルが見当たらない。たぶんruntimeも自分のプログラムも混ざってどこかに入ってる。外部jarを混ぜずに読み込む方法もわからない。

JREの生成(手動、Jar使用)

module-infoを使わない方法(使わないでいいのかは分からないけど)。

jdeps --list-deps JARファイルmodule-infoのないJARの依存するモジュールリストが出力される。

このリストをもとに下のコマンドでJREを生成すれば混ざることはないはず。

jlink -c 2 -p 'JDK_DIRECTORY/jmods' -m モジュールカンマ区切りリスト --output jre

生成されたjava実行ファイルに対してjava -jar JARファイルするスクリプトを書けば外にjarを置ける。

外部Jarライブラリをlibsディレクトリに置く、というのを次はやりたい。今まで通りクラスパスに追加して、JREだけ上の方法で生成する、実行はスクリプトから、は正解じゃない気がするので、どうすればいいのか。それから、Launch4j使いたい場合?