├── .gitignore ├── Dockerfile ├── README.md ├── icon.png └── rootfs ├── etc └── services.d │ └── deepguard │ └── run └── xshok-deepguard.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .xs_password 2 | .env 3 | .git/* 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM extremeshok/baseimage-ubuntu:latest AS BUILD 2 | 3 | LABEL mantainer="Adrian Kriel " vendor="eXtremeSHOK.com" 4 | 5 | RUN echo "**** install bash runtime packages ****" \ 6 | && apt-install \ 7 | bash \ 8 | ca-certificates \ 9 | curl \ 10 | file \ 11 | graphicsmagick \ 12 | gsfonts \ 13 | inotify-tools \ 14 | jq \ 15 | netcat \ 16 | mosquitto-clients 17 | 18 | # add local files 19 | COPY rootfs/ / 20 | 21 | RUN echo "**** create dirs ****" \ 22 | && mkdir -p /data/input \ 23 | && mkdir -p /data/output \ 24 | && mkdir -p /data/backup 25 | 26 | RUN echo "**** configure ****" \ 27 | && chmod 777 /xshok-deepguard.sh 28 | 29 | WORKDIR /data 30 | 31 | ENTRYPOINT ["/init"] 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-deepguard 2 | camera monitoring and alerts using deepstack 3 | 4 | https://hub.docker.com/r/extremeshok/deepguard 5 | 6 | ![alt text](https://raw.githubusercontent.com/extremeshok/docker-deepguard/master/icon.png "Logo") 7 | 8 | 9 | # Features 10 | * Latest ubuntu with S6 11 | * The power of Deepstack (machine learning / AI) to Guard your property 12 | * Notify (Notifications/triggers) to: url (blueiris, etc), zoneminder, mqtt (mosquitto) 13 | * Alerts to: pushover, telegram, whatsmate(whatsapp), nexmo(sms), twilio(sms) 14 | * Limit alerts to prevent flooding, ie ALERT_MAX_ALERTS=2 in ALERT_PERIOD_SECONDS=120 (eg send a max of 2 alerts in 120seconds) 15 | 16 | # Usage 17 | * View **docker-compose-sample.yml** in the source repository for usage 18 | 19 | # Note: 20 | notifications are always sent, even when alerts are disabled, ie max alerts have been reached 21 | notify is done before the alerts are sent 22 | when debug mode is disabled, alerts and notifications are done in parallel (separate threads) 23 | 24 | # ENVIRONMENT VARIABLES 25 | ### DEFAULTS 26 | * IGNORE_LIST="person, bicycle, car, motorcycle, airplane, bus, train, truck, boat, trafficlight, firehydrant, stop_sign, parkingmeter, bench, bird, cat, dog, horse, sheep, cow, elephant, bear, zebra, giraffe, backpack, umbrella, handbag, tie, suitcase, frisbee, skis, snowboard, sportsball, kite, baseballbat, baseballglove, skateboard, surfboard, tennisracket, bottle, wineglass, cup, fork, knife, spoon, bowl, banana, apple, sandwich, orange, broccoli, carrot, hotdog, pizza, donot, cake, chair, couch, pottedplant, bed, diningtable, toilet, tv, laptop, mouse, remote, keyboard, cellphone, microwave, oven, toaster, sink, refrigerator, book, clock, vase, scissors, teddybear, hairdryer, toothbrush" 27 | * NOTIFY_LIST="person, bear, cat, dog" 28 | * VALID_IMAGE_EXTENSION_LIST="png, jpg, jpeg, gif, bmp" 29 | * CAMERA_NAME_DELIMINATOR="." 30 | * DIR_INPUT="/data/input" 31 | * DIR_OUTPUT="/data/output" 32 | * DIR_BACKUP="/data/backup" 33 | * BACKUP_ORIGINAL="no" 34 | * SAVE_OUTPUT="yes" 35 | * EMPTY_INPUT_DIR_ON_START="yes" 36 | * PROCESS_BACKLOG="no" 37 | * DRAW_RESULTS="yes" 38 | * DEBUG="no" 39 | * IGNORE_NONE="no" 40 | * BE_VERBOSE="yes" 41 | * ALERT_ALL_MAX_ALERTS=4 42 | * ALERT_ALL_PERIOD_SECONDS=60 43 | * ALERT_CAMERA_MAX_ALERTS=2 44 | * ALERT_CAMERA_PERIOD_SECONDS=180 45 | 46 | ## OPTIONS 47 | * DEEPSTACK_URL="http://deepstack:5000" 48 | * DEEPSTACK_BACKUP_URL="http://deepstackbackup:5000" 49 | * DEEPSTACK_CONFIDENCE_LIMIT="65" 50 | 51 | ### NOTIFY : zoneminder 52 | * NOTIFY_ZONEMINDER="no" 53 | * ZONEMINDER_NOFITY_HOST="zoneminder" 54 | * ZONEMINDER_NOFITY_PORT="6802" 55 | 56 | ### NOTIFY : url 57 | * NOTIFY_URL="no" 58 | * URL_NOTIFY="http://blueiris/admin?trigger&camera=hd%%CAMERA%%&user=ai&pw=ai" 59 | 60 | ### NOTIFY : mqtt 61 | * NOTIFY_MQTT="no" 62 | * MQTT_NOTIFY_URL="mqtt://username:password@mqtthost:port/camera/%%CAMERA%%" 63 | * MQTT_NOTIFY_MESSAGE="alert" 64 | 65 | ### ALERT : pushover 66 | * ALERT_PUSHOVER="no" 67 | * PUSHOVER_TOKEN="" 68 | * PUSHOVER_KEY="" 69 | * PUSHOVER_PRIORITY="2" #2=emergency 70 | * PUSHOVER_EXPIRE="600" 71 | * PUSHOVER_RETRY="30" 72 | * PUSHOVER_DEVICE="" 73 | * PUSHOVER_SOUND="siren" 74 | 75 | ### ALERT : telegram 76 | * ALERT_TELEGRAM="no" 77 | * TELEGRAM_TOKEN="" 78 | * TELEGRAM_CHAT_ID="" 79 | 80 | ### ALERT : whatsmate (whatsapp) 81 | * ALERT_WHATSMATE="no" 82 | * WHATSMATE_CLIENT_ID="" 83 | * WHATSMATE_CLIENT_SECRET="" 84 | * WHATSMATE_WHATSAPP_NUMBER="" 85 | 86 | ### ALERT : nexmo (sms/text) 87 | * ALERT_NEXMO="no" 88 | * NEXMO_API_KEY="" 89 | * NEXMO_API_SECRET="" 90 | * NEXMO_SMS_TO_NUMBER="" 91 | * NEXMO_SMS_FROM="" 92 | 93 | ### ALERT : twilio (sms/text) 94 | * ALERT_TWILIO="no" 95 | * TWILIO_ACCOUNT_SID="" 96 | * TWILIO_AUTH_TOKEN="" 97 | * TWILIO_SMS_TO_NUMBER="" 98 | * TWILIO_SMS_FROM="" 99 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extremeshok/docker-deepguard/810f4b94e1ce0f228f45afce714997deefcdbe2b/icon.png -------------------------------------------------------------------------------- /rootfs/etc/services.d/deepguard/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv sh 2 | 3 | echo "Running Deepguard" 4 | 5 | exec /bin/bash /xshok-deepguard.sh 6 | -------------------------------------------------------------------------------- /rootfs/xshok-deepguard.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ################################################################################ 3 | # This is property of eXtremeSHOK.com 4 | # You are free to use, modify and distribute, however you may not remove this notice. 5 | # Copyright (c) Adrian Jon Kriel :: admin@extremeshok.com 6 | ################################################################################ 7 | # 8 | # Notes: 9 | # Script must be placed into the same directory as the docker-compose.yml 10 | # 11 | # Assumptions: Docker and Docker-compose Installed 12 | # 13 | # Tested on KVM, VirtualBox and Dedicated Server 14 | # 15 | ################################################################################ 16 | # 17 | # THERE ARE NO USER CONFIGURABLE OPTIONS IN THIS SCRIPT 18 | # 19 | ################################################################################ 20 | 21 | IGNORE_LIST="${IGNORE_LIST:-person, bicycle, car, motorcycle, airplane, bus, train, truck, boat, trafficlight, firehydrant, stop_sign, parkingmeter, bench, bird, cat, dog, horse, sheep, cow, elephant, bear, zebra, giraffe, backpack, umbrella, handbag, tie, suitcase, frisbee, skis, snowboard, sportsball, kite, baseballbat, baseballglove, skateboard, surfboard, tennisracket, bottle, wineglass, cup, fork, knife, spoon, bowl, banana, apple, sandwich, orange, broccoli, carrot, hotdog, pizza, donot, cake, chair, couch, pottedplant, bed, diningtable, toilet, tv, laptop, mouse, remote, keyboard, cellphone, microwave, oven, toaster, sink, refrigerator, book, clock, vase, scissors, teddybear, hairdryer, toothbrush}" 22 | 23 | NOTIFY_LIST="${NOTIFY_LIST:-person, bear, cat, dog}" #people are sometimes detected as bears 24 | 25 | VALID_IMAGE_EXTENSION_LIST="${VALID_IMAGE_EXTENSION_LIST:-png, jpg, jpeg, gif, bmp}" 26 | #requires graphicsmagick 27 | 28 | #CAMERA_NAME_DELIMINATOR="${CAMERA_NAME_DELIMINATOR:-_}" 29 | CAMERA_NAME_DELIMINATOR="${CAMERA_NAME_DELIMINATOR:-.}" 30 | 31 | DIR_INPUT="${DIR_INPUT:-/data/input}" 32 | DIR_OUTPUT="${DIR_OUTPUT:-/data/output}" 33 | DIR_BACKUP="${DIR_BACKUP:-/data/backup}" 34 | 35 | BACKUP_ORIGINAL="${BACKUP_ORIGINAL:-no}" 36 | SAVE_OUTPUT="${SAVE_OUTPUT:-yes}" 37 | EMPTY_INPUT_DIR_ON_START="${EMPTY_INPUT_DIR_ON_START:-yes}" 38 | PROCESS_BACKLOG="${PROCESS_BACKLOG:-no}" 39 | 40 | DRAW_RESULTS="${DRAW_RESULTS:-yes}" 41 | 42 | DEBUG="${DEBUG:-no}" 43 | BE_VERBOSE="${BE_VERBOSE:-yes}" 44 | IGNORE_NONE="${IGNORE_NONE:-no}" 45 | 46 | ALERT_ALL_MAX_ALERTS="${ALERT_ALL_MAX_ALERTS:-4}" 47 | ALERT_ALL_PERIOD_SECONDS="${ALERT_ALL_PERIOD_SECONDS:-60}" 48 | 49 | ALERT_CAMERA_MAX_ALERTS="${ALERT_CAMERA_MAX_ALERTS:-2}" 50 | ALERT_CAMERA_PERIOD_SECONDS="${ALERT_CAMERA_PERIOD_SECONDS:-180}" 51 | 52 | DEEPSTACK_URL="${DEEPSTACK_URL:-http://deepstack:5000}" 53 | DEEPSTACK_BACKUP_URL="${DEEPSTACK_BACKUP_URL:-}" 54 | DEEPSTACK_CONFIDENCE_LIMIT="${DEEPSTACK_CONFIDENCE_LIMIT:-65}" 55 | 56 | #NOTIFY 57 | NOTIFY_ZONEMINDER="${NOTIFY_ZONEMINDER:-no}" 58 | ZONEMINDER_NOFITY_HOST="${ZONEMINDER_NOFITY_HOST:-zoneminder}" 59 | ZONEMINDER_NOFITY_PORT="${ZONEMINDER_NOFITY_PORT:-6802}" 60 | 61 | NOTIFY_URL="${NOTIFY_URL:-no}" 62 | URL_NOTIFY="${URL_NOTIFY:-http://blueiris/admin?trigger&camera=hd%%CAMERA%%&user=ai&pw=ai}" 63 | 64 | NOTIFY_MQTT="${NOTIFY_MQTT:-no}" 65 | MQTT_NOTIFY_URL="${MQTT_NOTIFY_URL:-}" 66 | MQTT_NOTIFY_MESSAGE="${MQTT_NOTIFY_MESSAGE:-alert}" 67 | 68 | #ALERT 69 | ALERT_PUSHOVER="${ALERT_PUSHOVER:-no}" 70 | PUSHOVER_TOKEN="${PUSHOVER_TOKEN:-}" 71 | PUSHOVER_KEY="${PUSHOVER_KEY:-}" 72 | PUSHOVER_PRIORITY="${PUSHOVER_PRIORITY:-2}" #2=emergency 73 | PUSHOVER_EXPIRE="${PUSHOVER_EXPIRE:-600}" 74 | PUSHOVER_RETRY="${PUSHOVER_RETRY:-30}" 75 | PUSHOVER_DEVICE="${PUSHOVER_DEVICE:-}" 76 | PUSHOVER_SOUND="${PUSHOVER_SOUND:-siren}" 77 | 78 | ALERT_TELEGRAM="${ALERT_TELEGRAM:-no}" 79 | TELEGRAM_TOKEN="${TELEGRAM_TOKEN:-}" 80 | TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID:-}" 81 | 82 | ALERT_WHATSMATE="${ALERT_WHATSMATE:-no}" 83 | WHATSMATE_CLIENT_ID="${WHATSMATE_CLIENT_ID:-}" 84 | WHATSMATE_CLIENT_SECRET="${WHATSMATE_CLIENT_SECRET:-}" 85 | WHATSMATE_WHATSAPP_NUMBER="${WHATSMATE_WHATSAPP_NUMBER:-}" 86 | 87 | ALERT_NEXMO="${ALERT_NEXMO:-no}" 88 | NEXMO_API_KEY="${NEXMO_API_KEY:-}" 89 | NEXMO_API_SECRET="${NEXMO_API_SECRET:-}" 90 | NEXMO_SMS_TO_NUMBER="${NEXMO_SMS_TO_NUMBER:-}" 91 | NEXMO_SMS_FROM="${NEXMO_SMS_FROM:-}" 92 | 93 | ALERT_TWILIO="${ALERT_TWILIO:-no}" 94 | TWILIO_ACCOUNT_SID="${TWILIO_ACCOUNT_SID:-}" 95 | TWILIO_AUTH_TOKEN="${TWILIO_AUTH_TOKEN:-}" 96 | TWILIO_SMS_TO_NUMBER="${TWILIO_SMS_TO_NUMBER:-}" 97 | TWILIO_SMS_FROM="${TWILIO_SMS_FROM:-}" 98 | 99 | WATERMARK="${WATERMARK:-DeepGuard.co.za}" 100 | 101 | # PROGRAM SETTINGS 102 | CURL_OPTIONS="--retry 2 --retry-connrefused 2 --retry-max-time 10 --retry-delay 1 --insecure --show-error" 103 | # ls /usr/share/fonts/gsfonts/ 104 | FONT="/usr/share/fonts/gsfonts/NimbusSansNarrow-Regular.otf" 105 | #FONT="/usr/share/fonts/gsfonts/NimbusMonoPS-Regular.otf" 106 | #FONT="/usr/share/fonts/gsfonts/NimbusRoman-Regular.otf" 107 | 108 | ################### 109 | #### FUNCTIONS #### 110 | ################### 111 | function notify_url { #cameraname 112 | test "$BE_VERBOSE" == "1" && echo "Notify: url" 113 | if [ "$URL_NOTIFY" != "" ] && [[ "${URL_NOTIFY,,}" == "http"* ]] ; then 114 | result="$(curl $CURL_OPTIONS -s "${URL_NOTIFY/\%\%CAMERA\%\%/$1}" 2>&1)" 115 | test "$DEBUG" == "1" && echo "url @ ${cameraname}: ${result}" 116 | else 117 | echo "ERROR: URL_NOTIFY is empty or missing http/https" 118 | fi 119 | } 120 | 121 | function notify_mqtt { #cameraname 122 | test "$BE_VERBOSE" == "1" && echo "Notify: mqtt" 123 | if [ "$MQTT_NOTIFY_URL" != "" ] && [[ "${MQTT_NOTIFY_URL,,}" == "mqtt"* ]] ; then 124 | result="$(mosquitto_pub -L "${MQTT_NOTIFY_URL/\%\%CAMERA\%\%/$1}" -m "$MQTT_NOTIFY_MESSAGE" 2>&1)" 125 | test "$DEBUG" == "1" && echo "url @ ${cameraname}: ${result}" 126 | else 127 | echo "ERROR: MQTT_NOTIFY_URL is empty or missing mqtt/mqtts" 128 | fi 129 | } 130 | 131 | function notify_zoneminder { #cameraname 132 | test "$BE_VERBOSE" == "1" && echo "Notify: zoneminder" 133 | if [ "$ZONEMINDER_NOFITY_HOST" != "" ] && [[ $ZONEMINDER_NOFITY_PORT =~ ^-?[0-9]+$ ]] ; then 134 | result="$(echo "${BLUEIRIS_URL_NOTIFY/\%\%CAMERA\%\%/$1}|on+10|255|test|test" | nc $ZONEMINDER_NOFITY_HOST $ZONEMINDER_NOFITY_PORT 2>&1)" 135 | test "$DEBUG" == "1" && echo "zoneminder @ ${cameraname}: ${result}" 136 | else 137 | echo "ERROR: ZONEMINDER_NOFITY_HOST is empty or ZONEMINDER_NOFITY_PORT is not a number" 138 | fi 139 | } 140 | 141 | function alert_pushover { #message #image 142 | test "$BE_VERBOSE" == "1" && echo "Alert: pushover" 143 | if [ "$PUSHOVER_TOKEN" != "" ] && [ "$PUSHOVER_KEY" != "" ] ; then 144 | result="$(curl $CURL_OPTIONS -F "token=${PUSHOVER_TOKEN}" -F "user=${PUSHOVER_KEY}" -F "attachment=@${2}" -form-string "title=${1/ *}" --form-string "sound=${PUSHOVER_SOUND}" ${PUSHOVER_DEVICE:+ --form-string "device=${PUSHOVER_DEVICE}"} ${PUSHOVER_PRIORITY:+ --form-string "priority=${PUSHOVER_PRIORITY}"} ${PUSHOVER_EXPIRE:+ --form-string "expire=${PUSHOVER_EXPIRE}"} ${PUSHOVER_RETRY:+ --form-string "retry=${PUSHOVER_RETRY}"} -F "message=$1" https://api.pushover.net/1/messages.json 2>&1)" 145 | test "$DEBUG" == "1" && echo "pushover: $result" 146 | else 147 | echo "ERROR: PUSHOVER_TOKEN or PUSHOVER_KEY is empty" 148 | fi 149 | } 150 | 151 | function alert_telegram { #message #image 152 | test "$BE_VERBOSE" == "1" && echo "Alert: telegram" 153 | if [ "$TELEGRAM_CHAT_ID" != "" ] && [ "$TELEGRAM_TOKEN" != "" ] ; then 154 | result="$(curl $CURL_OPTIONS -F "chat_id=${TELEGRAM_CHAT_ID}" -F "text=$1" https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage 2>&1)" 155 | test "$DEBUG" == "1" && echo "$result" 156 | result="$(curl $CURL_OPTIONS -F "chat_id=${TELEGRAM_CHAT_ID}" -F "photo=@${2}" https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendPhoto 2>&1)" 157 | test "$DEBUG" == "1" && echo "telegram: $result" 158 | else 159 | echo "ERROR: TELEGRAM_CHAT_ID or TELEGRAM_TOKEN is empty" 160 | fi 161 | } 162 | 163 | #https://whatsmate.github.io/2017-10-25-send-whatsapp-image-group-bash-shell-script/ 164 | function alert_whatsmate { #message #image 165 | test "$BE_VERBOSE" == "1" && echo "Alert: whatsmate" 166 | if [ "$WHATSMATE_CLIENT_ID" != "" ] && [ "$WHATSMATE_CLIENT_SECRET" != "" ] && [ "$WHATSMATE_WHATSAPP_NUMBER" != "" ] ; then 167 | random="$(date '+%s')" 168 | cat > "/tmp/jsonbody_${random}" << _EOM_ 169 | { 170 | "number": "$WHATSMATE_WHATSAPP_NUMBER", 171 | "caption": "${1}", 172 | "image": "$(base64 -w 0 ${2})" 173 | } 174 | _EOM_ 175 | result="$(curl $CURL_OPTIONS -X "POST" -H "X-WM-CLIENT-ID: ${WHATSMATE_CLIENT_ID}" -H "X-WM-CLIENT-SECRET: ${WHATSMATE_CLIENT_SECRET}" -H "Content-Type: application/json" --data-binary @"/tmp/jsonbody_${random}" http://api.whatsmate.net/v3/whatsapp/group/image/message/${WHATSMATE_WHATSAPP_NUMBER} 2>&1)" 176 | test "$DEBUG" == "1" && echo "whatsmate: $result" 177 | rm -f "/tmp/jsonbody_${random}" 178 | else 179 | echo "ERROR: WHATSMATE_CLIENT_ID or WHATSMATE_CLIENT_SECRET or WHATSMATE_WHATSAPP_NUMBER is empty" 180 | fi 181 | } 182 | 183 | function alert_nexmo { #message 184 | test "$BE_VERBOSE" == "1" && echo "Alert: nexmo" 185 | if [ "$NEXMO_SMS_FROM" != "" ] && [ "$NEXMO_SMS_TO_NUMBER" != "" ] && [ "$NEXMO_API_KEY" != "" ] && [ "$NEXMO_API_SECRET" != "" ] ; then 186 | result="$(curl $CURL_OPTIONS -X "POST" "https://rest.nexmo.com/sms/json" -d "from=+${NEXMO_SMS_FROM/\+}" -d "text=${1:0:160}" -d "to=+${NEXMO_SMS_TO_NUMBER/\+}" -d "api_key=${NEXMO_API_KEY}" -d "api_secret=${NEXMO_API_SECRET}" 2>&1)" 187 | test "$DEBUG" == "1" && echo "nexmo: $result" 188 | else 189 | echo "ERROR: NEXMO_SMS_FROM or NEXMO_SMS_TO_NUMBER or NEXMO_API_KEY or NEXMO_API_SECRET is empty" 190 | fi 191 | } 192 | 193 | function alert_twilio { #message 194 | test "$BE_VERBOSE" == "1" && echo "Alert: twilio" 195 | if [ "$TWILIO_ACCOUNT_SID" != "" ] && [ "$TWILIO_AUTH_TOKEN" != "" ] && [ "$TWILIO_SMS_TO_NUMBER" != "" ] && [ "$TWILIO_SMS_TO_NUMBER" != "" ] ; then 196 | result="$(curl $CURL_OPTIONS -u "${TWILIO_ACCOUNT_SID}:${TWILIO_AUTH_TOKEN}" -d "From=+${TWILIO_SMS_FROM/\+}" -d "To=+${TWILIO_SMS_TO_NUMBER/\+}" -d "Body=${1:0:160}" "https://api.twilio.com/2010-04-01/Accounts/${TWILIO_ACCOUNT_SID}/SMS/Messages" 2>&1)" 197 | test "$DEBUG" == "1" && echo "twilio: $result" 198 | else 199 | echo "ERROR: TWILIO_ACCOUNT_SID or TWILIO_AUTH_TOKEN or TWILIO_SMS_FROM or TWILIO_SMS_TO_NUMBER is empty" 200 | fi 201 | } 202 | 203 | function process_image { #image_in #image_out 204 | export RUN_COUNT=$((RUN_COUNT+1)) 205 | 206 | image_in="$1" 207 | image_out="$2" 208 | 209 | # reset varibles 210 | cameraname="" 211 | result="" 212 | res="" 213 | confidence=0 214 | label="" 215 | y_min="" 216 | x_min="" 217 | y_max="" 218 | x_max="" 219 | declare -a MAIN_ARRAY 220 | declare -a SUB_ARRAY 221 | declare -A COUNTER 222 | MAIN_ARRAY_COUNT=0 223 | result_boxes="" 224 | 225 | if [ ! -f "$image_in" ] ; then 226 | echo "ERROR: unable to read image: ${image_in}" 227 | else 228 | test "$BE_VERBOSE" == "1" && echo "Processing image: $image_in" 229 | result="$(curl -k -X POST -F image=@"${image_in}" -F min_confidence="0.${DEEPSTACK_CONFIDENCE_LIMIT}" "${DEEPSTACK_URL}/v1/vision/detection")" 230 | res=$? 231 | if [ "$res" != 0 ] && [ "$DEEPSTACK_BACKUP_URL" != "" ] ; then 232 | test "$BE_VERBOSE" == "1" && echo "Retrying with deepstack backup" 233 | result="$(curl -k -X POST -F image=@"${image_in}" -F min_confidence="0.${DEEPSTACK_CONFIDENCE_LIMIT}" "${DEEPSTACK_BACKUP_URL}/v1/vision/detection")" 234 | res=$? 235 | fi 236 | 237 | #shellcheck disable=SC2076 238 | if [ "$res" != 0 ] || [ "$result" == "" ] ; then 239 | echo "RESULT ERROR: $result" 240 | elif [[ "${result,,}" =~ "error" ]] ; then 241 | echo "RESULT ERROR: $result" 242 | result="" 243 | elif [[ "${result,,}" =~ '"predictions":[]' ]] ; then 244 | test "$BE_VERBOSE" == "1" && echo "NO RESULTS: $result" 245 | result="" 246 | else 247 | thiscount=0 248 | while read "confidence" "label" "y_min" "x_min" "y_max" "x_max"; do 249 | if [ ! -z "$confidence" ] ; then 250 | confidence="${confidence:2:2}" 251 | fi 252 | test "$DEBUG" == "1" && echo "$thiscount | $confidence | $label | $y_min | $x_min | $y_max | $x_max" 253 | MAIN_ARRAY[$thiscount]="${confidence},${label},${y_min},${x_min},${y_max},${x_max}" 254 | thiscount=$((thiscount + 1)) 255 | done < <(echo "$result" | sed -e 's/[[:space:]]//g' | jq -r '.predictions[]|"\(.confidence) \(.label) \(.y_min) \(.x_min) \(.y_max) \(.x_max)"') 256 | 257 | MAIN_ARRAY_COUNT=${#MAIN_ARRAY[@]} 258 | test "$DEBUG" == "1" && echo "MAIN_ARRAY_COUNT : ${MAIN_ARRAY_COUNT}" 259 | 260 | result_boxes="" 261 | 262 | if [[ $MAIN_ARRAY_COUNT -gt 0 ]] ; then 263 | 264 | test "$DEBUG" == "1" && echo "processing MAIN_ARRAY" 265 | test "$DEBUG" == "1" && echo "MAIN_ARRAY: ${MAIN_ARRAY[*]}" 266 | 267 | for ((i=0; i<$MAIN_ARRAY_COUNT; i++)) ; do 268 | test "$DEBUG" == "1" && echo "processing SUB_ARRAY ${i}" 269 | 270 | readarray -td, SUB_ARRAY <<<"${MAIN_ARRAY[i]}"; 271 | 272 | #assign 273 | test "$BE_VERBOSE" == "1" && echo "confidence ${SUB_ARRAY[0]} | label ${SUB_ARRAY[1]} | y_min ${SUB_ARRAY[2]} | x_min ${SUB_ARRAY[3]} | y_max ${SUB_ARRAY[4]} | x_max ${SUB_ARRAY[5]}" 274 | 275 | #shellcheck disable=SC2076 276 | if [[ "${SUB_ARRAY[0]}" -ge "$DEEPSTACK_CONFIDENCE_LIMIT" ]] ; then 277 | 278 | test "$DEBUG" == "1" && echo "${SUB_ARRAY[0]} -ge $DEEPSTACK_CONFIDENCE_LIMIT" 279 | 280 | if [[ "$NOTIFY_LIST" =~ ",${SUB_ARRAY[1],,}," ]] || [[ ! "$IGNORE_LIST" =~ ",${SUB_ARRAY[1],,}," ]] ; then 281 | color="$(echo "${SUB_ARRAY[1]}" | md5sum)" 282 | color="${color:2:6}" 283 | 284 | test "$DEBUG" == "1" && echo "NOTIFY_LIST or not IGNORE_LIST" 285 | 286 | result_boxes="${result_boxes} -stroke \"#${color}\" -fill none -draw \"rectangle ${SUB_ARRAY[3]},${SUB_ARRAY[2]},${SUB_ARRAY[5]},${SUB_ARRAY[4]}\" -stroke none -fill \"#${color}\" -draw \"text ${SUB_ARRAY[3]},${SUB_ARRAY[2]} '${SUB_ARRAY[1]}'\" -draw \"text ${SUB_ARRAY[3]},${SUB_ARRAY[4]} ' ${SUB_ARRAY[0]} %'\"" 287 | 288 | COUNTER["${SUB_ARRAY[1]}"]=$((${COUNTER["${SUB_ARRAY[1]}"]}+1)) 289 | 290 | else 291 | test "$BE_VERBOSE" == "1" && echo "${SUB_ARRAY[1]} : not required or on ignore list" 292 | fi 293 | fi 294 | done 295 | 296 | fi 297 | 298 | if [ "$result_boxes" != "" ] ; then 299 | 300 | cameraname="${image_in/*\//}" 301 | cameraname="${cameraname/${CAMERA_NAME_DELIMINATOR}*/}" 302 | cameraname="${cameraname//[[:space:]]}" #remove spaces 303 | ALERT_COUNT_CAMERA="ALERT_COUNT_$cameraname" 304 | ALERT_LAST_CAMERA="ALERT_LAST_$cameraname" 305 | 306 | # NOTIFY 307 | test "$NOTIFY_URL" == "1" && notify_url "$cameraname" $PARALLEL 308 | test "$NOTIFY_MQTT" == "1" && notify_mqtt "$cameraname" $PARALLEL 309 | test "$NOTIFY_ZONEMINDER" == "1" && notify_zoneminder "$cameraname" $PARALLEL 310 | 311 | filetime="$(stat -c %W "${image_in}")" 312 | if [ "$filetime" != "0" ] && [ "$filetime" != "0" ] ; then 313 | filetime="$(date -d "$filetime" 2> /dev/null)" 314 | fi 315 | filetime="$(date)" 316 | filetime="${filetime//[[:space:]]/-}" #replace spaces with - 317 | 318 | if [ "${DRAW_RESULTS,,}" == "yes" ] || [ "${DRAW_RESULTS,,}" == "true" ] || [ "${DRAW_RESULTS}" == "1" ] ; then 319 | #shellcheck disable=SC2046 320 | eval gm convert "${image_in}" -font "$FONT" -fill none -strokewidth 1 -pointsize 16 $(echo "$result_boxes") -gravity SouthWest -draw \"fill blue stroke none text +5,+5 '$WATERMARK'\" -gravity NorthWest -draw \"fill blue stroke none text +15,+15 '${filetime}'\" -quality 82 "${image_out}" 321 | fi 322 | 323 | ALERT_NOW="$(date +%s)" 324 | ALERT_LAST_CAMERA_TIME=${!ALERT_LAST_CAMERA} 325 | if [[ $((ALERT_LAST + ALERT_ALL_PERIOD_SECONDS)) -lt $ALERT_NOW ]] || [[ $ALERT_COUNT -lt $ALERT_ALL_MAX_ALERTS ]] ; then 326 | if [[ $((ALERT_LAST_CAMERA_TIME + ALERT_ALL_PERIOD_SECONDS)) -lt $ALERT_NOW ]] || [[ ${!ALERT_COUNT_CAMERA} -lt $ALERT_CAMERA_MAX_ALERTS ]] ; then 327 | if [[ ALERT_COUNT -ge $ALERT_ALL_MAX_ALERTS ]] ; then 328 | export ALERT_COUNT=1 329 | else 330 | export ALERT_COUNT=$((ALERT_COUNT+1)) 331 | fi 332 | if [[ ${!ALERT_COUNT_CAMERA} -ge $ALERT_CAMERA_MAX_ALERTS ]] ; then 333 | #shellcheck disable=SC2140 334 | export "ALERT_COUNT_$cameraname"="1" 335 | else 336 | #shellcheck disable=SC2140,SC1102 337 | export "ALERT_COUNT_$cameraname"="$((ALERT_COUNT_$cameraname + 1))" 338 | fi 339 | 340 | test "$DEBUG" == "1" && echo "ALERT!!" 341 | export ALERT_LAST="$ALERT_NOW" 342 | #shellcheck disable=SC2140 343 | export "ALERT_LAST_$cameraname"="$ALERT_NOW" 344 | 345 | MESSAGE="$cameraname $filetime" 346 | for k in "${!COUNTER[@]}" ; do 347 | MESSAGE="${MESSAGE} $k:${COUNTER[$k]}" 348 | done 349 | 350 | test "$ALERT_PUSHOVER" == "1" && alert_pushover "${MESSAGE}" "${image_out}" $PARALLEL 351 | test "$ALERT_TELEGRAM" == "1" && alert_telegram "${MESSAGE}" "${image_out}" $PARALLEL 352 | test "$ALERT_WHATSMATE" == "1" && alert_whatsmate "${MESSAGE}" "${image_out}" $PARALLEL 353 | test "$ALERT_NEXMO" == "1" && alert_nexmo "${MESSAGE}" "${image_out}" $PARALLEL 354 | test "$ALERT_TWILIO" == "1" && alert_twilio "${MESSAGE}" "${image_out}" $PARALLEL 355 | else 356 | test "$DEBUG" == "1" && echo "NOALERT_CAMERA" 357 | fi 358 | else 359 | test "$DEBUG" == "1" && echo "NOALERT_ALL" 360 | fi 361 | fi 362 | 363 | if [ "${BACKUP_ORIGINAL}" == "1" ] ; then 364 | mv -f "$image_in" "${DIR_BACKUP}/${image_in/*\//}" 365 | else 366 | rm -f "$image_in" 367 | fi 368 | if [ "${SAVE_OUTPUT}" != "1" ] ; then 369 | rm -f "$image_out" 370 | fi 371 | fi 372 | fi 373 | 374 | test "$BE_VERBOSE" == "1" && echo "---->${RUN_COUNT} | AC ${ALERT_COUNT} @ ${ALERT_LAST}" 375 | } 376 | 377 | ################ 378 | ##### MAIN ##### 379 | ################ 380 | if [ "$(which stat 2> /dev/null)" == "" ] ; then 381 | echo "ERROR: stat binary not found" 382 | exit 1 383 | fi 384 | if [ "$(which inotifywait 2> /dev/null)" == "" ] ; then 385 | echo "ERROR: inotifywait binary not found, install inotify-tools" 386 | exit 1 387 | fi 388 | if [ "$(which curl 2> /dev/null)" == "" ] ; then 389 | echo "ERROR: curl binary not found" 390 | exit 1 391 | fi 392 | if [ "$(which gm 2> /dev/null)" == "" ] ; then 393 | echo "ERROR: gm binary not found, install graphicsmagick" 394 | exit 1 395 | fi 396 | if [ "$(which nc 2> /dev/null)" == "" ] ; then 397 | echo "ERROR: nc binary not found, install netcat" 398 | exit 1 399 | fi 400 | if [ "$(which jq 2> /dev/null)" == "" ] ; then 401 | echo "ERROR: jq binary not found" 402 | exit 1 403 | fi 404 | if [ "$DIR_INPUT" == "" ] || [ "$DIR_INPUT" == "/" ] ; then 405 | echo "ERROR: DIR_INPUT is invalid" 406 | exit 1 407 | fi 408 | if [[ "${DEEPSTACK_URL,,}" != "http"* ]] ; then 409 | echo "ERROR: Invalid DEEPSTACK_URL" 410 | exit 1 411 | fi 412 | if [[ "${DEEPSTACK_BACKUP_URL,,}" != "http"* ]] && [ "$DEEPSTACK_BACKUP_URL" != "" ] ; then 413 | echo "ERROR: Invalid DEEPSTACK_BACKUP_URL" 414 | exit 1 415 | fi 416 | 417 | DEEPSTACK_URL="${DEEPSTACK_URL/\/v1\/vision\/detection*}" 418 | DEEPSTACK_URL="${DEEPSTACK_URL%\/}" 419 | if [ "$DEEPSTACK_BACKUP_URL" != "" ] ; then 420 | DEEPSTACK_BACKUP_URL="${DEEPSTACK_BACKUP_URL/\/v1\/vision\/detection*}" 421 | DEEPSTACK_BACKUP_URL="${DEEPSTACK_BACKUP_URL%\/}" 422 | fi 423 | 424 | # Process lists 425 | IGNORE_LIST="${IGNORE_LIST//[[:space:]]}" #remove spaces 426 | IGNORE_LIST="${IGNORE_LIST//;/,}" #replace ; with , 427 | IGNORE_LIST=",${IGNORE_LIST,,}," #to lowercase and append , 428 | NOTIFY_LIST="${NOTIFY_LIST//[[:space:]]}" #remove spaces 429 | NOTIFY_LIST="${NOTIFY_LIST//;/,}" #replace ; with , 430 | NOTIFY_LIST=",${NOTIFY_LIST,,}," #to lowercase and append , 431 | VALID_IMAGE_EXTENSION_LIST="${VALID_IMAGE_EXTENSION_LIST//[[:space:]]}" #remove spaces 432 | VALID_IMAGE_EXTENSION_LIST="${VALID_IMAGE_EXTENSION_LIST//;/,}" #replace ; with , 433 | VALID_IMAGE_EXTENSION_LIST=",${VALID_IMAGE_EXTENSION_LIST,,}," #to lowercase and append , 434 | 435 | # Globals 436 | export RUN_COUNT=0 437 | export ALERT_COUNT=0 438 | export ALERT_LAST=0 439 | 440 | # Assign bools 441 | if [ "${DEBUG,,}" == "yes" ] || [ "${DEBUG,,}" == "true" ] || [ "${DEBUG,,}" == "1" ] ; then 442 | echo "DEBUG: enabled" 443 | DEBUG="1" 444 | BE_VERBOSE="1" 445 | PARALLEL="" 446 | else 447 | DEBUG="0" 448 | PARALLEL="&" 449 | if [ "${BE_VERBOSE,,}" == "yes" ] || [ "${BE_VERBOSE,,}" == "true" ] || [ "${BE_VERBOSE,,}" == "1" ] ; then 450 | echo "VERBOSE: enabled" 451 | BE_VERBOSE="1" 452 | else 453 | BE_VERBOSE="0" 454 | fi 455 | fi 456 | if [ "${IGNORE_NONE,,}" == "yes" ] || [ "${IGNORE_NONE,,}" == "true" ] || [ "${IGNORE_NONE,,}" == "1" ] ; then 457 | IGNORE_LIST="" 458 | fi 459 | if [ "${SAVE_OUTPUT,,}" == "yes" ] || [ "${SAVE_OUTPUT,,}" == "true" ] || [ "${SAVE_OUTPUT,,}" == "1" ] ; then 460 | test "$BE_VERBOSE" == "1" && echo "SAVE_OUTPUT: enabled" 461 | SAVE_OUTPUT="1" 462 | else 463 | SAVE_OUTPUT="0" 464 | fi 465 | if [ "${BACKUP_ORIGINAL,,}" == "yes" ] || [ "${BACKUP_ORIGINAL,,}" == "true" ] || [ "${BACKUP_ORIGINAL,,}" == "1" ] ; then 466 | test "$BE_VERBOSE" == "1" && echo "BACKUP_ORIGINAL: enabled" 467 | BACKUP_ORIGINAL="1" 468 | else 469 | BACKUP_ORIGINAL="0" 470 | fi 471 | if [ "${ALERT_PUSHOVER,,}" == "yes" ] || [ "${ALERT_PUSHOVER,,}" == "true" ] || [ "${ALERT_PUSHOVER,,}" == "1" ] ; then 472 | test "$BE_VERBOSE" == "1" && echo "ALERT_PUSHOVER: enabled" 473 | ALERT_PUSHOVER="1" 474 | else 475 | ALERT_PUSHOVER="0" 476 | fi 477 | if [ "${ALERT_TELEGRAM,,}" == "yes" ] || [ "${ALERT_TELEGRAM,,}" == "true" ] || [ "${ALERT_TELEGRAM,,}" == "1" ] ; then 478 | test "$BE_VERBOSE" == "1" && echo "ALERT_TELEGRAM: enabled" 479 | ALERT_TELEGRAM="1" 480 | else 481 | ALERT_TELEGRAM="0" 482 | fi 483 | if [ "${ALERT_WHATSMATE,,}" == "yes" ] || [ "${ALERT_WHATSMATE,,}" == "true" ] || [ "${ALERT_WHATSMATE,,}" == "1" ] ; then 484 | test "$BE_VERBOSE" == "1" && echo "ALERT_WHATSMATE: enabled" 485 | ALERT_WHATSMATE="1" 486 | else 487 | ALERT_WHATSMATE="0" 488 | fi 489 | if [ "${ALERT_NEXMO,,}" == "yes" ] || [ "${ALERT_NEXMO,,}" == "true" ] || [ "${ALERT_NEXMO,,}" == "1" ] ; then 490 | test "$BE_VERBOSE" == "1" && echo "ALERT_NEXMO: enabled" 491 | ALERT_NEXMO="1" 492 | else 493 | ALERT_NEXMO="0" 494 | fi 495 | if [ "${ALERT_TWILIO,,}" == "yes" ] || [ "${ALERT_TWILIO,,}" == "true" ] || [ "${ALERT_TWILIO,,}" == "1" ] ; then 496 | test "$BE_VERBOSE" == "1" && echo "ALERT_TWILIO: enabled" 497 | ALERT_TWILIO="1" 498 | else 499 | ALERT_TWILIO="0" 500 | fi 501 | if [ "${NOTIFY_ZONEMINDER,,}" == "yes" ] || [ "${NOTIFY_ZONEMINDER,,}" == "true" ] || [ "${NOTIFY_ZONEMINDER,,}" == "1" ] ; then 502 | test "$BE_VERBOSE" == "1" && echo "NOTIFY_ZONEMINDER: enabled" 503 | NOTIFY_ZONEMINDER="1" 504 | else 505 | NOTIFY_ZONEMINDER="0" 506 | fi 507 | if [ "${NOTIFY_MQTT,,}" == "yes" ] || [ "${NOTIFY_MQTT,,}" == "true" ] || [ "${NOTIFY_MQTT,,}" == "1" ] ; then 508 | if [ "$(which mosquitto_pub 2> /dev/null)" == "" ] ; then 509 | echo "ERROR: mosquitto_pub binary not found, install mosquitto-clients" 510 | exit 1 511 | fi 512 | test "$BE_VERBOSE" == "1" && echo "NOTIFY_MQTT: enabled" 513 | NOTIFY_MQTT="1" 514 | else 515 | NOTIFY_MQTT="0" 516 | fi 517 | if [ "${NOTIFY_URL,,}" == "yes" ] || [ "${NOTIFY_URL,,}" == "true" ] || [ "${NOTIFY_URL,,}" == "1" ] ; then 518 | test "$BE_VERBOSE" == "1" && echo "NOTIFY_URL: enabled" 519 | NOTIFY_URL="1" 520 | else 521 | NOTIFY_URL="0" 522 | fi 523 | 524 | DEEPSTACK_CONFIDENCE_LIMIT="${DEEPSTACK_CONFIDENCE_LIMIT/0./}" 525 | DEEPSTACK_CONFIDENCE_LIMIT="${DEEPSTACK_CONFIDENCE_LIMIT/1./}" 526 | if [[ $DEEPSTACK_CONFIDENCE_LIMIT -le 0 ]] || [[ $DEEPSTACK_CONFIDENCE_LIMIT -ge 100 ]] ; then 527 | echo "DEEPSTACK_CONFIDENCE_LIMIT too high or too low, setting to 65" 528 | DEEPSTACK_CONFIDENCE_LIMIT=65 529 | fi 530 | 531 | mkdir -p "$DIR_INPUT" 532 | if [ "${SAVE_OUTPUT}" != "1" ] ; then 533 | DIR_OUTPUT="/tmp/deepstack" 534 | fi 535 | if [ "${BACKUP_ORIGINAL}" == "1" ] ; then 536 | mkdir -p "$DIR_BACKUP" 537 | fi 538 | mkdir -p "$DIR_OUTPUT" 539 | 540 | if [ "${PROCESS_BACKLOG,,}" == "yes" ] || [ "${PROCESS_BACKLOG,,}" == "true" ] || [ "${PROCESS_BACKLOG,,}" == "1" ] ; then 541 | echo "Processing Backlog: ${DIR_INPUT}" 542 | while IFS= read -r -d $'\0' filepath; do 543 | filename="${filepath//*\/}" 544 | extension="${filename//*.}" 545 | test "$DEBUG" == "1" && echo "${filepath} : $extension" 546 | #shellcheck disable=SC2076 547 | if [[ "$VALID_IMAGE_EXTENSION_LIST" =~ ",${extension,,}," ]] ; then 548 | test "$DEBUG" == "1" && echo "valid image extension detected...." 549 | process_image "${filepath}" "$DIR_OUTPUT/${filename}" 550 | fi 551 | test "$DEBUG" == "1" && echo "====>${RUN_COUNT} | AC ${ALERT_COUNT} @ ${ALERT_LAST}" 552 | done < <(find "${DIR_INPUT}" -maxdepth 1 -type f -print0) 553 | fi 554 | if [ "${EMPTY_INPUT_DIR_ON_START,,}" == "yes" ] || [ "${EMPTY_INPUT_DIR_ON_START,,}" == "true" ] || [ "${EMPTY_INPUT_DIR_ON_START,,}" == "1" ] ; then 555 | test "$BE_VERBOSE" == "1" && echo "Emptying input dir: $DIR_INPUT" 556 | rm -f "${DIR_INPUT}/*.*" 557 | sync 558 | fi 559 | echo "Watching: ${DIR_INPUT}" 560 | inotifywait -m -e close_write,moved_to --exclude ".*(\.git|\.private|pr ivate|html|public_html|www|db|dbinfo|log|logs|sql|backup|backups|conf|config|configs)/" "${DIR_INPUT}" | 561 | while read -r directory events filename; do 562 | extension="${filename//*.}" 563 | test "$DEBUG" == "1" && echo "${directory}${filename} : $events : $extension" 564 | #shellcheck disable=SC2076 565 | if [[ "$VALID_IMAGE_EXTENSION_LIST" =~ ",${extension,,}," ]] ; then 566 | test "$DEBUG" == "1" && echo "valid image extension detected...." 567 | process_image "${directory}${filename}" "$DIR_OUTPUT/${filename}" 568 | fi 569 | test "$DEBUG" == "1" && echo "====>${RUN_COUNT} | AC ${ALERT_COUNT} @ ${ALERT_LAST}" 570 | done 571 | --------------------------------------------------------------------------------