Compare commits
423 Commits
v0.1.1
...
v0.8.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ad75a441d | ||
|
|
f006e9be8d | ||
|
|
03f3ba8008 | ||
|
|
96a44eb7bf | ||
|
|
006782fe3d | ||
|
|
ff3e95bbf7 | ||
|
|
4b95a37e65 | ||
|
|
38c661b3a8 | ||
|
|
0d6e4f6a66 | ||
|
|
1ad2219f1c | ||
|
|
dfcdd289c3 | ||
|
|
32f5f2cca9 | ||
|
|
24bfe9f3e8 | ||
|
|
004667dc99 | ||
|
|
9d785dc781 | ||
|
|
cbba5a7af0 | ||
|
|
29b29ee349 | ||
|
|
9ad53e09af | ||
|
|
c9278991c9 | ||
|
|
729de48934 | ||
|
|
7476bff5fb | ||
|
|
1e9eae8d9a | ||
|
|
8113a53381 | ||
|
|
72833686f1 | ||
|
|
096c21f105 | ||
|
|
181f66357b | ||
|
|
a54fbc483c | ||
|
|
92d5a002d3 | ||
|
|
f9184903d7 | ||
|
|
91cde6ce7b | ||
|
|
186a4587c7 | ||
|
|
6049acb1f3 | ||
|
|
2d2ebf313c | ||
|
|
3d329dcb52 | ||
|
|
06854fc34f | ||
|
|
e01e14d866 | ||
|
|
3dfd251ebb | ||
|
|
dcea807f77 | ||
|
|
87d83ff33a | ||
|
|
1d31cbdf0d | ||
|
|
e05b27b8dc | ||
|
|
7111bd208e | ||
|
|
04a80280da | ||
|
|
3bda092140 | ||
|
|
9086820479 | ||
|
|
d1da57aedc | ||
|
|
6ded12c566 | ||
|
|
70352566a7 | ||
|
|
cf5cc86588 | ||
|
|
e41db49ab8 | ||
|
|
1b7effafee | ||
|
|
69e9e0b0bf | ||
|
|
89624df411 | ||
|
|
d1a7405211 | ||
|
|
040f8c7c20 | ||
|
|
6d7acabf4c | ||
|
|
45a8b42157 | ||
|
|
8785be24b7 | ||
|
|
cc0812540c | ||
|
|
5cf38ca4f7 | ||
|
|
7e4395c30e | ||
|
|
598d3aeda2 | ||
|
|
012dbf81f7 | ||
|
|
f869def12e | ||
|
|
31f7666337 | ||
|
|
9e339acbca | ||
|
|
8f8054a299 | ||
|
|
f7021eec4c | ||
|
|
c124153da4 | ||
|
|
706c2f921e | ||
|
|
de1d66bcb9 | ||
|
|
4502ca8e80 | ||
|
|
32a66fe5e8 | ||
|
|
e1251aafdb | ||
|
|
587494068c | ||
|
|
7a4d90a47a | ||
|
|
d06b587d33 | ||
|
|
eef70e434b | ||
|
|
b39da3ee01 | ||
|
|
e07c4e0d8c | ||
|
|
2f41ba6f77 | ||
|
|
bf95af0f22 | ||
|
|
2e15847f86 | ||
|
|
5992e85dc8 | ||
|
|
24d416b869 | ||
|
|
5dbf368c4b | ||
|
|
7d56fe105f | ||
|
|
e9327aa18c | ||
|
|
df56e079de | ||
|
|
8c5bfbd187 | ||
|
|
2613e74f97 | ||
|
|
9a7fb96357 | ||
|
|
37f9dfed92 | ||
|
|
68c1544808 | ||
|
|
2b3d3c5824 | ||
|
|
efea87a3ea | ||
|
|
977785fb10 | ||
|
|
4e113e62c0 | ||
|
|
5080b2d781 | ||
|
|
5cfd6d1edb | ||
|
|
27ae4d8ab0 | ||
|
|
3db33302ec | ||
|
|
f2910d48e0 | ||
|
|
cf0f8892e2 | ||
|
|
4d22e172ff | ||
|
|
8874a55b0f | ||
|
|
24b703a875 | ||
|
|
8b8f5b5c40 | ||
|
|
eac81136d2 | ||
|
|
d1e27b43ea | ||
|
|
105dcb7094 | ||
|
|
c0a16efdc1 | ||
|
|
2800c54743 | ||
|
|
2a24e8abcb | ||
|
|
37ee746ebb | ||
|
|
7ee6bfe855 | ||
|
|
40f57a8754 | ||
|
|
e0da462223 | ||
|
|
47a9fc4292 | ||
|
|
03fe5158db | ||
|
|
72be6b480d | ||
|
|
a8964dcc1f | ||
|
|
732e91ee42 | ||
|
|
27da080ce6 | ||
|
|
075d06b108 | ||
|
|
95dc17ffcd | ||
|
|
408b53f8b4 | ||
|
|
3ef68a297a | ||
|
|
3e9b3711dc | ||
|
|
a1cc9ad1f0 | ||
|
|
29e8aa4020 | ||
|
|
777aff403f | ||
|
|
4b3b702459 | ||
|
|
893e6b40a7 | ||
|
|
a85d780020 | ||
|
|
34439699ae | ||
|
|
64b63142b1 | ||
|
|
cee1ab000b | ||
|
|
3ff98770c1 | ||
|
|
244203463d | ||
|
|
b6f7940b10 | ||
|
|
75312602aa | ||
|
|
75977128f0 | ||
|
|
eafde6c677 | ||
|
|
da0598baef | ||
|
|
35ba5e2f7c | ||
|
|
49258d6dbe | ||
|
|
5a081e4f00 | ||
|
|
4feae472e9 | ||
|
|
4e83239258 | ||
|
|
c4cccf44a5 | ||
|
|
64e7cbcc62 | ||
|
|
dd86e4f317 | ||
|
|
4db285a875 | ||
|
|
939d1ba091 | ||
|
|
0fe8d486d9 | ||
|
|
a3cb02af5c | ||
|
|
45a6b8452c | ||
|
|
9d594cc640 | ||
|
|
59e41ae1ac | ||
|
|
c6ed16465b | ||
|
|
8f14b36f5a | ||
|
|
b6c2491e3b | ||
|
|
8e31d04d90 | ||
|
|
bf93fbb357 | ||
|
|
c064b244db | ||
|
|
0280610e96 | ||
|
|
4363623c45 | ||
|
|
c960914ec3 | ||
|
|
9ecc80b443 | ||
|
|
3e146de0a2 | ||
|
|
bee54c39dc | ||
|
|
623d138d60 | ||
|
|
76befc1249 | ||
|
|
51251b9fb0 | ||
|
|
8c45076bb6 | ||
|
|
7d683ef399 | ||
|
|
e4da3822b1 | ||
|
|
12c4cd77c5 | ||
|
|
a611cbb942 | ||
|
|
f946813ccb | ||
|
|
49fca1b839 | ||
|
|
54cb4a2180 | ||
|
|
9954e3b11e | ||
|
|
82692b0ddc | ||
|
|
9d4fdec12f | ||
|
|
ed72c995ef | ||
|
|
66c77d1157 | ||
|
|
40c322ad47 | ||
|
|
83f1e0d713 | ||
|
|
2d89044bd3 | ||
|
|
dc4d24c2b9 | ||
|
|
d5fb20c524 | ||
|
|
7e92e8bfe8 | ||
|
|
efdcfcef97 | ||
|
|
574ee2a46f | ||
|
|
ec4d048905 | ||
|
|
b063099b2a | ||
|
|
2937dac4c3 | ||
|
|
7c283a1805 | ||
|
|
309c0dcda3 | ||
|
|
b35cc01035 | ||
|
|
6e79a5402e | ||
|
|
a989f8daaf | ||
|
|
7880d24b29 | ||
|
|
fdc8bbf72d | ||
|
|
005e188d38 | ||
|
|
adcc3e9b98 | ||
|
|
5fe201da25 | ||
|
|
974f7bd0df | ||
|
|
780ae7cd4f | ||
|
|
50e568b84c | ||
|
|
1ce993051e | ||
|
|
69406343ee | ||
|
|
1c33b8acb2 | ||
|
|
5e77436d39 | ||
|
|
e26308a05b | ||
|
|
c16ee3186f | ||
|
|
fedeeab561 | ||
|
|
bfcaabecfa | ||
|
|
606fa6f6d5 | ||
|
|
6a8d8bf53d | ||
|
|
1f81cba706 | ||
|
|
5db7b242aa | ||
|
|
0b7f65e227 | ||
|
|
2f758af097 | ||
|
|
f64320a464 | ||
|
|
3e87ef6426 | ||
|
|
acb75fa02d | ||
|
|
ea4ecae27c | ||
|
|
a8556a729b | ||
|
|
068df3ef2d | ||
|
|
b304139db2 | ||
|
|
df2aae5169 | ||
|
|
351ac4ec7d | ||
|
|
12e40291c0 | ||
|
|
8af7d51159 | ||
|
|
84ada716ac | ||
|
|
cbcc89be9c | ||
|
|
73a5e11b9b | ||
|
|
194baaeb56 | ||
|
|
469259d663 | ||
|
|
f3db69d975 | ||
|
|
0914cb71ad | ||
|
|
0ae2806eb4 | ||
|
|
adcfe699c2 | ||
|
|
e5048f98b6 | ||
|
|
e6c6338266 | ||
|
|
1f03c8cb8c | ||
|
|
69f5249788 | ||
|
|
3a1f1c946b | ||
|
|
d88745af6e | ||
|
|
709d917f0c | ||
|
|
918386bdc1 | ||
|
|
a8c0fadf95 | ||
|
|
6dc7b8f246 | ||
|
|
71f6f0bee4 | ||
|
|
a00afb61c0 | ||
|
|
5dbe6c5f36 | ||
|
|
16732aa5b3 | ||
|
|
3d2f1437e4 | ||
|
|
fbe721c860 | ||
|
|
7383db60b0 | ||
|
|
53ccc903da | ||
|
|
9d1f9f35e5 | ||
|
|
c1f522ff54 | ||
|
|
b345571a63 | ||
|
|
f29ee6165f | ||
|
|
ec6432cc5f | ||
|
|
8c917667b6 | ||
|
|
941434b8d8 | ||
|
|
2d0632adf8 | ||
|
|
f1afaf641a | ||
|
|
743116a733 | ||
|
|
8e77cf25d9 | ||
|
|
7d33e03943 | ||
|
|
0c44666c89 | ||
|
|
ddaa746807 | ||
|
|
760e1ffe1d | ||
|
|
15b4024715 | ||
|
|
918112a793 | ||
|
|
4ee200a81c | ||
|
|
e37eba49ff | ||
|
|
6de8e3bd1f | ||
|
|
3a9781c4f8 | ||
|
|
a60b9211d2 | ||
|
|
777fb1d5d1 | ||
|
|
8e9110f42e | ||
|
|
c80137e059 | ||
|
|
2768e1dadb | ||
|
|
2fbba01577 | ||
|
|
e7c536ea31 | ||
|
|
1734c0569a | ||
|
|
a5bef89123 | ||
|
|
d8aa73d26e | ||
|
|
791409d5e5 | ||
|
|
01bf89907d | ||
|
|
8e73c7e95e | ||
|
|
088bd18adb | ||
|
|
2e8c7ec225 | ||
|
|
9340a74371 | ||
|
|
5998de610b | ||
|
|
dfabff3846 | ||
|
|
76a7a3bad5 | ||
|
|
a3fa97dd52 | ||
|
|
1d2a41129c | ||
|
|
956298128d | ||
|
|
e6892d66b8 | ||
|
|
6ef22cf578 | ||
|
|
3e6f6edf7e | ||
|
|
81c5b96ed7 | ||
|
|
6f6d202c99 | ||
|
|
2fc389c3ad | ||
|
|
05951aa7da | ||
|
|
bb8e4621f5 | ||
|
|
04e9ab5ce4 | ||
|
|
1089a40943 | ||
|
|
68c3a069ba | ||
|
|
80b9652f7a | ||
|
|
569e07949f | ||
|
|
ffa9534549 | ||
|
|
c539993387 | ||
|
|
8a572f96d5 | ||
|
|
24cb3508e8 | ||
|
|
3f34c57e31 | ||
|
|
4c618daa90 | ||
|
|
cd057370e1 | ||
|
|
6263912655 | ||
|
|
af247275cf | ||
|
|
1198c29dac | ||
|
|
169603d3ff | ||
|
|
dc7eecebc6 | ||
|
|
0dd4087d5d | ||
|
|
6ecf87fc60 | ||
|
|
ebcf1482f8 | ||
|
|
50bcf60893 | ||
|
|
38efbd63ea | ||
|
|
50bcad8b77 | ||
|
|
cfffb219ae | ||
|
|
382d7be50a | ||
|
|
f43dc36a37 | ||
|
|
38e7fa07d2 | ||
|
|
e261c20819 | ||
|
|
3a66e672d3 | ||
|
|
2aada930e3 | ||
|
|
d87f4407a0 | ||
|
|
be5a114f6a | ||
|
|
32b212c7b6 | ||
|
|
76c8e3a12f | ||
|
|
16f7a361c3 | ||
|
|
634b87307f | ||
|
|
1d4fbbdba3 | ||
|
|
65579e9cbf | ||
|
|
49dc029c43 | ||
|
|
08174d8db2 | ||
|
|
5199242a68 | ||
|
|
725dd3220c | ||
|
|
10dc56f6ea | ||
|
|
cc2abe93a6 | ||
|
|
0c6717090c | ||
|
|
f5a2252b29 | ||
|
|
02efb6f415 | ||
|
|
5b4c6e50bc | ||
|
|
9cc46a71cb | ||
|
|
be1673b00a | ||
|
|
b6130e77ff | ||
|
|
4180c710cd | ||
|
|
ab3e70b4db | ||
|
|
d90e408d50 | ||
|
|
6c87ce0879 | ||
|
|
b7b4e38f62 | ||
|
|
480175d70f | ||
|
|
bee99ca6ff | ||
|
|
5c01720567 | ||
|
|
262f45c8bc | ||
|
|
22bb17b2fd | ||
|
|
3a3afe14bf | ||
|
|
01f058a482 | ||
|
|
d899ef158e | ||
|
|
39d64f7ba7 | ||
|
|
f148eb5a7b | ||
|
|
297e2f1c0c | ||
|
|
e818744d81 | ||
|
|
ceedfae993 | ||
|
|
e13563770d | ||
|
|
a659019d1a | ||
|
|
ba71927d53 | ||
|
|
04fed31eac | ||
|
|
ebaa8fac01 | ||
|
|
2ec45cd1b6 | ||
|
|
700bd1e3ef | ||
|
|
c9e9f7a735 | ||
|
|
aea4dc8724 | ||
|
|
12d5007b90 | ||
|
|
8970e73f75 | ||
|
|
1ba006b24f | ||
|
|
4a58f16637 | ||
|
|
436b876b24 | ||
|
|
a770ab7f69 | ||
|
|
806acaf445 | ||
|
|
c653567cc1 | ||
|
|
8fee8f86a2 | ||
|
|
59a4b0e650 | ||
|
|
834a3df0bc | ||
|
|
c41b104997 | ||
|
|
7028b05856 | ||
|
|
2d22a04391 | ||
|
|
baa587028b | ||
|
|
2b51dc3e5b | ||
|
|
9f8278ea8f | ||
|
|
56b9c754f5 | ||
|
|
5c4f5ef3f0 | ||
|
|
8c924896c5 | ||
|
|
2c2f0044b9 | ||
|
|
874e9085a7 | ||
|
|
e791d6646b | ||
|
|
3019b0218c | ||
|
|
6900e140d5 | ||
|
|
911c1b2bfa | ||
|
|
f4587462cf | ||
|
|
cac1faa8ac | ||
|
|
9525bae5a3 | ||
|
|
dbcfd109f6 |
@@ -1 +1,7 @@
|
||||
README.md
|
||||
docs/
|
||||
.gitignore
|
||||
debug
|
||||
config/
|
||||
*.pyc
|
||||
.git
|
||||
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
github: blakeblackshear
|
||||
56
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: Bug report or Support request
|
||||
about: ''
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what your issue is.
|
||||
|
||||
**Version of frigate**
|
||||
Output from `/version`
|
||||
|
||||
**Config file**
|
||||
Include your full config file wrapped in triple back ticks.
|
||||
```yaml
|
||||
config here
|
||||
```
|
||||
|
||||
**Frigate container logs**
|
||||
```
|
||||
Include relevant log output here
|
||||
```
|
||||
|
||||
**Frigate stats**
|
||||
```json
|
||||
Output from frigate's /stats endpoint
|
||||
```
|
||||
|
||||
**FFprobe from your camera**
|
||||
|
||||
Run the following command and paste output below
|
||||
```
|
||||
ffprobe <stream_url>
|
||||
```
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Computer Hardware**
|
||||
- OS: [e.g. Ubuntu, Windows]
|
||||
- Install method: [e.g. Addon, Docker Compose, Docker Command]
|
||||
- Virtualization: [e.g. Proxmox, Virtualbox]
|
||||
- Coral Version: [e.g. USB, PCIe, None]
|
||||
- Network Setup: [e.g. Wired, WiFi]
|
||||
|
||||
**Camera Info:**
|
||||
- Manufacturer: [e.g. Dahua]
|
||||
- Model: [e.g. IPC-HDW5231R-ZE]
|
||||
- Resolution: [e.g. 720p]
|
||||
- FPS: [e.g. 5]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
6
.gitignore
vendored
@@ -1,2 +1,8 @@
|
||||
*.pyc
|
||||
debug
|
||||
.vscode
|
||||
config/config.yml
|
||||
models
|
||||
*.mp4
|
||||
*.db
|
||||
frigate/version.py
|
||||
107
Dockerfile
@@ -1,107 +0,0 @@
|
||||
FROM ubuntu:16.04
|
||||
|
||||
# Install system packages
|
||||
RUN apt-get -qq update && apt-get -qq install --no-install-recommends -y python3 \
|
||||
python3-dev \
|
||||
python-pil \
|
||||
python-lxml \
|
||||
python-tk \
|
||||
build-essential \
|
||||
cmake \
|
||||
git \
|
||||
libgtk2.0-dev \
|
||||
pkg-config \
|
||||
libavcodec-dev \
|
||||
libavformat-dev \
|
||||
libswscale-dev \
|
||||
libtbb2 \
|
||||
libtbb-dev \
|
||||
libjpeg-dev \
|
||||
libpng-dev \
|
||||
libtiff-dev \
|
||||
libjasper-dev \
|
||||
libdc1394-22-dev \
|
||||
x11-apps \
|
||||
wget \
|
||||
vim \
|
||||
ffmpeg \
|
||||
unzip \
|
||||
libusb-1.0-0-dev \
|
||||
python3-setuptools \
|
||||
python3-numpy \
|
||||
zlib1g-dev \
|
||||
libgoogle-glog-dev \
|
||||
swig \
|
||||
libunwind-dev \
|
||||
libc++-dev \
|
||||
libc++abi-dev \
|
||||
build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install core packages
|
||||
RUN wget -q -O /tmp/get-pip.py --no-check-certificate https://bootstrap.pypa.io/get-pip.py && python3 /tmp/get-pip.py
|
||||
RUN pip install -U pip \
|
||||
numpy \
|
||||
pillow \
|
||||
matplotlib \
|
||||
notebook \
|
||||
Flask \
|
||||
imutils \
|
||||
paho-mqtt \
|
||||
PyYAML
|
||||
|
||||
# Install tensorflow models object detection
|
||||
RUN GIT_SSL_NO_VERIFY=true git clone -q https://github.com/tensorflow/models /usr/local/lib/python3.5/dist-packages/tensorflow/models
|
||||
RUN wget -q -P /usr/local/src/ --no-check-certificate https://github.com/google/protobuf/releases/download/v3.5.1/protobuf-python-3.5.1.tar.gz
|
||||
|
||||
# Download & build protobuf-python
|
||||
RUN cd /usr/local/src/ \
|
||||
&& tar xf protobuf-python-3.5.1.tar.gz \
|
||||
&& rm protobuf-python-3.5.1.tar.gz \
|
||||
&& cd /usr/local/src/protobuf-3.5.1/ \
|
||||
&& ./configure \
|
||||
&& make \
|
||||
&& make install \
|
||||
&& ldconfig \
|
||||
&& rm -rf /usr/local/src/protobuf-3.5.1/
|
||||
|
||||
# Download & build OpenCV
|
||||
RUN wget -q -P /usr/local/src/ --no-check-certificate https://github.com/opencv/opencv/archive/4.0.1.zip
|
||||
RUN cd /usr/local/src/ \
|
||||
&& unzip 4.0.1.zip \
|
||||
&& rm 4.0.1.zip \
|
||||
&& cd /usr/local/src/opencv-4.0.1/ \
|
||||
&& mkdir build \
|
||||
&& cd /usr/local/src/opencv-4.0.1/build \
|
||||
&& cmake -D CMAKE_INSTALL_TYPE=Release -D CMAKE_INSTALL_PREFIX=/usr/local/ .. \
|
||||
&& make -j4 \
|
||||
&& make install \
|
||||
&& rm -rf /usr/local/src/opencv-4.0.1
|
||||
|
||||
# Download and install EdgeTPU libraries
|
||||
RUN wget -q -O edgetpu_api.tar.gz --no-check-certificate http://storage.googleapis.com/cloud-iot-edge-pretrained-models/edgetpu_api.tar.gz
|
||||
|
||||
RUN tar xzf edgetpu_api.tar.gz \
|
||||
&& cd python-tflite-source \
|
||||
&& cp -p libedgetpu/libedgetpu_x86_64.so /lib/x86_64-linux-gnu/libedgetpu.so \
|
||||
&& cp edgetpu/swig/compiled_so/_edgetpu_cpp_wrapper_x86_64.so edgetpu/swig/_edgetpu_cpp_wrapper.so \
|
||||
&& cp edgetpu/swig/compiled_so/edgetpu_cpp_wrapper.py edgetpu/swig/ \
|
||||
&& python3 setup.py develop --user
|
||||
|
||||
# Minimize image size
|
||||
RUN (apt-get autoremove -y; \
|
||||
apt-get autoclean -y)
|
||||
|
||||
# symlink the model and labels
|
||||
RUN ln -s /python-tflite-source/edgetpu/test_data/mobilenet_ssd_v2_coco_quant_postprocess_edgetpu.tflite /frozen_inference_graph.pb
|
||||
RUN ln -s /python-tflite-source/edgetpu/test_data/coco_labels.txt /label_map.pbtext
|
||||
|
||||
# Set TF object detection available
|
||||
ENV PYTHONPATH "$PYTHONPATH:/usr/local/lib/python3.5/dist-packages/tensorflow/models/research:/usr/local/lib/python3.5/dist-packages/tensorflow/models/research/slim"
|
||||
RUN cd /usr/local/lib/python3.5/dist-packages/tensorflow/models/research && protoc object_detection/protos/*.proto --python_out=.
|
||||
|
||||
WORKDIR /opt/frigate/
|
||||
ADD frigate frigate/
|
||||
COPY detect_objects.py .
|
||||
|
||||
CMD ["python3", "-u", "detect_objects.py"]
|
||||
682
LICENSE
@@ -1,661 +1,21 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
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
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
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
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
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
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
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.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
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.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"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
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
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
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
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
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
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,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
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/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
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
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
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.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2020 Blake Blackshear
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
54
Makefile
Normal file
@@ -0,0 +1,54 @@
|
||||
default_target: amd64_frigate
|
||||
|
||||
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h")
|
||||
|
||||
version:
|
||||
echo "VERSION='0.8.0-$(COMMIT_HASH)'" > frigate/version.py
|
||||
|
||||
amd64_wheels:
|
||||
docker build --tag blakeblackshear/frigate-wheels:amd64 --file docker/Dockerfile.wheels .
|
||||
|
||||
amd64_ffmpeg:
|
||||
docker build --tag blakeblackshear/frigate-ffmpeg:1.1.0-amd64 --file docker/Dockerfile.ffmpeg.amd64 .
|
||||
|
||||
amd64_frigate: version
|
||||
docker build --tag frigate-base --build-arg ARCH=amd64 --build-arg FFMPEG_VERSION=1.1.0 --file docker/Dockerfile.base .
|
||||
docker build --tag frigate --file docker/Dockerfile.amd64 .
|
||||
|
||||
amd64_all: amd64_wheels amd64_ffmpeg amd64_frigate
|
||||
|
||||
amd64nvidia_wheels:
|
||||
docker build --tag blakeblackshear/frigate-wheels:amd64nvidia --file docker/Dockerfile.wheels .
|
||||
|
||||
amd64nvidia_ffmpeg:
|
||||
docker build --tag blakeblackshear/frigate-ffmpeg:1.0.0-amd64nvidia --file docker/Dockerfile.ffmpeg.amd64nvidia .
|
||||
|
||||
amd64nvidia_frigate: version
|
||||
docker build --tag frigate-base --build-arg ARCH=amd64nvidia --build-arg FFMPEG_VERSION=1.0.0 --file docker/Dockerfile.base .
|
||||
docker build --tag frigate --file docker/Dockerfile.amd64nvidia .
|
||||
|
||||
amd64nvidia_all: amd64nvidia_wheels amd64nvidia_ffmpeg amd64nvidia_frigate
|
||||
|
||||
aarch64_wheels:
|
||||
docker build --tag blakeblackshear/frigate-wheels:aarch64 --file docker/Dockerfile.wheels.aarch64 .
|
||||
|
||||
aarch64_ffmpeg:
|
||||
docker build --tag blakeblackshear/frigate-ffmpeg:1.0.0-aarch64 --file docker/Dockerfile.ffmpeg.aarch64 .
|
||||
|
||||
aarch64_frigate: version
|
||||
docker build --tag frigate-base --build-arg ARCH=aarch64 --build-arg FFMPEG_VERSION=1.0.0 --file docker/Dockerfile.base .
|
||||
docker build --tag frigate --file docker/Dockerfile.aarch64 .
|
||||
|
||||
armv7_all: armv7_wheels armv7_ffmpeg armv7_frigate
|
||||
|
||||
armv7_wheels:
|
||||
docker build --tag blakeblackshear/frigate-wheels:armv7 --file docker/Dockerfile.wheels .
|
||||
|
||||
armv7_ffmpeg:
|
||||
docker build --tag blakeblackshear/frigate-ffmpeg:1.0.0-armv7 --file docker/Dockerfile.ffmpeg.armv7 .
|
||||
|
||||
armv7_frigate: version
|
||||
docker build --tag frigate-base --build-arg ARCH=armv7 --build-arg FFMPEG_VERSION=1.0.0 --file docker/Dockerfile.base .
|
||||
docker build --tag frigate --file docker/Dockerfile.armv7 .
|
||||
|
||||
armv7_all: armv7_wheels armv7_ffmpeg armv7_frigate
|
||||
93
benchmark.py
Executable file
@@ -0,0 +1,93 @@
|
||||
import os
|
||||
from statistics import mean
|
||||
import multiprocessing as mp
|
||||
import numpy as np
|
||||
import datetime
|
||||
from frigate.edgetpu import LocalObjectDetector, EdgeTPUProcess, RemoteObjectDetector, load_labels
|
||||
|
||||
my_frame = np.expand_dims(np.full((300,300,3), 1, np.uint8), axis=0)
|
||||
labels = load_labels('/labelmap.txt')
|
||||
|
||||
######
|
||||
# Minimal same process runner
|
||||
######
|
||||
# object_detector = LocalObjectDetector()
|
||||
# tensor_input = np.expand_dims(np.full((300,300,3), 0, np.uint8), axis=0)
|
||||
|
||||
# start = datetime.datetime.now().timestamp()
|
||||
|
||||
# frame_times = []
|
||||
# for x in range(0, 1000):
|
||||
# start_frame = datetime.datetime.now().timestamp()
|
||||
|
||||
# tensor_input[:] = my_frame
|
||||
# detections = object_detector.detect_raw(tensor_input)
|
||||
# parsed_detections = []
|
||||
# for d in detections:
|
||||
# if d[1] < 0.4:
|
||||
# break
|
||||
# parsed_detections.append((
|
||||
# labels[int(d[0])],
|
||||
# float(d[1]),
|
||||
# (d[2], d[3], d[4], d[5])
|
||||
# ))
|
||||
# frame_times.append(datetime.datetime.now().timestamp()-start_frame)
|
||||
|
||||
# duration = datetime.datetime.now().timestamp()-start
|
||||
# print(f"Processed for {duration:.2f} seconds.")
|
||||
# print(f"Average frame processing time: {mean(frame_times)*1000:.2f}ms")
|
||||
|
||||
|
||||
def start(id, num_detections, detection_queue, event):
|
||||
object_detector = RemoteObjectDetector(str(id), '/labelmap.txt', detection_queue, event)
|
||||
start = datetime.datetime.now().timestamp()
|
||||
|
||||
frame_times = []
|
||||
for x in range(0, num_detections):
|
||||
start_frame = datetime.datetime.now().timestamp()
|
||||
detections = object_detector.detect(my_frame)
|
||||
frame_times.append(datetime.datetime.now().timestamp()-start_frame)
|
||||
|
||||
duration = datetime.datetime.now().timestamp()-start
|
||||
object_detector.cleanup()
|
||||
print(f"{id} - Processed for {duration:.2f} seconds.")
|
||||
print(f"{id} - FPS: {object_detector.fps.eps():.2f}")
|
||||
print(f"{id} - Average frame processing time: {mean(frame_times)*1000:.2f}ms")
|
||||
|
||||
######
|
||||
# Separate process runner
|
||||
######
|
||||
# event = mp.Event()
|
||||
# detection_queue = mp.Queue()
|
||||
# edgetpu_process = EdgeTPUProcess(detection_queue, {'1': event}, 'usb:0')
|
||||
|
||||
# start(1, 1000, edgetpu_process.detection_queue, event)
|
||||
# print(f"Average raw inference speed: {edgetpu_process.avg_inference_speed.value*1000:.2f}ms")
|
||||
|
||||
####
|
||||
# Multiple camera processes
|
||||
####
|
||||
camera_processes = []
|
||||
|
||||
events = {}
|
||||
for x in range(0, 10):
|
||||
events[str(x)] = mp.Event()
|
||||
detection_queue = mp.Queue()
|
||||
edgetpu_process_1 = EdgeTPUProcess(detection_queue, events, 'usb:0')
|
||||
edgetpu_process_2 = EdgeTPUProcess(detection_queue, events, 'usb:1')
|
||||
|
||||
for x in range(0, 10):
|
||||
camera_process = mp.Process(target=start, args=(x, 300, detection_queue, events[str(x)]))
|
||||
camera_process.daemon = True
|
||||
camera_processes.append(camera_process)
|
||||
|
||||
start_time = datetime.datetime.now().timestamp()
|
||||
|
||||
for p in camera_processes:
|
||||
p.start()
|
||||
|
||||
for p in camera_processes:
|
||||
p.join()
|
||||
|
||||
duration = datetime.datetime.now().timestamp()-start_time
|
||||
print(f"Total - Processed for {duration:.2f} seconds.")
|
||||
|
Before Width: | Height: | Size: 1.8 MiB |
@@ -1,29 +0,0 @@
|
||||
web_port: 5000
|
||||
|
||||
mqtt:
|
||||
host: mqtt.server.com
|
||||
topic_prefix: frigate
|
||||
|
||||
cameras:
|
||||
back:
|
||||
rtsp:
|
||||
user: viewer
|
||||
host: 10.0.10.10
|
||||
port: 554
|
||||
# values that begin with a "$" will be replaced with environment variable
|
||||
password: $RTSP_PASSWORD
|
||||
path: /cam/realmonitor?channel=1&subtype=2
|
||||
mask: back-mask.bmp
|
||||
regions:
|
||||
- size: 350
|
||||
x_offset: 0
|
||||
y_offset: 300
|
||||
min_person_area: 5000
|
||||
- size: 400
|
||||
x_offset: 350
|
||||
y_offset: 250
|
||||
min_person_area: 2000
|
||||
- size: 400
|
||||
x_offset: 750
|
||||
y_offset: 250
|
||||
min_person_area: 2000
|
||||
@@ -1,90 +0,0 @@
|
||||
import cv2
|
||||
import time
|
||||
import queue
|
||||
import yaml
|
||||
import numpy as np
|
||||
from flask import Flask, Response, make_response
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
from frigate.video import Camera
|
||||
from frigate.object_detection import PreppedQueueProcessor
|
||||
|
||||
with open('/config/config.yml') as f:
|
||||
CONFIG = yaml.safe_load(f)
|
||||
|
||||
MQTT_HOST = CONFIG['mqtt']['host']
|
||||
MQTT_PORT = CONFIG.get('mqtt', {}).get('port', 1883)
|
||||
MQTT_TOPIC_PREFIX = CONFIG.get('mqtt', {}).get('topic_prefix', 'frigate')
|
||||
MQTT_USER = CONFIG.get('mqtt', {}).get('user')
|
||||
MQTT_PASS = CONFIG.get('mqtt', {}).get('password')
|
||||
|
||||
WEB_PORT = CONFIG.get('web_port', 5000)
|
||||
DEBUG = (CONFIG.get('debug', '0') == '1')
|
||||
|
||||
def main():
|
||||
# connect to mqtt and setup last will
|
||||
def on_connect(client, userdata, flags, rc):
|
||||
print("On connect called")
|
||||
# publish a message to signal that the service is running
|
||||
client.publish(MQTT_TOPIC_PREFIX+'/available', 'online', retain=True)
|
||||
client = mqtt.Client()
|
||||
client.on_connect = on_connect
|
||||
client.will_set(MQTT_TOPIC_PREFIX+'/available', payload='offline', qos=1, retain=True)
|
||||
if not MQTT_USER is None:
|
||||
client.username_pw_set(MQTT_USER, password=MQTT_PASS)
|
||||
client.connect(MQTT_HOST, MQTT_PORT, 60)
|
||||
client.loop_start()
|
||||
|
||||
# Queue for prepped frames, max size set to (number of cameras * 5)
|
||||
max_queue_size = len(CONFIG['cameras'].items())*5
|
||||
prepped_frame_queue = queue.Queue(max_queue_size)
|
||||
|
||||
cameras = {}
|
||||
for name, config in CONFIG['cameras'].items():
|
||||
cameras[name] = Camera(name, config, prepped_frame_queue, client, MQTT_TOPIC_PREFIX)
|
||||
|
||||
prepped_queue_processor = PreppedQueueProcessor(
|
||||
cameras,
|
||||
prepped_frame_queue
|
||||
)
|
||||
prepped_queue_processor.start()
|
||||
|
||||
for name, camera in cameras.items():
|
||||
camera.start()
|
||||
print("Capture process for {}: {}".format(name, camera.get_capture_pid()))
|
||||
|
||||
# create a flask app that encodes frames a mjpeg on demand
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/<camera_name>/best_person.jpg')
|
||||
def best_person(camera_name):
|
||||
best_person_frame = cameras[camera_name].get_best_person()
|
||||
if best_person_frame is None:
|
||||
best_person_frame = np.zeros((720,1280,3), np.uint8)
|
||||
ret, jpg = cv2.imencode('.jpg', best_person_frame)
|
||||
response = make_response(jpg.tobytes())
|
||||
response.headers['Content-Type'] = 'image/jpg'
|
||||
return response
|
||||
|
||||
@app.route('/<camera_name>')
|
||||
def mjpeg_feed(camera_name):
|
||||
# return a multipart response
|
||||
return Response(imagestream(camera_name),
|
||||
mimetype='multipart/x-mixed-replace; boundary=frame')
|
||||
|
||||
def imagestream(camera_name):
|
||||
while True:
|
||||
# max out at 5 FPS
|
||||
time.sleep(0.2)
|
||||
frame = cameras[camera_name].get_current_frame_with_objects()
|
||||
# encode the image into a jpg
|
||||
ret, jpg = cv2.imencode('.jpg', frame)
|
||||
yield (b'--frame\r\n'
|
||||
b'Content-Type: image/jpeg\r\n\r\n' + jpg.tobytes() + b'\r\n\r\n')
|
||||
|
||||
app.run(host='0.0.0.0', port=WEB_PORT, debug=False)
|
||||
|
||||
camera.join()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
BIN
diagram.png
|
Before Width: | Height: | Size: 283 KiB |
22
docker/Dockerfile.aarch64
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM frigate-base
|
||||
LABEL maintainer "blakeb@blakeshome.com"
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
# Install packages for apt repo
|
||||
RUN apt-get -qq update \
|
||||
&& apt-get -qq install --no-install-recommends -y \
|
||||
# ffmpeg runtime dependencies
|
||||
libgomp1 \
|
||||
# runtime dependencies
|
||||
libopenexr24 \
|
||||
libgstreamer1.0-0 \
|
||||
libgstreamer-plugins-base1.0-0 \
|
||||
libopenblas-base \
|
||||
libjpeg-turbo8 \
|
||||
libpng16-16 \
|
||||
libtiff5 \
|
||||
libdc1394-22 \
|
||||
## Tensorflow lite
|
||||
&& pip3 install https://github.com/google-coral/pycoral/releases/download/release-frogfish/tflite_runtime-2.5.0-cp38-cp38-linux_aarch64.whl \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& (apt-get autoremove -y; apt-get autoclean -y)
|
||||
18
docker/Dockerfile.amd64
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM frigate-base
|
||||
LABEL maintainer "blakeb@blakeshome.com"
|
||||
|
||||
# By default, use the i965 driver
|
||||
ENV LIBVA_DRIVER_NAME=i965
|
||||
# Install packages for apt repo
|
||||
RUN apt-get -qq update \
|
||||
&& apt-get -qq install --no-install-recommends -y \
|
||||
# ffmpeg dependencies
|
||||
libgomp1 \
|
||||
# VAAPI drivers for Intel hardware accel
|
||||
libva-drm2 libva2 libmfx1 i965-va-driver vainfo intel-media-va-driver mesa-va-drivers \
|
||||
## Tensorflow lite
|
||||
&& wget -q https://github.com/google-coral/pycoral/releases/download/release-frogfish/tflite_runtime-2.5.0-cp38-cp38-linux_x86_64.whl \
|
||||
&& python3.8 -m pip install tflite_runtime-2.5.0-cp38-cp38-linux_x86_64.whl \
|
||||
&& rm tflite_runtime-2.5.0-cp38-cp38-linux_x86_64.whl \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& (apt-get autoremove -y; apt-get autoclean -y)
|
||||
47
docker/Dockerfile.amd64nvidia
Normal file
@@ -0,0 +1,47 @@
|
||||
FROM frigate-base
|
||||
LABEL maintainer "blakeb@blakeshome.com"
|
||||
|
||||
# Install packages for apt repo
|
||||
RUN apt-get -qq update \
|
||||
&& apt-get -qq install --no-install-recommends -y \
|
||||
# ffmpeg dependencies
|
||||
libgomp1 \
|
||||
## Tensorflow lite
|
||||
&& wget -q https://github.com/google-coral/pycoral/releases/download/release-frogfish/tflite_runtime-2.5.0-cp38-cp38-linux_x86_64.whl \
|
||||
&& python3.8 -m pip install tflite_runtime-2.5.0-cp38-cp38-linux_x86_64.whl \
|
||||
&& rm tflite_runtime-2.5.0-cp38-cp38-linux_x86_64.whl \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& (apt-get autoremove -y; apt-get autoclean -y)
|
||||
|
||||
|
||||
# nvidia layer (see https://gitlab.com/nvidia/container-images/cuda/blob/master/dist/11.1/ubuntu20.04-x86_64/base/Dockerfile)
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES compute,utility,video
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gnupg2 curl ca-certificates && \
|
||||
curl -fsSL https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/7fa2af80.pub | apt-key add - && \
|
||||
echo "deb https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64 /" > /etc/apt/sources.list.d/cuda.list && \
|
||||
echo "deb https://developer.download.nvidia.com/compute/machine-learning/repos/ubuntu2004/x86_64 /" > /etc/apt/sources.list.d/nvidia-ml.list && \
|
||||
apt-get purge --autoremove -y curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV CUDA_VERSION 11.1.1
|
||||
|
||||
# For libraries in the cuda-compat-* package: https://docs.nvidia.com/cuda/eula/index.html#attachment-a
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
cuda-cudart-11-1=11.1.74-1 \
|
||||
cuda-compat-11-1 \
|
||||
&& ln -s cuda-11.1 /usr/local/cuda && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Required for nvidia-docker v1
|
||||
RUN echo "/usr/local/nvidia/lib" >> /etc/ld.so.conf.d/nvidia.conf && \
|
||||
echo "/usr/local/nvidia/lib64" >> /etc/ld.so.conf.d/nvidia.conf
|
||||
|
||||
ENV PATH /usr/local/nvidia/bin:/usr/local/cuda/bin:${PATH}
|
||||
ENV LD_LIBRARY_PATH /usr/local/nvidia/lib:/usr/local/nvidia/lib64
|
||||
|
||||
# nvidia-container-runtime
|
||||
ENV NVIDIA_VISIBLE_DEVICES all
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES compute,utility,video
|
||||
ENV NVIDIA_REQUIRE_CUDA "cuda>=11.1 brand=tesla,driver>=418,driver<419 brand=tesla,driver>=440,driver<441 brand=tesla,driver>=450,driver<451"
|
||||
24
docker/Dockerfile.armv7
Normal file
@@ -0,0 +1,24 @@
|
||||
FROM frigate-base
|
||||
LABEL maintainer "blakeb@blakeshome.com"
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
# Install packages for apt repo
|
||||
RUN apt-get -qq update \
|
||||
&& apt-get -qq install --no-install-recommends -y \
|
||||
# ffmpeg runtime dependencies
|
||||
libgomp1 \
|
||||
# runtime dependencies
|
||||
libopenexr24 \
|
||||
libgstreamer1.0-0 \
|
||||
libgstreamer-plugins-base1.0-0 \
|
||||
libopenblas-base \
|
||||
libjpeg-turbo8 \
|
||||
libpng16-16 \
|
||||
libtiff5 \
|
||||
libdc1394-22 \
|
||||
libaom0 \
|
||||
libx265-179 \
|
||||
## Tensorflow lite
|
||||
&& pip3 install https://github.com/google-coral/pycoral/releases/download/release-frogfish/tflite_runtime-2.5.0-cp38-cp38-linux_armv7l.whl \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& (apt-get autoremove -y; apt-get autoclean -y)
|
||||
53
docker/Dockerfile.base
Normal file
@@ -0,0 +1,53 @@
|
||||
ARG ARCH=amd64
|
||||
ARG FFMPEG_VERSION
|
||||
FROM blakeblackshear/frigate-wheels:${ARCH} as wheels
|
||||
FROM blakeblackshear/frigate-ffmpeg:${FFMPEG_VERSION}-${ARCH} as ffmpeg
|
||||
|
||||
FROM ubuntu:20.04
|
||||
LABEL maintainer "blakeb@blakeshome.com"
|
||||
|
||||
COPY --from=ffmpeg /usr/local /usr/local/
|
||||
|
||||
COPY --from=wheels /wheels/. /wheels/
|
||||
|
||||
ENV FLASK_ENV=development
|
||||
# ENV FONTCONFIG_PATH=/etc/fonts
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
# Install packages for apt repo
|
||||
RUN apt-get -qq update \
|
||||
&& apt-get upgrade -y \
|
||||
&& apt-get -qq install --no-install-recommends -y \
|
||||
gnupg wget unzip tzdata nginx libnginx-mod-rtmp \
|
||||
&& apt-get -qq install --no-install-recommends -y \
|
||||
python3-pip \
|
||||
&& pip3 install -U /wheels/*.whl \
|
||||
&& APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn apt-key adv --fetch-keys https://packages.cloud.google.com/apt/doc/apt-key.gpg \
|
||||
&& echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" > /etc/apt/sources.list.d/coral-edgetpu.list \
|
||||
&& echo "libedgetpu1-max libedgetpu/accepted-eula select true" | debconf-set-selections \
|
||||
&& apt-get -qq update && apt-get -qq install --no-install-recommends -y \
|
||||
libedgetpu1-max=15.0 \
|
||||
&& rm -rf /var/lib/apt/lists/* /wheels \
|
||||
&& (apt-get autoremove -y; apt-get autoclean -y)
|
||||
|
||||
RUN pip3 install \
|
||||
peewee \
|
||||
zeroconf \
|
||||
voluptuous
|
||||
|
||||
COPY nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# get model and labels
|
||||
COPY labelmap.txt /labelmap.txt
|
||||
RUN wget -q https://github.com/google-coral/test_data/raw/master/ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite -O /edgetpu_model.tflite
|
||||
RUN wget -q https://github.com/google-coral/test_data/raw/master/ssdlite_mobiledet_coco_qat_postprocess.tflite -O /cpu_model.tflite
|
||||
|
||||
WORKDIR /opt/frigate/
|
||||
ADD frigate frigate/
|
||||
|
||||
COPY run.sh /run.sh
|
||||
RUN chmod +x /run.sh
|
||||
|
||||
EXPOSE 5000
|
||||
EXPOSE 1935
|
||||
|
||||
CMD ["/run.sh"]
|
||||
474
docker/Dockerfile.ffmpeg.aarch64
Normal file
@@ -0,0 +1,474 @@
|
||||
# inspired by:
|
||||
# https://github.com/collelog/ffmpeg/blob/master/4.3.1-alpine-rpi4-arm64v8.Dockerfile
|
||||
# https://github.com/mmastrac/ffmpeg-omx-rpi-docker/blob/master/Dockerfile
|
||||
# https://github.com/jrottenberg/ffmpeg/pull/158/files
|
||||
# https://github.com/jrottenberg/ffmpeg/pull/239
|
||||
FROM ubuntu:20.04 AS base
|
||||
|
||||
WORKDIR /tmp/workdir
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get -yqq update && \
|
||||
apt-get install -yq --no-install-recommends ca-certificates expat libgomp1 && \
|
||||
apt-get autoremove -y && \
|
||||
apt-get clean -y
|
||||
|
||||
FROM base as build
|
||||
|
||||
ENV FFMPEG_VERSION=4.3.1 \
|
||||
AOM_VERSION=v1.0.0 \
|
||||
FDKAAC_VERSION=0.1.5 \
|
||||
FREETYPE_VERSION=2.5.5 \
|
||||
FRIBIDI_VERSION=0.19.7 \
|
||||
KVAZAAR_VERSION=1.2.0 \
|
||||
LAME_VERSION=3.100 \
|
||||
LIBPTHREAD_STUBS_VERSION=0.4 \
|
||||
LIBVIDSTAB_VERSION=1.1.0 \
|
||||
LIBXCB_VERSION=1.13.1 \
|
||||
XCBPROTO_VERSION=1.13 \
|
||||
OGG_VERSION=1.3.2 \
|
||||
OPENCOREAMR_VERSION=0.1.5 \
|
||||
OPUS_VERSION=1.2 \
|
||||
OPENJPEG_VERSION=2.1.2 \
|
||||
THEORA_VERSION=1.1.1 \
|
||||
VORBIS_VERSION=1.3.5 \
|
||||
VPX_VERSION=1.8.0 \
|
||||
WEBP_VERSION=1.0.2 \
|
||||
X264_VERSION=20170226-2245-stable \
|
||||
X265_VERSION=3.1.1 \
|
||||
XAU_VERSION=1.0.9 \
|
||||
XORG_MACROS_VERSION=1.19.2 \
|
||||
XPROTO_VERSION=7.0.31 \
|
||||
XVID_VERSION=1.3.4 \
|
||||
LIBZMQ_VERSION=4.3.2 \
|
||||
SRC=/usr/local
|
||||
|
||||
ARG FREETYPE_SHA256SUM="5d03dd76c2171a7601e9ce10551d52d4471cf92cd205948e60289251daddffa8 freetype-2.5.5.tar.gz"
|
||||
ARG FRIBIDI_SHA256SUM="3fc96fa9473bd31dcb5500bdf1aa78b337ba13eb8c301e7c28923fea982453a8 0.19.7.tar.gz"
|
||||
ARG LIBVIDSTAB_SHA256SUM="14d2a053e56edad4f397be0cb3ef8eb1ec3150404ce99a426c4eb641861dc0bb v1.1.0.tar.gz"
|
||||
ARG OGG_SHA256SUM="e19ee34711d7af328cb26287f4137e70630e7261b17cbe3cd41011d73a654692 libogg-1.3.2.tar.gz"
|
||||
ARG OPUS_SHA256SUM="77db45a87b51578fbc49555ef1b10926179861d854eb2613207dc79d9ec0a9a9 opus-1.2.tar.gz"
|
||||
ARG THEORA_SHA256SUM="40952956c47811928d1e7922cda3bc1f427eb75680c3c37249c91e949054916b libtheora-1.1.1.tar.gz"
|
||||
ARG VORBIS_SHA256SUM="6efbcecdd3e5dfbf090341b485da9d176eb250d893e3eb378c428a2db38301ce libvorbis-1.3.5.tar.gz"
|
||||
ARG XVID_SHA256SUM="4e9fd62728885855bc5007fe1be58df42e5e274497591fec37249e1052ae316f xvidcore-1.3.4.tar.gz"
|
||||
ARG LIBZMQ_SHA256SUM="02ecc88466ae38cf2c8d79f09cfd2675ba299a439680b64ade733e26a349edeb v4.3.2.tar.gz"
|
||||
|
||||
|
||||
ARG LD_LIBRARY_PATH=/opt/ffmpeg/lib
|
||||
ARG MAKEFLAGS="-j2"
|
||||
ARG PKG_CONFIG_PATH="/opt/ffmpeg/share/pkgconfig:/opt/ffmpeg/lib/pkgconfig:/opt/ffmpeg/lib64/pkgconfig"
|
||||
ARG PREFIX=/opt/ffmpeg
|
||||
ARG LD_LIBRARY_PATH="/opt/ffmpeg/lib:/opt/ffmpeg/lib64:/usr/lib64:/usr/lib:/lib64:/lib"
|
||||
|
||||
|
||||
RUN buildDeps="autoconf \
|
||||
automake \
|
||||
cmake \
|
||||
curl \
|
||||
bzip2 \
|
||||
libexpat1-dev \
|
||||
g++ \
|
||||
gcc \
|
||||
git \
|
||||
gperf \
|
||||
libtool \
|
||||
make \
|
||||
nasm \
|
||||
perl \
|
||||
pkg-config \
|
||||
python \
|
||||
libssl-dev \
|
||||
yasm \
|
||||
linux-headers-raspi2 \
|
||||
libomxil-bellagio-dev \
|
||||
zlib1g-dev" && \
|
||||
apt-get -yqq update && \
|
||||
apt-get install -yq --no-install-recommends ${buildDeps}
|
||||
## opencore-amr https://sourceforge.net/projects/opencore-amr/
|
||||
RUN \
|
||||
DIR=/tmp/opencore-amr && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://versaweb.dl.sourceforge.net/project/opencore-amr/opencore-amr/opencore-amr-${OPENCOREAMR_VERSION}.tar.gz | \
|
||||
tar -zx --strip-components=1 && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
## x264 http://www.videolan.org/developers/x264.html
|
||||
RUN \
|
||||
DIR=/tmp/x264 && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://download.videolan.org/pub/videolan/x264/snapshots/x264-snapshot-${X264_VERSION}.tar.bz2 | \
|
||||
tar -jx --strip-components=1 && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared --enable-pic --disable-cli && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
### x265 http://x265.org/
|
||||
RUN \
|
||||
DIR=/tmp/x265 && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://download.videolan.org/pub/videolan/x265/x265_${X265_VERSION}.tar.gz | \
|
||||
tar -zx && \
|
||||
cd x265_${X265_VERSION}/build/linux && \
|
||||
sed -i "/-DEXTRA_LIB/ s/$/ -DCMAKE_INSTALL_PREFIX=\${PREFIX}/" multilib.sh && \
|
||||
sed -i "/^cmake/ s/$/ -DENABLE_CLI=OFF/" multilib.sh && \
|
||||
export CXXFLAGS="${CXXFLAGS} -fPIC" && \
|
||||
./multilib.sh && \
|
||||
make -C 8bit install && \
|
||||
rm -rf ${DIR}
|
||||
### libogg https://www.xiph.org/ogg/
|
||||
RUN \
|
||||
DIR=/tmp/ogg && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO http://downloads.xiph.org/releases/ogg/libogg-${OGG_VERSION}.tar.gz && \
|
||||
echo ${OGG_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f libogg-${OGG_VERSION}.tar.gz && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
### libopus https://www.opus-codec.org/
|
||||
RUN \
|
||||
DIR=/tmp/opus && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://archive.mozilla.org/pub/opus/opus-${OPUS_VERSION}.tar.gz && \
|
||||
echo ${OPUS_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f opus-${OPUS_VERSION}.tar.gz && \
|
||||
autoreconf -fiv && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
### libvorbis https://xiph.org/vorbis/
|
||||
RUN \
|
||||
DIR=/tmp/vorbis && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO http://downloads.xiph.org/releases/vorbis/libvorbis-${VORBIS_VERSION}.tar.gz && \
|
||||
echo ${VORBIS_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f libvorbis-${VORBIS_VERSION}.tar.gz && \
|
||||
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
### libtheora http://www.theora.org/
|
||||
RUN \
|
||||
DIR=/tmp/theora && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO http://downloads.xiph.org/releases/theora/libtheora-${THEORA_VERSION}.tar.gz && \
|
||||
echo ${THEORA_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f libtheora-${THEORA_VERSION}.tar.gz && \
|
||||
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess && \
|
||||
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub && \
|
||||
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
### libvpx https://www.webmproject.org/code/
|
||||
RUN \
|
||||
DIR=/tmp/vpx && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://codeload.github.com/webmproject/libvpx/tar.gz/v${VPX_VERSION} | \
|
||||
tar -zx --strip-components=1 && \
|
||||
./configure --prefix="${PREFIX}" --enable-vp8 --enable-vp9 --enable-vp9-highbitdepth --enable-pic --enable-shared \
|
||||
--disable-debug --disable-examples --disable-docs --disable-install-bins && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
### libwebp https://developers.google.com/speed/webp/
|
||||
RUN \
|
||||
DIR=/tmp/vebp && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-${WEBP_VERSION}.tar.gz | \
|
||||
tar -zx --strip-components=1 && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
### libmp3lame http://lame.sourceforge.net/
|
||||
RUN \
|
||||
DIR=/tmp/lame && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://versaweb.dl.sourceforge.net/project/lame/lame/$(echo ${LAME_VERSION} | sed -e 's/[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z-]*\)/\1.\2/')/lame-${LAME_VERSION}.tar.gz | \
|
||||
tar -zx --strip-components=1 && \
|
||||
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" --enable-shared --enable-nasm --disable-frontend && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
### xvid https://www.xvid.com/
|
||||
RUN \
|
||||
DIR=/tmp/xvid && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO http://downloads.xvid.org/downloads/xvidcore-${XVID_VERSION}.tar.gz && \
|
||||
echo ${XVID_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx -f xvidcore-${XVID_VERSION}.tar.gz && \
|
||||
cd xvidcore/build/generic && \
|
||||
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
### fdk-aac https://github.com/mstorsjo/fdk-aac
|
||||
RUN \
|
||||
DIR=/tmp/fdk-aac && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://github.com/mstorsjo/fdk-aac/archive/v${FDKAAC_VERSION}.tar.gz | \
|
||||
tar -zx --strip-components=1 && \
|
||||
autoreconf -fiv && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared --datadir="${DIR}" && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
## openjpeg https://github.com/uclouvain/openjpeg
|
||||
RUN \
|
||||
DIR=/tmp/openjpeg && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz | \
|
||||
tar -zx --strip-components=1 && \
|
||||
export CFLAGS="${CFLAGS} -DPNG_ARM_NEON_OPT=0" && \
|
||||
cmake -DBUILD_THIRDPARTY:BOOL=ON -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
## freetype https://www.freetype.org/
|
||||
RUN \
|
||||
DIR=/tmp/freetype && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://download.savannah.gnu.org/releases/freetype/freetype-${FREETYPE_VERSION}.tar.gz && \
|
||||
echo ${FREETYPE_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f freetype-${FREETYPE_VERSION}.tar.gz && \
|
||||
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
## libvstab https://github.com/georgmartius/vid.stab
|
||||
RUN \
|
||||
DIR=/tmp/vid.stab && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://github.com/georgmartius/vid.stab/archive/v${LIBVIDSTAB_VERSION}.tar.gz && \
|
||||
echo ${LIBVIDSTAB_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f v${LIBVIDSTAB_VERSION}.tar.gz && \
|
||||
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
## fridibi https://www.fribidi.org/
|
||||
RUN \
|
||||
DIR=/tmp/fribidi && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://github.com/fribidi/fribidi/archive/${FRIBIDI_VERSION}.tar.gz && \
|
||||
echo ${FRIBIDI_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f ${FRIBIDI_VERSION}.tar.gz && \
|
||||
sed -i 's/^SUBDIRS =.*/SUBDIRS=gen.tab charset lib bin/' Makefile.am && \
|
||||
./bootstrap --no-config --auto && \
|
||||
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
|
||||
make -j1 && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
## kvazaar https://github.com/ultravideo/kvazaar
|
||||
RUN \
|
||||
DIR=/tmp/kvazaar && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://github.com/ultravideo/kvazaar/archive/v${KVAZAAR_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f v${KVAZAAR_VERSION}.tar.gz && \
|
||||
./autogen.sh && \
|
||||
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/aom && \
|
||||
git clone --branch ${AOM_VERSION} --depth 1 https://aomedia.googlesource.com/aom ${DIR} ; \
|
||||
cd ${DIR} ; \
|
||||
rm -rf CMakeCache.txt CMakeFiles ; \
|
||||
mkdir -p ./aom_build ; \
|
||||
cd ./aom_build ; \
|
||||
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" -DBUILD_SHARED_LIBS=1 ..; \
|
||||
make ; \
|
||||
make install ; \
|
||||
rm -rf ${DIR}
|
||||
|
||||
## libxcb (and supporting libraries) for screen capture https://xcb.freedesktop.org/
|
||||
RUN \
|
||||
DIR=/tmp/xorg-macros && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://www.x.org/archive//individual/util/util-macros-${XORG_MACROS_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f util-macros-${XORG_MACROS_VERSION}.tar.gz && \
|
||||
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/xproto && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://www.x.org/archive/individual/proto/xproto-${XPROTO_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f xproto-${XPROTO_VERSION}.tar.gz && \
|
||||
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess && \
|
||||
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub && \
|
||||
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/libXau && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://www.x.org/archive/individual/lib/libXau-${XAU_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f libXau-${XAU_VERSION}.tar.gz && \
|
||||
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/libpthread-stubs && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://xcb.freedesktop.org/dist/libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
|
||||
./configure --prefix="${PREFIX}" && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/libxcb-proto && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://xcb.freedesktop.org/dist/xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
|
||||
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
|
||||
./configure --prefix="${PREFIX}" && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/libxcb && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://xcb.freedesktop.org/dist/libxcb-${LIBXCB_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f libxcb-${LIBXCB_VERSION}.tar.gz && \
|
||||
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
|
||||
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
## libzmq https://github.com/zeromq/libzmq/
|
||||
RUN \
|
||||
DIR=/tmp/libzmq && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://github.com/zeromq/libzmq/archive/v${LIBZMQ_VERSION}.tar.gz && \
|
||||
echo ${LIBZMQ_SHA256SUM} | sha256sum --check && \
|
||||
tar -xz --strip-components=1 -f v${LIBZMQ_VERSION}.tar.gz && \
|
||||
./autogen.sh && \
|
||||
./configure --prefix="${PREFIX}" && \
|
||||
make -j $(nproc) && \
|
||||
make check && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
## ffmpeg https://ffmpeg.org/
|
||||
RUN \
|
||||
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
curl -sLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
|
||||
tar -jx --strip-components=1 -f ffmpeg-${FFMPEG_VERSION}.tar.bz2
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
./configure \
|
||||
--disable-debug \
|
||||
--disable-doc \
|
||||
--disable-ffplay \
|
||||
--enable-shared \
|
||||
--enable-avresample \
|
||||
--enable-libopencore-amrnb \
|
||||
--enable-libopencore-amrwb \
|
||||
--enable-gpl \
|
||||
--enable-libfreetype \
|
||||
--enable-libvidstab \
|
||||
--enable-libmp3lame \
|
||||
--enable-libopus \
|
||||
--enable-libtheora \
|
||||
--enable-libvorbis \
|
||||
--enable-libvpx \
|
||||
--enable-libwebp \
|
||||
--enable-libxcb \
|
||||
--enable-libx265 \
|
||||
--enable-libxvid \
|
||||
--enable-libx264 \
|
||||
--enable-nonfree \
|
||||
--enable-openssl \
|
||||
--enable-libfdk_aac \
|
||||
--enable-postproc \
|
||||
--enable-small \
|
||||
--enable-version3 \
|
||||
--enable-libzmq \
|
||||
--extra-libs=-ldl \
|
||||
--prefix="${PREFIX}" \
|
||||
--enable-libopenjpeg \
|
||||
--enable-libkvazaar \
|
||||
--enable-libaom \
|
||||
--extra-libs=-lpthread \
|
||||
# --enable-omx \
|
||||
# --enable-omx-rpi \
|
||||
# --enable-mmal \
|
||||
--enable-v4l2_m2m \
|
||||
--enable-neon \
|
||||
--extra-cflags="-I${PREFIX}/include" \
|
||||
--extra-ldflags="-L${PREFIX}/lib" && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
make tools/zmqsend && cp tools/zmqsend ${PREFIX}/bin/ && \
|
||||
make distclean && \
|
||||
hash -r && \
|
||||
cd tools && \
|
||||
make qt-faststart && cp qt-faststart ${PREFIX}/bin/
|
||||
|
||||
## cleanup
|
||||
RUN \
|
||||
ldd ${PREFIX}/bin/ffmpeg | grep opt/ffmpeg | cut -d ' ' -f 3 | xargs -i cp {} /usr/local/lib/ && \
|
||||
for lib in /usr/local/lib/*.so.*; do ln -s "${lib##*/}" "${lib%%.so.*}".so; done && \
|
||||
cp ${PREFIX}/bin/* /usr/local/bin/ && \
|
||||
cp -r ${PREFIX}/share/ffmpeg /usr/local/share/ && \
|
||||
LD_LIBRARY_PATH=/usr/local/lib ffmpeg -buildconf && \
|
||||
cp -r ${PREFIX}/include/libav* ${PREFIX}/include/libpostproc ${PREFIX}/include/libsw* /usr/local/include && \
|
||||
mkdir -p /usr/local/lib/pkgconfig && \
|
||||
for pc in ${PREFIX}/lib/pkgconfig/libav*.pc ${PREFIX}/lib/pkgconfig/libpostproc.pc ${PREFIX}/lib/pkgconfig/libsw*.pc; do \
|
||||
sed "s:${PREFIX}:/usr/local:g" <"$pc" >/usr/local/lib/pkgconfig/"${pc##*/}"; \
|
||||
done
|
||||
|
||||
FROM base AS release
|
||||
|
||||
ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64:/usr/lib:/usr/lib64:/lib:/lib64
|
||||
|
||||
CMD ["--help"]
|
||||
ENTRYPOINT ["ffmpeg"]
|
||||
|
||||
COPY --from=build /usr/local /usr/local/
|
||||
|
||||
# Run ffmpeg with -c:v h264_v4l2m2m to enable HW accell for decoding on raspberry pi4 64-bit
|
||||
468
docker/Dockerfile.ffmpeg.amd64
Normal file
@@ -0,0 +1,468 @@
|
||||
# inspired by:
|
||||
# https://github.com/collelog/ffmpeg/blob/master/4.3.1-alpine-rpi4-arm64v8.Dockerfile
|
||||
# https://github.com/jrottenberg/ffmpeg/pull/158/files
|
||||
# https://github.com/jrottenberg/ffmpeg/pull/239
|
||||
FROM ubuntu:20.04 AS base
|
||||
|
||||
WORKDIR /tmp/workdir
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get -yqq update && \
|
||||
apt-get install -yq --no-install-recommends ca-certificates expat libgomp1 && \
|
||||
apt-get autoremove -y && \
|
||||
apt-get clean -y
|
||||
|
||||
FROM base as build
|
||||
|
||||
ENV FFMPEG_VERSION=4.3.1 \
|
||||
AOM_VERSION=v1.0.0 \
|
||||
FDKAAC_VERSION=0.1.5 \
|
||||
FREETYPE_VERSION=2.5.5 \
|
||||
FRIBIDI_VERSION=0.19.7 \
|
||||
KVAZAAR_VERSION=1.2.0 \
|
||||
LAME_VERSION=3.100 \
|
||||
LIBPTHREAD_STUBS_VERSION=0.4 \
|
||||
LIBVIDSTAB_VERSION=1.1.0 \
|
||||
LIBXCB_VERSION=1.13.1 \
|
||||
XCBPROTO_VERSION=1.13 \
|
||||
OGG_VERSION=1.3.2 \
|
||||
OPENCOREAMR_VERSION=0.1.5 \
|
||||
OPUS_VERSION=1.2 \
|
||||
OPENJPEG_VERSION=2.1.2 \
|
||||
THEORA_VERSION=1.1.1 \
|
||||
VORBIS_VERSION=1.3.5 \
|
||||
VPX_VERSION=1.8.0 \
|
||||
WEBP_VERSION=1.0.2 \
|
||||
X264_VERSION=20170226-2245-stable \
|
||||
X265_VERSION=3.1.1 \
|
||||
XAU_VERSION=1.0.9 \
|
||||
XORG_MACROS_VERSION=1.19.2 \
|
||||
XPROTO_VERSION=7.0.31 \
|
||||
XVID_VERSION=1.3.4 \
|
||||
LIBZMQ_VERSION=4.3.2 \
|
||||
SRC=/usr/local
|
||||
|
||||
ARG FREETYPE_SHA256SUM="5d03dd76c2171a7601e9ce10551d52d4471cf92cd205948e60289251daddffa8 freetype-2.5.5.tar.gz"
|
||||
ARG FRIBIDI_SHA256SUM="3fc96fa9473bd31dcb5500bdf1aa78b337ba13eb8c301e7c28923fea982453a8 0.19.7.tar.gz"
|
||||
ARG LIBVIDSTAB_SHA256SUM="14d2a053e56edad4f397be0cb3ef8eb1ec3150404ce99a426c4eb641861dc0bb v1.1.0.tar.gz"
|
||||
ARG OGG_SHA256SUM="e19ee34711d7af328cb26287f4137e70630e7261b17cbe3cd41011d73a654692 libogg-1.3.2.tar.gz"
|
||||
ARG OPUS_SHA256SUM="77db45a87b51578fbc49555ef1b10926179861d854eb2613207dc79d9ec0a9a9 opus-1.2.tar.gz"
|
||||
ARG THEORA_SHA256SUM="40952956c47811928d1e7922cda3bc1f427eb75680c3c37249c91e949054916b libtheora-1.1.1.tar.gz"
|
||||
ARG VORBIS_SHA256SUM="6efbcecdd3e5dfbf090341b485da9d176eb250d893e3eb378c428a2db38301ce libvorbis-1.3.5.tar.gz"
|
||||
ARG XVID_SHA256SUM="4e9fd62728885855bc5007fe1be58df42e5e274497591fec37249e1052ae316f xvidcore-1.3.4.tar.gz"
|
||||
ARG LIBZMQ_SHA256SUM="02ecc88466ae38cf2c8d79f09cfd2675ba299a439680b64ade733e26a349edeb v4.3.2.tar.gz"
|
||||
|
||||
|
||||
ARG LD_LIBRARY_PATH=/opt/ffmpeg/lib
|
||||
ARG MAKEFLAGS="-j2"
|
||||
ARG PKG_CONFIG_PATH="/opt/ffmpeg/share/pkgconfig:/opt/ffmpeg/lib/pkgconfig:/opt/ffmpeg/lib64/pkgconfig"
|
||||
ARG PREFIX=/opt/ffmpeg
|
||||
ARG LD_LIBRARY_PATH="/opt/ffmpeg/lib:/opt/ffmpeg/lib64:/usr/lib64:/usr/lib:/lib64:/lib"
|
||||
|
||||
|
||||
RUN buildDeps="autoconf \
|
||||
automake \
|
||||
cmake \
|
||||
curl \
|
||||
bzip2 \
|
||||
libexpat1-dev \
|
||||
g++ \
|
||||
gcc \
|
||||
git \
|
||||
gperf \
|
||||
libtool \
|
||||
make \
|
||||
nasm \
|
||||
perl \
|
||||
pkg-config \
|
||||
python \
|
||||
libssl-dev \
|
||||
yasm \
|
||||
libva-dev \
|
||||
libmfx-dev \
|
||||
zlib1g-dev" && \
|
||||
apt-get -yqq update && \
|
||||
apt-get install -yq --no-install-recommends ${buildDeps}
|
||||
## opencore-amr https://sourceforge.net/projects/opencore-amr/
|
||||
RUN \
|
||||
DIR=/tmp/opencore-amr && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://versaweb.dl.sourceforge.net/project/opencore-amr/opencore-amr/opencore-amr-${OPENCOREAMR_VERSION}.tar.gz | \
|
||||
tar -zx --strip-components=1 && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
## x264 http://www.videolan.org/developers/x264.html
|
||||
RUN \
|
||||
DIR=/tmp/x264 && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://download.videolan.org/pub/videolan/x264/snapshots/x264-snapshot-${X264_VERSION}.tar.bz2 | \
|
||||
tar -jx --strip-components=1 && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared --enable-pic --disable-cli && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
### x265 http://x265.org/
|
||||
RUN \
|
||||
DIR=/tmp/x265 && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://download.videolan.org/pub/videolan/x265/x265_${X265_VERSION}.tar.gz | \
|
||||
tar -zx && \
|
||||
cd x265_${X265_VERSION}/build/linux && \
|
||||
sed -i "/-DEXTRA_LIB/ s/$/ -DCMAKE_INSTALL_PREFIX=\${PREFIX}/" multilib.sh && \
|
||||
sed -i "/^cmake/ s/$/ -DENABLE_CLI=OFF/" multilib.sh && \
|
||||
./multilib.sh && \
|
||||
make -C 8bit install && \
|
||||
rm -rf ${DIR}
|
||||
### libogg https://www.xiph.org/ogg/
|
||||
RUN \
|
||||
DIR=/tmp/ogg && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO http://downloads.xiph.org/releases/ogg/libogg-${OGG_VERSION}.tar.gz && \
|
||||
echo ${OGG_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f libogg-${OGG_VERSION}.tar.gz && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
### libopus https://www.opus-codec.org/
|
||||
RUN \
|
||||
DIR=/tmp/opus && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://archive.mozilla.org/pub/opus/opus-${OPUS_VERSION}.tar.gz && \
|
||||
echo ${OPUS_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f opus-${OPUS_VERSION}.tar.gz && \
|
||||
autoreconf -fiv && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
### libvorbis https://xiph.org/vorbis/
|
||||
RUN \
|
||||
DIR=/tmp/vorbis && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO http://downloads.xiph.org/releases/vorbis/libvorbis-${VORBIS_VERSION}.tar.gz && \
|
||||
echo ${VORBIS_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f libvorbis-${VORBIS_VERSION}.tar.gz && \
|
||||
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
### libtheora http://www.theora.org/
|
||||
RUN \
|
||||
DIR=/tmp/theora && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO http://downloads.xiph.org/releases/theora/libtheora-${THEORA_VERSION}.tar.gz && \
|
||||
echo ${THEORA_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f libtheora-${THEORA_VERSION}.tar.gz && \
|
||||
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
### libvpx https://www.webmproject.org/code/
|
||||
RUN \
|
||||
DIR=/tmp/vpx && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://codeload.github.com/webmproject/libvpx/tar.gz/v${VPX_VERSION} | \
|
||||
tar -zx --strip-components=1 && \
|
||||
./configure --prefix="${PREFIX}" --enable-vp8 --enable-vp9 --enable-vp9-highbitdepth --enable-pic --enable-shared \
|
||||
--disable-debug --disable-examples --disable-docs --disable-install-bins && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
### libwebp https://developers.google.com/speed/webp/
|
||||
RUN \
|
||||
DIR=/tmp/vebp && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-${WEBP_VERSION}.tar.gz | \
|
||||
tar -zx --strip-components=1 && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
### libmp3lame http://lame.sourceforge.net/
|
||||
RUN \
|
||||
DIR=/tmp/lame && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://versaweb.dl.sourceforge.net/project/lame/lame/$(echo ${LAME_VERSION} | sed -e 's/[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z-]*\)/\1.\2/')/lame-${LAME_VERSION}.tar.gz | \
|
||||
tar -zx --strip-components=1 && \
|
||||
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" --enable-shared --enable-nasm --disable-frontend && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
### xvid https://www.xvid.com/
|
||||
RUN \
|
||||
DIR=/tmp/xvid && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO http://downloads.xvid.org/downloads/xvidcore-${XVID_VERSION}.tar.gz && \
|
||||
echo ${XVID_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx -f xvidcore-${XVID_VERSION}.tar.gz && \
|
||||
cd xvidcore/build/generic && \
|
||||
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
### fdk-aac https://github.com/mstorsjo/fdk-aac
|
||||
RUN \
|
||||
DIR=/tmp/fdk-aac && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://github.com/mstorsjo/fdk-aac/archive/v${FDKAAC_VERSION}.tar.gz | \
|
||||
tar -zx --strip-components=1 && \
|
||||
autoreconf -fiv && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared --datadir="${DIR}" && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
## openjpeg https://github.com/uclouvain/openjpeg
|
||||
RUN \
|
||||
DIR=/tmp/openjpeg && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz | \
|
||||
tar -zx --strip-components=1 && \
|
||||
cmake -DBUILD_THIRDPARTY:BOOL=ON -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
## freetype https://www.freetype.org/
|
||||
RUN \
|
||||
DIR=/tmp/freetype && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://download.savannah.gnu.org/releases/freetype/freetype-${FREETYPE_VERSION}.tar.gz && \
|
||||
echo ${FREETYPE_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f freetype-${FREETYPE_VERSION}.tar.gz && \
|
||||
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
## libvstab https://github.com/georgmartius/vid.stab
|
||||
RUN \
|
||||
DIR=/tmp/vid.stab && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://github.com/georgmartius/vid.stab/archive/v${LIBVIDSTAB_VERSION}.tar.gz && \
|
||||
echo ${LIBVIDSTAB_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f v${LIBVIDSTAB_VERSION}.tar.gz && \
|
||||
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
## fridibi https://www.fribidi.org/
|
||||
RUN \
|
||||
DIR=/tmp/fribidi && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://github.com/fribidi/fribidi/archive/${FRIBIDI_VERSION}.tar.gz && \
|
||||
echo ${FRIBIDI_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f ${FRIBIDI_VERSION}.tar.gz && \
|
||||
sed -i 's/^SUBDIRS =.*/SUBDIRS=gen.tab charset lib bin/' Makefile.am && \
|
||||
./bootstrap --no-config --auto && \
|
||||
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
|
||||
make -j1 && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
## kvazaar https://github.com/ultravideo/kvazaar
|
||||
RUN \
|
||||
DIR=/tmp/kvazaar && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://github.com/ultravideo/kvazaar/archive/v${KVAZAAR_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f v${KVAZAAR_VERSION}.tar.gz && \
|
||||
./autogen.sh && \
|
||||
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/aom && \
|
||||
git clone --branch ${AOM_VERSION} --depth 1 https://aomedia.googlesource.com/aom ${DIR} ; \
|
||||
cd ${DIR} ; \
|
||||
rm -rf CMakeCache.txt CMakeFiles ; \
|
||||
mkdir -p ./aom_build ; \
|
||||
cd ./aom_build ; \
|
||||
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" -DBUILD_SHARED_LIBS=1 ..; \
|
||||
make ; \
|
||||
make install ; \
|
||||
rm -rf ${DIR}
|
||||
|
||||
## libxcb (and supporting libraries) for screen capture https://xcb.freedesktop.org/
|
||||
RUN \
|
||||
DIR=/tmp/xorg-macros && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://www.x.org/archive//individual/util/util-macros-${XORG_MACROS_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f util-macros-${XORG_MACROS_VERSION}.tar.gz && \
|
||||
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/xproto && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://www.x.org/archive/individual/proto/xproto-${XPROTO_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f xproto-${XPROTO_VERSION}.tar.gz && \
|
||||
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/libXau && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://www.x.org/archive/individual/lib/libXau-${XAU_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f libXau-${XAU_VERSION}.tar.gz && \
|
||||
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/libpthread-stubs && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://xcb.freedesktop.org/dist/libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
|
||||
./configure --prefix="${PREFIX}" && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/libxcb-proto && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://xcb.freedesktop.org/dist/xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
|
||||
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
|
||||
./configure --prefix="${PREFIX}" && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/libxcb && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://xcb.freedesktop.org/dist/libxcb-${LIBXCB_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f libxcb-${LIBXCB_VERSION}.tar.gz && \
|
||||
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
|
||||
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
## libzmq https://github.com/zeromq/libzmq/
|
||||
RUN \
|
||||
DIR=/tmp/libzmq && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://github.com/zeromq/libzmq/archive/v${LIBZMQ_VERSION}.tar.gz && \
|
||||
echo ${LIBZMQ_SHA256SUM} | sha256sum --check && \
|
||||
tar -xz --strip-components=1 -f v${LIBZMQ_VERSION}.tar.gz && \
|
||||
./autogen.sh && \
|
||||
./configure --prefix="${PREFIX}" && \
|
||||
make && \
|
||||
make check && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
## ffmpeg https://ffmpeg.org/
|
||||
RUN \
|
||||
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
curl -sLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
|
||||
tar -jx --strip-components=1 -f ffmpeg-${FFMPEG_VERSION}.tar.bz2
|
||||
|
||||
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
./configure \
|
||||
--disable-debug \
|
||||
--disable-doc \
|
||||
--disable-ffplay \
|
||||
--enable-shared \
|
||||
--enable-avresample \
|
||||
--enable-libopencore-amrnb \
|
||||
--enable-libopencore-amrwb \
|
||||
--enable-gpl \
|
||||
--enable-libfreetype \
|
||||
--enable-libvidstab \
|
||||
--enable-libmfx \
|
||||
--enable-libmp3lame \
|
||||
--enable-libopus \
|
||||
--enable-libtheora \
|
||||
--enable-libvorbis \
|
||||
--enable-libvpx \
|
||||
--enable-libwebp \
|
||||
--enable-libxcb \
|
||||
--enable-libx265 \
|
||||
--enable-libxvid \
|
||||
--enable-libx264 \
|
||||
--enable-nonfree \
|
||||
--enable-openssl \
|
||||
--enable-libfdk_aac \
|
||||
--enable-postproc \
|
||||
--enable-small \
|
||||
--enable-version3 \
|
||||
--enable-libzmq \
|
||||
--extra-libs=-ldl \
|
||||
--prefix="${PREFIX}" \
|
||||
--enable-libopenjpeg \
|
||||
--enable-libkvazaar \
|
||||
--enable-libaom \
|
||||
--extra-libs=-lpthread \
|
||||
--enable-vaapi \
|
||||
--extra-cflags="-I${PREFIX}/include" \
|
||||
--extra-ldflags="-L${PREFIX}/lib" && \
|
||||
make && \
|
||||
make install && \
|
||||
make tools/zmqsend && cp tools/zmqsend ${PREFIX}/bin/ && \
|
||||
make distclean && \
|
||||
hash -r && \
|
||||
cd tools && \
|
||||
make qt-faststart && cp qt-faststart ${PREFIX}/bin/
|
||||
|
||||
## cleanup
|
||||
RUN \
|
||||
ldd ${PREFIX}/bin/ffmpeg | grep opt/ffmpeg | cut -d ' ' -f 3 | xargs -i cp {} /usr/local/lib/ && \
|
||||
for lib in /usr/local/lib/*.so.*; do ln -s "${lib##*/}" "${lib%%.so.*}".so; done && \
|
||||
cp ${PREFIX}/bin/* /usr/local/bin/ && \
|
||||
cp -r ${PREFIX}/share/ffmpeg /usr/local/share/ && \
|
||||
LD_LIBRARY_PATH=/usr/local/lib ffmpeg -buildconf && \
|
||||
cp -r ${PREFIX}/include/libav* ${PREFIX}/include/libpostproc ${PREFIX}/include/libsw* /usr/local/include && \
|
||||
mkdir -p /usr/local/lib/pkgconfig && \
|
||||
for pc in ${PREFIX}/lib/pkgconfig/libav*.pc ${PREFIX}/lib/pkgconfig/libpostproc.pc ${PREFIX}/lib/pkgconfig/libsw*.pc; do \
|
||||
sed "s:${PREFIX}:/usr/local:g" <"$pc" >/usr/local/lib/pkgconfig/"${pc##*/}"; \
|
||||
done
|
||||
|
||||
FROM base AS release
|
||||
|
||||
ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64:/usr/lib:/usr/lib64:/lib:/lib64
|
||||
|
||||
CMD ["--help"]
|
||||
ENTRYPOINT ["ffmpeg"]
|
||||
|
||||
COPY --from=build /usr/local /usr/local/
|
||||
|
||||
RUN \
|
||||
apt-get update -y && \
|
||||
apt-get install -y --no-install-recommends libva-drm2 libva2 i965-va-driver mesa-va-drivers && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
549
docker/Dockerfile.ffmpeg.amd64nvidia
Normal file
@@ -0,0 +1,549 @@
|
||||
# inspired by https://github.com/jrottenberg/ffmpeg/blob/master/docker-images/4.3/ubuntu1804/Dockerfile
|
||||
|
||||
# ffmpeg - http://ffmpeg.org/download.html
|
||||
#
|
||||
# From https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu
|
||||
#
|
||||
# https://hub.docker.com/r/jrottenberg/ffmpeg/
|
||||
#
|
||||
#
|
||||
|
||||
FROM nvidia/cuda:11.1-devel-ubuntu20.04 AS devel-base
|
||||
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES compute,utility,video
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
WORKDIR /tmp/workdir
|
||||
|
||||
RUN apt-get -yqq update && \
|
||||
apt-get install -yq --no-install-recommends ca-certificates expat libgomp1 && \
|
||||
apt-get autoremove -y && \
|
||||
apt-get clean -y
|
||||
|
||||
FROM nvidia/cuda:11.1-runtime-ubuntu20.04 AS runtime-base
|
||||
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES compute,utility,video
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
WORKDIR /tmp/workdir
|
||||
|
||||
RUN apt-get -yqq update && \
|
||||
apt-get install -yq --no-install-recommends ca-certificates expat libgomp1 libxcb-shape0-dev && \
|
||||
apt-get autoremove -y && \
|
||||
apt-get clean -y
|
||||
|
||||
|
||||
FROM devel-base as build
|
||||
|
||||
ENV NVIDIA_HEADERS_VERSION=9.1.23.1
|
||||
|
||||
ENV FFMPEG_VERSION=4.3.1 \
|
||||
AOM_VERSION=v1.0.0 \
|
||||
FDKAAC_VERSION=0.1.5 \
|
||||
FREETYPE_VERSION=2.5.5 \
|
||||
FRIBIDI_VERSION=0.19.7 \
|
||||
KVAZAAR_VERSION=1.2.0 \
|
||||
LAME_VERSION=3.100 \
|
||||
LIBPTHREAD_STUBS_VERSION=0.4 \
|
||||
LIBVIDSTAB_VERSION=1.1.0 \
|
||||
LIBXCB_VERSION=1.13.1 \
|
||||
XCBPROTO_VERSION=1.13 \
|
||||
OGG_VERSION=1.3.2 \
|
||||
OPENCOREAMR_VERSION=0.1.5 \
|
||||
OPUS_VERSION=1.2 \
|
||||
OPENJPEG_VERSION=2.1.2 \
|
||||
THEORA_VERSION=1.1.1 \
|
||||
VORBIS_VERSION=1.3.5 \
|
||||
VPX_VERSION=1.8.0 \
|
||||
WEBP_VERSION=1.0.2 \
|
||||
X264_VERSION=20170226-2245-stable \
|
||||
X265_VERSION=3.1.1 \
|
||||
XAU_VERSION=1.0.9 \
|
||||
XORG_MACROS_VERSION=1.19.2 \
|
||||
XPROTO_VERSION=7.0.31 \
|
||||
XVID_VERSION=1.3.4 \
|
||||
LIBZMQ_VERSION=4.3.2 \
|
||||
LIBSRT_VERSION=1.4.1 \
|
||||
LIBARIBB24_VERSION=1.0.3 \
|
||||
LIBPNG_VERSION=1.6.9 \
|
||||
SRC=/usr/local
|
||||
|
||||
ARG FREETYPE_SHA256SUM="5d03dd76c2171a7601e9ce10551d52d4471cf92cd205948e60289251daddffa8 freetype-2.5.5.tar.gz"
|
||||
ARG FRIBIDI_SHA256SUM="3fc96fa9473bd31dcb5500bdf1aa78b337ba13eb8c301e7c28923fea982453a8 0.19.7.tar.gz"
|
||||
ARG LIBVIDSTAB_SHA256SUM="14d2a053e56edad4f397be0cb3ef8eb1ec3150404ce99a426c4eb641861dc0bb v1.1.0.tar.gz"
|
||||
ARG OGG_SHA256SUM="e19ee34711d7af328cb26287f4137e70630e7261b17cbe3cd41011d73a654692 libogg-1.3.2.tar.gz"
|
||||
ARG OPUS_SHA256SUM="77db45a87b51578fbc49555ef1b10926179861d854eb2613207dc79d9ec0a9a9 opus-1.2.tar.gz"
|
||||
ARG THEORA_SHA256SUM="40952956c47811928d1e7922cda3bc1f427eb75680c3c37249c91e949054916b libtheora-1.1.1.tar.gz"
|
||||
ARG VORBIS_SHA256SUM="6efbcecdd3e5dfbf090341b485da9d176eb250d893e3eb378c428a2db38301ce libvorbis-1.3.5.tar.gz"
|
||||
ARG XVID_SHA256SUM="4e9fd62728885855bc5007fe1be58df42e5e274497591fec37249e1052ae316f xvidcore-1.3.4.tar.gz"
|
||||
ARG LIBZMQ_SHA256SUM="02ecc88466ae38cf2c8d79f09cfd2675ba299a439680b64ade733e26a349edeb v4.3.2.tar.gz"
|
||||
ARG LIBARIBB24_SHA256SUM="f61560738926e57f9173510389634d8c06cabedfa857db4b28fb7704707ff128 v1.0.3.tar.gz"
|
||||
|
||||
|
||||
ARG LD_LIBRARY_PATH=/opt/ffmpeg/lib
|
||||
ARG MAKEFLAGS="-j2"
|
||||
ARG PKG_CONFIG_PATH="/opt/ffmpeg/share/pkgconfig:/opt/ffmpeg/lib/pkgconfig:/opt/ffmpeg/lib64/pkgconfig"
|
||||
ARG PREFIX=/opt/ffmpeg
|
||||
ARG LD_LIBRARY_PATH="/opt/ffmpeg/lib:/opt/ffmpeg/lib64"
|
||||
|
||||
|
||||
RUN buildDeps="autoconf \
|
||||
automake \
|
||||
cmake \
|
||||
curl \
|
||||
bzip2 \
|
||||
libexpat1-dev \
|
||||
g++ \
|
||||
gcc \
|
||||
git \
|
||||
gperf \
|
||||
libtool \
|
||||
make \
|
||||
nasm \
|
||||
perl \
|
||||
pkg-config \
|
||||
python \
|
||||
libssl-dev \
|
||||
yasm \
|
||||
zlib1g-dev" && \
|
||||
apt-get -yqq update && \
|
||||
apt-get install -yq --no-install-recommends ${buildDeps}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/nv-codec-headers && \
|
||||
git clone https://github.com/FFmpeg/nv-codec-headers ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
git checkout n${NVIDIA_HEADERS_VERSION} && \
|
||||
make PREFIX="${PREFIX}" && \
|
||||
make install PREFIX="${PREFIX}" && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
## opencore-amr https://sourceforge.net/projects/opencore-amr/
|
||||
RUN \
|
||||
DIR=/tmp/opencore-amr && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://versaweb.dl.sourceforge.net/project/opencore-amr/opencore-amr/opencore-amr-${OPENCOREAMR_VERSION}.tar.gz | \
|
||||
tar -zx --strip-components=1 && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
## x264 http://www.videolan.org/developers/x264.html
|
||||
RUN \
|
||||
DIR=/tmp/x264 && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://download.videolan.org/pub/videolan/x264/snapshots/x264-snapshot-${X264_VERSION}.tar.bz2 | \
|
||||
tar -jx --strip-components=1 && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared --enable-pic --disable-cli && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
### x265 http://x265.org/
|
||||
RUN \
|
||||
DIR=/tmp/x265 && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://download.videolan.org/pub/videolan/x265/x265_${X265_VERSION}.tar.gz | \
|
||||
tar -zx && \
|
||||
cd x265_${X265_VERSION}/build/linux && \
|
||||
sed -i "/-DEXTRA_LIB/ s/$/ -DCMAKE_INSTALL_PREFIX=\${PREFIX}/" multilib.sh && \
|
||||
sed -i "/^cmake/ s/$/ -DENABLE_CLI=OFF/" multilib.sh && \
|
||||
./multilib.sh && \
|
||||
make -C 8bit install && \
|
||||
rm -rf ${DIR}
|
||||
### libogg https://www.xiph.org/ogg/
|
||||
RUN \
|
||||
DIR=/tmp/ogg && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO http://downloads.xiph.org/releases/ogg/libogg-${OGG_VERSION}.tar.gz && \
|
||||
echo ${OGG_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f libogg-${OGG_VERSION}.tar.gz && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
### libopus https://www.opus-codec.org/
|
||||
RUN \
|
||||
DIR=/tmp/opus && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://archive.mozilla.org/pub/opus/opus-${OPUS_VERSION}.tar.gz && \
|
||||
echo ${OPUS_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f opus-${OPUS_VERSION}.tar.gz && \
|
||||
autoreconf -fiv && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
### libvorbis https://xiph.org/vorbis/
|
||||
RUN \
|
||||
DIR=/tmp/vorbis && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO http://downloads.xiph.org/releases/vorbis/libvorbis-${VORBIS_VERSION}.tar.gz && \
|
||||
echo ${VORBIS_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f libvorbis-${VORBIS_VERSION}.tar.gz && \
|
||||
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
### libtheora http://www.theora.org/
|
||||
RUN \
|
||||
DIR=/tmp/theora && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO http://downloads.xiph.org/releases/theora/libtheora-${THEORA_VERSION}.tar.gz && \
|
||||
echo ${THEORA_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f libtheora-${THEORA_VERSION}.tar.gz && \
|
||||
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
### libvpx https://www.webmproject.org/code/
|
||||
RUN \
|
||||
DIR=/tmp/vpx && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://codeload.github.com/webmproject/libvpx/tar.gz/v${VPX_VERSION} | \
|
||||
tar -zx --strip-components=1 && \
|
||||
./configure --prefix="${PREFIX}" --enable-vp8 --enable-vp9 --enable-vp9-highbitdepth --enable-pic --enable-shared \
|
||||
--disable-debug --disable-examples --disable-docs --disable-install-bins && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
### libwebp https://developers.google.com/speed/webp/
|
||||
RUN \
|
||||
DIR=/tmp/vebp && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-${WEBP_VERSION}.tar.gz | \
|
||||
tar -zx --strip-components=1 && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
### libmp3lame http://lame.sourceforge.net/
|
||||
RUN \
|
||||
DIR=/tmp/lame && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://versaweb.dl.sourceforge.net/project/lame/lame/$(echo ${LAME_VERSION} | sed -e 's/[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z-]*\)/\1.\2/')/lame-${LAME_VERSION}.tar.gz | \
|
||||
tar -zx --strip-components=1 && \
|
||||
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" --enable-shared --enable-nasm --disable-frontend && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
### xvid https://www.xvid.com/
|
||||
RUN \
|
||||
DIR=/tmp/xvid && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO http://downloads.xvid.org/downloads/xvidcore-${XVID_VERSION}.tar.gz && \
|
||||
echo ${XVID_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx -f xvidcore-${XVID_VERSION}.tar.gz && \
|
||||
cd xvidcore/build/generic && \
|
||||
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
### fdk-aac https://github.com/mstorsjo/fdk-aac
|
||||
RUN \
|
||||
DIR=/tmp/fdk-aac && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://github.com/mstorsjo/fdk-aac/archive/v${FDKAAC_VERSION}.tar.gz | \
|
||||
tar -zx --strip-components=1 && \
|
||||
autoreconf -fiv && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared --datadir="${DIR}" && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
## openjpeg https://github.com/uclouvain/openjpeg
|
||||
RUN \
|
||||
DIR=/tmp/openjpeg && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz | \
|
||||
tar -zx --strip-components=1 && \
|
||||
cmake -DBUILD_THIRDPARTY:BOOL=ON -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
## freetype https://www.freetype.org/
|
||||
RUN \
|
||||
DIR=/tmp/freetype && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://download.savannah.gnu.org/releases/freetype/freetype-${FREETYPE_VERSION}.tar.gz && \
|
||||
echo ${FREETYPE_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f freetype-${FREETYPE_VERSION}.tar.gz && \
|
||||
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
## libvstab https://github.com/georgmartius/vid.stab
|
||||
RUN \
|
||||
DIR=/tmp/vid.stab && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://github.com/georgmartius/vid.stab/archive/v${LIBVIDSTAB_VERSION}.tar.gz && \
|
||||
echo ${LIBVIDSTAB_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f v${LIBVIDSTAB_VERSION}.tar.gz && \
|
||||
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
## fridibi https://www.fribidi.org/
|
||||
RUN \
|
||||
DIR=/tmp/fribidi && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://github.com/fribidi/fribidi/archive/${FRIBIDI_VERSION}.tar.gz && \
|
||||
echo ${FRIBIDI_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f ${FRIBIDI_VERSION}.tar.gz && \
|
||||
sed -i 's/^SUBDIRS =.*/SUBDIRS=gen.tab charset lib bin/' Makefile.am && \
|
||||
./bootstrap --no-config --auto && \
|
||||
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
|
||||
make -j1 && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
## kvazaar https://github.com/ultravideo/kvazaar
|
||||
RUN \
|
||||
DIR=/tmp/kvazaar && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://github.com/ultravideo/kvazaar/archive/v${KVAZAAR_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f v${KVAZAAR_VERSION}.tar.gz && \
|
||||
./autogen.sh && \
|
||||
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/aom && \
|
||||
git clone --branch ${AOM_VERSION} --depth 1 https://aomedia.googlesource.com/aom ${DIR} ; \
|
||||
cd ${DIR} ; \
|
||||
rm -rf CMakeCache.txt CMakeFiles ; \
|
||||
mkdir -p ./aom_build ; \
|
||||
cd ./aom_build ; \
|
||||
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" -DBUILD_SHARED_LIBS=1 ..; \
|
||||
make ; \
|
||||
make install ; \
|
||||
rm -rf ${DIR}
|
||||
|
||||
## libxcb (and supporting libraries) for screen capture https://xcb.freedesktop.org/
|
||||
RUN \
|
||||
DIR=/tmp/xorg-macros && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://www.x.org/archive//individual/util/util-macros-${XORG_MACROS_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f util-macros-${XORG_MACROS_VERSION}.tar.gz && \
|
||||
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/xproto && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://www.x.org/archive/individual/proto/xproto-${XPROTO_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f xproto-${XPROTO_VERSION}.tar.gz && \
|
||||
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/libXau && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://www.x.org/archive/individual/lib/libXau-${XAU_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f libXau-${XAU_VERSION}.tar.gz && \
|
||||
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/libpthread-stubs && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://xcb.freedesktop.org/dist/libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
|
||||
./configure --prefix="${PREFIX}" && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/libxcb-proto && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://xcb.freedesktop.org/dist/xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
|
||||
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
|
||||
./configure --prefix="${PREFIX}" && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/libxcb && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://xcb.freedesktop.org/dist/libxcb-${LIBXCB_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f libxcb-${LIBXCB_VERSION}.tar.gz && \
|
||||
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
|
||||
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
## libzmq https://github.com/zeromq/libzmq/
|
||||
RUN \
|
||||
DIR=/tmp/libzmq && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://github.com/zeromq/libzmq/archive/v${LIBZMQ_VERSION}.tar.gz && \
|
||||
echo ${LIBZMQ_SHA256SUM} | sha256sum --check && \
|
||||
tar -xz --strip-components=1 -f v${LIBZMQ_VERSION}.tar.gz && \
|
||||
./autogen.sh && \
|
||||
./configure --prefix="${PREFIX}" && \
|
||||
make && \
|
||||
make check && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
## libsrt https://github.com/Haivision/srt
|
||||
RUN \
|
||||
DIR=/tmp/srt && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://github.com/Haivision/srt/archive/v${LIBSRT_VERSION}.tar.gz && \
|
||||
tar -xz --strip-components=1 -f v${LIBSRT_VERSION}.tar.gz && \
|
||||
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
## libpng
|
||||
RUN \
|
||||
DIR=/tmp/png && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
git clone https://git.code.sf.net/p/libpng/code ${DIR} -b v${LIBPNG_VERSION} --depth 1 && \
|
||||
./autogen.sh && \
|
||||
./configure --prefix="${PREFIX}" && \
|
||||
make check && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
## libaribb24
|
||||
RUN \
|
||||
DIR=/tmp/b24 && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://github.com/nkoriyama/aribb24/archive/v${LIBARIBB24_VERSION}.tar.gz && \
|
||||
echo ${LIBARIBB24_SHA256SUM} | sha256sum --check && \
|
||||
tar -xz --strip-components=1 -f v${LIBARIBB24_VERSION}.tar.gz && \
|
||||
autoreconf -fiv && \
|
||||
./configure CFLAGS="-I${PREFIX}/include -fPIC" --prefix="${PREFIX}" && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
## ffmpeg https://ffmpeg.org/
|
||||
RUN \
|
||||
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
curl -sLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
|
||||
tar -jx --strip-components=1 -f ffmpeg-${FFMPEG_VERSION}.tar.bz2
|
||||
|
||||
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
./configure \
|
||||
--disable-debug \
|
||||
--disable-doc \
|
||||
--disable-ffplay \
|
||||
--enable-shared \
|
||||
--enable-avresample \
|
||||
--enable-libopencore-amrnb \
|
||||
--enable-libopencore-amrwb \
|
||||
--enable-gpl \
|
||||
--enable-libfreetype \
|
||||
--enable-libvidstab \
|
||||
--enable-libmp3lame \
|
||||
--enable-libopus \
|
||||
--enable-libtheora \
|
||||
--enable-libvorbis \
|
||||
--enable-libvpx \
|
||||
--enable-libwebp \
|
||||
--enable-libxcb \
|
||||
--enable-libx265 \
|
||||
--enable-libxvid \
|
||||
--enable-libx264 \
|
||||
--enable-nonfree \
|
||||
--enable-openssl \
|
||||
--enable-libfdk_aac \
|
||||
--enable-postproc \
|
||||
--enable-small \
|
||||
--enable-version3 \
|
||||
--enable-libzmq \
|
||||
--extra-libs=-ldl \
|
||||
--prefix="${PREFIX}" \
|
||||
--enable-libopenjpeg \
|
||||
--enable-libkvazaar \
|
||||
--enable-libaom \
|
||||
--extra-libs=-lpthread \
|
||||
--enable-libsrt \
|
||||
--enable-libaribb24 \
|
||||
--enable-nvenc \
|
||||
--enable-cuda \
|
||||
--enable-cuvid \
|
||||
--enable-libnpp \
|
||||
--extra-cflags="-I${PREFIX}/include -I${PREFIX}/include/ffnvcodec -I/usr/local/cuda/include/" \
|
||||
--extra-ldflags="-L${PREFIX}/lib -L/usr/local/cuda/lib64 -L/usr/local/cuda/lib32/" && \
|
||||
make && \
|
||||
make install && \
|
||||
make tools/zmqsend && cp tools/zmqsend ${PREFIX}/bin/ && \
|
||||
make distclean && \
|
||||
hash -r && \
|
||||
cd tools && \
|
||||
make qt-faststart && cp qt-faststart ${PREFIX}/bin/
|
||||
|
||||
## cleanup
|
||||
RUN \
|
||||
LD_LIBRARY_PATH="${PREFIX}/lib:${PREFIX}/lib64:${LD_LIBRARY_PATH}" ldd ${PREFIX}/bin/ffmpeg | grep opt/ffmpeg | cut -d ' ' -f 3 | xargs -i cp {} /usr/local/lib/ && \
|
||||
for lib in /usr/local/lib/*.so.*; do ln -s "${lib##*/}" "${lib%%.so.*}".so; done && \
|
||||
cp ${PREFIX}/bin/* /usr/local/bin/ && \
|
||||
cp -r ${PREFIX}/share/* /usr/local/share/ && \
|
||||
LD_LIBRARY_PATH=/usr/local/lib ffmpeg -buildconf && \
|
||||
cp -r ${PREFIX}/include/libav* ${PREFIX}/include/libpostproc ${PREFIX}/include/libsw* /usr/local/include && \
|
||||
mkdir -p /usr/local/lib/pkgconfig && \
|
||||
for pc in ${PREFIX}/lib/pkgconfig/libav*.pc ${PREFIX}/lib/pkgconfig/libpostproc.pc ${PREFIX}/lib/pkgconfig/libsw*.pc; do \
|
||||
sed "s:${PREFIX}:/usr/local:g; s:/lib64:/lib:g" <"$pc" >/usr/local/lib/pkgconfig/"${pc##*/}"; \
|
||||
done
|
||||
|
||||
|
||||
|
||||
FROM runtime-base AS release
|
||||
|
||||
ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64
|
||||
|
||||
CMD ["--help"]
|
||||
ENTRYPOINT ["ffmpeg"]
|
||||
|
||||
# copy only needed files, without copying nvidia dev files
|
||||
COPY --from=build /usr/local/bin /usr/local/bin/
|
||||
COPY --from=build /usr/local/share /usr/local/share/
|
||||
COPY --from=build /usr/local/lib /usr/local/lib/
|
||||
COPY --from=build /usr/local/include /usr/local/include/
|
||||
|
||||
# Let's make sure the app built correctly
|
||||
# Convenient to verify on https://hub.docker.com/r/jrottenberg/ffmpeg/builds/ console output
|
||||
490
docker/Dockerfile.ffmpeg.armv7
Normal file
@@ -0,0 +1,490 @@
|
||||
# inspired by:
|
||||
# https://github.com/collelog/ffmpeg/blob/master/4.3.1-alpine-rpi4-arm64v8.Dockerfile
|
||||
# https://github.com/mmastrac/ffmpeg-omx-rpi-docker/blob/master/Dockerfile
|
||||
# https://github.com/jrottenberg/ffmpeg/pull/158/files
|
||||
# https://github.com/jrottenberg/ffmpeg/pull/239
|
||||
FROM ubuntu:20.04 AS base
|
||||
|
||||
WORKDIR /tmp/workdir
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get -yqq update && \
|
||||
apt-get install -yq --no-install-recommends ca-certificates expat libgomp1 && \
|
||||
apt-get autoremove -y && \
|
||||
apt-get clean -y
|
||||
|
||||
FROM base as build
|
||||
|
||||
ENV FFMPEG_VERSION=4.3.1 \
|
||||
AOM_VERSION=v1.0.0 \
|
||||
FDKAAC_VERSION=0.1.5 \
|
||||
FREETYPE_VERSION=2.5.5 \
|
||||
FRIBIDI_VERSION=0.19.7 \
|
||||
KVAZAAR_VERSION=1.2.0 \
|
||||
LAME_VERSION=3.100 \
|
||||
LIBPTHREAD_STUBS_VERSION=0.4 \
|
||||
LIBVIDSTAB_VERSION=1.1.0 \
|
||||
LIBXCB_VERSION=1.13.1 \
|
||||
XCBPROTO_VERSION=1.13 \
|
||||
OGG_VERSION=1.3.2 \
|
||||
OPENCOREAMR_VERSION=0.1.5 \
|
||||
OPUS_VERSION=1.2 \
|
||||
OPENJPEG_VERSION=2.1.2 \
|
||||
THEORA_VERSION=1.1.1 \
|
||||
VORBIS_VERSION=1.3.5 \
|
||||
VPX_VERSION=1.8.0 \
|
||||
WEBP_VERSION=1.0.2 \
|
||||
X264_VERSION=20170226-2245-stable \
|
||||
X265_VERSION=3.1.1 \
|
||||
XAU_VERSION=1.0.9 \
|
||||
XORG_MACROS_VERSION=1.19.2 \
|
||||
XPROTO_VERSION=7.0.31 \
|
||||
XVID_VERSION=1.3.4 \
|
||||
LIBZMQ_VERSION=4.3.3 \
|
||||
SRC=/usr/local
|
||||
|
||||
ARG FREETYPE_SHA256SUM="5d03dd76c2171a7601e9ce10551d52d4471cf92cd205948e60289251daddffa8 freetype-2.5.5.tar.gz"
|
||||
ARG FRIBIDI_SHA256SUM="3fc96fa9473bd31dcb5500bdf1aa78b337ba13eb8c301e7c28923fea982453a8 0.19.7.tar.gz"
|
||||
ARG LIBVIDSTAB_SHA256SUM="14d2a053e56edad4f397be0cb3ef8eb1ec3150404ce99a426c4eb641861dc0bb v1.1.0.tar.gz"
|
||||
ARG OGG_SHA256SUM="e19ee34711d7af328cb26287f4137e70630e7261b17cbe3cd41011d73a654692 libogg-1.3.2.tar.gz"
|
||||
ARG OPUS_SHA256SUM="77db45a87b51578fbc49555ef1b10926179861d854eb2613207dc79d9ec0a9a9 opus-1.2.tar.gz"
|
||||
ARG THEORA_SHA256SUM="40952956c47811928d1e7922cda3bc1f427eb75680c3c37249c91e949054916b libtheora-1.1.1.tar.gz"
|
||||
ARG VORBIS_SHA256SUM="6efbcecdd3e5dfbf090341b485da9d176eb250d893e3eb378c428a2db38301ce libvorbis-1.3.5.tar.gz"
|
||||
ARG XVID_SHA256SUM="4e9fd62728885855bc5007fe1be58df42e5e274497591fec37249e1052ae316f xvidcore-1.3.4.tar.gz"
|
||||
|
||||
|
||||
ARG LD_LIBRARY_PATH=/opt/ffmpeg/lib
|
||||
ARG MAKEFLAGS="-j2"
|
||||
ARG PKG_CONFIG_PATH="/opt/ffmpeg/share/pkgconfig:/opt/ffmpeg/lib/pkgconfig:/opt/ffmpeg/lib64/pkgconfig:/opt/vc/lib/pkgconfig"
|
||||
ARG PREFIX=/opt/ffmpeg
|
||||
ARG LD_LIBRARY_PATH="/opt/ffmpeg/lib:/opt/ffmpeg/lib64:/usr/lib64:/usr/lib:/lib64:/lib:/opt/vc/lib"
|
||||
|
||||
|
||||
RUN buildDeps="autoconf \
|
||||
automake \
|
||||
cmake \
|
||||
curl \
|
||||
bzip2 \
|
||||
libexpat1-dev \
|
||||
g++ \
|
||||
gcc \
|
||||
git \
|
||||
gperf \
|
||||
libtool \
|
||||
make \
|
||||
nasm \
|
||||
perl \
|
||||
pkg-config \
|
||||
python \
|
||||
sudo \
|
||||
libssl-dev \
|
||||
yasm \
|
||||
linux-headers-raspi2 \
|
||||
libomxil-bellagio-dev \
|
||||
libx265-dev \
|
||||
libaom-dev \
|
||||
zlib1g-dev" && \
|
||||
apt-get -yqq update && \
|
||||
apt-get install -yq --no-install-recommends ${buildDeps}
|
||||
## opencore-amr https://sourceforge.net/projects/opencore-amr/
|
||||
RUN \
|
||||
DIR=/tmp/opencore-amr && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://versaweb.dl.sourceforge.net/project/opencore-amr/opencore-amr/opencore-amr-${OPENCOREAMR_VERSION}.tar.gz | \
|
||||
tar -zx --strip-components=1 && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
## x264 http://www.videolan.org/developers/x264.html
|
||||
RUN \
|
||||
DIR=/tmp/x264 && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://download.videolan.org/pub/videolan/x264/snapshots/x264-snapshot-${X264_VERSION}.tar.bz2 | \
|
||||
tar -jx --strip-components=1 && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared --enable-pic --disable-cli && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
# ### x265 http://x265.org/
|
||||
# RUN \
|
||||
# DIR=/tmp/x265 && \
|
||||
# mkdir -p ${DIR} && \
|
||||
# cd ${DIR} && \
|
||||
# curl -sL https://download.videolan.org/pub/videolan/x265/x265_${X265_VERSION}.tar.gz | \
|
||||
# tar -zx && \
|
||||
# cd x265_${X265_VERSION}/build/linux && \
|
||||
# sed -i "/-DEXTRA_LIB/ s/$/ -DCMAKE_INSTALL_PREFIX=\${PREFIX}/" multilib.sh && \
|
||||
# sed -i "/^cmake/ s/$/ -DENABLE_CLI=OFF/" multilib.sh && \
|
||||
# # export CXXFLAGS="${CXXFLAGS} -fPIC" && \
|
||||
# ./multilib.sh && \
|
||||
# make -C 8bit install && \
|
||||
# rm -rf ${DIR}
|
||||
### libogg https://www.xiph.org/ogg/
|
||||
RUN \
|
||||
DIR=/tmp/ogg && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO http://downloads.xiph.org/releases/ogg/libogg-${OGG_VERSION}.tar.gz && \
|
||||
echo ${OGG_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f libogg-${OGG_VERSION}.tar.gz && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
### libopus https://www.opus-codec.org/
|
||||
RUN \
|
||||
DIR=/tmp/opus && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://archive.mozilla.org/pub/opus/opus-${OPUS_VERSION}.tar.gz && \
|
||||
echo ${OPUS_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f opus-${OPUS_VERSION}.tar.gz && \
|
||||
autoreconf -fiv && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
### libvorbis https://xiph.org/vorbis/
|
||||
RUN \
|
||||
DIR=/tmp/vorbis && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO http://downloads.xiph.org/releases/vorbis/libvorbis-${VORBIS_VERSION}.tar.gz && \
|
||||
echo ${VORBIS_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f libvorbis-${VORBIS_VERSION}.tar.gz && \
|
||||
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
### libtheora http://www.theora.org/
|
||||
RUN \
|
||||
DIR=/tmp/theora && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO http://downloads.xiph.org/releases/theora/libtheora-${THEORA_VERSION}.tar.gz && \
|
||||
echo ${THEORA_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f libtheora-${THEORA_VERSION}.tar.gz && \
|
||||
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess && \
|
||||
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub && \
|
||||
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
### libvpx https://www.webmproject.org/code/
|
||||
RUN \
|
||||
DIR=/tmp/vpx && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://codeload.github.com/webmproject/libvpx/tar.gz/v${VPX_VERSION} | \
|
||||
tar -zx --strip-components=1 && \
|
||||
./configure --prefix="${PREFIX}" --enable-vp8 --enable-vp9 --enable-vp9-highbitdepth --enable-pic --enable-shared \
|
||||
--disable-debug --disable-examples --disable-docs --disable-install-bins && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
### libwebp https://developers.google.com/speed/webp/
|
||||
RUN \
|
||||
DIR=/tmp/vebp && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-${WEBP_VERSION}.tar.gz | \
|
||||
tar -zx --strip-components=1 && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
### libmp3lame http://lame.sourceforge.net/
|
||||
RUN \
|
||||
DIR=/tmp/lame && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://versaweb.dl.sourceforge.net/project/lame/lame/$(echo ${LAME_VERSION} | sed -e 's/[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z-]*\)/\1.\2/')/lame-${LAME_VERSION}.tar.gz | \
|
||||
tar -zx --strip-components=1 && \
|
||||
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" --enable-shared --enable-nasm --disable-frontend && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
### xvid https://www.xvid.com/
|
||||
RUN \
|
||||
DIR=/tmp/xvid && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO http://downloads.xvid.org/downloads/xvidcore-${XVID_VERSION}.tar.gz && \
|
||||
echo ${XVID_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx -f xvidcore-${XVID_VERSION}.tar.gz && \
|
||||
cd xvidcore/build/generic && \
|
||||
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
### fdk-aac https://github.com/mstorsjo/fdk-aac
|
||||
RUN \
|
||||
DIR=/tmp/fdk-aac && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://github.com/mstorsjo/fdk-aac/archive/v${FDKAAC_VERSION}.tar.gz | \
|
||||
tar -zx --strip-components=1 && \
|
||||
autoreconf -fiv && \
|
||||
./configure --prefix="${PREFIX}" --enable-shared --datadir="${DIR}" && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
## openjpeg https://github.com/uclouvain/openjpeg
|
||||
RUN \
|
||||
DIR=/tmp/openjpeg && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sL https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz | \
|
||||
tar -zx --strip-components=1 && \
|
||||
export CFLAGS="${CFLAGS} -DPNG_ARM_NEON_OPT=0" && \
|
||||
cmake -DBUILD_THIRDPARTY:BOOL=ON -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
## freetype https://www.freetype.org/
|
||||
RUN \
|
||||
DIR=/tmp/freetype && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://download.savannah.gnu.org/releases/freetype/freetype-${FREETYPE_VERSION}.tar.gz && \
|
||||
echo ${FREETYPE_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f freetype-${FREETYPE_VERSION}.tar.gz && \
|
||||
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
## libvstab https://github.com/georgmartius/vid.stab
|
||||
RUN \
|
||||
DIR=/tmp/vid.stab && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://github.com/georgmartius/vid.stab/archive/v${LIBVIDSTAB_VERSION}.tar.gz && \
|
||||
echo ${LIBVIDSTAB_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f v${LIBVIDSTAB_VERSION}.tar.gz && \
|
||||
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
## fridibi https://www.fribidi.org/
|
||||
RUN \
|
||||
DIR=/tmp/fribidi && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://github.com/fribidi/fribidi/archive/${FRIBIDI_VERSION}.tar.gz && \
|
||||
echo ${FRIBIDI_SHA256SUM} | sha256sum --check && \
|
||||
tar -zx --strip-components=1 -f ${FRIBIDI_VERSION}.tar.gz && \
|
||||
sed -i 's/^SUBDIRS =.*/SUBDIRS=gen.tab charset lib bin/' Makefile.am && \
|
||||
./bootstrap --no-config --auto && \
|
||||
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
|
||||
make -j1 && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
## kvazaar https://github.com/ultravideo/kvazaar
|
||||
RUN \
|
||||
DIR=/tmp/kvazaar && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://github.com/ultravideo/kvazaar/archive/v${KVAZAAR_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f v${KVAZAAR_VERSION}.tar.gz && \
|
||||
./autogen.sh && \
|
||||
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
# RUN \
|
||||
# DIR=/tmp/aom && \
|
||||
# git clone --branch ${AOM_VERSION} --depth 1 https://aomedia.googlesource.com/aom ${DIR} ; \
|
||||
# cd ${DIR} ; \
|
||||
# rm -rf CMakeCache.txt CMakeFiles ; \
|
||||
# mkdir -p ./aom_build ; \
|
||||
# cd ./aom_build ; \
|
||||
# cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" -DBUILD_SHARED_LIBS=1 ..; \
|
||||
# make ; \
|
||||
# make install ; \
|
||||
# rm -rf ${DIR}
|
||||
|
||||
## libxcb (and supporting libraries) for screen capture https://xcb.freedesktop.org/
|
||||
RUN \
|
||||
DIR=/tmp/xorg-macros && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://www.x.org/archive//individual/util/util-macros-${XORG_MACROS_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f util-macros-${XORG_MACROS_VERSION}.tar.gz && \
|
||||
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/xproto && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://www.x.org/archive/individual/proto/xproto-${XPROTO_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f xproto-${XPROTO_VERSION}.tar.gz && \
|
||||
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess && \
|
||||
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub && \
|
||||
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/libXau && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://www.x.org/archive/individual/lib/libXau-${XAU_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f libXau-${XAU_VERSION}.tar.gz && \
|
||||
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/libpthread-stubs && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://xcb.freedesktop.org/dist/libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
|
||||
./configure --prefix="${PREFIX}" && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/libxcb-proto && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://xcb.freedesktop.org/dist/xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
|
||||
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
|
||||
./configure --prefix="${PREFIX}" && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/libxcb && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://xcb.freedesktop.org/dist/libxcb-${LIBXCB_VERSION}.tar.gz && \
|
||||
tar -zx --strip-components=1 -f libxcb-${LIBXCB_VERSION}.tar.gz && \
|
||||
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
|
||||
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
## libzmq https://github.com/zeromq/libzmq/
|
||||
RUN \
|
||||
DIR=/tmp/libzmq && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
curl -sLO https://github.com/zeromq/libzmq/archive/v${LIBZMQ_VERSION}.tar.gz && \
|
||||
tar -xz --strip-components=1 -f v${LIBZMQ_VERSION}.tar.gz && \
|
||||
./autogen.sh && \
|
||||
./configure --prefix="${PREFIX}" && \
|
||||
make -j $(nproc) && \
|
||||
# make check && \
|
||||
make -j $(nproc) install && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
## userland https://github.com/raspberrypi/userland
|
||||
RUN \
|
||||
DIR=/tmp/userland && \
|
||||
mkdir -p ${DIR} && \
|
||||
cd ${DIR} && \
|
||||
git clone --depth 1 https://github.com/raspberrypi/userland.git . && \
|
||||
./buildme && \
|
||||
rm -rf ${DIR}
|
||||
|
||||
## ffmpeg https://ffmpeg.org/
|
||||
RUN \
|
||||
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
curl -sLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
|
||||
tar -jx --strip-components=1 -f ffmpeg-${FFMPEG_VERSION}.tar.bz2
|
||||
|
||||
RUN \
|
||||
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
./configure \
|
||||
--disable-debug \
|
||||
--disable-doc \
|
||||
--disable-ffplay \
|
||||
--enable-shared \
|
||||
--enable-avresample \
|
||||
--enable-libopencore-amrnb \
|
||||
--enable-libopencore-amrwb \
|
||||
--enable-gpl \
|
||||
--enable-libfreetype \
|
||||
--enable-libvidstab \
|
||||
--enable-libmp3lame \
|
||||
--enable-libopus \
|
||||
--enable-libtheora \
|
||||
--enable-libvorbis \
|
||||
--enable-libvpx \
|
||||
--enable-libwebp \
|
||||
--enable-libxcb \
|
||||
--enable-libx265 \
|
||||
--enable-libxvid \
|
||||
--enable-libx264 \
|
||||
--enable-nonfree \
|
||||
--enable-openssl \
|
||||
--enable-libfdk_aac \
|
||||
--enable-postproc \
|
||||
--enable-small \
|
||||
--enable-version3 \
|
||||
--enable-libzmq \
|
||||
--extra-libs=-ldl \
|
||||
--prefix="${PREFIX}" \
|
||||
--enable-libopenjpeg \
|
||||
--enable-libkvazaar \
|
||||
--enable-libaom \
|
||||
--extra-libs=-lpthread \
|
||||
--enable-omx \
|
||||
--enable-omx-rpi \
|
||||
--enable-mmal \
|
||||
--enable-v4l2_m2m \
|
||||
--enable-neon \
|
||||
--extra-cflags="-I${PREFIX}/include" \
|
||||
--extra-ldflags="-L${PREFIX}/lib" && \
|
||||
make -j $(nproc) && \
|
||||
make -j $(nproc) install && \
|
||||
make tools/zmqsend && cp tools/zmqsend ${PREFIX}/bin/ && \
|
||||
make distclean && \
|
||||
hash -r && \
|
||||
cd tools && \
|
||||
make qt-faststart && cp qt-faststart ${PREFIX}/bin/
|
||||
|
||||
## cleanup
|
||||
RUN \
|
||||
ldd ${PREFIX}/bin/ffmpeg | grep opt/ffmpeg | cut -d ' ' -f 3 | xargs -i cp {} /usr/local/lib/ && \
|
||||
# copy userland lib too
|
||||
ldd ${PREFIX}/bin/ffmpeg | grep opt/vc | cut -d ' ' -f 3 | xargs -i cp {} /usr/local/lib/ && \
|
||||
for lib in /usr/local/lib/*.so.*; do ln -s "${lib##*/}" "${lib%%.so.*}".so; done && \
|
||||
cp ${PREFIX}/bin/* /usr/local/bin/ && \
|
||||
cp -r ${PREFIX}/share/ffmpeg /usr/local/share/ && \
|
||||
LD_LIBRARY_PATH=/usr/local/lib ffmpeg -buildconf && \
|
||||
cp -r ${PREFIX}/include/libav* ${PREFIX}/include/libpostproc ${PREFIX}/include/libsw* /usr/local/include && \
|
||||
mkdir -p /usr/local/lib/pkgconfig && \
|
||||
for pc in ${PREFIX}/lib/pkgconfig/libav*.pc ${PREFIX}/lib/pkgconfig/libpostproc.pc ${PREFIX}/lib/pkgconfig/libsw*.pc; do \
|
||||
sed "s:${PREFIX}:/usr/local:g" <"$pc" >/usr/local/lib/pkgconfig/"${pc##*/}"; \
|
||||
done
|
||||
|
||||
FROM base AS release
|
||||
|
||||
ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64:/usr/lib:/usr/lib64:/lib:/lib64
|
||||
|
||||
RUN \
|
||||
apt-get -yqq update && \
|
||||
apt-get install -yq --no-install-recommends libx265-dev libaom-dev && \
|
||||
apt-get autoremove -y && \
|
||||
apt-get clean -y
|
||||
|
||||
CMD ["--help"]
|
||||
ENTRYPOINT ["ffmpeg"]
|
||||
|
||||
COPY --from=build /usr/local /usr/local/
|
||||
39
docker/Dockerfile.wheels
Normal file
@@ -0,0 +1,39 @@
|
||||
FROM ubuntu:20.04 as build
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get -qq update \
|
||||
&& apt-get -qq install -y \
|
||||
python3 \
|
||||
python3-dev \
|
||||
wget \
|
||||
# opencv dependencies
|
||||
build-essential cmake git pkg-config libgtk-3-dev \
|
||||
libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \
|
||||
libxvidcore-dev libx264-dev libjpeg-dev libpng-dev libtiff-dev \
|
||||
gfortran openexr libatlas-base-dev libssl-dev\
|
||||
libtbb2 libtbb-dev libdc1394-22-dev libopenexr-dev \
|
||||
libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev \
|
||||
# scipy dependencies
|
||||
gcc gfortran libopenblas-dev liblapack-dev cython
|
||||
|
||||
RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \
|
||||
&& python3 get-pip.py
|
||||
|
||||
RUN pip3 install scikit-build
|
||||
|
||||
RUN pip3 wheel --wheel-dir=/wheels \
|
||||
opencv-python-headless \
|
||||
numpy \
|
||||
imutils \
|
||||
scipy \
|
||||
psutil \
|
||||
Flask \
|
||||
paho-mqtt \
|
||||
PyYAML \
|
||||
matplotlib \
|
||||
click
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=build /wheels /wheels
|
||||
49
docker/Dockerfile.wheels.aarch64
Normal file
@@ -0,0 +1,49 @@
|
||||
FROM ubuntu:20.04 as build
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get -qq update \
|
||||
&& apt-get -qq install -y \
|
||||
python3 \
|
||||
python3-dev \
|
||||
wget \
|
||||
# opencv dependencies
|
||||
build-essential cmake git pkg-config libgtk-3-dev \
|
||||
libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \
|
||||
libxvidcore-dev libx264-dev libjpeg-dev libpng-dev libtiff-dev \
|
||||
gfortran openexr libatlas-base-dev libssl-dev\
|
||||
libtbb2 libtbb-dev libdc1394-22-dev libopenexr-dev \
|
||||
libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev \
|
||||
# scipy dependencies
|
||||
gcc gfortran libopenblas-dev liblapack-dev cython
|
||||
|
||||
RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \
|
||||
&& python3 get-pip.py
|
||||
|
||||
# need to build cmake from source because binary distribution is broken for arm64
|
||||
# https://github.com/scikit-build/cmake-python-distributions/issues/115
|
||||
# https://github.com/skvark/opencv-python/issues/366
|
||||
# https://github.com/scikit-build/cmake-python-distributions/issues/96#issuecomment-663062358
|
||||
RUN pip3 install scikit-build
|
||||
|
||||
RUN git clone https://github.com/scikit-build/cmake-python-distributions.git \
|
||||
&& cd cmake-python-distributions/ \
|
||||
&& python3 setup.py bdist_wheel
|
||||
|
||||
RUN pip3 install cmake-python-distributions/dist/*.whl
|
||||
|
||||
RUN pip3 wheel --wheel-dir=/wheels \
|
||||
opencv-python-headless \
|
||||
numpy \
|
||||
imutils \
|
||||
scipy \
|
||||
psutil \
|
||||
Flask \
|
||||
paho-mqtt \
|
||||
PyYAML \
|
||||
matplotlib \
|
||||
click
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=build /wheels /wheels
|
||||
21
docs/cameras.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Camera Specific Configuration
|
||||
Frigate should work with most RTSP cameras and h264 feeds such as Dahua.
|
||||
|
||||
## RTMP Cameras
|
||||
The input parameters need to be adjusted for RTMP cameras
|
||||
```yaml
|
||||
ffmpeg:
|
||||
input_args:
|
||||
- -avoid_negative_ts
|
||||
- make_zero
|
||||
- -fflags
|
||||
- nobuffer
|
||||
- -flags
|
||||
- low_delay
|
||||
- -strict
|
||||
- experimental
|
||||
- -fflags
|
||||
- +genpts+discardcorrupt
|
||||
- -use_wallclock_as_timestamps
|
||||
- '1'
|
||||
```
|
||||
BIN
docs/diagram.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
docs/example-mask-check-point.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
docs/example-mask-overlay.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
docs/example-mask-poly.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
docs/example-mask.bmp
Normal file
|
After Width: | Height: | Size: 6.0 MiB |
BIN
docs/frigate.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
10
docs/how-frigate-works.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# How Frigate Works
|
||||
Frigate is designed to minimize resource and maximize performance by only looking for objects when and where it is necessary
|
||||
|
||||

|
||||
|
||||
## 1. Look for Motion
|
||||
|
||||
## 2. Calculate Detection Regions
|
||||
|
||||
## 3. Run Object Detection
|
||||
BIN
docs/media_browser.png
Normal file
|
After Width: | Height: | Size: 781 KiB |
71
docs/notification-examples.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Notification examples
|
||||
|
||||
Here are some examples of notifications for the HomeAssistant android companion app:
|
||||
```yaml
|
||||
automation:
|
||||
|
||||
- alias: When a person enters a zone named yard
|
||||
trigger:
|
||||
platform: mqtt
|
||||
topic: frigate/events
|
||||
conditions:
|
||||
- "{{ trigger.payload_json['after']['label'] == 'person' }}"
|
||||
- "{{ 'yard' in trigger.payload_json['after']['entered_zones'] }}"
|
||||
action:
|
||||
- service: notify.mobile_app_pixel_3
|
||||
data_template:
|
||||
message: "A {{trigger.payload_json['after']['label']}} has entered the yard."
|
||||
data:
|
||||
image: "https://url.com/api/frigate/notifications/{{trigger.payload_json['after']['id']}}.jpg"
|
||||
tag: "{{trigger.payload_json['after']['id']}}"
|
||||
|
||||
- alias: When a person leaves a zone named yard
|
||||
trigger:
|
||||
platform: mqtt
|
||||
topic: frigate/events
|
||||
conditions:
|
||||
- "{{ trigger.payload_json['after']['label'] == 'person' }}"
|
||||
- "{{ 'yard' in trigger.payload_json['before']['current_zones'] }}"
|
||||
- "{{ not 'yard' in trigger.payload_json['after']['current_zones'] }}"
|
||||
action:
|
||||
- service: notify.mobile_app_pixel_3
|
||||
data_template:
|
||||
message: "A {{trigger.payload_json['after']['label']}} has left the yard."
|
||||
data:
|
||||
image: "https://url.com/api/frigate/notifications/{{trigger.payload_json['after']['id']}}.jpg"
|
||||
tag: "{{trigger.payload_json['after']['id']}}"
|
||||
|
||||
- alias: Notify for dogs in the front with a high top score
|
||||
trigger:
|
||||
platform: mqtt
|
||||
topic: frigate/events
|
||||
conditions:
|
||||
- "{{ trigger.payload_json['after']['label'] == 'dog' }}"
|
||||
- "{{ trigger.payload_json['after']['camera'] == 'front' }}"
|
||||
- "{{ trigger.payload_json['after']['top_score'] > 0.98 }}"
|
||||
action:
|
||||
- service: notify.mobile_app_pixel_3
|
||||
data_template:
|
||||
message: 'High confidence dog detection.'
|
||||
data:
|
||||
image: "https://url.com/api/frigate/notifications/{{trigger.payload_json['after']['id']}}.jpg"
|
||||
tag: "{{trigger.payload_json['after']['id']}}"
|
||||
```
|
||||
|
||||
If you are using telegram, you can fetch the image directly from Frigate:
|
||||
```yaml
|
||||
automation:
|
||||
- alias: Notify of events
|
||||
trigger:
|
||||
platform: mqtt
|
||||
topic: frigate/events
|
||||
action:
|
||||
- service: notify.telegram_full
|
||||
data_template:
|
||||
message: 'A {{trigger.payload_json["after"]["label"]}} was detected.'
|
||||
data:
|
||||
photo:
|
||||
# this url should work for addon users
|
||||
- url: 'http://ccab4aaf-frigate:5000/events/{{trigger.payload_json["after"]["id"]}}/snapshot.jpg'
|
||||
caption : 'A {{trigger.payload_json["after"]["label"]}} was detected on {{ trigger.payload_json["after"]["camera"] }} camera'
|
||||
```
|
||||
BIN
docs/notification.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
105
docs/nvdec.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# nVidia hardware decoder (NVDEC)
|
||||
|
||||
Certain nvidia cards include a hardware decoder, which can greatly improve the
|
||||
performance of video decoding. In order to use NVDEC, a special build of
|
||||
ffmpeg with NVDEC support is required. The special docker architecture 'amd64nvidia'
|
||||
includes this support for amd64 platforms. An aarch64 for the Jetson, which
|
||||
also includes NVDEC may be added in the future.
|
||||
|
||||
## Docker setup
|
||||
|
||||
### Requirements
|
||||
[nVidia closed source driver](https://www.nvidia.com/en-us/drivers/unix/) required to access NVDEC.
|
||||
[nvidia-docker](https://github.com/NVIDIA/nvidia-docker) required to pass NVDEC to docker.
|
||||
|
||||
### Setting up docker-compose
|
||||
|
||||
In order to pass NVDEC, the docker engine must be set to `nvidia` and the environment variables
|
||||
`NVIDIA_VISIBLE_DEVICES=all` and `NVIDIA_DRIVER_CAPABILITIES=compute,utility,video` must be set.
|
||||
|
||||
In a docker compose file, these lines need to be set:
|
||||
```
|
||||
services:
|
||||
frigate:
|
||||
...
|
||||
image: blakeblackshear/frigate:stable-amd64nvidia
|
||||
runtime: nvidia
|
||||
environment:
|
||||
- NVIDIA_VISIBLE_DEVICES=all
|
||||
- NVIDIA_DRIVER_CAPABILITIES=compute,utility,video
|
||||
```
|
||||
|
||||
### Setting up the configuration file
|
||||
|
||||
In your frigate config.yml, you'll need to set ffmpeg to use the hardware decoder.
|
||||
The decoder you choose will depend on the input video.
|
||||
|
||||
A list of supported codecs (you can use `ffmpeg -decoders | grep cuvid` in the container to get a list)
|
||||
```
|
||||
V..... h263_cuvid Nvidia CUVID H263 decoder (codec h263)
|
||||
V..... h264_cuvid Nvidia CUVID H264 decoder (codec h264)
|
||||
V..... hevc_cuvid Nvidia CUVID HEVC decoder (codec hevc)
|
||||
V..... mjpeg_cuvid Nvidia CUVID MJPEG decoder (codec mjpeg)
|
||||
V..... mpeg1_cuvid Nvidia CUVID MPEG1VIDEO decoder (codec mpeg1video)
|
||||
V..... mpeg2_cuvid Nvidia CUVID MPEG2VIDEO decoder (codec mpeg2video)
|
||||
V..... mpeg4_cuvid Nvidia CUVID MPEG4 decoder (codec mpeg4)
|
||||
V..... vc1_cuvid Nvidia CUVID VC1 decoder (codec vc1)
|
||||
V..... vp8_cuvid Nvidia CUVID VP8 decoder (codec vp8)
|
||||
V..... vp9_cuvid Nvidia CUVID VP9 decoder (codec vp9)
|
||||
```
|
||||
|
||||
For example, for H265 video (hevc), you'll select `hevc_cuvid`. Add
|
||||
`-c:v hevc_covid` to your ffmpeg input arguments:
|
||||
|
||||
```
|
||||
ffmpeg:
|
||||
input_args:
|
||||
...
|
||||
- -c:v
|
||||
- hevc_cuvid
|
||||
```
|
||||
|
||||
If everything is working correctly, you should see a significant improvement in performance.
|
||||
Verify that hardware decoding is working by running `nvidia-smi`, which should show the ffmpeg
|
||||
processes:
|
||||
|
||||
```
|
||||
+-----------------------------------------------------------------------------+
|
||||
| NVIDIA-SMI 455.38 Driver Version: 455.38 CUDA Version: 11.1 |
|
||||
|-------------------------------+----------------------+----------------------+
|
||||
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
|
||||
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
|
||||
| | | MIG M. |
|
||||
|===============================+======================+======================|
|
||||
| 0 GeForce GTX 166... Off | 00000000:03:00.0 Off | N/A |
|
||||
| 38% 41C P2 36W / 125W | 2082MiB / 5942MiB | 5% Default |
|
||||
| | | N/A |
|
||||
+-------------------------------+----------------------+----------------------+
|
||||
|
||||
+-----------------------------------------------------------------------------+
|
||||
| Processes: |
|
||||
| GPU GI CI PID Type Process name GPU Memory |
|
||||
| ID ID Usage |
|
||||
|=============================================================================|
|
||||
| 0 N/A N/A 12737 C ffmpeg 249MiB |
|
||||
| 0 N/A N/A 12751 C ffmpeg 249MiB |
|
||||
| 0 N/A N/A 12772 C ffmpeg 249MiB |
|
||||
| 0 N/A N/A 12775 C ffmpeg 249MiB |
|
||||
| 0 N/A N/A 12800 C ffmpeg 249MiB |
|
||||
| 0 N/A N/A 12811 C ffmpeg 417MiB |
|
||||
| 0 N/A N/A 12827 C ffmpeg 417MiB |
|
||||
+-----------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
To further improve performance, you can set ffmpeg to skip frames in the output,
|
||||
using the fps filter:
|
||||
|
||||
```
|
||||
output_args:
|
||||
- -filter:v
|
||||
- fps=fps=5
|
||||
```
|
||||
|
||||
This setting, for example, allows Frigate to consume my 10-15fps camera streams on
|
||||
my relatively low powered Haswell machine with relatively low cpu usage.
|
||||
|
||||
BIN
docs/zone_example.jpg
Normal file
|
After Width: | Height: | Size: 73 KiB |
0
frigate/__init__.py
Normal file
15
frigate/__main__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import faulthandler; faulthandler.enable()
|
||||
import sys
|
||||
import threading
|
||||
|
||||
threading.current_thread().name = "frigate"
|
||||
|
||||
from frigate.app import FrigateApp
|
||||
|
||||
cli = sys.modules['flask.cli']
|
||||
cli.show_server_banner = lambda *x: None
|
||||
|
||||
if __name__ == '__main__':
|
||||
frigate_app = FrigateApp()
|
||||
|
||||
frigate_app.start()
|
||||
235
frigate/app.py
Normal file
@@ -0,0 +1,235 @@
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing as mp
|
||||
import os
|
||||
from logging.handlers import QueueHandler
|
||||
from typing import Dict, List
|
||||
import sys
|
||||
import signal
|
||||
|
||||
import yaml
|
||||
from playhouse.sqlite_ext import SqliteExtDatabase
|
||||
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
|
||||
from frigate.edgetpu import EdgeTPUProcess
|
||||
from frigate.events import EventProcessor, EventCleanup
|
||||
from frigate.http import create_app
|
||||
from frigate.log import log_process, root_configurer
|
||||
from frigate.models import Event
|
||||
from frigate.mqtt import create_mqtt_client
|
||||
from frigate.object_processing import TrackedObjectProcessor
|
||||
from frigate.record import RecordingMaintainer
|
||||
from frigate.video import capture_camera, track_camera
|
||||
from frigate.watchdog import FrigateWatchdog
|
||||
from frigate.zeroconf import broadcast_zeroconf
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class FrigateApp():
|
||||
def __init__(self):
|
||||
self.stop_event = mp.Event()
|
||||
self.config: FrigateConfig = None
|
||||
self.detection_queue = mp.Queue()
|
||||
self.detectors: Dict[str, EdgeTPUProcess] = {}
|
||||
self.detection_out_events: Dict[str, mp.Event] = {}
|
||||
self.detection_shms: List[mp.shared_memory.SharedMemory] = []
|
||||
self.log_queue = mp.Queue()
|
||||
self.camera_metrics = {}
|
||||
|
||||
def ensure_dirs(self):
|
||||
tmpfs_size = self.config.save_clips.tmpfs_cache_size
|
||||
if tmpfs_size:
|
||||
logger.info(f"Creating tmpfs of size {tmpfs_size}")
|
||||
rc = os.system(f"mount -t tmpfs -o size={tmpfs_size} tmpfs {CACHE_DIR}")
|
||||
if rc != 0:
|
||||
logger.error(f"Failed to create tmpfs, error code: {rc}")
|
||||
|
||||
for d in [RECORD_DIR, CLIPS_DIR, CACHE_DIR]:
|
||||
if not os.path.exists(d) and not os.path.islink(d):
|
||||
logger.info(f"Creating directory: {d}")
|
||||
os.makedirs(d)
|
||||
else:
|
||||
logger.debug(f"Skipping directory: {d}")
|
||||
|
||||
def init_logger(self):
|
||||
self.log_process = mp.Process(target=log_process, args=(self.log_queue,), name='log_process')
|
||||
self.log_process.daemon = True
|
||||
self.log_process.start()
|
||||
root_configurer(self.log_queue)
|
||||
|
||||
def init_config(self):
|
||||
config_file = os.environ.get('CONFIG_FILE', '/config/config.yml')
|
||||
self.config = FrigateConfig(config_file=config_file)
|
||||
|
||||
for camera_name in self.config.cameras.keys():
|
||||
# create camera_metrics
|
||||
self.camera_metrics[camera_name] = {
|
||||
'camera_fps': mp.Value('d', 0.0),
|
||||
'skipped_fps': mp.Value('d', 0.0),
|
||||
'process_fps': mp.Value('d', 0.0),
|
||||
'detection_fps': mp.Value('d', 0.0),
|
||||
'detection_frame': mp.Value('d', 0.0),
|
||||
'read_start': mp.Value('d', 0.0),
|
||||
'ffmpeg_pid': mp.Value('i', 0),
|
||||
'frame_queue': mp.Queue(maxsize=2)
|
||||
}
|
||||
|
||||
def check_config(self):
|
||||
for name, camera in self.config.cameras.items():
|
||||
assigned_roles = list(set([r for i in camera.ffmpeg.inputs for r in i.roles]))
|
||||
if not camera.save_clips.enabled and 'clips' in assigned_roles:
|
||||
logger.warning(f"Camera {name} has clips assigned to an input, but save_clips is not enabled.")
|
||||
elif camera.save_clips.enabled and not 'clips' in assigned_roles:
|
||||
logger.warning(f"Camera {name} has save_clips enabled, but clips is not assigned to an input.")
|
||||
|
||||
if not camera.record.enabled and 'record' in assigned_roles:
|
||||
logger.warning(f"Camera {name} has record assigned to an input, but record is not enabled.")
|
||||
elif camera.record.enabled and not 'record' in assigned_roles:
|
||||
logger.warning(f"Camera {name} has record enabled, but record is not assigned to an input.")
|
||||
|
||||
if not camera.rtmp.enabled and 'rtmp' in assigned_roles:
|
||||
logger.warning(f"Camera {name} has rtmp assigned to an input, but rtmp is not enabled.")
|
||||
elif camera.rtmp.enabled and not 'rtmp' in assigned_roles:
|
||||
logger.warning(f"Camera {name} has rtmp enabled, but rtmp is not assigned to an input.")
|
||||
|
||||
def set_log_levels(self):
|
||||
logging.getLogger().setLevel(self.config.logger.default)
|
||||
for log, level in self.config.logger.logs.items():
|
||||
logging.getLogger(log).setLevel(level)
|
||||
|
||||
if not 'werkzeug' in self.config.logger.logs:
|
||||
logging.getLogger('werkzeug').setLevel('ERROR')
|
||||
|
||||
def init_queues(self):
|
||||
# Queues for clip processing
|
||||
self.event_queue = mp.Queue()
|
||||
self.event_processed_queue = mp.Queue()
|
||||
|
||||
# Queue for cameras to push tracked objects to
|
||||
self.detected_frames_queue = mp.Queue(maxsize=len(self.config.cameras.keys())*2)
|
||||
|
||||
def init_database(self):
|
||||
self.db = SqliteExtDatabase(self.config.database.path)
|
||||
models = [Event]
|
||||
self.db.bind(models)
|
||||
self.db.create_tables(models, safe=True)
|
||||
|
||||
def init_web_server(self):
|
||||
self.flask_app = create_app(self.config, self.db, self.camera_metrics, self.detectors, self.detected_frames_processor)
|
||||
|
||||
def init_mqtt(self):
|
||||
self.mqtt_client = create_mqtt_client(self.config.mqtt)
|
||||
|
||||
def start_detectors(self):
|
||||
model_shape = (self.config.model.height, self.config.model.width)
|
||||
for name in self.config.cameras.keys():
|
||||
self.detection_out_events[name] = mp.Event()
|
||||
shm_in = mp.shared_memory.SharedMemory(name=name, create=True, size=self.config.model.height*self.config.model.width*3)
|
||||
shm_out = mp.shared_memory.SharedMemory(name=f"out-{name}", create=True, size=20*6*4)
|
||||
self.detection_shms.append(shm_in)
|
||||
self.detection_shms.append(shm_out)
|
||||
|
||||
for name, detector in self.config.detectors.items():
|
||||
if detector.type == 'cpu':
|
||||
self.detectors[name] = EdgeTPUProcess(name, self.detection_queue, self.detection_out_events, model_shape, 'cpu', detector.num_threads)
|
||||
if detector.type == 'edgetpu':
|
||||
self.detectors[name] = EdgeTPUProcess(name, self.detection_queue, self.detection_out_events, model_shape, detector.device, detector.num_threads)
|
||||
|
||||
def start_detected_frames_processor(self):
|
||||
self.detected_frames_processor = TrackedObjectProcessor(self.config, self.mqtt_client, self.config.mqtt.topic_prefix,
|
||||
self.detected_frames_queue, self.event_queue, self.event_processed_queue, self.stop_event)
|
||||
self.detected_frames_processor.start()
|
||||
|
||||
def start_camera_processors(self):
|
||||
model_shape = (self.config.model.height, self.config.model.width)
|
||||
for name, config in self.config.cameras.items():
|
||||
camera_process = mp.Process(target=track_camera, name=f"camera_processor:{name}", args=(name, config, model_shape,
|
||||
self.detection_queue, self.detection_out_events[name], self.detected_frames_queue,
|
||||
self.camera_metrics[name]))
|
||||
camera_process.daemon = True
|
||||
self.camera_metrics[name]['process'] = camera_process
|
||||
camera_process.start()
|
||||
logger.info(f"Camera processor started for {name}: {camera_process.pid}")
|
||||
|
||||
def start_camera_capture_processes(self):
|
||||
for name, config in self.config.cameras.items():
|
||||
capture_process = mp.Process(target=capture_camera, name=f"camera_capture:{name}", args=(name, config,
|
||||
self.camera_metrics[name]))
|
||||
capture_process.daemon = True
|
||||
self.camera_metrics[name]['capture_process'] = capture_process
|
||||
capture_process.start()
|
||||
logger.info(f"Capture process started for {name}: {capture_process.pid}")
|
||||
|
||||
def start_event_processor(self):
|
||||
self.event_processor = EventProcessor(self.config, self.camera_metrics, self.event_queue, self.event_processed_queue, self.stop_event)
|
||||
self.event_processor.start()
|
||||
|
||||
def start_event_cleanup(self):
|
||||
self.event_cleanup = EventCleanup(self.config, self.stop_event)
|
||||
self.event_cleanup.start()
|
||||
|
||||
def start_recording_maintainer(self):
|
||||
self.recording_maintainer = RecordingMaintainer(self.config, self.stop_event)
|
||||
self.recording_maintainer.start()
|
||||
|
||||
def start_watchdog(self):
|
||||
self.frigate_watchdog = FrigateWatchdog(self.detectors, self.stop_event)
|
||||
self.frigate_watchdog.start()
|
||||
|
||||
def start(self):
|
||||
self.init_logger()
|
||||
try:
|
||||
try:
|
||||
self.init_config()
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing config: {e}")
|
||||
self.log_process.terminate()
|
||||
sys.exit(1)
|
||||
self.ensure_dirs()
|
||||
self.check_config()
|
||||
self.set_log_levels()
|
||||
self.init_queues()
|
||||
self.init_database()
|
||||
self.init_mqtt()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
self.log_process.terminate()
|
||||
sys.exit(1)
|
||||
self.start_detectors()
|
||||
self.start_detected_frames_processor()
|
||||
self.start_camera_processors()
|
||||
self.start_camera_capture_processes()
|
||||
self.init_web_server()
|
||||
self.start_event_processor()
|
||||
self.start_event_cleanup()
|
||||
self.start_recording_maintainer()
|
||||
self.start_watchdog()
|
||||
# self.zeroconf = broadcast_zeroconf(self.config.mqtt.client_id)
|
||||
|
||||
def receiveSignal(signalNumber, frame):
|
||||
self.stop()
|
||||
sys.exit()
|
||||
|
||||
signal.signal(signal.SIGTERM, receiveSignal)
|
||||
|
||||
self.flask_app.run(host='127.0.0.1', port=5001, debug=False)
|
||||
self.stop()
|
||||
|
||||
def stop(self):
|
||||
logger.info(f"Stopping...")
|
||||
self.stop_event.set()
|
||||
|
||||
self.detected_frames_processor.join()
|
||||
self.event_processor.join()
|
||||
self.event_cleanup.join()
|
||||
self.recording_maintainer.join()
|
||||
self.frigate_watchdog.join()
|
||||
|
||||
for detector in self.detectors.values():
|
||||
detector.stop()
|
||||
|
||||
while len(self.detection_shms) > 0:
|
||||
shm = self.detection_shms.pop()
|
||||
shm.close()
|
||||
shm.unlink()
|
||||
963
frigate/config.py
Normal file
@@ -0,0 +1,963 @@
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
from typing import Dict
|
||||
|
||||
import cv2
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import voluptuous as vol
|
||||
import yaml
|
||||
|
||||
from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
|
||||
|
||||
DETECTORS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(str): {
|
||||
vol.Required('type', default='edgetpu'): vol.In(['cpu', 'edgetpu']),
|
||||
vol.Optional('device', default='usb'): str,
|
||||
vol.Optional('num_threads', default=3): int
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
DEFAULT_DETECTORS = {
|
||||
'coral': {
|
||||
'type': 'edgetpu',
|
||||
'device': 'usb'
|
||||
}
|
||||
}
|
||||
|
||||
MQTT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required('host'): str,
|
||||
vol.Optional('port', default=1883): int,
|
||||
vol.Optional('topic_prefix', default='frigate'): str,
|
||||
vol.Optional('client_id', default='frigate'): str,
|
||||
'user': str,
|
||||
'password': str
|
||||
}
|
||||
)
|
||||
|
||||
SAVE_CLIPS_RETAIN_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required('default',default=10): int,
|
||||
'objects': {
|
||||
str: int
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
SAVE_CLIPS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional('max_seconds', default=300): int,
|
||||
'tmpfs_cache_size': str,
|
||||
vol.Optional('retain', default={}): SAVE_CLIPS_RETAIN_SCHEMA
|
||||
}
|
||||
)
|
||||
|
||||
FFMPEG_GLOBAL_ARGS_DEFAULT = ['-hide_banner','-loglevel','fatal']
|
||||
FFMPEG_INPUT_ARGS_DEFAULT = ['-avoid_negative_ts', 'make_zero',
|
||||
'-fflags', '+genpts+discardcorrupt',
|
||||
'-rtsp_transport', 'tcp',
|
||||
'-stimeout', '5000000',
|
||||
'-use_wallclock_as_timestamps', '1']
|
||||
DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT = ['-f', 'rawvideo',
|
||||
'-pix_fmt', 'yuv420p']
|
||||
RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT = ["-c", "copy", "-f", "flv"]
|
||||
SAVE_CLIPS_FFMPEG_OUTPUT_ARGS_DEFAULT = ["-f", "segment", "-segment_time",
|
||||
"10", "-segment_format", "mp4", "-reset_timestamps", "1", "-strftime",
|
||||
"1", "-c", "copy", "-an"]
|
||||
RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT = ["-f", "segment", "-segment_time",
|
||||
"60", "-segment_format", "mp4", "-reset_timestamps", "1", "-strftime",
|
||||
"1", "-c", "copy", "-an"]
|
||||
|
||||
GLOBAL_FFMPEG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional('global_args', default=FFMPEG_GLOBAL_ARGS_DEFAULT): vol.Any(str, [str]),
|
||||
vol.Optional('hwaccel_args', default=[]): vol.Any(str, [str]),
|
||||
vol.Optional('input_args', default=FFMPEG_INPUT_ARGS_DEFAULT): vol.Any(str, [str]),
|
||||
vol.Optional('output_args', default={}): {
|
||||
vol.Optional('detect', default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
|
||||
vol.Optional('record', default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
|
||||
vol.Optional('clips', default=SAVE_CLIPS_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
|
||||
vol.Optional('rtmp', default=RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
MOTION_SCHEMA = vol.Schema(
|
||||
{
|
||||
'threshold': vol.Range(min=1, max=255),
|
||||
'contour_area': int,
|
||||
'delta_alpha': float,
|
||||
'frame_alpha': float,
|
||||
'frame_height': int
|
||||
}
|
||||
)
|
||||
|
||||
DETECT_SCHEMA = vol.Schema(
|
||||
{
|
||||
'max_disappeared': int
|
||||
}
|
||||
)
|
||||
|
||||
FILTER_SCHEMA = vol.Schema(
|
||||
{
|
||||
str: {
|
||||
vol.Optional('min_area', default=0): int,
|
||||
vol.Optional('max_area', default=24000000): int,
|
||||
vol.Optional('threshold', default=0.7): float
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def filters_for_all_tracked_objects(object_config):
|
||||
for tracked_object in object_config.get('track', ['person']):
|
||||
if not 'filters' in object_config:
|
||||
object_config['filters'] = {}
|
||||
if not tracked_object in object_config['filters']:
|
||||
object_config['filters'][tracked_object] = {}
|
||||
return object_config
|
||||
|
||||
OBJECTS_SCHEMA = vol.Schema(vol.All(filters_for_all_tracked_objects,
|
||||
{
|
||||
vol.Optional('track', default=['person']): [str],
|
||||
vol.Optional('filters', default = {}): FILTER_SCHEMA.extend({ str: {vol.Optional('min_score', default=0.5): float}})
|
||||
}
|
||||
))
|
||||
|
||||
def each_role_used_once(inputs):
|
||||
roles = [role for i in inputs for role in i['roles']]
|
||||
roles_set = set(roles)
|
||||
if len(roles) > len(roles_set):
|
||||
raise ValueError
|
||||
return inputs
|
||||
|
||||
CAMERA_FFMPEG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required('inputs'): vol.All([{
|
||||
vol.Required('path'): str,
|
||||
vol.Required('roles'): ['detect', 'clips', 'record', 'rtmp'],
|
||||
'global_args': vol.Any(str, [str]),
|
||||
'hwaccel_args': vol.Any(str, [str]),
|
||||
'input_args': vol.Any(str, [str]),
|
||||
}], vol.Msg(each_role_used_once, msg="Each input role may only be used once")),
|
||||
'output_args': {
|
||||
vol.Optional('detect', default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
|
||||
vol.Optional('record', default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
|
||||
vol.Optional('clips', default=SAVE_CLIPS_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
|
||||
vol.Optional('rtmp', default=RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def ensure_zones_and_cameras_have_different_names(cameras):
|
||||
zones = [zone for camera in cameras.values() for zone in camera['zones'].keys()]
|
||||
for zone in zones:
|
||||
if zone in cameras.keys():
|
||||
raise ValueError
|
||||
return cameras
|
||||
|
||||
CAMERAS_SCHEMA = vol.Schema(vol.All(
|
||||
{
|
||||
str: {
|
||||
vol.Required('ffmpeg'): CAMERA_FFMPEG_SCHEMA,
|
||||
vol.Required('height'): int,
|
||||
vol.Required('width'): int,
|
||||
'fps': int,
|
||||
'mask': vol.Any(str, [str]),
|
||||
vol.Optional('best_image_timeout', default=60): int,
|
||||
vol.Optional('zones', default={}): {
|
||||
str: {
|
||||
vol.Required('coordinates'): vol.Any(str, [str]),
|
||||
vol.Optional('filters', default={}): FILTER_SCHEMA
|
||||
}
|
||||
},
|
||||
vol.Optional('save_clips', default={}): {
|
||||
vol.Optional('enabled', default=False): bool,
|
||||
vol.Optional('pre_capture', default=5): int,
|
||||
vol.Optional('post_capture', default=5): int,
|
||||
'objects': [str],
|
||||
vol.Optional('retain', default={}): SAVE_CLIPS_RETAIN_SCHEMA,
|
||||
},
|
||||
vol.Optional('record', default={}): {
|
||||
'enabled': bool,
|
||||
'retain_days': int,
|
||||
},
|
||||
vol.Optional('rtmp', default={}): {
|
||||
vol.Required('enabled', default=True): bool,
|
||||
},
|
||||
vol.Optional('snapshots', default={}): {
|
||||
vol.Optional('show_timestamp', default=True): bool,
|
||||
vol.Optional('draw_zones', default=False): bool,
|
||||
vol.Optional('draw_bounding_boxes', default=True): bool,
|
||||
vol.Optional('crop_to_region', default=True): bool,
|
||||
vol.Optional('height', default=175): int
|
||||
},
|
||||
'objects': OBJECTS_SCHEMA,
|
||||
vol.Optional('motion', default={}): MOTION_SCHEMA,
|
||||
vol.Optional('detect', default={}): DETECT_SCHEMA
|
||||
}
|
||||
}, vol.Msg(ensure_zones_and_cameras_have_different_names, msg='Zones cannot share names with cameras'))
|
||||
)
|
||||
|
||||
FRIGATE_CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional('database', default={}): {
|
||||
vol.Optional('path', default=os.path.join(CLIPS_DIR, 'frigate.db')): str
|
||||
},
|
||||
vol.Optional('model', default={'width': 320, 'height': 320}): {
|
||||
vol.Required('width'): int,
|
||||
vol.Required('height'): int
|
||||
},
|
||||
vol.Optional('detectors', default=DEFAULT_DETECTORS): DETECTORS_SCHEMA,
|
||||
'mqtt': MQTT_SCHEMA,
|
||||
vol.Optional('logger', default={'default': 'info', 'logs': {}}): {
|
||||
vol.Optional('default', default='info'): vol.In(['info', 'debug', 'warning', 'error', 'critical']),
|
||||
vol.Optional('logs', default={}): {str: vol.In(['info', 'debug', 'warning', 'error', 'critical']) }
|
||||
},
|
||||
vol.Optional('save_clips', default={}): SAVE_CLIPS_SCHEMA,
|
||||
vol.Optional('record', default={}): {
|
||||
vol.Optional('enabled', default=False): bool,
|
||||
vol.Optional('retain_days', default=30): int,
|
||||
},
|
||||
vol.Optional('ffmpeg', default={}): GLOBAL_FFMPEG_SCHEMA,
|
||||
vol.Optional('objects', default={}): OBJECTS_SCHEMA,
|
||||
vol.Optional('motion', default={}): MOTION_SCHEMA,
|
||||
vol.Optional('detect', default={}): DETECT_SCHEMA,
|
||||
vol.Required('cameras', default={}): CAMERAS_SCHEMA
|
||||
}
|
||||
)
|
||||
|
||||
class DatabaseConfig():
|
||||
def __init__(self, config):
|
||||
self._path = config['path']
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self._path
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'path': self.path
|
||||
}
|
||||
|
||||
class ModelConfig():
|
||||
def __init__(self, config):
|
||||
self._width = config['width']
|
||||
self._height = config['height']
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self._width
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self._height
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'width': self.width,
|
||||
'height': self.height
|
||||
}
|
||||
|
||||
class DetectorConfig():
|
||||
def __init__(self, config):
|
||||
self._type = config['type']
|
||||
self._device = config['device']
|
||||
self._num_threads = config['num_threads']
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self._type
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
return self._device
|
||||
|
||||
@property
|
||||
def num_threads(self):
|
||||
return self._num_threads
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'type': self.type,
|
||||
'device': self.device,
|
||||
'num_threads': self.num_threads
|
||||
}
|
||||
|
||||
class LoggerConfig():
|
||||
def __init__(self, config):
|
||||
self._default = config['default'].upper()
|
||||
self._logs = {k: v.upper() for k, v in config['logs'].items()}
|
||||
|
||||
@property
|
||||
def default(self):
|
||||
return self._default
|
||||
|
||||
@property
|
||||
def logs(self):
|
||||
return self._logs
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'default': self.default,
|
||||
'logs': self.logs
|
||||
}
|
||||
|
||||
class MqttConfig():
|
||||
def __init__(self, config):
|
||||
self._host = config['host']
|
||||
self._port = config['port']
|
||||
self._topic_prefix = config['topic_prefix']
|
||||
self._client_id = config['client_id']
|
||||
self._user = config.get('user')
|
||||
self._password = config.get('password')
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
return self._host
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
return self._port
|
||||
|
||||
@property
|
||||
def topic_prefix(self):
|
||||
return self._topic_prefix
|
||||
|
||||
@property
|
||||
def client_id(self):
|
||||
return self._client_id
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return self._user
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
return self._password
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'host': self.host,
|
||||
'port': self.port,
|
||||
'topic_prefix': self.topic_prefix,
|
||||
'client_id': self.client_id,
|
||||
'user': self.user
|
||||
}
|
||||
|
||||
class CameraInput():
|
||||
def __init__(self, global_config, ffmpeg_input):
|
||||
self._path = ffmpeg_input['path']
|
||||
self._roles = ffmpeg_input['roles']
|
||||
self._global_args = ffmpeg_input.get('global_args', global_config['global_args'])
|
||||
self._hwaccel_args = ffmpeg_input.get('hwaccel_args', global_config['hwaccel_args'])
|
||||
self._input_args = ffmpeg_input.get('input_args', global_config['input_args'])
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self._path
|
||||
|
||||
@property
|
||||
def roles(self):
|
||||
return self._roles
|
||||
|
||||
@property
|
||||
def global_args(self):
|
||||
return self._global_args if isinstance(self._global_args, list) else self._global_args.split(' ')
|
||||
|
||||
@property
|
||||
def hwaccel_args(self):
|
||||
return self._hwaccel_args if isinstance(self._hwaccel_args, list) else self._hwaccel_args.split(' ')
|
||||
|
||||
@property
|
||||
def input_args(self):
|
||||
return self._input_args if isinstance(self._input_args, list) else self._input_args.split(' ')
|
||||
|
||||
class CameraFfmpegConfig():
|
||||
def __init__(self, global_config, config):
|
||||
self._inputs = [CameraInput(global_config, i) for i in config['inputs']]
|
||||
self._output_args = config.get('output_args', global_config['output_args'])
|
||||
|
||||
@property
|
||||
def inputs(self):
|
||||
return self._inputs
|
||||
|
||||
@property
|
||||
def output_args(self):
|
||||
return {k: v if isinstance(v, list) else v.split(' ') for k, v in self._output_args.items()}
|
||||
|
||||
class SaveClipsRetainConfig():
|
||||
def __init__(self, global_config, config):
|
||||
self._default = config.get('default', global_config.get('default'))
|
||||
self._objects = config.get('objects', global_config.get('objects', {}))
|
||||
|
||||
@property
|
||||
def default(self):
|
||||
return self._default
|
||||
|
||||
@property
|
||||
def objects(self):
|
||||
return self._objects
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'default': self.default,
|
||||
'objects': self.objects
|
||||
}
|
||||
|
||||
class SaveClipsConfig():
|
||||
def __init__(self, config):
|
||||
self._max_seconds = config['max_seconds']
|
||||
self._tmpfs_cache_size = config.get('tmpfs_cache_size', '').strip()
|
||||
self._retain = SaveClipsRetainConfig(config['retain'], config['retain'])
|
||||
|
||||
@property
|
||||
def max_seconds(self):
|
||||
return self._max_seconds
|
||||
|
||||
@property
|
||||
def tmpfs_cache_size(self):
|
||||
return self._tmpfs_cache_size
|
||||
|
||||
@property
|
||||
def retain(self):
|
||||
return self._retain
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'max_seconds': self.max_seconds,
|
||||
'tmpfs_cache_size': self.tmpfs_cache_size,
|
||||
'retain': self.retain.to_dict()
|
||||
}
|
||||
|
||||
class RecordConfig():
|
||||
def __init__(self, global_config, config):
|
||||
self._enabled = config.get('enabled', global_config['enabled'])
|
||||
self._retain_days = config.get('retain_days', global_config['retain_days'])
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return self._enabled
|
||||
|
||||
@property
|
||||
def retain_days(self):
|
||||
return self._retain_days
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'enabled': self.enabled,
|
||||
'retain_days': self.retain_days,
|
||||
}
|
||||
|
||||
class FilterConfig():
|
||||
def __init__(self, config):
|
||||
self._min_area = config['min_area']
|
||||
self._max_area = config['max_area']
|
||||
self._threshold = config['threshold']
|
||||
self._min_score = config.get('min_score')
|
||||
|
||||
@property
|
||||
def min_area(self):
|
||||
return self._min_area
|
||||
|
||||
@property
|
||||
def max_area(self):
|
||||
return self._max_area
|
||||
|
||||
@property
|
||||
def threshold(self):
|
||||
return self._threshold
|
||||
|
||||
@property
|
||||
def min_score(self):
|
||||
return self._min_score
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'min_area': self.min_area,
|
||||
'max_area': self.max_area,
|
||||
'threshold': self.threshold,
|
||||
'min_score': self.min_score
|
||||
}
|
||||
|
||||
class ObjectConfig():
|
||||
def __init__(self, global_config, config):
|
||||
self._track = config.get('track', global_config['track'])
|
||||
if 'filters' in config:
|
||||
self._filters = { name: FilterConfig(c) for name, c in config['filters'].items() }
|
||||
else:
|
||||
self._filters = { name: FilterConfig(c) for name, c in global_config['filters'].items() }
|
||||
|
||||
@property
|
||||
def track(self):
|
||||
return self._track
|
||||
|
||||
@property
|
||||
def filters(self) -> Dict[str, FilterConfig]:
|
||||
return self._filters
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'track': self.track,
|
||||
'filters': { k: f.to_dict() for k, f in self.filters.items() }
|
||||
}
|
||||
|
||||
class CameraSnapshotsConfig():
|
||||
def __init__(self, config):
|
||||
self._show_timestamp = config['show_timestamp']
|
||||
self._draw_zones = config['draw_zones']
|
||||
self._draw_bounding_boxes = config['draw_bounding_boxes']
|
||||
self._crop_to_region = config['crop_to_region']
|
||||
self._height = config.get('height')
|
||||
|
||||
@property
|
||||
def show_timestamp(self):
|
||||
return self._show_timestamp
|
||||
|
||||
@property
|
||||
def draw_zones(self):
|
||||
return self._draw_zones
|
||||
|
||||
@property
|
||||
def draw_bounding_boxes(self):
|
||||
return self._draw_bounding_boxes
|
||||
|
||||
@property
|
||||
def crop_to_region(self):
|
||||
return self._crop_to_region
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self._height
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'show_timestamp': self.show_timestamp,
|
||||
'draw_zones': self.draw_zones,
|
||||
'draw_bounding_boxes': self.draw_bounding_boxes,
|
||||
'crop_to_region': self.crop_to_region,
|
||||
'height': self.height
|
||||
}
|
||||
|
||||
class CameraSaveClipsConfig():
|
||||
def __init__(self, global_config, config):
|
||||
self._enabled = config['enabled']
|
||||
self._pre_capture = config['pre_capture']
|
||||
self._post_capture = config['post_capture']
|
||||
self._objects = config.get('objects', global_config['objects']['track'])
|
||||
self._retain = SaveClipsRetainConfig(global_config['save_clips']['retain'], config['retain'])
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return self._enabled
|
||||
|
||||
@property
|
||||
def pre_capture(self):
|
||||
return self._pre_capture
|
||||
|
||||
@property
|
||||
def post_capture(self):
|
||||
return self._post_capture
|
||||
|
||||
@property
|
||||
def objects(self):
|
||||
return self._objects
|
||||
|
||||
@property
|
||||
def retain(self):
|
||||
return self._retain
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'enabled': self.enabled,
|
||||
'pre_capture': self.pre_capture,
|
||||
'post_capture': self.post_capture,
|
||||
'objects': self.objects,
|
||||
'retain': self.retain.to_dict()
|
||||
}
|
||||
|
||||
class CameraRtmpConfig():
|
||||
def __init__(self, global_config, config):
|
||||
self._enabled = config['enabled']
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return self._enabled
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'enabled': self.enabled,
|
||||
}
|
||||
|
||||
class MotionConfig():
|
||||
def __init__(self, global_config, config, camera_height: int):
|
||||
self._threshold = config.get('threshold', global_config.get('threshold', 25))
|
||||
self._contour_area = config.get('contour_area', global_config.get('contour_area', 100))
|
||||
self._delta_alpha = config.get('delta_alpha', global_config.get('delta_alpha', 0.2))
|
||||
self._frame_alpha = config.get('frame_alpha', global_config.get('frame_alpha', 0.2))
|
||||
self._frame_height = config.get('frame_height', global_config.get('frame_height', camera_height//6))
|
||||
|
||||
@property
|
||||
def threshold(self):
|
||||
return self._threshold
|
||||
|
||||
@property
|
||||
def contour_area(self):
|
||||
return self._contour_area
|
||||
|
||||
@property
|
||||
def delta_alpha(self):
|
||||
return self._delta_alpha
|
||||
|
||||
@property
|
||||
def frame_alpha(self):
|
||||
return self._frame_alpha
|
||||
|
||||
@property
|
||||
def frame_height(self):
|
||||
return self._frame_height
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'threshold': self.threshold,
|
||||
'contour_area': self.contour_area,
|
||||
'delta_alpha': self.delta_alpha,
|
||||
'frame_alpha': self.frame_alpha,
|
||||
'frame_height': self.frame_height,
|
||||
}
|
||||
|
||||
|
||||
|
||||
class DetectConfig():
|
||||
def __init__(self, global_config, config, camera_fps):
|
||||
self._max_disappeared = config.get('max_disappeared', global_config.get('max_disappeared', camera_fps*2))
|
||||
|
||||
@property
|
||||
def max_disappeared(self):
|
||||
return self._max_disappeared
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'max_disappeared': self._max_disappeared,
|
||||
}
|
||||
|
||||
class ZoneConfig():
|
||||
def __init__(self, name, config):
|
||||
self._coordinates = config['coordinates']
|
||||
self._filters = { name: FilterConfig(c) for name, c in config['filters'].items() }
|
||||
|
||||
if isinstance(self._coordinates, list):
|
||||
self._contour = np.array([[int(p.split(',')[0]), int(p.split(',')[1])] for p in self._coordinates])
|
||||
elif isinstance(self._coordinates, str):
|
||||
points = self._coordinates.split(',')
|
||||
self._contour = np.array([[int(points[i]), int(points[i+1])] for i in range(0, len(points), 2)])
|
||||
else:
|
||||
print(f"Unable to parse zone coordinates for {name}")
|
||||
self._contour = np.array([])
|
||||
|
||||
self._color = (0,0,0)
|
||||
|
||||
@property
|
||||
def coordinates(self):
|
||||
return self._coordinates
|
||||
|
||||
@property
|
||||
def contour(self):
|
||||
return self._contour
|
||||
|
||||
@contour.setter
|
||||
def contour(self, val):
|
||||
self._contour = val
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
return self._color
|
||||
|
||||
@color.setter
|
||||
def color(self, val):
|
||||
self._color = val
|
||||
|
||||
@property
|
||||
def filters(self):
|
||||
return self._filters
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'filters': {k: f.to_dict() for k, f in self.filters.items()}
|
||||
}
|
||||
|
||||
class CameraConfig():
|
||||
def __init__(self, name, config, global_config):
|
||||
self._name = name
|
||||
self._ffmpeg = CameraFfmpegConfig(global_config['ffmpeg'], config['ffmpeg'])
|
||||
self._height = config.get('height')
|
||||
self._width = config.get('width')
|
||||
self._frame_shape = (self._height, self._width)
|
||||
self._frame_shape_yuv = (self._frame_shape[0]*3//2, self._frame_shape[1])
|
||||
self._fps = config.get('fps')
|
||||
self._mask = self._create_mask(config.get('mask'))
|
||||
self._best_image_timeout = config['best_image_timeout']
|
||||
self._zones = { name: ZoneConfig(name, z) for name, z in config['zones'].items() }
|
||||
self._save_clips = CameraSaveClipsConfig(global_config, config['save_clips'])
|
||||
self._record = RecordConfig(global_config['record'], config['record'])
|
||||
self._rtmp = CameraRtmpConfig(global_config, config['rtmp'])
|
||||
self._snapshots = CameraSnapshotsConfig(config['snapshots'])
|
||||
self._objects = ObjectConfig(global_config['objects'], config.get('objects', {}))
|
||||
self._motion = MotionConfig(global_config['motion'], config['motion'], self._height)
|
||||
self._detect = DetectConfig(global_config['detect'], config['detect'], config.get('fps', 5))
|
||||
|
||||
self._ffmpeg_cmds = []
|
||||
for ffmpeg_input in self._ffmpeg.inputs:
|
||||
ffmpeg_cmd = self._get_ffmpeg_cmd(ffmpeg_input)
|
||||
if ffmpeg_cmd is None:
|
||||
continue
|
||||
|
||||
self._ffmpeg_cmds.append({
|
||||
'roles': ffmpeg_input.roles,
|
||||
'cmd': ffmpeg_cmd
|
||||
})
|
||||
|
||||
|
||||
self._set_zone_colors(self._zones)
|
||||
|
||||
def _create_mask(self, mask):
|
||||
mask_img = np.zeros(self.frame_shape, np.uint8)
|
||||
mask_img[:] = 255
|
||||
|
||||
if isinstance(mask, list):
|
||||
for m in mask:
|
||||
self._add_mask(m, mask_img)
|
||||
|
||||
elif isinstance(mask, str):
|
||||
self._add_mask(mask, mask_img)
|
||||
|
||||
return mask_img
|
||||
|
||||
def _add_mask(self, mask, mask_img):
|
||||
if mask.startswith('poly,'):
|
||||
points = mask.split(',')[1:]
|
||||
contour = np.array([[int(points[i]), int(points[i+1])] for i in range(0, len(points), 2)])
|
||||
cv2.fillPoly(mask_img, pts=[contour], color=(0))
|
||||
else:
|
||||
mask_file = cv2.imread(f"/config/{mask}", cv2.IMREAD_GRAYSCALE)
|
||||
if not mask_file.size == 0:
|
||||
mask_img[np.where(mask_file==[0])] = [0]
|
||||
|
||||
def _get_ffmpeg_cmd(self, ffmpeg_input):
|
||||
ffmpeg_output_args = []
|
||||
if 'detect' in ffmpeg_input.roles:
|
||||
ffmpeg_output_args = self.ffmpeg.output_args['detect'] + ffmpeg_output_args + ['pipe:']
|
||||
if self.fps:
|
||||
ffmpeg_output_args = ["-r", str(self.fps)] + ffmpeg_output_args
|
||||
if 'rtmp' in ffmpeg_input.roles and self.rtmp.enabled:
|
||||
ffmpeg_output_args = self.ffmpeg.output_args['rtmp'] + [
|
||||
f"rtmp://127.0.0.1/live/{self.name}"
|
||||
] + ffmpeg_output_args
|
||||
if 'clips' in ffmpeg_input.roles and self.save_clips.enabled:
|
||||
ffmpeg_output_args = self.ffmpeg.output_args['clips'] + [
|
||||
f"{os.path.join(CACHE_DIR, self.name)}-%Y%m%d%H%M%S.mp4"
|
||||
] + ffmpeg_output_args
|
||||
if 'record' in ffmpeg_input.roles and self.record.enabled:
|
||||
ffmpeg_output_args = self.ffmpeg.output_args['record'] + [
|
||||
f"{os.path.join(RECORD_DIR, self.name)}-%Y%m%d%H%M%S.mp4"
|
||||
] + ffmpeg_output_args
|
||||
|
||||
# if there arent any outputs enabled for this input
|
||||
if len(ffmpeg_output_args) == 0:
|
||||
return None
|
||||
|
||||
cmd = (['ffmpeg'] +
|
||||
ffmpeg_input.global_args +
|
||||
ffmpeg_input.hwaccel_args +
|
||||
ffmpeg_input.input_args +
|
||||
['-i', ffmpeg_input.path] +
|
||||
ffmpeg_output_args)
|
||||
|
||||
return [part for part in cmd if part != '']
|
||||
|
||||
def _set_zone_colors(self, zones: Dict[str, ZoneConfig]):
|
||||
# set colors for zones
|
||||
all_zone_names = zones.keys()
|
||||
zone_colors = {}
|
||||
colors = plt.cm.get_cmap('tab10', len(all_zone_names))
|
||||
for i, zone in enumerate(all_zone_names):
|
||||
zone_colors[zone] = tuple(int(round(255 * c)) for c in colors(i)[:3])
|
||||
|
||||
for name, zone in zones.items():
|
||||
zone.color = zone_colors[name]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def ffmpeg(self):
|
||||
return self._ffmpeg
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self._height
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self._width
|
||||
|
||||
@property
|
||||
def fps(self):
|
||||
return self._fps
|
||||
|
||||
@property
|
||||
def mask(self):
|
||||
return self._mask
|
||||
|
||||
@property
|
||||
def best_image_timeout(self):
|
||||
return self._best_image_timeout
|
||||
|
||||
@property
|
||||
def zones(self)-> Dict[str, ZoneConfig]:
|
||||
return self._zones
|
||||
|
||||
@property
|
||||
def save_clips(self):
|
||||
return self._save_clips
|
||||
|
||||
@property
|
||||
def record(self):
|
||||
return self._record
|
||||
|
||||
@property
|
||||
def rtmp(self):
|
||||
return self._rtmp
|
||||
|
||||
@property
|
||||
def snapshots(self):
|
||||
return self._snapshots
|
||||
|
||||
@property
|
||||
def objects(self):
|
||||
return self._objects
|
||||
|
||||
@property
|
||||
def motion(self):
|
||||
return self._motion
|
||||
|
||||
@property
|
||||
def detect(self):
|
||||
return self._detect
|
||||
|
||||
@property
|
||||
def frame_shape(self):
|
||||
return self._frame_shape
|
||||
|
||||
@property
|
||||
def frame_shape_yuv(self):
|
||||
return self._frame_shape_yuv
|
||||
|
||||
@property
|
||||
def ffmpeg_cmds(self):
|
||||
return self._ffmpeg_cmds
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'height': self.height,
|
||||
'width': self.width,
|
||||
'fps': self.fps,
|
||||
'best_image_timeout': self.best_image_timeout,
|
||||
'zones': {k: z.to_dict() for k, z in self.zones.items()},
|
||||
'save_clips': self.save_clips.to_dict(),
|
||||
'record': self.record.to_dict(),
|
||||
'rtmp': self.rtmp.to_dict(),
|
||||
'snapshots': self.snapshots.to_dict(),
|
||||
'objects': self.objects.to_dict(),
|
||||
'motion': self.motion.to_dict(),
|
||||
'detect': self.detect.to_dict(),
|
||||
'frame_shape': self.frame_shape,
|
||||
'ffmpeg_cmds': [{'roles': c['roles'], 'cmd': ' '.join(c['cmd'])} for c in self.ffmpeg_cmds],
|
||||
}
|
||||
|
||||
|
||||
class FrigateConfig():
|
||||
def __init__(self, config_file=None, config=None):
|
||||
if config is None and config_file is None:
|
||||
raise ValueError('config or config_file must be defined')
|
||||
elif not config_file is None:
|
||||
config = self._load_file(config_file)
|
||||
|
||||
config = FRIGATE_CONFIG_SCHEMA(config)
|
||||
|
||||
config = self._sub_env_vars(config)
|
||||
|
||||
self._database = DatabaseConfig(config['database'])
|
||||
self._model = ModelConfig(config['model'])
|
||||
self._detectors = { name: DetectorConfig(d) for name, d in config['detectors'].items() }
|
||||
self._mqtt = MqttConfig(config['mqtt'])
|
||||
self._save_clips = SaveClipsConfig(config['save_clips'])
|
||||
self._cameras = { name: CameraConfig(name, c, config) for name, c in config['cameras'].items() }
|
||||
self._logger = LoggerConfig(config['logger'])
|
||||
|
||||
def _sub_env_vars(self, config):
|
||||
frigate_env_vars = {k: v for k, v in os.environ.items() if k.startswith('FRIGATE_')}
|
||||
|
||||
if 'password' in config['mqtt']:
|
||||
config['mqtt']['password'] = config['mqtt']['password'].format(**frigate_env_vars)
|
||||
|
||||
for camera in config['cameras'].values():
|
||||
for i in camera['ffmpeg']['inputs']:
|
||||
i['path'] = i['path'].format(**frigate_env_vars)
|
||||
|
||||
return config
|
||||
|
||||
def _load_file(self, config_file):
|
||||
with open(config_file) as f:
|
||||
raw_config = f.read()
|
||||
|
||||
if config_file.endswith(".yml"):
|
||||
config = yaml.safe_load(raw_config)
|
||||
elif config_file.endswith(".json"):
|
||||
config = json.loads(raw_config)
|
||||
|
||||
return config
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'database': self.database.to_dict(),
|
||||
'model': self.model.to_dict(),
|
||||
'detectors': {k: d.to_dict() for k, d in self.detectors.items()},
|
||||
'mqtt': self.mqtt.to_dict(),
|
||||
'save_clips': self.save_clips.to_dict(),
|
||||
'cameras': {k: c.to_dict() for k, c in self.cameras.items()},
|
||||
'logger': self.logger.to_dict()
|
||||
}
|
||||
|
||||
@property
|
||||
def database(self):
|
||||
return self._database
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
return self._model
|
||||
|
||||
@property
|
||||
def detectors(self) -> Dict[str, DetectorConfig]:
|
||||
return self._detectors
|
||||
|
||||
@property
|
||||
def logger(self):
|
||||
return self._logger
|
||||
|
||||
@property
|
||||
def mqtt(self):
|
||||
return self._mqtt
|
||||
|
||||
@property
|
||||
def save_clips(self):
|
||||
return self._save_clips
|
||||
|
||||
@property
|
||||
def cameras(self) -> Dict[str, CameraConfig]:
|
||||
return self._cameras
|
||||
3
frigate/const.py
Normal file
@@ -0,0 +1,3 @@
|
||||
CLIPS_DIR = '/media/frigate/clips'
|
||||
RECORD_DIR = '/media/frigate/recordings'
|
||||
CACHE_DIR = '/tmp/cache'
|
||||
225
frigate/edgetpu.py
Normal file
@@ -0,0 +1,225 @@
|
||||
import datetime
|
||||
import hashlib
|
||||
import logging
|
||||
import multiprocessing as mp
|
||||
import os
|
||||
import queue
|
||||
import threading
|
||||
import signal
|
||||
from abc import ABC, abstractmethod
|
||||
from multiprocessing.connection import Connection
|
||||
from typing import Dict
|
||||
|
||||
import numpy as np
|
||||
import tflite_runtime.interpreter as tflite
|
||||
from tflite_runtime.interpreter import load_delegate
|
||||
|
||||
from frigate.util import EventsPerSecond, SharedMemoryFrameManager, listen
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def load_labels(path, encoding='utf-8'):
|
||||
"""Loads labels from file (with or without index numbers).
|
||||
Args:
|
||||
path: path to label file.
|
||||
encoding: label file encoding.
|
||||
Returns:
|
||||
Dictionary mapping indices to labels.
|
||||
"""
|
||||
with open(path, 'r', encoding=encoding) as f:
|
||||
lines = f.readlines()
|
||||
if not lines:
|
||||
return {}
|
||||
|
||||
if lines[0].split(' ', maxsplit=1)[0].isdigit():
|
||||
pairs = [line.split(' ', maxsplit=1) for line in lines]
|
||||
return {int(index): label.strip() for index, label in pairs}
|
||||
else:
|
||||
return {index: line.strip() for index, line in enumerate(lines)}
|
||||
|
||||
class ObjectDetector(ABC):
|
||||
@abstractmethod
|
||||
def detect(self, tensor_input, threshold = .4):
|
||||
pass
|
||||
|
||||
class LocalObjectDetector(ObjectDetector):
|
||||
def __init__(self, tf_device=None, num_threads=3, labels=None):
|
||||
self.fps = EventsPerSecond()
|
||||
if labels is None:
|
||||
self.labels = {}
|
||||
else:
|
||||
self.labels = load_labels(labels)
|
||||
|
||||
device_config = {"device": "usb"}
|
||||
if not tf_device is None:
|
||||
device_config = {"device": tf_device}
|
||||
|
||||
edge_tpu_delegate = None
|
||||
|
||||
if tf_device != 'cpu':
|
||||
try:
|
||||
logger.info(f"Attempting to load TPU as {device_config['device']}")
|
||||
edge_tpu_delegate = load_delegate('libedgetpu.so.1.0', device_config)
|
||||
logger.info("TPU found")
|
||||
except ValueError:
|
||||
logger.info("No EdgeTPU detected. Falling back to CPU.")
|
||||
|
||||
if edge_tpu_delegate is None:
|
||||
self.interpreter = tflite.Interpreter(
|
||||
model_path='/cpu_model.tflite', num_threads=num_threads)
|
||||
else:
|
||||
self.interpreter = tflite.Interpreter(
|
||||
model_path='/edgetpu_model.tflite',
|
||||
experimental_delegates=[edge_tpu_delegate])
|
||||
|
||||
self.interpreter.allocate_tensors()
|
||||
|
||||
self.tensor_input_details = self.interpreter.get_input_details()
|
||||
self.tensor_output_details = self.interpreter.get_output_details()
|
||||
|
||||
def detect(self, tensor_input, threshold=.4):
|
||||
detections = []
|
||||
|
||||
raw_detections = self.detect_raw(tensor_input)
|
||||
|
||||
for d in raw_detections:
|
||||
if d[1] < threshold:
|
||||
break
|
||||
detections.append((
|
||||
self.labels[int(d[0])],
|
||||
float(d[1]),
|
||||
(d[2], d[3], d[4], d[5])
|
||||
))
|
||||
self.fps.update()
|
||||
return detections
|
||||
|
||||
def detect_raw(self, tensor_input):
|
||||
self.interpreter.set_tensor(self.tensor_input_details[0]['index'], tensor_input)
|
||||
self.interpreter.invoke()
|
||||
boxes = np.squeeze(self.interpreter.get_tensor(self.tensor_output_details[0]['index']))
|
||||
label_codes = np.squeeze(self.interpreter.get_tensor(self.tensor_output_details[1]['index']))
|
||||
scores = np.squeeze(self.interpreter.get_tensor(self.tensor_output_details[2]['index']))
|
||||
|
||||
detections = np.zeros((20,6), np.float32)
|
||||
for i, score in enumerate(scores):
|
||||
detections[i] = [label_codes[i], score, boxes[i][0], boxes[i][1], boxes[i][2], boxes[i][3]]
|
||||
|
||||
return detections
|
||||
|
||||
def run_detector(name: str, detection_queue: mp.Queue, out_events: Dict[str, mp.Event], avg_speed, start, model_shape, tf_device, num_threads):
|
||||
threading.current_thread().name = f"detector:{name}"
|
||||
logger = logging.getLogger(f"detector.{name}")
|
||||
logger.info(f"Starting detection process: {os.getpid()}")
|
||||
listen()
|
||||
|
||||
stop_event = mp.Event()
|
||||
def receiveSignal(signalNumber, frame):
|
||||
stop_event.set()
|
||||
|
||||
signal.signal(signal.SIGTERM, receiveSignal)
|
||||
signal.signal(signal.SIGINT, receiveSignal)
|
||||
|
||||
frame_manager = SharedMemoryFrameManager()
|
||||
object_detector = LocalObjectDetector(tf_device=tf_device, num_threads=num_threads)
|
||||
|
||||
outputs = {}
|
||||
for name in out_events.keys():
|
||||
out_shm = mp.shared_memory.SharedMemory(name=f"out-{name}", create=False)
|
||||
out_np = np.ndarray((20,6), dtype=np.float32, buffer=out_shm.buf)
|
||||
outputs[name] = {
|
||||
'shm': out_shm,
|
||||
'np': out_np
|
||||
}
|
||||
|
||||
while True:
|
||||
if stop_event.is_set():
|
||||
break
|
||||
|
||||
try:
|
||||
connection_id = detection_queue.get(timeout=5)
|
||||
except queue.Empty:
|
||||
continue
|
||||
input_frame = frame_manager.get(connection_id, (1,model_shape[0],model_shape[1],3))
|
||||
|
||||
if input_frame is None:
|
||||
continue
|
||||
|
||||
# detect and send the output
|
||||
start.value = datetime.datetime.now().timestamp()
|
||||
detections = object_detector.detect_raw(input_frame)
|
||||
duration = datetime.datetime.now().timestamp()-start.value
|
||||
outputs[connection_id]['np'][:] = detections[:]
|
||||
out_events[connection_id].set()
|
||||
start.value = 0.0
|
||||
|
||||
avg_speed.value = (avg_speed.value*9 + duration)/10
|
||||
|
||||
class EdgeTPUProcess():
|
||||
def __init__(self, name, detection_queue, out_events, model_shape, tf_device=None, num_threads=3):
|
||||
self.name = name
|
||||
self.out_events = out_events
|
||||
self.detection_queue = detection_queue
|
||||
self.avg_inference_speed = mp.Value('d', 0.01)
|
||||
self.detection_start = mp.Value('d', 0.0)
|
||||
self.detect_process = None
|
||||
self.model_shape = model_shape
|
||||
self.tf_device = tf_device
|
||||
self.num_threads = num_threads
|
||||
self.start_or_restart()
|
||||
|
||||
def stop(self):
|
||||
self.detect_process.terminate()
|
||||
logging.info("Waiting for detection process to exit gracefully...")
|
||||
self.detect_process.join(timeout=30)
|
||||
if self.detect_process.exitcode is None:
|
||||
logging.info("Detection process didnt exit. Force killing...")
|
||||
self.detect_process.kill()
|
||||
self.detect_process.join()
|
||||
|
||||
def start_or_restart(self):
|
||||
self.detection_start.value = 0.0
|
||||
if (not self.detect_process is None) and self.detect_process.is_alive():
|
||||
self.stop()
|
||||
self.detect_process = mp.Process(target=run_detector, name=f"detector:{self.name}", args=(self.name, self.detection_queue, self.out_events, self.avg_inference_speed, self.detection_start, self.model_shape, self.tf_device, self.num_threads))
|
||||
self.detect_process.daemon = True
|
||||
self.detect_process.start()
|
||||
|
||||
class RemoteObjectDetector():
|
||||
def __init__(self, name, labels, detection_queue, event, model_shape):
|
||||
self.labels = load_labels(labels)
|
||||
self.name = name
|
||||
self.fps = EventsPerSecond()
|
||||
self.detection_queue = detection_queue
|
||||
self.event = event
|
||||
self.shm = mp.shared_memory.SharedMemory(name=self.name, create=False)
|
||||
self.np_shm = np.ndarray((1,model_shape[0],model_shape[1],3), dtype=np.uint8, buffer=self.shm.buf)
|
||||
self.out_shm = mp.shared_memory.SharedMemory(name=f"out-{self.name}", create=False)
|
||||
self.out_np_shm = np.ndarray((20,6), dtype=np.float32, buffer=self.out_shm.buf)
|
||||
|
||||
def detect(self, tensor_input, threshold=.4):
|
||||
detections = []
|
||||
|
||||
# copy input to shared memory
|
||||
self.np_shm[:] = tensor_input[:]
|
||||
self.event.clear()
|
||||
self.detection_queue.put(self.name)
|
||||
result = self.event.wait(timeout=10.0)
|
||||
|
||||
# if it timed out
|
||||
if result is None:
|
||||
return detections
|
||||
|
||||
for d in self.out_np_shm:
|
||||
if d[1] < threshold:
|
||||
break
|
||||
detections.append((
|
||||
self.labels[int(d[0])],
|
||||
float(d[1]),
|
||||
(d[2], d[3], d[4], d[5])
|
||||
))
|
||||
self.fps.update()
|
||||
return detections
|
||||
|
||||
def cleanup(self):
|
||||
self.shm.unlink()
|
||||
self.out_shm.unlink()
|
||||
289
frigate/events.py
Normal file
@@ -0,0 +1,289 @@
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import queue
|
||||
import subprocess as sp
|
||||
import threading
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
import psutil
|
||||
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
|
||||
from frigate.models import Event
|
||||
|
||||
from peewee import fn
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class EventProcessor(threading.Thread):
|
||||
def __init__(self, config, camera_processes, event_queue, event_processed_queue, stop_event):
|
||||
threading.Thread.__init__(self)
|
||||
self.name = 'event_processor'
|
||||
self.config = config
|
||||
self.camera_processes = camera_processes
|
||||
self.cached_clips = {}
|
||||
self.event_queue = event_queue
|
||||
self.event_processed_queue = event_processed_queue
|
||||
self.events_in_process = {}
|
||||
self.stop_event = stop_event
|
||||
|
||||
def refresh_cache(self):
|
||||
cached_files = os.listdir(CACHE_DIR)
|
||||
|
||||
files_in_use = []
|
||||
for process in psutil.process_iter():
|
||||
try:
|
||||
if process.name() != 'ffmpeg':
|
||||
continue
|
||||
|
||||
flist = process.open_files()
|
||||
if flist:
|
||||
for nt in flist:
|
||||
if nt.path.startswith(CACHE_DIR):
|
||||
files_in_use.append(nt.path.split('/')[-1])
|
||||
except:
|
||||
continue
|
||||
|
||||
for f in cached_files:
|
||||
if f in files_in_use or f in self.cached_clips:
|
||||
continue
|
||||
|
||||
camera = '-'.join(f.split('-')[:-1])
|
||||
start_time = datetime.datetime.strptime(f.split('-')[-1].split('.')[0], '%Y%m%d%H%M%S')
|
||||
|
||||
ffprobe_cmd = " ".join([
|
||||
'ffprobe',
|
||||
'-v',
|
||||
'error',
|
||||
'-show_entries',
|
||||
'format=duration',
|
||||
'-of',
|
||||
'default=noprint_wrappers=1:nokey=1',
|
||||
f"{os.path.join(CACHE_DIR,f)}"
|
||||
])
|
||||
p = sp.Popen(ffprobe_cmd, stdout=sp.PIPE, shell=True)
|
||||
(output, err) = p.communicate()
|
||||
p_status = p.wait()
|
||||
if p_status == 0:
|
||||
duration = float(output.decode('utf-8').strip())
|
||||
else:
|
||||
logger.info(f"bad file: {f}")
|
||||
os.remove(os.path.join(CACHE_DIR,f))
|
||||
continue
|
||||
|
||||
self.cached_clips[f] = {
|
||||
'path': f,
|
||||
'camera': camera,
|
||||
'start_time': start_time.timestamp(),
|
||||
'duration': duration
|
||||
}
|
||||
|
||||
if len(self.events_in_process) > 0:
|
||||
earliest_event = min(self.events_in_process.values(), key=lambda x:x['start_time'])['start_time']
|
||||
else:
|
||||
earliest_event = datetime.datetime.now().timestamp()
|
||||
|
||||
# if the earliest event exceeds the max seconds, cap it
|
||||
max_seconds = self.config.save_clips.max_seconds
|
||||
if datetime.datetime.now().timestamp()-earliest_event > max_seconds:
|
||||
earliest_event = datetime.datetime.now().timestamp()-max_seconds
|
||||
|
||||
for f, data in list(self.cached_clips.items()):
|
||||
if earliest_event-90 > data['start_time']+data['duration']:
|
||||
del self.cached_clips[f]
|
||||
os.remove(os.path.join(CACHE_DIR,f))
|
||||
|
||||
def create_clip(self, camera, event_data, pre_capture, post_capture):
|
||||
# get all clips from the camera with the event sorted
|
||||
sorted_clips = sorted([c for c in self.cached_clips.values() if c['camera'] == camera], key = lambda i: i['start_time'])
|
||||
|
||||
while len(sorted_clips) == 0 or sorted_clips[-1]['start_time'] + sorted_clips[-1]['duration'] < event_data['end_time']+post_capture:
|
||||
time.sleep(5)
|
||||
self.refresh_cache()
|
||||
# get all clips from the camera with the event sorted
|
||||
sorted_clips = sorted([c for c in self.cached_clips.values() if c['camera'] == camera], key = lambda i: i['start_time'])
|
||||
|
||||
playlist_start = event_data['start_time']-pre_capture
|
||||
playlist_end = event_data['end_time']+post_capture
|
||||
playlist_lines = []
|
||||
for clip in sorted_clips:
|
||||
# clip ends before playlist start time, skip
|
||||
if clip['start_time']+clip['duration'] < playlist_start:
|
||||
continue
|
||||
# clip starts after playlist ends, finish
|
||||
if clip['start_time'] > playlist_end:
|
||||
break
|
||||
playlist_lines.append(f"file '{os.path.join(CACHE_DIR,clip['path'])}'")
|
||||
# if this is the starting clip, add an inpoint
|
||||
if clip['start_time'] < playlist_start:
|
||||
playlist_lines.append(f"inpoint {int(playlist_start-clip['start_time'])}")
|
||||
# if this is the ending clip, add an outpoint
|
||||
if clip['start_time']+clip['duration'] > playlist_end:
|
||||
playlist_lines.append(f"outpoint {int(playlist_end-clip['start_time'])}")
|
||||
|
||||
clip_name = f"{camera}-{event_data['id']}"
|
||||
ffmpeg_cmd = [
|
||||
'ffmpeg',
|
||||
'-y',
|
||||
'-protocol_whitelist',
|
||||
'pipe,file',
|
||||
'-f',
|
||||
'concat',
|
||||
'-safe',
|
||||
'0',
|
||||
'-i',
|
||||
'-',
|
||||
'-c',
|
||||
'copy',
|
||||
'-movflags',
|
||||
'+faststart',
|
||||
f"{os.path.join(CLIPS_DIR, clip_name)}.mp4"
|
||||
]
|
||||
|
||||
p = sp.run(ffmpeg_cmd, input="\n".join(playlist_lines), encoding='ascii', capture_output=True)
|
||||
if p.returncode != 0:
|
||||
logger.error(p.stderr)
|
||||
return
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
if self.stop_event.is_set():
|
||||
logger.info(f"Exiting event processor...")
|
||||
break
|
||||
|
||||
try:
|
||||
event_type, camera, event_data = self.event_queue.get(timeout=10)
|
||||
except queue.Empty:
|
||||
if not self.stop_event.is_set():
|
||||
self.refresh_cache()
|
||||
continue
|
||||
|
||||
self.refresh_cache()
|
||||
|
||||
save_clips_config = self.config.cameras[camera].save_clips
|
||||
|
||||
# if save clips is not enabled for this camera, just continue
|
||||
if not save_clips_config.enabled:
|
||||
if event_type == 'end':
|
||||
self.event_processed_queue.put((event_data['id'], camera))
|
||||
continue
|
||||
|
||||
# if specific objects are listed for this camera, only save clips for them
|
||||
if not event_data['label'] in save_clips_config.objects:
|
||||
if event_type == 'end':
|
||||
self.event_processed_queue.put((event_data['id'], camera))
|
||||
continue
|
||||
|
||||
if event_type == 'start':
|
||||
self.events_in_process[event_data['id']] = event_data
|
||||
|
||||
if event_type == 'end':
|
||||
if len(self.cached_clips) > 0 and not event_data['false_positive']:
|
||||
self.create_clip(camera, event_data, save_clips_config.pre_capture, save_clips_config.post_capture)
|
||||
Event.create(
|
||||
id=event_data['id'],
|
||||
label=event_data['label'],
|
||||
camera=camera,
|
||||
start_time=event_data['start_time'],
|
||||
end_time=event_data['end_time'],
|
||||
top_score=event_data['top_score'],
|
||||
false_positive=event_data['false_positive'],
|
||||
zones=list(event_data['entered_zones']),
|
||||
thumbnail=event_data['thumbnail']
|
||||
)
|
||||
del self.events_in_process[event_data['id']]
|
||||
self.event_processed_queue.put((event_data['id'], camera))
|
||||
|
||||
class EventCleanup(threading.Thread):
|
||||
def __init__(self, config: FrigateConfig, stop_event):
|
||||
threading.Thread.__init__(self)
|
||||
self.name = 'event_cleanup'
|
||||
self.config = config
|
||||
self.stop_event = stop_event
|
||||
|
||||
def run(self):
|
||||
counter = 0
|
||||
while(True):
|
||||
if self.stop_event.is_set():
|
||||
logger.info(f"Exiting event cleanup...")
|
||||
break
|
||||
|
||||
# only expire events every 10 minutes, but check for stop events every 10 seconds
|
||||
time.sleep(10)
|
||||
counter = counter + 1
|
||||
if counter < 60:
|
||||
continue
|
||||
counter = 0
|
||||
|
||||
camera_keys = list(self.config.cameras.keys())
|
||||
|
||||
# Expire events from unlisted cameras based on the global config
|
||||
retain_config = self.config.save_clips.retain
|
||||
|
||||
distinct_labels = (Event.select(Event.label)
|
||||
.where(Event.camera.not_in(camera_keys))
|
||||
.distinct())
|
||||
|
||||
# loop over object types in db
|
||||
for l in distinct_labels:
|
||||
# get expiration time for this label
|
||||
expire_days = retain_config.objects.get(l.label, retain_config.default)
|
||||
expire_after = (datetime.datetime.now() - datetime.timedelta(days=expire_days)).timestamp()
|
||||
# grab all events after specific time
|
||||
expired_events = (
|
||||
Event.select()
|
||||
.where(Event.camera.not_in(camera_keys),
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label)
|
||||
)
|
||||
# delete the grabbed clips from disk
|
||||
for event in expired_events:
|
||||
clip_name = f"{event.camera}-{event.id}"
|
||||
clip = Path(f"{os.path.join(CLIPS_DIR, clip_name)}.mp4")
|
||||
clip.unlink(missing_ok=True)
|
||||
# delete the event for this type from the db
|
||||
delete_query = (
|
||||
Event.delete()
|
||||
.where(Event.camera.not_in(camera_keys),
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label)
|
||||
)
|
||||
delete_query.execute()
|
||||
|
||||
# Expire events from cameras based on the camera config
|
||||
for name, camera in self.config.cameras.items():
|
||||
retain_config = camera.save_clips.retain
|
||||
# get distinct objects in database for this camera
|
||||
distinct_labels = (Event.select(Event.label)
|
||||
.where(Event.camera == name)
|
||||
.distinct())
|
||||
|
||||
# loop over object types in db
|
||||
for l in distinct_labels:
|
||||
# get expiration time for this label
|
||||
expire_days = retain_config.objects.get(l.label, retain_config.default)
|
||||
expire_after = (datetime.datetime.now() - datetime.timedelta(days=expire_days)).timestamp()
|
||||
# grab all events after specific time
|
||||
expired_events = (
|
||||
Event.select()
|
||||
.where(Event.camera == name,
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label)
|
||||
)
|
||||
# delete the grabbed clips from disk
|
||||
for event in expired_events:
|
||||
clip_name = f"{event.camera}-{event.id}"
|
||||
clip = Path(f"{os.path.join(CLIPS_DIR, clip_name)}.mp4")
|
||||
clip.unlink(missing_ok=True)
|
||||
# delete the event for this type from the db
|
||||
delete_query = (
|
||||
Event.delete()
|
||||
.where( Event.camera == name,
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label)
|
||||
)
|
||||
delete_query.execute()
|
||||
269
frigate/http.py
Normal file
@@ -0,0 +1,269 @@
|
||||
import base64
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from functools import reduce
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from flask import (Blueprint, Flask, Response, current_app, jsonify,
|
||||
make_response, request)
|
||||
from peewee import SqliteDatabase, operator, fn, DoesNotExist
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
|
||||
from frigate.models import Event
|
||||
from frigate.util import calculate_region
|
||||
from frigate.version import VERSION
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
bp = Blueprint('frigate', __name__)
|
||||
|
||||
def create_app(frigate_config, database: SqliteDatabase, camera_metrics, detectors, detected_frames_processor):
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.before_request
|
||||
def _db_connect():
|
||||
database.connect()
|
||||
|
||||
@app.teardown_request
|
||||
def _db_close(exc):
|
||||
if not database.is_closed():
|
||||
database.close()
|
||||
|
||||
app.frigate_config = frigate_config
|
||||
app.camera_metrics = camera_metrics
|
||||
app.detectors = detectors
|
||||
app.detected_frames_processor = detected_frames_processor
|
||||
|
||||
app.register_blueprint(bp)
|
||||
|
||||
return app
|
||||
|
||||
@bp.route('/')
|
||||
def is_healthy():
|
||||
return "Frigate is running. Alive and healthy!"
|
||||
|
||||
@bp.route('/events/summary')
|
||||
def events_summary():
|
||||
groups = (
|
||||
Event
|
||||
.select(
|
||||
Event.camera,
|
||||
Event.label,
|
||||
fn.strftime('%Y-%m-%d', fn.datetime(Event.start_time, 'unixepoch', 'localtime')).alias('day'),
|
||||
Event.zones,
|
||||
fn.COUNT(Event.id).alias('count')
|
||||
)
|
||||
.group_by(
|
||||
Event.camera,
|
||||
Event.label,
|
||||
fn.strftime('%Y-%m-%d', fn.datetime(Event.start_time, 'unixepoch', 'localtime')),
|
||||
Event.zones
|
||||
)
|
||||
)
|
||||
|
||||
return jsonify([e for e in groups.dicts()])
|
||||
|
||||
@bp.route('/events/<id>')
|
||||
def event(id):
|
||||
try:
|
||||
return model_to_dict(Event.get(Event.id == id))
|
||||
except DoesNotExist:
|
||||
return "Event not found", 404
|
||||
|
||||
@bp.route('/events/<id>/snapshot.jpg')
|
||||
def event_snapshot(id):
|
||||
format = request.args.get('format', 'ios')
|
||||
thumbnail_bytes = None
|
||||
try:
|
||||
event = Event.get(Event.id == id)
|
||||
thumbnail_bytes = base64.b64decode(event.thumbnail)
|
||||
except DoesNotExist:
|
||||
# see if the object is currently being tracked
|
||||
try:
|
||||
for camera_state in current_app.detected_frames_processor.camera_states.values():
|
||||
if id in camera_state.tracked_objects:
|
||||
tracked_obj = camera_state.tracked_objects.get(id)
|
||||
if not tracked_obj is None:
|
||||
thumbnail_bytes = tracked_obj.get_jpg_bytes()
|
||||
except:
|
||||
return "Event not found", 404
|
||||
|
||||
if thumbnail_bytes is None:
|
||||
return "Event not found", 404
|
||||
|
||||
# android notifications prefer a 2:1 ratio
|
||||
if format == 'android':
|
||||
jpg_as_np = np.frombuffer(thumbnail_bytes, dtype=np.uint8)
|
||||
img = cv2.imdecode(jpg_as_np, flags=1)
|
||||
thumbnail = cv2.copyMakeBorder(img, 0, 0, int(img.shape[1]*0.5), int(img.shape[1]*0.5), cv2.BORDER_CONSTANT, (0,0,0))
|
||||
ret, jpg = cv2.imencode('.jpg', thumbnail)
|
||||
thumbnail_bytes = jpg.tobytes()
|
||||
|
||||
response = make_response(thumbnail_bytes)
|
||||
response.headers['Content-Type'] = 'image/jpg'
|
||||
return response
|
||||
|
||||
@bp.route('/events')
|
||||
def events():
|
||||
limit = request.args.get('limit', 100)
|
||||
camera = request.args.get('camera')
|
||||
label = request.args.get('label')
|
||||
zone = request.args.get('zone')
|
||||
after = request.args.get('after', type=int)
|
||||
before = request.args.get('before', type=int)
|
||||
|
||||
clauses = []
|
||||
|
||||
if camera:
|
||||
clauses.append((Event.camera == camera))
|
||||
|
||||
if label:
|
||||
clauses.append((Event.label == label))
|
||||
|
||||
if zone:
|
||||
clauses.append((Event.zones.cast('text') % f"*\"{zone}\"*"))
|
||||
|
||||
if after:
|
||||
clauses.append((Event.start_time >= after))
|
||||
|
||||
if before:
|
||||
clauses.append((Event.start_time <= before))
|
||||
|
||||
if len(clauses) == 0:
|
||||
clauses.append((1 == 1))
|
||||
|
||||
events = (Event.select()
|
||||
.where(reduce(operator.and_, clauses))
|
||||
.order_by(Event.start_time.desc())
|
||||
.limit(limit))
|
||||
|
||||
return jsonify([model_to_dict(e) for e in events])
|
||||
|
||||
@bp.route('/config')
|
||||
def config():
|
||||
return jsonify(current_app.frigate_config.to_dict())
|
||||
|
||||
@bp.route('/version')
|
||||
def version():
|
||||
return VERSION
|
||||
|
||||
@bp.route('/stats')
|
||||
def stats():
|
||||
camera_metrics = current_app.camera_metrics
|
||||
stats = {}
|
||||
|
||||
total_detection_fps = 0
|
||||
|
||||
for name, camera_stats in camera_metrics.items():
|
||||
total_detection_fps += camera_stats['detection_fps'].value
|
||||
stats[name] = {
|
||||
'camera_fps': round(camera_stats['camera_fps'].value, 2),
|
||||
'process_fps': round(camera_stats['process_fps'].value, 2),
|
||||
'skipped_fps': round(camera_stats['skipped_fps'].value, 2),
|
||||
'detection_fps': round(camera_stats['detection_fps'].value, 2),
|
||||
'pid': camera_stats['process'].pid,
|
||||
'capture_pid': camera_stats['capture_process'].pid
|
||||
}
|
||||
|
||||
stats['detectors'] = {}
|
||||
for name, detector in current_app.detectors.items():
|
||||
stats['detectors'][name] = {
|
||||
'inference_speed': round(detector.avg_inference_speed.value*1000, 2),
|
||||
'detection_start': detector.detection_start.value,
|
||||
'pid': detector.detect_process.pid
|
||||
}
|
||||
stats['detection_fps'] = round(total_detection_fps, 2)
|
||||
|
||||
return jsonify(stats)
|
||||
|
||||
@bp.route('/<camera_name>/<label>/best.jpg')
|
||||
def best(camera_name, label):
|
||||
if camera_name in current_app.frigate_config.cameras:
|
||||
best_object = current_app.detected_frames_processor.get_best(camera_name, label)
|
||||
best_frame = best_object.get('frame')
|
||||
if best_frame is None:
|
||||
best_frame = np.zeros((720,1280,3), np.uint8)
|
||||
else:
|
||||
best_frame = cv2.cvtColor(best_frame, cv2.COLOR_YUV2BGR_I420)
|
||||
|
||||
crop = bool(request.args.get('crop', 0, type=int))
|
||||
if crop:
|
||||
box = best_object.get('box', (0,0,300,300))
|
||||
region = calculate_region(best_frame.shape, box[0], box[1], box[2], box[3], 1.1)
|
||||
best_frame = best_frame[region[1]:region[3], region[0]:region[2]]
|
||||
|
||||
height = int(request.args.get('h', str(best_frame.shape[0])))
|
||||
width = int(height*best_frame.shape[1]/best_frame.shape[0])
|
||||
|
||||
best_frame = cv2.resize(best_frame, dsize=(width, height), interpolation=cv2.INTER_AREA)
|
||||
ret, jpg = cv2.imencode('.jpg', best_frame)
|
||||
response = make_response(jpg.tobytes())
|
||||
response.headers['Content-Type'] = 'image/jpg'
|
||||
return response
|
||||
else:
|
||||
return "Camera named {} not found".format(camera_name), 404
|
||||
|
||||
@bp.route('/<camera_name>')
|
||||
def mjpeg_feed(camera_name):
|
||||
fps = int(request.args.get('fps', '3'))
|
||||
height = int(request.args.get('h', '360'))
|
||||
draw_options = {
|
||||
'bounding_boxes': request.args.get('bbox', type=int),
|
||||
'timestamp': request.args.get('timestamp', type=int),
|
||||
'zones': request.args.get('zones', type=int),
|
||||
'mask': request.args.get('mask', type=int),
|
||||
'motion_boxes': request.args.get('motion', type=int),
|
||||
'regions': request.args.get('regions', type=int),
|
||||
}
|
||||
if camera_name in current_app.frigate_config.cameras:
|
||||
# return a multipart response
|
||||
return Response(imagestream(current_app.detected_frames_processor, camera_name, fps, height, draw_options),
|
||||
mimetype='multipart/x-mixed-replace; boundary=frame')
|
||||
else:
|
||||
return "Camera named {} not found".format(camera_name), 404
|
||||
|
||||
@bp.route('/<camera_name>/latest.jpg')
|
||||
def latest_frame(camera_name):
|
||||
draw_options = {
|
||||
'bounding_boxes': request.args.get('bbox', type=int),
|
||||
'timestamp': request.args.get('timestamp', type=int),
|
||||
'zones': request.args.get('zones', type=int),
|
||||
'mask': request.args.get('mask', type=int),
|
||||
'motion_boxes': request.args.get('motion', type=int),
|
||||
'regions': request.args.get('regions', type=int),
|
||||
}
|
||||
if camera_name in current_app.frigate_config.cameras:
|
||||
# max out at specified FPS
|
||||
frame = current_app.detected_frames_processor.get_current_frame(camera_name, draw_options)
|
||||
if frame is None:
|
||||
frame = np.zeros((720,1280,3), np.uint8)
|
||||
|
||||
height = int(request.args.get('h', str(frame.shape[0])))
|
||||
width = int(height*frame.shape[1]/frame.shape[0])
|
||||
|
||||
frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA)
|
||||
|
||||
ret, jpg = cv2.imencode('.jpg', frame)
|
||||
response = make_response(jpg.tobytes())
|
||||
response.headers['Content-Type'] = 'image/jpg'
|
||||
return response
|
||||
else:
|
||||
return "Camera named {} not found".format(camera_name), 404
|
||||
|
||||
def imagestream(detected_frames_processor, camera_name, fps, height, draw_options):
|
||||
while True:
|
||||
# max out at specified FPS
|
||||
time.sleep(1/fps)
|
||||
frame = detected_frames_processor.get_current_frame(camera_name, draw_options)
|
||||
if frame is None:
|
||||
frame = np.zeros((height,int(height*16/9),3), np.uint8)
|
||||
|
||||
width = int(height*frame.shape[1]/frame.shape[0])
|
||||
frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_LINEAR)
|
||||
|
||||
ret, jpg = cv2.imencode('.jpg', frame)
|
||||
yield (b'--frame\r\n'
|
||||
b'Content-Type: image/jpeg\r\n\r\n' + jpg.tobytes() + b'\r\n\r\n')
|
||||
75
frigate/log.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# adapted from https://medium.com/@jonathonbao/python3-logging-with-multiprocessing-f51f460b8778
|
||||
import logging
|
||||
import threading
|
||||
import os
|
||||
import signal
|
||||
import queue
|
||||
import multiprocessing as mp
|
||||
from logging import handlers
|
||||
|
||||
|
||||
def listener_configurer():
|
||||
root = logging.getLogger()
|
||||
console_handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter('%(name)-30s %(levelname)-8s: %(message)s')
|
||||
console_handler.setFormatter(formatter)
|
||||
root.addHandler(console_handler)
|
||||
root.setLevel(logging.INFO)
|
||||
|
||||
def root_configurer(queue):
|
||||
h = handlers.QueueHandler(queue)
|
||||
root = logging.getLogger()
|
||||
root.addHandler(h)
|
||||
root.setLevel(logging.INFO)
|
||||
|
||||
def log_process(log_queue):
|
||||
stop_event = mp.Event()
|
||||
def receiveSignal(signalNumber, frame):
|
||||
stop_event.set()
|
||||
|
||||
signal.signal(signal.SIGTERM, receiveSignal)
|
||||
signal.signal(signal.SIGINT, receiveSignal)
|
||||
|
||||
threading.current_thread().name = f"logger"
|
||||
listener_configurer()
|
||||
while True:
|
||||
if stop_event.is_set() and log_queue.empty():
|
||||
break
|
||||
try:
|
||||
record = log_queue.get(timeout=5)
|
||||
except queue.Empty:
|
||||
continue
|
||||
logger = logging.getLogger(record.name)
|
||||
logger.handle(record)
|
||||
|
||||
# based on https://codereview.stackexchange.com/a/17959
|
||||
class LogPipe(threading.Thread):
|
||||
def __init__(self, log_name, level):
|
||||
"""Setup the object with a logger and a loglevel
|
||||
and start the thread
|
||||
"""
|
||||
threading.Thread.__init__(self)
|
||||
self.daemon = False
|
||||
self.logger = logging.getLogger(log_name)
|
||||
self.level = level
|
||||
self.fdRead, self.fdWrite = os.pipe()
|
||||
self.pipeReader = os.fdopen(self.fdRead)
|
||||
self.start()
|
||||
|
||||
def fileno(self):
|
||||
"""Return the write file descriptor of the pipe
|
||||
"""
|
||||
return self.fdWrite
|
||||
|
||||
def run(self):
|
||||
"""Run the thread, logging everything.
|
||||
"""
|
||||
for line in iter(self.pipeReader.readline, ''):
|
||||
self.logger.log(self.level, line.strip('\n'))
|
||||
|
||||
self.pipeReader.close()
|
||||
|
||||
def close(self):
|
||||
"""Close the write end of the pipe.
|
||||
"""
|
||||
os.close(self.fdWrite)
|
||||
14
frigate/models.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from peewee import *
|
||||
from playhouse.sqlite_ext import *
|
||||
|
||||
|
||||
class Event(Model):
|
||||
id = CharField(null=False, primary_key=True, max_length=30)
|
||||
label = CharField(index=True, max_length=20)
|
||||
camera = CharField(index=True, max_length=20)
|
||||
start_time = DateTimeField()
|
||||
end_time = DateTimeField()
|
||||
top_score = FloatField()
|
||||
false_positive = BooleanField()
|
||||
zones = JSONField()
|
||||
thumbnail = TextField()
|
||||
85
frigate/motion.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import cv2
|
||||
import imutils
|
||||
import numpy as np
|
||||
from frigate.config import MotionConfig
|
||||
|
||||
|
||||
class MotionDetector():
|
||||
def __init__(self, frame_shape, mask, config: MotionConfig):
|
||||
self.config = config
|
||||
self.frame_shape = frame_shape
|
||||
self.resize_factor = frame_shape[0]/config.frame_height
|
||||
self.motion_frame_size = (config.frame_height, config.frame_height*frame_shape[1]//frame_shape[0])
|
||||
self.avg_frame = np.zeros(self.motion_frame_size, np.float)
|
||||
self.avg_delta = np.zeros(self.motion_frame_size, np.float)
|
||||
self.motion_frame_count = 0
|
||||
self.frame_counter = 0
|
||||
resized_mask = cv2.resize(mask, dsize=(self.motion_frame_size[1], self.motion_frame_size[0]), interpolation=cv2.INTER_LINEAR)
|
||||
self.mask = np.where(resized_mask==[0])
|
||||
|
||||
def detect(self, frame):
|
||||
motion_boxes = []
|
||||
|
||||
gray = frame[0:self.frame_shape[0], 0:self.frame_shape[1]]
|
||||
|
||||
# resize frame
|
||||
resized_frame = cv2.resize(gray, dsize=(self.motion_frame_size[1], self.motion_frame_size[0]), interpolation=cv2.INTER_LINEAR)
|
||||
|
||||
# TODO: can I improve the contrast of the grayscale image here?
|
||||
|
||||
# convert to grayscale
|
||||
# resized_frame = cv2.cvtColor(resized_frame, cv2.COLOR_BGR2GRAY)
|
||||
|
||||
# mask frame
|
||||
resized_frame[self.mask] = [255]
|
||||
|
||||
# it takes ~30 frames to establish a baseline
|
||||
# dont bother looking for motion
|
||||
if self.frame_counter < 30:
|
||||
self.frame_counter += 1
|
||||
else:
|
||||
# compare to average
|
||||
frameDelta = cv2.absdiff(resized_frame, cv2.convertScaleAbs(self.avg_frame))
|
||||
|
||||
# compute the average delta over the past few frames
|
||||
# higher values mean the current frame impacts the delta a lot, and a single raindrop may
|
||||
# register as motion, too low and a fast moving person wont be detected as motion
|
||||
cv2.accumulateWeighted(frameDelta, self.avg_delta, self.config.delta_alpha)
|
||||
|
||||
# compute the threshold image for the current frame
|
||||
# TODO: threshold
|
||||
current_thresh = cv2.threshold(frameDelta, self.config.threshold, 255, cv2.THRESH_BINARY)[1]
|
||||
|
||||
# black out everything in the avg_delta where there isnt motion in the current frame
|
||||
avg_delta_image = cv2.convertScaleAbs(self.avg_delta)
|
||||
avg_delta_image = cv2.bitwise_and(avg_delta_image, current_thresh)
|
||||
|
||||
# then look for deltas above the threshold, but only in areas where there is a delta
|
||||
# in the current frame. this prevents deltas from previous frames from being included
|
||||
thresh = cv2.threshold(avg_delta_image, self.config.threshold, 255, cv2.THRESH_BINARY)[1]
|
||||
|
||||
# dilate the thresholded image to fill in holes, then find contours
|
||||
# on thresholded image
|
||||
thresh = cv2.dilate(thresh, None, iterations=2)
|
||||
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
cnts = imutils.grab_contours(cnts)
|
||||
|
||||
# loop over the contours
|
||||
for c in cnts:
|
||||
# if the contour is big enough, count it as motion
|
||||
contour_area = cv2.contourArea(c)
|
||||
if contour_area > self.config.contour_area:
|
||||
x, y, w, h = cv2.boundingRect(c)
|
||||
motion_boxes.append((int(x*self.resize_factor), int(y*self.resize_factor), int((x+w)*self.resize_factor), int((y+h)*self.resize_factor)))
|
||||
|
||||
if len(motion_boxes) > 0:
|
||||
self.motion_frame_count += 1
|
||||
if self.motion_frame_count >= 10:
|
||||
# only average in the current frame if the difference persists for a bit
|
||||
cv2.accumulateWeighted(resized_frame, self.avg_frame, self.config.frame_alpha)
|
||||
else:
|
||||
# when no motion, just keep averaging the frames together
|
||||
cv2.accumulateWeighted(resized_frame, self.avg_frame, self.config.frame_alpha)
|
||||
self.motion_frame_count = 0
|
||||
|
||||
return motion_boxes
|
||||
@@ -1,33 +1,36 @@
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
|
||||
class MqttObjectPublisher(threading.Thread):
|
||||
def __init__(self, client, topic_prefix, objects_parsed, detected_objects):
|
||||
threading.Thread.__init__(self)
|
||||
self.client = client
|
||||
self.topic_prefix = topic_prefix
|
||||
self.objects_parsed = objects_parsed
|
||||
self._detected_objects = detected_objects
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
def run(self):
|
||||
last_sent_payload = ""
|
||||
while True:
|
||||
from frigate.config import MqttConfig
|
||||
|
||||
# initialize the payload
|
||||
payload = {}
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# wait until objects have been parsed
|
||||
with self.objects_parsed:
|
||||
self.objects_parsed.wait()
|
||||
def create_mqtt_client(config: MqttConfig):
|
||||
client = mqtt.Client(client_id=config.client_id)
|
||||
def on_connect(client, userdata, flags, rc):
|
||||
threading.current_thread().name = "mqtt"
|
||||
if rc != 0:
|
||||
if rc == 3:
|
||||
logger.error("MQTT Server unavailable")
|
||||
elif rc == 4:
|
||||
logger.error("MQTT Bad username or password")
|
||||
elif rc == 5:
|
||||
logger.error("MQTT Not authorized")
|
||||
else:
|
||||
logger.error("Unable to connect to MQTT: Connection refused. Error code: " + str(rc))
|
||||
|
||||
# add all the person scores in detected objects
|
||||
detected_objects = self._detected_objects.copy()
|
||||
person_score = sum([obj['score'] for obj in detected_objects if obj['name'] == 'person'])
|
||||
# if the person score is more than 100, set person to ON
|
||||
payload['person'] = 'ON' if int(person_score*100) > 100 else 'OFF'
|
||||
|
||||
# send message for objects if different
|
||||
new_payload = json.dumps(payload, sort_keys=True)
|
||||
if new_payload != last_sent_payload:
|
||||
last_sent_payload = new_payload
|
||||
self.client.publish(self.topic_prefix+'/objects', new_payload, retain=False)
|
||||
logger.info("MQTT connected")
|
||||
client.publish(config.topic_prefix+'/available', 'online', retain=True)
|
||||
client.on_connect = on_connect
|
||||
client.will_set(config.topic_prefix+'/available', payload='offline', qos=1, retain=True)
|
||||
if not config.user is None:
|
||||
client.username_pw_set(config.user, password=config.password)
|
||||
try:
|
||||
client.connect(config.host, config.port, 60)
|
||||
except Exception as e:
|
||||
logger.error(f"Unable to connect to MQTT server: {e}")
|
||||
raise
|
||||
client.loop_start()
|
||||
return client
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import datetime
|
||||
import time
|
||||
import cv2
|
||||
import threading
|
||||
import numpy as np
|
||||
from edgetpu.detection.engine import DetectionEngine
|
||||
from . util import tonumpyarray
|
||||
|
||||
# Path to frozen detection graph. This is the actual model that is used for the object detection.
|
||||
PATH_TO_CKPT = '/frozen_inference_graph.pb'
|
||||
# List of the strings that is used to add correct label for each box.
|
||||
PATH_TO_LABELS = '/label_map.pbtext'
|
||||
|
||||
# Function to read labels from text files.
|
||||
def ReadLabelFile(file_path):
|
||||
with open(file_path, 'r') as f:
|
||||
lines = f.readlines()
|
||||
ret = {}
|
||||
for line in lines:
|
||||
pair = line.strip().split(maxsplit=1)
|
||||
ret[int(pair[0])] = pair[1].strip()
|
||||
return ret
|
||||
|
||||
class PreppedQueueProcessor(threading.Thread):
|
||||
def __init__(self, cameras, prepped_frame_queue):
|
||||
|
||||
threading.Thread.__init__(self)
|
||||
self.cameras = cameras
|
||||
self.prepped_frame_queue = prepped_frame_queue
|
||||
|
||||
# Load the edgetpu engine and labels
|
||||
self.engine = DetectionEngine(PATH_TO_CKPT)
|
||||
self.labels = ReadLabelFile(PATH_TO_LABELS)
|
||||
|
||||
def run(self):
|
||||
# process queue...
|
||||
while True:
|
||||
frame = self.prepped_frame_queue.get()
|
||||
|
||||
# Actual detection.
|
||||
objects = self.engine.DetectWithInputTensor(frame['frame'], threshold=0.5, top_k=3)
|
||||
# parse and pass detected objects back to the camera
|
||||
parsed_objects = []
|
||||
for obj in objects:
|
||||
box = obj.bounding_box.flatten().tolist()
|
||||
parsed_objects.append({
|
||||
'frame_time': frame['frame_time'],
|
||||
'name': str(self.labels[obj.label_id]),
|
||||
'score': float(obj.score),
|
||||
'xmin': int((box[0] * frame['region_size']) + frame['region_x_offset']),
|
||||
'ymin': int((box[1] * frame['region_size']) + frame['region_y_offset']),
|
||||
'xmax': int((box[2] * frame['region_size']) + frame['region_x_offset']),
|
||||
'ymax': int((box[3] * frame['region_size']) + frame['region_y_offset'])
|
||||
})
|
||||
self.cameras[frame['camera_name']].add_objects(parsed_objects)
|
||||
|
||||
|
||||
# should this be a region class?
|
||||
class FramePrepper(threading.Thread):
|
||||
def __init__(self, camera_name, shared_frame, frame_time, frame_ready,
|
||||
frame_lock,
|
||||
region_size, region_x_offset, region_y_offset,
|
||||
prepped_frame_queue):
|
||||
|
||||
threading.Thread.__init__(self)
|
||||
self.camera_name = camera_name
|
||||
self.shared_frame = shared_frame
|
||||
self.frame_time = frame_time
|
||||
self.frame_ready = frame_ready
|
||||
self.frame_lock = frame_lock
|
||||
self.region_size = region_size
|
||||
self.region_x_offset = region_x_offset
|
||||
self.region_y_offset = region_y_offset
|
||||
self.prepped_frame_queue = prepped_frame_queue
|
||||
|
||||
def run(self):
|
||||
frame_time = 0.0
|
||||
while True:
|
||||
now = datetime.datetime.now().timestamp()
|
||||
|
||||
with self.frame_ready:
|
||||
# if there isnt a frame ready for processing or it is old, wait for a new frame
|
||||
if self.frame_time.value == frame_time or (now - self.frame_time.value) > 0.5:
|
||||
self.frame_ready.wait()
|
||||
|
||||
# make a copy of the cropped frame
|
||||
with self.frame_lock:
|
||||
cropped_frame = self.shared_frame[self.region_y_offset:self.region_y_offset+self.region_size, self.region_x_offset:self.region_x_offset+self.region_size].copy()
|
||||
frame_time = self.frame_time.value
|
||||
|
||||
# convert to RGB
|
||||
cropped_frame_rgb = cv2.cvtColor(cropped_frame, cv2.COLOR_BGR2RGB)
|
||||
# Resize to 300x300 if needed
|
||||
if cropped_frame_rgb.shape != (300, 300, 3):
|
||||
cropped_frame_rgb = cv2.resize(cropped_frame_rgb, dsize=(300, 300), interpolation=cv2.INTER_LINEAR)
|
||||
# Expand dimensions since the model expects images to have shape: [1, 300, 300, 3]
|
||||
frame_expanded = np.expand_dims(cropped_frame_rgb, axis=0)
|
||||
|
||||
# add the frame to the queue
|
||||
if not self.prepped_frame_queue.full():
|
||||
self.prepped_frame_queue.put({
|
||||
'camera_name': self.camera_name,
|
||||
'frame_time': frame_time,
|
||||
'frame': frame_expanded.flatten().copy(),
|
||||
'region_size': self.region_size,
|
||||
'region_x_offset': self.region_x_offset,
|
||||
'region_y_offset': self.region_y_offset
|
||||
})
|
||||
else:
|
||||
print("queue full. moving on")
|
||||
522
frigate/object_processing.py
Normal file
@@ -0,0 +1,522 @@
|
||||
import copy
|
||||
import base64
|
||||
import datetime
|
||||
import hashlib
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
from collections import Counter, defaultdict
|
||||
from statistics import mean, median
|
||||
from typing import Callable, Dict
|
||||
|
||||
import cv2
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
from frigate.config import FrigateConfig, CameraConfig
|
||||
from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
|
||||
from frigate.edgetpu import load_labels
|
||||
from frigate.util import SharedMemoryFrameManager, draw_box_with_label, calculate_region
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PATH_TO_LABELS = '/labelmap.txt'
|
||||
|
||||
LABELS = load_labels(PATH_TO_LABELS)
|
||||
cmap = plt.cm.get_cmap('tab10', len(LABELS.keys()))
|
||||
|
||||
COLOR_MAP = {}
|
||||
for key, val in LABELS.items():
|
||||
COLOR_MAP[val] = tuple(int(round(255 * c)) for c in cmap(key)[:3])
|
||||
|
||||
def on_edge(box, frame_shape):
|
||||
if (
|
||||
box[0] == 0 or
|
||||
box[1] == 0 or
|
||||
box[2] == frame_shape[1]-1 or
|
||||
box[3] == frame_shape[0]-1
|
||||
):
|
||||
return True
|
||||
|
||||
def is_better_thumbnail(current_thumb, new_obj, frame_shape) -> bool:
|
||||
# larger is better
|
||||
# cutoff images are less ideal, but they should also be smaller?
|
||||
# better scores are obviously better too
|
||||
|
||||
# if the new_thumb is on an edge, and the current thumb is not
|
||||
if on_edge(new_obj['box'], frame_shape) and not on_edge(current_thumb['box'], frame_shape):
|
||||
return False
|
||||
|
||||
# if the score is better by more than 5%
|
||||
if new_obj['score'] > current_thumb['score']+.05:
|
||||
return True
|
||||
|
||||
# if the area is 10% larger
|
||||
if new_obj['area'] > current_thumb['area']*1.1:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
class TrackedObject():
|
||||
def __init__(self, camera, camera_config: CameraConfig, frame_cache, obj_data):
|
||||
self.obj_data = obj_data
|
||||
self.camera = camera
|
||||
self.camera_config = camera_config
|
||||
self.frame_cache = frame_cache
|
||||
self.current_zones = []
|
||||
self.entered_zones = set()
|
||||
self.false_positive = True
|
||||
self.top_score = self.computed_score = 0.0
|
||||
self.thumbnail_data = None
|
||||
self.frame = None
|
||||
self.previous = self.to_dict()
|
||||
self._snapshot_jpg_time = 0
|
||||
ret, jpg = cv2.imencode('.jpg', np.zeros((300,300,3), np.uint8))
|
||||
self._snapshot_jpg = jpg.tobytes()
|
||||
|
||||
# start the score history
|
||||
self.score_history = [self.obj_data['score']]
|
||||
|
||||
def _is_false_positive(self):
|
||||
# once a true positive, always a true positive
|
||||
if not self.false_positive:
|
||||
return False
|
||||
|
||||
threshold = self.camera_config.objects.filters[self.obj_data['label']].threshold
|
||||
if self.computed_score < threshold:
|
||||
return True
|
||||
return False
|
||||
|
||||
def compute_score(self):
|
||||
scores = self.score_history[:]
|
||||
# pad with zeros if you dont have at least 3 scores
|
||||
if len(scores) < 3:
|
||||
scores += [0.0]*(3 - len(scores))
|
||||
return median(scores)
|
||||
|
||||
def update(self, current_frame_time, obj_data):
|
||||
significant_update = False
|
||||
self.obj_data.update(obj_data)
|
||||
# if the object is not in the current frame, add a 0.0 to the score history
|
||||
if self.obj_data['frame_time'] != current_frame_time:
|
||||
self.score_history.append(0.0)
|
||||
else:
|
||||
self.score_history.append(self.obj_data['score'])
|
||||
# only keep the last 10 scores
|
||||
if len(self.score_history) > 10:
|
||||
self.score_history = self.score_history[-10:]
|
||||
|
||||
# calculate if this is a false positive
|
||||
self.computed_score = self.compute_score()
|
||||
if self.computed_score > self.top_score:
|
||||
self.top_score = self.computed_score
|
||||
self.false_positive = self._is_false_positive()
|
||||
|
||||
if not self.false_positive:
|
||||
# determine if this frame is a better thumbnail
|
||||
if (
|
||||
self.thumbnail_data is None
|
||||
or is_better_thumbnail(self.thumbnail_data, self.obj_data, self.camera_config.frame_shape)
|
||||
):
|
||||
self.thumbnail_data = {
|
||||
'frame_time': self.obj_data['frame_time'],
|
||||
'box': self.obj_data['box'],
|
||||
'area': self.obj_data['area'],
|
||||
'region': self.obj_data['region'],
|
||||
'score': self.obj_data['score']
|
||||
}
|
||||
significant_update = True
|
||||
|
||||
# check zones
|
||||
current_zones = []
|
||||
bottom_center = (self.obj_data['centroid'][0], self.obj_data['box'][3])
|
||||
# check each zone
|
||||
for name, zone in self.camera_config.zones.items():
|
||||
contour = zone.contour
|
||||
# check if the object is in the zone
|
||||
if (cv2.pointPolygonTest(contour, bottom_center, False) >= 0):
|
||||
# if the object passed the filters once, dont apply again
|
||||
if name in self.current_zones or not zone_filtered(self, zone.filters):
|
||||
current_zones.append(name)
|
||||
self.entered_zones.add(name)
|
||||
|
||||
# if the zones changed, signal an update
|
||||
if not self.false_positive and set(self.current_zones) != set(current_zones):
|
||||
significant_update = True
|
||||
|
||||
self.current_zones = current_zones
|
||||
return significant_update
|
||||
|
||||
def to_dict(self, include_thumbnail: bool = False):
|
||||
return {
|
||||
'id': self.obj_data['id'],
|
||||
'camera': self.camera,
|
||||
'frame_time': self.obj_data['frame_time'],
|
||||
'label': self.obj_data['label'],
|
||||
'top_score': self.top_score,
|
||||
'false_positive': self.false_positive,
|
||||
'start_time': self.obj_data['start_time'],
|
||||
'end_time': self.obj_data.get('end_time', None),
|
||||
'score': self.obj_data['score'],
|
||||
'box': self.obj_data['box'],
|
||||
'area': self.obj_data['area'],
|
||||
'region': self.obj_data['region'],
|
||||
'current_zones': self.current_zones.copy(),
|
||||
'entered_zones': list(self.entered_zones).copy(),
|
||||
'thumbnail': base64.b64encode(self.get_jpg_bytes()).decode('utf-8') if include_thumbnail else None
|
||||
}
|
||||
|
||||
def get_jpg_bytes(self):
|
||||
if self.thumbnail_data is None or self._snapshot_jpg_time == self.thumbnail_data['frame_time']:
|
||||
return self._snapshot_jpg
|
||||
|
||||
if not self.thumbnail_data['frame_time'] in self.frame_cache:
|
||||
logger.error(f"Unable to create thumbnail for {self.obj_data['id']}")
|
||||
logger.error(f"Looking for frame_time of {self.thumbnail_data['frame_time']}")
|
||||
logger.error(f"Thumbnail frames: {','.join([str(k) for k in self.frame_cache.keys()])}")
|
||||
return self._snapshot_jpg
|
||||
|
||||
# TODO: crop first to avoid converting the entire frame?
|
||||
snapshot_config = self.camera_config.snapshots
|
||||
best_frame = cv2.cvtColor(self.frame_cache[self.thumbnail_data['frame_time']], cv2.COLOR_YUV2BGR_I420)
|
||||
|
||||
if snapshot_config.draw_bounding_boxes:
|
||||
thickness = 2
|
||||
color = COLOR_MAP[self.obj_data['label']]
|
||||
box = self.thumbnail_data['box']
|
||||
draw_box_with_label(best_frame, box[0], box[1], box[2], box[3], self.obj_data['label'],
|
||||
f"{int(self.thumbnail_data['score']*100)}% {int(self.thumbnail_data['area'])}", thickness=thickness, color=color)
|
||||
|
||||
if snapshot_config.crop_to_region:
|
||||
box = self.thumbnail_data['box']
|
||||
region = calculate_region(best_frame.shape, box[0], box[1], box[2], box[3], 1.1)
|
||||
best_frame = best_frame[region[1]:region[3], region[0]:region[2]]
|
||||
|
||||
if snapshot_config.height:
|
||||
height = snapshot_config.height
|
||||
width = int(height*best_frame.shape[1]/best_frame.shape[0])
|
||||
best_frame = cv2.resize(best_frame, dsize=(width, height), interpolation=cv2.INTER_AREA)
|
||||
|
||||
if snapshot_config.show_timestamp:
|
||||
time_to_show = datetime.datetime.fromtimestamp(self.thumbnail_data['frame_time']).strftime("%m/%d/%Y %H:%M:%S")
|
||||
size = cv2.getTextSize(time_to_show, cv2.FONT_HERSHEY_SIMPLEX, fontScale=1, thickness=2)
|
||||
text_width = size[0][0]
|
||||
desired_size = max(150, 0.33*best_frame.shape[1])
|
||||
font_scale = desired_size/text_width
|
||||
cv2.putText(best_frame, time_to_show, (5, best_frame.shape[0]-7), cv2.FONT_HERSHEY_SIMPLEX,
|
||||
fontScale=font_scale, color=(255, 255, 255), thickness=2)
|
||||
|
||||
ret, jpg = cv2.imencode('.jpg', best_frame)
|
||||
if ret:
|
||||
self._snapshot_jpg = jpg.tobytes()
|
||||
|
||||
return self._snapshot_jpg
|
||||
|
||||
def zone_filtered(obj: TrackedObject, object_config):
|
||||
object_name = obj.obj_data['label']
|
||||
|
||||
if object_name in object_config:
|
||||
obj_settings = object_config[object_name]
|
||||
|
||||
# if the min area is larger than the
|
||||
# detected object, don't add it to detected objects
|
||||
if obj_settings.min_area > obj.obj_data['area']:
|
||||
return True
|
||||
|
||||
# if the detected object is larger than the
|
||||
# max area, don't add it to detected objects
|
||||
if obj_settings.max_area < obj.obj_data['area']:
|
||||
return True
|
||||
|
||||
# if the score is lower than the threshold, skip
|
||||
if obj_settings.threshold > obj.computed_score:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# Maintains the state of a camera
|
||||
class CameraState():
|
||||
def __init__(self, name, config, frame_manager):
|
||||
self.name = name
|
||||
self.config = config
|
||||
self.camera_config = config.cameras[name]
|
||||
self.frame_manager = frame_manager
|
||||
self.best_objects: Dict[str, TrackedObject] = {}
|
||||
self.object_counts = defaultdict(lambda: 0)
|
||||
self.tracked_objects: Dict[str, TrackedObject] = {}
|
||||
self.frame_cache = {}
|
||||
self.zone_objects = defaultdict(lambda: [])
|
||||
self._current_frame = np.zeros(self.camera_config.frame_shape_yuv, np.uint8)
|
||||
self.current_frame_lock = threading.Lock()
|
||||
self.current_frame_time = 0.0
|
||||
self.previous_frame_id = None
|
||||
self.callbacks = defaultdict(lambda: [])
|
||||
|
||||
def get_current_frame(self, draw_options={}):
|
||||
with self.current_frame_lock:
|
||||
frame_copy = np.copy(self._current_frame)
|
||||
frame_time = self.current_frame_time
|
||||
tracked_objects = {k: v.to_dict() for k,v in self.tracked_objects.items()}
|
||||
motion_boxes = self.motion_boxes.copy()
|
||||
regions = self.regions.copy()
|
||||
|
||||
frame_copy = cv2.cvtColor(frame_copy, cv2.COLOR_YUV2BGR_I420)
|
||||
# draw on the frame
|
||||
if draw_options.get('bounding_boxes'):
|
||||
# draw the bounding boxes on the frame
|
||||
for obj in tracked_objects.values():
|
||||
thickness = 2
|
||||
color = COLOR_MAP[obj['label']]
|
||||
|
||||
if obj['frame_time'] != frame_time:
|
||||
thickness = 1
|
||||
color = (255,0,0)
|
||||
|
||||
# draw the bounding boxes on the frame
|
||||
box = obj['box']
|
||||
draw_box_with_label(frame_copy, box[0], box[1], box[2], box[3], obj['label'], f"{int(obj['score']*100)}% {int(obj['area'])}", thickness=thickness, color=color)
|
||||
|
||||
if draw_options.get('regions'):
|
||||
for region in regions:
|
||||
cv2.rectangle(frame_copy, (region[0], region[1]), (region[2], region[3]), (0,255,0), 2)
|
||||
|
||||
if draw_options.get('timestamp'):
|
||||
time_to_show = datetime.datetime.fromtimestamp(frame_time).strftime("%m/%d/%Y %H:%M:%S")
|
||||
cv2.putText(frame_copy, time_to_show, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, fontScale=.8, color=(255, 255, 255), thickness=2)
|
||||
|
||||
if draw_options.get('zones'):
|
||||
for name, zone in self.camera_config.zones.items():
|
||||
thickness = 8 if any([name in obj['current_zones'] for obj in tracked_objects.values()]) else 2
|
||||
cv2.drawContours(frame_copy, [zone.contour], -1, zone.color, thickness)
|
||||
|
||||
if draw_options.get('mask'):
|
||||
mask_overlay = np.where(self.camera_config.mask==[0])
|
||||
frame_copy[mask_overlay] = [0,0,0]
|
||||
|
||||
if draw_options.get('motion_boxes'):
|
||||
for m_box in motion_boxes:
|
||||
cv2.rectangle(frame_copy, (m_box[0], m_box[1]), (m_box[2], m_box[3]), (0,0,255), 2)
|
||||
|
||||
return frame_copy
|
||||
|
||||
def finished(self, obj_id):
|
||||
del self.tracked_objects[obj_id]
|
||||
|
||||
def on(self, event_type: str, callback: Callable[[Dict], None]):
|
||||
self.callbacks[event_type].append(callback)
|
||||
|
||||
def update(self, frame_time, current_detections, motion_boxes, regions):
|
||||
self.current_frame_time = frame_time
|
||||
self.motion_boxes = motion_boxes
|
||||
self.regions = regions
|
||||
# get the new frame
|
||||
frame_id = f"{self.name}{frame_time}"
|
||||
current_frame = self.frame_manager.get(frame_id, self.camera_config.frame_shape_yuv)
|
||||
|
||||
current_ids = current_detections.keys()
|
||||
previous_ids = self.tracked_objects.keys()
|
||||
removed_ids = list(set(previous_ids).difference(current_ids))
|
||||
new_ids = list(set(current_ids).difference(previous_ids))
|
||||
updated_ids = list(set(current_ids).intersection(previous_ids))
|
||||
|
||||
for id in new_ids:
|
||||
new_obj = self.tracked_objects[id] = TrackedObject(self.name, self.camera_config, self.frame_cache, current_detections[id])
|
||||
|
||||
# call event handlers
|
||||
for c in self.callbacks['start']:
|
||||
c(self.name, new_obj, frame_time)
|
||||
|
||||
for id in updated_ids:
|
||||
updated_obj = self.tracked_objects[id]
|
||||
significant_update = updated_obj.update(frame_time, current_detections[id])
|
||||
|
||||
if significant_update:
|
||||
# ensure this frame is stored in the cache
|
||||
if updated_obj.thumbnail_data['frame_time'] == frame_time and frame_time not in self.frame_cache:
|
||||
self.frame_cache[frame_time] = np.copy(current_frame)
|
||||
|
||||
# call event handlers
|
||||
for c in self.callbacks['update']:
|
||||
c(self.name, updated_obj, frame_time)
|
||||
|
||||
for id in removed_ids:
|
||||
# publish events to mqtt
|
||||
removed_obj = self.tracked_objects[id]
|
||||
if not 'end_time' in removed_obj.obj_data:
|
||||
removed_obj.obj_data['end_time'] = frame_time
|
||||
for c in self.callbacks['end']:
|
||||
c(self.name, removed_obj, frame_time)
|
||||
|
||||
# TODO: can i switch to looking this up and only changing when an event ends?
|
||||
# maintain best objects
|
||||
for obj in self.tracked_objects.values():
|
||||
object_type = obj.obj_data['label']
|
||||
# if the object's thumbnail is not from the current frame
|
||||
if obj.false_positive or obj.thumbnail_data['frame_time'] != self.current_frame_time:
|
||||
continue
|
||||
if object_type in self.best_objects:
|
||||
current_best = self.best_objects[object_type]
|
||||
now = datetime.datetime.now().timestamp()
|
||||
# if the object is a higher score than the current best score
|
||||
# or the current object is older than desired, use the new object
|
||||
if (is_better_thumbnail(current_best.thumbnail_data, obj.thumbnail_data, self.camera_config.frame_shape)
|
||||
or (now - current_best.thumbnail_data['frame_time']) > self.camera_config.best_image_timeout):
|
||||
self.best_objects[object_type] = obj
|
||||
for c in self.callbacks['snapshot']:
|
||||
c(self.name, self.best_objects[object_type], frame_time)
|
||||
else:
|
||||
self.best_objects[object_type] = obj
|
||||
for c in self.callbacks['snapshot']:
|
||||
c(self.name, self.best_objects[object_type], frame_time)
|
||||
|
||||
# update overall camera state for each object type
|
||||
obj_counter = Counter()
|
||||
for obj in self.tracked_objects.values():
|
||||
if not obj.false_positive:
|
||||
obj_counter[obj.obj_data['label']] += 1
|
||||
|
||||
# report on detected objects
|
||||
for obj_name, count in obj_counter.items():
|
||||
if count != self.object_counts[obj_name]:
|
||||
self.object_counts[obj_name] = count
|
||||
for c in self.callbacks['object_status']:
|
||||
c(self.name, obj_name, count)
|
||||
|
||||
# expire any objects that are >0 and no longer detected
|
||||
expired_objects = [obj_name for obj_name, count in self.object_counts.items() if count > 0 and not obj_name in obj_counter]
|
||||
for obj_name in expired_objects:
|
||||
self.object_counts[obj_name] = 0
|
||||
for c in self.callbacks['object_status']:
|
||||
c(self.name, obj_name, 0)
|
||||
for c in self.callbacks['snapshot']:
|
||||
c(self.name, self.best_objects[obj_name], frame_time)
|
||||
|
||||
# cleanup thumbnail frame cache
|
||||
current_thumb_frames = set([obj.thumbnail_data['frame_time'] for obj in self.tracked_objects.values() if not obj.false_positive])
|
||||
current_best_frames = set([obj.thumbnail_data['frame_time'] for obj in self.best_objects.values()])
|
||||
thumb_frames_to_delete = [t for t in self.frame_cache.keys() if not t in current_thumb_frames and not t in current_best_frames]
|
||||
for t in thumb_frames_to_delete:
|
||||
del self.frame_cache[t]
|
||||
|
||||
with self.current_frame_lock:
|
||||
self._current_frame = current_frame
|
||||
if not self.previous_frame_id is None:
|
||||
self.frame_manager.delete(self.previous_frame_id)
|
||||
self.previous_frame_id = frame_id
|
||||
|
||||
class TrackedObjectProcessor(threading.Thread):
|
||||
def __init__(self, config: FrigateConfig, client, topic_prefix, tracked_objects_queue, event_queue, event_processed_queue, stop_event):
|
||||
threading.Thread.__init__(self)
|
||||
self.name = "detected_frames_processor"
|
||||
self.config = config
|
||||
self.client = client
|
||||
self.topic_prefix = topic_prefix
|
||||
self.tracked_objects_queue = tracked_objects_queue
|
||||
self.event_queue = event_queue
|
||||
self.event_processed_queue = event_processed_queue
|
||||
self.stop_event = stop_event
|
||||
self.camera_states: Dict[str, CameraState] = {}
|
||||
self.frame_manager = SharedMemoryFrameManager()
|
||||
|
||||
def start(camera, obj: TrackedObject, current_frame_time):
|
||||
self.event_queue.put(('start', camera, obj.to_dict()))
|
||||
|
||||
def update(camera, obj: TrackedObject, current_frame_time):
|
||||
after = obj.to_dict()
|
||||
message = { 'before': obj.previous, 'after': after }
|
||||
self.client.publish(f"{self.topic_prefix}/events", json.dumps(message), retain=False)
|
||||
obj.previous = after
|
||||
|
||||
def end(camera, obj: TrackedObject, current_frame_time):
|
||||
if not obj.false_positive:
|
||||
message = { 'before': obj.previous, 'after': obj.to_dict() }
|
||||
self.client.publish(f"{self.topic_prefix}/events", json.dumps(message), retain=False)
|
||||
self.event_queue.put(('end', camera, obj.to_dict(include_thumbnail=True)))
|
||||
|
||||
def snapshot(camera, obj: TrackedObject, current_frame_time):
|
||||
self.client.publish(f"{self.topic_prefix}/{camera}/{obj.obj_data['label']}/snapshot", obj.get_jpg_bytes(), retain=True)
|
||||
|
||||
def object_status(camera, object_name, status):
|
||||
self.client.publish(f"{self.topic_prefix}/{camera}/{object_name}", status, retain=False)
|
||||
|
||||
for camera in self.config.cameras.keys():
|
||||
camera_state = CameraState(camera, self.config, self.frame_manager)
|
||||
camera_state.on('start', start)
|
||||
camera_state.on('update', update)
|
||||
camera_state.on('end', end)
|
||||
camera_state.on('snapshot', snapshot)
|
||||
camera_state.on('object_status', object_status)
|
||||
self.camera_states[camera] = camera_state
|
||||
|
||||
# {
|
||||
# 'zone_name': {
|
||||
# 'person': {
|
||||
# 'camera_1': 2,
|
||||
# 'camera_2': 1
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
self.zone_data = defaultdict(lambda: defaultdict(lambda: {}))
|
||||
|
||||
def get_best(self, camera, label):
|
||||
# TODO: need a lock here
|
||||
camera_state = self.camera_states[camera]
|
||||
if label in camera_state.best_objects:
|
||||
best_obj = camera_state.best_objects[label]
|
||||
best = best_obj.thumbnail_data.copy()
|
||||
best['frame'] = camera_state.frame_cache.get(best_obj.thumbnail_data['frame_time'])
|
||||
return best
|
||||
else:
|
||||
return {}
|
||||
|
||||
def get_current_frame(self, camera, draw_options={}):
|
||||
return self.camera_states[camera].get_current_frame(draw_options)
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
if self.stop_event.is_set():
|
||||
logger.info(f"Exiting object processor...")
|
||||
break
|
||||
|
||||
try:
|
||||
camera, frame_time, current_tracked_objects, motion_boxes, regions = self.tracked_objects_queue.get(True, 10)
|
||||
except queue.Empty:
|
||||
continue
|
||||
|
||||
camera_state = self.camera_states[camera]
|
||||
|
||||
camera_state.update(frame_time, current_tracked_objects, motion_boxes, regions)
|
||||
|
||||
# update zone counts for each label
|
||||
# for each zone in the current camera
|
||||
for zone in self.config.cameras[camera].zones.keys():
|
||||
# count labels for the camera in the zone
|
||||
obj_counter = Counter()
|
||||
for obj in camera_state.tracked_objects.values():
|
||||
if zone in obj.current_zones and not obj.false_positive:
|
||||
obj_counter[obj.obj_data['label']] += 1
|
||||
|
||||
# update counts and publish status
|
||||
for label in set(list(self.zone_data[zone].keys()) + list(obj_counter.keys())):
|
||||
# if we have previously published a count for this zone/label
|
||||
zone_label = self.zone_data[zone][label]
|
||||
if camera in zone_label:
|
||||
current_count = sum(zone_label.values())
|
||||
zone_label[camera] = obj_counter[label] if label in obj_counter else 0
|
||||
new_count = sum(zone_label.values())
|
||||
if new_count != current_count:
|
||||
self.client.publish(f"{self.topic_prefix}/{zone}/{label}", new_count, retain=False)
|
||||
# if this is a new zone/label combo for this camera
|
||||
else:
|
||||
if label in obj_counter:
|
||||
zone_label[camera] = obj_counter[label]
|
||||
self.client.publish(f"{self.topic_prefix}/{zone}/{label}", obj_counter[label], retain=False)
|
||||
|
||||
# cleanup event finished queue
|
||||
while not self.event_processed_queue.empty():
|
||||
event_id, camera = self.event_processed_queue.get()
|
||||
self.camera_states[camera].finished(event_id)
|
||||
@@ -1,96 +1,149 @@
|
||||
import time
|
||||
import copy
|
||||
import datetime
|
||||
import itertools
|
||||
import multiprocessing as mp
|
||||
import random
|
||||
import string
|
||||
import threading
|
||||
import time
|
||||
from collections import defaultdict
|
||||
|
||||
import cv2
|
||||
from object_detection.utils import visualization_utils as vis_util
|
||||
import numpy as np
|
||||
from scipy.spatial import distance as dist
|
||||
|
||||
class ObjectCleaner(threading.Thread):
|
||||
def __init__(self, objects_parsed, detected_objects):
|
||||
threading.Thread.__init__(self)
|
||||
self._objects_parsed = objects_parsed
|
||||
self._detected_objects = detected_objects
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
|
||||
# wait a bit before checking for expired frames
|
||||
time.sleep(0.2)
|
||||
|
||||
# expire the objects that are more than 1 second old
|
||||
now = datetime.datetime.now().timestamp()
|
||||
# look for the first object found within the last second
|
||||
# (newest objects are appended to the end)
|
||||
detected_objects = self._detected_objects.copy()
|
||||
|
||||
num_to_delete = 0
|
||||
for obj in detected_objects:
|
||||
if now-obj['frame_time']<2:
|
||||
break
|
||||
num_to_delete += 1
|
||||
if num_to_delete > 0:
|
||||
del self._detected_objects[:num_to_delete]
|
||||
|
||||
# notify that parsed objects were changed
|
||||
with self._objects_parsed:
|
||||
self._objects_parsed.notify_all()
|
||||
from frigate.config import DetectConfig
|
||||
from frigate.util import draw_box_with_label
|
||||
|
||||
|
||||
# Maintains the frame and person with the highest score from the most recent
|
||||
# motion event
|
||||
class BestPersonFrame(threading.Thread):
|
||||
def __init__(self, objects_parsed, recent_frames, detected_objects):
|
||||
threading.Thread.__init__(self)
|
||||
self.objects_parsed = objects_parsed
|
||||
self.recent_frames = recent_frames
|
||||
self.detected_objects = detected_objects
|
||||
self.best_person = None
|
||||
self.best_frame = None
|
||||
class ObjectTracker():
|
||||
def __init__(self, config: DetectConfig):
|
||||
self.tracked_objects = {}
|
||||
self.disappeared = {}
|
||||
self.max_disappeared = config.max_disappeared
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
def register(self, index, obj):
|
||||
rand_id = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
||||
id = f"{obj['frame_time']}-{rand_id}"
|
||||
obj['id'] = id
|
||||
obj['start_time'] = obj['frame_time']
|
||||
self.tracked_objects[id] = obj
|
||||
self.disappeared[id] = 0
|
||||
|
||||
# wait until objects have been parsed
|
||||
with self.objects_parsed:
|
||||
self.objects_parsed.wait()
|
||||
def deregister(self, id):
|
||||
del self.tracked_objects[id]
|
||||
del self.disappeared[id]
|
||||
|
||||
# make a copy of detected objects
|
||||
detected_objects = self.detected_objects.copy()
|
||||
detected_people = [obj for obj in detected_objects if obj['name'] == 'person']
|
||||
def update(self, id, new_obj):
|
||||
self.disappeared[id] = 0
|
||||
self.tracked_objects[id].update(new_obj)
|
||||
|
||||
# get the highest scoring person
|
||||
new_best_person = max(detected_people, key=lambda x:x['score'], default=self.best_person)
|
||||
def match_and_update(self, frame_time, new_objects):
|
||||
# group by name
|
||||
new_object_groups = defaultdict(lambda: [])
|
||||
for obj in new_objects:
|
||||
new_object_groups[obj[0]].append({
|
||||
'label': obj[0],
|
||||
'score': obj[1],
|
||||
'box': obj[2],
|
||||
'area': obj[3],
|
||||
'region': obj[4],
|
||||
'frame_time': frame_time
|
||||
})
|
||||
|
||||
# if there isnt a person, continue
|
||||
if new_best_person is None:
|
||||
# update any tracked objects with labels that are not
|
||||
# seen in the current objects and deregister if needed
|
||||
for obj in list(self.tracked_objects.values()):
|
||||
if not obj['label'] in new_object_groups:
|
||||
if self.disappeared[obj['id']] >= self.max_disappeared:
|
||||
self.deregister(obj['id'])
|
||||
else:
|
||||
self.disappeared[obj['id']] += 1
|
||||
|
||||
if len(new_objects) == 0:
|
||||
return
|
||||
|
||||
# track objects for each label type
|
||||
for label, group in new_object_groups.items():
|
||||
current_objects = [o for o in self.tracked_objects.values() if o['label'] == label]
|
||||
current_ids = [o['id'] for o in current_objects]
|
||||
current_centroids = np.array([o['centroid'] for o in current_objects])
|
||||
|
||||
# compute centroids of new objects
|
||||
for obj in group:
|
||||
centroid_x = int((obj['box'][0]+obj['box'][2]) / 2.0)
|
||||
centroid_y = int((obj['box'][1]+obj['box'][3]) / 2.0)
|
||||
obj['centroid'] = (centroid_x, centroid_y)
|
||||
|
||||
if len(current_objects) == 0:
|
||||
for index, obj in enumerate(group):
|
||||
self.register(index, obj)
|
||||
return
|
||||
|
||||
new_centroids = np.array([o['centroid'] for o in group])
|
||||
|
||||
# compute the distance between each pair of tracked
|
||||
# centroids and new centroids, respectively -- our
|
||||
# goal will be to match each new centroid to an existing
|
||||
# object centroid
|
||||
D = dist.cdist(current_centroids, new_centroids)
|
||||
|
||||
# in order to perform this matching we must (1) find the
|
||||
# smallest value in each row and then (2) sort the row
|
||||
# indexes based on their minimum values so that the row
|
||||
# with the smallest value is at the *front* of the index
|
||||
# list
|
||||
rows = D.min(axis=1).argsort()
|
||||
|
||||
# next, we perform a similar process on the columns by
|
||||
# finding the smallest value in each column and then
|
||||
# sorting using the previously computed row index list
|
||||
cols = D.argmin(axis=1)[rows]
|
||||
|
||||
# in order to determine if we need to update, register,
|
||||
# or deregister an object we need to keep track of which
|
||||
# of the rows and column indexes we have already examined
|
||||
usedRows = set()
|
||||
usedCols = set()
|
||||
|
||||
# loop over the combination of the (row, column) index
|
||||
# tuples
|
||||
for (row, col) in zip(rows, cols):
|
||||
# if we have already examined either the row or
|
||||
# column value before, ignore it
|
||||
if row in usedRows or col in usedCols:
|
||||
continue
|
||||
|
||||
# if there is no current best_person
|
||||
if self.best_person is None:
|
||||
self.best_person = new_best_person
|
||||
# if there is already a best_person
|
||||
# otherwise, grab the object ID for the current row,
|
||||
# set its new centroid, and reset the disappeared
|
||||
# counter
|
||||
objectID = current_ids[row]
|
||||
self.update(objectID, group[col])
|
||||
|
||||
# indicate that we have examined each of the row and
|
||||
# column indexes, respectively
|
||||
usedRows.add(row)
|
||||
usedCols.add(col)
|
||||
|
||||
# compute the column index we have NOT yet examined
|
||||
unusedRows = set(range(0, D.shape[0])).difference(usedRows)
|
||||
unusedCols = set(range(0, D.shape[1])).difference(usedCols)
|
||||
|
||||
# in the event that the number of object centroids is
|
||||
# equal or greater than the number of input centroids
|
||||
# we need to check and see if some of these objects have
|
||||
# potentially disappeared
|
||||
if D.shape[0] >= D.shape[1]:
|
||||
for row in unusedRows:
|
||||
id = current_ids[row]
|
||||
|
||||
if self.disappeared[id] >= self.max_disappeared:
|
||||
self.deregister(id)
|
||||
else:
|
||||
now = datetime.datetime.now().timestamp()
|
||||
# if the new best person is a higher score than the current best person
|
||||
# or the current person is more than 1 minute old, use the new best person
|
||||
if new_best_person['score'] > self.best_person['score'] or (now - self.best_person['frame_time']) > 60:
|
||||
self.best_person = new_best_person
|
||||
|
||||
# make a copy of the recent frames
|
||||
recent_frames = self.recent_frames.copy()
|
||||
|
||||
if not self.best_person is None and self.best_person['frame_time'] in recent_frames:
|
||||
best_frame = recent_frames[self.best_person['frame_time']]
|
||||
best_frame = cv2.cvtColor(best_frame, cv2.COLOR_BGR2RGB)
|
||||
# draw the bounding box on the frame
|
||||
vis_util.draw_bounding_box_on_image_array(best_frame,
|
||||
self.best_person['ymin'],
|
||||
self.best_person['xmin'],
|
||||
self.best_person['ymax'],
|
||||
self.best_person['xmax'],
|
||||
color='red',
|
||||
thickness=2,
|
||||
display_str_list=["{}: {}%".format(self.best_person['name'],int(self.best_person['score']*100))],
|
||||
use_normalized_coordinates=False)
|
||||
|
||||
# convert back to BGR
|
||||
self.best_frame = cv2.cvtColor(best_frame, cv2.COLOR_RGB2BGR)
|
||||
self.disappeared[id] += 1
|
||||
# if the number of input centroids is greater
|
||||
# than the number of existing object centroids we need to
|
||||
# register each new input centroid as a trackable object
|
||||
else:
|
||||
for col in unusedCols:
|
||||
self.register(col, group[col])
|
||||
|
||||
208
frigate/process_clip.py
Normal file
@@ -0,0 +1,208 @@
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing as mp
|
||||
import os
|
||||
import subprocess as sp
|
||||
import sys
|
||||
from unittest import TestCase, main
|
||||
|
||||
import click
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from frigate.config import FRIGATE_CONFIG_SCHEMA, FrigateConfig
|
||||
from frigate.edgetpu import LocalObjectDetector
|
||||
from frigate.motion import MotionDetector
|
||||
from frigate.object_processing import COLOR_MAP, CameraState
|
||||
from frigate.objects import ObjectTracker
|
||||
from frigate.util import (DictFrameManager, EventsPerSecond,
|
||||
SharedMemoryFrameManager, draw_box_with_label)
|
||||
from frigate.video import (capture_frames, process_frames,
|
||||
start_or_restart_ffmpeg)
|
||||
|
||||
logging.basicConfig()
|
||||
logging.root.setLevel(logging.DEBUG)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_frame_shape(source):
|
||||
ffprobe_cmd = " ".join([
|
||||
'ffprobe',
|
||||
'-v',
|
||||
'panic',
|
||||
'-show_error',
|
||||
'-show_streams',
|
||||
'-of',
|
||||
'json',
|
||||
'"'+source+'"'
|
||||
])
|
||||
p = sp.Popen(ffprobe_cmd, stdout=sp.PIPE, shell=True)
|
||||
(output, err) = p.communicate()
|
||||
p_status = p.wait()
|
||||
info = json.loads(output)
|
||||
|
||||
video_info = [s for s in info['streams'] if s['codec_type'] == 'video'][0]
|
||||
|
||||
if video_info['height'] != 0 and video_info['width'] != 0:
|
||||
return (video_info['height'], video_info['width'], 3)
|
||||
|
||||
# fallback to using opencv if ffprobe didnt succeed
|
||||
video = cv2.VideoCapture(source)
|
||||
ret, frame = video.read()
|
||||
frame_shape = frame.shape
|
||||
video.release()
|
||||
return frame_shape
|
||||
|
||||
class ProcessClip():
|
||||
def __init__(self, clip_path, frame_shape, config: FrigateConfig):
|
||||
self.clip_path = clip_path
|
||||
self.camera_name = 'camera'
|
||||
self.config = config
|
||||
self.camera_config = self.config.cameras['camera']
|
||||
self.frame_shape = self.camera_config.frame_shape
|
||||
self.ffmpeg_cmd = [c['cmd'] for c in self.camera_config.ffmpeg_cmds if 'detect' in c['roles']][0]
|
||||
self.frame_manager = SharedMemoryFrameManager()
|
||||
self.frame_queue = mp.Queue()
|
||||
self.detected_objects_queue = mp.Queue()
|
||||
self.camera_state = CameraState(self.camera_name, config, self.frame_manager)
|
||||
|
||||
def load_frames(self):
|
||||
fps = EventsPerSecond()
|
||||
skipped_fps = EventsPerSecond()
|
||||
current_frame = mp.Value('d', 0.0)
|
||||
frame_size = self.camera_config.frame_shape_yuv[0] * self.camera_config.frame_shape_yuv[1]
|
||||
ffmpeg_process = start_or_restart_ffmpeg(self.ffmpeg_cmd, logger, sp.DEVNULL, frame_size)
|
||||
capture_frames(ffmpeg_process, self.camera_name, self.camera_config.frame_shape_yuv, self.frame_manager,
|
||||
self.frame_queue, fps, skipped_fps, current_frame)
|
||||
ffmpeg_process.wait()
|
||||
ffmpeg_process.communicate()
|
||||
|
||||
def process_frames(self, objects_to_track=['person'], object_filters={}):
|
||||
mask = np.zeros((self.frame_shape[0], self.frame_shape[1], 1), np.uint8)
|
||||
mask[:] = 255
|
||||
motion_detector = MotionDetector(self.frame_shape, mask, self.camera_config.motion)
|
||||
|
||||
object_detector = LocalObjectDetector(labels='/labelmap.txt')
|
||||
object_tracker = ObjectTracker(self.camera_config.detect)
|
||||
process_info = {
|
||||
'process_fps': mp.Value('d', 0.0),
|
||||
'detection_fps': mp.Value('d', 0.0),
|
||||
'detection_frame': mp.Value('d', 0.0)
|
||||
}
|
||||
stop_event = mp.Event()
|
||||
model_shape = (self.config.model.height, self.config.model.width)
|
||||
|
||||
process_frames(self.camera_name, self.frame_queue, self.frame_shape, model_shape,
|
||||
self.frame_manager, motion_detector, object_detector, object_tracker,
|
||||
self.detected_objects_queue, process_info,
|
||||
objects_to_track, object_filters, mask, stop_event, exit_on_empty=True)
|
||||
|
||||
def top_object(self, debug_path=None):
|
||||
obj_detected = False
|
||||
top_computed_score = 0.0
|
||||
def handle_event(name, obj, frame_time):
|
||||
nonlocal obj_detected
|
||||
nonlocal top_computed_score
|
||||
if obj.computed_score > top_computed_score:
|
||||
top_computed_score = obj.computed_score
|
||||
if not obj.false_positive:
|
||||
obj_detected = True
|
||||
self.camera_state.on('new', handle_event)
|
||||
self.camera_state.on('update', handle_event)
|
||||
|
||||
while(not self.detected_objects_queue.empty()):
|
||||
camera_name, frame_time, current_tracked_objects, motion_boxes, regions = self.detected_objects_queue.get()
|
||||
if not debug_path is None:
|
||||
self.save_debug_frame(debug_path, frame_time, current_tracked_objects.values())
|
||||
|
||||
self.camera_state.update(frame_time, current_tracked_objects, motion_boxes, regions)
|
||||
|
||||
self.frame_manager.delete(self.camera_state.previous_frame_id)
|
||||
|
||||
return {
|
||||
'object_detected': obj_detected,
|
||||
'top_score': top_computed_score
|
||||
}
|
||||
|
||||
def save_debug_frame(self, debug_path, frame_time, tracked_objects):
|
||||
current_frame = cv2.cvtColor(self.frame_manager.get(f"{self.camera_name}{frame_time}", self.camera_config.frame_shape_yuv), cv2.COLOR_YUV2BGR_I420)
|
||||
# draw the bounding boxes on the frame
|
||||
for obj in tracked_objects:
|
||||
thickness = 2
|
||||
color = (0,0,175)
|
||||
|
||||
if obj['frame_time'] != frame_time:
|
||||
thickness = 1
|
||||
color = (255,0,0)
|
||||
else:
|
||||
color = (255,255,0)
|
||||
|
||||
# draw the bounding boxes on the frame
|
||||
box = obj['box']
|
||||
draw_box_with_label(current_frame, box[0], box[1], box[2], box[3], obj['id'], f"{int(obj['score']*100)}% {int(obj['area'])}", thickness=thickness, color=color)
|
||||
# draw the regions on the frame
|
||||
region = obj['region']
|
||||
draw_box_with_label(current_frame, region[0], region[1], region[2], region[3], 'region', "", thickness=1, color=(0,255,0))
|
||||
|
||||
cv2.imwrite(f"{os.path.join(debug_path, os.path.basename(self.clip_path))}.{int(frame_time*1000000)}.jpg", current_frame)
|
||||
|
||||
@click.command()
|
||||
@click.option("-p", "--path", required=True, help="Path to clip or directory to test.")
|
||||
@click.option("-l", "--label", default='person', help="Label name to detect.")
|
||||
@click.option("-t", "--threshold", default=0.85, help="Threshold value for objects.")
|
||||
@click.option("-s", "--scores", default=None, help="File to save csv of top scores")
|
||||
@click.option("--debug-path", default=None, help="Path to output frames for debugging.")
|
||||
def process(path, label, threshold, scores, debug_path):
|
||||
clips = []
|
||||
if os.path.isdir(path):
|
||||
files = os.listdir(path)
|
||||
files.sort()
|
||||
clips = [os.path.join(path, file) for file in files]
|
||||
elif os.path.isfile(path):
|
||||
clips.append(path)
|
||||
|
||||
json_config = {
|
||||
'mqtt': {
|
||||
'host': 'mqtt'
|
||||
},
|
||||
'cameras': {
|
||||
'camera': {
|
||||
'ffmpeg': {
|
||||
'inputs': [
|
||||
{ 'path': 'path.mp4', 'global_args': '', 'input_args': '', 'roles': ['detect'] }
|
||||
]
|
||||
},
|
||||
'height': 1920,
|
||||
'width': 1080
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results = []
|
||||
for c in clips:
|
||||
logger.info(c)
|
||||
frame_shape = get_frame_shape(c)
|
||||
|
||||
json_config['cameras']['camera']['height'] = frame_shape[0]
|
||||
json_config['cameras']['camera']['width'] = frame_shape[1]
|
||||
json_config['cameras']['camera']['ffmpeg']['inputs'][0]['path'] = c
|
||||
|
||||
config = FrigateConfig(config=FRIGATE_CONFIG_SCHEMA(json_config))
|
||||
|
||||
process_clip = ProcessClip(c, frame_shape, config)
|
||||
process_clip.load_frames()
|
||||
process_clip.process_frames(objects_to_track=[label])
|
||||
|
||||
results.append((c, process_clip.top_object(debug_path)))
|
||||
|
||||
if not scores is None:
|
||||
with open(scores, 'w') as writer:
|
||||
for result in results:
|
||||
writer.write(f"{result[0]},{result[1]['top_score']}\n")
|
||||
|
||||
positive_count = sum(1 for result in results if result[1]['object_detected'])
|
||||
print(f"Objects were detected in {positive_count}/{len(results)}({positive_count/len(results)*100:.2f}%) clip(s).")
|
||||
|
||||
if __name__ == '__main__':
|
||||
process()
|
||||
125
frigate/record.py
Normal file
@@ -0,0 +1,125 @@
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import queue
|
||||
import subprocess as sp
|
||||
import threading
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
import psutil
|
||||
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SECONDS_IN_DAY = 60 * 60 * 24
|
||||
|
||||
def remove_empty_directories(directory):
|
||||
# list all directories recursively and sort them by path,
|
||||
# longest first
|
||||
paths = sorted(
|
||||
[x[0] for x in os.walk(RECORD_DIR)],
|
||||
key=lambda p: len(str(p)),
|
||||
reverse=True,
|
||||
)
|
||||
for path in paths:
|
||||
# don't delete the parent
|
||||
if path == RECORD_DIR:
|
||||
continue
|
||||
if len(os.listdir(path)) == 0:
|
||||
os.rmdir(path)
|
||||
|
||||
class RecordingMaintainer(threading.Thread):
|
||||
def __init__(self, config: FrigateConfig, stop_event):
|
||||
threading.Thread.__init__(self)
|
||||
self.name = 'recording_maint'
|
||||
self.config = config
|
||||
self.stop_event = stop_event
|
||||
|
||||
def move_files(self):
|
||||
recordings = [d for d in os.listdir(RECORD_DIR) if os.path.isfile(os.path.join(RECORD_DIR, d)) and d.endswith(".mp4")]
|
||||
|
||||
files_in_use = []
|
||||
for process in psutil.process_iter():
|
||||
if process.name() != 'ffmpeg':
|
||||
continue
|
||||
try:
|
||||
flist = process.open_files()
|
||||
if flist:
|
||||
for nt in flist:
|
||||
if nt.path.startswith(RECORD_DIR):
|
||||
files_in_use.append(nt.path.split('/')[-1])
|
||||
except:
|
||||
continue
|
||||
|
||||
for f in recordings:
|
||||
if f in files_in_use:
|
||||
continue
|
||||
|
||||
camera = '-'.join(f.split('-')[:-1])
|
||||
start_time = datetime.datetime.strptime(f.split('-')[-1].split('.')[0], '%Y%m%d%H%M%S')
|
||||
|
||||
ffprobe_cmd = " ".join([
|
||||
'ffprobe',
|
||||
'-v',
|
||||
'error',
|
||||
'-show_entries',
|
||||
'format=duration',
|
||||
'-of',
|
||||
'default=noprint_wrappers=1:nokey=1',
|
||||
f"{os.path.join(RECORD_DIR,f)}"
|
||||
])
|
||||
p = sp.Popen(ffprobe_cmd, stdout=sp.PIPE, shell=True)
|
||||
(output, err) = p.communicate()
|
||||
p_status = p.wait()
|
||||
if p_status == 0:
|
||||
duration = float(output.decode('utf-8').strip())
|
||||
else:
|
||||
logger.info(f"bad file: {f}")
|
||||
os.remove(os.path.join(RECORD_DIR,f))
|
||||
continue
|
||||
|
||||
directory = os.path.join(RECORD_DIR, start_time.strftime('%Y-%m/%d/%H'), camera)
|
||||
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
|
||||
file_name = f"{start_time.strftime('%M.%S.mp4')}"
|
||||
|
||||
os.rename(os.path.join(RECORD_DIR,f), os.path.join(directory,file_name))
|
||||
|
||||
def expire_files(self):
|
||||
delete_before = {}
|
||||
for name, camera in self.config.cameras.items():
|
||||
delete_before[name] = datetime.datetime.now().timestamp() - SECONDS_IN_DAY*camera.record.retain_days
|
||||
|
||||
for p in Path('/media/frigate/recordings').rglob("*.mp4"):
|
||||
if not p.parent in delete_before:
|
||||
continue
|
||||
if p.stat().st_mtime < delete_before[p.parent]:
|
||||
p.unlink(missing_ok=True)
|
||||
|
||||
def run(self):
|
||||
counter = 0
|
||||
self.expire_files()
|
||||
while(True):
|
||||
if self.stop_event.is_set():
|
||||
logger.info(f"Exiting recording maintenance...")
|
||||
break
|
||||
|
||||
# only expire events every 10 minutes, but check for new files every 10 seconds
|
||||
time.sleep(10)
|
||||
counter = counter + 1
|
||||
if counter > 60:
|
||||
self.expire_files()
|
||||
remove_empty_directories(RECORD_DIR)
|
||||
counter = 0
|
||||
|
||||
self.move_files()
|
||||
|
||||
|
||||
|
||||
0
frigate/test/__init__.py
Normal file
344
frigate/test/test_config.py
Normal file
@@ -0,0 +1,344 @@
|
||||
import json
|
||||
from unittest import TestCase, main
|
||||
import voluptuous as vol
|
||||
from frigate.config import FRIGATE_CONFIG_SCHEMA, FrigateConfig
|
||||
|
||||
class TestConfig(TestCase):
|
||||
def setUp(self):
|
||||
self.minimal = {
|
||||
'mqtt': {
|
||||
'host': 'mqtt'
|
||||
},
|
||||
'cameras': {
|
||||
'back': {
|
||||
'ffmpeg': {
|
||||
'inputs': [
|
||||
{ 'path': 'rtsp://10.0.0.1:554/video', 'roles': ['detect'] }
|
||||
]
|
||||
},
|
||||
'height': 1080,
|
||||
'width': 1920
|
||||
}
|
||||
}
|
||||
}
|
||||
def test_empty(self):
|
||||
FRIGATE_CONFIG_SCHEMA({})
|
||||
|
||||
def test_minimal(self):
|
||||
FRIGATE_CONFIG_SCHEMA(self.minimal)
|
||||
|
||||
def test_config_class(self):
|
||||
FrigateConfig(config=self.minimal)
|
||||
|
||||
def test_inherit_tracked_objects(self):
|
||||
config = {
|
||||
'mqtt': {
|
||||
'host': 'mqtt'
|
||||
},
|
||||
'objects': {
|
||||
'track': ['person', 'dog']
|
||||
},
|
||||
'cameras': {
|
||||
'back': {
|
||||
'ffmpeg': {
|
||||
'inputs': [
|
||||
{ 'path': 'rtsp://10.0.0.1:554/video', 'roles': ['detect'] }
|
||||
]
|
||||
},
|
||||
'height': 1080,
|
||||
'width': 1920
|
||||
}
|
||||
}
|
||||
}
|
||||
frigate_config = FrigateConfig(config=config)
|
||||
assert('dog' in frigate_config.cameras['back'].objects.track)
|
||||
|
||||
def test_override_tracked_objects(self):
|
||||
config = {
|
||||
'mqtt': {
|
||||
'host': 'mqtt'
|
||||
},
|
||||
'objects': {
|
||||
'track': ['person', 'dog']
|
||||
},
|
||||
'cameras': {
|
||||
'back': {
|
||||
'ffmpeg': {
|
||||
'inputs': [
|
||||
{ 'path': 'rtsp://10.0.0.1:554/video', 'roles': ['detect'] }
|
||||
]
|
||||
},
|
||||
'height': 1080,
|
||||
'width': 1920,
|
||||
'objects': {
|
||||
'track': ['cat']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
frigate_config = FrigateConfig(config=config)
|
||||
assert('cat' in frigate_config.cameras['back'].objects.track)
|
||||
|
||||
def test_default_object_filters(self):
|
||||
config = {
|
||||
'mqtt': {
|
||||
'host': 'mqtt'
|
||||
},
|
||||
'objects': {
|
||||
'track': ['person', 'dog']
|
||||
},
|
||||
'cameras': {
|
||||
'back': {
|
||||
'ffmpeg': {
|
||||
'inputs': [
|
||||
{ 'path': 'rtsp://10.0.0.1:554/video', 'roles': ['detect'] }
|
||||
]
|
||||
},
|
||||
'height': 1080,
|
||||
'width': 1920
|
||||
}
|
||||
}
|
||||
}
|
||||
frigate_config = FrigateConfig(config=config)
|
||||
assert('dog' in frigate_config.cameras['back'].objects.filters)
|
||||
|
||||
def test_inherit_object_filters(self):
|
||||
config = {
|
||||
'mqtt': {
|
||||
'host': 'mqtt'
|
||||
},
|
||||
'objects': {
|
||||
'track': ['person', 'dog'],
|
||||
'filters': {
|
||||
'dog': {
|
||||
'threshold': 0.7
|
||||
}
|
||||
}
|
||||
},
|
||||
'cameras': {
|
||||
'back': {
|
||||
'ffmpeg': {
|
||||
'inputs': [
|
||||
{ 'path': 'rtsp://10.0.0.1:554/video', 'roles': ['detect'] }
|
||||
]
|
||||
},
|
||||
'height': 1080,
|
||||
'width': 1920
|
||||
}
|
||||
}
|
||||
}
|
||||
frigate_config = FrigateConfig(config=config)
|
||||
assert('dog' in frigate_config.cameras['back'].objects.filters)
|
||||
assert(frigate_config.cameras['back'].objects.filters['dog'].threshold == 0.7)
|
||||
|
||||
def test_override_object_filters(self):
|
||||
config = {
|
||||
'mqtt': {
|
||||
'host': 'mqtt'
|
||||
},
|
||||
'cameras': {
|
||||
'back': {
|
||||
'ffmpeg': {
|
||||
'inputs': [
|
||||
{ 'path': 'rtsp://10.0.0.1:554/video', 'roles': ['detect'] }
|
||||
]
|
||||
},
|
||||
'height': 1080,
|
||||
'width': 1920,
|
||||
'objects': {
|
||||
'track': ['person', 'dog'],
|
||||
'filters': {
|
||||
'dog': {
|
||||
'threshold': 0.7
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
frigate_config = FrigateConfig(config=config)
|
||||
assert('dog' in frigate_config.cameras['back'].objects.filters)
|
||||
assert(frigate_config.cameras['back'].objects.filters['dog'].threshold == 0.7)
|
||||
|
||||
def test_ffmpeg_params(self):
|
||||
config = {
|
||||
'ffmpeg': {
|
||||
'input_args': ['-re']
|
||||
},
|
||||
'mqtt': {
|
||||
'host': 'mqtt'
|
||||
},
|
||||
'cameras': {
|
||||
'back': {
|
||||
'ffmpeg': {
|
||||
'inputs': [
|
||||
{ 'path': 'rtsp://10.0.0.1:554/video', 'roles': ['detect'] }
|
||||
]
|
||||
},
|
||||
'height': 1080,
|
||||
'width': 1920,
|
||||
'objects': {
|
||||
'track': ['person', 'dog'],
|
||||
'filters': {
|
||||
'dog': {
|
||||
'threshold': 0.7
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
frigate_config = FrigateConfig(config=config)
|
||||
assert('-re' in frigate_config.cameras['back'].ffmpeg_cmds[0]['cmd'])
|
||||
|
||||
def test_inherit_save_clips_retention(self):
|
||||
config = {
|
||||
'mqtt': {
|
||||
'host': 'mqtt'
|
||||
},
|
||||
'save_clips': {
|
||||
'retain': {
|
||||
'default': 20,
|
||||
'objects': {
|
||||
'person': 30
|
||||
}
|
||||
}
|
||||
},
|
||||
'cameras': {
|
||||
'back': {
|
||||
'ffmpeg': {
|
||||
'inputs': [
|
||||
{ 'path': 'rtsp://10.0.0.1:554/video', 'roles': ['detect'] }
|
||||
]
|
||||
},
|
||||
'height': 1080,
|
||||
'width': 1920
|
||||
}
|
||||
}
|
||||
}
|
||||
frigate_config = FrigateConfig(config=config)
|
||||
assert(frigate_config.cameras['back'].save_clips.retain.objects['person'] == 30)
|
||||
|
||||
def test_roles_listed_twice_throws_error(self):
|
||||
config = {
|
||||
'mqtt': {
|
||||
'host': 'mqtt'
|
||||
},
|
||||
'save_clips': {
|
||||
'retain': {
|
||||
'default': 20,
|
||||
'objects': {
|
||||
'person': 30
|
||||
}
|
||||
}
|
||||
},
|
||||
'cameras': {
|
||||
'back': {
|
||||
'ffmpeg': {
|
||||
'inputs': [
|
||||
{ 'path': 'rtsp://10.0.0.1:554/video', 'roles': ['detect'] },
|
||||
{ 'path': 'rtsp://10.0.0.1:554/video2', 'roles': ['detect'] }
|
||||
]
|
||||
},
|
||||
'height': 1080,
|
||||
'width': 1920
|
||||
}
|
||||
}
|
||||
}
|
||||
self.assertRaises(vol.MultipleInvalid, lambda: FrigateConfig(config=config))
|
||||
|
||||
def test_zone_matching_camera_name_throws_error(self):
|
||||
config = {
|
||||
'mqtt': {
|
||||
'host': 'mqtt'
|
||||
},
|
||||
'save_clips': {
|
||||
'retain': {
|
||||
'default': 20,
|
||||
'objects': {
|
||||
'person': 30
|
||||
}
|
||||
}
|
||||
},
|
||||
'cameras': {
|
||||
'back': {
|
||||
'ffmpeg': {
|
||||
'inputs': [
|
||||
{ 'path': 'rtsp://10.0.0.1:554/video', 'roles': ['detect'] }
|
||||
]
|
||||
},
|
||||
'height': 1080,
|
||||
'width': 1920,
|
||||
'zones': {
|
||||
'back': {
|
||||
'coordinates': '1,1,1,1,1,1'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.assertRaises(vol.MultipleInvalid, lambda: FrigateConfig(config=config))
|
||||
|
||||
def test_save_clips_should_default_to_global_objects(self):
|
||||
config = {
|
||||
'mqtt': {
|
||||
'host': 'mqtt'
|
||||
},
|
||||
'save_clips': {
|
||||
'retain': {
|
||||
'default': 20,
|
||||
'objects': {
|
||||
'person': 30
|
||||
}
|
||||
}
|
||||
},
|
||||
'objects': {
|
||||
'track': ['person', 'dog']
|
||||
},
|
||||
'cameras': {
|
||||
'back': {
|
||||
'ffmpeg': {
|
||||
'inputs': [
|
||||
{ 'path': 'rtsp://10.0.0.1:554/video', 'roles': ['detect'] }
|
||||
]
|
||||
},
|
||||
'height': 1080,
|
||||
'width': 1920,
|
||||
'save_clips': {
|
||||
'enabled': True
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
config = FrigateConfig(config=config)
|
||||
assert(len(config.cameras['back'].save_clips.objects) == 2)
|
||||
assert('dog' in config.cameras['back'].save_clips.objects)
|
||||
assert('person' in config.cameras['back'].save_clips.objects)
|
||||
|
||||
def test_role_assigned_but_not_enabled(self):
|
||||
json_config = {
|
||||
'mqtt': {
|
||||
'host': 'mqtt'
|
||||
},
|
||||
'cameras': {
|
||||
'back': {
|
||||
'ffmpeg': {
|
||||
'inputs': [
|
||||
{ 'path': 'rtsp://10.0.0.1:554/video', 'roles': ['detect', 'rtmp'] },
|
||||
{ 'path': 'rtsp://10.0.0.1:554/clips', 'roles': ['clips'] }
|
||||
]
|
||||
},
|
||||
'height': 1080,
|
||||
'width': 1920
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config = FrigateConfig(config=json_config)
|
||||
ffmpeg_cmds = config.cameras['back'].ffmpeg_cmds
|
||||
assert(len(ffmpeg_cmds) == 1)
|
||||
assert(not 'clips' in ffmpeg_cmds[0]['roles'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(verbosity=2)
|
||||
39
frigate/test/test_yuv_region_2_rgb.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
from unittest import TestCase, main
|
||||
from frigate.util import yuv_region_2_rgb
|
||||
|
||||
class TestYuvRegion2RGB(TestCase):
|
||||
def setUp(self):
|
||||
self.bgr_frame = np.zeros((100, 200, 3), np.uint8)
|
||||
self.bgr_frame[:] = (0, 0, 255)
|
||||
self.bgr_frame[5:55, 5:55] = (255,0,0)
|
||||
# cv2.imwrite(f"bgr_frame.jpg", self.bgr_frame)
|
||||
self.yuv_frame = cv2.cvtColor(self.bgr_frame, cv2.COLOR_BGR2YUV_I420)
|
||||
|
||||
def test_crop_yuv(self):
|
||||
cropped = yuv_region_2_rgb(self.yuv_frame, (10,10,50,50))
|
||||
# ensure the upper left pixel is blue
|
||||
assert(np.all(cropped[0, 0] == [0, 0, 255]))
|
||||
|
||||
def test_crop_yuv_out_of_bounds(self):
|
||||
cropped = yuv_region_2_rgb(self.yuv_frame, (0,0,200,200))
|
||||
# cv2.imwrite(f"cropped.jpg", cv2.cvtColor(cropped, cv2.COLOR_RGB2BGR))
|
||||
# ensure the upper left pixel is red
|
||||
# the yuv conversion has some noise
|
||||
assert(np.all(cropped[0, 0] == [255, 1, 0]))
|
||||
# ensure the bottom right is black
|
||||
assert(np.all(cropped[199, 199] == [0, 0, 0]))
|
||||
|
||||
def test_crop_yuv_portrait(self):
|
||||
bgr_frame = np.zeros((1920, 1080, 3), np.uint8)
|
||||
bgr_frame[:] = (0, 0, 255)
|
||||
bgr_frame[5:55, 5:55] = (255,0,0)
|
||||
# cv2.imwrite(f"bgr_frame.jpg", self.bgr_frame)
|
||||
yuv_frame = cv2.cvtColor(bgr_frame, cv2.COLOR_BGR2YUV_I420)
|
||||
|
||||
cropped = yuv_region_2_rgb(yuv_frame, (0, 852, 648, 1500))
|
||||
# cv2.imwrite(f"cropped.jpg", cv2.cvtColor(cropped, cv2.COLOR_RGB2BGR))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(verbosity=2)
|
||||
354
frigate/util.py
Normal file → Executable file
@@ -1,5 +1,353 @@
|
||||
import collections
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import signal
|
||||
import subprocess as sp
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
from abc import ABC, abstractmethod
|
||||
from multiprocessing import shared_memory
|
||||
from typing import AnyStr
|
||||
|
||||
import cv2
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
# convert shared memory array into numpy array
|
||||
def tonumpyarray(mp_arr):
|
||||
return np.frombuffer(mp_arr.get_obj(), dtype=np.uint8)
|
||||
|
||||
def draw_box_with_label(frame, x_min, y_min, x_max, y_max, label, info, thickness=2, color=None, position='ul'):
|
||||
if color is None:
|
||||
color = (0,0,255)
|
||||
display_text = "{}: {}".format(label, info)
|
||||
cv2.rectangle(frame, (x_min, y_min), (x_max, y_max), color, thickness)
|
||||
font_scale = 0.5
|
||||
font = cv2.FONT_HERSHEY_SIMPLEX
|
||||
# get the width and height of the text box
|
||||
size = cv2.getTextSize(display_text, font, fontScale=font_scale, thickness=2)
|
||||
text_width = size[0][0]
|
||||
text_height = size[0][1]
|
||||
line_height = text_height + size[1]
|
||||
# set the text start position
|
||||
if position == 'ul':
|
||||
text_offset_x = x_min
|
||||
text_offset_y = 0 if y_min < line_height else y_min - (line_height+8)
|
||||
elif position == 'ur':
|
||||
text_offset_x = x_max - (text_width+8)
|
||||
text_offset_y = 0 if y_min < line_height else y_min - (line_height+8)
|
||||
elif position == 'bl':
|
||||
text_offset_x = x_min
|
||||
text_offset_y = y_max
|
||||
elif position == 'br':
|
||||
text_offset_x = x_max - (text_width+8)
|
||||
text_offset_y = y_max
|
||||
# make the coords of the box with a small padding of two pixels
|
||||
textbox_coords = ((text_offset_x, text_offset_y), (text_offset_x + text_width + 2, text_offset_y + line_height))
|
||||
cv2.rectangle(frame, textbox_coords[0], textbox_coords[1], color, cv2.FILLED)
|
||||
cv2.putText(frame, display_text, (text_offset_x, text_offset_y + line_height - 3), font, fontScale=font_scale, color=(0, 0, 0), thickness=2)
|
||||
|
||||
def calculate_region(frame_shape, xmin, ymin, xmax, ymax, multiplier=2):
|
||||
# size is the longest edge and divisible by 4
|
||||
size = int(max(xmax-xmin, ymax-ymin)//4*4*multiplier)
|
||||
# dont go any smaller than 300
|
||||
if size < 300:
|
||||
size = 300
|
||||
|
||||
# x_offset is midpoint of bounding box minus half the size
|
||||
x_offset = int((xmax-xmin)/2.0+xmin-size/2.0)
|
||||
# if outside the image
|
||||
if x_offset < 0:
|
||||
x_offset = 0
|
||||
elif x_offset > (frame_shape[1]-size):
|
||||
x_offset = max(0, (frame_shape[1]-size))
|
||||
|
||||
# y_offset is midpoint of bounding box minus half the size
|
||||
y_offset = int((ymax-ymin)/2.0+ymin-size/2.0)
|
||||
# # if outside the image
|
||||
if y_offset < 0:
|
||||
y_offset = 0
|
||||
elif y_offset > (frame_shape[0]-size):
|
||||
y_offset = max(0, (frame_shape[0]-size))
|
||||
|
||||
return (x_offset, y_offset, x_offset+size, y_offset+size)
|
||||
|
||||
def get_yuv_crop(frame_shape, crop):
|
||||
# crop should be (x1,y1,x2,y2)
|
||||
frame_height = frame_shape[0]//3*2
|
||||
frame_width = frame_shape[1]
|
||||
|
||||
# compute the width/height of the uv channels
|
||||
uv_width = frame_width//2 # width of the uv channels
|
||||
uv_height = frame_height//4 # height of the uv channels
|
||||
|
||||
# compute the offset for upper left corner of the uv channels
|
||||
uv_x_offset = crop[0]//2 # x offset of the uv channels
|
||||
uv_y_offset = crop[1]//4 # y offset of the uv channels
|
||||
|
||||
# compute the width/height of the uv crops
|
||||
uv_crop_width = (crop[2] - crop[0])//2 # width of the cropped uv channels
|
||||
uv_crop_height = (crop[3] - crop[1])//4 # height of the cropped uv channels
|
||||
|
||||
# ensure crop dimensions are multiples of 2 and 4
|
||||
y = (
|
||||
crop[0],
|
||||
crop[1],
|
||||
crop[0] + uv_crop_width*2,
|
||||
crop[1] + uv_crop_height*4
|
||||
)
|
||||
|
||||
u1 = (
|
||||
0 + uv_x_offset,
|
||||
frame_height + uv_y_offset,
|
||||
0 + uv_x_offset + uv_crop_width,
|
||||
frame_height + uv_y_offset + uv_crop_height
|
||||
)
|
||||
|
||||
u2 = (
|
||||
uv_width + uv_x_offset,
|
||||
frame_height + uv_y_offset,
|
||||
uv_width + uv_x_offset + uv_crop_width,
|
||||
frame_height + uv_y_offset + uv_crop_height
|
||||
)
|
||||
|
||||
v1 = (
|
||||
0 + uv_x_offset,
|
||||
frame_height + uv_height + uv_y_offset,
|
||||
0 + uv_x_offset + uv_crop_width,
|
||||
frame_height + uv_height + uv_y_offset + uv_crop_height
|
||||
)
|
||||
|
||||
v2 = (
|
||||
uv_width + uv_x_offset,
|
||||
frame_height + uv_height + uv_y_offset,
|
||||
uv_width + uv_x_offset + uv_crop_width,
|
||||
frame_height + uv_height + uv_y_offset + uv_crop_height
|
||||
)
|
||||
|
||||
return y, u1, u2, v1, v2
|
||||
|
||||
def yuv_region_2_rgb(frame, region):
|
||||
try:
|
||||
height = frame.shape[0]//3*2
|
||||
width = frame.shape[1]
|
||||
|
||||
# get the crop box if the region extends beyond the frame
|
||||
crop_x1 = max(0, region[0])
|
||||
crop_y1 = max(0, region[1])
|
||||
# ensure these are a multiple of 4
|
||||
crop_x2 = min(width, region[2])
|
||||
crop_y2 = min(height, region[3])
|
||||
crop_box = (crop_x1, crop_y1, crop_x2, crop_y2)
|
||||
|
||||
y, u1, u2, v1, v2 = get_yuv_crop(frame.shape, crop_box)
|
||||
|
||||
# if the region starts outside the frame, indent the start point in the cropped frame
|
||||
y_channel_x_offset = abs(min(0, region[0]))
|
||||
y_channel_y_offset = abs(min(0, region[1]))
|
||||
|
||||
uv_channel_x_offset = y_channel_x_offset//2
|
||||
uv_channel_y_offset = y_channel_y_offset//4
|
||||
|
||||
# create the yuv region frame
|
||||
# make sure the size is a multiple of 4
|
||||
size = (region[3] - region[1])//4*4
|
||||
yuv_cropped_frame = np.zeros((size+size//2, size), np.uint8)
|
||||
# fill in black
|
||||
yuv_cropped_frame[:] = 128
|
||||
yuv_cropped_frame[0:size,0:size] = 16
|
||||
|
||||
# copy the y channel
|
||||
yuv_cropped_frame[
|
||||
y_channel_y_offset:y_channel_y_offset + y[3] - y[1],
|
||||
y_channel_x_offset:y_channel_x_offset + y[2] - y[0]
|
||||
] = frame[
|
||||
y[1]:y[3],
|
||||
y[0]:y[2]
|
||||
]
|
||||
|
||||
uv_crop_width = u1[2] - u1[0]
|
||||
uv_crop_height = u1[3] - u1[1]
|
||||
|
||||
# copy u1
|
||||
yuv_cropped_frame[
|
||||
size + uv_channel_y_offset:size + uv_channel_y_offset + uv_crop_height,
|
||||
0 + uv_channel_x_offset:0 + uv_channel_x_offset + uv_crop_width
|
||||
] = frame[
|
||||
u1[1]:u1[3],
|
||||
u1[0]:u1[2]
|
||||
]
|
||||
|
||||
# copy u2
|
||||
yuv_cropped_frame[
|
||||
size + uv_channel_y_offset:size + uv_channel_y_offset + uv_crop_height,
|
||||
size//2 + uv_channel_x_offset:size//2 + uv_channel_x_offset + uv_crop_width
|
||||
] = frame[
|
||||
u2[1]:u2[3],
|
||||
u2[0]:u2[2]
|
||||
]
|
||||
|
||||
# copy v1
|
||||
yuv_cropped_frame[
|
||||
size+size//4 + uv_channel_y_offset:size+size//4 + uv_channel_y_offset + uv_crop_height,
|
||||
0 + uv_channel_x_offset:0 + uv_channel_x_offset + uv_crop_width
|
||||
] = frame[
|
||||
v1[1]:v1[3],
|
||||
v1[0]:v1[2]
|
||||
]
|
||||
|
||||
# copy v2
|
||||
yuv_cropped_frame[
|
||||
size+size//4 + uv_channel_y_offset:size+size//4 + uv_channel_y_offset + uv_crop_height,
|
||||
size//2 + uv_channel_x_offset:size//2 + uv_channel_x_offset + uv_crop_width
|
||||
] = frame[
|
||||
v2[1]:v2[3],
|
||||
v2[0]:v2[2]
|
||||
]
|
||||
|
||||
return cv2.cvtColor(yuv_cropped_frame, cv2.COLOR_YUV2RGB_I420)
|
||||
except:
|
||||
print(f"frame.shape: {frame.shape}")
|
||||
print(f"region: {region}")
|
||||
raise
|
||||
|
||||
def intersection(box_a, box_b):
|
||||
return (
|
||||
max(box_a[0], box_b[0]),
|
||||
max(box_a[1], box_b[1]),
|
||||
min(box_a[2], box_b[2]),
|
||||
min(box_a[3], box_b[3])
|
||||
)
|
||||
|
||||
def area(box):
|
||||
return (box[2]-box[0] + 1)*(box[3]-box[1] + 1)
|
||||
|
||||
def intersection_over_union(box_a, box_b):
|
||||
# determine the (x, y)-coordinates of the intersection rectangle
|
||||
intersect = intersection(box_a, box_b)
|
||||
|
||||
# compute the area of intersection rectangle
|
||||
inter_area = max(0, intersect[2] - intersect[0] + 1) * max(0, intersect[3] - intersect[1] + 1)
|
||||
|
||||
if inter_area == 0:
|
||||
return 0.0
|
||||
|
||||
# compute the area of both the prediction and ground-truth
|
||||
# rectangles
|
||||
box_a_area = (box_a[2] - box_a[0] + 1) * (box_a[3] - box_a[1] + 1)
|
||||
box_b_area = (box_b[2] - box_b[0] + 1) * (box_b[3] - box_b[1] + 1)
|
||||
|
||||
# compute the intersection over union by taking the intersection
|
||||
# area and dividing it by the sum of prediction + ground-truth
|
||||
# areas - the interesection area
|
||||
iou = inter_area / float(box_a_area + box_b_area - inter_area)
|
||||
|
||||
# return the intersection over union value
|
||||
return iou
|
||||
|
||||
def clipped(obj, frame_shape):
|
||||
# if the object is within 5 pixels of the region border, and the region is not on the edge
|
||||
# consider the object to be clipped
|
||||
box = obj[2]
|
||||
region = obj[4]
|
||||
if ((region[0] > 5 and box[0]-region[0] <= 5) or
|
||||
(region[1] > 5 and box[1]-region[1] <= 5) or
|
||||
(frame_shape[1]-region[2] > 5 and region[2]-box[2] <= 5) or
|
||||
(frame_shape[0]-region[3] > 5 and region[3]-box[3] <= 5)):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
class EventsPerSecond:
|
||||
def __init__(self, max_events=1000):
|
||||
self._start = None
|
||||
self._max_events = max_events
|
||||
self._timestamps = []
|
||||
|
||||
def start(self):
|
||||
self._start = datetime.datetime.now().timestamp()
|
||||
|
||||
def update(self):
|
||||
if self._start is None:
|
||||
self.start()
|
||||
self._timestamps.append(datetime.datetime.now().timestamp())
|
||||
# truncate the list when it goes 100 over the max_size
|
||||
if len(self._timestamps) > self._max_events+100:
|
||||
self._timestamps = self._timestamps[(1-self._max_events):]
|
||||
|
||||
def eps(self, last_n_seconds=10):
|
||||
if self._start is None:
|
||||
self.start()
|
||||
# compute the (approximate) events in the last n seconds
|
||||
now = datetime.datetime.now().timestamp()
|
||||
seconds = min(now-self._start, last_n_seconds)
|
||||
return len([t for t in self._timestamps if t > (now-last_n_seconds)]) / seconds
|
||||
|
||||
def print_stack(sig, frame):
|
||||
traceback.print_stack(frame)
|
||||
|
||||
def listen():
|
||||
signal.signal(signal.SIGUSR1, print_stack)
|
||||
|
||||
class FrameManager(ABC):
|
||||
@abstractmethod
|
||||
def create(self, name, size) -> AnyStr:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get(self, name, timeout_ms=0):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def close(self, name):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, name):
|
||||
pass
|
||||
|
||||
class DictFrameManager(FrameManager):
|
||||
def __init__(self):
|
||||
self.frames = {}
|
||||
|
||||
def create(self, name, size) -> AnyStr:
|
||||
mem = bytearray(size)
|
||||
self.frames[name] = mem
|
||||
return mem
|
||||
|
||||
def get(self, name, shape):
|
||||
mem = self.frames[name]
|
||||
return np.ndarray(shape, dtype=np.uint8, buffer=mem)
|
||||
|
||||
def close(self, name):
|
||||
pass
|
||||
|
||||
def delete(self, name):
|
||||
del self.frames[name]
|
||||
|
||||
class SharedMemoryFrameManager(FrameManager):
|
||||
def __init__(self):
|
||||
self.shm_store = {}
|
||||
|
||||
def create(self, name, size) -> AnyStr:
|
||||
shm = shared_memory.SharedMemory(name=name, create=True, size=size)
|
||||
self.shm_store[name] = shm
|
||||
return shm.buf
|
||||
|
||||
def get(self, name, shape):
|
||||
if name in self.shm_store:
|
||||
shm = self.shm_store[name]
|
||||
else:
|
||||
shm = shared_memory.SharedMemory(name=name)
|
||||
self.shm_store[name] = shm
|
||||
return np.ndarray(shape, dtype=np.uint8, buffer=shm.buf)
|
||||
|
||||
def close(self, name):
|
||||
if name in self.shm_store:
|
||||
self.shm_store[name].close()
|
||||
del self.shm_store[name]
|
||||
|
||||
def delete(self, name):
|
||||
if name in self.shm_store:
|
||||
self.shm_store[name].close()
|
||||
self.shm_store[name].unlink()
|
||||
del self.shm_store[name]
|
||||
|
||||
622
frigate/video.py
Normal file → Executable file
@@ -1,268 +1,418 @@
|
||||
import os
|
||||
import time
|
||||
import datetime
|
||||
import cv2
|
||||
import threading
|
||||
import base64
|
||||
import copy
|
||||
import ctypes
|
||||
import datetime
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing as mp
|
||||
from object_detection.utils import visualization_utils as vis_util
|
||||
from . util import tonumpyarray
|
||||
from . object_detection import FramePrepper
|
||||
from . objects import ObjectCleaner, BestPersonFrame
|
||||
from . mqtt import MqttObjectPublisher
|
||||
import os
|
||||
import queue
|
||||
import subprocess as sp
|
||||
import signal
|
||||
import threading
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from typing import Dict, List
|
||||
|
||||
# fetch the frames as fast a possible and store current frame in a shared memory array
|
||||
def fetch_frames(shared_arr, shared_frame_time, frame_lock, frame_ready, frame_shape, rtsp_url):
|
||||
# convert shared memory array into numpy and shape into image array
|
||||
arr = tonumpyarray(shared_arr).reshape(frame_shape)
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
# start the video capture
|
||||
video = cv2.VideoCapture()
|
||||
video.open(rtsp_url)
|
||||
# keep the buffer small so we minimize old data
|
||||
video.set(cv2.CAP_PROP_BUFFERSIZE,1)
|
||||
from frigate.config import CameraConfig
|
||||
from frigate.edgetpu import RemoteObjectDetector
|
||||
from frigate.log import LogPipe
|
||||
from frigate.motion import MotionDetector
|
||||
from frigate.objects import ObjectTracker
|
||||
from frigate.util import (EventsPerSecond, FrameManager,
|
||||
SharedMemoryFrameManager, area, calculate_region,
|
||||
clipped, draw_box_with_label, intersection,
|
||||
intersection_over_union, listen, yuv_region_2_rgb)
|
||||
|
||||
bad_frame_counter = 0
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def filtered(obj, objects_to_track, object_filters, mask=None):
|
||||
object_name = obj[0]
|
||||
|
||||
if not object_name in objects_to_track:
|
||||
return True
|
||||
|
||||
if object_name in object_filters:
|
||||
obj_settings = object_filters[object_name]
|
||||
|
||||
# if the min area is larger than the
|
||||
# detected object, don't add it to detected objects
|
||||
if obj_settings.min_area > obj[3]:
|
||||
return True
|
||||
|
||||
# if the detected object is larger than the
|
||||
# max area, don't add it to detected objects
|
||||
if obj_settings.max_area < obj[3]:
|
||||
return True
|
||||
|
||||
# if the score is lower than the min_score, skip
|
||||
if obj_settings.min_score > obj[1]:
|
||||
return True
|
||||
|
||||
# compute the coordinates of the object and make sure
|
||||
# the location isnt outside the bounds of the image (can happen from rounding)
|
||||
y_location = min(int(obj[2][3]), len(mask)-1)
|
||||
x_location = min(int((obj[2][2]-obj[2][0])/2.0)+obj[2][0], len(mask[0])-1)
|
||||
|
||||
# if the object is in a masked location, don't add it to detected objects
|
||||
if (not mask is None) and (mask[y_location][x_location] == 0):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def create_tensor_input(frame, model_shape, region):
|
||||
cropped_frame = yuv_region_2_rgb(frame, region)
|
||||
|
||||
# Resize to 300x300 if needed
|
||||
if cropped_frame.shape != (model_shape[0], model_shape[1], 3):
|
||||
cropped_frame = cv2.resize(cropped_frame, dsize=model_shape, interpolation=cv2.INTER_LINEAR)
|
||||
|
||||
# Expand dimensions since the model expects images to have shape: [1, height, width, 3]
|
||||
return np.expand_dims(cropped_frame, axis=0)
|
||||
|
||||
def stop_ffmpeg(ffmpeg_process, logger):
|
||||
logger.info("Terminating the existing ffmpeg process...")
|
||||
ffmpeg_process.terminate()
|
||||
try:
|
||||
logger.info("Waiting for ffmpeg to exit gracefully...")
|
||||
ffmpeg_process.communicate(timeout=30)
|
||||
except sp.TimeoutExpired:
|
||||
logger.info("FFmpeg didnt exit. Force killing...")
|
||||
ffmpeg_process.kill()
|
||||
ffmpeg_process.communicate()
|
||||
ffmpeg_process = None
|
||||
|
||||
def start_or_restart_ffmpeg(ffmpeg_cmd, logger, logpipe: LogPipe, frame_size=None, ffmpeg_process=None):
|
||||
if not ffmpeg_process is None:
|
||||
stop_ffmpeg(ffmpeg_process, logger)
|
||||
|
||||
if frame_size is None:
|
||||
process = sp.Popen(ffmpeg_cmd, stdout = sp.DEVNULL, stderr=logpipe, stdin = sp.DEVNULL, start_new_session=True)
|
||||
else:
|
||||
process = sp.Popen(ffmpeg_cmd, stdout = sp.PIPE, stderr=logpipe, stdin = sp.DEVNULL, bufsize=frame_size*10, start_new_session=True)
|
||||
return process
|
||||
|
||||
def capture_frames(ffmpeg_process, camera_name, frame_shape, frame_manager: FrameManager,
|
||||
frame_queue, fps:mp.Value, skipped_fps: mp.Value, current_frame: mp.Value):
|
||||
|
||||
frame_size = frame_shape[0] * frame_shape[1]
|
||||
frame_rate = EventsPerSecond()
|
||||
frame_rate.start()
|
||||
skipped_eps = EventsPerSecond()
|
||||
skipped_eps.start()
|
||||
while True:
|
||||
# check if the video stream is still open, and reopen if needed
|
||||
if not video.isOpened():
|
||||
success = video.open(rtsp_url)
|
||||
if not success:
|
||||
time.sleep(1)
|
||||
fps.value = frame_rate.eps()
|
||||
skipped_fps = skipped_eps.eps()
|
||||
|
||||
current_frame.value = datetime.datetime.now().timestamp()
|
||||
frame_name = f"{camera_name}{current_frame.value}"
|
||||
frame_buffer = frame_manager.create(frame_name, frame_size)
|
||||
try:
|
||||
frame_buffer[:] = ffmpeg_process.stdout.read(frame_size)
|
||||
except Exception as e:
|
||||
logger.info(f"{camera_name}: ffmpeg sent a broken frame. {e}")
|
||||
|
||||
if ffmpeg_process.poll() != None:
|
||||
logger.info(f"{camera_name}: ffmpeg process is not running. exiting capture thread...")
|
||||
frame_manager.delete(frame_name)
|
||||
break
|
||||
continue
|
||||
# grab the frame, but dont decode it yet
|
||||
ret = video.grab()
|
||||
# snapshot the time the frame was grabbed
|
||||
frame_time = datetime.datetime.now()
|
||||
if ret:
|
||||
# go ahead and decode the current frame
|
||||
ret, frame = video.retrieve()
|
||||
if ret:
|
||||
# Lock access and update frame
|
||||
with frame_lock:
|
||||
arr[:] = frame
|
||||
shared_frame_time.value = frame_time.timestamp()
|
||||
# Notify with the condition that a new frame is ready
|
||||
with frame_ready:
|
||||
frame_ready.notify_all()
|
||||
bad_frame_counter = 0
|
||||
else:
|
||||
print("Unable to decode frame")
|
||||
bad_frame_counter += 1
|
||||
else:
|
||||
print("Unable to grab a frame")
|
||||
bad_frame_counter += 1
|
||||
|
||||
if bad_frame_counter > 100:
|
||||
video.release()
|
||||
frame_rate.update()
|
||||
|
||||
video.release()
|
||||
# if the queue is full, skip this frame
|
||||
if frame_queue.full():
|
||||
skipped_eps.update()
|
||||
frame_manager.delete(frame_name)
|
||||
continue
|
||||
|
||||
# Stores 2 seconds worth of frames when motion is detected so they can be used for other threads
|
||||
class FrameTracker(threading.Thread):
|
||||
def __init__(self, shared_frame, frame_time, frame_ready, frame_lock, recent_frames):
|
||||
# close the frame
|
||||
frame_manager.close(frame_name)
|
||||
|
||||
# add to the queue
|
||||
frame_queue.put(current_frame.value)
|
||||
|
||||
class CameraWatchdog(threading.Thread):
|
||||
def __init__(self, camera_name, config, frame_queue, camera_fps, ffmpeg_pid, stop_event):
|
||||
threading.Thread.__init__(self)
|
||||
self.shared_frame = shared_frame
|
||||
self.frame_time = frame_time
|
||||
self.frame_ready = frame_ready
|
||||
self.frame_lock = frame_lock
|
||||
self.recent_frames = recent_frames
|
||||
self.logger = logging.getLogger(f"watchdog.{camera_name}")
|
||||
self.camera_name = camera_name
|
||||
self.config = config
|
||||
self.capture_thread = None
|
||||
self.ffmpeg_detect_process = None
|
||||
self.logpipe = LogPipe(f"ffmpeg.{self.camera_name}.detect", logging.ERROR)
|
||||
self.ffmpeg_other_processes = []
|
||||
self.camera_fps = camera_fps
|
||||
self.ffmpeg_pid = ffmpeg_pid
|
||||
self.frame_queue = frame_queue
|
||||
self.frame_shape = self.config.frame_shape_yuv
|
||||
self.frame_size = self.frame_shape[0] * self.frame_shape[1]
|
||||
self.stop_event = stop_event
|
||||
|
||||
def run(self):
|
||||
frame_time = 0.0
|
||||
self.start_ffmpeg_detect()
|
||||
|
||||
for c in self.config.ffmpeg_cmds:
|
||||
if 'detect' in c['roles']:
|
||||
continue
|
||||
logpipe = LogPipe(f"ffmpeg.{self.camera_name}.{'_'.join(sorted(c['roles']))}", logging.ERROR)
|
||||
self.ffmpeg_other_processes.append({
|
||||
'cmd': c['cmd'],
|
||||
'logpipe': logpipe,
|
||||
'process': start_or_restart_ffmpeg(c['cmd'], self.logger, logpipe)
|
||||
})
|
||||
|
||||
time.sleep(10)
|
||||
while True:
|
||||
now = datetime.datetime.now().timestamp()
|
||||
# wait for a frame
|
||||
with self.frame_ready:
|
||||
# if there isnt a frame ready for processing or it is old, wait for a signal
|
||||
if self.frame_time.value == frame_time or (now - self.frame_time.value) > 0.5:
|
||||
self.frame_ready.wait()
|
||||
|
||||
# lock and make a copy of the frame
|
||||
with self.frame_lock:
|
||||
frame = self.shared_frame.copy()
|
||||
frame_time = self.frame_time.value
|
||||
|
||||
# add the frame to recent frames
|
||||
self.recent_frames[frame_time] = frame
|
||||
|
||||
# delete any old frames
|
||||
stored_frame_times = list(self.recent_frames.keys())
|
||||
for k in stored_frame_times:
|
||||
if (now - k) > 2:
|
||||
del self.recent_frames[k]
|
||||
|
||||
def get_frame_shape(rtsp_url):
|
||||
# capture a single frame and check the frame shape so the correct array
|
||||
# size can be allocated in memory
|
||||
video = cv2.VideoCapture(rtsp_url)
|
||||
ret, frame = video.read()
|
||||
frame_shape = frame.shape
|
||||
video.release()
|
||||
return frame_shape
|
||||
|
||||
def get_rtsp_url(rtsp_config):
|
||||
if (rtsp_config['password'].startswith('$')):
|
||||
rtsp_config['password'] = os.getenv(rtsp_config['password'][1:])
|
||||
return 'rtsp://{}:{}@{}:{}{}'.format(rtsp_config['user'],
|
||||
rtsp_config['password'], rtsp_config['host'], rtsp_config['port'],
|
||||
rtsp_config['path'])
|
||||
|
||||
class Camera:
|
||||
def __init__(self, name, config, prepped_frame_queue, mqtt_client, mqtt_prefix):
|
||||
self.name = name
|
||||
self.config = config
|
||||
self.detected_objects = []
|
||||
self.recent_frames = {}
|
||||
self.rtsp_url = get_rtsp_url(self.config['rtsp'])
|
||||
self.regions = self.config['regions']
|
||||
self.frame_shape = get_frame_shape(self.rtsp_url)
|
||||
self.mqtt_client = mqtt_client
|
||||
self.mqtt_topic_prefix = '{}/{}'.format(mqtt_prefix, self.name)
|
||||
|
||||
# compute the flattened array length from the shape of the frame
|
||||
flat_array_length = self.frame_shape[0] * self.frame_shape[1] * self.frame_shape[2]
|
||||
# create shared array for storing the full frame image data
|
||||
self.shared_frame_array = mp.Array(ctypes.c_uint8, flat_array_length)
|
||||
# create shared value for storing the frame_time
|
||||
self.shared_frame_time = mp.Value('d', 0.0)
|
||||
# Lock to control access to the frame
|
||||
self.frame_lock = mp.Lock()
|
||||
# Condition for notifying that a new frame is ready
|
||||
self.frame_ready = mp.Condition()
|
||||
# Condition for notifying that objects were parsed
|
||||
self.objects_parsed = mp.Condition()
|
||||
|
||||
# shape current frame so it can be treated as a numpy image
|
||||
self.shared_frame_np = tonumpyarray(self.shared_frame_array).reshape(self.frame_shape)
|
||||
|
||||
# create the process to capture frames from the RTSP stream and store in a shared array
|
||||
self.capture_process = mp.Process(target=fetch_frames, args=(self.shared_frame_array,
|
||||
self.shared_frame_time, self.frame_lock, self.frame_ready, self.frame_shape, self.rtsp_url))
|
||||
self.capture_process.daemon = True
|
||||
|
||||
# for each region, create a separate thread to resize the region and prep for detection
|
||||
self.detection_prep_threads = []
|
||||
for region in self.config['regions']:
|
||||
self.detection_prep_threads.append(FramePrepper(
|
||||
self.name,
|
||||
self.shared_frame_np,
|
||||
self.shared_frame_time,
|
||||
self.frame_ready,
|
||||
self.frame_lock,
|
||||
region['size'], region['x_offset'], region['y_offset'],
|
||||
prepped_frame_queue
|
||||
))
|
||||
|
||||
# start a thread to store recent motion frames for processing
|
||||
self.frame_tracker = FrameTracker(self.shared_frame_np, self.shared_frame_time,
|
||||
self.frame_ready, self.frame_lock, self.recent_frames)
|
||||
self.frame_tracker.start()
|
||||
|
||||
# start a thread to store the highest scoring recent person frame
|
||||
self.best_person_frame = BestPersonFrame(self.objects_parsed, self.recent_frames, self.detected_objects)
|
||||
self.best_person_frame.start()
|
||||
|
||||
# start a thread to expire objects from the detected objects list
|
||||
self.object_cleaner = ObjectCleaner(self.objects_parsed, self.detected_objects)
|
||||
self.object_cleaner.start()
|
||||
|
||||
# start a thread to publish object scores (currently only person)
|
||||
mqtt_publisher = MqttObjectPublisher(self.mqtt_client, self.mqtt_topic_prefix, self.objects_parsed, self.detected_objects)
|
||||
mqtt_publisher.start()
|
||||
|
||||
# load in the mask for person detection
|
||||
if 'mask' in self.config:
|
||||
self.mask = cv2.imread("/config/{}".format(self.config['mask']), cv2.IMREAD_GRAYSCALE)
|
||||
else:
|
||||
self.mask = np.zeros((self.frame_shape[0], self.frame_shape[1], 1), np.uint8)
|
||||
self.mask[:] = 255
|
||||
|
||||
def start(self):
|
||||
self.capture_process.start()
|
||||
# start the object detection prep threads
|
||||
for detection_prep_thread in self.detection_prep_threads:
|
||||
detection_prep_thread.start()
|
||||
|
||||
def join(self):
|
||||
self.capture_process.join()
|
||||
|
||||
def get_capture_pid(self):
|
||||
return self.capture_process.pid
|
||||
|
||||
def add_objects(self, objects):
|
||||
if len(objects) == 0:
|
||||
return
|
||||
|
||||
for obj in objects:
|
||||
if obj['name'] == 'person':
|
||||
person_area = (obj['xmax']-obj['xmin'])*(obj['ymax']-obj['ymin'])
|
||||
# find the matching region
|
||||
region = None
|
||||
for r in self.regions:
|
||||
if (
|
||||
obj['xmin'] >= r['x_offset'] and
|
||||
obj['ymin'] >= r['y_offset'] and
|
||||
obj['xmax'] <= r['x_offset']+r['size'] and
|
||||
obj['ymax'] <= r['y_offset']+r['size']
|
||||
):
|
||||
region = r
|
||||
if self.stop_event.is_set():
|
||||
stop_ffmpeg(self.ffmpeg_detect_process, self.logger)
|
||||
for p in self.ffmpeg_other_processes:
|
||||
stop_ffmpeg(p['process'], self.logger)
|
||||
p['logpipe'].close()
|
||||
self.logpipe.close()
|
||||
break
|
||||
|
||||
# if the min person area is larger than the
|
||||
# detected person, don't add it to detected objects
|
||||
if region and region['min_person_area'] > person_area:
|
||||
now = datetime.datetime.now().timestamp()
|
||||
|
||||
if not self.capture_thread.is_alive():
|
||||
self.start_ffmpeg_detect()
|
||||
elif now - self.capture_thread.current_frame.value > 20:
|
||||
self.logger.info(f"No frames received from {self.camera_name} in 20 seconds. Exiting ffmpeg...")
|
||||
self.ffmpeg_detect_process.terminate()
|
||||
try:
|
||||
self.logger.info("Waiting for ffmpeg to exit gracefully...")
|
||||
self.ffmpeg_detect_process.communicate(timeout=30)
|
||||
except sp.TimeoutExpired:
|
||||
self.logger.info("FFmpeg didnt exit. Force killing...")
|
||||
self.ffmpeg_detect_process.kill()
|
||||
self.ffmpeg_detect_process.communicate()
|
||||
|
||||
for p in self.ffmpeg_other_processes:
|
||||
poll = p['process'].poll()
|
||||
if poll == None:
|
||||
continue
|
||||
p['process'] = start_or_restart_ffmpeg(p['cmd'], self.logger, p['logpipe'], ffmpeg_process=p['process'])
|
||||
|
||||
# wait a bit before checking again
|
||||
time.sleep(10)
|
||||
|
||||
def start_ffmpeg_detect(self):
|
||||
ffmpeg_cmd = [c['cmd'] for c in self.config.ffmpeg_cmds if 'detect' in c['roles']][0]
|
||||
self.ffmpeg_detect_process = start_or_restart_ffmpeg(ffmpeg_cmd, self.logger, self.logpipe, self.frame_size)
|
||||
self.ffmpeg_pid.value = self.ffmpeg_detect_process.pid
|
||||
self.capture_thread = CameraCapture(self.camera_name, self.ffmpeg_detect_process, self.frame_shape, self.frame_queue,
|
||||
self.camera_fps)
|
||||
self.capture_thread.start()
|
||||
|
||||
class CameraCapture(threading.Thread):
|
||||
def __init__(self, camera_name, ffmpeg_process, frame_shape, frame_queue, fps):
|
||||
threading.Thread.__init__(self)
|
||||
self.name = f"capture:{camera_name}"
|
||||
self.camera_name = camera_name
|
||||
self.frame_shape = frame_shape
|
||||
self.frame_queue = frame_queue
|
||||
self.fps = fps
|
||||
self.skipped_fps = EventsPerSecond()
|
||||
self.frame_manager = SharedMemoryFrameManager()
|
||||
self.ffmpeg_process = ffmpeg_process
|
||||
self.current_frame = mp.Value('d', 0.0)
|
||||
self.last_frame = 0
|
||||
|
||||
def run(self):
|
||||
self.skipped_fps.start()
|
||||
capture_frames(self.ffmpeg_process, self.camera_name, self.frame_shape, self.frame_manager, self.frame_queue,
|
||||
self.fps, self.skipped_fps, self.current_frame)
|
||||
|
||||
def capture_camera(name, config: CameraConfig, process_info):
|
||||
stop_event = mp.Event()
|
||||
def receiveSignal(signalNumber, frame):
|
||||
stop_event.set()
|
||||
|
||||
signal.signal(signal.SIGTERM, receiveSignal)
|
||||
signal.signal(signal.SIGINT, receiveSignal)
|
||||
|
||||
frame_queue = process_info['frame_queue']
|
||||
camera_watchdog = CameraWatchdog(name, config, frame_queue, process_info['camera_fps'], process_info['ffmpeg_pid'], stop_event)
|
||||
camera_watchdog.start()
|
||||
camera_watchdog.join()
|
||||
|
||||
def track_camera(name, config: CameraConfig, model_shape, detection_queue, result_connection, detected_objects_queue, process_info):
|
||||
stop_event = mp.Event()
|
||||
def receiveSignal(signalNumber, frame):
|
||||
stop_event.set()
|
||||
|
||||
signal.signal(signal.SIGTERM, receiveSignal)
|
||||
signal.signal(signal.SIGINT, receiveSignal)
|
||||
|
||||
threading.current_thread().name = f"process:{name}"
|
||||
listen()
|
||||
|
||||
frame_queue = process_info['frame_queue']
|
||||
|
||||
frame_shape = config.frame_shape
|
||||
objects_to_track = config.objects.track
|
||||
object_filters = config.objects.filters
|
||||
mask = config.mask
|
||||
|
||||
motion_detector = MotionDetector(frame_shape, mask, config.motion)
|
||||
object_detector = RemoteObjectDetector(name, '/labelmap.txt', detection_queue, result_connection, model_shape)
|
||||
|
||||
object_tracker = ObjectTracker(config.detect)
|
||||
|
||||
frame_manager = SharedMemoryFrameManager()
|
||||
|
||||
process_frames(name, frame_queue, frame_shape, model_shape, frame_manager, motion_detector, object_detector,
|
||||
object_tracker, detected_objects_queue, process_info, objects_to_track, object_filters, mask, stop_event)
|
||||
|
||||
logger.info(f"{name}: exiting subprocess")
|
||||
|
||||
def reduce_boxes(boxes):
|
||||
if len(boxes) == 0:
|
||||
return []
|
||||
reduced_boxes = cv2.groupRectangles([list(b) for b in itertools.chain(boxes, boxes)], 1, 0.2)[0]
|
||||
return [tuple(b) for b in reduced_boxes]
|
||||
|
||||
def detect(object_detector, frame, model_shape, region, objects_to_track, object_filters, mask):
|
||||
tensor_input = create_tensor_input(frame, model_shape, region)
|
||||
|
||||
detections = []
|
||||
region_detections = object_detector.detect(tensor_input)
|
||||
for d in region_detections:
|
||||
box = d[2]
|
||||
size = region[2]-region[0]
|
||||
x_min = int((box[1] * size) + region[0])
|
||||
y_min = int((box[0] * size) + region[1])
|
||||
x_max = int((box[3] * size) + region[0])
|
||||
y_max = int((box[2] * size) + region[1])
|
||||
det = (d[0],
|
||||
d[1],
|
||||
(x_min, y_min, x_max, y_max),
|
||||
(x_max-x_min)*(y_max-y_min),
|
||||
region)
|
||||
# apply object filters
|
||||
if filtered(det, objects_to_track, object_filters, mask):
|
||||
continue
|
||||
detections.append(det)
|
||||
return detections
|
||||
|
||||
def process_frames(camera_name: str, frame_queue: mp.Queue, frame_shape, model_shape,
|
||||
frame_manager: FrameManager, motion_detector: MotionDetector,
|
||||
object_detector: RemoteObjectDetector, object_tracker: ObjectTracker,
|
||||
detected_objects_queue: mp.Queue, process_info: Dict,
|
||||
objects_to_track: List[str], object_filters, mask, stop_event,
|
||||
exit_on_empty: bool = False):
|
||||
|
||||
fps = process_info['process_fps']
|
||||
detection_fps = process_info['detection_fps']
|
||||
current_frame_time = process_info['detection_frame']
|
||||
|
||||
fps_tracker = EventsPerSecond()
|
||||
fps_tracker.start()
|
||||
|
||||
while True:
|
||||
if stop_event.is_set():
|
||||
break
|
||||
|
||||
if exit_on_empty and frame_queue.empty():
|
||||
logger.info(f"Exiting track_objects...")
|
||||
break
|
||||
|
||||
try:
|
||||
frame_time = frame_queue.get(True, 10)
|
||||
except queue.Empty:
|
||||
continue
|
||||
|
||||
# compute the coordinates of the person and make sure
|
||||
# the location isnt outide the bounds of the image (can happen from rounding)
|
||||
y_location = min(int(obj['ymax']), len(self.mask)-1)
|
||||
x_location = min(int((obj['xmax']-obj['xmin'])/2.0), len(self.mask[0])-1)
|
||||
current_frame_time.value = frame_time
|
||||
|
||||
# if the person is in a masked location, continue
|
||||
if self.mask[y_location][x_location] == [0]:
|
||||
frame = frame_manager.get(f"{camera_name}{frame_time}", (frame_shape[0]*3//2, frame_shape[1]))
|
||||
|
||||
if frame is None:
|
||||
logger.info(f"{camera_name}: frame {frame_time} is not in memory store.")
|
||||
continue
|
||||
|
||||
self.detected_objects.append(obj)
|
||||
# look for motion
|
||||
motion_boxes = motion_detector.detect(frame)
|
||||
|
||||
with self.objects_parsed:
|
||||
self.objects_parsed.notify_all()
|
||||
tracked_object_boxes = [obj['box'] for obj in object_tracker.tracked_objects.values()]
|
||||
|
||||
def get_best_person(self):
|
||||
return self.best_person_frame.best_frame
|
||||
# combine motion boxes with known locations of existing objects
|
||||
combined_boxes = reduce_boxes(motion_boxes + tracked_object_boxes)
|
||||
|
||||
def get_current_frame_with_objects(self):
|
||||
# make a copy of the current detected objects
|
||||
detected_objects = self.detected_objects.copy()
|
||||
# lock and make a copy of the current frame
|
||||
with self.frame_lock:
|
||||
frame = self.shared_frame_np.copy()
|
||||
# compute regions
|
||||
regions = [calculate_region(frame_shape, a[0], a[1], a[2], a[3], 1.2)
|
||||
for a in combined_boxes]
|
||||
|
||||
# convert to RGB for drawing
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
# draw the bounding boxes on the screen
|
||||
for obj in detected_objects:
|
||||
vis_util.draw_bounding_box_on_image_array(frame,
|
||||
obj['ymin'],
|
||||
obj['xmin'],
|
||||
obj['ymax'],
|
||||
obj['xmax'],
|
||||
color='red',
|
||||
thickness=2,
|
||||
display_str_list=["{}: {}%".format(obj['name'],int(obj['score']*100))],
|
||||
use_normalized_coordinates=False)
|
||||
# combine overlapping regions
|
||||
combined_regions = reduce_boxes(regions)
|
||||
|
||||
for region in self.regions:
|
||||
color = (255,255,255)
|
||||
cv2.rectangle(frame, (region['x_offset'], region['y_offset']),
|
||||
(region['x_offset']+region['size'], region['y_offset']+region['size']),
|
||||
color, 2)
|
||||
# re-compute regions
|
||||
regions = [calculate_region(frame_shape, a[0], a[1], a[2], a[3], 1.0)
|
||||
for a in combined_regions]
|
||||
|
||||
# convert back to BGR
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
|
||||
# resize regions and detect
|
||||
detections = []
|
||||
for region in regions:
|
||||
detections.extend(detect(object_detector, frame, model_shape, region, objects_to_track, object_filters, mask))
|
||||
|
||||
return frame
|
||||
#########
|
||||
# merge objects, check for clipped objects and look again up to 4 times
|
||||
#########
|
||||
refining = True
|
||||
refine_count = 0
|
||||
while refining and refine_count < 4:
|
||||
refining = False
|
||||
|
||||
# group by name
|
||||
detected_object_groups = defaultdict(lambda: [])
|
||||
for detection in detections:
|
||||
detected_object_groups[detection[0]].append(detection)
|
||||
|
||||
selected_objects = []
|
||||
for group in detected_object_groups.values():
|
||||
|
||||
# apply non-maxima suppression to suppress weak, overlapping bounding boxes
|
||||
boxes = [(o[2][0], o[2][1], o[2][2]-o[2][0], o[2][3]-o[2][1])
|
||||
for o in group]
|
||||
confidences = [o[1] for o in group]
|
||||
idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)
|
||||
|
||||
for index in idxs:
|
||||
obj = group[index[0]]
|
||||
if clipped(obj, frame_shape):
|
||||
box = obj[2]
|
||||
# calculate a new region that will hopefully get the entire object
|
||||
region = calculate_region(frame_shape,
|
||||
box[0], box[1],
|
||||
box[2], box[3])
|
||||
|
||||
regions.append(region)
|
||||
|
||||
selected_objects.extend(detect(object_detector, frame, model_shape, region, objects_to_track, object_filters, mask))
|
||||
|
||||
refining = True
|
||||
else:
|
||||
selected_objects.append(obj)
|
||||
# set the detections list to only include top, complete objects
|
||||
# and new detections
|
||||
detections = selected_objects
|
||||
|
||||
if refining:
|
||||
refine_count += 1
|
||||
|
||||
# now that we have refined our detections, we need to track objects
|
||||
object_tracker.match_and_update(frame_time, detections)
|
||||
|
||||
# add to the queue if not full
|
||||
if(detected_objects_queue.full()):
|
||||
frame_manager.delete(f"{camera_name}{frame_time}")
|
||||
continue
|
||||
else:
|
||||
fps_tracker.update()
|
||||
fps.value = fps_tracker.eps()
|
||||
detected_objects_queue.put((camera_name, frame_time, object_tracker.tracked_objects, motion_boxes, regions))
|
||||
detection_fps.value = object_detector.fps.eps()
|
||||
frame_manager.close(f"{camera_name}{frame_time}")
|
||||
|
||||
36
frigate/watchdog.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import datetime
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class FrigateWatchdog(threading.Thread):
|
||||
def __init__(self, detectors, stop_event):
|
||||
threading.Thread.__init__(self)
|
||||
self.name = 'frigate_watchdog'
|
||||
self.detectors = detectors
|
||||
self.stop_event = stop_event
|
||||
|
||||
def run(self):
|
||||
time.sleep(10)
|
||||
while True:
|
||||
# wait a bit before checking
|
||||
time.sleep(10)
|
||||
|
||||
if self.stop_event.is_set():
|
||||
logger.info(f"Exiting watchdog...")
|
||||
break
|
||||
|
||||
now = datetime.datetime.now().timestamp()
|
||||
|
||||
# check the detection processes
|
||||
for detector in self.detectors.values():
|
||||
detection_start = detector.detection_start.value
|
||||
if (detection_start > 0.0 and
|
||||
now - detection_start > 10):
|
||||
logger.info("Detection appears to be stuck. Restarting detection process")
|
||||
detector.start_or_restart()
|
||||
elif not detector.detect_process.is_alive():
|
||||
logger.info("Detection appears to have stopped. Restarting detection process")
|
||||
detector.start_or_restart()
|
||||
58
frigate/zeroconf.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import logging
|
||||
import socket
|
||||
|
||||
from zeroconf import (
|
||||
ServiceInfo,
|
||||
NonUniqueNameException,
|
||||
InterfaceChoice,
|
||||
IPVersion,
|
||||
Zeroconf,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ZEROCONF_TYPE = "_frigate._tcp.local."
|
||||
|
||||
# Taken from: http://stackoverflow.com/a/11735897
|
||||
def get_local_ip() -> str:
|
||||
"""Try to determine the local IP address of the machine."""
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
|
||||
# Use Google Public DNS server to determine own IP
|
||||
sock.connect(("8.8.8.8", 80))
|
||||
|
||||
return sock.getsockname()[0] # type: ignore
|
||||
except OSError:
|
||||
try:
|
||||
return socket.gethostbyname(socket.gethostname())
|
||||
except socket.gaierror:
|
||||
return "127.0.0.1"
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
def broadcast_zeroconf(frigate_id):
|
||||
zeroconf = Zeroconf(interfaces=InterfaceChoice.Default, ip_version=IPVersion.V4Only)
|
||||
|
||||
host_ip = get_local_ip()
|
||||
|
||||
try:
|
||||
host_ip_pton = socket.inet_pton(socket.AF_INET, host_ip)
|
||||
except OSError:
|
||||
host_ip_pton = socket.inet_pton(socket.AF_INET6, host_ip)
|
||||
|
||||
info = ServiceInfo(
|
||||
ZEROCONF_TYPE,
|
||||
name=f"{frigate_id}.{ZEROCONF_TYPE}",
|
||||
addresses=[host_ip_pton],
|
||||
port=5000,
|
||||
)
|
||||
|
||||
logger.info("Starting Zeroconf broadcast")
|
||||
try:
|
||||
zeroconf.register_service(info)
|
||||
except NonUniqueNameException:
|
||||
logger.error(
|
||||
"Frigate instance with identical name present in the local network"
|
||||
)
|
||||
return zeroconf
|
||||
80
labelmap.txt
Normal file
@@ -0,0 +1,80 @@
|
||||
0 person
|
||||
1 bicycle
|
||||
2 car
|
||||
3 motorcycle
|
||||
4 airplane
|
||||
5 bus
|
||||
6 train
|
||||
7 car
|
||||
8 boat
|
||||
9 traffic light
|
||||
10 fire hydrant
|
||||
12 stop sign
|
||||
13 parking meter
|
||||
14 bench
|
||||
15 bird
|
||||
16 cat
|
||||
17 dog
|
||||
18 horse
|
||||
19 sheep
|
||||
20 cow
|
||||
21 elephant
|
||||
22 bear
|
||||
23 zebra
|
||||
24 giraffe
|
||||
26 backpack
|
||||
27 umbrella
|
||||
30 handbag
|
||||
31 tie
|
||||
32 suitcase
|
||||
33 frisbee
|
||||
34 skis
|
||||
35 snowboard
|
||||
36 sports ball
|
||||
37 kite
|
||||
38 baseball bat
|
||||
39 baseball glove
|
||||
40 skateboard
|
||||
41 surfboard
|
||||
42 tennis racket
|
||||
43 bottle
|
||||
45 wine glass
|
||||
46 cup
|
||||
47 fork
|
||||
48 knife
|
||||
49 spoon
|
||||
50 bowl
|
||||
51 banana
|
||||
52 apple
|
||||
53 sandwich
|
||||
54 orange
|
||||
55 broccoli
|
||||
56 carrot
|
||||
57 hot dog
|
||||
58 pizza
|
||||
59 donut
|
||||
60 cake
|
||||
61 chair
|
||||
62 couch
|
||||
63 potted plant
|
||||
64 bed
|
||||
66 dining table
|
||||
69 toilet
|
||||
71 tv
|
||||
72 laptop
|
||||
73 mouse
|
||||
74 remote
|
||||
75 keyboard
|
||||
76 cell phone
|
||||
77 microwave
|
||||
78 oven
|
||||
79 toaster
|
||||
80 sink
|
||||
81 refrigerator
|
||||
83 book
|
||||
84 clock
|
||||
85 vase
|
||||
86 scissors
|
||||
87 teddy bear
|
||||
88 hair drier
|
||||
89 toothbrush
|
||||
122
nginx/nginx.conf
Normal file
@@ -0,0 +1,122 @@
|
||||
worker_processes 1;
|
||||
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
load_module "modules/ngx_rtmp_module.so";
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
upstream frigate_api {
|
||||
server localhost:5001;
|
||||
keepalive 1024;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 5000;
|
||||
|
||||
location /stream/ {
|
||||
add_header 'Cache-Control' 'no-cache';
|
||||
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
|
||||
add_header 'Access-Control-Allow-Credentials' 'true';
|
||||
add_header 'Access-Control-Expose-Headers' 'Content-Length';
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||
add_header 'Access-Control-Max-Age' 1728000;
|
||||
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||
add_header 'Content-Length' 0;
|
||||
return 204;
|
||||
}
|
||||
|
||||
types {
|
||||
application/dash+xml mpd;
|
||||
application/vnd.apple.mpegurl m3u8;
|
||||
video/mp2t ts;
|
||||
image/jpeg jpg;
|
||||
}
|
||||
|
||||
root /tmp;
|
||||
}
|
||||
|
||||
location /clips/ {
|
||||
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
|
||||
add_header 'Access-Control-Allow-Credentials' 'true';
|
||||
add_header 'Access-Control-Expose-Headers' 'Content-Length';
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||
add_header 'Access-Control-Max-Age' 1728000;
|
||||
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||
add_header 'Content-Length' 0;
|
||||
return 204;
|
||||
}
|
||||
|
||||
types {
|
||||
video/mp4 mp4;
|
||||
image/jpeg jpg;
|
||||
}
|
||||
|
||||
autoindex on;
|
||||
root /media/frigate;
|
||||
}
|
||||
|
||||
location /recordings/ {
|
||||
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
|
||||
add_header 'Access-Control-Allow-Credentials' 'true';
|
||||
add_header 'Access-Control-Expose-Headers' 'Content-Length';
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||
add_header 'Access-Control-Max-Age' 1728000;
|
||||
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||
add_header 'Content-Length' 0;
|
||||
return 204;
|
||||
}
|
||||
|
||||
types {
|
||||
video/mp4 mp4;
|
||||
}
|
||||
|
||||
autoindex on;
|
||||
autoindex_format json;
|
||||
root /media/frigate;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://frigate_api/;
|
||||
proxy_pass_request_headers on;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rtmp {
|
||||
server {
|
||||
listen 1935;
|
||||
chunk_size 4096;
|
||||
allow publish 127.0.0.1;
|
||||
deny publish all;
|
||||
allow play all;
|
||||
application live {
|
||||
live on;
|
||||
record off;
|
||||
meta copy;
|
||||
}
|
||||
}
|
||||
}
|
||||