【Golang】プロセカのリザルト画像からスコアをまとめるプログラムを作った話

# はじめに

　どうも、Shakku です。
　都内某高専の情報科2年生です。(2023/1/25時点)
　バックエンドのエンジニアを目指すべく、Go言語を個人でお勉強中です。
　今回はじめてQiitaの記事を書いてみるので、誤字脱字や変なところがあると思いますがお許しを...

# 制作経緯

　普段[プロセカ](https://pjsekai.sega.jp/)をプレイしていて、楽曲ごとの良かったリザルトを見返したいなーと思った時がよくあります。

> <img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2998568/18b16a39-a7df-6c0f-1062-41991be15dc4.jpeg">
>惜しかったリザルト

　スクリーンショットを撮ったりTwitterに投稿したりはよくしますが、昔に出したリザルトを振り返るときはちょっと面倒なんですよね。
　また、ゲーム内の機能ではキャラ育成面でのハイスコアしか振り返れません。

　そこで、Go言語の勉強も兼ねて、プロセカのリザルト画像からスコアをまとめるプログラムを作成してみました。

　githubにも上げています。
https://github.com/Shakkuuu/pjsekai-score-collection

# 開発環境

- Macbook air M1
- Visual Studio Code
- Go 1.19.5
- [tesseract](https://ja.wikipedia.org/wiki/Tesseract_(%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2))

# 注意事項

- このプログラムは、自分が普段音ゲーに使用してる端末である、 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でのtesseractインストール
$ brew install tesseract
```

```: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](https://ja.wikipedia.org/wiki/Tesseract_(%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2))という、画像から文字や数字を認識して出力するOCRと呼ばれるものを利用してリザルトの数字を出力し、それをフォーマットを整えてtxtファイルに保存します。

# 使い方

1. imgフォルダにリザルトのスクリーンショットを入れる
2. ``` go run main.go ``` で起動
3. 画像ファイル名を入力してください: と聞かれるので、imgに入れたリザルト画像のファイル名を入力する(拡張子も入力してください)
4. 楽曲名を入力してください:　と聞かれるので、楽曲名を入力してください
5. scorelist.txtに保存される

# プログラム全文
>
```:main.go
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 `すると、下の画像のように聞かれるので、答えましょう
> <img width="80%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2998568/b2d05c0e-7704-e0bd-13a1-08534f796fd7.png">

- 答えたらターミナル上にも結果が表示されつつ、score.txtやscorelist.txtに下の画像のように出力されます
> <img width="40%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2998568/c8a53d02-6c85-39c0-d316-e30f4a882440.png">
> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2998568/b897e358-e59c-49a6-f84f-197bf4fe1127.png">
> scorelist.txtには過去に実行した内容の後に追記されます。

- 途中のスコア部分だけを切り取ったcut.jpegは下の画像のようになっています
> <img width="20%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2998568/7622891f-b910-04ae-501e-8c4786c2b397.png">

# 最後に

　今回は標準パッケージとtesseractというものを組み合わせて作成してみました。
　自分の場合は普段からTwitterに良かったリザルトを上げているため、そこから保存してimgフォルダに突っ込んで開ければいいのですが、もっと簡単にリザルト画像をアップロードできるようにしたいなーと考えています。(例:finderを開いてくれてそこから選択。localhostでwebサーバ起動して、htmlのform経由でアップロードする。)
　また、コードに関しては、ぐちゃぐちゃにならないよう、見やすく変数名もわかりやすく書いたつもりですが、まだまだ初心者なので何かご指摘あればお願いします。
　見返していて、冗長的であったりわざわざ一旦保存しなくても良かったんじゃね？っていうようなところがあったりしそうなので、修正や外部パッケージを使用して簡略化してみたいなと思います。

## 参考リンク
- [MacでTesseract OCRを使って画像内のテキストを取り出してみる](https://rooter.jp/ml/tesseract-with-mac/)
- [Golang で ImageMagick を使わずに画像をいじる](https://shiro-16.hatenablog.com/entry/2020/05/29/130508)
- [Goでファイル追記・存在しなかったらファイル作成したい！](https://qiita.com/pseuxide/items/fae07e8533b36f553714)