mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-12 22:26:12 -05:00
Scanner: handle more "struck" special/dead parsing
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -39,4 +39,5 @@ notepad.txt
|
||||
/assets/fonts/
|
||||
|
||||
*.mp4
|
||||
*.mkv
|
||||
*.mkv
|
||||
*.webm
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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 |
Reference in New Issue
Block a user