'quality', 'rotate', 'saturation', 'snapshot_filename', 'snapshot_interval', 'stream_auth_method', 'stream_authentication', 'stream_localhost', 'stream_maxrate',
'stream_motion', 'stream_port', 'stream_quality', 'target_dir', 'text_changes', 'text_double', 'text_left', 'text_right', 'threshold', 'videodevice', 'width',
'webcam_localhost', 'webcam_port', 'webcam_maxrate', 'webcam_quality', 'webcam_motion', 'ffmpeg_cap_new', 'output_normal', 'output_motion', 'jpeg_filename', 'output_all',
- 'gap', 'locate', 'netcam_url', 'netcam_userpass', 'netcam_http', 'netcam_tolerant_check', 'netcam_keepalive', 'rtsp_uses_tcp'
+ 'gap', 'locate', 'netcam_url', 'netcam_userpass', 'netcam_http', 'netcam_tolerant_check', 'netcam_keepalive', 'rtsp_uses_tcp',
+ 'mask_file', 'smart_mask_speed'
])
'pre_capture': int(ui['pre_capture']),
'post_capture': int(ui['post_capture']),
'minimum_motion_frames': int(ui['minimum_motion_frames']),
+ 'smart_mask_speed': 0,
+ 'mask_file': '',
# working schedule
'@working_schedule': '',
max_val = min(max_val, 9999999)
data['ffmpeg_bps'] = int(int(ui['movie_quality']) * max_val / 100)
-
+
+ # motion detection
+ if ui['mask']:
+ if ui['mask_type'] == 'smart':
+ data['smart_mask_speed'] = 10 - int(ui['smart_mask_slugginess'])
+
+ elif ui['mask_type'] == 'editable':
+ data['mask_file'] = utils.build_editable_mask_file(ui['editable_mask'])
+
+ else:
+ data['mask_file'] = ui['mask_file']
+
# working schedule
if ui['working_schedule']:
data['@working_schedule'] = (
'pre_capture': int(data['pre_capture']),
'post_capture': int(data['post_capture']),
'minimum_motion_frames': int(data['minimum_motion_frames']),
+ 'mask': False,
+ 'mask_type': 'smart',
+ 'smart_mask_slugginess': 5,
+ 'mask_file': '',
+ 'editable_mask': '',
# motion notifications
'email_notifications_enabled': False,
max_val = min(max_val, 9999999)
ui['movie_quality'] = min(100, int(round(ffmpeg_bps * 100.0 / max_val)))
+
+ # motion detection
+ if data['smart_mask_speed'] or data['mask_file']:
+ ui['mask'] = True
+ if data['smart_mask_speed']:
+ ui['mask_type'] = 'smart'
+ ui['smart_mask_slugginess'] = 10 - int(data['smart_mask_speed'])
+
+ else:
+ editable_mask = utils.parse_editable_mask_file(data['mask_file'])
+ if editable_mask:
+ ui['mask_type'] = 'editable'
+ ui['editable_mask'] = editable_mask
+
+ else:
+ ui['mask_type'] = 'file'
+ ui['mask_file'] = data['mask_file']
# working schedule
working_schedule = data['@working_schedule']
data.setdefault('noise_level', 32)
data.setdefault('lightswitch', 0)
data.setdefault('minimum_motion_frames', 20)
+ data.setdefault('smart_mask_speed', 0)
+ data.setdefault('mask_file', '')
data.setdefault('pre_capture', 1)
data.setdefault('post_capture', 1)
'pre_capture': $('#preCaptureEntry').val(),
'post_capture': $('#postCaptureEntry').val(),
'minimum_motion_frames': $('#minimumMotionFramesEntry').val(),
+ 'mask': $('#maskSwitch')[0].checked,
+ 'mask_type': $('#maskTypeSelect').val(),
+ 'smart_mask_slugginess': $('#smartMaskSlugginessSlider').val(),
+ 'mask_file': $('#maskFileEntry').val(),
/* motion notifications */
'email_notifications_enabled': $('#emailNotificationsEnabledSwitch')[0].checked,
$('#preCaptureEntry').val(dict['pre_capture']); markHideIfNull('pre_capture', 'preCaptureEntry');
$('#postCaptureEntry').val(dict['post_capture']); markHideIfNull('post_capture', 'postCaptureEntry');
$('#minimumMotionFramesEntry').val(dict['minimum_motion_frames']); markHideIfNull('minimum_motion_frames', 'minimumMotionFramesEntry');
+ $('#maskSwitch')[0].checked = dict['mask']; markHideIfNull('mask', 'maskSwitch');
+ $('#maskTypeSelect').val(dict['mask_type']); markHideIfNull('mask_type', 'maskTypeSelect');
+ $('#smartMaskSlugginessSlider').val(dict['smart_mask_slugginess']); markHideIfNull('smart_mask_slugginess', 'smartMaskSlugginessSlider');
+ $('#maskFileEntry').val(dict['mask_file']); markHideIfNull('mask_file', 'maskFileEntry');
/* motion notifications */
$('#emailNotificationsEnabledSwitch')[0].checked = dict['email_notifications_enabled']; markHideIfNull('email_notifications_enabled', 'emailNotificationsEnabledSwitch');
<tr class="settings-item" min="1" max="4" snap="1" ticksnum="4" decimals="0" id="fitFramesVerticallyRow">
<td class="settings-item-label"><span class="settings-item-label">Fit Frames Vertically</span></td>
<td class="settings-item-value"><input type="checkbox" class="styled prefs" id="fitFramesVerticallySwitch"></td>
- <td><span class="help-mark" title="controls whether frame sizes can be reduced to vertically fit the window or not">?</span></td>
+ <td><span class="help-mark" title="controls whether frame sizes can be reduced to fit the window vertically">?</span></td>
</tr>
<tr class="settings-item" min="0" max="100" snap="2" ticksnum="6" decimals="0">
<td class="settings-item-label"><span class="settings-item-label">Frame Rate Dimmer</span></td>
<td class="settings-item-value"><input type="text" class="number styled motion-detection camera-config" id="minimumMotionFramesEntry"><span class="settings-item-unit">frames</span></td>
<td><span class="help-mark" title="sets the minimum number of successive motion frames required to start a motion event">?</span></td>
</tr>
+ <tr class="settings-item advanced-setting">
+ <td colspan="100"><div class="settings-item-separator"></div></td>
+ </tr>
+ <tr class="settings-item advanced-setting">
+ <td class="settings-item-label"><span class="settings-item-label">Mask</span></td>
+ <td class="settings-item-value"><input type="checkbox" class="styled motion-detection camera-config" id="maskSwitch"></td>
+ <td><span class="help-mark" title="enables image masking for a more selective and precise motion detection">?</span></td>
+ </tr>
+ <tr class="settings-item advanced-setting" depends="mask">
+ <td class="settings-item-label"><span class="settings-item-label">Mask Type</span></td>
+ <td class="settings-item-value">
+ <select class="styled motion-detection camera-config" id="maskTypeSelect">
+ <option value="smart">Smart</option>
+ <option value="editable">Editable</option>
+ <option value="file">File</option>
+ </select>
+ </td>
+ <td><span class="help-mark" title="the smart option automatically detects regions with regular motion and builds an internal mask, the editable mask allows you to manually build it and the file option allows you to specify a mask file (not recommended)">?</span></td>
+ </tr>
+ <tr class="settings-item advanced-setting" min="1" max="10" snap="1" ticksnum="10" decimals="0" unit="" depends="mask maskType=smart">
+ <td class="settings-item-label"><span class="settings-item-label">Smart Mask Slugginess</span></td>
+ <td class="settings-item-value"><input type="text" class="range styled motion-detection camera-config" id="smartMaskSlugginessSlider"></td>
+ <td><span class="help-mark" title="lower values result in a longer-lasting smart mask with a slower building process">?</span></td>
+ </tr>
+ <tr class="settings-item advanced-setting" required="true" depends="mask maskType=file" strip="true">
+ <td class="settings-item-label"><span class="settings-item-label">Mask File</span></td>
+ <td class="settings-item-value"><input type="text" class="styled motion-detection camera-config" id="maskFileEntry"></td>
+ <td><span class="help-mark" title="full path to the PGM mask image file">?</span></td>
+ </tr>
{% for config in camera_sections.get('motion-detection', {}).get('configs', []) %}
{{config_item(config)}}
{% endfor %}
import os
import re
import socket
+import struct
import time
import urllib
import urlparse
+from PIL import Image, ImageDraw
+
from tornado.httpclient import AsyncHTTPClient, HTTPRequest
from tornado.iostream import IOStream
from tornado.ioloop import IOLoop
state['nonce_count'] = nonce_count
return 'Digest %s' % (base)
+
+
+def build_editable_mask_file(editable_mask):
+ width = editable_mask[0]
+ height = editable_mask[1]
+ nx = editable_mask[2]
+ ny = editable_mask[3]
+ lines = editable_mask[4:]
+
+ data = struct.pack('<HHBB', width, height, nx, ny)
+ for line in lines:
+ data += struct.pack('<I', line)
+
+ name = base64.b64encode(data, '-_')
+
+ # draw the actual mask image content
+ im = Image.new('L', (width, height), 255) # all white
+ dr = ImageDraw.Draw(im)
+
+ rw = width / nx
+ rh = height / ny
+
+ for y in xrange(ny):
+ line = lines[y]
+ for x in xrange(nx):
+ if line & (31 - x):
+ print line & (31 - x)
+ dr.rectangle((x * rw, y * rh, (x + 1) * rw, (y + 1) * rh), fill=0)
+
+ file_name = os.path.join(settings.CONF_PATH, name) + '.pgm'
+ im.save(file_name, 'ppm')
+
+ return name
+
+
+def parse_editable_mask_file(file_name):
+ name = os.path.splitext(os.path.basename(file_name))[0]
+ try:
+ data = base64.b64decode(name, '-_')
+ width, height, nx, ny = struct.unpack('<HHBB', data[:6])
+ fmt = '<' + 'I' * ny
+ lines = struct.unpack(fmt, data[6:])
+
+ return [width, height, nx, ny] + list(lines)
+
+ except Exception:
+ return None