174 lines
11 KiB
JavaScript
174 lines
11 KiB
JavaScript
// Taobao ar-camera contract: official component docType=20/docId=1484.
|
|
// mode=faceDetection2D; onARFrame receives e.detail.{width,height,anchors}.
|
|
// rect=[x1,y1,x2,y2], landmarks=[x0,y0,...x105,y105] are in frame pixels.
|
|
// Native yaw/pitch/roll units are unspecified: use the same 68-point geometry
|
|
// and capture gates as H5 instead. No network, bitmap decoding or photo storage.
|
|
var HeadPose = require('../vendor/head-pose');
|
|
var CaptureQuality = require('../vendor/capture-quality');
|
|
|
|
// Official 106-point diagram: contour 0..32, brows/nose/eyes 33..63,
|
|
// lips 84..103; 104/105 are additional eye centers, not mouth points.
|
|
var POINTS_68 = [];
|
|
for (var jaw = 0; jaw <= 32; jaw += 2) POINTS_68.push(jaw);
|
|
for (var feature = 33; feature <= 63; feature++) POINTS_68.push(feature);
|
|
for (var lip = 84; lip <= 103; lip++) POINTS_68.push(lip);
|
|
|
|
function finite(value) { return typeof value === 'number' && isFinite(value); }
|
|
function frameOf(event) { return event && event.detail ? event.detail : event; }
|
|
function points68(flat) {
|
|
if (!flat || typeof flat.length !== 'number' || flat.length < 212) return null;
|
|
var points = POINTS_68.map(function (index) { return { x: flat[index * 2], y: flat[index * 2 + 1] }; });
|
|
return points.every(function (point) { return finite(point.x) && finite(point.y); }) ? points : null;
|
|
}
|
|
function normalize(event) {
|
|
var frame = frameOf(event);
|
|
if (!frame || !finite(frame.width) || !finite(frame.height) || frame.width <= 0 || frame.height <= 0 || !Array.isArray(frame.anchors)) return null;
|
|
return { width: frame.width, height: frame.height, count: frame.anchors.length,
|
|
detections: frame.anchors.map(function (anchor) {
|
|
if (!anchor || !Array.isArray(anchor.rect) || anchor.rect.length !== 4 || !anchor.rect.every(finite)) return null;
|
|
var positions = points68(anchor.landmarks), r = anchor.rect;
|
|
if (!positions || r[2] <= r[0] || r[3] <= r[1]) return null;
|
|
return { detection: { box: { x: r[0], y: r[1], width: r[2] - r[0], height: r[3] - r[1] },
|
|
// Native anchors expose no confidence. This is an explicit native
|
|
// detection marker, not a measured face-api confidence value.
|
|
score: 1 }, nativeAnchor: true, landmarks: { positions: positions } };
|
|
}) };
|
|
}
|
|
function evaluate(event, target, mirrored, options) {
|
|
options = options || {};
|
|
var frame = normalize(event);
|
|
if (!frame) return { ok: false, reason: 'frame', hint: '等待相机真实画面…', frameReceived: false };
|
|
var detection = frame.detections[0], offsetX = 0;
|
|
if (detection) {
|
|
var box = detection.detection.box;
|
|
// Match H5 object-fit:cover framing when viewport dimensions are known.
|
|
var scale = finite(options.viewWidth) && options.viewWidth > 0 && finite(options.viewHeight) && options.viewHeight > 0
|
|
? Math.max(options.viewWidth / frame.width, options.viewHeight / frame.height) / options.viewWidth : 1 / frame.width;
|
|
offsetX = (box.x + box.width / 2 - frame.width / 2) * scale;
|
|
}
|
|
var verdict = CaptureQuality.evaluate(detection, { count: frame.count, target: target,
|
|
mirrored: !!mirrored, width: frame.width, height: frame.height, offsetX: offsetX,
|
|
luma: finite(options.luma) ? options.luma : null });
|
|
// Do not attach native buffers/landmarks to verdict: callers may setData it.
|
|
verdict.frameReceived = true; verdict.faceCount = frame.count;
|
|
verdict.width = frame.width; verdict.height = frame.height;
|
|
return verdict;
|
|
}
|
|
function capability(platform) {
|
|
var signal = 'unknown';
|
|
if (platform && typeof platform.canIUse === 'function') {
|
|
try { signal = platform.canIUse('ar-camera') ? 'yes' : 'no'; } catch (_) {}
|
|
}
|
|
return { componentSignal: signal, contextFactory: !!(platform && typeof platform.createArCameraContext === 'function'), frameVerified: false };
|
|
}
|
|
function context(platform, id) {
|
|
if (!platform || typeof platform.createArCameraContext !== 'function') throw new Error('当前淘宝版本未提供 AR 相机上下文');
|
|
return platform.createArCameraContext(id);
|
|
}
|
|
function inspect(event) {
|
|
var frame = frameOf(event), fields = [], seen = [];
|
|
function visit(value, path, depth) {
|
|
if (value && typeof value === 'object' && finite(value.byteLength)) {
|
|
fields.push(path + ': ' + Object.prototype.toString.call(value).slice(8, -1) + '(' + value.byteLength + ' bytes)'); return;
|
|
}
|
|
if (Array.isArray(value)) { fields.push(path + ': Array(' + value.length + ')'); return; }
|
|
if (value && typeof value === 'object' && depth < 2 && seen.indexOf(value) < 0) {
|
|
seen.push(value); Object.keys(value).slice(0, 30).forEach(function (key) { visit(value[key], path ? path + '.' + key : key, depth + 1); }); return;
|
|
}
|
|
if (/(^|\.)(pixelFormat|format|width|height|stride|bytesPerRow)$/.test(path) && (typeof value === 'string' || finite(value))) {
|
|
fields.push(path + ': ' + String(value).slice(0, 48)); return;
|
|
}
|
|
fields.push(path + ': ' + (typeof value === 'string' ? 'String(' + value.length + ')' : typeof value));
|
|
}
|
|
if (frame && typeof frame === 'object') visit(frame, '', 0);
|
|
return fields;
|
|
}
|
|
|
|
// Diagnostics use data descriptors only: inspecting a context/event must not
|
|
// invoke native getters, stringify native objects, or reveal biometric values.
|
|
function diagnosticValue(object, key) {
|
|
var current = object, seen = [];
|
|
for (var depth = 0; current && depth < 6 && seen.indexOf(current) < 0; depth++) {
|
|
seen.push(current);
|
|
try {
|
|
var descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
if (descriptor) return Object.prototype.hasOwnProperty.call(descriptor, 'value') ? descriptor.value : undefined;
|
|
current = Object.getPrototypeOf(current);
|
|
} catch (_) { return undefined; }
|
|
}
|
|
}
|
|
function diagnosticMethods(object) {
|
|
var current = object, names = [], seen = [], keys = [], hidden = ['constructor', 'toString', 'toLocaleString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable'];
|
|
for (var depth = 0; current && depth < 6 && seen.indexOf(current) < 0; depth++) {
|
|
seen.push(current);
|
|
try {
|
|
Object.getOwnPropertyNames(current).slice(0, 200).forEach(function (name) {
|
|
if (!/^[A-Za-z][A-Za-z0-9_$]{0,79}$/.test(name) || hidden.indexOf(name) >= 0 || keys.indexOf(name) >= 0) return;
|
|
keys.push(name);
|
|
var descriptor = Object.getOwnPropertyDescriptor(current, name);
|
|
if (descriptor && typeof descriptor.value === 'function') names.push(name);
|
|
});
|
|
current = Object.getPrototypeOf(current);
|
|
} catch (_) { break; }
|
|
}
|
|
return names.sort().slice(0, 100);
|
|
}
|
|
function diagnosticText(value) {
|
|
if (typeof value !== 'string' || !value || value.length > 120 || /data:|base64|https?:\/\/|bearer\s/i.test(value)) return null;
|
|
return value.replace(/[\r\n\t]/g, ' ').slice(0, 100);
|
|
}
|
|
function diagnosticSystem(platform) {
|
|
var fields = [], info, getInfo = diagnosticValue(platform, 'getSystemInfoSync');
|
|
if (typeof getInfo === 'function') {
|
|
try { info = getInfo.call(platform); } catch (_) { fields.push('getSystemInfoSync=failed'); }
|
|
} else fields.push('getSystemInfoSync=unavailable');
|
|
['brand', 'model', 'system', 'platform', 'app', 'appName', 'version', 'appVersion', 'clientVersion', 'SDKVersion', 'appxVersion', 'appXVersion', 'AppXVersion'].forEach(function (key) {
|
|
var value = diagnosticValue(info, key), text = diagnosticText(value);
|
|
if (text !== null) fields.push('system.' + key + '=' + text);
|
|
});
|
|
['SDKVersion', 'version', 'appVersion', 'appxVersion', 'appXVersion', 'AppXVersion'].forEach(function (key) {
|
|
var text = diagnosticText(diagnosticValue(platform, key));
|
|
if (text !== null) fields.push('runtime.' + key + '=' + text);
|
|
});
|
|
return fields;
|
|
}
|
|
function diagnosticToolbarTop(platform) {
|
|
var info, getInfo = diagnosticValue(platform, 'getSystemInfoSync');
|
|
try { if (typeof getInfo === 'function') info = getInfo.call(platform); } catch (_) {}
|
|
function inset(value) { return finite(value) && value >= 0 && value <= 200 ? value : 0; }
|
|
var status = inset(diagnosticValue(info, 'statusBarHeight'));
|
|
var safeTop = inset(diagnosticValue(diagnosticValue(info, 'safeArea'), 'top'));
|
|
var title = inset(diagnosticValue(info, 'titleBarHeight')) || 44;
|
|
// Some hosts extend fixed content under both native bars. Keep a generous
|
|
// fallback even when system geometry is missing, with the page body following.
|
|
return Math.max(112, Math.max(status, safeTop) + title + 12);
|
|
}
|
|
function diagnosticEvent(event) {
|
|
var metadata = [], seen = [], type = diagnosticText(diagnosticValue(event, 'type'));
|
|
if (!type || !/^[A-Za-z][A-Za-z0-9_.:-]{0,63}$/.test(type)) type = null;
|
|
var numeric = ['width', 'height', 'imageWidth', 'imageHeight', 'cameraWidth', 'cameraHeight', 'stride', 'bytesPerRow', 'rotation', 'timestamp', 'timeStamp', 'error', 'errorCode', 'code'];
|
|
var facing = ['devicePosition', 'cameraPosition', 'cameraFacing', 'facing', 'facingMode', 'position', 'isFrontCamera', 'frontCamera', 'mirrored', 'isMirrored'];
|
|
var formats = ['pixelFormat', 'format', 'orientation', 'mode', 'trackMode'];
|
|
var containers = ['detail', 'frame', 'data', 'camera', 'cameraInfo', 'image', 'capturedImage', 'result', 'metadata'];
|
|
function visit(value, path, depth) {
|
|
if (!value || typeof value !== 'object' || seen.indexOf(value) >= 0 || depth > 3) return;
|
|
seen.push(value);
|
|
numeric.concat(facing, formats).forEach(function (key) {
|
|
var scalar = diagnosticValue(value, key), allowed = finite(scalar) || typeof scalar === 'boolean';
|
|
if (typeof scalar === 'string' && facing.indexOf(key) >= 0) allowed = /^(front|back|rear|user|environment|unknown|auto|0|1|2)$/i.test(scalar);
|
|
if (typeof scalar === 'string' && formats.indexOf(key) >= 0) allowed = /^(RGBA|BGRA|ARGB|ABGR|RGB|RGB24|RGBA8888|BGRA8888|ARGB8888|ABGR8888|NV12|NV21|YUV|YUV420|YUV420P|JPEG|JPG|PNG|portrait|landscape|landscapeLeft|landscapeRight|portraitUpsideDown|faceDetection2D|faceTracking3D|imageTracking|worldTracking)$/i.test(scalar);
|
|
if (allowed) metadata.push((path ? path + '.' : '') + key + '=' + String(scalar));
|
|
});
|
|
['anchors', 'faces'].forEach(function (key) {
|
|
var list = diagnosticValue(value, key);
|
|
if (Array.isArray(list)) metadata.push((path ? path + '.' : '') + key + '.count=' + list.length);
|
|
});
|
|
if (depth < 3) containers.forEach(function (key) { visit(diagnosticValue(value, key), path ? path + '.' + key : key, depth + 1); });
|
|
}
|
|
visit(event, '', 0);
|
|
return { type: type, metadata: metadata.slice(0, 80) };
|
|
}
|
|
module.exports = { normalize: normalize, points68: points68, evaluate: evaluate, hold: CaptureQuality.hold,
|
|
config: CaptureQuality.config, poseConfig: HeadPose.config, capability: capability, context: context, inspect: inspect,
|
|
diagnostics: { methods: diagnosticMethods, system: diagnosticSystem, toolbarTop: diagnosticToolbarTop, event: diagnosticEvent } };
|