├── .github
└── FUNDING.yml
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── README.md
├── defaults
├── default-ssl.conf
├── mysql_defaults.sql
├── mysql_secure_installation.sql
├── opencv_compile.sh
└── zoneminder
├── docker-compose - gpu support.yaml
├── docker-compose.yaml
├── docker-run.sh
├── init
├── 05_set_the_time.sh
├── 06_set_php_time.sh
├── 07_set_dri_permissions.sh
├── 20_apt_update.sh
├── 30_gen_ssl_keys.sh
└── 999_advanced_script.sh
└── zmeventnotification
├── EventServer.sh
├── EventServer.tgz
└── zmeventnotification
├── README.md
├── config_upgrade.py
├── debug_opencv.sh
├── objectconfig.ini
├── opencv.sh
├── pushapi_pushover.py
├── secrets.ini
├── setup.py
├── train_faces.py
├── zm_detect.py
├── zm_detect_old.py
├── zm_event_end.sh
├── zm_event_start.sh
├── zm_train_faces.py
├── zmes_hook_helpers
├── __init__.py
├── apigw.py
├── common_params.py
├── image_manip.py
├── log.py
└── utils.py
├── zmeventnotification.ini
└── zmeventnotification.pl
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: dlandon # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | #### Change Log
2 | ### 2021-01-17
3 | - Prepare for new Docker container with ES/ML preloaded.
4 |
5 | ### 2021-01-07
6 | - Update opencv.sh to Ubuntu 20.04.
7 | - Remove /config/mysql/ib_logfile* files to insure that mysql starts.
8 | - Increase shm-size to 8G.
9 |
10 | ### 2021-01-03
11 | - Fix syslog-ng configuration file version.
12 | - Update zmNinja Event Notification Server to version 6.1.5.
13 |
14 | ### 2021-01-02
15 | - Remove SHMEM environment variable and set --shm-size instead.
16 | - Turn off privileged mode.
17 | - Update zmNinja Event Notification Server to version 6.1.0.
18 |
19 | ### 2021-01-01
20 | - Update zmNinja Event Notification Server to version 6.1.0 (pre-release).
21 |
22 | ### 2020-12-31
23 | - Update Docker baseimage to Focal Alpha (Ubuntu 20.04).
24 |
25 | ### 2020-11-18
26 | - Add NO_START_ZM environment variable to keep MySql and Zoneminder from starting so the docker stays running and a user can troubleshoot.
27 |
28 | ### 2020-10-24
29 | - Update zmNinja Event Notification Server to version 6.0.5.
30 | - Update opencv to 4.3.
31 | - Improve opencv compile time.
32 |
33 | ### 2020-10-16
34 | - Update zmNinja Event Notification Server to version 6.0.2.
35 |
36 | ### 2020-08-20
37 | - Fix typo in first run.
38 | - Update opencv script for verson 4.3.0 of opencv.
39 |
40 | ### 2020-08-12
41 | - Fix ES Yolo model download destination file names.
42 | - Create unknown_faces folder.
43 |
44 | ### 2020-08-11
45 | - Modify ES model downloads to the new paths and add Yolo V4.
46 | - Changed Yolo model download environment variables.
47 |
48 | ### 2020-08-08
49 | - Update zmNinja Event Notification Server to version 5.15.6.
50 |
51 | ### 2020-07-19
52 | - Update baseimage to bionic-1.0.0.
53 | - Update zmNinja Event Notification Server to version 5.15.5.
54 | - Update to 1.35 master.
55 |
56 | ### 2020-01-05
57 | - Disable all apache protocols except for TLSv1.2. Other protocols are obsolete.
58 |
59 | ### 2019-12-28
60 | - Upgrade to Phusion 11.0 (Ubuntu 18.04).
61 |
62 | ### 2019-12-22
63 | - Upgrade to php 7.4.
64 | - Update zmNinja Event Notification Server to version 5.4.
65 |
66 | ### 2019-12-21
67 | - Update zmNinja Event Notification Server to version 5.3.
68 |
69 | ### 2019-12-01
70 | - Fix ES file references to newer versions of zm_detect_wrapper.sh amnd zm_detect.py
71 |
72 | ### 2019-11-26
73 | - Remove set_php_time_zone script. The time is now set in options.
74 |
75 | ### 2019-11-23
76 | - Update zmNinja Event Notification Server to version 4.6.
77 |
78 | ### 2019-11-09
79 | - Update zmNinja Event Notification Server to version 4.5.
80 |
81 | ### 2019-11-06
82 | - Add libavutil-dev for hwaccel support.
83 |
84 | ### 2019-10-26
85 | - Remove cambozola legacy browser support.
86 |
87 | ### 2019-10-05
88 | - Update zmNinja Event Notification Server to version 4.4.
89 |
90 | ### 2019-09-22
91 | - Upgrade to php 7.3.
92 |
93 | ### 2019-09-21
94 | - Fix /hook folder permission check.
95 |
96 | ### 2019-09-19
97 | - Update zmes_hook_helpers.
98 |
99 | ### 2019-09-16
100 | - Initial master release.
101 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | -FROM phusion/baseimage:focal-1.0.0alpha1-amd64 as builder
2 |
3 | LABEL maintainer="dlandon"
4 |
5 | ENV DEBCONF_NONINTERACTIVE_SEEN="true" \
6 | DEBIAN_FRONTEND="noninteractive" \
7 | DISABLE_SSH="true" \
8 | HOME="/root" \
9 | LC_ALL="C.UTF-8" \
10 | LANG="en_US.UTF-8" \
11 | LANGUAGE="en_US.UTF-8" \
12 | TZ="Etc/UTC" \
13 | TERM="xterm" \
14 | PHP_VERS="7.4" \
15 | ZM_VERS="master" \
16 | PUID="99" \
17 | PGID="100"
18 |
19 | FROM builder as build1
20 | COPY init/ /etc/my_init.d/
21 | COPY defaults/ /root/
22 | COPY zmeventnotification/EventServer.tgz /root/
23 |
24 | RUN add-apt-repository -y ppa:iconnor/zoneminder-$ZM_VERS && \
25 | add-apt-repository ppa:ondrej/php && \
26 | apt-get update && \
27 | apt-get -y upgrade -o Dpkg::Options::="--force-confold" && \
28 | apt-get -y dist-upgrade -o Dpkg::Options::="--force-confold" && \
29 | apt-get -y install apache2 mariadb-server && \
30 | apt-get -y install ssmtp mailutils net-tools wget sudo make && \
31 | apt-get -y install php$PHP_VERS php$PHP_VERS-fpm libapache2-mod-php$PHP_VERS php$PHP_VERS-mysql php$PHP_VERS-gd && \
32 | apt-get -y install libcrypt-mysql-perl libyaml-perl libjson-perl libavutil-dev ffmpeg && \
33 | apt-get -y install --no-install-recommends libvlc-dev libvlccore-dev vlc && \
34 | apt-get -y install zoneminder
35 |
36 | FROM build1 as build2
37 | RUN rm /etc/mysql/my.cnf && \
38 | cp /etc/mysql/mariadb.conf.d/50-server.cnf /etc/mysql/my.cnf && \
39 | adduser www-data video && \
40 | a2enmod php$PHP_VERS proxy_fcgi ssl rewrite expires headers && \
41 | a2enconf php$PHP_VERS-fpm zoneminder && \
42 | echo "extension=apcu.so" > /etc/php/$PHP_VERS/mods-available/apcu.ini && \
43 | echo "extension=mcrypt.so" > /etc/php/$PHP_VERS/mods-available/mcrypt.ini && \
44 | perl -MCPAN -e "force install Net::WebSocket::Server" && \
45 | perl -MCPAN -e "force install LWP::Protocol::https" && \
46 | perl -MCPAN -e "force install Config::IniFiles" && \
47 | perl -MCPAN -e "force install Net::MQTT::Simple" && \
48 | perl -MCPAN -e "force install Net::MQTT::Simple::Auth" && \
49 | perl -MCPAN -e "force install Time::Piece"
50 |
51 | FROM build2 as build3
52 | RUN cd /root && \
53 | chown -R www-data:www-data /usr/share/zoneminder/ && \
54 | echo "ServerName localhost" >> /etc/apache2/apache2.conf && \
55 | sed -i "s|^;date.timezone =.*|date.timezone = ${TZ}|" /etc/php/$PHP_VERS/apache2/php.ini && \
56 | service mysql start && \
57 | mysql -uroot < /usr/share/zoneminder/db/zm_create.sql && \
58 | mysql -uroot -e "grant all on zm.* to 'zmuser'@localhost identified by 'zmpass';" && \
59 | mysqladmin -uroot reload && \
60 | mysql -sfu root < "mysql_secure_installation.sql" && \
61 | rm mysql_secure_installation.sql && \
62 | mysql -sfu root < "mysql_defaults.sql" && \
63 | rm mysql_defaults.sql
64 |
65 | FROM build3 as build4
66 | RUN mv /root/zoneminder /etc/init.d/zoneminder && \
67 | chmod +x /etc/init.d/zoneminder && \
68 | service mysql restart && \
69 | sleep 5 && \
70 | service apache2 restart && \
71 | service zoneminder start
72 |
73 | FROM build4 as build5
74 | RUN systemd-tmpfiles --create zoneminder.conf && \
75 | mv /root/default-ssl.conf /etc/apache2/sites-enabled/default-ssl.conf && \
76 | mkdir /etc/apache2/ssl/ && \
77 | mkdir -p /var/lib/zmeventnotification/images && \
78 | chown -R www-data:www-data /var/lib/zmeventnotification/ && \
79 | chmod -R +x /etc/my_init.d/ && \
80 | cp -p /etc/zm/zm.conf /root/zm.conf && \
81 | echo "#!/bin/sh\n\n/usr/bin/zmaudit.pl -f" >> /etc/cron.weekly/zmaudit && \
82 | chmod +x /etc/cron.weekly/zmaudit && \
83 | cp /etc/apache2/ports.conf /etc/apache2/ports.conf.default && \
84 | cp /etc/apache2/sites-enabled/default-ssl.conf /etc/apache2/sites-enabled/default-ssl.conf.default && \
85 | sed -i s#3.13#3.25#g /etc/syslog-ng/syslog-ng.conf
86 |
87 | FROM build5 as build6
88 | RUN apt-get -y remove make && \
89 | apt-get -y clean && \
90 | apt-get -y autoremove && \
91 | rm -rf /tmp/* /var/tmp/* && \
92 | chmod +x /etc/my_init.d/*.sh
93 |
94 | FROM build6 as build7
95 | VOLUME \
96 | ["/config"] \
97 | ["/var/cache/zoneminder"]
98 |
99 | FROM build7 as build8
100 | EXPOSE 80 443 9000
101 |
102 | FROM build8
103 | CMD ["/sbin/my_init"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 | {description}
294 | Copyright (C) {year} {fullname}
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | {signature of Ty Coon}, 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
341 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Zoneminder Docker
2 | (Current version: 1.35 (master))
3 |
4 | DEPRECATED and no longer available.
5 |
6 | ### About
7 | This is an easy to run dockerized image of [ZoneMinder](https://github.com/ZoneMinder/zoneminder) along with the the [ZM Event Notification Server](https://github.com/pliablepixels/zmeventnotification) and its machine learning subsystem (which is disabled by default but can be enabled by a simple configuration).
8 |
9 | The configuration settings that are needed for this implementation of Zoneminder are pre-applied and do not need to be changed on the first run of Zoneminder.
10 |
11 | This verson will now upgrade from previous versions.
12 |
13 | ### Installation
14 | Install the docker by going to a command line and enter the command:
15 |
16 | ```bash
17 | docker pull dlandon/zoneminder.master
18 | ```
19 |
20 | This will pull the zoneminder master docker image. Note that ZoneMinder master should always be treated as a development release. If you want to run the latest stable release, please use [this](https://github.com/dlandon/zoneminder) repo. Once it is installed you are ready to run the docker.
21 |
22 | Before you run the image, feel free to read configuration section below to customize various settings
23 |
24 | To run Zoneminder:
25 |
26 | ```bash
27 | docker run -d --name="Zoneminder" \
28 | --net="bridge" \
29 | --privileged="false" \
30 | --shm-size="8G" \
31 | -p 8443:443/tcp \
32 | -p 9000:9000/tcp \
33 | -e TZ="America/New_York" \
34 | -e PUID="99" \
35 | -e PGID="100" \
36 | -v "/mnt/Zoneminder":"/config":rw \
37 | -v "/mnt/Zoneminder/data":"/var/cache/zoneminder":rw \
38 | dlandon/zoneminder.master
39 | ```
40 |
41 | For http:// access use: -p 8080:80/tcp
42 |
43 | ### Shared Memory
44 | Set your shared memory to half of your installed memory.
45 |
46 | **Note**: If you have opted to install face recognition, and/or have opted to download the yolo models, it takes time.
47 | Face recognition in particular can take several minutes (or more). Once the `docker run` command above completes, you may not be able to access ZoneMinder till all the downloads are done. To follow along the installation progress, do a `docker logs -f zoneminder` to see the syslog for the container that was created above.
48 |
49 | ### Subsequent runs
50 |
51 | You can start/stop/restart the container anytime. You don't need to run the command above every time. If you have already created the container once (by the `docker run` command above), you can simply do a `docker stop Zoneminder` to stop it and a `docker start Zoneminder` to start it anytime (or do a `docker restart zoneminder`)
52 |
53 | #### Customization
54 |
55 | - The command above use a host path of `/mnt/Zoneminder` to map the container config and cache directories. This is going to be persistent directory that will retain data across container/image stop/restart/deletes. ZM mysql/other config data/event files/etc are kept here. You can change this to any directory in your host path that you want to.
56 |
57 | #### Post install configuration and caveats
58 |
59 | - After successful installation, please refer to the [ZoneMinder](https://zoneminder.readthedocs.io/en/stable/), [Event Server and Machine Learning](https://zmeventnotification.readthedocs.io/en/latest/index.html) configuration guides from the authors of these components to set it up to your needs. Specifically, if you are using the Event Server and the Machine learning hooks, you will need to customize `/etc/zm/zmeventnotification.ini` and `/etc/zm/objectconfig.ini`
60 |
61 | - Note that by default, this docker build runs ZM on port 443 inside the docker container and maps it to port 8443 for the outside world. Therefore, if you are configuring `/etc/zm/objectconfig.ini` or `/etc/zm/zmeventnotification.ini` remember to use `https://localhost:443/` as the base URL
62 |
63 | - Push notifications with images will not work unless you replace the self-signed certificates that are auto-generated. Feel free to use the excellent and free [LetsEncrypt](https://letsencrypt.org) service if you'd like.
64 |
65 | #### Usage
66 |
67 | To access the Zoneminder gui, browse to: `https://:8443/zm`
68 |
69 | The zmNinja Event Notification Server is accessed at port `9000`. Security with a self signed certificate is enabled. You may have to install the certificate on iOS devices for the event notification to work properly.
70 |
71 | #### Troubleshooting when the docker fails
72 |
73 | If you have a situation where the docker fails to start, you can set an environemtnt variable when the docker is started and MySql and Zoneminder will not be started. This will keep the docker running so you can get into a command line in the docker and troubleshoot the problem.
74 |
75 | Create an environment variable:
76 | NO_START_ZM="1"
77 |
78 | MySql and Zoneminder will not be started.
79 |
80 | Get into a command line in the docker and troubleshoot your issue by using the following commands to start MySql and zonemonder and fix any errors/problems with them starting.
81 |
82 | service mysql start
83 |
84 | service zoneminder start
85 |
--------------------------------------------------------------------------------
/defaults/default-ssl.conf:
--------------------------------------------------------------------------------
1 |
2 |
3 | ServerAdmin webmaster@localhost
4 |
5 | DocumentRoot /var/www/html
6 |
7 | # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
8 | # error, crit, alert, emerg.
9 | # It is also possible to configure the loglevel for particular
10 | # modules, e.g.
11 | #LogLevel info ssl:warn
12 |
13 | ErrorLog ${APACHE_LOG_DIR}/error.log
14 | CustomLog ${APACHE_LOG_DIR}/access.log combined
15 |
16 | # For most configuration files from conf-available/, which are
17 | # enabled or disabled at a global level, it is possible to
18 | # include a line for only one particular virtual host. For example the
19 | # following line enables the CGI configuration for this host only
20 | # after it has been globally disabled with "a2disconf".
21 | #Include conf-available/serve-cgi-bin.conf
22 |
23 | # SSL Engine Switch:
24 | # Enable/Disable SSL for this virtual host.
25 | SSLEngine on
26 |
27 | # Disable all protocols except for TLSv1.2. Other protocols are obsolete.
28 | SSLProtocol -all +TLSv1.2
29 |
30 | # A self-signed (snakeoil) certificate can be created by installing
31 | # the ssl-cert package. See
32 | # /usr/share/doc/apache2/README.Debian.gz for more info.
33 | # If both key and certificate are stored in the same file, only the
34 | # SSLCertificateFile directive is needed.
35 | SSLCertificateFile /etc/apache2/ssl/zoneminder.crt
36 | SSLCertificateKeyFile /etc/apache2/ssl/zoneminder.key
37 |
38 | # Server Certificate Chain:
39 | # Point SSLCertificateChainFile at a file containing the
40 | # concatenation of PEM encoded CA certificates which form the
41 | # certificate chain for the server certificate. Alternatively
42 | # the referenced file can be the same as SSLCertificateFile
43 | # when the CA certificates are directly appended to the server
44 | # certificate for convinience.
45 | #SSLCertificateChainFile /etc/apache2/ssl.crt/server-ca.crt
46 |
47 | # Certificate Authority (CA):
48 | # Set the CA certificate verification path where to find CA
49 | # certificates for client authentication or alternatively one
50 | # huge file containing all of them (file must be PEM encoded)
51 | # Note: Inside SSLCACertificatePath you need hash symlinks
52 | # to point to the certificate files. Use the provided
53 | # Makefile to update the hash symlinks after changes.
54 | #SSLCACertificatePath /etc/ssl/certs/
55 | #SSLCACertificateFile /etc/apache2/ssl.crt/ca-bundle.crt
56 |
57 | # Certificate Revocation Lists (CRL):
58 | # Set the CA revocation path where to find CA CRLs for client
59 | # authentication or alternatively one huge file containing all
60 | # of them (file must be PEM encoded)
61 | # Note: Inside SSLCARevocationPath you need hash symlinks
62 | # to point to the certificate files. Use the provided
63 | # Makefile to update the hash symlinks after changes.
64 | #SSLCARevocationPath /etc/apache2/ssl.crl/
65 | #SSLCARevocationFile /etc/apache2/ssl.crl/ca-bundle.crl
66 |
67 | # Client Authentication (Type):
68 | # Client certificate verification type and depth. Types are
69 | # none, optional, require and optional_no_ca. Depth is a
70 | # number which specifies how deeply to verify the certificate
71 | # issuer chain before deciding the certificate is not valid.
72 | #SSLVerifyClient optional_no_ca
73 | #SSLVerifyDepth 10
74 |
75 | # SSL Engine Options:
76 | # Set various options for the SSL engine.
77 | # o FakeBasicAuth:
78 | # Translate the client X.509 into a Basic Authorisation. This means that
79 | # the standard Auth/DBMAuth methods can be used for access control. The
80 | # user name is the `one line' version of the client's X.509 certificate.
81 | # Note that no password is obtained from the user. Every entry in the user
82 | # file needs this password: `xxj31ZMTZzkVA'.
83 | # o ExportCertData:
84 | # This exports two additional environment variables: SSL_CLIENT_CERT and
85 | # SSL_SERVER_CERT. These contain the PEM-encoded certificates of the
86 | # server (always existing) and the client (only existing when client
87 | # authentication is used). This can be used to import the certificates
88 | # into CGI scripts.
89 | # o StdEnvVars:
90 | # This exports the standard SSL/TLS related `SSL_*' environment variables.
91 | # Per default this exportation is switched off for performance reasons,
92 | # because the extraction step is an expensive operation and is usually
93 | # useless for serving static content. So one usually enables the
94 | # exportation for CGI and SSI requests only.
95 | # o OptRenegotiate:
96 | # This enables optimized SSL connection renegotiation handling when SSL
97 | # directives are used in per-directory context.
98 | #SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire
99 |
100 | SSLOptions +StdEnvVars
101 |
102 |
103 | SSLOptions +StdEnvVars
104 |
105 |
106 | # SSL Protocol Adjustments:
107 | # The safe and default but still SSL/TLS standard compliant shutdown
108 | # approach is that mod_ssl sends the close notify alert but doesn't wait for
109 | # the close notify alert from client. When you need a different shutdown
110 | # approach you can use one of the following variables:
111 | # o ssl-unclean-shutdown:
112 | # This forces an unclean shutdown when the connection is closed, i.e. no
113 | # SSL close notify alert is send or allowed to received. This violates
114 | # the SSL/TLS standard but is needed for some brain-dead browsers. Use
115 | # this when you receive I/O errors because of the standard approach where
116 | # mod_ssl sends the close notify alert.
117 | # o ssl-accurate-shutdown:
118 | # This forces an accurate shutdown when the connection is closed, i.e. a
119 | # SSL close notify alert is send and mod_ssl waits for the close notify
120 | # alert of the client. This is 100% SSL/TLS standard compliant, but in
121 | # practice often causes hanging connections with brain-dead browsers. Use
122 | # this only for browsers where you know that their SSL implementation
123 | # works correctly.
124 | # Notice: Most problems of broken clients are also related to the HTTP
125 | # keep-alive facility, so you usually additionally want to disable
126 | # keep-alive for those clients, too. Use variable "nokeepalive" for this.
127 | # Similarly, one has to force some clients to use HTTP/1.0 to workaround
128 | # their broken HTTP/1.1 implementation. Use variables "downgrade-1.0" and
129 | # "force-response-1.0" for this.
130 | # BrowserMatch "MSIE [2-6]" \
131 | # nokeepalive ssl-unclean-shutdown \
132 | # downgrade-1.0 force-response-1.0
133 |
134 |
135 |
136 |
137 | # vim: syntax=apache ts=4 sw=4 sts=4 sr noet
138 |
--------------------------------------------------------------------------------
/defaults/mysql_defaults.sql:
--------------------------------------------------------------------------------
1 | # Set up some defaults
2 | update zm.Config SET Value='/usr/bin/ffmpeg' WHERE Name='ZM_PATH_FFMPEG';
3 | update zm.Config SET Value=1 WHERE Name='ZM_OPT_FFMPEG';
4 | update zm.Config SET Value='-vcodec libx264 -threads 2 -b 2000k -minrate 800k -maxrate 5000k' WHERE Name='ZM_FFMPEG_OUTPUT_OPTIONS';
5 | update zm.Config SET Value='mp4* mpg mpeg wmv asf avi mov swf 3gp**' WHERE Name='ZM_FFMPEG_FORMATS';
6 | update zm.Config SET Value='/usr/sbin/ssmtp' WHERE Name='ZM_SSMTP_PATH';
7 | update zm.Config SET Value=0 WHERE Name='ZM_RUN_AUDIT';
8 |
--------------------------------------------------------------------------------
/defaults/mysql_secure_installation.sql:
--------------------------------------------------------------------------------
1 | # Setup mysql
2 | UPDATE mysql.user SET Password=PASSWORD('zoneminder') WHERE User='root';
3 | DELETE FROM mysql.user WHERE User='';
4 | DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');
5 | DROP DATABASE IF EXISTS test;
6 | DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';
7 | FLUSH PRIVILEGES;
8 |
--------------------------------------------------------------------------------
/defaults/opencv_compile.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | #
4 | # Script to compile opencv without gpu support.
5 | #
6 | #
7 | OPENCV_VER=4.5.1
8 | OPENCV_URL=https://github.com/opencv/opencv/archive/$OPENCV_VER.zip
9 | OPENCV_CONTRIB_URL=https://github.com/opencv/opencv_contrib/archive/$OPENCV_VER.zip
10 | #
11 | # Compile opencv
12 | #
13 |
14 | logger "Compiling opencv without GPU Support" -tEventServer
15 |
16 | cd ~
17 | wget -q -O opencv.zip $OPENCV_URL
18 | wget -q -O opencv_contrib.zip $OPENCV_CONTRIB_URL
19 | unzip opencv.zip
20 | unzip opencv_contrib.zip
21 | mv $(ls -d opencv-*) opencv
22 | mv opencv_contrib-$OPENCV_VER opencv_contrib
23 | rm *.zip
24 |
25 | logger "Compiling opencv..." -tEventServer
26 |
27 | cd ~/opencv
28 | mkdir build
29 | cd build
30 | cmake -D CMAKE_BUILD_TYPE=RELEASE \
31 | -D CMAKE_INSTALL_PREFIX=/usr/local \
32 | -D INSTALL_PYTHON_EXAMPLES=OFF \
33 | -D INSTALL_C_EXAMPLES=OFF \
34 | -D OPENCV_ENABLE_NONFREE=ON \
35 | -D OPENCV_EXTRA_MODULES_PATH=~/opencv_contrib/modules \
36 | -D HAVE_opencv_python3=ON \
37 | -D PYTHON_EXECUTABLE=/usr/bin/python3 \
38 | -D PYTHON2_EXECUTABLE=/usr/bin/python2 \
39 | -D BUILD_EXAMPLES=OFF .. >/dev/null
40 |
41 | make -j$(nproc)
42 |
43 | logger "Installing opencv..." -tEventServer
44 | make install
45 |
46 | logger "Cleaning up..." -tEventServer
47 | cd ~
48 | rm -r opencv*
49 |
--------------------------------------------------------------------------------
/defaults/zoneminder:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | ### BEGIN INIT INFO
3 | # Provides: zoneminder
4 | # Required-Start: $network $remote_fs $syslog
5 | # Required-Stop: $network $remote_fs $syslog
6 | # Should-Start: mysql
7 | # Should-Stop: mysql
8 | # Default-Start: 2 3 4 5
9 | # Default-Stop: 0 1 6
10 | # Short-Description: Control ZoneMinder as a Service
11 | # Description: ZoneMinder CCTV recording and surveillance system
12 | ### END INIT INFO
13 | # chkconfig: 2345 20 20
14 |
15 | # Source function library.
16 | . /lib/lsb/init-functions
17 |
18 | prog=ZoneMinder
19 | ZM_PATH_BIN="/usr/bin"
20 | RUNDIR="/var/run/zm"
21 | TMPDIR="/tmp/zm"
22 | command="$ZM_PATH_BIN/zmpkg.pl"
23 |
24 | start() {
25 | echo "Starting $prog:"
26 | export TZ=:/etc/localtime
27 | mkdir -p "$RUNDIR" && chown www-data:www-data "$RUNDIR"
28 | mkdir -p "$TMPDIR" && chown www-data:www-data "$TMPDIR"
29 | rm -rf "$RUNDIR"/*
30 | $command start
31 | RETVAL=$?
32 | [ $RETVAL = 0 ] && echo "$prog started successfully"
33 | [ $RETVAL != 0 ] && echo "$prog failed to start"
34 | echo
35 | [ $RETVAL = 0 ] && touch /var/lock/zm
36 | return $RETVAL
37 | }
38 | stop() {
39 | echo "Stopping $prog:"
40 | #
41 | # Why is this status check being done?
42 | # as $command stop returns 1 if zoneminder
43 | # is stopped, which will result in
44 | # this returning 1, which will stuff
45 | # dpkg when it tries to stop zoneminder before
46 | # uninstalling . . .
47 | #
48 | result=`$command status`
49 | if [ ! "$result" = "running" ]; then
50 | echo "$prog already stopped"
51 | echo
52 | RETVAL=0
53 | else
54 | $command stop
55 | RETVAL=$?
56 | [ $RETVAL = 0 ] && echo "$prog stopped successfully"
57 | [ $RETVAL != 0 ] && echo "$prog failed to stop"
58 | echo
59 | [ $RETVAL = 0 ] && rm -f /var/lock/zm
60 | fi
61 | }
62 | status() {
63 | result=`$command status`
64 | if [ "$result" = "running" ]; then
65 | echo "$prog is running"
66 | RETVAL=0
67 | else
68 | echo "$prog is stopped"
69 | RETVAL=1
70 | fi
71 | }
72 |
73 | case "$1" in
74 | 'start')
75 | start
76 | ;;
77 | 'stop')
78 | stop
79 | ;;
80 | 'restart' | 'force-reload')
81 | stop
82 | start
83 | ;;
84 | 'status')
85 | status
86 | ;;
87 | *)
88 | echo "Usage: $0 { start | stop | restart | status }"
89 | RETVAL=1
90 | ;;
91 | esac
92 | exit $RETVAL
93 |
--------------------------------------------------------------------------------
/docker-compose - gpu support.yaml:
--------------------------------------------------------------------------------
1 | version: '3.1'
2 | services:
3 | zoneminder:
4 | container_name: zoneminder
5 | image: dlandon/zoneminder.master:latest
6 | restart: unless-stopped
7 | ports:
8 | - 8443:443/tcp
9 | - 9000:9000/tcp
10 | runtime: nvidia
11 | network_mode: "bridge"
12 | privileged: false
13 | shm_size=8G
14 | environment:
15 | - TZ=America/New_York
16 | - PUID=99
17 | - PGID=100
18 | - INSTALL_HOOK=0
19 | - MULTI_PORT_START=0
20 | - MULTI_PORT_END=0
21 | volumes:
22 | - config:/config:rw
23 | - data:/var/cache/zoneminder:rw
24 | - cuda:/usr/local/cuda:/usr/local/cuda
25 | volumes:
26 | config:
27 | data:
28 | cuda:
29 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3.1'
2 | services:
3 | zoneminder:
4 | container_name: zoneminder
5 | image: dlandon/zoneminder.master:latest
6 | restart: unless-stopped
7 | ports:
8 | - 8443:443/tcp
9 | - 9000:9000/tcp
10 | network_mode: "bridge"
11 | privileged: false
12 | shm_size=8G
13 | environment:
14 | - TZ=America/New_York
15 | - PUID=99
16 | - PGID=100
17 | - MULTI_PORT_START=0
18 | - MULTI_PORT_END=0
19 | volumes:
20 | - config:/config:rw
21 | - data:/var/cache/zoneminder:rw
22 | volumes:
23 | config:
24 | data:
25 |
--------------------------------------------------------------------------------
/docker-run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | docker run -d --name="Zoneminder" \
4 | --net="bridge" \
5 | --privileged="false" \
6 | --shm-size="8G" \
7 | -p 8443:443/tcp \
8 | -p 9000:9000/tcp \
9 | -e TZ="America/New_York" \
10 | -e PUID="99" \
11 | -e PGID="100" \
12 | -e MULTI_PORT_START="0" \
13 | -e MULTI_PORT_END="0" \
14 | -v "/mnt/cache/appdata/Zoneminder":"/config":rw \
15 | -v "/mnt/cache/appdata/Zoneminder/data":"/var/cache/zoneminder":rw \
16 | dlandon/zoneminder.master
17 |
--------------------------------------------------------------------------------
/init/05_set_the_time.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # 05_set_the_time.sh
4 | #
5 |
6 | # Get docker env timezone and set system timezone
7 | if [[ $(cat /etc/timezone) != "$TZ" ]] ; then
8 | echo "Setting the timezone to : $TZ"
9 | echo $TZ > /etc/timezone
10 | ln -fs /usr/share/zoneinfo/$TZ /etc/localtime
11 | dpkg-reconfigure tzdata
12 | echo "Date: `date`"
13 | fi
14 |
--------------------------------------------------------------------------------
/init/06_set_php_time.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # 06_set_php_time.sh
4 | #
5 |
6 | sed -i "s|^date.timezone =.*$|date.timezone = ${TZ}|" /etc/php/$PHP_VERS/apache2/php.ini
7 |
--------------------------------------------------------------------------------
/init/07_set_dri_permissions.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Script to set up permissions on hardware devices for GPU support.
4 | # Inspired by how the guys over at linuxserver did this for their Plex image:
5 | # https://github.com/linuxserver/docker-plex/blob/master/root/etc/cont-init.d/50-gid-video
6 | #
7 |
8 | echo "Granting permissions on /dev/dri/* devices..."
9 |
10 | FILES=$(find /dev/dri /dev/dvb -type c -print 2>/dev/null)
11 |
12 | for i in $FILES
13 | do
14 | VIDEO_GID=$(stat -c '%g' "$i")
15 | if id -G www-data | grep -qw "$VIDEO_GID"; then
16 | echo "The www-data user already has appropriate permissions on $i"
17 | touch /groupadd
18 | else
19 | if [ ! "${VIDEO_GID}" == '0' ]; then
20 | VIDEO_NAME=$(getent group "${VIDEO_GID}" | awk -F: '{print $1}')
21 | if [ -z "${VIDEO_NAME}" ]; then
22 | VIDEO_NAME="video$(head /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c8)"
23 | groupadd "$VIDEO_NAME"
24 | groupmod -g "$VIDEO_GID" "$VIDEO_NAME"
25 | echo "Generated a new group called: $VIDEO_NAME with id: $VIDEO_GID to match existing group on: $i"
26 | fi
27 | usermod -a -G "$VIDEO_NAME" www-data
28 | echo "Added user www-data to group $VIDEO_NAME so that it has permission to use: $i"
29 | touch /groupadd
30 | fi
31 | fi
32 | done
33 |
34 | if [ -n "${FILES}" ] && [ ! -f "/groupadd" ]; then
35 | usermod -a -G root www-data
36 | echo "Added user www-data to root group for lack of a better option."
37 | fi
38 |
--------------------------------------------------------------------------------
/init/20_apt_update.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # 20_apt_update.sh
4 | #
5 |
6 | # Update repositories
7 | echo "Performing updates..."
8 | apt-get update 2>&1 | tee /tmp/test_update
9 |
10 | # Verify that the updates will work.
11 | if [ "`cat /tmp/test_update | grep 'Failed'`" = "" ]; then
12 | # Perform Upgrade
13 | apt-get -y upgrade -o Dpkg::Options::="--force-confold"
14 |
15 | # Clean + purge old/obsoleted packages
16 | apt-get -y autoremove
17 | else
18 | echo "Warning: Unable to update! Check Internet connection."
19 | fi
20 |
--------------------------------------------------------------------------------
/init/30_gen_ssl_keys.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # 30_gen_ssl_keys.sh
4 | #
5 |
6 | if [[ -f /config/keys/cert.key && -f /config/keys/cert.crt ]]; then
7 | echo "using existing keys in \"/config/keys\""
8 | if [[ ! -f /config/keys/ServerName ]]; then
9 | echo "localhost" > /config/keys/ServerName
10 | fi
11 | SERVER=`cat /config/keys/ServerName`
12 | sed -i "/ServerName/c\ServerName $SERVER" /etc/apache2/apache2.conf
13 | else
14 | echo "generating self-signed keys in /config/keys, you can replace these with your own keys if required"
15 | mkdir -p /config/keys
16 | echo "localhost" >> /config/keys/ServerName
17 | openssl req -x509 -nodes -days 4096 -newkey rsa:2048 -out /config/keys/cert.crt -keyout /config/keys/cert.key -subj "/C=US/ST=NY/L=New York/O=Zoneminder/OU=Zoneminder/CN=localhost"
18 | fi
19 |
20 | chown root:root /config/keys
21 | chmod 777 /config/keys
22 |
--------------------------------------------------------------------------------
/init/999_advanced_script.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # 999_advanced_script.sh
4 | #
5 |
6 | [ "$ADVANCED_SCRIPT" ] && echo "Scripting Enabled by: $ADVANCED_SCRIPT"
7 | [ -f /config/userscript.sh ] && (chmod +x /config/userscript.sh && echo "Userscript Provided")
8 | [ "$ADVANCED_SCRIPT" ] && [ -x /config/userscript.sh ] && /config/userscript.sh
9 |
10 | exit 0
11 |
--------------------------------------------------------------------------------
/zmeventnotification/EventServer.sh:
--------------------------------------------------------------------------------
1 | #
2 | # Make EventSerever distribution package
3 | #
4 | # Build the tgz.
5 | tar -cvzf EventServer.tgz zmeventnotification/
6 |
--------------------------------------------------------------------------------
/zmeventnotification/EventServer.tgz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dlandon/zoneminder.master-docker/a7aa278a6d509c3e65baea7c217b6956879873cc/zmeventnotification/EventServer.tgz
--------------------------------------------------------------------------------
/zmeventnotification/zmeventnotification/README.md:
--------------------------------------------------------------------------------
1 | # ZM ES hook helpers
2 | # This file is needed for pip to work
3 |
--------------------------------------------------------------------------------
/zmeventnotification/zmeventnotification/config_upgrade.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | import re
3 | from configparser import ConfigParser
4 | import sys
5 | import argparse
6 | import os
7 | import re
8 | '''
9 | wej qaStaHvIS wa' ghu'maj. wa'maHlu'chugh, vaj pagh.
10 | chotlhej'a' qaDanganpu'. chenmoH tlhInganpu'.
11 | '''
12 |
13 | breaking = False
14 |
15 | def sanity_check(s, c, v):
16 |
17 | for attr in s:
18 | print (f'Doing a sanity check, {attr} should not be there...')
19 | #if attr in c:
20 | if re.search(f'(^| |\t|\n){attr}(=| |\t)',c):
21 | print (
22 | '''
23 | There is an error in your config. While upgrading to version:{}
24 | I found a key ({}) that should not have been there.
25 | This usually means when you last upgraded, your version attribute
26 | was not upgraded for some reason. To be safe, this script will not
27 | upgrade the script till you fix the potential issues (or manually
28 | bump up version)
29 | '''.format(v,attr))
30 | exit(1)
31 |
32 | print ('Sanity check passed!')
33 | return True
34 |
35 | def replace_attributes (orig, replacements):
36 | new_string = ''
37 | for line in orig.splitlines():
38 | new_line = ''
39 | for k,v in replacements.items():
40 | #print ("Replacing "+k+" with "+v)
41 | line = re.sub(r"(\s|^)({})(\s|^|$|=)".format(k), r"\g<1>{}\g<3>".format(v), line)
42 | #line = new_line
43 | new_string = new_string + line + '\n'
44 | return new_string
45 |
46 |
47 | def create_attributes(orig_string, new_additions):
48 | def match_attrs(match):
49 | return new_additions[match.group(0)]
50 |
51 | new_string = (re.sub('|'.join(r'%s' % re.escape(s) for s in new_additions),
52 | match_attrs, orig_string) )
53 | return new_string
54 |
55 |
56 | # add new version migrations as functions
57 | # def f__to_(str_conf,new_version):
58 |
59 | def f_1_1_to_1_2(str_conf,new_version):
60 |
61 | print ('*** Only basic changes have been made. Please study the sample objectconfig.ini file to see all the other parameters ***')
62 |
63 | should_not_be_there = {
64 | 'ml_sequence',
65 | 'stream_sequence'
66 | }
67 | replacements = {
68 | 'version=1.1': 'version='+new_version,
69 | '# config for object': ''
70 | }
71 | new_additions= {
72 | '\n[object]\n':
73 | '''
74 | [ml]
75 |
76 | # NEW: If enabled (default = no), we will use ml_sequence and stream_sequence
77 | # and ignore everything in [object] [face] and [alpr]
78 | #
79 | # use_sequence = yes
80 |
81 | # NEW: You can use this structure to create a chain of detection algorithms
82 | # See https://pyzm.readthedocs.io/en/latest/source/pyzm.html#detectsequence for options (look at options field)
83 |
84 | # VERY IMPORTANT - The curly braces need to be indented to be inside ml_sequence
85 | # tag, or parsing will fail
86 | # NOTE: If you specify this attribute, all settings in [object], [face] and [alpr] will be ignored
87 |
88 | ml_sequence= {
89 | 'general': {
90 | 'model_sequence': 'object,face,alpr',
91 |
92 | },
93 | 'object': {
94 | 'general':{
95 | 'pattern':'(person)',
96 | 'same_model_sequence_strategy': 'first' # also 'most', 'most_unique's
97 | },
98 | 'sequence': [{
99 | #First run on TPU with higher confidence
100 | 'object_weights':'{{base_data_path}}/models/coral_edgetpu/ssd_mobilenet_v2_coco_quant_postprocess_edgetpu.tflite',
101 | 'object_labels': '{{base_data_path}}/models/coral_edgetpu/coco_indexed.names',
102 | 'object_min_confidence': 0.6,
103 | 'object_framework':'coral_edgetpu'
104 | },
105 | {
106 | # YoloV4 on GPU if TPU fails (because sequence strategy is 'first')
107 | 'object_config':'{{base_data_path}}/models/yolov4/yolov4.cfg',
108 | 'object_weights':'{{base_data_path}}/models/yolov4/yolov4.weights',
109 | 'object_labels': '{{base_data_path}}/models/yolov4/coco.names',
110 | 'object_min_confidence': 0.3,
111 | 'object_framework':'opencv',
112 | 'object_processor': 'gpu'
113 | }]
114 | },
115 | 'face': {
116 | 'general':{
117 | 'pattern': '.*',
118 | 'same_model_sequence_strategy': 'first'
119 | },
120 | 'sequence': [{
121 | 'face_detection_framework': 'dlib',
122 | 'known_images_path': '{{base_data_path}}/known_faces',
123 | 'face_model': 'cnn',
124 | 'face_train_model': 'cnn',
125 | 'face_recog_dist_threshold': 0.6,
126 | 'face_num_jitters': 1,
127 | 'face_upsample_times':1,
128 | 'save_unknown_faces':'yes',
129 | 'save_unknown_faces_leeway_pixels':100
130 | }]
131 | },
132 |
133 | 'alpr': {
134 | 'general':{
135 | 'same_model_sequence_strategy': 'first',
136 | 'pre_existing_labels':['car', 'motorbike', 'bus', 'truck', 'boat'],
137 |
138 | },
139 | 'sequence': [{
140 | 'alpr_api_type': 'cloud',
141 | 'alpr_service': 'plate_recognizer',
142 | 'alpr_key': '!PLATEREC_ALPR_KEY',
143 | 'platrec_stats': 'no',
144 | 'platerec_min_dscore': 0.1,
145 | 'platerec_min_score': 0.2,
146 | }]
147 | }
148 | }
149 |
150 | # NEW: This new structure can be used to select arbitrary frames for analysis
151 | # See https://pyzm.readthedocs.io/en/latest/source/pyzm.html#pyzm.ml.detect_sequence.DetectSequence.detect_stream
152 | # for options (look at options field)
153 | # NOTE: If stream_sequence specified, the following pre-existing attributes in objectconfig.ini will be ignored
154 | # as you are now able to specify them inside the sequence:
155 | # - frame_id (replaced by frame_set inside stream_sequence),
156 | # - resize
157 | # - delete_after_analyze
158 | #
159 | stream_sequence = {
160 | 'detection_mode': 'most_models',
161 | 'frame_set': 'snapshot,alarm',
162 |
163 | }
164 |
165 | # config for object
166 | [object]
167 | '''
168 |
169 |
170 | }
171 | if sanity_check(should_not_be_there, str_conf, new_version):
172 | s1=replace_attributes(str_conf,replacements)
173 | return (create_attributes(s1, new_additions))
174 |
175 |
176 | def f_1_0_to_1_1(str_conf,new_version):
177 | replacements = {
178 | 'version=1.0': 'version='+new_version
179 | }
180 | new_additions = {
181 | '\n[animation]\n':
182 | '''
183 | #NEW: if animation_types is gif then when can generate a fast preview gif
184 | # every second frame is skipped and the frame rate doubled
185 | # to give quick preview, Default (no)
186 | fast_gif=no
187 | '''
188 |
189 | }
190 | should_not_be_there = {
191 | 'fast_gif'
192 | }
193 |
194 | if sanity_check(should_not_be_there, str_conf, new_version):
195 | s1=replace_attributes(str_conf,replacements)
196 | return (create_attributes(s1, new_additions))
197 |
198 |
199 |
200 |
201 | def f_unknown_to_1_0(str_conf, new_version):
202 | global breaking
203 | breaking = True
204 | should_not_be_there = {
205 | 'cpu_max_processes',
206 | 'tpu_max_processes',
207 | 'gpu_max_processes',
208 | 'cpu_max_lock_wait',
209 | 'tpu_max_lock_wait',
210 | 'gpu_max_lock_wait',
211 | 'object_framework',
212 | 'object_processor',
213 | 'face_detection_framework',
214 | 'face_recognition_framework'
215 |
216 |
217 | }
218 |
219 | replacements = {
220 | 'models':'detection_sequence',
221 | '[yolo]': '[object]',
222 | 'yolo': 'object',
223 | 'yolo_min_confidence': 'object_min_confidence',
224 | '[ml]': '[remote]',
225 | 'config': 'object_config',
226 | 'weights': 'object_weights',
227 | 'labels': 'object_labels',
228 | 'tiny_config': '#REMOVE tiny_config',
229 | 'tiny_weights': '#REMOVE tiny_weights',
230 | 'tiny_labels': '#REMOVE tiny_labels',
231 | 'yolo_type': '#REMOVE yolo_type',
232 | 'alpr_pattern': 'alpr_detection_pattern',
233 | 'detect_pattern': 'object_detection_pattern'
234 | }
235 |
236 | new_additions={
237 | '\n[general]\n':
238 | '''
239 | [general]
240 | # Please don't change this. It is used by the config upgrade script
241 | version=1.0
242 |
243 | # NEW: You can now limit the # of detection process
244 | # per target processor. If not specified, default is 1
245 | # Other detection processes will wait to acquire lock
246 |
247 | cpu_max_processes=3
248 | tpu_max_processes=1
249 | gpu_max_processes=1
250 |
251 | # NEW: Time to wait in seconds per processor to be free, before
252 | # erroring out. Default is 120 (2 mins)
253 | cpu_max_lock_wait=120
254 | tpu_max_lock_wait=120
255 | gpu_max_lock_wait=120
256 |
257 | ''',
258 | '\n[alpr]\n':
259 | '''
260 | [alpr]
261 |
262 | #NEW: You can specify a license plate matching pattern here
263 | alpr_detection_pattern=.*
264 | ''',
265 |
266 | '\n[object]\n':
267 | '''
268 | [object]
269 |
270 | #NEW: opencv or coral_edgetpu
271 | #object_framework=opencv
272 |
273 | #NEW: CPU or GPU
274 | #object_processor=cpu #or gpu
275 | ''',
276 |
277 | '\n[face]\n':
278 | '''
279 | [face]
280 |
281 | # NEW: You can specify a face matching pattern here
282 | face_detection_pattern=.*
283 | #face_detection_pattern=(King|Kong)
284 |
285 | # As of today, only dlib can be used
286 | # Coral TPU only supports face detection
287 | # Maybe in future, we can do different frameworks
288 | # for detection and recognition
289 |
290 | face_detection_framework=dlib
291 | face_recognition_framework=dlib
292 | ''',
293 |
294 | }
295 |
296 | if sanity_check(should_not_be_there, str_conf, new_version):
297 | s1=replace_attributes(str_conf,replacements)
298 | return (create_attributes(s1, new_additions))
299 |
300 | # MAIN
301 |
302 | upgrade_path = [
303 | {'from_version': 'unknown',
304 | 'to_version': '1.0',
305 | 'migrate':f_unknown_to_1_0
306 | },
307 | {'from_version': '1.0',
308 | 'to_version': '1.1',
309 | 'migrate':f_1_0_to_1_1
310 | },
311 | {'from_version': '1.1',
312 | 'to_version': '1.2',
313 | 'migrate':f_1_1_to_1_2
314 | },
315 |
316 | ]
317 |
318 | ap = argparse.ArgumentParser(description='objectconfig.ini upgrade script')
319 | ap.add_argument('-c', '--config', help='objectconfig file with path', required=True)
320 | ap.add_argument('-o', '--output', help='output file to write to')
321 |
322 |
323 | args, u = ap.parse_known_args()
324 | args = vars(args)
325 |
326 |
327 |
328 | config_file = ConfigParser(interpolation=None, inline_comment_prefixes='#')
329 | config_file.read(args.get('config'))
330 |
331 | version = 'unknown'
332 | if config_file.has_option('general', 'version'):
333 | version = config_file.get('general', 'version')
334 |
335 | print (f'Current version of file is {version}')
336 | f=open(args.get('config'))
337 | str_conf = f.read()
338 | f.close()
339 |
340 | for i,u in enumerate(upgrade_path):
341 | if u['from_version'] == version:
342 | break
343 | else:
344 | i = -1
345 |
346 | if i >=0:
347 | for u in upgrade_path[i:]:
348 | print ('-------------------------------------------------')
349 | print ('Migrating from {} to {}\n'.format(u['from_version'],u['to_version']))
350 | str_conf = u['migrate'](str_conf, u['to_version'])
351 |
352 |
353 | out_file = args.get('output') if args.get('output') else 'migrated-'+os.path.basename(args.get('config'))
354 | f = open ( out_file,'w')
355 | f.write(str_conf)
356 | f.close()
357 | print ('''
358 |
359 | ----------------------| NOTE |-------------------------
360 | The migration is best effort. May contain errors.
361 | Please review the modified file.
362 | Items commented out with #REMOVE can be deleted.
363 | Items marked with #NEW are new options to customize.
364 |
365 | ''')
366 |
367 | print ('Migrated config written to: {}'.format(out_file))
368 | else:
369 | print ('Nothing to migrate')
370 |
371 | if breaking:
372 | print("\nTHIS IS A BREAKING CHANGE RELEASE. THINGS WILL NOT WORK TILL YOU FOLLOW https://zmeventnotification.readthedocs.io/en/latest/guides/breaking.html#version-5-16-0-onwards\n")
373 |
374 | exit(0)
375 | #from_unknown_to_1_0()
376 |
--------------------------------------------------------------------------------
/zmeventnotification/zmeventnotification/debug_opencv.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | #
4 | # Script to debug opencv compile with CUDA support.
5 | #
6 |
7 | zip opencv *.log opencv.sh
8 |
9 | echo "##################################################################################"
10 | echo
11 | echo "Post the 'opencv.zip' file on the Unraid forum so we can help with debgging."
12 | echo "If you are unable to post on the Unraid forum, you can post it at"
13 | echo "https://github.com/dlandon/zoneminder as an issue."
14 | echo
15 | echo "##################################################################################"
16 |
--------------------------------------------------------------------------------
/zmeventnotification/zmeventnotification/objectconfig.ini:
--------------------------------------------------------------------------------
1 | # Configuration file for object detection
2 |
3 | # NOTE: ALL parameters here can be overriden
4 | # on a per monitor basis if you want. Just
5 | # duplicate it inside the correct [monitor-] section
6 |
7 | # You can create your own custom attributes in the [custom] section
8 |
9 | [general]
10 |
11 | # Please don't change this. It is used by the config upgrade script
12 | version=1.2
13 |
14 | # You can now limit the # of detection process
15 | # per target processor. If not specified, default is 1
16 | # Other detection processes will wait to acquire lock
17 |
18 | cpu_max_processes=3
19 | tpu_max_processes=1
20 | gpu_max_processes=1
21 |
22 | # Time to wait in seconds per processor to be free, before
23 | # erroring out. Default is 120 (2 mins)
24 | cpu_max_lock_wait=100
25 | tpu_max_lock_wait=100
26 | gpu_max_lock_wait=100
27 |
28 |
29 | #pyzm_overrides={'conf_path':'/etc/zm','log_level_debug':0}
30 | pyzm_overrides={'log_level_debug':5}
31 |
32 | # This is an optional file
33 | # If specified, you can specify tokens with secret values in that file
34 | # and onlt refer to the tokens in your main config file
35 | secrets = /etc/zm/secrets.ini
36 |
37 | # portal/user/password are needed if you plan on using ZM's legacy
38 | # auth mechanism to get images
39 | portal=!ZM_PORTAL
40 | user=!ZM_USER
41 | password=!ZM_PASSWORD
42 |
43 | # api portal is needed if you plan to use tokens to get images
44 | # requires ZM 1.33 or above
45 | api_portal=!ZM_API_PORTAL
46 |
47 | allow_self_signed=yes
48 | # if yes, last detection will be stored for monitors
49 | # and bounding boxes that match, along with labels
50 | # will be discarded for new detections. This may be helpful
51 | # in getting rid of static objects that get detected
52 | # due to some motion.
53 | match_past_detections=no
54 | # The max difference in area between the objects if match_past_detection is on
55 | # can also be specified in px like 300px. Default is 5%. Basically, bounding boxes of the same
56 | # object can slightly differ ever so slightly between detection. Contributor @neillbell put in this PR
57 | # to calculate the difference in areas and based on his tests, 5% worked well. YMMV. Change it if needed.
58 | past_det_max_diff_area=5%
59 |
60 | max_detection_size=90%
61 |
62 | # sequence of models to run for detection
63 | detection_sequence=object,face,alpr
64 | # if all, then we will loop through all models
65 | # if first then the first success will break out
66 | detection_mode=all
67 |
68 | # If you need basic auth to access ZM
69 | #basic_user=user
70 | #basic_password=password
71 |
72 | # base data path for various files the ES+OD needs
73 | # we support in config variable substitution as well
74 | base_data_path=/var/lib/zmeventnotification
75 |
76 | # global settings for
77 | # bestmatch, alarm, snapshot OR a specific frame ID
78 | frame_id=bestmatch
79 |
80 | # this is the to resize the image before analysis is done
81 | resize=800
82 | # set to yes, if you want to remove images after analysis
83 | # setting to yes is recommended to avoid filling up space
84 | # keep to no while debugging/inspecting masks
85 | # Note this does NOT delete debug images later
86 | delete_after_analyze=yes
87 |
88 | # If yes, will write an image called -bbox.jpg as well
89 | # which contains the bounding boxes. This has NO relation to
90 | # write_image_to_zm
91 | # Typically, if you enable delete_after_analyze you may
92 | # also want to set write_debug_image to no.
93 | write_debug_image=no
94 |
95 | # if yes, will write an image with bounding boxes
96 | # this needs to be yes to be able to write a bounding box
97 | # image to ZoneMinder that is visible from its console
98 | write_image_to_zm=yes
99 |
100 |
101 | # Adds percentage to detections
102 | # hog/face shows 100% always
103 | show_percent=yes
104 |
105 | # color to be used to draw the polygons you specified
106 | poly_color=(255,255,255)
107 | poly_thickness=2
108 | #import_zm_zones=yes
109 | only_triggered_zm_zones=no
110 |
111 | # This section gives you an option to get brief animations
112 | # of the event, delivered as part of the push notification to mobile devices
113 | # Animations are created only if an object is detected
114 | #
115 | # NOTE: This will DELAY the time taken to send you push notifications
116 | # It will try to first creat the animation, which may take upto a minute
117 | # depending on how soon it gets access to frames. See notes below
118 |
119 | [animation]
120 |
121 | # If yes, object detection will attempt to create
122 | # a short GIF file around the object detection frame
123 | # that can be sent via push notifications for instant playback
124 | # Note this required additional software support. Default:no
125 | create_animation=no
126 |
127 | # Format of animation burst
128 | # valid options are "mp4", "gif", "mp4,gif"
129 | # Note that gifs will be of a shorter duration
130 | # as they take up much more disk space than mp4
131 | animation_types='mp4,gif'
132 |
133 | # default width of animation image. Be cautious when you increase this
134 | # most mobile platforms give a very brief amount of time (in seconds)
135 | # to download the image.
136 | # Given your ZM instance will be serving the image, it will anyway be slow
137 | # Making the total animation size bigger resulted in the notification not
138 | # getting an image at all (timed out)
139 | animation_width=640
140 |
141 | # When an event is detected, ZM it writes frames a little late
142 | # On top of that, it looks like with caching enabled, the API layer doesn't
143 | # get access to DB records for much longer (around 30 seconds), at least on my
144 | # system. animation_retry_sleep refers to how long to wait before trying to grab
145 | # frame information if it failed. animation_max_tries defines how many times it
146 | # will try and retrieve frames before it gives up
147 | animation_retry_sleep=15
148 | animation_max_tries=4
149 |
150 | # if animation_types is gif then when can generate a fast preview gif
151 | # every second frame is skipped and the frame rate doubled
152 | # to give quick preview, Default (no)
153 | fast_gif=no
154 |
155 | [remote]
156 | # You can now run the machine learning code on a different server
157 | # This frees up your ZM server for other things
158 | # To do this, you need to setup https://github.com/pliablepixels/mlapi
159 | # on your desired server and confiure it with a user. See its instructions
160 | # once set up, you can choose to do object/face recognition via that
161 | # external serer
162 |
163 | # URL that will be used
164 | #ml_gateway=http://192.168.1.183:5000/api/v1
165 | #ml_gateway=http://10.6.1.13:5000/api/v1
166 | #ml_gateway=http://192.168.1.21:5000/api/v1
167 | #ml_gateway=http://10.9.0.2:5000/api/v1
168 | #ml_fallback_local=yes
169 | # API/password for remote gateway
170 | ml_user=!ML_USER
171 | ml_password=!ML_PASSWORD
172 |
173 |
174 | # config for object
175 | [object]
176 |
177 | # If you are using legacy format (use_sequence=no) then these parameters will
178 | # be used during ML inferencing
179 | object_detection_pattern=(person|car|motorbike|bus|truck|boat)
180 | object_min_confidence=0.3
181 | object_framework=coral_edgetpu
182 | object_processor=tpu
183 | object_weights={{base_data_path}}/models/coral_edgetpu/ssd_mobilenet_v2_coco_quant_postprocess_edgetpu.tflite
184 | object_labels={{base_data_path}}/models/coral_edgetpu/coco_indexed.names
185 |
186 | # If you are using the new ml_sequence format (use_sequence=yes) then
187 | # you can fiddle with these parameters and look at ml_sequence later
188 | # Note that these can be named anything. You can add custom variables, ad-infinitum
189 |
190 | # Google Coral
191 | tpu_object_weights={{base_data_path}}/models/coral_edgetpu/ssd_mobilenet_v2_coco_quant_postprocess_edgetpu.tflite
192 | tpu_object_labels={{base_data_path}}/models/coral_edgetpu/coco_indexed.names
193 | tpu_object_framework=coral_edgetpu
194 | tpu_object_processor=tpu
195 | tpu_min_confidence=0.6
196 |
197 | # Yolo v4 on GPU (falls back to CPU if no GPU)
198 | yolo4_object_weights={{base_data_path}}/models/yolov4/yolov4.weights
199 | yolo4_object_labels={{base_data_path}}/models/yolov4/coco.names
200 | yolo4_object_config={{base_data_path}}/models/yolov4/yolov4.cfg
201 | yolo4_object_framework=opencv
202 | yolo4_object_processor=gpu
203 |
204 | # Yolo v3 on GPU (falls back to CPU if no GPU)
205 | yolo3_object_weights={{base_data_path}}/models/yolov3/yolov3.weights
206 | yolo3_object_labels={{base_data_path}}/models/yolov3/coco.names
207 | yolo3_object_config={{base_data_path}}/models/yolov3/yolov3.cfg
208 | yolo3_object_framework=opencv
209 | yolo3_object_processor=gpu
210 |
211 | # Tiny Yolo V4 on GPU (falls back to CPU if no GPU)
212 | tinyyolo_object_config={{base_data_path}}/models/tinyyolov4/yolov4-tiny.cfg
213 | tinyyolo_object_weights={{base_data_path}}/models/tinyyolov4/yolov4-tiny.weights
214 | tinyyolo_object_labels={{base_data_path}}/models/tinyyolov4/coco.names
215 | tinyyolo_object_framework=opencv
216 | tinyyolo_object_processor=gpu
217 |
218 |
219 | [face]
220 | face_detection_pattern=.*
221 | known_images_path={{base_data_path}}/known_faces
222 | unknown_images_path={{base_data_path}}/unknown_faces
223 | save_unknown_faces=yes
224 | save_unknown_faces_leeway_pixels=100
225 | face_detection_framework=dlib
226 |
227 | # read https://github.com/ageitgey/face_recognition/wiki/Face-Recognition-Accuracy-Problems
228 | # read https://github.com/ageitgey/face_recognition#automatically-find-all-the-faces-in-an-image
229 | # and play around
230 |
231 | # quick overview:
232 | # num_jitters is how many times to distort images
233 | # upsample_times is how many times to upsample input images (for small faces, for example)
234 | # model can be hog or cnn. cnn may be more accurate, but I haven't found it to be
235 |
236 | face_num_jitters=1
237 | face_model=cnn
238 | face_upsample_times=1
239 |
240 | # This is maximum distance of the face under test to the closest matched
241 | # face cluster. The larger this distance, larger the chances of misclassification.
242 | #
243 | face_recog_dist_threshold=0.6
244 | # When we are first training the face recognition model with known faces,
245 | # by default we use hog because we assume you will supply well lit, front facing faces
246 | # However, if you are planning to train with profile photos or hard to see faces, you
247 | # may want to change this to cnn. Note that this increases training time, but training only
248 | # happens once, unless you retrain again by removing the training model
249 | face_train_model=cnn
250 | #if a face doesn't match known names, we will detect it as 'unknown face'
251 | # you can change that to something that suits your personality better ;-)
252 | #unknown_face_name=invader
253 |
254 | [alpr]
255 | alpr_detection_pattern=.*
256 | alpr_use_after_detection_only=yes
257 | # Many of the ALPR providers offer both a cloud version
258 | # and local SDK version. Sometimes local SDK format differs from
259 | # the cloud instance. Set this to local or cloud. Default cloud
260 | alpr_api_type=cloud
261 |
262 | # -----| If you are using plate recognizer | ------
263 | alpr_service=plate_recognizer
264 | #alpr_service=open_alpr_cmdline
265 |
266 | # If you want to host a local SDK https://app.platerecognizer.com/sdk/
267 | #alpr_url=http://192.168.1.21:8080/alpr
268 | # Plate recog replace with your api key
269 | alpr_key=!PLATEREC_ALPR_KEY
270 | # if yes, then it will log usage statistics of the ALPR service
271 | platerec_stats=yes
272 | # If you want to specify regions. See http://docs.platerecognizer.com/#regions-supported
273 | #platerec_regions=['us','cn','kr']
274 | # minimal confidence for actually detecting a plate
275 | platerec_min_dscore=0.1
276 | # minimal confidence for the translated text
277 | platerec_min_score=0.2
278 |
279 |
280 | # ----| If you are using openALPR |-----
281 | #alpr_service=open_alpr
282 | #alpr_key=!OPENALPR_ALPR_KEY
283 |
284 | # For an explanation of params, see http://doc.openalpr.com/api/?api=cloudapi
285 | #openalpr_recognize_vehicle=1
286 | #openalpr_country=us
287 | #openalpr_state=ca
288 | # openalpr returns percents, but we convert to between 0 and 1
289 | #openalpr_min_confidence=0.3
290 |
291 | # ----| If you are using openALPR command line |-----
292 |
293 | openalpr_cmdline_binary=alpr
294 |
295 | # Do an alpr -help to see options, plug them in here
296 | # like say '-j -p ca -c US' etc.
297 | # keep the -j because its JSON
298 |
299 | # Note that alpr_pattern is honored
300 | # For the rest, just stuff them in the cmd line options
301 |
302 | openalpr_cmdline_params=-j -d
303 | openalpr_cmdline_min_confidence=0.3
304 |
305 |
306 | ## Monitor specific settings
307 |
308 |
309 | # Examples:
310 | # Let's assume your monitor ID is 999
311 | [monitor-999]
312 | # my driveway
313 | match_past_detections=no
314 | wait=5
315 | object_detection_pattern=(person)
316 |
317 | # Advanced example - here we want anything except potted plant
318 | # exclusion in regular expressions is not
319 | # as straightforward as you may think, so
320 | # follow this pattern
321 | # object_detection_pattern = ^(?!object1|object2|objectN)
322 | # the characters in front implement what is
323 | # called a negative look ahead
324 |
325 | # object_detection_pattern=^(?!potted plant|pottedplant|bench|broccoli)
326 | #alpr_detection_pattern=^(.*x11)
327 | #delete_after_analyze=no
328 | #detection_pattern=.*
329 | #import_zm_zones=yes
330 |
331 | # polygon areas where object detection will be done.
332 | # You can name them anything except the keywords defined in the optional
333 | # params below. You can put as many polygons as you want per [monitor-]
334 | # (see examples).
335 |
336 | my_driveway=306,356 1003,341 1074,683 154,715
337 |
338 | # You are now allowed to specify detection pattern per zone
339 | # the format is _zone_detection_pattern=
340 | # So if your polygon is called my_driveway, its associated
341 | # detection pattern will be my_driveway_zone_detection_pattern
342 | # If none is specified, the value in object_detection_pattern
343 | # will be used
344 | # This also applies to ZM zones. Let's assume you have
345 | # import_zm_zones=yes and let's suppose you have a zone in ZM
346 | # called Front_Door. In that case, all you need to do is put in a
347 | # front_door_zone_detection_pattern=(person|car) here
348 | #
349 | # NOTE: ZM Zones are converted to lowercase, and spaces are replaced
350 | # with underscores@3
351 |
352 | my_driveway_zone_detection_pattern=(person)
353 | some_other_area=0,0 200,300 700,900
354 | # use license plate recognition for my driveway
355 | # see alpr section later for more data needed
356 | resize=no
357 | detection_sequence=object,alpr
358 |
359 |
360 | [ml]
361 | # When enabled, you can specify complex ML inferencing logic in ml_sequence
362 | # Anything specified in ml_sequence will override any other ml attributes
363 |
364 | # Also, when enabled, stream_sequence will override any other frame related
365 | # attributes
366 | use_sequence = yes
367 |
368 | # if enabled, will not grab exclusive locks before running inferencing
369 | # locking seems to cause issues on some unique file systems
370 | disable_locks= no
371 |
372 | # Chain of frames
373 | # See https://zmeventnotification.readthedocs.io/en/latest/guides/hooks.html#understanding-detection-configuration
374 | # Also see https://pyzm.readthedocs.io/en/latest/source/pyzm.html#pyzm.ml.detect_sequence.DetectSequence.detect_stream
375 | # Very important: Make sure final ending brace is indented
376 | stream_sequence = {
377 | 'frame_strategy': 'most_models',
378 | 'frame_set': 'snapshot,alarm',
379 | 'contig_frames_before_error': 5,
380 | 'max_attempts': 3,
381 | 'sleep_between_attempts': 4,
382 | 'resize':800
383 |
384 | }
385 |
386 | # Chain of ML models to use
387 | # See https://zmeventnotification.readthedocs.io/en/latest/guides/hooks.html#understanding-detection-configuration
388 | # Also see https://pyzm.readthedocs.io/en/latest/source/pyzm.html#pyzm.ml.detect_sequence.DetectSequence
389 | # Very important: Make sure final ending brace is indented
390 | ml_sequence= {
391 | 'general': {
392 | 'model_sequence': 'object,face,alpr',
393 | 'disable_locks': '{{disable_locks}}',
394 |
395 | },
396 | 'object': {
397 | 'general':{
398 | 'pattern':'{{object_detection_pattern}}',
399 | 'same_model_sequence_strategy': 'first' # also 'most', 'most_unique's
400 | },
401 | 'sequence': [{
402 | #First run on TPU with higher confidence
403 | 'object_weights':'{{tpu_object_weights}}',
404 | 'object_labels': '{{tpu_object_labels}}',
405 | 'object_min_confidence': {{tpu_min_confidence}},
406 | 'object_framework':'{{tpu_object_framework}}',
407 | 'tpu_max_processes': {{tpu_max_processes}},
408 | 'tpu_max_lock_wait': {{tpu_max_lock_wait}},
409 | 'max_detection_size':'{{max_detection_size}}'
410 |
411 |
412 | },
413 | {
414 | # YoloV4 on GPU if TPU fails (because sequence strategy is 'first')
415 | 'object_config':'{{yolo4_object_config}}',
416 | 'object_weights':'{{yolo4_object_weights}}',
417 | 'object_labels': '{{yolo4_object_labels}}',
418 | 'object_min_confidence': {{object_min_confidence}},
419 | 'object_framework':'{{yolo4_object_framework}}',
420 | 'object_processor': '{{yolo4_object_processor}}',
421 | 'gpu_max_processes': {{gpu_max_processes}},
422 | 'gpu_max_lock_wait': {{gpu_max_lock_wait}},
423 | 'cpu_max_processes': {{cpu_max_processes}},
424 | 'cpu_max_lock_wait': {{cpu_max_lock_wait}},
425 | 'max_detection_size':'{{max_detection_size}}'
426 |
427 | }]
428 | },
429 | 'face': {
430 | 'general':{
431 | 'pattern': '{{face_detection_pattern}}',
432 | 'same_model_sequence_strategy': 'first'
433 | },
434 | 'sequence': [{
435 | 'save_unknown_faces':'{{save_unknown_faces}}',
436 | 'save_unknown_faces_leeway_pixels':{{save_unknown_faces_leeway_pixels}},
437 | 'face_detection_framework': '{{face_detection_framework}}',
438 | 'known_images_path': '{{known_images_path}}',
439 | 'unknown_images_path': '{{unknown_images_path}}',
440 | 'face_model': '{{face_model}}',
441 | 'face_train_model': '{{face_train_model}}',
442 | 'face_recog_dist_threshold': '{{face_recog_dist_threshold}}',
443 | 'face_num_jitters': '{{face_num_jitters}}',
444 | 'face_upsample_times':'{{face_upsample_times}}',
445 | 'gpu_max_processes': {{gpu_max_processes}},
446 | 'gpu_max_lock_wait': {{gpu_max_lock_wait}},
447 | 'cpu_max_processes': {{cpu_max_processes}},
448 | 'cpu_max_lock_wait': {{cpu_max_lock_wait}},
449 | 'max_size':800
450 | }]
451 | },
452 |
453 | 'alpr': {
454 | 'general':{
455 | 'same_model_sequence_strategy': 'first',
456 | 'pre_existing_labels':['car', 'motorbike', 'bus', 'truck', 'boat'],
457 | 'pattern': '{{alpr_detection_pattern}}'
458 |
459 | },
460 | 'sequence': [{
461 | 'alpr_api_type': '{{alpr_api_type}}',
462 | 'alpr_service': '{{alpr_service}}',
463 | 'alpr_key': '{{alpr_key}}',
464 | 'platrec_stats': '{{platerec_stats}}',
465 | 'platerec_min_dscore': {{platerec_min_dscore}},
466 | 'platerec_min_score': {{platerec_min_score}},
467 | 'max_size':1600
468 | }]
469 | }
470 | }
471 |
472 |
--------------------------------------------------------------------------------
/zmeventnotification/zmeventnotification/opencv.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | #
4 | # Script to compile opencv with CUDA support.
5 | #
6 | #############################################################################################################################
7 | #
8 | # You need to prepare for compiling the opencv with CUDA support.
9 | #
10 | # You need to start with a clean docker image if you are going to recompile opencv.
11 | # This can be done by switching to "Advanced View" and clicking "Force Update",
12 | # or remove the Docker image then reinstall it.
13 | # Hook processing has to be enabled to run this script.
14 | #
15 | # Install the Unraid Nvidia plugin and be sure your graphics card can be seen in the
16 | # Zoneminder Docker. This will also be checked as part of the compile process.
17 | # You will not get a working compile if your graphics card is not seen. It may appear
18 | # to compile properly but will not work.
19 | #
20 | # Download the cuDNN run time and dev packages for your GPU configuration. You want the deb packages for Ubuntu 20.04.
21 | # You wll need to have an account with Nvidia to download these packages.
22 | # https://developer.nvidia.com/rdp/form/cudnn-download-survey
23 | # Place them in the /config/opencv/ folder.
24 | #
25 | CUDNN_RUN=libcudnn8_8.0.5.39-1+cuda11.1_amd64.deb
26 | CUDNN_DEV=libcudnn8-dev_8.0.5.39-1+cuda11.1_amd64.deb
27 | #
28 | # Download the cuda tools package. You want the deb package for Ubuntu 20.04.
29 | # https://developer.nvidia.com/cuda-downloads?target_os=Linux&target_arch=x86_64&target_distro=Ubuntu&target_version=2004&target_type=deblocal
30 | # Place the download in the /config/opencv/ folder.
31 | #
32 | CUDA_TOOL=cuda-repo-ubuntu2004-11-2-local_11.2.0-460.27.04-1_amd64.deb
33 | CUDA_PIN=cuda-ubuntu2004.pin
34 | CUDA_KEY=/var/cuda-repo-ubuntu2004-11-2-local/7fa2af80.pub
35 | CUDA_VER=11.2
36 | #
37 | #
38 | # Github URL for opencv zip file download.
39 | # Current default is to pull the version 4.5.0 release.
40 | # Note: You shouldn't need to change these.
41 | #
42 | OPENCV_VER=4.5.1
43 | OPENCV_URL=https://github.com/opencv/opencv/archive/$OPENCV_VER.zip
44 | OPENCV_CONTRIB_URL=https://github.com/opencv/opencv_contrib/archive/$OPENCV_VER.zip
45 | #
46 | # You can run this script in a quiet mode so it will run without any user interaction.
47 | #
48 | # Once you are satisfied that the compile is working, run the following command:
49 | # echo "yes" > opencv_ok
50 | #
51 | # The opencv.sh script will run when the Docker is updated so you won't have to do it manually.
52 | #
53 | #############################################################################################################################
54 |
55 | QUIET_MODE=$1
56 | if [[ $QUIET_MODE == 'quiet' ]]; then
57 | QUIET_MODE='yes'
58 | echo "Running in quiet mode."
59 | sleep 10
60 | else
61 | QUIET_MODE='no'
62 | fi
63 |
64 | #
65 | # Display warning.
66 | #
67 | if [ $QUIET_MODE != 'yes' ];then
68 | echo "##################################################################################"
69 | echo
70 | echo "This script will compile 'opencv' with GPU support."
71 | echo
72 | echo "WARNING:"
73 | echo "The compile process needs 15GB of disk (Docker image) free space, at least 4GB of"
74 | echo "memory, and will generate a huge Zoneminder Docker that is 10GB in size! The apt"
75 | echo "update will be disabled so you won't get Linux updates. Zoneminder will no"
76 | echo "longer update. In order to get updates you will have to force update, or remove"
77 | echo "and re-install the Zoneminder Docker and then re-compile 'opencv'."
78 | echo
79 | echo "There are several stopping points to give you a chance to see if the process is"
80 | echo "progressing without errors."
81 | echo
82 | echo "The compile script can take an hour or more to complete!"
83 | echo "Press any key to continue, or ctrl-C to stop."
84 | echo
85 | echo "##################################################################################"
86 | read -n 1 -s
87 | fi
88 |
89 | #
90 | # Remove log files.
91 | #
92 | rm -f /config/opencv/*.log
93 |
94 | #
95 | # Be sure we have enough disk space to compile opencv.
96 | #
97 | SPACE_AVAIL=`/bin/df / | /usr/bin/awk '{print $4}' | grep -v 'Available'`
98 | if [[ $((SPACE_AVAIL/1000)) -lt 15360 ]];then
99 | if [ $QUIET_MODE != 'yes' ];then
100 | echo
101 | echo "Not enough disk space to compile opencv!"
102 | echo "Expand your Docker image to leave 15GB of free space."
103 | echo "Force update or remove and re-install Zoneminder to allow more space if your compile did not complete."
104 | fi
105 | logger "Not enough disk space to compile opencv!" -tEventServer
106 | exit
107 | fi
108 |
109 | #
110 | # Check for enough memory to compile opencv.
111 | #
112 | MEM_AVAILABLE=`cat /proc/meminfo | grep MemAvailable | /usr/bin/awk '{print $2}'`
113 | if [[ $((MEM_AVAILABLE/1000)) -lt 4096 ]];then
114 | if [ $QUIET_MODE != 'yes' ];then
115 | echo
116 | echo "Not enough memory available to compile opencv!"
117 | echo "You should have at least 4GB available."
118 | echo "Check that you have not over committed SHM."
119 | echo "You can also stop Zoneminder to free up memory while you compile."
120 | echo " service zoneminder stop"
121 | fi
122 | logger "Not enough memory available to compile opencv!" -tEventServer
123 | exit
124 | fi
125 |
126 | #
127 | # Insure hook processing has been installed.
128 | #
129 | if [ "$INSTALL_HOOK" != "1" ]; then
130 | echo "Hook processing has to be installed before you can compile opencv!"
131 | exit
132 | fi
133 |
134 | #
135 | # Remove hook installed opencv module and face-recognition module
136 | #
137 | if [ "$INSTALL_FACE" == "1" ]; then
138 | pip3 uninstall -y face-recognition
139 | fi
140 |
141 | logger "Compiling opencv with GPU Support" -tEventServer
142 |
143 | #
144 | # Install cuda toolkit
145 | #
146 | logger "Installing cuda toolkit..." -tEventServer
147 | cd ~
148 | if [ -f /config/opencv/$CUDA_PIN ]; then
149 | cp /config/opencv/$CUDA_PIN /etc/apt/preferences.d/cuda-repository-pin-600
150 | else
151 | echo "Please download CUDA_PIN."
152 | logger "CUDA_PIN not downloaded!" -tEventServer
153 | exit
154 | fi
155 |
156 | if [ -f /config/opencv/$CUDA_TOOL ];then
157 | dpkg -i /config/opencv/$CUDA_TOOL
158 | else
159 | echo "Please download CUDA_TOOL package."
160 | logger "CUDA_TOOL package not downloaded!" -tEventServer
161 | exit
162 | fi
163 |
164 | apt-key add $CUDA_KEY >/dev/null
165 | apt-get update
166 | apt-get -y upgrade -o Dpkg::Options::="--force-confold"
167 | apt-get -y install cuda-toolkit-$CUDA_VER
168 |
169 | echo "export PATH=/usr/local/cuda/bin:$PATH" >/etc/profile.d/cuda.sh
170 | echo "export LD_LIBRARY_PATH=/usr/local/cuda/lib64:/usr/local/cuda/extras/CUPTI/lib64:/usr/local/lib:$LD_LIBRARY_PATH" >> /etc/profile.d/cuda.sh
171 | echo "export CUDADIR=/usr/local/cuda" >> /etc/profile.d/cuda.sh
172 | echo "export CUDA_HOME=/usr/local/cuda" >> /etc/profile.d/cuda.sh
173 | echo "/usr/local/cuda/lib64" > /etc/ld.so.conf.d/cuda.conf
174 | ldconfig
175 |
176 | #
177 | # check for expected install location
178 | #
179 | CUDADIR=/usr/local/cuda-$CUDA_VER
180 | if [ ! -d "$CUDADIR" ]; then
181 | echo "Failed to install cuda toolkit!"
182 | logger "Failed to install cuda toolkit!" -tEventServer
183 | exit
184 | elif [ ! -L "/usr/local/cuda" ]; then
185 | ln -s $CUDADIR /usr/local/cuda
186 | fi
187 |
188 | logger "Cuda toolkit installed" -tEventServer
189 |
190 | #
191 | # Ask user to check that the GPU is seen.
192 | #
193 | if [ -x /usr/bin/nvidia-smi ]; then
194 | /usr/bin/nvidia-smi >/config/opencv/nvidia-smi.log
195 | if [ $QUIET_MODE != 'yes' ];then
196 | echo "##################################################################################"
197 | echo
198 | cat /config/opencv/nvidia-smi.log
199 | echo "##################################################################################"
200 | echo "Verify your Nvidia GPU is seen and the driver is loaded."
201 | echo "If not, stop the script and fix the problem."
202 | echo "Press any key to continue, or ctrl-C to stop."
203 | read -n 1 -s
204 | fi
205 | else
206 | echo "'nvidia-smi' not found! Check that the Nvidia drivers are installed."
207 | logger "'nvidia-smi' not found! Check that the Nvidia drivers are installed." -tEventServer
208 | fi
209 | #
210 | # Install cuDNN run time and dev packages
211 | #
212 | logger "Installing cuDNN Package..." -tEventServer
213 | #
214 | if [ -f /config/opencv/$CUDNN_RUN ];then
215 | dpkg -i /config/opencv/$CUDNN_RUN
216 | else
217 | echo "Please download CUDNN_RUN package."
218 | logger "CUDNN_RUN package not downloaded!" -tEventServer
219 | exit
220 | fi
221 | if [ -f /config/opencv/$CUDNN_DEV ];then
222 | dpkg -i /config/opencv/$CUDNN_DEV
223 | else
224 | echo "Please download CUDNN_DEV package."
225 | logger "CUDNN_DEV package not downloaded!" -tEventServer
226 | exit
227 | fi
228 | logger "cuDNN Package installed" -tEventServer
229 |
230 | #
231 | # Compile opencv with cuda support
232 | #
233 | logger "Installing cuda support packages..." -tEventServer
234 | apt-get -y install libjpeg-dev libpng-dev libtiff-dev libavcodec-dev libavformat-dev libswscale-dev
235 | apt-get -y install libv4l-dev libxvidcore-dev libx264-dev libgtk-3-dev libatlas-base-dev gfortran
236 | logger "Cuda support packages installed" -tEventServer
237 |
238 | #
239 | # Get opencv source
240 | #
241 | logger "Downloading opencv source..." -tEventServer
242 | wget -q -O opencv.zip $OPENCV_URL
243 | wget -q -O opencv_contrib.zip $OPENCV_CONTRIB_URL
244 | unzip opencv.zip
245 | unzip opencv_contrib.zip
246 | mv $(ls -d opencv-*) opencv
247 | mv opencv_contrib-$OPENCV_VER opencv_contrib
248 | rm *.zip
249 |
250 | cd ~/opencv
251 | mkdir build
252 | cd build
253 | logger "Opencv source downloaded" -tEventServer
254 |
255 | #
256 | # Make opencv
257 | #
258 | logger "Compiling opencv..." -tEventServer
259 |
260 | #
261 | # Have user confirm that cuda and cudnn are enabled by the cmake.
262 | #
263 | cmake -D CMAKE_BUILD_TYPE=RELEASE \
264 | -D CMAKE_INSTALL_PREFIX=/usr/local \
265 | -D INSTALL_PYTHON_EXAMPLES=OFF \
266 | -D INSTALL_C_EXAMPLES=OFF \
267 | -D OPENCV_ENABLE_NONFREE=ON \
268 | -D WITH_CUDA=ON \
269 | -D WITH_CUDNN=ON \
270 | -D OPENCV_DNN_CUDA=ON \
271 | -D ENABLE_FAST_MATH=1 \
272 | -D CUDA_FAST_MATH=1 \
273 | -D WITH_CUBLAS=1 \
274 | -D OPENCV_EXTRA_MODULES_PATH=~/opencv_contrib/modules \
275 | -D HAVE_opencv_python3=ON \
276 | -D PYTHON_EXECUTABLE=/usr/bin/python3 \
277 | -D PYTHON2_EXECUTABLE=/usr/bin/python2 \
278 | -D BUILD_EXAMPLES=OFF .. >/config/opencv/cmake.log
279 |
280 | if [ $QUIET_MODE != 'yes' ];then
281 | echo "######################################################################################"
282 | echo
283 | cat /config/opencv/cmake.log
284 | echo
285 | echo "######################################################################################"
286 | echo "Verify that CUDA and cuDNN are both enabled in the cmake output above."
287 | echo "Look for the lines with CUDA and cuDNN."
288 | echo "You may have to scroll up the page to see them."
289 | echo "If those lines don't show 'YES', then stop the script and fix the problem."
290 | echo "Check that you have the correct versions of CUDA ond cuDNN for your GPU."
291 | echo "Press any key to continue, or ctrl-C to stop."
292 | read -n 1 -s
293 | fi
294 |
295 | make -j$(nproc)
296 |
297 | logger "Installing opencv..." -tEventServer
298 | make install
299 | ldconfig
300 |
301 | #
302 | # Now reinstall face-recognition package to ensure it detects GPU.
303 | #
304 | if [ "$INSTALL_FACE" == "1" ]; then
305 | pip3 install face-recognition
306 | fi
307 |
308 | #
309 | # Clean up/remove unnecessary packages
310 | #
311 | logger "Cleaning up..." -tEventServer
312 |
313 | cd ~
314 | rm -r opencv*
315 | rm /etc/my_init.d/20_apt_update.sh
316 |
317 | logger "Opencv compile completed" -tEventServer
318 |
319 | if [ $QUIET_MODE != 'yes' ];then
320 | echo "Compile is complete."
321 | echo "Now check that the cv2 module in python is working."
322 | echo "Execute the following commands:"
323 | echo " python3"
324 | echo " import cv2"
325 | echo " Ctrl-D to exit"
326 | echo
327 | echo "Verify that the import does not show errors."
328 | echo "If you don't see any errors, then you have successfully compiled opencv."
329 | echo
330 | echo "Once you are satisfied that the compile is working, run the following"
331 | echo "command:"
332 | echo " echo "yes" > opencv_ok"
333 | echo
334 | echo "The opencv.sh script will run when the Docker is updated so you won't"
335 | echo "have to do it manually."
336 | fi
337 |
--------------------------------------------------------------------------------
/zmeventnotification/zmeventnotification/pushapi_pushover.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 |
3 | version = 0.1
4 |
5 | # This is a sample python script for sending notifications over pushover
6 | # Write your own script to add a new push service, and modify
7 | # api_push_script in zmeventnotification.ini to invoke that script
8 |
9 | # Example taken from https://support.pushover.net/i44-example-code-and-pushover-libraries#python
10 |
11 | # Arguments passed
12 | # ARG1 = event ID
13 | # ARG2 = monitor ID
14 | # ARG3 = monitor name
15 | # ARG4 = Alarm cause
16 | # ARG5 = type of event (event_start or event_end)
17 | # ARG6 (Optional) = image path
18 |
19 | # ===============================================================
20 | # MODIFY THESE
21 | # ===============================================================
22 |
23 |
24 |
25 | # Look at https://pushover.net/api and put anything you want here
26 | # just don't add image, title and message as it gets automatically
27 | # populated later
28 |
29 | param_dict = {
30 | 'token': None, # Leave it as None to read from secrets or put a string value here
31 | 'user' : None, # Leave it as None to read from secrets or put a string value here
32 | #'sound':'tugboat',
33 | #'priority': 0,
34 | # 'device': 'a specific device',
35 | # 'url': 'http://whateeveryouwant',
36 | # 'url_title': 'My URL title',
37 |
38 | }
39 |
40 |
41 |
42 |
43 | # ========== Don't change anything below here, unless you know what you are doing
44 |
45 | import sys
46 | from datetime import datetime
47 | import requests
48 | import pyzm.ZMLog as zmlog
49 | import os
50 |
51 |
52 | # ES passes the image path, this routine figures out which image
53 | # to use inside that path
54 | def get_image(path, cause):
55 | # as of Mar 2020, pushover doesn't support
56 | # mp4
57 | if os.path.exists(path+'/objdetect.gif'):
58 | return path+'/objdetect.gif'
59 | elif os.path.exists(path+'/objdetect.jpg'):
60 | return path+'/objdetect.jpg'
61 | prefix = cause[0:2]
62 | if prefix == '[a]':
63 | return path+'/alarm.jpg'
64 | else:
65 | return path+'/snapshot.jpg'
66 |
67 | # Simple function to read variables from secret file
68 | def read_secrets(config='/etc/zm/secrets.ini'):
69 | from configparser import ConfigParser
70 | secrets_object = ConfigParser(interpolation=None, inline_comment_prefixes='#')
71 | secrets_object.optionxform=str
72 | zmlog.Debug(1,'eid:{} Reading secrets from {}'.format(eid,config))
73 | with open(config) as f:
74 | secrets_object.read_file(f)
75 | return secrets_object._sections['secrets']
76 |
77 | # -------- MAIN ---------------
78 | zmlog.init(name='zmeventnotification_pushapi')
79 | zmlog.Info('--------| Pushover Plugin v{} |--------'.format(version))
80 | if len(sys.argv) < 6:
81 | zmlog.Error ('Missing arguments, got {} arguments, was expecting at least 6: {}'.format(len(sys.argv)-1, sys.argv))
82 | zmlog.close()
83 | exit(1)
84 |
85 | eid = sys.argv[1]
86 | mid = sys.argv[2]
87 | mname = sys.argv[3]
88 | cause = sys.argv[4]
89 | event_type = sys.argv[5]
90 | image_path = None
91 | files = None
92 |
93 | if len(sys.argv) == 7:
94 | image_path = sys.argv[6]
95 | fname=get_image(image_path, cause)
96 |
97 | zmlog.Debug (1,'eid:{} Image to be used is: {}'.format(eid,fname))
98 | f,e=os.path.splitext(fname)
99 | if e.lower() == '.mp4':
100 | ctype = 'video/mp4'
101 | else:
102 | ctype = 'image/jpeg'
103 | zmlog.Debug (1,'Setting ctype to {} for extension {}'.format(ctype, e.lower()))
104 | files = {
105 | "attachment": ("image"+e.lower(), open(fname,"rb"), ctype)
106 | }
107 |
108 |
109 | if not param_dict['token'] or param_dict['user']:
110 | # read from secrets
111 | secrets = read_secrets()
112 | if not param_dict['token']:
113 | param_dict['token'] = secrets.get('PUSHOVER_APP_TOKEN')
114 | zmlog.Debug(1, "eid:{} Reading token from secrets".format(eid))
115 | if not param_dict['user']:
116 | param_dict['user'] = secrets.get('PUSHOVER_USER_KEY'),
117 | zmlog.Debug(1, "eid:{} Reading user from secrets".format(eid))
118 |
119 | param_dict['title'] = '{} Alarm ({})'.format(mname,eid)
120 | param_dict['message'] = cause + datetime.now().strftime(' at %I:%M %p, %b-%d')
121 | if event_type == 'event_end':
122 | param_dict['title'] = 'Ended:' + param_dict['title']
123 |
124 | disp_param_dict=param_dict.copy()
125 | disp_param_dict['token']=''
126 | disp_param_dict['user']=''
127 | zmlog.Debug (1, "eid:{} Pushover payload: data={} files={}".format(eid,disp_param_dict,files))
128 | r = requests.post("https://api.pushover.net/1/messages.json", data = param_dict, files = files)
129 | zmlog.Debug(1,"eid:{} Pushover returned:{}".format(eid, r.text))
130 | print(r.text)
131 | zmlog.close()
132 |
133 |
--------------------------------------------------------------------------------
/zmeventnotification/zmeventnotification/secrets.ini:
--------------------------------------------------------------------------------
1 | # your secrets file
2 | [secrets]
3 |
4 | # fid can have the following values:
5 | # a particular , alarm or snapshot
6 | # starting ZM 1.35, you can also specify
7 | # objdetect_mp4, objdetect_gif or objdetect_image
8 | # this needs create_animation enabled in objectconfig.ini and associated flags
9 | # If you keep it to objdetect, if you created a GIF file in objectconfig, then
10 | # a GIF file will be used else an image. If you opted for MP4 in objectconfig,
11 | # you need to change this to objdetect_mp4
12 |
13 | # Note that on Android, mp4/gif does not work. iOS only.
14 | ZMES_PICTURE_URL=https://portal/zm/index.php?view=image&eid=EVENTID&fid=objdetect&width=600
15 |
16 | #ZMES_PICTURE_URL=https://portal/zm/index.php?view=image&eid=EVENTID&fid=snapshot&width=600
17 | ZM_USER=user
18 | ZM_PASSWORD=password
19 | ES_ADMIN_INTERFACE_PASSWORD=your_admin_interface_password
20 |
21 | ZM_PORTAL=https://portal/zm
22 | ZM_API_PORTAL=https://portal/zm/api
23 | ES_CERT_FILE = /etc/apache2/ssl/zoneminder.crt
24 | ES_KEY_FILE = /etc/apache2/ssl/zoneminder.key
25 | ML_USER=your_mlapi_user
26 | ML_PASSWORD=your_mlapi_password
27 | PLATEREC_ALPR_KEY=your_plate_recognizer_api_key
28 | OPENALPR_ALPR_KEY=your_openalpr_api_key
29 |
30 | ESCONTROL_INTERFACE_PASSWORD=yourescontrolpassword
31 |
32 | MQTT_USERNAME=your_mqtt_username
33 | MQTT_PASSWORD=your_mqtt_password
34 |
35 | PUSHOVER_APP_TOKEN=your_pushover_app_token
36 | PUSHOVER_USER_KEY=your_pushover_user_key
37 |
--------------------------------------------------------------------------------
/zmeventnotification/zmeventnotification/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 |
3 | import io
4 | import os
5 | import re
6 | import codecs
7 |
8 | from setuptools import setup
9 |
10 | #Package meta-data
11 | NAME = 'zmes_hook_helpers'
12 | DESCRIPTION = 'ZoneMinder EventServer hook helper functions'
13 | URL = 'https://github.com/pliablepixels/zmeventserver/'
14 | AUTHOR_EMAIL = 'pliablepixels@gmail.com'
15 | AUTHOR = 'Pliable Pixels'
16 | LICENSE = 'GPL'
17 | INSTALL_REQUIRES = [
18 | 'numpy', 'requests', 'Shapely', 'imutils',
19 | 'pyzm==0.3.25', 'scikit-learn', 'future', 'imageio',
20 | 'imageio-ffmpeg','pygifsicle', 'Pillow'
21 | ]
22 |
23 | here = os.path.abspath(os.path.dirname(__file__))
24 | # read the contents of your README file
25 | with open(os.path.join(here, 'README.md'), encoding='utf-8') as f:
26 | long_description = f.read()
27 |
28 |
29 | def read(*parts):
30 | with codecs.open(os.path.join(here, *parts), 'r') as fp:
31 | return fp.read()
32 |
33 |
34 | def find_version(*file_paths):
35 | version_file = read(*file_paths)
36 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
37 | version_file, re.M)
38 | if version_match:
39 | return version_match.group(1)
40 | raise RuntimeError("Unable to find version string.")
41 |
42 |
43 | setup(name=NAME,
44 | version=find_version('zmes_hook_helpers', '__init__.py'),
45 | description=DESCRIPTION,
46 | author=AUTHOR,
47 | author_email=AUTHOR_EMAIL,
48 | long_description=long_description,
49 | long_description_content_type='text/markdown',
50 | url=URL,
51 | license=LICENSE,
52 | install_requires=INSTALL_REQUIRES,
53 | py_modules=[
54 | 'zmes_hook_helpers.common_params',
55 | 'zmes_hook_helpers.log',
56 | 'zmes_hook_helpers.image_manip',
57 | 'zmes_hook_helpers.apigw',
58 | 'zmes_hook_helpers.utils'
59 | ])
60 |
--------------------------------------------------------------------------------
/zmeventnotification/zmeventnotification/train_faces.py:
--------------------------------------------------------------------------------
1 |
2 | #!/usr/bin/python3
3 | import argparse
4 | import ssl
5 | import pyzm.ZMLog as log
6 | import zmes_hook_helpers.common_params as g
7 | import zmes_hook_helpers.utils as utils
8 | import pyzm.ml.face_train as train
9 |
10 |
11 | if __name__ == "__main__":
12 | g.ctx = ssl.create_default_context()
13 | ap = argparse.ArgumentParser()
14 | ap.add_argument('-c', '--config',default='/etc/zm/objectconfig.ini' , help='config file with path')
15 |
16 | args, u = ap.parse_known_args()
17 | args = vars(args)
18 |
19 | log.init(name='zm_face_train', dump_console=True)
20 | g.logger = log
21 | utils.process_config(args, g.ctx)
22 |
23 | train.FaceTrain(options=g.config).train()
24 |
25 |
--------------------------------------------------------------------------------
/zmeventnotification/zmeventnotification/zm_detect.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 |
3 | # Main detection script that loads different detection models
4 | # look at pyzm.ml for different detectors
5 |
6 | from __future__ import division
7 | import sys
8 | #lets do this _after_ log init so we log it
9 | #import cv2
10 | import argparse
11 | import datetime
12 | import os
13 | import numpy as np
14 | import re
15 | import imutils
16 | import ssl
17 | import pickle
18 | import json
19 | import time
20 | import requests
21 | import subprocess
22 | import traceback
23 | import ast
24 | # Modules that load cv2 will go later
25 | # so we can log misses
26 | import pyzm.ZMLog as log
27 | import zmes_hook_helpers.utils as utils
28 | import pyzm.helpers.utils as pyzmutils
29 | import zmes_hook_helpers.common_params as g
30 | from pyzm import __version__ as pyzm_version
31 | from zmes_hook_helpers import __version__ as hooks_version
32 |
33 |
34 | auth_header = None
35 |
36 |
37 |
38 | def remote_detect(stream=None, options=None, api=None):
39 | # This uses mlapi (https://github.com/pliablepixels/mlapi) to run inferencing and converts format to what is required by the rest of the code.
40 |
41 | import requests
42 | import cv2
43 |
44 | bbox = []
45 | label = []
46 | conf = []
47 | model = 'object'
48 | api_url = g.config['ml_gateway']
49 | g.logger.Info('Detecting using remote API Gateway {}'.format(api_url))
50 | login_url = api_url + '/login'
51 | object_url = api_url + '/detect/object?type='+model
52 | access_token = None
53 | global auth_header
54 |
55 | data_file = g.config['base_data_path'] + '/zm_login.json'
56 | if os.path.exists(data_file):
57 | g.logger.Debug(2,'Found token file, checking if token has not expired')
58 | with open(data_file) as json_file:
59 | data = json.load(json_file)
60 | generated = data['time']
61 | expires = data['expires']
62 | access_token = data['token']
63 | now = time.time()
64 | # lets make sure there is at least 30 secs left
65 | if int(now + 30 - generated) >= expires:
66 | g.logger.Debug(
67 | 1,'Found access token, but it has expired (or is about to expire)'
68 | )
69 | access_token = None
70 | else:
71 | g.logger.Debug(1,'Access token is valid for {} more seconds'.format(
72 | int(now - generated)))
73 | # Get API access token
74 | if not access_token:
75 | g.logger.Debug(1,'Invoking remote API login')
76 | r = requests.post(url=login_url,
77 | data=json.dumps({
78 | 'username': g.config['ml_user'],
79 | 'password': g.config['ml_password'],
80 |
81 |
82 | }),
83 | headers={'content-type': 'application/json'})
84 | data = r.json()
85 | access_token = data.get('access_token')
86 | if not access_token:
87 | raise ValueError('Error getting remote API token {}'.format(data))
88 | return
89 | g.logger.Debug(2,'Writing new token for future use')
90 | with open(data_file, 'w') as json_file:
91 | wdata = {
92 | 'token': access_token,
93 | 'expires': data.get('expires'),
94 | 'time': time.time()
95 | }
96 | json.dump(wdata, json_file)
97 | json_file.close()
98 |
99 | auth_header = {'Authorization': 'Bearer ' + access_token}
100 |
101 | params = {'delete': True, 'response_format': 'zm_detect'}
102 | files = {}
103 | #print (object_url)
104 |
105 |
106 | ml_overrides = {
107 | 'model_sequence':g.config['ml_sequence'].get('general',{}).get('model_sequence'),
108 | 'object': {
109 | 'pattern': g.config['ml_sequence'].get('object',{}).get('general',{}).get('pattern')
110 | },
111 | 'face': {
112 | 'pattern': g.config['ml_sequence'].get('face',{}).get('general',{}).get('pattern')
113 | },
114 | 'alpr': {
115 | 'pattern': g.config['ml_sequence'].get('alpr',{}).get('general',{}).get('pattern')
116 | },
117 | }
118 | g.logger.Debug(2,f'Invoking mlapi with url:{object_url} and json: stream={stream}, stream_options={options} ml_overrides={ml_overrides}')
119 | start = datetime.datetime.now()
120 |
121 | r = requests.post(url=object_url,
122 | headers=auth_header,
123 | params=params,
124 | files=files,
125 | json = {
126 | 'stream': stream,
127 | 'stream_options':options,
128 | 'ml_overrides':ml_overrides
129 | }
130 | )
131 | diff_time = (datetime.datetime.now() - start)
132 | g.logger.Debug(1,'remote detection inferencing took: {}'.format(diff_time))
133 | data = r.json()
134 | matched_data = data['matched_data']
135 | if g.config['write_image_to_zm'] == 'yes' and matched_data['frame_id']:
136 | url = '{}/index.php?view=image&eid={}&fid={}'.format(g.config['portal'], stream,matched_data['frame_id'] )
137 | g.logger.Debug(2,'Grabbing image from {} as we need to write objdetect.jpg'.format(url))
138 | try:
139 | response = api._make_request(url=url, type='get')
140 | img = np.asarray(bytearray(response.content), dtype='uint8')
141 | img = cv2.imdecode (img, cv2.IMREAD_COLOR)
142 | if options.get('resize') and options.get('resize') != 'no':
143 | img = imutils.resize(img,width=options.get('resize'))
144 | matched_data['image'] = img
145 |
146 | # we also need to recompute polygons scale as it was remotely done
147 | oldw = matched_data['image_dimensions']['original'][0]
148 | oldh = matched_data['image_dimensions']['original'][1]
149 | neww = matched_data['image_dimensions']['resized'][0]
150 | newh = matched_data['image_dimensions']['resized'][1]
151 | g.logger.Debug (2, 'Rescaling polygons for remote_detect {}x{} => {}x{}'.format(oldw,oldh, neww, newh))
152 | utils.rescale_polygons(neww / oldw, newh / oldh)
153 |
154 | except Exception as e:
155 | g.logger.Error ('Error during image grab: {}'.format(str(e)))
156 | g.logger.Debug(2,traceback.format_exc())
157 | return data['matched_data'], data['all_matches']
158 |
159 |
160 | def append_suffix(filename, token):
161 | f, e = os.path.splitext(filename)
162 | if not e:
163 | e = '.jpg'
164 | return f + token + e
165 |
166 |
167 | # main handler
168 |
169 | def main_handler():
170 | # set up logging to syslog
171 | # construct the argument parse and parse the arguments
172 |
173 | ap = argparse.ArgumentParser()
174 | ap.add_argument('-c', '--config', help='config file with path')
175 | ap.add_argument('-e', '--eventid', help='event ID to retrieve')
176 | ap.add_argument('-p',
177 | '--eventpath',
178 | help='path to store object image file',
179 | default='')
180 | ap.add_argument('-m', '--monitorid', help='monitor id - needed for mask')
181 | ap.add_argument('-v',
182 | '--version',
183 | help='print version and quit',
184 | action='store_true')
185 |
186 | ap.add_argument('-o', '--output-path',
187 | help='internal testing use only - path for debug images to be written')
188 |
189 | ap.add_argument('-f',
190 | '--file',
191 | help='internal testing use only - skips event download')
192 |
193 |
194 | ap.add_argument('-r', '--reason', help='reason for event (notes field in ZM)')
195 |
196 | ap.add_argument('-n', '--notes', help='updates notes field in ZM with detections', action='store_true')
197 | ap.add_argument('-d', '--debug', help='enables debug on console', action='store_true')
198 |
199 | args, u = ap.parse_known_args()
200 | args = vars(args)
201 |
202 | if args.get('version'):
203 | print('hooks:{} pyzm:{}'.format(hooks_version, pyzm_version))
204 | exit(0)
205 |
206 | if not args.get('config'):
207 | print ('--config required')
208 | exit(1)
209 |
210 | if not args.get('file')and not args.get('eventid'):
211 | print ('--eventid required')
212 | exit(1)
213 |
214 | utils.get_pyzm_config(args)
215 |
216 | if args.get('debug'):
217 | g.config['pyzm_overrides']['dump_console'] = True
218 | g.config['pyzm_overrides']['log_debug'] = True
219 | g.config['pyzm_overrides']['log_level_debug'] = 5
220 | g.config['pyzm_overrides']['log_debug_target'] = None
221 |
222 | if args.get('monitorid'):
223 | log.init(name='zmesdetect_' + 'm' + args.get('monitorid'), override=g.config['pyzm_overrides'])
224 | else:
225 | log.init(name='zmesdetect',override=g.config['pyzm_overrides'])
226 | g.logger = log
227 |
228 | es_version='(?)'
229 | try:
230 | es_version=subprocess.check_output(['/usr/bin/zmeventnotification.pl', '--version']).decode('ascii')
231 | except:
232 | pass
233 |
234 |
235 | try:
236 | import cv2
237 | except ImportError as e:
238 | g.logger.Fatal (f'{e}: You might not have installed OpenCV as per install instructions. Remember, it is NOT automatically installed')
239 |
240 | g.logger.Info('---------| pyzm version:{}, hook version:{}, ES version:{} , OpenCV version:{}|------------'.format(pyzm_version, hooks_version, es_version, cv2.__version__))
241 |
242 |
243 |
244 | # load modules that depend on cv2
245 | try:
246 | import zmes_hook_helpers.image_manip as img
247 | except Exception as e:
248 | g.logger.Error (f'{e}')
249 | exit(1)
250 | g.polygons = []
251 |
252 | # process config file
253 | g.ctx = ssl.create_default_context()
254 | utils.process_config(args, g.ctx)
255 |
256 |
257 | # misc came later, so lets be safe
258 | if not os.path.exists(g.config['base_data_path'] + '/misc/'):
259 | try:
260 | os.makedirs(g.config['base_data_path'] + '/misc/')
261 | except FileExistsError:
262 | pass # if two detects run together with a race here
263 |
264 | if not g.config['ml_gateway']:
265 | g.logger.Info('Importing local classes for Object/Face')
266 | import pyzm.ml.object as object_detection
267 |
268 | else:
269 | g.logger.Info('Importing remote shim classes for Object/Face')
270 | from zmes_hook_helpers.apigw import ObjectRemote, FaceRemote, AlprRemote
271 | # now download image(s)
272 |
273 |
274 | start = datetime.datetime.now()
275 |
276 | obj_json = []
277 |
278 | import pyzm.api as zmapi
279 | api_options = {
280 | 'apiurl': g.config['api_portal'],
281 | 'portalurl': g.config['portal'],
282 | 'user': g.config['user'],
283 | 'password': g.config['password'] ,
284 | 'logger': g.logger, # use none if you don't want to log to ZM,
285 | 'disable_ssl_cert_check': False if g.config['allow_self_signed']=='no' else True
286 | }
287 |
288 | g.logger.Info('Connecting with ZM APIs')
289 | zmapi = zmapi.ZMApi(options=api_options)
290 | stream = args.get('eventid') or args.get('file')
291 | ml_options = {}
292 | stream_options={}
293 | secrets = None
294 |
295 | if g.config['ml_sequence'] and g.config['use_sequence'] == 'yes':
296 | g.logger.Debug(2,'using ml_sequence')
297 | ml_options = g.config['ml_sequence']
298 | secrets = pyzmutils.read_config(g.config['secrets'])
299 | ml_options = pyzmutils.template_fill(input_str=ml_options, config=None, secrets=secrets._sections.get('secrets'))
300 | ml_options = ast.literal_eval(ml_options)
301 | g.config['ml_sequence'] = ml_options
302 | else:
303 | g.logger.Debug(2,'mapping legacy ml data from config')
304 | ml_options = utils.convert_config_to_ml_sequence()
305 | g.config['ml_sequence'] = ml_options
306 |
307 | if g.config['stream_sequence'] and g.config['use_sequence'] == 'yes': # new sequence
308 | g.logger.Debug(2,'using stream_sequence')
309 | stream_options = g.config['stream_sequence']
310 | stream_options = ast.literal_eval(stream_options)
311 | else: # legacy
312 | g.logger.Debug(2,'mapping legacy stream data from config')
313 | if g.config['detection_mode'] == 'all':
314 | g.config['detection_mode'] = 'most_models'
315 | frame_set = g.config['frame_id']
316 | if g.config['frame_id'] == 'bestmatch':
317 | if g.config['bestmatch_order'] == 's,a':
318 | frame_set = 'snapshot,alarm'
319 | else:
320 | frame_set = 'alarm,snapshot'
321 | stream_options['resize'] =int(g.config['resize']) if g.config['resize'] != 'no' else None
322 |
323 | stream_options['strategy'] = g.config['detection_mode']
324 | stream_options['frame_set'] = frame_set
325 | stream_options['disable_ssl_cert_check'] = False if g.config['allow_self_signed']=='no' else True
326 |
327 |
328 | # These are stream options that need to be set outside of supplied configs
329 | stream_options['api'] = zmapi
330 | stream_options['polygons'] = g.polygons
331 | g.config['stream_sequence'] = stream_options
332 |
333 |
334 | '''
335 | stream_options = {
336 | 'api': zmapi,
337 | 'download': False,
338 | 'frame_set': frame_set,
339 | 'strategy': g.config['detection_mode'],
340 | 'polygons': g.polygons,
341 | 'resize': int(g.config['resize']) if g.config['resize'] != 'no' else None
342 |
343 | }
344 | '''
345 |
346 |
347 | m = None
348 | matched_data = None
349 | all_data = None
350 |
351 | if not args['file'] and int(g.config['wait']) > 0:
352 | g.logger.Info('Sleeping for {} seconds before inferencing'.format(
353 | g.config['wait']))
354 | time.sleep(g.config['wait'])
355 |
356 | if g.config['ml_gateway']:
357 | stream_options['api'] = None
358 | stream_options['monitorid'] = args.get('monitorid')
359 | start = datetime.datetime.now()
360 | try:
361 | matched_data,all_data = remote_detect(stream=stream, options=stream_options, api=zmapi)
362 | diff_time = (datetime.datetime.now() - start)
363 | g.logger.Debug(1,'Total remote detection detection took: {}'.format(diff_time))
364 | except Exception as e:
365 | g.logger.Error ("Error with remote mlapi:{}".format(e))
366 | g.logger.Debug(2,traceback.format_exc())
367 |
368 | if g.config['ml_fallback_local'] == 'yes':
369 | g.logger.Debug (1, "Falling back to local detection")
370 | stream_options['api'] = zmapi
371 | from pyzm.ml.detect_sequence import DetectSequence
372 | m = DetectSequence(options=ml_options, logger=g.logger)
373 | matched_data,all_data = m.detect_stream(stream=stream, options=stream_options)
374 |
375 |
376 | else:
377 | from pyzm.ml.detect_sequence import DetectSequence
378 | m = DetectSequence(options=ml_options, logger=g.logger)
379 | matched_data,all_data = m.detect_stream(stream=stream, options=stream_options)
380 |
381 |
382 |
383 | #print(f'ALL FRAMES: {all_data}\n\n')
384 | #print (f"SELECTED FRAME {matched_data['frame_id']}, size {matched_data['image_dimensions']} with LABELS {matched_data['labels']} {matched_data['boxes']} {matched_data['confidences']}")
385 | #print (matched_data)
386 | '''
387 | matched_data = {
388 | 'boxes': matched_b,
389 | 'labels': matched_l,
390 | 'confidences': matched_c,
391 | 'frame_id': matched_frame_id,
392 | 'image_dimensions': self.media.image_dimensions(),
393 | 'image': matched_frame_img
394 | }
395 | '''
396 |
397 | # let's remove past detections first, if enabled
398 | if g.config['match_past_detections'] == 'yes' and args.get('monitorid'):
399 | # point detections to post processed data set
400 | g.logger.Info('Removing matches to past detections')
401 | bbox_t, label_t, conf_t = img.processPastDetection(
402 | matched_data['boxes'], matched_data['labels'], matched_data['confidences'], args.get('monitorid'))
403 | # save current objects for future comparisons
404 | g.logger.Debug(1,
405 | 'Saving detections for monitor {} for future match'.format(
406 | args.get('monitorid')))
407 | try:
408 | mon_file = g.config['image_path'] + '/monitor-' + args.get(
409 | 'monitorid') + '-data.pkl'
410 | f = open(mon_file, "wb")
411 | pickle.dump(matched_data['boxes'], f)
412 | pickle.dump(matched_data['labels'], f)
413 | pickle.dump(matched_data['confidences'], f)
414 | f.close()
415 | except Exception as e:
416 | g.logger.Error(f'Error writing to {mon_file}, past detections not recorded:{e}')
417 |
418 | matched_data['boxes'] = bbox_t
419 | matched_data['labels'] = label_t
420 | matched_data['confidences'] = conf_t
421 |
422 | obj_json = {
423 | 'labels': matched_data['labels'],
424 | 'boxes': matched_data['boxes'],
425 | 'frame_id': matched_data['frame_id'],
426 | 'confidences': matched_data['confidences'],
427 | 'image_dimensions': matched_data['image_dimensions']
428 | }
429 |
430 | # 'confidences': ["{:.2f}%".format(item * 100) for item in matched_data['confidences']],
431 |
432 | detections = []
433 | seen = {}
434 | pred=''
435 | prefix = ''
436 |
437 | if matched_data['frame_id'] == 'snapshot':
438 | prefix = '[s] '
439 | elif matched_data['frame_id'] == 'alarm':
440 | prefix = '[a] '
441 | else:
442 | prefix = '[x] '
443 | #g.logger.Debug (1,'CONFIDENCE ARRAY:{}'.format(conf))
444 | for idx, l in enumerate(matched_data['labels']):
445 | if l not in seen:
446 | if g.config['show_percent'] == 'no':
447 | pred = pred + l + ','
448 | else:
449 | pred = pred + l + ':{:.0%}'.format(matched_data['confidences'][idx]) + ' '
450 | seen[l] = 1
451 |
452 | if pred != '':
453 | pred = pred.rstrip(',')
454 | pred = prefix + 'detected:' + pred
455 | g.logger.Info('Prediction string:{}'.format(pred))
456 | jos = json.dumps(obj_json)
457 | g.logger.Debug(1,'Prediction string JSON:{}'.format(jos))
458 | print(pred + '--SPLIT--' + jos)
459 |
460 | if (matched_data['image'] is not None) and (g.config['write_image_to_zm'] == 'yes' or g.config['write_debug_image'] == 'yes'):
461 | debug_image = pyzmutils.draw_bbox(image=matched_data['image'],boxes=matched_data['boxes'],
462 | labels=matched_data['labels'], confidences=matched_data['confidences'],
463 | polygons=g.polygons, poly_thickness = g.config['poly_thickness'])
464 |
465 | if g.config['write_debug_image'] == 'yes':
466 | for _b in matched_data['error_boxes']:
467 | cv2.rectangle(debug_image, (_b[0], _b[1]), (_b[2], _b[3]),
468 | (0,0,255), 1)
469 | filename_debug = g.config['image_path']+'/'+os.path.basename(append_suffix(stream, '-{}-debug'.format(matched_data['frame_id'])))
470 | g.logger.Debug (1,'Writing bound boxes to debug image: {}'.format(filename_debug))
471 | cv2.imwrite(filename_debug,debug_image)
472 |
473 | if g.config['write_image_to_zm'] == 'yes' and args.get('eventpath'):
474 | g.logger.Debug(1,'Writing detected image to {}/objdetect.jpg'.format(
475 | args.get('eventpath')))
476 | cv2.imwrite(args.get('eventpath') + '/objdetect.jpg', debug_image)
477 | jf = args.get('eventpath')+ '/objects.json'
478 | g.logger.Debug(1,'Writing JSON output to {}'.format(jf))
479 | try:
480 | with open(jf, 'w') as jo:
481 | json.dump(obj_json, jo)
482 | jo.close()
483 | except Exception as e:
484 | g.logger.Error(f'Error creating {jf}:{e}')
485 |
486 | if args.get('notes'):
487 | url = '{}/events/{}.json'.format(g.config['api_portal'], args['eventid'])
488 | try:
489 | ev = zmapi._make_request(url=url, type='get')
490 | except Exception as e:
491 | g.logger.Error ('Error during event notes retrieval: {}'.format(str(e)))
492 | g.logger.Debug(2,traceback.format_exc())
493 | exit(0) # Let's continue with zmdetect
494 |
495 | new_notes = pred
496 | if ev.get('event',{}).get('Event',{}).get('Notes'):
497 | old_notes = ev['event']['Event']['Notes']
498 | old_notes_split = old_notes.split('Motion:')
499 | old_d = old_notes_split[0] # old detection
500 | try:
501 | old_m = old_notes_split[1]
502 | except IndexError:
503 | old_m = ''
504 | new_notes = pred + 'Motion:'+ old_m
505 | g.logger.Debug (1,'Replacing old note:{} with new note:{}'.format(old_notes, new_notes))
506 |
507 |
508 | payload = {}
509 | payload['Event[Notes]'] = new_notes
510 | try:
511 | ev = zmapi._make_request(url=url, payload=payload, type='put')
512 | except Exception as e:
513 | g.logger.Error ('Error during notes update: {}'.format(str(e)))
514 | g.logger.Debug(2,traceback.format_exc())
515 |
516 | if g.config['create_animation'] == 'yes':
517 | if not args.get('eventid'):
518 | g.logger.Error ('Cannot create animation as you did not pass an event ID')
519 | else:
520 | g.logger.Debug(1,'animation: Creating burst...')
521 | try:
522 | img.createAnimation(matched_data['frame_id'], args.get('eventid'), args.get('eventpath')+'/objdetect', g.config['animation_types'])
523 | except Exception as e:
524 | g.logger.Error('Error creating animation:{}'.format(e))
525 | g.logger.Error('animation: Traceback:{}'.format(traceback.format_exc()))
526 |
527 |
528 |
529 | if __name__ == '__main__':
530 | try:
531 | main_handler()
532 | except Exception as e:
533 | if g.logger:
534 | g.logger.Fatal('Unrecoverable error:{} Traceback:{}'.format(e,traceback.format_exc()))
535 | else:
536 | print('Unrecoverable error:{} Traceback:{}'.format(e,traceback.format_exc()))
537 | exit(1)
--------------------------------------------------------------------------------
/zmeventnotification/zmeventnotification/zm_event_end.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Just a dummy script for event end. Do what you want here
4 |
5 | # When invoked by zmeventnotification.pl it will be passed:
6 | # $1 = eventId that triggered an alarm
7 | # $2 = monitor ID of monitor that triggered an alarm
8 | # $3 = monitor Name of monitor that triggered an alarm
9 | # $4 = cause of alarm
10 |
11 | # If people run it as is, without modifying it, lets make sure we
12 | # return the cause back so its sent in the notification
13 | echo "${4}"
14 | #echo "$(date): POST EVENT FOR EID:${1} FOR MONITOR ${2} NAME ${3} CAUSE ${4}" > /tmp/post_log.txt
15 | exit 0
16 |
--------------------------------------------------------------------------------
/zmeventnotification/zmeventnotification/zm_event_start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # When invoked by zmeventnotification.pl it will be passed:
4 | # $1 = eventId that triggered an alarm
5 | # $2 = monitor ID of monitor that triggered an alarm
6 | # $3 = monitor Name of monitor that triggered an alarm
7 | # $4 = cause of alarm
8 | # $5 = path to event store (if store_frame_in_zm is 1)
9 |
10 |
11 |
12 | # Only tested with ZM 1.32.3+. May or may not work with older versions
13 | # Logic:
14 | # This script is invoked by zmeventnotification is you've specified its location in the hook= variable of zmeventnotification.pl
15 |
16 |
17 | # change this to the path of the object detection config"
18 | CONFIG_FILE="/etc/zm/objectconfig.ini"
19 | EVENT_PATH="$5"
20 | REASON="$4"
21 |
22 |
23 | # use arrays instead of strings to avoid quote hell
24 | DETECTION_SCRIPT=(/var/lib/zmeventnotification/bin/zm_detect.py --monitorid $2 --eventid $1 --config "${CONFIG_FILE}" --eventpath "${EVENT_PATH}" --reason "${REASON}" )
25 |
26 | RESULTS=$("${DETECTION_SCRIPT[@]}" | grep "detected:")
27 |
28 | _RETVAL=1
29 | # The script needs to return a 0 for success ( detected) or 1 for failure (not detected)
30 | if [[ ! -z "${RESULTS}" ]]; then
31 | _RETVAL=0
32 | fi
33 | echo ${RESULTS}
34 | exit ${_RETVAL}
35 |
--------------------------------------------------------------------------------
/zmeventnotification/zmeventnotification/zm_train_faces.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | import argparse
3 | import ssl
4 | import pyzm.ZMLog as log
5 | import zmes_hook_helpers.common_params as g
6 | import zmes_hook_helpers.utils as utils
7 |
8 | if __name__ == "__main__":
9 | log.init(name='zm_train_faces', override={'dump_console':True})
10 | # needs to be after log init
11 |
12 | import pyzm.ml.face_train as train
13 |
14 | if __name__ == "__main__":
15 | g.ctx = ssl.create_default_context()
16 | ap = argparse.ArgumentParser()
17 | ap.add_argument('-c',
18 | '--config',
19 | default='/etc/zm/objectconfig.ini',
20 | help='config file with path')
21 |
22 | ap.add_argument('-s',
23 | '--size',
24 | type=int,
25 | help='resize amount (if you run out of memory)')
26 |
27 | args, u = ap.parse_known_args()
28 | args = vars(args)
29 |
30 | #log.init(name='zm_face_train', dump_console=True)
31 | g.logger = log
32 | utils.process_config(args, g.ctx)
33 | train.FaceTrain(options=g.config).train(size=args['size'])
34 |
--------------------------------------------------------------------------------
/zmeventnotification/zmeventnotification/zmes_hook_helpers/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "6.1.5"
2 | VERSION=__version__
3 |
--------------------------------------------------------------------------------
/zmeventnotification/zmeventnotification/zmes_hook_helpers/apigw.py:
--------------------------------------------------------------------------------
1 | import zmes_hook_helpers.common_params as g
2 | import pyzm.ZMLog as log
3 | import sys
4 |
5 |
6 | class ObjectRemote:
7 | def __init__(self):
8 |
9 | class_file_abs_path = g.config['object_labels']
10 | f = open(class_file_abs_path, 'r')
11 | self.classes = [line.strip() for line in f.readlines()]
12 |
13 | def set_classes(self, classes):
14 | self.classes = classes
15 |
16 | def get_classes(self):
17 | return self.classes
18 |
19 |
20 | class FaceRemote:
21 | def __init__(self):
22 | self.classes = []
23 |
24 | def set_classes(self, classes):
25 | self.classes = classes
26 |
27 | def get_classes(self):
28 | return self.classes
29 |
30 |
31 | class AlprRemote:
32 | def __init__(self):
33 | self.classes = []
34 |
35 | def set_classes(self, classes):
36 | self.classes = classes
37 |
38 | def get_classes(self):
39 | return self.classes
--------------------------------------------------------------------------------
/zmeventnotification/zmeventnotification/zmes_hook_helpers/common_params.py:
--------------------------------------------------------------------------------
1 | # list of variables that are common
2 | # do not include model specific variables
3 |
4 | ctx = None # SSL context
5 | logger = None # logging handler
6 | config = {} # object that will hold config values
7 | polygons = [] # will contain mask(s) for a monitor
8 |
9 | # valid config keys and defaults
10 | config_vals = {
11 | 'version':{
12 | 'section': 'general',
13 | 'default': None,
14 | 'type': 'string',
15 | },
16 |
17 | 'cpu_max_processes':{
18 | 'section': 'general',
19 | 'default': '1',
20 | 'type': 'int',
21 | },
22 | 'gpu_max_processes':{
23 | 'section': 'general',
24 | 'default': '1',
25 | 'type': 'int',
26 | },
27 | 'tpu_max_processes':{
28 | 'section': 'general',
29 | 'default': '1',
30 | 'type': 'int',
31 | },
32 |
33 | 'cpu_max_lock_wait':{
34 | 'section': 'general',
35 | 'default': '120',
36 | 'type': 'int',
37 | },
38 |
39 | 'gpu_max_lock_wait':{
40 | 'section': 'general',
41 | 'default': '120',
42 | 'type': 'int',
43 | },
44 | 'tpu_max_lock_wait':{
45 | 'section': 'general',
46 | 'default': '120',
47 | 'type': 'int',
48 | },
49 |
50 |
51 |
52 | 'secrets':{
53 | 'section': 'general',
54 | 'default': None,
55 | 'type': 'string',
56 | },
57 | 'base_data_path': {
58 | 'section': 'general',
59 | 'default': '/var/lib/zmeventnotification',
60 | 'type': 'string'
61 | },
62 | 'pyzm_overrides': {
63 | 'section': 'general',
64 | 'default': "{}",
65 | 'type': 'dict',
66 |
67 | },
68 | 'portal':{
69 | 'section': 'general',
70 | 'default': '',
71 | 'type': 'string',
72 | },
73 | 'api_portal':{
74 | 'section': 'general',
75 | 'default': '',
76 | 'type': 'string',
77 | },
78 | 'user':{
79 | 'section': 'general',
80 | 'default': None,
81 | 'type': 'string'
82 | },
83 | 'password':{
84 | 'section': 'general',
85 | 'default': None,
86 | 'type': 'string'
87 | },
88 | 'basic_user':{
89 | 'section': 'general',
90 | 'default': '',
91 | 'type': 'string'
92 | },
93 |
94 | 'basic_password':{
95 | 'section': 'general',
96 | 'default': '',
97 | 'type': 'string'
98 | },
99 | 'image_path':{
100 | 'section': 'general',
101 | 'default': '/var/lib/zmeventnotification/images',
102 | 'type': 'string'
103 | },
104 |
105 | 'match_past_detections':{
106 | 'section': 'general',
107 | 'default': 'no',
108 | 'type': 'string'
109 | },
110 | 'past_det_max_diff_area':{
111 | 'section': 'general',
112 | 'default': '5%',
113 | 'type': 'string'
114 | },
115 | 'max_detection_size':{
116 | 'section': 'general',
117 | 'default': '',
118 | 'type': 'string'
119 | },
120 | 'frame_id':{
121 | 'section': 'general',
122 | 'default': 'snapshot',
123 | 'type': 'string'
124 | },
125 | 'bestmatch_order': {
126 | 'section':'general',
127 | 'default': 'a,s',
128 | 'type':'string',
129 | },
130 | 'wait': {
131 | 'section': 'general',
132 | 'default':'0',
133 | 'type': 'int'
134 | },
135 |
136 | 'resize':{
137 | 'section': 'general',
138 | 'default': 'no',
139 | 'type': 'string'
140 | },
141 | 'delete_after_analyze':{
142 | 'section': 'general',
143 | 'default': 'no',
144 | 'type': 'string',
145 | },
146 | 'show_percent':{
147 | 'section': 'general',
148 | 'default': 'no',
149 | 'type': 'string'
150 | },
151 | 'allow_self_signed':{
152 | 'section': 'general',
153 | 'default': 'yes',
154 | 'type': 'string'
155 | },
156 | 'write_image_to_zm':{
157 | 'section': 'general',
158 | 'default': 'yes',
159 | 'type': 'string'
160 | },
161 | 'write_debug_image':{
162 | 'section': 'general',
163 | 'default': 'yes',
164 | 'type': 'string'
165 | },
166 | 'detection_sequence':{
167 | 'section': 'general',
168 | 'default': 'object',
169 | 'type': 'str_split'
170 | },
171 | 'detection_mode': {
172 | 'section':'general',
173 | 'default':'all',
174 | 'type':'string'
175 | },
176 | 'import_zm_zones':{
177 | 'section': 'general',
178 | 'default': 'no',
179 | 'type': 'string',
180 | },
181 | 'only_triggered_zm_zones':{
182 | 'section': 'general',
183 | 'default': 'no',
184 | 'type': 'string',
185 | },
186 | 'poly_color':{
187 | 'section': 'general',
188 | 'default': '(127,140,141)',
189 | 'type': 'eval'
190 | },
191 | 'poly_thickness':{
192 | 'section': 'general',
193 | 'default': '2',
194 | 'type': 'int'
195 | },
196 |
197 | # animation for push
198 |
199 | 'create_animation':{
200 | 'section': 'animation',
201 | 'default': 'no',
202 | 'type': 'string'
203 | },
204 | 'animation_types':{
205 | 'section': 'animation',
206 | 'default': 'mp4',
207 | 'type': 'string'
208 | },
209 | 'animation_width':{
210 | 'section': 'animation',
211 | 'default': '400',
212 | 'type': 'int'
213 | },
214 | 'animation_retry_sleep':{
215 | 'section': 'animation',
216 | 'default': '15',
217 | 'type': 'int'
218 | },
219 | 'animation_max_tries':{
220 | 'section': 'animation',
221 | 'default': '3',
222 | 'type': 'int'
223 | },
224 | 'fast_gif':{
225 | 'section': 'animation',
226 | 'default': 'no',
227 | 'type': 'string'
228 | },
229 |
230 | # remote ML
231 |
232 |
233 | 'ml_gateway': {
234 | 'section': 'remote',
235 | 'default': None,
236 | 'type': 'string'
237 | },
238 |
239 | 'ml_fallback_local': {
240 | 'section': 'remote',
241 | 'default': 'no',
242 | 'type': 'string'
243 | },
244 |
245 | 'ml_user': {
246 | 'section': 'remote',
247 | 'default': None,
248 | 'type': 'string'
249 | },
250 | 'ml_password': {
251 | 'section': 'remote',
252 | 'default': None,
253 | 'type': 'string'
254 | },
255 |
256 | 'disable_locks': {
257 | 'section': 'ml',
258 | 'default': 'no',
259 | 'type': 'string'
260 | },
261 | 'use_sequence': {
262 | 'section': 'ml',
263 | 'default': 'no',
264 | 'type': 'string'
265 | },
266 | 'ml_sequence': {
267 | 'section': 'ml',
268 | 'default': None,
269 | 'type': 'string'
270 | },
271 | 'stream_sequence': {
272 | 'section': 'ml',
273 | 'default': None,
274 | 'type': 'string'
275 | },
276 |
277 |
278 |
279 | 'object_detection_pattern':{
280 | 'section': 'object',
281 | 'default': '.*',
282 | 'type': 'string'
283 | },
284 |
285 | 'object_framework':{
286 | 'section': 'object',
287 | 'default': 'opencv',
288 | 'type': 'string'
289 | },
290 | 'object_processor':{
291 | 'section': 'object',
292 | 'default': 'cpu',
293 | 'type': 'string'
294 | },
295 | 'object_config':{
296 | 'section': 'object',
297 | 'default': '/var/lib/zmeventnotification/models/yolov3/yolov3.cfg',
298 | 'type': 'string'
299 | },
300 | 'object_weights':{
301 | 'section': 'object',
302 | 'default': '/var/lib/zmeventnotification/models/yolov3/yolov3.weights',
303 | 'type': 'string'
304 | },
305 | 'object_labels':{
306 | 'section': 'object',
307 | 'default': '/var/lib/zmeventnotification/models/yolov3/yolov3_classes.txt',
308 | 'type': 'string'
309 | },
310 |
311 |
312 | 'object_min_confidence': {
313 | 'section': 'object',
314 | 'default': '0.4',
315 | 'type': 'float'
316 | },
317 |
318 | # Face
319 | 'face_detection_pattern':{
320 | 'section': 'face',
321 | 'default': '.*',
322 | 'type': 'string'
323 | },
324 | 'face_detection_framework':{
325 | 'section': 'face',
326 | 'default': 'dlib',
327 | 'type': 'string'
328 | },
329 | 'face_recognition_framework':{
330 | 'section': 'face',
331 | 'default': 'dlib',
332 | 'type': 'string'
333 | },
334 | 'face_processor': {
335 | 'section' : 'face',
336 | 'default' : 'cpu',
337 | 'type' : 'string'
338 | },
339 | 'face_num_jitters':{
340 | 'section': 'face',
341 | 'default': '0',
342 | 'type': 'int',
343 | },
344 | 'face_upsample_times':{
345 | 'section': 'face',
346 | 'default': '1',
347 | 'type': 'int',
348 | },
349 | 'face_model':{
350 | 'section': 'face',
351 | 'default': 'hog',
352 | 'type': 'string',
353 | },
354 | 'face_train_model':{
355 | 'section': 'face',
356 | 'default': 'hog',
357 | 'type': 'string',
358 | },
359 | 'face_recog_dist_threshold': {
360 | 'section': 'face',
361 | 'default': '0.6',
362 | 'type': 'float'
363 | },
364 | 'face_recog_knn_algo': {
365 | 'section': 'face',
366 | 'default': 'ball_tree',
367 | 'type': 'string'
368 | },
369 | 'known_images_path':{
370 | 'section': 'face',
371 | 'default': '/var/lib/zmeventnotification/known_faces',
372 | 'type': 'string',
373 | },
374 | 'unknown_images_path':{
375 | 'section': 'face',
376 | 'default': '/var/lib/zmeventnotification/unknown_faces',
377 | 'type': 'string',
378 | },
379 | 'unknown_face_name':{
380 | 'section': 'face',
381 | 'default': 'unknown face',
382 | 'type': 'string',
383 | },
384 | 'save_unknown_faces':{
385 | 'section': 'face',
386 | 'default': 'yes',
387 | 'type': 'string',
388 | },
389 |
390 | 'save_unknown_faces_leeway_pixels':{
391 | 'section': 'face',
392 | 'default': '50',
393 | 'type': 'int',
394 | },
395 |
396 | # generic ALPR
397 | 'alpr_service': {
398 | 'section': 'alpr',
399 | 'default': 'plate_recognizer',
400 | 'type': 'string',
401 | },
402 | 'alpr_detection_pattern':{
403 | 'section': 'alpr',
404 | 'default': '.*',
405 | 'type': 'string'
406 | },
407 | 'alpr_url': {
408 | 'section': 'alpr',
409 | 'default': None,
410 | 'type': 'string',
411 | },
412 | 'alpr_key': {
413 | 'section': 'alpr',
414 | 'default': '',
415 | 'type': 'string',
416 | },
417 | 'alpr_use_after_detection_only': {
418 | 'section': 'alpr',
419 | 'type': 'string',
420 | 'default': 'yes',
421 | },
422 | 'alpr_api_type':{
423 | 'section': 'alpr',
424 | 'default': 'cloud',
425 | 'type': 'string'
426 | },
427 |
428 | # Plate recognition specific
429 | 'platerec_stats':{
430 | 'section': 'alpr',
431 | 'default': 'no',
432 | 'type': 'string'
433 | },
434 |
435 |
436 | 'platerec_regions':{
437 | 'section': 'alpr',
438 | 'default': None,
439 | 'type': 'eval'
440 | },
441 | 'platerec_min_dscore':{
442 | 'section': 'alpr',
443 | 'default': '0.3',
444 | 'type': 'float'
445 | },
446 |
447 | 'platerec_min_score':{
448 | 'section': 'alpr',
449 | 'default': '0.5',
450 | 'type': 'float'
451 | },
452 |
453 | # OpenALPR specific
454 | 'openalpr_recognize_vehicle':{
455 | 'section': 'alpr',
456 | 'default': '0',
457 | 'type': 'int'
458 | },
459 | 'openalpr_country':{
460 | 'section': 'alpr',
461 | 'default': 'us',
462 | 'type': 'string'
463 | },
464 | 'openalpr_state':{
465 | 'section': 'alpr',
466 | 'default': None,
467 | 'type': 'string'
468 | },
469 |
470 | 'openalpr_min_confidence': {
471 | 'section': 'alpr',
472 | 'default': '0.3',
473 | 'type': 'float'
474 | },
475 |
476 | # OpenALPR command line specfic
477 |
478 | 'openalpr_cmdline_binary':{
479 | 'section': 'alpr',
480 | 'default': 'alpr',
481 | 'type': 'string'
482 | },
483 |
484 | 'openalpr_cmdline_params':{
485 | 'section': 'alpr',
486 | 'default': '-j',
487 | 'type': 'string'
488 | },
489 | 'openalpr_cmdline_min_confidence': {
490 | 'section': 'alpr',
491 | 'default': '0.3',
492 | 'type': 'float'
493 | },
494 |
495 |
496 | }
497 |
--------------------------------------------------------------------------------
/zmeventnotification/zmeventnotification/zmes_hook_helpers/image_manip.py:
--------------------------------------------------------------------------------
1 | from configparser import ConfigParser
2 | import zmes_hook_helpers.common_params as g
3 | from shapely.geometry import Polygon
4 | import cv2
5 | import numpy as np
6 | import pickle
7 | import re
8 | import requests
9 | import time
10 | import os
11 | import traceback
12 | import urllib.parse
13 | # Generic image related algorithms
14 |
15 |
16 | def createAnimation(frametype, eid, fname, types):
17 | import imageio
18 |
19 | url = '{}/index.php?view=image&width={}&eid={}&username={}&password={}'.format(g.config['portal'],g.config['animation_width'],eid,g.config['user'],urllib.parse.quote(g.config['password'], safe=''))
20 | api_url = '{}/events/{}.json?username={}&password={}'.format(g.config['api_portal'],eid,g.config['user'],urllib.parse.quote(g.config['password'], safe=''))
21 | disp_api_url='{}/events/{}.json?username={}&password=***'.format(g.config['api_portal'],eid,g.config['user'])
22 |
23 | rtries = g.config['animation_max_tries']
24 | sleep_secs = g.config['animation_retry_sleep']
25 | fid = None
26 | totframes = 0
27 | length = 0
28 | fps = 0
29 |
30 | target_fps = 2
31 | buffer_seconds = 5 #seconds
32 | fast_gif = False
33 | if g.config['fast_gif'] == 'yes':
34 | fast_gif = True
35 |
36 | while True and rtries:
37 | g.logger.Debug (1,f"animation: Try:{g.config['animation_max_tries']-rtries+1} Getting {disp_api_url}")
38 | r = None
39 | try:
40 | resp = requests.get(api_url)
41 | resp.raise_for_status()
42 | r = resp.json()
43 | except requests.exceptions.RequestException as e:
44 | g.logger.Error(f'{e}')
45 | continue
46 |
47 | r_event = r['event']['Event']
48 | r_frame = r['event']['Frame']
49 | r_frame_len = len(r_frame)
50 |
51 | if frametype == 'alarm':
52 | fid = int(r_event.get('AlarmFrameId'))
53 | elif frametype == 'snapshot':
54 | fid = int(r_event.get('MaxScoreFrameId'))
55 | else:
56 | fid = int(frametype)
57 |
58 | #g.logger.Debug (1,f'animation: Response {r}')
59 | if r_frame is None or not r_frame_len:
60 | g.logger.Debug (1,f'No frames found yet via API, deferring check for {sleep_secs} seconds...')
61 | rtries = rtries - 1
62 | time.sleep(sleep_secs)
63 | continue
64 |
65 | totframes=len(r_frame)
66 | total_time=round(float(r_frame[-1]['Delta']))
67 | fps=round(totframes/total_time)
68 |
69 | if not r_frame_len >= fid+fps*buffer_seconds:
70 | g.logger.Debug (1,f'I\'ve got {r_frame_len} frames, but that\'s not enough as anchor frame is type:{frametype}:{fid}, deferring check for {sleep_secs} seconds...')
71 | rtries = rtries - 1
72 | time.sleep(sleep_secs)
73 | continue
74 |
75 | g.logger.Debug (1,'animation: Got {} frames'.format(r_frame_len))
76 | break
77 | # fid is the anchor frame
78 | if not rtries:
79 | g.logger.Error ('animation: Bailing, failed too many times')
80 | return
81 |
82 |
83 |
84 | g.logger.Debug (1,'animation: event fps={}'.format(fps))
85 | start_frame = int(max(fid - (buffer_seconds*fps),1))
86 | end_frame = int(min(totframes, fid + (buffer_seconds*fps)))
87 | skip = round(fps/target_fps)
88 |
89 | g.logger.Debug (1,f'animation: anchor={frametype} start={start_frame} end={end_frame} skip={skip}')
90 | g.logger.Debug(1,'animation: Grabbing frames...')
91 | images = []
92 | od_images = []
93 |
94 | # use frametype (alarm/snapshot) to get od anchor, because fid can be wrong when translating from videos
95 | od_url= '{}/index.php?view=image&eid={}&fid={}&username={}&password={}&width={}'.format(g.config['portal'],eid,frametype,g.config['user'],urllib.parse.quote(g.config['password'], safe=''),g.config['animation_width'])
96 | g.logger.Debug (1,f'Grabbing anchor frame: {frametype}...')
97 | try:
98 | od_frame = imageio.imread(od_url)
99 | # 1 second @ 2fps
100 | od_images.append(od_frame)
101 | od_images.append(od_frame)
102 | except Exception as e:
103 | g.logger.Error (f'Error downloading anchor frame: Error:{e}')
104 |
105 | for i in range(start_frame, end_frame+1, skip):
106 | p_url=url+'&fid={}'.format(i)
107 | g.logger.Debug (2,f'animation: Grabbing Frame:{i}')
108 | try:
109 | images.append(imageio.imread(p_url))
110 | except Exception as e:
111 | g.logger.Error (f'Error downloading frame {i}: Error:{e}')
112 |
113 | g.logger.Debug (1,f'animation: Saving {fname}...')
114 | try:
115 | if 'mp4' in types.lower():
116 | g.logger.Debug (1,'Creating MP4...')
117 | mp4_final = od_images.copy()
118 | mp4_final.extend(images)
119 | imageio.mimwrite(fname+'.mp4', mp4_final, format='mp4', fps=target_fps)
120 | size = os.stat(fname+'.mp4').st_size
121 | g.logger.Debug (1,f'animation: saved to {fname}.mp4, size {size} bytes, frames: {len(images)}')
122 |
123 | if 'gif' in types.lower():
124 | from pygifsicle import optimize
125 | g.logger.Debug (1,'Creating GIF...')
126 |
127 | # Let's slice the right amount from images
128 | # GIF uses a +- 2 second buffer
129 | gif_buffer_seconds=2
130 | if fast_gif:
131 | gif_buffer_seconds = gif_buffer_seconds * 1.5
132 | target_fps = target_fps * 2
133 | gif_start_frame = int(max(fid - (gif_buffer_seconds*fps),1))
134 | gif_end_frame = int(min(totframes, fid + (gif_buffer_seconds*fps)))
135 | s1 = round((gif_start_frame - start_frame)/skip)
136 | s2 = round((end_frame - gif_end_frame)/skip)
137 | if s1 >=0 and s2 >=0:
138 | gif_images = None
139 | if fast_gif and 'gif' in types.lower():
140 | gif_images = images[0+s1:-s2:2]
141 | else:
142 | gif_images = images[0+s1:-s2]
143 | g.logger.Debug (1,f'For GIF, slicing {s1} to -{s2} from a total of {len(images)}')
144 | g.logger.Debug (1,'animation:Saving...')
145 | gif_final = gif_images.copy()
146 | imageio.mimwrite(fname+'.gif', gif_final, format='gif', fps=target_fps)
147 | g.logger.Debug (1,'animation:Optimizing...')
148 | optimize(source=fname+'.gif', colors=256)
149 | size = os.stat(fname+'.gif').st_size
150 | g.logger.Debug (1,f'animation: saved to {fname}.gif, size {size} bytes, frames:{len(gif_images)}')
151 | else:
152 | g.logger.Debug (1,f'Bailing in GIF creation, range is weird start:{s1}:end offset {-s2}')
153 |
154 |
155 |
156 | except Exception as e:
157 | g.logger.Error('animation: Traceback:{}'.format(traceback.format_exc()))
158 |
159 | # once all bounding boxes are detected, we check to see if any of them
160 | # intersect the polygons, if specified
161 | # it also makes sure only patterns specified in detect_pattern are drawn
162 | def processPastDetection(bbox, label, conf, mid):
163 |
164 | try:
165 | FileNotFoundError
166 | except NameError:
167 | FileNotFoundError = IOError
168 |
169 | if not mid:
170 | g.logger.Debug(1,
171 | 'Monitor ID not specified, cannot match past detections')
172 | return bbox, label, conf
173 | mon_file = g.config['image_path'] + '/monitor-' + mid + '-data.pkl'
174 | g.logger.Debug(2,'trying to load ' + mon_file)
175 | try:
176 | fh = open(mon_file, "rb")
177 | saved_bs = pickle.load(fh)
178 | saved_ls = pickle.load(fh)
179 | saved_cs = pickle.load(fh)
180 | except FileNotFoundError:
181 | g.logger.Debug(1,'No history data file found for monitor {}'.format(mid))
182 | return bbox, label, conf
183 | except EOFError:
184 | g.logger.Debug(1,'Empty file found for monitor {}'.format(mid))
185 | g.logger.Debug (1,'Going to remove {}'.format(mon_file))
186 | try:
187 | os.remove(mon_file)
188 | except Exception as e:
189 | g.logger.Error (f'Could not delete: {e}')
190 | pass
191 | except Exception as e:
192 | g.logger.Error(f'Error in processPastDetection: {e}')
193 | #g.logger.Error('Traceback:{}'.format(traceback.format_exc()))
194 | return bbox, label, conf
195 |
196 | # load past detection
197 |
198 | m = re.match('(\d+)(px|%)?$', g.config['past_det_max_diff_area'],
199 | re.IGNORECASE)
200 | if m:
201 | max_diff_area = int(m.group(1))
202 | use_percent = True if m.group(2) is None or m.group(
203 | 2) == '%' else False
204 | else:
205 | g.logger.Error('past_det_max_diff_area misformatted: {}'.format(
206 | g.config['past_det_max_diff_area']))
207 | return bbox, label, conf
208 |
209 | # it's very easy to forget to add 'px' when using pixels
210 | if use_percent and (max_diff_area < 0 or max_diff_area > 100):
211 | g.logger.Error(
212 | 'past_det_max_diff_area must be in the range 0-100 when using percentages: {}'
213 | .format(g.config['past_det_max_diff_area']))
214 | return bbox, label, conf
215 |
216 | #g.logger.Debug (1,'loaded past: bbox={}, labels={}'.format(saved_bs, saved_ls));
217 |
218 | new_label = []
219 | new_bbox = []
220 | new_conf = []
221 |
222 | for idx, b in enumerate(bbox):
223 | # iterate list of detections
224 | old_b = b
225 | it = iter(b)
226 | b = list(zip(it, it))
227 |
228 | b.insert(1, (b[1][0], b[0][1]))
229 | b.insert(3, (b[0][0], b[2][1]))
230 | #g.logger.Debug (1,"Past detection: {}@{}".format(saved_ls[idx],b))
231 | #g.logger.Debug (1,'BOBK={}'.format(b))
232 | obj = Polygon(b)
233 | foundMatch = False
234 | for saved_idx, saved_b in enumerate(saved_bs):
235 | # compare current detection element with saved list from file
236 | if saved_ls[saved_idx] != label[idx]: continue
237 | it = iter(saved_b)
238 | saved_b = list(zip(it, it))
239 | saved_b.insert(1, (saved_b[1][0], saved_b[0][1]))
240 | saved_b.insert(3, (saved_b[0][0], saved_b[2][1]))
241 | saved_obj = Polygon(saved_b)
242 | max_diff_pixels = max_diff_area
243 |
244 | if saved_obj.intersects(obj):
245 | if obj.contains(saved_obj):
246 | diff_area = obj.difference(saved_obj).area
247 | if use_percent:
248 | max_diff_pixels = obj.area * max_diff_area / 100
249 | else:
250 | diff_area = saved_obj.difference(obj).area
251 | if use_percent:
252 | max_diff_pixels = saved_obj.area * max_diff_area / 100
253 |
254 | if diff_area <= max_diff_pixels:
255 | g.logger.Debug(1,
256 | 'past detection {}@{} approximately matches {}@{} removing'
257 | .format(saved_ls[saved_idx], saved_b, label[idx], b))
258 | foundMatch = True
259 | break
260 | if not foundMatch:
261 | new_bbox.append(old_b)
262 | new_label.append(label[idx])
263 | new_conf.append(conf[idx])
264 |
265 | return new_bbox, new_label, new_conf
266 |
267 |
268 | def processFilters(bbox, label, conf, match, model):
269 | # bbox is the set of bounding boxes
270 | # labels are set of corresponding object names
271 | # conf are set of confidence scores (for face this is set to 1)
272 | # match contains the list of labels that will be allowed based on detect_pattern
273 | #g.logger.Debug (1,"PROCESS INTERSECTION {} AND {}".format(bbox,label))
274 | new_label = []
275 | new_bbox = []
276 | new_conf = []
277 |
278 |
279 |
280 | for idx, b in enumerate(bbox):
281 |
282 | doesIntersect = False
283 | # cv2 rectangle only needs top left and bottom right
284 | # but to check for polygon intersection, we need all 4 corners
285 | # b has [a,b,c,d] -> convert to [a,b, c,b, c,d, a,d]
286 | # https://stackoverflow.com/a/23286299/1361529
287 | old_b = b
288 | it = iter(b)
289 | b = list(zip(it, it))
290 | #g.logger.Debug (1,"BB={}".format(b))
291 | #g.logger.Debug (1,"BEFORE INSERT: {}".format(b))
292 | b.insert(1, (b[1][0], b[0][1]))
293 | b.insert(3, (b[0][0], b[2][1]))
294 | g.logger.Debug(2,"intersection: polygon in process={}".format(b))
295 | obj = Polygon(b)
296 |
297 |
298 |
299 | for p in g.polygons:
300 | poly = Polygon(p['value'])
301 | if obj.intersects(poly):
302 | if model == 'object' and p['pattern'] and p['pattern'] != g.config['object_detection_pattern']:
303 | g.logger.Debug(2, '{} polygon/zone has its own pattern of {}, using that'.format(p['name'],p['pattern']))
304 | r = re.compile(p['pattern'])
305 | match = list(filter(r.match, label))
306 | if label[idx] in match:
307 | g.logger.Debug(2,'{} intersects object:{}[{}]'.format(
308 | p['name'], label[idx], b))
309 | new_label.append(label[idx])
310 | new_bbox.append(old_b)
311 | new_conf.append(conf[idx])
312 | else:
313 | g.logger.Info(
314 | 'discarding "{}" as it does not match your filters'.
315 | format(label[idx]))
316 | g.logger.Debug(1,
317 | '{} intersects object:{}[{}] but does NOT match your detect pattern filter'
318 | .format(p['name'], label[idx], b))
319 | doesIntersect = True
320 | break
321 | # out of poly loop
322 | if not doesIntersect:
323 | g.logger.Info(
324 | 'object:{} at {} does not fall into any polygons, removing...'.
325 | format(label[idx], obj))
326 | #out of object loop
327 | return new_bbox, new_label, new_conf
328 |
329 |
330 | def getValidPlateDetections(bbox, label, conf):
331 | # FIXME: merge this into the function above and do it correctly
332 | # bbox is the set of bounding boxes
333 | # labels are set of corresponding object names
334 | # conf are set of confidence scores
335 |
336 | if not len(label):
337 | return bbox, label, conf
338 | new_label = []
339 | new_bbox = []
340 | new_conf = []
341 | g.logger.Debug(1,'Checking vehicle plates for validity')
342 |
343 | try:
344 | r = re.compile(g.config['alpr_detection_pattern'])
345 | except re.error:
346 | g.logger.Error('invalid pattern {}, using .*'.format(
347 | g.config['alpr_detection_pattern']))
348 | r = re.compile('.*')
349 |
350 | match = list(filter(r.match, label))
351 |
352 | for idx, b in enumerate(bbox):
353 | if not label[idx] in match:
354 | g.logger.Debug(1,
355 | 'discarding plate:{} as it does not match alpr filter pattern:{}'
356 | .format(label[idx], g.config['alpr_detection_pattern']))
357 | continue
358 |
359 | old_b = b
360 | it = iter(b)
361 | b = list(zip(it, it))
362 | #g.logger.Debug (1,"BB={}".format(b))
363 | b.insert(1, (b[1][0], b[0][1]))
364 | b.insert(3, (b[0][0], b[2][1]))
365 | #g.logger.Debug (1,"valid plate: polygon in process={}".format(b))
366 | obj = Polygon(b)
367 | doesIntersect = False
368 | for p in g.polygons:
369 | # g.logger.Debug (1,"valid plate: mask in process={}".format(p['value']))
370 | poly = Polygon(p['value'])
371 | # Lets make sure the license plate doesn't cover the full polygon area
372 | # if it did, its very likey a bogus reading
373 |
374 | if obj.intersects(poly):
375 | res = 'Plate:{} at {} intersects polygon:{} at {} '.format(
376 | label[idx], obj, p['name'], poly)
377 | if not obj.contains(poly):
378 | res = res + 'but does not contain polgyon, assuming it to be VALID'
379 | new_label.append(label[idx])
380 | new_bbox.append(old_b)
381 | new_conf.append(conf[idx])
382 | doesIntersect = True
383 | else:
384 | res = res + 'but also contains polygon, assuming it to be INVALID'
385 | g.logger.Debug(2,res)
386 | if doesIntersect: break
387 | # out of poly loop
388 | if not doesIntersect:
389 | g.logger.Debug(1,
390 | 'plate:{} at {} does not fall into any polygons, removing...'.
391 | format(label[idx], obj))
392 | #out of object loop
393 | return new_bbox, new_label, new_conf
394 |
395 |
396 | # draws bounding boxes of identified objects and polygons
397 |
398 |
399 | def draw_bbox(img,
400 | bbox,
401 | labels,
402 | classes,
403 | confidence,
404 | color=None,
405 | write_conf=True):
406 |
407 | # g.logger.Debug (1,"DRAW BBOX={} LAB={}".format(bbox,labels))
408 | slate_colors = [(39, 174, 96), (142, 68, 173), (0, 129, 254),
409 | (254, 60, 113), (243, 134, 48), (91, 177, 47)]
410 | # if no color is specified, use my own slate
411 | if color is None:
412 | # opencv is BGR
413 | bgr_slate_colors = slate_colors[::-1]
414 |
415 | polycolor = g.config['poly_color']
416 | # first draw the polygons, if any
417 | newh, neww = img.shape[:2]
418 |
419 | if g.config['poly_thickness']:
420 | for ps in g.polygons:
421 | cv2.polylines(img, [np.asarray(ps['value'])],
422 | True,
423 | polycolor,
424 | thickness=g.config['poly_thickness'])
425 |
426 | # now draw object boundaries
427 |
428 | arr_len = len(bgr_slate_colors)
429 | for i, label in enumerate(labels):
430 | #=g.logger.Debug (1,'drawing box for: {}'.format(label))
431 | color = bgr_slate_colors[i % arr_len]
432 | if write_conf and confidence:
433 | label += ' ' + str(format(confidence[i] * 100, '.2f')) + '%'
434 | # draw bounding box around object
435 |
436 | #g.logger.Debug (1,"DRAWING RECT={},{} {},{}".format(bbox[i][0], bbox[i][1],bbox[i][2], bbox[i][3]))
437 | cv2.rectangle(img, (bbox[i][0], bbox[i][1]), (bbox[i][2], bbox[i][3]),
438 | color, 2)
439 |
440 | # write text
441 | font_scale = 0.8
442 | font_type = cv2.FONT_HERSHEY_SIMPLEX
443 | font_thickness = 1
444 | #cv2.getTextSize(text, font, font_scale, thickness)
445 | text_size = cv2.getTextSize(label, font_type, font_scale,
446 | font_thickness)[0]
447 | text_width_padded = text_size[0] + 4
448 | text_height_padded = text_size[1] + 4
449 |
450 | r_top_left = (bbox[i][0], bbox[i][1] - text_height_padded)
451 | r_bottom_right = (bbox[i][0] + text_width_padded, bbox[i][1])
452 | cv2.rectangle(img, r_top_left, r_bottom_right, color, -1)
453 | #cv2.putText(image, text, (x, y), font, font_scale, color, thickness)
454 | # location of text is botom left
455 | cv2.putText(img, label, (bbox[i][0] + 2, bbox[i][1] - 2), font_type,
456 | font_scale, [255, 255, 255], font_thickness)
457 |
458 | return img
459 |
--------------------------------------------------------------------------------
/zmeventnotification/zmeventnotification/zmes_hook_helpers/log.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import logging.handlers
3 | import zmes_hook_helpers.common_params as g
4 | import pyzm.ZMLog as zmlog
5 | from inspect import getframeinfo, stack
6 |
7 |
8 | class wrapperLogger():
9 | def __init__(self, name, override, dump_console):
10 | zmlog.init(name=name, override=override)
11 | self.dump_console = dump_console
12 |
13 |
14 |
15 | def debug(self, msg, level=1):
16 | idx = min(len(stack()), 1)
17 | caller = getframeinfo(stack()[idx][0])
18 | zmlog.Debug(level, msg, caller)
19 | if (self.dump_console):
20 | print('CONSOLE:' + msg)
21 |
22 | def info(self, msg):
23 | idx = min(len(stack()), 1)
24 | caller = getframeinfo(stack()[idx][0])
25 | zmlog.Info(msg, caller)
26 | if (self.dump_console):
27 | print('CONSOLE:' + msg)
28 |
29 | def error(self, msg):
30 | idx = min(len(stack()), 1)
31 | caller = getframeinfo(stack()[idx][0])
32 | zmlog.Error(msg, caller)
33 | if (self.dump_console):
34 | print('CONSOLE:' + msg)
35 |
36 | def fatal(self, msg):
37 | idx = min(len(stack()), 1)
38 | caller = getframeinfo(stack()[idx][0])
39 | zmlog.Fatal(msg, caller)
40 | if (self.dump_console):
41 | print('CONSOLE:' + msg)
42 |
43 | def setLevel(self, level):
44 | pass
45 |
46 |
47 |
48 | def init(process_name=None, override={}, dump_console=False):
49 | g.logger = wrapperLogger(name=process_name, override=override, dump_console=dump_console)
50 |
--------------------------------------------------------------------------------
/zmeventnotification/zmeventnotification/zmes_hook_helpers/utils.py:
--------------------------------------------------------------------------------
1 | # utility functions that are not generic to a specific model
2 |
3 |
4 | from __future__ import division
5 | import logging
6 | import logging.handlers
7 | import sys
8 | import datetime
9 | import ssl
10 | import urllib
11 | import json
12 | import time
13 | import re
14 | import ast
15 | import urllib.parse
16 | import traceback
17 |
18 | from configparser import ConfigParser
19 | import zmes_hook_helpers.common_params as g
20 |
21 | from future import standard_library
22 | standard_library.install_aliases()
23 | from urllib.error import HTTPError
24 |
25 | #resize polygons based on analysis scale
26 |
27 |
28 |
29 |
30 | def convert_config_to_ml_sequence():
31 | ml_options={}
32 | for ds in g.config['detection_sequence']:
33 | if ds == 'object':
34 |
35 | ml_options['object'] = {
36 | 'general':{
37 | 'pattern': g.config['object_detection_pattern'],
38 | 'disable_locks': g.config['disable_locks'],
39 | 'same_model_sequence_strategy': 'first' # 'first' 'most', 'most_unique'
40 |
41 | },
42 | 'sequence': [{
43 | 'tpu_max_processes': g.config['tpu_max_processes'],
44 | 'tpu_max_lock_wait': g.config['tpu_max_lock_wait'],
45 | 'gpu_max_processes': g.config['gpu_max_processes'],
46 | 'gpu_max_lock_wait': g.config['gpu_max_lock_wait'],
47 | 'cpu_max_processes': g.config['cpu_max_processes'],
48 | 'cpu_max_lock_wait': g.config['cpu_max_lock_wait'],
49 | 'max_detection_size': g.config['max_detection_size'],
50 | 'object_config':g.config['object_config'],
51 | 'object_weights':g.config['object_weights'],
52 | 'object_labels': g.config['object_labels'],
53 | 'object_min_confidence': g.config['object_min_confidence'],
54 | 'object_framework':g.config['object_framework'],
55 | 'object_processor': g.config['object_processor'],
56 | }]
57 | }
58 | elif ds == 'face':
59 | ml_options['face'] = {
60 | 'general':{
61 | 'pattern': g.config['face_detection_pattern'],
62 | 'same_model_sequence_strategy': 'first',
63 | # 'pre_existing_labels':['person'],
64 | },
65 | 'sequence': [{
66 | 'tpu_max_processes': g.config['tpu_max_processes'],
67 | 'tpu_max_lock_wait': g.config['tpu_max_lock_wait'],
68 | 'gpu_max_processes': g.config['gpu_max_processes'],
69 | 'gpu_max_lock_wait': g.config['gpu_max_lock_wait'],
70 | 'cpu_max_processes': g.config['cpu_max_processes'],
71 | 'cpu_max_lock_wait': g.config['cpu_max_lock_wait'],
72 | 'face_detection_framework': g.config['face_detection_framework'],
73 | 'face_recognition_framework': g.config['face_recognition_framework'],
74 | 'face_processor': g.config['face_processor'],
75 | 'known_images_path': g.config['known_images_path'],
76 | 'face_model': g.config['face_model'],
77 | 'face_train_model':g.config['face_train_model'],
78 | 'unknown_images_path': g.config['unknown_images_path'],
79 | 'unknown_face_name': g.config['unknown_face_name'],
80 | 'save_unknown_faces': g.config['save_unknown_faces'],
81 | 'save_unknown_faces_leeway_pixels': g.config['save_unknown_faces_leeway_pixels'],
82 | 'face_recog_dist_threshold': g.config['face_recog_dist_threshold'],
83 | 'face_num_jitters': g.config['face_num_jitters'],
84 | 'face_upsample_times':g.config['face_upsample_times']
85 | }]
86 |
87 | }
88 | elif ds == 'alpr':
89 | ml_options['alpr'] = {
90 | 'general':{
91 | 'pattern': g.config['alpr_detection_pattern'],
92 | 'same_model_sequence_strategy': 'first',
93 | # 'pre_existing_labels':['person'],
94 | },
95 | 'sequence': [{
96 | 'tpu_max_processes': g.config['tpu_max_processes'],
97 | 'tpu_max_lock_wait': g.config['tpu_max_lock_wait'],
98 | 'gpu_max_processes': g.config['gpu_max_processes'],
99 | 'gpu_max_lock_wait': g.config['gpu_max_lock_wait'],
100 | 'cpu_max_processes': g.config['cpu_max_processes'],
101 | 'cpu_max_lock_wait': g.config['cpu_max_lock_wait'],
102 | 'alpr_service': g.config['alpr_service'],
103 | 'alpr_url': g.config['alpr_url'],
104 | 'alpr_key': g.config['alpr_key'],
105 | 'alpr_api_type': g.config['alpr_api_type'],
106 | 'platerec_stats': g.config['platerec_stats'],
107 | 'platerec_regions': g.config['platerec_regions'],
108 | 'platerec_min_dscore': g.config['platerec_min_dscore'],
109 | 'platerec_min_score': g.config['platerec_min_score'],
110 | 'openalpr_recognize_vehicle': g.config['openalpr_recognize_vehicle'],
111 | 'openalpr_country': g.config['openalpr_country'],
112 | 'openalpr_state': g.config['openalpr_state'],
113 | 'openalpr_min_confidence': g.config['openalpr_min_confidence'],
114 | 'openalpr_cmdline_binary': g.config['openalpr_cmdline_binary'],
115 | 'openalpr_cmdline_params': g.config['openalpr_cmdline_params'],
116 | 'openalpr_cmdline_min_confidence': g.config['openalpr_cmdline_min_confidence'],
117 | }]
118 |
119 | }
120 | ml_options['general'] = {
121 | 'model_sequence': ','.join(str(e) for e in g.config['detection_sequence'])
122 | #'model_sequence': 'object,face',
123 | }
124 | if g.config['detection_mode'] == 'all':
125 | g.logger.Debug(3, 'Changing detection_mode from all to most_models to adapt to new features')
126 | g.config['detection_mode'] = 'most_models'
127 | return ml_options
128 |
129 | def rescale_polygons(xfactor, yfactor):
130 | newps = []
131 | for p in g.polygons:
132 | newp = []
133 | for x, y in p['value']:
134 | newx = int(x * xfactor)
135 | newy = int(y * yfactor)
136 | newp.append((newx, newy))
137 | newps.append({'name': p['name'], 'value': newp, 'pattern': p['pattern']})
138 | g.logger.Debug(2,'resized polygons x={}/y={}: {}'.format(
139 | xfactor, yfactor, newps))
140 | g.polygons = newps
141 |
142 |
143 | # converts a string of cordinates 'x1,y1 x2,y2 ...' to a tuple set. We use this
144 | # to parse the polygon parameters in the ini file
145 |
146 |
147 | def str2tuple(str):
148 | return [tuple(map(int, x.strip().split(','))) for x in str.split(' ')]
149 |
150 |
151 | def str2arr(str):
152 | return [map(int, x.strip().split(',')) for x in str.split(' ')]
153 |
154 |
155 | def str_split(my_str):
156 | return [x.strip() for x in my_str.split(',')]
157 |
158 |
159 |
160 | # credit: https://stackoverflow.com/a/5320179
161 | def findWholeWord(w):
162 | return re.compile(r'\b({0})\b'.format(w), flags=re.IGNORECASE).search
163 |
164 |
165 | # Imports zone definitions from ZM
166 | def import_zm_zones(mid, reason):
167 |
168 | match_reason = False
169 | if reason:
170 | match_reason = True if g.config['only_triggered_zm_zones']=='yes' else False
171 | g.logger.Debug(2,'import_zm_zones: match_reason={} and reason={}'.format(match_reason, reason))
172 |
173 | url = g.config['portal'] + '/api/zones/forMonitor/' + mid + '.json'
174 | g.logger.Debug(2,'Getting ZM zones using {}?username=xxx&password=yyy&user=xxx&pass=yyy'.format(url))
175 | url = url + '?username=' + g.config['user']
176 | url = url + '&password=' + urllib.parse.quote(g.config['password'], safe='')
177 | url = url + '&user=' + g.config['user']
178 | url = url + '&pass=' + urllib.parse.quote(g.config['password'], safe='')
179 |
180 | if g.config['portal'].lower().startswith('https://'):
181 | main_handler = urllib.request.HTTPSHandler(context=g.ctx)
182 | else:
183 | main_handler = urllib.request.HTTPHandler()
184 |
185 | if g.config['basic_user']:
186 | g.logger.Debug(2,'Basic auth config found, associating handlers')
187 | password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
188 | top_level_url = g.config['portal']
189 | password_mgr.add_password(None, top_level_url, g.config['basic_user'],
190 | g.config['basic_password'])
191 | handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
192 | opener = urllib.request.build_opener(handler, main_handler)
193 |
194 | else:
195 | opener = urllib.request.build_opener(main_handler)
196 | try:
197 | input_file = opener.open(url)
198 | except HTTPError as e:
199 | g.logger.Error(f'HTTP Error in import_zm_zones:{e}')
200 | raise
201 | except Exception as e:
202 | g.logger.Error(f'General error in import_zm_zones:{e}')
203 | raise
204 |
205 | c = input_file.read()
206 | j = json.loads(c)
207 |
208 | # Now lets look at reason to see if we need to
209 | # honor ZM motion zones
210 |
211 |
212 | #reason_zones = [x.strip() for x in rz.split(',')]
213 | #g.logger.Debug(1,'Found motion zones provided in alarm cause: {}'.format(reason_zones))
214 |
215 | for item in j['zones']:
216 | #print ('********* ITEM TYPE {}'.format(item['Zone']['Type']))
217 | if item['Zone']['Type'] == 'Inactive':
218 | g.logger.Debug(2, 'Skipping {} as it is inactive'.format(item['Zone']['Name']))
219 | continue
220 | if match_reason:
221 | if not findWholeWord(item['Zone']['Name'])(reason):
222 | g.logger.Debug(1,'dropping {} as zones in alarm cause is {}'.format(item['Zone']['Name'], reason))
223 | continue
224 | item['Zone']['Name'] = item['Zone']['Name'].replace(' ','_').lower()
225 | g.logger.Debug(2,'importing zoneminder polygon: {} [{}]'.format(item['Zone']['Name'], item['Zone']['Coords']))
226 | g.polygons.append({
227 | 'name': item['Zone']['Name'],
228 | 'value': str2tuple(item['Zone']['Coords']),
229 | 'pattern': None
230 |
231 | })
232 |
233 |
234 |
235 | # downloaded ZM image files for future analysis
236 | def download_files(args):
237 | if int(g.config['wait']) > 0:
238 | g.logger.Info('Sleeping for {} seconds before downloading'.format(
239 | g.config['wait']))
240 | time.sleep(g.config['wait'])
241 |
242 |
243 | if g.config['portal'].lower().startswith('https://'):
244 | main_handler = urllib.request.HTTPSHandler(context=g.ctx)
245 | else:
246 | main_handler = urllib.request.HTTPHandler()
247 |
248 | if g.config['basic_user']:
249 | g.logger.Debug(2,'Basic auth config found, associating handlers')
250 | password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
251 | top_level_url = g.config['portal']
252 | password_mgr.add_password(None, top_level_url, g.config['basic_user'],
253 | g.config['basic_password'])
254 | handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
255 | opener = urllib.request.build_opener(handler, main_handler)
256 |
257 | else:
258 | opener = urllib.request.build_opener(main_handler)
259 |
260 | if g.config['frame_id'] == 'bestmatch':
261 | # download both alarm and snapshot
262 | filename1 = g.config['image_path'] + '/' + args.get(
263 | 'eventid') + '-alarm.jpg'
264 | filename1_bbox = g.config['image_path'] + '/' + args.get(
265 | 'eventid') + '-alarm-bbox.jpg'
266 | filename2 = g.config['image_path'] + '/' + args.get(
267 | 'eventid') + '-snapshot.jpg'
268 | filename2_bbox = g.config['image_path'] + '/' + args.get(
269 | 'eventid') + '-snapshot-bbox.jpg'
270 |
271 | url = g.config['portal'] + '/index.php?view=image&eid=' + args.get(
272 | 'eventid')+ '&fid=alarm'
273 | durl = g.config['portal'] + '/index.php?view=image&eid=' + args.get(
274 | 'eventid') + '&fid=alarm'
275 | if g.config['user']:
276 | url = url + '&username=' + g.config[
277 | 'user'] + '&password=' + urllib.parse.quote(
278 | g.config['password'], safe='')
279 | durl = durl + '&username=' + g.config['user'] + '&password=*****'
280 |
281 | g.logger.Debug(1,'Trying to download {}'.format(durl))
282 | try:
283 | input_file = opener.open(url)
284 | except HTTPError as e:
285 | g.logger.Error(e)
286 | raise
287 | with open(filename1, 'wb') as output_file:
288 | output_file.write(input_file.read())
289 | output_file.close()
290 |
291 | url = g.config['portal'] + '/index.php?view=image&eid=' + args.get(
292 | 'eventid') + '&fid=snapshot'
293 | durl = g.config['portal'] + '/index.php?view=image&eid=' + args.get(
294 | 'eventid') + '&fid=snapshot'
295 | if g.config['user']:
296 | url = url + '&username=' + g.config[
297 | 'user'] + '&password=' + urllib.parse.quote(
298 | g.config['password'], safe='')
299 | durl = durl + '&username=' + g.config['user'] + '&password=*****'
300 | g.logger.Debug(1,'Trying to download {}'.format(durl))
301 | try:
302 | input_file = opener.open(url)
303 | except HTTPError as e:
304 | g.logger.Error(e)
305 | raise
306 | with open(filename2, 'wb') as output_file:
307 | output_file.write(input_file.read())
308 | output_file.close()
309 |
310 | else:
311 | # only download one
312 | filename1 = g.config['image_path'] + '/' + args.get('eventid') + '.jpg'
313 | filename1_bbox = g.config['image_path'] + '/' + args.get(
314 | 'eventid') + '-bbox.jpg'
315 | filename2 = None
316 | filename2_bbox = None
317 |
318 | url = g.config['portal'] + '/index.php?view=image&eid=' + args.get(
319 | 'eventid') + '&fid=' + g.config['frame_id']
320 | durl = g.config['portal'] + '/index.php?view=image&eid=' + args.get(
321 | 'eventid') + '&fid=' + g.config['frame_id']
322 | if g.config['user']:
323 | url = url + '&username=' + g.config[
324 | 'user'] + '&password=' + urllib.parse.quote(
325 | g.config['password'], safe='')
326 | durl = durl + '&username=' + g.config['user'] + '&password=*****'
327 | g.logger.Debug(1,'Trying to download {}'.format(durl))
328 | input_file = opener.open(url)
329 | with open(filename1, 'wb') as output_file:
330 | output_file.write(input_file.read())
331 | output_file.close()
332 | return filename1, filename2, filename1_bbox, filename2_bbox
333 |
334 | def get_pyzm_config(args):
335 | g.config['pyzm_overrides'] = {}
336 | config_file = ConfigParser(interpolation=None, inline_comment_prefixes='#')
337 | config_file.read(args.get('config'))
338 | if config_file.has_option('general', 'pyzm_overrides'):
339 | pyzm_overrides = config_file.get('general', 'pyzm_overrides')
340 | g.config['pyzm_overrides'] = ast.literal_eval(pyzm_overrides) if pyzm_overrides else {}
341 |
342 |
343 | def process_config(args, ctx):
344 | # parse config file into a dictionary with defaults
345 |
346 | #g.config = {}
347 | has_secrets = False
348 | secrets_file = None
349 |
350 | def _correct_type(val, t):
351 | if t == 'int':
352 | return int(val)
353 | elif t == 'eval' or t == 'dict':
354 | return ast.literal_eval(val) if val else None
355 | elif t == 'str_split':
356 | return str_split(val) if val else None
357 | elif t == 'string':
358 | return val
359 | elif t == 'float':
360 | return float(val)
361 | else:
362 | g.logger.Error(
363 | 'Unknown conversion type {} for config key:{}'.format(
364 | e['type'], e['key']))
365 | return val
366 |
367 | def _set_config_val(k, v):
368 | # internal function to parse all keys
369 | #print (f'inside set_config_val with {k}={v}')
370 | if config_file.has_section(v['section']):
371 | val = config_file[v['section']].get(k, v.get('default'))
372 | else:
373 | val = v.get('default')
374 | g.logger.Debug(1,
375 | 'Section [{}] missing in config file, using key:{} default: {}'
376 | .format(v['section'], k, val))
377 |
378 | if val and val[0] == '!': # its a secret token, so replace
379 | g.logger.Debug(2,'Secret token found in config: {}'.format(val))
380 | if not has_secrets:
381 | raise ValueError(
382 | 'Secret token found, but no secret file specified')
383 | if secrets_file.has_option('secrets', val[1:]):
384 | vn = secrets_file.get('secrets', val[1:])
385 | #g.logger.Debug (1,'Replacing {} with {}'.format(val,vn))
386 | val = vn
387 | else:
388 | raise ValueError(
389 | 'secret token {} not found in secrets file {}'.format(
390 | val, secrets_filename))
391 |
392 | g.config[k] = _correct_type(val, v['type'])
393 | if k.find('password') == -1:
394 | dval = g.config[k]
395 | else:
396 | dval = '***********'
397 | #g.logger.Debug (1,'Config: setting {} to {}'.format(k,dval))
398 |
399 | # main
400 | try:
401 | config_file = ConfigParser(interpolation=None, inline_comment_prefixes='#')
402 | config_file.read(args.get('config'))
403 |
404 | if config_file.has_option('general', 'secrets'):
405 | secrets_filename = config_file.get('general', 'secrets')
406 | g.logger.Debug(1,'secret filename: {}'.format(secrets_filename))
407 | has_secrets = True
408 | g.config['secrets'] = secrets_filename
409 | secrets_file = ConfigParser(interpolation=None, inline_comment_prefixes='#')
410 | try:
411 | with open(secrets_filename) as f:
412 | secrets_file.read_file(f)
413 | except:
414 | raise
415 | else:
416 | g.logger.Debug(1,'No secrets file configured')
417 | # now read config values
418 |
419 | # first, fill in config with default values
420 | for k,v in g.config_vals.items():
421 | val = v.get('default', None)
422 | g.config[k] = _correct_type(val, v['type'])
423 | # now iterate the file
424 | for sec in config_file.sections():
425 | if sec.startswith('monitor-'):
426 | #g.logger.Debug(4, 'Skipping {} for now'.format(sec))
427 | continue
428 | if sec == 'secrets':
429 | continue
430 | for (k, v) in config_file.items(sec):
431 | if g.config_vals.get(k):
432 | _set_config_val(k,g.config_vals[k] )
433 | else:
434 | #g.logger.Debug(4, 'storing unknown attribute {}={}'.format(k,v))
435 | g.config[k] = v
436 | #_set_config_val(k,{'section': sec, 'default': None, 'type': 'string'} )
437 |
438 | if g.config['allow_self_signed'] == 'yes':
439 | ctx.check_hostname = False
440 | ctx.verify_mode = ssl.CERT_NONE
441 | g.logger.Debug(1,'allowing self-signed certs to work...')
442 | else:
443 | g.logger.Debug(1,'strict SSL cert checking is on...')
444 |
445 |
446 | g.polygons = []
447 | poly_patterns = []
448 |
449 |
450 | # Check if we have a custom overrides for this monitor
451 | g.logger.Debug(4,'Now checking for monitor overrides')
452 | if 'monitorid' in args and args.get('monitorid'):
453 | sec = 'monitor-{}'.format(args.get('monitorid'))
454 | if sec in config_file:
455 | # we have a specific section for this monitor
456 | for item in config_file[sec].items():
457 | k = item[0]
458 | v = item[1]
459 | g.config[k] = v
460 |
461 | if k.endswith('_zone_detection_pattern'):
462 | zone_name = k.split('_zone_detection_pattern')[0]
463 | g.logger.Debug(2, 'found zone specific pattern:{} storing'.format(zone_name))
464 | poly_patterns.append({'name': zone_name, 'pattern':v});
465 | continue
466 |
467 | if k in g.config_vals:
468 | # This means its a legit config key that needs to be overriden
469 | g.logger.Debug(4,
470 | '[{}] overrides key:{} with value:{}'.format(
471 | sec, k, v))
472 | g.config[k] = _correct_type(v,
473 | g.config_vals[k]['type'])
474 | else:
475 | # This means its a polygon for the monitor
476 | if k.startswith(('object_','face_', 'alpr_')):
477 | g.logger.Debug(2,'assuming {} is an ML sequence, adding to config'.format(k))
478 | else:
479 | if not g.config['only_triggered_zm_zones'] == 'yes':
480 | try:
481 | g.polygons.append({'name': k, 'value': str2tuple(v),'pattern': None})
482 | g.logger.Debug(2,'adding polygon: {} [{}]'.format(k, v ))
483 | except Exception as e:
484 | g.logger.Debug(3,'{}={} is not a polygon definition. Error was {}. Ignoring.'.format(k,v,e))
485 |
486 | else:
487 | g.logger.Debug (2,'ignoring polygon: {} as only_triggered_zm_zones is true'.format(k))
488 | # now import zones if needed
489 | # this should be done irrespective of a monitor section
490 | if g.config['only_triggered_zm_zones'] == 'yes':
491 | g.config['import_zm_zones'] = 'yes'
492 | if g.config['import_zm_zones'] == 'yes':
493 | import_zm_zones(args.get('monitorid'), args.get('reason'))
494 |
495 | # finally, iterate polygons and put in detection patterns
496 | for poly in g.polygons:
497 | for poly_pat in poly_patterns:
498 | if poly['name'] == poly_pat['name']:
499 | poly['pattern'] = poly_pat['pattern']
500 | g.logger.Debug(2, 'replacing match pattern for polygon:{} with: {}'.format( poly['name'],poly_pat['pattern'] ))
501 |
502 |
503 | else:
504 | g.logger.Info(
505 | 'Ignoring monitor specific settings, as you did not provide a monitor id'
506 | )
507 | except Exception as e:
508 | g.logger.Error('Error parsing config:{}'.format(args.get('config')))
509 | g.logger.Error('Error was:{}'.format(e))
510 | g.logger.Fatal('error: Traceback:{}'.format(traceback.format_exc()))
511 | exit(0)
512 |
513 | # Now lets make sure we take care of parameter substitutions {{}}
514 | g.logger.Debug (4,'Finally, doing parameter substitution')
515 | p = r'{{(\w+?)}}'
516 | for gk, gv in g.config.items():
517 | #input ('Continue')
518 | #print(f"PROCESSING {gk} {gv}")
519 | gv = '{}'.format(gv)
520 | #if not isinstance(gv, str):
521 | # continue
522 | while True:
523 | matches = re.findall(p,gv)
524 | replaced = False
525 | for match_key in matches:
526 | if match_key in g.config:
527 | replaced = True
528 | new_val = g.config[gk].replace('{{' + match_key + '}}',str(g.config[match_key]))
529 | #print ('replacing {} with {}'.format(g.config[gk], new_val))
530 | g.config[gk] = new_val
531 | gv = new_val
532 | if not replaced:
533 | break
534 |
535 | # Now munge config if testing args provide
536 | if args.get('file'):
537 | g.config['wait'] = 0
538 | g.config['write_image_to_zm'] = 'no'
539 | g.polygons = []
540 |
541 |
542 | if args.get('output_path'):
543 | g.logger.Debug (1,'Output path modified to {}'.format(args.get('output_path')))
544 | g.config['image_path'] = args.get('output_path')
545 | g.config['write_debug_image'] = 'yes'
546 |
547 |
548 |
--------------------------------------------------------------------------------
/zmeventnotification/zmeventnotification/zmeventnotification.ini:
--------------------------------------------------------------------------------
1 | # Configuration file for zmeventnotification.pl
2 | [general]
3 |
4 | secrets = /etc/zm/secrets.ini
5 | base_data_path=/var/lib/zmeventnotification
6 |
7 | # The ES now supports a means for a special kind of
8 | # websocket connection which can dynamically control ES
9 | # behaviour
10 | # Default is no
11 | use_escontrol_interface=no
12 |
13 | # this is where all escontrol admin overrides
14 | # will be stored.
15 | escontrol_interface_file=/var/lib/zmeventnotification/misc/escontrol_interface.dat
16 |
17 | # the password for accepting control interfaces
18 | escontrol_interface_password=!ESCONTROL_INTERFACE_PASSWORD
19 |
20 | # If you see the ES getting 'stuck' after several hours
21 | # see https://rt.cpan.org/Public/Bug/Display.html?id=131058
22 | # You can use restart_interval to have it automatically restart
23 | # every X seconds. (Default is 7200 = 2 hours) Set to 0 to disable this.
24 | # restart_interval = 432000
25 | restart_interval = 0
26 |
27 | # list of monitors which ES will ignore
28 | # Note that there is an attribute later that does
29 | # not process hooks for specific monitors. This one is different
30 | # It can be used to completely skip ES processing for the
31 | # monitors defined
32 | # skip_monitors = 2,3,4
33 |
34 | [network]
35 | # Port for Websockets connection (default: 9000).
36 | port = 9000
37 |
38 | [auth]
39 | # Check username/password against ZoneMinder database (default: yes).
40 | enable = yes
41 |
42 | # Authentication timeout, in seconds (default: 20).
43 | timeout = 20
44 |
45 | [push]
46 | # This is to enable sending push notifications via any 3rd party service.
47 | # Typically, if you enable this, you might want to turn off fcm
48 | # Note that zmNinja will only receive notifications via FCM, but other 3rd
49 | # party services have their own apps to get notifications
50 | use_api_push = no
51 |
52 | # This is the script that will send the notification
53 | # Some sample scripts are provided, write your own
54 | # Each script gets:
55 | # arg1 - event ID
56 | # arg2 - Monitor ID
57 | # arg3 - Monitor Name
58 | # arg4 - alarm cause
59 | # arg5 - Type of event (event_start or event_end)
60 | # arg6 (optional) - image path
61 |
62 | api_push_script=/var/lib/zmeventnotification/bin/pushapi_pushover.py
63 |
64 | [fcm]
65 | # Use FCM for messaging (default: yes).
66 | enable = yes
67 |
68 | # Use the new FCM V1 protocol (recommended)
69 | use_fcmv1 = yes
70 |
71 | # if yes, will replace notifications with the latest one
72 | # default: no
73 | replace_push_messages = no
74 |
75 | # Custom FCM API key. Uncomment if you are using
76 | # your own API key (most people will not need to uncomment)
77 | # api_key =
78 |
79 | # Auth token store location (default: /var/lib/zmeventnotification/push/tokens.txt).
80 | token_file = {{base_data_path}}/push/tokens.txt
81 |
82 | # Date format to use when sending notification
83 | # over push (FCM)
84 | # See https://metacpan.org/pod/POSIX::strftime::GNU
85 | # For example, a 24 hr format would be
86 | #date_format = %H:%M, %d-%b
87 |
88 | date_format = %I:%M %p, %d-%b
89 |
90 | # Set priority for android push. Default is default.
91 | # You can set it to default, min, low, high or max
92 | # There is weird foo going on here. If you set it to high,
93 | # and don't interact with push, users report after a while they
94 | # get delayed by Google. I haven't quite figured out what is the precise
95 | # value to put here to make sure it always reaches you. Also make sure
96 | # you read the zmES faq on delayed push
97 | fcm_android_priority = default
98 |
99 | # Use MQTT for messaging (default: no)
100 | [mqtt]
101 | enable = no
102 | # Allow you to set a custom MQTT topic name
103 | # default: zoneminder
104 | #topic = my topic name
105 |
106 | # MQTT server (default: 127.0.0.1)
107 | server = 127.0.0.1
108 |
109 | # Authenticate to MQTT server as user
110 | # username = !MQTT_USERNAME
111 |
112 | # Password
113 | # password = !MQTT_PASSWORD
114 |
115 | # Set retain flag on MQTT messages (default: no)
116 | retain = no
117 |
118 | # MQTT over TLS
119 | # Location to MQTT broker CA certificate. Uncomment this line will enable MQTT over TLS.
120 | # tls_ca = /config/certs/ca.pem
121 |
122 | # To enable 2-ways TLS, add client certificate and private key
123 | # Location to client certificate and private key
124 | # tls_cert = /config/es-pub.pem
125 | # tls_key = /config/es-key.pem
126 |
127 | # To allow insecure TLS (disable peer verifier), (default: no)
128 | # tls_insecure = yes
129 |
130 |
131 |
132 | [ssl]
133 | # Enable SSL (default: yes)
134 | enable = yes
135 |
136 | cert = !ES_CERT_FILE
137 | key = !ES_KEY_FILE
138 |
139 | #cert = /etc/apache2/ssl/zoneminder.crt
140 | #key = /etc/apache2/ssl/zoneminder.key
141 |
142 | # Location to SSL cert (no default).
143 | # cert = /etc/apache2/ssl/yourportal/zoneminder.crt
144 |
145 | # Location to SSL key (no default).
146 | # key = /etc/apache2/ssl/yourportal/zoneminder.key
147 |
148 | [customize]
149 | # Link to json file that has rules which can be customized
150 | # es_rules=/etc/zm/es_rules.json
151 |
152 | # Display messages to console (default: no).
153 | # Note that you can keep this to no and just
154 | # use --debug when running from CLI too
155 | console_logs = no
156 | # debug level for ES messages. Default 4. Note that this is
157 | # not controllable by ZM LOG_DEBUG_LEVEL as in Perl, ZM doesn't
158 | # support debug levels
159 | es_debug_level = 4
160 |
161 | # Interval, in seconds, after which we will check for new events (default: 5).
162 | event_check_interval = 5
163 |
164 | # Interval, in seconds, to reload known monitors (default: 300).
165 | monitor_reload_interval = 300
166 |
167 | # Read monitor alarm cause (Requires ZoneMinder >= 1.31.2, default: no)
168 | # Enabling this to 1 for lower versions of ZM will result in a crash
169 | read_alarm_cause = yes
170 |
171 | # Tag event IDs with the alarm (default: no).
172 | tag_alarm_event_id = yes
173 |
174 | # Use custom notification sound (default: no).
175 | use_custom_notification_sound = no
176 |
177 | # include picture in alarm (default: no).
178 | include_picture = yes
179 |
180 |
181 | # send event start notifications (default: yes)
182 | # If no, starting notifications will not be sent out
183 | send_event_start_notification = yes
184 |
185 | # send event end notifications (default: no)
186 | # Note that if you are using hooks for end notifications, they may change
187 | # the final decision. This needs to be yes if you want end notifications with
188 | # or without hooks
189 | send_event_end_notification = no
190 |
191 | # URL to access the event image
192 | # This URL can be anything you want
193 | # What I've put here is a way to extract an image with the highest score given an eventID (even one that is recording)
194 | # This requires the latest version of index.php which was merged on Oct 9, 2018 and may only work in ZM 1.32+
195 | # https://github.com/ZoneMinder/zoneminder/blob/master/web/index.php
196 | # If you use this URL as I've specified below, keep the EVENTID phrase intact.
197 | # The notification server will replace it with the correct eid of the alarm
198 |
199 | # BESTMATCH should be used only if you are using bestmatch for FID in detect_wrapper.sh
200 | # objdetect is ONLY available in ZM 1.33+
201 | # objdetect_mp4 and objdetect_gif is ONLY available
202 | # in ZM 1.35+
203 | picture_url = !ZMES_PICTURE_URL
204 | picture_portal_username=!ZM_USER
205 | picture_portal_password=!ZM_PASSWORD
206 |
207 | # This is a master on/off setting for hooks. If it is set to no
208 | # hooks will not be used no matter what is set in the [hook] section
209 | # This makes it easy for folks not using hooks to just turn this off
210 | # default:no
211 |
212 | use_hooks = no
213 |
214 | [hook]
215 |
216 | # NOTE: This entire section is only valid if use_hooks is yes above
217 |
218 | # Shell script name here to be called every time an alarm is detected
219 | # the script will get passed $1=alarmEventID, $2=alarmMonitorId
220 | # $3 monitor Name, $4 alarm cause
221 | # script needs to return 0 to send alarm (default: none)
222 | #
223 |
224 | # This script is called when an event first starts. If the script returns "0"
225 | # (success), then a notification is sent to channels specified in
226 | # event_start_notify_on_hook_success. If the script returns "1" (fail)
227 | # then a notification is sent to channels specified in
228 | # event_start_notify_on_hook_fail
229 | event_start_hook = '{{base_data_path}}/bin/zm_event_start.sh'
230 |
231 | #This script is called after event_start_hook completes. You can do
232 | # your housekeeping work here
233 | #event_start_hook_notify_userscript = '{{base_data_path}}/contrib/example.py'
234 |
235 |
236 | # This script is called when an event ends. If the script returns "0"
237 | # (success), then a notification is sent to channels specified in
238 | # event_end_notify_on_hook_success. If the script returns "1" (fail)
239 | # then a notification is sent to channels specified in
240 | # event_end_notify_on_hook_fail
241 | event_end_hook = '{{base_data_path}}/bin/zm_event_end.sh'
242 |
243 | #This script is called after event_end_hook completes. You can do
244 | # your housekeeping work here
245 | #event_end_hook_notify_userscript = '{{base_data_path}}/contrib/example.py'
246 |
247 |
248 | # Possible channels = web,fcm,mqtt,api
249 | # all is short for web,fcm,mqtt,api
250 | # use none for no notifications, or comment out the attribute
251 |
252 | # When an event starts and hook returns 0, send notification to all. Default: none
253 | event_start_notify_on_hook_success = all
254 |
255 | # When an event starts and hook returns 1, send notification only to desktop. Default: none
256 | event_start_notify_on_hook_fail = none
257 |
258 | # When an event ends and hook returns 0, send notification to fcm,web,api. Default: none
259 | event_end_notify_on_hook_success = fcm,web,api
260 |
261 | # When an event ends and hook returns 1, don't send notifications. Default: none
262 | event_end_notify_on_hook_fail = none
263 | #event_end_notify_on_hook_fail = web
264 |
265 | # Since event_end and event_start are two different hooks, it is entirely possible
266 | # that you can get an end notification but not a start notification. This can happen
267 | # if your start script returns 1 but the end script returns 0, for example. To avoid
268 | # this, set this to yes (default:yes)
269 | event_end_notify_if_start_success = yes
270 |
271 | # If yes, the text returned by the script
272 | # overwrites the alarm header
273 | # useful if your script is detecting people, for example
274 | # and you want that to be shown in your notification (default:yes)
275 | use_hook_description = yes
276 |
277 | # If yes will will append an [a] for alarmed frame match
278 | # [s] for snapshot match or [x] if not using bestmatch
279 | # really only a debugging feature but useful to know
280 | # where object detection is working or failing
281 | keep_frame_match_type = yes
282 |
283 | # list of monitors for which hooks will not run
284 | # hook_skip_monitors = 2
285 |
286 |
287 | # if enabled, will pass the right folder for the hook script
288 | # to store the detected image, so it shows up in ZM console view too
289 | # Requires ZM >=1.33. Don't enable this if you are running an older version
290 |
291 | # Note: you also need to set write_image_to_zm=yes in objectconfig.ini
292 | # default: no
293 | hook_pass_image_path = yes
294 |
295 |
296 |
--------------------------------------------------------------------------------