Skip to content

Commit a295194

Browse files
authored
increase motion detection threshold when image is darker (#52)
* increase motion detection threshold when image is darker * more log info * still trying to stop false alarms in darkness * add missing PiCameraRuntimeError * misc fixes * misc fixes * Update README.md * misc fixes * switch to new context based telegram thing * misc * camera fixes * switch back to kamene due to bug in scapy
1 parent 12b1941 commit a295194

File tree

6 files changed

+80
-55
lines changed

6 files changed

+80
-55
lines changed

README.md

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ You can send the Telegram bot commands that trigger certain actions.
7474
The application is written in python 3 and large parts of the functionality are provided by the following pip packages:
7575

7676
- [picamera](https://github.com/waveform80/picamera)
77-
- [scapy](https://github.com/secdev/scapy)
77+
- [kamene](https://github.com/phaethon/kamene)
7878
- [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot)
7979
- [opencv-python](https://github.com/skvark/opencv-python)
8080

@@ -89,18 +89,26 @@ The application uses multithreading in order to process events asynchronously. T
8989

9090
First ensure your WiFi is [set up correctly](#WiFi-adapter-arrangement)
9191

92-
Install required packages:
92+
Ensure your GPU/memory split gives 128MB to the GPU. You can see or set this value with `raspi-config`.
93+
94+
Install required packages for python:
9395

9496
```console
9597
sudo apt update
96-
sudo apt install -y libhdf5-100 libharfbuzz0b libwebp6 libjasper1 libilmbase12 libopenexr22 libgstreamer1.0-0 libavcodec-extra57 libavformat57 libswscale4 libgtk-3-0 libqtgui4 libqt4-test libatlas-base-dev tcpdump iw python3-dev python3-pip libjpeg8-dev zlib1g-dev libffi-dev python3-numpy libopenjp2-7-dev libtiff5
98+
sudo apt install -y tcpdump iw python3-dev python3-pip python3-numpy
99+
```
100+
101+
Install required packages for OpenCV:
102+
103+
```console
104+
sudo apt install -y libhdf5-103 libharfbuzz0b libwebp6 libjasper1 libopenexr23 libgstreamer1.0-0 libatlas-base-dev libgtk-3-0 libqtgui4 libqt4-test libilmbase23 libavcodec-extra58 libavformat58 libswscale5 libjpeg8-dev zlib1g-dev libffi-dev libopenjp2-7-dev libtiff5
97105
```
98106

99-
Install open-cv and rpi-security:
107+
Install OpenCV and rpi-security:
100108

101109
```console
102-
sudo pip3 install opencv-contrib-python opencv-contrib-python-headless
103-
sudo pip3 install --no-binary :all: https://github.com/FutureSharks/rpi-security/archive/1.4.zip
110+
sudo pip3 install opencv-contrib-python==3.4.6.27 opencv-contrib-python-headless==3.4.6.27
111+
sudo pip3 install --no-binary :all: https://github.com/FutureSharks/rpi-security/archive/1.5.zip
104112
```
105113

106114
Reload systemd configuration and enable the service:
@@ -185,16 +193,17 @@ phy#1
185193
Interface mon0
186194
ifindex 4
187195
wdev 0x100000002
188-
addr 00:0f:60:08:9c:01
196+
addr 00:0e:8e:58:d6:af
189197
type monitor
190-
txpower 20.00 dBm
198+
txpower 26.00 dBm
191199
Interface wlan1
192200
ifindex 3
193201
wdev 0x100000001
194-
addr 00:0f:60:08:9c:01
202+
addr 00:0e:8e:58:d6:af
203+
ssid Connecting...
195204
type managed
196-
channel 1 (2412 MHz), width: 20 MHz, center1: 2412 MHz
197-
txpower 20.00 dBm
205+
channel 124 (5620 MHz), width: 40 MHz, center1: 5630 MHz
206+
txpower 26.00 dBm
198207
phy#0
199208
Interface wlan0
200209
ifindex 2

rpisec/rpis_camera.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
import cv2
1010
from picamera.array import PiMotionAnalysis
1111
from picamera import PiCamera
12+
from picamera import PiCameraRuntimeError
1213
import numpy as np
1314
from PIL import Image
1415
from threading import Lock, Event
1516
from queue import Queue
1617
from .exit_clean import exit_error
1718
from datetime import datetime
19+
from fractions import Fraction
1820

1921

2022
logger = logging.getLogger()
@@ -41,12 +43,13 @@ def __init__(self, photo_size, gif_size, motion_size, camera_vflip,
4143
self.camera_save_path = '/var/tmp'
4244
self.camera_capture_length = camera_capture_length
4345
self.camera_mode = camera_mode
46+
self.motion_detection_running = False
47+
self.too_dark_message_printed = False
4448

4549
try:
4650
self.camera = PiCamera()
4751
self.camera.vflip = self.camera_vflip
4852
self.camera.hflip = self.camera_hflip
49-
self.camera.awb_mode = 'off'
5053
except Exception as e:
5154
exit_error('Camera module failed to intialise with error {0}'.format(repr(e)))
5255

@@ -57,10 +60,10 @@ def take_photo(self, filename_extra_suffix=''):
5760
timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S")
5861
photo = '{0}/rpi-security-{1}{2}.jpeg'.format(self.camera_save_path, timestamp, filename_extra_suffix)
5962
try:
60-
self.camera.resolution = self.photo_size
6163
with self.lock:
6264
while self.camera.recording:
6365
time.sleep(0.1)
66+
time.sleep(1)
6467
self.camera.resolution = self.photo_size
6568
self.camera.capture(photo, use_video_port=False)
6669
except PiCameraRuntimeError as e:
@@ -79,11 +82,11 @@ def take_gif(self):
7982
temp_jpeg_path = '{0}/rpi-security-{1}-gif-part'.format(self.temp_directory, timestamp)
8083
jpeg_files = ['{0}-{1}.jpg'.format(temp_jpeg_path, i) for i in range(self.camera_capture_length*3)]
8184
try:
82-
self.camera.resolution = self.gif_size
8385
for jpeg in jpeg_files:
8486
with self.lock:
8587
while self.camera.recording:
8688
time.sleep(0.1)
89+
time.sleep(1)
8790
self.camera.resolution = self.gif_size
8891
self.camera.capture(jpeg)
8992
im=Image.open(jpeg_files[0])
@@ -112,10 +115,12 @@ def trigger_camera(self):
112115

113116
def start_motion_detection(self, rpis):
114117
past_frame = None
115-
logger.debug("Starting motion detection")
116-
self.camera.resolution = self.motion_size
117118
while not self.lock.locked() and rpis.state.current == 'armed':
119+
if not self.motion_detection_running:
120+
logger.debug("Starting motion detection")
121+
self.motion_detection_running = True
118122
stream = io.BytesIO()
123+
self.camera.resolution = self.motion_size
119124
self.camera.capture(stream, format='jpeg', use_video_port=False)
120125
data = np.fromstring(stream.getvalue(), dtype=np.uint8)
121126
frame = cv2.imdecode(data, 1)
@@ -126,7 +131,7 @@ def start_motion_detection(self, rpis):
126131
else:
127132
logger.error("No more frame")
128133
rpis.state.check()
129-
time.sleep(0.3)
134+
time.sleep(0.2)
130135
else:
131136
self.stop_motion_detection()
132137

@@ -135,7 +140,6 @@ def handle_new_frame(self, frame, past_frame):
135140
r = 500 / float(w)
136141
dim = (500, int(h * r))
137142
frame = cv2.resize(frame, dim, cv2.INTER_AREA) # We resize the frame
138-
139143
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # We apply a black & white filter
140144
gray = cv2.GaussianBlur(gray, (21, 21), 0) # Then we blur the picture
141145

@@ -151,10 +155,19 @@ def handle_new_frame(self, frame, past_frame):
151155
logger.error('Past frame and current frame do not have the same sizes {0} {1} {2} {3}'.format(h_past_frame, w_past_frame, h_current_frame, w_current_frame))
152156
return
153157

158+
# Detect when too dark to reduce false alarms
159+
if self.camera.digital_gain == Fraction(187/128) and self.camera.analog_gain == Fraction(8):
160+
if not self.too_dark_message_printed:
161+
logger.info("Too dark to run motion detection")
162+
self.too_dark_message_printed = True
163+
return None
164+
else:
165+
self.too_dark_message_printed = False
166+
154167
# compute the absolute difference between the current frame and first frame
155-
frame_detla = cv2.absdiff(past_frame, gray)
168+
frame_delta = cv2.absdiff(past_frame, gray)
156169
# then apply a threshold to remove camera motion and other false positives (like light changes)
157-
thresh = cv2.threshold(frame_detla, 50, 255, cv2.THRESH_BINARY)[1]
170+
thresh = cv2.threshold(frame_delta, 50, 255, cv2.THRESH_BINARY)[1]
158171

159172
# dilate the thresholded image to fill in holes, then find contours on thresholded image
160173
thresh = cv2.dilate(thresh, None, iterations=2)
@@ -165,19 +178,20 @@ def handle_new_frame(self, frame, past_frame):
165178
for c in cnts:
166179
# if the contour is too small, ignore it
167180
countour_area = cv2.contourArea(c)
181+
168182
if countour_area < self.motion_detection_threshold:
169183
continue
170184

171-
logger.debug("Motion detected! Motion level is {0} (threshold is {1})".format(countour_area, self.motion_detection_threshold))
185+
logger.info("Motion detected! Motion level is {0}, threshold is {1}".format(countour_area, self.motion_detection_threshold))
172186
# Motion detected because there is a contour that is larger than the specified self.motion_detection_threshold
173187
# compute the bounding box for the contour, draw it on the frame,
174188
(x, y, w, h) = cv2.boundingRect(c)
175189
cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
176-
self.handle_motion_detected(frame, gray, frame_detla, thresh)
190+
self.handle_motion_detected(frame)
177191

178192
return None
179193

180-
def handle_motion_detected(self, frame, gray, frame_detla, thresh):
194+
def handle_motion_detected(self, frame):
181195
timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S")
182196
bounding_box_path = '{0}/rpi-security-{1}-box.jpeg'.format(self.camera_save_path, timestamp)
183197
cv2.imwrite(bounding_box_path, frame)
@@ -187,10 +201,12 @@ def handle_motion_detected(self, frame, gray, frame_detla, thresh):
187201

188202
def stop_motion_detection(self):
189203
try:
204+
if self.motion_detection_running:
205+
logger.debug("Stopping motion detection")
206+
self.motion_detection_running = False
190207
if not self.camera.recording:
191208
return
192209
else:
193-
logger.debug("Stopping motion detection")
194210
self.camera.stop_recording()
195211
except Exception as e:
196212
logger.error('Error in stop_motion_detection: {0}'.format(repr(e)))

rpisec/rpis_security.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
import yaml
77
import logging
88

9-
logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
9+
logging.getLogger("kamene.runtime").setLevel(logging.ERROR)
1010

1111
from configparser import SafeConfigParser
1212
from netaddr import IPNetwork
1313
from netifaces import ifaddresses
14-
from scapy.all import srp, Ether, ARP
14+
from kamene.all import srp, Ether, ARP
1515
from telegram import Bot as TelegramBot
1616
from .exit_clean import exit_error
1717
from .rpis_state import RpisState
@@ -63,7 +63,7 @@ def _read_data_file(self):
6363
result = None
6464
try:
6565
with open(self.data_file, 'r') as stream:
66-
result = yaml.load(stream) or {}
66+
result = yaml.load(stream, Loader=yaml.FullLoader)
6767
except Exception as e:
6868
logger.error('Failed to read data file {0}: {1}'.format(self.data_file, repr(e)))
6969
else:

rpisec/threads/capture_packets.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,21 @@
22

33
import _thread
44
import logging
5-
from scapy.all import sniff
6-
from scapy.all import conf as scapy_conf
7-
scapy_conf.promisc=0
8-
scapy_conf.sniff_promisc=0
5+
from kamene.all import sniff
6+
from kamene.all import conf as kamene_conf
7+
kamene_conf.promisc=0
8+
kamene_conf.sniff_promisc=0
99

1010

1111
logger = logging.getLogger()
1212

1313

1414
def capture_packets(rpis):
1515
"""
16-
This function uses scapy to sniff packets for our MAC addresses and updates
16+
This function uses kamene to sniff packets for our MAC addresses and updates
1717
the alarm state when packets are detected.
1818
"""
19-
logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
19+
logging.getLogger("kamene.runtime").setLevel(logging.ERROR)
2020
def update_time(packet):
2121
packet_mac = set(rpis.mac_addresses) & set([packet[0].addr2, packet[0].addr3])
2222
packet_mac_str = list(packet_mac)[0]
@@ -36,5 +36,5 @@ def calculate_filter(mac_addresses):
3636
try:
3737
sniff(iface=rpis.network_interface, store=0, prn=update_time, filter=calculate_filter(rpis.mac_addresses))
3838
except Exception as e:
39-
logger.error('scapy failed to sniff packets with error {0}'.format(repr(e)))
39+
logger.error('kamene failed to sniff packets with error {0}'.format(repr(e)))
4040
_thread.interrupt_main()

rpisec/threads/telegram_bot.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import logging
44
import os
5-
from telegram.ext import Updater, CommandHandler, RegexHandler
5+
from telegram.ext import Updater, CommandHandler, Filters, MessageHandler
66
import _thread
77

88

@@ -16,7 +16,7 @@ def telegram_bot(rpis, camera):
1616
"""
1717
This function runs the telegram bot that responds to commands like /enable, /disable or /status.
1818
"""
19-
def save_chat_id(bot, update):
19+
def save_chat_id(update, context):
2020
if 'telegram_chat_ids' not in rpis.saved_data or rpis.saved_data['telegram_chat_ids'] is None:
2121
rpis.save_telegram_chat_id(update.message.chat_id)
2222
logger.debug('Set Telegram chat_id {0}'.format(update.message.chat_id))
@@ -26,7 +26,7 @@ def save_chat_id(bot, update):
2626
rpis.save_telegram_chat_id(update.message.chat_id)
2727
logger.debug('Set Telegram chat_id {0}'.format(update.message.chat_id))
2828

29-
def debug(bot, update):
29+
def debug(update, context):
3030
logger.debug('Received Telegram bot message: {0}'.format(update.message.text))
3131

3232
def check_chat_id(update):
@@ -36,44 +36,44 @@ def check_chat_id(update):
3636
else:
3737
return True
3838

39-
def help(bot, update):
39+
def help(update, context):
4040
if check_chat_id(update):
41-
bot.sendMessage(update.message.chat_id, parse_mode='Markdown', text='/status: Request status\n/disable: Disable alarm\n/enable: Enable alarm\n/photo: Take a photo\n/gif: Take a gif\n/reboot: reboot\n', timeout=10)
41+
update.message.reply_text(parse_mode='Markdown', text='/status: Request status\n/disable: Disable alarm\n/enable: Enable alarm\n/photo: Take a photo\n/gif: Take a gif\n/reboot: reboot\n', timeout=10)
4242

43-
def status(bot, update):
43+
def status(update, context):
4444
if check_chat_id(update):
45-
bot.sendMessage(update.message.chat_id, parse_mode='Markdown', text=rpis.state.generate_status_text(), timeout=10)
45+
update.message.reply_text(parse_mode='Markdown', text=rpis.state.generate_status_text(), timeout=10)
4646

47-
def disable(bot, update):
47+
def disable(update, context):
4848
if check_chat_id(update):
4949
rpis.state.update_state('disabled')
5050

51-
def enable(bot, update):
51+
def enable(update, context):
5252
if check_chat_id(update):
5353
rpis.state.update_state('disarmed')
5454

55-
def photo(bot, update):
55+
def photo(update, context):
5656
if check_chat_id(update):
5757
photo = camera.take_photo()
5858
rpis.telegram_send_file(photo)
5959

60-
def gif(bot, update):
60+
def gif(update, context):
6161
if check_chat_id(update):
6262
gif = camera.take_gif()
6363
rpis.telegram_send_file(gif)
6464

65-
def reboot(bot, update):
65+
def reboot(update, context):
6666
logger.info('Rebooting after receiving reboot command')
6767
os.system('reboot')
6868

69-
def error_callback(bot, update, error):
70-
logger.error('Update "{0}" caused error "{1}"'.format(update, error))
69+
def error_callback(update, context):
70+
logger.error('Update "{0}" caused error "{1}"'.format(update, context.error))
7171

7272
try:
73-
updater = Updater(rpis.telegram_bot_token)
73+
updater = Updater(rpis.telegram_bot_token, use_context=True)
7474
dp = updater.dispatcher
75-
dp.add_handler(RegexHandler('.*', save_chat_id), group=1)
76-
dp.add_handler(RegexHandler('.*', debug), group=2)
75+
dp.add_handler(MessageHandler(Filters.regex('.*'), save_chat_id), group=1)
76+
dp.add_handler(MessageHandler(Filters.regex('.*'), debug), group=2)
7777
dp.add_handler(CommandHandler("help", help), group=3)
7878
dp.add_handler(CommandHandler("status", status), group=3)
7979
dp.add_handler(CommandHandler("disable", disable), group=3)

setup.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
setup(
44
name = 'rpi-security',
5-
version = '1.4',
5+
version = '1.5',
66
author = 'Max Williams',
77
author_email = '[email protected]',
88
url = 'https://github.com/FutureSharks/rpi-security',
@@ -30,10 +30,10 @@
3030
'netaddr',
3131
'netifaces',
3232
'pyyaml',
33-
'scapy==2.4.3',
33+
'kamene==0.32',
3434
'Pillow==6.2.1',
35-
'opencv-contrib-python',
36-
'opencv-contrib-python-headless',
35+
'opencv-contrib-python==3.4.6.27',
36+
'opencv-contrib-python-headless==3.4.6.27',
3737
],
3838
classifiers = [
3939
'Environment :: Console',

0 commit comments

Comments
 (0)