Compare commits
51 Commits
Author | SHA1 | Date |
---|---|---|
|
d86b9d99f7 | |
|
2e021feac5 | |
|
2a786411f7 | |
|
ee66a2f428 | |
|
a4d11cddd0 | |
|
771154cbef | |
|
4c398b9603 | |
|
5e29974839 | |
|
f2d5fa8cf9 | |
|
0b224cce31 | |
|
030b27ba9d | |
|
d56cee6751 | |
|
f7f5db9f41 | |
|
835e19ed18 | |
|
4285be54b7 | |
|
5c1a22fa72 | |
|
37d39d434f | |
|
c7d488d993 | |
|
b48edef250 | |
|
bbcede0b3e | |
|
8f500e0186 | |
|
494708a376 | |
|
e9ace0f5e1 | |
|
1a09004e3f | |
|
a9ab9db892 | |
|
0e8b7909c7 | |
|
401c5cee16 | |
|
3ac460a060 | |
|
d3c157df4d | |
|
f5a341dbc1 | |
|
5cc5e04642 | |
|
82abe8b6d5 | |
|
06bd1ccbd7 | |
|
e7b63126d2 | |
|
bec1d5b979 | |
|
e2e4554031 | |
|
beeffdd8b8 | |
|
fc943644fc | |
|
ecf47a05aa | |
|
6928fdace5 | |
|
de5d6c1ab0 | |
|
b5d95ed963 | |
|
1cf74e13ed | |
|
8026fd88f2 | |
|
85b59f4c21 | |
|
9e39132506 | |
|
5af2b24fe4 | |
|
792a095782 | |
|
32d523b727 | |
|
eedc2783c9 | |
|
f669a39056 |
|
@ -0,0 +1,12 @@
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
[[analyzers]]
|
||||||
|
name = "python"
|
||||||
|
|
||||||
|
[analyzers.meta]
|
||||||
|
runtime_version = "3.x.x"
|
||||||
|
max_line_length = 135
|
||||||
|
|
||||||
|
|
||||||
|
[[analyzers]]
|
||||||
|
name = "docker"
|
|
@ -0,0 +1,45 @@
|
||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/python
|
||||||
|
{
|
||||||
|
"name": "Python 3",
|
||||||
|
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/python:0-3.11",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers-contrib/features/poetry:2": {
|
||||||
|
"version": "latest"
|
||||||
|
},
|
||||||
|
"ghcr.io/devcontainers-contrib/features/nox:2": {
|
||||||
|
"version": "latest"
|
||||||
|
},
|
||||||
|
"ghcr.io/devcontainers/features/github-cli:1": {
|
||||||
|
"version": "latest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
|
// "features": {},
|
||||||
|
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
// "forwardPorts": [],
|
||||||
|
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
"postCreateCommand": "poetry install",
|
||||||
|
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"settings": {},
|
||||||
|
"extensions": [
|
||||||
|
"ms-python.python",
|
||||||
|
"ms-python.vscode-pylance"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mounts": [
|
||||||
|
// Re-use local Git configuration
|
||||||
|
"source=${localEnv:HOME}/.gitconfig,target=/home/vscode/.gitconfig,type=bind,consistency=cached"
|
||||||
|
]
|
||||||
|
|
||||||
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "vscode"
|
||||||
|
}
|
|
@ -1,3 +1,6 @@
|
||||||
Dockerfile
|
Dockerfile
|
||||||
.venv
|
.venv
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
|
*.ipnyb
|
||||||
|
dist/
|
||||||
|
*.pkl
|
|
@ -0,0 +1 @@
|
||||||
|
github: [slashtechno]
|
|
@ -8,12 +8,15 @@
|
||||||
|
|
||||||
name: Upload Python Package
|
name: Upload Python Package
|
||||||
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
|
|
|
@ -4,14 +4,44 @@
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Quick Debug",
|
||||||
|
"type": "python",
|
||||||
|
"request": "launch",
|
||||||
|
"module": "wyzely_detect",
|
||||||
|
"args": [
|
||||||
|
"--run-scale", "0.25", "--view-scale", "0.5", "--no-remove-representations", "--fake-second-source"
|
||||||
|
],
|
||||||
|
"justMyCode": true
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// "name": "Quick, Specific Debug",
|
||||||
|
// "type": "python",
|
||||||
|
// "request": "launch",
|
||||||
|
// "module": "wyzely_detect",
|
||||||
|
// "args": [
|
||||||
|
// "--run-scale", "0.25", "--view-scale", "0.5", "--no-remove-representations", "--detect-object", "person", "--detect-object", "cell phone"
|
||||||
|
// ],
|
||||||
|
// "justMyCode": true
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
// "name": "Python: Module",
|
// "name": "Python: Module",
|
||||||
"name": "Debug Wyzely Detect",
|
"name": "Full Debug",
|
||||||
"type": "python",
|
"type": "python",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "wyzely_detect",
|
"module": "wyzely_detect",
|
||||||
// "justMyCode": true
|
// "justMyCode": true
|
||||||
"justMyCode": false
|
"justMyCode": false
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug --help",
|
||||||
|
"type": "python",
|
||||||
|
"request": "launch",
|
||||||
|
"module": "wyzely_detect",
|
||||||
|
"args": [
|
||||||
|
"--help"
|
||||||
|
],
|
||||||
|
"justMyCode": false
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -1,12 +1,19 @@
|
||||||
FROM python:3.10.5-buster
|
FROM python:3.10.5-buster
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.description "Docker image for running wyzely-detect"
|
||||||
|
LABEL org.opencontainers.image.source "https://github.com/slashtechno/wyzely-detect"
|
||||||
|
|
||||||
RUN apt update && apt install libgl1 -y
|
RUN apt update && apt install libgl1 -y
|
||||||
RUN pip install poetry
|
RUN pip install poetry
|
||||||
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN poetry install
|
RUN poetry install
|
||||||
|
|
||||||
ENTRYPOINT ["poetry", "run", "python", "-m", "wyzely_detect"]
|
# RUN poetry run pip uninstall -y torchvision
|
||||||
|
# RUN poetry run pip install torchvision
|
||||||
|
|
||||||
|
ENTRYPOINT ["poetry", "run", "python", "-m", "--", "wyzely_detect", "--no-display"]
|
143
LICENSE
143
LICENSE
|
@ -1,5 +1,5 @@
|
||||||
GNU GENERAL PUBLIC LICENSE
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
@ -7,17 +7,15 @@
|
||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
software and other kinds of works.
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
share and change all versions of a program--to make sure it remains free
|
share and change all versions of a program--to make sure it remains free
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
software for all its users.
|
||||||
GNU General Public License for most of our software; it applies also to
|
|
||||||
any other work released this way by its authors. You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
||||||
want it, that you can change the software or use pieces of it in new
|
want it, that you can change the software or use pieces of it in new
|
||||||
free programs, and that you know you can do these things.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
Developers that use our General Public Licenses protect your rights
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
you this License which gives you legal permission to copy, distribute
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
and/or modify the software.
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
A secondary benefit of defending all users' freedom is that
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
improvements made in alternate versions of the program, if they
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
receive widespread use, become available for other developers to
|
||||||
or can get the source code. And you must show them these terms so they
|
incorporate. Many developers of free software are heartened and
|
||||||
know their rights.
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
The GNU Affero General Public License is designed specifically to
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
ensure that, in such cases, the modified source code becomes available
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
An older license, called the Affero General Public License and
|
||||||
that there is no warranty for this free software. For both users' and
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
changed, so that their problems will not be attributed erroneously to
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
authors of previous versions.
|
this license.
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
|
@ -72,7 +60,7 @@ modification follow.
|
||||||
|
|
||||||
0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
works, such as semiconductor masks.
|
works, such as semiconductor masks.
|
||||||
|
@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
||||||
the Program, the only way you could satisfy both those terms and this
|
the Program, the only way you could satisfy both those terms and this
|
||||||
License would be to refrain entirely from conveying the Program.
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
permission to link or combine any covered work with a work licensed
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
under version 3 of the GNU General Public License into a single
|
||||||
combined work, and to convey the resulting work. The terms of this
|
combined work, and to convey the resulting work. The terms of this
|
||||||
License will continue to apply to the part which is the covered work,
|
License will continue to apply to the part which is the covered work,
|
||||||
but the special requirements of the GNU Affero General Public License,
|
but the work with which it is combined will remain governed by version
|
||||||
section 13, concerning interaction through a network will apply to the
|
3 of the GNU General Public License.
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
the GNU General Public License from time to time. Such new versions will
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
Each version is given a distinguishing version number. If the
|
||||||
Program specifies that a certain numbered version of the GNU General
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
Public License "or any later version" applies to it, you have the
|
Public License "or any later version" applies to it, you have the
|
||||||
option of following the terms and conditions either of that numbered
|
option of following the terms and conditions either of that numbered
|
||||||
version or of any later version published by the Free Software
|
version or of any later version published by the Free Software
|
||||||
Foundation. If the Program does not specify a version number of the
|
Foundation. If the Program does not specify a version number of the
|
||||||
GNU General Public License, you may choose any version ever published
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
by the Free Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
If the Program specifies that a proxy can decide which future
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
public statement of acceptance of a version permanently authorizes you
|
public statement of acceptance of a version permanently authorizes you
|
||||||
to choose that version for the Program.
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||||
Copyright (C) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU Affero General Public License as published
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU General Public License for more details.
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
If your software can interact with users remotely through a computer
|
||||||
notice like this when it starts in an interactive mode:
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
<program> Copyright (C) <year> <name of author>
|
interface could display a "Source" link that leads users to an archive
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
of the code. There are many ways you could offer source, and different
|
||||||
This is free software, and you are welcome to redistribute it
|
solutions will be better for different programs; see section 13 for the
|
||||||
under certain conditions; type `show c' for details.
|
specific requirements.
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
<https://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
||||||
|
|
29
README.md
29
README.md
|
@ -11,11 +11,16 @@ Recognize faces/objects in a video stream (from a webcam or a security camera) a
|
||||||
|
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
### Poetry/Python
|
### Python
|
||||||
- Camera, either a webcam or a Wyze Cam
|
- Camera, either a webcam or a Wyze Cam
|
||||||
- All RTSP feeds _should_ work, however.
|
- All RTSP feeds _should_ work, however.
|
||||||
|
- **WSL, by default, does not support USB devices.** It is recommended to natively run this, but it is possible to use it on WSL with streams or some workarounds.
|
||||||
- Python 3.10 or 3.11
|
- Python 3.10 or 3.11
|
||||||
- Poetry
|
- Poetry (optional)
|
||||||
|
- Windows or Linux
|
||||||
|
- I've tested this on MacOS - it works on my 2014 MacBook Air but not a 2011 MacBook Pro
|
||||||
|
- Both were upgraded with OpenCore, with the MacBook Air running Monterey and the MacBook Pro running a newer version of MacOS, which may have been the problem
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
- A Wyze Cam
|
- A Wyze Cam
|
||||||
- Any other RTSP feed _should_ work, as mentioned above
|
- Any other RTSP feed _should_ work, as mentioned above
|
||||||
|
@ -28,17 +33,29 @@ Recognize faces/objects in a video stream (from a webcam or a security camera) a
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
### Installation
|
### Installation
|
||||||
|
Cloning the repository is not required when installing from PyPi but is required when installing from source
|
||||||
1. Clone this repo with `git clone https://github.com/slashtechno/wyzely-detect`
|
1. Clone this repo with `git clone https://github.com/slashtechno/wyzely-detect`
|
||||||
2. `cd` into the cloned repository
|
2. `cd` into the cloned repository
|
||||||
3. Then, either install with [Poetry](https://python-poetry.org/) or run with Docker
|
3. Then, either install with [Poetry](https://python-poetry.org/) or run with Docker
|
||||||
|
|
||||||
#### Docker
|
|
||||||
1. Modify to `docker-compose.yml` to achieve desired configuration
|
|
||||||
2. Run in the background with `docker compose up -d
|
|
||||||
|
|
||||||
#### Poetry
|
#### Installing from PyPi with pip (recommended)
|
||||||
|
This assumes you have Python 3.10 or 3.11 installed
|
||||||
|
1. `pip install wyzely-detect`
|
||||||
|
a. You may need to use `pip3` instead of `pip`
|
||||||
|
2. `wyzely-detect`
|
||||||
|
|
||||||
|
#### Poetry (best for GPU support)
|
||||||
1. `poetry install`
|
1. `poetry install`
|
||||||
|
a. For GPU support, use `poetry install -E cuda --with gpu`
|
||||||
2. `poetry run -- wyzely-detect`
|
2. `poetry run -- wyzely-detect`
|
||||||
|
|
||||||
|
#### Docker
|
||||||
|
Running with Docker has the benefit of having easier configuration, the ability to run headlessly, and easy setup of Ntfy and [mrlt8/docker-wyze-bridge](https://github.com/mrlt8/docker-wyze-bridge). However, for now, CPU-only is supported. Contributions are welcome to add GPU support. In addition, Docker is tested a less-tested method of running this program.
|
||||||
|
|
||||||
|
1. Modify to `docker-compose.yml` to achieve desired configuration
|
||||||
|
2. Run in the background with `docker compose up -d`
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
The following are some basic CLI options. Most flags have environment variable equivalents which can be helpful when using Docker.
|
The following are some basic CLI options. Most flags have environment variable equivalents which can be helpful when using Docker.
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
"# cv2.imwrite(str(uuid_path), frame)\n",
|
"# cv2.imwrite(str(uuid_path), frame)\n",
|
||||||
"# dfs = DeepFace.find(img_path=str(uuid_path), db_path = \"faces\")\n",
|
"# dfs = DeepFace.find(img_path=str(uuid_path), db_path = \"faces\")\n",
|
||||||
"# Don't throw an error if no face is detected (enforce_detection=False)\n",
|
"# Don't throw an error if no face is detected (enforce_detection=False)\n",
|
||||||
"dfs = DeepFace.find(frame, db_path = \"faces\", enforce_detection=False, silent=False, model_name=\"ArcFace\", detector_backend=\"opencv\")\n",
|
"dfs = DeepFace.find(frame, db_path = \"faces\", enforce_detection=True, silent=False, model_name=\"ArcFace\", detector_backend=\"opencv\")\n",
|
||||||
"# Get the identity of the person\n",
|
"# Get the identity of the person\n",
|
||||||
"for i, pd_dataframe in enumerate(dfs):\n",
|
"for i, pd_dataframe in enumerate(dfs):\n",
|
||||||
" # Sort the dataframe by confidence\n",
|
" # Sort the dataframe by confidence\n",
|
||||||
|
|
|
@ -6,16 +6,19 @@ services:
|
||||||
container_name: bridge-wyzely-detect
|
container_name: bridge-wyzely-detect
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
image: mrlt8/wyze-bridge:latest
|
image: mrlt8/wyze-bridge:latest
|
||||||
# I think we can remove the ports, since we're using the network
|
# The ports can be removed since we're using the network
|
||||||
# Just an unnecesary security risk
|
# Just an unnecesary security risk to expose them but can be useful for debugging
|
||||||
# ports:
|
# ports:
|
||||||
# - 1935:1935 # RTMP
|
# - 1935:1935 # RTMP
|
||||||
# - 8554:8554 # RTSP (this is really the only one we need)
|
# - 8554:8554 # RTSP (this is really the only one we need)
|
||||||
# - 8888:8888 # HLS
|
# - 8888:8888 # HLS
|
||||||
# - 5000:5000 # WEB-UI
|
# - 5000:5000 # WEB-UI
|
||||||
environment:
|
environment:
|
||||||
- WYZE_EMAIL=${WYZE_EMAIL} # Replace with wyze email
|
# This is a simple configuration without 2FA.
|
||||||
- WYZE_PASSWORD=${WYZE_PASSWORD} # Replace with wyze password
|
# For advanced configuration, including using an API key, see https://github.com/mrlt8/docker-wyze-bridge/wiki/Two-Factor-Authentication
|
||||||
|
# Either replace the following with your Wyze username and password, or set the environment variables
|
||||||
|
- WYZE_EMAIL=${WYZE_EMAIL}
|
||||||
|
- WYZE_PASSWORD=${WYZE_PASSWORD}
|
||||||
networks:
|
networks:
|
||||||
all:
|
all:
|
||||||
ntfy:
|
ntfy:
|
||||||
|
@ -36,18 +39,27 @@ services:
|
||||||
wyzely-detect:
|
wyzely-detect:
|
||||||
container_name: wyzely-detect
|
container_name: wyzely-detect
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# image: ghcr.io/slashtechno/wyzely-detect:latest
|
image: ghcr.io/slashtechno/wyzely-detect:latest
|
||||||
build:
|
# Building from source is also an option
|
||||||
context: .
|
# build:
|
||||||
dockerfile: Dockerfile
|
# context: .
|
||||||
|
# dockerfile: Dockerfile
|
||||||
|
command:
|
||||||
|
- "--ntfy-url"
|
||||||
|
# Replace "wyzely-detect" with the desired notification stream
|
||||||
|
- "http://ntfy:80/wyzely-detect"
|
||||||
|
|
||||||
|
- "--rtsp-url"
|
||||||
|
# Replace "cv" with the desired rtsp stream
|
||||||
|
- "rtsp://bridge:8554/cv"
|
||||||
|
|
||||||
|
# Example second rtsp stream
|
||||||
|
# - "--rtsp-url"
|
||||||
|
# - "rtsp://bridge:8554/camera"
|
||||||
volumes:
|
volumes:
|
||||||
- ./faces:/app/faces
|
- ./faces:/app/faces
|
||||||
networks:
|
networks:
|
||||||
all:
|
all:
|
||||||
environment:
|
|
||||||
- URL=rtsp://bridge:8554/cv
|
|
||||||
- NO_DISPLAY=true
|
|
||||||
- NTFY_URL=http://ntfy:80/wyzely-detect
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- bridge
|
- bridge
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "wyzely-detect"
|
name = "wyzely-detect"
|
||||||
version = "0.1.0"
|
version = "0.2.1"
|
||||||
description = "Recognize faces/objects in a video stream (from a webcam or a security camera) and send notifications to your devices"
|
description = "Recognize faces/objects in a video stream (from a webcam or a security camera) and send notifications to your devices"
|
||||||
authors = ["slashtechno <77907286+slashtechno@users.noreply.github.com>"]
|
authors = ["slashtechno <77907286+slashtechno@users.noreply.github.com>"]
|
||||||
repository = "https://github.com/slashtechno/wyzely-detect"
|
repository = "https://github.com/slashtechno/wyzely-detect"
|
||||||
|
@ -21,17 +21,56 @@ ultralytics = "^8.0.190"
|
||||||
hjson = "^3.1.0"
|
hjson = "^3.1.0"
|
||||||
numpy = "^1.23.2"
|
numpy = "^1.23.2"
|
||||||
|
|
||||||
# https://github.com/python-poetry/poetry/issues/6409
|
# https://github.com/python-poetry/poetry/issues/6409#issuecomment-1911735833
|
||||||
torch = ">=2.0.0, !=2.0.1, !=2.1.0"
|
# If GPU support doesn't work, `poetry install -E cuda --with gpu` will force it to be installed from the GPU PyTorch repo
|
||||||
|
# However, PyPi's `torch` has CUDA 12.1 support by default on Linux, so in that case it should not be needed.
|
||||||
|
torch = [
|
||||||
|
{version = "^2.2.1", source = "pypi", markers = "extra!='cuda' and (platform_system=='Linux' or platform_system=='Darwin')"},
|
||||||
|
{version = "^2.2.1", source = "pytorch-cpu", markers = "extra!='cuda' and platform_system=='Windows'"},
|
||||||
|
]
|
||||||
# https://stackoverflow.com/a/76477590/18270659
|
# https://stackoverflow.com/a/76477590/18270659
|
||||||
# https://discuss.tensorflow.org/t/tensorflow-io-gcs-filesystem-with-windows/18849/4
|
# https://discfuss.tensorflow.org/t/tensorflow-io-gcs-filesystem-with-windows/18849/4
|
||||||
|
# https://github.com/python-poetry/poetry/issues/8271#issuecomment-1712020965
|
||||||
# Might be able to remove this version constraint later
|
# Might be able to remove this version constraint later
|
||||||
tensorflow-io-gcs-filesystem = "0.31.0"
|
# Working versions:
|
||||||
tensorflow = "^2.14.0"
|
# Python version 3.10.12 and 3.10.5 both work
|
||||||
|
# CUDA version - 12.2
|
||||||
|
# cuDNN version - 8.8.1
|
||||||
|
# Installed from Nvidia website - nvidia-cuda-toolkit is not installed, but default PopOS drivers are installed
|
||||||
|
absl-py = "^2.1.0"
|
||||||
|
tensorflow = {version = "^2.13.0", markers = "extra!='cuda'"}
|
||||||
|
# TODO: Change platform to markers
|
||||||
|
tensorflow-macos = { version = "^2.13.0", platform = "darwin", markers = "platform_machine=='arm64'" }
|
||||||
|
tensorflow-intel = { version = "^2.13.0", platform = "win32" }
|
||||||
|
tensorflow-io-gcs-filesystem = [
|
||||||
|
{ version = "< 0.32.0", markers = "platform_system == 'Windows'" }
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
deepface = "^0.0.79"
|
deepface = "^0.0.79"
|
||||||
|
prettytable = "^3.9.0"
|
||||||
|
|
||||||
|
|
||||||
|
[tool.poetry.group.gpu]
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[tool.poetry.group.gpu.dependencies]
|
||||||
|
torch = {version = "^2.2.1", source = "pytorch-cu121", markers = "extra=='cuda'"}
|
||||||
|
tensorflow = {version = "^2.14.0", extras = ["and-cuda"], markers = "extra=='cuda' and platform_system == 'Linux'"}
|
||||||
|
|
||||||
|
[tool.poetry.extras]
|
||||||
|
# Might be better to rename this to nocpu since it's more accurate
|
||||||
|
cuda = []
|
||||||
|
|
||||||
|
[[tool.poetry.source]]
|
||||||
|
name = "pytorch-cpu"
|
||||||
|
url = "https://download.pytorch.org/whl/cpu"
|
||||||
|
priority = "explicit"
|
||||||
|
|
||||||
|
[[tool.poetry.source]]
|
||||||
|
name = "pytorch-cu121"
|
||||||
|
url = "https://download.pytorch.org/whl/cu121"
|
||||||
|
priority = "explicit"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
black = "^23.9.1"
|
black = "^23.9.1"
|
||||||
|
|
|
@ -1,162 +1,22 @@
|
||||||
# import face_recognition
|
# import face_recognition
|
||||||
import cv2
|
|
||||||
import dotenv
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import os
|
import cv2
|
||||||
|
import sys
|
||||||
|
from prettytable import PrettyTable
|
||||||
|
|
||||||
# import hjson as json
|
# import hjson as json
|
||||||
import torch
|
import torch
|
||||||
from ultralytics import YOLO
|
from ultralytics import YOLO
|
||||||
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
from .utils import notify
|
|
||||||
from .utils import utils
|
from .utils import utils
|
||||||
|
from .utils.cli_args import argparser
|
||||||
|
|
||||||
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
|
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||||
args = None
|
args = None
|
||||||
|
|
||||||
objects_and_peoples = {
|
|
||||||
"objects": {},
|
|
||||||
"peoples": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
global objects_and_peoples
|
|
||||||
global args
|
global args
|
||||||
# RUN_BY_COMPOSE = os.getenv("RUN_BY_COMPOSE") # Replace this with code to check for gpu
|
|
||||||
|
|
||||||
if Path(".env").is_file():
|
|
||||||
dotenv.load_dotenv()
|
|
||||||
print("Loaded .env file")
|
|
||||||
else:
|
|
||||||
print("No .env file found")
|
|
||||||
|
|
||||||
# TODO: If possible, move the argparse stuff to a separate file
|
|
||||||
# It's taking up too many lines in this file
|
|
||||||
argparser = argparse.ArgumentParser(
|
|
||||||
prog="Wyzely Detect",
|
|
||||||
description="Recognize faces/objects in a video stream (from a webcam or a security camera) and send notifications to your devices", # noqa: E501
|
|
||||||
epilog=":)",
|
|
||||||
)
|
|
||||||
|
|
||||||
# required='RUN_SCALE' not in os.environ,
|
|
||||||
|
|
||||||
argparser.add_argument(
|
|
||||||
"--run-scale",
|
|
||||||
# Set it to the env RUN_SCALE if it isn't blank, otherwise set it to 0.25
|
|
||||||
default=os.environ["RUN_SCALE"]
|
|
||||||
if "RUN_SCALE" in os.environ and os.environ["RUN_SCALE"] != ""
|
|
||||||
# else 0.25,
|
|
||||||
else 1,
|
|
||||||
type=float,
|
|
||||||
help="The scale to run the detection at, default is 0.25",
|
|
||||||
)
|
|
||||||
argparser.add_argument(
|
|
||||||
"--view-scale",
|
|
||||||
# Set it to the env VIEW_SCALE if it isn't blank, otherwise set it to 0.75
|
|
||||||
default=os.environ["VIEW_SCALE"]
|
|
||||||
if "VIEW_SCALE" in os.environ and os.environ["VIEW_SCALE"] != ""
|
|
||||||
# else 0.75,
|
|
||||||
else 1,
|
|
||||||
type=float,
|
|
||||||
help="The scale to view the detection at, default is 0.75",
|
|
||||||
)
|
|
||||||
|
|
||||||
argparser.add_argument(
|
|
||||||
"--no-display",
|
|
||||||
default=os.environ["NO_DISPLAY"]
|
|
||||||
if "NO_DISPLAY" in os.environ and os.environ["NO_DISPLAY"] != ""
|
|
||||||
else False,
|
|
||||||
action="store_true",
|
|
||||||
help="Don't display the video feed",
|
|
||||||
)
|
|
||||||
|
|
||||||
argparser.add_argument(
|
|
||||||
"--confidence-threshold",
|
|
||||||
default=os.environ["CONFIDENCE_THRESHOLD"]
|
|
||||||
if "CONFIDENCE_THRESHOLD" in os.environ
|
|
||||||
and os.environ["CONFIDENCE_THRESHOLD"] != ""
|
|
||||||
else 0.6,
|
|
||||||
type=float,
|
|
||||||
help="The confidence threshold to use",
|
|
||||||
)
|
|
||||||
|
|
||||||
argparser.add_argument(
|
|
||||||
"--faces-directory",
|
|
||||||
default=os.environ["FACES_DIRECTORY"]
|
|
||||||
if "FACES_DIRECTORY" in os.environ and os.environ["FACES_DIRECTORY"] != ""
|
|
||||||
else "faces",
|
|
||||||
type=str,
|
|
||||||
help="The directory to store the faces. Can either contain images or subdirectories with images, the latter being the preferred method", # noqa: E501
|
|
||||||
)
|
|
||||||
argparser.add_argument(
|
|
||||||
"--detect-object",
|
|
||||||
nargs="*",
|
|
||||||
default=[],
|
|
||||||
type=str,
|
|
||||||
help="The object(s) to detect. Must be something the model is trained to detect",
|
|
||||||
)
|
|
||||||
|
|
||||||
stream_source = argparser.add_mutually_exclusive_group()
|
|
||||||
stream_source.add_argument(
|
|
||||||
"--url",
|
|
||||||
default=os.environ["URL"]
|
|
||||||
if "URL" in os.environ and os.environ["URL"] != ""
|
|
||||||
else None, # noqa: E501
|
|
||||||
type=str,
|
|
||||||
help="The URL of the stream to use",
|
|
||||||
)
|
|
||||||
stream_source.add_argument(
|
|
||||||
"--capture-device",
|
|
||||||
default=os.environ["CAPTURE_DEVICE"]
|
|
||||||
if "CAPTURE_DEVICE" in os.environ and os.environ["CAPTURE_DEVICE"] != ""
|
|
||||||
else 0, # noqa: E501
|
|
||||||
type=int,
|
|
||||||
help="The capture device to use. Can also be a url.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Defaults for the stuff here and down are already set in notify.py.
|
|
||||||
# Setting them here just means that argparse will display the default values as defualt
|
|
||||||
# TODO: Perhaps just remove the default parameter and just add to the help message that the default is set is x
|
|
||||||
# TODO: Make ntfy optional in ntfy.py. Currently, unless there is a local or LAN instance of ntfy, this can't run offline
|
|
||||||
notifcation_services = argparser.add_argument_group("Notification Services")
|
|
||||||
notifcation_services.add_argument(
|
|
||||||
"--ntfy-url",
|
|
||||||
default=os.environ["NTFY_URL"]
|
|
||||||
if "NTFY_URL" in os.environ and os.environ["NTFY_URL"] != ""
|
|
||||||
else "https://ntfy.sh/wyzely-detect",
|
|
||||||
type=str,
|
|
||||||
help="The URL to send notifications to",
|
|
||||||
)
|
|
||||||
|
|
||||||
timers = argparser.add_argument_group("Timers")
|
|
||||||
timers.add_argument(
|
|
||||||
"--detection-duration",
|
|
||||||
default=os.environ["DETECTION_DURATION"]
|
|
||||||
if "DETECTION_DURATION" in os.environ and os.environ["DETECTION_DURATION"] != ""
|
|
||||||
else 2,
|
|
||||||
type=int,
|
|
||||||
help="The duration (in seconds) that an object must be detected for before sending a notification",
|
|
||||||
)
|
|
||||||
timers.add_argument(
|
|
||||||
"--detection-window",
|
|
||||||
default=os.environ["DETECTION_WINDOW"]
|
|
||||||
if "DETECTION_WINDOW" in os.environ and os.environ["DETECTION_WINDOW"] != ""
|
|
||||||
else 15,
|
|
||||||
type=int,
|
|
||||||
help="The time (seconds) before the detection duration resets",
|
|
||||||
)
|
|
||||||
timers.add_argument(
|
|
||||||
"--notification-window",
|
|
||||||
default=os.environ["NOTIFICATION_WINDOW"]
|
|
||||||
if "NOTIFICATION_WINDOW" in os.environ
|
|
||||||
and os.environ["NOTIFICATION_WINDOW"] != ""
|
|
||||||
else 30,
|
|
||||||
type=int,
|
|
||||||
help="The time (seconds) before another notification can be sent",
|
|
||||||
)
|
|
||||||
|
|
||||||
args = argparser.parse_args()
|
args = argparser.parse_args()
|
||||||
|
|
||||||
|
@ -164,149 +24,111 @@ def main():
|
||||||
# https://github.com/ultralytics/ultralytics/issues/3084#issuecomment-1732433168
|
# https://github.com/ultralytics/ultralytics/issues/3084#issuecomment-1732433168
|
||||||
# Currently, I have been unable to set up Poetry to use GPU for Torch
|
# Currently, I have been unable to set up Poetry to use GPU for Torch
|
||||||
for i in range(torch.cuda.device_count()):
|
for i in range(torch.cuda.device_count()):
|
||||||
print(torch.cuda.get_device_properties(i).name)
|
print(f"Using {torch.cuda.get_device_properties(i).name} for pytorch")
|
||||||
if torch.cuda.is_available():
|
if torch.cuda.is_available():
|
||||||
torch.cuda.set_device(0)
|
torch.cuda.set_device(0)
|
||||||
print("Set CUDA device")
|
print("Set CUDA device")
|
||||||
else:
|
else:
|
||||||
print("No CUDA device available, using CPU")
|
print("No CUDA device available, using CPU")
|
||||||
|
# Seems automatically, deepface (tensorflow) tried to use my GPU on Pop!_OS (I did not set up cudnn or anything)
|
||||||
|
# Not sure the best way, in Poetry, to manage GPU libraries so for now, just use CPU
|
||||||
|
if args.force_disable_tensorflow_gpu:
|
||||||
|
print("Forcing tensorflow to use CPU")
|
||||||
|
import tensorflow as tf
|
||||||
|
|
||||||
|
tf.config.set_visible_devices([], "GPU")
|
||||||
|
if tf.config.experimental.list_logical_devices("GPU"):
|
||||||
|
print("GPU disabled unsuccessfully")
|
||||||
|
else:
|
||||||
|
print("GPU disabled successfully")
|
||||||
|
|
||||||
model = YOLO("yolov8n.pt")
|
model = YOLO("yolov8n.pt")
|
||||||
|
|
||||||
# Depending on if the user wants to use a stream or a capture device,
|
# Depending on if the user wants to use a stream or a capture device,
|
||||||
# Set the video capture to the appropriate source
|
# Set the video capture to the appropriate source
|
||||||
if args.url:
|
if not args.rtsp_url and not args.capture_device:
|
||||||
video_capture = cv2.VideoCapture(args.url)
|
print("No stream or capture device set, defaulting to capture device 0")
|
||||||
|
video_sources = {"devices": [cv2.VideoCapture(0)]}
|
||||||
else:
|
else:
|
||||||
video_capture = cv2.VideoCapture(args.capture_device)
|
video_sources = {
|
||||||
|
"streams": [cv2.VideoCapture(url) for url in args.rtsp_url],
|
||||||
|
"devices": [cv2.VideoCapture(device) for device in args.capture_device],
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.fake_second_source:
|
||||||
|
try:
|
||||||
|
video_sources["devices"].append(video_sources["devices"][0])
|
||||||
|
except KeyError:
|
||||||
|
print("No capture device to use as second source. Trying stream.")
|
||||||
|
try:
|
||||||
|
video_sources["devices"].append(video_sources["devices"][0])
|
||||||
|
except KeyError:
|
||||||
|
print("No stream to use as a second source")
|
||||||
|
# When the code tries to resize the nonexistent capture device 1, the program will fail
|
||||||
|
|
||||||
# Eliminate lag by setting the buffer size to 1
|
# Eliminate lag by setting the buffer size to 1
|
||||||
# This makes it so that the video capture will only grab the most recent frame
|
# This makes it so that the video capture will only grab the most recent frame
|
||||||
# However, this means that the video may be choppy
|
# However, this means that the video may be choppy
|
||||||
video_capture.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
# Only do this for streams
|
||||||
|
try:
|
||||||
|
for stream in video_sources["streams"]:
|
||||||
|
stream.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
||||||
|
# If there are no streams, this will throw a KeyError
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Print the resolution of the video
|
# Print out the resolution of the video sources. Ideally, change this so the device ID/url is also printed
|
||||||
print(
|
pretty_table = PrettyTable(field_names=["Source Type", "Resolution"])
|
||||||
f"Video resolution: {video_capture.get(cv2.CAP_PROP_FRAME_WIDTH)}x{video_capture.get(cv2.CAP_PROP_FRAME_HEIGHT)}" # noqa: E501
|
for source_type, sources in video_sources.items():
|
||||||
|
for source in sources:
|
||||||
|
if (
|
||||||
|
source.get(cv2.CAP_PROP_FRAME_WIDTH) == 0
|
||||||
|
or source.get(cv2.CAP_PROP_FRAME_HEIGHT) == 0
|
||||||
|
):
|
||||||
|
message = "Capture for a source failed as resolution is 0x0.\n"
|
||||||
|
if source_type == "streams":
|
||||||
|
message += "Check if the stream URL is correct and if the stream is online."
|
||||||
|
else:
|
||||||
|
message += "Check if the capture device is connected, working, and not in use by another program."
|
||||||
|
print(message)
|
||||||
|
sys.exit(1)
|
||||||
|
pretty_table.add_row(
|
||||||
|
[
|
||||||
|
source_type,
|
||||||
|
f"{source.get(cv2.CAP_PROP_FRAME_WIDTH)}x{source.get(cv2.CAP_PROP_FRAME_HEIGHT)}",
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
print(pretty_table)
|
||||||
print("Beginning video capture...")
|
print("Beginning video capture...")
|
||||||
while True:
|
while True:
|
||||||
# Grab a single frame of video
|
# Grab a single frame of video
|
||||||
ret, frame = video_capture.read()
|
frames = []
|
||||||
# Only process every other frame of video to save time
|
# frames = [source.read() for sources in video_sources.values() for source in sources]
|
||||||
# Resize frame of video to a smaller size for faster recognition processing
|
for list_of_sources in video_sources.values():
|
||||||
run_frame = cv2.resize(frame, (0, 0), fx=args.run_scale, fy=args.run_scale)
|
frames.extend([source.read()[1] for source in list_of_sources])
|
||||||
# view_frame = cv2.resize(frame, (0, 0), fx=args.view_scale, fy=args.view_scale)
|
frames_to_show = []
|
||||||
|
for frame in frames:
|
||||||
results = model(run_frame, verbose=False)
|
frames_to_show.append(
|
||||||
|
utils.process_footage(
|
||||||
path_to_faces = Path(args.faces_directory)
|
frame=frame,
|
||||||
path_to_faces_exists = path_to_faces.is_dir()
|
|
||||||
|
|
||||||
for i, r in enumerate(results):
|
|
||||||
# list of dicts with each dict containing a label, x1, y1, x2, y2
|
|
||||||
plot_boxes = []
|
|
||||||
|
|
||||||
# The following is stuff for people
|
|
||||||
# This is still in the for loop as each result, no matter if anything is detected, will be present.
|
|
||||||
# Thus, there will always be one result (r)
|
|
||||||
|
|
||||||
# Only run if path_to_faces exists
|
|
||||||
# May be better to check every iteration, but this also works
|
|
||||||
if path_to_faces_exists:
|
|
||||||
if face_details := utils.recognize_face(
|
|
||||||
path_to_directory=path_to_faces, run_frame=run_frame
|
|
||||||
):
|
|
||||||
plot_boxes.append(face_details)
|
|
||||||
objects_and_peoples = notify.thing_detected(
|
|
||||||
thing_name=face_details["label"],
|
|
||||||
objects_and_peoples=objects_and_peoples,
|
|
||||||
detection_type="peoples",
|
|
||||||
detection_window=args.detection_window,
|
|
||||||
detection_duration=args.detection_duration,
|
|
||||||
notification_window=args.notification_window,
|
|
||||||
ntfy_url=args.ntfy_url,
|
|
||||||
)
|
|
||||||
|
|
||||||
# The following is stuff for objects
|
|
||||||
# Setup dictionary of object names
|
|
||||||
if (
|
|
||||||
objects_and_peoples["objects"] == {}
|
|
||||||
or objects_and_peoples["objects"] is None
|
|
||||||
):
|
|
||||||
for name in r.names.values():
|
|
||||||
objects_and_peoples["objects"][name] = {
|
|
||||||
"last_detection_time": None,
|
|
||||||
"detection_duration": None,
|
|
||||||
# "first_detection_time": None,
|
|
||||||
"last_notification_time": None,
|
|
||||||
}
|
|
||||||
# Also, make sure that the objects to detect are in the list of objects_and_peoples
|
|
||||||
# If it isn't, print a warning
|
|
||||||
for obj in args.detect_object:
|
|
||||||
if obj not in objects_and_peoples:
|
|
||||||
print(
|
|
||||||
f"Warning: {obj} is not in the list of objects the model can detect!"
|
|
||||||
)
|
|
||||||
|
|
||||||
for box in r.boxes:
|
|
||||||
# Get the name of the object
|
|
||||||
class_id = r.names[box.cls[0].item()]
|
|
||||||
# Get the coordinates of the object
|
|
||||||
cords = box.xyxy[0].tolist()
|
|
||||||
cords = [round(x) for x in cords]
|
|
||||||
# Get the confidence
|
|
||||||
conf = round(box.conf[0].item(), 2)
|
|
||||||
# Print it out, adding a spacer between each object
|
|
||||||
# print("Object type:", class_id)
|
|
||||||
# print("Coordinates:", cords)
|
|
||||||
# print("Probability:", conf)
|
|
||||||
# print("---")
|
|
||||||
|
|
||||||
# Now do stuff (if conf > 0.5)
|
|
||||||
if conf < args.confidence_threshold or (
|
|
||||||
class_id not in args.detect_object and args.detect_object != []
|
|
||||||
):
|
|
||||||
# If the confidence is too low
|
|
||||||
# or if the object is not in the list of objects to detect and the list of objects to detect is not empty
|
|
||||||
# then skip this iteration
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Add the object to the list of objects to plot
|
|
||||||
plot_boxes.append(
|
|
||||||
{
|
|
||||||
"label": class_id,
|
|
||||||
"x1": cords[0],
|
|
||||||
"y1": cords[1],
|
|
||||||
"x2": cords[2],
|
|
||||||
"y2": cords[3],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
objects_and_peoples = notify.thing_detected(
|
|
||||||
thing_name=class_id,
|
|
||||||
objects_and_peoples=objects_and_peoples,
|
|
||||||
detection_type="objects",
|
|
||||||
detection_window=args.detection_window,
|
|
||||||
detection_duration=args.detection_duration,
|
|
||||||
notification_window=args.notification_window,
|
|
||||||
ntfy_url=args.ntfy_url,
|
|
||||||
)
|
|
||||||
|
|
||||||
# To debug plotting, use r.plot() to cross reference the bounding boxes drawn by the plot_label() and r.plot()
|
|
||||||
frame_to_show = utils.plot_label(
|
|
||||||
boxes=plot_boxes,
|
|
||||||
full_frame=frame,
|
|
||||||
# full_frame=r.plot(),
|
|
||||||
run_scale=args.run_scale,
|
run_scale=args.run_scale,
|
||||||
view_scale=args.view_scale,
|
view_scale=args.view_scale,
|
||||||
|
faces_directory=Path(args.faces_directory),
|
||||||
|
face_confidence_threshold=args.face_confidence_threshold,
|
||||||
|
no_remove_representations=args.no_remove_representations,
|
||||||
|
detection_window=args.detection_window,
|
||||||
|
detection_duration=args.detection_duration,
|
||||||
|
notification_window=args.notification_window,
|
||||||
|
ntfy_url=args.ntfy_url,
|
||||||
|
model=model,
|
||||||
|
detect_object=args.detect_object,
|
||||||
|
object_confidence_threshold=args.object_confidence_threshold,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Display the resulting frame
|
# Display the resulting frame
|
||||||
# cv2.imshow("", r)
|
|
||||||
if not args.no_display:
|
if not args.no_display:
|
||||||
cv2.imshow(f"Video{i}", frame_to_show)
|
for i, frame_to_show in enumerate(frames_to_show):
|
||||||
|
cv2.imshow(f"Video {i}", frame_to_show)
|
||||||
|
|
||||||
# Hit 'q' on the keyboard to quit!
|
# Hit 'q' on the keyboard to quit!
|
||||||
if cv2.waitKey(1) & 0xFF == ord("q"):
|
if cv2.waitKey(1) & 0xFF == ord("q"):
|
||||||
|
@ -314,7 +136,7 @@ def main():
|
||||||
|
|
||||||
# Release handle to the webcam
|
# Release handle to the webcam
|
||||||
print("Releasing video capture")
|
print("Releasing video capture")
|
||||||
video_capture.release()
|
[source.release() for sources in video_sources.values() for source in sources]
|
||||||
cv2.destroyAllWindows()
|
cv2.destroyAllWindows()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,198 @@
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import dotenv
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
argparser = None
|
||||||
|
|
||||||
|
|
||||||
|
def set_argparse():
|
||||||
|
global argparser
|
||||||
|
|
||||||
|
if Path(".env").is_file():
|
||||||
|
dotenv.load_dotenv()
|
||||||
|
print("Loaded .env file")
|
||||||
|
else:
|
||||||
|
print("No .env file found")
|
||||||
|
|
||||||
|
# One important thing to consider is that most function parameters are optional and have a default value
|
||||||
|
# However, with argparse, those are never used since a argparse always passes something, even if it's None
|
||||||
|
argparser = argparse.ArgumentParser(
|
||||||
|
prog="Wyzely Detect",
|
||||||
|
description="Recognize faces/objects in a video stream (from a webcam or a security camera) and send notifications to your devices", # noqa: E501
|
||||||
|
epilog="For env bool options, setting them to anything except for an empty string will enable them.",
|
||||||
|
)
|
||||||
|
|
||||||
|
video_options = argparser.add_argument_group("Video Options")
|
||||||
|
stream_source = video_options.add_mutually_exclusive_group()
|
||||||
|
stream_source.add_argument(
|
||||||
|
"--rtsp-url",
|
||||||
|
action="append",
|
||||||
|
# If RTSP_URL is in the environment, use it, otherwise just use a blank list
|
||||||
|
# This may cause problems down the road, but if it does, env for this can be removed
|
||||||
|
default=[os.environ["RTSP_URL"]]
|
||||||
|
if "RTSP_URL" in os.environ and os.environ["RTSP_URL"] != ""
|
||||||
|
else [],
|
||||||
|
type=str,
|
||||||
|
help="RTSP camera URL",
|
||||||
|
)
|
||||||
|
stream_source.add_argument(
|
||||||
|
"--capture-device",
|
||||||
|
action="append",
|
||||||
|
# If CAPTURE_DEVICE is in the environment, use it, otherwise just use a blank list
|
||||||
|
# If __main__.py detects that no capture device or remote stream is set, it will default to 0
|
||||||
|
default=[int(os.environ["CAPTURE_DEVICE"])]
|
||||||
|
if "CAPTURE_DEVICE" in os.environ and os.environ["CAPTURE_DEVICE"] != ""
|
||||||
|
else [],
|
||||||
|
type=int,
|
||||||
|
help="Capture device number",
|
||||||
|
)
|
||||||
|
video_options.add_argument(
|
||||||
|
"--run-scale",
|
||||||
|
# Set it to the env RUN_SCALE if it isn't blank, otherwise set it to 0.25
|
||||||
|
default=os.environ["RUN_SCALE"]
|
||||||
|
if "RUN_SCALE" in os.environ and os.environ["RUN_SCALE"] != ""
|
||||||
|
# else 0.25,
|
||||||
|
else 1,
|
||||||
|
type=float,
|
||||||
|
help="The scale to run the detection at, default is 0.25",
|
||||||
|
)
|
||||||
|
video_options.add_argument(
|
||||||
|
"--view-scale",
|
||||||
|
# Set it to the env VIEW_SCALE if it isn't blank, otherwise set it to 0.75
|
||||||
|
default=os.environ["VIEW_SCALE"]
|
||||||
|
if "VIEW_SCALE" in os.environ and os.environ["VIEW_SCALE"] != ""
|
||||||
|
# else 0.75,
|
||||||
|
else 1,
|
||||||
|
type=float,
|
||||||
|
help="The scale to view the detection at, default is 0.75",
|
||||||
|
)
|
||||||
|
|
||||||
|
video_options.add_argument(
|
||||||
|
"--no-display",
|
||||||
|
default=os.environ["NO_DISPLAY"]
|
||||||
|
if "NO_DISPLAY" in os.environ
|
||||||
|
and os.environ["NO_DISPLAY"] != ""
|
||||||
|
and os.environ["NO_DISPLAY"].lower() != "false"
|
||||||
|
else False,
|
||||||
|
action="store_true",
|
||||||
|
help="Don't display the video feed",
|
||||||
|
)
|
||||||
|
video_options.add_argument(
|
||||||
|
"-c",
|
||||||
|
"--force-disable-tensorflow-gpu",
|
||||||
|
default=os.environ["FORCE_DISABLE_TENSORFLOW_GPU"]
|
||||||
|
if "FORCE_DISABLE_TENSORFLOW_GPU" in os.environ
|
||||||
|
and os.environ["FORCE_DISABLE_TENSORFLOW_GPU"] != ""
|
||||||
|
and os.environ["FORCE_DISABLE_TENSORFLOW_GPU"].lower() != "false"
|
||||||
|
else False,
|
||||||
|
action="store_true",
|
||||||
|
help="Force disable tensorflow GPU through env since sometimes it's not worth it to install cudnn and whatnot",
|
||||||
|
)
|
||||||
|
|
||||||
|
notifcation_services = argparser.add_argument_group("Notification Services")
|
||||||
|
notifcation_services.add_argument(
|
||||||
|
"--ntfy-url",
|
||||||
|
default=os.environ["NTFY_URL"]
|
||||||
|
if "NTFY_URL" in os.environ and os.environ["NTFY_URL"] != ""
|
||||||
|
# This is None but there is a default set in notify.py
|
||||||
|
else None,
|
||||||
|
type=str,
|
||||||
|
help="The URL to send notifications to",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Various timers
|
||||||
|
timers = argparser.add_argument_group("Timers")
|
||||||
|
timers.add_argument(
|
||||||
|
"--detection-duration",
|
||||||
|
default=os.environ["DETECTION_DURATION"]
|
||||||
|
if "DETECTION_DURATION" in os.environ and os.environ["DETECTION_DURATION"] != ""
|
||||||
|
else 2,
|
||||||
|
type=int,
|
||||||
|
help="The duration (in seconds) that an object must be detected for before sending a notification",
|
||||||
|
)
|
||||||
|
timers.add_argument(
|
||||||
|
"--detection-window",
|
||||||
|
default=os.environ["DETECTION_WINDOW"]
|
||||||
|
if "DETECTION_WINDOW" in os.environ and os.environ["DETECTION_WINDOW"] != ""
|
||||||
|
else 15,
|
||||||
|
type=int,
|
||||||
|
help="The time (seconds) before the detection duration resets",
|
||||||
|
)
|
||||||
|
timers.add_argument(
|
||||||
|
"--notification-window",
|
||||||
|
default=os.environ["NOTIFICATION_WINDOW"]
|
||||||
|
if "NOTIFICATION_WINDOW" in os.environ
|
||||||
|
and os.environ["NOTIFICATION_WINDOW"] != ""
|
||||||
|
else 30,
|
||||||
|
type=int,
|
||||||
|
help="The time (seconds) before another notification can be sent",
|
||||||
|
)
|
||||||
|
|
||||||
|
face_recognition = argparser.add_argument_group("Face Recognition options")
|
||||||
|
face_recognition.add_argument(
|
||||||
|
"--faces-directory",
|
||||||
|
default=os.environ["FACES_DIRECTORY"]
|
||||||
|
if "FACES_DIRECTORY" in os.environ and os.environ["FACES_DIRECTORY"] != ""
|
||||||
|
else "faces",
|
||||||
|
type=str,
|
||||||
|
help="The directory to store the faces. Can either contain images or subdirectories with images, the latter being the preferred method", # noqa: E501
|
||||||
|
)
|
||||||
|
face_recognition.add_argument(
|
||||||
|
"--face-confidence-threshold",
|
||||||
|
default=os.environ["FACE_CONFIDENCE_THRESHOLD"]
|
||||||
|
if "FACE_CONFIDENCE_THRESHOLD" in os.environ
|
||||||
|
and os.environ["FACE_CONFIDENCE_THRESHOLD"] != ""
|
||||||
|
else 0.3,
|
||||||
|
type=float,
|
||||||
|
help="The confidence (currently cosine similarity) threshold to use for face recognition",
|
||||||
|
)
|
||||||
|
face_recognition.add_argument(
|
||||||
|
"--no-remove-representations",
|
||||||
|
default=os.environ["NO_REMOVE_REPRESENTATIONS"]
|
||||||
|
if "NO_REMOVE_REPRESENTATIONS" in os.environ
|
||||||
|
and os.environ["NO_REMOVE_REPRESENTATIONS"] != ""
|
||||||
|
and os.environ["NO_REMOVE_REPRESENTATIONS"].lower() != "false"
|
||||||
|
else False,
|
||||||
|
action="store_true",
|
||||||
|
help="Don't remove representations_<model>.pkl at the start of the program. Greatly improves startup time, but doesn't take into account changes to the faces directory since it was created", # noqa: E501
|
||||||
|
)
|
||||||
|
|
||||||
|
object_detection = argparser.add_argument_group("Object Detection options")
|
||||||
|
object_detection.add_argument(
|
||||||
|
"--detect-object",
|
||||||
|
action="append",
|
||||||
|
# Stuff is appended to default, as far as I can tell
|
||||||
|
default=[],
|
||||||
|
type=str,
|
||||||
|
help="The object(s) to detect. Must be something the model is trained to detect",
|
||||||
|
)
|
||||||
|
object_detection.add_argument(
|
||||||
|
"--object-confidence-threshold",
|
||||||
|
default=os.environ["OBJECT_CONFIDENCE_THRESHOLD"]
|
||||||
|
if "OBJECT_CONFIDENCE_THRESHOLD" in os.environ
|
||||||
|
and os.environ["OBJECT_CONFIDENCE_THRESHOLD"] != ""
|
||||||
|
# I think this should always be a str so using lower shouldn't be a problem.
|
||||||
|
# Also, if the first check fails the rest shouldn't be run
|
||||||
|
and os.environ["OBJECT_CONFIDENCE_THRESHOLD"].lower() != "false" else 0.6,
|
||||||
|
type=float,
|
||||||
|
help="The confidence threshold to use",
|
||||||
|
)
|
||||||
|
|
||||||
|
debug = argparser.add_argument_group("Debug options")
|
||||||
|
debug.add_argument(
|
||||||
|
"--fake-second-source",
|
||||||
|
help="Duplicate the first source and use it as a second source. Capture device takes priority.",
|
||||||
|
action="store_true",
|
||||||
|
default=os.environ["FAKE_SECOND_SOURCE"]
|
||||||
|
if "FAKE_SECOND_SOURCE" in os.environ
|
||||||
|
and os.environ["FAKE_SECOND_SOURCE"] != ""
|
||||||
|
and os.environ["FAKE_SECOND_SOURCE"].lower() != "false"
|
||||||
|
else False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# return argparser
|
||||||
|
|
||||||
|
|
||||||
|
# This will run when this file is imported
|
||||||
|
set_argparse()
|
|
@ -104,6 +104,11 @@ def thing_detected(
|
||||||
):
|
):
|
||||||
respective_type[thing_name]["last_notification_time"] = time.time()
|
respective_type[thing_name]["last_notification_time"] = time.time()
|
||||||
print(f"Detected {thing_name} for {detection_duration} seconds")
|
print(f"Detected {thing_name} for {detection_duration} seconds")
|
||||||
|
if ntfy_url is None:
|
||||||
|
print(
|
||||||
|
"ntfy_url is None. Not sending notification. Set ntfy_url to send notifications"
|
||||||
|
)
|
||||||
|
else:
|
||||||
headers = construct_ntfy_headers(
|
headers = construct_ntfy_headers(
|
||||||
title=f"{thing_name} detected",
|
title=f"{thing_name} detected",
|
||||||
tag="rotating_light",
|
tag="rotating_light",
|
||||||
|
|
|
@ -1,10 +1,163 @@
|
||||||
import cv2
|
import cv2
|
||||||
|
import os
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from deepface import DeepFace
|
|
||||||
|
# https://stackoverflow.com/a/42121886/18270659
|
||||||
|
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
|
||||||
|
|
||||||
|
|
||||||
|
from deepface import DeepFace # noqa: E402
|
||||||
|
from . import notify # noqa: E402
|
||||||
|
|
||||||
first_face_try = True
|
first_face_try = True
|
||||||
|
|
||||||
|
# TODO: When multi-camera support is ~~added~~ improved, this will need to be changed so that each camera has its own dict
|
||||||
|
objects_and_peoples = {
|
||||||
|
"objects": {},
|
||||||
|
"peoples": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def process_footage(
|
||||||
|
# Frame
|
||||||
|
frame: np.ndarray = None,
|
||||||
|
# scale
|
||||||
|
run_scale: float = None,
|
||||||
|
view_scale: float = None,
|
||||||
|
# Face stuff
|
||||||
|
faces_directory: str = None,
|
||||||
|
face_confidence_threshold: float = None,
|
||||||
|
no_remove_representations: bool = False,
|
||||||
|
# Timer stuff
|
||||||
|
detection_window: int = None,
|
||||||
|
detection_duration: int = None,
|
||||||
|
notification_window: int = None,
|
||||||
|
ntfy_url: str = None,
|
||||||
|
# Object stuff
|
||||||
|
# YOLO object
|
||||||
|
model=None,
|
||||||
|
detect_object: list = None,
|
||||||
|
object_confidence_threshold=None,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Takes in a frame and processes it"""
|
||||||
|
global objects_and_peoples
|
||||||
|
|
||||||
|
# Resize frame of video to a smaller size for faster recognition processing
|
||||||
|
run_frame = cv2.resize(frame, (0, 0), fx=run_scale, fy=run_scale)
|
||||||
|
# view_frame = cv2.resize(frame, (0, 0), fx=args.view_scale, fy=args.view_scale)
|
||||||
|
|
||||||
|
results = model(run_frame, verbose=False)
|
||||||
|
|
||||||
|
path_to_faces = Path(faces_directory)
|
||||||
|
path_to_faces_exists = path_to_faces.is_dir()
|
||||||
|
|
||||||
|
for r in results:
|
||||||
|
# list of dicts with each dict containing a label, x1, y1, x2, y2
|
||||||
|
plot_boxes = []
|
||||||
|
|
||||||
|
# The following is stuff for people
|
||||||
|
# This is still in the for loop as each result, no matter if anything is detected, will be present.
|
||||||
|
# Thus, there will always be one result (r)
|
||||||
|
|
||||||
|
# Only run if path_to_faces exists
|
||||||
|
# May be better to check every iteration, but this also works
|
||||||
|
if path_to_faces_exists:
|
||||||
|
if face_details := recognize_face(
|
||||||
|
path_to_directory=path_to_faces,
|
||||||
|
run_frame=run_frame,
|
||||||
|
# Perhaps make these names match?
|
||||||
|
min_confidence=face_confidence_threshold,
|
||||||
|
no_remove_representations=no_remove_representations,
|
||||||
|
):
|
||||||
|
plot_boxes.append(face_details)
|
||||||
|
objects_and_peoples = notify.thing_detected(
|
||||||
|
thing_name=face_details["label"],
|
||||||
|
objects_and_peoples=objects_and_peoples,
|
||||||
|
detection_type="peoples",
|
||||||
|
detection_window=detection_window,
|
||||||
|
detection_duration=detection_duration,
|
||||||
|
notification_window=notification_window,
|
||||||
|
ntfy_url=ntfy_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
# The following is stuff for objects
|
||||||
|
# Setup dictionary of object names
|
||||||
|
if (
|
||||||
|
objects_and_peoples["objects"] == {}
|
||||||
|
or objects_and_peoples["objects"] is None
|
||||||
|
):
|
||||||
|
for name in r.names.values():
|
||||||
|
objects_and_peoples["objects"][name] = {
|
||||||
|
"last_detection_time": None,
|
||||||
|
"detection_duration": None,
|
||||||
|
# "first_detection_time": None,
|
||||||
|
"last_notification_time": None,
|
||||||
|
}
|
||||||
|
# Also, make sure that the objects to detect are in the list of objects_and_peoples
|
||||||
|
# If it isn't, print a warning
|
||||||
|
for obj in detect_object:
|
||||||
|
# .keys() shouldn't be needed
|
||||||
|
if obj not in objects_and_peoples["objects"]:
|
||||||
|
print(
|
||||||
|
f"Warning: {obj} is not in the list of objects the model can detect!"
|
||||||
|
)
|
||||||
|
|
||||||
|
for box in r.boxes:
|
||||||
|
# Get the name of the object
|
||||||
|
class_id = r.names[box.cls[0].item()]
|
||||||
|
# Get the coordinates of the object
|
||||||
|
cords = box.xyxy[0].tolist()
|
||||||
|
cords = [round(x) for x in cords]
|
||||||
|
# Get the confidence
|
||||||
|
conf = round(box.conf[0].item(), 2)
|
||||||
|
# Print it out, adding a spacer between each object
|
||||||
|
# print("Object type:", class_id)
|
||||||
|
# print("Coordinates:", cords)
|
||||||
|
# print("Probability:", conf)
|
||||||
|
# print("---")
|
||||||
|
|
||||||
|
# Now do stuff (if conf > 0.5)
|
||||||
|
if conf < object_confidence_threshold or (
|
||||||
|
class_id not in detect_object and detect_object != []
|
||||||
|
):
|
||||||
|
# If the confidence is too low
|
||||||
|
# or if the object is not in the list of objects to detect and the list of objects to detect is not empty
|
||||||
|
# then skip this iteration
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add the object to the list of objects to plot
|
||||||
|
plot_boxes.append(
|
||||||
|
{
|
||||||
|
"label": class_id,
|
||||||
|
"x1": cords[0],
|
||||||
|
"y1": cords[1],
|
||||||
|
"x2": cords[2],
|
||||||
|
"y2": cords[3],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
objects_and_peoples = notify.thing_detected(
|
||||||
|
thing_name=class_id,
|
||||||
|
objects_and_peoples=objects_and_peoples,
|
||||||
|
detection_type="objects",
|
||||||
|
detection_window=detection_window,
|
||||||
|
detection_duration=detection_duration,
|
||||||
|
notification_window=notification_window,
|
||||||
|
ntfy_url=ntfy_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
# To debug plotting, use r.plot() to cross reference the bounding boxes drawn by the plot_label() and r.plot()
|
||||||
|
frame_to_show = plot_label(
|
||||||
|
boxes=plot_boxes,
|
||||||
|
full_frame=frame,
|
||||||
|
# full_frame=r.plot(),
|
||||||
|
run_scale=run_scale,
|
||||||
|
view_scale=view_scale,
|
||||||
|
)
|
||||||
|
# Unsure if this should also return the objects_and_peoples dict
|
||||||
|
return frame_to_show
|
||||||
|
|
||||||
|
|
||||||
def plot_label(
|
def plot_label(
|
||||||
# list of dicts with each dict containing a label, x1, y1, x2, y2
|
# list of dicts with each dict containing a label, x1, y1, x2, y2
|
||||||
|
@ -18,7 +171,7 @@ def plot_label(
|
||||||
# So the coordinates will be scaled appropriately when coming from run_frame
|
# So the coordinates will be scaled appropriately when coming from run_frame
|
||||||
view_scale: float = None,
|
view_scale: float = None,
|
||||||
font: int = cv2.FONT_HERSHEY_SIMPLEX,
|
font: int = cv2.FONT_HERSHEY_SIMPLEX,
|
||||||
):
|
) -> np.ndarray:
|
||||||
# x1 and y1 are the top left corner of the box
|
# x1 and y1 are the top left corner of the box
|
||||||
# x2 and y2 are the bottom right corner of the box
|
# x2 and y2 are the bottom right corner of the box
|
||||||
# Example scaling: full_frame: 1 run_frame: 0.5 view_frame: 0.25
|
# Example scaling: full_frame: 1 run_frame: 0.5 view_frame: 0.25
|
||||||
|
@ -68,6 +221,8 @@ def recognize_face(
|
||||||
path_to_directory: Path = Path("faces"),
|
path_to_directory: Path = Path("faces"),
|
||||||
# opencv image
|
# opencv image
|
||||||
run_frame: np.ndarray = None,
|
run_frame: np.ndarray = None,
|
||||||
|
min_confidence: float = 0.3,
|
||||||
|
no_remove_representations: bool = False,
|
||||||
) -> np.ndarray:
|
) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
Accepts a path to a directory of images of faces to be used as a refference
|
Accepts a path to a directory of images of faces to be used as a refference
|
||||||
|
@ -75,7 +230,8 @@ def recognize_face(
|
||||||
|
|
||||||
Returns a single dictonary as currently only 1 face can be detected in each frame
|
Returns a single dictonary as currently only 1 face can be detected in each frame
|
||||||
Cosine threshold is 0.3, so if the confidence is less than that, it will return None
|
Cosine threshold is 0.3, so if the confidence is less than that, it will return None
|
||||||
dict contains the following keys: label, x1, y1, x2, y2
|
dict conta # Maybe use os.exit() instead?
|
||||||
|
ins the following keys: label, x1, y1, x2, y2
|
||||||
The directory should be structured as follows:
|
The directory should be structured as follows:
|
||||||
faces/
|
faces/
|
||||||
name/
|
name/
|
||||||
|
@ -94,13 +250,16 @@ def recognize_face(
|
||||||
global first_face_try
|
global first_face_try
|
||||||
|
|
||||||
# If it's the first time the function is being run, remove representations_arcface.pkl, if it exists
|
# If it's the first time the function is being run, remove representations_arcface.pkl, if it exists
|
||||||
if first_face_try:
|
if first_face_try and not no_remove_representations:
|
||||||
try:
|
try:
|
||||||
path_to_directory.joinpath("representations_arcface.pkl").unlink()
|
path_to_directory.joinpath("representations_arcface.pkl").unlink()
|
||||||
print("Removing representations_arcface.pkl")
|
print("Removing representations_arcface.pkl")
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print("representations_arcface.pkl does not exist")
|
print("representations_arcface.pkl does not exist")
|
||||||
first_face_try = False
|
first_face_try = False
|
||||||
|
elif first_face_try and no_remove_representations:
|
||||||
|
print("Not attempting to remove representations_arcface.pkl")
|
||||||
|
first_face_try = False
|
||||||
|
|
||||||
# face_dataframes is a vanilla list of dataframes
|
# face_dataframes is a vanilla list of dataframes
|
||||||
# It seems face_dataframes is empty if the face database (directory) doesn't exist. Seems to work if it's empty though
|
# It seems face_dataframes is empty if the face database (directory) doesn't exist. Seems to work if it's empty though
|
||||||
|
@ -119,6 +278,10 @@ def recognize_face(
|
||||||
model_name="ArcFace",
|
model_name="ArcFace",
|
||||||
detector_backend="opencv",
|
detector_backend="opencv",
|
||||||
)
|
)
|
||||||
|
'''
|
||||||
|
Example dataframe, for reference
|
||||||
|
identity (path to image) | source_x | source_y | source_w | source_h | VGG-Face_cosine (pretty much the confidence \\_('_')_/)
|
||||||
|
'''
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
if (
|
if (
|
||||||
str(e)
|
str(e)
|
||||||
|
@ -126,6 +289,14 @@ def recognize_face(
|
||||||
):
|
):
|
||||||
# print("No faces recognized") # For debugging
|
# print("No faces recognized") # For debugging
|
||||||
return None
|
return None
|
||||||
|
elif (
|
||||||
|
# Check if the error message contains "Validate .jpg or .png files exist in this path."
|
||||||
|
"Validate .jpg or .png files exist in this path."
|
||||||
|
in str(e)
|
||||||
|
):
|
||||||
|
# If a verbose/silent flag is added, this should be changed to print only if verbose is true
|
||||||
|
# print("No faces found in database")
|
||||||
|
return None
|
||||||
else:
|
else:
|
||||||
raise e
|
raise e
|
||||||
# Iteate over the dataframes
|
# Iteate over the dataframes
|
||||||
|
@ -133,8 +304,13 @@ def recognize_face(
|
||||||
# The last row is the highest confidence
|
# The last row is the highest confidence
|
||||||
# So we can just grab the path from there
|
# So we can just grab the path from there
|
||||||
# iloc = Integer LOCation
|
# iloc = Integer LOCation
|
||||||
|
try:
|
||||||
path_to_image = Path(df.iloc[-1]["identity"])
|
path_to_image = Path(df.iloc[-1]["identity"])
|
||||||
# If the parent name is the same as the path to the database, then set label to the image name instead of the parent directory name
|
# Seems this is caused when someone steps into frame and their face is detected but not recognized
|
||||||
|
except IndexError:
|
||||||
|
print("Face present but not recognized")
|
||||||
|
continue
|
||||||
|
# If the parent name is the same as the path to the database, then set label to the image name instead of the parent name
|
||||||
if path_to_image.parent == Path(path_to_directory):
|
if path_to_image.parent == Path(path_to_directory):
|
||||||
label = path_to_image.name
|
label = path_to_image.name
|
||||||
else:
|
else:
|
||||||
|
@ -149,19 +325,13 @@ def recognize_face(
|
||||||
"y2": df.iloc[-1]["source_y"] + df.iloc[-1]["source_h"],
|
"y2": df.iloc[-1]["source_y"] + df.iloc[-1]["source_h"],
|
||||||
}
|
}
|
||||||
# After some brief testing, it seems positive matches are > 0.3
|
# After some brief testing, it seems positive matches are > 0.3
|
||||||
distance = df.iloc[-1]["ArcFace_cosine"]
|
cosine_similarity = df.iloc[-1]["ArcFace_cosine"]
|
||||||
# TODO: Make this a CLI argument
|
if cosine_similarity < min_confidence:
|
||||||
if distance < 0.3:
|
|
||||||
return None
|
return None
|
||||||
# if 0.5 < distance < 0.7:
|
|
||||||
# label = "Unknown"
|
# label = "Unknown"
|
||||||
to_return = dict(label=label, **coordinates)
|
to_return = dict(label=label, **coordinates)
|
||||||
print(
|
print(
|
||||||
f"Confindence: {distance}, filname: {path_to_image.name}, to_return: {to_return}"
|
f"Cosine similarity: {cosine_similarity}, filname: {path_to_image.name}, to_return: {to_return}"
|
||||||
)
|
)
|
||||||
return to_return
|
return to_return
|
||||||
|
return None
|
||||||
"""
|
|
||||||
Example dataframe, for reference
|
|
||||||
identity (path to image) | source_x | source_y | source_w | source_h | VGG-Face_cosine (pretty much the confidence \_('_')_/)
|
|
||||||
"""
|
|
||||||
|
|
Loading…
Reference in New Issue