はじめに
どうも、Shakku です。
都内某高専の情報科2年生です。(2023/1/25時点)
バックエンドのエンジニアを目指すべく、Go言語を個人でお勉強中です。
今回はじめてQiitaの記事を書いてみるので、誤字脱字や変なところがあると思いますがお許しを...
制作経緯
普段プロセカをプレイしていて、楽曲ごとの良かったリザルトを見返したいなーと思った時がよくあります。
惜しかったリザルト
スクリーンショットを撮ったりTwitterに投稿したりはよくしますが、昔に出したリザルトを振り返るときはちょっと面倒なんですよね。
また、ゲーム内の機能ではキャラ育成面でのハイスコアしか振り返れません。
そこで、Go言語の勉強も兼ねて、プロセカのリザルト画像からスコアをまとめるプログラムを作成してみました。
githubにも上げています。
https://github.com/Shakkuuu/pjsekai-score-collection
開発環境
- Macbook air M1
- Visual Studio Code
- Go 1.19.5
- tesseract
注意事項
- このプログラムは、自分が普段音ゲーに使用してる端末である、 GalaxyS10 のスクリーンショット画像の縦横比に合わせて作成されています。使用する端末のスクリーンショット画像の縦横比に合わせて、読み込み用の切り取る範囲の設定を行ってください。
cimg := img1.(SubImager).SubImage(image.Rect(950, 570, 1075, 820))
// image.Rect(x0,y0,x1,y1) x0,y0,x1,y1,の部分を変更して端末ごとのスクリーンショットに合わせる
- os/execを使用してtesseractを実行しているため、環境によっては動作しない可能性があります。
$ brew install tesseract
ファイルに結果を出力する場合
$ tesseract 読み込むファイル 出力するファイル
ターミナルに出力する場合
$ tesseract 読み込むファイル -
-
「ひとりでライブ」でのリザルト用に制作したため、「チャレンジライブ」のリザルトには対応していません。(切り取る範囲の設定をすればいけるかも?)
-
勉強のために作ったちょっとしたプログラムなので、煮るなり焼くなり自由に参考,引用してもらっても大丈夫です。(記事にいいねを押してもらえたら承認欲求が爆あがりします)
ディレクトリ構成
.
├──img
│ └──リザルトの画像
├──cut.jpeg
├──score.txt
├──scorelist.txt
└──main.go
- img:リザルトのスクリーンショット画像を入れておくフォルダ
- cut.jpeg:リザルト読み込みに使用する加工後の画像(事前に用意する必要なし)
- score.txt:読み込ませたそのリザルト画像のリザルトを保存するtxtファイル(事前に用意する必要なし)
- scorelist.txt:これまでに読み込ませたリザルトの一覧が書かれているtxtファイル(事前に用意する必要なし)
- main.go:作成したプログラム
機能
リザルトのスクリーンショットをimgフォルダに入れて実行します。
ターミナル上で読み込ませるファイル名と楽曲名を聞かれるので入力すると、リザルト画像をリザルトだけになるよう切り取り(cut.jpeg)、その画像をtesseractという、画像から文字や数字を認識して出力するOCRと呼ばれるものを利用してリザルトの数字を出力し、それをフォーマットを整えてtxtファイルに保存します。
使い方
- imgフォルダにリザルトのスクリーンショットを入れる
-
go run main.go
で起動 - 画像ファイル名を入力してください: と聞かれるので、imgに入れたリザルト画像のファイル名を入力する(拡張子も入力してください)
- 楽曲名を入力してください: と聞かれるので、楽曲名を入力してください
- scorelist.txtに保存される
プログラム全文
package main
import (
"bufio"
"fmt"
"image"
"image/jpeg"
"os"
"os/exec"
"strconv"
)
// Score struct
type Score struct {
Name string
Perfect string
Great string
Good string
Bad string
Miss string
}
// 画像加工用のインターフェイス
type SubImager interface {
SubImage(r image.Rectangle) image.Image
}
var (
openimg, name, sperfect, sgreat, sgood, sbad, smiss string // s〇〇はstringのスコア
scanls []string // tesseractで読み込んだ値の配列
iperfect, igreat, igood, ibad, imiss int // i〇〇はintのスコア
atoierr error
)
func main() {
fmt.Print("画像ファイル名を入力してください: ")
fmt.Scanln(&openimg)
fmt.Print("楽曲名を入力してください: ")
namebufio := bufio.NewScanner(os.Stdin)
namebufio.Scan()
name = namebufio.Text()
// 指定された画像のオープン
f, err := os.Open("./img/" + openimg)
if err != nil {
fmt.Println("open err:", err)
return
}
defer f.Close()
img1, _, err := image.Decode(f)
if err != nil {
fmt.Println("decode err:", err)
return
}
fso, err := os.Create("cut.jpg")
if err != nil {
fmt.Println("create err:", err)
return
}
defer fso.Close()
cimg := img1.(SubImager).SubImage(image.Rect(950, 570, 1075, 820)) // image.Rect(x0,y0,x1,y1) ここを変更して端末ごとのスクリーンショットに合わせる
jpeg.Encode(fso, cimg, &jpeg.Options{Quality: 100})
// 数字取得
imgname := "cut.jpg"
out, err := exec.Command("tesseract", imgname, "-").Output() // execでtesseract実行
if err != nil {
fmt.Println("command err", err)
return
}
os.WriteFile("score.txt", out, 0644)
fp, err := os.Open("score.txt")
if err != nil {
fmt.Println("open err", err)
return
}
defer fp.Close()
// score.txtに出力されたスコアを取り出してリスト化
scanner := bufio.NewScanner(fp)
for scanner.Scan() {
fmt.Println(scanner.Text())
scanls = append(scanls, scanner.Text())
}
fmt.Println(scanls)
// 頭の 0 削除
iperfect, atoierr = strconv.Atoi(scanls[0])
igreat, atoierr = strconv.Atoi(scanls[1])
igood, atoierr = strconv.Atoi(scanls[2])
ibad, atoierr = strconv.Atoi(scanls[3])
imiss, atoierr = strconv.Atoi(scanls[4])
if atoierr != nil {
fmt.Println(err)
}
// stringに戻す
sperfect = strconv.Itoa(iperfect)
sgreat = strconv.Itoa(igreat)
sgood = strconv.Itoa(igood)
sbad = strconv.Itoa(ibad)
smiss = strconv.Itoa(imiss)
// 構造体に埋め込み
sc := Score{
Name: name,
Perfect: sperfect,
Great: sgreat,
Good: sgood,
Bad: sbad,
Miss: smiss,
}
msg := "楽曲名: " + sc.Name + ", Perfect: " + sc.Perfect + ", Great: " + sc.Great + ", Good: " + sc.Good + ", Bad: " + sc.Bad + ", Miss: " + sc.Miss
// fmt.Printf("楽曲名: %s, Perfect: %d, Great: %d, Good: %d, Bad: %d, Miss: %d", sc.Name, sc.Perfect, sc.Great, sc.Good, sc.Bad, sc.Miss)
fmt.Println(msg)
// scorelist.txtに保存。ファイルがない場合は作成
addf, err := os.OpenFile("scorelist.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
fmt.Println(err)
return
}
defer addf.Close()
fmt.Fprintln(addf, msg)
}
プログラムの補足
-
go run main.go
すると、下の画像のように聞かれるので、答えましょう
- 答えたらターミナル上にも結果が表示されつつ、score.txtやscorelist.txtに下の画像のように出力されます
scorelist.txtには過去に実行した内容の後に追記されます。
- 途中のスコア部分だけを切り取ったcut.jpegは下の画像のようになっています
最後に
今回は標準パッケージとtesseractというものを組み合わせて作成してみました。
自分の場合は普段からTwitterに良かったリザルトを上げているため、そこから保存してimgフォルダに突っ込んで開ければいいのですが、もっと簡単にリザルト画像をアップロードできるようにしたいなーと考えています。(例:finderを開いてくれてそこから選択。localhostでwebサーバ起動して、htmlのform経由でアップロードする。)
また、コードに関しては、ぐちゃぐちゃにならないよう、見やすく変数名もわかりやすく書いたつもりですが、まだまだ初心者なので何かご指摘あればお願いします。
見返していて、冗長的であったりわざわざ一旦保存しなくても良かったんじゃね?っていうようなところがあったりしそうなので、修正や外部パッケージを使用して簡略化してみたいなと思います。