diff --git a/app/features/scanner/core/detectors/quick-scoreboard-battle-log/rois.ts b/app/features/scanner/core/detectors/quick-scoreboard-battle-log/rois.ts index 5b57e2c67..aecfcb111 100644 --- a/app/features/scanner/core/detectors/quick-scoreboard-battle-log/rois.ts +++ b/app/features/scanner/core/detectors/quick-scoreboard-battle-log/rois.ts @@ -81,6 +81,12 @@ function statRoi(cy: number, index: 0 | 1 | 2): Roi { return { x, y: cy + 3, w: 30, h: 20 }; } +/** + * The rectified card's digits render softer than the atlas crops: at the default 150 a 7's + * antialiased bar thins out and the ink penalty hands it to 1. 90-130 read every stat cell. + */ +const STAT_BIN_THRESHOLD = 110; + /** POV arrow left of the pill (x ~901-944), reaching the pill's rounded cap (~x 940). */ function povArrowRoi(cy: number): Roi { return { x: 890, y: cy - 32, w: 56, h: 62 }; @@ -162,6 +168,7 @@ export const ROIS: BattleLogRois = { paintRoi, paintSuffixRoi, statRoi, + STAT_BIN_THRESHOLD, povArrowRoi, teamScoreRoi, resultTagRoi, diff --git a/app/features/scanner/core/detectors/scoreboard-battle-log/detector.ts b/app/features/scanner/core/detectors/scoreboard-battle-log/detector.ts index 7dfcd3e84..d3d1ec7d0 100644 --- a/app/features/scanner/core/detectors/scoreboard-battle-log/detector.ts +++ b/app/features/scanner/core/detectors/scoreboard-battle-log/detector.ts @@ -70,6 +70,8 @@ export interface BattleLogRois { paintRoi(cy: number): Roi; paintSuffixRoi(cy: number): Roi; statRoi(cy: number, index: 0 | 1 | 2): Roi; + /** stat digit binarization threshold when the default (150) erodes this layout's digits */ + STAT_BIN_THRESHOLD?: number; povArrowRoi(cy: number): Roi; teamScoreRoi(panel: PanelIndex): Roi; resultTagRoi(panel: PanelIndex): Roi; @@ -322,7 +324,7 @@ export function createBattleLogDetector( base + dy, rowRois, resources, - {}, + { statBinThreshold: rois.STAT_BIN_THRESHOLD }, speculative, ), ), diff --git a/app/features/scanner/core/detectors/scoreboard/row.ts b/app/features/scanner/core/detectors/scoreboard/row.ts index 16487a086..4e7885aaa 100644 --- a/app/features/scanner/core/detectors/scoreboard/row.ts +++ b/app/features/scanner/core/detectors/scoreboard/row.ts @@ -48,6 +48,8 @@ export interface RowOptions { weaponInkThreshold?: number; /** replay only: the left-aligned paint puts the "p" suffix inside the ROI under 4 digits */ paintDropLoweredTrailing?: boolean; + /** binarization threshold for the stat digits, for layouts whose digits render softer */ + statBinThreshold?: number; } /** @@ -71,7 +73,7 @@ export function* parseScoreboardRowSteps( const [weaponRead, { paint, name }, stats] = yield* all([ readWeapon(rgb, cy, rois, resources, options), readPaintAndName(gray, cy, rois, resources, options, speculative), - readStats(gray, cy, rois, resources, speculative), + readStats(gray, cy, rois, resources, options, speculative), ]); const { weapon, special } = weaponRead ?? { weapon: null }; @@ -178,6 +180,7 @@ function* readStats( cy: number, rois: RowRois, resources: RowResources, + options: RowOptions, speculative: boolean, ): MatchSteps { const { statDigits } = resources; @@ -186,7 +189,14 @@ function* readStats( cropRoi(gray, rois.stat(cy, i)), ); const parsed = yield* all( - crops.map((crop) => parseNumberSteps(crop, statDigits, {}, speculative)), + crops.map((crop) => + parseNumberSteps( + crop, + statDigits, + { binThreshold: options.statBinThreshold }, + speculative, + ), + ), ); for (const crop of crops) crop.delete(); return parsed; diff --git a/app/features/scanner/tests/fixtures/quick-scoreboard-battle-log/private-battle-splat-zones-bluefin-depot/expected.json b/app/features/scanner/tests/fixtures/quick-scoreboard-battle-log/private-battle-splat-zones-bluefin-depot/expected.json new file mode 100644 index 000000000..29bf93ee8 --- /dev/null +++ b/app/features/scanner/tests/fixtures/quick-scoreboard-battle-log/private-battle-splat-zones-bluefin-depot/expected.json @@ -0,0 +1,87 @@ +{ + "event": "QuickScoreboardBattleLog", + "data": { + "lobby": "PRIVATE", + "mode": "SZ", + "stage": 20, + "matchScores": [ + 100, + 0 + ], + "povIndex": 1, + "players": [ + { + "name": "でんさく", + "paint": 894, + "ka": 13, + "d": 2, + "s": 3, + "weaponId": 60 + }, + { + "name": "R.O.B.O.T", + "paint": 662, + "ka": 12, + "d": 2, + "s": 1, + "weaponId": 211 + }, + { + "name": "いちごシロップ", + "paint": 398, + "ka": 6, + "d": 2, + "s": 1, + "weaponId": 2030 + }, + { + "name": "でんさく", + "paint": 464, + "ka": 6, + "d": 4, + "s": 1, + "weaponId": 3000 + }, + { + "name": "Chronos", + "paint": 532, + "ka": 4, + "d": 7, + "s": 3, + "weaponId": 6011 + }, + { + "name": "Hawaiian", + "paint": 1102, + "ka": 3, + "d": 4, + "s": 4, + "weaponId": 7011 + }, + { + "name": "GIMME", + "paint": 588, + "ka": 3, + "d": 6, + "s": 3, + "weaponId": 5000 + }, + { + "name": "Pika ☆", + "paint": 330, + "ka": 2, + "d": 7, + "s": 2, + "weaponId": 50 + } + ], + "stageLabel": "Bluefin Depot" + }, + "options": { + "skipFields": [ + "players.1.name", + "players.2.name" + ], + "notes": "1080p VoD frame (InTheZone #52, t=11491.5) of the lobby's quick battle log, opened 16s after this game's results screen. players.4.stats and players.7.stats (deaths x07) once read 1: the rectified card's digits are softer than the atlas crops, so at the default binarization the 7's antialiased bar thins out and the ink penalty hands the segment to 1. The misread broke the match builder's history-screen fingerprint and the revisit became a duplicate match card. players.1.name reads R_O_B_Or and players.2.name ι′ちごシロッフ (small-size name misreads, unrelated). timestamp omitted: the screen shows 10:43PM on a 12h clock and the parse drops the PM." + } +} diff --git a/app/features/scanner/tests/fixtures/quick-scoreboard-battle-log/private-battle-splat-zones-bluefin-depot/frame.png b/app/features/scanner/tests/fixtures/quick-scoreboard-battle-log/private-battle-splat-zones-bluefin-depot/frame.png new file mode 100644 index 000000000..a0a6a7103 Binary files /dev/null and b/app/features/scanner/tests/fixtures/quick-scoreboard-battle-log/private-battle-splat-zones-bluefin-depot/frame.png differ