├── .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 | --------------------------------------------------------------------------------