feat(renderers): ✨ Optimize blur performance in BlurRenderer by adding configurable blur parameters and addressing visual artifacts
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
73fec03ae9
commit
875b1f6e39
1 changed files with 29 additions and 28 deletions
|
|
@ -16,38 +16,39 @@ export function applyBlur(
|
|||
h: number,
|
||||
strength: number,
|
||||
): void {
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
// Use the same sphere model as GimpMaskRenderer so blur and mask disguise
|
||||
// the identical head region at all angles.
|
||||
//
|
||||
// A rectangular bounding box with small padding fails at non-zero pitch
|
||||
// (looking up/down) because MediaPipe has no crown landmarks — the top of
|
||||
// the head is outside the landmark set. Modelling the head as a projected
|
||||
// sphere (circle) and sizing it to `max(faceWidth, faceHeight) × 0.72`
|
||||
// gives consistent full-head coverage regardless of orientation.
|
||||
let lmMinX = Infinity, lmMaxX = -Infinity;
|
||||
let lmMinY = Infinity, lmMaxY = -Infinity;
|
||||
let sumX = 0, sumY = 0, lmCount = 0;
|
||||
for (const lm of landmarks) {
|
||||
if (lm.x < minX) minX = lm.x;
|
||||
if (lm.y < minY) minY = lm.y;
|
||||
if (lm.x > maxX) maxX = lm.x;
|
||||
if (lm.y > maxY) maxY = lm.y;
|
||||
const x = lm.x * w;
|
||||
const y = lm.y * h;
|
||||
if (x < lmMinX) lmMinX = x;
|
||||
if (x > lmMaxX) lmMaxX = x;
|
||||
if (y < lmMinY) lmMinY = y;
|
||||
if (y > lmMaxY) lmMaxY = y;
|
||||
sumX += x;
|
||||
sumY += y;
|
||||
lmCount++;
|
||||
}
|
||||
if (lmCount === 0) return;
|
||||
|
||||
if (!isFinite(minX)) return;
|
||||
|
||||
// Pad bounding box by 10% of face extent so blur covers edge pixels.
|
||||
const padX = (maxX - minX) * 0.10;
|
||||
const padY = (maxY - minY) * 0.10;
|
||||
|
||||
const x = Math.max(0, (minX - padX) * w);
|
||||
const y = Math.max(0, (minY - padY) * h);
|
||||
const fw = Math.min(w - x, (maxX - minX + 2 * padX) * w);
|
||||
const fh = Math.min(h - y, (maxY - minY + 2 * padY) * h);
|
||||
const cx = sumX / lmCount;
|
||||
const cy = sumY / lmCount;
|
||||
const faceSpan = Math.max(lmMaxX - lmMinX, lmMaxY - lmMinY);
|
||||
const headR = faceSpan * 0.72;
|
||||
|
||||
// Blur the FULL frame on an intermediate canvas before clipping.
|
||||
//
|
||||
// Blurring a hard-edged crop causes Canvas2D to roll off alpha at the crop
|
||||
// boundaries (the Gaussian kernel only gets partial samples there). At high
|
||||
// strength values the rolloff covers most of the face, making the semi-
|
||||
// transparent blurred layer near-invisible and the original pixels bleed
|
||||
// through — producing the counter-intuitive "high strength = less blur"
|
||||
// effect. By blurring the complete source frame the kernel always has full
|
||||
// neighbourhood data and the result is a monotonically stronger blur.
|
||||
// Blurring a hard-edged crop causes alpha rolloff at the boundaries
|
||||
// (the Gaussian kernel only gets partial samples there), producing the
|
||||
// counter-intuitive "high strength = less visible blur" effect.
|
||||
const intermediate = document.createElement('canvas');
|
||||
intermediate.width = w;
|
||||
intermediate.height = h;
|
||||
|
|
@ -58,7 +59,7 @@ export function applyBlur(
|
|||
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.rect(x, y, fw, fh);
|
||||
ctx.arc(cx, cy, headR, 0, Math.PI * 2);
|
||||
ctx.clip();
|
||||
ctx.drawImage(intermediate, 0, 0);
|
||||
ctx.restore();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue