From 97f62cfb789733693c537d2ad68bbf96260a1c86 Mon Sep 17 00:00:00 2001
From: Teagan Glenn <that@teagantotally.rocks>
Date: Thu, 8 Jun 2017 03:26:24 -0600
Subject: [PATCH] [WIP] Fix opencv (#7864)

* Updates to opencv image processor

* Remove opencv hub

* Requirements

* Remove extra line

* Fix linting errors

* Indentation

* Requirements

* Linting

* Check for import on platform setup

* Remove opencv requirement

* Linting

* fix style

* fix lint
---
 .../components/image_processing/opencv.py     | 156 +++++++++++----
 homeassistant/components/opencv.py            | 183 ------------------
 requirements_all.txt                          |   5 +-
 3 files changed, 124 insertions(+), 220 deletions(-)
 delete mode 100644 homeassistant/components/opencv.py

diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py
index e6cdd0fcef9..f19d50300b8 100644
--- a/homeassistant/components/image_processing/opencv.py
+++ b/homeassistant/components/image_processing/opencv.py
@@ -7,22 +7,56 @@ https://home-assistant.io/components/image_processing.opencv/
 from datetime import timedelta
 import logging
 
+import requests
+
+import voluptuous as vol
+
 from homeassistant.core import split_entity_id
 from homeassistant.components.image_processing import (
-    ImageProcessingEntity, PLATFORM_SCHEMA)
-from homeassistant.components.opencv import (
-    ATTR_MATCHES, CLASSIFIER_GROUP_CONFIG, CONF_CLASSIFIER, CONF_ENTITY_ID,
-    CONF_NAME, process_image)
+    CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, PLATFORM_SCHEMA,
+    ImageProcessingEntity)
+import homeassistant.helpers.config_validation as cv
+
+REQUIREMENTS = ['numpy==1.12.0']
 
 _LOGGER = logging.getLogger(__name__)
 
-DEPENDENCIES = ['opencv']
+ATTR_MATCHES = 'matches'
+ATTR_TOTAL_MATCHES = 'total_matches'
+
+CASCADE_URL = \
+    'https://raw.githubusercontent.com/opencv/opencv/master/data/' + \
+    'lbpcascades/lbpcascade_frontalface.xml'
 
+CONF_CLASSIFIER = 'classifer'
+CONF_FILE = 'file'
+CONF_MIN_SIZE = 'min_size'
+CONF_NEIGHBORS = 'neighbors'
+CONF_SCALE = 'scale'
+
+DEFAULT_CLASSIFIER_PATH = 'lbp_frontalface.xml'
+DEFAULT_MIN_SIZE = (30, 30)
+DEFAULT_NEIGHBORS = 4
+DEFAULT_SCALE = 1.1
 DEFAULT_TIMEOUT = 10
 
 SCAN_INTERVAL = timedelta(seconds=2)
 
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(CLASSIFIER_GROUP_CONFIG)
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+    vol.Optional(CONF_CLASSIFIER, default=None): {
+        cv.string: vol.Any(
+            cv.isfile,
+            vol.Schema({
+                vol.Required(CONF_FILE): cv.isfile,
+                vol.Optional(CONF_SCALE, DEFAULT_SCALE): float,
+                vol.Optional(CONF_NEIGHBORS, DEFAULT_NEIGHBORS):
+                    cv.positive_int,
+                vol.Optional(CONF_MIN_SIZE, DEFAULT_MIN_SIZE):
+                    vol.Schema((int, int))
+            })
+        )
+    }
+})
 
 
 def _create_processor_from_config(hass, camera_entity, config):
@@ -37,41 +71,63 @@ def _create_processor_from_config(hass, camera_entity, config):
     return processor
 
 
+def _get_default_classifier(dest_path):
+    """Download the default OpenCV classifier."""
+    _LOGGER.info('Downloading default classifier')
+    req = requests.get(CASCADE_URL, stream=True)
+    with open(dest_path, 'wb') as fil:
+        for chunk in req.iter_content(chunk_size=1024):
+            if chunk:  # filter out keep-alive new chunks
+                fil.write(chunk)
+
+
 def setup_platform(hass, config, add_devices, discovery_info=None):
     """Set up the OpenCV image processing platform."""
-    if discovery_info is None:
+    try:
+        # Verify opencv python package is preinstalled
+        # pylint: disable=unused-import,unused-variable
+        import cv2  # noqa
+    except ImportError:
+        _LOGGER.error("No opencv library found! " +
+                      "Install or compile for your system " +
+                      "following instructions here: " +
+                      "http://opencv.org/releases.html")
         return
 
-    devices = []
-    for camera_entity in discovery_info[CONF_ENTITY_ID]:
-        devices.append(
-            _create_processor_from_config(hass, camera_entity, discovery_info))
+    entities = []
+    if config[CONF_CLASSIFIER] is None:
+        dest_path = hass.config.path(DEFAULT_CLASSIFIER_PATH)
+        _get_default_classifier(dest_path)
+        config[CONF_CLASSIFIER] = {
+            'Face': dest_path
+        }
+
+    for camera in config[CONF_SOURCE]:
+        entities.append(OpenCVImageProcessor(
+            hass, camera[CONF_ENTITY_ID], camera.get(CONF_NAME),
+            config[CONF_CLASSIFIER]
+        ))
 
-    add_devices(devices)
+    add_devices(entities)
 
 
 class OpenCVImageProcessor(ImageProcessingEntity):
     """Representation of an OpenCV image processor."""
 
-    def __init__(self, hass, camera_entity, name, classifier_configs):
+    def __init__(self, hass, camera_entity, name, classifiers):
         """Initialize the OpenCV entity."""
         self.hass = hass
         self._camera_entity = camera_entity
-        self._name = name
-        self._classifier_configs = classifier_configs
+        if name:
+            self._name = name
+        else:
+            self._name = "OpenCV {0}".format(
+                split_entity_id(camera_entity)[1])
+        self._classifiers = classifiers
         self._matches = {}
+        self._total_matches = 0
         self._last_image = None
 
-    @property
-    def last_image(self):
-        """Return the last image."""
-        return self._last_image
-
-    @property
-    def matches(self):
-        """Return the matches it found."""
-        return self._matches
-
     @property
     def camera_entity(self):
         """Return camera entity id from process pictures."""
@@ -85,20 +141,54 @@ class OpenCVImageProcessor(ImageProcessingEntity):
     @property
     def state(self):
         """Return the state of the entity."""
-        total_matches = 0
-        for group in self._matches.values():
-            total_matches += len(group)
-        return total_matches
+        return self._total_matches
 
     @property
     def state_attributes(self):
         """Return device specific state attributes."""
         return {
-            ATTR_MATCHES: self._matches
+            ATTR_MATCHES: self._matches,
+            ATTR_TOTAL_MATCHES: self._total_matches
         }
 
     def process_image(self, image):
         """Process the image."""
-        self._last_image = image
-        self._matches = process_image(
-            image, self._classifier_configs, False)
+        import cv2  # pylint: disable=import-error
+        import numpy
+
+        # pylint: disable=no-member
+        cv_image = cv2.imdecode(numpy.asarray(bytearray(image)),
+                                cv2.IMREAD_UNCHANGED)
+
+        for name, classifier in self._classifiers.items():
+            scale = DEFAULT_SCALE
+            neighbors = DEFAULT_NEIGHBORS
+            min_size = DEFAULT_MIN_SIZE
+            if isinstance(classifier, dict):
+                path = classifier[CONF_FILE]
+                scale = classifier.get(CONF_SCALE, scale)
+                neighbors = classifier.get(CONF_NEIGHBORS, neighbors)
+                min_size = classifier.get(CONF_MIN_SIZE, min_size)
+            else:
+                path = classifier
+
+            # pylint: disable=no-member
+            cascade = cv2.CascadeClassifier(path)
+
+            detections = cascade.detectMultiScale(
+                cv_image,
+                scaleFactor=scale,
+                minNeighbors=neighbors,
+                minSize=min_size)
+            matches = {}
+            total_matches = 0
+            regions = []
+            # pylint: disable=invalid-name
+            for (x, y, w, h) in detections:
+                regions.append((int(x), int(y), int(w), int(h)))
+                total_matches += 1
+
+            matches[name] = regions
+
+        self._matches = matches
+        self._total_matches = total_matches
diff --git a/homeassistant/components/opencv.py b/homeassistant/components/opencv.py
deleted file mode 100644
index 634e8e156a1..00000000000
--- a/homeassistant/components/opencv.py
+++ /dev/null
@@ -1,183 +0,0 @@
-"""
-Support for OpenCV image/video processing.
-
-For more details about this component, please refer to the documentation at
-https://home-assistant.io/components/opencv/
-"""
-import logging
-import os
-import voluptuous as vol
-
-import requests
-
-from homeassistant.const import (
-    CONF_NAME,
-    CONF_ENTITY_ID,
-    CONF_FILE_PATH
-)
-from homeassistant.helpers import (
-    discovery,
-    config_validation as cv,
-)
-
-REQUIREMENTS = ['opencv-python==3.2.0.6', 'numpy==1.12.0']
-
-_LOGGER = logging.getLogger(__name__)
-
-ATTR_MATCHES = 'matches'
-
-BASE_PATH = os.path.realpath(__file__)
-
-CASCADE_URL = \
-    'https://raw.githubusercontent.com/opencv/opencv/master/data/' +\
-    'lbpcascades/lbpcascade_frontalface.xml'
-
-CONF_CLASSIFIER = 'classifier'
-CONF_COLOR = 'color'
-CONF_GROUPS = 'classifier_group'
-CONF_MIN_SIZE = 'min_size'
-CONF_NEIGHBORS = 'neighbors'
-CONF_SCALE = 'scale'
-
-DATA_CLASSIFIER_GROUPS = 'classifier_groups'
-
-DEFAULT_COLOR = (255, 255, 0)
-DEFAULT_CLASSIFIER_PATH = 'lbp_frontalface.xml'
-DEFAULT_NAME = 'OpenCV'
-DEFAULT_MIN_SIZE = (30, 30)
-DEFAULT_NEIGHBORS = 4
-DEFAULT_SCALE = 1.1
-
-DOMAIN = 'opencv'
-
-CLASSIFIER_GROUP_CONFIG = {
-    vol.Required(CONF_CLASSIFIER): vol.All(
-        cv.ensure_list,
-        [vol.Schema({
-            vol.Optional(CONF_COLOR, default=DEFAULT_COLOR):
-                vol.Schema((int, int, int)),
-            vol.Optional(CONF_FILE_PATH, default=None): cv.isfile,
-            vol.Optional(CONF_NAME, default=DEFAULT_NAME):
-                cv.string,
-            vol.Optional(CONF_MIN_SIZE, default=DEFAULT_MIN_SIZE):
-                vol.Schema((int, int)),
-            vol.Optional(CONF_NEIGHBORS, default=DEFAULT_NEIGHBORS):
-                cv.positive_int,
-            vol.Optional(CONF_SCALE, default=DEFAULT_SCALE):
-                float
-        })]),
-    vol.Required(CONF_ENTITY_ID): cv.entity_ids,
-    vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
-}
-CLASSIFIER_GROUP_SCHEMA = vol.Schema(CLASSIFIER_GROUP_CONFIG)
-
-CONFIG_SCHEMA = vol.Schema({
-    DOMAIN: vol.Schema({
-        vol.Required(CONF_GROUPS): vol.All(
-            cv.ensure_list,
-            [CLASSIFIER_GROUP_SCHEMA]
-        ),
-    })
-}, extra=vol.ALLOW_EXTRA)
-
-
-# NOTE:
-# pylint cannot find any of the members of cv2, using disable=no-member
-# to pass linting
-
-
-def cv_image_to_bytes(cv_image):
-    """Convert OpenCV image to bytes."""
-    import cv2  # pylint: disable=import-error
-
-    # pylint: disable=no-member
-    encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 90]
-    # pylint: disable=no-member
-    success, data = cv2.imencode('.jpg', cv_image, encode_param)
-
-    if success:
-        return data.tobytes()
-
-    return None
-
-
-def cv_image_from_bytes(image):
-    """Convert image bytes to OpenCV image."""
-    import cv2  # pylint: disable=import-error
-    import numpy
-
-    # pylint: disable=no-member
-    return cv2.imdecode(numpy.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED)
-
-
-def process_image(image, classifier_group, is_camera):
-    """Process the image given a classifier group."""
-    import cv2  # pylint: disable=import-error
-    import numpy
-
-    # pylint: disable=no-member
-    cv_image = cv2.imdecode(numpy.asarray(bytearray(image)),
-                            cv2.IMREAD_UNCHANGED)
-    group_matches = {}
-    for classifier_config in classifier_group:
-        classifier_path = classifier_config[CONF_FILE_PATH]
-        classifier_name = classifier_config[CONF_NAME]
-        color = classifier_config[CONF_COLOR]
-        scale = classifier_config[CONF_SCALE]
-        neighbors = classifier_config[CONF_NEIGHBORS]
-        min_size = classifier_config[CONF_MIN_SIZE]
-
-        # pylint: disable=no-member
-        classifier = cv2.CascadeClassifier(classifier_path)
-
-        detections = classifier.detectMultiScale(cv_image,
-                                                 scaleFactor=scale,
-                                                 minNeighbors=neighbors,
-                                                 minSize=min_size)
-        regions = []
-        # pylint: disable=invalid-name
-        for (x, y, w, h) in detections:
-            if is_camera:
-                # pylint: disable=no-member
-                cv2.rectangle(cv_image,
-                              (x, y),
-                              (x + w, y + h),
-                              color,
-                              2)
-            else:
-                regions.append((int(x), int(y), int(w), int(h)))
-        group_matches[classifier_name] = regions
-
-    if is_camera:
-        return cv_image_to_bytes(cv_image)
-    else:
-        return group_matches
-
-
-def setup(hass, config):
-    """Set up the OpenCV platform entities."""
-    default_classifier = hass.config.path(DEFAULT_CLASSIFIER_PATH)
-
-    if not os.path.isfile(default_classifier):
-        _LOGGER.info('Downloading default classifier')
-
-        req = requests.get(CASCADE_URL, stream=True)
-        with open(default_classifier, 'wb') as fil:
-            for chunk in req.iter_content(chunk_size=1024):
-                if chunk:  # filter out keep-alive new chunks
-                    fil.write(chunk)
-
-    for group in config[DOMAIN][CONF_GROUPS]:
-        grp = {}
-
-        for classifier, config in group.items():
-            config = dict(config)
-
-            if config[CONF_FILE_PATH] is None:
-                config[CONF_FILE_PATH] = default_classifier
-
-            grp[classifier] = config
-
-        discovery.load_platform(hass, 'image_processing', DOMAIN, grp)
-
-    return True
diff --git a/requirements_all.txt b/requirements_all.txt
index bf7ad1d9d0d..987779c1155 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -387,7 +387,7 @@ netdisco==1.0.1
 # homeassistant.components.sensor.neurio_energy
 neurio==0.3.1
 
-# homeassistant.components.opencv
+# homeassistant.components.image_processing.opencv
 numpy==1.12.0
 
 # homeassistant.components.google
@@ -399,9 +399,6 @@ oemthermostat==1.1
 # homeassistant.components.media_player.onkyo
 onkyo-eiscp==1.1
 
-# homeassistant.components.opencv
-# opencv-python==3.2.0.6
-
 # homeassistant.components.sensor.openevse
 openevsewifi==0.4
 
-- 
GitLab