@camstack/addon-post-analysis 1.2.235 → 1.2.237
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 +213 -24
- package/dist/pipeline-analytics/index.mjs +213 -24
- package/package.json +1 -1
|
@@ -31853,7 +31853,8 @@ function analyseDebugTrack(inventory) {
|
|
|
31853
31853
|
}
|
|
31854
31854
|
}
|
|
31855
31855
|
if (closed && facts.bestEventId === void 0) out.push(finding("NO_BEST_EVENT", "info", "No representative event was chosen for this track."));
|
|
31856
|
-
if (facts.
|
|
31856
|
+
if (facts.labelStepId === "detector:raw-class") out.push(finding("UNSCORED_LABEL", "info", `The label is the detector's raw class (${facts.label ?? facts.className}) — no scored classifier answered for this track.`));
|
|
31857
|
+
else if (facts.labelScore !== void 0 && facts.labelScore < .5) out.push(finding("LOW_BEST_SCORE", "info", `Best label score ${facts.labelScore.toFixed(2)} is under ${String(LOW_BEST_SCORE)}.`));
|
|
31857
31858
|
if (thumbnailProvenance === "standin") out.push(finding("BEST_SHOT_STANDIN", "warn", "Best shot is a stand-in: a face/plate detail crop copied in because no frame of the subject was captured."));
|
|
31858
31859
|
if (thumbnailProvenance === "raster") out.push(finding("BEST_SHOT_RASTER", "info", "Best shot is a sub-native raster crop of the subject, not a native capture."));
|
|
31859
31860
|
return out;
|
|
@@ -31911,6 +31912,7 @@ function toFacts(track) {
|
|
|
31911
31912
|
className: track.className,
|
|
31912
31913
|
...track.label === void 0 ? {} : { label: track.label },
|
|
31913
31914
|
...track.labelScore === void 0 ? {} : { labelScore: track.labelScore },
|
|
31915
|
+
...track.labelMeta?.stepId === void 0 ? {} : { labelStepId: track.labelMeta.stepId },
|
|
31914
31916
|
firstSeen: track.firstSeen,
|
|
31915
31917
|
lastSeen: track.lastSeen,
|
|
31916
31918
|
active: track.active,
|
|
@@ -47581,31 +47583,54 @@ var EventMediaService = class {
|
|
|
47581
47583
|
//#endregion
|
|
47582
47584
|
//#region src/shared/frame/crop-extractor.ts
|
|
47583
47585
|
/**
|
|
47584
|
-
*
|
|
47585
|
-
*
|
|
47586
|
+
* The ONE reading of a normalized box against a frame: round, then clamp so
|
|
47587
|
+
* the rectangle is inside the frame and never empty. Both extractors below
|
|
47588
|
+
* share it, so the JPEG half and the raw half can never disagree about WHICH
|
|
47589
|
+
* pixels the crop is — only about how they are encoded.
|
|
47590
|
+
*/
|
|
47591
|
+
function clampNormalizedBox(frameWidth, frameHeight, bbox) {
|
|
47592
|
+
const left = Math.max(0, Math.min(Math.round(bbox.x * frameWidth), frameWidth - 1));
|
|
47593
|
+
const top = Math.max(0, Math.min(Math.round(bbox.y * frameHeight), frameHeight - 1));
|
|
47594
|
+
return {
|
|
47595
|
+
left,
|
|
47596
|
+
top,
|
|
47597
|
+
width: Math.max(1, Math.min(Math.round(bbox.w * frameWidth), frameWidth - left)),
|
|
47598
|
+
height: Math.max(1, Math.min(Math.round(bbox.h * frameHeight), frameHeight - top))
|
|
47599
|
+
};
|
|
47600
|
+
}
|
|
47601
|
+
/**
|
|
47602
|
+
* Extracts a **JPEG-encoded** crop from a raw frame buffer using a normalized
|
|
47603
|
+
* bounding box. The name does not say JPEG; the return type is what you must
|
|
47604
|
+
* read. A caller whose surrounding frame is raw wants {@link extractCropRaw} —
|
|
47605
|
+
* handing this output on as raw pixels is what produced 717 `VipsImage: memory
|
|
47606
|
+
* area too small` failures and cost the fleet a day of group shots.
|
|
47586
47607
|
*/
|
|
47587
47608
|
async function extractCrop(frameData, frameWidth, frameHeight, bbox) {
|
|
47588
|
-
const
|
|
47589
|
-
const rawTop = Math.round(bbox.y * frameHeight);
|
|
47590
|
-
const rawWidth = Math.round(bbox.w * frameWidth);
|
|
47591
|
-
const rawHeight = Math.round(bbox.h * frameHeight);
|
|
47592
|
-
const left = Math.max(0, Math.min(rawLeft, frameWidth - 1));
|
|
47593
|
-
const top = Math.max(0, Math.min(rawTop, frameHeight - 1));
|
|
47594
|
-
const width = Math.max(1, Math.min(rawWidth, frameWidth - left));
|
|
47595
|
-
const height = Math.max(1, Math.min(rawHeight, frameHeight - top));
|
|
47609
|
+
const rect = clampNormalizedBox(frameWidth, frameHeight, bbox);
|
|
47596
47610
|
return {
|
|
47597
47611
|
crop: await (0, sharp.default)(frameData, { raw: {
|
|
47598
47612
|
width: frameWidth,
|
|
47599
47613
|
height: frameHeight,
|
|
47600
47614
|
channels: 3
|
|
47601
|
-
} }).extract({
|
|
47602
|
-
|
|
47603
|
-
|
|
47604
|
-
|
|
47605
|
-
|
|
47606
|
-
|
|
47607
|
-
|
|
47608
|
-
|
|
47615
|
+
} }).extract(rect).jpeg({ quality: 90 }).toBuffer(),
|
|
47616
|
+
width: rect.width,
|
|
47617
|
+
height: rect.height
|
|
47618
|
+
};
|
|
47619
|
+
}
|
|
47620
|
+
/**
|
|
47621
|
+
* The same cut, raw in and **raw RGB out** — for a caller that carries a
|
|
47622
|
+
* `DecodedFrame` and must keep `format: 'rgb'` true of the bytes it holds.
|
|
47623
|
+
*/
|
|
47624
|
+
async function extractCropRaw(frameData, frameWidth, frameHeight, bbox) {
|
|
47625
|
+
const rect = clampNormalizedBox(frameWidth, frameHeight, bbox);
|
|
47626
|
+
return {
|
|
47627
|
+
data: await (0, sharp.default)(frameData, { raw: {
|
|
47628
|
+
width: frameWidth,
|
|
47629
|
+
height: frameHeight,
|
|
47630
|
+
channels: 3
|
|
47631
|
+
} }).extract(rect).raw().toBuffer(),
|
|
47632
|
+
width: rect.width,
|
|
47633
|
+
height: rect.height
|
|
47609
47634
|
};
|
|
47610
47635
|
}
|
|
47611
47636
|
//#endregion
|
|
@@ -72430,6 +72455,92 @@ var RawGlobalStoreMirror = class {
|
|
|
72430
72455
|
}
|
|
72431
72456
|
};
|
|
72432
72457
|
//#endregion
|
|
72458
|
+
//#region src/pipeline-analytics/summary/group-crop.ts
|
|
72459
|
+
/** Padding around the union, as a fraction of its own size. */
|
|
72460
|
+
var GROUP_CROP_PADDING = .12;
|
|
72461
|
+
/**
|
|
72462
|
+
* Smallest crop, per axis, as a fraction of the frame.
|
|
72463
|
+
*
|
|
72464
|
+
* A quarter. Below that a thumbnail stops being legible at the sizes the
|
|
72465
|
+
* gallery draws it, and the extra context costs nothing — the subjects stay the
|
|
72466
|
+
* dominant thing in the picture at this size.
|
|
72467
|
+
*/
|
|
72468
|
+
var MIN_CROP_FRACTION = .25;
|
|
72469
|
+
/**
|
|
72470
|
+
* The crop, or `null` when there is nothing to cut to — no visible member, or
|
|
72471
|
+
* a frame with no dimensions. `null` means the caller keeps the full frame,
|
|
72472
|
+
* which is the only honest answer when nobody could say where the group was.
|
|
72473
|
+
*/
|
|
72474
|
+
function groupCropRect(input) {
|
|
72475
|
+
const { frameWidth: fw, frameHeight: fh } = input;
|
|
72476
|
+
if (fw <= 0 || fh <= 0) return null;
|
|
72477
|
+
const boxes = input.boxes.filter((b) => b.width > 0 && b.height > 0);
|
|
72478
|
+
if (boxes.length === 0) return null;
|
|
72479
|
+
let left = Number.POSITIVE_INFINITY;
|
|
72480
|
+
let top = Number.POSITIVE_INFINITY;
|
|
72481
|
+
let right = Number.NEGATIVE_INFINITY;
|
|
72482
|
+
let bottom = Number.NEGATIVE_INFINITY;
|
|
72483
|
+
for (const b of boxes) {
|
|
72484
|
+
left = Math.min(left, b.x);
|
|
72485
|
+
top = Math.min(top, b.y);
|
|
72486
|
+
right = Math.max(right, b.x + b.width);
|
|
72487
|
+
bottom = Math.max(bottom, b.y + b.height);
|
|
72488
|
+
}
|
|
72489
|
+
const padX = (right - left) * GROUP_CROP_PADDING;
|
|
72490
|
+
const padY = (bottom - top) * GROUP_CROP_PADDING;
|
|
72491
|
+
let x0 = left - padX;
|
|
72492
|
+
let y0 = top - padY;
|
|
72493
|
+
let x1 = right + padX;
|
|
72494
|
+
let y1 = bottom + padY;
|
|
72495
|
+
const minW = fw * MIN_CROP_FRACTION;
|
|
72496
|
+
const minH = fh * MIN_CROP_FRACTION;
|
|
72497
|
+
if (x1 - x0 < minW) {
|
|
72498
|
+
const cx = (x0 + x1) / 2;
|
|
72499
|
+
x0 = cx - minW / 2;
|
|
72500
|
+
x1 = cx + minW / 2;
|
|
72501
|
+
}
|
|
72502
|
+
if (y1 - y0 < minH) {
|
|
72503
|
+
const cy = (y0 + y1) / 2;
|
|
72504
|
+
y0 = cy - minH / 2;
|
|
72505
|
+
y1 = cy + minH / 2;
|
|
72506
|
+
}
|
|
72507
|
+
const width = Math.min(x1 - x0, fw);
|
|
72508
|
+
const height = Math.min(y1 - y0, fh);
|
|
72509
|
+
const x = Math.max(0, Math.min(x0, fw - width));
|
|
72510
|
+
const y = Math.max(0, Math.min(y0, fh - height));
|
|
72511
|
+
return {
|
|
72512
|
+
x: Math.round(x),
|
|
72513
|
+
y: Math.round(y),
|
|
72514
|
+
width: Math.round(width),
|
|
72515
|
+
height: Math.round(height)
|
|
72516
|
+
};
|
|
72517
|
+
}
|
|
72518
|
+
/**
|
|
72519
|
+
* Carry a crop computed on the DETECTION frame onto the NATIVE surface.
|
|
72520
|
+
*
|
|
72521
|
+
* The two are different sizes by construction: detection runs on a downscaled
|
|
72522
|
+
* frame and the group shot is fetched at up to 4096 px. Applying the rectangle
|
|
72523
|
+
* raw would cut a corner of the picture and call it the group — the failure
|
|
72524
|
+
* mode is silent and looks like a framing bug.
|
|
72525
|
+
*
|
|
72526
|
+
* Scaled per axis rather than by one factor, because the two surfaces are not
|
|
72527
|
+
* guaranteed to share an aspect, and clamped to the native frame so rounding
|
|
72528
|
+
* can never ask for a pixel that is not there.
|
|
72529
|
+
*/
|
|
72530
|
+
function scaleCropToFrame(crop, from, to) {
|
|
72531
|
+
if (from.width <= 0 || from.height <= 0 || to.width <= 0 || to.height <= 0) return null;
|
|
72532
|
+
const sx = to.width / from.width;
|
|
72533
|
+
const sy = to.height / from.height;
|
|
72534
|
+
const x = Math.max(0, Math.min(Math.round(crop.x * sx), to.width - 1));
|
|
72535
|
+
const y = Math.max(0, Math.min(Math.round(crop.y * sy), to.height - 1));
|
|
72536
|
+
return {
|
|
72537
|
+
x,
|
|
72538
|
+
y,
|
|
72539
|
+
width: Math.max(1, Math.min(Math.round(crop.width * sx), to.width - x)),
|
|
72540
|
+
height: Math.max(1, Math.min(Math.round(crop.height * sy), to.height - y))
|
|
72541
|
+
};
|
|
72542
|
+
}
|
|
72543
|
+
//#endregion
|
|
72433
72544
|
//#region src/pipeline-analytics/summary/summary-score.ts
|
|
72434
72545
|
/** A member whose own box is covered this much by others is hidden. */
|
|
72435
72546
|
var OCCLUDED_COVER_FRACTION = .5;
|
|
@@ -72478,12 +72589,24 @@ function scoreSummaryFrame(input) {
|
|
|
72478
72589
|
const visible = present.filter((d) => coveredFraction(d.bbox, present.filter((o) => o !== d).map((o) => o.bbox)) < OCCLUDED_COVER_FRACTION);
|
|
72479
72590
|
if (visible.length === 0) return {
|
|
72480
72591
|
visible: 0,
|
|
72481
|
-
quality: 0
|
|
72592
|
+
quality: 0,
|
|
72593
|
+
crop: null
|
|
72482
72594
|
};
|
|
72483
72595
|
const quality = visible.reduce((sum, d) => sum + memberQuality(d, input.frameWidth, input.frameHeight), 0) / visible.length;
|
|
72596
|
+
const crop = groupCropRect({
|
|
72597
|
+
boxes: visible.map((d) => ({
|
|
72598
|
+
x: d.bbox.x,
|
|
72599
|
+
y: d.bbox.y,
|
|
72600
|
+
width: d.bbox.width,
|
|
72601
|
+
height: d.bbox.height
|
|
72602
|
+
})),
|
|
72603
|
+
frameWidth: input.frameWidth,
|
|
72604
|
+
frameHeight: input.frameHeight
|
|
72605
|
+
});
|
|
72484
72606
|
return {
|
|
72485
72607
|
visible: visible.length,
|
|
72486
|
-
quality
|
|
72608
|
+
quality,
|
|
72609
|
+
crop
|
|
72487
72610
|
};
|
|
72488
72611
|
}
|
|
72489
72612
|
/**
|
|
@@ -72603,6 +72726,54 @@ var GroupShotTracker = class {
|
|
|
72603
72726
|
};
|
|
72604
72727
|
//#endregion
|
|
72605
72728
|
//#region src/pipeline-analytics/summary/group-shot-materialiser.ts
|
|
72729
|
+
/**
|
|
72730
|
+
* Apply the group crop to the native surface, or hand it back untouched.
|
|
72731
|
+
*
|
|
72732
|
+
* Every reason to skip is a reason NOT to guess: no rectangle, no detection
|
|
72733
|
+
* geometry to scale from, or a cropper this deployment does not have. The full
|
|
72734
|
+
* frame is worse than a crop and far better than a wrong crop.
|
|
72735
|
+
*/
|
|
72736
|
+
async function cropNative(native, input, deps) {
|
|
72737
|
+
const crop = input.crop;
|
|
72738
|
+
if (!crop || !deps.cropFrame) return native;
|
|
72739
|
+
const fw = input.cropFrameWidth;
|
|
72740
|
+
const fh = input.cropFrameHeight;
|
|
72741
|
+
if (fw === void 0 || fh === void 0) return native;
|
|
72742
|
+
const scaled = scaleCropToFrame(crop, {
|
|
72743
|
+
width: fw,
|
|
72744
|
+
height: fh
|
|
72745
|
+
}, native);
|
|
72746
|
+
if (scaled === null) return native;
|
|
72747
|
+
if (scaled.width >= native.width && scaled.height >= native.height) return native;
|
|
72748
|
+
try {
|
|
72749
|
+
const cut = await deps.cropFrame(native, scaled);
|
|
72750
|
+
if (cut === null) return native;
|
|
72751
|
+
const expected = cut.width * cut.height * 3;
|
|
72752
|
+
if (cut.format === "rgb" && cut.data.length !== expected) {
|
|
72753
|
+
deps.logger.warn("group shot crop returned a surface that is not its geometry", {
|
|
72754
|
+
tags: { deviceId: input.deviceId },
|
|
72755
|
+
meta: {
|
|
72756
|
+
summaryId: input.summaryId,
|
|
72757
|
+
width: cut.width,
|
|
72758
|
+
height: cut.height,
|
|
72759
|
+
expectedBytes: expected,
|
|
72760
|
+
gotBytes: cut.data.length
|
|
72761
|
+
}
|
|
72762
|
+
});
|
|
72763
|
+
return native;
|
|
72764
|
+
}
|
|
72765
|
+
return cut;
|
|
72766
|
+
} catch (err) {
|
|
72767
|
+
deps.logger.warn("group shot crop failed — storing the full frame", {
|
|
72768
|
+
tags: { deviceId: input.deviceId },
|
|
72769
|
+
meta: {
|
|
72770
|
+
summaryId: input.summaryId,
|
|
72771
|
+
error: require_dist.errMsg(err)
|
|
72772
|
+
}
|
|
72773
|
+
});
|
|
72774
|
+
return native;
|
|
72775
|
+
}
|
|
72776
|
+
}
|
|
72606
72777
|
async function persistSummaryGroupShot(deps, input) {
|
|
72607
72778
|
const { deviceId, summaryId, frameHandle } = input;
|
|
72608
72779
|
const drop = {
|
|
@@ -72636,7 +72807,8 @@ async function persistSummaryGroupShot(deps, input) {
|
|
|
72636
72807
|
});
|
|
72637
72808
|
return { kind: "ram-tier" };
|
|
72638
72809
|
}
|
|
72639
|
-
const
|
|
72810
|
+
const surface = await cropNative(native, input, deps);
|
|
72811
|
+
const { keyFrame, keyFrameSmall } = await deps.encodeVariants(surface);
|
|
72640
72812
|
const keyFrameMediaId = await deps.media.putReplacing({
|
|
72641
72813
|
deviceId,
|
|
72642
72814
|
ownerKind: "summary",
|
|
@@ -79856,13 +80028,30 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
79856
80028
|
fetchNativeFullFrame: getNative,
|
|
79857
80029
|
encodeVariants: encodeKeyFrameVariants,
|
|
79858
80030
|
media: mediaStore,
|
|
79859
|
-
logger: this.ctx.logger
|
|
80031
|
+
logger: this.ctx.logger,
|
|
80032
|
+
cropFrame: async (frame, rect) => {
|
|
80033
|
+
const out = await extractCropRaw(frame.data, frame.width, frame.height, {
|
|
80034
|
+
x: rect.x / frame.width,
|
|
80035
|
+
y: rect.y / frame.height,
|
|
80036
|
+
w: rect.width / frame.width,
|
|
80037
|
+
h: rect.height / frame.height
|
|
80038
|
+
});
|
|
80039
|
+
return {
|
|
80040
|
+
...frame,
|
|
80041
|
+
data: out.data,
|
|
80042
|
+
width: out.width,
|
|
80043
|
+
height: out.height
|
|
80044
|
+
};
|
|
80045
|
+
}
|
|
79860
80046
|
}, {
|
|
79861
80047
|
deviceId,
|
|
79862
80048
|
summaryId,
|
|
79863
80049
|
timestamp: result.timestamp,
|
|
79864
80050
|
frameHandle,
|
|
79865
|
-
maxWidth: KEYFRAME_NATIVE_MAX_WIDTH
|
|
80051
|
+
maxWidth: KEYFRAME_NATIVE_MAX_WIDTH,
|
|
80052
|
+
crop: score.crop,
|
|
80053
|
+
cropFrameWidth: result.frameWidth,
|
|
80054
|
+
cropFrameHeight: result.frameHeight
|
|
79866
80055
|
})
|
|
79867
80056
|
}).then((outcome) => {
|
|
79868
80057
|
if (outcome?.kind === "landed") {
|
|
@@ -31830,7 +31830,8 @@ function analyseDebugTrack(inventory) {
|
|
|
31830
31830
|
}
|
|
31831
31831
|
}
|
|
31832
31832
|
if (closed && facts.bestEventId === void 0) out.push(finding("NO_BEST_EVENT", "info", "No representative event was chosen for this track."));
|
|
31833
|
-
if (facts.
|
|
31833
|
+
if (facts.labelStepId === "detector:raw-class") out.push(finding("UNSCORED_LABEL", "info", `The label is the detector's raw class (${facts.label ?? facts.className}) — no scored classifier answered for this track.`));
|
|
31834
|
+
else if (facts.labelScore !== void 0 && facts.labelScore < .5) out.push(finding("LOW_BEST_SCORE", "info", `Best label score ${facts.labelScore.toFixed(2)} is under ${String(LOW_BEST_SCORE)}.`));
|
|
31834
31835
|
if (thumbnailProvenance === "standin") out.push(finding("BEST_SHOT_STANDIN", "warn", "Best shot is a stand-in: a face/plate detail crop copied in because no frame of the subject was captured."));
|
|
31835
31836
|
if (thumbnailProvenance === "raster") out.push(finding("BEST_SHOT_RASTER", "info", "Best shot is a sub-native raster crop of the subject, not a native capture."));
|
|
31836
31837
|
return out;
|
|
@@ -31888,6 +31889,7 @@ function toFacts(track) {
|
|
|
31888
31889
|
className: track.className,
|
|
31889
31890
|
...track.label === void 0 ? {} : { label: track.label },
|
|
31890
31891
|
...track.labelScore === void 0 ? {} : { labelScore: track.labelScore },
|
|
31892
|
+
...track.labelMeta?.stepId === void 0 ? {} : { labelStepId: track.labelMeta.stepId },
|
|
31891
31893
|
firstSeen: track.firstSeen,
|
|
31892
31894
|
lastSeen: track.lastSeen,
|
|
31893
31895
|
active: track.active,
|
|
@@ -47507,31 +47509,54 @@ var EventMediaService = class {
|
|
|
47507
47509
|
//#endregion
|
|
47508
47510
|
//#region src/shared/frame/crop-extractor.ts
|
|
47509
47511
|
/**
|
|
47510
|
-
*
|
|
47511
|
-
*
|
|
47512
|
+
* The ONE reading of a normalized box against a frame: round, then clamp so
|
|
47513
|
+
* the rectangle is inside the frame and never empty. Both extractors below
|
|
47514
|
+
* share it, so the JPEG half and the raw half can never disagree about WHICH
|
|
47515
|
+
* pixels the crop is — only about how they are encoded.
|
|
47516
|
+
*/
|
|
47517
|
+
function clampNormalizedBox(frameWidth, frameHeight, bbox) {
|
|
47518
|
+
const left = Math.max(0, Math.min(Math.round(bbox.x * frameWidth), frameWidth - 1));
|
|
47519
|
+
const top = Math.max(0, Math.min(Math.round(bbox.y * frameHeight), frameHeight - 1));
|
|
47520
|
+
return {
|
|
47521
|
+
left,
|
|
47522
|
+
top,
|
|
47523
|
+
width: Math.max(1, Math.min(Math.round(bbox.w * frameWidth), frameWidth - left)),
|
|
47524
|
+
height: Math.max(1, Math.min(Math.round(bbox.h * frameHeight), frameHeight - top))
|
|
47525
|
+
};
|
|
47526
|
+
}
|
|
47527
|
+
/**
|
|
47528
|
+
* Extracts a **JPEG-encoded** crop from a raw frame buffer using a normalized
|
|
47529
|
+
* bounding box. The name does not say JPEG; the return type is what you must
|
|
47530
|
+
* read. A caller whose surrounding frame is raw wants {@link extractCropRaw} —
|
|
47531
|
+
* handing this output on as raw pixels is what produced 717 `VipsImage: memory
|
|
47532
|
+
* area too small` failures and cost the fleet a day of group shots.
|
|
47512
47533
|
*/
|
|
47513
47534
|
async function extractCrop(frameData, frameWidth, frameHeight, bbox) {
|
|
47514
|
-
const
|
|
47515
|
-
const rawTop = Math.round(bbox.y * frameHeight);
|
|
47516
|
-
const rawWidth = Math.round(bbox.w * frameWidth);
|
|
47517
|
-
const rawHeight = Math.round(bbox.h * frameHeight);
|
|
47518
|
-
const left = Math.max(0, Math.min(rawLeft, frameWidth - 1));
|
|
47519
|
-
const top = Math.max(0, Math.min(rawTop, frameHeight - 1));
|
|
47520
|
-
const width = Math.max(1, Math.min(rawWidth, frameWidth - left));
|
|
47521
|
-
const height = Math.max(1, Math.min(rawHeight, frameHeight - top));
|
|
47535
|
+
const rect = clampNormalizedBox(frameWidth, frameHeight, bbox);
|
|
47522
47536
|
return {
|
|
47523
47537
|
crop: await sharp(frameData, { raw: {
|
|
47524
47538
|
width: frameWidth,
|
|
47525
47539
|
height: frameHeight,
|
|
47526
47540
|
channels: 3
|
|
47527
|
-
} }).extract({
|
|
47528
|
-
|
|
47529
|
-
|
|
47530
|
-
|
|
47531
|
-
|
|
47532
|
-
|
|
47533
|
-
|
|
47534
|
-
|
|
47541
|
+
} }).extract(rect).jpeg({ quality: 90 }).toBuffer(),
|
|
47542
|
+
width: rect.width,
|
|
47543
|
+
height: rect.height
|
|
47544
|
+
};
|
|
47545
|
+
}
|
|
47546
|
+
/**
|
|
47547
|
+
* The same cut, raw in and **raw RGB out** — for a caller that carries a
|
|
47548
|
+
* `DecodedFrame` and must keep `format: 'rgb'` true of the bytes it holds.
|
|
47549
|
+
*/
|
|
47550
|
+
async function extractCropRaw(frameData, frameWidth, frameHeight, bbox) {
|
|
47551
|
+
const rect = clampNormalizedBox(frameWidth, frameHeight, bbox);
|
|
47552
|
+
return {
|
|
47553
|
+
data: await sharp(frameData, { raw: {
|
|
47554
|
+
width: frameWidth,
|
|
47555
|
+
height: frameHeight,
|
|
47556
|
+
channels: 3
|
|
47557
|
+
} }).extract(rect).raw().toBuffer(),
|
|
47558
|
+
width: rect.width,
|
|
47559
|
+
height: rect.height
|
|
47535
47560
|
};
|
|
47536
47561
|
}
|
|
47537
47562
|
//#endregion
|
|
@@ -72356,6 +72381,92 @@ var RawGlobalStoreMirror = class {
|
|
|
72356
72381
|
}
|
|
72357
72382
|
};
|
|
72358
72383
|
//#endregion
|
|
72384
|
+
//#region src/pipeline-analytics/summary/group-crop.ts
|
|
72385
|
+
/** Padding around the union, as a fraction of its own size. */
|
|
72386
|
+
var GROUP_CROP_PADDING = .12;
|
|
72387
|
+
/**
|
|
72388
|
+
* Smallest crop, per axis, as a fraction of the frame.
|
|
72389
|
+
*
|
|
72390
|
+
* A quarter. Below that a thumbnail stops being legible at the sizes the
|
|
72391
|
+
* gallery draws it, and the extra context costs nothing — the subjects stay the
|
|
72392
|
+
* dominant thing in the picture at this size.
|
|
72393
|
+
*/
|
|
72394
|
+
var MIN_CROP_FRACTION = .25;
|
|
72395
|
+
/**
|
|
72396
|
+
* The crop, or `null` when there is nothing to cut to — no visible member, or
|
|
72397
|
+
* a frame with no dimensions. `null` means the caller keeps the full frame,
|
|
72398
|
+
* which is the only honest answer when nobody could say where the group was.
|
|
72399
|
+
*/
|
|
72400
|
+
function groupCropRect(input) {
|
|
72401
|
+
const { frameWidth: fw, frameHeight: fh } = input;
|
|
72402
|
+
if (fw <= 0 || fh <= 0) return null;
|
|
72403
|
+
const boxes = input.boxes.filter((b) => b.width > 0 && b.height > 0);
|
|
72404
|
+
if (boxes.length === 0) return null;
|
|
72405
|
+
let left = Number.POSITIVE_INFINITY;
|
|
72406
|
+
let top = Number.POSITIVE_INFINITY;
|
|
72407
|
+
let right = Number.NEGATIVE_INFINITY;
|
|
72408
|
+
let bottom = Number.NEGATIVE_INFINITY;
|
|
72409
|
+
for (const b of boxes) {
|
|
72410
|
+
left = Math.min(left, b.x);
|
|
72411
|
+
top = Math.min(top, b.y);
|
|
72412
|
+
right = Math.max(right, b.x + b.width);
|
|
72413
|
+
bottom = Math.max(bottom, b.y + b.height);
|
|
72414
|
+
}
|
|
72415
|
+
const padX = (right - left) * GROUP_CROP_PADDING;
|
|
72416
|
+
const padY = (bottom - top) * GROUP_CROP_PADDING;
|
|
72417
|
+
let x0 = left - padX;
|
|
72418
|
+
let y0 = top - padY;
|
|
72419
|
+
let x1 = right + padX;
|
|
72420
|
+
let y1 = bottom + padY;
|
|
72421
|
+
const minW = fw * MIN_CROP_FRACTION;
|
|
72422
|
+
const minH = fh * MIN_CROP_FRACTION;
|
|
72423
|
+
if (x1 - x0 < minW) {
|
|
72424
|
+
const cx = (x0 + x1) / 2;
|
|
72425
|
+
x0 = cx - minW / 2;
|
|
72426
|
+
x1 = cx + minW / 2;
|
|
72427
|
+
}
|
|
72428
|
+
if (y1 - y0 < minH) {
|
|
72429
|
+
const cy = (y0 + y1) / 2;
|
|
72430
|
+
y0 = cy - minH / 2;
|
|
72431
|
+
y1 = cy + minH / 2;
|
|
72432
|
+
}
|
|
72433
|
+
const width = Math.min(x1 - x0, fw);
|
|
72434
|
+
const height = Math.min(y1 - y0, fh);
|
|
72435
|
+
const x = Math.max(0, Math.min(x0, fw - width));
|
|
72436
|
+
const y = Math.max(0, Math.min(y0, fh - height));
|
|
72437
|
+
return {
|
|
72438
|
+
x: Math.round(x),
|
|
72439
|
+
y: Math.round(y),
|
|
72440
|
+
width: Math.round(width),
|
|
72441
|
+
height: Math.round(height)
|
|
72442
|
+
};
|
|
72443
|
+
}
|
|
72444
|
+
/**
|
|
72445
|
+
* Carry a crop computed on the DETECTION frame onto the NATIVE surface.
|
|
72446
|
+
*
|
|
72447
|
+
* The two are different sizes by construction: detection runs on a downscaled
|
|
72448
|
+
* frame and the group shot is fetched at up to 4096 px. Applying the rectangle
|
|
72449
|
+
* raw would cut a corner of the picture and call it the group — the failure
|
|
72450
|
+
* mode is silent and looks like a framing bug.
|
|
72451
|
+
*
|
|
72452
|
+
* Scaled per axis rather than by one factor, because the two surfaces are not
|
|
72453
|
+
* guaranteed to share an aspect, and clamped to the native frame so rounding
|
|
72454
|
+
* can never ask for a pixel that is not there.
|
|
72455
|
+
*/
|
|
72456
|
+
function scaleCropToFrame(crop, from, to) {
|
|
72457
|
+
if (from.width <= 0 || from.height <= 0 || to.width <= 0 || to.height <= 0) return null;
|
|
72458
|
+
const sx = to.width / from.width;
|
|
72459
|
+
const sy = to.height / from.height;
|
|
72460
|
+
const x = Math.max(0, Math.min(Math.round(crop.x * sx), to.width - 1));
|
|
72461
|
+
const y = Math.max(0, Math.min(Math.round(crop.y * sy), to.height - 1));
|
|
72462
|
+
return {
|
|
72463
|
+
x,
|
|
72464
|
+
y,
|
|
72465
|
+
width: Math.max(1, Math.min(Math.round(crop.width * sx), to.width - x)),
|
|
72466
|
+
height: Math.max(1, Math.min(Math.round(crop.height * sy), to.height - y))
|
|
72467
|
+
};
|
|
72468
|
+
}
|
|
72469
|
+
//#endregion
|
|
72359
72470
|
//#region src/pipeline-analytics/summary/summary-score.ts
|
|
72360
72471
|
/** A member whose own box is covered this much by others is hidden. */
|
|
72361
72472
|
var OCCLUDED_COVER_FRACTION = .5;
|
|
@@ -72404,12 +72515,24 @@ function scoreSummaryFrame(input) {
|
|
|
72404
72515
|
const visible = present.filter((d) => coveredFraction(d.bbox, present.filter((o) => o !== d).map((o) => o.bbox)) < OCCLUDED_COVER_FRACTION);
|
|
72405
72516
|
if (visible.length === 0) return {
|
|
72406
72517
|
visible: 0,
|
|
72407
|
-
quality: 0
|
|
72518
|
+
quality: 0,
|
|
72519
|
+
crop: null
|
|
72408
72520
|
};
|
|
72409
72521
|
const quality = visible.reduce((sum, d) => sum + memberQuality(d, input.frameWidth, input.frameHeight), 0) / visible.length;
|
|
72522
|
+
const crop = groupCropRect({
|
|
72523
|
+
boxes: visible.map((d) => ({
|
|
72524
|
+
x: d.bbox.x,
|
|
72525
|
+
y: d.bbox.y,
|
|
72526
|
+
width: d.bbox.width,
|
|
72527
|
+
height: d.bbox.height
|
|
72528
|
+
})),
|
|
72529
|
+
frameWidth: input.frameWidth,
|
|
72530
|
+
frameHeight: input.frameHeight
|
|
72531
|
+
});
|
|
72410
72532
|
return {
|
|
72411
72533
|
visible: visible.length,
|
|
72412
|
-
quality
|
|
72534
|
+
quality,
|
|
72535
|
+
crop
|
|
72413
72536
|
};
|
|
72414
72537
|
}
|
|
72415
72538
|
/**
|
|
@@ -72529,6 +72652,54 @@ var GroupShotTracker = class {
|
|
|
72529
72652
|
};
|
|
72530
72653
|
//#endregion
|
|
72531
72654
|
//#region src/pipeline-analytics/summary/group-shot-materialiser.ts
|
|
72655
|
+
/**
|
|
72656
|
+
* Apply the group crop to the native surface, or hand it back untouched.
|
|
72657
|
+
*
|
|
72658
|
+
* Every reason to skip is a reason NOT to guess: no rectangle, no detection
|
|
72659
|
+
* geometry to scale from, or a cropper this deployment does not have. The full
|
|
72660
|
+
* frame is worse than a crop and far better than a wrong crop.
|
|
72661
|
+
*/
|
|
72662
|
+
async function cropNative(native, input, deps) {
|
|
72663
|
+
const crop = input.crop;
|
|
72664
|
+
if (!crop || !deps.cropFrame) return native;
|
|
72665
|
+
const fw = input.cropFrameWidth;
|
|
72666
|
+
const fh = input.cropFrameHeight;
|
|
72667
|
+
if (fw === void 0 || fh === void 0) return native;
|
|
72668
|
+
const scaled = scaleCropToFrame(crop, {
|
|
72669
|
+
width: fw,
|
|
72670
|
+
height: fh
|
|
72671
|
+
}, native);
|
|
72672
|
+
if (scaled === null) return native;
|
|
72673
|
+
if (scaled.width >= native.width && scaled.height >= native.height) return native;
|
|
72674
|
+
try {
|
|
72675
|
+
const cut = await deps.cropFrame(native, scaled);
|
|
72676
|
+
if (cut === null) return native;
|
|
72677
|
+
const expected = cut.width * cut.height * 3;
|
|
72678
|
+
if (cut.format === "rgb" && cut.data.length !== expected) {
|
|
72679
|
+
deps.logger.warn("group shot crop returned a surface that is not its geometry", {
|
|
72680
|
+
tags: { deviceId: input.deviceId },
|
|
72681
|
+
meta: {
|
|
72682
|
+
summaryId: input.summaryId,
|
|
72683
|
+
width: cut.width,
|
|
72684
|
+
height: cut.height,
|
|
72685
|
+
expectedBytes: expected,
|
|
72686
|
+
gotBytes: cut.data.length
|
|
72687
|
+
}
|
|
72688
|
+
});
|
|
72689
|
+
return native;
|
|
72690
|
+
}
|
|
72691
|
+
return cut;
|
|
72692
|
+
} catch (err) {
|
|
72693
|
+
deps.logger.warn("group shot crop failed — storing the full frame", {
|
|
72694
|
+
tags: { deviceId: input.deviceId },
|
|
72695
|
+
meta: {
|
|
72696
|
+
summaryId: input.summaryId,
|
|
72697
|
+
error: errMsg$1(err)
|
|
72698
|
+
}
|
|
72699
|
+
});
|
|
72700
|
+
return native;
|
|
72701
|
+
}
|
|
72702
|
+
}
|
|
72532
72703
|
async function persistSummaryGroupShot(deps, input) {
|
|
72533
72704
|
const { deviceId, summaryId, frameHandle } = input;
|
|
72534
72705
|
const drop = {
|
|
@@ -72562,7 +72733,8 @@ async function persistSummaryGroupShot(deps, input) {
|
|
|
72562
72733
|
});
|
|
72563
72734
|
return { kind: "ram-tier" };
|
|
72564
72735
|
}
|
|
72565
|
-
const
|
|
72736
|
+
const surface = await cropNative(native, input, deps);
|
|
72737
|
+
const { keyFrame, keyFrameSmall } = await deps.encodeVariants(surface);
|
|
72566
72738
|
const keyFrameMediaId = await deps.media.putReplacing({
|
|
72567
72739
|
deviceId,
|
|
72568
72740
|
ownerKind: "summary",
|
|
@@ -79782,13 +79954,30 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
79782
79954
|
fetchNativeFullFrame: getNative,
|
|
79783
79955
|
encodeVariants: encodeKeyFrameVariants,
|
|
79784
79956
|
media: mediaStore,
|
|
79785
|
-
logger: this.ctx.logger
|
|
79957
|
+
logger: this.ctx.logger,
|
|
79958
|
+
cropFrame: async (frame, rect) => {
|
|
79959
|
+
const out = await extractCropRaw(frame.data, frame.width, frame.height, {
|
|
79960
|
+
x: rect.x / frame.width,
|
|
79961
|
+
y: rect.y / frame.height,
|
|
79962
|
+
w: rect.width / frame.width,
|
|
79963
|
+
h: rect.height / frame.height
|
|
79964
|
+
});
|
|
79965
|
+
return {
|
|
79966
|
+
...frame,
|
|
79967
|
+
data: out.data,
|
|
79968
|
+
width: out.width,
|
|
79969
|
+
height: out.height
|
|
79970
|
+
};
|
|
79971
|
+
}
|
|
79786
79972
|
}, {
|
|
79787
79973
|
deviceId,
|
|
79788
79974
|
summaryId,
|
|
79789
79975
|
timestamp: result.timestamp,
|
|
79790
79976
|
frameHandle,
|
|
79791
|
-
maxWidth: KEYFRAME_NATIVE_MAX_WIDTH
|
|
79977
|
+
maxWidth: KEYFRAME_NATIVE_MAX_WIDTH,
|
|
79978
|
+
crop: score.crop,
|
|
79979
|
+
cropFrameWidth: result.frameWidth,
|
|
79980
|
+
cropFrameHeight: result.frameHeight
|
|
79792
79981
|
})
|
|
79793
79982
|
}).then((outcome) => {
|
|
79794
79983
|
if (outcome?.kind === "landed") {
|
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.237",
|
|
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",
|