├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── build.sh
├── containerbuild.sh
├── convert.sh
├── nativecontainerbuild.sh
├── release.sh
├── src
├── instrumentation.rs
├── main.rs
├── ml.rs
├── vio.rs
├── zoneminder.rs
└── zoneminder
│ ├── db.rs
│ └── shm.rs
├── yolov4-tiny.cfg
├── yolov4-tiny.weights
└── zm-aidect@.service
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | .idea
3 | artifact
4 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "adler"
7 | version = "1.0.2"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
10 |
11 | [[package]]
12 | name = "ahash"
13 | version = "0.7.6"
14 | source = "registry+https://github.com/rust-lang/crates.io-index"
15 | checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
16 | dependencies = [
17 | "getrandom",
18 | "once_cell",
19 | "version_check",
20 | ]
21 |
22 | [[package]]
23 | name = "aho-corasick"
24 | version = "0.7.18"
25 | source = "registry+https://github.com/rust-lang/crates.io-index"
26 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
27 | dependencies = [
28 | "memchr",
29 | ]
30 |
31 | [[package]]
32 | name = "anyhow"
33 | version = "1.0.58"
34 | source = "registry+https://github.com/rust-lang/crates.io-index"
35 | checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704"
36 |
37 | [[package]]
38 | name = "ascii"
39 | version = "1.0.0"
40 | source = "registry+https://github.com/rust-lang/crates.io-index"
41 | checksum = "bbf56136a5198c7b01a49e3afcbef6cf84597273d298f54432926024107b0109"
42 |
43 | [[package]]
44 | name = "atty"
45 | version = "0.2.14"
46 | source = "registry+https://github.com/rust-lang/crates.io-index"
47 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
48 | dependencies = [
49 | "hermit-abi",
50 | "libc",
51 | "winapi",
52 | ]
53 |
54 | [[package]]
55 | name = "autocfg"
56 | version = "1.1.0"
57 | source = "registry+https://github.com/rust-lang/crates.io-index"
58 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
59 |
60 | [[package]]
61 | name = "base64"
62 | version = "0.13.0"
63 | source = "registry+https://github.com/rust-lang/crates.io-index"
64 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
65 |
66 | [[package]]
67 | name = "bindgen"
68 | version = "0.59.2"
69 | source = "registry+https://github.com/rust-lang/crates.io-index"
70 | checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8"
71 | dependencies = [
72 | "bitflags",
73 | "cexpr",
74 | "clang-sys",
75 | "lazy_static",
76 | "lazycell",
77 | "peeking_take_while",
78 | "proc-macro2",
79 | "quote",
80 | "regex",
81 | "rustc-hash",
82 | "shlex",
83 | ]
84 |
85 | [[package]]
86 | name = "bitflags"
87 | version = "1.3.2"
88 | source = "registry+https://github.com/rust-lang/crates.io-index"
89 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
90 |
91 | [[package]]
92 | name = "bitvec"
93 | version = "0.22.3"
94 | source = "registry+https://github.com/rust-lang/crates.io-index"
95 | checksum = "5237f00a8c86130a0cc317830e558b966dd7850d48a953d998c813f01a41b527"
96 | dependencies = [
97 | "funty",
98 | "radium",
99 | "tap",
100 | "wyz",
101 | ]
102 |
103 | [[package]]
104 | name = "block-buffer"
105 | version = "0.10.2"
106 | source = "registry+https://github.com/rust-lang/crates.io-index"
107 | checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
108 | dependencies = [
109 | "generic-array",
110 | ]
111 |
112 | [[package]]
113 | name = "bufstream"
114 | version = "0.1.4"
115 | source = "registry+https://github.com/rust-lang/crates.io-index"
116 | checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8"
117 |
118 | [[package]]
119 | name = "byteorder"
120 | version = "1.4.3"
121 | source = "registry+https://github.com/rust-lang/crates.io-index"
122 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
123 |
124 | [[package]]
125 | name = "bytes"
126 | version = "1.1.0"
127 | source = "registry+https://github.com/rust-lang/crates.io-index"
128 | checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
129 |
130 | [[package]]
131 | name = "cc"
132 | version = "1.0.73"
133 | source = "registry+https://github.com/rust-lang/crates.io-index"
134 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
135 | dependencies = [
136 | "jobserver",
137 | ]
138 |
139 | [[package]]
140 | name = "cexpr"
141 | version = "0.6.0"
142 | source = "registry+https://github.com/rust-lang/crates.io-index"
143 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
144 | dependencies = [
145 | "nom",
146 | ]
147 |
148 | [[package]]
149 | name = "cfg-if"
150 | version = "1.0.0"
151 | source = "registry+https://github.com/rust-lang/crates.io-index"
152 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
153 |
154 | [[package]]
155 | name = "chrono"
156 | version = "0.4.19"
157 | source = "registry+https://github.com/rust-lang/crates.io-index"
158 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
159 | dependencies = [
160 | "libc",
161 | "num-integer",
162 | "num-traits",
163 | "time 0.1.44",
164 | "winapi",
165 | ]
166 |
167 | [[package]]
168 | name = "chunked_transfer"
169 | version = "1.4.0"
170 | source = "registry+https://github.com/rust-lang/crates.io-index"
171 | checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
172 |
173 | [[package]]
174 | name = "clang"
175 | version = "2.0.0"
176 | source = "registry+https://github.com/rust-lang/crates.io-index"
177 | checksum = "84c044c781163c001b913cd018fc95a628c50d0d2dfea8bca77dad71edb16e37"
178 | dependencies = [
179 | "clang-sys",
180 | "libc",
181 | ]
182 |
183 | [[package]]
184 | name = "clang-sys"
185 | version = "1.3.3"
186 | source = "registry+https://github.com/rust-lang/crates.io-index"
187 | checksum = "5a050e2153c5be08febd6734e29298e844fdb0fa21aeddd63b4eb7baa106c69b"
188 | dependencies = [
189 | "glob",
190 | "libc",
191 | "libloading",
192 | ]
193 |
194 | [[package]]
195 | name = "clap"
196 | version = "3.2.12"
197 | source = "registry+https://github.com/rust-lang/crates.io-index"
198 | checksum = "ab8b79fe3946ceb4a0b1c080b4018992b8d27e9ff363644c1c9b6387c854614d"
199 | dependencies = [
200 | "atty",
201 | "bitflags",
202 | "clap_derive",
203 | "clap_lex",
204 | "indexmap",
205 | "once_cell",
206 | "strsim",
207 | "termcolor",
208 | "textwrap",
209 | ]
210 |
211 | [[package]]
212 | name = "clap_derive"
213 | version = "3.2.7"
214 | source = "registry+https://github.com/rust-lang/crates.io-index"
215 | checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902"
216 | dependencies = [
217 | "heck",
218 | "proc-macro-error",
219 | "proc-macro2",
220 | "quote",
221 | "syn",
222 | ]
223 |
224 | [[package]]
225 | name = "clap_lex"
226 | version = "0.2.4"
227 | source = "registry+https://github.com/rust-lang/crates.io-index"
228 | checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
229 | dependencies = [
230 | "os_str_bytes",
231 | ]
232 |
233 | [[package]]
234 | name = "cmake"
235 | version = "0.1.48"
236 | source = "registry+https://github.com/rust-lang/crates.io-index"
237 | checksum = "e8ad8cef104ac57b68b89df3208164d228503abbdce70f6880ffa3d970e7443a"
238 | dependencies = [
239 | "cc",
240 | ]
241 |
242 | [[package]]
243 | name = "cpufeatures"
244 | version = "0.2.2"
245 | source = "registry+https://github.com/rust-lang/crates.io-index"
246 | checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
247 | dependencies = [
248 | "libc",
249 | ]
250 |
251 | [[package]]
252 | name = "crc32fast"
253 | version = "1.3.2"
254 | source = "registry+https://github.com/rust-lang/crates.io-index"
255 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
256 | dependencies = [
257 | "cfg-if",
258 | ]
259 |
260 | [[package]]
261 | name = "crossbeam"
262 | version = "0.8.1"
263 | source = "registry+https://github.com/rust-lang/crates.io-index"
264 | checksum = "4ae5588f6b3c3cb05239e90bd110f257254aecd01e4635400391aeae07497845"
265 | dependencies = [
266 | "cfg-if",
267 | "crossbeam-channel",
268 | "crossbeam-deque",
269 | "crossbeam-epoch",
270 | "crossbeam-queue",
271 | "crossbeam-utils",
272 | ]
273 |
274 | [[package]]
275 | name = "crossbeam-channel"
276 | version = "0.5.5"
277 | source = "registry+https://github.com/rust-lang/crates.io-index"
278 | checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c"
279 | dependencies = [
280 | "cfg-if",
281 | "crossbeam-utils",
282 | ]
283 |
284 | [[package]]
285 | name = "crossbeam-deque"
286 | version = "0.8.1"
287 | source = "registry+https://github.com/rust-lang/crates.io-index"
288 | checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e"
289 | dependencies = [
290 | "cfg-if",
291 | "crossbeam-epoch",
292 | "crossbeam-utils",
293 | ]
294 |
295 | [[package]]
296 | name = "crossbeam-epoch"
297 | version = "0.9.9"
298 | source = "registry+https://github.com/rust-lang/crates.io-index"
299 | checksum = "07db9d94cbd326813772c968ccd25999e5f8ae22f4f8d1b11effa37ef6ce281d"
300 | dependencies = [
301 | "autocfg",
302 | "cfg-if",
303 | "crossbeam-utils",
304 | "memoffset",
305 | "once_cell",
306 | "scopeguard",
307 | ]
308 |
309 | [[package]]
310 | name = "crossbeam-queue"
311 | version = "0.3.5"
312 | source = "registry+https://github.com/rust-lang/crates.io-index"
313 | checksum = "1f25d8400f4a7a5778f0e4e52384a48cbd9b5c495d110786187fc750075277a2"
314 | dependencies = [
315 | "cfg-if",
316 | "crossbeam-utils",
317 | ]
318 |
319 | [[package]]
320 | name = "crossbeam-utils"
321 | version = "0.8.10"
322 | source = "registry+https://github.com/rust-lang/crates.io-index"
323 | checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83"
324 | dependencies = [
325 | "cfg-if",
326 | "once_cell",
327 | ]
328 |
329 | [[package]]
330 | name = "crypto-common"
331 | version = "0.1.6"
332 | source = "registry+https://github.com/rust-lang/crates.io-index"
333 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
334 | dependencies = [
335 | "generic-array",
336 | "typenum",
337 | ]
338 |
339 | [[package]]
340 | name = "derive_utils"
341 | version = "0.11.2"
342 | source = "registry+https://github.com/rust-lang/crates.io-index"
343 | checksum = "532b4c15dccee12c7044f1fcad956e98410860b22231e44a3b827464797ca7bf"
344 | dependencies = [
345 | "proc-macro2",
346 | "quote",
347 | "syn",
348 | ]
349 |
350 | [[package]]
351 | name = "digest"
352 | version = "0.10.3"
353 | source = "registry+https://github.com/rust-lang/crates.io-index"
354 | checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
355 | dependencies = [
356 | "block-buffer",
357 | "crypto-common",
358 | ]
359 |
360 | [[package]]
361 | name = "dunce"
362 | version = "1.0.2"
363 | source = "registry+https://github.com/rust-lang/crates.io-index"
364 | checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541"
365 |
366 | [[package]]
367 | name = "flate2"
368 | version = "1.0.24"
369 | source = "registry+https://github.com/rust-lang/crates.io-index"
370 | checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
371 | dependencies = [
372 | "crc32fast",
373 | "libz-sys",
374 | "miniz_oxide",
375 | ]
376 |
377 | [[package]]
378 | name = "fnv"
379 | version = "1.0.7"
380 | source = "registry+https://github.com/rust-lang/crates.io-index"
381 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
382 |
383 | [[package]]
384 | name = "form_urlencoded"
385 | version = "1.0.1"
386 | source = "registry+https://github.com/rust-lang/crates.io-index"
387 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
388 | dependencies = [
389 | "matches",
390 | "percent-encoding",
391 | ]
392 |
393 | [[package]]
394 | name = "funty"
395 | version = "1.2.0"
396 | source = "registry+https://github.com/rust-lang/crates.io-index"
397 | checksum = "1847abb9cb65d566acd5942e94aea9c8f547ad02c98e1649326fc0e8910b8b1e"
398 |
399 | [[package]]
400 | name = "generic-array"
401 | version = "0.14.5"
402 | source = "registry+https://github.com/rust-lang/crates.io-index"
403 | checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
404 | dependencies = [
405 | "typenum",
406 | "version_check",
407 | ]
408 |
409 | [[package]]
410 | name = "getrandom"
411 | version = "0.2.7"
412 | source = "registry+https://github.com/rust-lang/crates.io-index"
413 | checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
414 | dependencies = [
415 | "cfg-if",
416 | "libc",
417 | "wasi 0.11.0+wasi-snapshot-preview1",
418 | ]
419 |
420 | [[package]]
421 | name = "glob"
422 | version = "0.3.0"
423 | source = "registry+https://github.com/rust-lang/crates.io-index"
424 | checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
425 |
426 | [[package]]
427 | name = "hashbrown"
428 | version = "0.11.2"
429 | source = "registry+https://github.com/rust-lang/crates.io-index"
430 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
431 | dependencies = [
432 | "ahash",
433 | ]
434 |
435 | [[package]]
436 | name = "hashbrown"
437 | version = "0.12.2"
438 | source = "registry+https://github.com/rust-lang/crates.io-index"
439 | checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022"
440 |
441 | [[package]]
442 | name = "heck"
443 | version = "0.4.0"
444 | source = "registry+https://github.com/rust-lang/crates.io-index"
445 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
446 |
447 | [[package]]
448 | name = "hermit-abi"
449 | version = "0.1.19"
450 | source = "registry+https://github.com/rust-lang/crates.io-index"
451 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
452 | dependencies = [
453 | "libc",
454 | ]
455 |
456 | [[package]]
457 | name = "hex"
458 | version = "0.4.3"
459 | source = "registry+https://github.com/rust-lang/crates.io-index"
460 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
461 |
462 | [[package]]
463 | name = "idna"
464 | version = "0.2.3"
465 | source = "registry+https://github.com/rust-lang/crates.io-index"
466 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
467 | dependencies = [
468 | "matches",
469 | "unicode-bidi",
470 | "unicode-normalization",
471 | ]
472 |
473 | [[package]]
474 | name = "indexmap"
475 | version = "1.9.1"
476 | source = "registry+https://github.com/rust-lang/crates.io-index"
477 | checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
478 | dependencies = [
479 | "autocfg",
480 | "hashbrown 0.12.2",
481 | ]
482 |
483 | [[package]]
484 | name = "io-enum"
485 | version = "1.0.1"
486 | source = "registry+https://github.com/rust-lang/crates.io-index"
487 | checksum = "03e3306b0f260aad2872563eb0d5d1a59f2420fad270a661dce59a01e92d806b"
488 | dependencies = [
489 | "autocfg",
490 | "derive_utils",
491 | "quote",
492 | "syn",
493 | ]
494 |
495 | [[package]]
496 | name = "itoa"
497 | version = "1.0.2"
498 | source = "registry+https://github.com/rust-lang/crates.io-index"
499 | checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
500 |
501 | [[package]]
502 | name = "jobserver"
503 | version = "0.1.24"
504 | source = "registry+https://github.com/rust-lang/crates.io-index"
505 | checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa"
506 | dependencies = [
507 | "libc",
508 | ]
509 |
510 | [[package]]
511 | name = "lazy_static"
512 | version = "1.4.0"
513 | source = "registry+https://github.com/rust-lang/crates.io-index"
514 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
515 |
516 | [[package]]
517 | name = "lazycell"
518 | version = "1.3.0"
519 | source = "registry+https://github.com/rust-lang/crates.io-index"
520 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
521 |
522 | [[package]]
523 | name = "lexical"
524 | version = "6.1.1"
525 | source = "registry+https://github.com/rust-lang/crates.io-index"
526 | checksum = "c7aefb36fd43fef7003334742cbf77b243fcd36418a1d1bdd480d613a67968f6"
527 | dependencies = [
528 | "lexical-core",
529 | ]
530 |
531 | [[package]]
532 | name = "lexical-core"
533 | version = "0.8.5"
534 | source = "registry+https://github.com/rust-lang/crates.io-index"
535 | checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46"
536 | dependencies = [
537 | "lexical-parse-float",
538 | "lexical-parse-integer",
539 | "lexical-util",
540 | "lexical-write-float",
541 | "lexical-write-integer",
542 | ]
543 |
544 | [[package]]
545 | name = "lexical-parse-float"
546 | version = "0.8.5"
547 | source = "registry+https://github.com/rust-lang/crates.io-index"
548 | checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f"
549 | dependencies = [
550 | "lexical-parse-integer",
551 | "lexical-util",
552 | "static_assertions",
553 | ]
554 |
555 | [[package]]
556 | name = "lexical-parse-integer"
557 | version = "0.8.6"
558 | source = "registry+https://github.com/rust-lang/crates.io-index"
559 | checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9"
560 | dependencies = [
561 | "lexical-util",
562 | "static_assertions",
563 | ]
564 |
565 | [[package]]
566 | name = "lexical-util"
567 | version = "0.8.5"
568 | source = "registry+https://github.com/rust-lang/crates.io-index"
569 | checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc"
570 | dependencies = [
571 | "static_assertions",
572 | ]
573 |
574 | [[package]]
575 | name = "lexical-write-float"
576 | version = "0.8.5"
577 | source = "registry+https://github.com/rust-lang/crates.io-index"
578 | checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862"
579 | dependencies = [
580 | "lexical-util",
581 | "lexical-write-integer",
582 | "static_assertions",
583 | ]
584 |
585 | [[package]]
586 | name = "lexical-write-integer"
587 | version = "0.8.5"
588 | source = "registry+https://github.com/rust-lang/crates.io-index"
589 | checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446"
590 | dependencies = [
591 | "lexical-util",
592 | "static_assertions",
593 | ]
594 |
595 | [[package]]
596 | name = "libc"
597 | version = "0.2.126"
598 | source = "registry+https://github.com/rust-lang/crates.io-index"
599 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
600 |
601 | [[package]]
602 | name = "libloading"
603 | version = "0.7.3"
604 | source = "registry+https://github.com/rust-lang/crates.io-index"
605 | checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd"
606 | dependencies = [
607 | "cfg-if",
608 | "winapi",
609 | ]
610 |
611 | [[package]]
612 | name = "libz-sys"
613 | version = "1.1.8"
614 | source = "registry+https://github.com/rust-lang/crates.io-index"
615 | checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf"
616 | dependencies = [
617 | "cc",
618 | "pkg-config",
619 | "vcpkg",
620 | ]
621 |
622 | [[package]]
623 | name = "lock_api"
624 | version = "0.4.7"
625 | source = "registry+https://github.com/rust-lang/crates.io-index"
626 | checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
627 | dependencies = [
628 | "autocfg",
629 | "scopeguard",
630 | ]
631 |
632 | [[package]]
633 | name = "log"
634 | version = "0.4.17"
635 | source = "registry+https://github.com/rust-lang/crates.io-index"
636 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
637 | dependencies = [
638 | "cfg-if",
639 | ]
640 |
641 | [[package]]
642 | name = "lru"
643 | version = "0.7.7"
644 | source = "registry+https://github.com/rust-lang/crates.io-index"
645 | checksum = "c84e6fe5655adc6ce00787cf7dcaf8dc4f998a0565d23eafc207a8b08ca3349a"
646 | dependencies = [
647 | "hashbrown 0.11.2",
648 | ]
649 |
650 | [[package]]
651 | name = "maplit"
652 | version = "1.0.2"
653 | source = "registry+https://github.com/rust-lang/crates.io-index"
654 | checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
655 |
656 | [[package]]
657 | name = "matches"
658 | version = "0.1.9"
659 | source = "registry+https://github.com/rust-lang/crates.io-index"
660 | checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
661 |
662 | [[package]]
663 | name = "memchr"
664 | version = "2.5.0"
665 | source = "registry+https://github.com/rust-lang/crates.io-index"
666 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
667 |
668 | [[package]]
669 | name = "memoffset"
670 | version = "0.6.5"
671 | source = "registry+https://github.com/rust-lang/crates.io-index"
672 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
673 | dependencies = [
674 | "autocfg",
675 | ]
676 |
677 | [[package]]
678 | name = "minimal-lexical"
679 | version = "0.2.1"
680 | source = "registry+https://github.com/rust-lang/crates.io-index"
681 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
682 |
683 | [[package]]
684 | name = "miniz_oxide"
685 | version = "0.5.3"
686 | source = "registry+https://github.com/rust-lang/crates.io-index"
687 | checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc"
688 | dependencies = [
689 | "adler",
690 | ]
691 |
692 | [[package]]
693 | name = "mysql"
694 | version = "22.2.0"
695 | source = "registry+https://github.com/rust-lang/crates.io-index"
696 | checksum = "b9d8136c78f78cda5c1a4eee4ce555281b71e3e6db715817bc50e186e623b36f"
697 | dependencies = [
698 | "bufstream",
699 | "bytes",
700 | "crossbeam",
701 | "flate2",
702 | "io-enum",
703 | "libc",
704 | "lru",
705 | "mysql_common",
706 | "named_pipe",
707 | "once_cell",
708 | "pem",
709 | "percent-encoding",
710 | "serde",
711 | "serde_json",
712 | "socket2",
713 | "twox-hash",
714 | "url",
715 | ]
716 |
717 | [[package]]
718 | name = "mysql_common"
719 | version = "0.28.2"
720 | source = "registry+https://github.com/rust-lang/crates.io-index"
721 | checksum = "4140827f2d12750de1e8755442577e4292a835f26ff2f659f0a380d1d71020b0"
722 | dependencies = [
723 | "base64",
724 | "bindgen",
725 | "bitflags",
726 | "bitvec",
727 | "byteorder",
728 | "bytes",
729 | "cc",
730 | "cmake",
731 | "crc32fast",
732 | "flate2",
733 | "lazy_static",
734 | "lexical",
735 | "num-bigint",
736 | "num-traits",
737 | "rand",
738 | "regex",
739 | "saturating",
740 | "serde",
741 | "serde_json",
742 | "sha-1",
743 | "sha2",
744 | "smallvec",
745 | "subprocess",
746 | "thiserror",
747 | ]
748 |
749 | [[package]]
750 | name = "named_pipe"
751 | version = "0.4.1"
752 | source = "registry+https://github.com/rust-lang/crates.io-index"
753 | checksum = "ad9c443cce91fc3e12f017290db75dde490d685cdaaf508d7159d7cf41f0eb2b"
754 | dependencies = [
755 | "winapi",
756 | ]
757 |
758 | [[package]]
759 | name = "nom"
760 | version = "7.1.1"
761 | source = "registry+https://github.com/rust-lang/crates.io-index"
762 | checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
763 | dependencies = [
764 | "memchr",
765 | "minimal-lexical",
766 | ]
767 |
768 | [[package]]
769 | name = "num-bigint"
770 | version = "0.4.3"
771 | source = "registry+https://github.com/rust-lang/crates.io-index"
772 | checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
773 | dependencies = [
774 | "autocfg",
775 | "num-integer",
776 | "num-traits",
777 | ]
778 |
779 | [[package]]
780 | name = "num-integer"
781 | version = "0.1.45"
782 | source = "registry+https://github.com/rust-lang/crates.io-index"
783 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
784 | dependencies = [
785 | "autocfg",
786 | "num-traits",
787 | ]
788 |
789 | [[package]]
790 | name = "num-traits"
791 | version = "0.2.15"
792 | source = "registry+https://github.com/rust-lang/crates.io-index"
793 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
794 | dependencies = [
795 | "autocfg",
796 | ]
797 |
798 | [[package]]
799 | name = "num_threads"
800 | version = "0.1.6"
801 | source = "registry+https://github.com/rust-lang/crates.io-index"
802 | checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
803 | dependencies = [
804 | "libc",
805 | ]
806 |
807 | [[package]]
808 | name = "once_cell"
809 | version = "1.13.0"
810 | source = "registry+https://github.com/rust-lang/crates.io-index"
811 | checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
812 |
813 | [[package]]
814 | name = "opencv"
815 | version = "0.66.0"
816 | source = "registry+https://github.com/rust-lang/crates.io-index"
817 | checksum = "ae1211402fae55d0054e5779bcb231660752b8da825f88aa8a837aef7944c68d"
818 | dependencies = [
819 | "cc",
820 | "clang",
821 | "dunce",
822 | "glob",
823 | "jobserver",
824 | "libc",
825 | "num-traits",
826 | "once_cell",
827 | "opencv-binding-generator",
828 | "pkg-config",
829 | "semver",
830 | "shlex",
831 | "vcpkg",
832 | ]
833 |
834 | [[package]]
835 | name = "opencv-binding-generator"
836 | version = "0.45.0"
837 | source = "registry+https://github.com/rust-lang/crates.io-index"
838 | checksum = "1070da825ea3584b7ef0a2951fc002843f986ec430681b2273d9d425da2ffd61"
839 | dependencies = [
840 | "clang",
841 | "clang-sys",
842 | "dunce",
843 | "maplit",
844 | "once_cell",
845 | "percent-encoding",
846 | "regex",
847 | ]
848 |
849 | [[package]]
850 | name = "os_str_bytes"
851 | version = "6.2.0"
852 | source = "registry+https://github.com/rust-lang/crates.io-index"
853 | checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4"
854 |
855 | [[package]]
856 | name = "parking_lot"
857 | version = "0.12.1"
858 | source = "registry+https://github.com/rust-lang/crates.io-index"
859 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
860 | dependencies = [
861 | "lock_api",
862 | "parking_lot_core",
863 | ]
864 |
865 | [[package]]
866 | name = "parking_lot_core"
867 | version = "0.9.3"
868 | source = "registry+https://github.com/rust-lang/crates.io-index"
869 | checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
870 | dependencies = [
871 | "cfg-if",
872 | "libc",
873 | "redox_syscall",
874 | "smallvec",
875 | "windows-sys",
876 | ]
877 |
878 | [[package]]
879 | name = "peeking_take_while"
880 | version = "0.1.2"
881 | source = "registry+https://github.com/rust-lang/crates.io-index"
882 | checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
883 |
884 | [[package]]
885 | name = "pem"
886 | version = "1.1.0"
887 | source = "registry+https://github.com/rust-lang/crates.io-index"
888 | checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4"
889 | dependencies = [
890 | "base64",
891 | ]
892 |
893 | [[package]]
894 | name = "percent-encoding"
895 | version = "2.1.0"
896 | source = "registry+https://github.com/rust-lang/crates.io-index"
897 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
898 |
899 | [[package]]
900 | name = "pkg-config"
901 | version = "0.3.25"
902 | source = "registry+https://github.com/rust-lang/crates.io-index"
903 | checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
904 |
905 | [[package]]
906 | name = "ppv-lite86"
907 | version = "0.2.16"
908 | source = "registry+https://github.com/rust-lang/crates.io-index"
909 | checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
910 |
911 | [[package]]
912 | name = "proc-macro-error"
913 | version = "1.0.4"
914 | source = "registry+https://github.com/rust-lang/crates.io-index"
915 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
916 | dependencies = [
917 | "proc-macro-error-attr",
918 | "proc-macro2",
919 | "quote",
920 | "syn",
921 | "version_check",
922 | ]
923 |
924 | [[package]]
925 | name = "proc-macro-error-attr"
926 | version = "1.0.4"
927 | source = "registry+https://github.com/rust-lang/crates.io-index"
928 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
929 | dependencies = [
930 | "proc-macro2",
931 | "quote",
932 | "version_check",
933 | ]
934 |
935 | [[package]]
936 | name = "proc-macro2"
937 | version = "1.0.40"
938 | source = "registry+https://github.com/rust-lang/crates.io-index"
939 | checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7"
940 | dependencies = [
941 | "unicode-ident",
942 | ]
943 |
944 | [[package]]
945 | name = "procfs"
946 | version = "0.12.0"
947 | source = "registry+https://github.com/rust-lang/crates.io-index"
948 | checksum = "0941606b9934e2d98a3677759a971756eb821f75764d0e0d26946d08e74d9104"
949 | dependencies = [
950 | "bitflags",
951 | "byteorder",
952 | "hex",
953 | "lazy_static",
954 | "libc",
955 | ]
956 |
957 | [[package]]
958 | name = "prometheus"
959 | version = "0.13.1"
960 | source = "registry+https://github.com/rust-lang/crates.io-index"
961 | checksum = "cface98dfa6d645ea4c789839f176e4b072265d085bfcc48eaa8d137f58d3c39"
962 | dependencies = [
963 | "cfg-if",
964 | "fnv",
965 | "lazy_static",
966 | "libc",
967 | "memchr",
968 | "parking_lot",
969 | "procfs",
970 | "thiserror",
971 | ]
972 |
973 | [[package]]
974 | name = "quote"
975 | version = "1.0.20"
976 | source = "registry+https://github.com/rust-lang/crates.io-index"
977 | checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
978 | dependencies = [
979 | "proc-macro2",
980 | ]
981 |
982 | [[package]]
983 | name = "radium"
984 | version = "0.6.2"
985 | source = "registry+https://github.com/rust-lang/crates.io-index"
986 | checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb"
987 |
988 | [[package]]
989 | name = "rand"
990 | version = "0.8.5"
991 | source = "registry+https://github.com/rust-lang/crates.io-index"
992 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
993 | dependencies = [
994 | "libc",
995 | "rand_chacha",
996 | "rand_core",
997 | ]
998 |
999 | [[package]]
1000 | name = "rand_chacha"
1001 | version = "0.3.1"
1002 | source = "registry+https://github.com/rust-lang/crates.io-index"
1003 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
1004 | dependencies = [
1005 | "ppv-lite86",
1006 | "rand_core",
1007 | ]
1008 |
1009 | [[package]]
1010 | name = "rand_core"
1011 | version = "0.6.3"
1012 | source = "registry+https://github.com/rust-lang/crates.io-index"
1013 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
1014 | dependencies = [
1015 | "getrandom",
1016 | ]
1017 |
1018 | [[package]]
1019 | name = "redox_syscall"
1020 | version = "0.2.13"
1021 | source = "registry+https://github.com/rust-lang/crates.io-index"
1022 | checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
1023 | dependencies = [
1024 | "bitflags",
1025 | ]
1026 |
1027 | [[package]]
1028 | name = "regex"
1029 | version = "1.6.0"
1030 | source = "registry+https://github.com/rust-lang/crates.io-index"
1031 | checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
1032 | dependencies = [
1033 | "aho-corasick",
1034 | "memchr",
1035 | "regex-syntax",
1036 | ]
1037 |
1038 | [[package]]
1039 | name = "regex-syntax"
1040 | version = "0.6.27"
1041 | source = "registry+https://github.com/rust-lang/crates.io-index"
1042 | checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
1043 |
1044 | [[package]]
1045 | name = "rustc-hash"
1046 | version = "1.1.0"
1047 | source = "registry+https://github.com/rust-lang/crates.io-index"
1048 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
1049 |
1050 | [[package]]
1051 | name = "ryu"
1052 | version = "1.0.10"
1053 | source = "registry+https://github.com/rust-lang/crates.io-index"
1054 | checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
1055 |
1056 | [[package]]
1057 | name = "saturating"
1058 | version = "0.1.0"
1059 | source = "registry+https://github.com/rust-lang/crates.io-index"
1060 | checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71"
1061 |
1062 | [[package]]
1063 | name = "scopeguard"
1064 | version = "1.1.0"
1065 | source = "registry+https://github.com/rust-lang/crates.io-index"
1066 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
1067 |
1068 | [[package]]
1069 | name = "semver"
1070 | version = "1.0.12"
1071 | source = "registry+https://github.com/rust-lang/crates.io-index"
1072 | checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1"
1073 |
1074 | [[package]]
1075 | name = "serde"
1076 | version = "1.0.139"
1077 | source = "registry+https://github.com/rust-lang/crates.io-index"
1078 | checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6"
1079 | dependencies = [
1080 | "serde_derive",
1081 | ]
1082 |
1083 | [[package]]
1084 | name = "serde_derive"
1085 | version = "1.0.139"
1086 | source = "registry+https://github.com/rust-lang/crates.io-index"
1087 | checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb"
1088 | dependencies = [
1089 | "proc-macro2",
1090 | "quote",
1091 | "syn",
1092 | ]
1093 |
1094 | [[package]]
1095 | name = "serde_json"
1096 | version = "1.0.82"
1097 | source = "registry+https://github.com/rust-lang/crates.io-index"
1098 | checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7"
1099 | dependencies = [
1100 | "itoa",
1101 | "ryu",
1102 | "serde",
1103 | ]
1104 |
1105 | [[package]]
1106 | name = "sha-1"
1107 | version = "0.10.0"
1108 | source = "registry+https://github.com/rust-lang/crates.io-index"
1109 | checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
1110 | dependencies = [
1111 | "cfg-if",
1112 | "cpufeatures",
1113 | "digest",
1114 | ]
1115 |
1116 | [[package]]
1117 | name = "sha2"
1118 | version = "0.10.2"
1119 | source = "registry+https://github.com/rust-lang/crates.io-index"
1120 | checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676"
1121 | dependencies = [
1122 | "cfg-if",
1123 | "cpufeatures",
1124 | "digest",
1125 | ]
1126 |
1127 | [[package]]
1128 | name = "shlex"
1129 | version = "1.1.0"
1130 | source = "registry+https://github.com/rust-lang/crates.io-index"
1131 | checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
1132 |
1133 | [[package]]
1134 | name = "simple_moving_average"
1135 | version = "0.1.2"
1136 | source = "registry+https://github.com/rust-lang/crates.io-index"
1137 | checksum = "cdd19d3808aad2604c824399fd270260d634678b010328c9d96851bb0fb63121"
1138 | dependencies = [
1139 | "num-traits",
1140 | ]
1141 |
1142 | [[package]]
1143 | name = "smallvec"
1144 | version = "1.9.0"
1145 | source = "registry+https://github.com/rust-lang/crates.io-index"
1146 | checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
1147 |
1148 | [[package]]
1149 | name = "socket2"
1150 | version = "0.4.4"
1151 | source = "registry+https://github.com/rust-lang/crates.io-index"
1152 | checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
1153 | dependencies = [
1154 | "libc",
1155 | "winapi",
1156 | ]
1157 |
1158 | [[package]]
1159 | name = "static_assertions"
1160 | version = "1.1.0"
1161 | source = "registry+https://github.com/rust-lang/crates.io-index"
1162 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
1163 |
1164 | [[package]]
1165 | name = "stderrlog"
1166 | version = "0.5.3"
1167 | source = "registry+https://github.com/rust-lang/crates.io-index"
1168 | checksum = "af95cb8a5f79db5b2af2a46f44da7594b5adbcbb65cbf87b8da0959bfdd82460"
1169 | dependencies = [
1170 | "atty",
1171 | "chrono",
1172 | "log",
1173 | "termcolor",
1174 | "thread_local",
1175 | ]
1176 |
1177 | [[package]]
1178 | name = "strsim"
1179 | version = "0.10.0"
1180 | source = "registry+https://github.com/rust-lang/crates.io-index"
1181 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
1182 |
1183 | [[package]]
1184 | name = "subprocess"
1185 | version = "0.2.9"
1186 | source = "registry+https://github.com/rust-lang/crates.io-index"
1187 | checksum = "0c2e86926081dda636c546d8c5e641661049d7562a68f5488be4a1f7f66f6086"
1188 | dependencies = [
1189 | "libc",
1190 | "winapi",
1191 | ]
1192 |
1193 | [[package]]
1194 | name = "syn"
1195 | version = "1.0.98"
1196 | source = "registry+https://github.com/rust-lang/crates.io-index"
1197 | checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd"
1198 | dependencies = [
1199 | "proc-macro2",
1200 | "quote",
1201 | "unicode-ident",
1202 | ]
1203 |
1204 | [[package]]
1205 | name = "tap"
1206 | version = "1.0.1"
1207 | source = "registry+https://github.com/rust-lang/crates.io-index"
1208 | checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
1209 |
1210 | [[package]]
1211 | name = "termcolor"
1212 | version = "1.1.3"
1213 | source = "registry+https://github.com/rust-lang/crates.io-index"
1214 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
1215 | dependencies = [
1216 | "winapi-util",
1217 | ]
1218 |
1219 | [[package]]
1220 | name = "textwrap"
1221 | version = "0.15.0"
1222 | source = "registry+https://github.com/rust-lang/crates.io-index"
1223 | checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
1224 |
1225 | [[package]]
1226 | name = "thiserror"
1227 | version = "1.0.31"
1228 | source = "registry+https://github.com/rust-lang/crates.io-index"
1229 | checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
1230 | dependencies = [
1231 | "thiserror-impl",
1232 | ]
1233 |
1234 | [[package]]
1235 | name = "thiserror-impl"
1236 | version = "1.0.31"
1237 | source = "registry+https://github.com/rust-lang/crates.io-index"
1238 | checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
1239 | dependencies = [
1240 | "proc-macro2",
1241 | "quote",
1242 | "syn",
1243 | ]
1244 |
1245 | [[package]]
1246 | name = "thread_local"
1247 | version = "1.1.4"
1248 | source = "registry+https://github.com/rust-lang/crates.io-index"
1249 | checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
1250 | dependencies = [
1251 | "once_cell",
1252 | ]
1253 |
1254 | [[package]]
1255 | name = "time"
1256 | version = "0.1.44"
1257 | source = "registry+https://github.com/rust-lang/crates.io-index"
1258 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
1259 | dependencies = [
1260 | "libc",
1261 | "wasi 0.10.0+wasi-snapshot-preview1",
1262 | "winapi",
1263 | ]
1264 |
1265 | [[package]]
1266 | name = "time"
1267 | version = "0.3.11"
1268 | source = "registry+https://github.com/rust-lang/crates.io-index"
1269 | checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217"
1270 | dependencies = [
1271 | "itoa",
1272 | "libc",
1273 | "num_threads",
1274 | "time-macros",
1275 | ]
1276 |
1277 | [[package]]
1278 | name = "time-macros"
1279 | version = "0.2.4"
1280 | source = "registry+https://github.com/rust-lang/crates.io-index"
1281 | checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792"
1282 |
1283 | [[package]]
1284 | name = "tiny_http"
1285 | version = "0.11.0"
1286 | source = "registry+https://github.com/rust-lang/crates.io-index"
1287 | checksum = "e0d6ef4e10d23c1efb862eecad25c5054429a71958b4eeef85eb5e7170b477ca"
1288 | dependencies = [
1289 | "ascii",
1290 | "chunked_transfer",
1291 | "log",
1292 | "time 0.3.11",
1293 | "url",
1294 | ]
1295 |
1296 | [[package]]
1297 | name = "tinyvec"
1298 | version = "1.6.0"
1299 | source = "registry+https://github.com/rust-lang/crates.io-index"
1300 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
1301 | dependencies = [
1302 | "tinyvec_macros",
1303 | ]
1304 |
1305 | [[package]]
1306 | name = "tinyvec_macros"
1307 | version = "0.1.0"
1308 | source = "registry+https://github.com/rust-lang/crates.io-index"
1309 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
1310 |
1311 | [[package]]
1312 | name = "twox-hash"
1313 | version = "1.6.3"
1314 | source = "registry+https://github.com/rust-lang/crates.io-index"
1315 | checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
1316 | dependencies = [
1317 | "cfg-if",
1318 | "rand",
1319 | "static_assertions",
1320 | ]
1321 |
1322 | [[package]]
1323 | name = "typenum"
1324 | version = "1.15.0"
1325 | source = "registry+https://github.com/rust-lang/crates.io-index"
1326 | checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
1327 |
1328 | [[package]]
1329 | name = "unicode-bidi"
1330 | version = "0.3.8"
1331 | source = "registry+https://github.com/rust-lang/crates.io-index"
1332 | checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
1333 |
1334 | [[package]]
1335 | name = "unicode-ident"
1336 | version = "1.0.2"
1337 | source = "registry+https://github.com/rust-lang/crates.io-index"
1338 | checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7"
1339 |
1340 | [[package]]
1341 | name = "unicode-normalization"
1342 | version = "0.1.21"
1343 | source = "registry+https://github.com/rust-lang/crates.io-index"
1344 | checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6"
1345 | dependencies = [
1346 | "tinyvec",
1347 | ]
1348 |
1349 | [[package]]
1350 | name = "url"
1351 | version = "2.2.2"
1352 | source = "registry+https://github.com/rust-lang/crates.io-index"
1353 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
1354 | dependencies = [
1355 | "form_urlencoded",
1356 | "idna",
1357 | "matches",
1358 | "percent-encoding",
1359 | ]
1360 |
1361 | [[package]]
1362 | name = "vcpkg"
1363 | version = "0.2.15"
1364 | source = "registry+https://github.com/rust-lang/crates.io-index"
1365 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
1366 |
1367 | [[package]]
1368 | name = "version_check"
1369 | version = "0.9.4"
1370 | source = "registry+https://github.com/rust-lang/crates.io-index"
1371 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
1372 |
1373 | [[package]]
1374 | name = "wasi"
1375 | version = "0.10.0+wasi-snapshot-preview1"
1376 | source = "registry+https://github.com/rust-lang/crates.io-index"
1377 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
1378 |
1379 | [[package]]
1380 | name = "wasi"
1381 | version = "0.11.0+wasi-snapshot-preview1"
1382 | source = "registry+https://github.com/rust-lang/crates.io-index"
1383 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
1384 |
1385 | [[package]]
1386 | name = "winapi"
1387 | version = "0.3.9"
1388 | source = "registry+https://github.com/rust-lang/crates.io-index"
1389 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
1390 | dependencies = [
1391 | "winapi-i686-pc-windows-gnu",
1392 | "winapi-x86_64-pc-windows-gnu",
1393 | ]
1394 |
1395 | [[package]]
1396 | name = "winapi-i686-pc-windows-gnu"
1397 | version = "0.4.0"
1398 | source = "registry+https://github.com/rust-lang/crates.io-index"
1399 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
1400 |
1401 | [[package]]
1402 | name = "winapi-util"
1403 | version = "0.1.5"
1404 | source = "registry+https://github.com/rust-lang/crates.io-index"
1405 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
1406 | dependencies = [
1407 | "winapi",
1408 | ]
1409 |
1410 | [[package]]
1411 | name = "winapi-x86_64-pc-windows-gnu"
1412 | version = "0.4.0"
1413 | source = "registry+https://github.com/rust-lang/crates.io-index"
1414 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
1415 |
1416 | [[package]]
1417 | name = "windows-sys"
1418 | version = "0.36.1"
1419 | source = "registry+https://github.com/rust-lang/crates.io-index"
1420 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
1421 | dependencies = [
1422 | "windows_aarch64_msvc",
1423 | "windows_i686_gnu",
1424 | "windows_i686_msvc",
1425 | "windows_x86_64_gnu",
1426 | "windows_x86_64_msvc",
1427 | ]
1428 |
1429 | [[package]]
1430 | name = "windows_aarch64_msvc"
1431 | version = "0.36.1"
1432 | source = "registry+https://github.com/rust-lang/crates.io-index"
1433 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
1434 |
1435 | [[package]]
1436 | name = "windows_i686_gnu"
1437 | version = "0.36.1"
1438 | source = "registry+https://github.com/rust-lang/crates.io-index"
1439 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
1440 |
1441 | [[package]]
1442 | name = "windows_i686_msvc"
1443 | version = "0.36.1"
1444 | source = "registry+https://github.com/rust-lang/crates.io-index"
1445 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
1446 |
1447 | [[package]]
1448 | name = "windows_x86_64_gnu"
1449 | version = "0.36.1"
1450 | source = "registry+https://github.com/rust-lang/crates.io-index"
1451 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
1452 |
1453 | [[package]]
1454 | name = "windows_x86_64_msvc"
1455 | version = "0.36.1"
1456 | source = "registry+https://github.com/rust-lang/crates.io-index"
1457 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
1458 |
1459 | [[package]]
1460 | name = "wyz"
1461 | version = "0.4.0"
1462 | source = "registry+https://github.com/rust-lang/crates.io-index"
1463 | checksum = "129e027ad65ce1453680623c3fb5163cbf7107bfe1aa32257e7d0e63f9ced188"
1464 | dependencies = [
1465 | "tap",
1466 | ]
1467 |
1468 | [[package]]
1469 | name = "zoneminder-aidect"
1470 | version = "0.1.0"
1471 | dependencies = [
1472 | "anyhow",
1473 | "clap",
1474 | "flate2",
1475 | "lazy_static",
1476 | "libc",
1477 | "log",
1478 | "mysql",
1479 | "opencv",
1480 | "prometheus",
1481 | "regex",
1482 | "serde",
1483 | "serde_json",
1484 | "simple_moving_average",
1485 | "stderrlog",
1486 | "tiny_http",
1487 | ]
1488 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "zoneminder-aidect"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [[bin]]
9 | name = "zm-aidect"
10 | path = "src/main.rs"
11 |
12 | [dependencies]
13 | libc = "0.2"
14 | simple_moving_average = "0.1.2"
15 | mysql = { version = "22.2.0", default-features = false }
16 | flate2 = { version = "*", default-features = false, features = ["zlib"] }
17 | tiny_http = "0.11.0"
18 | prometheus = { version = "0.13.1", features = ["process"], default-features = false }
19 | lazy_static = "1.4.0"
20 | log = "0.4.17"
21 | stderrlog = "0.5.3"
22 | clap = { version = "3.2.12", features = ["derive"] }
23 | anyhow = "1.0.58"
24 | regex = "1.6.0"
25 | serde = { version = "1.0", features = ["derive"] }
26 | serde_json = "1"
27 |
28 | [dependencies.opencv]
29 | version = "0.66.0"
30 | default-features = false
31 | features = [
32 | "dnn",
33 | "imgcodecs",
34 | "imgproc",
35 | "features2d", # required for some reason I don't understand
36 | ]
37 |
38 | [profile.tagged]
39 | inherits = "release"
40 | # -rwxr-xr-x 1 root root 10177232 Jun 13 18:33 zm-aidect
41 |
42 | lto = true
43 | # -rwxr-xr-x 1 root root 6398288 Jun 13 18:25 zm-aidect
44 |
45 | opt-level = "z"
46 | # -rwxr-xr-x 1 root root 6622352 Jun 13 18:27 zm-aidect
47 |
48 | #strip = true
49 | # -rwxr-xr-x 1 root root 3512792 Jun 13 18:28 zm-aidect
50 |
51 | panic = "abort"
52 | # -rwxr-xr-x 1 root root 3164632 Jun 13 18:29 zm-aidect
53 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published by
637 | the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # zm-aidect
2 |
3 | -- A turnkey object (human, animal, vehicle) detection system for ZoneMinder --
4 |
5 | How it works: zm-aidect runs alongside ZoneMinder and feeds images from ZoneMinder into a highly sophisticated
6 | artifical intelligence (actually it's tinyYOLO-V4 and ML and not AI but marketing said otherwise). If the AI says something is there,
7 | zm-aidect tells ZoneMinder to record it. Configuration happens through ZoneMinder itself, just like motion detection. Events are recorded
8 | by ZoneMinder as per the usual.
9 |
10 | zm-aidect uses images decoded by ZoneMinder, so there is no double decoding of streams, and also no increase in camera traffic.
11 | zm-aidect works with any camera/video source configurable in ZoneMinder, not just IP cameras.
12 |
13 | ## Installation
14 |
15 | ### When using a Debian Bullseye or newer system
16 |
17 | You're in luck - I make binaries for these.
18 |
19 | ### From source
20 |
21 | * install opencv in whichever flavor you need (e.g. opencv-cuda package if you want to use CUDA)
22 | * install Rust
23 | * "cargo build --release"
24 | * ???
25 | * copy some files around I guess
26 |
27 | ## Configuration
28 |
29 | Assuming the systemd service has been installed, you only need to enable (and start) zm-aidect for the monitors it
30 | should run on (using `3` as the monitor ID; repeat these commands for every monitor you wish to use):
31 |
32 | # systemctl enable zm-aidect@3
33 | # systemctl start zm-aidect@3
34 |
35 | # systemctl status zm-aidect@3
36 | ● zm-aidect@3.service
37 | Loaded: loaded (/etc/systemd/system/zm-aidect@.service; enabled; vendor preset: enabled)
38 | Drop-In: /run/systemd/system/service.d
39 | └─zzz-lxc-service.conf
40 | Active: active (running) since Fri 2022-06-10 22:51:56 CEST; 1h 6min ago
41 | Main PID: 293563 (zm-aidect)
42 | Tasks: 4 (limit: 19145)
43 | Memory: 60.7M
44 | CPU: 3.141s
45 | CGroup: /system.slice/system-zm\x2daidect.slice/zm-aidect@3.service
46 | └─293563 /zm-aidect/zm-aidect run 3
47 |
48 | zm-aidect is pretty turnkey beyond this. You configure it ZoneMinder's web interface by adding a zone
49 | named "aidect". Objects will be detected if within the zone. You can additionally tweak various settings by
50 | adding them to the zone's name:
51 |
52 | * Threshold=XX adjusts the confidence threshold for detection (0-100 %). The default is 50%.
53 | * Size=XX adjusts the input size handed to the model. The default is 416 (pixels), 256 pixels works generally fine.
54 | 128 and 192 requires the objects to be fairly large in the frame. Inference is sped up by the *square* of this number, e.g.
55 | 256 is around 2.5x faster than 416, 128 can be up to 10x faster (depending on CPU thread count and/or if an accelerator is used).
56 | * Classes=1,2,3,... sets which classes trigger detection. By default only humans and cars will be detected.
57 | See class names. Because the length of the zone name is limited, we can't use human-readable names here.
58 | The default is: 1,3,15,16,17 (persons, cars, birds, cats and dogs).
59 | * MinArea=51600 filters detections by their area. In triggered events the area is indicated "aidect: Human (51.1%) 90x177 (=**15930**) at 440x385".
60 | Some things can persistently trigger medium-to-high confidence detections and filtering by area is a simple way to get rid of these.
61 | Alternatively, consider having the aidect zone not cover those patterns if they are static.
62 | * FPS=XX sets the maximum analysis fps for zm-aidect and zm-aidect alone. The default is the analysis FPS set in the monitor,
63 | and if that isn't set zm-aidect will, just like ZoneMinder's own analysis, run as fast as possible to try and catch them all.
64 | * Trigger=XX sets an alternative monitor ID for triggering. This is useful when evaluating zm-aidect, because
65 | you can attach zm-aidect to your normal substream monitor, but trigger events on a secondary nodect monitor so that
66 | you can compare whatever method you normally use and zm-aidect, without having to have two monitors decode
67 | the same stream. That's the only thing this option is good for.
68 |
69 | For example:
70 |
71 | aidect Size=128 Threshold=40 FPS=5
72 |
73 | Multiple "aidect" zones should not be added to a single monitor and aren't supported.
74 |
75 | Changing settings in ZoneMinder will be reflected within a few seconds in zm-aidect; you don't need to systemctl-restart
76 | it manually.
77 |
78 | Making the zone smaller does not speed things up (except if it's really small), but can improve detection accuracy.
79 | Note that the zone is embedded in a rectangular region, meaning that the ML model will see a rectangle fitting around
80 | the zone; having long protrusions thus reduces effectiveness. Overall, the ML model used here is good enough even
81 | for full-screen detection on very wide angle cameras. Don't sweat it. Zone placement and size matters A LOT less with
82 | zm-aidect than it does with traditional motion detection.
83 |
84 | ### Testing changes
85 |
86 | You can also run `zm-aidect test `, which will go through the startup, perform a single inference
87 | to ensure the process works, trigger an event, and exit. Some diagnostics will be printed as well, like if and which
88 | hardware accelerator is used by zm-aidect. This can be used to confirm that the settings are applied as wanted.
89 |
90 | Run `zm-aidect event [--monitor=ID] ` to have zm-aidect analyze the given event as-if it were watching live, using the current settings
91 | of the monitor the event belongs to. Detections will be printed, no triggering takes place.
92 | This can be used to verify that aidect does (not) detect something you (don't) want to detect without getting up.
93 | If you let zm-aidect analyze one monitor and trigger another, then you can use the `--monitor` option here
94 | to have zm-aidect use the correct monitor for reading the zone configuration.
95 |
96 | ## Performance
97 |
98 | Machine learning is very resource intensive. It *can* be done on CPUs, but it is vastly more CPU-intensive than
99 | ZoneMinder's motion detection (~100x more CPU used). This means that 1.) if you use zm-aidect without a GPU or other
100 | accelerator, you almost certainly will have to reduce the analysis FPS **a lot**. "Going from 10 fps to 1-2 fps"-lot.
101 | 2.) You will see **way** higher CPU load (and temperatures, power draw as well as possibly noise).
102 |
103 | That being said, the fact that ML is *extremely* insensitive towards lighting changes, as well as rapid non-object motion
104 | and very slow object motion (e.g. objects approaching head-on, slowly)¸ makes it worth it in my opinion. As far as I can tell
105 | analysis running at just one or two fps is not a significant issue for detection. ML is also very good at rejecting noise,
106 | probably in not-so-small part because the image is downscaled a ton.
107 |
108 | Finally, this uses a general-purpose object detection model, which has been pre-trained on a generic dataset. Hardly optimal.
109 | The model is designed to detect *eighty different classes* of objects, but even if you want to detect and capture cats and
110 | stray dogs - you most likely use less than five classes. It seems a certainty to me that performance could be greatly improved
111 | with a model tailored to and trained for this application.
112 |
113 | ### Performance at full-size (416x416)
114 |
115 | Input image size: 1280x720
116 |
117 | Network input size: 416x416 (default for tinyYOLO-v4)
118 |
119 | Note that this testing has been performed like the application works in practice, i.e. the batch size is one,
120 | with one inferrence being serially pre-processed, inferred and post-processed on the same thread. OpenCV can
121 | parallelize only the inferrence itself (theoretically the RGB->NCHW conversion and the NMS suppression as well,
122 | but that seems unlikely, the former is basically memcpy).
123 |
124 | * System idle
125 | * CPU: 26-30 W
126 | * GPU: 13 W (P8)
127 | * Ryzen 5600X using all cores, running at ~4.5 GHz with this load:
128 | * 17-18 ms per inference
129 | * 900 % CPU (~75 % utilization)
130 | * ~300 MB CPU memory
131 | * 100% PPT (83 W), core power ~60 W
132 | * Ryzen 5600X using one thread (at ~4.7 GHz):
133 | * 70-72 ms per inference
134 | * ~300 MB CPU memory
135 | * 60% PPT (~50 W), core power 20-23 W
136 | * 1080 Ti using CUDA/cuDNN and CV-managed threads:
137 | * 2.2-2.4 ms per inference
138 | * 75% core load
139 | * roughly 180/250 W
140 | * 140 % CPU (apparently CPU-limited)
141 | * CPU consumes around 50 W
142 | * Peak GPU memory use ~500 MB
143 | * Peak main memory use ~1.3 GB
144 | * 1080 Ti with just one CPU thread:
145 | * 2.3-2.4 ms per inference
146 | * 71% core load
147 | * ~170 W, 500 MB GPU mem, ~1.3 GB CPU mem
148 |
149 | GPU usage, power and memory numbers were taken from `nvidia-smi`, while the Ryzen power figures are taken from the SMU.
150 |
151 | Conclusions: Unsurprisingly, the GPU is quite a lot quicker with far superior perf/W. Single-threaded inference
152 | is very wasteful on the Ryzen due to the large I/O die and fabric power overhead (generally around ~20 W, which
153 | for lightly threaded workloads on Ryzen means that less than 50 % of the power goes into the cores).
154 |
155 | ```
156 | Ryzen R5 5600X 1T 14 Hz 50 W = 0.3 Hz/W
157 | Ryzen R5 5600X All-core 58 Hz 83 W = 0.7 Hz/W
158 | nVidia 1080 Ti 455 Hz 180+50 W = 2 Hz/W
159 | - 3x better power efficiency, even including CPU power on the inefficient Zen 3 Ryzen
160 | - 8x higher performance
161 | ```
162 |
163 | Another way to look at power efficiency is to consider the rise above idle. Because the 1080 Ti is much better at
164 | having low-power modes for idle and low-intensity workloads (like P8, with a claimed, but also independently
165 | verified ~13 W total board power), its power spread is larger than the Ryzen, which is generally quite poor about this
166 | (the multi-die SKUs that is, the monolithically made Ryzen APUs are quite good, because they're a totally different
167 | SoC architecture).
168 |
169 | Regardless, the four year older GPU maintains a more than comfortable ~2.4x power efficiency lead, as you would expect.
170 |
171 | ```
172 | Ryzen R5 5600X 1T 24 W = 0.6 Hz/W
173 | Ryzen R5 5600X All-core 57 W = 1 Hz/W
174 | nVidia 1080 Ti 167+24 W = 2.4 Hz/W
175 | ```
176 |
177 | Moral of the story: Don't do ML inference on CPUs. At least not desktop parts. Maybe AVX-512 on an Intel 7 Xeon changes
178 | the picture (a bit).
179 |
180 | ### Performance at reduced size (256x256)
181 |
182 | tinyYOLO-v4 retains pretty good detection performance for large-ish objects at this resolution. Depending on the
183 | exact application, 192x192 may still work satisfactorily as well.
184 |
185 | ```
186 | 5600X 1T 37 Hz (2.6x) 50 W = 0.7 Hz/W
187 | 5600X All-core 139 Hz (2.4x) 83 W = 1.7 Hz/W
188 | 1080 Ti 740 Hz (1.6x) 140+45 W = 4 Hz/W
189 | ```
190 |
191 | Efficiency gap shrinks (unsurprisingly: GPUs don't like small, one-off jobs). Performance scales almost perfectly in
192 | accordance with the size reduction (416/256**2 = 2.65), at least when using one thread. Multi-threading diminishes
193 | the impact a little bit; this gap would likely increase if you were to use more cores (per Amdahl). The GPU benefits
194 | least from reducing the size, which I'd interpret as the overhead of submitting the small inference jobs. Throughput
195 | should increase in a much more faithful manner if batching or multiple parallel processes are used.
196 |
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -xue
2 | /bin/time podman run --rm \
3 | -v "$PWD":/code -w /code -e CARGO_HOME=/code/target/podmancargocache \
4 | rust:1-bullseye \
5 | ./nativecontainerbuild.sh
6 |
7 |
--------------------------------------------------------------------------------
/containerbuild.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -xue
2 | apt update
3 | apt install -y clang libclang-dev llvm-dev cmake
4 |
5 | cd /
6 | git clone --branch 4.6.0 --depth 1 https://github.com/opencv/opencv
7 | mkdir opencv/build
8 | cd opencv/build
9 | cmake .. -DCPU_BASELINE=SSE4_2 -DCPU_DISPATCH= -DOPENCV_GENERATE_PKGCONFIG=YES -DBUILD_opencv_python2=OFF -DBUILD_opencv_python3=OFF \
10 | -DWITH_OPENCL=OFF -DWITH_FFMPEG=OFF -DWITH_V4L=OFF -DWITH_GSTREAMER=OFF -DWITH_1394=OFF \
11 | -DWITH_OPENEXR=OFF -DWITH_OPENJPEG=OFF -DWITH_JASPER=OFF -DWITH_WEBP=OFF -DWITH_TIFF=OFF -DWITH_JPEG=OFF \
12 | -DWITH_IMGCODEC_HDR=OFF -DWITH_IMGCODEC_SUNRASTER=OFF -DWITH_IMGCODEC_PXM=OFF -DWITH_IMGCODEC_PFM=OFF \
13 | -DBUILD_LIST=dnn -DBUILD_WITH_DYNAMIC_IPP=OFF -DWITH_GTK=OFF -DWITH_QT=OFF -DWITH_OPENGL=OFF
14 | make -j12
15 | make install
16 |
17 | cd /code
18 |
19 | #export OPENCV_INCLUDE_PATHS=/opencv/install/usr/local/include/opencv4/
20 | #export OPENCV_LINK_PATHS=/opencv/usr/local/lib
21 | #export OPENCV_LINK_LIBS=opencv_core,opencv_dnn,opencv_imgproc
22 | #,ittnotify,libprotobuf,ippip,ippicv,z,va,va-drm,OpenGL,GLX,GLU,dl,m,pthread,rt
23 | # ittnotify -llibprotobuf -lippiw -lippicv -L/lib64 -lz -lva -lva-drm -L/usr/lib -lOpenGL -lGLX -lGLU -ldl -lm -lpthread -lrt
24 | RUSTFLAGS='-C link-args=-Wl,-rpath,$ORIGIN' cargo build --release
25 |
26 | mkdir -p artifact
27 | cp target/release/zm-aidect artifact
28 | cp yolov4-tiny.cfg yolov4-tiny.weights artifact
29 | cp /usr/local/lib/libopencv_*.so.406 artifact
30 |
--------------------------------------------------------------------------------
/convert.sh:
--------------------------------------------------------------------------------
1 | scp $1/tmp/imago .
2 | convert -size 1280x720 -depth 8 rgba:imago imago.png
3 |
--------------------------------------------------------------------------------
/nativecontainerbuild.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -xue
2 |
3 | # Builds a binary linked against system OpenCV
4 |
5 | apt-get update
6 | apt-get install -y clang libclang-dev llvm-dev libopencv-dev libopencv-dnn-dev
7 |
8 | cargo build --release
9 | mkdir -p artifact
10 | cp target/release/zm-aidect artifact
11 | cp yolov4-tiny.cfg yolov4-tiny.weights artifact
12 |
13 | cp zm-aidect@.service artifact/
14 | sed -i "s#ExecStart=#ExecStart=/opt#" artifact/zm-aidect@.service
15 |
16 | tee artifact/INSTALL < String {
17 | let mut buffer = Vec::new();
18 | let encoder = TextEncoder::new();
19 | let metric_families = prometheus::gather();
20 | encoder.encode(&metric_families, &mut buffer).unwrap();
21 | String::from_utf8(buffer.clone()).unwrap()
22 | }
23 |
24 | pub fn spawn_prometheus_client(address: String, port: u16) {
25 | std::thread::spawn(move || {
26 | let server = tiny_http::Server::http((address, port)).unwrap();
27 | for request in server.incoming_requests() {
28 | let response = tiny_http::Response::from_string(collect());
29 | let _ = request.respond(response);
30 | }
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 | use std::env;
3 | use std::sync::mpsc;
4 | use std::time::{Duration, Instant};
5 |
6 | use anyhow::{anyhow, Context, Result};
7 | use clap::{Parser, Subcommand};
8 | use lazy_static::lazy_static;
9 | use log::{debug, error, info, warn};
10 | use opencv::core::{Mat, MatTraitConst, Rect};
11 | use simple_moving_average::SMA;
12 |
13 | use crate::ml::Detection;
14 | use crate::zoneminder::db::Bounding;
15 | use crate::zoneminder::{MonitorTrait};
16 |
17 | mod instrumentation;
18 | mod ml;
19 | mod vio;
20 | mod zoneminder;
21 |
22 | // TODO: Heed analysis images setting in ZM and generate those from within zm-aidect (sparsely, only for frames actually analyzed, not sure if the DB schema allows for that)
23 |
24 | #[derive(Parser, Debug)]
25 | #[clap(disable_help_subcommand = true)]
26 | struct Args {
27 | #[clap(
28 | long,
29 | short = 'v',
30 | parse(from_occurrences),
31 | global = true,
32 | help = "Increase log verbosity (stacks up to -vvv)"
33 | )]
34 | verbose: usize,
35 |
36 | #[clap(subcommand)]
37 | mode: Mode,
38 | }
39 |
40 | #[derive(Subcommand, Debug)]
41 | enum Mode {
42 | Run {
43 | /// Zoneminder monitor ID
44 | #[clap(value_parser)]
45 | monitor_id: u32,
46 | #[clap(long)]
47 | instrumentation_address: Option,
48 | #[clap(long, default_value_t = 9000)]
49 | instrumentation_port: u16,
50 | },
51 | Test {
52 | /// Zoneminder monitor ID
53 | #[clap(value_parser)]
54 | monitor_id: u32,
55 | },
56 | Event {
57 | /// Zoneminder event ID to check for detections
58 | #[clap(value_parser)]
59 | event_id: u64,
60 |
61 | /// Zoneminder monitor ID for the zone configuration
62 | #[clap(long, short = 'm')]
63 | monitor_id: Option,
64 | },
65 | }
66 |
67 | fn main() -> Result<()> {
68 | env::set_current_dir(env::current_exe()?.parent().unwrap())?;
69 |
70 | let args: Args = Args::parse();
71 | stderrlog::new()
72 | .module(module_path!())
73 | .verbosity(args.verbose + 1)
74 | .timestamp(stderrlog::Timestamp::Off)
75 | .init()
76 | .unwrap();
77 |
78 | match args.mode {
79 | Mode::Run { monitor_id, instrumentation_address, instrumentation_port } => run(monitor_id, instrumentation_address, instrumentation_port),
80 | Mode::Test { monitor_id } => test(monitor_id),
81 | Mode::Event {
82 | event_id,
83 | monitor_id,
84 | } => event(event_id, monitor_id),
85 | }
86 | }
87 |
88 | fn event(event_id: u64, monitor_id: Option) -> Result<()> {
89 | let zm_conf = zoneminder::ZoneMinderConf::parse_default()?;
90 | let event = zoneminder::db::Event::query(&zm_conf, event_id)?;
91 | let monitor_id = monitor_id.unwrap_or(event.monitor_id);
92 | let mut ctx = connect_zm(monitor_id, &zm_conf)?; // TODO: If this errors on "Error: No aidect zone found for monitor 6", suggest --monitor-id
93 |
94 | let video_path = event.video_path()?;
95 | println!("Analyzing video file {}", video_path.display());
96 | let props = vio::properties(&video_path)?;
97 |
98 | if props.width != ctx.monitor_settings.width || props.height != ctx.monitor_settings.height {
99 | println!("Note: Recording is from a different (higher?) resolution, so performance is not indicative due to rescaling");
100 | }
101 |
102 | println!("Note: Timestamps [mm:ss:ts] are at best a rough approximation.");
103 | println!("Note: Because analysis start frames aren't aligned between what zm-aidect might have originally done,");
104 | println!(" and this run, results can and will differ."); // TODO: This can be a good thing of course, but maybe add a way to analyse the logged alarm frames only or something like that
105 |
106 | let mut inference_durations = vec![];
107 | let mut videotime = Duration::default(); // EXTREMELY approximate
108 | let timestep = Duration::from_secs_f32(1f32 / ctx.max_fps); // video people are crying at this
109 | for image in vio::stream_file(
110 | &video_path,
111 | ctx.monitor_settings.width,
112 | ctx.monitor_settings.height,
113 | ctx.max_fps,
114 | )? {
115 | let result = infer(image, ctx.bounding_box, &ctx.zone_config, &mut ctx.yolo)?;
116 | if result.detections.len() > 0 {
117 | // TODO: How could we get the actual frame number or timestamp here?
118 |
119 | let ts = videotime.as_secs_f32();
120 | let frac = (ts.fract() * 1000f32) as u32;
121 | let seconds = ts.trunc() as u32;
122 | let secs = seconds % 60;
123 | let mins = seconds / 60;
124 |
125 | let description: Vec = result
126 | .detections
127 | .iter()
128 | .map(|d| describe(&CLASSES, &d))
129 | .collect();
130 | println!(
131 | "[{:02}:{:02}:{:03}] Inference took {:?}: {}",
132 | mins,
133 | secs,
134 | frac,
135 | result.duration,
136 | description.join(", ")
137 | );
138 | }
139 | inference_durations.push(result.duration);
140 | videotime += timestep;
141 | }
142 |
143 | let total_duration = inference_durations.iter().sum::();
144 | println!(
145 | "Processed {} frames, total ML time {:?}, average time {:?}",
146 | inference_durations.len(),
147 | total_duration,
148 | total_duration / inference_durations.len() as u32
149 | );
150 |
151 | Ok(())
152 | }
153 |
154 | struct MonitorContext<'zm_conf> {
155 | zm_conf: &'zm_conf zoneminder::ZoneMinderConf,
156 | monitor: zoneminder::Monitor<'zm_conf>,
157 | trigger_monitor: zoneminder::Monitor<'zm_conf>,
158 | zone_config: zoneminder::db::ZoneConfig,
159 | monitor_settings: zoneminder::db::MonitorSettings,
160 | bounding_box: Rect,
161 | yolo: ml::YoloV4Tiny,
162 | max_fps: f32,
163 | }
164 |
165 | fn connect_zm(monitor_id: u32, zm_conf: &zoneminder::ZoneMinderConf) -> Result {
166 | let monitor = zoneminder::Monitor::connect(zm_conf, monitor_id)?;
167 | let zone_config = zoneminder::db::ZoneConfig::get_zone_config(zm_conf, monitor_id)?;
168 | let monitor_settings = zoneminder::db::MonitorSettings::query(zm_conf, monitor_id)?;
169 |
170 | info!(
171 | "{}: Picked up zone configuration: {:?}",
172 | monitor_id, zone_config
173 | );
174 |
175 | let bounding_box = zone_config.shape.bounding_box();
176 | info!("{}: Picked up zone bounds {:?}", monitor_id, bounding_box);
177 |
178 | let max_fps = monitor_settings.analysis_fps_limit;
179 | let max_fps = zone_config.fps.or(max_fps);
180 | let max_fps = max_fps.ok_or(anyhow!("No analysis FPS limit set - set either \"Analysis FPS\" in the Zoneminder web console, or set the FPS key in the aidect zone."))?;
181 | info!("{}: Setting maximum fps to {}", monitor_id, max_fps);
182 |
183 | let trigger_id = zone_config.trigger.unwrap_or(monitor_id);
184 | info!("{}: Connecting to trigger monitor {}", monitor_id, trigger_id);
185 | let trigger_monitor = zoneminder::Monitor::connect(zm_conf, trigger_id)?;
186 |
187 | let size = zone_config.size.unwrap_or(256);
188 | let threshold = zone_config.threshold.unwrap_or(0.5);
189 | let yolo = ml::YoloV4Tiny::new(
190 | threshold,
191 | size,
192 | false,
193 | )?;
194 |
195 | instrumentation::SIZE.set(size as f64);
196 |
197 | Ok(MonitorContext {
198 | zm_conf,
199 | monitor,
200 | trigger_monitor,
201 | zone_config,
202 | monitor_settings,
203 | bounding_box,
204 | yolo,
205 | max_fps,
206 | })
207 | }
208 |
209 | struct Inferred {
210 | duration: Duration,
211 | detections: Vec,
212 | }
213 |
214 | fn infer(
215 | image: Mat,
216 | bounding_box: Rect,
217 | zone_config: &zoneminder::db::ZoneConfig,
218 | yolo: &mut ml::YoloV4Tiny,
219 | ) -> Result {
220 | assert_eq!(image.typ(), opencv::core::CV_8UC3);
221 | // TODO: blank remaining area outside zone polygon
222 | let image = Mat::roi(&image, bounding_box)?;
223 |
224 | let start = Instant::now();
225 | let detections = yolo.infer(&image)?;
226 | let duration = start.elapsed();
227 |
228 | let detections: Vec = detections
229 | .iter()
230 | .filter(|d| CLASSES.contains_key(&d.class_id))
231 | .filter(|d| {
232 | (d.bounding_box.width * d.bounding_box.height) as u32
233 | > zone_config.min_area.unwrap_or(0)
234 | })
235 | .map(|d| Detection {
236 | // Adjust bounding box to zone bounding box (RoI)
237 | bounding_box: Rect {
238 | x: d.bounding_box.x + bounding_box.x,
239 | y: d.bounding_box.y + bounding_box.y,
240 | ..d.bounding_box
241 | },
242 | ..*d
243 | })
244 | .collect();
245 |
246 | Ok(Inferred {
247 | duration,
248 | detections,
249 | })
250 | }
251 |
252 | fn trigger(ctx: &MonitorContext, description: &str, score: u32) -> Result {
253 | ctx.trigger_monitor
254 | .trigger("aidect", description, score)
255 | .with_context(|| format!("Failed to trigger monitor ID {}", ctx.trigger_monitor.id()))
256 | }
257 |
258 | fn test(monitor_id: u32) -> Result<()> {
259 | let zm_conf = zoneminder::ZoneMinderConf::parse_default()?;
260 | let mut ctx = connect_zm(monitor_id, &zm_conf)?;
261 |
262 | println!(
263 | "Connected to monitor ID {}: {}",
264 | monitor_id, ctx.monitor_settings.name
265 | );
266 |
267 | let num_images = 3;
268 | println!("Grabbing {} images and running detection", num_images);
269 | for image in ctx.monitor.stream_images()?.take(num_images) {
270 | let image = image?.convert_to_rgb24()?;
271 | let result = infer(image, ctx.bounding_box, &ctx.zone_config, &mut ctx.yolo)?;
272 | let description: Vec = result
273 | .detections
274 | .iter()
275 | .map(|d| describe(&CLASSES, &d))
276 | .collect();
277 | println!(
278 | "Inference took {:?}: {}",
279 | result.duration,
280 | description.join(", ")
281 | );
282 | }
283 |
284 | println!("Triggering an event on monitor {}", ctx.trigger_monitor.id());
285 | let event_id = trigger(&ctx, "zm-aidect test", 1)?;
286 | println!("Success, event ID is {}", event_id);
287 |
288 | Ok(())
289 | }
290 |
291 | lazy_static! {
292 | static ref CLASSES: HashMap = [ // TODO this should be loaded at runtime from the model definition
293 | (1, "Human"),
294 | (3, "Car"),
295 | (15, "Bird"),
296 | (16, "Cat"),
297 | (17, "Dog"),
298 | ].into();
299 | }
300 |
301 | fn run(monitor_id: u32, instrumentation_address: Option, instrumentation_port: u16) -> Result<()> {
302 | let zm_conf = zoneminder::ZoneMinderConf::parse_default()?;
303 | let mut ctx = connect_zm(monitor_id, &zm_conf)?;
304 |
305 | if let Some(address) = instrumentation_address {
306 | instrumentation::spawn_prometheus_client(address, instrumentation_port + monitor_id as u16);
307 | }
308 |
309 | let mut pacemaker = RealtimePacemaker::new(ctx.max_fps);
310 | let mut event_tracker = coalescing::EventTracker::new();
311 |
312 | // watchdog is set to 20x max_fps frame interval
313 | let watchdog = ThreadedWatchdog::new(Duration::from_secs_f32(20.0 / ctx.max_fps));
314 |
315 | fn process_update_event(ctx: &MonitorContext, update: Option) {
316 | if let Some(update) = update {
317 | let description = describe(&CLASSES, &update.detection);
318 | if let Err(e) =
319 | zoneminder::db::update_event_notes(&ctx.zm_conf, update.event_id, &description)
320 | {
321 | error!(
322 | "{}: Failed to update event {} notes: {}",
323 | ctx.trigger_monitor.id(), update.event_id, e
324 | );
325 | }
326 | }
327 | }
328 |
329 | // For yolov4-tiny and moderate input sizes, multithreading does speed things up, but at the expense
330 | // of higher overall CPU usage. As you would usually have multiple zm-aidect processes running, as
331 | // well as zmc, there is no particular need for a single zm-aidect process to scale to multiple cores,
332 | // especially when that comes with an efficiency hit. Large inputs and/or high framerates aren't
333 | // sensible on a CPU anyway.
334 | opencv::core::set_num_threads(1)?;
335 |
336 | for image in ctx.monitor.stream_images()? {
337 | let image = image?.convert_to_rgb24()?;
338 | let Inferred {
339 | duration: inference_duration,
340 | detections,
341 | } = infer(image, ctx.bounding_box, &ctx.zone_config, &mut ctx.yolo)?;
342 |
343 | if detections.len() > 0 {
344 | debug!(
345 | "{}: Inference result (took {:?}): {:?}",
346 | monitor_id, inference_duration, detections
347 | );
348 |
349 | let d = detections
350 | .iter()
351 | .max_by_key(|d| (d.confidence * 1000.0) as u32)
352 | .unwrap(); // generally there will only be one anyway
353 | let score = (d.confidence * 100.0) as u32;
354 | let description = describe(&CLASSES, &d);
355 |
356 | let event_id = trigger(&ctx, &description, score)?;
357 | let update = event_tracker.push_detection(d.clone(), event_id);
358 | process_update_event(&ctx, update);
359 | }
360 |
361 | if ctx.trigger_monitor.is_idle()? {
362 | // Not recording any more, flush current event description if any
363 | let update = event_tracker.clear();
364 | if update.is_some() {
365 | debug!("Flushing event because idle");
366 | }
367 | process_update_event(&ctx, update);
368 | }
369 |
370 | if inference_duration.as_secs_f32() > pacemaker.target_interval {
371 | warn!(
372 | "{}: Cannot keep up with max-analysis-fps (inference taking {:?})!",
373 | monitor_id, inference_duration,
374 | );
375 | }
376 |
377 | instrumentation::INFERENCE_DURATION.observe(inference_duration.as_secs_f64());
378 | instrumentation::INFERENCES.inc();
379 |
380 | pacemaker.tick();
381 | watchdog.reset();
382 | let current_fps = pacemaker.current_frequency() as f64;
383 | instrumentation::FPS.set(current_fps);
384 | instrumentation::FPS_DEVIATION.set(current_fps - ctx.max_fps as f64);
385 | }
386 | Ok(())
387 | }
388 |
389 | fn describe(classes: &HashMap, d: &Detection) -> String {
390 | format!(
391 | "{} ({:.1}%) {}x{} (={}) at {}x{}",
392 | classes[&d.class_id],
393 | d.confidence * 100.0,
394 | d.bounding_box.width,
395 | d.bounding_box.height,
396 | d.bounding_box.width * d.bounding_box.height,
397 | d.bounding_box.x,
398 | d.bounding_box.y,
399 | )
400 | }
401 |
402 | mod coalescing {
403 | use log::trace;
404 |
405 | use crate::ml::Detection;
406 |
407 | struct TrackedEvent {
408 | event_id: u64,
409 | detections: Vec,
410 | }
411 |
412 | pub struct UpdateEvent {
413 | pub event_id: u64,
414 | pub detection: Detection,
415 | }
416 |
417 | pub struct EventTracker {
418 | current_event: Option,
419 | }
420 |
421 | impl EventTracker {
422 | pub fn new() -> EventTracker {
423 | EventTracker {
424 | current_event: None,
425 | }
426 | }
427 |
428 | pub fn push_detection(&mut self, d: Detection, event_id: u64) -> Option {
429 | let mut update = None;
430 | if let Some(current_event) = self.current_event.as_mut() {
431 | if current_event.event_id != event_id {
432 | trace!("Flushing event {} -> {}", current_event.event_id, event_id);
433 | update = self.clear();
434 | } else {
435 | current_event.detections.push(d);
436 | return None;
437 | }
438 | }
439 | self.current_event = Some(TrackedEvent {
440 | event_id,
441 | detections: vec![d],
442 | });
443 | update
444 | }
445 |
446 | pub fn clear(&mut self) -> Option {
447 | let current_event = self.current_event.take()?;
448 | let detection = current_event
449 | .detections
450 | .iter()
451 | .max_by_key(|d| (d.confidence * 1000.0) as u32)
452 | .unwrap();
453 | // TODO: aggregate by classes, annotate counts.
454 | trace!(
455 | "Coalesce {} with {:?} to {:?}",
456 | current_event.event_id,
457 | current_event.detections,
458 | detection
459 | );
460 | Some(UpdateEvent {
461 | event_id: current_event.event_id,
462 | detection: detection.clone(),
463 | })
464 | }
465 | }
466 | }
467 |
468 | trait Pacemaker {
469 | fn tick(&mut self);
470 | fn current_frequency(&self) -> f32;
471 | }
472 |
473 | struct RealtimePacemaker {
474 | target_interval: f32,
475 | last_tick: Option,
476 | avg: simple_moving_average::NoSumSMA,
477 | current_frequency: f32,
478 | }
479 |
480 | impl RealtimePacemaker {
481 | fn new(frequency: f32) -> RealtimePacemaker {
482 | RealtimePacemaker {
483 | target_interval: 1.0f32 / frequency,
484 | last_tick: None,
485 | avg: simple_moving_average::NoSumSMA::new(),
486 | current_frequency: 0.0,
487 | }
488 | }
489 | }
490 |
491 | impl Pacemaker for RealtimePacemaker {
492 | fn tick(&mut self) {
493 | if let Some(last_iteration) = self.last_tick {
494 | let now = Instant::now();
495 | let frame_duration = (now - last_iteration).as_secs_f32(); // how long the paced workload ran
496 | // smoothing using moving average
497 | self.avg.add_sample(frame_duration);
498 | let average_duration = self.avg.get_average();
499 |
500 | let sleep_duration = self.target_interval - average_duration;
501 | if sleep_duration > 0.0 {
502 | std::thread::sleep(Duration::from_secs_f32(sleep_duration));
503 | }
504 |
505 | // calculate current frequency from the tick interval (workload + sleeping)
506 | let tick_interval = Instant::now() - last_iteration;
507 | self.current_frequency = 1.0f32 / tick_interval.as_secs_f32();
508 | }
509 | self.last_tick = Some(Instant::now());
510 | }
511 |
512 | fn current_frequency(&self) -> f32 {
513 | self.current_frequency
514 | }
515 | }
516 |
517 | trait Watchdog {
518 | fn reset(&self) -> ();
519 | }
520 |
521 | struct ThreadedWatchdog {
522 | tx: mpsc::Sender<()>,
523 | }
524 |
525 | impl ThreadedWatchdog {
526 | fn new(timeout: Duration) -> ThreadedWatchdog {
527 | let (tx, rx) = mpsc::channel();
528 |
529 | std::thread::spawn(move || loop {
530 | if let Err(mpsc::RecvTimeoutError::Timeout) = rx.recv_timeout(timeout) {
531 | error!("Watchdog expired, terminating.");
532 | std::process::exit(1);
533 | }
534 | });
535 |
536 | ThreadedWatchdog { tx }
537 | }
538 | }
539 |
540 | impl Watchdog for ThreadedWatchdog {
541 | fn reset(&self) -> () {
542 | self.tx.send(()).unwrap()
543 | }
544 | }
545 |
--------------------------------------------------------------------------------
/src/ml.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 | use std::hash::{Hash, Hasher};
3 |
4 | use opencv::core::{Mat, MatTraitConst, MatTraitConstManual, Rect, Vector, CV_8U};
5 | use opencv::dnn::{
6 | blob_from_image, nms_boxes, read_net, DictValue, LayerTraitConst, Net, NetTrait, NetTraitConst,
7 | };
8 | use opencv::types::{VectorOfMat, VectorOfRect};
9 |
10 | #[derive(Clone, Debug)]
11 | pub struct Detection {
12 | pub confidence: f32,
13 | pub class_id: i32,
14 | pub bounding_box: Rect,
15 | }
16 |
17 | impl Detection {
18 | fn confidence_pct(&self) -> u8 {
19 | (self.confidence * 100.0) as u8
20 | }
21 | }
22 |
23 | impl Hash for Detection {
24 | fn hash(&self, state: &mut H) {
25 | state.write_u8(self.confidence_pct());
26 | state.write_i32(self.class_id);
27 | state.write_i32(self.bounding_box.width);
28 | state.write_i32(self.bounding_box.height);
29 | state.write_i32(self.bounding_box.x);
30 | state.write_i32(self.bounding_box.y);
31 | }
32 | }
33 |
34 | impl PartialEq for Detection {
35 | fn eq(&self, other: &Self) -> bool {
36 | self.confidence_pct() == other.confidence_pct()
37 | && self.class_id == other.class_id
38 | && self.bounding_box == other.bounding_box
39 | }
40 | }
41 |
42 | impl Eq for Detection {}
43 |
44 | pub struct YoloV4Tiny {
45 | net: Net,
46 | confidence_threshold: f32,
47 | nms_threshold: f32,
48 | size: u32,
49 |
50 | out_names: Vector,
51 | }
52 |
53 | impl YoloV4Tiny {
54 | pub fn new(confidence_threshold: f32, size: u32, use_cuda: bool) -> opencv::Result {
55 | let mut net = read_net("yolov4-tiny.weights", "yolov4-tiny.cfg", "")?;
56 | if use_cuda {
57 | net.set_preferable_target(opencv::dnn::DNN_TARGET_CUDA)?;
58 | net.set_preferable_backend(opencv::dnn::DNN_BACKEND_CUDA)?;
59 | } else {
60 | net.set_preferable_target(opencv::dnn::DNN_TARGET_CPU)?;
61 | net.set_preferable_backend(opencv::dnn::DNN_BACKEND_OPENCV)?;
62 | }
63 |
64 | let out_names = net.get_unconnected_out_layers_names()?;
65 | let out_layers = net.get_unconnected_out_layers()?;
66 | let out_layer_type = net
67 | .get_layer(DictValue::from_i32(out_layers.get(0).unwrap())?)
68 | .unwrap()
69 | .typ();
70 | assert_eq!(out_layer_type, "Region");
71 |
72 | Ok(YoloV4Tiny {
73 | net,
74 | size,
75 | out_names,
76 | confidence_threshold,
77 | nms_threshold: 0.4,
78 | })
79 | }
80 |
81 | pub fn infer(&mut self, image: &Mat) -> opencv::Result> {
82 | let size = self.size as i32;
83 | let size = (size, size);
84 | let mean = (0.0, 0.0, 0.0);
85 | let blob = blob_from_image(&image, 1.0, size.into(), mean.into(), false, false, CV_8U)?;
86 | let scale = 1.0 / 255.0;
87 | self.net.set_input(&blob, "", scale, mean.into())?;
88 |
89 | let outs = {
90 | let mut outs = VectorOfMat::new();
91 | self.net.forward(&mut outs, &self.out_names)?;
92 | outs
93 | };
94 |
95 | let image_width = image.cols() as f32;
96 | let image_height = image.rows() as f32;
97 |
98 | let detections: Vec = outs
99 | .iter()
100 | .map(|out| {
101 | // Network produces output blob with a shape NxC where N is a number of
102 | // detected objects and C is a number of classes + 4 where the first 4
103 | // numbers are [center_x, center_y, width, height]
104 |
105 | (0..out.rows())
106 | .map(move |i| {
107 | let row = out.at_row::(i).unwrap();
108 |
109 | let get_bounding_box = |row: &[f32]| -> Rect {
110 | let (center_x, center_y) = (row[0], row[1]);
111 | let (width, height) = (row[2], row[3]);
112 |
113 | let center_x = (center_x * image_width) as i32;
114 | let center_y = (center_y * image_height) as i32;
115 | let width = (width * image_width) as i32;
116 | let height = (height * image_height) as i32;
117 |
118 | let left_edge = (center_x - width / 2).max(0);
119 | let top_edge = (center_y - height / 2).max(0);
120 |
121 | Rect::new(left_edge, top_edge, width, height)
122 | };
123 |
124 | let get_class = |row: &[f32]| {
125 | let class = row[4..]
126 | .iter()
127 | //.cloned()
128 | .zip(1..) // 1.. for 1-based class index, 0.. for 0-based
129 | .max_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
130 | let (&confidence, class_id) = class.unwrap();
131 | (confidence, class_id)
132 | };
133 |
134 | let (confidence, class_id) = get_class(row);
135 | let bounding_box = get_bounding_box(row);
136 |
137 | Detection {
138 | confidence,
139 | class_id,
140 | bounding_box,
141 | }
142 | })
143 | .filter(|detection| detection.confidence >= self.confidence_threshold)
144 | })
145 | .flatten()
146 | .collect();
147 |
148 | // Perform NMS filtering
149 | let mut class2detections: HashMap> = HashMap::new();
150 | for detection in &detections {
151 | let dets = class2detections
152 | .entry(detection.class_id)
153 | .or_insert_with(Vec::new);
154 | dets.push(&detection);
155 | }
156 |
157 | let mut nms_detections = vec![];
158 |
159 | for (_, detections) in &class2detections {
160 | let bounding_boxes: VectorOfRect =
161 | detections.iter().map(|det| det.bounding_box).collect();
162 | let confidences: Vector = detections.iter().map(|det| det.confidence).collect();
163 | let mut chosen_indices = Vector::new();
164 | nms_boxes(
165 | &bounding_boxes,
166 | &confidences,
167 | self.confidence_threshold,
168 | self.nms_threshold,
169 | &mut chosen_indices,
170 | 1.0,
171 | 0,
172 | )?;
173 |
174 | for index in chosen_indices {
175 | nms_detections.push(detections[index as usize].clone());
176 | }
177 | }
178 |
179 | Ok(nms_detections)
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/src/vio.rs:
--------------------------------------------------------------------------------
1 | use std::io::Read;
2 | use std::path::Path;
3 | use std::process::{Child, Command, Stdio};
4 |
5 | use anyhow::{anyhow, Result};
6 | use opencv::core::{Mat, MatTraitManual};
7 | use serde::Deserialize;
8 |
9 | #[derive(Debug, Deserialize, Eq, PartialEq)]
10 | struct ProbeOutput {
11 | streams: Vec,
12 | }
13 |
14 | #[derive(Debug, Deserialize, Eq, PartialEq)]
15 | pub struct VideoProperties {
16 | codec_name: String,
17 | avg_frame_rate: String,
18 | pub width: u32,
19 | pub height: u32,
20 | }
21 |
22 | impl VideoProperties {
23 | pub fn get_fps(&self) -> f32 {
24 | let (a, b) = self.avg_frame_rate.split_once('/').unwrap();
25 | let (a, b) = (a.parse::().unwrap(), (b.parse::().unwrap()));
26 | a / b
27 | }
28 | }
29 |
30 | impl ToString for VideoProperties {
31 | fn to_string(&self) -> String {
32 | format!(
33 | "{}x{} {:.1} fps ({})",
34 | self.width,
35 | self.height,
36 | self.get_fps(),
37 | self.codec_name
38 | )
39 | }
40 | }
41 |
42 | pub fn properties(path: &Path) -> Result {
43 | let output = Command::new("ffprobe")
44 | .args([
45 | "-v",
46 | "error",
47 | "-print_format",
48 | "json",
49 | "-select_streams",
50 | "v:0",
51 | "-show_streams",
52 | ])
53 | .arg(path)
54 | .output()?;
55 | if !output.status.success() {
56 | return Err(anyhow!(
57 | "ffprobe failed: {}",
58 | String::from_utf8_lossy(&output.stderr)
59 | ));
60 | }
61 | let output = String::from_utf8(output.stdout)?;
62 | let mut output: ProbeOutput = serde_json::from_str(&output)?;
63 | Ok(output.streams.remove(0))
64 | }
65 |
66 | pub struct ImageStream {
67 | width: u32,
68 | height: u32,
69 | ffmpeg: Child,
70 | }
71 |
72 | impl Iterator for ImageStream {
73 | type Item = Mat;
74 |
75 | fn next(&mut self) -> Option {
76 | let mut mat = Mat::new_size_with_default(
77 | (self.width as i32, self.height as i32).into(),
78 | opencv::core::CV_8UC3,
79 | 0.into(),
80 | )
81 | .ok()?;
82 | let mut slice = mat.data_bytes_mut().expect("Got an non-continuous Mat for some reason?");
83 | let stdout = self.ffmpeg.stdout.as_mut()?;
84 | stdout.read_exact(&mut slice).ok()?;
85 | return Some(mat);
86 | }
87 | }
88 |
89 | pub fn stream_file(path: &Path, width: u32, height: u32, framerate: f32) -> Result {
90 | let video_size = format!("{}x{}", width, height);
91 | let framerate = framerate.to_string();
92 | let ffmpeg = Command::new("ffmpeg")
93 | .args(["-v", "error", "-i"])
94 | .arg(path)
95 | .args([
96 | "-f",
97 | "rawvideo",
98 | "-pix_fmt",
99 | "rgb24",
100 | "-s:v",
101 | &video_size,
102 | "-sws_flags",
103 | "neighbor",
104 | "-r",
105 | &framerate,
106 | "-",
107 | ])
108 | .stdout(Stdio::piped())
109 | .spawn()?;
110 |
111 | Ok(ImageStream {
112 | width,
113 | height,
114 | ffmpeg,
115 | })
116 | }
117 |
118 | #[cfg(test)]
119 | mod tests {
120 | use super::*;
121 | use anyhow::Result;
122 |
123 | #[test]
124 | fn test_parse_ffprobe() -> Result<()> {
125 | let ffprobe = r#"
126 | {
127 | "streams": [
128 | {
129 | "index": 0,
130 | "codec_name": "h264",
131 | "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
132 | "profile": "High",
133 | "codec_type": "video",
134 | "codec_time_base": "7491/449600",
135 | "codec_tag_string": "avc1",
136 | "codec_tag": "0x31637661",
137 | "width": 1920,
138 | "height": 1080,
139 | "coded_width": 1920,
140 | "coded_height": 1088,
141 | "closed_captions": 0,
142 | "has_b_frames": 0,
143 | "pix_fmt": "yuvj420p",
144 | "level": 42,
145 | "color_range": "pc",
146 | "color_space": "bt470bg",
147 | "color_transfer": "bt709",
148 | "color_primaries": "bt470bg",
149 | "chroma_location": "left",
150 | "refs": 1,
151 | "is_avc": "true",
152 | "nal_length_size": "4",
153 | "r_frame_rate": "100/1",
154 | "avg_frame_rate": "2248/74",
155 | "time_base": "1/90000",
156 | "start_pts": 0,
157 | "start_time": "0.000000",
158 | "duration_ts": 6741900,
159 | "duration": "74.910000",
160 | "bit_rate": "3046762",
161 | "bits_per_raw_sample": "8",
162 | "disposition": {
163 | "default": 1,
164 | "dub": 0,
165 | "original": 0,
166 | "comment": 0,
167 | "lyrics": 0,
168 | "karaoke": 0,
169 | "forced": 0,
170 | "hearing_impaired": 0,
171 | "visual_impaired": 0,
172 | "clean_effects": 0,
173 | "attached_pic": 0,
174 | "timed_thumbnails": 0
175 | },
176 | "tags": {
177 | "language": "und",
178 | "handler_name": "VideoHandler"
179 | }
180 | }
181 | ]
182 | }"#;
183 |
184 | assert_eq!(
185 | serde_json::from_str::(&ffprobe)?,
186 | ProbeOutput {
187 | streams: vec![VideoProperties {
188 | codec_name: "h264".to_string(),
189 | avg_frame_rate: "2248/74".to_string(),
190 | width: 1920,
191 | height: 1080,
192 | }]
193 | }
194 | );
195 | Ok(())
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/src/zoneminder.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 | use std::fs::{self, File, OpenOptions};
3 | use std::mem::size_of;
4 | use std::os::unix::fs::{FileExt, MetadataExt};
5 | use std::time::Duration;
6 |
7 | use anyhow::{anyhow, Context, Result};
8 | use libc::timeval;
9 | use log::error;
10 | use opencv::core::{Mat, MatTraitConst, MatTraitManual};
11 |
12 | use crate::zoneminder::db::MonitorSettings;
13 |
14 | pub mod db;
15 | mod shm;
16 |
17 | pub trait MonitorTrait<'this> {
18 | // for lack of a better term
19 | type ImageIterator: Iterator- >;
20 |
21 | fn stream_images(&'this self) -> Result;
22 |
23 | fn is_idle(&self) -> Result;
24 |
25 | fn trigger(&self, cause: &str, description: &str, score: u32) -> Result;
26 |
27 | fn id(&self) -> u32;
28 | }
29 |
30 | pub struct Monitor<'zmconf> {
31 | monitor_id: u32,
32 | zm_conf: &'zmconf ZoneMinderConf,
33 |
34 | mmap_path: String,
35 | ino: u64,
36 | shm: shm::MonitorShm,
37 | }
38 |
39 | impl<'this> MonitorTrait<'this> for Monitor<'this> {
40 | type ImageIterator = ImageStream<'this>;
41 |
42 | fn stream_images(&'this self) -> Result {
43 | let state = self.read()?;
44 | let settings = MonitorSettings::query(self.zm_conf, self.monitor_id)?;
45 | let image_buffer_count = settings.image_buffer_count;
46 |
47 | // now that we have the image buffer size we can figure the dynamic offsets out
48 | let shared_timestamps_offset = self.shm.read_field::(shm::ShmField::SHARED_SIZE)?
49 | + self.shm.read_field::(shm::ShmField::TRIGGER_SIZE)?
50 | + self.shm.read_field::(shm::ShmField::VIDEOSTORE_SIZE)?;
51 | let shared_images_offset =
52 | shared_timestamps_offset as usize + image_buffer_count as usize * size_of::();
53 | let shared_images_offset = shared_images_offset + 64 - (shared_images_offset % 64);
54 |
55 | Ok(ImageStream {
56 | width: settings.width,
57 | height: settings.height,
58 | image_buffer_count,
59 | monitor: self,
60 | last_read_index: image_buffer_count,
61 | image_size: state.imagesize,
62 | format: state.format,
63 | shared_images_offset: shared_images_offset as u64,
64 | })
65 | }
66 |
67 | fn is_idle(&self) -> Result {
68 | Ok(self.read()?.state == shm::MonitorState::Idle)
69 | }
70 |
71 | /// Mark at least one frame as an alarm frame with the given score. Wait for event to be created,
72 | /// then return event ID. Does not necessarily cause creation of a new event.
73 | fn trigger(&self, cause: &str, description: &str, score: u32) -> Result {
74 | let poll_interval = 10;
75 | self.set_trigger(cause, description, score)?;
76 | for n in 0.. {
77 | let state = self.read()?.state;
78 | // Alarm sorta implies that we just triggered an alarm frame, while
79 | // Alert sorta implies there's an on-going event.
80 | // Wait for Alarm state to become active so that the frame is marked.
81 | if state == shm::MonitorState::Alarm {
82 | break;
83 | }
84 | std::thread::sleep(Duration::from_millis(poll_interval));
85 | if n > 500 {
86 | error!("Waited {} ms for zoneminder to notice our bulgy wulgy, giving up and canceling it :c", n * poll_interval);
87 | }
88 | }
89 | self.reset_trigger()?;
90 | Ok(self.read()?.last_event_id)
91 | }
92 |
93 | fn id(&self) -> u32 {
94 | self.monitor_id
95 | }
96 | }
97 |
98 | impl Monitor<'_> {
99 | pub fn connect(zm_conf: &ZoneMinderConf, monitor_id: u32) -> Result {
100 | let mmap_path = format!("{}/zm.mmap.{}", zm_conf.mmap_path, monitor_id);
101 | let file = OpenOptions::new()
102 | .read(true)
103 | .write(true)
104 | .open(&mmap_path)
105 | .with_context(|| {
106 | format!(
107 | "Failed to open mmap file {} for monitor {}",
108 | mmap_path, monitor_id
109 | )
110 | })?;
111 |
112 | Ok(Monitor {
113 | monitor_id,
114 | zm_conf,
115 | mmap_path,
116 | ino: file.metadata()?.ino(),
117 | shm: shm::MonitorShm::new(file)?,
118 | })
119 | }
120 |
121 | fn set_trigger(&self, cause: &str, description: &str, score: u32) -> Result<()> {
122 | self.shm.write_string(shm::ShmField::TRIGGER_CAUSE, cause)?;
123 | self.shm
124 | .write_string(shm::ShmField::TRIGGER_TEXT, description)?;
125 | self.shm.write_string(shm::ShmField::TRIGGER_SHOWTEXT, "")?;
126 | self.shm.write_field(shm::ShmField::TRIGGER_SCORE, &score)?;
127 | // all of this is terribly racy but pwritin' the data before the state change should reduce the odds of problems
128 | self.shm
129 | .write_field(shm::ShmField::TRIGGER_STATE, &shm::TriggerState::TriggerOn)
130 | }
131 |
132 | fn reset_trigger(&self) -> Result<()> {
133 | self.shm.write_string(shm::ShmField::TRIGGER_CAUSE, "")?;
134 | self.shm.write_string(shm::ShmField::TRIGGER_TEXT, "")?;
135 | self.shm.write_string(shm::ShmField::TRIGGER_SHOWTEXT, "")?;
136 | self.shm.write_field(shm::ShmField::TRIGGER_SCORE, &0)?;
137 | self.shm.write_field(
138 | shm::ShmField::TRIGGER_STATE,
139 | &shm::TriggerState::TriggerCancel,
140 | )
141 | }
142 |
143 | fn read(&self) -> Result {
144 | if self.shm.read_field::(shm::ShmField::VALID)? == 0 {
145 | return Err(anyhow!("Monitor shm is not valid"));
146 | }
147 | self.check_file_stale()?;
148 |
149 | Ok(MonitorState {
150 | last_write_index: self.shm.read_field(shm::ShmField::LAST_WRITE_INDEX)?,
151 | state: self.shm.read_field(shm::ShmField::STATE)?,
152 | last_event_id: self.shm.read_field(shm::ShmField::LAST_EVENT_ID)?,
153 | format: self.shm.read_field(shm::ShmField::FORMAT)?,
154 | imagesize: self.shm.read_field(shm::ShmField::IMAGESIZE)?,
155 | })
156 | }
157 |
158 | fn check_file_stale(&self) -> Result<()> {
159 | // Additional sanity check, if the file-on-tmpfs is now a different file, we're definitely listening to a stranger.
160 | // ZM seems to be quite good about ensuring shared_data.valid gets flipped to 0 even when zmc crashes though.
161 | if fs::metadata(&self.mmap_path)
162 | .with_context(|| format!("Monitor mmap file {} does not exist", self.mmap_path))?
163 | .ino()
164 | != self.ino
165 | {
166 | return Err(anyhow!("Monitor shm fd is stale, must reconnect"));
167 | }
168 | Ok(())
169 | }
170 | }
171 |
172 | fn zm_format_to_cv_format(format: shm::SubpixelOrder) -> i32 {
173 | match format {
174 | shm::SubpixelOrder::NONE => opencv::core::CV_8UC1,
175 | shm::SubpixelOrder::RGB => opencv::core::CV_8UC3,
176 | shm::SubpixelOrder::BGR => opencv::core::CV_8UC3,
177 | shm::SubpixelOrder::BGRA => opencv::core::CV_8UC4,
178 | shm::SubpixelOrder::RGBA => opencv::core::CV_8UC4,
179 | shm::SubpixelOrder::ABGR => opencv::core::CV_8UC4,
180 | shm::SubpixelOrder::ARGB => opencv::core::CV_8UC4,
181 | }
182 | }
183 |
184 | pub struct Image {
185 | image: Mat,
186 | format: shm::SubpixelOrder,
187 | }
188 |
189 | impl Image {
190 | pub fn convert_to_rgb24(self) -> Result {
191 | let conversion = match self.format {
192 | shm::SubpixelOrder::NONE => Some(opencv::imgproc::COLOR_GRAY2RGB),
193 | shm::SubpixelOrder::RGB => None,
194 | shm::SubpixelOrder::BGR => Some(opencv::imgproc::COLOR_BGR2RGB),
195 | shm::SubpixelOrder::BGRA => Some(opencv::imgproc::COLOR_BGRA2RGB),
196 | shm::SubpixelOrder::RGBA => Some(opencv::imgproc::COLOR_RGBA2RGB),
197 | _ => panic!("Unsupported pixel format: {:?}", self.format),
198 | };
199 | self.convert(conversion)
200 | }
201 |
202 | #[allow(dead_code)]
203 | pub fn convert_to_rgb32(self) -> Result {
204 | let conversion = match self.format {
205 | shm::SubpixelOrder::NONE => Some(opencv::imgproc::COLOR_GRAY2RGBA),
206 | shm::SubpixelOrder::RGB => Some(opencv::imgproc::COLOR_RGB2RGBA),
207 | shm::SubpixelOrder::BGR => Some(opencv::imgproc::COLOR_BGR2RGBA),
208 | shm::SubpixelOrder::BGRA => Some(opencv::imgproc::COLOR_BGRA2RGBA),
209 | shm::SubpixelOrder::RGBA => None,
210 | _ => panic!("Unsupported pixel format: {:?}", self.format),
211 | };
212 | self.convert(conversion)
213 | }
214 |
215 | #[allow(dead_code)]
216 | pub fn convert_to_gray(self) -> Result {
217 | let conversion = match self.format {
218 | shm::SubpixelOrder::NONE => None,
219 | shm::SubpixelOrder::RGB => Some(opencv::imgproc::COLOR_RGB2GRAY),
220 | shm::SubpixelOrder::BGR => Some(opencv::imgproc::COLOR_BGR2GRAY),
221 | shm::SubpixelOrder::BGRA => Some(opencv::imgproc::COLOR_BGRA2GRAY),
222 | shm::SubpixelOrder::RGBA => Some(opencv::imgproc::COLOR_RGBA2GRAY),
223 | _ => panic!("Unsupported pixel format: {:?}", self.format),
224 | };
225 | self.convert(conversion)
226 | }
227 |
228 | fn convert(self, conversion: Option) -> Result {
229 | if let Some(conversion) = conversion {
230 | let mut rgb_image = Mat::default();
231 | // You could do this in-place as well, though it's probably not worth it
232 | opencv::imgproc::cvt_color(&self.image, &mut rgb_image, conversion, 0)?;
233 | return Ok(rgb_image);
234 | }
235 | Ok(self.image)
236 | }
237 | }
238 |
239 | pub struct ImageStream<'mon> {
240 | monitor: &'mon Monitor<'mon>,
241 | last_read_index: u32,
242 | width: u32,
243 | height: u32,
244 | image_size: u32,
245 | format: shm::SubpixelOrder,
246 | image_buffer_count: u32,
247 | shared_images_offset: u64,
248 | }
249 |
250 | impl ImageStream<'_> {
251 | fn wait_for_image(&mut self) -> Result {
252 | loop {
253 | let state = self.monitor.read()?;
254 | let last_write_index = state.last_write_index as u32;
255 | if last_write_index != self.last_read_index
256 | && last_write_index != self.image_buffer_count
257 | {
258 | self.last_read_index = last_write_index;
259 | let image = self.read_image(last_write_index)?;
260 | return Ok(Image {
261 | image,
262 | format: self.format,
263 | });
264 | }
265 | std::thread::sleep(Duration::from_millis(5));
266 | }
267 | }
268 |
269 | fn read_image(&self, index: u32) -> Result {
270 | assert_eq!(self.width * self.height * 4, self.image_size);
271 | let mut mat = Mat::new_size_with_default(
272 | (self.width as i32, self.height as i32).into(),
273 | zm_format_to_cv_format(self.format),
274 | 0.into(),
275 | )?;
276 | self.read_image_into(index, &mut mat)?;
277 | Ok(mat)
278 | }
279 |
280 | fn read_image_into(&self, index: u32, mat: &mut Mat) -> Result<()> {
281 | assert_eq!(self.width * self.height, mat.total() as u32);
282 | assert_eq!(mat.typ(), zm_format_to_cv_format(self.format));
283 | self.monitor.check_file_stale()?;
284 | let mut slice = mat.data_bytes_mut()?;
285 | let image_offset = self.shared_images_offset as u64 + self.image_size as u64 * index as u64;
286 | self.monitor
287 | .shm
288 | .file
289 | .read_exact_at(&mut slice, image_offset)
290 | .with_context(|| "Failed to read image")?;
291 | Ok(())
292 | }
293 | }
294 |
295 | impl Iterator for ImageStream<'_> {
296 | type Item = Result;
297 |
298 | fn next(&mut self) -> Option {
299 | Some(self.wait_for_image())
300 | }
301 | }
302 |
303 | struct MonitorState {
304 | pub last_write_index: i32,
305 | pub state: shm::MonitorState,
306 | pub last_event_id: u64,
307 | pub format: shm::SubpixelOrder,
308 | pub imagesize: u32,
309 | }
310 |
311 | #[derive(Debug)]
312 | pub struct ZoneMinderConf {
313 | db_host: String,
314 | db_name: String,
315 | db_user: String,
316 | db_password: String,
317 | mmap_path: String,
318 | }
319 |
320 | impl ZoneMinderConf {
321 | fn parse_zm_conf(zm_conf_contents: &str) -> ZoneMinderConf {
322 | let keys: HashMap<&str, &str> = zm_conf_contents
323 | .lines()
324 | .map(|line| line.trim())
325 | .filter(|line| line.starts_with("ZM_"))
326 | .filter_map(|line| line.split_once('='))
327 | .collect();
328 |
329 | ZoneMinderConf {
330 | db_host: keys["ZM_DB_HOST"].to_string(),
331 | db_name: keys["ZM_DB_NAME"].to_string(),
332 | db_user: keys["ZM_DB_USER"].to_string(),
333 | db_password: keys["ZM_DB_PASS"].to_string(),
334 | mmap_path: keys["ZM_PATH_MAP"].to_string(),
335 | }
336 | }
337 |
338 | pub fn parse_default() -> Result {
339 | let zm_conf = "/etc/zm/zm.conf";
340 | let zm_conf_d = "/etc/zm/conf.d";
341 | let contents = fs::read_to_string(zm_conf).with_context(|| {
342 | format!("Failed to parse Zoneminder configuration file {}", zm_conf)
343 | })?;
344 | let contents = contents
345 | + "\n"
346 | + &fs::read_dir(zm_conf_d)
347 | .with_context(|| format!("Failed to read Zoneminder overrides from {}", zm_conf_d))?
348 | .filter_map(Result::ok)
349 | .map(|entry| fs::read_to_string(entry.path()))
350 | .filter_map(Result::ok)
351 | .fold(String::new(), |a, b| a + "\n" + &b); // O(n**2)
352 |
353 | Ok(Self::parse_zm_conf(&contents))
354 | }
355 | }
356 |
357 | #[cfg(test)]
358 | mod tests {
359 | use super::*;
360 |
361 | #[test]
362 | fn test_parse_zm_conf() {
363 | let conf = "# ZoneMinder database hostname or ip address and optionally port or unix socket
364 | # Acceptable formats include hostname[:port], ip_address[:port], or
365 | # localhost:/path/to/unix_socket
366 | ZM_DB_HOST=localhost
367 |
368 | # ZoneMinder database name
369 | ZM_DB_NAME=zm
370 |
371 | # ZoneMinder database user
372 | ZM_DB_USER=zmuser
373 |
374 | # ZoneMinder database password
375 | ZM_DB_PASS=zmpass
376 |
377 | ZM_PATH_MAP=/dev/shm
378 | ";
379 |
380 | let parsed = ZoneMinderConf::parse_zm_conf(conf);
381 | assert_eq!(parsed.db_host, "localhost");
382 | assert_eq!(parsed.db_name, "zm");
383 | assert_eq!(parsed.db_user, "zmuser");
384 | assert_eq!(parsed.db_password, "zmpass");
385 | assert_eq!(parsed.mmap_path, "/dev/shm");
386 | }
387 | }
388 |
--------------------------------------------------------------------------------
/src/zoneminder/db.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 | use std::path::PathBuf;
3 |
4 | use anyhow::{anyhow, Result};
5 | use mysql::params;
6 | use mysql::prelude::Queryable;
7 | use opencv::core::Rect;
8 |
9 | use crate::zoneminder::ZoneMinderConf;
10 |
11 | trait ZoneMinderDB {
12 | fn connect_db(&self) -> mysql::Result;
13 | }
14 |
15 | impl ZoneMinderDB for ZoneMinderConf {
16 | fn connect_db(&self) -> mysql::Result {
17 | let builder = mysql::OptsBuilder::new()
18 | .ip_or_hostname(Some(&self.db_host))
19 | .db_name(Some(&self.db_name))
20 | .user(Some(&self.db_user))
21 | .pass(Some(&self.db_password));
22 | mysql::Conn::new(builder)
23 | }
24 | }
25 |
26 | pub fn update_event_notes(zm_conf: &ZoneMinderConf, event_id: u64, notes: &str) -> Result<()> {
27 | let mut db = zm_conf.connect_db()?;
28 | Ok(db.exec_drop(
29 | "UPDATE Events SET Notes = :notes WHERE Id = :id",
30 | params! {
31 | "id" => event_id,
32 | "notes" => notes,
33 | },
34 | )?)
35 | }
36 |
37 | #[derive(Debug)]
38 | pub struct MonitorSettings {
39 | pub name: String,
40 | pub storage_id: u32,
41 | pub enabled: bool,
42 | pub width: u32,
43 | pub height: u32,
44 | pub colours: u32,
45 | pub image_buffer_count: u32,
46 | pub analysis_fps_limit: Option,
47 | }
48 |
49 | impl MonitorSettings {
50 | pub fn query(zm_conf: &ZoneMinderConf, monitor_id: u32) -> Result {
51 | let mut db = zm_conf.connect_db()?;
52 | Ok(db.exec_map("SELECT Name, StorageId, Enabled, Width, Height, Colours, ImageBufferCount, AnalysisFPSLimit FROM Monitors WHERE Id = :id",
53 | params! { "id" => monitor_id },
54 | |(name, storage_id, enabled, width, height, colours, image_buffer_count, analysis_fps_limit)| {
55 | MonitorSettings {
56 | name,
57 | storage_id,
58 | enabled,
59 | width,
60 | height,
61 | colours,
62 | image_buffer_count,
63 | analysis_fps_limit,
64 | }
65 | }
66 | )?.remove(0))
67 | }
68 | }
69 |
70 | #[derive(Debug)]
71 | pub struct Event {
72 | pub id: u64,
73 | pub monitor_id: u32,
74 | pub name: String,
75 | pub max_score: u32,
76 | pub avg_score: u32,
77 | pub total_score: u32,
78 | default_video: String,
79 | start_datetime: String, // local time, 2022-01-27 18:45:59
80 |
81 | storage: Storage,
82 | }
83 |
84 | impl Event {
85 | pub fn query(zm_conf: &ZoneMinderConf, event_id: u64) -> Result {
86 | let mut db = zm_conf.connect_db()?;
87 |
88 | let storage_id = db.exec_first(
89 | "SELECT StorageId FROM Events WHERE Id = :id",
90 | params! { "id" => event_id },
91 | )?;
92 | let storage = get_storage_by_id(&mut db, storage_id.unwrap())?;
93 |
94 | // the "date time" handling here is janky af but sufficient for what's needed (only used to derive the file name)
95 | Ok(db.exec_map("SELECT Name, MonitorId, MaxScore, AvgScore, TotScore, DefaultVideo, CAST(StartDateTime AS CHAR) FROM Events WHERE Id = :id",
96 | params! { "id" => event_id },
97 | |(name, monitor_id, max_score, avg_score, total_score, default_video, start_datetime)| {
98 | Event {
99 | id: event_id,
100 | name,
101 | monitor_id,
102 | max_score,
103 | avg_score,
104 | total_score,
105 | default_video,
106 | start_datetime,
107 | storage: storage.clone(),
108 | }
109 | }
110 | )?.remove(0))
111 | }
112 |
113 | pub fn video_path(&self) -> Result {
114 | if self.storage.storage_type != "local" {
115 | return Err(anyhow!(
116 | "Unsupported storage type {} for event {}",
117 | self.storage.storage_type,
118 | self.id
119 | ));
120 | }
121 |
122 | let event_path = match self.storage.scheme {
123 | StorageScheme::Deep => {
124 | let re = regex::Regex::new("[-: ]").unwrap();
125 | format!("{}/{}", re.replace_all(&self.start_datetime, "/"), self.id)
126 | }
127 | StorageScheme::Medium => format!(
128 | "{}/{}",
129 | self.start_datetime.split_once(" ").unwrap().0,
130 | self.id
131 | ),
132 | StorageScheme::Shallow => format!("{}", self.id),
133 | };
134 |
135 | let monitor_path = self.monitor_id.to_string();
136 |
137 | let path: PathBuf = [
138 | &self.storage.path,
139 | &monitor_path,
140 | &event_path,
141 | &self.default_video,
142 | ]
143 | .iter()
144 | .collect();
145 | Ok(path)
146 | }
147 | }
148 |
149 | #[derive(Debug, Copy, Clone)]
150 | enum StorageScheme {
151 | Deep,
152 | Medium,
153 | Shallow,
154 | }
155 |
156 | impl TryFrom<&str> for StorageScheme {
157 | type Error = anyhow::Error;
158 |
159 | fn try_from(input: &str) -> std::result::Result {
160 | Ok(match input {
161 | "Deep" => StorageScheme::Deep,
162 | "Medium" => StorageScheme::Medium,
163 | "Shallow" => StorageScheme::Shallow,
164 | _ => return Err(anyhow!("Invalid/unknown storage scheme {}", input)),
165 | })
166 | }
167 | }
168 |
169 | #[derive(Debug, Clone)]
170 | struct Storage {
171 | #[allow(dead_code)]
172 | id: u64,
173 | #[allow(dead_code)]
174 | name: String,
175 | path: String,
176 | storage_type: String,
177 | scheme: StorageScheme,
178 | }
179 |
180 | fn get_storage_by_id(db: &mut mysql::Conn, storage_id: u64) -> Result {
181 | //let mut db = zm_conf.connect_db()?;
182 | Ok(db
183 | .exec_map(
184 | "SELECT Name, Path, Type, Scheme FROM Storage WHERE Id = :id",
185 | params! { "id" => storage_id },
186 | |(name, path, storage_type, scheme)| -> Result {
187 | let scheme: String = scheme;
188 | Ok(Storage {
189 | id: storage_id,
190 | name,
191 | path,
192 | storage_type,
193 | scheme: StorageScheme::try_from(scheme.as_str())?,
194 | })
195 | },
196 | )?
197 | .remove(0)?)
198 | }
199 |
200 | pub type ZoneShape = Vec<(i32, i32)>;
201 |
202 | pub trait Bounding {
203 | fn bounding_box(&self) -> Rect;
204 | }
205 |
206 | impl Bounding for ZoneShape {
207 | fn bounding_box(&self) -> Rect {
208 | let min_x = self.iter().map(|xy| xy.0).min().unwrap();
209 | let min_y = self.iter().map(|xy| xy.1).min().unwrap();
210 | let max_x = self.iter().map(|xy| xy.0).max().unwrap();
211 | let max_y = self.iter().map(|xy| xy.1).max().unwrap();
212 |
213 | let width = max_x - min_x;
214 | let height = max_y - min_y;
215 | Rect {
216 | x: min_x,
217 | y: min_y,
218 | width,
219 | height,
220 | }
221 | }
222 | }
223 |
224 | #[derive(Debug)]
225 | pub struct ZoneConfig {
226 | pub size: Option,
227 | pub threshold: Option,
228 | pub shape: ZoneShape,
229 | pub trigger: Option,
230 | pub fps: Option,
231 | pub min_area: Option,
232 | }
233 |
234 | impl ZoneConfig {
235 | pub fn get_zone_config(zm_conf: &ZoneMinderConf, monitor_id: u32) -> Result {
236 | let mut db = zm_conf.connect_db()?;
237 | let dbzone = db.exec_first(
238 | "SELECT Name, Type, Coords FROM Zones WHERE MonitorId = :id AND Name LIKE \"aidect%\"",
239 | params! { "id" => monitor_id },
240 | )?;
241 | let dbzone: mysql::Row =
242 | dbzone.ok_or(anyhow!("No aidect zone found for monitor {}", monitor_id))?;
243 |
244 | Ok(ZoneConfig::parse(
245 | &dbzone.get::("Name").unwrap(),
246 | &dbzone.get::("Coords").unwrap(),
247 | ))
248 | }
249 |
250 | fn parse(name: &str, coords: &str) -> ZoneConfig {
251 | ZoneConfig {
252 | shape: Self::parse_zone_coords(coords),
253 | ..Self::parse_zone_name(name)
254 | }
255 | }
256 |
257 | fn parse_zone_name(zone_name: &str) -> ZoneConfig {
258 | let keys: HashMap<&str, &str> = zone_name
259 | .split_ascii_whitespace()
260 | .skip(1)
261 | .map(|item| item.split_once('='))
262 | .filter_map(|x| x)
263 | .collect();
264 |
265 | let get_int = |key| keys.get(key).and_then(|v| v.trim().parse::().ok());
266 | let get_f32 = |key| keys.get(key).and_then(|v| v.trim().parse::().ok());
267 |
268 | ZoneConfig {
269 | shape: Vec::new(),
270 | threshold: keys
271 | .get("Threshold")
272 | .and_then(|v| v.trim().parse::().ok())
273 | .map(|v| v / 100.0),
274 | size: get_int("Size"),
275 | trigger: get_int("Trigger"),
276 | fps: get_f32("FPS"),
277 | min_area: get_int("MinArea"),
278 | }
279 | }
280 |
281 | fn parse_zone_coords(coords: &str) -> ZoneShape {
282 | let parse = |v: &str| v.trim().parse::().unwrap();
283 | coords
284 | .split_ascii_whitespace()
285 | .map(|point| point.split_once(','))
286 | .filter_map(|v| v)
287 | .map(|(x, y)| (parse(x), parse(y)))
288 | .collect()
289 | }
290 | }
291 |
292 | #[cfg(test)]
293 | mod tests {
294 | use super::*;
295 |
296 | #[test]
297 | fn test_parse_zone_name_basic() {
298 | let zone_name = "aidect";
299 | let parsed = ZoneConfig::parse_zone_name(zone_name);
300 | assert_eq!(parsed.shape.len(), 0);
301 | assert_eq!(parsed.threshold, None);
302 | assert_eq!(parsed.size, None);
303 | }
304 |
305 | #[test]
306 | fn test_parse_zone_name() {
307 | let zone_name = "aidect Size=128 Threshold=50";
308 | let parsed = ZoneConfig::parse_zone_name(zone_name);
309 | assert_eq!(parsed.shape.len(), 0);
310 | assert_eq!(parsed.threshold, Some(0.5));
311 | assert_eq!(parsed.size, Some(128));
312 | }
313 |
314 | #[test]
315 | fn test_parse_zone_coords() {
316 | let coords = "123,56 899,41 687,425";
317 | let parsed = ZoneConfig::parse_zone_coords(coords);
318 | assert_eq!(parsed, vec![(123, 56), (899, 41), (687, 425)]);
319 | }
320 | }
321 |
--------------------------------------------------------------------------------
/src/zoneminder/shm.rs:
--------------------------------------------------------------------------------
1 | use std::io::Read;
2 | use std::mem::{align_of, size_of};
3 | use std::os::unix::fs::FileExt;
4 | use std::slice;
5 |
6 | use anyhow::{anyhow, Context, Result};
7 | use lazy_static::lazy_static;
8 | use libc::time_t;
9 | use regex::Regex;
10 |
11 | // TODO: panic! wrapper which adds a bit that this requires maintainer attention
12 |
13 | #[derive(Debug, Eq, PartialEq)]
14 | struct Type {
15 | size: usize,
16 | alignment: usize,
17 | }
18 |
19 | impl Type {
20 | fn new() -> Type {
21 | Type {
22 | size: size_of::(),
23 | alignment: align_of::(),
24 | }
25 | }
26 |
27 | fn array_of(&self, num_elements: usize) -> Type {
28 | Type {
29 | size: self.size * num_elements,
30 | alignment: self.alignment,
31 | }
32 | }
33 | }
34 |
35 | fn parse_basic_typename(typename: &str) -> Type {
36 | match typename {
37 | "uint8" => Type::new::(),
38 | "int8" => Type::new::(),
39 | "uint32" => Type::new::(),
40 | "int32" => Type::new::(),
41 | "uint64" => Type::new::(),
42 | "int64" => Type::new::(),
43 | "float" => Type::new::(),
44 | "double" => Type::new::(),
45 | "time_t64" => Type::new::(),
46 | _ => panic!(
47 | "Unhandled ABI type in Memory.pm shm definition: {}",
48 | typename
49 | ),
50 | }
51 | }
52 |
53 | fn parse_typename(typename: &str) -> Type {
54 | match typename.split_once('[') {
55 | None => parse_basic_typename(typename),
56 | Some((basic_typename, array_size)) => {
57 | let t = parse_basic_typename(basic_typename);
58 | assert!(array_size.ends_with(']'));
59 | let array_size = &array_size[0..array_size.len() - 1];
60 | let elements = array_size
61 | .parse::()
62 | .with_context(|| {
63 | format!(
64 | "Could not parse array size in Memory.pm shm definition: {}",
65 | typename
66 | )
67 | })
68 | .unwrap();
69 | t.array_of(elements)
70 | }
71 | }
72 | }
73 |
74 | #[derive(Debug, Eq, PartialEq)]
75 | struct ParsedField {
76 | name: String,
77 | typ: Type,
78 | }
79 |
80 | fn parse_field_definition(line: &str) -> ParsedField {
81 | lazy_static! {
82 | static ref RE: Regex = Regex::new(r"(\w+)\s+=> \{ type=>'([a-z0-9_\[\]]+)'").unwrap();
83 | }
84 | let m = RE
85 | .captures(line)
86 | .ok_or(anyhow!(
87 | "Could not parse field definition in Memory.pm shm definition: {:?}",
88 | line
89 | ))
90 | .unwrap();
91 | ParsedField {
92 | name: m[1].to_string(),
93 | typ: parse_typename(&m[2]),
94 | }
95 | }
96 |
97 | #[derive(Debug, Eq, PartialEq)]
98 | struct ParsedStruct {
99 | name: String,
100 | fields: Vec,
101 | }
102 |
103 | #[derive(Debug, Eq, PartialEq)]
104 | struct Field {
105 | name: String,
106 | offset: usize,
107 | typ: Type,
108 | }
109 |
110 | #[derive(Debug, Eq, PartialEq)]
111 | struct Struct {
112 | name: String,
113 | size: usize,
114 | fields: Vec,
115 | }
116 |
117 | impl ParsedStruct {
118 | fn calculate_offsets(self) -> Struct {
119 | let mut offset = 0;
120 | let mut fields = vec![];
121 |
122 | for ParsedField { name, typ } in self.fields {
123 | let field = Field {
124 | offset: align_to(offset, typ.alignment),
125 | name,
126 | typ,
127 | };
128 | offset = field.offset + field.typ.size;
129 | fields.push(field);
130 | }
131 |
132 | Struct {
133 | name: self.name,
134 | size: offset,
135 | fields,
136 | }
137 | }
138 | }
139 |
140 | fn align_to(offset: usize, alignment: usize) -> usize {
141 | if offset % alignment == 0 {
142 | offset
143 | } else {
144 | offset + alignment - (offset % alignment)
145 | }
146 | }
147 |
148 | fn parse_struct_definition(input: &mut std::str::Lines) -> Option {
149 | lazy_static! {
150 | static ref RE: Regex =
151 | Regex::new(r"\w+\s+=> \{ type=>'(\w+)', seq=>\$mem_seq\+\+, '?contents'?").unwrap();
152 | }
153 | let struct_def = input.next().expect("Empty struct definition in Memory.pm");
154 | if struct_def.trim_start().starts_with("end =>") {
155 | return None;
156 | }
157 |
158 | let m = RE
159 | .captures(struct_def)
160 | .ok_or(anyhow!(
161 | "Could not parse struct definition in Memory.pm shm definition: {:?}",
162 | struct_def
163 | ))
164 | .unwrap();
165 |
166 | let mut fields = vec![];
167 | loop {
168 | let line = input.next().expect("Unexpected EOR in Memory.pm");
169 | let line = line.trim_start();
170 | if line == "}" {
171 | continue;
172 | }
173 | if line == "}," {
174 | break;
175 | }
176 | fields.push(parse_field_definition(line));
177 | }
178 |
179 | Some(ParsedStruct {
180 | name: m[1].to_string(),
181 | fields,
182 | })
183 | }
184 |
185 | fn parse_memory_pm(input: &str) -> ParsedStruct {
186 | let re = Regex::new(r"(?ms)our \$mem_data = \{\n(.*?)};").unwrap();
187 | let m = re
188 | .captures(input)
189 | .expect("No shm definitions found in Memory.pm");
190 |
191 | let mut lines = m[1].lines();
192 | let mut fields = vec![];
193 | loop {
194 | if let Some(s) = parse_struct_definition(&mut lines) {
195 | fields.extend(s.fields.into_iter().map(|f| ParsedField {
196 | name: format!("{}::{}", s.name, f.name),
197 | ..f
198 | }));
199 | } else {
200 | break;
201 | }
202 | }
203 |
204 | // Memory.pm does not define this struct, but we need to read this field to calculate
205 | // the offset of the timestamps and the shared image buffer.
206 | fields.push(ParsedField {
207 | name: "VideoStoreData::size".into(),
208 | typ: Type::new::(),
209 | });
210 |
211 | ParsedStruct {
212 | name: "memory".into(),
213 | fields,
214 | }
215 | }
216 |
217 | fn read_memory_pm(mut input: T) -> Result {
218 | let input = {
219 | let mut contents = String::new();
220 | input.read_to_string(&mut contents)?;
221 | contents
222 | };
223 | Ok(parse_memory_pm(&input).calculate_offsets())
224 | }
225 |
226 | lazy_static! {
227 | static ref LAYOUT: Struct = {
228 | let file = std::fs::File::open("/usr/share/perl5/ZoneMinder/Memory.pm").expect("Failed to open ZoneMinder Memory.pm - ZM not installed or installed in unknown location.");
229 | read_memory_pm(file).unwrap()
230 | };
231 | }
232 |
233 | #[non_exhaustive]
234 | pub struct MonitorShm {
235 | pub file: T,
236 | pub videostore_size: u32,
237 | }
238 |
239 | impl MonitorShm {
240 | pub fn new(file: File) -> Result> {
241 | let mut mshm = MonitorShm {
242 | file,
243 | videostore_size: 0,
244 | };
245 | mshm.videostore_size = mshm.read_field("VideoStoreData::size")?;
246 | Ok(mshm)
247 | }
248 |
249 | fn lookup_field(&self, name: &str) -> &Field {
250 | for field in LAYOUT.fields.iter() {
251 | if field.name == name {
252 | return field;
253 | }
254 | }
255 | panic!("Field not found in Memory.pm: {name}");
256 | }
257 |
258 | fn typecheck(&self, field: &Field) {
259 | let typ = Type::new::();
260 | if field.typ != typ {
261 | panic!(
262 | "Mismatched field type for {} (wanted: {typ:?}, got: {:?}",
263 | field.name, field.typ
264 | );
265 | }
266 | }
267 |
268 | pub fn read_field(&self, name: &str) -> Result {
269 | let field = self.lookup_field(name);
270 | self.typecheck::(field);
271 | self.pread(field.offset)
272 | }
273 |
274 | pub fn write_field(&self, name: &str, value: &T) -> Result<()> {
275 | let field = self.lookup_field(name);
276 | self.typecheck::(field);
277 | self.pwrite(field.offset, value)
278 | }
279 |
280 | pub fn write_string(&self, name: &str, value: &str) -> Result<()> {
281 | let field = self.lookup_field(name);
282 | let terminated_len = value.len() + 1;
283 | assert!(field.typ.size >= terminated_len);
284 | let mut s = String::with_capacity(terminated_len);
285 | s.push_str(value);
286 | s.push('\0');
287 | self.file.write_all_at(s.as_bytes(), field.offset as u64)?;
288 | Ok(())
289 | }
290 |
291 | fn pread(&self, offset: usize) -> Result {
292 | let mut buf = Vec::new();
293 | buf.resize(size_of::(), 0);
294 | self.file.read_exact_at(&mut buf, offset as u64)?;
295 | unsafe { Ok(std::ptr::read(buf.as_ptr() as *const _)) }
296 | }
297 |
298 | fn pwrite(&self, offset: usize, data: &T) -> Result<()> {
299 | let data = unsafe { slice::from_raw_parts(data as *const T as *const u8, size_of::()) };
300 | self.file.write_all_at(data, offset as u64)?;
301 | Ok(())
302 | }
303 | }
304 |
305 | #[non_exhaustive]
306 | pub(super) struct ShmField;
307 |
308 | // TODO: This should be an enum and we should associate the name and expected type internally,
309 | // TODO: so that all fields we may can be validated after parsing Memory.pm
310 | impl ShmField {
311 | pub const LAST_WRITE_INDEX: &'static str = "SharedData::last_write_index";
312 | pub const STATE: &'static str = "SharedData::state";
313 | pub const LAST_EVENT_ID: &'static str = "SharedData::last_event";
314 | pub const VALID: &'static str = "SharedData::valid";
315 | pub const FORMAT: &'static str = "SharedData::format";
316 | pub const IMAGESIZE: &'static str = "SharedData::imagesize";
317 |
318 | pub const TRIGGER_STATE: &'static str = "TriggerData::trigger_state";
319 | pub const TRIGGER_SCORE: &'static str = "TriggerData::trigger_score";
320 | pub const TRIGGER_CAUSE: &'static str = "TriggerData::trigger_cause";
321 | pub const TRIGGER_TEXT: &'static str = "TriggerData::trigger_text";
322 | pub const TRIGGER_SHOWTEXT: &'static str = "TriggerData::trigger_showtext";
323 |
324 | pub const SHARED_SIZE: &'static str = "SharedData::size";
325 | pub const TRIGGER_SIZE: &'static str = "TriggerData::size";
326 | pub const VIDEOSTORE_SIZE: &'static str = "VideoStoreData::size";
327 | }
328 |
329 | #[cfg(test)]
330 | mod tests {
331 | use super::*;
332 |
333 | #[test]
334 | fn test_parse_typename() {
335 | assert_eq!(parse_typename("int32"), Type::new::());
336 | assert_eq!(parse_typename("int32[44]"), Type::new::().array_of(44));
337 | }
338 |
339 | #[test]
340 | #[should_panic]
341 | fn test_parse_typename_panic() {
342 | parse_typename("int32[44x]");
343 | }
344 |
345 | #[test]
346 | #[should_panic]
347 | fn test_parse_typename_panic2() {
348 | parse_typename("int32[44");
349 | }
350 |
351 | #[test]
352 | fn test_parse_field_definition() {
353 | assert_eq!(
354 | parse_field_definition(" size => { type=>'uint32', seq=>$mem_seq++ },"),
355 | ParsedField {
356 | name: "size".into(),
357 | typ: Type::new::(),
358 | }
359 | );
360 | assert_eq!(
361 | parse_field_definition(" size => { type=>'uint32[5]', seq=>$mem_seq++ },"),
362 | ParsedField {
363 | name: "size".into(),
364 | typ: Type::new::().array_of(5),
365 | }
366 | );
367 | }
368 |
369 | #[test]
370 | fn test_parse_struct_definition() {
371 | assert_eq!(
372 | parse_struct_definition(
373 | &mut r#"trigger_data => { type=>'TriggerData', seq=>$mem_seq++, 'contents'=> {
374 | size => { type=>'uint32', seq=>$mem_seq++ },
375 | trigger_cause => { type=>'int8[32]', seq=>$mem_seq++ },
376 | }
377 | },"#
378 | .lines()
379 | )
380 | .unwrap(),
381 | ParsedStruct {
382 | name: "TriggerData".into(),
383 | fields: vec![
384 | ParsedField {
385 | name: "size".into(),
386 | typ: Type::new::()
387 | },
388 | ParsedField {
389 | name: "trigger_cause".into(),
390 | typ: Type::new::().array_of(32)
391 | },
392 | ],
393 | }
394 | );
395 | }
396 |
397 | const INPUT: &str = "our $mem_seq = 0;
398 |
399 | our $mem_data = {
400 | shared_data => { type=>'SharedData', seq=>$mem_seq++, contents=> {
401 | size => { type=>'uint32', seq=>$mem_seq++ },
402 | startup_time => { type=>'time_t64', seq=>$mem_seq++ },
403 | audio_fifo => { type=>'int8[64]', seq=>$mem_seq++ },
404 | }
405 | },
406 | trigger_data => { type=>'TriggerData', seq=>$mem_seq++, 'contents'=> {
407 | size => { type=>'uint32', seq=>$mem_seq++ },
408 | trigger_cause => { type=>'int8[32]', seq=>$mem_seq++ },
409 | }
410 | },
411 | end => { seq=>$mem_seq++, size=>0 }
412 | };
413 |
414 | our $mem_size = 0;
415 |
416 | sub zmMemInit {
417 | ";
418 |
419 | #[test]
420 | fn test_parse_memory_pm() {
421 | assert_eq!(
422 | parse_memory_pm(INPUT),
423 | ParsedStruct {
424 | name: "memory".into(),
425 | fields: vec![
426 | ParsedField {
427 | name: "SharedData::size".into(),
428 | typ: Type::new::()
429 | },
430 | ParsedField {
431 | name: "SharedData::startup_time".into(),
432 | typ: Type::new::()
433 | },
434 | ParsedField {
435 | name: "SharedData::audio_fifo".into(),
436 | typ: Type::new::().array_of(64)
437 | },
438 | ParsedField {
439 | name: "TriggerData::size".into(),
440 | typ: Type::new::()
441 | },
442 | ParsedField {
443 | name: "TriggerData::trigger_cause".into(),
444 | typ: Type::new::().array_of(32)
445 | },
446 | ParsedField {
447 | name: "VideoStoreData::size".into(),
448 | typ: Type::new::()
449 | },
450 | ],
451 | },
452 | );
453 | }
454 |
455 | #[test]
456 | fn test_align_to() {
457 | assert_eq!(align_to(6, 8), 8);
458 | assert_eq!(align_to(8, 8), 8);
459 | assert_eq!(align_to(0, 8), 0);
460 | assert_eq!(align_to(7, 1), 7);
461 | }
462 |
463 | #[test]
464 | fn test_read_memory_pm() {
465 | assert_eq!(
466 | read_memory_pm(INPUT.as_bytes()).unwrap(),
467 | Struct {
468 | name: "memory".into(),
469 | size: 4 + 4 + 8 + 64 + 4 + 32 + 4,
470 | fields: vec![
471 | Field {
472 | name: "SharedData::size".into(),
473 | typ: Type::new::(),
474 | offset: 0,
475 | },
476 | Field {
477 | name: "SharedData::startup_time".into(),
478 | typ: Type::new::(),
479 | offset: align_of::(),
480 | },
481 | Field {
482 | name: "SharedData::audio_fifo".into(),
483 | typ: Type::new::().array_of(64),
484 | offset: align_of::() + 8,
485 | },
486 | Field {
487 | name: "TriggerData::size".into(),
488 | typ: Type::new::(),
489 | offset: align_of::() + 8 + 64,
490 | },
491 | Field {
492 | name: "TriggerData::trigger_cause".into(),
493 | typ: Type::new::().array_of(32),
494 | offset: align_of::() + 8 + 64 + 4,
495 | },
496 | Field {
497 | name: "VideoStoreData::size".into(),
498 | typ: Type::new::(),
499 | offset: align_of::() + 8 + 64 + 4 + 32,
500 | },
501 | ],
502 | },
503 | );
504 | }
505 | }
506 |
507 | #[derive(Copy, Clone, Debug, PartialEq, Eq)]
508 | #[repr(u32)]
509 | #[allow(dead_code)]
510 | pub(super) enum MonitorState {
511 | Unknown = 0,
512 | Idle,
513 | Prealarm, // Likely when there are alarm frames but not enough to trigger an event
514 | Alarm, // I believe "current" frame is an alarm frame
515 | Alert, // Current frame is not an alarm frame, but we're still in an alarmed state
516 | Tape, // I think this is the idle state of Mocord and Record
517 | }
518 |
519 | // zm_rgb.h
520 |
521 | #[derive(Copy, Clone, Debug)]
522 | #[repr(u32)]
523 | #[allow(dead_code)]
524 | pub(super) enum ColourType {
525 | GRAY8 = 1,
526 | RGB24 = 3,
527 | RGB32 = 4,
528 | }
529 |
530 | #[derive(Copy, Clone, Debug)]
531 | #[repr(u8)]
532 | #[allow(dead_code)]
533 | pub(super) enum SubpixelOrder {
534 | NONE = 2, // grayscale
535 | RGB = 6,
536 | BGR = 5,
537 | BGRA = 7,
538 | RGBA = 8,
539 | ABGR = 9,
540 | ARGB = 10,
541 | }
542 |
543 | #[derive(Copy, Clone, Debug)]
544 | #[repr(u32)]
545 | #[allow(dead_code)]
546 | pub(super) enum TriggerState {
547 | TriggerCancel,
548 | TriggerOn,
549 | TriggerOff,
550 | }
551 |
--------------------------------------------------------------------------------
/yolov4-tiny.cfg:
--------------------------------------------------------------------------------
1 | [net]
2 | # Testing
3 | #batch=1
4 | #subdivisions=1
5 | # Training
6 | batch=64
7 | subdivisions=1
8 | width=416
9 | height=416
10 | channels=3
11 | momentum=0.9
12 | decay=0.0005
13 | angle=0
14 | saturation = 1.5
15 | exposure = 1.5
16 | hue=.1
17 |
18 | learning_rate=0.00261
19 | burn_in=1000
20 |
21 | max_batches = 2000200
22 | policy=steps
23 | steps=1600000,1800000
24 | scales=.1,.1
25 |
26 |
27 | #weights_reject_freq=1001
28 | #ema_alpha=0.9998
29 | #equidistant_point=1000
30 | #num_sigmas_reject_badlabels=3
31 | #badlabels_rejection_percentage=0.2
32 |
33 |
34 | [convolutional]
35 | batch_normalize=1
36 | filters=32
37 | size=3
38 | stride=2
39 | pad=1
40 | activation=leaky
41 |
42 | [convolutional]
43 | batch_normalize=1
44 | filters=64
45 | size=3
46 | stride=2
47 | pad=1
48 | activation=leaky
49 |
50 | [convolutional]
51 | batch_normalize=1
52 | filters=64
53 | size=3
54 | stride=1
55 | pad=1
56 | activation=leaky
57 |
58 | [route]
59 | layers=-1
60 | groups=2
61 | group_id=1
62 |
63 | [convolutional]
64 | batch_normalize=1
65 | filters=32
66 | size=3
67 | stride=1
68 | pad=1
69 | activation=leaky
70 |
71 | [convolutional]
72 | batch_normalize=1
73 | filters=32
74 | size=3
75 | stride=1
76 | pad=1
77 | activation=leaky
78 |
79 | [route]
80 | layers = -1,-2
81 |
82 | [convolutional]
83 | batch_normalize=1
84 | filters=64
85 | size=1
86 | stride=1
87 | pad=1
88 | activation=leaky
89 |
90 | [route]
91 | layers = -6,-1
92 |
93 | [maxpool]
94 | size=2
95 | stride=2
96 |
97 | [convolutional]
98 | batch_normalize=1
99 | filters=128
100 | size=3
101 | stride=1
102 | pad=1
103 | activation=leaky
104 |
105 | [route]
106 | layers=-1
107 | groups=2
108 | group_id=1
109 |
110 | [convolutional]
111 | batch_normalize=1
112 | filters=64
113 | size=3
114 | stride=1
115 | pad=1
116 | activation=leaky
117 |
118 | [convolutional]
119 | batch_normalize=1
120 | filters=64
121 | size=3
122 | stride=1
123 | pad=1
124 | activation=leaky
125 |
126 | [route]
127 | layers = -1,-2
128 |
129 | [convolutional]
130 | batch_normalize=1
131 | filters=128
132 | size=1
133 | stride=1
134 | pad=1
135 | activation=leaky
136 |
137 | [route]
138 | layers = -6,-1
139 |
140 | [maxpool]
141 | size=2
142 | stride=2
143 |
144 | [convolutional]
145 | batch_normalize=1
146 | filters=256
147 | size=3
148 | stride=1
149 | pad=1
150 | activation=leaky
151 |
152 | [route]
153 | layers=-1
154 | groups=2
155 | group_id=1
156 |
157 | [convolutional]
158 | batch_normalize=1
159 | filters=128
160 | size=3
161 | stride=1
162 | pad=1
163 | activation=leaky
164 |
165 | [convolutional]
166 | batch_normalize=1
167 | filters=128
168 | size=3
169 | stride=1
170 | pad=1
171 | activation=leaky
172 |
173 | [route]
174 | layers = -1,-2
175 |
176 | [convolutional]
177 | batch_normalize=1
178 | filters=256
179 | size=1
180 | stride=1
181 | pad=1
182 | activation=leaky
183 |
184 | [route]
185 | layers = -6,-1
186 |
187 | [maxpool]
188 | size=2
189 | stride=2
190 |
191 | [convolutional]
192 | batch_normalize=1
193 | filters=512
194 | size=3
195 | stride=1
196 | pad=1
197 | activation=leaky
198 |
199 | ##################################
200 |
201 | [convolutional]
202 | batch_normalize=1
203 | filters=256
204 | size=1
205 | stride=1
206 | pad=1
207 | activation=leaky
208 |
209 | [convolutional]
210 | batch_normalize=1
211 | filters=512
212 | size=3
213 | stride=1
214 | pad=1
215 | activation=leaky
216 |
217 | [convolutional]
218 | size=1
219 | stride=1
220 | pad=1
221 | filters=255
222 | activation=linear
223 |
224 |
225 |
226 | [yolo]
227 | mask = 3,4,5
228 | anchors = 10,14, 23,27, 37,58, 81,82, 135,169, 344,319
229 | classes=80
230 | num=6
231 | jitter=.3
232 | scale_x_y = 1.05
233 | cls_normalizer=1.0
234 | iou_normalizer=0.07
235 | iou_loss=ciou
236 | ignore_thresh = .7
237 | truth_thresh = 1
238 | random=0
239 | resize=1.5
240 | nms_kind=greedynms
241 | beta_nms=0.6
242 | #new_coords=1
243 | #scale_x_y = 2.0
244 |
245 | [route]
246 | layers = -4
247 |
248 | [convolutional]
249 | batch_normalize=1
250 | filters=128
251 | size=1
252 | stride=1
253 | pad=1
254 | activation=leaky
255 |
256 | [upsample]
257 | stride=2
258 |
259 | [route]
260 | layers = -1, 23
261 |
262 | [convolutional]
263 | batch_normalize=1
264 | filters=256
265 | size=3
266 | stride=1
267 | pad=1
268 | activation=leaky
269 |
270 | [convolutional]
271 | size=1
272 | stride=1
273 | pad=1
274 | filters=255
275 | activation=linear
276 |
277 | [yolo]
278 | mask = 1,2,3
279 | anchors = 10,14, 23,27, 37,58, 81,82, 135,169, 344,319
280 | classes=80
281 | num=6
282 | jitter=.3
283 | scale_x_y = 1.05
284 | cls_normalizer=1.0
285 | iou_normalizer=0.07
286 | iou_loss=ciou
287 | ignore_thresh = .7
288 | truth_thresh = 1
289 | random=0
290 | resize=1.5
291 | nms_kind=greedynms
292 | beta_nms=0.6
293 | #new_coords=1
294 | #scale_x_y = 2.0
295 |
--------------------------------------------------------------------------------
/yolov4-tiny.weights:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/enkore/zm-aidect/e26dd5207e5730de8159afffa9b7429410ca4029/yolov4-tiny.weights
--------------------------------------------------------------------------------
/zm-aidect@.service:
--------------------------------------------------------------------------------
1 | [Service]
2 | Type=simple
3 | ExecStart=/zm-aidect/zm-aidect run %i
4 | Restart=always
5 | RestartSec=1
6 | User=www-data
7 | Group=www-data
8 |
9 | [Unit]
10 | StartLimitIntervalSec=0
11 |
12 | [Install]
13 | WantedBy=multi-user.target
14 |
--------------------------------------------------------------------------------