@camstack/addon-post-analysis 1.2.139 → 1.2.141
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pipeline-analytics/index.js +179 -12
- package/dist/pipeline-analytics/index.mjs +179 -12
- package/package.json +1 -1
|
@@ -30502,6 +30502,8 @@ var DEFAULT_TRACKER_CONFIG = {
|
|
|
30502
30502
|
rescueIouThreshold: .1,
|
|
30503
30503
|
rescueCentroidFactor: .75,
|
|
30504
30504
|
resurrectionWindowMs: 8e3,
|
|
30505
|
+
resurrectionCrowdGuard: true,
|
|
30506
|
+
resurrectionCrowdedIouThreshold: .3,
|
|
30505
30507
|
stationarySpeedPx: 2,
|
|
30506
30508
|
dedupEnabled: true,
|
|
30507
30509
|
dedupSpawnIou: .6,
|
|
@@ -30751,6 +30753,35 @@ var SortTracker = class SortTracker {
|
|
|
30751
30753
|
};
|
|
30752
30754
|
}
|
|
30753
30755
|
/**
|
|
30756
|
+
* How many same-association-group subjects OTHER than `corpse` share the
|
|
30757
|
+
* scene with it: every track LIVE on this frame, plus any other resurrectable
|
|
30758
|
+
* corpse whose lifetime OVERLAPPED the candidate's.
|
|
30759
|
+
*
|
|
30760
|
+
* The overlap clause on the graveyard half is what keeps this guard off the
|
|
30761
|
+
* case the resurrection pass exists for. A single flickering subject leaves a
|
|
30762
|
+
* TRAIL of corpses behind it — that trail is the fragmentation the pass
|
|
30763
|
+
* repairs — but those fragments never coexist: each is born only after the
|
|
30764
|
+
* previous one died. Two distinct people do coexist. Counting corpses without
|
|
30765
|
+
* the overlap test would let a subject's own debris read as a crowd and
|
|
30766
|
+
* re-fragment the very case being protected.
|
|
30767
|
+
*
|
|
30768
|
+
* A live track is counted unconditionally, overlap or not: the subject that
|
|
30769
|
+
* steals an id is very often one whose OWN track was born after the corpse
|
|
30770
|
+
* died (camera 618 — the toddler's track post-dates the older boy's).
|
|
30771
|
+
*/
|
|
30772
|
+
sameGroupPeerCount(corpse, live) {
|
|
30773
|
+
const group = this.assocGroup(corpse.class);
|
|
30774
|
+
let peers = 0;
|
|
30775
|
+
for (const t of live) if (t !== corpse && this.assocGroup(t.class) === group) peers++;
|
|
30776
|
+
for (const other of this.lostTracks) {
|
|
30777
|
+
if (other === corpse) continue;
|
|
30778
|
+
if (!other.resurrectable) continue;
|
|
30779
|
+
if (this.assocGroup(other.class) !== group) continue;
|
|
30780
|
+
if (other.firstSeen <= corpse.lastSeen && corpse.firstSeen <= other.lastSeen) peers++;
|
|
30781
|
+
}
|
|
30782
|
+
return peers;
|
|
30783
|
+
}
|
|
30784
|
+
/**
|
|
30754
30785
|
* Loose same-group gate used by the rescue and resurrection passes: accept
|
|
30755
30786
|
* when the detection overlaps the track's LAST-KNOWN bbox by `rescueIou`, or
|
|
30756
30787
|
* its centroid is within `rescueCentroidFactor × bbox-diagonal`. Always group-
|
|
@@ -30865,6 +30896,7 @@ var SortTracker = class SortTracker {
|
|
|
30865
30896
|
}
|
|
30866
30897
|
update(detections, timestamp, frameContext) {
|
|
30867
30898
|
const frameArea = frameContext ? frameContext.frameWidth * frameContext.frameHeight : 0;
|
|
30899
|
+
const resurrectedScene = /* @__PURE__ */ new Map();
|
|
30868
30900
|
if (this.config.maxTrackLifetimeMs > 0) {
|
|
30869
30901
|
const alive = [];
|
|
30870
30902
|
for (const track of this.tracks) if (timestamp - track.firstSeen > this.config.maxTrackLifetimeMs) {
|
|
@@ -30975,6 +31007,10 @@ var SortTracker = class SortTracker {
|
|
|
30975
31007
|
let best;
|
|
30976
31008
|
let bestScore = -1;
|
|
30977
31009
|
let bestGate = "iou";
|
|
31010
|
+
let bestPeers = 0;
|
|
31011
|
+
let refused;
|
|
31012
|
+
let refusedGate = "iou";
|
|
31013
|
+
let refusedScore = -1;
|
|
30978
31014
|
for (const lost of this.lostTracks) {
|
|
30979
31015
|
if (!lost.resurrectable) continue;
|
|
30980
31016
|
if (timestamp - lost.lostAt > this.config.resurrectionWindowMs) continue;
|
|
@@ -30982,13 +31018,27 @@ var SortTracker = class SortTracker {
|
|
|
30982
31018
|
if (gate === null) continue;
|
|
30983
31019
|
if (gate === "centroid" && this.isPlausibleBirth(det, frameArea)) continue;
|
|
30984
31020
|
const score = iou$2(lost.bbox, det.bbox);
|
|
31021
|
+
const peers = this.sameGroupPeerCount(lost, surviving);
|
|
31022
|
+
if (this.config.resurrectionCrowdGuard && peers > 0 && score < this.config.resurrectionCrowdedIouThreshold) {
|
|
31023
|
+
if (score > refusedScore) {
|
|
31024
|
+
refused = lost;
|
|
31025
|
+
refusedGate = gate;
|
|
31026
|
+
refusedScore = score;
|
|
31027
|
+
}
|
|
31028
|
+
continue;
|
|
31029
|
+
}
|
|
30985
31030
|
if (score > bestScore) {
|
|
30986
31031
|
best = lost;
|
|
30987
31032
|
bestScore = score;
|
|
30988
31033
|
bestGate = gate;
|
|
31034
|
+
bestPeers = peers;
|
|
30989
31035
|
}
|
|
30990
31036
|
}
|
|
30991
|
-
if (!best)
|
|
31037
|
+
if (!best) {
|
|
31038
|
+
if (refused !== void 0) this.reportBind("resurrection-refused", refusedGate, refused, det, timestamp);
|
|
31039
|
+
continue;
|
|
31040
|
+
}
|
|
31041
|
+
resurrectedScene.set(best, bestPeers > 0 ? "multi-subject" : "sole-subject");
|
|
30992
31042
|
this.reportBind("resurrection", bestGate, best, det, timestamp);
|
|
30993
31043
|
this.lostTracks = this.lostTracks.filter((t) => t !== best);
|
|
30994
31044
|
best.bbox = det.bbox;
|
|
@@ -31056,17 +31106,21 @@ var SortTracker = class SortTracker {
|
|
|
31056
31106
|
}
|
|
31057
31107
|
this.tracks = surviving;
|
|
31058
31108
|
this.resolveDuplicates();
|
|
31059
|
-
return this.tracks.filter((t) => this.isConfirmedForEmit(t)).map((t) =>
|
|
31060
|
-
|
|
31061
|
-
|
|
31062
|
-
|
|
31063
|
-
|
|
31064
|
-
|
|
31065
|
-
|
|
31066
|
-
|
|
31067
|
-
|
|
31068
|
-
|
|
31069
|
-
|
|
31109
|
+
return this.tracks.filter((t) => this.isConfirmedForEmit(t)).map((t) => {
|
|
31110
|
+
const scene = resurrectedScene.get(t);
|
|
31111
|
+
return {
|
|
31112
|
+
class: this.resolveReportedClass(t),
|
|
31113
|
+
originalClass: t.originalClass,
|
|
31114
|
+
score: t.score,
|
|
31115
|
+
bbox: t.bbox,
|
|
31116
|
+
trackId: t.id,
|
|
31117
|
+
trackAge: t.hits,
|
|
31118
|
+
velocity: t.velocity,
|
|
31119
|
+
path: [...t.path],
|
|
31120
|
+
matchedThisFrame: t.lastSeen === timestamp,
|
|
31121
|
+
...scene !== void 0 ? { resurrectionScene: scene } : {}
|
|
31122
|
+
};
|
|
31123
|
+
});
|
|
31070
31124
|
}
|
|
31071
31125
|
/**
|
|
31072
31126
|
* Remove a track from BOTH the live and graveyard sets so it neither emits
|
|
@@ -31780,6 +31834,7 @@ var FrameProcessor = class {
|
|
|
31780
31834
|
zones: zonesByTrack.get(td.trackId) ?? [],
|
|
31781
31835
|
state,
|
|
31782
31836
|
matchedThisFrame: td.matchedThisFrame !== false,
|
|
31837
|
+
...td.resurrectionScene !== void 0 ? { resurrectionScene: td.resurrectionScene } : {},
|
|
31783
31838
|
...labelPatch !== void 0 ? { labelPatch } : {},
|
|
31784
31839
|
...emb !== void 0 ? {
|
|
31785
31840
|
embedding: emb.embedding,
|
|
@@ -38750,8 +38805,77 @@ var FaceRecognizer = class {
|
|
|
38750
38805
|
}
|
|
38751
38806
|
});
|
|
38752
38807
|
}
|
|
38808
|
+
/**
|
|
38809
|
+
* The track id this face is held under was re-attached from the graveyard in a
|
|
38810
|
+
* scene that had other same-class subjects in it, so the id's continuity is no
|
|
38811
|
+
* longer self-evident. Stop vouching for what is held, without throwing it
|
|
38812
|
+
* away (D282).
|
|
38813
|
+
*
|
|
38814
|
+
* Three things happen, and the asymmetry between them is the whole decision:
|
|
38815
|
+
*
|
|
38816
|
+
* • the crop and the embedding are KEPT. Discarding them would punish exactly
|
|
38817
|
+
* the case the resurrection pass exists to serve — one subject flickering in
|
|
38818
|
+
* and out — and a good face is not cheap to come by.
|
|
38819
|
+
* • `recognizedIdentityId` is STRIPPED, along with the peak match/suggestion
|
|
38820
|
+
* scores. An identity is a claim about WHO, and the one fact just
|
|
38821
|
+
* established is that nobody knows who this id is on any more.
|
|
38822
|
+
* • the face becomes non-promotable: {@link onTrackEnd} will not write it to
|
|
38823
|
+
* the gallery while the flag stands.
|
|
38824
|
+
*
|
|
38825
|
+
* The aggregate is deliberately left alone — see the note in `onTrackEnd`.
|
|
38826
|
+
*/
|
|
38827
|
+
quarantineHeldFace(deviceId, trackId) {
|
|
38828
|
+
const held = this.bestFace.get(trackId);
|
|
38829
|
+
if (held === void 0 || held.unverified === true) return;
|
|
38830
|
+
const quarantined = {
|
|
38831
|
+
score: held.score,
|
|
38832
|
+
embedding: held.embedding,
|
|
38833
|
+
embeddingModelId: held.embeddingModelId,
|
|
38834
|
+
bbox: held.bbox,
|
|
38835
|
+
timestamp: held.timestamp,
|
|
38836
|
+
unverified: true,
|
|
38837
|
+
...held.crop !== void 0 ? { crop: held.crop } : {},
|
|
38838
|
+
...held.nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx: held.nativeFaceShortSidePx } : {}
|
|
38839
|
+
};
|
|
38840
|
+
this.bestFace.set(trackId, quarantined);
|
|
38841
|
+
this.bestMatchScore.delete(trackId);
|
|
38842
|
+
this.bestSuggestion.delete(trackId);
|
|
38843
|
+
this.deps.logger.info("face: held face unverified — track id re-attached in a crowded scene", {
|
|
38844
|
+
tags: {
|
|
38845
|
+
deviceId,
|
|
38846
|
+
trackId
|
|
38847
|
+
},
|
|
38848
|
+
meta: {
|
|
38849
|
+
heldScore: held.score,
|
|
38850
|
+
heldAt: held.timestamp,
|
|
38851
|
+
strippedIdentityId: held.recognizedIdentityId ?? null,
|
|
38852
|
+
hasCrop: held.crop !== void 0
|
|
38853
|
+
}
|
|
38854
|
+
});
|
|
38855
|
+
}
|
|
38856
|
+
/**
|
|
38857
|
+
* Ask a FRESH face whether the quarantined held face is still about the same
|
|
38858
|
+
* person, at the same bar the gallery uses to say two vectors are one person
|
|
38859
|
+
* (`similarityThreshold` — one knob over one band, D222).
|
|
38860
|
+
*
|
|
38861
|
+
* Only a face-BEARING frame may answer. An embedding inherited onto the person
|
|
38862
|
+
* track on a face-less frame is the held vector's own descendant, so it would
|
|
38863
|
+
* confirm itself; callers gate on `faceBbox` being present.
|
|
38864
|
+
*/
|
|
38865
|
+
verifyHeldFace(held, probe, threshold) {
|
|
38866
|
+
if (held.embeddingModelId !== probe.embeddingModelId || held.embedding.length !== probe.embedding.length) return {
|
|
38867
|
+
verdict: "incomparable",
|
|
38868
|
+
cosine: 0
|
|
38869
|
+
};
|
|
38870
|
+
const cosine = require_dist.cosineSimilarity(new Float32Array(held.embedding), new Float32Array(probe.embedding));
|
|
38871
|
+
return {
|
|
38872
|
+
verdict: cosine >= threshold ? "confirmed" : "rejected",
|
|
38873
|
+
cosine
|
|
38874
|
+
};
|
|
38875
|
+
}
|
|
38753
38876
|
async processFrame(input) {
|
|
38754
38877
|
const { settings } = input;
|
|
38878
|
+
for (const t of input.tracked) if (t.resurrectionScene === "multi-subject") this.quarantineHeldFace(input.deviceId, t.trackId);
|
|
38755
38879
|
const candidates = input.tracked.filter((t) => Array.isArray(t.embedding) && t.embedding.length > 0 && typeof t.embeddingModelId === "string" && t.confidence >= settings.minFaceConfidence && (t.nativeFaceShortSidePx ?? (t.faceBbox ? Math.min(t.faceBbox.w, t.faceBbox.h) : void 0) ?? Number.POSITIVE_INFINITY) >= settings.minFacePx);
|
|
38756
38880
|
if (candidates.length === 0) return;
|
|
38757
38881
|
this.deps.logger.debug("face: frame candidates", {
|
|
@@ -38811,6 +38935,35 @@ var FaceRecognizer = class {
|
|
|
38811
38935
|
}
|
|
38812
38936
|
const labelWork = [];
|
|
38813
38937
|
for (const c of candidates) {
|
|
38938
|
+
const quarantined = this.bestFace.get(c.trackId);
|
|
38939
|
+
if (quarantined?.unverified === true && c.faceBbox !== void 0) {
|
|
38940
|
+
const { verdict, cosine } = this.verifyHeldFace(quarantined, {
|
|
38941
|
+
embedding: c.embedding,
|
|
38942
|
+
embeddingModelId: c.embeddingModelId
|
|
38943
|
+
}, settings.similarityThreshold);
|
|
38944
|
+
if (verdict === "confirmed") this.bestFace.set(c.trackId, {
|
|
38945
|
+
...quarantined,
|
|
38946
|
+
unverified: false
|
|
38947
|
+
});
|
|
38948
|
+
else if (verdict === "rejected") {
|
|
38949
|
+
this.bestFace.delete(c.trackId);
|
|
38950
|
+
this.bestTracker.delete(c.trackId);
|
|
38951
|
+
this.aggregates.delete(c.trackId);
|
|
38952
|
+
}
|
|
38953
|
+
if (verdict !== "confirmed") this.deps.logger.info("face: held face not confirmed after a crowded re-attach", {
|
|
38954
|
+
tags: {
|
|
38955
|
+
deviceId: input.deviceId,
|
|
38956
|
+
trackId: c.trackId
|
|
38957
|
+
},
|
|
38958
|
+
meta: {
|
|
38959
|
+
verdict,
|
|
38960
|
+
cosine: Number(cosine.toFixed(3)),
|
|
38961
|
+
threshold: settings.similarityThreshold,
|
|
38962
|
+
heldAt: quarantined.timestamp,
|
|
38963
|
+
droppedHeldFace: verdict === "rejected"
|
|
38964
|
+
}
|
|
38965
|
+
});
|
|
38966
|
+
}
|
|
38814
38967
|
const match = matches.get(c.trackId) ?? null;
|
|
38815
38968
|
const { state, changed } = updateTrackAggregate(this.aggregates.get(c.trackId) ?? EMPTY_AGGREGATE, match, { confirmFrames: settings.confirmFrames });
|
|
38816
38969
|
this.aggregates.set(c.trackId, state);
|
|
@@ -38981,6 +39134,20 @@ var FaceRecognizer = class {
|
|
|
38981
39134
|
} });
|
|
38982
39135
|
return;
|
|
38983
39136
|
}
|
|
39137
|
+
if (held.unverified === true) {
|
|
39138
|
+
this.deps.logger.info("face: dropping unverified held face at close", {
|
|
39139
|
+
tags: {
|
|
39140
|
+
deviceId,
|
|
39141
|
+
trackId
|
|
39142
|
+
},
|
|
39143
|
+
meta: {
|
|
39144
|
+
score: held.score,
|
|
39145
|
+
heldAt: held.timestamp,
|
|
39146
|
+
hasCrop: held.crop !== void 0
|
|
39147
|
+
}
|
|
39148
|
+
});
|
|
39149
|
+
return;
|
|
39150
|
+
}
|
|
38984
39151
|
if (held.crop === void 0) {
|
|
38985
39152
|
const now = Date.now();
|
|
38986
39153
|
if (now - this.lastImagelessLogAt >= FACE_IMAGELESS_LOG_THROTTLE_MS) {
|
|
@@ -30477,6 +30477,8 @@ var DEFAULT_TRACKER_CONFIG = {
|
|
|
30477
30477
|
rescueIouThreshold: .1,
|
|
30478
30478
|
rescueCentroidFactor: .75,
|
|
30479
30479
|
resurrectionWindowMs: 8e3,
|
|
30480
|
+
resurrectionCrowdGuard: true,
|
|
30481
|
+
resurrectionCrowdedIouThreshold: .3,
|
|
30480
30482
|
stationarySpeedPx: 2,
|
|
30481
30483
|
dedupEnabled: true,
|
|
30482
30484
|
dedupSpawnIou: .6,
|
|
@@ -30726,6 +30728,35 @@ var SortTracker = class SortTracker {
|
|
|
30726
30728
|
};
|
|
30727
30729
|
}
|
|
30728
30730
|
/**
|
|
30731
|
+
* How many same-association-group subjects OTHER than `corpse` share the
|
|
30732
|
+
* scene with it: every track LIVE on this frame, plus any other resurrectable
|
|
30733
|
+
* corpse whose lifetime OVERLAPPED the candidate's.
|
|
30734
|
+
*
|
|
30735
|
+
* The overlap clause on the graveyard half is what keeps this guard off the
|
|
30736
|
+
* case the resurrection pass exists for. A single flickering subject leaves a
|
|
30737
|
+
* TRAIL of corpses behind it — that trail is the fragmentation the pass
|
|
30738
|
+
* repairs — but those fragments never coexist: each is born only after the
|
|
30739
|
+
* previous one died. Two distinct people do coexist. Counting corpses without
|
|
30740
|
+
* the overlap test would let a subject's own debris read as a crowd and
|
|
30741
|
+
* re-fragment the very case being protected.
|
|
30742
|
+
*
|
|
30743
|
+
* A live track is counted unconditionally, overlap or not: the subject that
|
|
30744
|
+
* steals an id is very often one whose OWN track was born after the corpse
|
|
30745
|
+
* died (camera 618 — the toddler's track post-dates the older boy's).
|
|
30746
|
+
*/
|
|
30747
|
+
sameGroupPeerCount(corpse, live) {
|
|
30748
|
+
const group = this.assocGroup(corpse.class);
|
|
30749
|
+
let peers = 0;
|
|
30750
|
+
for (const t of live) if (t !== corpse && this.assocGroup(t.class) === group) peers++;
|
|
30751
|
+
for (const other of this.lostTracks) {
|
|
30752
|
+
if (other === corpse) continue;
|
|
30753
|
+
if (!other.resurrectable) continue;
|
|
30754
|
+
if (this.assocGroup(other.class) !== group) continue;
|
|
30755
|
+
if (other.firstSeen <= corpse.lastSeen && corpse.firstSeen <= other.lastSeen) peers++;
|
|
30756
|
+
}
|
|
30757
|
+
return peers;
|
|
30758
|
+
}
|
|
30759
|
+
/**
|
|
30729
30760
|
* Loose same-group gate used by the rescue and resurrection passes: accept
|
|
30730
30761
|
* when the detection overlaps the track's LAST-KNOWN bbox by `rescueIou`, or
|
|
30731
30762
|
* its centroid is within `rescueCentroidFactor × bbox-diagonal`. Always group-
|
|
@@ -30840,6 +30871,7 @@ var SortTracker = class SortTracker {
|
|
|
30840
30871
|
}
|
|
30841
30872
|
update(detections, timestamp, frameContext) {
|
|
30842
30873
|
const frameArea = frameContext ? frameContext.frameWidth * frameContext.frameHeight : 0;
|
|
30874
|
+
const resurrectedScene = /* @__PURE__ */ new Map();
|
|
30843
30875
|
if (this.config.maxTrackLifetimeMs > 0) {
|
|
30844
30876
|
const alive = [];
|
|
30845
30877
|
for (const track of this.tracks) if (timestamp - track.firstSeen > this.config.maxTrackLifetimeMs) {
|
|
@@ -30950,6 +30982,10 @@ var SortTracker = class SortTracker {
|
|
|
30950
30982
|
let best;
|
|
30951
30983
|
let bestScore = -1;
|
|
30952
30984
|
let bestGate = "iou";
|
|
30985
|
+
let bestPeers = 0;
|
|
30986
|
+
let refused;
|
|
30987
|
+
let refusedGate = "iou";
|
|
30988
|
+
let refusedScore = -1;
|
|
30953
30989
|
for (const lost of this.lostTracks) {
|
|
30954
30990
|
if (!lost.resurrectable) continue;
|
|
30955
30991
|
if (timestamp - lost.lostAt > this.config.resurrectionWindowMs) continue;
|
|
@@ -30957,13 +30993,27 @@ var SortTracker = class SortTracker {
|
|
|
30957
30993
|
if (gate === null) continue;
|
|
30958
30994
|
if (gate === "centroid" && this.isPlausibleBirth(det, frameArea)) continue;
|
|
30959
30995
|
const score = iou$2(lost.bbox, det.bbox);
|
|
30996
|
+
const peers = this.sameGroupPeerCount(lost, surviving);
|
|
30997
|
+
if (this.config.resurrectionCrowdGuard && peers > 0 && score < this.config.resurrectionCrowdedIouThreshold) {
|
|
30998
|
+
if (score > refusedScore) {
|
|
30999
|
+
refused = lost;
|
|
31000
|
+
refusedGate = gate;
|
|
31001
|
+
refusedScore = score;
|
|
31002
|
+
}
|
|
31003
|
+
continue;
|
|
31004
|
+
}
|
|
30960
31005
|
if (score > bestScore) {
|
|
30961
31006
|
best = lost;
|
|
30962
31007
|
bestScore = score;
|
|
30963
31008
|
bestGate = gate;
|
|
31009
|
+
bestPeers = peers;
|
|
30964
31010
|
}
|
|
30965
31011
|
}
|
|
30966
|
-
if (!best)
|
|
31012
|
+
if (!best) {
|
|
31013
|
+
if (refused !== void 0) this.reportBind("resurrection-refused", refusedGate, refused, det, timestamp);
|
|
31014
|
+
continue;
|
|
31015
|
+
}
|
|
31016
|
+
resurrectedScene.set(best, bestPeers > 0 ? "multi-subject" : "sole-subject");
|
|
30967
31017
|
this.reportBind("resurrection", bestGate, best, det, timestamp);
|
|
30968
31018
|
this.lostTracks = this.lostTracks.filter((t) => t !== best);
|
|
30969
31019
|
best.bbox = det.bbox;
|
|
@@ -31031,17 +31081,21 @@ var SortTracker = class SortTracker {
|
|
|
31031
31081
|
}
|
|
31032
31082
|
this.tracks = surviving;
|
|
31033
31083
|
this.resolveDuplicates();
|
|
31034
|
-
return this.tracks.filter((t) => this.isConfirmedForEmit(t)).map((t) =>
|
|
31035
|
-
|
|
31036
|
-
|
|
31037
|
-
|
|
31038
|
-
|
|
31039
|
-
|
|
31040
|
-
|
|
31041
|
-
|
|
31042
|
-
|
|
31043
|
-
|
|
31044
|
-
|
|
31084
|
+
return this.tracks.filter((t) => this.isConfirmedForEmit(t)).map((t) => {
|
|
31085
|
+
const scene = resurrectedScene.get(t);
|
|
31086
|
+
return {
|
|
31087
|
+
class: this.resolveReportedClass(t),
|
|
31088
|
+
originalClass: t.originalClass,
|
|
31089
|
+
score: t.score,
|
|
31090
|
+
bbox: t.bbox,
|
|
31091
|
+
trackId: t.id,
|
|
31092
|
+
trackAge: t.hits,
|
|
31093
|
+
velocity: t.velocity,
|
|
31094
|
+
path: [...t.path],
|
|
31095
|
+
matchedThisFrame: t.lastSeen === timestamp,
|
|
31096
|
+
...scene !== void 0 ? { resurrectionScene: scene } : {}
|
|
31097
|
+
};
|
|
31098
|
+
});
|
|
31045
31099
|
}
|
|
31046
31100
|
/**
|
|
31047
31101
|
* Remove a track from BOTH the live and graveyard sets so it neither emits
|
|
@@ -31755,6 +31809,7 @@ var FrameProcessor = class {
|
|
|
31755
31809
|
zones: zonesByTrack.get(td.trackId) ?? [],
|
|
31756
31810
|
state,
|
|
31757
31811
|
matchedThisFrame: td.matchedThisFrame !== false,
|
|
31812
|
+
...td.resurrectionScene !== void 0 ? { resurrectionScene: td.resurrectionScene } : {},
|
|
31758
31813
|
...labelPatch !== void 0 ? { labelPatch } : {},
|
|
31759
31814
|
...emb !== void 0 ? {
|
|
31760
31815
|
embedding: emb.embedding,
|
|
@@ -38687,8 +38742,77 @@ var FaceRecognizer = class {
|
|
|
38687
38742
|
}
|
|
38688
38743
|
});
|
|
38689
38744
|
}
|
|
38745
|
+
/**
|
|
38746
|
+
* The track id this face is held under was re-attached from the graveyard in a
|
|
38747
|
+
* scene that had other same-class subjects in it, so the id's continuity is no
|
|
38748
|
+
* longer self-evident. Stop vouching for what is held, without throwing it
|
|
38749
|
+
* away (D282).
|
|
38750
|
+
*
|
|
38751
|
+
* Three things happen, and the asymmetry between them is the whole decision:
|
|
38752
|
+
*
|
|
38753
|
+
* • the crop and the embedding are KEPT. Discarding them would punish exactly
|
|
38754
|
+
* the case the resurrection pass exists to serve — one subject flickering in
|
|
38755
|
+
* and out — and a good face is not cheap to come by.
|
|
38756
|
+
* • `recognizedIdentityId` is STRIPPED, along with the peak match/suggestion
|
|
38757
|
+
* scores. An identity is a claim about WHO, and the one fact just
|
|
38758
|
+
* established is that nobody knows who this id is on any more.
|
|
38759
|
+
* • the face becomes non-promotable: {@link onTrackEnd} will not write it to
|
|
38760
|
+
* the gallery while the flag stands.
|
|
38761
|
+
*
|
|
38762
|
+
* The aggregate is deliberately left alone — see the note in `onTrackEnd`.
|
|
38763
|
+
*/
|
|
38764
|
+
quarantineHeldFace(deviceId, trackId) {
|
|
38765
|
+
const held = this.bestFace.get(trackId);
|
|
38766
|
+
if (held === void 0 || held.unverified === true) return;
|
|
38767
|
+
const quarantined = {
|
|
38768
|
+
score: held.score,
|
|
38769
|
+
embedding: held.embedding,
|
|
38770
|
+
embeddingModelId: held.embeddingModelId,
|
|
38771
|
+
bbox: held.bbox,
|
|
38772
|
+
timestamp: held.timestamp,
|
|
38773
|
+
unverified: true,
|
|
38774
|
+
...held.crop !== void 0 ? { crop: held.crop } : {},
|
|
38775
|
+
...held.nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx: held.nativeFaceShortSidePx } : {}
|
|
38776
|
+
};
|
|
38777
|
+
this.bestFace.set(trackId, quarantined);
|
|
38778
|
+
this.bestMatchScore.delete(trackId);
|
|
38779
|
+
this.bestSuggestion.delete(trackId);
|
|
38780
|
+
this.deps.logger.info("face: held face unverified — track id re-attached in a crowded scene", {
|
|
38781
|
+
tags: {
|
|
38782
|
+
deviceId,
|
|
38783
|
+
trackId
|
|
38784
|
+
},
|
|
38785
|
+
meta: {
|
|
38786
|
+
heldScore: held.score,
|
|
38787
|
+
heldAt: held.timestamp,
|
|
38788
|
+
strippedIdentityId: held.recognizedIdentityId ?? null,
|
|
38789
|
+
hasCrop: held.crop !== void 0
|
|
38790
|
+
}
|
|
38791
|
+
});
|
|
38792
|
+
}
|
|
38793
|
+
/**
|
|
38794
|
+
* Ask a FRESH face whether the quarantined held face is still about the same
|
|
38795
|
+
* person, at the same bar the gallery uses to say two vectors are one person
|
|
38796
|
+
* (`similarityThreshold` — one knob over one band, D222).
|
|
38797
|
+
*
|
|
38798
|
+
* Only a face-BEARING frame may answer. An embedding inherited onto the person
|
|
38799
|
+
* track on a face-less frame is the held vector's own descendant, so it would
|
|
38800
|
+
* confirm itself; callers gate on `faceBbox` being present.
|
|
38801
|
+
*/
|
|
38802
|
+
verifyHeldFace(held, probe, threshold) {
|
|
38803
|
+
if (held.embeddingModelId !== probe.embeddingModelId || held.embedding.length !== probe.embedding.length) return {
|
|
38804
|
+
verdict: "incomparable",
|
|
38805
|
+
cosine: 0
|
|
38806
|
+
};
|
|
38807
|
+
const cosine = cosineSimilarity$1(new Float32Array(held.embedding), new Float32Array(probe.embedding));
|
|
38808
|
+
return {
|
|
38809
|
+
verdict: cosine >= threshold ? "confirmed" : "rejected",
|
|
38810
|
+
cosine
|
|
38811
|
+
};
|
|
38812
|
+
}
|
|
38690
38813
|
async processFrame(input) {
|
|
38691
38814
|
const { settings } = input;
|
|
38815
|
+
for (const t of input.tracked) if (t.resurrectionScene === "multi-subject") this.quarantineHeldFace(input.deviceId, t.trackId);
|
|
38692
38816
|
const candidates = input.tracked.filter((t) => Array.isArray(t.embedding) && t.embedding.length > 0 && typeof t.embeddingModelId === "string" && t.confidence >= settings.minFaceConfidence && (t.nativeFaceShortSidePx ?? (t.faceBbox ? Math.min(t.faceBbox.w, t.faceBbox.h) : void 0) ?? Number.POSITIVE_INFINITY) >= settings.minFacePx);
|
|
38693
38817
|
if (candidates.length === 0) return;
|
|
38694
38818
|
this.deps.logger.debug("face: frame candidates", {
|
|
@@ -38748,6 +38872,35 @@ var FaceRecognizer = class {
|
|
|
38748
38872
|
}
|
|
38749
38873
|
const labelWork = [];
|
|
38750
38874
|
for (const c of candidates) {
|
|
38875
|
+
const quarantined = this.bestFace.get(c.trackId);
|
|
38876
|
+
if (quarantined?.unverified === true && c.faceBbox !== void 0) {
|
|
38877
|
+
const { verdict, cosine } = this.verifyHeldFace(quarantined, {
|
|
38878
|
+
embedding: c.embedding,
|
|
38879
|
+
embeddingModelId: c.embeddingModelId
|
|
38880
|
+
}, settings.similarityThreshold);
|
|
38881
|
+
if (verdict === "confirmed") this.bestFace.set(c.trackId, {
|
|
38882
|
+
...quarantined,
|
|
38883
|
+
unverified: false
|
|
38884
|
+
});
|
|
38885
|
+
else if (verdict === "rejected") {
|
|
38886
|
+
this.bestFace.delete(c.trackId);
|
|
38887
|
+
this.bestTracker.delete(c.trackId);
|
|
38888
|
+
this.aggregates.delete(c.trackId);
|
|
38889
|
+
}
|
|
38890
|
+
if (verdict !== "confirmed") this.deps.logger.info("face: held face not confirmed after a crowded re-attach", {
|
|
38891
|
+
tags: {
|
|
38892
|
+
deviceId: input.deviceId,
|
|
38893
|
+
trackId: c.trackId
|
|
38894
|
+
},
|
|
38895
|
+
meta: {
|
|
38896
|
+
verdict,
|
|
38897
|
+
cosine: Number(cosine.toFixed(3)),
|
|
38898
|
+
threshold: settings.similarityThreshold,
|
|
38899
|
+
heldAt: quarantined.timestamp,
|
|
38900
|
+
droppedHeldFace: verdict === "rejected"
|
|
38901
|
+
}
|
|
38902
|
+
});
|
|
38903
|
+
}
|
|
38751
38904
|
const match = matches.get(c.trackId) ?? null;
|
|
38752
38905
|
const { state, changed } = updateTrackAggregate(this.aggregates.get(c.trackId) ?? EMPTY_AGGREGATE, match, { confirmFrames: settings.confirmFrames });
|
|
38753
38906
|
this.aggregates.set(c.trackId, state);
|
|
@@ -38918,6 +39071,20 @@ var FaceRecognizer = class {
|
|
|
38918
39071
|
} });
|
|
38919
39072
|
return;
|
|
38920
39073
|
}
|
|
39074
|
+
if (held.unverified === true) {
|
|
39075
|
+
this.deps.logger.info("face: dropping unverified held face at close", {
|
|
39076
|
+
tags: {
|
|
39077
|
+
deviceId,
|
|
39078
|
+
trackId
|
|
39079
|
+
},
|
|
39080
|
+
meta: {
|
|
39081
|
+
score: held.score,
|
|
39082
|
+
heldAt: held.timestamp,
|
|
39083
|
+
hasCrop: held.crop !== void 0
|
|
39084
|
+
}
|
|
39085
|
+
});
|
|
39086
|
+
return;
|
|
39087
|
+
}
|
|
38921
39088
|
if (held.crop === void 0) {
|
|
38922
39089
|
const now = Date.now();
|
|
38923
39090
|
if (now - this.lastImagelessLogAt >= FACE_IMAGELESS_LOG_THROTTLE_MS) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-post-analysis",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.141",
|
|
4
4
|
"description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|