mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-24 03:55:49 -05:00
Objective fixes
This commit is contained in:
@@ -806,10 +806,7 @@ app/styles/vars.css (the emberz copies of those tokens were dropped).
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* frosted-glass plate: keeps the score readable over vivid banner art
|
||||
without dimming the image itself */
|
||||
.scanner-app .match-score {
|
||||
font-size: var(--font-lg);
|
||||
font-weight: var(--weight-extra);
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1;
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
/**
|
||||
* ObjectiveDetector: parses the ranked in-match counter overlay top-center —
|
||||
* each team's count plate, the penalty pill under it, which team is in
|
||||
* control (the controlling team's plate swaps to a near-black fill with the
|
||||
* digits in the team's ink color; see rois.ts for the layout), and the M:SS
|
||||
* match timer above the plates.
|
||||
* control (the controlling team's plate keeps its saturated team-color
|
||||
* fill; a non-controlling team's — or both, while the objective is neutral
|
||||
* — swaps to a near-black fill with the digits in the team's ink color;
|
||||
* see rois.ts for the layout), and the M:SS match timer above the plates.
|
||||
*
|
||||
* Digits are read as the trailing digit run (banner.ts) of the band under
|
||||
* several channel extractions: team-color ink on the black plate needs the
|
||||
* brightest channel (dark blue ink is near-black in luminance), white ink
|
||||
* on a team-color fill needs the darkest channel (which drops the fill and
|
||||
* keeps white) — every extraction is tried at each threshold/size and the
|
||||
* best-scoring read wins. A gate hit that yields no readable count on
|
||||
* either side emits nothing (lookalike frame).
|
||||
* brightest channel (dark blue ink is near-black in luminance), white or
|
||||
* near-black ink on a team-color fill needs the darkest channel (which
|
||||
* drops the fill and keeps the contrast) — every extraction is tried at
|
||||
* each threshold/size and the best-scoring read wins. A gate hit that
|
||||
* yields no readable count on either side emits nothing (lookalike frame).
|
||||
*
|
||||
* The data is a discriminated union on `mode`, prepared for the counter
|
||||
* semantics of the other modes; only the SZ member exists so far and every
|
||||
@@ -41,9 +42,7 @@ import {
|
||||
import type { ScoreboardResources } from "../scoreboard/index";
|
||||
import type { DetectedEvent, Detector, GateResult } from "../types";
|
||||
import {
|
||||
CONTROL_PLATE_MAX_MEAN,
|
||||
CONTROL_PLATE_MAX_SATURATION,
|
||||
GATE_PLATE_MAX_MEAN,
|
||||
CONTROL_PLATE_MIN_SATURATION,
|
||||
GATE_PLATE_MAX_STD,
|
||||
GATE_SCORE_MIN_MAX_BRIGHTNESS,
|
||||
GATE_TIMER_MAX_MEAN,
|
||||
@@ -80,7 +79,7 @@ export interface SplatZonesObjectiveData {
|
||||
score: [number | null, number | null];
|
||||
/** penalty pill value per team; null = no pill (or unreadable) */
|
||||
penalty: [number | null, number | null];
|
||||
/** which team currently holds the zone (team-ink digits on black plate) */
|
||||
/** which team currently holds the zone (team-color plate fill) */
|
||||
control: [boolean, boolean];
|
||||
}
|
||||
|
||||
@@ -156,8 +155,7 @@ export function createObjectiveDetector(
|
||||
}
|
||||
|
||||
function plateProbeOk(gray: Mat, roi: Roi): boolean {
|
||||
const { mean, std } = meanStd(gray, roi);
|
||||
return mean <= GATE_PLATE_MAX_MEAN && std <= GATE_PLATE_MAX_STD;
|
||||
return meanStd(gray, roi).std <= GATE_PLATE_MAX_STD;
|
||||
}
|
||||
|
||||
function scoreInkOk(frame: Mat, roi: Roi): boolean {
|
||||
@@ -277,8 +275,8 @@ export function createObjectiveDetector(
|
||||
|
||||
/**
|
||||
* Control: the plate's fill (sampled over the probe strip) is the
|
||||
* neutral near-black style — dark AND unsaturated — instead of the
|
||||
* non-controlling team-color fill (see CONTROL_PLATE_MAX_* in rois.ts).
|
||||
* saturated team-color style instead of the near-black fill shown while
|
||||
* not in control (see CONTROL_PLATE_MIN_SATURATION in rois.ts).
|
||||
*/
|
||||
function plateFill(
|
||||
frame: Mat,
|
||||
@@ -315,8 +313,7 @@ export function createObjectiveDetector(
|
||||
penalty,
|
||||
control:
|
||||
score.value !== null &&
|
||||
fill.mean <= CONTROL_PLATE_MAX_MEAN &&
|
||||
fill.saturation <= CONTROL_PLATE_MAX_SATURATION,
|
||||
fill.saturation >= CONTROL_PLATE_MIN_SATURATION,
|
||||
fill,
|
||||
};
|
||||
}) as [SideRead, SideRead];
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
/**
|
||||
* ALL objective-counter ROI coordinates, in canonical 1920x1080 space.
|
||||
* Calibrated against the objective/ fixtures (pixel maps over the top-center
|
||||
* band; the layout is mirror-symmetric around x=960, so right-side ROIs are
|
||||
* derived with mirrorRoi and stay locked to the left-side measurements).
|
||||
* band). The layout is only roughly mirror-symmetric around x=960: the
|
||||
* widest attested digit run ("100") measures x816..888 on the left plate
|
||||
* but x1039..1112 on the right — a few px right of mirror-center — so the
|
||||
* score and plate-probe ROIs are measured per side instead of mirrored.
|
||||
*
|
||||
* The ranked in-match HUD shows one counter plate per team flanking the
|
||||
* mode's objective badge, under the timer strip:
|
||||
* - each plate is a rounded box with a small localized label line
|
||||
* ("Remaining", のこり, ...) over big BlitzBold count digits (~42px);
|
||||
* - the team currently in control has its plate restyled: near-black fill
|
||||
* with the digits in the team's ink color; the other plate keeps a
|
||||
* team-color fill with white digits. The fill/ink swap — not the digit
|
||||
* values — is what identifies control;
|
||||
* ("Remaining", のこり, ...) over big BlitzBold count digits (~41px);
|
||||
* - the team currently in control has its plate in its team-color fill
|
||||
* (digits near-black on bright inks, white on dark inks); a team not in
|
||||
* control — including both teams while the objective is neutral — has a
|
||||
* near-black plate with the digits in its team ink color. The fill swap —
|
||||
* not the digit values — is what identifies control;
|
||||
* - a penalty adds a translucent dark pill under the plate with a white
|
||||
* "+N" (~26px digits; the '+' is shorter than any digit and is dropped by
|
||||
* "+N" (~29px digits; the '+' is shorter than any digit and is dropped by
|
||||
* the trailing-digit height floor).
|
||||
*
|
||||
* Only one fixture is attested so far (SZ, two-digit counts on both sides).
|
||||
* Assumed until more fixtures land: the plates keep their geometry with
|
||||
* three-digit counts ("100"), and the penalty pill sits mirror-symmetric
|
||||
* under the right plate.
|
||||
*/
|
||||
import { CANONICAL_WIDTH, type Roi } from "../../canonical";
|
||||
|
||||
@@ -28,11 +26,16 @@ function mirrorRoi(roi: Roi): Roi {
|
||||
return { ...roi, x: CANONICAL_WIDTH - roi.x - roi.w };
|
||||
}
|
||||
|
||||
/** Count digit band, inside the plate below the label line. */
|
||||
export const SCORE_ROIS: readonly [Roi, Roi] = (() => {
|
||||
const left: Roi = { x: 812, y: 156, w: 84, h: 50 };
|
||||
return [left, mirrorRoi(left)];
|
||||
})();
|
||||
/**
|
||||
* Count digit band, inside the plate below the label line. Wide enough for
|
||||
* the full three-digit "100" run on each side (measured x816..888 left,
|
||||
* x1039..1112 right) while staying clear of the plate rims, whose edge glow
|
||||
* against a bright scene can read as ink.
|
||||
*/
|
||||
export const SCORE_ROIS: readonly [Roi, Roi] = [
|
||||
{ x: 810, y: 156, w: 90, h: 46 },
|
||||
{ x: 1034, y: 156, w: 84, h: 46 },
|
||||
];
|
||||
|
||||
/**
|
||||
* Penalty "+N" band inside the pill. The pill measures x 826..920,
|
||||
@@ -44,17 +47,20 @@ export const PENALTY_ROIS: readonly [Roi, Roi] = (() => {
|
||||
})();
|
||||
|
||||
/**
|
||||
* Plate-fill probe strips at the plates' outer edges (x 796..903 is the
|
||||
* left plate; the strip sits inside its left rim, clear of the widest
|
||||
* attested digit run). Both plate styles are a flat fill there — near-black
|
||||
* when in control, saturated team color when not — while gameplay showing
|
||||
* through a lookalike frame is busy: the gate accepts a strip on mean
|
||||
* brightness plus flatness (std).
|
||||
* Plate-fill probe strips inside each plate's LEFT rim (x 796..903 is the
|
||||
* left plate, x 1017..1124 the right), clear of the widest attested digit
|
||||
* run ("100", see SCORE_ROIS) — the right strip is not mirrored because the
|
||||
* right plate's digit run reaches x1112, into where a mirrored strip would
|
||||
* sit. Height stops at y192 to stay above the bottom-corner edge glow.
|
||||
* Both plate styles are a flat fill there — saturated team color when in
|
||||
* control, near-black when not — while gameplay showing through a
|
||||
* lookalike frame is busy: the gate accepts a strip on flatness (std)
|
||||
* alone, since fill brightness spans near-black to bright yellow.
|
||||
*/
|
||||
export const PLATE_PROBE_ROIS: readonly [Roi, Roi] = (() => {
|
||||
const left: Roi = { x: 800, y: 162, w: 10, h: 36 };
|
||||
return [left, mirrorRoi(left)];
|
||||
})();
|
||||
export const PLATE_PROBE_ROIS: readonly [Roi, Roi] = [
|
||||
{ x: 800, y: 162, w: 10, h: 30 },
|
||||
{ x: 1021, y: 162, w: 10, h: 30 },
|
||||
];
|
||||
|
||||
/**
|
||||
* Penalty-pill presence probes at the pill's rounded ends, clear of the
|
||||
@@ -69,7 +75,6 @@ export const PENALTY_PROBE_ROIS: readonly [[Roi, Roi], [Roi, Roi]] = (() => {
|
||||
return [leftPill, [mirrorRoi(leftPill[1]), mirrorRoi(leftPill[0])]];
|
||||
})();
|
||||
|
||||
export const GATE_PLATE_MAX_MEAN = 165;
|
||||
export const GATE_PLATE_MAX_STD = 30;
|
||||
/** A count digit's brightest channel clears this on either plate style. */
|
||||
export const GATE_SCORE_MIN_MAX_BRIGHTNESS = 200;
|
||||
@@ -110,7 +115,7 @@ export const TIMER_DIGIT_MIN_HEIGHT_RATIO = 0.82;
|
||||
* banner's settled/mid-pop sizes).
|
||||
*/
|
||||
export const SCORE_TEXT_HEIGHTS = [40, 44] as const;
|
||||
export const PENALTY_TEXT_HEIGHT = 26;
|
||||
export const PENALTY_TEXT_HEIGHT = 29;
|
||||
|
||||
/**
|
||||
* Binarization thresholds tried per channel extraction. The ink/background
|
||||
@@ -126,11 +131,11 @@ export const PENALTY_PROBE_MAX_MEAN = 165;
|
||||
export const PENALTY_PROBE_MAX_STD = 30;
|
||||
|
||||
/**
|
||||
* Control = the plate's fill is the near-black style (measured over the
|
||||
* plate probe strip): a controlling team's plate is neutral dark — low
|
||||
* brightness AND low saturation — while the team-color fill of a
|
||||
* non-controlling plate stays saturated even for dark inks (deep blue
|
||||
* measures ~130 spread at ~50 brightness).
|
||||
* Control = the plate's fill is the team-color style (measured over the
|
||||
* plate probe strip): a controlling team's plate keeps its saturated ink
|
||||
* fill even for dark inks (deep blue measures ~130 spread at ~50
|
||||
* brightness), while the near-black plate of a non-controlling team — or
|
||||
* of both teams while the objective is neutral — is unsaturated (attested
|
||||
* fills measure >=112 vs <=19).
|
||||
*/
|
||||
export const CONTROL_PLATE_MAX_MEAN = 90;
|
||||
export const CONTROL_PLATE_MAX_SATURATION = 60;
|
||||
export const CONTROL_PLATE_MIN_SATURATION = 60;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"event": "Objective",
|
||||
"data": {
|
||||
"mode": "SZ",
|
||||
"time": 277,
|
||||
"score": [97, 100],
|
||||
"penalty": [null, null],
|
||||
"control": [true, false]
|
||||
},
|
||||
"options": {
|
||||
"notes": "Alpha (bright yellow-green ink) in control: their plate keeps the bright team-color fill (POV frame shows the We're in control! banner). Bravo still at the three-digit 100."
|
||||
}
|
||||
}
|
||||
BIN
app/features/scanner/tests/fixtures/objective/splat-zones-barnacle-dime-bright-plate/frame.png
vendored
Normal file
BIN
app/features/scanner/tests/fixtures/objective/splat-zones-barnacle-dime-bright-plate/frame.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1002 KiB |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"event": "Objective",
|
||||
"data": {
|
||||
"mode": "SZ",
|
||||
"time": 157,
|
||||
"score": [2, 15],
|
||||
"penalty": [64, 65],
|
||||
"control": [true, false]
|
||||
},
|
||||
"options": {
|
||||
"notes": "Penalty pills under both plates at once; alpha (bright green fill) in control."
|
||||
}
|
||||
}
|
||||
BIN
app/features/scanner/tests/fixtures/objective/splat-zones-barnacle-dime-double-penalty/frame.png
vendored
Normal file
BIN
app/features/scanner/tests/fixtures/objective/splat-zones-barnacle-dime-double-penalty/frame.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 950 KiB |
13
app/features/scanner/tests/fixtures/objective/splat-zones-barnacle-dime-penalty/expected.json
vendored
Normal file
13
app/features/scanner/tests/fixtures/objective/splat-zones-barnacle-dime-penalty/expected.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"event": "Objective",
|
||||
"data": {
|
||||
"mode": "SZ",
|
||||
"time": 209,
|
||||
"score": [2, 84],
|
||||
"penalty": [75, null],
|
||||
"control": [false, true]
|
||||
},
|
||||
"options": {
|
||||
"notes": "Single-digit count with a large penalty on alpha; bravo (purple) in control with the team-color fill."
|
||||
}
|
||||
}
|
||||
BIN
app/features/scanner/tests/fixtures/objective/splat-zones-barnacle-dime-penalty/frame.png
vendored
Normal file
BIN
app/features/scanner/tests/fixtures/objective/splat-zones-barnacle-dime-penalty/frame.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
13
app/features/scanner/tests/fixtures/objective/splat-zones-barnacle-dime-start/expected.json
vendored
Normal file
13
app/features/scanner/tests/fixtures/objective/splat-zones-barnacle-dime-start/expected.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"event": "Objective",
|
||||
"data": {
|
||||
"mode": "SZ",
|
||||
"time": 299,
|
||||
"score": [100, 100],
|
||||
"penalty": [null, null],
|
||||
"control": [false, false]
|
||||
},
|
||||
"options": {
|
||||
"notes": "Match start: zone neutral, both plates near-black with three-digit 100s (green/pink digits)."
|
||||
}
|
||||
}
|
||||
BIN
app/features/scanner/tests/fixtures/objective/splat-zones-barnacle-dime-start/frame.png
vendored
Normal file
BIN
app/features/scanner/tests/fixtures/objective/splat-zones-barnacle-dime-start/frame.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@@ -5,6 +5,6 @@
|
||||
"time": 215,
|
||||
"score": [95, 53],
|
||||
"penalty": [4, null],
|
||||
"control": [true, false]
|
||||
"control": [false, true]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user