@visutry/tryon-core 0.2.0
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/README.md +58 -0
- package/dist/__fixtures__/faceFixtures.d.ts +13 -0
- package/dist/__fixtures__/faceFixtures.d.ts.map +1 -0
- package/dist/__fixtures__/faceFixtures.js +206 -0
- package/dist/__fixtures__/faceFixtures.js.map +1 -0
- package/dist/adapter/visutry-site.d.ts +76 -0
- package/dist/adapter/visutry-site.d.ts.map +1 -0
- package/dist/adapter/visutry-site.js +172 -0
- package/dist/adapter/visutry-site.js.map +1 -0
- package/dist/analytics.d.ts +89 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +49 -0
- package/dist/analytics.js.map +1 -0
- package/dist/coordinate/CoordinateSystem.d.ts +46 -0
- package/dist/coordinate/CoordinateSystem.d.ts.map +1 -0
- package/dist/coordinate/CoordinateSystem.js +88 -0
- package/dist/coordinate/CoordinateSystem.js.map +1 -0
- package/dist/face/FaceMetricsCalculator.d.ts +52 -0
- package/dist/face/FaceMetricsCalculator.d.ts.map +1 -0
- package/dist/face/FaceMetricsCalculator.js +375 -0
- package/dist/face/FaceMetricsCalculator.js.map +1 -0
- package/dist/face/FaceSemanticMapper.d.ts +54 -0
- package/dist/face/FaceSemanticMapper.d.ts.map +1 -0
- package/dist/face/FaceSemanticMapper.js +129 -0
- package/dist/face/FaceSemanticMapper.js.map +1 -0
- package/dist/face/FaceShapeScorer.d.ts +35 -0
- package/dist/face/FaceShapeScorer.d.ts.map +1 -0
- package/dist/face/FaceShapeScorer.js +265 -0
- package/dist/face/FaceShapeScorer.js.map +1 -0
- package/dist/face/feature-extractor.d.ts +46 -0
- package/dist/face/feature-extractor.d.ts.map +1 -0
- package/dist/face/feature-extractor.js +106 -0
- package/dist/face/feature-extractor.js.map +1 -0
- package/dist/face/logreg-classifier.d.ts +117 -0
- package/dist/face/logreg-classifier.d.ts.map +1 -0
- package/dist/face/logreg-classifier.js +304 -0
- package/dist/face/logreg-classifier.js.map +1 -0
- package/dist/feature-flag.d.ts +43 -0
- package/dist/feature-flag.d.ts.map +1 -0
- package/dist/feature-flag.js +58 -0
- package/dist/feature-flag.js.map +1 -0
- package/dist/i18n/index.d.ts +15 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +91 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest/ManifestValidator.d.ts +23 -0
- package/dist/manifest/ManifestValidator.d.ts.map +1 -0
- package/dist/manifest/ManifestValidator.js +129 -0
- package/dist/manifest/ManifestValidator.js.map +1 -0
- package/dist/pose/GlassesPoseSolver.d.ts +64 -0
- package/dist/pose/GlassesPoseSolver.d.ts.map +1 -0
- package/dist/pose/GlassesPoseSolver.js +299 -0
- package/dist/pose/GlassesPoseSolver.js.map +1 -0
- package/dist/privacy/PrivacyGuard.d.ts +44 -0
- package/dist/privacy/PrivacyGuard.d.ts.map +1 -0
- package/dist/privacy/PrivacyGuard.js +105 -0
- package/dist/privacy/PrivacyGuard.js.map +1 -0
- package/dist/quality/QualityGate.d.ts +22 -0
- package/dist/quality/QualityGate.d.ts.map +1 -0
- package/dist/quality/QualityGate.js +161 -0
- package/dist/quality/QualityGate.js.map +1 -0
- package/dist/smoothing/PoseSmoothing.d.ts +37 -0
- package/dist/smoothing/PoseSmoothing.d.ts.map +1 -0
- package/dist/smoothing/PoseSmoothing.js +146 -0
- package/dist/smoothing/PoseSmoothing.js.map +1 -0
- package/dist/types/index.d.ts +444 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/errors.d.ts +7 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +20 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/math.d.ts +40 -0
- package/dist/utils/math.d.ts.map +1 -0
- package/dist/utils/math.js +149 -0
- package/dist/utils/math.js.map +1 -0
- package/package.json +31 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { clamp, clamp01, distance3D, DEG2RAD } from "../utils/math.js";
|
|
2
|
+
import { CoordinateSystem } from "../coordinate/CoordinateSystem.js";
|
|
3
|
+
/**
|
|
4
|
+
* Calibration constant: maps millimetres to render-world units.
|
|
5
|
+
*
|
|
6
|
+
* The render-world is defined so that the full frame height maps to 1.0 unit and
|
|
7
|
+
* the x-axis is scaled by aspect ratio. We assume a representative adult face
|
|
8
|
+
* height of ~200 mm, hence 1 mm ≈ 0.005 render-world units. This is a documented
|
|
9
|
+
* calibration anchor; the manifest `defaultScale` and `GlassesFittingConfig`
|
|
10
|
+
* fine-tune per model.
|
|
11
|
+
*/
|
|
12
|
+
export const MM_TO_RENDER_WORLD = 1 / 200;
|
|
13
|
+
/** Default fitting configuration. */
|
|
14
|
+
export const DEFAULT_FITTING_CONFIG = {
|
|
15
|
+
scaleMultiplier: 1,
|
|
16
|
+
positionOffset: { x: 0, y: 0, z: 0 },
|
|
17
|
+
rotationOffset: { x: 0, y: 0, z: 0 },
|
|
18
|
+
useTransformationMatrix: false,
|
|
19
|
+
fitBy: "eyeOuterDistance",
|
|
20
|
+
verticalAnchor: "noseBridge",
|
|
21
|
+
depthStrategy: "noseTip",
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Converts a normalized `NormalizedFaceResult` into a `GlassesPose` that the
|
|
25
|
+
* renderer can apply directly to a glasses model.
|
|
26
|
+
*
|
|
27
|
+
* Coordinate contract:
|
|
28
|
+
* - Face landmarks arrive in *normalized-image* space (origin top-left, y down).
|
|
29
|
+
* - The solver converts them to *render-world* space (origin centre, y up,
|
|
30
|
+
* x scaled by aspect ratio, 1.0 unit = frame height) before computing.
|
|
31
|
+
* - The renderer must use the same render-world convention so that
|
|
32
|
+
* `GlassesPose.position` lands the model on the face.
|
|
33
|
+
*
|
|
34
|
+
* Solve pipeline (spec §15.5):
|
|
35
|
+
* 1. eye centre line → roll
|
|
36
|
+
* 2. outer-eye distance → scale
|
|
37
|
+
* 3. noseBridge / eyeLine → position
|
|
38
|
+
* 4. yaw / pitch (or matrix) → 3D rotation
|
|
39
|
+
* 5. manifest defaults + config offsets applied
|
|
40
|
+
*/
|
|
41
|
+
export class GlassesPoseSolver {
|
|
42
|
+
/**
|
|
43
|
+
* Solve the glasses pose for a single face result.
|
|
44
|
+
*/
|
|
45
|
+
solve(input) {
|
|
46
|
+
const { face, asset, config } = input;
|
|
47
|
+
const cfg = { ...DEFAULT_FITTING_CONFIG, ...config };
|
|
48
|
+
const aspect = this.deriveAspect(face);
|
|
49
|
+
const sem = face.landmarks.semantic;
|
|
50
|
+
// Visibility / confidence gate (tryon-level, spec §11.4).
|
|
51
|
+
const visibility = this.assessVisibility(face);
|
|
52
|
+
if (!visibility.visible) {
|
|
53
|
+
return {
|
|
54
|
+
position: { x: 0, y: 0, z: 0 },
|
|
55
|
+
rotation: { x: 0, y: 0, z: 0 },
|
|
56
|
+
scale: { x: asset.fitting.defaultScale, y: asset.fitting.defaultScale, z: asset.fitting.defaultScale },
|
|
57
|
+
visible: false,
|
|
58
|
+
confidence: visibility.confidence,
|
|
59
|
+
reason: visibility.reason,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// --- 1. Roll from the eye centre line --------------------------------
|
|
63
|
+
const roll = this.computeRoll(sem, aspect);
|
|
64
|
+
// --- 2. Scale from the chosen fit metric -----------------------------
|
|
65
|
+
const scale = this.computeScale(face, asset, cfg, aspect);
|
|
66
|
+
// --- 3. Position -----------------------------------------------------
|
|
67
|
+
const position = this.computePosition(sem, asset, cfg, aspect, face);
|
|
68
|
+
// --- 4. Rotation (yaw/pitch + roll) ----------------------------------
|
|
69
|
+
const rotation = this.computeRotation(face, cfg, roll);
|
|
70
|
+
// --- 5. Apply manifest defaults & config offsets ---------------------
|
|
71
|
+
const finalPosition = this.applyPositionOffsets(position, asset, cfg);
|
|
72
|
+
const finalRotation = this.applyRotationOffsets(rotation, asset, cfg);
|
|
73
|
+
const finalScale = this.clampScale(scale, asset, cfg);
|
|
74
|
+
return {
|
|
75
|
+
position: finalPosition,
|
|
76
|
+
rotation: finalRotation,
|
|
77
|
+
scale: { x: finalScale, y: finalScale, z: finalScale },
|
|
78
|
+
visible: true,
|
|
79
|
+
confidence: clamp01(visibility.confidence * 0.7 + face.pose.confidence * 0.3),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// -----------------------------------------------------------------------
|
|
83
|
+
// Roll
|
|
84
|
+
// -----------------------------------------------------------------------
|
|
85
|
+
computeRoll(sem, aspect) {
|
|
86
|
+
const le = sem.leftEyeCenter ?? sem.leftEyeOuter;
|
|
87
|
+
const re = sem.rightEyeCenter ?? sem.rightEyeOuter;
|
|
88
|
+
if (!le || !re)
|
|
89
|
+
return 0;
|
|
90
|
+
const leRW = CoordinateSystem.normalizedToRenderWorld(le, aspect);
|
|
91
|
+
const reRW = CoordinateSystem.normalizedToRenderWorld(re, aspect);
|
|
92
|
+
// Angle of the eye line in render-world (y up). Rotating the glasses by
|
|
93
|
+
// this angle around Z keeps it parallel to the eyes.
|
|
94
|
+
return Math.atan2(reRW.y - leRW.y, reRW.x - leRW.x);
|
|
95
|
+
}
|
|
96
|
+
// -----------------------------------------------------------------------
|
|
97
|
+
// Scale
|
|
98
|
+
// -----------------------------------------------------------------------
|
|
99
|
+
computeScale(face, asset, cfg, aspect) {
|
|
100
|
+
const sem = face.landmarks.semantic;
|
|
101
|
+
const fitMetric = this.fitMetricRW(sem, cfg.fitBy ?? "eyeOuterDistance", aspect);
|
|
102
|
+
const modelWidthMm = this.modelFrameWidthMm(asset);
|
|
103
|
+
const modelWidthRW = modelWidthMm * MM_TO_RENDER_WORLD;
|
|
104
|
+
if (fitMetric <= 1e-6 || modelWidthRW <= 1e-6) {
|
|
105
|
+
return asset.fitting.defaultScale;
|
|
106
|
+
}
|
|
107
|
+
const rawScale = fitMetric / modelWidthRW;
|
|
108
|
+
const scale = rawScale * asset.fitting.defaultScale * (cfg.scaleMultiplier ?? 1);
|
|
109
|
+
return scale;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The face-side width in render-world units used to fit the glasses. The model
|
|
113
|
+
* frame width should match the outer-eye span, so we use that directly.
|
|
114
|
+
*/
|
|
115
|
+
fitMetricRW(sem, fitBy, aspect) {
|
|
116
|
+
const toRW = (p) => (p ? CoordinateSystem.normalizedToRenderWorld(p, aspect) : undefined);
|
|
117
|
+
let a;
|
|
118
|
+
let b;
|
|
119
|
+
switch (fitBy) {
|
|
120
|
+
case "eyeOuterDistance":
|
|
121
|
+
a = toRW(sem.leftEyeOuter);
|
|
122
|
+
b = toRW(sem.rightEyeOuter);
|
|
123
|
+
break;
|
|
124
|
+
case "eyeCenterDistance":
|
|
125
|
+
a = toRW(sem.leftEyeCenter);
|
|
126
|
+
b = toRW(sem.rightEyeCenter);
|
|
127
|
+
break;
|
|
128
|
+
case "faceWidth":
|
|
129
|
+
a = toRW(sem.leftCheek);
|
|
130
|
+
b = toRW(sem.rightCheek);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
if (!a || !b) {
|
|
134
|
+
// Fallback to whatever eye points exist.
|
|
135
|
+
a = toRW(sem.leftEyeOuter ?? sem.leftEyeCenter);
|
|
136
|
+
b = toRW(sem.rightEyeOuter ?? sem.rightEyeCenter);
|
|
137
|
+
}
|
|
138
|
+
if (!a || !b)
|
|
139
|
+
return 0;
|
|
140
|
+
return distance3D(a, b);
|
|
141
|
+
}
|
|
142
|
+
modelFrameWidthMm(asset) {
|
|
143
|
+
const unitFactor = asset.coordinateSystem.unit === "millimeter"
|
|
144
|
+
? 1
|
|
145
|
+
: asset.coordinateSystem.unit === "centimeter"
|
|
146
|
+
? 10
|
|
147
|
+
: 1000;
|
|
148
|
+
return asset.dimensions.frameWidthMm * unitFactor;
|
|
149
|
+
}
|
|
150
|
+
// -----------------------------------------------------------------------
|
|
151
|
+
// Position
|
|
152
|
+
// -----------------------------------------------------------------------
|
|
153
|
+
computePosition(sem, _asset, cfg, aspect, face) {
|
|
154
|
+
const anchor = cfg.verticalAnchor ?? "noseBridge";
|
|
155
|
+
let baseNorm;
|
|
156
|
+
switch (anchor) {
|
|
157
|
+
case "noseBridge":
|
|
158
|
+
baseNorm = sem.noseBridge ?? sem.eyesCenter;
|
|
159
|
+
break;
|
|
160
|
+
case "eyeLine":
|
|
161
|
+
baseNorm = sem.eyesCenter;
|
|
162
|
+
break;
|
|
163
|
+
case "browLine":
|
|
164
|
+
baseNorm =
|
|
165
|
+
sem.leftBrowCenter && sem.rightBrowCenter
|
|
166
|
+
? {
|
|
167
|
+
x: (sem.leftBrowCenter.x + sem.rightBrowCenter.x) / 2,
|
|
168
|
+
y: (sem.leftBrowCenter.y + sem.rightBrowCenter.y) / 2,
|
|
169
|
+
z: (sem.leftBrowCenter.z + sem.rightBrowCenter.z) / 2,
|
|
170
|
+
}
|
|
171
|
+
: sem.eyesCenter;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
if (!baseNorm) {
|
|
175
|
+
return { x: 0, y: 0, z: 0 };
|
|
176
|
+
}
|
|
177
|
+
const rw = CoordinateSystem.normalizedToRenderWorld(baseNorm, aspect);
|
|
178
|
+
// Depth strategy.
|
|
179
|
+
let z = 0;
|
|
180
|
+
const depth = cfg.depthStrategy ?? "noseTip";
|
|
181
|
+
if (depth === "noseTip" && sem.noseTip) {
|
|
182
|
+
// The nose tip z (relative to face centre) pushes the glasses forward.
|
|
183
|
+
z = (sem.noseTip.z ?? 0) * 0.5;
|
|
184
|
+
}
|
|
185
|
+
else if (depth === "matrix" && face.pose.matrix) {
|
|
186
|
+
z = face.pose.matrix[14] ?? 0; // translation z of a column-major 4x4
|
|
187
|
+
}
|
|
188
|
+
return { x: rw.x, y: rw.y, z };
|
|
189
|
+
}
|
|
190
|
+
// -----------------------------------------------------------------------
|
|
191
|
+
// Rotation
|
|
192
|
+
// -----------------------------------------------------------------------
|
|
193
|
+
computeRotation(face, cfg, roll) {
|
|
194
|
+
if (cfg.useTransformationMatrix && face.pose.matrix) {
|
|
195
|
+
const euler = decomposeMatrixToEuler(face.pose.matrix);
|
|
196
|
+
// Replace roll with the eye-line-derived value (more stable than matrix roll).
|
|
197
|
+
return { x: euler.x, y: euler.y, z: roll };
|
|
198
|
+
}
|
|
199
|
+
return { x: face.pose.pitch, y: face.pose.yaw, z: roll };
|
|
200
|
+
}
|
|
201
|
+
// -----------------------------------------------------------------------
|
|
202
|
+
// Offsets & clamping
|
|
203
|
+
// -----------------------------------------------------------------------
|
|
204
|
+
applyPositionOffsets(position, asset, cfg) {
|
|
205
|
+
const off = asset.fitting.defaultOffset;
|
|
206
|
+
const cfgOff = cfg.positionOffset ?? { x: 0, y: 0, z: 0 };
|
|
207
|
+
return {
|
|
208
|
+
x: position.x + off.x + cfgOff.x,
|
|
209
|
+
y: position.y + off.y + cfgOff.y,
|
|
210
|
+
z: position.z + off.z + cfgOff.z,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
applyRotationOffsets(rotation, asset, cfg) {
|
|
214
|
+
const off = asset.fitting.defaultRotation;
|
|
215
|
+
const cfgOff = cfg.rotationOffset ?? { x: 0, y: 0, z: 0 };
|
|
216
|
+
return {
|
|
217
|
+
x: rotation.x + off.x + cfgOff.x,
|
|
218
|
+
y: rotation.y + off.y + cfgOff.y,
|
|
219
|
+
z: rotation.z + off.z + cfgOff.z,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
clampScale(scale, asset, cfg) {
|
|
223
|
+
const min = asset.fitting.minScale ?? 0.1;
|
|
224
|
+
const max = asset.fitting.maxScale ?? 5;
|
|
225
|
+
const mult = cfg.scaleMultiplier ?? 1;
|
|
226
|
+
return clamp(scale / (mult || 1) * mult, min, max);
|
|
227
|
+
}
|
|
228
|
+
// -----------------------------------------------------------------------
|
|
229
|
+
// Visibility
|
|
230
|
+
// -----------------------------------------------------------------------
|
|
231
|
+
assessVisibility(face) {
|
|
232
|
+
const sem = face.landmarks.semantic;
|
|
233
|
+
const hasEyesCenter = !!sem.eyesCenter;
|
|
234
|
+
const hasNose = !!sem.noseBridge || !!sem.noseTip;
|
|
235
|
+
const confidence = face.pose.confidence;
|
|
236
|
+
const bboxWidth = face.bbox.width;
|
|
237
|
+
if (!hasEyesCenter) {
|
|
238
|
+
return { visible: false, confidence, reason: "MISSING_EYES_CENTER" };
|
|
239
|
+
}
|
|
240
|
+
if (!hasNose) {
|
|
241
|
+
return { visible: false, confidence, reason: "MISSING_NOSE_REFERENCE" };
|
|
242
|
+
}
|
|
243
|
+
if (confidence < 0.55) {
|
|
244
|
+
return { visible: false, confidence, reason: "LOW_TRACKER_CONFIDENCE" };
|
|
245
|
+
}
|
|
246
|
+
if (bboxWidth < 0.18) {
|
|
247
|
+
return { visible: false, confidence, reason: "FACE_TOO_SMALL" };
|
|
248
|
+
}
|
|
249
|
+
return { visible: true, confidence };
|
|
250
|
+
}
|
|
251
|
+
// -----------------------------------------------------------------------
|
|
252
|
+
deriveAspect(face) {
|
|
253
|
+
// Without an explicit frame size we fall back to a 4:3 aspect, which is the
|
|
254
|
+
// default camera resolution. The web adapter passes the real aspect via the
|
|
255
|
+
// renderer setup; the solver stays robust to a missing value.
|
|
256
|
+
const b = face.bbox;
|
|
257
|
+
if (b.width > 0 && b.height > 0) {
|
|
258
|
+
return b.width / b.height;
|
|
259
|
+
}
|
|
260
|
+
return 4 / 3;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Decompose a column-major 4x4 rigid transformation matrix into Euler angles
|
|
265
|
+
* (radians) in YXZ order. Used when `useTransformationMatrix` is enabled.
|
|
266
|
+
*/
|
|
267
|
+
export function decomposeMatrixToEuler(matrix) {
|
|
268
|
+
// Column-major: matrix[0..3] = column 0, [4..7] = column 1, etc.
|
|
269
|
+
// Rotation sub-matrix:
|
|
270
|
+
// | m0 m4 m8 |
|
|
271
|
+
// | m1 m5 m9 |
|
|
272
|
+
// | m2 m6 m10|
|
|
273
|
+
const m0 = matrix[0] ?? 1;
|
|
274
|
+
const m1 = matrix[1] ?? 0;
|
|
275
|
+
const m2 = matrix[2] ?? 0;
|
|
276
|
+
const m5 = matrix[5] ?? 1;
|
|
277
|
+
const m6 = matrix[6] ?? 0;
|
|
278
|
+
const m9 = matrix[9] ?? 0;
|
|
279
|
+
const m10 = matrix[10] ?? 1;
|
|
280
|
+
// YXZ extraction.
|
|
281
|
+
const y = Math.asin(clamp(-m2, -1, 1));
|
|
282
|
+
let x;
|
|
283
|
+
let z;
|
|
284
|
+
if (Math.abs(m2) < 0.99999) {
|
|
285
|
+
x = Math.atan2(m6, m10);
|
|
286
|
+
z = Math.atan2(m1, m0);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
// Gimbal lock fallback.
|
|
290
|
+
x = Math.atan2(-m9, m5);
|
|
291
|
+
z = 0;
|
|
292
|
+
}
|
|
293
|
+
return { x, y, z };
|
|
294
|
+
}
|
|
295
|
+
/** Convert degrees to radians for manifest authors who think in degrees. */
|
|
296
|
+
export function degreesToRadians(deg) {
|
|
297
|
+
return deg * DEG2RAD;
|
|
298
|
+
}
|
|
299
|
+
//# sourceMappingURL=GlassesPoseSolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GlassesPoseSolver.js","sourceRoot":"","sources":["../../src/pose/GlassesPoseSolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { PrivacyConfig, SDKError } from "../types/index.js";
|
|
2
|
+
/** Safe default privacy configuration (spec §19.2). */
|
|
3
|
+
export declare const DEFAULT_PRIVACY_CONFIG: PrivacyConfig;
|
|
4
|
+
/**
|
|
5
|
+
* Enforces the SDK privacy contract.
|
|
6
|
+
*
|
|
7
|
+
* The guard is the single source of truth for what may leave the device. By
|
|
8
|
+
* default everything stays on-device; analytics is off; snapshots require
|
|
9
|
+
* explicit opt-in. The guard exposes intent-revealing methods so the rest of
|
|
10
|
+
* the SDK never has to reason about the raw config flags.
|
|
11
|
+
*/
|
|
12
|
+
export declare class PrivacyGuard {
|
|
13
|
+
private config;
|
|
14
|
+
constructor(config?: Partial<PrivacyConfig>);
|
|
15
|
+
/** Re-apply a (partial) configuration. */
|
|
16
|
+
configure(config: Partial<PrivacyConfig>): void;
|
|
17
|
+
get config_(): PrivacyConfig;
|
|
18
|
+
/** Whether image / video frames may be sent to a server. Always false in v1.0. */
|
|
19
|
+
canUploadFrames(): boolean;
|
|
20
|
+
/** Whether face landmarks may leave the device. Always false in v1.0. */
|
|
21
|
+
canUploadLandmarks(): boolean;
|
|
22
|
+
/** Whether face geometry / metrics may leave the device. Always false in v1.0. */
|
|
23
|
+
canUploadFaceGeometry(): boolean;
|
|
24
|
+
/** Whether the business layer may request a snapshot export. */
|
|
25
|
+
canExportSnapshot(): boolean;
|
|
26
|
+
/** Whether any analytics payload may be emitted. */
|
|
27
|
+
canEmitAnalytics(): boolean;
|
|
28
|
+
/** Whether performance metrics may be reported (only if analytics enabled). */
|
|
29
|
+
canReportPerformance(): boolean;
|
|
30
|
+
/** Whether diagnostic (non-image) data may be reported. */
|
|
31
|
+
canReportDiagnostics(): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Guarded executor: runs `action` only if `predicate` allows it, otherwise
|
|
34
|
+
* returns a privacy-violation error. Centralises the "ask permission" pattern.
|
|
35
|
+
*/
|
|
36
|
+
guard<T>(predicate: () => boolean, action: () => T, errorCode?: SDKError["code"], message?: string): T;
|
|
37
|
+
/**
|
|
38
|
+
* Returns a redaction summary for diagnostic logging: lists which data
|
|
39
|
+
* categories are allowed to leave the device. Never includes actual data.
|
|
40
|
+
*/
|
|
41
|
+
redactionSummary(): Record<string, boolean>;
|
|
42
|
+
private assertDefaults;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=PrivacyGuard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PrivacyGuard.d.ts","sourceRoot":"","sources":["../../src/privacy/PrivacyGuard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAEjE,uDAAuD;AACvD,eAAO,MAAM,sBAAsB,EAAE,aAKpC,CAAC;AAEF;;;;;;;GAOG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAgB;gBAElB,MAAM,GAAE,OAAO,CAAC,aAAa,CAAM;IAK/C,0CAA0C;IAC1C,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI;IAK/C,IAAI,OAAO,IAAI,aAAa,CAE3B;IAED,kFAAkF;IAClF,eAAe,IAAI,OAAO;IAK1B,yEAAyE;IACzE,kBAAkB,IAAI,OAAO;IAI7B,kFAAkF;IAClF,qBAAqB,IAAI,OAAO;IAIhC,gEAAgE;IAChE,iBAAiB,IAAI,OAAO;IAI5B,oDAAoD;IACpD,gBAAgB,IAAI,OAAO;IAI3B,+EAA+E;IAC/E,oBAAoB,IAAI,OAAO;IAM/B,2DAA2D;IAC3D,oBAAoB,IAAI,OAAO;IAK/B;;;OAGG;IACH,KAAK,CAAC,CAAC,EACL,SAAS,EAAE,MAAM,OAAO,EACxB,MAAM,EAAE,MAAM,CAAC,EACf,SAAS,GAAE,QAAQ,CAAC,MAAM,CAAa,EACvC,OAAO,SAAqC,GAC3C,CAAC;IAYJ;;;OAGG;IACH,gBAAgB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAW3C,OAAO,CAAC,cAAc;CAMvB"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** Safe default privacy configuration (spec §19.2). */
|
|
2
|
+
export const DEFAULT_PRIVACY_CONFIG = {
|
|
3
|
+
processOnDeviceOnly: true,
|
|
4
|
+
allowSnapshotExport: true,
|
|
5
|
+
allowAnalytics: false,
|
|
6
|
+
analyticsLevel: "none",
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Enforces the SDK privacy contract.
|
|
10
|
+
*
|
|
11
|
+
* The guard is the single source of truth for what may leave the device. By
|
|
12
|
+
* default everything stays on-device; analytics is off; snapshots require
|
|
13
|
+
* explicit opt-in. The guard exposes intent-revealing methods so the rest of
|
|
14
|
+
* the SDK never has to reason about the raw config flags.
|
|
15
|
+
*/
|
|
16
|
+
export class PrivacyGuard {
|
|
17
|
+
constructor(config = {}) {
|
|
18
|
+
Object.defineProperty(this, "config", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
configurable: true,
|
|
21
|
+
writable: true,
|
|
22
|
+
value: void 0
|
|
23
|
+
});
|
|
24
|
+
this.config = { ...DEFAULT_PRIVACY_CONFIG, ...config };
|
|
25
|
+
this.assertDefaults();
|
|
26
|
+
}
|
|
27
|
+
/** Re-apply a (partial) configuration. */
|
|
28
|
+
configure(config) {
|
|
29
|
+
this.config = { ...this.config, ...config };
|
|
30
|
+
this.assertDefaults();
|
|
31
|
+
}
|
|
32
|
+
get config_() {
|
|
33
|
+
return { ...this.config };
|
|
34
|
+
}
|
|
35
|
+
/** Whether image / video frames may be sent to a server. Always false in v1.0. */
|
|
36
|
+
canUploadFrames() {
|
|
37
|
+
// On-device-only is the hard contract; uploads are never permitted.
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
/** Whether face landmarks may leave the device. Always false in v1.0. */
|
|
41
|
+
canUploadLandmarks() {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
/** Whether face geometry / metrics may leave the device. Always false in v1.0. */
|
|
45
|
+
canUploadFaceGeometry() {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
/** Whether the business layer may request a snapshot export. */
|
|
49
|
+
canExportSnapshot() {
|
|
50
|
+
return this.config.allowSnapshotExport ?? false;
|
|
51
|
+
}
|
|
52
|
+
/** Whether any analytics payload may be emitted. */
|
|
53
|
+
canEmitAnalytics() {
|
|
54
|
+
return this.config.allowAnalytics === true && this.config.analyticsLevel !== "none";
|
|
55
|
+
}
|
|
56
|
+
/** Whether performance metrics may be reported (only if analytics enabled). */
|
|
57
|
+
canReportPerformance() {
|
|
58
|
+
if (!this.canEmitAnalytics())
|
|
59
|
+
return false;
|
|
60
|
+
const level = this.config.analyticsLevel ?? "none";
|
|
61
|
+
return level === "performance" || level === "diagnostic";
|
|
62
|
+
}
|
|
63
|
+
/** Whether diagnostic (non-image) data may be reported. */
|
|
64
|
+
canReportDiagnostics() {
|
|
65
|
+
if (!this.canEmitAnalytics())
|
|
66
|
+
return false;
|
|
67
|
+
return this.config.analyticsLevel === "diagnostic";
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Guarded executor: runs `action` only if `predicate` allows it, otherwise
|
|
71
|
+
* returns a privacy-violation error. Centralises the "ask permission" pattern.
|
|
72
|
+
*/
|
|
73
|
+
guard(predicate, action, errorCode = "UNKNOWN", message = "Action blocked by privacy policy") {
|
|
74
|
+
if (!predicate()) {
|
|
75
|
+
const error = {
|
|
76
|
+
code: errorCode,
|
|
77
|
+
message,
|
|
78
|
+
recoverable: false,
|
|
79
|
+
};
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
return action();
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Returns a redaction summary for diagnostic logging: lists which data
|
|
86
|
+
* categories are allowed to leave the device. Never includes actual data.
|
|
87
|
+
*/
|
|
88
|
+
redactionSummary() {
|
|
89
|
+
return {
|
|
90
|
+
frames: this.canUploadFrames(),
|
|
91
|
+
landmarks: this.canUploadLandmarks(),
|
|
92
|
+
faceGeometry: this.canUploadFaceGeometry(),
|
|
93
|
+
snapshot: this.canExportSnapshot(),
|
|
94
|
+
performance: this.canReportPerformance(),
|
|
95
|
+
diagnostics: this.canReportDiagnostics(),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
assertDefaults() {
|
|
99
|
+
// The on-device-only flag is a hard requirement; it cannot be disabled.
|
|
100
|
+
if (!this.config.processOnDeviceOnly) {
|
|
101
|
+
this.config.processOnDeviceOnly = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=PrivacyGuard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PrivacyGuard.js","sourceRoot":"","sources":["../../src/privacy/PrivacyGuard.ts"],"names":[],"mappings":"AAEA,uDAAuD;AACvD,MAAM,CAAC,MAAM,sBAAsB,GAAkB;IACnD,mBAAmB,EAAE,IAAI;IACzB,mBAAmB,EAAE,IAAI;IACzB,cAAc,EAAE,KAAK;IACrB,cAAc,EAAE,MAAM;CACvB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,OAAO,YAAY;IAGvB,YAAY,SAAiC,EAAE;QAFvC;;;;;WAAsB;QAG5B,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,sBAAsB,EAAE,GAAG,MAAM,EAAE,CAAC;QACvD,IAAI,CAAC,cAAc,EAAE,CAAC;IACxB,CAAC;IAED,0CAA0C;IAC1C,SAAS,CAAC,MAA8B;QACtC,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC;QAC5C,IAAI,CAAC,cAAc,EAAE,CAAC;IACxB,CAAC;IAED,IAAI,OAAO;QACT,OAAO,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;IAC5B,CAAC;IAED,kFAAkF;IAClF,eAAe;QACb,oEAAoE;QACpE,OAAO,KAAK,CAAC;IACf,CAAC;IAED,yEAAyE;IACzE,kBAAkB;QAChB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,kFAAkF;IAClF,qBAAqB;QACnB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,gEAAgE;IAChE,iBAAiB;QACf,OAAO,IAAI,CAAC,MAAM,CAAC,mBAAmB,IAAI,KAAK,CAAC;IAClD,CAAC;IAED,oDAAoD;IACpD,gBAAgB;QACd,OAAO,IAAI,CAAC,MAAM,CAAC,cAAc,KAAK,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,cAAc,KAAK,MAAM,CAAC;IACtF,CAAC;IAED,+EAA+E;IAC/E,oBAAoB;QAClB,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE;YAAE,OAAO,KAAK,CAAC;QAC3C,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,MAAM,CAAC;QACnD,OAAO,KAAK,KAAK,aAAa,IAAI,KAAK,KAAK,YAAY,CAAC;IAC3D,CAAC;IAED,2DAA2D;IAC3D,oBAAoB;QAClB,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE;YAAE,OAAO,KAAK,CAAC;QAC3C,OAAO,IAAI,CAAC,MAAM,CAAC,cAAc,KAAK,YAAY,CAAC;IACrD,CAAC;IAED;;;OAGG;IACH,KAAK,CACH,SAAwB,EACxB,MAAe,EACf,YAA8B,SAAS,EACvC,OAAO,GAAG,kCAAkC;QAE5C,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC;YACjB,MAAM,KAAK,GAAa;gBACtB,IAAI,EAAE,SAAS;gBACf,OAAO;gBACP,WAAW,EAAE,KAAK;aACnB,CAAC;YACF,MAAM,KAAK,CAAC;QACd,CAAC;QACD,OAAO,MAAM,EAAE,CAAC;IAClB,CAAC;IAED;;;OAGG;IACH,gBAAgB;QACd,OAAO;YACL,MAAM,EAAE,IAAI,CAAC,eAAe,EAAE;YAC9B,SAAS,EAAE,IAAI,CAAC,kBAAkB,EAAE;YACpC,YAAY,EAAE,IAAI,CAAC,qBAAqB,EAAE;YAC1C,QAAQ,EAAE,IAAI,CAAC,iBAAiB,EAAE;YAClC,WAAW,EAAE,IAAI,CAAC,oBAAoB,EAAE;YACxC,WAAW,EAAE,IAAI,CAAC,oBAAoB,EAAE;SACzC,CAAC;IACJ,CAAC;IAEO,cAAc;QACpB,wEAAwE;QACxE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,mBAAmB,EAAE,CAAC;YACrC,IAAI,CAAC,MAAM,CAAC,mBAAmB,GAAG,IAAI,CAAC;QACzC,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { QualityGateInput, QualityGateResult } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Decides whether a face result is good enough for analysis, try-on, or
|
|
4
|
+
* snapshot, emitting structured warnings when it is not (spec §11).
|
|
5
|
+
*
|
|
6
|
+
* The gate is stateless and pure: given the same input it always returns the
|
|
7
|
+
* same verdict, which makes it trivially testable.
|
|
8
|
+
*/
|
|
9
|
+
export declare class QualityGate {
|
|
10
|
+
evaluate(input: QualityGateInput): QualityGateResult;
|
|
11
|
+
/**
|
|
12
|
+
* Photo quality checks adapted from visutry: eye line tilt, facial symmetry,
|
|
13
|
+
* and face span. These help reject poor-quality selfies before analysis.
|
|
14
|
+
*/
|
|
15
|
+
private checkPhotoQuality;
|
|
16
|
+
/**
|
|
17
|
+
* Composite quality score in [0,1] blending confidence, frontality,
|
|
18
|
+
* stability, lighting and occlusion (when available).
|
|
19
|
+
*/
|
|
20
|
+
private computeScore;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=QualityGate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"QualityGate.d.ts","sourceRoot":"","sources":["../../src/quality/QualityGate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIV,gBAAgB,EAEhB,iBAAiB,EAClB,MAAM,mBAAmB,CAAC;AA2C3B;;;;;;GAMG;AACH,qBAAa,WAAW;IACtB,QAAQ,CAAC,KAAK,EAAE,gBAAgB,GAAG,iBAAiB;IAsEpD;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IA2CzB;;;OAGG;IACH,OAAO,CAAC,YAAY;CAUrB"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { FaceSemanticMapper } from "../face/FaceSemanticMapper.js";
|
|
2
|
+
import { clamp01 } from "../utils/math.js";
|
|
3
|
+
const THRESHOLDS = {
|
|
4
|
+
analysis: {
|
|
5
|
+
minConfidence: 0.75,
|
|
6
|
+
minFrontalScore: 0.75,
|
|
7
|
+
minStabilityScore: 0.7,
|
|
8
|
+
minBboxWidth: 0.25,
|
|
9
|
+
requiredSemanticPoints: [
|
|
10
|
+
"leftEyeCenter",
|
|
11
|
+
"rightEyeCenter",
|
|
12
|
+
"noseBridge",
|
|
13
|
+
"chin",
|
|
14
|
+
"leftCheek",
|
|
15
|
+
"rightCheek",
|
|
16
|
+
"leftJaw",
|
|
17
|
+
"rightJaw",
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
tryon: {
|
|
21
|
+
minConfidence: 0.55,
|
|
22
|
+
minBboxWidth: 0.18,
|
|
23
|
+
requiredSemanticPoints: ["eyesCenter"],
|
|
24
|
+
anyOfSemanticPoints: [["noseBridge", "noseTip"]],
|
|
25
|
+
},
|
|
26
|
+
snapshot: {
|
|
27
|
+
minConfidence: 0.6,
|
|
28
|
+
minBboxWidth: 0.1,
|
|
29
|
+
requiredSemanticPoints: ["eyesCenter"],
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Decides whether a face result is good enough for analysis, try-on, or
|
|
34
|
+
* snapshot, emitting structured warnings when it is not (spec §11).
|
|
35
|
+
*
|
|
36
|
+
* The gate is stateless and pure: given the same input it always returns the
|
|
37
|
+
* same verdict, which makes it trivially testable.
|
|
38
|
+
*/
|
|
39
|
+
export class QualityGate {
|
|
40
|
+
evaluate(input) {
|
|
41
|
+
const { face, mode } = input;
|
|
42
|
+
const thresholds = THRESHOLDS[mode];
|
|
43
|
+
const warnings = [];
|
|
44
|
+
const sem = face.landmarks.semantic;
|
|
45
|
+
// --- Confidence -------------------------------------------------------
|
|
46
|
+
if (face.quality.confidence < thresholds.minConfidence) {
|
|
47
|
+
warnings.push("LOW_CONFIDENCE");
|
|
48
|
+
}
|
|
49
|
+
// --- Frontality -------------------------------------------------------
|
|
50
|
+
if (thresholds.minFrontalScore !== undefined && face.quality.frontalScore < thresholds.minFrontalScore) {
|
|
51
|
+
warnings.push("NOT_FRONTAL");
|
|
52
|
+
}
|
|
53
|
+
// --- Stability --------------------------------------------------------
|
|
54
|
+
if (thresholds.minStabilityScore !== undefined && face.quality.stabilityScore < thresholds.minStabilityScore) {
|
|
55
|
+
warnings.push("UNSTABLE");
|
|
56
|
+
}
|
|
57
|
+
// --- Bounding box size ------------------------------------------------
|
|
58
|
+
if (face.bbox.width < thresholds.minBboxWidth) {
|
|
59
|
+
warnings.push("FACE_TOO_SMALL");
|
|
60
|
+
}
|
|
61
|
+
if (face.bbox.width > 0.7) {
|
|
62
|
+
warnings.push("FACE_TOO_CLOSE");
|
|
63
|
+
}
|
|
64
|
+
// --- Semantic point presence -----------------------------------------
|
|
65
|
+
if (thresholds.requiredSemanticPoints) {
|
|
66
|
+
const missing = FaceSemanticMapper.countMissing(sem, thresholds.requiredSemanticPoints).missing;
|
|
67
|
+
if (missing.length > 0)
|
|
68
|
+
warnings.push("MISSING_KEY_POINTS");
|
|
69
|
+
}
|
|
70
|
+
if (thresholds.anyOfSemanticPoints) {
|
|
71
|
+
for (const group of thresholds.anyOfSemanticPoints) {
|
|
72
|
+
const any = group.some((k) => sem[k]);
|
|
73
|
+
if (!any) {
|
|
74
|
+
if (!warnings.includes("MISSING_KEY_POINTS"))
|
|
75
|
+
warnings.push("MISSING_KEY_POINTS");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// --- Lighting / occlusion passthrough --------------------------------
|
|
80
|
+
if (face.quality.lightingScore !== undefined && face.quality.lightingScore < 0.4) {
|
|
81
|
+
warnings.push("LOW_LIGHT");
|
|
82
|
+
}
|
|
83
|
+
if (face.quality.occlusionScore !== undefined && face.quality.occlusionScore < 0.4) {
|
|
84
|
+
warnings.push("OCCLUDED");
|
|
85
|
+
}
|
|
86
|
+
// --- visutry additions: photo quality checks (analysis mode only) ----
|
|
87
|
+
if (mode === "analysis") {
|
|
88
|
+
const photoWarnings = this.checkPhotoQuality(sem);
|
|
89
|
+
for (const w of photoWarnings) {
|
|
90
|
+
if (!warnings.includes(w))
|
|
91
|
+
warnings.push(w);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// --- Snapshot-specific: faceVisible ----------------------------------
|
|
95
|
+
if (mode === "snapshot" && !face.quality.faceVisible) {
|
|
96
|
+
warnings.push("LOW_CONFIDENCE");
|
|
97
|
+
}
|
|
98
|
+
const score = this.computeScore(face, mode);
|
|
99
|
+
const passed = warnings.length === 0 && face.quality.faceVisible;
|
|
100
|
+
return { passed, score, warnings };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Photo quality checks adapted from visutry: eye line tilt, facial symmetry,
|
|
104
|
+
* and face span. These help reject poor-quality selfies before analysis.
|
|
105
|
+
*/
|
|
106
|
+
checkPhotoQuality(sem) {
|
|
107
|
+
const warnings = [];
|
|
108
|
+
const MAX_TILT_DEG = 15;
|
|
109
|
+
const MAX_SYMMETRY_OFFSET = 0.14;
|
|
110
|
+
const MIN_FACE_SPAN = 0.16;
|
|
111
|
+
// Eye line tilt
|
|
112
|
+
if (sem.leftEyeOuter && sem.rightEyeOuter) {
|
|
113
|
+
const dx = sem.rightEyeOuter.x - sem.leftEyeOuter.x;
|
|
114
|
+
const dy = sem.rightEyeOuter.y - sem.leftEyeOuter.y;
|
|
115
|
+
if (Math.abs(dx) > 1e-6) {
|
|
116
|
+
const tiltDeg = Math.abs(Math.atan2(dy, dx) * (180 / Math.PI));
|
|
117
|
+
if (tiltDeg > MAX_TILT_DEG) {
|
|
118
|
+
warnings.push("EXCESSIVE_TILT");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Symmetry offset: nose bridge deviation from face center
|
|
123
|
+
if (sem.noseBridge && sem.leftFace && sem.rightFace) {
|
|
124
|
+
const faceWidth = Math.abs(sem.rightFace.x - sem.leftFace.x);
|
|
125
|
+
if (faceWidth > 1e-6) {
|
|
126
|
+
const faceCenterX = (sem.leftFace.x + sem.rightFace.x) / 2;
|
|
127
|
+
const offset = Math.abs(sem.noseBridge.x - faceCenterX) / faceWidth;
|
|
128
|
+
if (offset > MAX_SYMMETRY_OFFSET) {
|
|
129
|
+
warnings.push("ASYMMETRIC_FACE");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Face span: bounding box max dimension
|
|
134
|
+
if (sem.leftFace && sem.rightFace && sem.foreheadCenter && sem.chin) {
|
|
135
|
+
const w = Math.abs(sem.rightFace.x - sem.leftFace.x);
|
|
136
|
+
const h = Math.abs(sem.chin.y - sem.foreheadCenter.y);
|
|
137
|
+
const span = Math.max(w, h);
|
|
138
|
+
if (span < MIN_FACE_SPAN) {
|
|
139
|
+
warnings.push("FACE_TOO_SMALL");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return warnings;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Composite quality score in [0,1] blending confidence, frontality,
|
|
146
|
+
* stability, lighting and occlusion (when available).
|
|
147
|
+
*/
|
|
148
|
+
computeScore(face, mode) {
|
|
149
|
+
const q = face.quality;
|
|
150
|
+
let score = q.confidence * 0.4 + q.frontalScore * 0.3 + q.stabilityScore * 0.3;
|
|
151
|
+
if (q.lightingScore !== undefined)
|
|
152
|
+
score = score * 0.8 + q.lightingScore * 0.2;
|
|
153
|
+
if (q.occlusionScore !== undefined)
|
|
154
|
+
score = score * 0.9 + q.occlusionScore * 0.1;
|
|
155
|
+
// Mode weighting: analysis is stricter.
|
|
156
|
+
if (mode === "analysis")
|
|
157
|
+
score *= 0.95;
|
|
158
|
+
return clamp01(score);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=QualityGate.js.map
|