Scanner: handle more "struck" special/dead parsing

This commit is contained in:
Kalle
2026-09-09 08:37:52 +03:00
parent 834128351a
commit 9fc1dde2ee
9 changed files with 111 additions and 28 deletions

3
.gitignore vendored
View File

@@ -39,4 +39,5 @@ notepad.txt
/assets/fonts/
*.mp4
*.mkv
*.mkv
*.webm

View File

@@ -101,7 +101,9 @@ sequenceDiagram
badge-less frame scores the geometries on how decisively the bodies
read and sticks with the established layout unless another wins
clearly — the special-ready wash also pulses, so its dim trough is told
apart from a splat by its pale body, and a narrow-layout ready read
apart from a splat by its team tint: a splat is a neutral grey plate under
a grey X, which a blown-out backdrop turns near-white while a wash stays
tinted at every pulse phase, and a narrow-layout ready read
must also see a washed (ink-poor) body: pale backdrop or the lead
banner leaking past an icon edge fakes the shoulder glow on the
overhead map view's badge-less strip), with

View File

@@ -1,10 +1,11 @@
/**
* PlayerStatus: per-player state off the eight squid/octo icons flanking the
* timer, emitted alongside each Objective read (same frame, same `time`).
* Three pixel-class fractions decide a slot (calibration in rois.ts): alive =
* saturated team-ink body; special held = bright pale wash that PULSES (bright
* frames light the shoulder probe, trough frames only read pale); splatted =
* unsaturated grey X with none of the three. Three geometries, named by which
* Pixel-class fractions decide a slot (calibration in rois.ts): alive =
* saturated team-ink body; special held = pale team-tinted wash that PULSES
* (bright frames light the shoulder probe, trough frames only read tinted);
* splatted = neutral grey plate under a grey X, ink-poor and untinted whatever
* the backdrop brightness. Three geometries, named by which
* side sits at the packed pitch: "even", "narrow-right" (usual spectator HUD;
* S3 POV draws it too) and "narrow-left" (right column nearly coincides with
* even's). Camera badges prove a broadcast, but broadcasts can hide them, so a
@@ -27,7 +28,6 @@ import {
STATUS_COMB_MAX_SHIFT,
STATUS_COMB_SIDE_SPANS,
STATUS_DEAD_MAX_BODY_INK,
STATUS_DEAD_MAX_BODY_PALE,
STATUS_DEAD_MAX_SHOULDER_GLOW,
STATUS_DPAD_PROBES_EVEN,
STATUS_DPAD_PROBES_NARROW_LEFT,
@@ -58,6 +58,9 @@ import {
STATUS_SLOT_CENTERS_NARROW_LEFT,
STATUS_SLOT_CENTERS_NARROW_RIGHT,
STATUS_STICKY_FLIP_COMB_MIN,
STATUS_TINT_MIN_SPREAD,
STATUS_TINT_MIN_VALUE,
STATUS_WASH_MIN_BODY_TINT,
STATUS_WHITE_MAX_SPREAD,
STATUS_WHITE_MIN_VALUE,
} from "./rois";
@@ -109,6 +112,7 @@ interface SlotRead {
confidence: number;
bodyInk: number;
bodyPale: number;
bodyTint: number;
shoulderGlow: number;
shoulderPaleGlow: number;
}
@@ -157,6 +161,7 @@ export function parsePlayerStatus(
: "badges",
bodyInk: reads.map((read) => Number(read.bodyInk.toFixed(2))),
bodyPale: reads.map((read) => Number(read.bodyPale.toFixed(2))),
bodyTint: reads.map((read) => Number(read.bodyTint.toFixed(2))),
shoulderGlow: reads.map((read) => Number(read.shoulderGlow.toFixed(2))),
shoulderPaleGlow: reads.map((read) =>
Number(read.shoulderPaleGlow.toFixed(2)),
@@ -197,6 +202,7 @@ function readSlots(
return classifySlot(
body.ink,
body.pale,
body.tint,
shoulder.glow,
shoulder.paleGlow,
layout,
@@ -347,35 +353,43 @@ function sideDecisiveness(reads: SlotRead[]): number {
/**
* State from the class fractions; confidence scales with distance to the
* nearest boundary (1 at twice the threshold / at zero). On narrow layouts the
* wash replaces the body's ink, so an ink-heavy body means backdrop leak unless
* strongly pale too (graded STATUS_READY_*WASH* guards), and only unsaturated
* glow counts (STATUS_GLOW_MAX_SPREAD). A pale backdrop can still light a DEAD
* icon's shoulder, so narrow ready reads also need the wash's pale body and the
* nearest boundary (1 at twice the threshold / at zero). An ink-poor body is a
* splat or a wash, and its tint tells them apart: the wash is a pale team tint
* at every pulse phase while the splat is neutral grey — even when a blown-out
* backdrop turns the plate near-white, or the trough dims the wash under both
* ready floors. On narrow layouts the wash replaces the body's ink, so an
* ink-heavy body means backdrop leak unless strongly pale too (graded
* STATUS_READY_*WASH* guards), and only unsaturated glow counts
* (STATUS_GLOW_MAX_SPREAD). A pale backdrop can still light a DEAD icon's
* shoulder, so narrow ready reads also need the wash's pale body and the
* narrow dead read trusts the body classes alone.
*/
function classifySlot(
bodyInk: number,
bodyPale: number,
bodyTint: number,
shoulderGlow: number,
shoulderPaleGlow: number,
layout: PlayerStatusLayout,
): SlotRead {
const washGlow = layout === "even" ? shoulderGlow : shoulderPaleGlow;
const inkPoor = bodyInk <= STATUS_DEAD_MAX_BODY_INK;
const tinted = bodyTint >= STATUS_WASH_MIN_BODY_TINT;
const dead =
bodyInk <= STATUS_DEAD_MAX_BODY_INK &&
(layout !== "even" || washGlow <= STATUS_DEAD_MAX_SHOULDER_GLOW) &&
bodyPale <= STATUS_DEAD_MAX_BODY_PALE;
inkPoor &&
!tinted &&
(layout !== "even" || washGlow <= STATUS_DEAD_MAX_SHOULDER_GLOW);
const washedBody =
bodyInk <= STATUS_READY_CLEAN_WASH_MAX_BODY_INK ||
(bodyInk <= STATUS_READY_WASH_MAX_BODY_INK &&
bodyPale >= STATUS_READY_INKY_WASH_MIN_BODY_PALE);
const special =
!dead &&
(washGlow >= STATUS_READY_MIN_SHOULDER_GLOW ||
bodyPale >= STATUS_READY_MIN_BODY_PALE) &&
(layout === "even" ||
(washedBody && bodyPale >= STATUS_READY_MIN_WASH_BODY_PALE));
((inkPoor && tinted) ||
((washGlow >= STATUS_READY_MIN_SHOULDER_GLOW ||
bodyPale >= STATUS_READY_MIN_BODY_PALE) &&
(layout === "even" ||
(washedBody && bodyPale >= STATUS_READY_MIN_WASH_BODY_PALE))));
const confidence = dead
? Math.min(
1,
@@ -387,6 +401,7 @@ function classifySlot(
Math.max(
washGlow / (STATUS_READY_MIN_SHOULDER_GLOW * 2),
bodyPale / (STATUS_READY_MIN_BODY_PALE * 2),
inkPoor ? bodyTint / (STATUS_WASH_MIN_BODY_TINT * 2) : 0,
),
)
: Math.min(1, bodyInk / (STATUS_DEAD_MAX_BODY_INK * 2));
@@ -396,16 +411,17 @@ function classifySlot(
confidence,
bodyInk,
bodyPale,
bodyTint,
shoulderGlow,
shoulderPaleGlow,
};
}
/** Ink, glow, and pale pixel fractions of a ROI (see rois.ts for the classes). */
/** Ink, glow, pale, and tint pixel fractions of a ROI (see rois.ts for the classes). */
function classFractions(
frame: Mat,
roi: Roi,
): { ink: number; glow: number; paleGlow: number; pale: number } {
): { ink: number; glow: number; paleGlow: number; pale: number; tint: number } {
const crop = copyRoi(frame, roi);
const { data } = crop;
const channels = crop.channels();
@@ -413,6 +429,7 @@ function classFractions(
let glow = 0;
let paleGlow = 0;
let pale = 0;
let tint = 0;
let count = 0;
for (let i = 0; i < data.length; i += channels) {
const r = data[i]!;
@@ -427,6 +444,12 @@ function classFractions(
}
if (value >= STATUS_PALE_MIN_VALUE && spread <= STATUS_PALE_MAX_SPREAD)
pale++;
if (
value >= STATUS_TINT_MIN_VALUE &&
spread > STATUS_TINT_MIN_SPREAD &&
spread < STATUS_INK_MIN_SPREAD
)
tint++;
count++;
}
crop.delete();
@@ -435,6 +458,7 @@ function classFractions(
glow: glow / count,
paleGlow: paleGlow / count,
pale: pale / count,
tint: tint / count,
};
}

View File

@@ -189,15 +189,33 @@ export const STATUS_PALE_MIN_VALUE = 185;
export const STATUS_PALE_MAX_SPREAD = 70;
/**
* Splatted: body ink under the floor (dead <=0.20 vs alive >=0.26) and the
* body-pale guard keeping the wash out across its pulse (wash >=0.22 vs dead
* <=0.15 under the 2026-08-22 VoD's skylight). Even reads add the shoulder-glow
* Tinted pixel: bright-ish and unsaturated, but not neutral. The special-ready
* wash is a pale team tint (lavender, pink, ...) at every pulse phase, while a
* splatted icon is a neutral grey plate under a grey X — and under a blown-out
* backdrop that plate reads near-white, so brightness alone cannot tell them
* apart. Spread floor sits above chroma-subsampling noise on grey (splats
* <=0.19 tinted) and the value floor above the dimmed plate.
*/
export const STATUS_TINT_MIN_VALUE = 100;
export const STATUS_TINT_MIN_SPREAD = 12;
/**
* Splatted: body ink under the floor (dead <=0.20 vs alive >=0.26) and a
* neutral body (STATUS_WASH_MIN_BODY_TINT). Even reads add the shoulder-glow
* guard (ready >=0.40 vs dead <=0.03); on narrow layouts backdrop leak past a
* shrunken X reads 0.26-0.35 there, so only the body classes decide.
*/
export const STATUS_DEAD_MAX_BODY_INK = 0.23;
export const STATUS_DEAD_MAX_SHOULDER_GLOW = 0.2;
export const STATUS_DEAD_MAX_BODY_PALE = 0.15;
/**
* Wash body: tinted past this on an ink-poor body means the special-ready
* wash at any pulse phase (bright frames and the dim trough alike), under it
* a splat. Splats read <=0.19 (SWS26 splat on a blown-out white sky: 0.07 at
* pale 0.80), ink-poor washes >=0.45 (SWS26 even-layout trough: 0.57 at pale
* 0.13, shoulder glow 0.20 — under both ready floors).
*/
export const STATUS_WASH_MIN_BODY_TINT = 0.3;
/** Special ready: shoulder glow past this (attested >=0.40 vs <=0.06). */
export const STATUS_READY_MIN_SHOULDER_GLOW = 0.25;

View File

@@ -5,14 +5,14 @@
"time": 152,
"special": [
[true, false, false, false],
[false, false, true, false]
[false, false, false, false]
],
"dead": [
[false, false, true, true],
[true, false, false, false]
[true, false, true, false]
]
},
"options": {
"notes": "Actually dead[1][2] should be true but it is not in proper view so accepted."
"notes": "dead[1][2] sits on the overhead map's pale backdrop, which turns its grey plate near-white; it used to be accepted as a ready misread until the body-tint split told the untinted plate from a wash."
}
}

View File

@@ -0,0 +1,19 @@
{
"event": "PlayerStatus",
"data": {
"layout": "even",
"time": 161,
"cast": true,
"special": [
[false, false, false, false],
[true, false, false, false]
],
"dead": [
[false, false, false, false],
[false, false, false, true]
]
},
"options": {
"notes": "Casted stream (SWS26 REWOUND, Rush vs Let's Go Brother, Sturgeon game 1) at 2:41, yellow team left vs purple right, both strips at the even pitch with the camera badges drawn. All four yellow icons are alive (gauge 87/45/60/7). Purple's slot nearest the timer wears the special-ready wash (pale lavender body, number hidden) and its outer slot is crossed out (gauge 37 kept); the read flagged the ready slot as splatted and missed the special entirely — the washed body's grey charger silhouette on the pale plate scored like a crossed-out icon. Purple's third slot shows no gauge number while its special is active (normal purple body, not ready)."
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@@ -0,0 +1,19 @@
{
"event": "PlayerStatus",
"data": {
"layout": "narrow-right",
"time": 188,
"cast": true,
"special": [
[false, true, false, false],
[false, false, false, false]
],
"dead": [
[true, false, false, false],
[false, false, false, true]
]
},
"options": {
"notes": "Casted stream (SWS26 REWOUND, Rush vs Let's Go Brother, Hagglefish game 2) at 3:08, yellow team left vs purple right with the A/B/Y/X and D-pad camera badges drawn. Yellow's outer slot is crossed out (gauge 40 kept on screen) and its second slot wears the special-ready wash with the number hidden; both read fine. Purple's outer slot is also crossed out (gauge 41 kept) but sits on a blown-out white sky, and the read came back alive with special ready instead of splatted — the bright backdrop leaking around the grey X'd body fakes the ready wash. Gauge numbers 4/37 on yellow's inner slots, 29/57/15 on purple's alive slots."
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB