Compare commits
No commits in common. "master" and "feature/homebridge" have entirely different histories.
master
...
feature/ho
41
.drone.yml
41
.drone.yml
@ -1,41 +0,0 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
clone:
|
||||
disable: true
|
||||
|
||||
steps:
|
||||
- name: clone
|
||||
image: alpine/git
|
||||
commands:
|
||||
- git clone https://gitea.watsonlabs.net/watsonb8/homebridge-face-location.git .
|
||||
- git checkout $DRONE_COMMIT
|
||||
|
||||
- name: build
|
||||
image: node
|
||||
commands:
|
||||
- npm install
|
||||
- npm run build
|
||||
|
||||
- name: publish
|
||||
image: plugins/npm:1.0.0
|
||||
settings:
|
||||
username: admin
|
||||
password:
|
||||
from_secret: npm_password
|
||||
email: brandon@watsonlabs.net
|
||||
registry: "http://linuxhost.me:4873/"
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
notify:
|
||||
image: drillster/drone-email
|
||||
host: smtp.watsonlabs.net
|
||||
username: srvGitea
|
||||
password:
|
||||
from_secret: smtp_password
|
||||
from: drone@watsonlabs.net
|
||||
when:
|
||||
status: [failure]
|
@ -1,4 +1,3 @@
|
||||
REF_IMAGE_DIR=
|
||||
TRAINED_MODEL_DIR=
|
||||
OUT_DIR=
|
||||
CONFIDENCE=
|
||||
OUT_DIR=
|
75
README.md
75
README.md
@ -1,77 +1,2 @@
|
||||
# homebridge-face-location
|
||||
|
||||
Homebridge plugin providing real time location tracking via facial recognition.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Download FFMPEG [here](https://ffmpeg.org/download.html)
|
||||
|
||||
> Note: FFMPEG is required in order to run homebridge-face-location
|
||||
|
||||
1. Clone the repository by running `git clone ssh://git@thebword.ddns.net:3122/watsonb8/homebridge-face-location.git`
|
||||
1. Run `npm install` to install required modules
|
||||
1. Run `npm run build` to build the module
|
||||
1. Run `npm link` to link this instance to your global homebridge instance
|
||||
|
||||
## Configuration
|
||||
|
||||
```
|
||||
{
|
||||
"platform": "HomeLocation",
|
||||
"name": "HomeLocation",
|
||||
"refImageDirectory": "/path/to/reference/images/",
|
||||
"trainedModelDirectory": "/path/to/trainedModel/data.json/",
|
||||
"weightDirectory": "/path/to/trained/weights/",
|
||||
"outputDirectory": "/path/to/desired/output/image/directory",
|
||||
"trainOnStartup": false,
|
||||
"rate": 1,
|
||||
"detectionTimeout": 90000,
|
||||
"watchdogTimeout": 10000,
|
||||
"debug": false,
|
||||
"writeOutput": true,
|
||||
"rooms": [
|
||||
{
|
||||
"name": "Kitchen",
|
||||
"rtspConnectionStrings": [
|
||||
"rtsp://username:password@example.com"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "LivingRoom",
|
||||
"rtspConnectionStrings": [
|
||||
"rtsp://username:password@example.com"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Platform
|
||||
|
||||
- `refImageDirectory`: The location of the images used to train the facial recognition matcher
|
||||
|
||||
> NOTE: This directory should only contain sub directories labeled with the name of the person to be matched with their corresponding images in the labeled folder. Any labels found in this directory will be used in face tracking
|
||||
|
||||
- `trainedModelDirectory`: The location of the trained `data.json` file if it exists. This is also the location where training data will be placed if `trainOnStartup` is true
|
||||
|
||||
- `weightDirectory`: The location of the pre-trained weight files used for face detection
|
||||
|
||||
- `outputDirectory`: The directory to be used if debug output images are wanted
|
||||
|
||||
- `trainOnStartup`: If true, a trained model will be created and persisted in the `trainedModelDirectory` during startup
|
||||
|
||||
- `rate`: The rate at which collect images from rtsp stream in fps
|
||||
|
||||
- `detectionTimeout`: The number of milliseconds to wait after a person is detected before returing to the undetected state
|
||||
|
||||
- `watchdogTimeout`: The number of milliseconds to wait before restarting the rtsp stream when a new image has not been recieved
|
||||
|
||||
- `debug`: If true, additional debug logging is displayed in the console
|
||||
|
||||
- `writeOutput`: If true, an image will be written to disk for every frame received from the rtsp stream
|
||||
|
||||
#### Rooms
|
||||
|
||||
- `name`: The name of the room in which to detect
|
||||
|
||||
- `rtspConnectionStrings`: A list of camera rtsp connection strings to associate with the room
|
||||
|
23
deploy.sh
23
deploy.sh
@ -1,23 +0,0 @@
|
||||
#!/bin/bash
|
||||
remote_user="bmw"
|
||||
remote_server="linuxhost.me"
|
||||
deploy_location="/home/bmw/homebridge-face-location"
|
||||
|
||||
#build
|
||||
tsc --build
|
||||
#copy files to remote machine
|
||||
scp -r bin $remote_user@$remote_server:$deploy_location
|
||||
scp -r out $remote_user@$remote_server:$deploy_location
|
||||
scp -r weights $remote_user@$remote_server:$deploy_location
|
||||
scp -r trainedModels $remote_user@$remote_server:$deploy_location
|
||||
scp package.json $remote_user@$remote_server:$deploy_location
|
||||
|
||||
#install package
|
||||
ssh -t $remote_user@$remote_server "sudo npm install -g --unsafe-perm $deploy_location"
|
||||
|
||||
#restart service
|
||||
ssh -t
|
||||
ssh -t $remote_user@$remote_server "sudo systemctl restart homebridge.service"
|
||||
|
||||
echo done
|
||||
exit
|
295
package-lock.json
generated
295
package-lock.json
generated
@ -135,14 +135,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.27.tgz",
|
||||
"integrity": "sha1-nbVjk33YaRX2kJK8QyWdL0hXjkE="
|
||||
},
|
||||
"@types/sharp": {
|
||||
"version": "0.26.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.26.1.tgz",
|
||||
"integrity": "sha512-vOFcnP0+aQFDb+ToKVIj8ZV6xQ7pNYGGPeYweLHxyjoQUcIGj8iY9R3OVmJyRR5KUkb0Y4obBbMjoTrBXw6AQA==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/webgl-ext": {
|
||||
"version": "0.0.30",
|
||||
"resolved": "https://registry.npmjs.org/@types/webgl-ext/-/webgl-ext-0.0.30.tgz",
|
||||
@ -257,33 +249,6 @@
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
},
|
||||
"base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
||||
},
|
||||
"bl": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz",
|
||||
"integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==",
|
||||
"requires": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"bonjour-hap": {
|
||||
"version": "3.5.11",
|
||||
"resolved": "https://registry.npmjs.org/bonjour-hap/-/bonjour-hap-3.5.11.tgz",
|
||||
@ -306,15 +271,6 @@
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"requires": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"buffer-from": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
|
||||
@ -394,30 +350,6 @@
|
||||
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
|
||||
},
|
||||
"color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz",
|
||||
"integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==",
|
||||
"requires": {
|
||||
"color-convert": "^1.9.1",
|
||||
"color-string": "^1.5.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"requires": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
|
||||
}
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@ -431,15 +363,6 @@
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"color-string": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz",
|
||||
"integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==",
|
||||
"requires": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@ -688,14 +611,6 @@
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||
"requires": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"es-abstract": {
|
||||
"version": "1.18.0-next.1",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
|
||||
@ -769,11 +684,6 @@
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
|
||||
},
|
||||
"expand-template": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="
|
||||
},
|
||||
"fast-srp-hap": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-srp-hap/-/fast-srp-hap-2.0.2.tgz",
|
||||
@ -806,11 +716,6 @@
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
|
||||
},
|
||||
"fs-minipass": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
|
||||
@ -908,11 +813,6 @@
|
||||
"has-symbols": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"github-from-package": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||
"integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4="
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||
@ -1022,11 +922,6 @@
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
}
|
||||
},
|
||||
"ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
|
||||
},
|
||||
"ignore-walk": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz",
|
||||
@ -1066,11 +961,6 @@
|
||||
"integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==",
|
||||
"dev": true
|
||||
},
|
||||
"is-arrayish": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
||||
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
|
||||
},
|
||||
"is-bigint": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.0.tgz",
|
||||
@ -1212,21 +1102,6 @@
|
||||
"p-locate": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
@ -1289,11 +1164,6 @@
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
@ -1320,11 +1190,6 @@
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
|
||||
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ=="
|
||||
},
|
||||
"napi-build-utils": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
|
||||
"integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg=="
|
||||
},
|
||||
"needle": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/needle/-/needle-2.5.2.tgz",
|
||||
@ -1335,19 +1200,6 @@
|
||||
"sax": "^1.2.4"
|
||||
}
|
||||
},
|
||||
"node-abi": {
|
||||
"version": "2.19.3",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.19.3.tgz",
|
||||
"integrity": "sha512-9xZrlyfvKhWme2EXFKQhZRp1yNWT/uI1luYPr3sFl+H4keYY4xR+1jO7mvTTijIsHf1M+QDe9uWuKeEpLInIlg==",
|
||||
"requires": {
|
||||
"semver": "^5.4.1"
|
||||
}
|
||||
},
|
||||
"node-addon-api": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.0.2.tgz",
|
||||
"integrity": "sha512-+D4s2HCnxPd5PjjI0STKwncjXTUKKqm74MDMz9OPXavjsGmjkvwgLtA5yoxJUdmpj52+2u+RrXgPipahKczMKg=="
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
||||
@ -1416,11 +1268,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"noop-logger": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz",
|
||||
"integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI="
|
||||
},
|
||||
"noop6": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/noop6/-/noop6-1.0.9.tgz",
|
||||
@ -1580,28 +1427,6 @@
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
|
||||
},
|
||||
"prebuild-install": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.0.0.tgz",
|
||||
"integrity": "sha512-h2ZJ1PXHKWZpp1caLw0oX9sagVpL2YTk+ZwInQbQ3QqNd4J03O6MpFNmMTJlkfgPENWqe5kP0WjQLqz5OjLfsw==",
|
||||
"requires": {
|
||||
"detect-libc": "^1.0.3",
|
||||
"expand-template": "^2.0.3",
|
||||
"github-from-package": "0.0.0",
|
||||
"minimist": "^1.2.3",
|
||||
"mkdirp-classic": "^0.5.3",
|
||||
"napi-build-utils": "^1.0.1",
|
||||
"node-abi": "^2.7.0",
|
||||
"noop-logger": "^0.1.1",
|
||||
"npmlog": "^4.0.1",
|
||||
"pump": "^3.0.0",
|
||||
"rc": "^1.2.7",
|
||||
"simple-get": "^3.0.3",
|
||||
"tar-fs": "^2.0.0",
|
||||
"tunnel-agent": "^0.6.0",
|
||||
"which-pm-runs": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
@ -1612,15 +1437,6 @@
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="
|
||||
},
|
||||
"pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||
"requires": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"q": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/q/-/q-1.1.2.tgz",
|
||||
@ -1743,61 +1559,6 @@
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
|
||||
},
|
||||
"sharp": {
|
||||
"version": "0.26.3",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.26.3.tgz",
|
||||
"integrity": "sha512-NdEJ9S6AMr8Px0zgtFo1TJjMK/ROMU92MkDtYn2BBrDjIx3YfH9TUyGdzPC+I/L619GeYQc690Vbaxc5FPCCWg==",
|
||||
"requires": {
|
||||
"array-flatten": "^3.0.0",
|
||||
"color": "^3.1.3",
|
||||
"detect-libc": "^1.0.3",
|
||||
"node-addon-api": "^3.0.2",
|
||||
"npmlog": "^4.1.2",
|
||||
"prebuild-install": "^6.0.0",
|
||||
"semver": "^7.3.2",
|
||||
"simple-get": "^4.0.0",
|
||||
"tar-fs": "^2.1.1",
|
||||
"tunnel-agent": "^0.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"array-flatten": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz",
|
||||
"integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA=="
|
||||
},
|
||||
"decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"requires": {
|
||||
"mimic-response": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
|
||||
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"simple-get": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.0.tgz",
|
||||
"integrity": "sha512-ZalZGexYr3TA0SwySsr5HlgOOinS4Jsa8YB2GJ6lUNAazyAu4KG/VmzMTwAt2YVXzzVj8QmefmAonZIK2BSGcQ==",
|
||||
"requires": {
|
||||
"decompress-response": "^6.0.0",
|
||||
"once": "^1.3.1",
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@ -1841,14 +1602,6 @@
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"simple-swizzle": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||
"integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
|
||||
"requires": {
|
||||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@ -1943,41 +1696,6 @@
|
||||
"yallist": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"tar-fs": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
|
||||
"integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
|
||||
"requires": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.1.4"
|
||||
}
|
||||
},
|
||||
"tar-stream": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.4.tgz",
|
||||
"integrity": "sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw==",
|
||||
"requires": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"through2": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
|
||||
@ -2020,14 +1738,6 @@
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
|
||||
"requires": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"tweetnacl": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
||||
@ -2098,11 +1808,6 @@
|
||||
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
|
||||
"dev": true
|
||||
},
|
||||
"which-pm-runs": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz",
|
||||
"integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs="
|
||||
},
|
||||
"which-typed-array": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.2.tgz",
|
||||
|
@ -27,12 +27,10 @@
|
||||
"@tensorflow/tfjs": "^2.6.0",
|
||||
"@tensorflow/tfjs-node": "^2.6.0",
|
||||
"@types/mime-types": "^2.1.0",
|
||||
"@types/sharp": "^0.26.1",
|
||||
"@vladmandic/face-api": "^0.8.8",
|
||||
"canvas": "^2.6.1",
|
||||
"dotenv-extended": "^2.9.0",
|
||||
"mime-types": "^2.1.27",
|
||||
"sharp": "^0.26.3",
|
||||
"tsyringe": "^4.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -1,74 +0,0 @@
|
||||
import { nets } from "@vladmandic/face-api";
|
||||
import * as faceapi from "@vladmandic/face-api";
|
||||
import canvas from "canvas";
|
||||
import fs from "fs";
|
||||
import * as path from "path";
|
||||
import dotenv from "dotenv-extended";
|
||||
import { getFaceDetectorOptions } from "../src/common";
|
||||
import * as mime from "mime-types";
|
||||
require("@tensorflow/tfjs-node");
|
||||
|
||||
const { Canvas, Image, ImageData } = canvas;
|
||||
//@ts-ignore
|
||||
faceapi.env.monkeyPatch({ Canvas, Image, ImageData });
|
||||
|
||||
const main = async () => {
|
||||
dotenv.load({
|
||||
silent: false,
|
||||
errorOnMissing: true,
|
||||
});
|
||||
|
||||
const modelDir = process.env.TRAINED_MODEL_DIR as string;
|
||||
const faceDetectionNet = nets.ssdMobilenetv1;
|
||||
|
||||
await faceDetectionNet.loadFromDisk(path.join(__dirname, "../weights"));
|
||||
await nets.faceLandmark68Net.loadFromDisk(path.join(__dirname, "../weights"));
|
||||
await nets.faceRecognitionNet.loadFromDisk(
|
||||
path.join(__dirname, "../weights")
|
||||
);
|
||||
|
||||
const raw = fs.readFileSync(path.join(modelDir, "data.json"), "utf-8");
|
||||
const content = JSON.parse(raw);
|
||||
const matcher = faceapi.FaceMatcher.fromJSON(content);
|
||||
|
||||
let dir = path.join(process.env.OUT_DIR as string);
|
||||
const files = fs.readdirSync(dir);
|
||||
await Promise.all(
|
||||
files.map(async (file: string) => {
|
||||
const mimeType = mime.contentType(path.extname(path.join(dir, file)));
|
||||
if (!mimeType || !mimeType.startsWith("image")) {
|
||||
return;
|
||||
}
|
||||
const imgData = fs.readFileSync(path.join(dir, file));
|
||||
const input = ((await canvas.loadImage(imgData)) as unknown) as ImageData;
|
||||
const out = faceapi.createCanvasFromMedia(input);
|
||||
const resultsQuery = await faceapi
|
||||
.detectAllFaces(out, getFaceDetectorOptions(faceDetectionNet))
|
||||
.withFaceLandmarks()
|
||||
.withFaceDescriptors();
|
||||
|
||||
if (resultsQuery.length > 0) {
|
||||
for (const res of resultsQuery) {
|
||||
const bestMatch = matcher.findBestMatch(res.descriptor);
|
||||
console.log(
|
||||
`Face Detected with ${
|
||||
res.detection.score * 100
|
||||
}% accuracy and a distance of ${bestMatch.distance}: ${
|
||||
bestMatch.label
|
||||
} in file ${path.join(dir, file)}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`No faces detected in file ${path.join(
|
||||
process.env.REF_IMAGE_DIR as string,
|
||||
"aline",
|
||||
file
|
||||
)}`
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
main();
|
@ -1,16 +1,100 @@
|
||||
import * as faceapi from "@vladmandic/face-api";
|
||||
import canvas from "canvas";
|
||||
import fs, { lstatSync } from "fs";
|
||||
import * as path from "path";
|
||||
import { LabeledFaceDescriptors, TNetInput } from "@vladmandic/face-api";
|
||||
import * as mime from "mime-types";
|
||||
import dotenv from "dotenv-extended";
|
||||
import { Trainer } from "../src/trainer";
|
||||
import { getFaceDetectorOptions } from "../src/common";
|
||||
require("@tensorflow/tfjs-node");
|
||||
|
||||
const { Canvas, Image, ImageData } = canvas;
|
||||
//@ts-ignore
|
||||
faceapi.env.monkeyPatch({ Canvas, Image, ImageData });
|
||||
|
||||
const main = async () => {
|
||||
dotenv.load({
|
||||
silent: false,
|
||||
errorOnMissing: true,
|
||||
});
|
||||
const inputDir = process.env.REF_IMAGE_DIR as string;
|
||||
const outDir = process.env.TRAINED_MODEL_DIR as string;
|
||||
|
||||
const trainer = new Trainer(
|
||||
process.env.REF_IMAGE_DIR as string,
|
||||
process.env.TRAINED_MODEL_DIR as string
|
||||
const faceDetectionNet = faceapi.nets.ssdMobilenetv1;
|
||||
await faceDetectionNet.loadFromDisk(path.join(__dirname, "../weights"));
|
||||
await faceapi.nets.faceLandmark68Net.loadFromDisk(
|
||||
path.join(__dirname, "../weights")
|
||||
);
|
||||
await faceapi.nets.faceRecognitionNet.loadFromDisk(
|
||||
path.join(__dirname, "../weights")
|
||||
);
|
||||
|
||||
const options = getFaceDetectorOptions(faceDetectionNet);
|
||||
|
||||
const dirs = fs.readdirSync(inputDir);
|
||||
|
||||
const refs: Array<LabeledFaceDescriptors> = [];
|
||||
for (const dir of dirs) {
|
||||
if (!lstatSync(path.join(inputDir, dir)).isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
const files = fs.readdirSync(path.join(inputDir, dir));
|
||||
let referenceResults = await Promise.all(
|
||||
files.map(async (file: string) => {
|
||||
const mimeType = mime.contentType(
|
||||
path.extname(path.join(inputDir, dir, file))
|
||||
);
|
||||
if (!mimeType || !mimeType.startsWith("image")) {
|
||||
return;
|
||||
}
|
||||
console.log(path.join(inputDir, dir, file));
|
||||
|
||||
try {
|
||||
const referenceImage = (await canvas.loadImage(
|
||||
path.join(inputDir, dir, file)
|
||||
)) as unknown;
|
||||
|
||||
const descriptor = await faceapi
|
||||
.detectSingleFace(referenceImage as TNetInput, options)
|
||||
.withFaceLandmarks()
|
||||
.withFaceDescriptor();
|
||||
if (!descriptor || !descriptor.descriptor) {
|
||||
throw new Error("No face found");
|
||||
}
|
||||
|
||||
const faceDescriptors = [descriptor.descriptor];
|
||||
return new faceapi.LabeledFaceDescriptors(dir, faceDescriptors);
|
||||
} catch (err) {
|
||||
console.log(
|
||||
"An error occurred loading image at path: " +
|
||||
path.join(inputDir, dir, file)
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
);
|
||||
|
||||
if (referenceResults) {
|
||||
refs.push(
|
||||
...(referenceResults.filter((e) => e) as LabeledFaceDescriptors[])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const faceMatcher = new faceapi.FaceMatcher(refs);
|
||||
|
||||
fs.writeFile(
|
||||
path.join(outDir, "data.json"),
|
||||
JSON.stringify(faceMatcher.toJSON()),
|
||||
"utf8",
|
||||
(err) => {
|
||||
if (err) {
|
||||
console.log(`An error occurred while writing data model to file`);
|
||||
}
|
||||
|
||||
console.log(`Successfully wrote data model to file`);
|
||||
}
|
||||
);
|
||||
await trainer.train(true);
|
||||
};
|
||||
|
||||
main();
|
||||
|
@ -3,72 +3,46 @@ import * as path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
// SsdMobilenetv1Options
|
||||
export const minConfidence = 0.4;
|
||||
export const minConfidence = 0.5;
|
||||
|
||||
// TinyFaceDetectorOptions
|
||||
export const inputSize = 416;
|
||||
export const inputSize = 408;
|
||||
export const scoreThreshold = 0.5;
|
||||
|
||||
export const getFaceDetectorOptions = (
|
||||
net: faceapi.NeuralNetwork<any>,
|
||||
options?: {
|
||||
confidence?: number;
|
||||
inputSize?: number;
|
||||
scoreThreshold?: number;
|
||||
}
|
||||
) => {
|
||||
export const getFaceDetectorOptions = (net: faceapi.NeuralNetwork<any>) => {
|
||||
return net === faceapi.nets.ssdMobilenetv1
|
||||
? new faceapi.SsdMobilenetv1Options({
|
||||
minConfidence: options?.confidence ?? minConfidence,
|
||||
})
|
||||
: new faceapi.TinyFaceDetectorOptions({
|
||||
inputSize: options?.inputSize ?? inputSize,
|
||||
scoreThreshold: options?.scoreThreshold ?? scoreThreshold,
|
||||
});
|
||||
? new faceapi.SsdMobilenetv1Options({ minConfidence })
|
||||
: new faceapi.TinyFaceDetectorOptions({ inputSize, scoreThreshold });
|
||||
};
|
||||
|
||||
export const saveFile = async (
|
||||
export function saveFile(
|
||||
basePath: string,
|
||||
fileName: string,
|
||||
buf: Buffer
|
||||
): Promise<void> => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
//Create directory if it does not exist
|
||||
await makeDirectory(basePath);
|
||||
} catch (err) {
|
||||
return reject(err);
|
||||
}
|
||||
//Write file to directory
|
||||
try {
|
||||
const asdf = fs.writeFileSync(
|
||||
path.join(basePath, fileName),
|
||||
buf,
|
||||
"base64"
|
||||
);
|
||||
} catch (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve();
|
||||
});
|
||||
};
|
||||
|
||||
export const makeDirectory = (path: string): Promise<void> => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (!fs.existsSync(path)) {
|
||||
fs.mkdir(path, async (err) => {
|
||||
): Promise<void> {
|
||||
const writeFile = (): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.writeFile(path.resolve(basePath, fileName), buf, "base64", (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (!fs.existsSync(basePath)) {
|
||||
fs.mkdir(basePath, async (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(await writeFile());
|
||||
});
|
||||
} else {
|
||||
resolve(await writeFile());
|
||||
}
|
||||
|
||||
return resolve();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const delay = (ms: number): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
|
@ -7,12 +7,9 @@ export interface IConfig extends PlatformConfig {
|
||||
outputDirectory: string;
|
||||
trainOnStartup: boolean;
|
||||
rooms: Array<IRoom>;
|
||||
detectionTimeout?: number;
|
||||
watchdogTimeout?: number;
|
||||
debug?: boolean;
|
||||
writeOutput?: boolean;
|
||||
rate?: number;
|
||||
confidence?: number;
|
||||
detectionTimeout: number;
|
||||
debug: boolean;
|
||||
writeOutput: boolean;
|
||||
}
|
||||
|
||||
export interface IRoom {
|
||||
@ -28,13 +25,14 @@ export const isConfig = (object: any): object is IConfig => {
|
||||
const roomsOkay =
|
||||
object["rooms"].filter((room: any) => isRoom(room)).length ===
|
||||
object["rooms"].length;
|
||||
|
||||
return (
|
||||
"refImageDirectory" in object &&
|
||||
"trainedModelDirectory" in object &&
|
||||
"weightDirectory" in object &&
|
||||
"outputDirectory" in object &&
|
||||
"trainOnStartup" in object &&
|
||||
"detectionTimeout" in object &&
|
||||
"writeOutput" in object &&
|
||||
"rooms" in object &&
|
||||
roomsOkay
|
||||
);
|
||||
|
@ -10,12 +10,17 @@ import {
|
||||
import { IConfig, isConfig } from "./config";
|
||||
import * as faceapi from "@vladmandic/face-api";
|
||||
import canvas from "canvas";
|
||||
import fs from "fs";
|
||||
import fs, { lstatSync } from "fs";
|
||||
import * as path from "path";
|
||||
import { nets } from "@vladmandic/face-api";
|
||||
import { FaceMatcher } from "@vladmandic/face-api";
|
||||
import { Monitor } from "./monitor/monitor";
|
||||
import { Trainer } from "./trainer";
|
||||
import {
|
||||
LabeledFaceDescriptors,
|
||||
TNetInput,
|
||||
FaceMatcher,
|
||||
} from "@vladmandic/face-api";
|
||||
import * as mime from "mime-types";
|
||||
import { Monitor } from "./monitor";
|
||||
import { getFaceDetectorOptions } from "./common";
|
||||
require("@tensorflow/tfjs-node");
|
||||
|
||||
const { Canvas, Image, ImageData } = canvas;
|
||||
@ -77,21 +82,17 @@ export class HomeLocationPlatform implements DynamicPlatformPlugin {
|
||||
* must not be registered again to prevent "duplicate UUID" errors.
|
||||
*/
|
||||
public async discoverDevices() {
|
||||
const faceDetectionNet = nets.ssdMobilenetv1;
|
||||
await faceDetectionNet.loadFromDisk(this.config.weightDirectory);
|
||||
await nets.faceLandmark68Net.loadFromDisk(this.config.weightDirectory);
|
||||
await nets.faceRecognitionNet.loadFromDisk(this.config.weightDirectory);
|
||||
|
||||
//Train facial recognition model
|
||||
let faceMatcher: FaceMatcher;
|
||||
if (this.config.trainOnStartup) {
|
||||
const trainer = new Trainer(
|
||||
this.config.refImageDirectory,
|
||||
this.config.trainedModelDirectory,
|
||||
this.config.confidence
|
||||
);
|
||||
faceMatcher = await trainer.train(true);
|
||||
faceMatcher = await this.trainModels();
|
||||
} else {
|
||||
const faceDetectionNet = nets.ssdMobilenetv1;
|
||||
|
||||
await faceDetectionNet.loadFromDisk(this.config.weightDirectory);
|
||||
await nets.faceLandmark68Net.loadFromDisk(this.config.weightDirectory);
|
||||
await nets.faceRecognitionNet.loadFromDisk(this.config.weightDirectory);
|
||||
|
||||
const raw = fs.readFileSync(
|
||||
path.join(this.config.trainedModelDirectory, "data.json"),
|
||||
"utf-8"
|
||||
@ -141,4 +142,88 @@ export class HomeLocationPlatform implements DynamicPlatformPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async trainModels(): Promise<FaceMatcher> {
|
||||
const faceDetectionNet = faceapi.nets.ssdMobilenetv1;
|
||||
await faceDetectionNet.loadFromDisk(this.config.weightDirectory);
|
||||
await faceapi.nets.faceLandmark68Net.loadFromDisk(
|
||||
this.config.weightDirectory
|
||||
);
|
||||
await faceapi.nets.faceRecognitionNet.loadFromDisk(
|
||||
this.config.weightDirectory
|
||||
);
|
||||
|
||||
const options = getFaceDetectorOptions(faceDetectionNet);
|
||||
|
||||
const dirs = fs.readdirSync(this.config.refImageDirectory);
|
||||
|
||||
const refs: Array<LabeledFaceDescriptors> = [];
|
||||
for (const dir of dirs) {
|
||||
if (
|
||||
!lstatSync(path.join(this.config.refImageDirectory, dir)).isDirectory()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const files = fs.readdirSync(
|
||||
path.join(this.config.refImageDirectory, dir)
|
||||
);
|
||||
let referenceResults = await Promise.all(
|
||||
files.map(async (file: string) => {
|
||||
const mimeType = mime.contentType(
|
||||
path.extname(path.join(this.config.refImageDirectory, dir, file))
|
||||
);
|
||||
if (!mimeType || !mimeType.startsWith("image")) {
|
||||
return;
|
||||
}
|
||||
console.log(path.join(this.config.refImageDirectory, dir, file));
|
||||
|
||||
try {
|
||||
const referenceImage = (await canvas.loadImage(
|
||||
path.join(this.config.refImageDirectory, dir, file)
|
||||
)) as unknown;
|
||||
|
||||
const descriptor = await faceapi
|
||||
.detectSingleFace(referenceImage as TNetInput, options)
|
||||
.withFaceLandmarks()
|
||||
.withFaceDescriptor();
|
||||
if (!descriptor || !descriptor.descriptor) {
|
||||
throw new Error("No face found");
|
||||
}
|
||||
|
||||
const faceDescriptors = [descriptor.descriptor];
|
||||
return new faceapi.LabeledFaceDescriptors(dir, faceDescriptors);
|
||||
} catch (err) {
|
||||
console.log(
|
||||
"An error occurred loading image at path: " +
|
||||
path.join(this.config.refImageDirectory, dir, file)
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
);
|
||||
|
||||
if (referenceResults) {
|
||||
refs.push(
|
||||
...(referenceResults.filter((e) => e) as LabeledFaceDescriptors[])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const faceMatcher = new faceapi.FaceMatcher(refs);
|
||||
|
||||
fs.writeFile(
|
||||
path.join(this.config.trainedModelDirectory, "data.json"),
|
||||
JSON.stringify(faceMatcher.toJSON()),
|
||||
"utf8",
|
||||
(err) => {
|
||||
if (err) {
|
||||
console.log(`An error occurred while writing data model to file`);
|
||||
}
|
||||
|
||||
console.log(`Successfully wrote data model to file`);
|
||||
}
|
||||
);
|
||||
|
||||
return faceMatcher;
|
||||
}
|
||||
}
|
||||
|
@ -3,24 +3,17 @@ import {
|
||||
CharacteristicGetCallback,
|
||||
PlatformAccessory,
|
||||
} from "homebridge";
|
||||
import { Monitor, IStateChangeEventArgs } from "./monitor/monitor";
|
||||
import { Monitor, IStateChangeEventArgs } from "./monitor";
|
||||
import { HomeLocationPlatform } from "./homeLocationPlatform";
|
||||
import { IRoom } from "./config";
|
||||
|
||||
const defaultDetectionTimeout = 180000;
|
||||
|
||||
interface IMotionDetectionService {
|
||||
service: Service;
|
||||
detectionTimeout: NodeJS.Timeout | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform Accessory
|
||||
* An instance of this class is created for each accessory your platform registers
|
||||
* Each accessory may expose multiple services of different service types.
|
||||
*/
|
||||
export class LocationAccessory {
|
||||
private _services: Array<IMotionDetectionService>;
|
||||
private _services: Array<Service>;
|
||||
|
||||
constructor(
|
||||
private readonly _platform: HomeLocationPlatform,
|
||||
@ -61,10 +54,7 @@ export class LocationAccessory {
|
||||
this.onMotionDetectedGet(label, callback)
|
||||
);
|
||||
|
||||
this._services.push({
|
||||
service: newService,
|
||||
detectionTimeout: null,
|
||||
});
|
||||
this._services.push(newService);
|
||||
}
|
||||
|
||||
//Register monitor state change events
|
||||
@ -88,31 +78,14 @@ export class LocationAccessory {
|
||||
sender: Monitor,
|
||||
args: IStateChangeEventArgs
|
||||
) => {
|
||||
const motionService = this._services.find(
|
||||
(motionService) => motionService.service.displayName == args.label
|
||||
const service = this._services.find(
|
||||
(service) => service.displayName == args.label
|
||||
);
|
||||
if (motionService) {
|
||||
//Set accessory state
|
||||
motionService.service.setCharacteristic(
|
||||
if (service) {
|
||||
service.setCharacteristic(
|
||||
this._platform.Characteristic.MotionDetected,
|
||||
args.new === this._room.name
|
||||
);
|
||||
|
||||
//Reset detectionTimeout
|
||||
clearTimeout(motionService.detectionTimeout!);
|
||||
motionService.detectionTimeout = setTimeout(
|
||||
() => this.onDetectionTimeout(motionService),
|
||||
this._platform.config.detectionTimeout ?? defaultDetectionTimeout
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private onDetectionTimeout = (motionService: IMotionDetectionService) => {
|
||||
//Set accessory state
|
||||
motionService.service.setCharacteristic(
|
||||
this._platform.Characteristic.MotionDetected,
|
||||
0
|
||||
);
|
||||
this._monitor.resetState(motionService.service.displayName);
|
||||
};
|
||||
}
|
||||
|
161
src/monitor.ts
Normal file
161
src/monitor.ts
Normal file
@ -0,0 +1,161 @@
|
||||
import { FaceMatcher } from "@vladmandic/face-api";
|
||||
import { IRoom } from "./config";
|
||||
import {
|
||||
Rtsp,
|
||||
IStreamEventArgs,
|
||||
ICloseEventArgs,
|
||||
IErrorEventArgs,
|
||||
IMessageEventArgs,
|
||||
} from "./rtsp/rtsp";
|
||||
import canvas from "canvas";
|
||||
import * as faceapi from "@vladmandic/face-api";
|
||||
import { getFaceDetectorOptions, saveFile } from "./common";
|
||||
import { nets } from "@vladmandic/face-api";
|
||||
import { Logger } from "homebridge";
|
||||
import { Event } from "./events";
|
||||
import { IConfig } from "./config";
|
||||
const { Canvas, Image, ImageData } = canvas;
|
||||
|
||||
export type MonitorState = { [label: string]: string | null };
|
||||
export interface IStateChangeEventArgs {
|
||||
label: string;
|
||||
old: string | null;
|
||||
new: string;
|
||||
}
|
||||
|
||||
export class Monitor {
|
||||
private _state: MonitorState = {};
|
||||
private _streamsByRoom: { [roomName: string]: Array<Rtsp> } = {};
|
||||
private _faceDetectionNet = nets.ssdMobilenetv1;
|
||||
private _stateChangedEvent: Event<this, IStateChangeEventArgs>;
|
||||
|
||||
constructor(
|
||||
private _rooms: Array<IRoom>,
|
||||
private _matcher: FaceMatcher,
|
||||
private _logger: Logger,
|
||||
private _config: IConfig
|
||||
) {
|
||||
this._stateChangedEvent = new Event();
|
||||
|
||||
//Initialize state
|
||||
for (const room of this._rooms) {
|
||||
this._streamsByRoom[room.name] = [
|
||||
...room.rtspConnectionStrings.map((connectionString) => {
|
||||
const rtsp = new Rtsp(connectionString, {
|
||||
rate: 0.7,
|
||||
image: true,
|
||||
});
|
||||
rtsp.dataEvent.push((sender: Rtsp, args: IStreamEventArgs) =>
|
||||
this.onData(room.name, args)
|
||||
);
|
||||
rtsp.closeEvent.push((sender: Rtsp, args: ICloseEventArgs) =>
|
||||
this.onExit(connectionString, args)
|
||||
);
|
||||
rtsp.errorEvent.push((sender: Rtsp, args: IErrorEventArgs) =>
|
||||
this.onError(args, connectionString)
|
||||
);
|
||||
if (this._config.debug) {
|
||||
rtsp.messageEvent.push((sender: Rtsp, args: IMessageEventArgs) => {
|
||||
this._logger.info(`[${connectionString}] ${args.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
return rtsp;
|
||||
}),
|
||||
];
|
||||
|
||||
_matcher.labeledDescriptors.forEach((descriptor) => {
|
||||
this._state[descriptor.label] = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @method getState
|
||||
*
|
||||
* @param label The name of the label to retrieve state for
|
||||
*
|
||||
* The last known room of the requested label
|
||||
*/
|
||||
public getState(label: string): string | null {
|
||||
return this._state[label];
|
||||
}
|
||||
|
||||
/**
|
||||
* @property labels
|
||||
*
|
||||
* Gets the list of labels associated with the monitor
|
||||
*/
|
||||
public get labels(): Array<string> {
|
||||
return this._matcher.labeledDescriptors
|
||||
.map((descriptor) => descriptor.label)
|
||||
.filter(
|
||||
(label: string, index: number, array: Array<string>) =>
|
||||
array.indexOf(label) === index
|
||||
);
|
||||
}
|
||||
|
||||
public get stateChangedEvent(): Event<this, IStateChangeEventArgs> {
|
||||
return this._stateChangedEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method startStreams
|
||||
*
|
||||
* Starts monitoring rtsp streams
|
||||
*/
|
||||
public startStreams() {
|
||||
for (const key in this._streamsByRoom) {
|
||||
for (const stream of this._streamsByRoom[key]) {
|
||||
stream.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @method closeStreams
|
||||
*
|
||||
* Stops monitoring rtsp streams
|
||||
*/
|
||||
public closeStreams() {
|
||||
for (const key in this._streamsByRoom) {
|
||||
for (const stream of this._streamsByRoom[key]) {
|
||||
stream.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onData = async (room: string, args: IStreamEventArgs) => {
|
||||
const input = ((await canvas.loadImage(args.data)) as unknown) as ImageData;
|
||||
const out = faceapi.createCanvasFromMedia(input);
|
||||
const resultsQuery = await faceapi
|
||||
.detectAllFaces(out, getFaceDetectorOptions(this._faceDetectionNet))
|
||||
.withFaceLandmarks()
|
||||
.withFaceDescriptors();
|
||||
|
||||
//Write to output image
|
||||
if (this._config.writeOutput) {
|
||||
await saveFile(this._config.outputDirectory, room + ".jpg", args.data);
|
||||
}
|
||||
|
||||
for (const res of resultsQuery) {
|
||||
const bestMatch = this._matcher.matchDescriptor(res.descriptor);
|
||||
const old = this._state[bestMatch.label];
|
||||
this._state[bestMatch.label] = room;
|
||||
this._stateChangedEvent.fire(this, {
|
||||
old: old,
|
||||
new: room,
|
||||
label: bestMatch.label,
|
||||
});
|
||||
|
||||
this._logger.info(`Face Detected: ${bestMatch.label} in room ${room}`);
|
||||
}
|
||||
};
|
||||
|
||||
private onError = (args: IErrorEventArgs, streamName: string) => {
|
||||
this._logger.info(`[${streamName}] ${args.message}`);
|
||||
};
|
||||
private onExit = (streamName: string, args: ICloseEventArgs) => {
|
||||
this._logger.info(`[${streamName}] Stream has exited: ${args.message}`);
|
||||
};
|
||||
}
|
@ -1,259 +0,0 @@
|
||||
import { FaceMatcher } from "@vladmandic/face-api";
|
||||
import { IRoom } from "../config";
|
||||
import {
|
||||
Rtsp,
|
||||
IStreamEventArgs,
|
||||
ICloseEventArgs,
|
||||
IErrorEventArgs,
|
||||
IMessageEventArgs,
|
||||
} from "../rtsp/rtsp";
|
||||
import canvas from "canvas";
|
||||
import * as faceapi from "@vladmandic/face-api";
|
||||
import { getFaceDetectorOptions, saveFile } from "../common";
|
||||
import { nets } from "@vladmandic/face-api";
|
||||
import { Logger } from "homebridge";
|
||||
import { Event } from "../events";
|
||||
import { IConfig } from "../config";
|
||||
import { MonitorState } from "./monitorState";
|
||||
import { IStream } from "./stream";
|
||||
import sharp from "sharp";
|
||||
const { Canvas, Image, ImageData } = canvas;
|
||||
|
||||
const defaultWatchDog = 30000;
|
||||
const defaultRate = 0.7;
|
||||
|
||||
export interface IStateChangeEventArgs {
|
||||
label: string;
|
||||
old: string | null;
|
||||
new: string;
|
||||
}
|
||||
|
||||
export class Monitor {
|
||||
private _state: MonitorState = {};
|
||||
private _streamsByRoom: { [roomName: string]: Array<IStream> } = {};
|
||||
private _faceDetectionNet = nets.ssdMobilenetv1;
|
||||
private _stateChangedEvent: Event<this, IStateChangeEventArgs>;
|
||||
|
||||
constructor(
|
||||
rooms: Array<IRoom>,
|
||||
private _matcher: FaceMatcher,
|
||||
private _logger: Logger,
|
||||
private _config: IConfig
|
||||
) {
|
||||
this._stateChangedEvent = new Event();
|
||||
|
||||
//Initialize state
|
||||
for (const room of rooms) {
|
||||
this._streamsByRoom[room.name] = [
|
||||
...room.rtspConnectionStrings.map((connectionString) => {
|
||||
return this.getNewStream(connectionString, room.name);
|
||||
}),
|
||||
];
|
||||
|
||||
_matcher.labeledDescriptors.forEach((descriptor) => {
|
||||
this._state[descriptor.label] = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @method getState
|
||||
*
|
||||
* @param label The name of the label to retrieve state for
|
||||
*
|
||||
* The last known room of the requested label
|
||||
*/
|
||||
public getState(label: string): string | null {
|
||||
return this._state[label];
|
||||
}
|
||||
|
||||
public resetState(label: string): Monitor {
|
||||
this._state[label] = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @property labels
|
||||
*
|
||||
* Gets the list of labels associated with the monitor
|
||||
*/
|
||||
public get labels(): Array<string> {
|
||||
return this._matcher.labeledDescriptors
|
||||
.map((descriptor) => descriptor.label)
|
||||
.filter(
|
||||
(label: string, index: number, array: Array<string>) =>
|
||||
array.indexOf(label) === index
|
||||
);
|
||||
}
|
||||
|
||||
public get stateChangedEvent(): Event<this, IStateChangeEventArgs> {
|
||||
return this._stateChangedEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method startStreams
|
||||
*
|
||||
* Starts monitoring rtsp streams
|
||||
*/
|
||||
public startStreams(): Monitor {
|
||||
for (const key in this._streamsByRoom) {
|
||||
for (const stream of this._streamsByRoom[key]) {
|
||||
//Start stream
|
||||
stream.rtsp.start();
|
||||
|
||||
//Start watchdog timer
|
||||
stream.watchdogTimer = setTimeout(
|
||||
() => this.onWatchdogTimeout(stream, key),
|
||||
this._config.watchdogTimeout ?? defaultWatchDog
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method closeStreams
|
||||
*
|
||||
* Stops monitoring rtsp streams
|
||||
*/
|
||||
public closeStreams(): Monitor {
|
||||
for (const key in this._streamsByRoom) {
|
||||
for (const stream of this._streamsByRoom[key]) {
|
||||
stream.rtsp.close();
|
||||
|
||||
//Stop watchdog timer
|
||||
if (stream.watchdogTimer) {
|
||||
clearTimeout(stream.watchdogTimer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private onData = async (
|
||||
room: string,
|
||||
stream: IStream,
|
||||
args: IStreamEventArgs
|
||||
) => {
|
||||
//Reset watchdog timer for the stream
|
||||
clearTimeout(stream.watchdogTimer!);
|
||||
stream.watchdogTimer = setTimeout(
|
||||
() => this.onWatchdogTimeout(stream, room),
|
||||
this._config.watchdogTimeout ?? 30000
|
||||
);
|
||||
|
||||
const regularizedImgData = await sharp(args.data)
|
||||
.modulate({ brightness: 3 })
|
||||
.sharpen()
|
||||
.toBuffer();
|
||||
|
||||
//Detect faces in image
|
||||
const input = ((await canvas.loadImage(
|
||||
regularizedImgData
|
||||
)) as unknown) as ImageData;
|
||||
const out = faceapi.createCanvasFromMedia(input);
|
||||
const resultsQuery = await faceapi
|
||||
.detectAllFaces(
|
||||
out,
|
||||
getFaceDetectorOptions(this._faceDetectionNet, {
|
||||
confidence: this._config.confidence,
|
||||
})
|
||||
)
|
||||
.withFaceLandmarks()
|
||||
.withFaceDescriptors();
|
||||
|
||||
//Write to output image
|
||||
if (this._config.writeOutput) {
|
||||
await saveFile(
|
||||
this._config.outputDirectory,
|
||||
room + ".jpg",
|
||||
regularizedImgData
|
||||
);
|
||||
}
|
||||
for (const res of resultsQuery) {
|
||||
const bestMatch = this._matcher.findBestMatch(res.descriptor);
|
||||
const old = this._state[bestMatch.label];
|
||||
this._state[bestMatch.label] = room;
|
||||
this._stateChangedEvent.fire(this, {
|
||||
old: old,
|
||||
new: room,
|
||||
label: bestMatch.label,
|
||||
});
|
||||
|
||||
this._logger.info(
|
||||
`Face Detected with ${
|
||||
res.detection.score * 100
|
||||
}% accuracy and a distance of ${bestMatch.distance}: ${
|
||||
bestMatch.label
|
||||
} in room ${room}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private getNewStream(connectionString: string, roomName: string): IStream {
|
||||
const stream = {
|
||||
rtsp: new Rtsp(connectionString, {
|
||||
rate: this._config.rate ?? defaultRate,
|
||||
image: true,
|
||||
}),
|
||||
watchdogTimer: null,
|
||||
detectionTimer: null,
|
||||
connectionString: connectionString,
|
||||
};
|
||||
|
||||
connectionString = this.getRedactedConnectionString(connectionString);
|
||||
|
||||
//Subscribe to rtsp events
|
||||
stream.rtsp.dataEvent.push((sender: Rtsp, args: IStreamEventArgs) =>
|
||||
this.onData(roomName, stream, args)
|
||||
);
|
||||
//Only subscribe to these events if debug
|
||||
if (this._config.debug) {
|
||||
stream.rtsp.messageEvent.push((sender: Rtsp, args: IMessageEventArgs) => {
|
||||
this._logger.info(`[${connectionString}] ${args.message}`);
|
||||
});
|
||||
stream.rtsp.errorEvent.push((sender: Rtsp, args: IErrorEventArgs) => {
|
||||
this._logger.info(`[${connectionString}] ${args.message}`);
|
||||
});
|
||||
stream.rtsp.closeEvent.push((sender: Rtsp, args: ICloseEventArgs) => {
|
||||
this._logger.info(
|
||||
`[${connectionString}] Stream has exited: ${args.message}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
private onWatchdogTimeout = async (stream: IStream, roomName: string) => {
|
||||
this._logger.info(
|
||||
`[${this.getRedactedConnectionString(
|
||||
stream.connectionString
|
||||
)}] Watchdog timeout: restarting stream`
|
||||
);
|
||||
|
||||
//Close and remove old stream
|
||||
stream.rtsp.close();
|
||||
this._streamsByRoom[roomName].splice(
|
||||
this._streamsByRoom[roomName].indexOf(stream),
|
||||
1
|
||||
);
|
||||
|
||||
//Create and add new stream
|
||||
this._streamsByRoom[roomName].push(
|
||||
this.getNewStream(stream.connectionString, roomName)
|
||||
);
|
||||
stream.rtsp.start();
|
||||
};
|
||||
|
||||
private getRedactedConnectionString(connectionString: string) {
|
||||
const pwSepIdx = connectionString.lastIndexOf(":") + 1;
|
||||
const pwEndIdx = connectionString.indexOf("@");
|
||||
return (
|
||||
connectionString.substring(0, pwSepIdx) +
|
||||
connectionString.substring(pwEndIdx)
|
||||
);
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export type MonitorState = { [label: string]: string | null };
|
@ -1,8 +0,0 @@
|
||||
import { Rtsp } from "../rtsp/rtsp";
|
||||
|
||||
export interface IStream {
|
||||
rtsp: Rtsp;
|
||||
connectionString: string;
|
||||
watchdogTimer: NodeJS.Timeout | null;
|
||||
detectionTimer: NodeJS.Timeout | null;
|
||||
}
|
@ -76,9 +76,7 @@ export class Rtsp {
|
||||
|
||||
public start(): void {
|
||||
const argStrings = [
|
||||
`-rtsp_transport tcp`,
|
||||
`-i ${this._connecteionString}`,
|
||||
`-qscale:v 1`,
|
||||
`-r ${this._options.rate ?? 10}`,
|
||||
`-vf mpdecimate,setpts=N/FRAME_RATE/TB`,
|
||||
this._options.image
|
||||
@ -94,20 +92,11 @@ export class Rtsp {
|
||||
}
|
||||
|
||||
this._childProcess.stdout?.on("data", this.onData);
|
||||
|
||||
this._childProcess.stdout?.on("error", (error: Error) =>
|
||||
this._errorEvent.fire(this, { err: error })
|
||||
);
|
||||
this._childProcess.stdout?.on("close", () =>
|
||||
this._closeEvent.fire(this, {
|
||||
message: "Stream closed",
|
||||
})
|
||||
);
|
||||
this._childProcess.stdout?.on("end", () =>
|
||||
this._closeEvent.fire(this, {
|
||||
message: "Stream ended",
|
||||
})
|
||||
this._childProcess.stdout?.on("error", (err) =>
|
||||
console.log("And error occurred" + err)
|
||||
);
|
||||
this._childProcess.stdout?.on("close", () => console.log("Stream closed"));
|
||||
this._childProcess.stdout?.on("end", () => console.log("Stream ended"));
|
||||
|
||||
//Only register this event if there are subscribers
|
||||
if (this._childProcess.stderr && this._messageEvent.length > 0) {
|
||||
|
117
src/trainer.ts
117
src/trainer.ts
@ -1,117 +0,0 @@
|
||||
import * as faceapi from "@vladmandic/face-api";
|
||||
import canvas from "canvas";
|
||||
import fs, { lstatSync } from "fs";
|
||||
import * as path from "path";
|
||||
import { LabeledFaceDescriptors, TNetInput } from "@vladmandic/face-api";
|
||||
import * as mime from "mime-types";
|
||||
import { getFaceDetectorOptions } from "./common";
|
||||
require("@tensorflow/tfjs-node");
|
||||
|
||||
const { Canvas, Image, ImageData } = canvas;
|
||||
//@ts-ignore
|
||||
faceapi.env.monkeyPatch({ Canvas, Image, ImageData });
|
||||
|
||||
export class Trainer {
|
||||
constructor(
|
||||
private _refImageDir: string,
|
||||
private _trainedModelDir: string,
|
||||
private _confidence?: number
|
||||
) {}
|
||||
|
||||
public async train(writeToDisk: boolean): Promise<faceapi.FaceMatcher> {
|
||||
const faceDetectionNet = faceapi.nets.ssdMobilenetv1;
|
||||
await faceDetectionNet.loadFromDisk(path.join(__dirname, "../weights"));
|
||||
await faceapi.nets.faceLandmark68Net.loadFromDisk(
|
||||
path.join(__dirname, "../weights")
|
||||
);
|
||||
await faceapi.nets.faceRecognitionNet.loadFromDisk(
|
||||
path.join(__dirname, "../weights")
|
||||
);
|
||||
|
||||
const options = getFaceDetectorOptions(faceDetectionNet, {
|
||||
confidence: this._confidence,
|
||||
});
|
||||
|
||||
const dirs = fs.readdirSync(this._refImageDir);
|
||||
|
||||
const refs = [];
|
||||
for (const dir of dirs) {
|
||||
const descriptor = new LabeledFaceDescriptors(dir, []);
|
||||
try {
|
||||
await this.getLabeledFaceDescriptorFromDir(
|
||||
path.join(this._refImageDir, dir),
|
||||
descriptor,
|
||||
options
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
if (descriptor) {
|
||||
refs.push(descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
const faceMatcher = new faceapi.FaceMatcher(refs);
|
||||
|
||||
if (writeToDisk) {
|
||||
fs.writeFile(
|
||||
path.join(this._trainedModelDir, "data.json"),
|
||||
JSON.stringify(faceMatcher.toJSON()),
|
||||
"utf8",
|
||||
(err) => {
|
||||
if (err) {
|
||||
console.log(`An error occurred while writing data model to file`);
|
||||
}
|
||||
|
||||
console.log(`Successfully wrote data model to file`);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return faceMatcher;
|
||||
}
|
||||
|
||||
private getLabeledFaceDescriptorFromDir = async (
|
||||
dir: string,
|
||||
labeldFaceDescriptors: LabeledFaceDescriptors,
|
||||
options: faceapi.TinyFaceDetectorOptions | faceapi.SsdMobilenetv1Options
|
||||
): Promise<void> => {
|
||||
if (!lstatSync(dir).isDirectory()) {
|
||||
return;
|
||||
}
|
||||
const files = fs.readdirSync(dir);
|
||||
await Promise.all(
|
||||
files.map(async (file: string) => {
|
||||
const mimeType = mime.contentType(path.extname(path.join(dir, file)));
|
||||
if (!mimeType || !mimeType.startsWith("image")) {
|
||||
return;
|
||||
}
|
||||
console.log(path.join(dir, file));
|
||||
|
||||
try {
|
||||
const referenceImage = (await canvas.loadImage(
|
||||
path.join(dir, file)
|
||||
)) as unknown;
|
||||
|
||||
const descriptor = await faceapi
|
||||
.detectSingleFace(referenceImage as TNetInput, options)
|
||||
.withFaceLandmarks()
|
||||
.withFaceDescriptor();
|
||||
if (!descriptor || !descriptor.descriptor) {
|
||||
throw new Error("No face found");
|
||||
}
|
||||
|
||||
labeldFaceDescriptors.descriptors.push(descriptor.descriptor);
|
||||
} catch (err) {
|
||||
console.log(
|
||||
"An error occurred loading image at " +
|
||||
path.join(dir, file) +
|
||||
": " +
|
||||
err.message
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user