Skip to content

Commit

Permalink
Merge pull request #60 from jakowenko/beta
Browse files Browse the repository at this point in the history
v0.8.0
  • Loading branch information
jakowenko authored Jul 2, 2021
2 parents c2fd8dd + 9df223d commit dc2b373
Show file tree
Hide file tree
Showing 24 changed files with 556 additions and 214 deletions.
38 changes: 19 additions & 19 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,26 @@ module.exports = {
env: {
amd: true,
},
extends: ["airbnb-base", "plugin:prettier/recommended"],
plugins: ["prettier"],
extends: ['airbnb-base', 'plugin:prettier/recommended'],
plugins: ['prettier'],
rules: {
"linebreak-style": 0,
"no-console": 0,
"no-plusplus": 0,
"max-len": 0,
"no-return-assign": 0,
"no-await-in-loop": 0,
'linebreak-style': 0,
'no-console': 0,
'no-plusplus': 0,
'max-len': 0,
'no-return-assign': 0,
'no-await-in-loop': 0,
indent: 0, // Allowing prettier to handle this
"consistent-return": 0,
"comma-dangle": 0,
"operator-linebreak": 0,
"implicit-arrow-linebreak": 0,
"function-paren-newline": 0,
"object-curly-newline": 0,
"newline-per-chained-call": 0,
"prettier/prettier": "error",
"no-param-reassign": 0,
"no-restricted-syntax": 0,
"no-nested-ternary": 0,
'consistent-return': 0,
'comma-dangle': 0,
'operator-linebreak': 0,
'implicit-arrow-linebreak': 0,
'function-paren-newline': 0,
'object-curly-newline': 0,
'newline-per-chained-call': 0,
'prettier/prettier': 'error',
'no-param-reassign': 0,
'no-restricted-syntax': 0,
'no-nested-ternary': 0,
},
};
7 changes: 4 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
id: version
uses: notiz-dev/github-action-json-property@release
with:
path: ./api/package.json
path: ./package.json
prop_path: version
- name: Set ENV to beta
if: endsWith(github.ref, '/beta')
Expand All @@ -36,8 +36,9 @@ jobs:
echo "VERSION=${{steps.version.outputs.prop}}" >> $GITHUB_ENV
- name: Version with SHA-7
run: |
cd ./api && npm version ${{steps.version.outputs.prop}}-$(echo ${GITHUB_SHA} | cut -c1-7)
cd ../frontend && npm version ${{steps.version.outputs.prop}}-$(echo ${GITHUB_SHA} | cut -c1-7)
npm version ${{steps.version.outputs.prop}}-$(echo ${GITHUB_SHA} | cut -c1-7) --no-git-tag-version
cd ./api && npm version ${{steps.version.outputs.prop}}-$(echo ${GITHUB_SHA} | cut -c1-7) --no-git-tag-version
cd ../frontend && npm version ${{steps.version.outputs.prop}}-$(echo ${GITHUB_SHA} | cut -c1-7) --no-git-tag-version
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
Expand Down
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
[![Double Take](https://badgen.net/github/release/jakowenko/double-take/stable)](https://github.com/jakowenko/double-take) [![Double Take](https://badgen.net/github/stars/jakowenko/double-take)](https://github.com/jakowenko/double-take/stargazers) [![Docker Pulls](https://flat.badgen.net/docker/pulls/jakowenko/double-take)](https://hub.docker.com/r/jakowenko/double-take)
[![Double Take](https://badgen.net/github/release/jakowenko/double-take/stable)](https://github.com/jakowenko/double-take) [![Double Take](https://badgen.net/github/stars/jakowenko/double-take)](https://github.com/jakowenko/double-take/stargazers) [![Docker Pulls](https://flat.badgen.net/docker/pulls/jakowenko/double-take)](https://hub.docker.com/r/jakowenko/double-take) [![Discord](https://flat.badgen.net/discord/members/3pumsskdN5?label=Discord)](https://discord.gg/3pumsskdN5)

# Double Take

Unified UI and API for processing and training images for facial recognition.

<p align="center">
<img src="https://user-images.githubusercontent.com/1081811/124193519-f4ae2300-da94-11eb-9720-15f2e7579355.jpg" width="100%">
</p>

## Why?

There's a lot of great open source software to perform facial recognition, but each of them behave differently. Double Take was created to abstract the complexities of the detection services and combine them into an easy to use UI and API.
Expand All @@ -16,7 +20,7 @@ There's a lot of great open source software to perform facial recognition, but e

### Supported NVRs

- [Frigate](https://github.com/blakeblackshear/frigate) v0.8.0-0.8.4
- [Frigate](https://github.com/blakeblackshear/frigate) v0.8.0-0.9.0

## Use Cases

Expand Down Expand Up @@ -84,10 +88,6 @@ notify:

The UI is accessible from `http://localhost:3000/#/`.

<p align="center">
<img src="https://user-images.githubusercontent.com/1081811/118581518-c633ed00-b75f-11eb-9c9d-77535484787d.png" width="80%">
</p>

### `/#/config`

Make changes to the configuration and restart the API.
Expand Down Expand Up @@ -334,7 +334,7 @@ Render match image.

## MQTT

Publish results to `double-take/matches/${name}` and `double-take/cameras/${camera}`.
Publish results to `double-take/matches/${name}` and `double-take/cameras/${camera}`. The number of results will also be published to `double-take/cameras/${camera}/person` and will reset back to `0` after 30 seconds.

```yaml
mqtt:
Expand Down Expand Up @@ -477,6 +477,7 @@ detectors:
key: xxx-xxx-xxx-xxx-xxx # key from recognition service in created app
deepstack:
url: http://192.168.1.1:8001
key: xxx-xxx-xxx-xxx-xxx # optional api key
facebox:
url: http://192.168.1.1:8002
Expand Down Expand Up @@ -513,6 +514,7 @@ time:
| detectors.compreface.key | | API Key for CompreFace collection |
| detectors.compreface.face_plugins | | Comma-separated slugs of [face plugins](https://github.com/exadel-inc/CompreFace/blob/master/docs/Face-services-and-plugins.md) |
| detectors.deepstack.url | | Base URL for DeepStack API |
| detectors.deepstack.key | | API Key for DeepStack |
| detectors.facebox.url | | Base URL for Facebox API |
| notify.gotify.url | | Base URL for Gotify |
| notify.gotify.token | | Gotify application token Gotify |
Expand Down
2 changes: 1 addition & 1 deletion api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "double-take-api",
"version": "0.7.0",
"version": "0.8.0",
"description": "Unified UI and API for processing and training images for facial recognition",
"scripts": {
"start": "node server.js",
Expand Down
9 changes: 6 additions & 3 deletions api/src/controllers/recognize.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@ module.exports.test = async (req, res) => {
try {
const promises = [];
for (const [detector] of Object.entries(lowercaseKeys(DETECTORS))) {
promises.push(actions.recognize({ detector, key: `${__dirname}/../static/img/lenna.png` }));
promises.push(
actions.recognize({ detector, test: true, key: `${__dirname}/../static/img/lenna.jpg` })
);
}
const results = await Promise.all(promises);
const output = results.map((result, i) => ({
detector: Object.entries(lowercaseKeys(DETECTORS))[i][0],
status: result.status,
response: result.data,
}));
respond(HTTPError(OK, output), res);
Expand All @@ -50,10 +53,10 @@ module.exports.start = async (req, res) => {
};

if (event.type === 'frigate') {
const { type } = req.body;
const { type: frigateEventType } = req.body;
const attributes = req.body.after ? req.body.after : req.body.before;
const { id, label, camera, current_zones: zones } = attributes;
event = { id, label, camera, zones, type, ...event };
event = { id, label, camera, zones, frigateEventType, ...event };
} else {
const { url, camera } = req.query;

Expand Down
Binary file added api/src/static/img/lenna.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed api/src/static/img/lenna.png
Binary file not shown.
3 changes: 2 additions & 1 deletion api/src/util/detectors/actions/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const factory = require('../factory');

module.exports.recognize = ({ detector, key }) => factory.get(detector).recognize(key);
module.exports.recognize = ({ detector, test, key }) =>
factory.get(detector).recognize({ test, key });
module.exports.train = ({ name, key, detector }) => factory.get(detector).train({ name, key });
module.exports.remove = ({ name, detector }) => factory.get(detector).remove({ name });
module.exports.normalize = ({ detector, data }) => factory.get(detector).normalize({ data });
6 changes: 5 additions & 1 deletion api/src/util/detectors/compreface.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
const axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');
const { doesUrlResolve } = require('../validators.util');
const { CONFIDENCE, DETECTORS, OBJECTS } = require('../../constants');

module.exports.config = () => {
return DETECTORS.COMPREFACE;
};

module.exports.recognize = (key) => {
module.exports.recognize = async ({ test, key }) => {
const { URL, KEY, FACE_PLUGINS } = this.config();
if (test && !(await doesUrlResolve(URL))) {
return { status: 404 };
}
const formData = new FormData();
formData.append('file', fs.createReadStream(key));
return axios({
Expand Down
16 changes: 12 additions & 4 deletions api/src/util/detectors/deepstack.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
const axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');
const { doesUrlResolve } = require('../validators.util');
const { CONFIDENCE, DETECTORS, OBJECTS } = require('../../constants');

module.exports.config = () => {
return DETECTORS.DEEPSTACK;
};

module.exports.recognize = (key) => {
const { URL } = this.config();
module.exports.recognize = async ({ test, key }) => {
const { URL, KEY } = this.config();
if (test && !(await doesUrlResolve(URL))) {
return { status: 404 };
}
const formData = new FormData();
formData.append('image', fs.createReadStream(key));
if (KEY) formData.append('api_key', KEY);
return axios({
method: 'post',
headers: {
Expand All @@ -25,10 +30,11 @@ module.exports.recognize = (key) => {
};

module.exports.train = ({ name, key }) => {
const { URL } = this.config();
const { URL, KEY } = this.config();
const formData = new FormData();
formData.append('image', fs.createReadStream(key));
formData.append('userid', name);
if (KEY) formData.append('api_key', KEY);
return axios({
method: 'post',
headers: {
Expand All @@ -40,9 +46,10 @@ module.exports.train = ({ name, key }) => {
};

module.exports.remove = ({ name }) => {
const { URL } = this.config();
const { URL, KEY } = this.config();
const formData = new FormData();
formData.append('userid', name);
if (KEY) formData.append('api_key', KEY);
return axios({
method: 'post',
url: `${URL}/v1/vision/face/delete`,
Expand All @@ -57,6 +64,7 @@ module.exports.remove = ({ name }) => {
};

module.exports.normalize = ({ data }) => {
if (data.success === false) return [];
const { MIN_AREA_MATCH } = OBJECTS.FACE;
const normalized = data.predictions.map((obj) => {
const confidence = parseFloat((obj.confidence * 100).toFixed(2));
Expand Down
6 changes: 5 additions & 1 deletion api/src/util/detectors/facebox.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
const axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');
const { doesUrlResolve } = require('../validators.util');
const { DETECTORS, CONFIDENCE, OBJECTS } = require('../../constants');

module.exports.config = () => {
return DETECTORS.FACEBOX;
};

module.exports.recognize = (key) => {
module.exports.recognize = async ({ test, key }) => {
const { URL } = this.config();
if (test && !(await doesUrlResolve(URL))) {
return { status: 404 };
}
const formData = new FormData();
formData.append('file', fs.createReadStream(key));
return axios({
Expand Down
14 changes: 11 additions & 3 deletions api/src/util/frigate.util.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@ const { FRIGATE } = require('../constants');

const frigate = this;

module.exports.checks = async ({ id, type, label, camera, zones, PROCESSING, IDS }) => {
module.exports.checks = async ({
id,
frigateEventType: type,
label,
camera,
zones,
PROCESSING,
IDS,
}) => {
try {
if (!FRIGATE.URL) {
return `Frigate URL not configured`;
}

await frigate.status();

if (FRIGATE.CAMERAS && !FRIGATE.CAMERAS.includes(camera)) {
return `${id} - ${camera} not on approved list`;
}
Expand Down Expand Up @@ -48,6 +54,8 @@ module.exports.checks = async ({ id, type, label, camera, zones, PROCESSING, IDS
return `already processed ${id}`;
}

await frigate.status();

return true;
} catch (error) {
throw new Error(error.message);
Expand Down
9 changes: 9 additions & 0 deletions api/src/util/helpers.util.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
module.exports.contains = (a, b) => {
return !(
b.left < a.left ||
b.top < a.top ||
b.left + b.width > a.left + a.width ||
b.top + b.height > a.top + a.height
);
};

module.exports.lowercaseKeys = (obj) =>
Object.keys(obj).reduce((acc, key) => {
acc[key.toLowerCase()] = obj[key];
Expand Down
25 changes: 25 additions & 0 deletions api/src/util/mqtt.util.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ const axios = require('axios');
const mqtt = require('mqtt');
const fs = require('fs');
const logger = require('./logger.util');
const { contains } = require('./helpers.util');
const { SERVER, MQTT, FRIGATE, CAMERAS } = require('../constants');

let previousMqttLengths = [];
let justSubscribed = false;
let client = false;
const personResetTimeout = {};

const cameraTopics = () => {
return CAMERAS
Expand Down Expand Up @@ -135,6 +137,29 @@ module.exports.publish = (data) => {
{ retain: true }
);
}

let personCount = matches.length;
// check to see if unknown bounding box is contained within or contains any of the match bounding boxes
// if false, then add 1 to the person count
if (matches.length && unknown && Object.keys(unknown).length) {
let unknownContained = false;
matches.forEach((match) => {
if (contains(match.box, unknown.box) || contains(unknown.box, match.box))
unknownContained = true;
});
if (!unknownContained) personCount += 1;
}

client.publish(`${MQTT.TOPICS.CAMERAS}/${camera}/person`, personCount.toString(), {
retain: true,
});

clearTimeout(personResetTimeout[camera]);
personResetTimeout[camera] = setTimeout(() => {
client.publish(`${MQTT.TOPICS.CAMERAS}/${camera}/person`, '0', {
retain: true,
});
}, 30000);
} catch (error) {
logger.log(`MQTT: publish error: ${error.message}`);
}
Expand Down
18 changes: 18 additions & 0 deletions api/src/util/validators.util.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const axios = require('axios');
const logger = require('./logger.util');
const { expressValidator } = require('./validate.util');

const { query, param /* , body */ } = expressValidator;
Expand Down Expand Up @@ -76,3 +78,19 @@ module.exports.tryParseJSON = (json) => {

return false;
};

module.exports.doesUrlResolve = async (url) => {
try {
const instance = axios.create({
timeout: 1000,
});
const data = await instance({
method: 'get',
url,
});
return data;
} catch (error) {
logger.log(`url resolve error: ${error.message}`);
return false;
}
};
Loading

0 comments on commit dc2b373

Please sign in to comment.