公開
duel-loggerの画面取り込みの負荷と、自動入力の取りこぼしを改善した
duel-loggerの画面取り込みで、使わない値の照合や毎フレームの再描画を減らし、保存前に読めたレート・DPが記録に入らないことがある問題を直した記録です。テンプレート照合の枝刈り、ランク照合のWorker移動、書き込み先の固定と入力済みの値の確認を、テストで確かめた範囲と分けて説明します。
duel-logger の画面取り込みは、共有したマスターデュエルの画面をブラウザ内で解析し、コイントス、先攻後攻、勝敗に加えて、ホーム画面のランク、レート、DPを対戦記録へ入れる機能です。 使い方は画面キャプチャで対戦を自動記録する記事にまとめています。

画面取り込みを始める前のパネルです。「ランク/レート/DP自動記録」の歯車から、個別の自動記録を設定できます。
2026年10月9日と10日に、この取り込みについて2つの問題を直しました。 1つ目は、取り込み中の処理に、記録に使わない値の照合や、表示が変わらない再描画が含まれていたことです。 2つ目は、対戦の保存が終わる前にホーム画面でレートやDPを読めた場合に、その値が記録に入らないことがあった点です。
この記事では、変更前と変更後の処理を実装に沿って説明します。 本稿では変更前後の処理時間やCPU使用率の比較計測を行っていないため、高速化の倍率は示しません。 以下で示す処理の検証結果は、どれもテストによるものです。 テンプレート照合の最適化で照合結果が変わらないことと、各処理が意図した条件で動くことを確かめています。
画面取り込みの処理の流れ
画面共有で受け取った映像は、取り込み用のWorkerがフレームごとに受け取ります。 Workerは、ブラウザで画面の描画とは別のスレッドとして動く処理です。 取り込み用のWorkerは、判定に使う矩形の領域だけをフレームから切り出し、グレースケールにしてメイン側へ送ります。 この領域をROI(Region of Interest)と呼び、コイントスの文字、リザルトの見出し、ホーム画面のレートの数字などにそれぞれ割り当てています。
メイン側は、画面の描画と同じスレッドです。 受け取ったROIを白と黒の2値画像にし、あらかじめ用意した文字やラベルの画像(テンプレート)を位置をずらしながら重ねて、一致の度合いを点数にします。 点数にはIoU(Intersection over Union)を使います。 2値化で値が1になった画素を前景と呼ぶと、IoUは次の式で求めます。
IoU = 両方で前景の画素数 ÷ どちらかで前景の画素数
ホーム画面のレートとDPの数字は、メイン側とは別の、ホーム画面の値を照合する専用のWorkerで照合していました。 ランクの照合は、10月10日の変更までメイン側で行っていました。
記録に使わない値は照合しない
変更前は、ホーム画面のDUELボタンを検出している間、レート、DP、ランクの3つを、対戦種別や設定に関係なく一定の周期で照合していました。 記録フォームで値を採用するのは対戦種別と設定に合う値だけなので、残りの照合結果は使われていませんでした。
10月9日の変更で、照合する値を、記録側で値を採用する条件と同じにしました。 レート戦ではレート、DCではDP、ランクマッチではランクだけを照合します。 いずれも、取り込みパネルの「ランク/レート/DP自動記録」と、歯車の設定の「レート自動記録」などの個別設定がオンのときだけです。 対象から外した値は照合もWorkerへの送信もせず、外した時点で照合中だった結果も反映しません。
10月10日には、取り込み用のWorkerでも、照合しない値のROIを切り出さないようにしました。 DUELボタンの領域は、ホーム画面とデッキ画面の判定にも使うため、常に切り出します。
ランク帯を縮小画像で絞り込んでから原寸で確かめる
ホーム画面のランクは、ルーキーからマスターまでの7つのランク帯(以下、帯)と、帯の後ろの段位の数字で表示されます。 帯の判定では、帯ごとのテンプレートをランクのROIの中で動かし、最も点数が高い位置を探します。 変更前は、7つの帯それぞれについて、ROI全体を原寸のまま3画素ずつずらして探し、最良の位置の周りを1画素ずつ探し直していました。
10月9日の変更で、この探索を2段階にしました。
- ROIの画像と帯のテンプレートを、縦横4分の1に縮小します。縮小後の各画素には、元の4×4画素の中にある前景の数を入れます。
- 縮小画像のすべての位置で、重み付きのIoU(各画素の小さいほうの値の合計 ÷ 大きいほうの値の合計)を求めます。
- 帯ごとに、隣の位置に自分より点数の高い位置がない位置(極大)を上位6件まで残し、7つの帯を合わせた中から点数の高い20件を選びます。
- 20件それぞれについて、縮小画像の1画素分の余白を付けた狭い範囲を原寸で探し、原寸の点数が最も高い帯と位置を採用します。
縮小画像では4×4画素の中の形を区別できないため、縮小した段階の順位は原寸の順位と一致するとは限りません。 そこで帯ごとに複数の位置を残し、帯をまたいで上位の候補を原寸で確かめる形にしています。
この変更は探索の方法そのものを変えるため、ROI全体を原寸で探す方法と常に同じ結果になることは保証していません。 テストでは、正解と同じ帯の模様が別の位置にある画像や、他の帯の模様によって正解の帯の縮小段階の順位が4位以下になる画像を合成し、正しいランクを判定することを確かめています。 既存の回帰テストでは、実際のゲーム画面から切り出したランクの画像7枚(ルーキーからマスターまで各1枚)で、期待するランクと最低点数を満たすことを確かめています。
テンプレート照合の計算を減らす
テンプレート照合では、探索する範囲の位置ごとにIoUを計算します。 変更前は位置ごとにテンプレートの全画素を走査し、窓の中の前景の数と、重なった前景の数を数えていました。 10月10日の変更で、この計算に3つの手を加えました。
1つ目は積分画像です。 積分画像は、画像の左上から各画素までの矩形に含まれる前景の数を、あらかじめ数えておいた表です。 任意の矩形の前景の数を、表の4つの値の足し引きで求められます。 同じ画像を複数のテンプレートで探すときは、積分画像を1回だけ作って使い回します。
2つ目は前景のオフセットです。 テンプレートの前景がある画素について、重ねたときの画像上の位置を、画像の1行の幅を使った通し番号に直した一覧を作ります。 この一覧は、テンプレートと画像の幅の組ごとに1回だけ作ります。 重なった前景を数えるときは一覧の位置だけを調べるため、テンプレートの背景の画素は走査しません。
3つ目は、IoUの上限を使った枝刈りです。 窓の中の前景の数をW、テンプレートの前景の数をTとします。 重なった前景の数はWとTの小さいほうを超えず、どちらかで前景の画素数はWとTの大きいほうを下回りません。 したがって、IoUには次の上限があります。
IoU ≦ WとTの小さいほう ÷ WとTの大きいほう
Wは積分画像から4回の参照で分かるため、この上限がその時点の最良の点数以下になる位置では、重なりを数えずに次の位置へ進みます。 最良の位置は点数が最良の点数を上回ったときだけ更新するので、飛ばした位置が結果になることはありません。 探索結果の点数と位置は、すべての位置を数える場合と同じです。 あわせて、ランクの帯の縮小テンプレートと、段位の数字の拡大縮小したテンプレートも、テンプレートごとに1回だけ作るようにしました。
結果が変わらないことは、3種類のテストで確かめています。
- 枝刈りをしない場合に、すべての位置で変更前の採点と同じIoUになることを確かめるテスト
- ランダムに作った画像、テンプレート、探索範囲で、点数と位置が変更前の実装と完全に一致し、同点の位置が複数あるときも同じ位置を返すことを確かめるテスト
- 実際のゲーム映像から取り出した22枚のフレームについて、コイントス、先攻後攻、ライフ、リザルト、勝敗の表示、手札確認、デッキ画面、ランクの各照合の点数、位置、判定を、保存した値と比べるテスト
保存した値は、テンプレート、ROI、閾値を意図して変えたときだけ作り直します。 以後の最適化で照合結果が変わった場合は、このテストで検出できます。
ランクの照合をWorkerへ移す
10月10日の変更で、ランクの照合もメイン側からホーム画面の値を照合するWorkerへ移しました。 照合はWorker側で行い、メイン側はランクのROIの画像を複製して送り、結果を受け取ります。
Workerの結果は後から非同期に届くため、対戦が始まったあとに、始まる前の画像の結果が届くことがあります。 ランクの値は直近3回の読み取りの多数決で採用し、新しい対戦の記録が始まると読み取りの候補を作り直します。 ここに開始前の画像の結果が遅れて入ると、対戦が始まる前のホーム画面で読んだランクが、新しい対戦の候補に混ざります。
そこで、照合の依頼ごとに、取り込み用のWorkerが画像を受け取った時刻を持たせ、対戦開始の時刻と比べるようにしました。 画像を受け取った時刻が対戦開始の時刻と同じか、それより前なら、結果がいつ届いてもランクとしては採用しません。 画面の判定で対戦開始を検出した場合は、開始を判定したフレームの時刻を対戦開始の時刻として扱います。 レートとDPは、直前の対戦の記録へ入れる値なので、開始前の画像の結果も採用します。
テストでは、開始時刻以前に受け取った画像の結果が届くタイミングを変えても採用しないことと、開始と同じ時刻の画像は採用せず、直後の画像は採用することを確かめています。 読み取りを停止したあとや、検出の設定を変えたあとに古い結果が届いた場合に反映しないことも、テストで確かめています。
画面の再描画を減らす
取り込みの処理は、フレームを受け取るたびに状態を更新します。 状態には、最後にフレームを受け取った時刻、フレームレート、各照合の結果などが含まれます。 変更前は、取り込みパネルなどの通常の画面がこの状態全体を購読していたため、フレームごとに再描画が起きていました。
10月10日の変更で、通常の画面が購読する状態を、取り込みの開始・停止やエラー、判定の段階、映像の解像度など、画面に表示する項目だけに分けました。 これらの値が変わったときだけ通知し、同じ処理の中で複数回変わった場合は1回の通知にまとめます。 映像の解像度は、値が変わったときだけ新しいオブジェクトにし、毎フレーム作り直さないようにしました。 照合の点数などをすべて表示するデバッグ用のパネルは、取り込みパネルとデバッグ用のパネルの両方を開いている間だけ、状態全体を購読します。
記録ページでは、画面の部品に渡す値を、元になる値が変わったときだけ作り直すようにし、関係のない入力で取り込みパネルと記録一覧を再描画しないようにしました。 テストでは、メモを入力しても取り込みパネルと記録一覧が再描画されないことを確かめています。 デッキ名の変更を確かめるための15秒ごとの再取得は、全ページで共通の枠組みの1か所だけで行うようにしました。 集計ページの本体は、この再取得のたびに再描画されません。
閉じたプレビューに映像を流さない
取り込みパネルには、共有中の画面のプレビューが表示されます。 変更前は、取り込み中であればプレビューに映像をつないだままで、パネルの開閉やタブの表示状態では切り替えていませんでした。
10月10日の変更で、取り込みパネルを閉じている間と、プレビューを置いた文書が非表示の間は、プレビューから映像を外すようにしました。 プレビューを別ウィンドウに表示している場合は、そのウィンドウの文書が非表示かどうかで判断します。 表示に戻ると映像をつなぎ直して再生します。
解析は、取り込み用のWorkerへ渡した映像の複製から行っています。 プレビューは元の映像を表示しているだけなので、プレビューから映像を外しても元の映像は止まらず、自動記録は続きます。 テストでは、パネルの開閉と文書の表示状態に合わせて映像を付け外しすることと、外しても元の映像を止めないことを確かめています。
保存前に読めたレートとDPを、保存した対戦へ書き込む
対戦が終わってホーム画面に戻ると、レート戦ならレート、DCならDPが更新後の値で表示されます。 duel-logger はこの値を繰り返し読み取ります。 直近3回の読み取りで最も多い値が最新の値と同じで、その値を2回以上読めたときに採用し、対戦記録へ書き込みます。
変更前は、書き込み先を「シートの中で最新のレート戦またはDCの対戦」としていました。 最新の対戦は、値を採用する直前にサーバーから取り直していました。 読み取りの候補は、その時点の書き込み先の対戦に結び付けて集めていました。
ホーム画面に更新後の値が表示されるタイミングは、終わった対戦の保存が完了する前のことがあります。 その時点でサーバーにある最新の対戦は、まだ1つ前の対戦です。 このため、保存の完了前に読めた値は1つ前の対戦に結び付けて集められ、採用するかの検証も1つ前の対戦の値と比べて行われていました。 保存が完了して最新の対戦が切り替わると、候補は集め直しになりました。 集め直す前にホーム画面を離れると、その対戦にはレートやDPが入りませんでした。
10月9日の変更で、書き込み先を、対戦の終了で保存を始めた対戦に固定しました。 保存を始めるときに書き込み先を作り、そこに試合種別と勝敗、比較に使う保存前の基準値を持たせます。 基準値は、現在のシートで直近のレート戦のレートと、直近のDCのDPです。 読み取りの候補はその時点からこの書き込み先に集め、保存の完了前に採用した値も保持します。 保存に成功すると保存した対戦を書き込み先に結び付けて書き込み、保存に失敗した場合は書き込み先を破棄して書き込みません。 次の対戦の保存を始めると新しい書き込み先に替わり、それ以降の読み取り結果は、前の対戦への新しい書き込みには使いません。 採用のたびにサーバーから書き込み先を取り直す処理は、この変更でなくしました。
採用する値の検証も、書き込み先の基準値を使う形にしました。 レートは、保存前の値と同じ読み取り値を採用しません。 ホーム画面に更新前のレートが残っている間の読み取りを除くためで、変更前にあった変動幅の条件(勝敗の向きに1.5〜20)は使わなくなりました。 DPの条件は変更前と同じで、勝ちなら基準値から1,000〜1,500増、負けなら500〜1,200減の範囲だけを採用し、勝敗が不明で基準値がある場合は採用しません。
書き込む前に入力済みの値を確かめ、失敗したら再試行する
同じ10月9日の変更で、書き込みの条件も見直しました。 自動の書き込みは、始める前に、保存した対戦にレートやDPがすでに反映されているかを確かめ、反映されていれば行いません。 保存時に入力済みだった値も、保存後に記録画面で同じ対戦へ入力した値も、この確認の対象です。 記録画面で値を入力すると、その更新結果が書き込み先にも反映され、それ以降に採用した値は書き込みません。
書き込みに失敗した場合は完了にせず、次に読み取りで値が採用されたときに再試行します。 書き込み中に別の値が採用されていて、書き込みが失敗した場合は、追加の読み取りを待たずに新しい値を書き込みます。 書き込みに一度成功したら、その対戦には同じ書き込み先から再度書き込みません。
テストでは、保存前に採用した値が保存後に書き込まれること、新しい書き込み先を始めたあとの読み取り結果を前の対戦へ書き込まないこと、値が入力済みの対戦へ書き込まないこと、失敗後の再試行の順序などを確かめています。
テストで確かめたことと、本稿で計測していないこと
テストで確かめたのは、次の内容です。
- テンプレート照合の最適化の前後で、点数、位置、判定が変わらないこと
- ランク帯の2段階の探索で、紛らわしい模様を合成した画像と実際のランクの画像7枚のランクを正しく判定すること
- ランクの結果を、対戦開始の前後で正しく採用または破棄すること
- 通常の画面とデバッグ用のパネルの購読、プレビューの映像の付け外し
- 書き込み先の固定、入力済みの値の確認、書き込みの再試行
一方で、本稿では変更前後の処理時間やCPU使用率の比較計測を行っていません。 本番環境での負荷の変化も、本稿では確認していません。 照合する値と再描画を減らしたことは実装から説明できますが、それによってどれだけ軽くなったかは、この記事では数値で示していません。
記事内容に誤りを見つけた場合は お問い合わせ から報告してください。