From: Calin Crisan Date: Fri, 16 Sep 2016 19:00:49 +0000 (+0300) Subject: more work on the mask editor X-Git-Url: http://www.vanbest.org/gitweb/?a=commitdiff_plain;h=21d228055543ac11362ed66b20171c6a4620d04b;p=motioneye-debian more work on the mask editor --- diff --git a/motioneye/config.py b/motioneye/config.py index 413101c..f303736 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'] and data.get('width') and data.get('height'): + 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(old_config['@id'], data['width'], data['height'], ui['mask_lines']) + data['mask_file'] = utils.build_editable_mask_file(old_config['@id'], data.get('width'), data.get('height'), ui['mask_lines']) # working schedule if ui['working_schedule']: @@ -1245,10 +1245,10 @@ def motion_camera_dict_to_ui(data): ui['movie_quality'] = int(q) # mask - if data['mask_file'] and data.get('width') and data.get('height'): + if data['mask_file']: ui['mask'] = True ui['mask_type'] = 'editable' - ui['mask_lines'] = utils.parse_editable_mask_file(data['@id'], data['width'], data['height']) + ui['mask_lines'] = utils.parse_editable_mask_file(data['@id'], data.get('width'), data.get('height')) elif data['smart_mask_speed']: ui['mask'] = True diff --git a/motioneye/handlers.py b/motioneye/handlers.py index 85ea8d0..40d51d4 100644 --- a/motioneye/handlers.py +++ b/motioneye/handlers.py @@ -208,7 +208,8 @@ class MainHandler(BaseHandler): has_streaming_auth=motionctl.has_streaming_auth(), has_new_movie_format_support=motionctl.has_new_movie_format_support(), has_motion=bool(motionctl.find_motion()), - mask_width=utils.MASK_WIDTH) + mask_width=utils.MASK_WIDTH, + mask_default_resolution=utils.MASK_DEFAULT_RESOLUTION) class ConfigHandler(BaseHandler): diff --git a/motioneye/static/css/main.css b/motioneye/static/css/main.css index 1c7e6f3..d5621d7 100644 --- a/motioneye/static/css/main.css +++ b/motioneye/static/css/main.css @@ -395,10 +395,18 @@ div.normal-button { div.update-button, div.backup-button, div.restore-button, +div.edit-mask-button, +div.save-mask-button, +div.clear-mask-button, div.test-button { background: #317CAD; } +div.save-mask-button, +div.clear-mask-button { + display: none; +} + div.shut-down-button, div.reboot-button { background: #c0392b; @@ -406,7 +414,11 @@ div.reboot-button { div.update-button:HOVER, div.backup-button:HOVER, -div.restore-button:HOVER { +div.restore-button:HOVER, +div.edit-mask-button:HOVER, +div.save-mask-button:HOVER, +div.clear-mask-button:HOVER, +div.test-button:HOVER { background: #3498db; } @@ -417,7 +429,11 @@ div.reboot-button:HOVER { div.update-button:ACTIVE, div.backup-button:ACTIVE, -div.restore-button:ACTIVE { +div.restore-button:ACTIVE, +div.edit-mask-button:ACTIVE, +div.save-mask-button:ACTIVE, +div.clear-mask-button:ACTIVE, +div.test-button:ACTIVE { background: #317CAD; } @@ -460,6 +476,10 @@ input[type=text].working-schedule.number { width: 90%; } +#editableMaskEntry { + display: none; +} + div.hidden, tr.hidden { display: none !important; @@ -904,6 +924,7 @@ div.camera-overlay-bottom { left: 0px; width: 100%; z-index: 1; + transition: height 0.2s ease; } div.camera-frame:HOVER div.camera-overlay-top, @@ -921,10 +942,11 @@ div.camera-overlay-top { height: 2.5em; line-height: 2.5em; white-space: nowrap; + overflow: hidden; } div.camera-overlay-mask { - background: #888; + background: rgba(255, 255, 255, 0.2); opacity: 0; position: absolute; top: 0px; @@ -932,21 +954,27 @@ div.camera-overlay-mask { bottom: 0px; left: 0px; z-index: 0; + transition: opacity 0.2s ease; +} + +div.camera-overlay.mask-edit > div.camera-overlay-top { + height: 0px; + bottom: auto; } -div.camera-overlay.mask-edit > div.camera-overlay-top, div.camera-overlay.mask-edit > div.camera-overlay-bottom { - display: none; + height: 0px; + top: auto; } div.camera-overlay.mask-edit > div.camera-overlay-mask { - opacity: 0.3; + opacity: 1; } div.mask-element { position: absolute; box-sizing: border-box; - border: 1px solid #aaa; + border: 1px solid rgba(255, 255, 255, 0.2); border-width: 0px 1px 1px 0px; background: transparent; } @@ -960,7 +988,7 @@ div.mask-element.last-line { } div.mask-element.on { - background: #444; + background: rgba(0, 0, 0, 0.7); } div.camera-name { diff --git a/motioneye/static/js/main.js b/motioneye/static/js/main.js index 095f37d..e2e475d 100644 --- a/motioneye/static/js/main.js +++ b/motioneye/static/js/main.js @@ -795,6 +795,7 @@ function initUI() { this._prevSelectedIndex = this.selectedIndex; beginProgress([$(this).val()]); fetchCurrentCameraConfig(endProgress); + disableMaskEdit(); } }); $('input.main-config, select.main-config, textarea.main-config').change(function () { @@ -969,6 +970,8 @@ function showCameraOverlay() { setTimeout(function () { getCameraFrames().find('div.camera-overlay').addClass('visible'); }, 10); + + overlayVisible = true; } function hideCameraOverlay() { @@ -976,6 +979,10 @@ function hideCameraOverlay() { setTimeout(function () { getCameraFrames().find('div.camera-overlay').css('display', 'none'); }, 300); + + overlayVisible = false; + + disableMaskEdit(); } function enableMaskEdit(cameraId, width, height) { @@ -983,6 +990,10 @@ function enableMaskEdit(cameraId, width, height) { var overlayDiv = cameraFrame.find('div.camera-overlay'); var maskDiv = cameraFrame.find('div.camera-overlay-mask'); + if (overlayDiv.hasClass('mask-edit')) { + return; /* already enabled */ + } + overlayDiv.addClass('mask-edit'); var nx = maskWidth; /* number of rectangles */ @@ -1010,6 +1021,14 @@ function enableMaskEdit(cameraId, width, height) { rh = parseInt(height / ny); /* rectangle height */ + var mouseDown = false; + var currentState = false; + + function handleMouseUp() { + mouseDown = false; + $('html').unbind('mouseup', handleMouseUp); + } + function makeMaskElement(x, y, px, py, pw, ph) { px = px * 100 / width; py = py * 100 / height; @@ -1028,10 +1047,30 @@ function enableMaskEdit(cameraId, width, height) { el.addClass('last-line'); } maskDiv.append(el); + + el.mousedown(function () { + mouseDown = true; + el.toggleClass('on'); + currentState = el.hasClass('on'); + $('html').mouseup(handleMouseUp); + }); + + el.mouseenter(function () { + if (!mouseDown) { + return; + } + + el.toggleClass('on', currentState); + }); } - + /* make sure the mask is empty */ maskDiv.html(''); + + /* prevent editor closing by accidental click on mask container */ + maskDiv.click(function () { + return false; + }) var x, y; for (y = 0; y < ny; y++) { @@ -1053,15 +1092,48 @@ function enableMaskEdit(cameraId, width, height) { makeMaskElement(x, y, nx * rw, ny * rh, rx, ry); } } + + var selectedCameraId = $('#cameraSelect').val(); + if (selectedCameraId && (!cameraId || cameraId == selectedCameraId)) { + $('#saveMaskButton, #clearMaskButton').css('display', 'inline-block'); + $('#editMaskButton').css('display', 'none'); + } + + if (!overlayVisible) { + showCameraOverlay(); + } } function disableMaskEdit(cameraId) { + var cameraFrames; + if (cameraId) { + cameraFrames = [getCameraFrame(cameraId)]; + } + else { /* disable mask editor on any camera */ + cameraFrames = getCameraFrames().toArray().map(function (f) {return $(f);}); + } + + cameraFrames.forEach(function (cameraFrame) { + var overlayDiv = cameraFrame.find('div.camera-overlay'); + var maskDiv = cameraFrame.find('div.camera-overlay-mask'); + + overlayDiv.removeClass('mask-edit'); + maskDiv.html(''); + maskDiv.unbind('click'); + }); + + var selectedCameraId = $('#cameraSelect').val(); + if (selectedCameraId && (!cameraId || cameraId == selectedCameraId)) { + $('#editMaskButton').css('display', 'inline-block'); + $('#saveMaskButton, #clearMaskButton').css('display', 'none'); + } +} + +function clearMask(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(''); + + maskDiv.find('div.mask-element').removeClass('on'); } @@ -1643,8 +1715,8 @@ function cameraUi2Dict() { 'mask': $('#maskSwitch')[0].checked, 'mask_type': $('#maskTypeSelect').val(), 'smart_mask_slugginess': $('#smartMaskSlugginessSlider').val(), - 'mask_lines': [], // TODO generate mask lines - + 'mask_lines': $('#maskLinesEntry').val().split(','), + /* motion notifications */ 'email_notifications_enabled': $('#emailNotificationsEnabledSwitch')[0].checked, 'email_notifications_from': $('#emailFromEntry').val(), @@ -1987,8 +2059,8 @@ 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'); - //TODO use dict['mask_lines']; markHideIfNull('mask_file', 'maskFileEntry'); - + $('#maskLinesEntry').val((dict['mask_lines'] or []).join(',')); markHideIfNull('mask_lines', 'maskLinesEntry'); + /* motion notifications */ $('#emailNotificationsEnabledSwitch')[0].checked = dict['email_notifications_enabled']; markHideIfNull('email_notifications_enabled', 'emailNotificationsEnabledSwitch'); $('#emailFromEntry').val(dict['email_notifications_from']); @@ -4154,12 +4226,10 @@ function addCameraFrameUi(cameraConfig) { cameraImg.click(function () { showCameraOverlay(); - overlayVisible = true; }); cameraOverlay.click(function () { hideCameraOverlay(); - overlayVisible = false; }); cameraOverlay.find('div.camera-overlay-top, div.camera-overlay-bottom').click(function () { @@ -4703,6 +4773,43 @@ $(document).ready(function () { $('div#emailTestButton').click(doTestEmail); $('div#networkShareTestButton').click(doTestNetworkShare); + /* mask editor buttons */ + $('div#editMaskButton').click(function () { + var cameraId = $('#cameraSelect').val(); + var resolution = $('#resolutionSelect').val(); + if (!cameraId) { + return; + } + + if (!resolution) { + /* + * TODO motion requires the mask file to be the same size as the + * captured images; however for netcams we have no means to know in + * advance the size of the stream; therefore, for netcams, we impose + * here a standard fixed mask size, which WILL NOT WORK for netcam + * streams of a different resolution + */ + resolution = maskDefaultResolution; + } + + resolution = resolution.split('x'); + var width = resolution[0]; + var height = resolution[1]; + + enableMaskEdit(cameraId, width, height); + }); + $('div#saveMaskButton').click(function () { + disableMaskEdit(); + }); + $('div#clearMaskButton').click(function () { + var cameraId = $('#cameraSelect').val(); + if (!cameraId) { + return; + } + + clearMask(cameraId); + }); + initUI(); beginProgress(); diff --git a/motioneye/templates/main.html b/motioneye/templates/main.html index c339ab4..28a679a 100644 --- a/motioneye/templates/main.html +++ b/motioneye/templates/main.html @@ -69,7 +69,10 @@ 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 %} + {% if mask_width %} + var maskWidth = {{mask_width}}; + var maskDefaultResolution = '{{mask_default_resolution[0]}}x{{mask_default_resolution[1]}}'; + {% endif %} {% endblock %} @@ -836,6 +839,20 @@ ? + + + +
Edit Mask
+
Save Mask
+ + + ? + + + +
Clear Mask
+ ? + {% for config in camera_sections.get('motion-detection', {}).get('configs', []) %} {{config_item(config)}} {% endfor %} diff --git a/motioneye/utils.py b/motioneye/utils.py index 6aafcdf..b14e6e7 100644 --- a/motioneye/utils.py +++ b/motioneye/utils.py @@ -48,6 +48,7 @@ _SIGNATURE_REGEX = re.compile('[^a-zA-Z0-9/?_.=&{}\[\]":, _-]') _SPECIAL_COOKIE_NAMES = {'expires', 'domain', 'path', 'secure', 'httponly'} MASK_WIDTH = 32 +MASK_DEFAULT_RESOLUTION = (640, 480) DEV_NULL = open('/dev/null', 'w') @@ -787,6 +788,12 @@ def urlopen(*args, **kwargs): def build_editable_mask_file(camera_id, width, height, mask_lines): + logging.debug('building editable mask for camera with id %s (%sx%s)' % + (camera_id, width, height)) + + width = width or MASK_DEFAULT_RESOLUTION[0] + height = height or MASK_DEFAULT_RESOLUTION[1] + # horizontal rectangles nx = MASK_WIDTH # number of rectangles if width % nx: @@ -799,7 +806,7 @@ def build_editable_mask_file(camera_id, width, height, mask_lines): rw = width / nx # rectangle width # vertical rectangles - ny = height * MASK_WIDTH / width # number of rectangles + ny = mask_height = height * MASK_WIDTH / width # number of rectangles if height % ny: ny -= 1 ry = height % ny # remainder @@ -812,6 +819,10 @@ def build_editable_mask_file(camera_id, width, height, mask_lines): # draw the actual mask image content im = Image.new('L', (width, height), 255) # all white dr = ImageDraw.Draw(im) + + while len(mask_lines) < mask_height: + # add empty mask lines until mask is tall enough + mask_lines.append(0) for y in xrange(ny): line = mask_lines[y] @@ -833,12 +844,20 @@ def build_editable_mask_file(camera_id, width, height, mask_lines): file_name = os.path.join(settings.CONF_PATH, 'mask_%s.pgm' % camera_id) im.save(file_name, 'ppm') + + return file_name def parse_editable_mask_file(camera_id, width, height): + logging.debug('parsing editable mask for camera with id %s (%sx%s)' % + (camera_id, width, height)) + # width and height arguments represent the current size of the camera image, # as it might be different from that of the associated mask + width = width or MASK_DEFAULT_RESOLUTION[0] + height = height or MASK_DEFAULT_RESOLUTION[1] + # horizontal rectangles nx = MASK_WIDTH # number of rectangles if width % nx: @@ -871,10 +890,13 @@ 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 [0] * mask_height # resize the image if necessary if im.size != (width, height): + logging.debug('editable mask needs resizing from %sx%s to %sx%s' % + (im.size[0], im.size[1], width, height)) + im = im.resize((width, height)) pixels = list(im.getdata())