Buckets:
| /** | |
| * @license | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import {StoredUserProfile} from './voice-memory'; | |
| export interface PixelFaceBox { | |
| x: number; | |
| y: number; | |
| width: number; | |
| height: number; | |
| } | |
| export interface FaceAnalysis { | |
| box: PixelFaceBox; | |
| fingerprint: number[]; | |
| brightness: number; | |
| sharpness: number; | |
| warmth: number; | |
| } | |
| export interface FaceMatchResult { | |
| profile: StoredUserProfile; | |
| similarity: number; | |
| } | |
| function clamp(value: number, min: number, max: number): number { | |
| return Math.min(max, Math.max(min, value)); | |
| } | |
| function cosineSimilarity(a: number[], b: number[]): number { | |
| if (!a.length || a.length !== b.length) return 0; | |
| let dot = 0; | |
| let normA = 0; | |
| let normB = 0; | |
| for (let i = 0; i < a.length; i++) { | |
| dot += a[i] * b[i]; | |
| normA += a[i] * a[i]; | |
| normB += b[i] * b[i]; | |
| } | |
| if (normA === 0 || normB === 0) return 0; | |
| return dot / (Math.sqrt(normA) * Math.sqrt(normB)); | |
| } | |
| export function faceFingerprintSimilarity(a: number[], b: number[]): number { | |
| return cosineSimilarity(a, b); | |
| } | |
| function normalizeVector(vector: number[]): number[] { | |
| let norm = 0; | |
| for (const value of vector) { | |
| norm += value * value; | |
| } | |
| norm = Math.sqrt(norm) || 1; | |
| return vector.map((value) => value / norm); | |
| } | |
| function sampleGridLuminance( | |
| data: Uint8ClampedArray, | |
| imageWidth: number, | |
| imageHeight: number, | |
| box: PixelFaceBox, | |
| ): number[] { | |
| const grid = 6; | |
| const result: number[] = []; | |
| const startX = clamp(Math.floor(box.x), 0, imageWidth - 1); | |
| const startY = clamp(Math.floor(box.y), 0, imageHeight - 1); | |
| const width = clamp(Math.floor(box.width), 1, imageWidth - startX); | |
| const height = clamp(Math.floor(box.height), 1, imageHeight - startY); | |
| for (let gy = 0; gy < grid; gy++) { | |
| for (let gx = 0; gx < grid; gx++) { | |
| const cellStartX = startX + Math.floor((gx * width) / grid); | |
| const cellEndX = startX + Math.floor(((gx + 1) * width) / grid); | |
| const cellStartY = startY + Math.floor((gy * height) / grid); | |
| const cellEndY = startY + Math.floor(((gy + 1) * height) / grid); | |
| let lumSum = 0; | |
| let samples = 0; | |
| const step = 2; | |
| for (let y = cellStartY; y < cellEndY; y += step) { | |
| for (let x = cellStartX; x < cellEndX; x += step) { | |
| const idx = (y * imageWidth + x) * 4; | |
| const r = data[idx] / 255; | |
| const g = data[idx + 1] / 255; | |
| const b = data[idx + 2] / 255; | |
| lumSum += 0.2126 * r + 0.7152 * g + 0.0722 * b; | |
| samples++; | |
| } | |
| } | |
| result.push(samples ? lumSum / samples : 0); | |
| } | |
| } | |
| return result; | |
| } | |
| export function extractFaceAnalysis( | |
| imageData: ImageData, | |
| box: PixelFaceBox, | |
| ): FaceAnalysis { | |
| const data = imageData.data; | |
| const imageWidth = imageData.width; | |
| const imageHeight = imageData.height; | |
| const startX = clamp(Math.floor(box.x), 0, imageWidth - 1); | |
| const startY = clamp(Math.floor(box.y), 0, imageHeight - 1); | |
| const width = clamp(Math.floor(box.width), 1, imageWidth - startX); | |
| const height = clamp(Math.floor(box.height), 1, imageHeight - startY); | |
| let rSum = 0; | |
| let gSum = 0; | |
| let bSum = 0; | |
| let lSum = 0; | |
| let lSquared = 0; | |
| let samples = 0; | |
| let edgeX = 0; | |
| let edgeY = 0; | |
| for (let y = startY; y < startY + height; y += 2) { | |
| for (let x = startX; x < startX + width; x += 2) { | |
| const idx = (y * imageWidth + x) * 4; | |
| const r = data[idx] / 255; | |
| const g = data[idx + 1] / 255; | |
| const b = data[idx + 2] / 255; | |
| const l = 0.2126 * r + 0.7152 * g + 0.0722 * b; | |
| rSum += r; | |
| gSum += g; | |
| bSum += b; | |
| lSum += l; | |
| lSquared += l * l; | |
| samples++; | |
| if (x + 2 < startX + width) { | |
| const idxX = (y * imageWidth + (x + 2)) * 4; | |
| const lX = | |
| 0.2126 * (data[idxX] / 255) + | |
| 0.7152 * (data[idxX + 1] / 255) + | |
| 0.0722 * (data[idxX + 2] / 255); | |
| edgeX += Math.abs(lX - l); | |
| } | |
| if (y + 2 < startY + height) { | |
| const idxY = ((y + 2) * imageWidth + x) * 4; | |
| const lY = | |
| 0.2126 * (data[idxY] / 255) + | |
| 0.7152 * (data[idxY + 1] / 255) + | |
| 0.0722 * (data[idxY + 2] / 255); | |
| edgeY += Math.abs(lY - l); | |
| } | |
| } | |
| } | |
| const count = Math.max(samples, 1); | |
| const meanR = rSum / count; | |
| const meanG = gSum / count; | |
| const meanB = bSum / count; | |
| const meanL = lSum / count; | |
| const varianceL = Math.max(0, lSquared / count - meanL * meanL); | |
| const stdL = Math.sqrt(varianceL); | |
| const gridLuma = sampleGridLuminance(data, imageWidth, imageHeight, { | |
| x: startX, | |
| y: startY, | |
| width, | |
| height, | |
| }); | |
| const ratio = width / Math.max(height, 1); | |
| const rawFingerprint = [ | |
| ...gridLuma, | |
| meanR, | |
| meanG, | |
| meanB, | |
| stdL, | |
| edgeX / count, | |
| edgeY / count, | |
| ratio, | |
| clamp(width / imageWidth, 0, 1), | |
| clamp(height / imageHeight, 0, 1), | |
| ]; | |
| return { | |
| box: {x: startX, y: startY, width, height}, | |
| fingerprint: normalizeVector(rawFingerprint), | |
| brightness: meanL, | |
| sharpness: (edgeX + edgeY) / count, | |
| warmth: meanR - meanB, | |
| }; | |
| } | |
| export function identifyFaceMatch( | |
| fingerprint: number[], | |
| profiles: StoredUserProfile[], | |
| threshold = 0.94, | |
| ): FaceMatchResult | null { | |
| let best: FaceMatchResult | null = null; | |
| for (const profile of profiles) { | |
| const prints = profile.facePrints ?? []; | |
| for (const print of prints) { | |
| if (!print.length || print.length !== fingerprint.length) continue; | |
| const similarity = cosineSimilarity(print, fingerprint); | |
| if (!best || similarity > best.similarity) { | |
| best = {profile, similarity}; | |
| } | |
| } | |
| } | |
| if (!best || best.similarity < threshold) { | |
| return null; | |
| } | |
| return best; | |
| } | |
| export function describeFaceAnalysis(analysis: FaceAnalysis): string { | |
| const lighting = | |
| analysis.brightness > 0.62 | |
| ? 'well-lit face' | |
| : analysis.brightness > 0.38 | |
| ? 'balanced lighting' | |
| : 'low-light face'; | |
| const detail = | |
| analysis.sharpness > 0.18 | |
| ? 'high detail' | |
| : analysis.sharpness > 0.1 | |
| ? 'normal detail' | |
| : 'soft detail'; | |
| const tone = | |
| analysis.warmth > 0.05 | |
| ? 'warm color profile' | |
| : analysis.warmth < -0.05 | |
| ? 'cool color profile' | |
| : 'neutral color profile'; | |
| return `${lighting}, ${detail}, ${tone}`; | |
| } | |
Xet Storage Details
- Size:
- 6.32 kB
- Xet hash:
- 8df80b93ca835882837f468de56fde8f0aa46f847affab656be5874420585db9
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.