]> www.vanbest.org Git - motioneye-debian/commitdiff
more work on the mask editor
authorCalin Crisan <ccrisan@gmail.com>
Fri, 16 Sep 2016 19:00:49 +0000 (22:00 +0300)
committerCalin Crisan <ccrisan@gmail.com>
Fri, 16 Sep 2016 19:00:49 +0000 (22:00 +0300)
motioneye/config.py
motioneye/handlers.py
motioneye/static/css/main.css
motioneye/static/js/main.js
motioneye/templates/main.html
motioneye/utils.py

index 413101c3ad03fb5af9821d94d7a870a0cfa8096a..f303736777b3568511970ffc376ad5c21095c2b3 100644 (file)
@@ -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
index 85ea8d09b199994aa501400e609619a5340980cc..40d51d4154131b10a410b401807ceb6063e8afae 100644 (file)
@@ -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):
index 1c7e6f3b0f4623b507746a24c7188f807e29e5eb..d5621d7a1bc99ae0e1014ab5b640ae61b6fa421c 100644 (file)
@@ -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 {
index 095f37d788599d6e2c07af523493104923eacfdd..e2e475df8d9c7d6aac80025a1d0c6e0f8ee66655 100644 (file)
@@ -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();
     
index c339ab4861b54accfdaf46e21e68ffcf1ba9711b..28a679a2db23faa8d005348adfa646f8c4a1a7dd 100644 (file)
         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 %}
     </script>
 {% endblock %}
 
                         <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" depends="mask maskType=editable">
+                        <td class="settings-item-label"><span class="settings-item-label"></span></td>
+                        <td class="settings-item-value">
+                            <div class="button normal-button edit-mask-button" id="editMaskButton">Edit Mask</div>
+                            <div class="button normal-button save-mask-button" id="saveMaskButton">Save Mask</div>
+                            <input type="text" id="maskLinesEntry">
+                        </td>
+                        <td><span class="help-mark" title="click this button to enable/disable the mask editor">?</span></td>
+                    </tr>
+                    <tr class="settings-item advanced-setting" depends="mask maskType=editable">
+                        <td class="settings-item-label"><span class="settings-item-label"></span></td>
+                        <td class="settings-item-value"><div class="button normal-button clear-mask-button" id="clearMaskButton">Clear Mask</div></td>
+                        <td><span class="help-mark" title="click this button to clear the current mask">?</span></td>
+                    </tr>
                     {% for config in camera_sections.get('motion-detection', {}).get('configs', []) %}
                         {{config_item(config)}}
                     {% endfor %}
index 6aafcdf31ee149d5764fd700c2b7a91391c11c7a..b14e6e7efd86b56fadbd8614941bfc7094f9caa4 100644 (file)
@@ -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())