Rename cv -> scanner

This commit is contained in:
Kalle
2026-08-05 11:15:32 +03:00
parent 7195e7c588
commit d55712c68a
244 changed files with 383 additions and 362 deletions

View File

@@ -6,43 +6,43 @@
* scoreboard captured through different pipelines (OBS virtual camera at
* 720p, game capture at native 1080p) yields subtly different pixels, so
* every source contributes its own crop per character and recognition takes
* the best-scoring one. scripts/cv/build-glyph-atlas.ts then fills in the rest of
* the best-scoring one. scripts/scanner/build-glyph-atlas.ts then fills in the rest of
* the charset from the fonts, preserving these fixture-tagged glyphs.
*
* Usage: pnpm cv:bootstrap-atlas
* Usage: pnpm scanner:bootstrap-atlas
* Writes public/assets/glyphs/scoreboard-{names,paint-digits,stat-digits}.{png,json}
*/
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { loadOpenCV, type Mat } from "../../app/features/cv/core/cv";
import { loadOpenCV, type Mat } from "../../app/features/scanner/core/cv";
import {
TAG_NAME_INNER,
TAG_NAME_OUTER,
TAG_NAME_TEXT_HEIGHT,
TAG_TILT_DEG,
} from "../../app/features/cv/core/detectors/death/rois";
} from "../../app/features/scanner/core/detectors/death/rois";
import {
nameRoi,
paintRoi,
ROW_CENTERS,
statRoi,
TEAM_SCORE_ROIS,
} from "../../app/features/cv/core/detectors/scoreboard/rois";
} from "../../app/features/scanner/core/detectors/scoreboard/rois";
import {
CODE_TEXT_HEIGHT,
REPLAY_CODE_ROI,
} from "../../app/features/cv/core/detectors/scoreboard-replay/rois";
import type { AtlasMeta } from "../../app/features/cv/core/glyphs";
} from "../../app/features/scanner/core/detectors/scoreboard-replay/rois";
import type { AtlasMeta } from "../../app/features/scanner/core/glyphs";
import {
normalizeFrame,
type Roi,
toMat,
} from "../../app/features/cv/core/image";
import { CV_ASSETS_DIR } from "../../app/features/cv/node/assets-dir";
import { FIXTURES_DIR } from "../../app/features/cv/node/fixtures";
import { readImage, writePng } from "../../app/features/cv/node/image-io";
} from "../../app/features/scanner/core/image";
import { SCANNER_ASSETS_DIR } from "../../app/features/scanner/node/assets-dir";
import { FIXTURES_DIR } from "../../app/features/scanner/node/fixtures";
import { readImage, writePng } from "../../app/features/scanner/node/image-io";
const OUT_DIR = join(CV_ASSETS_DIR, "glyphs");
const OUT_DIR = join(SCANNER_ASSETS_DIR, "glyphs");
const BIN_THRESHOLD = 150;
/** A labeled header tag region (positions are fixture-specific: tags size to their text). */
@@ -667,7 +667,7 @@ for (const source of REPLAY_SOURCES) {
// Death splash-tag names (BlitzBold + Rowdy kana at ~46px). The atlas is
// nominal 42 with the detector upscaling to 46 at load (see the builder
// comment in scripts/cv/build-glyph-atlas.ts), so native-size crops shrink to
// comment in scripts/scanner/build-glyph-atlas.ts), so native-size crops shrink to
// 42 here and come back to native after the load-time upscale.
const TAG_BIN_THRESHOLD = 160; // TAG_NAME_BIN_THRESHOLD in death/index.ts
const TAG_ATLAS_HEIGHT = 42;

View File

@@ -12,10 +12,10 @@
*
* The fonts ship with the game and are not committed drop them into
* assets/fonts/ (see README). Fixture-crop bootstrapping
* (scripts/cv/bootstrap-atlas-from-fixture.ts) remains as a cross-check and as
* (scripts/scanner/bootstrap-atlas-from-fixture.ts) remains as a cross-check and as
* the fallback when the fonts are unavailable.
*
* Usage: pnpm cv:build-glyph-atlas
* Usage: pnpm scanner:build-glyph-atlas
* Writes public/assets/glyphs/scoreboard-*.{png,json}
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
@@ -24,30 +24,30 @@ import { createCanvas, GlobalFonts } from "@napi-rs/canvas";
import {
DEATH_MESSAGE_TEMPLATES,
LOCALIZED_WEAPON_NAMES,
} from "../../app/features/cv/core/detectors/death/localized-messages";
import { ALL_WEAPON_ENTRIES } from "../../app/features/cv/core/detectors/death/weapon-names";
import type { AtlasMeta } from "../../app/features/cv/core/glyphs";
} from "../../app/features/scanner/core/detectors/death/localized-messages";
import { ALL_WEAPON_ENTRIES } from "../../app/features/scanner/core/detectors/death/weapon-names";
import type { AtlasMeta } from "../../app/features/scanner/core/glyphs";
import {
ALL_LOBBY_ENTRIES,
ALL_MODE_ENTRIES,
ALL_MODE_LABELS,
ALL_STAGE_ENTRIES,
RESULT_TAG_ENTRIES,
} from "../../app/features/cv/core/localized";
import { CV_ASSETS_DIR } from "../../app/features/cv/node/assets-dir";
import { readImage, writePng } from "../../app/features/cv/node/image-io";
} from "../../app/features/scanner/core/localized";
import { SCANNER_ASSETS_DIR } from "../../app/features/scanner/node/assets-dir";
import { readImage, writePng } from "../../app/features/scanner/node/image-io";
import { readFontCoverage } from "./otf-cmap";
/**
* Atlases are hybrids: glyphs harvested from labeled fixtures
* (scripts/cv/bootstrap-atlas-from-fixture.ts, tagged source:"fixture") are exact
* (scripts/scanner/bootstrap-atlas-from-fixture.ts, tagged source:"fixture") are exact
* in-game pixels and are preserved across rebuilds; font-rendered glyphs
* fill in the rest of the charset. Recognition takes the best-scoring glyph,
* so fixture crops dominate wherever they exist.
*/
const FONTS_DIR = new URL("../../assets/fonts", import.meta.url).pathname;
const OUT_DIR = join(CV_ASSETS_DIR, "glyphs");
const OUT_DIR = join(SCANNER_ASSETS_DIR, "glyphs");
const FONT_FILES = {
BlitzMain: "BlitzMain.otf",
@@ -77,7 +77,7 @@ for (const [family, file] of Object.entries(FONT_FILES)) {
"The Splatoon fonts are proprietary and not committed. Get",
"Decrypted/BlitzMain.otf and Decrypted/BlitzBold.otf (e.g. from the",
"splatoon3-fonts repo) into assets/fonts/, or fall back to",
" pnpm cv:bootstrap-atlas",
" pnpm scanner:bootstrap-atlas",
].join("\n"),
);
process.exit(1);

View File

@@ -21,14 +21,14 @@
* CommonMsg/Weapon/WeaponName_* weapon names, mapped to the canonical
* entries via their USen value
*
* Usage: pnpm cv:build-localized-entries [path-to-splat3]
* Writes app/features/cv/core/localized-entries.ts
* and app/features/cv/core/detectors/death/localized-messages.ts
* Usage: pnpm scanner:build-localized-entries [path-to-splat3]
* Writes app/features/scanner/core/localized-entries.ts
* and app/features/scanner/core/detectors/death/localized-messages.ts
*/
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { ALL_WEAPON_ENTRIES } from "../../app/features/cv/core/detectors/death/weapon-names";
import type { CvLobby } from "../../app/features/cv/cv-types";
import { ALL_WEAPON_ENTRIES } from "../../app/features/scanner/core/detectors/death/weapon-names";
import type { ScannerLobby } from "../../app/features/scanner/scanner-types";
import { stageIds } from "../../app/modules/in-game-lists/stage-ids";
import type { ModeShort, StageId } from "../../app/modules/in-game-lists/types";
import gameMisc from "../../locales/en/game-misc.json";
@@ -37,11 +37,11 @@ const SPLAT3_DIR =
process.argv[2] ?? new URL("../../../splat3", import.meta.url).pathname;
const LANG_DIR = join(SPLAT3_DIR, "data", "language");
const OUT_ENTRIES = new URL(
"../../app/features/cv/core/localized-entries.ts",
"../../app/features/scanner/core/localized-entries.ts",
import.meta.url,
).pathname;
const OUT_MESSAGES = new URL(
"../../app/features/cv/core/detectors/death/localized-messages.ts",
"../../app/features/scanner/core/detectors/death/localized-messages.ts",
import.meta.url,
).pathname;
@@ -58,7 +58,7 @@ const RULE_KEYS: Record<string, ModeShort> = {
};
/** MatchMode key -> lobby code; USen values validate against these names. */
const LOBBY_KEYS: Record<string, CvLobby> = {
const LOBBY_KEYS: Record<string, ScannerLobby> = {
XMatch: "X",
Bankara: "SERIES",
BankaraOpen: "OPEN",
@@ -66,7 +66,7 @@ const LOBBY_KEYS: Record<string, CvLobby> = {
};
/** the English lobby tags as the game shows them, for USen validation */
const LOBBY_ENGLISH: Record<CvLobby, string> = {
const LOBBY_ENGLISH: Record<ScannerLobby, string> = {
X: "X Battle",
SERIES: "Anarchy Battle (Series)",
OPEN: "Anarchy Battle (Open)",
@@ -129,7 +129,7 @@ for (const [key, mode] of Object.entries(RULE_KEYS)) {
}
for (const [key, lobby] of Object.entries(LOBBY_KEYS)) {
const value = clean(usen["CommonMsg/MatchMode"]![key]!);
const expected = LOBBY_ENGLISH[lobby as CvLobby];
const expected = LOBBY_ENGLISH[lobby as ScannerLobby];
if (value !== expected)
throw new Error(`USen lobby ${key} is "${value}", expected "${expected}"`);
}
@@ -156,7 +156,7 @@ for (const [name, stageId] of stageIdByEnglishName) {
interface LocalizedLobby {
text: string;
lobby: CvLobby;
lobby: ScannerLobby;
}
interface LocalizedMode {
text: string;
@@ -332,7 +332,7 @@ for (const lang of languages) {
const banner = (extra: string) =>
`/**
* GENERATED by scripts/cv/build-localized-entries.ts from the splat3 repo's
* GENERATED by scripts/scanner/build-localized-entries.ts from the splat3 repo's
* language dumps do not edit by hand; regenerate when the game adds
* content. ${extra}
*/`;
@@ -345,11 +345,11 @@ writeFileSync(
* flattened match sets detectors snap OCR output against).`,
)}
import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
import type { CvLobby } from "../cv-types";
import type { ScannerLobby } from "../scanner-types";
export interface LocalizedLobby {
text: string;
lobby: CvLobby;
lobby: ScannerLobby;
}
export interface LocalizedMode {

View File

@@ -6,38 +6,38 @@
* planner PNGs (~340MB), named "<stageId>-<MODE>-<TYPE>.png" (MODE in
* CB/RM/SZ/TC/TW; TYPE in OVER/MINI/ITEMS). This tool reduces each
* PLANNER_TYPE render to the ink-invariant structural signature
* (app/features/cv/core/detectors/minimap/stage.ts) and packs all of them,
* (app/features/scanner/core/detectors/minimap/stage.ts) and packs all of them,
* quantized to uint8, into a single grayscale atlas PNG plus a manifest
* (keys "<stageId>-<MODE>") a few hundred KB the minimap detector loads
* to identify the stage. Output goes to the assets checkout
* (CV_ASSETS_DIR/planner); shipping a regen means pushing the assets repo.
* (SCANNER_ASSETS_DIR/planner); shipping a regen means pushing the assets repo.
*
* pnpm cv:build-planner-signatures
* pnpm scanner:build-planner-signatures
*/
import { mkdirSync, readdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { loadOpenCV } from "../../app/features/cv/core/cv";
import { loadOpenCV } from "../../app/features/scanner/core/cv";
import {
PLANNER_SIG_H,
PLANNER_SIG_W,
type PlannerManifest,
plannerSignature,
} from "../../app/features/cv/core/detectors/minimap/stage";
} from "../../app/features/scanner/core/detectors/minimap/stage";
import {
type FrameData,
normalizeFrame,
toMat,
} from "../../app/features/cv/core/image";
import { CV_ASSETS_DIR } from "../../app/features/cv/node/assets-dir";
import { readImage, writePng } from "../../app/features/cv/node/image-io";
} from "../../app/features/scanner/core/image";
import { SCANNER_ASSETS_DIR } from "../../app/features/scanner/node/assets-dir";
import { readImage, writePng } from "../../app/features/scanner/node/image-io";
/** which render variant the signatures are built from */
const PLANNER_TYPE = process.env.CV_PLANNER_TYPE ?? "MINI";
const PLANNER_TYPE = process.env.SCANNER_PLANNER_TYPE ?? "MINI";
const SRC_DIR =
process.env.CV_PLANNER_MAPS_DIR ??
process.env.SCANNER_PLANNER_MAPS_DIR ??
new URL("../../../assets/assets/planner-maps", import.meta.url).pathname;
const OUT_DIR =
process.env.CV_PLANNER_OUT_DIR ?? join(CV_ASSETS_DIR, "planner");
process.env.SCANNER_PLANNER_OUT_DIR ?? join(SCANNER_ASSETS_DIR, "planner");
const COLS = 5;
await loadOpenCV();

View File

@@ -4,24 +4,24 @@
* (optionally scaled up) and/or a grid overlay for visual inspection.
*
* Usage:
* vite-node -c scripts/cv/vite-node.config.ts scripts/cv/dump-crops.ts <image> <outdir> grid
* vite-node -c scripts/cv/vite-node.config.ts scripts/cv/dump-crops.ts <image> <outdir> x,y,w,h[,scale][:label] ...
* vite-node -c scripts/scanner/vite-node.config.ts scripts/scanner/dump-crops.ts <image> <outdir> grid
* vite-node -c scripts/scanner/vite-node.config.ts scripts/scanner/dump-crops.ts <image> <outdir> x,y,w,h[,scale][:label] ...
*/
import { mkdirSync } from "node:fs";
import { join } from "node:path";
import { loadOpenCV } from "../../app/features/cv/core/cv";
import { loadOpenCV } from "../../app/features/scanner/core/cv";
import {
cropRoi,
matToFrameData,
normalizeFrame,
toMat,
} from "../../app/features/cv/core/image";
import { readImage, writePng } from "../../app/features/cv/node/image-io";
} from "../../app/features/scanner/core/image";
import { readImage, writePng } from "../../app/features/scanner/node/image-io";
const [imagePath, outDir, ...specs] = process.argv.slice(2);
if (!imagePath || !outDir || specs.length === 0) {
console.error(
"usage: vite-node -c scripts/cv/vite-node.config.ts scripts/cv/dump-crops.ts <image> <outdir> (grid | x,y,w,h[,scale][:label])...",
"usage: vite-node -c scripts/scanner/vite-node.config.ts scripts/scanner/dump-crops.ts <image> <outdir> (grid | x,y,w,h[,scale][:label])...",
);
process.exit(1);
}

View File

@@ -1,27 +1,27 @@
/** biome-ignore-all lint/suspicious/noConsole: CLI script output */
/**
* Draw all scoreboard ROIs on a (normalized) frame for visual calibration.
* Usage: vite-node -c scripts/cv/vite-node.config.ts scripts/cv/overlay-rois.ts <image> [out.png] [scoreboard|scoreboard-replay]
* Usage: vite-node -c scripts/scanner/vite-node.config.ts scripts/scanner/overlay-rois.ts <image> [out.png] [scoreboard|scoreboard-replay]
*/
import { loadOpenCV, type Mat } from "../../app/features/cv/core/cv";
import * as death from "../../app/features/cv/core/detectors/death/rois";
import * as mapStart from "../../app/features/cv/core/detectors/map-start/rois";
import * as minimap from "../../app/features/cv/core/detectors/minimap/rois";
import * as sb from "../../app/features/cv/core/detectors/scoreboard/rois";
import * as replay from "../../app/features/cv/core/detectors/scoreboard-replay/rois";
import { loadOpenCV, type Mat } from "../../app/features/scanner/core/cv";
import * as death from "../../app/features/scanner/core/detectors/death/rois";
import * as mapStart from "../../app/features/scanner/core/detectors/map-start/rois";
import * as minimap from "../../app/features/scanner/core/detectors/minimap/rois";
import * as sb from "../../app/features/scanner/core/detectors/scoreboard/rois";
import * as replay from "../../app/features/scanner/core/detectors/scoreboard-replay/rois";
import {
matToFrameData,
normalizeFrame,
type Roi,
toMat,
} from "../../app/features/cv/core/image";
import { readImage, writePng } from "../../app/features/cv/node/image-io";
} from "../../app/features/scanner/core/image";
import { readImage, writePng } from "../../app/features/scanner/node/image-io";
const [imagePath, outPath = "roi-overlay.png", detector = "scoreboard"] =
process.argv.slice(2);
if (!imagePath) {
console.error(
"usage: vite-node -c scripts/cv/vite-node.config.ts scripts/cv/overlay-rois.ts <image> [out.png] [scoreboard|scoreboard-replay|death|map-start|minimap]",
"usage: vite-node -c scripts/scanner/vite-node.config.ts scripts/scanner/overlay-rois.ts <image> [out.png] [scoreboard|scoreboard-replay|death|map-start|minimap]",
);
process.exit(1);
}

View File

@@ -5,36 +5,36 @@
* per-field accuracy plus character error rate (CER) for names, the metric
* that drives glyph atlas expansion.
*
* Usage: pnpm cv:report
* Usage: pnpm scanner:report
*/
import { loadOpenCV } from "../../app/features/cv/core/cv";
import { loadOpenCV } from "../../app/features/scanner/core/cv";
import {
createDeathDetector,
type DeathData,
} from "../../app/features/cv/core/detectors/death/index";
} from "../../app/features/scanner/core/detectors/death/index";
import {
createMapStartDetector,
type MapStartData,
} from "../../app/features/cv/core/detectors/map-start/index";
} from "../../app/features/scanner/core/detectors/map-start/index";
import {
createMinimapDetector,
type MinimapData,
} from "../../app/features/cv/core/detectors/minimap/index";
import { createScoreboardDetector } from "../../app/features/cv/core/detectors/scoreboard/index";
} from "../../app/features/scanner/core/detectors/minimap/index";
import { createScoreboardDetector } from "../../app/features/scanner/core/detectors/scoreboard/index";
import {
createScoreboardOwnDetector,
type ScoreboardOwnData,
} from "../../app/features/cv/core/detectors/scoreboard-own/index";
} from "../../app/features/scanner/core/detectors/scoreboard-own/index";
import {
createScoreboardReplayDetector,
type ScoreboardReplayData,
} from "../../app/features/cv/core/detectors/scoreboard-replay/index";
import type { Detector } from "../../app/features/cv/core/detectors/types";
} from "../../app/features/scanner/core/detectors/scoreboard-replay/index";
import type { Detector } from "../../app/features/scanner/core/detectors/types";
import {
loadFixtures,
runDetectorOnFixture,
} from "../../app/features/cv/node/fixtures";
import { loadScoreboardResources } from "../../app/features/cv/node/resources";
} from "../../app/features/scanner/node/fixtures";
import { loadScoreboardResources } from "../../app/features/scanner/node/resources";
await loadOpenCV();
const resources = await loadScoreboardResources();

View File

@@ -3,18 +3,18 @@
* CLI harness: run the ScoreboardDetector over fixtures and print everything
* it saw gate result, parsed fields, per-field scores, top weapon candidates.
*
* Usage: pnpm cv:fixtures [case-name-substring]
* Usage: pnpm scanner:fixtures [case-name-substring]
*/
import { loadOpenCV } from "../../app/features/cv/core/cv";
import { loadOpenCV } from "../../app/features/scanner/core/cv";
import {
createScoreboardDetector,
type ScoreboardRowDebug,
} from "../../app/features/cv/core/detectors/scoreboard/index";
} from "../../app/features/scanner/core/detectors/scoreboard/index";
import {
loadFixtures,
runDetectorOnFixture,
} from "../../app/features/cv/node/fixtures";
import { loadScoreboardResources } from "../../app/features/cv/node/resources";
} from "../../app/features/scanner/node/fixtures";
import { loadScoreboardResources } from "../../app/features/scanner/node/resources";
const filter = process.argv[2];

View File

@@ -1,5 +1,5 @@
/**
* Config for running the CV scripts with vite-node. Deliberately minimal:
* Config for running the scanner scripts with vite-node. Deliberately minimal:
* the root vite.config.ts pre-bundles @techstark/opencv-js for the browser
* worker, and vite-node would resolve that browser prebundle (which
* crashes on __dirname in Node). Without the include, vite-node