From 3220546071e71111b1e9947be24fcc2af659be19 Mon Sep 17 00:00:00 2001 From: Calin Crisan Date: Wed, 14 Sep 2016 22:17:31 +0300 Subject: [PATCH] added a mask grid overlaid on top of camera frames --- motioneye/config.py | 10 ++-- motioneye/handlers.py | 5 +- motioneye/static/css/main.css | 41 +++++++++++++ motioneye/static/js/main.js | 91 +++++++++++++++++++++++++++- motioneye/templates/main.html | 1 + motioneye/utils.py | 108 +++++++++++++++++++--------------- 6 files changed, 199 insertions(+), 57 deletions(-) diff --git a/motioneye/config.py b/motioneye/config.py index 11ee4a5..413101c 100644 --- a/motioneye/config.py +++ b/motioneye/config.py @@ -869,12 +869,12 @@ def motion_camera_ui_to_dict(ui, old_config=None): data['ffmpeg_variable_bitrate'] = int(vbr) # motion detection - if ui['mask']: + if ui['mask'] and data.get('width') and data.get('height'): 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']) + data['mask_file'] = utils.build_editable_mask_file(old_config['@id'], data['width'], data['height'], ui['mask_lines']) # working schedule if ui['working_schedule']: @@ -1058,7 +1058,7 @@ def motion_camera_dict_to_ui(data): 'mask': False, 'mask_type': 'smart', 'smart_mask_slugginess': 5, - 'editable_mask': '', + 'mask_lines': [], # motion notifications 'email_notifications_enabled': False, @@ -1245,10 +1245,10 @@ def motion_camera_dict_to_ui(data): ui['movie_quality'] = int(q) # mask - if data['mask_file']: + if data['mask_file'] and data.get('width') and data.get('height'): ui['mask'] = True ui['mask_type'] = 'editable' - ui['editable_mask'] = utils.parse_editable_mask_file(data['mask_file']) + ui['mask_lines'] = utils.parse_editable_mask_file(data['@id'], data['width'], data['height']) elif data['smart_mask_speed']: ui['mask'] = True diff --git a/motioneye/handlers.py b/motioneye/handlers.py index 4486861..85ea8d0 100644 --- a/motioneye/handlers.py +++ b/motioneye/handlers.py @@ -207,8 +207,9 @@ class MainHandler(BaseHandler): admin_username=config.get_main().get('@admin_username'), has_streaming_auth=motionctl.has_streaming_auth(), has_new_movie_format_support=motionctl.has_new_movie_format_support(), - has_motion=bool(motionctl.find_motion())) - + has_motion=bool(motionctl.find_motion()), + mask_width=utils.MASK_WIDTH) + class ConfigHandler(BaseHandler): @asynchronous diff --git a/motioneye/static/css/main.css b/motioneye/static/css/main.css index 191f167..1c7e6f3 100644 --- a/motioneye/static/css/main.css +++ b/motioneye/static/css/main.css @@ -903,6 +903,7 @@ div.camera-overlay-bottom { transition: background-color 0.1s ease; left: 0px; width: 100%; + z-index: 1; } div.camera-frame:HOVER div.camera-overlay-top, @@ -922,6 +923,46 @@ div.camera-overlay-top { white-space: nowrap; } +div.camera-overlay-mask { + background: #888; + opacity: 0; + position: absolute; + top: 0px; + right: 0px; + bottom: 0px; + left: 0px; + z-index: 0; +} + +div.camera-overlay.mask-edit > div.camera-overlay-top, +div.camera-overlay.mask-edit > div.camera-overlay-bottom { + display: none; +} + +div.camera-overlay.mask-edit > div.camera-overlay-mask { + opacity: 0.3; +} + +div.mask-element { + position: absolute; + box-sizing: border-box; + border: 1px solid #aaa; + border-width: 0px 1px 1px 0px; + background: transparent; +} + +div.mask-element.last-row { + border-right-width: 0px; +} + +div.mask-element.last-line { + border-bottom-width: 0px; +} + +div.mask-element.on { + background: #444; +} + div.camera-name { display: inline-block; font-size: 1.2em; diff --git a/motioneye/static/js/main.js b/motioneye/static/js/main.js index 8a7f79d..095f37d 100644 --- a/motioneye/static/js/main.js +++ b/motioneye/static/js/main.js @@ -978,6 +978,92 @@ function hideCameraOverlay() { }, 300); } +function enableMaskEdit(cameraId, width, height) { + var cameraFrame = getCameraFrame(cameraId); + var overlayDiv = cameraFrame.find('div.camera-overlay'); + var maskDiv = cameraFrame.find('div.camera-overlay-mask'); + + overlayDiv.addClass('mask-edit'); + + var nx = maskWidth; /* number of rectangles */ + var rx, rw; + if (width % nx) { + nx--; + rx = width % nx; /* remainder */ + } + else { + rx = 0; + } + + rw = parseInt(width / nx); /* rectangle width */ + + var maskHeight; + var ny = maskHeight = parseInt(height * maskWidth / width); /* number of rectangles */ + var ry, rh; + if (height % ny) { + ny--; + ry = height % ny; /* remainder */ + } + else { + ry = 0; + } + + rh = parseInt(height / ny); /* rectangle height */ + + function makeMaskElement(x, y, px, py, pw, ph) { + px = px * 100 / width; + py = py * 100 / height; + pw = pw * 100 / width; + ph = ph * 100 / height; + + var el = $('
'); + el.css('left', px + '%'); + el.css('top', py + '%'); + el.css('width', pw + '%'); + el.css('height', ph + '%'); + if (x == maskWidth - 1) { + el.addClass('last-row'); + } + if (y == maskHeight - 1) { + el.addClass('last-line'); + } + maskDiv.append(el); + } + + /* make sure the mask is empty */ + maskDiv.html(''); + + var x, y; + for (y = 0; y < ny; y++) { + for (x = 0; x < nx; x++) { + makeMaskElement(x, y, x * rw, y * rh, rw, rh); + } + + if (rx) { + makeMaskElement(x, y, nx * rw, y * rh, rx, rh); + } + } + + if (ry) { + for (x = 0; x < nx; x++) { + makeMaskElement(x, y, x * rw, ny * rh, rw, ry); + } + + if (rx) { + makeMaskElement(x, y, nx * rw, ny * rh, rx, ry); + } + } +} + +function disableMaskEdit(cameraId) { + var cameraFrame = getCameraFrame(cameraId); + var overlayDiv = cameraFrame.find('div.camera-overlay'); + var maskDiv = cameraFrame.find('div.camera-overlay-mask'); + + overlayDiv.removeClass('mask-edit'); + maskDiv.html(''); +} + /* settings */ @@ -1557,7 +1643,7 @@ function cameraUi2Dict() { 'mask': $('#maskSwitch')[0].checked, 'mask_type': $('#maskTypeSelect').val(), 'smart_mask_slugginess': $('#smartMaskSlugginessSlider').val(), - 'mask_file': $('#maskFileEntry').val(), + 'mask_lines': [], // TODO generate mask lines /* motion notifications */ 'email_notifications_enabled': $('#emailNotificationsEnabledSwitch')[0].checked, @@ -1901,7 +1987,7 @@ function dict2CameraUi(dict) { $('#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'); + //TODO use dict['mask_lines']; markHideIfNull('mask_file', 'maskFileEntry'); /* motion notifications */ $('#emailNotificationsEnabledSwitch')[0].checked = dict['email_notifications_enabled']; markHideIfNull('email_notifications_enabled', 'emailNotificationsEnabledSwitch'); @@ -3973,6 +4059,7 @@ function addCameraFrameUi(cameraConfig) { '
' + '' + '' + + '
' + '
' + '
' + '' + diff --git a/motioneye/templates/main.html b/motioneye/templates/main.html index 9b579e3..c339ab4 100644 --- a/motioneye/templates/main.html +++ b/motioneye/templates/main.html @@ -69,6 +69,7 @@ var frame = {% if frame %}true{% else %}false{% endif %}; var hasLocalCamSupport = {% if has_motion %}true{% else %}false{% endif %}; var hasNetCamSupport = {% if has_motion %}true{% else %}false{% endif %}; + {% if mask_width %}var maskWidth = {{mask_width}};{% endif %} {% endblock %} diff --git a/motioneye/utils.py b/motioneye/utils.py index 84d75eb..6aafcdf 100644 --- a/motioneye/utils.py +++ b/motioneye/utils.py @@ -47,7 +47,7 @@ except: _SIGNATURE_REGEX = re.compile('[^a-zA-Z0-9/?_.=&{}\[\]":, _-]') _SPECIAL_COOKIE_NAMES = {'expires', 'domain', 'path', 'secure', 'httponly'} -_MASK_WIDTH = 32 +MASK_WIDTH = 32 DEV_NULL = open('/dev/null', 'w') @@ -788,18 +788,26 @@ def urlopen(*args, **kwargs): def build_editable_mask_file(camera_id, width, height, mask_lines): # horizontal rectangles - nx = _MASK_WIDTH # number of rectangles - rw = width / nx # rectangle width - rx = width % nx # remainder - if rx: + nx = MASK_WIDTH # number of rectangles + if width % nx: nx -= 1 + rx = width % nx # remainder + + else: + rx = 0 + + rw = width / nx # rectangle width # vertical rectangles - ny = height * _MASK_WIDTH / width # number of rectangles - rh = height / ny # rectangle height - ry = height % ny # remainder - if ry: + ny = height * MASK_WIDTH / width # number of rectangles + if height % ny: ny -= 1 + ry = height % ny # remainder + + else: + ry = 0 + + rh = height / ny # rectangle height # draw the actual mask image content im = Image.new('L', (width, height), 255) # all white @@ -808,20 +816,20 @@ def build_editable_mask_file(camera_id, width, height, mask_lines): for y in xrange(ny): line = mask_lines[y] for x in xrange(nx): - if line & (_MASK_WIDTH - 1 - x): - dr.rectangle((x * rw, y * rh, (x + 1) * rw, (y + 1) * rh), fill=0) + if line & (1 << (MASK_WIDTH - 1 - x)): + dr.rectangle((x * rw, y * rh, (x + 1) * rw - 1, (y + 1) * rh - 1), fill=0) - if rx and line & (nx + 1): - dr.rectangle((nx * rw, y * rh, nx * rw + rx, (y + 1) * rh), fill=0) + if rx and line & 1: + dr.rectangle((nx * rw, y * rh, nx * rw + rx - 1, (y + 1) * rh - 1), fill=0) if ry: line = mask_lines[ny] for x in xrange(nx): - if line & (_MASK_WIDTH - 1 - x): - dr.rectangle((x * rw, ny * rh, (x + 1) * rw, ny * rh + ry), fill=0) + if line & (1 << (MASK_WIDTH - 1 - x)): + dr.rectangle((x * rw, ny * rh, (x + 1) * rw - 1, ny * rh + ry - 1), fill=0) - if rx and line & (nx + 1): - dr.rectangle((nx * rw, ny * rh, nx * rw + rx, ny * rh + ry), fill=0) + if rx and line & 1: + dr.rectangle((nx * rw, ny * rh, nx * rw + rx - 1, ny * rh + ry - 1), fill=0) file_name = os.path.join(settings.CONF_PATH, 'mask_%s.pgm' % camera_id) im.save(file_name, 'ppm') @@ -832,18 +840,26 @@ def parse_editable_mask_file(camera_id, width, height): # as it might be different from that of the associated mask # horizontal rectangles - nx = _MASK_WIDTH # number of rectangles - rw = width / nx # rectangle width - rx = width % nx # remainder - if rx: + nx = MASK_WIDTH # number of rectangles + if width % nx: nx -= 1 + rx = width % nx # remainder + + else: + rx = 0 + + rw = width / nx # rectangle width # vertical rectangles - ny = mask_height = height * _MASK_WIDTH / width # number of rectangles - rh = height / ny # rectangle height - ry = height % ny # remainder - if ry: + ny = mask_height = height * MASK_WIDTH / width # number of rectangles + if height % ny: ny -= 1 + ry = height % ny # remainder + + else: + ry = 0 + + rh = height / ny # rectangle height file_name = os.path.join(settings.CONF_PATH, 'mask_%s.pgm' % camera_id) @@ -855,7 +871,7 @@ def parse_editable_mask_file(camera_id, width, height): logging.error('failed to read mask file %s: %s' % (file_name, e)) # empty mask - return [2 ** _MASK_WIDTH - 1] * mask_height + return [2 ** MASK_WIDTH - 1] * mask_height # resize the image if necessary if im.size != (width, height): @@ -868,48 +884,44 @@ def parse_editable_mask_file(camera_id, width, height): for y in xrange(ny): bits = [] for x in xrange(nx): - px = (x + 0.5) * rw - py = (y + 0.5) * rh - pixel = pixels[py * height + px] - if pixel == 0: - bits.append(not bool(pixel)) + px = int((x + 0.5) * rw) + py = int((y + 0.5) * rh) + pixel = pixels[py * width + px] + bits.append(not bool(pixel)) if rx: - px = nx * rw + rx / 2 - py = (y + 0.5) * rh - pixel = pixels[py * height + px] - if pixel == 0: - bits.append(not bool(pixel)) + px = int(nx * rw + rx / 2) + py = int((y + 0.5) * rh) + pixel = pixels[py * width + px] + bits.append(not bool(pixel)) # build the binary packed mask line line = 0 for i, bit in enumerate(bits): if bit: - line |= 1 << (_MASK_WIDTH - 1 - i) + line |= 1 << (MASK_WIDTH - 1 - i) mask_lines.append(line) if ry: bits = [] for x in xrange(nx): - px = (x + 0.5) * rw - py = ny * rh + ry / 2 - pixel = pixels[py * height + px] - if pixel == 0: - bits.append(not bool(pixel)) + px = int((x + 0.5) * rw) + py = int(ny * rh + ry / 2) + pixel = pixels[py * width + px] + bits.append(not bool(pixel)) if rx: - px = nx * rw + rx / 2 - py = ny * rh + ry / 2 - pixel = pixels[py * height + px] - if pixel == 0: - bits.append(not bool(pixel)) + px = int(nx * rw + rx / 2) + py = int(ny * rh + ry / 2) + pixel = pixels[py * width + px] + bits.append(not bool(pixel)) # build the binary packed mask line line = 0 for i, bit in enumerate(bits): if bit: - line |= 1 << (_MASK_WIDTH - 1 - i) + line |= 1 << (MASK_WIDTH - 1 - i) mask_lines.append(line) -- 2.39.5