@mostajs/face 1.0.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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/hooks/useCamera.d.ts +17 -0
- package/dist/hooks/useCamera.js +78 -0
- package/dist/hooks/useFaceDetection.d.ts +21 -0
- package/dist/hooks/useFaceDetection.js +97 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +28 -0
- package/dist/lib/face-api.d.ts +23 -0
- package/dist/lib/face-api.js +107 -0
- package/dist/lib/face-matcher.d.ts +18 -0
- package/dist/lib/face-matcher.js +57 -0
- package/dist/lib/face-utils.d.ts +20 -0
- package/dist/lib/face-utils.js +45 -0
- package/dist/types/index.d.ts +27 -0
- package/dist/types/index.js +4 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @mostajs/face
|
|
2
|
+
|
|
3
|
+
> Reusable face recognition module — detection, descriptor extraction, 1:N matching.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@mostajs/face)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Part of the [@mosta suite](https://mostajs.dev).
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @mostajs/face
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
### 1. Load models and detect faces
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { loadModels, detectFace, extractDescriptor } from '@mostajs/face'
|
|
24
|
+
|
|
25
|
+
await loadModels('/models') // path to face-api.js model files
|
|
26
|
+
|
|
27
|
+
const detection = await detectFace(imageElement)
|
|
28
|
+
const descriptor = await extractDescriptor(imageElement)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 2. Compare faces
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { compareFaces, findMatch } from '@mostajs/face'
|
|
35
|
+
|
|
36
|
+
const distance = compareFaces(descriptor1, descriptor2)
|
|
37
|
+
|
|
38
|
+
const match = findMatch(unknownDescriptor, knownFaces, 0.6)
|
|
39
|
+
if (match) {
|
|
40
|
+
console.log('Matched:', match.label, 'distance:', match.distance)
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 3. React hooks
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { useCamera } from '@mostajs/face/hooks/useCamera'
|
|
48
|
+
import { useFaceDetection } from '@mostajs/face/hooks/useFaceDetection'
|
|
49
|
+
|
|
50
|
+
function FaceCapture() {
|
|
51
|
+
const { videoRef, start, stop } = useCamera()
|
|
52
|
+
const { detection, descriptor } = useFaceDetection(videoRef)
|
|
53
|
+
// ...
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## API Reference
|
|
58
|
+
|
|
59
|
+
| Export | Description |
|
|
60
|
+
|--------|-------------|
|
|
61
|
+
| `loadModels(path)` | Load face-api.js models |
|
|
62
|
+
| `detectFace(input)` | Detect single face with landmarks |
|
|
63
|
+
| `detectAllFaces(input)` | Detect all faces |
|
|
64
|
+
| `extractDescriptor(input)` | Extract 128-dim face descriptor |
|
|
65
|
+
| `compareFaces(d1, d2)` | Euclidean distance between descriptors |
|
|
66
|
+
| `findMatch(descriptor, known, threshold)` | Find best match |
|
|
67
|
+
| `findAllMatches(descriptor, known, threshold)` | Find all matches |
|
|
68
|
+
| `useCamera()` | Camera management hook |
|
|
69
|
+
| `useFaceDetection()` | Continuous detection hook |
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT — © 2025 Dr Hamid MADANI <drmdh@msn.com>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type CameraStatus = 'idle' | 'requesting' | 'active' | 'denied' | 'error';
|
|
2
|
+
interface UseCameraOptions {
|
|
3
|
+
facingMode?: 'user' | 'environment';
|
|
4
|
+
width?: number;
|
|
5
|
+
height?: number;
|
|
6
|
+
autoStart?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function useCamera(options?: UseCameraOptions): {
|
|
9
|
+
videoRef: import("react").RefObject<HTMLVideoElement | null>;
|
|
10
|
+
stream: MediaStream | null;
|
|
11
|
+
status: CameraStatus;
|
|
12
|
+
error: string | null;
|
|
13
|
+
start: () => Promise<void>;
|
|
14
|
+
stop: () => void;
|
|
15
|
+
switchCamera: () => Promise<void>;
|
|
16
|
+
};
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @mosta/face — useCamera hook
|
|
3
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
4
|
+
'use client';
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.useCamera = useCamera;
|
|
7
|
+
const react_1 = require("react");
|
|
8
|
+
function useCamera(options) {
|
|
9
|
+
const { facingMode = 'user', width = 640, height = 480, autoStart = false } = options || {};
|
|
10
|
+
const videoRef = (0, react_1.useRef)(null);
|
|
11
|
+
const [stream, setStream] = (0, react_1.useState)(null);
|
|
12
|
+
const [status, setStatus] = (0, react_1.useState)('idle');
|
|
13
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
14
|
+
const start = (0, react_1.useCallback)(async () => {
|
|
15
|
+
try {
|
|
16
|
+
setStatus('requesting');
|
|
17
|
+
setError(null);
|
|
18
|
+
const mediaStream = await navigator.mediaDevices.getUserMedia({
|
|
19
|
+
video: { facingMode, width: { ideal: width }, height: { ideal: height } },
|
|
20
|
+
});
|
|
21
|
+
if (videoRef.current) {
|
|
22
|
+
videoRef.current.srcObject = mediaStream;
|
|
23
|
+
await videoRef.current.play();
|
|
24
|
+
}
|
|
25
|
+
setStream(mediaStream);
|
|
26
|
+
setStatus('active');
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
if (err.name === 'NotAllowedError') {
|
|
30
|
+
setStatus('denied');
|
|
31
|
+
setError('Camera permission denied');
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
setStatus('error');
|
|
35
|
+
setError(err.message || 'Camera error');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}, [facingMode, width, height]);
|
|
39
|
+
const stop = (0, react_1.useCallback)(() => {
|
|
40
|
+
if (stream) {
|
|
41
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
42
|
+
setStream(null);
|
|
43
|
+
}
|
|
44
|
+
if (videoRef.current) {
|
|
45
|
+
videoRef.current.srcObject = null;
|
|
46
|
+
}
|
|
47
|
+
setStatus('idle');
|
|
48
|
+
}, [stream]);
|
|
49
|
+
const switchCamera = (0, react_1.useCallback)(async () => {
|
|
50
|
+
stop();
|
|
51
|
+
// Toggle facing mode
|
|
52
|
+
const newMode = facingMode === 'user' ? 'environment' : 'user';
|
|
53
|
+
try {
|
|
54
|
+
const mediaStream = await navigator.mediaDevices.getUserMedia({
|
|
55
|
+
video: { facingMode: newMode, width: { ideal: width }, height: { ideal: height } },
|
|
56
|
+
});
|
|
57
|
+
if (videoRef.current) {
|
|
58
|
+
videoRef.current.srcObject = mediaStream;
|
|
59
|
+
await videoRef.current.play();
|
|
60
|
+
}
|
|
61
|
+
setStream(mediaStream);
|
|
62
|
+
setStatus('active');
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
setStatus('error');
|
|
66
|
+
setError(err.message);
|
|
67
|
+
}
|
|
68
|
+
}, [stop, facingMode, width, height]);
|
|
69
|
+
(0, react_1.useEffect)(() => {
|
|
70
|
+
if (autoStart)
|
|
71
|
+
start();
|
|
72
|
+
return () => {
|
|
73
|
+
// Cleanup on unmount
|
|
74
|
+
stream?.getTracks().forEach((t) => t.stop());
|
|
75
|
+
};
|
|
76
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
77
|
+
return { videoRef, stream, status, error, start, stop, switchCamera };
|
|
78
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { MostaFaceConfig } from '../types';
|
|
2
|
+
export type DetectionStatus = 'loading' | 'detecting' | 'detected' | 'noFace';
|
|
3
|
+
interface UseFaceDetectionOptions {
|
|
4
|
+
/** Interval between detections in ms (default: 300) */
|
|
5
|
+
interval?: number;
|
|
6
|
+
/** Auto-start detection (default: true) */
|
|
7
|
+
autoStart?: boolean;
|
|
8
|
+
/** Also extract descriptor on each frame (default: false) */
|
|
9
|
+
extractDescriptor?: boolean;
|
|
10
|
+
/** Face-api config */
|
|
11
|
+
config?: MostaFaceConfig;
|
|
12
|
+
}
|
|
13
|
+
export declare function useFaceDetection(videoRef: React.RefObject<HTMLVideoElement | null>, options?: UseFaceDetectionOptions): {
|
|
14
|
+
detection: any;
|
|
15
|
+
descriptor: Float32Array<ArrayBufferLike> | null;
|
|
16
|
+
status: DetectionStatus;
|
|
17
|
+
modelsLoaded: boolean;
|
|
18
|
+
stop: () => void;
|
|
19
|
+
start: () => Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @mosta/face — useFaceDetection hook
|
|
3
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
4
|
+
'use client';
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.useFaceDetection = useFaceDetection;
|
|
40
|
+
const react_1 = require("react");
|
|
41
|
+
function useFaceDetection(videoRef, options) {
|
|
42
|
+
const { interval = 300, autoStart = true, extractDescriptor: doExtract = false, config } = options || {};
|
|
43
|
+
const [detection, setDetection] = (0, react_1.useState)(null);
|
|
44
|
+
const [descriptor, setDescriptor] = (0, react_1.useState)(null);
|
|
45
|
+
const [status, setStatus] = (0, react_1.useState)('loading');
|
|
46
|
+
const [modelsLoaded, setModelsLoaded] = (0, react_1.useState)(false);
|
|
47
|
+
const running = (0, react_1.useRef)(false);
|
|
48
|
+
const timerRef = (0, react_1.useRef)(null);
|
|
49
|
+
const loadAndStart = (0, react_1.useCallback)(async () => {
|
|
50
|
+
const faceApi = await Promise.resolve().then(() => __importStar(require('../lib/face-api')));
|
|
51
|
+
await faceApi.loadModels(config);
|
|
52
|
+
setModelsLoaded(true);
|
|
53
|
+
setStatus('detecting');
|
|
54
|
+
running.current = true;
|
|
55
|
+
const detect = async () => {
|
|
56
|
+
if (!running.current || !videoRef.current)
|
|
57
|
+
return;
|
|
58
|
+
const video = videoRef.current;
|
|
59
|
+
if (video.readyState < 2) {
|
|
60
|
+
timerRef.current = setTimeout(detect, interval);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const det = await faceApi.detectFace(video, config);
|
|
64
|
+
setDetection(det);
|
|
65
|
+
if (det) {
|
|
66
|
+
setStatus('detected');
|
|
67
|
+
if (doExtract) {
|
|
68
|
+
const desc = await faceApi.extractDescriptor(video, config);
|
|
69
|
+
setDescriptor(desc);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
setStatus('noFace');
|
|
74
|
+
setDescriptor(null);
|
|
75
|
+
}
|
|
76
|
+
if (running.current) {
|
|
77
|
+
timerRef.current = setTimeout(detect, interval);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
detect();
|
|
81
|
+
}, [videoRef, interval, doExtract, config]);
|
|
82
|
+
(0, react_1.useEffect)(() => {
|
|
83
|
+
if (autoStart)
|
|
84
|
+
loadAndStart();
|
|
85
|
+
return () => {
|
|
86
|
+
running.current = false;
|
|
87
|
+
if (timerRef.current)
|
|
88
|
+
clearTimeout(timerRef.current);
|
|
89
|
+
};
|
|
90
|
+
}, [autoStart, loadAndStart]);
|
|
91
|
+
const stop = (0, react_1.useCallback)(() => {
|
|
92
|
+
running.current = false;
|
|
93
|
+
if (timerRef.current)
|
|
94
|
+
clearTimeout(timerRef.current);
|
|
95
|
+
}, []);
|
|
96
|
+
return { detection, descriptor, status, modelsLoaded, stop, start: loadAndStart };
|
|
97
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { loadModels, isLoaded, detectFace, detectAllFaces, extractDescriptor } from './lib/face-api';
|
|
2
|
+
export { compareFaces, findMatch, findAllMatches } from './lib/face-matcher';
|
|
3
|
+
export { descriptorToArray, arrayToDescriptor, isValidDescriptor, drawDetection } from './lib/face-utils';
|
|
4
|
+
export { useCamera } from './hooks/useCamera';
|
|
5
|
+
export { useFaceDetection } from './hooks/useFaceDetection';
|
|
6
|
+
export type { MostaFaceConfig, FaceDetectionResult, FaceMatchResult, FaceDescriptor } from './types';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @mosta/face — Barrel exports
|
|
3
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.useFaceDetection = exports.useCamera = exports.drawDetection = exports.isValidDescriptor = exports.arrayToDescriptor = exports.descriptorToArray = exports.findAllMatches = exports.findMatch = exports.compareFaces = exports.extractDescriptor = exports.detectAllFaces = exports.detectFace = exports.isLoaded = exports.loadModels = void 0;
|
|
6
|
+
// Core face-api service
|
|
7
|
+
var face_api_1 = require("./lib/face-api");
|
|
8
|
+
Object.defineProperty(exports, "loadModels", { enumerable: true, get: function () { return face_api_1.loadModels; } });
|
|
9
|
+
Object.defineProperty(exports, "isLoaded", { enumerable: true, get: function () { return face_api_1.isLoaded; } });
|
|
10
|
+
Object.defineProperty(exports, "detectFace", { enumerable: true, get: function () { return face_api_1.detectFace; } });
|
|
11
|
+
Object.defineProperty(exports, "detectAllFaces", { enumerable: true, get: function () { return face_api_1.detectAllFaces; } });
|
|
12
|
+
Object.defineProperty(exports, "extractDescriptor", { enumerable: true, get: function () { return face_api_1.extractDescriptor; } });
|
|
13
|
+
// Matching
|
|
14
|
+
var face_matcher_1 = require("./lib/face-matcher");
|
|
15
|
+
Object.defineProperty(exports, "compareFaces", { enumerable: true, get: function () { return face_matcher_1.compareFaces; } });
|
|
16
|
+
Object.defineProperty(exports, "findMatch", { enumerable: true, get: function () { return face_matcher_1.findMatch; } });
|
|
17
|
+
Object.defineProperty(exports, "findAllMatches", { enumerable: true, get: function () { return face_matcher_1.findAllMatches; } });
|
|
18
|
+
// Utils
|
|
19
|
+
var face_utils_1 = require("./lib/face-utils");
|
|
20
|
+
Object.defineProperty(exports, "descriptorToArray", { enumerable: true, get: function () { return face_utils_1.descriptorToArray; } });
|
|
21
|
+
Object.defineProperty(exports, "arrayToDescriptor", { enumerable: true, get: function () { return face_utils_1.arrayToDescriptor; } });
|
|
22
|
+
Object.defineProperty(exports, "isValidDescriptor", { enumerable: true, get: function () { return face_utils_1.isValidDescriptor; } });
|
|
23
|
+
Object.defineProperty(exports, "drawDetection", { enumerable: true, get: function () { return face_utils_1.drawDetection; } });
|
|
24
|
+
// Hooks
|
|
25
|
+
var useCamera_1 = require("./hooks/useCamera");
|
|
26
|
+
Object.defineProperty(exports, "useCamera", { enumerable: true, get: function () { return useCamera_1.useCamera; } });
|
|
27
|
+
var useFaceDetection_1 = require("./hooks/useFaceDetection");
|
|
28
|
+
Object.defineProperty(exports, "useFaceDetection", { enumerable: true, get: function () { return useFaceDetection_1.useFaceDetection; } });
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MostaFaceConfig } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Load the face-api.js library and 3 models (TinyFaceDetector, landmarks68, recognition).
|
|
4
|
+
*/
|
|
5
|
+
export declare function loadModels(config?: MostaFaceConfig): Promise<void>;
|
|
6
|
+
/** Check if models are loaded */
|
|
7
|
+
export declare function isLoaded(): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Detect a single face with landmarks.
|
|
10
|
+
*/
|
|
11
|
+
export declare function detectFace(input: HTMLVideoElement | HTMLCanvasElement, config?: MostaFaceConfig): Promise<import("@vladmandic/face-api").WithFaceLandmarks<{
|
|
12
|
+
detection: import("@vladmandic/face-api").FaceDetection;
|
|
13
|
+
}, import("@vladmandic/face-api").FaceLandmarks68> | null>;
|
|
14
|
+
/**
|
|
15
|
+
* Detect all faces with landmarks.
|
|
16
|
+
*/
|
|
17
|
+
export declare function detectAllFaces(input: HTMLVideoElement | HTMLCanvasElement, config?: MostaFaceConfig): Promise<import("@vladmandic/face-api").WithFaceLandmarks<{
|
|
18
|
+
detection: import("@vladmandic/face-api").FaceDetection;
|
|
19
|
+
}, import("@vladmandic/face-api").FaceLandmarks68>[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Extract the 128-dimensional face descriptor from a single face.
|
|
22
|
+
*/
|
|
23
|
+
export declare function extractDescriptor(input: HTMLVideoElement | HTMLCanvasElement, config?: MostaFaceConfig): Promise<Float32Array | null>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @mosta/face — Core face-api.js service (CLIENT-SIDE only)
|
|
3
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
4
|
+
//
|
|
5
|
+
// Usage: const faceApi = await import('@mosta/face') — always use dynamic import in useEffect
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.loadModels = loadModels;
|
|
41
|
+
exports.isLoaded = isLoaded;
|
|
42
|
+
exports.detectFace = detectFace;
|
|
43
|
+
exports.detectAllFaces = detectAllFaces;
|
|
44
|
+
exports.extractDescriptor = extractDescriptor;
|
|
45
|
+
let fapi = null;
|
|
46
|
+
let modelsLoaded = false;
|
|
47
|
+
/**
|
|
48
|
+
* Load the face-api.js library and 3 models (TinyFaceDetector, landmarks68, recognition).
|
|
49
|
+
*/
|
|
50
|
+
async function loadModels(config) {
|
|
51
|
+
if (modelsLoaded)
|
|
52
|
+
return;
|
|
53
|
+
fapi = await Promise.resolve().then(() => __importStar(require('@vladmandic/face-api')));
|
|
54
|
+
const modelUrl = config?.modelsPath ?? '/models/face-api';
|
|
55
|
+
await Promise.all([
|
|
56
|
+
fapi.nets.tinyFaceDetector.loadFromUri(modelUrl),
|
|
57
|
+
fapi.nets.faceLandmark68Net.loadFromUri(modelUrl),
|
|
58
|
+
fapi.nets.faceRecognitionNet.loadFromUri(modelUrl),
|
|
59
|
+
]);
|
|
60
|
+
modelsLoaded = true;
|
|
61
|
+
}
|
|
62
|
+
/** Check if models are loaded */
|
|
63
|
+
function isLoaded() {
|
|
64
|
+
return modelsLoaded;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Detect a single face with landmarks.
|
|
68
|
+
*/
|
|
69
|
+
async function detectFace(input, config) {
|
|
70
|
+
if (!fapi)
|
|
71
|
+
throw new Error('face-api not loaded — call loadModels() first');
|
|
72
|
+
const detection = await fapi
|
|
73
|
+
.detectSingleFace(input, new fapi.TinyFaceDetectorOptions({
|
|
74
|
+
inputSize: config?.inputSize ?? 320,
|
|
75
|
+
scoreThreshold: config?.scoreThreshold ?? 0.5,
|
|
76
|
+
}))
|
|
77
|
+
.withFaceLandmarks();
|
|
78
|
+
return detection || null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Detect all faces with landmarks.
|
|
82
|
+
*/
|
|
83
|
+
async function detectAllFaces(input, config) {
|
|
84
|
+
if (!fapi)
|
|
85
|
+
throw new Error('face-api not loaded — call loadModels() first');
|
|
86
|
+
return fapi
|
|
87
|
+
.detectAllFaces(input, new fapi.TinyFaceDetectorOptions({
|
|
88
|
+
inputSize: config?.inputSize ?? 320,
|
|
89
|
+
scoreThreshold: config?.scoreThreshold ?? 0.5,
|
|
90
|
+
}))
|
|
91
|
+
.withFaceLandmarks();
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Extract the 128-dimensional face descriptor from a single face.
|
|
95
|
+
*/
|
|
96
|
+
async function extractDescriptor(input, config) {
|
|
97
|
+
if (!fapi)
|
|
98
|
+
throw new Error('face-api not loaded — call loadModels() first');
|
|
99
|
+
const result = await fapi
|
|
100
|
+
.detectSingleFace(input, new fapi.TinyFaceDetectorOptions({
|
|
101
|
+
inputSize: config?.inputSize ?? 320,
|
|
102
|
+
scoreThreshold: config?.scoreThreshold ?? 0.5,
|
|
103
|
+
}))
|
|
104
|
+
.withFaceLandmarks()
|
|
105
|
+
.withFaceDescriptor();
|
|
106
|
+
return result?.descriptor || null;
|
|
107
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FaceDescriptor, FaceMatchResult } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Compute Euclidean distance between two 128-dim face descriptors.
|
|
4
|
+
*/
|
|
5
|
+
export declare function compareFaces(d1: FaceDescriptor, d2: FaceDescriptor): number;
|
|
6
|
+
/**
|
|
7
|
+
* Find the best match among candidates.
|
|
8
|
+
* Returns null if no match is below the threshold.
|
|
9
|
+
*/
|
|
10
|
+
export declare function findMatch<T extends {
|
|
11
|
+
faceDescriptor: number[];
|
|
12
|
+
}>(descriptor: FaceDescriptor, candidates: T[], threshold?: number): FaceMatchResult<T> | null;
|
|
13
|
+
/**
|
|
14
|
+
* Find all matches below the threshold, sorted by distance.
|
|
15
|
+
*/
|
|
16
|
+
export declare function findAllMatches<T extends {
|
|
17
|
+
faceDescriptor: number[];
|
|
18
|
+
}>(descriptor: FaceDescriptor, candidates: T[], threshold?: number): FaceMatchResult<T>[];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.compareFaces = compareFaces;
|
|
4
|
+
exports.findMatch = findMatch;
|
|
5
|
+
exports.findAllMatches = findAllMatches;
|
|
6
|
+
/**
|
|
7
|
+
* Compute Euclidean distance between two 128-dim face descriptors.
|
|
8
|
+
*/
|
|
9
|
+
function compareFaces(d1, d2) {
|
|
10
|
+
const a = d1 instanceof Float32Array ? d1 : new Float32Array(d1);
|
|
11
|
+
const b = d2 instanceof Float32Array ? d2 : new Float32Array(d2);
|
|
12
|
+
if (a.length !== 128 || b.length !== 128) {
|
|
13
|
+
throw new Error('Descriptors must be 128 elements');
|
|
14
|
+
}
|
|
15
|
+
let sum = 0;
|
|
16
|
+
for (let i = 0; i < 128; i++) {
|
|
17
|
+
const diff = a[i] - b[i];
|
|
18
|
+
sum += diff * diff;
|
|
19
|
+
}
|
|
20
|
+
return Math.sqrt(sum);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Find the best match among candidates.
|
|
24
|
+
* Returns null if no match is below the threshold.
|
|
25
|
+
*/
|
|
26
|
+
function findMatch(descriptor, candidates, threshold = 0.6) {
|
|
27
|
+
let bestMatch = null;
|
|
28
|
+
let bestDistance = Infinity;
|
|
29
|
+
for (const candidate of candidates) {
|
|
30
|
+
if (!candidate.faceDescriptor || candidate.faceDescriptor.length !== 128)
|
|
31
|
+
continue;
|
|
32
|
+
const distance = compareFaces(descriptor, candidate.faceDescriptor);
|
|
33
|
+
if (distance < bestDistance) {
|
|
34
|
+
bestDistance = distance;
|
|
35
|
+
bestMatch = candidate;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (bestMatch && bestDistance < threshold) {
|
|
39
|
+
return { match: bestMatch, distance: bestDistance };
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Find all matches below the threshold, sorted by distance.
|
|
45
|
+
*/
|
|
46
|
+
function findAllMatches(descriptor, candidates, threshold = 0.6) {
|
|
47
|
+
const results = [];
|
|
48
|
+
for (const candidate of candidates) {
|
|
49
|
+
if (!candidate.faceDescriptor || candidate.faceDescriptor.length !== 128)
|
|
50
|
+
continue;
|
|
51
|
+
const distance = compareFaces(descriptor, candidate.faceDescriptor);
|
|
52
|
+
if (distance < threshold) {
|
|
53
|
+
results.push({ match: candidate, distance });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return results.sort((a, b) => a.distance - b.distance);
|
|
57
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Convert Float32Array descriptor to number[] for JSON/DB storage */
|
|
2
|
+
export declare function descriptorToArray(descriptor: Float32Array): number[];
|
|
3
|
+
/** Convert number[] back to Float32Array for comparison */
|
|
4
|
+
export declare function arrayToDescriptor(arr: number[]): Float32Array;
|
|
5
|
+
/** Validate that a descriptor is valid (128 elements) */
|
|
6
|
+
export declare function isValidDescriptor(data: unknown): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Draw a detection bounding box + score on a canvas overlay.
|
|
9
|
+
*/
|
|
10
|
+
export declare function drawDetection(canvas: HTMLCanvasElement, detection: {
|
|
11
|
+
detection: {
|
|
12
|
+
score: number;
|
|
13
|
+
box: {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
} | null, videoWidth: number, videoHeight: number): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @mosta/face — Utility functions
|
|
3
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.descriptorToArray = descriptorToArray;
|
|
6
|
+
exports.arrayToDescriptor = arrayToDescriptor;
|
|
7
|
+
exports.isValidDescriptor = isValidDescriptor;
|
|
8
|
+
exports.drawDetection = drawDetection;
|
|
9
|
+
/** Convert Float32Array descriptor to number[] for JSON/DB storage */
|
|
10
|
+
function descriptorToArray(descriptor) {
|
|
11
|
+
return Array.from(descriptor);
|
|
12
|
+
}
|
|
13
|
+
/** Convert number[] back to Float32Array for comparison */
|
|
14
|
+
function arrayToDescriptor(arr) {
|
|
15
|
+
return new Float32Array(arr);
|
|
16
|
+
}
|
|
17
|
+
/** Validate that a descriptor is valid (128 elements) */
|
|
18
|
+
function isValidDescriptor(data) {
|
|
19
|
+
if (data instanceof Float32Array)
|
|
20
|
+
return data.length === 128;
|
|
21
|
+
if (Array.isArray(data))
|
|
22
|
+
return data.length === 128 && data.every((v) => typeof v === 'number');
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Draw a detection bounding box + score on a canvas overlay.
|
|
27
|
+
*/
|
|
28
|
+
function drawDetection(canvas, detection, videoWidth, videoHeight) {
|
|
29
|
+
const ctx = canvas.getContext('2d');
|
|
30
|
+
if (!ctx)
|
|
31
|
+
return;
|
|
32
|
+
canvas.width = videoWidth;
|
|
33
|
+
canvas.height = videoHeight;
|
|
34
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
35
|
+
if (!detection)
|
|
36
|
+
return;
|
|
37
|
+
const box = detection.detection.box;
|
|
38
|
+
ctx.strokeStyle = '#22c55e';
|
|
39
|
+
ctx.lineWidth = 3;
|
|
40
|
+
ctx.strokeRect(box.x, box.y, box.width, box.height);
|
|
41
|
+
const score = Math.round(detection.detection.score * 100);
|
|
42
|
+
ctx.fillStyle = '#22c55e';
|
|
43
|
+
ctx.font = 'bold 14px sans-serif';
|
|
44
|
+
ctx.fillText(`Visage ${score}%`, box.x, box.y - 6);
|
|
45
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface MostaFaceConfig {
|
|
2
|
+
/** Path to face-api.js model files (default: '/models/face-api') */
|
|
3
|
+
modelsPath?: string;
|
|
4
|
+
/** Detection score threshold (default: 0.5) */
|
|
5
|
+
scoreThreshold?: number;
|
|
6
|
+
/** TinyFaceDetector input size (default: 320) */
|
|
7
|
+
inputSize?: number;
|
|
8
|
+
/** Face matching distance threshold (default: 0.6) */
|
|
9
|
+
matchThreshold?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface FaceDetectionResult {
|
|
12
|
+
detection: {
|
|
13
|
+
score: number;
|
|
14
|
+
box: {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
landmarks?: any;
|
|
22
|
+
}
|
|
23
|
+
export interface FaceMatchResult<T> {
|
|
24
|
+
match: T;
|
|
25
|
+
distance: number;
|
|
26
|
+
}
|
|
27
|
+
export type FaceDescriptor = Float32Array | number[];
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mostajs/face",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Reusable face recognition module — detection, descriptor extraction, 1:N matching",
|
|
5
|
+
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./hooks/useCamera": {
|
|
17
|
+
"types": "./dist/hooks/useCamera.d.ts",
|
|
18
|
+
"import": "./dist/hooks/useCamera.js",
|
|
19
|
+
"require": "./dist/hooks/useCamera.js",
|
|
20
|
+
"default": "./dist/hooks/useCamera.js"
|
|
21
|
+
},
|
|
22
|
+
"./hooks/useFaceDetection": {
|
|
23
|
+
"types": "./dist/hooks/useFaceDetection.d.ts",
|
|
24
|
+
"import": "./dist/hooks/useFaceDetection.js",
|
|
25
|
+
"require": "./dist/hooks/useFaceDetection.js",
|
|
26
|
+
"default": "./dist/hooks/useFaceDetection.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": ["dist", "LICENSE", "README.md"],
|
|
30
|
+
"keywords": ["face-recognition", "face-detection", "face-api", "biometrics", "react", "mosta"],
|
|
31
|
+
"repository": { "type": "git", "url": "https://github.com/apolocine/mosta-face" },
|
|
32
|
+
"homepage": "https://mostajs.dev/packages/face",
|
|
33
|
+
"engines": { "node": ">=18.0.0" },
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc",
|
|
36
|
+
"prepublishOnly": "npm run build"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@vladmandic/face-api": "^1.7.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"react": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/react": "^19.0.0",
|
|
46
|
+
"react": "^19.0.0",
|
|
47
|
+
"typescript": "^5.6.0"
|
|
48
|
+
}
|
|
49
|
+
}
|